UNPKG

chrome-devtools-frontend

Version:
674 lines (598 loc) • 26.3 kB
/* * Copyright (C) 2007 Apple Inc. All rights reserved. * Copyright (C) 2009 Joseph Pecoraro * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as Common from '../common/common.js'; import * as Components from '../components/components.js'; import * as i18n from '../i18n/i18n.js'; import * as InlineEditor from '../inline_editor/inline_editor.js'; import * as Platform from '../platform/platform.js'; import * as SDK from '../sdk/sdk.js'; import * as UI from '../ui/ui.js'; import {ComputedStyle, ComputedStyleModel, Events} from './ComputedStyleModel.js'; // eslint-disable-line no-unused-vars import {ComputedStyleProperty} from './ComputedStyleProperty.js'; import {ComputedStyleTrace} from './ComputedStyleTrace.js'; import {ImagePreviewPopover} from './ImagePreviewPopover.js'; import {PlatformFontsWidget} from './PlatformFontsWidget.js'; import {categorizePropertyName, Category, DefaultCategoryOrder} from './PropertyNameCategories.js'; // eslint-disable-line no-unused-vars import {IdleCallbackManager, StylePropertiesSection, StylesSidebarPane, StylesSidebarPropertyRenderer} from './StylesSidebarPane.js'; export const UIStrings = { /** *@description Text to filter result items */ filter: 'Filter', /** *@description ARIA accessible name in Computed Style Widget of the Elements panel */ filterComputedStyles: 'Filter Computed Styles', /** *@description Text in Computed Style Widget of the Elements panel */ showAll: 'Show all', /** *@description Group toggle text in Computed Style Widget of the Elements panel */ group: 'Group', /** *@description No matches element text content in Computed Style Widget of the Elements panel */ noMatchingProperty: 'No matching property', /** *@description Context menu item in Elements panel to navigate to css selector source */ navigateToSelectorSource: 'Navigate to selector source', /** *@description Context menu item in Elements panel to navigate to style in styles pane */ navigateToStyle: 'Navigate to style', }; const str_ = i18n.i18n.registerUIStrings('elements/ComputedStyleWidget.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); /** * @param {!SDK.DOMModel.DOMNode} node * @param {string} propertyName * @param {string} propertyValue */ const createPropertyElement = (node, propertyName, propertyValue) => { const propertyElement = new ComputedStyleProperty(); const renderer = new StylesSidebarPropertyRenderer(null, node, propertyName, propertyValue); renderer.setColorHandler(processComputedColor); const propertyNameElement = renderer.renderName(); propertyNameElement.slot = 'property-name'; propertyElement.appendChild(propertyNameElement); const propertyValueElement = renderer.renderValue(); propertyValueElement.slot = 'property-value'; propertyElement.appendChild(propertyValueElement); return propertyElement; }; /** * @param {!SDK.DOMModel.DOMNode} node * @param {!SDK.CSSProperty.CSSProperty} property * @param {boolean} isPropertyOverloaded * @param {!SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @param {!Components.Linkifier.Linkifier} linkifier */ const createTraceElement = (node, property, isPropertyOverloaded, matchedStyles, linkifier) => { const trace = new ComputedStyleTrace(); const renderer = new StylesSidebarPropertyRenderer(null, node, property.name, /** @type {string} */ (property.value)); renderer.setColorHandler(processColor); const valueElement = renderer.renderValue(); valueElement.slot = 'trace-value'; trace.appendChild(valueElement); const rule = /** @type {?SDK.CSSRule.CSSStyleRule} */ (property.ownerStyle.parentRule); if (rule) { const linkSpan = document.createElement('span'); linkSpan.appendChild(StylePropertiesSection.createRuleOriginNode(matchedStyles, linkifier, rule)); linkSpan.slot = 'trace-link'; trace.appendChild(linkSpan); } trace.data = { selector: rule ? rule.selectorText() : 'element.style', active: !isPropertyOverloaded, onNavigateToSource: /** @type {function(!Event=):void} */ (navigateToSource.bind(null, property)), }; return trace; }; /** * @param {string} text * @return {!Node} */ const processColor = text => { const swatch = new InlineEditor.ColorSwatch.ColorSwatch(); swatch.renderColor(text, true); swatch.createChild('span').textContent = text; return swatch; }; /** * @param {string} text * @return {!Node} */ const processComputedColor = text => { const swatch = new InlineEditor.ColorSwatch.ColorSwatch(); // Computed styles don't provide the original format, so switch to RGB. swatch.renderColor(text, Common.Color.Format.RGB); swatch.createChild('span').textContent = text; return swatch; }; /** * @param {!SDK.CSSProperty.CSSProperty} cssProperty * @param {!Event} event */ const navigateToSource = (cssProperty, event) => { Common.Revealer.reveal(cssProperty); event.consume(true); }; /** * @param {string} propA * @param {string} propB * @return {number} */ const propertySorter = (propA, propB) => { if (propA.startsWith('--') !== propB.startsWith('--')) { return propA.startsWith('--') ? 1 : -1; } if (propA.startsWith('-webkit') !== propB.startsWith('-webkit')) { return propA.startsWith('-webkit') ? 1 : -1; } const canonicalA = SDK.CSSMetadata.cssMetadata().canonicalPropertyName(propA); const canonicalB = SDK.CSSMetadata.cssMetadata().canonicalPropertyName(propB); return Platform.StringUtilities.compare(canonicalA, canonicalB); }; export class ComputedStyleWidget extends UI.ThrottledWidget.ThrottledWidget { constructor() { super(true); this.registerRequiredCSS('elements/computedStyleSidebarPane.css', {enableLegacyPatching: true}); this._computedStyleModel = new ComputedStyleModel(); this._computedStyleModel.addEventListener(Events.ComputedStyleChanged, this.update, this); this._showInheritedComputedStylePropertiesSetting = Common.Settings.Settings.instance().createSetting('showInheritedComputedStyleProperties', false); this._showInheritedComputedStylePropertiesSetting.addChangeListener(this.update.bind(this)); this._groupComputedStylesSetting = Common.Settings.Settings.instance().createSetting('groupComputedStyles', false); this._groupComputedStylesSetting.addChangeListener(() => { this.update(); }); const hbox = this.contentElement.createChild('div', 'hbox styles-sidebar-pane-toolbar'); const filterContainerElement = hbox.createChild('div', 'styles-sidebar-pane-filter-box'); const filterInput = StylesSidebarPane.createPropertyFilterElement(i18nString(UIStrings.filter), hbox, filterCallback.bind(this)); UI.ARIAUtils.setAccessibleName(filterInput, i18nString(UIStrings.filterComputedStyles)); filterContainerElement.appendChild(filterInput); /** @type {?RegExp} */ this._filterRegex = null; const toolbar = new UI.Toolbar.Toolbar('styles-pane-toolbar', hbox); toolbar.appendToolbarItem(new UI.Toolbar.ToolbarSettingCheckbox( this._showInheritedComputedStylePropertiesSetting, undefined, i18nString(UIStrings.showAll))); toolbar.appendToolbarItem(new UI.Toolbar.ToolbarSettingCheckbox( this._groupComputedStylesSetting, undefined, i18nString(UIStrings.group))); this._noMatchesElement = this.contentElement.createChild('div', 'gray-info-message'); this._noMatchesElement.textContent = i18nString(UIStrings.noMatchingProperty); this._propertiesOutline = new UI.TreeOutline.TreeOutlineInShadow(); this._propertiesOutline.hideOverflow(); this._propertiesOutline.setShowSelectionOnKeyboardFocus(true); this._propertiesOutline.setFocusable(true); this._propertiesOutline.registerRequiredCSS('elements/computedStyleWidgetTree.css', {enableLegacyPatching: true}); this._propertiesOutline.element.classList.add('monospace', 'computed-properties'); this._propertiesOutline.addEventListener(UI.TreeOutline.Events.ElementExpanded, this._onTreeElementToggled, this); this._propertiesOutline.addEventListener(UI.TreeOutline.Events.ElementCollapsed, this._onTreeElementToggled, this); this.contentElement.appendChild(this._propertiesOutline.element); /** @type {!WeakMap<!UI.TreeOutline.TreeElement, {name: string, value: string}>} */ this._propertyByTreeElement = new WeakMap(); /** @type {!WeakMap<!UI.TreeOutline.TreeElement, !Category>} */ this._categoryByTreeElement = new WeakMap(); /** @type {!Set<string>} */ this._expandedProperties = new Set(); /** @type {!Set<!Category>} */ this._expandedGroups = new Set(DefaultCategoryOrder); this._linkifier = new Components.Linkifier.Linkifier(_maxLinkLength); this._imagePreviewPopover = new ImagePreviewPopover(this.contentElement, event => { const link = event.composedPath()[0]; if (link instanceof Element) { return link; } return null; }, () => this._computedStyleModel.node()); /** * @param {?RegExp} regex * @this {ComputedStyleWidget} */ function filterCallback(regex) { this._filterRegex = regex; if (this._groupComputedStylesSetting.get()) { this._filterGroupLists(); } else { this._filterAlphabeticalList(); } } const fontsWidget = new PlatformFontsWidget(this._computedStyleModel); fontsWidget.show(this.contentElement); /** @type {!IdleCallbackManager} */ this._idleCallbackManager = new IdleCallbackManager(); } /** * @override */ onResize() { const isNarrow = this.contentElement.offsetWidth < 260; this._propertiesOutline.contentElement.classList.toggle('computed-narrow', isNarrow); } _showInheritedComputedStyleChanged() { this.update(); } /** * @override */ update() { if (this._idleCallbackManager) { this._idleCallbackManager.discard(); } this._idleCallbackManager = new IdleCallbackManager(); super.update(); } /** * @override * @return {!Promise.<?>} */ async doUpdate() { const [nodeStyles, matchedStyles] = await Promise.all([this._computedStyleModel.fetchComputedStyle(), this._fetchMatchedCascade()]); const shouldGroupComputedStyles = this._groupComputedStylesSetting.get(); this._propertiesOutline.contentElement.classList.toggle('grouped-list', shouldGroupComputedStyles); this._propertiesOutline.contentElement.classList.toggle('alphabetical-list', !shouldGroupComputedStyles); if (shouldGroupComputedStyles) { await this._rebuildGroupedList(nodeStyles, matchedStyles); } else { await this._rebuildAlphabeticalList(nodeStyles, matchedStyles); } } /** * @return {!Promise.<?SDK.CSSMatchedStyles.CSSMatchedStyles>} */ async _fetchMatchedCascade() { const node = this._computedStyleModel.node(); if (!node || !this._computedStyleModel.cssModel()) { return /** @type {?SDK.CSSMatchedStyles.CSSMatchedStyles} */ (null); } const cssModel = this._computedStyleModel.cssModel(); if (!cssModel) { return null; } return cssModel.cachedMatchedCascadeForNode(node).then(validateStyles.bind(this)); /** * @param {?SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @return {?SDK.CSSMatchedStyles.CSSMatchedStyles} * @this {ComputedStyleWidget} */ function validateStyles(matchedStyles) { return matchedStyles && matchedStyles.node() === this._computedStyleModel.node() ? matchedStyles : null; } } /** * @param {?ComputedStyle} nodeStyle * @param {?SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles */ async _rebuildAlphabeticalList(nodeStyle, matchedStyles) { const hadFocus = this._propertiesOutline.element.hasFocus(); this._imagePreviewPopover.hide(); this._propertiesOutline.removeChildren(); this._linkifier.reset(); const cssModel = this._computedStyleModel.cssModel(); if (!nodeStyle || !matchedStyles || !cssModel) { this._noMatchesElement.classList.remove('hidden'); return; } const uniqueProperties = [...nodeStyle.computedStyle.keys()]; uniqueProperties.sort(propertySorter); const node = nodeStyle.node; const propertyTraces = this._computePropertyTraces(matchedStyles); const nonInheritedProperties = this._computeNonInheritedProperties(matchedStyles); const showInherited = this._showInheritedComputedStylePropertiesSetting.get(); const computedStyleQueue = []; // filter and preprocess properties to line up in the computed style queue for (const propertyName of uniqueProperties) { const propertyValue = nodeStyle.computedStyle.get(propertyName) || ''; const canonicalName = SDK.CSSMetadata.cssMetadata().canonicalPropertyName(propertyName); const isInherited = !nonInheritedProperties.has(canonicalName); if (!showInherited && isInherited && !_alwaysShownComputedProperties.has(propertyName)) { continue; } if (!showInherited && propertyName.startsWith('--')) { continue; } if (propertyName !== canonicalName && propertyValue === nodeStyle.computedStyle.get(canonicalName)) { continue; } computedStyleQueue.push({propertyName, propertyValue, isInherited}); } this._propertiesOutline.contentElement.classList.add('render-flash'); // Render computed style properties in batches via idle callbacks to avoid a // very long task. The batchSize and timeoutInterval should be tweaked in // pair. Currently, updating, laying-out, rendering, and painting 20 items // in every 100ms seems to be a good balance between updating too lazy vs. // updating too much in one cycle. const batchSize = 20; const timeoutInterval = 100; let timeout = 100; while (computedStyleQueue.length > 0) { const currentBatch = computedStyleQueue.splice(0, batchSize); this._idleCallbackManager.schedule(() => { for (const {propertyName, propertyValue, isInherited} of currentBatch) { const treeElement = this._buildPropertyTreeElement( propertyTraces, node, /** @type {!SDK.CSSMatchedStyles.CSSMatchedStyles} */ (matchedStyles), propertyName, propertyValue, isInherited, hadFocus); this._propertiesOutline.appendChild(treeElement); } this._filterAlphabeticalList(); }, timeout); timeout += timeoutInterval; } await this._idleCallbackManager.awaitDone(); this._propertiesOutline.contentElement.classList.remove('render-flash'); } /** * @param {?ComputedStyle} nodeStyle * @param {?SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles */ async _rebuildGroupedList(nodeStyle, matchedStyles) { const hadFocus = this._propertiesOutline.element.hasFocus(); this._imagePreviewPopover.hide(); this._propertiesOutline.removeChildren(); this._linkifier.reset(); const cssModel = this._computedStyleModel.cssModel(); if (!nodeStyle || !matchedStyles || !cssModel) { this._noMatchesElement.classList.remove('hidden'); return; } const node = nodeStyle.node; const propertyTraces = this._computePropertyTraces(matchedStyles); const nonInheritedProperties = this._computeNonInheritedProperties(matchedStyles); const showInherited = this._showInheritedComputedStylePropertiesSetting.get(); const propertiesByCategory = new Map(); for (const [propertyName, propertyValue] of nodeStyle.computedStyle) { const canonicalName = SDK.CSSMetadata.cssMetadata().canonicalPropertyName(propertyName); const isInherited = !nonInheritedProperties.has(canonicalName); if (!showInherited && isInherited && !_alwaysShownComputedProperties.has(propertyName)) { continue; } if (!showInherited && propertyName.startsWith('--')) { continue; } if (propertyName !== canonicalName && propertyValue === nodeStyle.computedStyle.get(canonicalName)) { continue; } const categories = categorizePropertyName(propertyName); for (const category of categories) { const treeElement = this._buildPropertyTreeElement( propertyTraces, node, matchedStyles, propertyName, propertyValue, isInherited, hadFocus); if (!propertiesByCategory.has(category)) { propertiesByCategory.set(category, []); } propertiesByCategory.get(category).push(treeElement); } } for (const category of DefaultCategoryOrder) { const properties = propertiesByCategory.get(category); if (properties && properties.length > 0) { const title = document.createElement('h1'); title.textContent = category; const group = new UI.TreeOutline.TreeElement(title); group.listItemElement.classList.add('group-title'); group.toggleOnClick = true; for (const property of properties) { group.appendChild(property); } this._propertiesOutline.appendChild(group); if (this._expandedGroups.has(category)) { group.expand(); } this._categoryByTreeElement.set(group, category); } } this._filterGroupLists(); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _onTreeElementToggled(event) { const treeElement = /** @type {!UI.TreeOutline.TreeElement} */ (event.data); const property = this._propertyByTreeElement.get(treeElement); if (property) { treeElement.expanded ? this._expandedProperties.add(property.name) : this._expandedProperties.delete(property.name); } else { const category = this._categoryByTreeElement.get(treeElement); if (category) { treeElement.expanded ? this._expandedGroups.add(category) : this._expandedGroups.delete(category); } } } /** * * @param {!Map<string, !Array<!SDK.CSSProperty.CSSProperty>>} propertyTraces * @param {!SDK.DOMModel.DOMNode} node * @param {!SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @param {string} propertyName * @param {string} propertyValue * @param {boolean} isInherited * @param {boolean} hadFocus * @return {!UI.TreeOutline.TreeElement} */ _buildPropertyTreeElement(propertyTraces, node, matchedStyles, propertyName, propertyValue, isInherited, hadFocus) { const treeElement = new UI.TreeOutline.TreeElement(); const trace = propertyTraces.get(propertyName); /** @type {function(!Event=):void} */ let navigate = () => {}; if (trace) { const activeProperty = this._renderPropertyTrace( /** @type {!SDK.CSSMatchedStyles.CSSMatchedStyles} */ (matchedStyles), node, treeElement, trace); treeElement.setExpandable(true); treeElement.listItemElement.addEventListener('click', event => { treeElement.expanded ? treeElement.collapse() : treeElement.expand(); event.consume(); }, false); navigate = /** @type {function(!Event=):void} */ (navigateToSource.bind(this, activeProperty)); } const propertyElement = createPropertyElement(node, propertyName, propertyValue); propertyElement.data = { traceable: propertyTraces.has(propertyName), inherited: isInherited, onNavigateToSource: navigate, }; treeElement.title = propertyElement; this._propertyByTreeElement.set(treeElement, {name: propertyName, value: propertyValue}); if (!this._propertiesOutline.selectedTreeElement) { treeElement.select(!hadFocus); } if (this._expandedProperties.has(propertyName)) { treeElement.expand(); } return treeElement; } /** * @param {!SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @param {!SDK.DOMModel.DOMNode} node * @param {!UI.TreeOutline.TreeElement} rootTreeElement * @param {!Array<!SDK.CSSProperty.CSSProperty>} tracedProperties * @return {!SDK.CSSProperty.CSSProperty} */ _renderPropertyTrace(matchedStyles, node, rootTreeElement, tracedProperties) { let activeProperty = null; for (const property of tracedProperties) { const isPropertyOverloaded = matchedStyles.propertyState(property) === SDK.CSSMatchedStyles.PropertyState.Overloaded; if (!isPropertyOverloaded) { activeProperty = property; rootTreeElement.listItemElement.addEventListener( 'contextmenu', this._handleContextMenuEvent.bind(this, matchedStyles, property)); } const trace = createTraceElement(node, property, isPropertyOverloaded, matchedStyles, this._linkifier); const traceTreeElement = new UI.TreeOutline.TreeElement(); traceTreeElement.title = trace; traceTreeElement.listItemElement.addEventListener( 'contextmenu', this._handleContextMenuEvent.bind(this, matchedStyles, property)); rootTreeElement.appendChild(traceTreeElement); } return /** @type {!SDK.CSSProperty.CSSProperty} */ (activeProperty); } /** * @param {!SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @param {!SDK.CSSProperty.CSSProperty} property * @param {!Event} event */ _handleContextMenuEvent(matchedStyles, property, event) { const contextMenu = new UI.ContextMenu.ContextMenu(event); const rule = property.ownerStyle.parentRule; if (rule) { const header = rule.styleSheetId ? matchedStyles.cssModel().styleSheetHeaderForId(rule.styleSheetId) : null; if (header && !header.isAnonymousInlineStyleSheet()) { contextMenu.defaultSection().appendItem(i18nString(UIStrings.navigateToSelectorSource), () => { StylePropertiesSection.tryNavigateToRuleLocation(matchedStyles, rule); }); } } contextMenu.defaultSection().appendItem( i18nString(UIStrings.navigateToStyle), () => Common.Revealer.reveal(property)); contextMenu.show(); } /** * @param {!SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @return {!Map<string, !Array<!SDK.CSSProperty.CSSProperty>>} */ _computePropertyTraces(matchedStyles) { const result = new Map(); for (const style of matchedStyles.nodeStyles()) { const allProperties = style.allProperties(); for (const property of allProperties) { if (!property.activeInStyle() || !matchedStyles.propertyState(property)) { continue; } if (!result.has(property.name)) { result.set(property.name, []); } result.get(property.name).push(property); } } return result; } /** * @param {!SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @return {!Set<string>} */ _computeNonInheritedProperties(matchedStyles) { const result = new Set(); for (const style of matchedStyles.nodeStyles()) { for (const property of style.allProperties()) { if (!matchedStyles.propertyState(property)) { continue; } result.add(SDK.CSSMetadata.cssMetadata().canonicalPropertyName(property.name)); } } return result; } _filterAlphabeticalList() { const regex = this._filterRegex; const children = this._propertiesOutline.rootElement().children(); let hasMatch = false; for (const child of children) { const property = this._propertyByTreeElement.get(child); if (!property) { continue; } const matched = !regex || regex.test(property.name) || regex.test(property.value); child.hidden = !matched; hasMatch = hasMatch || matched; } this._noMatchesElement.classList.toggle('hidden', Boolean(hasMatch)); } _filterGroupLists() { const regex = this._filterRegex; const groups = this._propertiesOutline.rootElement().children(); let hasOverallMatch = false; let foundFirstGroup = false; for (const group of groups) { let hasGroupMatch = false; const properties = group.children(); for (const propertyTreeElement of properties) { const property = this._propertyByTreeElement.get(propertyTreeElement); if (!property) { continue; } const matched = !regex || regex.test(property.name) || regex.test(property.value); propertyTreeElement.hidden = !matched; hasOverallMatch = hasOverallMatch || matched; hasGroupMatch = hasGroupMatch || matched; } group.hidden = !hasGroupMatch; // the first visible group won't have a divider before the group title group.listItemElement.classList.toggle('first-group', hasGroupMatch && !foundFirstGroup); foundFirstGroup = foundFirstGroup || hasGroupMatch; } this._noMatchesElement.classList.toggle('hidden', hasOverallMatch); } } const _maxLinkLength = 30; const _alwaysShownComputedProperties = new Set(['display', 'height', 'width']);