Web Inspector: round sub-pixel values we get from computed style in visual sidebar
[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         if (this._possibleValues) {
48             this._createValueOptions(this._possibleValues.basic);
49             this._keywordSelectElement.appendChild(document.createElement("hr"));
50         }
51
52         if (this._possibleUnits)
53             this._createUnitOptions(this._possibleUnits.basic);
54
55         this._advancedUnitsElements = null;
56
57         this._keywordSelectElement.addEventListener("focus", this._focusContentElement.bind(this));
58         this._keywordSelectElement.addEventListener("mousedown", this._keywordSelectMouseDown.bind(this));
59         this._keywordSelectElement.addEventListener("change", this._keywordChanged.bind(this));
60         this._keywordSelectElement.addEventListener("blur", this._blurContentElement.bind(this));
61         this.contentElement.appendChild(this._keywordSelectElement);
62
63         this._numberUnitsContainer = document.createElement("div");
64         this._numberUnitsContainer.classList.add("number-input-container");
65
66         this._valueNumberInputElement = document.createElement("input");
67         this._valueNumberInputElement.classList.add("number-input-value");
68         this._valueNumberInputElement.spellcheck = false;
69         this._valueNumberInputElement.addEventListener("focus", this._focusContentElement.bind(this));
70         this._valueNumberInputElement.addEventListener("keydown", this._valueNumberInputKeyDown.bind(this));
71         this._valueNumberInputElement.addEventListener("keyup", this._numberInputChanged.bind(this));
72         this._valueNumberInputElement.addEventListener("blur", this._blurContentElement.bind(this));
73         this._numberUnitsContainer.appendChild(this._valueNumberInputElement);
74
75         this._unitsElement = document.createElement("span");
76         this._numberUnitsContainer.appendChild(this._unitsElement);
77         this.contentElement.appendChild(this._numberUnitsContainer);
78
79         this._numberInputIsEditable = true;
80         this.contentElement.classList.add("number-input-editable");
81         this._valueNumberInputElement.value = null;
82         this._valueNumberInputElement.setAttribute("placeholder", 0);
83         if (this._hasUnits && this.valueIsSupportedUnit("px"))
84             this._unitsElement.textContent = this._keywordSelectElement.value = "px";
85     }
86
87     // Public
88
89     get value()
90     {
91         if (this._numberInputIsEditable)
92             return parseFloat(this._valueNumberInputElement.value);
93
94         if (!this._numberInputIsEditable)
95             return this._keywordSelectElement.value;
96
97         return null;
98     }
99
100     set value(value)
101     {
102         if (value && value === this.value)
103             return;
104
105         if (!isNaN(value)) {
106             this._numberInputIsEditable = true;
107             this.contentElement.classList.add("number-input-editable");
108             this._valueNumberInputElement.value = Math.round(value * 100) / 100;
109             return;
110         }
111
112         if (!value) {
113             this._valueNumberInputElement.value = null;
114             return;
115         }
116
117         if (this.valueIsSupportedKeyword(value)) {
118             this._numberInputIsEditable = false;
119             this.contentElement.classList.remove("number-input-editable");
120             this._keywordSelectElement.value = value;
121             return;
122         }
123     }
124
125     get units()
126     {
127         let keyword = this._keywordSelectElement.value;
128         if (!this.valueIsSupportedUnit(keyword))
129             return;
130
131         return keyword;
132     }
133
134     set units(unit)
135     {
136         if (!unit || unit === this.units)
137             return;
138
139         if (!this.valueIsSupportedUnit(unit))
140             return;
141
142         if (this._valueIsSupportedAdvancedUnit(unit))
143             this._addAdvancedUnits();
144
145         this._numberInputIsEditable = true;
146         this.contentElement.classList.add("number-input-editable");
147         this._keywordSelectElement.value = unit;
148
149         if (this._hasUnits)
150             this._unitsElement.textContent = unit;
151     }
152
153     get placeholder()
154     {
155         return this._valueNumberInputElement.getAttribute("placeholder");
156     }
157
158     set placeholder(text)
159     {
160         if (text === this.placeholder)
161             return;
162
163         let onlyNumericalText = text && !isNaN(text) && (Math.round(text * 100) / 100);
164         this._valueNumberInputElement.setAttribute("placeholder", onlyNumericalText || 0);
165     }
166
167     get synthesizedValue()
168     {
169         let value = this._valueNumberInputElement.value;
170         if (this._numberInputIsEditable && !value)
171             return null;
172
173         let keyword = this._keywordSelectElement.value;
174         return this.valueIsSupportedUnit(keyword) ? value + (this._hasUnits ? keyword : "") : keyword;
175     }
176
177     updateValueFromText(text, value)
178     {
179         let match = this.parseValue(value);
180         this.value = match ? match[1] : value;
181         this.units = match ? match[2] : null;
182         return this.modifyPropertyText(text, value);
183     }
184
185     // Protected
186
187     parseValue(text)
188     {
189         return /^(-?[\d.]+)([^\s\d]{0,4})(?:\s*;?)$/.exec(text);
190     }
191
192     // Private
193
194     _keywordChanged()
195     {
196         let selectedKeywordIsUnit = this.valueIsSupportedUnit(this._keywordSelectElement.value);
197         if (!this._numberInputIsEditable && selectedKeywordIsUnit)
198             this._valueNumberInputElement.value = null;
199
200         if (this._hasUnits)
201             this._unitsElement.textContent = this._keywordSelectElement.value;
202
203         this._numberInputIsEditable = selectedKeywordIsUnit;
204         this.contentElement.classList.toggle("number-input-editable", selectedKeywordIsUnit);
205         this._valueDidChange();
206     }
207
208     _valueNumberInputKeyDown(event)
209     {
210         if (!this._numberInputIsEditable)
211             return;
212
213         function adjustValue(delta)
214         {
215             let newValue;
216             let value = this.value;
217             if (!value && isNaN(value)) {
218                 let placeholderValue = this.placeholder && !isNaN(this.placeholder) ? parseFloat(this.placeholder) : 0;
219                 newValue = placeholderValue + delta;
220             } else
221                 newValue = value + delta;
222
223             if (!this._allowNegativeValues && newValue < 0)
224                 newValue = 0;
225
226             this.value = Math.round(newValue * 100) / 100;
227             this._valueDidChange();
228         }
229
230         let shift = 1;
231         if (event.ctrlKey)
232             shift /= 10;
233         else if (event.shiftKey)
234             shift *= 10;
235
236         let key = event.keyIdentifier;
237         if (key.startsWith("Page"))
238             shift *= 10;
239
240         if (key === "Up" || key === "PageUp") {
241             event.preventDefault();
242             adjustValue.call(this, shift);
243             return;
244         }
245
246         if (key === "Down" || key === "PageDown") {
247             event.preventDefault();
248             adjustValue.call(this, -shift);
249             return;
250         }
251
252         this._valueDidChange();
253     }
254
255     _numberInputChanged()
256     {
257         if (!this._numberInputIsEditable)
258             return;
259
260         this._valueDidChange();
261     }
262
263     _keywordSelectMouseDown(event)
264     {
265         if (event.altKey)
266             this._addAdvancedUnits();
267         else if (!this._valueIsSupportedAdvancedUnit())
268             this._removeAdvancedUnits();
269     }
270
271     _createValueOptions(values)
272     {
273         let addedElements = [];
274         for (let key in values) {
275             let option = document.createElement("option");
276             option.value = key;
277             option.text = values[key];
278             this._keywordSelectElement.appendChild(option);
279             addedElements.push(option);
280         }
281
282         return addedElements;
283     }
284
285     _createUnitOptions(units)
286     {
287         let addedElements = [];
288         for (let unit of units) {
289             let option = document.createElement("option");
290             option.text = unit;
291             this._keywordSelectElement.appendChild(option);
292             addedElements.push(option);
293         }
294
295         return addedElements;
296     }
297
298     _addAdvancedUnits()
299     {
300         if (this._advancedUnitsElements)
301             return;
302
303         this._keywordSelectElement.appendChild(document.createElement("hr"));
304         this._advancedUnitsElements = this._createUnitOptions(this._possibleUnits.advanced);
305     }
306
307     _removeAdvancedUnits()
308     {
309         if (!this._advancedUnitsElements)
310             return;
311
312         this._keywordSelectElement.removeChild(this._advancedUnitsElements[0].previousSibling);
313         for (let element of this._advancedUnitsElements)
314             this._keywordSelectElement.removeChild(element);
315
316         this._advancedUnitsElements = null;
317     }
318
319     _focusContentElement(event)
320     {
321         this.contentElement.classList.add("focused");
322     }
323
324     _blurContentElement(event)
325     {
326         this.contentElement.classList.remove("focused");
327     }
328
329     _toggleTabbingOfSelectableElements(disabled)
330     {
331         this._keywordSelectElement.tabIndex = disabled ? "-1" : null;
332         this._valueNumberInputElement.tabIndex = disabled ? "-1" : null;
333     }
334 };