Web Inspector: Show the computed value in an overlay for numerical Visual Editors
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / VisualStyleNumberInputBox.js
1 /*
2  * Copyright (C) 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.VisualStyleNumberInputBox = class VisualStyleNumberInputBox extends WebInspector.VisualStylePropertyEditor
27 {
28     constructor(propertyNames, text, possibleValues, possibleUnits, allowNegativeValues, layoutReversed)
29     {
30         super(propertyNames, text, possibleValues, possibleUnits || [WebInspector.UIString("Number")], "number-input-box", layoutReversed);
31
32         this._hasUnits = !!possibleUnits;
33         this._allowNegativeValues = !!allowNegativeValues || false;
34
35         this.contentElement.classList.toggle("no-values", !possibleValues || !possibleValues.length);
36         this.contentElement.classList.toggle("no-units", !this._hasUnits);
37
38         let focusRingElement = document.createElement("div");
39         focusRingElement.classList.add("focus-ring");
40         this.contentElement.appendChild(focusRingElement);
41
42         this._keywordSelectElement = document.createElement("select");
43         this._keywordSelectElement.classList.add("number-input-keyword-select");
44         if (this._possibleUnits.advanced)
45             this._keywordSelectElement.title = WebInspector.UIString("Option-click to show all units");
46
47         this._unchangedOptionElement = document.createElement("option");
48         this._unchangedOptionElement.value = "";
49         this._unchangedOptionElement.text = WebInspector.UIString("Unchanged");
50         this._keywordSelectElement.appendChild(this._unchangedOptionElement);
51
52         this._keywordSelectElement.appendChild(document.createElement("hr"));
53
54         if (this._possibleValues) {
55             this._createValueOptions(this._possibleValues.basic);
56             this._keywordSelectElement.appendChild(document.createElement("hr"));
57         }
58
59         if (this._possibleUnits)
60             this._createUnitOptions(this._possibleUnits.basic);
61
62         this._advancedUnitsElements = null;
63
64         this._keywordSelectElement.addEventListener("focus", this._focusContentElement.bind(this));
65         this._keywordSelectElement.addEventListener("mousedown", this._keywordSelectMouseDown.bind(this));
66         this._keywordSelectElement.addEventListener("change", this._keywordChanged.bind(this));
67         this._keywordSelectElement.addEventListener("blur", this._blurContentElement.bind(this));
68         this.contentElement.appendChild(this._keywordSelectElement);
69
70         this._numberUnitsContainer = document.createElement("div");
71         this._numberUnitsContainer.classList.add("number-input-container");
72
73         this._valueNumberInputElement = document.createElement("input");
74         this._valueNumberInputElement.classList.add("number-input-value");
75         this._valueNumberInputElement.spellcheck = false;
76         this._valueNumberInputElement.addEventListener("focus", this._focusContentElement.bind(this));
77         this._valueNumberInputElement.addEventListener("keydown", this._valueNumberInputKeyDown.bind(this));
78         this._valueNumberInputElement.addEventListener("keyup", this._numberInputChanged.bind(this));
79         this._valueNumberInputElement.addEventListener("blur", this._blurContentElement.bind(this));
80         this._numberUnitsContainer.appendChild(this._valueNumberInputElement);
81
82         this._unitsElement = document.createElement("span");
83         this._numberUnitsContainer.appendChild(this._unitsElement);
84
85         this.contentElement.appendChild(this._numberUnitsContainer);
86
87         this._numberInputIsEditable = true;
88         this.contentElement.classList.add("number-input-editable");
89         this._valueNumberInputElement.value = null;
90         this._valueNumberInputElement.setAttribute("placeholder", 0);
91         if (this._hasUnits && this.valueIsSupportedUnit("px"))
92             this._unitsElementTextContent = this._keywordSelectElement.value = "px";
93     }
94
95     // Public
96
97     get value()
98     {
99         if (this._numberInputIsEditable)
100             return parseFloat(this._valueNumberInputElement.value);
101
102         if (!this._numberInputIsEditable)
103             return this._keywordSelectElement.value;
104
105         return null;
106     }
107
108     set value(value)
109     {
110         if (value && value === this.value)
111             return;
112
113         if (this._updatedValues.propertyMissing && isNaN(value)) {
114             this._unchangedOptionElement.selected = true;
115             this._numberInputIsEditable = false;
116             this.contentElement.classList.remove("number-input-editable");
117             this.specialPropertyPlaceholderElement.hidden = false;
118             return;
119         }
120
121         this.specialPropertyPlaceholderElement.hidden = true;
122
123         if (!isNaN(value)) {
124             this._numberInputIsEditable = true;
125             this.contentElement.classList.add("number-input-editable");
126             this._valueNumberInputElement.value = Math.round(value * 100) / 100;
127             return;
128         }
129
130         if (!value) {
131             this._valueNumberInputElement.value = null;
132             return;
133         }
134
135         if (this.valueIsSupportedKeyword(value)) {
136             this._numberInputIsEditable = false;
137             this.contentElement.classList.remove("number-input-editable");
138             this._keywordSelectElement.value = value;
139             return;
140         }
141     }
142
143     get units()
144     {
145         if (this._unchangedOptionElement.selected)
146             return null;
147
148         let keyword = this._keywordSelectElement.value;
149         if (!this.valueIsSupportedUnit(keyword))
150             return null;
151
152         return keyword;
153     }
154
155     set units(unit)
156     {
157         if (this._unchangedOptionElement.selected)
158             return;
159
160         if (!unit || unit === this.units)
161             return;
162
163         if (!this.valueIsSupportedUnit(unit))
164             return;
165
166         if (this._valueIsSupportedAdvancedUnit(unit))
167             this._addAdvancedUnits();
168
169         this._numberInputIsEditable = true;
170         this.contentElement.classList.add("number-input-editable");
171         this._keywordSelectElement.value = unit;
172         this._unitsElementTextContent = unit;
173     }
174
175     get placeholder()
176     {
177         return this._valueNumberInputElement.getAttribute("placeholder");
178     }
179
180     set placeholder(text)
181     {
182         if (text === this.placeholder)
183             return;
184
185         let onlyNumericalText = text && !isNaN(text) && (Math.round(text * 100) / 100);
186         this._valueNumberInputElement.setAttribute("placeholder", onlyNumericalText || 0);
187
188         if (!onlyNumericalText)
189             this.specialPropertyPlaceholderElement.textContent = this._canonicalizedKeywordForKey(text) || text;
190     }
191
192     get synthesizedValue()
193     {
194         if (this._unchangedOptionElement.selected)
195             return null;
196
197         let value = this._valueNumberInputElement.value;
198         if (this._numberInputIsEditable && !value)
199             return null;
200
201         let keyword = this._keywordSelectElement.value;
202         return this.valueIsSupportedUnit(keyword) ? value + (this._hasUnits ? keyword : "") : keyword;
203     }
204
205     updateValueFromText(text, value)
206     {
207         let match = this.parseValue(value);
208         this.value = match ? match[1] : value;
209         this.units = match ? match[2] : null;
210         return this.modifyPropertyText(text, value);
211     }
212
213     // Protected
214
215     parseValue(text)
216     {
217         return /^(-?[\d.]+)([^\s\d]{0,4})(?:\s*;?)$/.exec(text);
218     }
219
220     // Private
221
222     set _unitsElementTextContent(text)
223     {
224         if (!this._hasUnits)
225             return;
226
227         this._unitsElement.textContent = text;
228         this._markUnitsContainerIfInputHasValue();
229     }
230
231     _markUnitsContainerIfInputHasValue()
232     {
233         let numberInputValue = this._valueNumberInputElement.value;
234         this._numberUnitsContainer.classList.toggle("has-value", numberInputValue && numberInputValue.length);
235     }
236
237     _keywordChanged()
238     {
239         let unchangedOptionSelected = this._unchangedOptionElement.selected;
240         if (!unchangedOptionSelected) {
241             let selectedKeywordIsUnit = this.valueIsSupportedUnit(this._keywordSelectElement.value);
242             if (!this._numberInputIsEditable && selectedKeywordIsUnit)
243                 this._valueNumberInputElement.value = null;
244
245             this._unitsElementTextContent = this._keywordSelectElement.value;
246             this._numberInputIsEditable = selectedKeywordIsUnit;
247             this.contentElement.classList.toggle("number-input-editable", selectedKeywordIsUnit);
248         }
249
250         this._valueDidChange();
251         this.specialPropertyPlaceholderElement.hidden = !unchangedOptionSelected;
252     }
253
254     _valueNumberInputKeyDown(event)
255     {
256         if (!this._numberInputIsEditable)
257             return;
258
259         function adjustValue(delta)
260         {
261             let newValue;
262             let value = this.value;
263             if (!value && isNaN(value)) {
264                 let placeholderValue = this.placeholder && !isNaN(this.placeholder) ? parseFloat(this.placeholder) : 0;
265                 newValue = placeholderValue + delta;
266             } else
267                 newValue = value + delta;
268
269             if (!this._allowNegativeValues && newValue < 0)
270                 newValue = 0;
271
272             this.value = Math.round(newValue * 100) / 100;
273             this._valueDidChange();
274         }
275
276         let shift = 1;
277         if (event.ctrlKey)
278             shift /= 10;
279         else if (event.shiftKey)
280             shift *= 10;
281
282         let key = event.keyIdentifier;
283         if (key.startsWith("Page"))
284             shift *= 10;
285
286         if (key === "Up" || key === "PageUp") {
287             event.preventDefault();
288             adjustValue.call(this, shift);
289             return;
290         }
291
292         if (key === "Down" || key === "PageDown") {
293             event.preventDefault();
294             adjustValue.call(this, -shift);
295             return;
296         }
297
298         this._markUnitsContainerIfInputHasValue();
299         this._valueDidChange();
300     }
301
302     _numberInputChanged()
303     {
304         if (!this._numberInputIsEditable)
305             return;
306
307         this._markUnitsContainerIfInputHasValue();
308         this._valueDidChange();
309     }
310
311     _keywordSelectMouseDown(event)
312     {
313         if (event.altKey)
314             this._addAdvancedUnits();
315         else if (!this._valueIsSupportedAdvancedUnit())
316             this._removeAdvancedUnits();
317     }
318
319     _createValueOptions(values)
320     {
321         let addedElements = [];
322         for (let key in values) {
323             let option = document.createElement("option");
324             option.value = key;
325             option.text = values[key];
326             this._keywordSelectElement.appendChild(option);
327             addedElements.push(option);
328         }
329
330         return addedElements;
331     }
332
333     _createUnitOptions(units)
334     {
335         let addedElements = [];
336         for (let unit of units) {
337             let option = document.createElement("option");
338             option.text = unit;
339             this._keywordSelectElement.appendChild(option);
340             addedElements.push(option);
341         }
342
343         return addedElements;
344     }
345
346     _addAdvancedUnits()
347     {
348         if (this._advancedUnitsElements)
349             return;
350
351         this._keywordSelectElement.appendChild(document.createElement("hr"));
352         this._advancedUnitsElements = this._createUnitOptions(this._possibleUnits.advanced);
353     }
354
355     _removeAdvancedUnits()
356     {
357         if (!this._advancedUnitsElements)
358             return;
359
360         this._keywordSelectElement.removeChild(this._advancedUnitsElements[0].previousSibling);
361         for (let element of this._advancedUnitsElements)
362             this._keywordSelectElement.removeChild(element);
363
364         this._advancedUnitsElements = null;
365     }
366
367     _focusContentElement(event)
368     {
369         this.contentElement.classList.add("focused");
370     }
371
372     _blurContentElement(event)
373     {
374         this.contentElement.classList.remove("focused");
375     }
376
377     _toggleTabbingOfSelectableElements(disabled)
378     {
379         this._keywordSelectElement.tabIndex = disabled ? "-1" : null;
380         this._valueNumberInputElement.tabIndex = disabled ? "-1" : null;
381     }
382 };