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