1/* 2 * Copyright (C) 2013 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 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26WebInspector.BoxModelDetailsSectionRow = function() { 27 WebInspector.DetailsSectionRow.call(this, WebInspector.UIString("No Box Model Information")); 28 29 this.element.classList.add(WebInspector.BoxModelDetailsSectionRow.StyleClassName); 30 31 this._nodeStyles = null; 32}; 33 34WebInspector.BoxModelDetailsSectionRow.StyleClassName = "box-model"; 35WebInspector.BoxModelDetailsSectionRow.StyleValueDelimiters = " \xA0\t\n\"':;,/()"; 36WebInspector.BoxModelDetailsSectionRow.CSSNumberRegex = /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/; 37 38WebInspector.BoxModelDetailsSectionRow.prototype = { 39 constructor: WebInspector.BoxModelDetailsSectionRow, 40 41 // Public 42 43 get nodeStyles() 44 { 45 return this._nodeStyles; 46 }, 47 48 set nodeStyles(nodeStyles) 49 { 50 this._nodeStyles = nodeStyles; 51 52 this._refresh(); 53 }, 54 55 // Private 56 57 _refresh: function() 58 { 59 if (this._ignoreNextRefresh) { 60 delete this._ignoreNextRefresh; 61 return; 62 } 63 64 this._updateMetrics(); 65 }, 66 67 _getPropertyValueAsPx: function(style, propertyName) 68 { 69 return Number(style.propertyForName(propertyName).value.replace(/px$/, "") || 0); 70 }, 71 72 _getBox: function(computedStyle, componentName) 73 { 74 var suffix = componentName === "border" ? "-width" : ""; 75 var left = this._getPropertyValueAsPx(computedStyle, componentName + "-left" + suffix); 76 var top = this._getPropertyValueAsPx(computedStyle, componentName + "-top" + suffix); 77 var right = this._getPropertyValueAsPx(computedStyle, componentName + "-right" + suffix); 78 var bottom = this._getPropertyValueAsPx(computedStyle, componentName + "-bottom" + suffix); 79 return { left: left, top: top, right: right, bottom: bottom }; 80 }, 81 82 _highlightDOMNode: function(showHighlight, mode, event) 83 { 84 event.stopPropagation(); 85 86 var nodeId = showHighlight ? this.nodeStyles.node.id : 0; 87 if (nodeId) { 88 if (this._highlightMode === mode) 89 return; 90 this._highlightMode = mode; 91 WebInspector.domTreeManager.highlightDOMNode(nodeId, mode); 92 } else { 93 delete this._highlightMode; 94 WebInspector.domTreeManager.hideDOMNodeHighlight(); 95 } 96 97 for (var i = 0; this._boxElements && i < this._boxElements.length; ++i) { 98 var element = this._boxElements[i]; 99 if (nodeId && (mode === "all" || element._name === mode)) 100 element.classList.add("active"); 101 else 102 element.classList.remove("active"); 103 } 104 }, 105 106 _updateMetrics: function() 107 { 108 // Updating with computed style. 109 var metricsElement = document.createElement("div"); 110 111 var self = this; 112 var style = this._nodeStyles.computedStyle; 113 114 function createElement(type, value, name, propertyName, style) 115 { 116 // Check if the value is a float and whether it should be rounded. 117 var floatValue = parseFloat(value); 118 var shouldRoundValue = (!isNaN(floatValue) && floatValue % 1 !== 0); 119 120 var element = document.createElement(type); 121 element.textContent = shouldRoundValue ? ("~" + Math.round(floatValue * 100) / 100) : value; 122 if (shouldRoundValue) 123 element.title = value; 124 element.addEventListener("dblclick", this._startEditing.bind(this, element, name, propertyName, style), false); 125 return element; 126 } 127 128 function createBoxPartElement(style, name, side, suffix) 129 { 130 var propertyName = (name !== "position" ? name + "-" : "") + side + suffix; 131 var value = style.propertyForName(propertyName).value; 132 if (value === "" || (name !== "position" && value === "0px")) 133 value = "\u2012"; 134 else if (name === "position" && value === "auto") 135 value = "\u2012"; 136 value = value.replace(/px$/, ""); 137 138 var element = createElement.call(this, "div", value, name, propertyName, style); 139 element.className = side; 140 return element; 141 } 142 143 function createContentAreaWidthElement(style) 144 { 145 var width = style.propertyForName("width").value.replace(/px$/, ""); 146 if (style.propertyForName("box-sizing").value === "border-box") { 147 var borderBox = self._getBox(style, "border"); 148 var paddingBox = self._getBox(style, "padding"); 149 150 width = width - borderBox.left - borderBox.right - paddingBox.left - paddingBox.right; 151 } 152 153 return createElement.call(this, "span", width, "width", "width", style); 154 } 155 156 function createContentAreaHeightElement(style) 157 { 158 var height = style.propertyForName("height").value.replace(/px$/, ""); 159 if (style.propertyForName("box-sizing").value === "border-box") { 160 var borderBox = self._getBox(style, "border"); 161 var paddingBox = self._getBox(style, "padding"); 162 163 height = height - borderBox.top - borderBox.bottom - paddingBox.top - paddingBox.bottom; 164 } 165 166 return createElement.call(this, "span", height, "height", "height", style); 167 } 168 169 // Display types for which margin is ignored. 170 var noMarginDisplayType = { 171 "table-cell": true, 172 "table-column": true, 173 "table-column-group": true, 174 "table-footer-group": true, 175 "table-header-group": true, 176 "table-row": true, 177 "table-row-group": true 178 }; 179 180 // Display types for which padding is ignored. 181 var noPaddingDisplayType = { 182 "table-column": true, 183 "table-column-group": true, 184 "table-footer-group": true, 185 "table-header-group": true, 186 "table-row": true, 187 "table-row-group": true 188 }; 189 190 // Position types for which top, left, bottom and right are ignored. 191 var noPositionType = { 192 "static": true 193 }; 194 195 this._boxElements = []; 196 var boxes = ["content", "padding", "border", "margin", "position"]; 197 198 if (!style.properties.length) { 199 this.showEmptyMessage(); 200 return; 201 } 202 203 var previousBox = null; 204 for (var i = 0; i < boxes.length; ++i) { 205 var name = boxes[i]; 206 207 if (name === "margin" && noMarginDisplayType[style.propertyForName("display").value]) 208 continue; 209 if (name === "padding" && noPaddingDisplayType[style.propertyForName("display").value]) 210 continue; 211 if (name === "position" && noPositionType[style.propertyForName("position").value]) 212 continue; 213 214 var boxElement = document.createElement("div"); 215 boxElement.className = name; 216 boxElement._name = name; 217 boxElement.addEventListener("mouseover", this._highlightDOMNode.bind(this, true, name === "position" ? "all" : name), false); 218 this._boxElements.push(boxElement); 219 220 if (name === "content") { 221 var widthElement = createContentAreaWidthElement.call(this, style); 222 var heightElement = createContentAreaHeightElement.call(this, style); 223 224 boxElement.appendChild(widthElement); 225 boxElement.appendChild(document.createTextNode(" \u00D7 ")); 226 boxElement.appendChild(heightElement); 227 } else { 228 var suffix = (name === "border" ? "-width" : ""); 229 230 var labelElement = document.createElement("div"); 231 labelElement.className = "label"; 232 labelElement.textContent = boxes[i]; 233 boxElement.appendChild(labelElement); 234 235 boxElement.appendChild(createBoxPartElement.call(this, style, name, "top", suffix)); 236 boxElement.appendChild(document.createElement("br")); 237 boxElement.appendChild(createBoxPartElement.call(this, style, name, "left", suffix)); 238 239 if (previousBox) 240 boxElement.appendChild(previousBox); 241 242 boxElement.appendChild(createBoxPartElement.call(this, style, name, "right", suffix)); 243 boxElement.appendChild(document.createElement("br")); 244 boxElement.appendChild(createBoxPartElement.call(this, style, name, "bottom", suffix)); 245 } 246 247 previousBox = boxElement; 248 } 249 250 metricsElement.appendChild(previousBox); 251 metricsElement.addEventListener("mouseover", this._highlightDOMNode.bind(this, false, ""), false); 252 253 this.hideEmptyMessage(); 254 this.element.appendChild(metricsElement); 255 }, 256 257 _startEditing: function(targetElement, box, styleProperty, computedStyle) 258 { 259 if (WebInspector.isBeingEdited(targetElement)) 260 return; 261 262 // If the target element has a title use it as the editing value 263 // since the current text is likely truncated/rounded. 264 if (targetElement.title) 265 targetElement.textContent = targetElement.title; 266 267 var context = {box: box, styleProperty: styleProperty}; 268 var boundKeyDown = this._handleKeyDown.bind(this, context, styleProperty); 269 context.keyDownHandler = boundKeyDown; 270 targetElement.addEventListener("keydown", boundKeyDown, false); 271 272 this._isEditingMetrics = true; 273 274 var config = new WebInspector.EditingConfig(this._editingCommitted.bind(this), this._editingCancelled.bind(this), context); 275 WebInspector.startEditing(targetElement, config); 276 277 window.getSelection().setBaseAndExtent(targetElement, 0, targetElement, 1); 278 }, 279 280 _alteredFloatNumber: function(number, event) 281 { 282 var arrowKeyPressed = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down"); 283 284 // Jump by 10 when shift is down or jump by 0.1 when Alt/Option is down. 285 // Also jump by 10 for page up and down, or by 100 if shift is held with a page key. 286 var changeAmount = 1; 287 if (event.shiftKey && !arrowKeyPressed) 288 changeAmount = 100; 289 else if (event.shiftKey || !arrowKeyPressed) 290 changeAmount = 10; 291 else if (event.altKey) 292 changeAmount = 0.1; 293 294 if (event.keyIdentifier === "Down" || event.keyIdentifier === "PageDown") 295 changeAmount *= -1; 296 297 // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns. 298 // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1. 299 var result = Number((number + changeAmount).toFixed(6)); 300 if (!String(result).match(WebInspector.BoxModelDetailsSectionRow.CSSNumberRegex)) 301 return null; 302 303 return result; 304 }, 305 306 _handleKeyDown: function(context, styleProperty, event) 307 { 308 if (!/^(?:Page)?(?:Up|Down)$/.test(event.keyIdentifier)) 309 return; 310 311 var element = event.currentTarget; 312 313 var selection = window.getSelection(); 314 if (!selection.rangeCount) 315 return; 316 317 var selectionRange = selection.getRangeAt(0); 318 if (!selectionRange.commonAncestorContainer.isSelfOrDescendant(element)) 319 return; 320 321 var originalValue = element.textContent; 322 var wordRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, WebInspector.BoxModelDetailsSectionRow.StyleValueDelimiters, element); 323 var wordString = wordRange.toString(); 324 325 var matches = /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/.exec(wordString); 326 var replacementString; 327 if (matches && matches.length) { 328 var prefix = matches[1]; 329 var suffix = matches[3]; 330 var number = this._alteredFloatNumber(parseFloat(matches[2]), event); 331 if (number === null) { 332 // Need to check for null explicitly. 333 return; 334 } 335 336 if (styleProperty !== "margin" && number < 0) 337 number = 0; 338 339 replacementString = prefix + number + suffix; 340 } 341 342 if (!replacementString) 343 return; 344 345 var replacementTextNode = document.createTextNode(replacementString); 346 347 wordRange.deleteContents(); 348 wordRange.insertNode(replacementTextNode); 349 350 var finalSelectionRange = document.createRange(); 351 finalSelectionRange.setStart(replacementTextNode, 0); 352 finalSelectionRange.setEnd(replacementTextNode, replacementString.length); 353 354 selection.removeAllRanges(); 355 selection.addRange(finalSelectionRange); 356 357 event.handled = true; 358 event.preventDefault(); 359 360 this._ignoreNextRefresh = true; 361 362 this._applyUserInput(element, replacementString, originalValue, context, false); 363 }, 364 365 _editingEnded: function(element, context) 366 { 367 delete this.originalPropertyData; 368 delete this.previousPropertyDataCandidate; 369 element.removeEventListener("keydown", context.keyDownHandler, false); 370 delete this._isEditingMetrics; 371 }, 372 373 _editingCancelled: function(element, context) 374 { 375 this._editingEnded(element, context); 376 this._refresh(); 377 }, 378 379 _applyUserInput: function(element, userInput, previousContent, context, commitEditor) 380 { 381 if (commitEditor && userInput === previousContent) 382 return this._editingCancelled(element, context); // nothing changed, so cancel 383 384 if (context.box !== "position" && (!userInput || userInput === "\u2012")) 385 userInput = "0px"; 386 else if (context.box === "position" && (!userInput || userInput === "\u2012")) 387 userInput = "auto"; 388 389 userInput = userInput.toLowerCase(); 390 // Append a "px" unit if the user input was just a number. 391 if (/^-?(?:\d+(?:\.\d+)?|\.\d+)$/.test(userInput)) 392 userInput += "px"; 393 394 var styleProperty = context.styleProperty; 395 var computedStyle = this._nodeStyles.computedStyle; 396 397 if (computedStyle.propertyForName("box-sizing").value === "border-box" && (styleProperty === "width" || styleProperty === "height")) { 398 if (!userInput.match(/px$/)) { 399 console.error("For elements with box-sizing: border-box, only absolute content area dimensions can be applied"); 400 return; 401 } 402 403 var borderBox = this._getBox(computedStyle, "border"); 404 var paddingBox = this._getBox(computedStyle, "padding"); 405 var userValuePx = Number(userInput.replace(/px$/, "")); 406 if (isNaN(userValuePx)) 407 return; 408 if (styleProperty === "width") 409 userValuePx += borderBox.left + borderBox.right + paddingBox.left + paddingBox.right; 410 else 411 userValuePx += borderBox.top + borderBox.bottom + paddingBox.top + paddingBox.bottom; 412 413 userInput = userValuePx + "px"; 414 } 415 416 var property = this._nodeStyles.inlineStyle.propertyForName(context.styleProperty); 417 property.value = userInput; 418 property.add(); 419 }, 420 421 _editingCommitted: function(element, userInput, previousContent, context) 422 { 423 this._editingEnded(element, context); 424 this._applyUserInput(element, userInput, previousContent, context, true); 425 } 426}; 427 428WebInspector.BoxModelDetailsSectionRow.prototype.__proto__ = WebInspector.DetailsSectionRow.prototype; 429