UNPKG

debug-server-next

Version:

Dev server for hippy-core.

1,160 lines 113 kB
// Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* * 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. */ /* eslint-disable rulesdir/no_underscored_properties */ import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as Root from '../../core/root/root.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import * as IconButton from '../../ui/components/icon_button/icon_button.js'; import * as InlineEditor from '../../ui/legacy/components/inline_editor/inline_editor.js'; import * as Components from '../../ui/legacy/components/utils/utils.js'; import * as UI from '../../ui/legacy/legacy.js'; import { FontEditorSectionManager } from './ColorSwatchPopoverIcon.js'; import * as ElementsComponents from './components/components.js'; import { ComputedStyleModel } from './ComputedStyleModel.js'; import { linkifyDeferredNodeReference } from './DOMLinkifier.js'; import { ElementsPanel } from './ElementsPanel.js'; import { ElementsSidebarPane } from './ElementsSidebarPane.js'; import { ImagePreviewPopover } from './ImagePreviewPopover.js'; import { StyleEditorWidget } from './StyleEditorWidget.js'; import { StylePropertyHighlighter } from './StylePropertyHighlighter.js'; import { StylePropertyTreeElement } from './StylePropertyTreeElement.js'; 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 Announcement string for invalid properties. *@example {Invalid property value} PH1 *@example {font-size} PH2 *@example {invalidValue} PH3 */ invalidString: '{PH1}, property name: {PH2}, property value: {PH3}', /** *@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('panels/elements/StylesSidebarPane.ts', 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'] }, { mode: 'flexibility', properties: ['flex', 'flex-basis', 'flex-grow', 'flex-shrink'] }, ]; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/naming-convention let _stylesSidebarPaneInstance; // TODO(crbug.com/1172300) This workaround is needed to keep the linter happy. // Otherwise it complains about: Unknown word CssSyntaxError const STYLE_TAG = '<' + 'style>'; export class StylesSidebarPane extends ElementsSidebarPane { _currentToolbarPane; _animatedToolbarPane; _pendingWidget; _pendingWidgetToggle; _toolbar; _toolbarPaneElement; _computedStyleModel; _noMatchesElement; _sectionsContainer; sectionByElement; _swatchPopoverHelper; _linkifier; _decorator; _lastRevealedProperty; _userOperation; _isEditingStyle; _filterRegex; _isActivePropertyHighlighted; _initialUpdateCompleted; hasMatchedStyles; _sectionBlocks; _idleCallbackManager; _needsForceUpdate; _resizeThrottler; _imagePreviewPopover; activeCSSAngle; static instance() { if (!_stylesSidebarPaneInstance) { _stylesSidebarPaneInstance = new StylesSidebarPane(); } return _stylesSidebarPaneInstance; } constructor() { super(true /* delegatesFocus */); this.setMinimumSize(96, 26); this.registerRequiredCSS('panels/elements/stylesSidebarPane.css'); Common.Settings.Settings.instance().moduleSetting('colorFormat').addChangeListener(this.update.bind(this)); Common.Settings.Settings.instance().moduleSetting('textEditorIndent').addChangeListener(this.update.bind(this)); this._currentToolbarPane = null; this._animatedToolbarPane = null; this._pendingWidget = null; this._pendingWidgetToggle = null; 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.markAsList(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); 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); this._decorator = new StylePropertyHighlighter(this); this._lastRevealedProperty = null; this._userOperation = false; this._isEditingStyle = false; this._filterRegex = null; this._isActivePropertyHighlighted = false; this._initialUpdateCompleted = false; this.hasMatchedStyles = false; this.contentElement.classList.add('styles-pane'); this._sectionBlocks = []; 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()); this.activeCSSAngle = null; } swatchPopoverHelper() { return this._swatchPopoverHelper; } setUserOperation(userOperation) { this._userOperation = userOperation; } static createExclamationMark(property, title) { const exclamationElement = document.createElement('span', { is: 'dt-icon-label' }); exclamationElement.className = 'exclamation-mark'; if (!StylesSidebarPane.ignoreErrorsForProperty(property)) { exclamationElement.type = 'smallicon-warning'; } let invalidMessage; if (title) { UI.Tooltip.Tooltip.install(exclamationElement, title); invalidMessage = title; } else { invalidMessage = SDK.CSSMetadata.cssMetadata().isCSSPropertyName(property.name) ? i18nString(UIStrings.invalidPropertyValue) : i18nString(UIStrings.unknownPropertyName); UI.Tooltip.Tooltip.install(exclamationElement, invalidMessage); } const invalidString = i18nString(UIStrings.invalidString, { PH1: invalidMessage, PH2: property.name, PH3: property.value }); // Storing the invalidString for future screen reader support when editing the property property.setDisplayedStringForInvalidProperty(invalidString); return exclamationElement; } static ignoreErrorsForProperty(property) { 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; } 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); function keydownHandler(event) { const keyboardEvent = event; if (keyboardEvent.key !== Platform.KeyboardUtilities.ESCAPE_KEY || !input.value) { return; } keyboardEvent.consume(true); input.value = ''; searchHandler(); } input.addEventListener('keydown', keydownHandler, false); return input; } static formatLeadingProperties(section) { const selectorText = section._headerText(); const indent = Common.Settings.Settings.instance().moduleSetting('textEditorIndent').get(); const style = section._style; 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};`); } } const allDeclarationText = lines.join('\n'); const ruleText = `${selectorText} {\n${allDeclarationText}\n}`; return { allDeclarationText, ruleText, }; } revealProperty(cssProperty) { this._decorator.highlightProperty(cssProperty); this._lastRevealedProperty = cssProperty; this.update(); } jumpToProperty(propertyName) { this._decorator.findAndHighlightPropertyName(propertyName); } forceUpdate() { this._needsForceUpdate = true; this._swatchPopoverHelper.hide(); this._resetCache(); this.update(); } _sectionsContainerKeyDown(event) { const activeElement = this._sectionsContainer.ownerDocument.deepActiveElement(); if (!activeElement) { return; } const section = this.sectionByElement.get(activeElement); if (!section) { return; } let sectionToFocus = null; let willIterateForward = false; switch ( /** @type {!KeyboardEvent} */event.key) { case 'ArrowUp': case 'ArrowLeft': { sectionToFocus = section.previousSibling() || section.lastSibling(); willIterateForward = false; break; } case 'ArrowDown': case 'ArrowRight': { sectionToFocus = section.nextSibling() || section.firstSibling(); willIterateForward = true; break; } case 'Home': { sectionToFocus = section.firstSibling(); willIterateForward = true; break; } case 'End': { sectionToFocus = section.lastSibling(); willIterateForward = false; break; } } if (sectionToFocus && this._filterRegex) { sectionToFocus = sectionToFocus.findCurrentOrNextVisible(/* willIterateForward= */ willIterateForward); } if (sectionToFocus) { sectionToFocus.element.focus(); event.consume(true); } } _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._noMatchesElement.classList.contains('hidden')) { return; } if (this._sectionBlocks[0] && this._sectionBlocks[0].sections[0]) { const firstVisibleSection = this._sectionBlocks[0].sections[0].findCurrentOrNextVisible(/* willIterateForward= */ true); if (firstVisibleSection) { firstVisibleSection.element.tabIndex = this._sectionsContainer.hasFocus() ? -1 : 0; } } } _onAddButtonLongClick(event) { const cssModel = this.cssModel(); if (!cssModel) { return; } const headers = cssModel.styleSheetHeaders().filter(styleSheetResourceHeader); 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(); function compareDescriptors(descriptor1, descriptor2) { return Platform.StringUtilities.naturalOrderComparator(descriptor1.text, descriptor2.text); } function styleSheetResourceHeader(header) { return !header.isViaInspector() && !header.isInline && Boolean(header.resourceURL()); } } _onFilterChanged(regex) { this._filterRegex = regex; this._updateFilter(); this.resetFocus(); } _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); } 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("InitialUpdateCompleted" /* InitialUpdateCompleted */); } this.dispatchEventToListeners("StylesUpdateCompleted" /* StylesUpdateCompleted */, { hasMatchedStyles: this.hasMatchedStyles }); } onResize() { this._resizeThrottler.schedule(this._innerResize.bind(this)); } _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(); } } _fetchMatchedCascade() { const node = this.node(); if (!node || !this.cssModel()) { return Promise.resolve(null); } const cssModel = this.cssModel(); if (!cssModel) { return Promise.resolve(null); } return cssModel.cachedMatchedCascadeForNode(node).then(validateStyles.bind(this)); function validateStyles(matchedStyles) { return matchedStyles && matchedStyles.node() === this.node() ? matchedStyles : null; } } setEditingStyle(editing, _treeElement) { if (this._isEditingStyle === editing) { return; } this.contentElement.classList.toggle('is-editing-style', editing); this._isEditingStyle = editing; this._setActiveProperty(null); } _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: this.node(), selectorList }, mode); this._isActivePropertyHighlighted = true; break; } } onCSSModelChanged(event) { const edit = event && event.data ? 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(); } 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; } continueEditingElement(sectionIndex, propertyIndex) { const section = this.allSections()[sectionIndex]; if (section) { const element = section.closestPropertyForEditing(propertyIndex); if (!element) { section.element.focus(); return; } element.startEditing(); } } 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(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 styleEditorWidget = StyleEditorWidget.instance(); const boundSection = styleEditorWidget.getSection(); if (boundSection) { styleEditorWidget.unbindContext(); for (const [index, prevSection] of prevSections.entries()) { if (boundSection === prevSection && index < newSections.length) { styleEditorWidget.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(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("StylesUpdateCompleted" /* StylesUpdateCompleted */, { hasStyle: true }); } _nodeStylesUpdatedForTest(_node, _rebuild) { // For sniffing in tests. } 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("before" /* Before */)) { pseudoTypes.push("before" /* 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(node); this.setUserOperation(false); await this._createNewRuleInStyleSheet(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); } } _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(); } } 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(); } } 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)); } willHide() { this.hideAllPopovers(); super.willHide(); } hideAllPopovers() { this._swatchPopoverHelper.hide(); this._imagePreviewPopover.hide(); if (this.activeCSSAngle) { this.activeCSSAngle.minify(); this.activeCSSAngle = null; } } allSections() { let sections = []; for (const block of this._sectionBlocks) { sections = sections.concat(block.sections); } return sections; } _clipboardCopy(_event) { Host.userMetrics.actionTaken(Host.UserMetrics.Action.StyleRuleCopied); } _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 = toolbarPaneContainer.createChild('div', 'styles-sidebar-toolbar-pane'); return toolbarPaneContent; } 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); } } appendToolbarItem(item) { if (this._toolbar) { this._toolbar.appendToolbarItem(item); } } _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); 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; } } } } // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/naming-convention export const _maxLinkLength = 23; export class SectionBlock { _titleElement; sections; constructor(titleElement) { this._titleElement = titleElement; this.sections = []; } static createPseudoTypeBlock(pseudoType) { const separatorElement = document.createElement('div'); separatorElement.className = 'sidebar-separator'; separatorElement.textContent = i18nString(UIStrings.pseudoSElement, { PH1: pseudoType }); return new SectionBlock(separatorElement); } static createKeyframesBlock(keyframesName) { const separatorElement = document.createElement('div'); separatorElement.className = 'sidebar-separator'; separatorElement.textContent = `@keyframes ${keyframesName}`; return new SectionBlock(separatorElement); } 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); } 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); } titleElement() { return this._titleElement; } } export class IdleCallbackManager { _discarded; _promises; constructor() { this._discarded = false; this._promises = []; } discard() { this._discarded = true; } 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 { _parentPane; _style; _matchedStyles; editable; _hoverTimer; _willCauseCancelEditing; _forceShowAll; _originalPropertiesCount; element; _innerElement; _titleElement; propertiesTreeOutline; _showAllButton; _selectorElement; _newStyleRuleToolbar; _fontEditorToolbar; _fontEditorSectionManager; _fontEditorButton; _selectedSinceMouseDown; _elementToSelectorIndex; navigable; _selectorRefElement; _selectorContainer; _fontPopoverIcon; _hoverableSelectorsMode; _isHidden; queryListElement; constructor(parentPane, matchedStyles, style) { this._parentPane = parentPane; this._style = style; this._matchedStyles = matchedStyles; this.editable = Boolean(style.styleSheetId && style.range); 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.markAsListitem(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('panels/elements/stylesSectionTree.css'); 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; 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.queryListElement = this._titleElement.createChild('div', 'query-list query-matches'); this._selectorRefElement = this._titleElement.createChild('div', 'styles-section-subtitle'); this.updateQueryList(); 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'); } this._fontPopoverIcon = null; this._hoverableSelectorsMode = false; this._isHidden = false; this._markSelectorMatches(); this.onpopulate(); } 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'); } } 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; function linkifyRuleLocation() { if (!rule) { return null; } if (ruleLocation && rule.styleSheetId && header && !header.isAnonymousInlineStyleSheet()) { return StylePropertiesSection._linkifyRuleLocation(matc