[go: nahoru, domu]

blob: 07eeb84b867dde65f6ec12f2f21406e2e1465592 [file] [log] [blame]
// 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 Protocol from '../../generated/protocol.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 type {Context} from './StylePropertyTreeElement.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: StylesSidebarPane;
// 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 {
private currentToolbarPane: UI.Widget.Widget|null;
private animatedToolbarPane: UI.Widget.Widget|null;
private pendingWidget: UI.Widget.Widget|null;
private pendingWidgetToggle: UI.Toolbar.ToolbarToggle|null;
private toolbar: UI.Toolbar.Toolbar|null;
private toolbarPaneElement: HTMLElement;
private noMatchesElement: HTMLElement;
private sectionsContainer: HTMLElement;
sectionByElement: WeakMap<Node, StylePropertiesSection>;
private readonly swatchPopoverHelperInternal: InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper;
readonly linkifier: Components.Linkifier.Linkifier;
private readonly decorator: StylePropertyHighlighter;
private lastRevealedProperty: SDK.CSSProperty.CSSProperty|null;
private userOperation: boolean;
isEditingStyle: boolean;
private filterRegexInternal: RegExp|null;
private isActivePropertyHighlighted: boolean;
private initialUpdateCompleted: boolean;
hasMatchedStyles: boolean;
private sectionBlocks: SectionBlock[];
private idleCallbackManager: IdleCallbackManager|null;
private needsForceUpdate: boolean;
private readonly resizeThrottler: Common.Throttler.Throttler;
private readonly imagePreviewPopover: ImagePreviewPopover;
activeCSSAngle: InlineEditor.CSSAngle.CSSAngle|null;
static instance(): StylesSidebarPane {
if (!_stylesSidebarPaneInstance) {
_stylesSidebarPaneInstance = new StylesSidebarPane();
}
return _stylesSidebarPaneInstance;
}
private 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.computedStyleModelInternal = 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.swatchPopoverHelperInternal = new InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper();
this.swatchPopoverHelperInternal.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.filterRegexInternal = 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(): InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper {
return this.swatchPopoverHelperInternal;
}
setUserOperation(userOperation: boolean): void {
this.userOperation = userOperation;
}
static createExclamationMark(property: SDK.CSSProperty.CSSProperty, title: string|null): Element {
const exclamationElement = (document.createElement('span', {is: 'dt-icon-label'}) as UI.UIUtils.DevToolsIconLabel);
exclamationElement.className = 'exclamation-mark';
if (!StylesSidebarPane.ignoreErrorsForProperty(property)) {
exclamationElement.type = 'smallicon-warning';
}
let invalidMessage: string|Common.UIString.LocalizedString;
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: SDK.CSSProperty.CSSProperty): boolean {
function hasUnknownVendorPrefix(string: string): boolean {
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: string, container: Element, filterCallback: (arg0: RegExp|null) => void): Element {
const input = document.createElement('input');
input.type = 'search';
input.classList.add('custom-search-input');
input.placeholder = placeholder;
function searchHandler(): void {
const regex = input.value ? new RegExp(Platform.StringUtilities.escapeForRegExp(input.value), 'i') : null;
filterCallback(regex);
}
input.addEventListener('input', searchHandler, false);
function keydownHandler(event: Event): void {
const keyboardEvent = (event as KeyboardEvent);
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: StylePropertiesSection): {
allDeclarationText: string,
ruleText: string,
} {
const selectorText = section.headerText();
const indent = Common.Settings.Settings.instance().moduleSetting('textEditorIndent').get();
const style = section.style();
const lines: string[] = [];
// 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: string = lines.join('\n');
const ruleText: string = `${selectorText} {\n${allDeclarationText}\n}`;
return {
allDeclarationText,
ruleText,
};
}
revealProperty(cssProperty: SDK.CSSProperty.CSSProperty): void {
this.decorator.highlightProperty(cssProperty);
this.lastRevealedProperty = cssProperty;
this.update();
}
jumpToProperty(propertyName: string): void {
this.decorator.findAndHighlightPropertyName(propertyName);
}
forceUpdate(): void {
this.needsForceUpdate = true;
this.swatchPopoverHelperInternal.hide();
this.resetCache();
this.update();
}
private sectionsContainerKeyDown(event: Event): void {
const activeElement = this.sectionsContainer.ownerDocument.deepActiveElement();
if (!activeElement) {
return;
}
const section = this.sectionByElement.get(activeElement);
if (!section) {
return;
}
let sectionToFocus: (StylePropertiesSection|null)|null = null;
let willIterateForward = false;
switch (/** @type {!KeyboardEvent} */ (event as KeyboardEvent).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.filterRegexInternal) {
sectionToFocus = sectionToFocus.findCurrentOrNextVisible(/* willIterateForward= */ willIterateForward);
}
if (sectionToFocus) {
sectionToFocus.element.focus();
event.consume(true);
}
}
private sectionsContainerFocusChanged(): void {
this.resetFocus();
}
resetFocus(): void {
// 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: Event): void {
const cssModel = this.cssModel();
if (!cssModel) {
return;
}
const headers = cssModel.styleSheetHeaders().filter(styleSheetResourceHeader);
const contextMenuDescriptors: {
text: string,
handler: () => Promise<void>,
}[] = [];
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: {
text: string,
handler: () => Promise<void>,
},
descriptor2: {
text: string,
handler: () => Promise<void>,
}): number {
return Platform.StringUtilities.naturalOrderComparator(descriptor1.text, descriptor2.text);
}
function styleSheetResourceHeader(header: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader): boolean {
return !header.isViaInspector() && !header.isInline && Boolean(header.resourceURL());
}
}
private onFilterChanged(regex: RegExp|null): void {
this.filterRegexInternal = regex;
this.updateFilter();
this.resetFocus();
}
refreshUpdate(editedSection: StylePropertiesSection, editedTreeElement?: StylePropertyTreeElement): void {
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.filterRegexInternal) {
this.updateFilter();
}
this.nodeStylesUpdatedForTest(node, false);
}
async doUpdate(): Promise<void> {
if (!this.initialUpdateCompleted) {
setTimeout(() => {
if (!this.initialUpdateCompleted) {
// the spinner will get automatically removed when innerRebuildUpdate is called
this.sectionsContainer.createChild('span', 'spinner');
}
}, 200 /* only spin for loading time > 200ms to avoid unpleasant render flashes */);
}
const matchedStyles = await this.fetchMatchedCascade();
await this.innerRebuildUpdate(matchedStyles);
if (!this.initialUpdateCompleted) {
this.initialUpdateCompleted = true;
this.dispatchEventToListeners(Events.InitialUpdateCompleted);
}
this.dispatchEventToListeners(Events.StylesUpdateCompleted, {hasMatchedStyles: this.hasMatchedStyles});
}
onResize(): void {
this.resizeThrottler.schedule(this.innerResize.bind(this));
}
private innerResize(): Promise<void> {
const width = this.contentElement.getBoundingClientRect().width + 'px';
this.allSections().forEach(section => {
section.propertiesTreeOutline.element.style.width = width;
});
return Promise.resolve();
}
private resetCache(): void {
const cssModel = this.cssModel();
if (cssModel) {
cssModel.discardCachedMatchedCascade();
}
}
private fetchMatchedCascade(): Promise<SDK.CSSMatchedStyles.CSSMatchedStyles|null> {
const node = this.node();
if (!node || !this.cssModel()) {
return Promise.resolve((null as SDK.CSSMatchedStyles.CSSMatchedStyles | null));
}
const cssModel = this.cssModel();
if (!cssModel) {
return Promise.resolve(null);
}
return cssModel.cachedMatchedCascadeForNode(node).then(validateStyles.bind(this));
function validateStyles(this: StylesSidebarPane, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles|null):
SDK.CSSMatchedStyles.CSSMatchedStyles|null {
return matchedStyles && matchedStyles.node() === this.node() ? matchedStyles : null;
}
}
setEditingStyle(editing: boolean, _treeElement?: StylePropertyTreeElement): void {
if (this.isEditingStyle === editing) {
return;
}
this.contentElement.classList.toggle('is-editing-style', editing);
this.isEditingStyle = editing;
this.setActiveProperty(null);
}
setActiveProperty(treeElement: StylePropertyTreeElement|null): void {
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() as SDK.DOMModel.DOMNode), selectorList}, mode);
this.isActivePropertyHighlighted = true;
break;
}
}
onCSSModelChanged(event?: Common.EventTarget.EventTargetEvent): void {
const edit = event && event.data ? event.data.edit as SDK.CSSModel.Edit | null : null;
if (edit) {
for (const section of this.allSections()) {
section.styleSheetEdited(edit);
}
return;
}
if (this.userOperation || this.isEditingStyle) {
return;
}
this.resetCache();
this.update();
}
focusedSectionIndex(): number {
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: number, propertyIndex: number): void {
const section = this.allSections()[sectionIndex];
if (section) {
const element = (section.closestPropertyForEditing(propertyIndex) as StylePropertyTreeElement | null);
if (!element) {
section.element.focus();
return;
}
element.startEditing();
}
}
private async innerRebuildUpdate(matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles|null): Promise<void> {
// 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 as SDK.CSSMatchedStyles.CSSMatchedStyles));
// 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: HTMLDivElement|null = 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.filterRegexInternal) {
this.updateFilter();
} else {
this.noMatchesElement.classList.toggle('hidden', this.sectionBlocks.length > 0);
}
this.nodeStylesUpdatedForTest((node as SDK.DOMModel.DOMNode), true);
if (this.lastRevealedProperty) {
this.decorator.highlightProperty(this.lastRevealedProperty);
this.lastRevealedProperty = null;
}
// Record the elements tool load time after the sidepane has loaded.
Host.userMetrics.panelLoaded('elements', 'DevTools.Launch.Elements');
this.dispatchEventToListeners(Events.StylesUpdateCompleted, {hasStyle: true});
}
private nodeStylesUpdatedForTest(_node: SDK.DOMModel.DOMNode, _rebuild: boolean): void {
// For sniffing in tests.
}
private async rebuildSectionsForMatchedStyleRules(matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles):
Promise<SectionBlock[]> {
if (this.idleCallbackManager) {
this.idleCallbackManager.discard();
}
this.idleCallbackManager = new IdleCallbackManager();
const blocks = [new SectionBlock(null)];
let lastParentNode: SDK.DOMModel.DOMNode|null = 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: Protocol.DOM.PseudoType[] = [];
const keys = matchedStyles.pseudoTypes();
if (keys.delete(Protocol.DOM.PseudoType.Before)) {
pseudoTypes.push(Protocol.DOM.PseudoType.Before);
}
pseudoTypes = pseudoTypes.concat([...keys].sort());
for (const pseudoType of pseudoTypes) {
const block = SectionBlock.createPseudoTypeBlock(pseudoType);
for (const style of matchedStyles.pseudoStyles(pseudoType)) {
this.idleCallbackManager.schedule(() => {
const section = new StylePropertiesSection(this, matchedStyles, style);
block.sections.push(section);
});
}
blocks.push(block);
}
for (const keyframesRule of matchedStyles.keyframes()) {
const block = SectionBlock.createKeyframesBlock(keyframesRule.name().text);
for (const keyframe of keyframesRule.keyframes()) {
this.idleCallbackManager.schedule(() => {
block.sections.push(new KeyframePropertiesSection(this, matchedStyles, keyframe.style));
});
}
blocks.push(block);
}
await this.idleCallbackManager.awaitDone();
return blocks;
}
async createNewRuleInViaInspectorStyleSheet(): Promise<void> {
const cssModel = this.cssModel();
const node = this.node();
if (!cssModel || !node) {
return;
}
this.setUserOperation(true);
const styleSheetHeader = await cssModel.requestViaInspectorStylesheet((node as SDK.DOMModel.DOMNode));
this.setUserOperation(false);
await this.createNewRuleInStyleSheet(styleSheetHeader);
}
private async createNewRuleInStyleSheet(styleSheetHeader: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader|
null): Promise<void> {
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: StylePropertiesSection, styleSheetId: Protocol.CSS.StyleSheetId,
ruleLocation: TextUtils.TextRange.TextRange): void {
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: StylePropertiesSection): void {
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(): RegExp|null {
return this.filterRegexInternal;
}
private updateFilter(): void {
let hasAnyVisibleBlock = false;
for (const block of this.sectionBlocks) {
hasAnyVisibleBlock = block.updateFilter() || hasAnyVisibleBlock;
}
this.noMatchesElement.classList.toggle('hidden', Boolean(hasAnyVisibleBlock));
}
willHide(): void {
this.hideAllPopovers();
super.willHide();
}
hideAllPopovers(): void {
this.swatchPopoverHelperInternal.hide();
this.imagePreviewPopover.hide();
if (this.activeCSSAngle) {
this.activeCSSAngle.minify();
this.activeCSSAngle = null;
}
}
allSections(): StylePropertiesSection[] {
let sections: StylePropertiesSection[] = [];
for (const block of this.sectionBlocks) {
sections = sections.concat(block.sections);
}
return sections;
}
private clipboardCopy(_event: Event): void {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.StyleRuleCopied);
}
private createStylesSidebarToolbar(): HTMLElement {
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') as HTMLElement);
return toolbarPaneContent;
}
showToolbarPane(widget: UI.Widget.Widget|null, toggle: UI.Toolbar.ToolbarToggle|null): void {
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: UI.Toolbar.ToolbarItem): void {
if (this.toolbar) {
this.toolbar.appendToolbarItem(item);
}
}
private startToolbarPaneAnimation(widget: UI.Widget.Widget|null): void {
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: StylesSidebarPane): void {
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;
}
}
}
}
export const enum Events {
InitialUpdateCompleted = 'InitialUpdateCompleted',
StylesUpdateCompleted = 'StylesUpdateCompleted',
}
// 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 {
private readonly titleElementInternal: Element|null;
sections: StylePropertiesSection[];
constructor(titleElement: Element|null) {
this.titleElementInternal = titleElement;
this.sections = [];
}
static createPseudoTypeBlock(pseudoType: Protocol.DOM.PseudoType): SectionBlock {
const separatorElement = document.createElement('div');
separatorElement.className = 'sidebar-separator';
separatorElement.textContent = i18nString(UIStrings.pseudoSElement, {PH1: pseudoType});
return new SectionBlock(separatorElement);
}
static createKeyframesBlock(keyframesName: string): SectionBlock {
const separatorElement = document.createElement('div');
separatorElement.className = 'sidebar-separator';
separatorElement.textContent = `@keyframes ${keyframesName}`;
return new SectionBlock(separatorElement);
}
static async createInheritedNodeBlock(node: SDK.DOMModel.DOMNode): Promise<SectionBlock> {
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(): boolean {
let hasAnyVisibleSection = false;
for (const section of this.sections) {
hasAnyVisibleSection = section.updateFilter() || hasAnyVisibleSection;
}
if (this.titleElementInternal) {
this.titleElementInternal.classList.toggle('hidden', !hasAnyVisibleSection);
}
return Boolean(hasAnyVisibleSection);
}
titleElement(): Element|null {
return this.titleElementInternal;
}
}
export class IdleCallbackManager {
private discarded: boolean;
private readonly promises: Promise<void>[];
constructor() {
this.discarded = false;
this.promises = [];
}
discard(): void {
this.discarded = true;
}
schedule(fn: () => void, timeout: number = 100): void {
if (this.discarded) {
return;
}
this.promises.push(new Promise((resolve, reject) => {
const run = (): void => {
try {
fn();
resolve();
} catch (err) {
reject(err);
}
};
window.requestIdleCallback(() => {
if (this.discarded) {
return resolve();
}
run();
}, {timeout});
}));
}
awaitDone(): Promise<void[]> {
return Promise.all(this.promises);
}
}
export class StylePropertiesSection {
protected parentPane: StylesSidebarPane;
styleInternal: SDK.CSSStyleDeclaration.CSSStyleDeclaration;
readonly matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles;
editable: boolean;
private hoverTimer: number|null;
private willCauseCancelEditing: boolean;
private forceShowAll: boolean;
private readonly originalPropertiesCount: number;
element: HTMLDivElement;
private readonly innerElement: HTMLElement;
private readonly titleElement: HTMLElement;
propertiesTreeOutline: UI.TreeOutline.TreeOutlineInShadow;
private showAllButton: HTMLButtonElement;
protected selectorElement: HTMLSpanElement;
private readonly newStyleRuleToolbar: UI.Toolbar.Toolbar|undefined;
private readonly fontEditorToolbar: UI.Toolbar.Toolbar|undefined;
private readonly fontEditorSectionManager: FontEditorSectionManager|undefined;
private readonly fontEditorButton: UI.Toolbar.ToolbarButton|undefined;
private selectedSinceMouseDown: boolean;
private readonly elementToSelectorIndex: WeakMap<Element, number>;
navigable: boolean|null|undefined;
protected readonly selectorRefElement: HTMLElement;
private readonly selectorContainer: HTMLDivElement;
private readonly fontPopoverIcon: FontEditorSectionManager|null;
private hoverableSelectorsMode: boolean;
private isHiddenInternal: boolean;
private queryListElement: HTMLElement;
constructor(
parentPane: StylesSidebarPane, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles,
style: SDK.CSSStyleDeclaration.CSSStyleDeclaration) {
this.parentPane = parentPane;
this.styleInternal = 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.styleInternal.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.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.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.isHiddenInternal = false;
this.markSelectorMatches();
this.onpopulate();
}
registerFontProperty(treeElement: StylePropertyTreeElement): void {
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(): void {
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: SDK.CSSMatchedStyles.CSSMatchedStyles, linkifier: Components.Linkifier.Linkifier,
rule: SDK.CSSRule.CSSRule|null): Node {
if (!rule) {
return document.createTextNode('');
}
const ruleLocation = this.getRuleLocationFromCSSRule(rule);
const header = rule.styleSheetId ? matchedStyles.cssModel().styleSheetHeaderForId(rule.styleSheetId) : null;
function linkifyRuleLocation(): Node|null {
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: string): Node|null {
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('');
}
private static getRuleLocationFromCSSRule(rule: SDK.CSSRule.CSSRule): TextUtils.TextRange.TextRange|null|undefined {
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: SDK.CSSMatchedStyles.CSSMatchedStyles, rule: SDK.CSSRule.CSSRule|null): void {
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);
}
}
protected static linkifyRuleLocation(
cssModel: SDK.CSSModel.CSSModel, linkifier: Components.Linkifier.Linkifier,
styleSheetId: Protocol.CSS.StyleSheetId, ruleLocation: TextUtils.TextRange.TextRange): Node {
const matchingSelectorLocation = this.getCSSSelectorLocation(cssModel, styleSheetId, ruleLocation);
return linkifier.linkifyCSSLocation(matchingSelectorLocation);
}
private static getCSSSelectorLocation(
cssModel: SDK.CSSModel.CSSModel, styleSheetId: Protocol.CSS.StyleSheetId,
ruleLocation: TextUtils.TextRange.TextRange): SDK.CSSModel.CSSLocation {
const styleSheetHeader =
(cssModel.styleSheetHeaderForId(styleSheetId) as SDK.CSSStyleSheetHeader.CSSStyleSheetHeader);
const lineNumber = styleSheetHeader.lineNumberInSource(ruleLocation.startLine);
const columnNumber = styleSheetHeader.columnNumberInSource(ruleLocation.startLine, ruleLocation.startColumn);
return new SDK.CSSModel.CSSLocation(styleSheetHeader, lineNumber, columnNumber);
}
private getFocused(): HTMLElement|null {
return (this.propertiesTreeOutline._shadowRoot.activeElement as HTMLElement) || null;
}
private focusNext(element: HTMLElement): void {
// 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;
}
}
private ruleNavigation(keyboardEvent: KeyboardEvent): void {
if (keyboardEvent.altKey || keyboardEvent.ctrlKey || keyboardEvent.metaKey || keyboardEvent.shiftKey) {
return;
}
const focused = this.getFocused();
let focusNext: HTMLElement|null = null;
const focusable =
Array.from((this.propertiesTreeOutline._shadowRoot.querySelectorAll('[tabindex]') as NodeListOf<HTMLElement>));
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);
}
}
private onKeyDown(event: Event): void {
const keyboardEvent = (event as KeyboardEvent);
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;
}
}
private setSectionHovered(isHovered: boolean): void {
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();
}
}
private onMouseLeave(_event: Event): void {
this.setSectionHovered(false);
this.parentPane.setActiveProperty(null);
}
private onMouseMove(event: MouseEvent): void {
const hasCtrlOrMeta = UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event);
this.setSectionHovered(hasCtrlOrMeta);
const treeElement = this.propertiesTreeOutline.treeElementFromEvent(event);
if (treeElement instanceof StylePropertyTreeElement) {
this.parentPane.setActiveProperty((treeElement as StylePropertyTreeElement));
} else {
this.parentPane.setActiveProperty(null);
}
const selection = this.element.getComponentSelection();
if (!this.selectedSinceMouseDown && selection && selection.toString()) {
this.selectedSinceMouseDown = true;
}
}
private onFontEditorButtonClicked(): void {
if (this.fontEditorSectionManager && this.fontEditorButton) {
Host.userMetrics.cssEditorOpened('fontEditor');
this.fontEditorSectionManager.showPopover(this.fontEditorButton.element, this.parentPane);
}
}
style(): SDK.CSSStyleDeclaration.CSSStyleDeclaration {
return this.styleInternal;
}
headerText(): string {
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 '';
}
private onMouseOutSelector(): void {
if (this.hoverTimer) {
clearTimeout(this.hoverTimer);
}
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
}
private onMouseEnterSelector(): void {
if (this.hoverTimer) {
clearTimeout(this.hoverTimer);
}
this.hoverTimer = setTimeout(this.highlight.bind(this), 300);
}
highlight(mode: string|undefined = 'all'): void {
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(): StylePropertiesSection|null {
const parent = this.element.parentElement;
if (!parent) {
return null;
}
let childElement: (ChildNode|null) = parent.firstChild;
while (childElement) {
const childSection = this.parentPane.sectionByElement.get(childElement);
if (childSection) {
return childSection;
}
childElement = childElement.nextSibling;
}
return null;
}
findCurrentOrNextVisible(willIterateForward: boolean, originalSection?: StylePropertiesSection):
StylePropertiesSection|null {
if (!this.isHidden()) {
return this;
}
if (this === originalSection) {
return null;
}
if (!originalSection) {
originalSection = this;
}
let visibleSibling: (StylePropertiesSection|null)|null = 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(): StylePropertiesSection|null {
const parent = this.element.parentElement;
if (!parent) {
return null;
}
let childElement: (ChildNode|null) = parent.lastChild;
while (childElement) {
const childSection = this.parentPane.sectionByElement.get(childElement);
if (childSection) {
return childSection;
}
childElement = childElement.previousSibling;
}
return null;
}
nextSibling(): StylePropertiesSection|undefined {
let curElement: (ChildNode|null)|HTMLDivElement = this.element;
do {
curElement = curElement.nextSibling;
} while (curElement && !this.parentPane.sectionByElement.has(curElement));
if (curElement) {
return this.parentPane.sectionByElement.get(curElement);
}
return;
}
previousSibling(): StylePropertiesSection|undefined {
let curElement: (ChildNode|null)|HTMLDivElement = this.element;
do {
curElement = curElement.previousSibling;
} while (curElement && !this.parentPane.sectionByElement.has(curElement));
if (curElement) {
return this.parentPane.sectionByElement.get(curElement);
}
return;
}
private onNewRuleClick(event: Common.EventTarget.EventTargetEvent): void {
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: SDK.CSSModel.Edit): void {
const rule = this.styleInternal.parentRule;
if (rule) {
rule.rebase(edit);
} else {
this.styleInternal.rebase(edit);
}
this.updateQueryList();
this.updateRuleOrigin();
}
protected createMediaList(mediaRules: SDK.CSSMedia.CSSMedia[]): void {
for (let i = mediaRules.length - 1; i >= 0; --i) {
const media = mediaRules[i];
// Don't display trivial non-print media types.
const isMedia = !media.text || !media.text.includes('(') && media.text !== 'print';
if (isMedia) {
continue;
}
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,
};
this.queryListElement.append(mediaQueryElement);
}
}
protected createContainerQueryList(containerQueries: SDK.CSSContainerQuery.CSSContainerQuery[]): void {
for (let i = containerQueries.length - 1; i >= 0; --i) {
const containerQuery = containerQueries[i];
if (!containerQuery.text) {
continue;
}
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,
};
this.queryListElement.append(containerQueryElement);
this.addContainerForContainerQuery(containerQuery);
}
}
private async addContainerForContainerQuery(containerQuery: SDK.CSSContainerQuery.CSSContainerQuery): Promise<void> {
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: (): void => {
ElementsPanel.instance().revealAndSelectNode(container.containerNode, true, true);
container.containerNode.scrollIntoView();
},
};
containerElement.addEventListener('queriedsizerequested', async () => {
const details = await container.getContainerSizeDetails();
if (details) {
containerElement.updateContainerQueriedSizeDetails(details);
}
});
this.queryListElement.prepend(containerElement);
}
private updateQueryList(): void {
this.queryListElement.removeChildren();
if (this.styleInternal.parentRule && this.styleInternal.parentRule instanceof SDK.CSSRule.CSSStyleRule) {
this.createMediaList(this.styleInternal.parentRule.media);
this.createContainerQueryList(this.styleInternal.parentRule.containerQueries);
}
}
isPropertyInherited(propertyName: string): boolean {
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(): StylePropertiesSection|null {
let curSection: (StylePropertiesSection|undefined)|(StylePropertiesSection | null)|this = 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(): StylePropertiesSection|null {
let curSection: (StylePropertiesSection|undefined)|(StylePropertiesSection | null)|this = 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: StylePropertyTreeElement): void {
this.parentPane.refreshUpdate(this, editedTreeElement);
}
updateVarFunctions(editedTreeElement: StylePropertyTreeElement): void {
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: boolean): void {
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 */);
}
}
}
private showAllItems(event?: Event): void {
if (event) {
event.consume();
}
if (this.forceShowAll) {
return;
}
this.forceShowAll = true;
this.onpopulate();
}
onpopulate(): void {
this.parentPane.setActiveProperty(null);
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 = Boolean(style.longhandProperties(property.name).length);
const inherited = this.isPropertyInherited(property.name);
const overloaded = this.isPropertyOverloaded(property);
if (style.parentRule && style.parentRule.isUserAgent() && inherited) {
continue;
}
const item = new StylePropertyTreeElement(
this.parentPane, this.matchedStyles, property, isShorthand, inherited, overloaded, false);
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: SDK.CSSProperty.CSSProperty): boolean {
return this.matchedStyles.propertyState(property) === SDK.CSSMatchedStyles.PropertyState.Overloaded;
}
updateFilter(): boolean {
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(): boolean {
return this.isHiddenInternal;
}
markSelectorMatches(): void {
const rule = this.styleInternal.parentRule;
if (!rule || !(rule instanceof SDK.CSSRule.CSSStyleRule)) {
return;
}
this.queryListElement.classList.toggle('query-matches', this.matchedStyles.mediaMatches(this.styleInternal));
const selectorTexts = rule.selectors.map(selector => selector.text);
const matchingSelectorIndexes = this.matchedStyles.getMatchingSelectors(rule);
const matchingSelectors = (new Array(selectorTexts.length).fill(false) as boolean[]);
for (const matchingIndex of matchingSelectorIndexes) {
matchingSelectors[matchingIndex] = true;
}
if (this.parentPane.isEditingStyle) {
return;
}
const fragment = this.hoverableSelectorsMode ? this.renderHoverableSelectors(selectorTexts, matchingSelectors) :
this.renderSimplifiedSelectors(selectorTexts, matchingSelectors);
this.selectorElement.removeChildren();
this.selectorElement.appendChild(fragment);
this.markSelectorHighlights();
}
private renderHoverableSelectors(selectors: string[], matchingSelectors: boolean[]): DocumentFragment {
const fragment = document.createDocumentFragment();
for (let i = 0; i < selectors.length; ++i) {
if (i) {
UI.UIUtils.createTextChild(fragment, ', ');
}
fragment.appendChild(this.createSelectorElement(selectors[i], matchingSelectors[i], i));
}
return fragment;
}
private createSelectorElement(text: string, isMatching: boolean, navigationIndex?: number): Element {
const element = document.createElement('span');
element.classList.add('simple-selector');
element.classList.toggle('selector-matches', isMatching);
if (typeof navigationIndex === 'number') {
this.elementToSelectorIndex.set(element, navigationIndex);
}
element.textContent = text;
return element;
}
private renderSimplifiedSelectors(selectors: string[], matchingSelectors: boolean[]): DocumentFragment {
const fragment = document.createDocumentFragment();
let currentMatching = false;
let text = '';
for (let i = 0; i < selectors.length; ++i) {
if (currentMatching !== matchingSelectors[i] && text) {
fragment.appendChild(this.createSelectorElement(text, currentMatching));
text = '';
}
currentMatching = matchingSelectors[i];
text += selectors[i] + (i === selectors.length - 1 ? '' : ', ');
}
if (text) {
fragment.appendChild(this.createSelectorElement(text, currentMatching));
}
return fragment;
}
markSelectorHighlights(): void {
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);
}
}
private checkWillCancelEditing(): boolean {
const willCauseCancelEditing = this.willCauseCancelEditing;
this.willCauseCancelEditing = false;
return willCauseCancelEditing;
}
private handleSelectorContainerClick(event: Event): void {
if (this.checkWillCancelEditing() || !this.editable) {
return;
}
if (event.target === this.selectorContainer) {
this.addNewBlankProperty(0).startEditing();
event.consume(true);
}
}
addNewBlankProperty(index: number|undefined = this.propertiesTreeOutline.rootElement().childCount()):
StylePropertyTreeElement {
const property = this.styleInternal.newBlankProperty(index);
const item = new StylePropertyTreeElement(this.parentPane, this.matchedStyles, property, false, false, false, true);
this.propertiesTreeOutline.insertChild(item, property.index);
return item;
}
private handleEmptySpaceMouseDown(): void {
this.willCauseCancelEditing = this.parentPane.isEditingStyle;
this.selectedSinceMouseDown = false;
}
private handleEmptySpaceClick(event: Event): void {
if (!this.editable || this.element.hasSelection() || this.checkWillCancelEditing() || this.selectedSinceMouseDown) {
return;
}
const target = (event.target as Element);
if (target.classList.contains('header') || this.element.classList.contains('read-only') ||
target.enclosingNodeOrSelfWithClass('query')) {
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 {
this.addNewBlankProperty().startEditing();
}
event.consume(true);
}
private handleQueryRuleClick(query: SDK.CSSMedia.CSSMedia|SDK.CSSContainerQuery.CSSContainerQuery, event: Event):
void {
const element = event.currentTarget as Element;
if (UI.UIUtils.isBeingEdited(element)) {
return;
}
if (UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event as MouseEvent) && this.navigable) {
const location = query.rawLocation();
if (!location) {
event.consume(true);
return;
}
const uiLocation = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().rawLocationToUILocation(location);
if (uiLocation) {
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);
}
private editingMediaFinished(element: Element): void {
this.parentPane.setEditingStyle(false);
const parentMediaElement = element.enclosingNodeOrSelfWithClass('query');
parentMediaElement.classList.remove('editing-query');
}
private editingMediaCancelled(element: Element): void {
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);
}
}
private editingMediaBlurHandler(): boolean {
return true;
}
private editingMediaCommitted(
query: SDK.CSSMedia.CSSMedia|SDK.CSSContainerQuery.CSSContainerQuery, element: Element, newContent: string,
_oldContent: string, _context: Context|undefined, _moveDirection: string): void {
this.parentPane.setEditingStyle(false);
this.editingMediaFinished(element);
if (newContent) {
newContent = newContent.trim();
}
function userCallback(this: StylePropertiesSection, success: boolean): void {
if (success) {
this.matchedStyles.resetActiveProperties();
this.parentPane.refreshUpdate(this);
}
this.parentPane.setUserOperation(false);
this.editingMediaTextCommittedForTest();
}
// 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 setQueryText =
query instanceof SDK.CSSMedia.CSSMedia ? cssModel.setMediaText : cssModel.setContainerQueryText;
setQueryText.call(cssModel, query.styleSheetId, (query.range as TextUtils.TextRange.TextRange), newContent)
.then(userCallback.bind(this));
}
}
private editingMediaTextCommittedForTest(): void {
}
private handleSelectorClick(event: Event): void {
const target = (event.target as Element | null);
if (!target) {
return;
}
if (UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey((event as MouseEvent)) && 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);
}
private handleContextMenuEvent(event: Event): void {
const target = (event.target as Element | null);
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);
});
contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyRule), () => {
const ruleText = StylesSidebarPane.formatLeadingProperties(this).ruleText;
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(ruleText);
});
contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyAllDeclarations), () => {
const allDeclarationText = StylesSidebarPane.formatLeadingProperties(this).allDeclarationText;
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(allDeclarationText);
});
contextMenu.show();
}
private navigateToSelectorSource(index: number, focus: boolean): void {
const cssModel = this.parentPane.cssModel();
if (!cssModel) {
return;
}
const rule = (this.styleInternal.parentRule as SDK.CSSRule.CSSStyleRule | null);
if (!rule || rule.styleSheetId === undefined) {
return;
}
const header = cssModel.styleSheetHeaderForId(rule.styleSheetId);
if (!header) {
return;
}
const rawLocation =
new SDK.CSSModel.CSSLocation(header, rule.lineNumberInSource(index), rule.columnNumberInSource(index));
StylePropertiesSection.revealSelectorSource(rawLocation, focus);
}
private static revealSelectorSource(rawLocation: SDK.CSSModel.CSSLocation, focus: boolean): void {
const uiLocation = Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding.instance().rawLocationToUILocation(rawLocation);
if (uiLocation) {
Common.Revealer.reveal(uiLocation, !focus);
}
}
private startEditingAtFirstPosition(): void {
if (!this.editable) {
return;
}
if (!this.styleInternal.parentRule) {
this.moveEditorFromSelector('forward');
return;
}
this.startEditingSelector();
}
startEditingSelector(): void {
const element = this.selectorElement;
if (UI.UIUtils.isBeingEdited(element)) {
return;
}
element.scrollIntoViewIfNeeded(false);
// Reset selector marks in group, and normalize whitespace.
const textContent = element.textContent;
if (textContent !== null) {
element.textContent = textContent.replace(/\s+/g, ' ').trim();
}
const config =
new UI.InplaceEditor.Config(this.editingSelectorCommitted.bind(this), this.editingSelectorCancelled.bind(this));
UI.InplaceEditor.InplaceEditor.startEditing(this.selectorElement, config);
const selection = element.getComponentSelection();
if (selection) {
selection.selectAllChildren(element);
}
this.parentPane.setEditingStyle(true);
if (element.classList.contains('simple-selector')) {
this.navigateToSelectorSource(0, false);
}
}
moveEditorFromSelector(moveDirection: string): void {
this.markSelectorMatches();
if (!moveDirection) {
return;
}
if (moveDirection === 'forward') {
const firstChild = (this.propertiesTreeOutline.firstChild() as StylePropertyTreeElement);
let currentChild: (StylePropertyTreeElement|null)|StylePropertyTreeElement = firstChild;
while (currentChild && currentChild.inherited()) {
const sibling: UI.TreeOutline.TreeElement|null = currentChild.nextSibling;
currentChild = sibling instanceof StylePropertyTreeElement ? sibling : null;
}
if (!currentChild) {
this.addNewBlankProperty().startEditing();
} else {
currentChild.startEditing(currentChild.nameElement);
}
} else {
const previousSection = this.previousEditableSibling();
if (!previousSection) {
return;
}
previousSection.addNewBlankProperty().startEditing();
}
}
editingSelectorCommitted(
element: Element, newContent: string, oldContent: string, context: Context|undefined,
moveDirection: string): void {
this.editingSelectorEnded();
if (newContent) {
newContent = newContent.trim();
}
if (newContent === oldContent) {
// Revert to a trimmed version of the selector if need be.
this.selectorElement.textContent = newContent;
this.moveEditorFromSelector(moveDirection);
return;
}
const rule = this.styleInternal.parentRule;
if (!rule) {
return;
}
function headerTextCommitted(this: StylePropertiesSection): void {
this.parentPane.setUserOperation(false);
this.moveEditorFromSelector(moveDirection);
this.editingSelectorCommittedForTest();
}
// This gets deleted in finishOperationAndMoveEditor(), which is called both on success and failure.
this.parentPane.setUserOperation(true);
this.setHeaderText(rule, newContent).then(headerTextCommitted.bind(this));
}
setHeaderText(rule: SDK.CSSRule.CSSRule, newContent: string): Promise<void> {
function onSelectorsUpdated(
this: StylePropertiesSection, rule: SDK.CSSRule.CSSStyleRule, success: boolean): Promise<void> {
if (!success) {
return Promise.resolve();
}
return this.matchedStyles.recomputeMatchingSelectors(rule).then(updateSourceRanges.bind(this, rule));
}
function updateSourceRanges(this: StylePropertiesSection, rule: SDK.CSSRule.CSSStyleRule): void {
const doesAffectSelectedNode = this.matchedStyles.getMatchingSelectors(rule).length > 0;
this.propertiesTreeOutline.element.classList.toggle('no-affect', !doesAffectSelectedNode);
this.matchedStyles.resetActiveProperties();
this.parentPane.refreshUpdate(this);
}
if (!(rule instanceof SDK.CSSRule.CSSStyleRule)) {
return Promise.resolve();
}
const oldSelectorRange = rule.selectorRange();
if (!oldSelectorRange) {
return Promise.resolve();
}
return rule.setSelectorText(newContent).then(onSelectorsUpdated.bind(this, rule, Boolean(oldSelectorRange)));
}
protected editingSelectorCommittedForTest(): void {
}
protected updateRuleOrigin(): void {
this.selectorRefElement.removeChildren();
this.selectorRefElement.appendChild(StylePropertiesSection.createRuleOriginNode(
this.matchedStyles, this.parentPane.linkifier, this.styleInternal.parentRule));
}
protected editingSelectorEnded(): void {
this.parentPane.setEditingStyle(false);
}
editingSelectorCancelled(): void {
this.editingSelectorEnded();
// Mark the selectors in group if necessary.
// This is overridden by BlankStylePropertiesSection.
this.markSelectorMatches();
}
/**
* A property at or near an index and suitable for subsequent editing.
* Either the last property, if index out-of-upper-bound,
* or property at index, if such a property exists,
* or otherwise, null.
*/
closestPropertyForEditing(propertyIndex: number): UI.TreeOutline.TreeElement|null {
const rootElement = this.propertiesTreeOutline.rootElement();
if (propertyIndex >= rootElement.childCount()) {
return rootElement.lastChild();
}
return rootElement.childAt(propertyIndex);
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/naming-convention
static MaxProperties = 50;
}
export class BlankStylePropertiesSection extends StylePropertiesSection {
private normal: boolean;
private readonly ruleLocation: TextUtils.TextRange.TextRange;
private readonly styleSheetId: Protocol.CSS.StyleSheetId;
constructor(
stylesPane: StylesSidebarPane, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, defaultSelectorText: string,
styleSheetId: Protocol.CSS.StyleSheetId, ruleLocation: TextUtils.TextRange.TextRange,
insertAfterStyle: SDK.CSSStyleDeclaration.CSSStyleDeclaration) {
const cssModel = (stylesPane.cssModel() as SDK.CSSModel.CSSModel);
const rule = SDK.CSSRule.CSSStyleRule.createDummyRule(cssModel, defaultSelectorText);
super(stylesPane, matchedStyles, rule.style);
this.normal = false;
this.ruleLocation = ruleLocation;
this.styleSheetId = styleSheetId;
this.selectorRefElement.removeChildren();
this.selectorRefElement.appendChild(StylePropertiesSection.linkifyRuleLocation(
cssModel, this.parentPane.linkifier, styleSheetId, this.actualRuleLocation()));
if (insertAfterStyle && insertAfterStyle.parentRule &&
insertAfterStyle.parentRule instanceof SDK.CSSRule.CSSStyleRule) {
this.createMediaList(insertAfterStyle.parentRule.media);
this.createContainerQueryList(insertAfterStyle.parentRule.containerQueries);
}
this.element.classList.add('blank-section');
}
private actualRuleLocation(): TextUtils.TextRange.TextRange {
const prefix = this.rulePrefix();
const lines = prefix.split('\n');
const lastLine = lines[lines.length - 1];
const editRange = new TextUtils.TextRange.TextRange(0, 0, lines.length - 1, lastLine ? lastLine.length : 0);
return this.ruleLocation.rebaseAfterTextEdit(TextUtils.TextRange.TextRange.createFromLocation(0, 0), editRange);
}
private rulePrefix(): string {
return this.ruleLocation.startLine === 0 && this.ruleLocation.startColumn === 0 ? '' : '\n\n';
}
get isBlank(): boolean {
return !this.normal;
}
editingSelectorCommitted(
element: Element, newContent: string, oldContent: string, context: Context|undefined,
moveDirection: string): void {
if (!this.isBlank) {
super.editingSelectorCommitted(element, newContent, oldContent, context, moveDirection);
return;
}
function onRuleAdded(this: BlankStylePropertiesSection, newRule: SDK.CSSRule.CSSStyleRule|null): Promise<void> {
if (!newRule) {
this.editingSelectorCancelled();
this.editingSelectorCommittedForTest();
return Promise.resolve();
}
return this.matchedStyles.addNewRule(newRule, this.matchedStyles.node())
.then(onAddedToCascade.bind(this, newRule));
}
function onAddedToCascade(this: BlankStylePropertiesSection, newRule: SDK.CSSRule.CSSStyleRule): void {
const doesSelectorAffectSelectedNode = this.matchedStyles.getMatchingSelectors(newRule).length > 0;
this.makeNormal(newRule);
if (!doesSelectorAffectSelectedNode) {
this.propertiesTreeOutline.element.classList.add('no-affect');
}
this.updateRuleOrigin();
this.parentPane.setUserOperation(false);
this.editingSelectorEnded();
if (this.element.parentElement) // Might have been detached already.
{
this.moveEditorFromSelector(moveDirection);
}
this.markSelectorMatches();
this.editingSelectorCommittedForTest();
}
if (newContent) {
newContent = newContent.trim();
}
this.parentPane.setUserOperation(true);
const cssModel = this.parentPane.cssModel();
const ruleText = this.rulePrefix() + newContent + ' {}';
if (cssModel) {
cssModel.addRule(this.styleSheetId, ruleText, this.ruleLocation).then(onRuleAdded.bind(this));
}
}
editingSelectorCancelled(): void {
this.parentPane.setUserOperation(false);
if (!this.isBlank) {
super.editingSelectorCancelled();
return;
}
this.editingSelectorEnded();
this.parentPane.removeSection(this);
}
private makeNormal(newRule: SDK.CSSRule.CSSRule): void {
this.element.classList.remove('blank-section');
this.styleInternal = newRule.style;
// FIXME: replace this instance by a normal StylePropertiesSection.
this.normal = true;
}
}
export class KeyframePropertiesSection extends StylePropertiesSection {
constructor(
stylesPane: StylesSidebarPane, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles,
style: SDK.CSSStyleDeclaration.CSSStyleDeclaration) {
super(stylesPane, matchedStyles, style);
this.selectorElement.className = 'keyframe-key';
}
headerText(): string {
if (this.styleInternal.parentRule instanceof SDK.CSSRule.CSSKeyframeRule) {
return this.styleInternal.parentRule.key().text;
}
return '';
}
setHeaderText(rule: SDK.CSSRule.CSSRule, newContent: string): Promise<void> {
function updateSourceRanges(this: KeyframePropertiesSection, success: boolean): void {
if (!success) {
return;
}
this.parentPane.refreshUpdate(this);
}
if (!(rule instanceof SDK.CSSRule.CSSKeyframeRule)) {
return Promise.resolve();
}
const oldRange = rule.key().range;
if (!oldRange) {
return Promise.resolve();
}
return rule.setKeyText(newContent).then(updateSourceRanges.bind(this));
}
isPropertyInherited(_propertyName: string): boolean {
return false;
}
isPropertyOverloaded(_property: SDK.CSSProperty.CSSProperty): boolean {
return false;
}
markSelectorHighlights(): void {
}
markSelectorMatches(): void {
if (this.styleInternal.parentRule instanceof SDK.CSSRule.CSSKeyframeRule) {
this.selectorElement.textContent = this.styleInternal.parentRule.key().text;
}
}
highlight(): void {
}
}
export function quoteFamilyName(familyName: string): string {
return `'${familyName.replaceAll('\'', '\\\'')}'`;
}
export class CSSPropertyPrompt extends UI.TextPrompt.TextPrompt {
private readonly isColorAware: boolean;
private readonly cssCompletions: string[];
private selectedNodeComputedStyles: Map<string, string>|null;
private parentNodeComputedStyles: Map<string, string>|null;
private treeElement: StylePropertyTreeElement;
private isEditingName: boolean;
private readonly cssVariables: string[];
constructor(treeElement: StylePropertyTreeElement, isEditingName: boolean) {
// Use the same callback both for applyItemCallback and acceptItemCallback.
super();
this.initialize(this.buildPropertyCompletions.bind(this), UI.UIUtils.StyleValueDelimiters);
const cssMetadata = SDK.CSSMetadata.cssMetadata();
this.isColorAware = SDK.CSSMetadata.cssMetadata().isColorAwareProperty(treeElement.property.name);
this.cssCompletions = [];
const node = treeElement.node();
if (isEditingName) {
this.cssCompletions = cssMetadata.allProperties();
if (node && !node.isSVGNode()) {
this.cssCompletions = this.cssCompletions.filter(property => !cssMetadata.isSVGProperty(property));
}
} else {
this.cssCompletions = cssMetadata.getPropertyValues(treeElement.property.name);
if (node && cssMetadata.isFontFamilyProperty(treeElement.property.name)) {
const fontFamilies = node.domModel().cssModel().fontFaces().map(font => quoteFamilyName(font.getFontFamily()));
this.cssCompletions.unshift(...fontFamilies);
}
}
/**
* Computed styles cache populated for flexbox features.
*/
this.selectedNodeComputedStyles = null;
/**
* Computed styles cache populated for flexbox features.
*/
this.parentNodeComputedStyles = null;
this.treeElement = treeElement;
this.isEditingName = isEditingName;
this.cssVariables = treeElement.matchedStyles().availableCSSVariables(treeElement.property.ownerStyle);
if (this.cssVariables.length < 1000) {
this.cssVariables.sort(Platform.StringUtilities.naturalOrderComparator);
} else {
this.cssVariables.sort();
}
if (!isEditingName) {
this.disableDefaultSuggestionForEmptyInput();
// If a CSS value is being edited that has a numeric or hex substring, hint that precision modifier shortcuts are available.
if (treeElement && treeElement.valueElement) {
const cssValueText = treeElement.valueElement.textContent;
const cmdOrCtrl = Host.Platform.isMac() ? 'Cmd' : 'Ctrl';
if (cssValueText !== null) {
if (cssValueText.match(/#[\da-f]{3,6}$/i)) {
this.setTitle(i18nString(UIStrings.incrementdecrementWithMousewheelOne, {PH1: cmdOrCtrl}));
} else if (cssValueText.match(/\d+/)) {
this.setTitle(i18nString(UIStrings.incrementdecrementWithMousewheelHundred, {PH1: cmdOrCtrl}));
}
}
}
}
}
onKeyDown(event: Event): void {
const keyboardEvent = (event as KeyboardEvent);
switch (keyboardEvent.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'PageUp':
case 'PageDown':
if (!this.isSuggestBoxVisible() && this.handleNameOrValueUpDown(keyboardEvent)) {
keyboardEvent.preventDefault();
return;
}
break;
case 'Enter':
if (keyboardEvent.shiftKey) {
return;
}
// Accept any available autocompletions and advance to the next field.
this.tabKeyPressed();
keyboardEvent.preventDefault();
return;
}
super.onKeyDown(keyboardEvent);
}
onMouseWheel(event: Event): void {
if (this.handleNameOrValueUpDown(event)) {
event.consume(true);
return;
}
super.onMouseWheel(event);
}
tabKeyPressed(): boolean {
this.acceptAutoComplete();
// Always tab to the next field.
return false;
}
private handleNameOrValueUpDown(event: Event): boolean {
function finishHandler(this: CSSPropertyPrompt, _originalValue: string, _replacementString: string): void {
// Synthesize property text disregarding any comments, custom whitespace etc.
if (this.treeElement.nameElement && this.treeElement.valueElement) {
this.treeElement.applyStyleText(
this.treeElement.nameElement.textContent + ': ' + this.treeElement.valueElement.textContent, false);
}
}
function customNumberHandler(this: CSSPropertyPrompt, prefix: string, number: number, suffix: string): string {
if (number !== 0 && !suffix.length &&
SDK.CSSMetadata.cssMetadata().isLengthProperty(this.treeElement.property.name) &&
!this.treeElement.property.value.toLowerCase().startsWith('calc(')) {
suffix = 'px';
}
return prefix + number + suffix;
}
// Handle numeric value increment/decrement only at this point.
if (!this.isEditingName && this.treeElement.valueElement &&
UI.UIUtils.handleElementValueModifications(
event, this.treeElement.valueElement, finishHandler.bind(this), this.isValueSuggestion.bind(this),
customNumberHandler.bind(this))) {
return true;
}
return false;
}
private isValueSuggestion(word: string): boolean {
if (!word) {
return false;
}
word = word.toLowerCase();
return this.cssCompletions.indexOf(word) !== -1 || word.startsWith('--');
}
private async buildPropertyCompletions(expression: string, query: string, force?: boolean):
Promise<UI.SuggestBox.Suggestions> {
const lowerQuery = query.toLowerCase();
const editingVariable = !this.isEditingName && expression.trim().endsWith('var(');
if (!query && !force && !editingVariable && (this.isEditingName || expression)) {
return Promise.resolve([]);
}
const prefixResults: UI.SuggestBox.Suggestions = [];
const anywhereResults: UI.SuggestBox.Suggestions = [];
if (!editingVariable) {
this.cssCompletions.forEach(completion => filterCompletions.call(this, completion, false /* variable */));
}
const node = this.treeElement.node();
if (this.isEditingName && node) {
const nameValuePresets = SDK.CSSMetadata.cssMetadata().nameValuePresets(node.isSVGNode());
nameValuePresets.forEach(
preset => filterCompletions.call(this, preset, false /* variable */, true /* nameValue */));
}
if (this.isEditingName || editingVariable) {
this.cssVariables.forEach(variable => filterCompletions.call(this, variable, true /* variable */));
}
const results = prefixResults.concat(anywhereResults);
if (!this.isEditingName && !results.length && query.length > 1 && '!important'.startsWith(lowerQuery)) {
results.push({
text: '!important',
title: undefined,
subtitle: undefined,
iconType: undefined,
priority: undefined,
isSecondary: undefined,
subtitleRenderer: undefined,
selectionRange: undefined,
hideGhostText: undefined,
iconElement: undefined,
});
}
const userEnteredText = query.replace('-', '');
if (userEnteredText && (userEnteredText === userEnteredText.toUpperCase())) {
for (let i = 0; i < results.length; ++i) {
if (!results[i].text.startsWith('--')) {
results[i].text = results[i].text.toUpperCase();
}
}
}
for (const result of results) {
if (editingVariable) {
result.title = result.text;
result.text += ')';
continue;
}
const valuePreset = SDK.CSSMetadata.cssMetadata().getValuePreset(this.treeElement.name, result.text);
if (!this.isEditingName && valuePreset) {
result.title = result.text;
result.text = valuePreset.text;
result.selectionRange = {startColumn: valuePreset.startColumn, endColumn: valuePreset.endColumn};
}
}
const ensureComputedStyles = async(): Promise<void> => {
if (!node || this.selectedNodeComputedStyles) {
return;
}
this.selectedNodeComputedStyles = await node.domModel().cssModel().computedStylePromise(node.id);
const parentNode = node.parentNode;
if (parentNode) {
this.parentNodeComputedStyles = await parentNode.domModel().cssModel().computedStylePromise(parentNode.id);
}
};
for (const result of results) {
await ensureComputedStyles();
// Using parent node's computed styles does not work in all cases. For example:
//
// <div id="container" style="display: flex;">
// <div id="useless" style="display: contents;">
// <div id="item">item</div>
// </div>
// </div>
// TODO(crbug/1139945): Find a better way to get the flex container styles.
const iconInfo = ElementsComponents.CSSPropertyIconResolver.findIcon(
this.isEditingName ? result.text : `${this.treeElement.property.name}: ${result.text}`,
this.selectedNodeComputedStyles, this.parentNodeComputedStyles);
if (!iconInfo) {
continue;
}
const icon = new IconButton.Icon.Icon();
const width = '12.5px';
const height = '12.5px';
icon.data = {
iconName: iconInfo.iconName,
width,
height,
color: 'black',
};
icon.style.transform = `rotate(${iconInfo.rotate}deg) scale(${iconInfo.scaleX * 1.1}, ${iconInfo.scaleY * 1.1})`;
icon.style.maxHeight = height;
icon.style.maxWidth = width;
result.iconElement = icon;
}
if (this.isColorAware && !this.isEditingName) {
results.sort((a, b) => {
if (Boolean(a.subtitleRenderer) === Boolean(b.subtitleRenderer)) {
return 0;
}
return a.subtitleRenderer ? -1 : 1;
});
}
return Promise.resolve(results);
function filterCompletions(
this: CSSPropertyPrompt, completion: string, variable: boolean, nameValue?: boolean): void {
const index = completion.toLowerCase().indexOf(lowerQuery);
const result: UI.SuggestBox.Suggestion = {
text: completion,
title: undefined,
subtitle: undefined,
iconType: undefined,
priority: undefined,
isSecondary: undefined,
subtitleRenderer: undefined,
selectionRange: undefined,
hideGhostText: undefined,
iconElement: undefined,
};
if (variable) {
const computedValue =
this.treeElement.matchedStyles().computeCSSVariable(this.treeElement.property.ownerStyle, completion);
if (computedValue) {
const color = Common.Color.Color.parse(computedValue);
if (color) {
result.subtitleRenderer = swatchRenderer.bind(null, color);
}
}
}
if (nameValue) {
result.hideGhostText = true;
}
if (index === 0) {
result.priority = this.isEditingName ? SDK.CSSMetadata.cssMetadata().propertyUsageWeight(completion) : 1;
prefixResults.push(result);
} else if (index > -1) {
anywhereResults.push(result);
}
}
function swatchRenderer(color: Common.Color.Color): Element {
const swatch = new InlineEditor.ColorSwatch.ColorSwatch();
swatch.renderColor(color);
swatch.style.pointerEvents = 'none';
return swatch;
}
}
}
export function unescapeCssString(input: string): string {
// https://drafts.csswg.org/css-syntax/#consume-escaped-code-point
const reCssEscapeSequence = /(?<!\\)\\(?:([a-fA-F0-9]{1,6})|(.))[\n\t\x20]?/gs;
return input.replace(reCssEscapeSequence, (_, $1, $2) => {
if ($2) { // Handle the single-character escape sequence.
return $2;
}
// Otherwise, handle the code point escape sequence.
const codePoint = parseInt($1, 16);
const isSurrogate = 0xD800 <= codePoint && codePoint <= 0xDFFF;
if (isSurrogate || codePoint === 0x0000 || codePoint > 0x10FFFF) {
return '\uFFFD';
}
return String.fromCodePoint(codePoint);
});
}
export class StylesSidebarPropertyRenderer {
private rule: SDK.CSSRule.CSSRule|null;
private node: SDK.DOMModel.DOMNode|null;
private propertyName: string;
private propertyValue: string;
private colorHandler: ((arg0: string) => Node)|null;
private bezierHandler: ((arg0: string) => Node)|null;
private fontHandler: ((arg0: string) => Node)|null;
private shadowHandler: ((arg0: string, arg1: string) => Node)|null;
private gridHandler: ((arg0: string, arg1: string) => Node)|null;
private varHandler: ((arg0: string) => Node)|null;
private angleHandler: ((arg0: string) => Node)|null;
constructor(rule: SDK.CSSRule.CSSRule|null, node: SDK.DOMModel.DOMNode|null, name: string, value: string) {
this.rule = rule;
this.node = node;
this.propertyName = name;
this.propertyValue = value;
this.colorHandler = null;
this.bezierHandler = null;
this.fontHandler = null;
this.shadowHandler = null;
this.gridHandler = null;
this.varHandler = document.createTextNode.bind(document);
this.angleHandler = null;
}
setColorHandler(handler: (arg0: string) => Node): void {
this.colorHandler = handler;
}
setBezierHandler(handler: (arg0: string) => Node): void {
this.bezierHandler = handler;
}
setFontHandler(handler: (arg0: string) => Node): void {
this.fontHandler = handler;
}
setShadowHandler(handler: (arg0: string, arg1: string) => Node): void {
this.shadowHandler = handler;
}
setGridHandler(handler: (arg0: string, arg1: string) => Node): void {
this.gridHandler = handler;
}
setVarHandler(handler: (arg0: string) => Node): void {
this.varHandler = handler;
}
setAngleHandler(handler: (arg0: string) => Node): void {
this.angleHandler = handler;
}
renderName(): Element {
const nameElement = document.createElement('span');
nameElement.className = 'webkit-css-property';
nameElement.textContent = this.propertyName;
nameElement.normalize();
return nameElement;
}
renderValue(): Element {
const valueElement = document.createElement('span');
valueElement.className = 'value';
if (!this.propertyValue) {
return valueElement;
}
const metadata = SDK.CSSMetadata.cssMetadata();
if (this.shadowHandler && metadata.isShadowProperty(this.propertyName) &&
!SDK.CSSMetadata.VariableRegex.test(this.propertyValue)) {
valueElement.appendChild(this.shadowHandler(this.propertyValue, this.propertyName));
valueElement.normalize();
return valueElement;
}
if (this.gridHandler && metadata.isGridAreaDefiningProperty(this.propertyName)) {
valueElement.appendChild(this.gridHandler(this.propertyValue, this.propertyName));
valueElement.normalize();
return valueElement;
}
if (metadata.isStringProperty(this.propertyName)) {
UI.Tooltip.Tooltip.install(valueElement, unescapeCssString(this.propertyValue));
}
const regexes = [SDK.CSSMetadata.VariableRegex, SDK.CSSMetadata.URLRegex];
const processors = [this.varHandler, this.processURL.bind(this)];
if (this.bezierHandler && metadata.isBezierAwareProperty(this.propertyName)) {
regexes.push(UI.Geometry.CubicBezier.Regex);
processors.push(this.bezierHandler);
}
if (this.colorHandler && metadata.isColorAwareProperty(this.propertyName)) {
regexes.push(Common.Color.Regex);
processors.push(this.colorHandler);
}
if (this.angleHandler && metadata.isAngleAwareProperty(this.propertyName)) {
// TODO(changhaohan): crbug.com/1138628 refactor this to handle unitless 0 cases
regexes.push(InlineEditor.CSSAngleUtils.CSSAngleRegex);
processors.push(this.angleHandler);
}
if (this.fontHandler && metadata.isFontAwareProperty(this.propertyName)) {
if (this.propertyName === 'font-family') {
regexes.push(InlineEditor.FontEditorUtils.FontFamilyRegex);
} else {
regexes.push(InlineEditor.FontEditorUtils.FontPropertiesRegex);
}
processors.push(this.fontHandler);
}
const results = TextUtils.TextUtils.Utils.splitStringByRegexes(this.propertyValue, regexes);
for (let i = 0; i < results.length; i++) {
const result = results[i];
const processor =
result.regexIndex === -1 ? document.createTextNode.bind(document) : processors[result.regexIndex];
if (processor) {
valueElement.appendChild(processor(result.value));
}
}
valueElement.normalize();
return valueElement;
}
private processURL(text: string): Node {
// Strip "url(" and ")" along with whitespace.
let url = text.substring(4, text.length - 1).trim();
const isQuoted = /^'.*'$/s.test(url) || /^".*"$/s.test(url);
if (isQuoted) {
url = url.substring(1, url.length - 1);
}
const container = document.createDocumentFragment();
UI.UIUtils.createTextChild(container, 'url(');
let hrefUrl: (string|null)|null = null;
if (this.rule && this.rule.resourceURL()) {
hrefUrl = Common.ParsedURL.ParsedURL.completeURL(this.rule.resourceURL(), url);
} else if (this.node) {
hrefUrl = this.node.resolveURL(url);
}
const link = ImagePreviewPopover.setImageUrl(
Components.Linkifier.Linkifier.linkifyURL(hrefUrl || url, {
text: url,
preventClick: false,
// crbug.com/1027168
// We rely on CSS text-overflow: ellipsis to hide long URLs in the Style panel,
// so that we don't have to keep two versions (original vs. trimmed) of URL
// at the same time, which complicates both StylesSidebarPane and StylePropertyTreeElement.
bypassURLTrimming: true,
className: undefined,
lineNumber: undefined,
columnNumber: undefined,
inlineFrameIndex: 0,
maxLength: undefined,
tabStop: undefined,
}),
hrefUrl || url);
container.appendChild(link);
UI.UIUtils.createTextChild(container, ')');
return container;
}
}
let buttonProviderInstance: ButtonProvider;
export class ButtonProvider implements UI.Toolbar.Provider {
private readonly button: UI.Toolbar.ToolbarButton;
private constructor() {
this.button = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.newStyleRule), 'largeicon-add');
this.button.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this.clicked, this);
const longclickTriangle = UI.Icon.Icon.create('largeicon-longclick-triangle', 'long-click-glyph');
this.button.element.appendChild(longclickTriangle);
new UI.UIUtils.LongClickController(this.button.element, this.longClicked.bind(this));
UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, onNodeChanged.bind(this));
onNodeChanged.call(this);
function onNodeChanged(this: ButtonProvider): void {
let node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
node = node ? node.enclosingElementOrSelf() : null;
this.button.setEnabled(Boolean(node));
}
}
static instance(opts: {
forceNew: boolean|null,
} = {forceNew: null}): ButtonProvider {
const {forceNew} = opts;
if (!buttonProviderInstance || forceNew) {
buttonProviderInstance = new ButtonProvider();
}
return buttonProviderInstance;
}
private clicked(_event: Common.EventTarget.EventTargetEvent): void {
StylesSidebarPane.instance().createNewRuleInViaInspectorStyleSheet();
}
private longClicked(event: Event): void {
StylesSidebarPane.instance().onAddButtonLongClick(event);
}
item(): UI.Toolbar.ToolbarItem {
return this.button;
}
}