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