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 */ 28 29 /** 30 * @constructor 31 * @extends {WebInspector.SidebarPane} 32 */ 33 WebInspector.MetricsSidebarPane = function() 34 { 35 WebInspector.SidebarPane.call(this, WebInspector.UIString("Metrics")); 36 37 WebInspector.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._styleSheetOrMediaQueryResultChanged, this); 38 WebInspector.cssModel.addEventListener(WebInspector.CSSStyleModel.Events.MediaQueryResultChanged, this._styleSheetOrMediaQueryResultChanged, this); 39 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrModified, this._attributesUpdated, this); 40 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrRemoved, this._attributesUpdated, this); 41 } 42 43 WebInspector.MetricsSidebarPane.prototype = { 44 /** 45 * @param {WebInspector.DOMNode=} node 46 */ 47 update: function(node) 48 { 49 if (node) 50 this.node = node; 51 this._innerUpdate(); 52 }, 53 54 _innerUpdate: function() 55 { 56 // "style" attribute might have changed. Update metrics unless they are being edited 57 // (if a CSS property is added, a StyleSheetChanged event is dispatched). 58 if (this._isEditingMetrics) 59 return; 60 61 // FIXME: avoid updates of a collapsed pane. 62 var node = this.node; 63 64 if (!node || node.nodeType() !== Node.ELEMENT_NODE) { 65 this.bodyElement.removeChildren(); 66 return; 67 } 68 69 function callback(style) 70 { 71 if (!style || this.node !== node) 72 return; 73 this._updateMetrics(style); 74 } 75 WebInspector.cssModel.getComputedStyleAsync(node.id, callback.bind(this)); 76 77 function inlineStyleCallback(style) 78 { 79 if (!style || this.node !== node) 80 return; 81 this.inlineStyle = style; 82 } 83 WebInspector.cssModel.getInlineStylesAsync(node.id, inlineStyleCallback.bind(this)); 84 }, 85 86 _styleSheetOrMediaQueryResultChanged: function() 87 { 88 this._innerUpdate(); 89 }, 90 91 _attributesUpdated: function(event) 92 { 93 if (this.node !== event.data.node) 94 return; 95 96 this._innerUpdate(); 97 }, 98 99 _getPropertyValueAsPx: function(style, propertyName) 100 { 101 return Number(style.getPropertyValue(propertyName).replace(/px$/, "") || 0); 102 }, 103 104 _getBox: function(computedStyle, componentName) 105 { 106 var suffix = componentName === "border" ? "-width" : ""; 107 var left = this._getPropertyValueAsPx(computedStyle, componentName + "-left" + suffix); 108 var top = this._getPropertyValueAsPx(computedStyle, componentName + "-top" + suffix); 109 var right = this._getPropertyValueAsPx(computedStyle, componentName + "-right" + suffix); 110 var bottom = this._getPropertyValueAsPx(computedStyle, componentName + "-bottom" + suffix); 111 return { left: left, top: top, right: right, bottom: bottom }; 112 }, 113 114 _highlightDOMNode: function(showHighlight, mode, event) 115 { 116 event.consume(); 117 var nodeId = showHighlight && this.node ? this.node.id : 0; 118 if (nodeId) { 119 if (this._highlightMode === mode) 120 return; 121 this._highlightMode = mode; 122 WebInspector.domAgent.highlightDOMNode(nodeId, mode); 123 } else { 124 delete this._highlightMode; 125 WebInspector.domAgent.hideDOMNodeHighlight(); 126 } 127 128 for (var i = 0; this._boxElements && i < this._boxElements.length; ++i) { 129 var element = this._boxElements[i]; 130 if (!nodeId || mode === "all" || element._name === mode) 131 element.style.backgroundColor = element._backgroundColor; 132 else 133 element.style.backgroundColor = ""; 134 } 135 }, 136 137 _updateMetrics: function(style) 138 { 139 // Updating with computed style. 140 var metricsElement = document.createElement("div"); 141 metricsElement.className = "metrics"; 142 var self = this; 143 144 function createBoxPartElement(style, name, side, suffix) 145 { 146 var propertyName = (name !== "position" ? name + "-" : "") + side + suffix; 147 var value = style.getPropertyValue(propertyName); 148 if (value === "" || (name !== "position" && value === "0px")) 149 value = "\u2012"; 150 else if (name === "position" && value === "auto") 151 value = "\u2012"; 152 value = value.replace(/px$/, ""); 153 value = Number.toFixedIfFloating(value); 154 155 var element = document.createElement("div"); 156 element.className = side; 157 element.textContent = value; 158 element.addEventListener("dblclick", this.startEditing.bind(this, element, name, propertyName, style), false); 159 return element; 160 } 161 162 function getContentAreaWidthPx(style) 163 { 164 var width = style.getPropertyValue("width").replace(/px$/, ""); 165 if (style.getPropertyValue("box-sizing") === "border-box") { 166 var borderBox = self._getBox(style, "border"); 167 var paddingBox = self._getBox(style, "padding"); 168 169 width = width - borderBox.left - borderBox.right - paddingBox.left - paddingBox.right; 170 } 171 172 return Number.toFixedIfFloating(width); 173 } 174 175 function getContentAreaHeightPx(style) 176 { 177 var height = style.getPropertyValue("height").replace(/px$/, ""); 178 if (style.getPropertyValue("box-sizing") === "border-box") { 179 var borderBox = self._getBox(style, "border"); 180 var paddingBox = self._getBox(style, "padding"); 181 182 height = height - borderBox.top - borderBox.bottom - paddingBox.top - paddingBox.bottom; 183 } 184 185 return Number.toFixedIfFloating(height); 186 } 187 188 // Display types for which margin is ignored. 189 var noMarginDisplayType = { 190 "table-cell": true, 191 "table-column": true, 192 "table-column-group": true, 193 "table-footer-group": true, 194 "table-header-group": true, 195 "table-row": true, 196 "table-row-group": true 197 }; 198 199 // Display types for which padding is ignored. 200 var noPaddingDisplayType = { 201 "table-column": true, 202 "table-column-group": true, 203 "table-footer-group": true, 204 "table-header-group": true, 205 "table-row": true, 206 "table-row-group": true 207 }; 208 209 // Position types for which top, left, bottom and right are ignored. 210 var noPositionType = { 211 "static": true 212 }; 213 214 var boxes = ["content", "padding", "border", "margin", "position"]; 215 var boxColors = [ 216 WebInspector.Color.PageHighlight.Content, 217 WebInspector.Color.PageHighlight.Padding, 218 WebInspector.Color.PageHighlight.Border, 219 WebInspector.Color.PageHighlight.Margin, 220 WebInspector.Color.fromRGBA([0, 0, 0, 0]) 221 ]; 222 var boxLabels = [WebInspector.UIString("content"), WebInspector.UIString("padding"), WebInspector.UIString("border"), WebInspector.UIString("margin"), WebInspector.UIString("position")]; 223 var previousBox = null; 224 this._boxElements = []; 225 for (var i = 0; i < boxes.length; ++i) { 226 var name = boxes[i]; 227 228 if (name === "margin" && noMarginDisplayType[style.getPropertyValue("display")]) 229 continue; 230 if (name === "padding" && noPaddingDisplayType[style.getPropertyValue("display")]) 231 continue; 232 if (name === "position" && noPositionType[style.getPropertyValue("position")]) 233 continue; 234 235 var boxElement = document.createElement("div"); 236 boxElement.className = name; 237 boxElement._backgroundColor = boxColors[i].toString(WebInspector.Color.Format.RGBA); 238 boxElement._name = name; 239 boxElement.style.backgroundColor = boxElement._backgroundColor; 240 boxElement.addEventListener("mouseover", this._highlightDOMNode.bind(this, true, name === "position" ? "all" : name), false); 241 this._boxElements.push(boxElement); 242 243 if (name === "content") { 244 var widthElement = document.createElement("span"); 245 widthElement.textContent = getContentAreaWidthPx(style); 246 widthElement.addEventListener("dblclick", this.startEditing.bind(this, widthElement, "width", "width", style), false); 247 248 var heightElement = document.createElement("span"); 249 heightElement.textContent = getContentAreaHeightPx(style); 250 heightElement.addEventListener("dblclick", this.startEditing.bind(this, heightElement, "height", "height", style), false); 251 252 boxElement.appendChild(widthElement); 253 boxElement.appendChild(document.createTextNode(" \u00D7 ")); 254 boxElement.appendChild(heightElement); 255 } else { 256 var suffix = (name === "border" ? "-width" : ""); 257 258 var labelElement = document.createElement("div"); 259 labelElement.className = "label"; 260 labelElement.textContent = boxLabels[i]; 261 boxElement.appendChild(labelElement); 262 263 boxElement.appendChild(createBoxPartElement.call(this, style, name, "top", suffix)); 264 boxElement.appendChild(document.createElement("br")); 265 boxElement.appendChild(createBoxPartElement.call(this, style, name, "left", suffix)); 266 267 if (previousBox) 268 boxElement.appendChild(previousBox); 269 270 boxElement.appendChild(createBoxPartElement.call(this, style, name, "right", suffix)); 271 boxElement.appendChild(document.createElement("br")); 272 boxElement.appendChild(createBoxPartElement.call(this, style, name, "bottom", suffix)); 273 } 274 275 previousBox = boxElement; 276 } 277 278 metricsElement.appendChild(previousBox); 279 metricsElement.addEventListener("mouseover", this._highlightDOMNode.bind(this, false, ""), false); 280 this.bodyElement.removeChildren(); 281 this.bodyElement.appendChild(metricsElement); 282 }, 283 284 startEditing: function(targetElement, box, styleProperty, computedStyle) 285 { 286 if (WebInspector.isBeingEdited(targetElement)) 287 return; 288 289 var context = { box: box, styleProperty: styleProperty, computedStyle: computedStyle }; 290 var boundKeyDown = this._handleKeyDown.bind(this, context, styleProperty); 291 context.keyDownHandler = boundKeyDown; 292 targetElement.addEventListener("keydown", boundKeyDown, false); 293 294 this._isEditingMetrics = true; 295 296 var config = new WebInspector.EditingConfig(this.editingCommitted.bind(this), this.editingCancelled.bind(this), context); 297 WebInspector.startEditing(targetElement, config); 298 299 window.getSelection().setBaseAndExtent(targetElement, 0, targetElement, 1); 300 }, 301 302 _handleKeyDown: function(context, styleProperty, event) 303 { 304 var element = event.currentTarget; 305 306 function finishHandler(originalValue, replacementString) 307 { 308 this._applyUserInput(element, replacementString, originalValue, context, false); 309 } 310 311 function customNumberHandler(number) 312 { 313 if (styleProperty !== "margin" && number < 0) 314 number = 0; 315 return number; 316 } 317 318 WebInspector.handleElementValueModifications(event, element, finishHandler.bind(this), undefined, customNumberHandler); 319 }, 320 321 editingEnded: function(element, context) 322 { 323 delete this.originalPropertyData; 324 delete this.previousPropertyDataCandidate; 325 element.removeEventListener("keydown", context.keyDownHandler, false); 326 delete this._isEditingMetrics; 327 }, 328 329 editingCancelled: function(element, context) 330 { 331 if ("originalPropertyData" in this && this.inlineStyle) { 332 if (!this.originalPropertyData) { 333 // An added property, remove the last property in the style. 334 var pastLastSourcePropertyIndex = this.inlineStyle.pastLastSourcePropertyIndex(); 335 if (pastLastSourcePropertyIndex) 336 this.inlineStyle.allProperties[pastLastSourcePropertyIndex - 1].setText("", false); 337 } else 338 this.inlineStyle.allProperties[this.originalPropertyData.index].setText(this.originalPropertyData.propertyText, false); 339 } 340 this.editingEnded(element, context); 341 this.update(); 342 }, 343 344 _applyUserInput: function(element, userInput, previousContent, context, commitEditor) 345 { 346 if (!this.inlineStyle) { 347 // Element has no renderer. 348 return this.editingCancelled(element, context); // nothing changed, so cancel 349 } 350 351 if (commitEditor && userInput === previousContent) 352 return this.editingCancelled(element, context); // nothing changed, so cancel 353 354 if (context.box !== "position" && (!userInput || userInput === "\u2012")) 355 userInput = "0px"; 356 else if (context.box === "position" && (!userInput || userInput === "\u2012")) 357 userInput = "auto"; 358 359 userInput = userInput.toLowerCase(); 360 // Append a "px" unit if the user input was just a number. 361 if (/^\d+$/.test(userInput)) 362 userInput += "px"; 363 364 var styleProperty = context.styleProperty; 365 var computedStyle = context.computedStyle; 366 367 if (computedStyle.getPropertyValue("box-sizing") === "border-box" && (styleProperty === "width" || styleProperty === "height")) { 368 if (!userInput.match(/px$/)) { 369 WebInspector.log("For elements with box-sizing: border-box, only absolute content area dimensions can be applied", WebInspector.ConsoleMessage.MessageLevel.Error, true); 370 return; 371 } 372 373 var borderBox = this._getBox(computedStyle, "border"); 374 var paddingBox = this._getBox(computedStyle, "padding"); 375 var userValuePx = Number(userInput.replace(/px$/, "")); 376 if (isNaN(userValuePx)) 377 return; 378 if (styleProperty === "width") 379 userValuePx += borderBox.left + borderBox.right + paddingBox.left + paddingBox.right; 380 else 381 userValuePx += borderBox.top + borderBox.bottom + paddingBox.top + paddingBox.bottom; 382 383 userInput = userValuePx + "px"; 384 } 385 386 this.previousPropertyDataCandidate = null; 387 var self = this; 388 var callback = function(style) { 389 if (!style) 390 return; 391 self.inlineStyle = style; 392 if (!("originalPropertyData" in self)) 393 self.originalPropertyData = self.previousPropertyDataCandidate; 394 395 if (typeof self._highlightMode !== "undefined") { 396 WebInspector.domAgent.highlightDOMNode(self.node.id, self._highlightMode); 397 } 398 399 if (commitEditor) { 400 self.dispatchEventToListeners("metrics edited"); 401 self.update(); 402 } 403 }; 404 405 var allProperties = this.inlineStyle.allProperties; 406 for (var i = 0; i < allProperties.length; ++i) { 407 var property = allProperties[i]; 408 if (property.name !== context.styleProperty || property.inactive) 409 continue; 410 411 this.previousPropertyDataCandidate = property; 412 property.setValue(userInput, commitEditor, true, callback); 413 return; 414 } 415 416 this.inlineStyle.appendProperty(context.styleProperty, userInput, callback); 417 }, 418 419 editingCommitted: function(element, userInput, previousContent, context) 420 { 421 this.editingEnded(element, context); 422 this._applyUserInput(element, userInput, previousContent, context, true); 423 }, 424 425 __proto__: WebInspector.SidebarPane.prototype 426 } 427