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