blob: 0b7eb871dc52f6bc0e209b2c485a1d60b39a574d [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371/*
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 */
32Elements.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 Lissiakd2f1a2f2019-03-26 17:36:51300
301 // Record the elements tool load time after the sidepane has loaded.
302 Host.userMetrics.panelLoaded('elements', 'DevTools.Launch.Elements');
Blink Reformat4c46d092018-04-07 15:32:37303 }
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};