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