UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

1,149 lines 63.9 kB
// Copyright 2022 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. */ 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 UI from '../../ui/legacy/legacy.js'; import { FontEditorSectionManager } from './ColorSwatchPopoverIcon.js'; import * as ElementsComponents from './components/components.js'; import { linkifyDeferredNodeReference } from './DOMLinkifier.js'; import { ElementsPanel } from './ElementsPanel.js'; import { StylePropertyTreeElement } from './StylePropertyTreeElement.js'; import stylesSectionTreeStyles from './stylesSectionTree.css.js'; import { StylesSidebarPane } from './StylesSidebarPane.js'; const UIStrings = { /** *@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 A context menu item in Styles panel to copy all the CSS changes */ copyAllCSSChanges: 'Copy all CSS changes', /** *@description Text that is announced by the screen reader when the user focuses on an input field for editing the name of a CSS selector in the Styles panel */ cssSelector: '`CSS` selector', }; const str_ = i18n.i18n.registerUIStrings('panels/elements/StylePropertiesSection.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); // 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 StylePropertiesSection { parentPane; styleInternal; matchedStyles; computedStyles; parentsComputedStyles; editable; hoverTimer; willCauseCancelEditing; forceShowAll; originalPropertiesCount; element; innerElement; titleElement; propertiesTreeOutline; showAllButton; selectorElement; newStyleRuleToolbar; fontEditorToolbar; fontEditorSectionManager; fontEditorButton; selectedSinceMouseDown; elementToSelectorIndex; navigable; selectorRefElement; hoverableSelectorsMode; isHiddenInternal; ancestorRuleListElement; // Used to identify buttons that trigger a flexbox or grid editor. nextEditorTriggerButtonIdx = 1; sectionIdx = 0; // Used to keep track of Specificity Information static #nodeElementToSpecificity = new WeakMap(); #customHeaderText; constructor(parentPane, matchedStyles, style, sectionIdx, computedStyles, parentsComputedStyles, headerText) { this.#customHeaderText = headerText; this.parentPane = parentPane; this.sectionIdx = sectionIdx; this.styleInternal = style; this.matchedStyles = matchedStyles; this.computedStyles = computedStyles; this.parentsComputedStyles = parentsComputedStyles; 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.setLabel(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.registerCSSFiles([stylesSectionTreeStyles]); 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'); selectorContainer.classList.add('selector-container'); this.selectorElement = document.createElement('span'); UI.ARIAUtils.setLabel(this.selectorElement, i18nString(UIStrings.cssSelector)); 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('mouseleave', this.onMouseOutSelector.bind(this), false); const openBrace = selectorContainer.createChild('span', 'sidebar-pane-open-brace'); openBrace.textContent = ' {'; const closeBrace = this.innerElement.createChild('div', 'sidebar-pane-closing-brace'); closeBrace.textContent = '}'; if (this.styleInternal.parentRule) { const newRuleButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.insertStyleRuleBelow), 'plus'); 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', 'custom-typography'); this.fontEditorButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => { this.onFontEditorButtonClicked(); }, this); this.fontEditorButton.element.addEventListener('keydown', event => { if (Platform.KeyboardUtilities.isEnterOrSpaceKey(event)) { event.consume(true); this.onFontEditorButtonClicked(); } }, false); this.fontEditorToolbar.appendToolbarItem(this.fontEditorButton); if (this.styleInternal.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.ancestorRuleListElement = this.titleElement.createChild('div', 'ancestor-rule-list'); this.selectorRefElement = this.titleElement.createChild('div', 'styles-section-subtitle'); this.updateQueryList(); this.updateRuleOrigin(); this.titleElement.appendChild(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.hoverableSelectorsMode = false; this.isHiddenInternal = false; this.markSelectorMatches(); this.onpopulate(); } setComputedStyles(computedStyles) { this.computedStyles = computedStyles; } setParentsComputedStyles(parentsComputedStyles) { this.parentsComputedStyles = parentsComputedStyles; } updateAuthoringHint() { let child = this.propertiesTreeOutline.firstChild(); while (child) { if (child instanceof StylePropertyTreeElement) { child.setComputedStyles(this.computedStyles); child.setParentsComputedStyles(this.parentsComputedStyles); child.updateAuthoringHint(); } child = child.nextSibling; } } setSectionIdx(sectionIdx) { this.sectionIdx = sectionIdx; this.onpopulate(); } getSectionIdx() { return this.sectionIdx; } 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.styleInternal.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 = StylePropertiesSection.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(matchedStyles.cssModel(), linkifier, rule.styleSheetId, ruleLocation); } return null; } function linkifyNode(label) { if (header?.ownerNode) { const link = linkifyDeferredNodeReference(header.ownerNode, { preventKeyboardFocus: false, tooltip: undefined, }); link.textContent = label; return link; } return null; } if (header?.isMutable && !header.isViaInspector()) { const location = header.isConstructedByNew() ? null : linkifyRuleLocation(); if (location) { return location; } const label = header.isConstructedByNew() ? i18nString(UIStrings.constructedStylesheet) : STYLE_TAG; const node = linkifyNode(label); if (node) { return node; } return document.createTextNode(label); } const location = linkifyRuleLocation(); if (location) { return location; } 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)); } const node = linkifyNode(STYLE_TAG); if (node) { return node; } return document.createTextNode(''); } createRuleOriginNode(matchedStyles, linkifier, rule) { return StylePropertiesSection.createRuleOriginNode(matchedStyles, linkifier, rule); } 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; } static tryNavigateToRuleLocation(matchedStyles, rule) { if (!rule) { return; } const ruleLocation = this.getRuleLocationFromCSSRule(rule); const header = rule.styleSheetId ? matchedStyles.cssModel().styleSheetHeaderForId(rule.styleSheetId) : null; if (ruleLocation && rule.styleSheetId && header && !header.isAnonymousInlineStyleSheet()) { const matchingSelectorLocation = this.getCSSSelectorLocation(matchedStyles.cssModel(), rule.styleSheetId, ruleLocation); this.revealSelectorSource(matchingSelectorLocation, true); } } static linkifyRuleLocation(cssModel, linkifier, styleSheetId, ruleLocation) { const matchingSelectorLocation = this.getCSSSelectorLocation(cssModel, styleSheetId, ruleLocation); return linkifier.linkifyCSSLocation(matchingSelectorLocation); } static getCSSSelectorLocation(cssModel, styleSheetId, ruleLocation) { const styleSheetHeader = cssModel.styleSheetHeaderForId(styleSheetId); const lineNumber = styleSheetHeader.lineNumberInSource(ruleLocation.startLine); const columnNumber = styleSheetHeader.columnNumberInSource(ruleLocation.startLine, ruleLocation.startColumn); return new SDK.CSSModel.CSSLocation(styleSheetHeader, lineNumber, columnNumber); } getFocused() { return this.propertiesTreeOutline.shadowRoot.activeElement || null; } focusNext(element) { // Clear remembered focused item (if any). const focused = this.getFocused(); if (focused) { focused.tabIndex = -1; } // Focus the next item and remember it (if in our subtree). element.focus(); if (this.propertiesTreeOutline.shadowRoot.contains(element)) { element.tabIndex = 0; } } ruleNavigation(keyboardEvent) { if (keyboardEvent.altKey || keyboardEvent.ctrlKey || keyboardEvent.metaKey || keyboardEvent.shiftKey) { return; } const focused = this.getFocused(); let focusNext = null; const focusable = Array.from(this.propertiesTreeOutline.shadowRoot.querySelectorAll('[tabindex]')); if (focusable.length === 0) { return; } const focusedIndex = focused ? focusable.indexOf(focused) : -1; if (keyboardEvent.key === 'ArrowLeft') { focusNext = focusable[focusedIndex - 1] || this.element; } else if (keyboardEvent.key === 'ArrowRight') { focusNext = focusable[focusedIndex + 1] || this.element; } else if (keyboardEvent.key === 'ArrowUp' || keyboardEvent.key === 'ArrowDown') { this.focusNext(this.element); return; } if (focusNext) { this.focusNext(focusNext); keyboardEvent.consume(true); } } onKeyDown(event) { const keyboardEvent = event; if (UI.UIUtils.isEditing() || !this.editable || keyboardEvent.altKey || keyboardEvent.ctrlKey || keyboardEvent.metaKey) { return; } switch (keyboardEvent.key) { case 'Enter': case ' ': this.startEditingAtFirstPosition(); keyboardEvent.consume(true); break; case 'ArrowLeft': case 'ArrowRight': case 'ArrowUp': case 'ArrowDown': this.ruleNavigation(keyboardEvent); break; default: // Filter out non-printable key strokes. if (keyboardEvent.key.length === 1) { this.addNewBlankProperty(0).startEditing(); } break; } } setSectionHovered(isHovered) { this.element.classList.toggle('styles-panel-hovered', isHovered); this.propertiesTreeOutline.element.classList.toggle('styles-panel-hovered', isHovered); if (this.hoverableSelectorsMode !== isHovered) { this.hoverableSelectorsMode = isHovered; this.markSelectorMatches(); } } onMouseLeave(_event) { this.setSectionHovered(false); this.parentPane.setActiveProperty(null); } onMouseMove(event) { const hasCtrlOrMeta = UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event); this.setSectionHovered(hasCtrlOrMeta); const treeElement = this.propertiesTreeOutline.treeElementFromEvent(event); if (treeElement instanceof StylePropertyTreeElement) { this.parentPane.setActiveProperty(treeElement); } else { this.parentPane.setActiveProperty(null); } const selection = this.element.getComponentSelection(); if (!this.selectedSinceMouseDown && selection && selection.toString()) { this.selectedSinceMouseDown = true; } } onFontEditorButtonClicked() { if (this.fontEditorSectionManager && this.fontEditorButton) { void this.fontEditorSectionManager.showPopover(this.fontEditorButton.element, this.parentPane); } } style() { return this.styleInternal; } headerText() { if (this.#customHeaderText) { return this.#customHeaderText; } const node = this.matchedStyles.nodeForStyle(this.styleInternal); if (this.styleInternal.type === SDK.CSSStyleDeclaration.Type.Inline) { return this.matchedStyles.isInherited(this.styleInternal) ? i18nString(UIStrings.styleAttribute) : 'element.style'; } if (node && this.styleInternal.type === SDK.CSSStyleDeclaration.Type.Attributes) { return i18nString(UIStrings.sattributesStyle, { PH1: node.nodeNameInCorrectCase() }); } if (this.styleInternal.parentRule instanceof SDK.CSSRule.CSSStyleRule) { return this.styleInternal.parentRule.selectorText(); } return ''; } onMouseOutSelector() { if (this.hoverTimer) { clearTimeout(this.hoverTimer); } SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); } onMouseEnterSelector() { if (this.hoverTimer) { clearTimeout(this.hoverTimer); } this.hoverTimer = window.setTimeout(this.highlight.bind(this), 300); } highlight(mode = 'all') { SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); const node = this.parentPane.node(); if (!node) { return; } const selectorList = this.styleInternal.parentRule && this.styleInternal.parentRule instanceof SDK.CSSRule.CSSStyleRule ? this.styleInternal.parentRule.selectorText() : undefined; node.domModel().overlayModel().highlightInOverlay({ node, selectorList }, mode); } firstSibling() { const parent = this.element.parentElement; if (!parent) { return null; } let childElement = parent.firstChild; while (childElement) { const childSection = this.parentPane.sectionByElement.get(childElement); if (childSection) { return childSection; } childElement = childElement.nextSibling; } return null; } findCurrentOrNextVisible(willIterateForward, originalSection) { if (!this.isHidden()) { return this; } if (this === originalSection) { return null; } if (!originalSection) { originalSection = this; } let visibleSibling = null; const nextSibling = willIterateForward ? this.nextSibling() : this.previousSibling(); if (nextSibling) { visibleSibling = nextSibling.findCurrentOrNextVisible(willIterateForward, originalSection); } else { const loopSibling = willIterateForward ? this.firstSibling() : this.lastSibling(); if (loopSibling) { visibleSibling = loopSibling.findCurrentOrNextVisible(willIterateForward, originalSection); } } return visibleSibling; } lastSibling() { const parent = this.element.parentElement; if (!parent) { return null; } let childElement = parent.lastChild; while (childElement) { const childSection = this.parentPane.sectionByElement.get(childElement); if (childSection) { return childSection; } childElement = childElement.previousSibling; } return null; } nextSibling() { let curElement = this.element; do { curElement = curElement.nextSibling; } while (curElement && !this.parentPane.sectionByElement.has(curElement)); if (curElement) { return this.parentPane.sectionByElement.get(curElement); } return; } previousSibling() { let curElement = this.element; do { curElement = curElement.previousSibling; } while (curElement && !this.parentPane.sectionByElement.has(curElement)); if (curElement) { return this.parentPane.sectionByElement.get(curElement); } return; } onNewRuleClick(event) { event.data.consume(); const rule = this.styleInternal.parentRule; if (!rule || !rule.style.range || rule.styleSheetId === undefined) { return; } const range = TextUtils.TextRange.TextRange.createFromLocation(rule.style.range.endLine, rule.style.range.endColumn + 1); this.parentPane.addBlankSection(this, rule.styleSheetId, range); } styleSheetEdited(edit) { const rule = this.styleInternal.parentRule; if (rule) { rule.rebase(edit); } else { this.styleInternal.rebase(edit); } this.updateQueryList(); this.updateRuleOrigin(); } createAncestorRules(rule) { let mediaIndex = 0; let containerIndex = 0; let scopeIndex = 0; let supportsIndex = 0; let nestingIndex = 0; for (const ruleType of rule.ruleTypes) { let ancestorRuleElement; switch (ruleType) { case "MediaRule" /* Protocol.CSS.CSSRuleType.MediaRule */: ancestorRuleElement = this.createMediaElement(rule.media[mediaIndex++]); break; case "ContainerRule" /* Protocol.CSS.CSSRuleType.ContainerRule */: ancestorRuleElement = this.createContainerQueryElement(rule.containerQueries[containerIndex++]); break; case "ScopeRule" /* Protocol.CSS.CSSRuleType.ScopeRule */: ancestorRuleElement = this.createScopeElement(rule.scopes[scopeIndex++]); break; case "SupportsRule" /* Protocol.CSS.CSSRuleType.SupportsRule */: ancestorRuleElement = this.createSupportsElement(rule.supports[supportsIndex++]); break; case "StyleRule" /* Protocol.CSS.CSSRuleType.StyleRule */: ancestorRuleElement = this.createNestingElement(rule.nestingSelectors?.[nestingIndex++]); break; } ancestorRuleElement && this.ancestorRuleListElement.prepend(ancestorRuleElement); } } createMediaElement(media) { // Don't display trivial non-print media types. const isMedia = !media.text || !media.text.includes('(') && media.text !== 'print'; if (isMedia) { return; } let queryPrefix = ''; let queryText = ''; let onQueryTextClick; switch (media.source) { case SDK.CSSMedia.Source.LINKED_SHEET: case SDK.CSSMedia.Source.INLINE_SHEET: { queryText = `media="${media.text}"`; break; } case SDK.CSSMedia.Source.MEDIA_RULE: { queryPrefix = '@media'; queryText = media.text; if (media.styleSheetId) { onQueryTextClick = this.handleQueryRuleClick.bind(this, media); } break; } case SDK.CSSMedia.Source.IMPORT_RULE: { queryText = `@import ${media.text}`; break; } } const mediaQueryElement = new ElementsComponents.CSSQuery.CSSQuery(); mediaQueryElement.data = { queryPrefix, queryText, onQueryTextClick, }; return mediaQueryElement; } createContainerQueryElement(containerQuery) { if (!containerQuery.text) { return; } let onQueryTextClick; if (containerQuery.styleSheetId) { onQueryTextClick = this.handleQueryRuleClick.bind(this, containerQuery); } const containerQueryElement = new ElementsComponents.CSSQuery.CSSQuery(); containerQueryElement.data = { queryPrefix: '@container', queryName: containerQuery.name, queryText: containerQuery.text, onQueryTextClick, }; void this.addContainerForContainerQuery(containerQuery); return containerQueryElement; } createScopeElement(scope) { if (!scope.text) { return; } let onQueryTextClick; if (scope.styleSheetId) { onQueryTextClick = this.handleQueryRuleClick.bind(this, scope); } const scopeElement = new ElementsComponents.CSSQuery.CSSQuery(); scopeElement.data = { queryPrefix: '@scope', queryText: scope.text, onQueryTextClick, }; return scopeElement; } createSupportsElement(supports) { if (!supports.text) { return; } let onQueryTextClick; if (supports.styleSheetId) { onQueryTextClick = this.handleQueryRuleClick.bind(this, supports); } const supportsElement = new ElementsComponents.CSSQuery.CSSQuery(); supportsElement.data = { queryPrefix: '@supports', queryText: supports.text, onQueryTextClick, }; return supportsElement; } createNestingElement(nestingSelector) { if (!nestingSelector) { return; } const nestingElement = document.createElement('div'); nestingElement.textContent = nestingSelector; return nestingElement; } async addContainerForContainerQuery(containerQuery) { const container = await containerQuery.getContainerForNode(this.matchedStyles.node().id); if (!container) { return; } const containerElement = new ElementsComponents.QueryContainer.QueryContainer(); containerElement.data = { container: ElementsComponents.Helper.legacyNodeToElementsComponentsNode(container.containerNode), queryName: containerQuery.name, onContainerLinkClick: (event) => { event.preventDefault(); void ElementsPanel.instance().revealAndSelectNode(container.containerNode, true, true); void container.containerNode.scrollIntoView(); }, }; containerElement.addEventListener('queriedsizerequested', async () => { const details = await container.getContainerSizeDetails(); if (details) { containerElement.updateContainerQueriedSizeDetails(details); } }); this.ancestorRuleListElement.prepend(containerElement); } updateQueryList() { this.ancestorRuleListElement.removeChildren(); if (this.styleInternal.parentRule && this.styleInternal.parentRule instanceof SDK.CSSRule.CSSStyleRule) { this.createAncestorRules(this.styleInternal.parentRule); } } isPropertyInherited(propertyName) { if (this.matchedStyles.isInherited(this.styleInternal)) { // While rendering inherited stylesheet, reverse meaning of this property. // Render truly inherited properties with black, i.e. return them as non-inherited. return !SDK.CSSMetadata.cssMetadata().isPropertyInherited(propertyName); } return false; } nextEditableSibling() { let curSection = this; do { curSection = curSection.nextSibling(); } while (curSection && !curSection.editable); if (!curSection) { curSection = this.firstSibling(); while (curSection && !curSection.editable) { curSection = curSection.nextSibling(); } } return (curSection && curSection.editable) ? curSection : null; } previousEditableSibling() { let curSection = this; do { curSection = curSection.previousSibling(); } while (curSection && !curSection.editable); if (!curSection) { curSection = this.lastSibling(); while (curSection && !curSection.editable) { curSection = curSection.previousSibling(); } } return (curSection && curSection.editable) ? curSection : null; } refreshUpdate(editedTreeElement) { this.parentPane.refreshUpdate(this, editedTreeElement); } updateVarFunctions(editedTreeElement) { let child = this.propertiesTreeOutline.firstChild(); while (child) { if (child !== editedTreeElement && child instanceof StylePropertyTreeElement) { child.updateTitleIfComputedValueChanged(); } child = child.traverseNextTreeElement(false /* skipUnrevealed */, null /* stayWithin */, true /* dontPopulate */); } } update(full) { this.selectorElement.textContent = this.headerText(); this.markSelectorMatches(); if (full) { this.onpopulate(); } else { let child = this.propertiesTreeOutline.firstChild(); while (child && child instanceof StylePropertyTreeElement) { child.setOverloaded(this.isPropertyOverloaded(child.property)); child = child.traverseNextTreeElement(false /* skipUnrevealed */, null /* stayWithin */, true /* dontPopulate */); } } } showAllItems(event) { if (event) { event.consume(); } if (this.forceShowAll) { return; } this.forceShowAll = true; this.onpopulate(); } onpopulate() { this.parentPane.setActiveProperty(null); this.nextEditorTriggerButtonIdx = 1; this.propertiesTreeOutline.removeChildren(); const style = this.styleInternal; let count = 0; const properties = style.leadingProperties(); const maxProperties = StylePropertiesSection.MaxProperties + properties.length - this.originalPropertiesCount; for (const property of properties) { if (!this.forceShowAll && count >= maxProperties) { break; } count++; const isShorthand = property.getLonghandProperties().length > 0; const inherited = this.isPropertyInherited(property.name); const overloaded = this.isPropertyOverloaded(property); if (style.parentRule && style.parentRule.isUserAgent() && inherited) { continue; } const item = new StylePropertyTreeElement({ stylesPane: this.parentPane, matchedStyles: this.matchedStyles, property, isShorthand, inherited, overloaded, newProperty: false, }); item.setComputedStyles(this.computedStyles); item.setParentsComputedStyles(this.parentsComputedStyles); this.propertiesTreeOutline.appendChild(item); } if (count < properties.length) { this.showAllButton.classList.remove('hidden'); this.showAllButton.textContent = i18nString(UIStrings.showAllPropertiesSMore, { PH1: properties.length - count }); } else { this.showAllButton.classList.add('hidden'); } } isPropertyOverloaded(property) { return this.matchedStyles.propertyState(property) === SDK.CSSMatchedStyles.PropertyState.Overloaded; } updateFilter() { let hasMatchingChild = false; this.showAllItems(); for (const child of this.propertiesTreeOutline.rootElement().children()) { if (child instanceof StylePropertyTreeElement) { const childHasMatches = child.updateFilter(); hasMatchingChild = hasMatchingChild || childHasMatches; } } const regex = this.parentPane.filterRegex(); const hideRule = !hasMatchingChild && regex !== null && !regex.test(this.element.deepTextContent()); this.isHiddenInternal = hideRule; this.element.classList.toggle('hidden', hideRule); if (!hideRule && this.styleInternal.parentRule) { this.markSelectorHighlights(); } return !hideRule; } isHidden() { return this.isHiddenInternal; } markSelectorMatches() { const rule = this.styleInternal.parentRule; if (!rule || !(rule instanceof SDK.CSSRule.CSSStyleRule)) { return; } const matchingSelectorIndexes = this.matchedStyles.getMatchingSelectors(rule); const matchingSelectors = new Array(rule.selectors.length).fill(false); for (const matchingIndex of matchingSelectorIndexes) { matchingSelectors[matchingIndex] = true; } if (this.parentPane.isEditingStyle) { return; } const fragment = StylePropertiesSection.renderSelectors(rule.selectors, matchingSelectors, this.elementToSelectorIndex); this.selectorElement.removeChildren(); this.selectorElement.appendChild(fragment); this.markSelectorHighlights(); } static getSpecificityStoredForNodeElement(element) { return StylePropertiesSection.#nodeElementToSpecificity.get(element); } static renderSelectors(selectors, matchingSelectors, elementToSelectorIndex) { const fragment = document.createDocumentFragment(); for (const [i, selector] of selectors.entries()) { if (i) { UI.UIUtils.createTextChild(fragment, ', '); } const selectorElement = document.createElement('span'); selectorElement.classList.add('simple-selector'); selectorElement.classList.toggle('selector-matches', matchingSelectors[i]); if (selector.specificity) { StylePropertiesSection.#nodeElementToSpecificity.set(selectorElement, selector.specificity); } elementToSelectorIndex.set(selectorElement, i); selectorElement.textContent = selectors[i].text; fragment.append(selectorElement); } return fragment; } markSelectorHighlights() { const selectors = this.selectorElement.getElementsByClassName('simple-selector'); const regex = this.parentPane.filterRegex(); for (let i = 0; i < selectors.length; ++i) { const selectorMatchesFilter = regex !== null && regex.test(selectors[i].textContent || ''); selectors[i].classList.toggle('filter-match', selectorMatchesFilter); } } addNewBlankProperty(index = this.propertiesTreeOutline.rootElement().childCount()) { const property = this.styleInternal.newBlankProperty(index); const item = new StylePropertyTreeElement({ stylesPane: this.parentPane, matchedStyles: this.matchedStyles, property, isShorthand: false, inherited: false, overloaded: false, newProperty: true, }); this.propertiesTreeOutline.insertChild(item, property.index); return item; } handleEmptySpaceMouseDown() { this.willCauseCancelEditing = this.parentPane.isEditingStyle; this.selectedSinceMouseDown = false; } handleEmptySpaceClick(event) { // `this.willCauseCancelEditing` is a hacky way to understand whether we should // create a new property or not on empty space click. // For empty space clicks, the order of events are: // when there isn't an edit operation going on: // * empty space mousedown -> empty space click // when there is an edit operation going on: // * empty space mousedown -> text prompt blur -> empty space click // text prompt blur sets the `isEditingStyle` to be `false` in parent pane. // If we check `isEditingStyle` inside empty space click handler, it will // always say `false` and will always cause a new blank property to be added. // Because of this, we're checking and saving whether there is an ongoing // edit operation inside empty space mousedown handler. if (!this.editable || this.element.hasSelection() || this.willCauseCancelEditing || this.selectedSinceMouseDown) { return; } const target = event.target; if (target.classList.contains('header') || this.element.classList.contains('read-only') || target.enclosingNodeOrSelfWithClass('ancestor-rule-list')) { event.consume(); return; } const deepTarget = UI.UIUtils.deepElementFromEvent(event); const treeElement = deepTarget && UI.TreeOutline.TreeElement.getTreeElementBylistItemNode(deepTarget); if (treeElement && treeElement instanceof StylePropertyTreeElement) { this.addNewBlankProperty(treeElement.property.index + 1).startEditing(); } else if (target.classList.contains('selector-container') || target.classList.contains('styles-section-subtitle')) { this.addNewBlankProperty(0).startEditing(); } else { this.addNewBlankProperty().startEditing(); } event.consume(true); } handleQueryRuleClick(query, event) { const element = event.currentTarget; if (UI.UIUtils.isBeingEdited(element)) { return; } if (UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event) && this.navigable) { const location = query.rawLocation(); if (!location) { event.consume(true); return; } const uiLocation = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().rawLocationToUILocation(location); if (uiLocation) { void Common.Revealer.reveal(uiLocation); } event.consume(true); return; } if (!this.editable) { return; } const config = new UI.InplaceEditor.Config(this.editingMediaCommitted.bind(this, query), this.editingMediaCancelled.bind(this, element), undefined, this.editingMediaBlurHandler.bind(this)); UI.InplaceEditor.InplaceEditor.startEditing(element, config); const selection = element.getComponentSelection(); if (selection) { selection.selectAllChildren(element); } this.parentPane.setEditingStyle(true); const parentMediaElement = element.enclosingNodeOrSelfWithClass('query'); parentMediaElement.classList.add('editing-query'); event.consume(true); } editingMediaFinished(element) { this.parentPane.setEditingStyle(false); const parentMediaElement = element.enclosingNodeOrSelfWithClass('query'); parentMediaElement.classList.remove('editing-query'); } editingMediaCancelled(element) { this.editingMediaFinished(element); // Mark the selectors in group if necessary. // This is overridden by BlankStylePropertiesSection. this.markSelectorMatches(); const selection = element.getComponentSelection(); if (selection) { selection.collapse(element, 0); } } editingMediaBlurHandler() { return true; } async editingMediaCommitted(query, element, newContent, _oldContent, _context, _moveDirection) { this.parentPane.setEditingStyle(false); this.editingMediaFinished(element); if (newContent) { newContent = newContent.trim(); } // This gets deleted in finishOperation(), which is called both on success and failure. this.parentPane.setUserOperation(true); const cssModel = this.parentPane.cssModel(); if (cssModel && query.styleSheetId) { const range = query.range; let success = false; if (query instanceof SDK.CSSContainerQuery.CSSContainerQuery) { success = await cssModel.setContainerQueryText(query.styleSheetId, range, newContent); } else if (query instanceof SDK.CSSSupports.CSSSupports) { success = await cssModel.setSupportsText(query.styleSheetId, range, newContent); } else if (query instanceof SDK.CSSScope.CSSScope) { success = await cssModel.setScopeText(query.styleSheetId, range, newContent); } else { success = await cssModel.setMediaText(query.styleSheetId, range, newContent); } if (success) { this.matchedStyles.resetActiveProperties(); this.parentPane.refreshUpdate(this); } this.parentPane.setUserOperation(false); this.editingMediaTextCommittedForTest(); } } editingMediaTextCommittedForTest() { } handleSelectorClick(event) { const target = event.target; if (!target) { return; } if (UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event) && this.navigable && target.classList.contains('simple-selector')) { const selectorIndex = this.elementToSelectorIndex.get(target); if (selectorIndex) { this.navigateToSelectorSource(selectorIndex, true); } event.consume(true); return; } if (this.element.hasSelection()) { return; } this.startEditingAtFirstPosition(); event.consume(true); } handleContextMenuEvent(event) { const target = event.target; if (!target) { return; } const contextMenu = new UI.ContextMenu.ContextMenu(event); contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copySelector), () => { const selectorText = this.headerText(); Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(selectorText); Host.userMetrics.styleTextCopied(Host.UserMetrics.StyleTextCopied.SelectorViaContextMenu); }); contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyRule), () => { const ruleText = StylesSidebarPane.formatLeadingProperties(this).ruleText; Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(ruleText); Host.userMetrics.styleTextCopied(Host.UserMetrics.StyleTextCopied.RuleViaContextMenu); }); contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyAllDeclarations), () => { const allDeclarationText = StylesSidebarPane.formatLeadingProperties(this).allDeclarationText; Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(allDeclarationText); Host.userMetrics.styleTextCopied(Host.UserMetrics.StyleTextCopied.AllDeclarationsViaContextMenu); }); // TODO(changhaohan): conditionally add this item only when there are changes to copy contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyAllCSSChanges), async () => { const allCh