@quick-game/cli
Version:
Command line interface for rapid qg development
1,149 lines • 63.9 kB
JavaScript
// 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