Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / elements / MetricsSidebarPane.js
blob620f76f707ed912ebc72e4266251e4220c8276fb
1 /*
2  * Copyright (C) 2007 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  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer.
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution.
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
29 /**
30  * @constructor
31  * @extends {WebInspector.ElementsSidebarPane}
32  */
33 WebInspector.MetricsSidebarPane = function()
35     WebInspector.ElementsSidebarPane.call(this, WebInspector.UIString("Metrics"));
38 WebInspector.MetricsSidebarPane.prototype = {
39     /**
40      * @override
41      * @protected
42      * @return {!Promise.<?>}
43      */
44     doUpdate: function()
45     {
46         // "style" attribute might have changed. Update metrics unless they are being edited
47         // (if a CSS property is added, a StyleSheetChanged event is dispatched).
48         if (this._isEditingMetrics)
49             return Promise.resolve();
51         // FIXME: avoid updates of a collapsed pane.
52         var node = this.node();
53         var cssModel = this.cssModel();
54         if (!node || node.nodeType() !== Node.ELEMENT_NODE || !cssModel) {
55             this.element.removeChildren();
56             return Promise.resolve();
57         }
59         /**
60          * @param {?Map.<string, string>} style
61          * @this {WebInspector.MetricsSidebarPane}
62          */
63         function callback(style)
64         {
65             if (!style || this.node() !== node)
66                 return;
67             this._updateMetrics(style);
68         }
69         /**
70          * @param {?WebInspector.CSSStyleModel.InlineStyleResult} inlineStyleResult
71          * @this {WebInspector.MetricsSidebarPane}
72          */
73         function inlineStyleCallback(inlineStyleResult)
74         {
75             if (inlineStyleResult && this.node() === node)
76                 this.inlineStyle = inlineStyleResult.inlineStyle;
77         }
79         var promises = [
80             cssModel.computedStylePromise(node.id).then(callback.bind(this)),
81             cssModel.inlineStylesPromise(node.id).then(inlineStyleCallback.bind(this))
82         ];
83         return Promise.all(promises);
84     },
86     /**
87      * @override
88      */
89     onDOMModelChanged: function()
90     {
91         this.update();
92     },
94     /**
95      * @override
96      */
97     onCSSModelChanged: function()
98     {
99         this.update();
100     },
102     /**
103      * @override
104      */
105     onFrameResizedThrottled: function()
106     {
107         this.update();
108     },
110     /**
111      * @param {!Map.<string, string>} style
112      * @param {string} propertyName
113      * @return {number}
114      */
115     _getPropertyValueAsPx: function(style, propertyName)
116     {
117         return Number(style.get(propertyName).replace(/px$/, "") || 0);
118     },
120     /**
121      * @param {!Map.<string, string>} computedStyle
122      * @param {string} componentName
123      */
124     _getBox: function(computedStyle, componentName)
125     {
126         var suffix = componentName === "border" ? "-width" : "";
127         var left = this._getPropertyValueAsPx(computedStyle, componentName + "-left" + suffix);
128         var top = this._getPropertyValueAsPx(computedStyle, componentName + "-top" + suffix);
129         var right = this._getPropertyValueAsPx(computedStyle, componentName + "-right" + suffix);
130         var bottom = this._getPropertyValueAsPx(computedStyle, componentName + "-bottom" + suffix);
131         return { left: left, top: top, right: right, bottom: bottom };
132     },
134     /**
135      * @param {boolean} showHighlight
136      * @param {string} mode
137      * @param {!Event} event
138      */
139     _highlightDOMNode: function(showHighlight, mode, event)
140     {
141         event.consume();
142         if (showHighlight && this.node()) {
143             if (this._highlightMode === mode)
144                 return;
145             this._highlightMode = mode;
146             this.node().highlight(mode);
147         } else {
148             delete this._highlightMode;
149             WebInspector.DOMModel.hideDOMNodeHighlight();
150         }
152         for (var i = 0; this._boxElements && i < this._boxElements.length; ++i) {
153             var element = this._boxElements[i];
154             if (!this.node() || mode === "all" || element._name === mode)
155                 element.style.backgroundColor = element._backgroundColor;
156             else
157                 element.style.backgroundColor = "";
158         }
159     },
161     /**
162      * @param {!Map.<string, string>} style
163      */
164     _updateMetrics: function(style)
165     {
166         // Updating with computed style.
167         var metricsElement = createElement("div");
168         metricsElement.className = "metrics";
169         var self = this;
171         /**
172          * @param {!Map.<string, string>} style
173          * @param {string} name
174          * @param {string} side
175          * @param {string} suffix
176          * @this {WebInspector.MetricsSidebarPane}
177          */
178         function createBoxPartElement(style, name, side, suffix)
179         {
180             var propertyName = (name !== "position" ? name + "-" : "") + side + suffix;
181             var value = style.get(propertyName);
182             if (value === "" || (name !== "position" && value === "0px"))
183                 value = "\u2012";
184             else if (name === "position" && value === "auto")
185                 value = "\u2012";
186             value = value.replace(/px$/, "");
187             value = Number.toFixedIfFloating(value);
189             var element = createElement("div");
190             element.className = side;
191             element.textContent = value;
192             element.addEventListener("dblclick", this.startEditing.bind(this, element, name, propertyName, style), false);
193             return element;
194         }
196         /**
197          * @param {!Map.<string, string>} style
198          * @return {string}
199          */
200         function getContentAreaWidthPx(style)
201         {
202             var width = style.get("width").replace(/px$/, "");
203             if (!isNaN(width) && style.get("box-sizing") === "border-box") {
204                 var borderBox = self._getBox(style, "border");
205                 var paddingBox = self._getBox(style, "padding");
207                 width = width - borderBox.left - borderBox.right - paddingBox.left - paddingBox.right;
208             }
210             return Number.toFixedIfFloating(width.toString());
211         }
213         /**
214          * @param {!Map.<string, string>} style
215          * @return {string}
216          */
217         function getContentAreaHeightPx(style)
218         {
219             var height = style.get("height").replace(/px$/, "");
220             if (!isNaN(height) && style.get("box-sizing") === "border-box") {
221                 var borderBox = self._getBox(style, "border");
222                 var paddingBox = self._getBox(style, "padding");
224                 height = height - borderBox.top - borderBox.bottom - paddingBox.top - paddingBox.bottom;
225             }
227             return Number.toFixedIfFloating(height.toString());
228         }
230         // Display types for which margin is ignored.
231         var noMarginDisplayType = {
232             "table-cell": true,
233             "table-column": true,
234             "table-column-group": true,
235             "table-footer-group": true,
236             "table-header-group": true,
237             "table-row": true,
238             "table-row-group": true
239         };
241         // Display types for which padding is ignored.
242         var noPaddingDisplayType = {
243             "table-column": true,
244             "table-column-group": true,
245             "table-footer-group": true,
246             "table-header-group": true,
247             "table-row": true,
248             "table-row-group": true
249         };
251         // Position types for which top, left, bottom and right are ignored.
252         var noPositionType = {
253             "static": true
254         };
256         var boxes = ["content", "padding", "border", "margin", "position"];
257         var boxColors = [
258             WebInspector.Color.PageHighlight.Content,
259             WebInspector.Color.PageHighlight.Padding,
260             WebInspector.Color.PageHighlight.Border,
261             WebInspector.Color.PageHighlight.Margin,
262             WebInspector.Color.fromRGBA([0, 0, 0, 0])
263         ];
264         var boxLabels = [WebInspector.UIString("content"), WebInspector.UIString("padding"), WebInspector.UIString("border"), WebInspector.UIString("margin"), WebInspector.UIString("position")];
265         var previousBox = null;
266         this._boxElements = [];
267         for (var i = 0; i < boxes.length; ++i) {
268             var name = boxes[i];
270             if (name === "margin" && noMarginDisplayType[style.get("display")])
271                 continue;
272             if (name === "padding" && noPaddingDisplayType[style.get("display")])
273                 continue;
274             if (name === "position" && noPositionType[style.get("position")])
275                 continue;
277             var boxElement = createElement("div");
278             boxElement.className = name;
279             boxElement._backgroundColor = boxColors[i].asString(WebInspector.Color.Format.RGBA);
280             boxElement._name = name;
281             boxElement.style.backgroundColor = boxElement._backgroundColor;
282             boxElement.addEventListener("mouseover", this._highlightDOMNode.bind(this, true, name === "position" ? "all" : name), false);
283             this._boxElements.push(boxElement);
285             if (name === "content") {
286                 var widthElement = createElement("span");
287                 widthElement.textContent = getContentAreaWidthPx(style);
288                 widthElement.addEventListener("dblclick", this.startEditing.bind(this, widthElement, "width", "width", style), false);
290                 var heightElement = createElement("span");
291                 heightElement.textContent = getContentAreaHeightPx(style);
292                 heightElement.addEventListener("dblclick", this.startEditing.bind(this, heightElement, "height", "height", style), false);
294                 boxElement.appendChild(widthElement);
295                 boxElement.createTextChild(" \u00D7 ");
296                 boxElement.appendChild(heightElement);
297             } else {
298                 var suffix = (name === "border" ? "-width" : "");
300                 var labelElement = createElement("div");
301                 labelElement.className = "label";
302                 labelElement.textContent = boxLabels[i];
303                 boxElement.appendChild(labelElement);
305                 boxElement.appendChild(createBoxPartElement.call(this, style, name, "top", suffix));
306                 boxElement.appendChild(createElement("br"));
307                 boxElement.appendChild(createBoxPartElement.call(this, style, name, "left", suffix));
309                 if (previousBox)
310                     boxElement.appendChild(previousBox);
312                 boxElement.appendChild(createBoxPartElement.call(this, style, name, "right", suffix));
313                 boxElement.appendChild(createElement("br"));
314                 boxElement.appendChild(createBoxPartElement.call(this, style, name, "bottom", suffix));
315             }
317             previousBox = boxElement;
318         }
320         metricsElement.appendChild(previousBox);
321         metricsElement.addEventListener("mouseover", this._highlightDOMNode.bind(this, false, "all"), false);
322         this.element.removeChildren();
323         this.element.appendChild(metricsElement);
324     },
326     /**
327      * @param {!Element} targetElement
328      * @param {string} box
329      * @param {string} styleProperty
330      * @param {!Map.<string, string>} computedStyle
331      */
332     startEditing: function(targetElement, box, styleProperty, computedStyle)
333     {
334         if (WebInspector.isBeingEdited(targetElement))
335             return;
337         var context = { box: box, styleProperty: styleProperty, computedStyle: computedStyle };
338         var boundKeyDown = this._handleKeyDown.bind(this, context, styleProperty);
339         context.keyDownHandler = boundKeyDown;
340         targetElement.addEventListener("keydown", boundKeyDown, false);
342         this._isEditingMetrics = true;
344         var config = new WebInspector.InplaceEditor.Config(this.editingCommitted.bind(this), this.editingCancelled.bind(this), context);
345         WebInspector.InplaceEditor.startEditing(targetElement, config);
347         targetElement.getComponentSelection().setBaseAndExtent(targetElement, 0, targetElement, 1);
348     },
350     _handleKeyDown: function(context, styleProperty, event)
351     {
352         var element = event.currentTarget;
354         /**
355          * @param {string} originalValue
356          * @param {string} replacementString
357          * @this {WebInspector.MetricsSidebarPane}
358          */
359         function finishHandler(originalValue, replacementString)
360         {
361             this._applyUserInput(element, replacementString, originalValue, context, false);
362         }
364         /**
365          * @param {string} prefix
366          * @param {number} number
367          * @param {string} suffix
368          * @return {string}
369          */
370         function customNumberHandler(prefix, number, suffix)
371         {
372             if (styleProperty !== "margin" && number < 0)
373                 number = 0;
374             return prefix + number + suffix;
375         }
377         WebInspector.handleElementValueModifications(event, element, finishHandler.bind(this), undefined, customNumberHandler);
378     },
380     editingEnded: function(element, context)
381     {
382         delete this.originalPropertyData;
383         delete this.previousPropertyDataCandidate;
384         element.removeEventListener("keydown", context.keyDownHandler, false);
385         delete this._isEditingMetrics;
386     },
388     editingCancelled: function(element, context)
389     {
390         if ("originalPropertyData" in this && this.inlineStyle) {
391             if (!this.originalPropertyData) {
392                 // An added property, remove the last property in the style.
393                 var pastLastSourcePropertyIndex = this.inlineStyle.pastLastSourcePropertyIndex();
394                 if (pastLastSourcePropertyIndex)
395                     this.inlineStyle.allProperties[pastLastSourcePropertyIndex - 1].setText("", false);
396             } else
397                 this.inlineStyle.allProperties[this.originalPropertyData.index].setText(this.originalPropertyData.propertyText, false);
398         }
399         this.editingEnded(element, context);
400         this.update();
401     },
403     _applyUserInput: function(element, userInput, previousContent, context, commitEditor)
404     {
405         if (!this.inlineStyle) {
406             // Element has no renderer.
407             return this.editingCancelled(element, context); // nothing changed, so cancel
408         }
410         if (commitEditor && userInput === previousContent)
411             return this.editingCancelled(element, context); // nothing changed, so cancel
413         if (context.box !== "position" && (!userInput || userInput === "\u2012"))
414             userInput = "0px";
415         else if (context.box === "position" && (!userInput || userInput === "\u2012"))
416             userInput = "auto";
418         userInput = userInput.toLowerCase();
419         // Append a "px" unit if the user input was just a number.
420         if (/^\d+$/.test(userInput))
421             userInput += "px";
423         var styleProperty = context.styleProperty;
424         var computedStyle = context.computedStyle;
426         if (computedStyle.get("box-sizing") === "border-box" && (styleProperty === "width" || styleProperty === "height")) {
427             if (!userInput.match(/px$/)) {
428                 WebInspector.console.error("For elements with box-sizing: border-box, only absolute content area dimensions can be applied");
429                 return;
430             }
432             var borderBox = this._getBox(computedStyle, "border");
433             var paddingBox = this._getBox(computedStyle, "padding");
434             var userValuePx = Number(userInput.replace(/px$/, ""));
435             if (isNaN(userValuePx))
436                 return;
437             if (styleProperty === "width")
438                 userValuePx += borderBox.left + borderBox.right + paddingBox.left + paddingBox.right;
439             else
440                 userValuePx += borderBox.top + borderBox.bottom + paddingBox.top + paddingBox.bottom;
442             userInput = userValuePx + "px";
443         }
445         this.previousPropertyDataCandidate = null;
447         var allProperties = this.inlineStyle.allProperties;
448         for (var i = 0; i < allProperties.length; ++i) {
449             var property = allProperties[i];
450             if (property.name !== context.styleProperty || !property.activeInStyle())
451                 continue;
453             this.previousPropertyDataCandidate = property;
454             property.setValue(userInput, commitEditor, true, callback.bind(this));
455             return;
456         }
458         this.inlineStyle.appendProperty(context.styleProperty, userInput, callback.bind(this));
460         /**
461          * @param {boolean} success
462          * @this {WebInspector.MetricsSidebarPane}
463          */
464         function callback(success)
465         {
466             if (!success)
467                 return;
468             if (!("originalPropertyData" in this))
469                 this.originalPropertyData = this.previousPropertyDataCandidate;
471             if (typeof this._highlightMode !== "undefined")
472                 this._node.highlight(this._highlightMode);
474             if (commitEditor)
475                 this.update();
476         }
477     },
479     editingCommitted: function(element, userInput, previousContent, context)
480     {
481         this.editingEnded(element, context);
482         this._applyUserInput(element, userInput, previousContent, context, true);
483     },
485     __proto__: WebInspector.ElementsSidebarPane.prototype