UNPKG

chrome-devtools-frontend

Version:
1,510 lines (1,340 loc) 109 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 Bindings from '../bindings/bindings.js'; import * as Common from '../common/common.js'; import * as Components from '../components/components.js'; import * as Host from '../host/host.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 Root from '../root/root.js'; import * as SDK from '../sdk/sdk.js'; import * as TextUtils from '../text_utils/text_utils.js'; import * as WebComponents from '../ui/components/components.js'; import * as UI from '../ui/ui.js'; import {FontEditorSectionManager} from './ColorSwatchPopoverIcon.js'; import {ComputedStyleModel} from './ComputedStyleModel.js'; import {findIcon} from './CSSPropertyIconResolver.js'; import {linkifyDeferredNodeReference} from './DOMLinkifier.js'; import {ElementsSidebarPane} from './ElementsSidebarPane.js'; import {FlexboxEditorWidget} from './FlexboxEditorWidget.js'; import {ImagePreviewPopover} from './ImagePreviewPopover.js'; import {StylePropertyHighlighter} from './StylePropertyHighlighter.js'; import {Context, StylePropertyTreeElement} from './StylePropertyTreeElement.js'; // eslint-disable-line no-unused-vars export const UIStrings = { /** *@description No matches element text content in Styles Sidebar Pane of the Elements panel */ noMatchingSelectorOrStyle: 'No matching selector or style', /** *@description Text in Styles Sidebar Pane of the Elements panel */ invalidPropertyValue: 'Invalid property value', /** *@description Text in Styles Sidebar Pane of the Elements panel */ unknownPropertyName: 'Unknown property name', /** *@description Text to filter result items */ filter: 'Filter', /** *@description ARIA accessible name in Styles Sidebar Pane of the Elements panel */ filterStyles: 'Filter Styles', /** *@description Separator element text content in Styles Sidebar Pane of the Elements panel *@example {scrollbar-corner} PH1 */ pseudoSElement: 'Pseudo ::{PH1} element', /** *@description Text of a DOM element in Styles Sidebar Pane of the Elements panel */ inheritedFroms: 'Inherited from ', /** *@description Tooltip text that appears when hovering over the largeicon add button in the Styles Sidebar Pane of the Elements panel */ insertStyleRuleBelow: 'Insert Style Rule Below', /** *@description Text in Styles Sidebar Pane of the Elements panel */ constructedStylesheet: 'constructed stylesheet', /** *@description Text in Styles Sidebar Pane of the Elements panel */ userAgentStylesheet: 'user agent stylesheet', /** *@description Text in Styles Sidebar Pane of the Elements panel */ injectedStylesheet: 'injected stylesheet', /** *@description Text in Styles Sidebar Pane of the Elements panel */ viaInspector: 'via inspector', /** *@description Text in Styles Sidebar Pane of the Elements panel */ styleAttribute: 'Style Attribute', /** *@description Text in Styles Sidebar Pane of the Elements panel *@example {html} PH1 */ sattributesStyle: '{PH1}[Attributes Style]', /** *@description Show all button text content in Styles Sidebar Pane of the Elements panel *@example {3} PH1 */ showAllPropertiesSMore: 'Show All Properties ({PH1} more)', /** *@description Text in Elements Tree Element of the Elements panel, copy should be used as a verb */ copySelector: 'Copy `selector`', /** *@description A context menu item in Styles panel to copy CSS rule */ copyRule: 'Copy rule', /** *@description A context menu item in Styles panel to copy all CSS declarations */ copyAllDeclarations: 'Copy all declarations', /** *@description Title of in styles sidebar pane of the elements panel *@example {Ctrl} PH1 */ incrementdecrementWithMousewheelOne: 'Increment/decrement with mousewheel or up/down keys. {PH1}: R ±1, Shift: G ±1, Alt: B ±1', /** *@description Title of in styles sidebar pane of the elements panel *@example {Ctrl} PH1 */ incrementdecrementWithMousewheelHundred: 'Increment/decrement with mousewheel or up/down keys. {PH1}: ±100, Shift: ±10, Alt: ±0.1', /** *@description Tooltip text that appears when hovering over the largeicon add button in the Styles Sidebar Pane of the Elements panel */ newStyleRule: 'New Style Rule', }; const str_ = i18n.i18n.registerUIStrings('elements/StylesSidebarPane.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); // Highlightable properties are those that can be hovered in the sidebar to trigger a specific // highlighting mode on the current element. const HIGHLIGHTABLE_PROPERTIES = [ {mode: 'padding', properties: ['padding']}, {mode: 'border', properties: ['border']}, {mode: 'margin', properties: ['margin']}, {mode: 'gap', properties: ['gap', 'grid-gap']}, {mode: 'column-gap', properties: ['column-gap', 'grid-column-gap']}, {mode: 'row-gap', properties: ['row-gap', 'grid-row-gap']}, {mode: 'grid-template-columns', properties: ['grid-template-columns']}, {mode: 'grid-template-rows', properties: ['grid-template-rows']}, {mode: 'grid-template-areas', properties: ['grid-areas']}, {mode: 'justify-content', properties: ['justify-content']}, {mode: 'align-content', properties: ['align-content']}, {mode: 'align-items', properties: ['align-items']}, ]; /** @type {!StylesSidebarPane} */ let _stylesSidebarPaneInstance; export class StylesSidebarPane extends ElementsSidebarPane { /** * @return {!StylesSidebarPane} */ static instance() { if (!_stylesSidebarPaneInstance) { _stylesSidebarPaneInstance = new StylesSidebarPane(); } return _stylesSidebarPaneInstance; } /** * @private */ constructor() { super(true /* delegatesFocus */); this.setMinimumSize(96, 26); this.registerRequiredCSS('elements/stylesSidebarPane.css', {enableLegacyPatching: true}); Common.Settings.Settings.instance().moduleSetting('colorFormat').addChangeListener(this.update.bind(this)); Common.Settings.Settings.instance().moduleSetting('textEditorIndent').addChangeListener(this.update.bind(this)); /** @type {?UI.Widget.Widget} */ this._currentToolbarPane = null; /** @type {?UI.Widget.Widget} */ this._animatedToolbarPane = null; /** @type {?UI.Widget.Widget} */ this._pendingWidget = null; /** @type {?UI.Toolbar.ToolbarToggle} */ this._pendingWidgetToggle = null; /** @type {?UI.Toolbar.Toolbar} */ this._toolbar = null; this._toolbarPaneElement = this._createStylesSidebarToolbar(); this._computedStyleModel = new ComputedStyleModel(); this._noMatchesElement = this.contentElement.createChild('div', 'gray-info-message hidden'); this._noMatchesElement.textContent = i18nString(UIStrings.noMatchingSelectorOrStyle); this._sectionsContainer = this.contentElement.createChild('div'); UI.ARIAUtils.markAsTree(this._sectionsContainer); this._sectionsContainer.addEventListener('keydown', this._sectionsContainerKeyDown.bind(this), false); this._sectionsContainer.addEventListener('focusin', this._sectionsContainerFocusChanged.bind(this), false); this._sectionsContainer.addEventListener('focusout', this._sectionsContainerFocusChanged.bind(this), false); /** @type {!WeakMap<!Node, !StylePropertiesSection>} */ this.sectionByElement = new WeakMap(); this._swatchPopoverHelper = new InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper(); this._swatchPopoverHelper.addEventListener( InlineEditor.SwatchPopoverHelper.Events.WillShowPopover, this.hideAllPopovers, this); this._linkifier = new Components.Linkifier.Linkifier(_maxLinkLength, /* useLinkDecorator */ true); /** @type {!StylePropertyHighlighter} */ this._decorator = new StylePropertyHighlighter(this); /** @type {?SDK.CSSProperty.CSSProperty} */ this._lastRevealedProperty = null; this._userOperation = false; this._isEditingStyle = false; /** @type {?RegExp} */ this._filterRegex = null; this._isActivePropertyHighlighted = false; this._initialUpdateCompleted = false; this.hasMatchedStyles = false; this.contentElement.classList.add('styles-pane'); /** @type {!Array<!SectionBlock>} */ this._sectionBlocks = []; /** @type {?IdleCallbackManager} */ this._idleCallbackManager = null; this._needsForceUpdate = false; _stylesSidebarPaneInstance = this; UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.forceUpdate, this); this.contentElement.addEventListener('copy', this._clipboardCopy.bind(this)); this._resizeThrottler = new Common.Throttler.Throttler(100); this._imagePreviewPopover = new ImagePreviewPopover(this.contentElement, event => { const link = event.composedPath()[0]; if (link instanceof Element) { return link; } return null; }, () => this.node()); /** @type {?InlineEditor.CSSAngle.CSSAngle} */ this.activeCSSAngle = null; } /** * @return {!InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper} */ swatchPopoverHelper() { return this._swatchPopoverHelper; } /** * @param {boolean} userOperation */ setUserOperation(userOperation) { this._userOperation = userOperation; } /** * @param {!SDK.CSSProperty.CSSProperty} property * @param {?string} title * @return {!Element} */ static createExclamationMark(property, title) { const exclamationElement = /** @type {!UI.UIUtils.DevToolsIconLabel} */ (document.createElement('span', {is: 'dt-icon-label'})); exclamationElement.className = 'exclamation-mark'; if (!StylesSidebarPane.ignoreErrorsForProperty(property)) { exclamationElement.type = 'smallicon-warning'; } if (title) { UI.Tooltip.Tooltip.install(exclamationElement, title); } else { UI.Tooltip.Tooltip.install( exclamationElement, SDK.CSSMetadata.cssMetadata().isCSSPropertyName(property.name) ? i18nString(UIStrings.invalidPropertyValue) : i18nString(UIStrings.unknownPropertyName)); } return exclamationElement; } /** * @param {!SDK.CSSProperty.CSSProperty} property * @return {boolean} */ static ignoreErrorsForProperty(property) { /** * @param {string} string */ function hasUnknownVendorPrefix(string) { return !string.startsWith('-webkit-') && /^[-_][\w\d]+-\w/.test(string); } const name = property.name.toLowerCase(); // IE hack. if (name.charAt(0) === '_') { return true; } // IE has a different format for this. if (name === 'filter') { return true; } // Common IE-specific property prefix. if (name.startsWith('scrollbar-')) { return true; } if (hasUnknownVendorPrefix(name)) { return true; } const value = property.value.toLowerCase(); // IE hack. if (value.endsWith('\\9')) { return true; } if (hasUnknownVendorPrefix(value)) { return true; } return false; } /** * @param {string} placeholder * @param {!Element} container * @param {function(?RegExp):void} filterCallback * @return {!Element} */ static createPropertyFilterElement(placeholder, container, filterCallback) { const input = document.createElement('input'); input.type = 'search'; input.classList.add('custom-search-input'); input.placeholder = placeholder; function searchHandler() { const regex = input.value ? new RegExp(Platform.StringUtilities.escapeForRegExp(input.value), 'i') : null; filterCallback(regex); } input.addEventListener('input', searchHandler, false); /** * @param {!Event} event */ function keydownHandler(event) { const keyboardEvent = /** @type {!KeyboardEvent} */ (event); if (keyboardEvent.key !== 'Escape' || !input.value) { return; } keyboardEvent.consume(true); input.value = ''; searchHandler(); } input.addEventListener('keydown', keydownHandler, false); return input; } /** * @param {!StylePropertiesSection} section * @return {{allDeclarationText: string, ruleText: string}} */ static formatLeadingProperties(section) { const selectorText = section._headerText(); const indent = Common.Settings.Settings.instance().moduleSetting('textEditorIndent').get(); const style = section._style; /** @type {!Array<string>} */ const lines = []; // Invalid property should also be copied. // For example: *display: inline. for (const property of style.leadingProperties()) { if (property.disabled) { lines.push(`${indent}/* ${property.name}: ${property.value}; */`); } else { lines.push(`${indent}${property.name}: ${property.value};`); } } /** @type {string} */ const allDeclarationText = lines.join('\n'); /** @type {string} */ const ruleText = `${selectorText} {\n${allDeclarationText}\n}`; return { allDeclarationText, ruleText, }; } /** * @param {!SDK.CSSProperty.CSSProperty} cssProperty */ revealProperty(cssProperty) { this._decorator.highlightProperty(cssProperty); this._lastRevealedProperty = cssProperty; this.update(); } /** * @param {string} propertyName */ jumpToProperty(propertyName) { this._decorator.findAndHighlightPropertyName(propertyName); } forceUpdate() { this._needsForceUpdate = true; this._swatchPopoverHelper.hide(); this._resetCache(); this.update(); } /** * @param {!Event} event */ _sectionsContainerKeyDown(event) { const activeElement = this._sectionsContainer.ownerDocument.deepActiveElement(); if (!activeElement) { return; } const section = this.sectionByElement.get(activeElement); if (!section) { return; } switch (/** @type {!KeyboardEvent} */ (event).key) { case 'ArrowUp': case 'ArrowLeft': { const sectionToFocus = section.previousSibling() || section.lastSibling(); if (sectionToFocus) { sectionToFocus.element.focus(); event.consume(true); } break; } case 'ArrowDown': case 'ArrowRight': { const sectionToFocus = section.nextSibling() || section.firstSibling(); if (sectionToFocus) { sectionToFocus.element.focus(); event.consume(true); } break; } case 'Home': { const sectionToFocus = section.firstSibling(); if (sectionToFocus) { sectionToFocus.element.focus(); event.consume(true); } break; } case 'End': { const sectionToFocus = section.lastSibling(); if (sectionToFocus) { sectionToFocus.element.focus(); event.consume(true); } break; } } } _sectionsContainerFocusChanged() { this.resetFocus(); } resetFocus() { // When a styles section is focused, shift+tab should leave the section. // Leaving tabIndex = 0 on the first element would cause it to be focused instead. if (this._sectionBlocks[0] && this._sectionBlocks[0].sections[0]) { this._sectionBlocks[0].sections[0].element.tabIndex = this._sectionsContainer.hasFocus() ? -1 : 0; } } /** * @param {!Event} event */ _onAddButtonLongClick(event) { const cssModel = this.cssModel(); if (!cssModel) { return; } const headers = cssModel.styleSheetHeaders().filter(styleSheetResourceHeader); /** @type {!Array.<{text: string, handler: function():Promise<void>}>} */ const contextMenuDescriptors = []; for (let i = 0; i < headers.length; ++i) { const header = headers[i]; const handler = this._createNewRuleInStyleSheet.bind(this, header); contextMenuDescriptors.push({text: Bindings.ResourceUtils.displayNameForURL(header.resourceURL()), handler}); } contextMenuDescriptors.sort(compareDescriptors); const contextMenu = new UI.ContextMenu.ContextMenu(event); for (let i = 0; i < contextMenuDescriptors.length; ++i) { const descriptor = contextMenuDescriptors[i]; contextMenu.defaultSection().appendItem(descriptor.text, descriptor.handler); } contextMenu.footerSection().appendItem( 'inspector-stylesheet', this._createNewRuleInViaInspectorStyleSheet.bind(this)); contextMenu.show(); /** * @param {!{text: string, handler: function():Promise<void>}} descriptor1 * @param {!{text: string, handler: function():Promise<void>}} descriptor2 * @return {number} */ function compareDescriptors(descriptor1, descriptor2) { return Platform.StringUtilities.naturalOrderComparator(descriptor1.text, descriptor2.text); } /** * @param {!SDK.CSSStyleSheetHeader.CSSStyleSheetHeader} header * @return {boolean} */ function styleSheetResourceHeader(header) { return !header.isViaInspector() && !header.isInline && Boolean(header.resourceURL()); } } /** * @param {?RegExp} regex */ _onFilterChanged(regex) { this._filterRegex = regex; this._updateFilter(); } /** * @param {!StylePropertiesSection} editedSection * @param {!StylePropertyTreeElement=} editedTreeElement */ _refreshUpdate(editedSection, editedTreeElement) { if (editedTreeElement) { for (const section of this.allSections()) { if (section instanceof BlankStylePropertiesSection && section.isBlank) { continue; } section._updateVarFunctions(editedTreeElement); } } if (this._isEditingStyle) { return; } const node = this.node(); if (!node) { return; } for (const section of this.allSections()) { if (section instanceof BlankStylePropertiesSection && section.isBlank) { continue; } section.update(section === editedSection); } if (this._filterRegex) { this._updateFilter(); } this._nodeStylesUpdatedForTest(node, false); } /** * @override * @return {!Promise.<?>} */ async doUpdate() { if (!this._initialUpdateCompleted) { setTimeout(() => { if (!this._initialUpdateCompleted) { // the spinner will get automatically removed when _innerRebuildUpdate is called this._sectionsContainer.createChild('span', 'spinner'); } }, 200 /* only spin for loading time > 200ms to avoid unpleasant render flashes */); } const matchedStyles = await this._fetchMatchedCascade(); await this._innerRebuildUpdate(matchedStyles); if (!this._initialUpdateCompleted) { this._initialUpdateCompleted = true; this.dispatchEventToListeners(Events.InitialUpdateCompleted); } this.dispatchEventToListeners(Events.StylesUpdateCompleted, {hasMatchedStyles: this.hasMatchedStyles}); } /** * @override */ onResize() { this._resizeThrottler.schedule(this._innerResize.bind(this)); } /** * @return {!Promise<void>} */ _innerResize() { const width = this.contentElement.getBoundingClientRect().width + 'px'; this.allSections().forEach(section => { section.propertiesTreeOutline.element.style.width = width; }); return Promise.resolve(); } _resetCache() { const cssModel = this.cssModel(); if (cssModel) { cssModel.discardCachedMatchedCascade(); } } /** * @return {!Promise.<?SDK.CSSMatchedStyles.CSSMatchedStyles>} */ _fetchMatchedCascade() { const node = this.node(); if (!node || !this.cssModel()) { return Promise.resolve(/** @type {?SDK.CSSMatchedStyles.CSSMatchedStyles} */ (null)); } const cssModel = this.cssModel(); if (!cssModel) { return Promise.resolve(null); } return cssModel.cachedMatchedCascadeForNode(node).then(validateStyles.bind(this)); /** * @param {?SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @return {?SDK.CSSMatchedStyles.CSSMatchedStyles} * @this {StylesSidebarPane} */ function validateStyles(matchedStyles) { return matchedStyles && matchedStyles.node() === this.node() ? matchedStyles : null; } } /** * @param {boolean} editing * @param {!StylePropertyTreeElement=} treeElement */ setEditingStyle(editing, treeElement) { if (this._isEditingStyle === editing) { return; } this.contentElement.classList.toggle('is-editing-style', editing); this._isEditingStyle = editing; this._setActiveProperty(null); } /** * @param {?StylePropertyTreeElement} treeElement */ _setActiveProperty(treeElement) { if (this._isActivePropertyHighlighted) { SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); } this._isActivePropertyHighlighted = false; if (!this.node()) { return; } if (!treeElement || treeElement.overloaded() || treeElement.inherited()) { return; } const rule = treeElement.property.ownerStyle.parentRule; const selectorList = (rule instanceof SDK.CSSRule.CSSStyleRule) ? rule.selectorText() : undefined; for (const {properties, mode} of HIGHLIGHTABLE_PROPERTIES) { if (!properties.includes(treeElement.name)) { continue; } const node = this.node(); if (!node) { continue; } node.domModel().overlayModel().highlightInOverlay( {node: /** @type {!SDK.DOMModel.DOMNode} */ (this.node()), selectorList}, mode); this._isActivePropertyHighlighted = true; break; } } /** * @override * @param {!Common.EventTarget.EventTargetEvent=} event */ onCSSModelChanged(event) { const edit = event && event.data ? /** @type {?SDK.CSSModel.Edit} */ (event.data.edit) : null; if (edit) { for (const section of this.allSections()) { section._styleSheetEdited(edit); } return; } if (this._userOperation || this._isEditingStyle) { return; } this._resetCache(); this.update(); } /** * @return {number} */ focusedSectionIndex() { let index = 0; for (const block of this._sectionBlocks) { for (const section of block.sections) { if (section.element.hasFocus()) { return index; } index++; } } return -1; } /** * @param {number} sectionIndex * @param {number} propertyIndex */ continueEditingElement(sectionIndex, propertyIndex) { const section = this.allSections()[sectionIndex]; if (section) { const element = /** @type {?StylePropertyTreeElement} */ (section.closestPropertyForEditing(propertyIndex)); if (!element) { section.element.focus(); return; } element.startEditing(); } } /** * @param {?SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @return {!Promise<void>} */ async _innerRebuildUpdate(matchedStyles) { // ElementsSidebarPane's throttler schedules this method. Usually, // rebuild is suppressed while editing (see onCSSModelChanged()), but we need a // 'force' flag since the currently running throttler process cannot be canceled. if (this._needsForceUpdate) { this._needsForceUpdate = false; } else if (this._isEditingStyle || this._userOperation) { return; } const focusedIndex = this.focusedSectionIndex(); this._linkifier.reset(); const prevSections = this._sectionBlocks.map(block => block.sections).flat(); this._sectionBlocks = []; const node = this.node(); this.hasMatchedStyles = matchedStyles !== null && node !== null; if (!this.hasMatchedStyles) { this._sectionsContainer.removeChildren(); this._noMatchesElement.classList.remove('hidden'); return; } this._sectionBlocks = await this._rebuildSectionsForMatchedStyleRules( /** @type {!SDK.CSSMatchedStyles.CSSMatchedStyles} */ (matchedStyles)); // Style sections maybe re-created when flexbox editor is activated. // With the following code we re-bind the flexbox editor to the new // section with the same index as the previous section had. const newSections = this._sectionBlocks.map(block => block.sections).flat(); const flexEditorWidget = FlexboxEditorWidget.instance(); const boundSection = flexEditorWidget.getSection(); if (boundSection) { flexEditorWidget.unbindContext(); for (const [index, prevSection] of prevSections.entries()) { if (boundSection === prevSection && index < newSections.length) { flexEditorWidget.bindContext(this, newSections[index]); } } } this._sectionsContainer.removeChildren(); const fragment = document.createDocumentFragment(); let index = 0; let elementToFocus = null; for (const block of this._sectionBlocks) { const titleElement = block.titleElement(); if (titleElement) { fragment.appendChild(titleElement); } for (const section of block.sections) { fragment.appendChild(section.element); if (index === focusedIndex) { elementToFocus = section.element; } index++; } } this._sectionsContainer.appendChild(fragment); if (elementToFocus) { elementToFocus.focus(); } if (focusedIndex >= index) { this._sectionBlocks[0].sections[0].element.focus(); } this._sectionsContainerFocusChanged(); if (this._filterRegex) { this._updateFilter(); } else { this._noMatchesElement.classList.toggle('hidden', this._sectionBlocks.length > 0); } this._nodeStylesUpdatedForTest(/** @type {!SDK.DOMModel.DOMNode} */ (node), true); if (this._lastRevealedProperty) { this._decorator.highlightProperty(this._lastRevealedProperty); this._lastRevealedProperty = null; } // Record the elements tool load time after the sidepane has loaded. Host.userMetrics.panelLoaded('elements', 'DevTools.Launch.Elements'); this.dispatchEventToListeners(Events.StylesUpdateCompleted, {hasStyle: true}); } /** * @param {!SDK.DOMModel.DOMNode} node * @param {boolean} rebuild */ _nodeStylesUpdatedForTest(node, rebuild) { // For sniffing in tests. } /** * @param {!SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @return {!Promise<!Array.<!SectionBlock>>} */ async _rebuildSectionsForMatchedStyleRules(matchedStyles) { if (this._idleCallbackManager) { this._idleCallbackManager.discard(); } this._idleCallbackManager = new IdleCallbackManager(); const blocks = [new SectionBlock(null)]; let lastParentNode = null; for (const style of matchedStyles.nodeStyles()) { const parentNode = matchedStyles.isInherited(style) ? matchedStyles.nodeForStyle(style) : null; if (parentNode && parentNode !== lastParentNode) { lastParentNode = parentNode; const block = await SectionBlock._createInheritedNodeBlock(lastParentNode); blocks.push(block); } const lastBlock = blocks[blocks.length - 1]; if (lastBlock) { this._idleCallbackManager.schedule(() => { const section = new StylePropertiesSection(this, matchedStyles, style); lastBlock.sections.push(section); }); } } let pseudoTypes = []; const keys = matchedStyles.pseudoTypes(); if (keys.delete(Protocol.DOM.PseudoType.Before)) { pseudoTypes.push(Protocol.DOM.PseudoType.Before); } pseudoTypes = pseudoTypes.concat([...keys].sort()); for (const pseudoType of pseudoTypes) { const block = SectionBlock.createPseudoTypeBlock(pseudoType); for (const style of matchedStyles.pseudoStyles(pseudoType)) { this._idleCallbackManager.schedule(() => { const section = new StylePropertiesSection(this, matchedStyles, style); block.sections.push(section); }); } blocks.push(block); } for (const keyframesRule of matchedStyles.keyframes()) { const block = SectionBlock.createKeyframesBlock(keyframesRule.name().text); for (const keyframe of keyframesRule.keyframes()) { this._idleCallbackManager.schedule(() => { block.sections.push(new KeyframePropertiesSection(this, matchedStyles, keyframe.style)); }); } blocks.push(block); } await this._idleCallbackManager.awaitDone(); return blocks; } async _createNewRuleInViaInspectorStyleSheet() { const cssModel = this.cssModel(); const node = this.node(); if (!cssModel || !node) { return; } this.setUserOperation(true); const styleSheetHeader = await cssModel.requestViaInspectorStylesheet(/** @type {!SDK.DOMModel.DOMNode} */ (node)); this.setUserOperation(false); await this._createNewRuleInStyleSheet(styleSheetHeader); } /** * @param {?SDK.CSSStyleSheetHeader.CSSStyleSheetHeader} styleSheetHeader */ async _createNewRuleInStyleSheet(styleSheetHeader) { if (!styleSheetHeader) { return; } const text = (await styleSheetHeader.requestContent()).content || ''; const lines = text.split('\n'); const range = TextUtils.TextRange.TextRange.createFromLocation(lines.length - 1, lines[lines.length - 1].length); if (this._sectionBlocks && this._sectionBlocks.length > 0) { this._addBlankSection(this._sectionBlocks[0].sections[0], styleSheetHeader.id, range); } } /** * @param {!StylePropertiesSection} insertAfterSection * @param {string} styleSheetId * @param {!TextUtils.TextRange.TextRange} ruleLocation */ _addBlankSection(insertAfterSection, styleSheetId, ruleLocation) { const node = this.node(); const blankSection = new BlankStylePropertiesSection( this, insertAfterSection._matchedStyles, node ? node.simpleSelector() : '', styleSheetId, ruleLocation, insertAfterSection._style); this._sectionsContainer.insertBefore(blankSection.element, insertAfterSection.element.nextSibling); for (const block of this._sectionBlocks) { const index = block.sections.indexOf(insertAfterSection); if (index === -1) { continue; } block.sections.splice(index + 1, 0, blankSection); blankSection.startEditingSelector(); } } /** * @param {!StylePropertiesSection} section */ removeSection(section) { for (const block of this._sectionBlocks) { const index = block.sections.indexOf(section); if (index === -1) { continue; } block.sections.splice(index, 1); section.element.remove(); } } /** * @return {?RegExp} */ filterRegex() { return this._filterRegex; } _updateFilter() { let hasAnyVisibleBlock = false; for (const block of this._sectionBlocks) { hasAnyVisibleBlock = block.updateFilter() || hasAnyVisibleBlock; } this._noMatchesElement.classList.toggle('hidden', Boolean(hasAnyVisibleBlock)); } /** * @override */ willHide() { this.hideAllPopovers(); super.willHide(); } hideAllPopovers() { this._swatchPopoverHelper.hide(); this._imagePreviewPopover.hide(); if (this.activeCSSAngle) { this.activeCSSAngle.minify(); this.activeCSSAngle = null; } } /** * @return {!Array<!StylePropertiesSection>} */ allSections() { /** @type {!Array<!StylePropertiesSection>} */ let sections = []; for (const block of this._sectionBlocks) { sections = sections.concat(block.sections); } return sections; } /** * @param {!Event} event */ _clipboardCopy(event) { Host.userMetrics.actionTaken(Host.UserMetrics.Action.StyleRuleCopied); } /** * @return {!HTMLElement} */ _createStylesSidebarToolbar() { const container = this.contentElement.createChild('div', 'styles-sidebar-pane-toolbar-container'); const hbox = container.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, this._onFilterChanged.bind(this)); UI.ARIAUtils.setAccessibleName(filterInput, i18nString(UIStrings.filterStyles)); filterContainerElement.appendChild(filterInput); const toolbar = new UI.Toolbar.Toolbar('styles-pane-toolbar', hbox); toolbar.makeToggledGray(); toolbar.appendItemsAtLocation('styles-sidebarpane-toolbar'); this._toolbar = toolbar; const toolbarPaneContainer = container.createChild('div', 'styles-sidebar-toolbar-pane-container'); const toolbarPaneContent = /** @type {!HTMLElement} */ (toolbarPaneContainer.createChild('div', 'styles-sidebar-toolbar-pane')); return toolbarPaneContent; } /** * @param {?UI.Widget.Widget} widget * @param {?UI.Toolbar.ToolbarToggle} toggle */ showToolbarPane(widget, toggle) { if (this._pendingWidgetToggle) { this._pendingWidgetToggle.setToggled(false); } this._pendingWidgetToggle = toggle; if (this._animatedToolbarPane) { this._pendingWidget = widget; } else { this._startToolbarPaneAnimation(widget); } if (widget && toggle) { toggle.setToggled(true); } } /** * @param {!UI.Toolbar.ToolbarItem} item */ appendToolbarItem(item) { if (this._toolbar) { this._toolbar.appendToolbarItem(item); } } /** * @param {?UI.Widget.Widget} widget */ _startToolbarPaneAnimation(widget) { if (widget === this._currentToolbarPane) { return; } if (widget && this._currentToolbarPane) { this._currentToolbarPane.detach(); widget.show(this._toolbarPaneElement); this._currentToolbarPane = widget; this._currentToolbarPane.focus(); return; } this._animatedToolbarPane = widget; if (this._currentToolbarPane) { this._toolbarPaneElement.style.animationName = 'styles-element-state-pane-slideout'; } else if (widget) { this._toolbarPaneElement.style.animationName = 'styles-element-state-pane-slidein'; } if (widget) { widget.show(this._toolbarPaneElement); } const listener = onAnimationEnd.bind(this); this._toolbarPaneElement.addEventListener('animationend', listener, false); /** * @this {!StylesSidebarPane} */ function onAnimationEnd() { this._toolbarPaneElement.style.removeProperty('animation-name'); this._toolbarPaneElement.removeEventListener('animationend', listener, false); if (this._currentToolbarPane) { this._currentToolbarPane.detach(); } this._currentToolbarPane = this._animatedToolbarPane; if (this._currentToolbarPane) { this._currentToolbarPane.focus(); } this._animatedToolbarPane = null; if (this._pendingWidget) { this._startToolbarPaneAnimation(this._pendingWidget); this._pendingWidget = null; } } } } /** @enum {symbol} */ export const Events = { InitialUpdateCompleted: Symbol('InitialUpdateCompleted'), StylesUpdateCompleted: Symbol('StylesUpdateCompleted'), }; export const _maxLinkLength = 23; export class SectionBlock { /** * @param {?Element} titleElement */ constructor(titleElement) { this._titleElement = titleElement; /** @type {!Array<!StylePropertiesSection>} */ this.sections = []; } /** * @param {!Protocol.DOM.PseudoType} pseudoType * @return {!SectionBlock} */ static createPseudoTypeBlock(pseudoType) { const separatorElement = document.createElement('div'); separatorElement.className = 'sidebar-separator'; separatorElement.textContent = i18nString(UIStrings.pseudoSElement, {PH1: pseudoType}); return new SectionBlock(separatorElement); } /** * @param {string} keyframesName * @return {!SectionBlock} */ static createKeyframesBlock(keyframesName) { const separatorElement = document.createElement('div'); separatorElement.className = 'sidebar-separator'; separatorElement.textContent = `@keyframes ${keyframesName}`; return new SectionBlock(separatorElement); } /** * @param {!SDK.DOMModel.DOMNode} node * @return {!Promise<!SectionBlock>} */ static async _createInheritedNodeBlock(node) { const separatorElement = document.createElement('div'); separatorElement.className = 'sidebar-separator'; UI.UIUtils.createTextChild(separatorElement, i18nString(UIStrings.inheritedFroms)); const link = await Common.Linkifier.Linkifier.linkify(node, { preventKeyboardFocus: true, tooltip: undefined, }); separatorElement.appendChild(link); return new SectionBlock(separatorElement); } /** * @return {boolean} */ updateFilter() { let hasAnyVisibleSection = false; for (const section of this.sections) { hasAnyVisibleSection = section._updateFilter() || hasAnyVisibleSection; } if (this._titleElement) { this._titleElement.classList.toggle('hidden', !hasAnyVisibleSection); } return Boolean(hasAnyVisibleSection); } /** * @return {?Element} */ titleElement() { return this._titleElement; } } export class IdleCallbackManager { constructor() { this._discarded = false; /** @type {!Array<!Promise<void>>} */ this._promises = []; } discard() { this._discarded = true; } /** * @param {function():void} fn * @param {number} timeout */ schedule(fn, timeout = 100) { if (this._discarded) { return; } this._promises.push(new Promise((resolve, reject) => { const run = () => { try { fn(); resolve(); } catch (err) { reject(err); } }; window.requestIdleCallback(() => { if (this._discarded) { return resolve(); } run(); }, {timeout}); })); } awaitDone() { return Promise.all(this._promises); } } export class StylePropertiesSection { /** * @param {!StylesSidebarPane} parentPane * @param {!SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @param {!SDK.CSSStyleDeclaration.CSSStyleDeclaration} style */ constructor(parentPane, matchedStyles, style) { this._parentPane = parentPane; this._style = style; this._matchedStyles = matchedStyles; this.editable = Boolean(style.styleSheetId && style.range); /** @type {?number} */ this._hoverTimer = null; this._willCauseCancelEditing = false; this._forceShowAll = false; this._originalPropertiesCount = style.leadingProperties().length; const rule = style.parentRule; this.element = document.createElement('div'); this.element.classList.add('styles-section'); this.element.classList.add('matched-styles'); this.element.classList.add('monospace'); UI.ARIAUtils.setAccessibleName(this.element, `${this._headerText()}, css selector`); this.element.tabIndex = -1; UI.ARIAUtils.markAsTreeitem(this.element); this.element.addEventListener('keydown', this._onKeyDown.bind(this), false); parentPane.sectionByElement.set(this.element, this); this._innerElement = this.element.createChild('div'); this._titleElement = this._innerElement.createChild('div', 'styles-section-title ' + (rule ? 'styles-selector' : '')); this.propertiesTreeOutline = new UI.TreeOutline.TreeOutlineInShadow(); this.propertiesTreeOutline.setFocusable(false); this.propertiesTreeOutline.registerRequiredCSS('elements/stylesSectionTree.css', {enableLegacyPatching: true}); this.propertiesTreeOutline.element.classList.add('style-properties', 'matched-styles', 'monospace'); // @ts-ignore TODO: fix ad hoc section property in a separate CL to be safe this.propertiesTreeOutline.section = this; this._innerElement.appendChild(this.propertiesTreeOutline.element); this._showAllButton = UI.UIUtils.createTextButton('', this._showAllItems.bind(this), 'styles-show-all'); this._innerElement.appendChild(this._showAllButton); const selectorContainer = document.createElement('div'); this._selectorElement = document.createElement('span'); this._selectorElement.classList.add('selector'); this._selectorElement.textContent = this._headerText(); selectorContainer.appendChild(this._selectorElement); this._selectorElement.addEventListener('mouseenter', this._onMouseEnterSelector.bind(this), false); this._selectorElement.addEventListener('mousemove', event => event.consume(), false); this._selectorElement.addEventListener('mouseleave', this._onMouseOutSelector.bind(this), false); const openBrace = selectorContainer.createChild('span', 'sidebar-pane-open-brace'); openBrace.textContent = ' {'; selectorContainer.addEventListener('mousedown', this._handleEmptySpaceMouseDown.bind(this), false); selectorContainer.addEventListener('click', this._handleSelectorContainerClick.bind(this), false); const closeBrace = this._innerElement.createChild('div', 'sidebar-pane-closing-brace'); closeBrace.textContent = '}'; if (this._style.parentRule) { const newRuleButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.insertStyleRuleBelow), 'largeicon-add'); newRuleButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._onNewRuleClick, this); newRuleButton.element.tabIndex = -1; if (!this._newStyleRuleToolbar) { this._newStyleRuleToolbar = new UI.Toolbar.Toolbar('sidebar-pane-section-toolbar new-rule-toolbar', this._innerElement); } this._newStyleRuleToolbar.appendToolbarItem(newRuleButton); UI.ARIAUtils.markAsHidden(this._newStyleRuleToolbar.element); } if (Root.Runtime.experiments.isEnabled('fontEditor') && this.editable) { this._fontEditorToolbar = new UI.Toolbar.Toolbar('sidebar-pane-section-toolbar', this._innerElement); this._fontEditorSectionManager = new FontEditorSectionManager(this._parentPane.swatchPopoverHelper(), this); this._fontEditorButton = new UI.Toolbar.ToolbarButton('Font Editor', 'largeicon-font-editor'); this._fontEditorButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => { this._onFontEditorButtonClicked(); }, this); this._fontEditorButton.element.addEventListener('keydown', event => { if (isEnterOrSpaceKey(event)) { event.consume(true); this._onFontEditorButtonClicked(); } }, false); this._fontEditorToolbar.appendToolbarItem(this._fontEditorButton); if (this._style.type === SDK.CSSStyleDeclaration.Type.Inline) { if (this._newStyleRuleToolbar) { this._newStyleRuleToolbar.element.classList.add('shifted-toolbar'); } } else { this._fontEditorToolbar.element.classList.add('font-toolbar-hidden'); } } this._selectorElement.addEventListener('click', this._handleSelectorClick.bind(this), false); this.element.addEventListener('contextmenu', this._handleContextMenuEvent.bind(this), false); this.element.addEventListener('mousedown', this._handleEmptySpaceMouseDown.bind(this), false); this.element.addEventListener('click', this._handleEmptySpaceClick.bind(this), false); this.element.addEventListener('mousemove', this._onMouseMove.bind(this), false); this.element.addEventListener('mouseleave', this._onMouseLeave.bind(this), false); this._selectedSinceMouseDown = false; /** @type {!WeakMap<!Element, number>} */ this._elementToSelectorIndex = new WeakMap(); if (rule) { // Prevent editing the user agent and user rules. if (rule.isUserAgent() || rule.isInjected()) { this.editable = false; } else { // Check this is a real CSSRule, not a bogus object coming from BlankStylePropertiesSection. if (rule.styleSheetId) { const header = rule.cssModel().styleSheetHeaderForId(rule.styleSheetId); this.navigable = header && !header.isAnonymousInlineStyleSheet(); } } } this._mediaListElement = this._titleElement.createChild('div', 'media-list media-matches'); this._selectorRefElement = this._titleElement.createChild('div', 'styles-section-subtitle'); this._updateMediaList(); this._updateRuleOrigin(); this._titleElement.appendChild(selectorContainer); this._selectorContainer = selectorContainer; if (this.navigable) { this.element.classList.add('navigable'); } if (!this.editable) { this.element.classList.add('read-only'); this.propertiesTreeOutline.element.classList.add('read-only'); } /** @type {?FontEditorSectionManager} */ this._fontPopoverIcon = null; this._hoverableSelectorsMode = false; this._markSelectorMatches(); this.onpopulate(); } /** * @param {!StylePropertyTreeElement} treeElement */ registerFontProperty(treeElement) { if (this._fontEditorSectionManager) { this._fontEditorSectionManager.registerFontProperty(treeElement); } if (this._fontEditorToolbar) { this._fontEditorToolbar.element.classList.remove('font-toolbar-hidden'); if (this._newStyleRuleToolbar) { this._newStyleRuleToolbar.element.classList.add('shifted-toolbar'); } } } resetToolbars() { if (this._parentPane.swatchPopoverHelper().isShowing() || this._style.type === SDK.CSSStyleDeclaration.Type.Inline) { return; } if (this._fontEditorToolbar) { this._fontEditorToolbar.element.classList.add('font-toolbar-hidden'); } if (this._newStyleRuleToolbar) { this._newStyleRuleToolbar.element.classList.remove('shifted-toolbar'); } } /** * @param {!SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @param {!Components.Linkifier.Linkifier} linkifier * @param {?SDK.CSSRule.CSSRule} rule * @return {!Node} */ static createRuleOriginNode(matchedStyles, linkifier, rule) { if (!rule) { return document.createTextNode(''); } const ruleLocation = this._getRuleLocationFromCSSRule(rule); const header = rule.styleSheetId ? matchedStyles.cssModel().styleSheetHeaderForId(rule.styleSheetId) : null; if (header && header.isMutable && !header.isViaInspector()) { const label = header.isConstructed ? i18nString(UIStrings.constructedStylesheet) : '<style>'; if (header.ownerNode) { const link = linkifyDeferredNodeReference(header.ownerNode); link.textContent = label; return link; } return document.createTextNode(label); } if (ruleLocation && rule.styleSheetId && header && !header.isAnonymousInlineStyleSheet()) { return StylePropertiesSection._linkifyRuleLocation( matchedStyles.cssModel(), linkifier, rule.styleSheetId, ruleLocation); } if (rule.isUserAgent()) { return document.createTextNode(i18nString(UIStrings.userAgentStylesheet)); } if (rule.isInjected()) { return document.createTextNode(i18nString(UIStrings.injectedStylesheet)); } if (rule.isViaInspector()) { return document.createTextNode(i18nString(UIStrings.viaInspector)); } if (header && header.ownerNode) { const link = linkifyDeferredNodeReference(header.ownerNode, { preventKeyboardFocus: true, tooltip: undefined, }); link.textContent = '<style>'; return link; } return document.createTextNode(''); } /** * @param {!SDK.CSSRule.CSSRule} rule * @return {?TextUtils.TextRange.TextRange|undefined} */ static _getRuleLocationFromCSSRule(rule) { let ruleLocation; if (rule instanceof SDK.CSSRule.CSSStyleRule) { ruleLocation = rule.style.range; } else if (rule instanceof SDK.CSSRule.CSSKeyframeRule) { ruleLocation = rule.key().range; } return ruleLocation; } /** * @param {!SDK.CSSMatchedStyles.CSSMatchedStyles} matchedStyles * @param {?SDK.CSSRule.CSSRule} rule */ static tryNavigateToRuleLocation(matchedStyles, rule) { if (!rule) { return; } const ruleLocation = this._getRuleLocationFromCSSRule(rule); cons