2 * Copyright (C) 2007 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
31 * @extends {WebInspector.ElementsSidebarPane}
33 WebInspector.MetricsSidebarPane = function()
35 WebInspector.ElementsSidebarPane.call(this, WebInspector.UIString("Metrics"));
38 WebInspector.MetricsSidebarPane.prototype = {
42 * @return {!Promise.<?>}
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();
60 * @param {?Map.<string, string>} style
61 * @this {WebInspector.MetricsSidebarPane}
63 function callback(style)
65 if (!style || this.node() !== node)
67 this._updateMetrics(style);
70 * @param {?WebInspector.CSSStyleModel.InlineStyleResult} inlineStyleResult
71 * @this {WebInspector.MetricsSidebarPane}
73 function inlineStyleCallback(inlineStyleResult)
75 if (inlineStyleResult && this.node() === node)
76 this.inlineStyle = inlineStyleResult.inlineStyle;
80 cssModel.computedStylePromise(node.id).then(callback.bind(this)),
81 cssModel.inlineStylesPromise(node.id).then(inlineStyleCallback.bind(this))
83 return Promise.all(promises);
89 onDOMModelChanged: function()
97 onCSSModelChanged: function()
105 onFrameResizedThrottled: function()
111 * @param {!Map.<string, string>} style
112 * @param {string} propertyName
115 _getPropertyValueAsPx: function(style, propertyName)
117 return Number(style.get(propertyName).replace(/px$/, "") || 0);
121 * @param {!Map.<string, string>} computedStyle
122 * @param {string} componentName
124 _getBox: function(computedStyle, componentName)
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 };
135 * @param {boolean} showHighlight
136 * @param {string} mode
137 * @param {!Event} event
139 _highlightDOMNode: function(showHighlight, mode, event)
142 if (showHighlight && this.node()) {
143 if (this._highlightMode === mode)
145 this._highlightMode = mode;
146 this.node().highlight(mode);
148 delete this._highlightMode;
149 WebInspector.DOMModel.hideDOMNodeHighlight();
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;
157 element.style.backgroundColor = "";
162 * @param {!Map.<string, string>} style
164 _updateMetrics: function(style)
166 // Updating with computed style.
167 var metricsElement = createElement("div");
168 metricsElement.className = "metrics";
172 * @param {!Map.<string, string>} style
173 * @param {string} name
174 * @param {string} side
175 * @param {string} suffix
176 * @this {WebInspector.MetricsSidebarPane}
178 function createBoxPartElement(style, name, side, suffix)
180 var propertyName = (name !== "position" ? name + "-" : "") + side + suffix;
181 var value = style.get(propertyName);
182 if (value === "" || (name !== "position" && value === "0px"))
184 else if (name === "position" && value === "auto")
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);
197 * @param {!Map.<string, string>} style
200 function getContentAreaWidthPx(style)
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;
210 return Number.toFixedIfFloating(width.toString());
214 * @param {!Map.<string, string>} style
217 function getContentAreaHeightPx(style)
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;
227 return Number.toFixedIfFloating(height.toString());
230 // Display types for which margin is ignored.
231 var noMarginDisplayType = {
233 "table-column": true,
234 "table-column-group": true,
235 "table-footer-group": true,
236 "table-header-group": true,
238 "table-row-group": true
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,
248 "table-row-group": true
251 // Position types for which top, left, bottom and right are ignored.
252 var noPositionType = {
256 var boxes = ["content", "padding", "border", "margin", "position"];
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])
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) {
270 if (name === "margin" && noMarginDisplayType[style.get("display")])
272 if (name === "padding" && noPaddingDisplayType[style.get("display")])
274 if (name === "position" && noPositionType[style.get("position")])
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);
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));
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));
317 previousBox = boxElement;
320 metricsElement.appendChild(previousBox);
321 metricsElement.addEventListener("mouseover", this._highlightDOMNode.bind(this, false, "all"), false);
322 this.element.removeChildren();
323 this.element.appendChild(metricsElement);
327 * @param {!Element} targetElement
328 * @param {string} box
329 * @param {string} styleProperty
330 * @param {!Map.<string, string>} computedStyle
332 startEditing: function(targetElement, box, styleProperty, computedStyle)
334 if (WebInspector.isBeingEdited(targetElement))
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);
350 _handleKeyDown: function(context, styleProperty, event)
352 var element = event.currentTarget;
355 * @param {string} originalValue
356 * @param {string} replacementString
357 * @this {WebInspector.MetricsSidebarPane}
359 function finishHandler(originalValue, replacementString)
361 this._applyUserInput(element, replacementString, originalValue, context, false);
365 * @param {string} prefix
366 * @param {number} number
367 * @param {string} suffix
370 function customNumberHandler(prefix, number, suffix)
372 if (styleProperty !== "margin" && number < 0)
374 return prefix + number + suffix;
377 WebInspector.handleElementValueModifications(event, element, finishHandler.bind(this), undefined, customNumberHandler);
380 editingEnded: function(element, context)
382 delete this.originalPropertyData;
383 delete this.previousPropertyDataCandidate;
384 element.removeEventListener("keydown", context.keyDownHandler, false);
385 delete this._isEditingMetrics;
388 editingCancelled: function(element, context)
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);
397 this.inlineStyle.allProperties[this.originalPropertyData.index].setText(this.originalPropertyData.propertyText, false);
399 this.editingEnded(element, context);
403 _applyUserInput: function(element, userInput, previousContent, context, commitEditor)
405 if (!this.inlineStyle) {
406 // Element has no renderer.
407 return this.editingCancelled(element, context); // nothing changed, so cancel
410 if (commitEditor && userInput === previousContent)
411 return this.editingCancelled(element, context); // nothing changed, so cancel
413 if (context.box !== "position" && (!userInput || userInput === "\u2012"))
415 else if (context.box === "position" && (!userInput || userInput === "\u2012"))
418 userInput = userInput.toLowerCase();
419 // Append a "px" unit if the user input was just a number.
420 if (/^\d+$/.test(userInput))
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");
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))
437 if (styleProperty === "width")
438 userValuePx += borderBox.left + borderBox.right + paddingBox.left + paddingBox.right;
440 userValuePx += borderBox.top + borderBox.bottom + paddingBox.top + paddingBox.bottom;
442 userInput = userValuePx + "px";
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())
453 this.previousPropertyDataCandidate = property;
454 property.setValue(userInput, commitEditor, true, callback.bind(this));
458 this.inlineStyle.appendProperty(context.styleProperty, userInput, callback.bind(this));
461 * @param {boolean} success
462 * @this {WebInspector.MetricsSidebarPane}
464 function callback(success)
468 if (!("originalPropertyData" in this))
469 this.originalPropertyData = this.previousPropertyDataCandidate;
471 if (typeof this._highlightMode !== "undefined")
472 this._node.highlight(this._highlightMode);
479 editingCommitted: function(element, userInput, previousContent, context)
481 this.editingEnded(element, context);
482 this._applyUserInput(element, userInput, previousContent, context, true);
485 __proto__: WebInspector.ElementsSidebarPane.prototype