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