ITMLKit Inspector: Computed Style Box Model section throws exceptions
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / BoxModelDetailsSectionRow.js
1 /*
2  * Copyright (C) 2013, 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 WI.BoxModelDetailsSectionRow = class BoxModelDetailsSectionRow extends WI.DetailsSectionRow
27 {
28     constructor()
29     {
30         super(WI.UIString("No Box Model Information"));
31
32         this.element.classList.add("box-model");
33
34         this._nodeStyles = null;
35     }
36
37     // Public
38
39     get nodeStyles()
40     {
41         return this._nodeStyles;
42     }
43
44     set nodeStyles(nodeStyles)
45     {
46         if (this._nodeStyles && this._nodeStyles.computedStyle)
47             this._nodeStyles.computedStyle.removeEventListener(WI.CSSStyleDeclaration.Event.PropertiesChanged, this._refresh, this);
48
49         this._nodeStyles = nodeStyles;
50         if (this._nodeStyles && this._nodeStyles.computedStyle)
51             this._nodeStyles.computedStyle.addEventListener(WI.CSSStyleDeclaration.Event.PropertiesChanged, this._refresh, this);
52
53         this._refresh();
54     }
55
56     // Private
57
58     _refresh()
59     {
60         if (this._ignoreNextRefresh) {
61             this._ignoreNextRefresh = false;
62             return;
63         }
64
65         this._updateMetrics();
66     }
67
68     _getPropertyValueAsPx(style, propertyName)
69     {
70         return Number(style.propertyForName(propertyName).value.replace(/px$/, "") || 0);
71     }
72
73     _getBox(computedStyle, componentName)
74     {
75         var suffix = this._getComponentSuffix(componentName);
76         var left = this._getPropertyValueAsPx(computedStyle, componentName + "-left" + suffix);
77         var top = this._getPropertyValueAsPx(computedStyle, componentName + "-top" + suffix);
78         var right = this._getPropertyValueAsPx(computedStyle, componentName + "-right" + suffix);
79         var bottom = this._getPropertyValueAsPx(computedStyle, componentName + "-bottom" + suffix);
80         return {left, top, right, bottom};
81     }
82
83     _getComponentSuffix(componentName)
84     {
85         return componentName === "border" ? "-width" : "";
86     }
87
88     _highlightDOMNode(showHighlight, mode, event)
89     {
90         event.stopPropagation();
91
92         var nodeId = showHighlight ? this.nodeStyles.node.id : 0;
93         if (nodeId) {
94             if (this._highlightMode === mode)
95                 return;
96             this._highlightMode = mode;
97             WI.domManager.highlightDOMNode(nodeId, mode);
98         } else {
99             this._highlightMode = null;
100             WI.domManager.hideDOMNodeHighlight();
101         }
102
103         for (var i = 0; this._boxElements && i < this._boxElements.length; ++i) {
104             var element = this._boxElements[i];
105             if (nodeId && (mode === "all" || element._name === mode))
106                 element.classList.add("active");
107             else
108                 element.classList.remove("active");
109         }
110
111         this.element.classList.toggle("hovered", showHighlight);
112     }
113
114     _updateMetrics()
115     {
116         var metricsElement = document.createElement("div");
117         var style = this._nodeStyles.computedStyle;
118
119         function createValueElement(type, value, name, propertyName)
120         {
121             // Check if the value is a float and whether it should be rounded.
122             let floatValue = parseFloat(value);
123             let shouldRoundValue = !isNaN(floatValue) && (floatValue % 1 !== 0);
124
125             if (isNaN(floatValue))
126                 value = figureDash;
127
128             let element = document.createElement(type);
129             element.textContent = shouldRoundValue ? ("~" + Math.round(floatValue * 100) / 100) : value;
130             if (shouldRoundValue)
131                 element.title = value;
132             element.addEventListener("dblclick", this._startEditing.bind(this, element, name, propertyName, style), false);
133             return element;
134         }
135
136         function createBoxPartElement(name, side)
137         {
138             let suffix = this._getComponentSuffix(name);
139             let propertyName = (name !== "position" ? name + "-" : "") + side + suffix;
140             let property = style.propertyForName(propertyName);
141             if (!property)
142                 return null;
143
144             let value = property.value;
145             if (value === "" || (name !== "position" && value === "0px") || (name === "position" && value === "auto"))
146                 value = "";
147             else
148                 value = value.replace(/px$/, "");
149
150             let element = createValueElement.call(this, "div", value, name, propertyName);
151             element.className = side;
152             return element;
153         }
154
155         function createContentAreaElement(name)
156         {
157             console.assert(name === "width" || name === "height");
158
159             let property = style.propertyForName(name);
160             if (!property)
161                 return null;
162
163             let size = property.value.replace(/px$/, "");
164             if (style.propertyForName("box-sizing").value === "border-box") {
165                 let borderBox = this._getBox(style, "border");
166                 let paddingBox = this._getBox(style, "padding");
167
168                 let [side, oppositeSide] = name === "width" ? ["left", "right"] : ["top", "bottom"];
169                 size = size - borderBox[side] - borderBox[oppositeSide] - paddingBox[side] - paddingBox[oppositeSide];
170             }
171
172             return createValueElement.call(this, "span", size, name, name);
173         }
174
175         // Display types for which margin is ignored.
176         var noMarginDisplayType = {
177             "table-cell": true,
178             "table-column": true,
179             "table-column-group": true,
180             "table-footer-group": true,
181             "table-header-group": true,
182             "table-row": true,
183             "table-row-group": true
184         };
185
186         // Display types for which padding is ignored.
187         var noPaddingDisplayType = {
188             "table-column": true,
189             "table-column-group": true,
190             "table-footer-group": true,
191             "table-header-group": true,
192             "table-row": true,
193             "table-row-group": true
194         };
195
196         // Position types for which top, left, bottom and right are ignored.
197         var noPositionType = {
198             "static": true
199         };
200
201         this._boxElements = [];
202
203         if (!style.hasProperties()) {
204             this.showEmptyMessage();
205             return;
206         }
207
208         let displayProperty = style.propertyForName("display");
209         let positionProperty = style.propertyForName("position");
210         if (!displayProperty || !positionProperty) {
211             this.showEmptyMessage();
212             return;
213         }
214
215         var previousBox = null;
216         for (let name of ["content", "padding", "border", "margin", "position"]) {
217
218             if (name === "margin" && noMarginDisplayType[displayProperty.value])
219                 continue;
220             if (name === "padding" && noPaddingDisplayType[displayProperty.value])
221                 continue;
222             if (name === "position" && noPositionType[positionProperty.value])
223                 continue;
224
225             let boxElement = document.createElement("div");
226             boxElement.className = name;
227             boxElement._name = name;
228             boxElement.addEventListener("mouseover", this._highlightDOMNode.bind(this, true, name === "position" ? "all" : name), false);
229             this._boxElements.push(boxElement);
230
231             if (name === "content") {
232                 let widthElement = createContentAreaElement.call(this, "width");
233                 let heightElement = createContentAreaElement.call(this, "height");
234                 if (!widthElement || !heightElement) {
235                     this.showEmptyMessage();
236                     return;
237                 }
238
239                 boxElement.append(widthElement, " \u00D7 ", heightElement);
240             } else {
241                 let topElement = createBoxPartElement.call(this, name, "top");
242                 let leftElement = createBoxPartElement.call(this, name, "left");
243                 let rightElement = createBoxPartElement.call(this, name, "right");
244                 let bottomElement = createBoxPartElement.call(this, name, "bottom");
245                 if (!topElement || !leftElement || !rightElement || !bottomElement) {
246                     this.showEmptyMessage();
247                     return;
248                 }
249
250                 let labelElement = document.createElement("div");
251                 labelElement.className = "label";
252                 labelElement.textContent = name;
253                 boxElement.appendChild(labelElement);
254
255                 boxElement.appendChild(topElement);
256                 boxElement.appendChild(document.createElement("br"));
257                 boxElement.appendChild(leftElement);
258
259                 if (previousBox)
260                     boxElement.appendChild(previousBox);
261
262                 boxElement.appendChild(rightElement);
263                 boxElement.appendChild(document.createElement("br"));
264                 boxElement.appendChild(bottomElement);
265             }
266
267             previousBox = boxElement;
268         }
269
270         metricsElement.appendChild(previousBox);
271         metricsElement.addEventListener("mouseover", this._highlightDOMNode.bind(this, false, ""), false);
272
273         this.hideEmptyMessage();
274         this.element.appendChild(metricsElement);
275     }
276
277     _startEditing(targetElement, box, styleProperty, computedStyle)
278     {
279         if (WI.isBeingEdited(targetElement))
280             return;
281
282         // If the target element has a title use it as the editing value
283         // since the current text is likely truncated/rounded.
284         if (targetElement.title)
285             targetElement.textContent = targetElement.title;
286
287         var context = {box, styleProperty};
288         var boundKeyDown = this._handleKeyDown.bind(this, context, styleProperty);
289         context.keyDownHandler = boundKeyDown;
290         targetElement.addEventListener("keydown", boundKeyDown, false);
291
292         this._isEditingMetrics = true;
293
294         var config = new WI.EditingConfig(this._editingCommitted.bind(this), this._editingCancelled.bind(this), context);
295         WI.startEditing(targetElement, config);
296
297         window.getSelection().setBaseAndExtent(targetElement, 0, targetElement, 1);
298     }
299
300     _alteredFloatNumber(number, event)
301     {
302         var arrowKeyPressed = event.keyIdentifier === "Up" || event.keyIdentifier === "Down";
303
304         // Jump by 10 when shift is down or jump by 0.1 when Alt/Option is down.
305         // Also jump by 10 for page up and down, or by 100 if shift is held with a page key.
306         var changeAmount = 1;
307         if (event.shiftKey && !arrowKeyPressed)
308             changeAmount = 100;
309         else if (event.shiftKey || !arrowKeyPressed)
310             changeAmount = 10;
311         else if (event.altKey)
312             changeAmount = 0.1;
313
314         if (event.keyIdentifier === "Down" || event.keyIdentifier === "PageDown")
315             changeAmount *= -1;
316
317         // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
318         // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
319         var result = Number((number + changeAmount).toFixed(6));
320         if (!String(result).match(WI.EditingSupport.NumberRegex))
321             return null;
322
323         return result;
324     }
325
326     _handleKeyDown(context, styleProperty, event)
327     {
328         if (!/^(?:Page)?(?:Up|Down)$/.test(event.keyIdentifier))
329             return;
330
331         var element = event.currentTarget;
332
333         var selection = window.getSelection();
334         if (!selection.rangeCount)
335             return;
336
337         var selectionRange = selection.getRangeAt(0);
338         console.assert(selectionRange, "We should have a range if we are handling a key down event");
339         if (!element.contains(selectionRange.commonAncestorContainer))
340             return;
341
342         var originalValue = element.textContent;
343         var wordRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, WI.EditingSupport.StyleValueDelimiters, element);
344         var wordString = wordRange.toString();
345
346         var matches = WI.EditingSupport.NumberRegex.exec(wordString);
347         var replacementString;
348         if (matches && matches.length) {
349             var prefix = matches[1];
350             var suffix = matches[3];
351             var number = this._alteredFloatNumber(parseFloat(matches[2]), event);
352             if (number === null) {
353                 // Need to check for null explicitly.
354                 return;
355             }
356
357             if (styleProperty !== "margin" && number < 0)
358                 number = 0;
359
360             replacementString = prefix + number + suffix;
361         }
362
363         if (!replacementString)
364             return;
365
366         var replacementTextNode = document.createTextNode(replacementString);
367
368         wordRange.deleteContents();
369         wordRange.insertNode(replacementTextNode);
370
371         var finalSelectionRange = document.createRange();
372         finalSelectionRange.setStart(replacementTextNode, 0);
373         finalSelectionRange.setEnd(replacementTextNode, replacementString.length);
374
375         selection.removeAllRanges();
376         selection.addRange(finalSelectionRange);
377
378         event.handled = true;
379         event.preventDefault();
380
381         this._ignoreNextRefresh = true;
382
383         this._applyUserInput(element, replacementString, originalValue, context, false);
384     }
385
386     _editingEnded(element, context)
387     {
388         element.removeEventListener("keydown", context.keyDownHandler, false);
389         this._isEditingMetrics = false;
390     }
391
392     _editingCancelled(element, context)
393     {
394         this._editingEnded(element, context);
395         this._refresh();
396     }
397
398     _applyUserInput(element, userInput, previousContent, context, commitEditor)
399     {
400         if (commitEditor && userInput === previousContent) {
401             // Nothing changed, so cancel.
402             this._editingCancelled(element, context);
403             return;
404         }
405
406         if (context.box !== "position" && (!userInput || userInput === figureDash))
407             userInput = "0px";
408         else if (context.box === "position" && (!userInput || userInput === figureDash))
409             userInput = "auto";
410
411         userInput = userInput.toLowerCase();
412         // Append a "px" unit if the user input was just a number.
413         if (/^-?(?:\d+(?:\.\d+)?|\.\d+)$/.test(userInput))
414             userInput += "px";
415
416         var styleProperty = context.styleProperty;
417         var computedStyle = this._nodeStyles.computedStyle;
418
419         if (computedStyle.propertyForName("box-sizing").value === "border-box" && (styleProperty === "width" || styleProperty === "height")) {
420             if (!userInput.match(/px$/)) {
421                 console.error("For elements with box-sizing: border-box, only absolute content area dimensions can be applied");
422                 return;
423             }
424
425             var borderBox = this._getBox(computedStyle, "border");
426             var paddingBox = this._getBox(computedStyle, "padding");
427             var userValuePx = Number(userInput.replace(/px$/, ""));
428             if (isNaN(userValuePx))
429                 return;
430             if (styleProperty === "width")
431                 userValuePx += borderBox.left + borderBox.right + paddingBox.left + paddingBox.right;
432             else
433                 userValuePx += borderBox.top + borderBox.bottom + paddingBox.top + paddingBox.bottom;
434
435             userInput = userValuePx + "px";
436         }
437
438         WI.RemoteObject.resolveNode(this._nodeStyles.node).then((object) => {
439             function inspectedPage_node_toggleInlineStyleProperty(property, value) {
440                 this.style.setProperty(property, value, "important");
441             }
442
443             let didToggle = () => {
444                 this._nodeStyles.refresh();
445             };
446
447             object.callFunction(inspectedPage_node_toggleInlineStyleProperty, [styleProperty, userInput], false, didToggle);
448             object.release();
449         });
450     }
451
452     _editingCommitted(element, userInput, previousContent, context)
453     {
454         this._editingEnded(element, context);
455         this._applyUserInput(element, userInput, previousContent, context, true);
456     }
457 };