| /* |
| * 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 {ColorSwatchPopoverIcon, ShadowSwatchPopoverHelper} from './ColorSwatchPopoverIcon.js'; |
| import {linkifyDeferredNodeReference} from './DOMLinkifier.js'; |
| import {ElementsSidebarPane} from './ElementsSidebarPane.js'; |
| import {StylePropertyHighlighter} from './StylePropertyHighlighter.js'; |
| import {StylePropertyTreeElement} from './StylePropertyTreeElement.js'; |
| |
| export class StylesSidebarPane extends ElementsSidebarPane { |
| constructor() { |
| super(true /* delegatesFocus */); |
| this.setMinimumSize(96, 26); |
| this.registerRequiredCSS('elements/stylesSidebarPane.css'); |
| |
| Common.moduleSetting('colorFormat').addChangeListener(this.update.bind(this)); |
| Common.moduleSetting('textEditorIndent').addChangeListener(this.update.bind(this)); |
| |
| /** @type {?UI.Widget} */ |
| this._currentToolbarPane = null; |
| /** @type {?UI.Widget} */ |
| this._animatedToolbarPane = null; |
| /** @type {?UI.Widget} */ |
| this._pendingWidget = null; |
| /** @type {?UI.ToolbarToggle} */ |
| this._pendingWidgetToggle = null; |
| this._toolbarPaneElement = this._createStylesSidebarToolbar(); |
| |
| this._noMatchesElement = this.contentElement.createChild('div', 'gray-info-message hidden'); |
| this._noMatchesElement.textContent = ls`No matching selector or style`; |
| |
| 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); |
| |
| this._swatchPopoverHelper = new InlineEditor.SwatchPopoverHelper(); |
| this._linkifier = new Components.Linkifier(_maxLinkLength, /* useLinkDecorator */ true); |
| /** @type {?StylePropertyHighlighter} */ |
| this._decorator = null; |
| this._userOperation = false; |
| this._isEditingStyle = false; |
| /** @type {?RegExp} */ |
| this._filterRegex = null; |
| this._isActivePropertyHighlighted = false; |
| |
| this.contentElement.classList.add('styles-pane'); |
| |
| /** @type {!Array<!SectionBlock>} */ |
| this._sectionBlocks = []; |
| this._needsForceUpdate = false; |
| StylesSidebarPane._instance = this; |
| UI.context.addFlavorChangeListener(SDK.DOMNode, this.forceUpdate, this); |
| this.contentElement.addEventListener('copy', this._clipboardCopy.bind(this)); |
| this._resizeThrottler = new Common.Throttler(100); |
| } |
| |
| /** |
| * @return {!InlineEditor.SwatchPopoverHelper} |
| */ |
| swatchPopoverHelper() { |
| return this._swatchPopoverHelper; |
| } |
| |
| /** |
| * @param {boolean} userOperation |
| */ |
| setUserOperation(userOperation) { |
| this._userOperation = userOperation; |
| } |
| |
| /** |
| * @param {!SDK.CSSProperty} property |
| * @return {!Element} |
| */ |
| static createExclamationMark(property) { |
| const exclamationElement = createElement('span', 'dt-icon-label'); |
| exclamationElement.className = 'exclamation-mark'; |
| if (!StylesSidebarPane.ignoreErrorsForProperty(property)) { |
| exclamationElement.type = 'smallicon-warning'; |
| } |
| exclamationElement.title = SDK.cssMetadata().isCSSPropertyName(property.name) ? |
| Common.UIString('Invalid property value') : |
| Common.UIString('Unknown property name'); |
| return exclamationElement; |
| } |
| |
| /** |
| * @param {!SDK.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)} filterCallback |
| * @return {!Element} |
| */ |
| static createPropertyFilterElement(placeholder, container, filterCallback) { |
| const input = createElementWithClass('input'); |
| input.placeholder = placeholder; |
| |
| function searchHandler() { |
| const regex = input.value ? new RegExp(input.value.escapeForRegExp(), 'i') : null; |
| filterCallback(regex); |
| } |
| input.addEventListener('input', searchHandler, false); |
| |
| /** |
| * @param {!Event} event |
| */ |
| function keydownHandler(event) { |
| if (event.key !== 'Escape' || !input.value) { |
| return; |
| } |
| event.consume(true); |
| input.value = ''; |
| searchHandler(); |
| } |
| input.addEventListener('keydown', keydownHandler, false); |
| |
| input.setFilterValue = setFilterValue; |
| |
| /** |
| * @param {string} value |
| */ |
| function setFilterValue(value) { |
| input.value = value; |
| input.focus(); |
| searchHandler(); |
| } |
| |
| return input; |
| } |
| |
| /** |
| * @param {!SDK.CSSProperty} cssProperty |
| */ |
| revealProperty(cssProperty) { |
| this._decorator = new StylePropertyHighlighter(this, cssProperty); |
| this._decorator.perform(); |
| this.update(); |
| } |
| |
| 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 = activeElement._section; |
| if (!section) { |
| return; |
| } |
| |
| switch (event.key) { |
| case 'ArrowUp': |
| case 'ArrowLeft': |
| const sectionToFocus = section.previousSibling() || section.lastSibling(); |
| sectionToFocus.element.focus(); |
| event.consume(true); |
| break; |
| case 'ArrowDown': |
| case 'ArrowRight': { |
| const sectionToFocus = section.nextSibling() || section.firstSibling(); |
| sectionToFocus.element.focus(); |
| event.consume(true); |
| break; |
| } |
| case 'Home': |
| section.firstSibling().element.focus(); |
| event.consume(true); |
| break; |
| case 'End': |
| section.lastSibling().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()}>} */ |
| 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.displayNameForURL(header.resourceURL()), handler: handler}); |
| } |
| |
| contextMenuDescriptors.sort(compareDescriptors); |
| |
| const contextMenu = new UI.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()}} descriptor1 |
| * @param {!{text: string, handler: function()}} descriptor2 |
| * @return {number} |
| */ |
| function compareDescriptors(descriptor1, descriptor2) { |
| return String.naturalOrderComparator(descriptor1.text, descriptor2.text); |
| } |
| |
| /** |
| * @param {!SDK.CSSStyleSheetHeader} header |
| * @return {boolean} |
| */ |
| function styleSheetResourceHeader(header) { |
| return !header.isViaInspector() && !header.isInline && !!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.isBlank) { |
| continue; |
| } |
| section._updateVarFunctions(editedTreeElement); |
| } |
| } |
| |
| if (this._isEditingStyle) { |
| return; |
| } |
| const node = this.node(); |
| if (!node) { |
| return; |
| } |
| |
| for (const section of this.allSections()) { |
| if (section.isBlank) { |
| continue; |
| } |
| section.update(section === editedSection); |
| } |
| |
| if (this._filterRegex) { |
| this._updateFilter(); |
| } |
| this._nodeStylesUpdatedForTest(node, false); |
| } |
| |
| /** |
| * @override |
| * @return {!Promise.<?>} |
| */ |
| doUpdate() { |
| return this._fetchMatchedCascade().then(this._innerRebuildUpdate.bind(this)); |
| } |
| |
| /** |
| * @override |
| */ |
| onResize() { |
| this._resizeThrottler.schedule(this._innerResize.bind(this)); |
| } |
| |
| /** |
| * @return {!Promise} |
| */ |
| _innerResize() { |
| const width = this.contentElement.getBoundingClientRect().width + 'px'; |
| this.allSections().forEach(section => section.propertiesTreeOutline.element.style.width = width); |
| return Promise.resolve(); |
| } |
| |
| _resetCache() { |
| if (this.cssModel()) { |
| this.cssModel().discardCachedMatchedCascade(); |
| } |
| } |
| |
| /** |
| * @return {!Promise.<?SDK.CSSMatchedStyles>} |
| */ |
| _fetchMatchedCascade() { |
| const node = this.node(); |
| if (!node || !this.cssModel()) { |
| return Promise.resolve(/** @type {?SDK.CSSMatchedStyles} */ (null)); |
| } |
| |
| return this.cssModel().cachedMatchedCascadeForNode(node).then(validateStyles.bind(this)); |
| |
| /** |
| * @param {?SDK.CSSMatchedStyles} matchedStyles |
| * @return {?SDK.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.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.CSSStyleRule) ? rule.selectorText() : undefined; |
| for (const mode of ['padding', 'border', 'margin']) { |
| if (!treeElement.name.startsWith(mode)) { |
| continue; |
| } |
| this.node().domModel().overlayModel().highlightInOverlay( |
| {node: /** @type {!SDK.DOMNode} */ (this.node()), selectorList}, mode); |
| this._isActivePropertyHighlighted = true; |
| break; |
| } |
| } |
| |
| /** |
| * @override |
| * @param {!Common.Event=} 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) { |
| section.propertiesTreeOutline.rootElement().childAt(propertyIndex).startEditing(); |
| } |
| } |
| |
| /** |
| * @param {?SDK.CSSMatchedStyles} matchedStyles |
| * @return {!Promise} |
| */ |
| 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(); |
| this._sectionsContainer.removeChildren(); |
| this._sectionBlocks = []; |
| |
| const node = this.node(); |
| if (!matchedStyles || !node) { |
| this._noMatchesElement.classList.remove('hidden'); |
| return; |
| } |
| |
| this._sectionBlocks = |
| await this._rebuildSectionsForMatchedStyleRules(/** @type {!SDK.CSSMatchedStyles} */ (matchedStyles)); |
| let pseudoTypes = []; |
| const keys = matchedStyles.pseudoTypes(); |
| if (keys.delete(Protocol.DOM.PseudoType.Before)) { |
| pseudoTypes.push(Protocol.DOM.PseudoType.Before); |
| } |
| pseudoTypes = pseudoTypes.concat(keys.valuesArray().sort()); |
| for (const pseudoType of pseudoTypes) { |
| const block = SectionBlock.createPseudoTypeBlock(pseudoType); |
| for (const style of matchedStyles.pseudoStyles(pseudoType)) { |
| const section = new StylePropertiesSection(this, matchedStyles, style); |
| block.sections.push(section); |
| } |
| this._sectionBlocks.push(block); |
| } |
| |
| for (const keyframesRule of matchedStyles.keyframes()) { |
| const block = SectionBlock.createKeyframesBlock(keyframesRule.name().text); |
| for (const keyframe of keyframesRule.keyframes()) { |
| block.sections.push(new KeyframePropertiesSection(this, matchedStyles, keyframe.style)); |
| } |
| this._sectionBlocks.push(block); |
| } |
| let index = 0; |
| for (const block of this._sectionBlocks) { |
| const titleElement = block.titleElement(); |
| if (titleElement) { |
| this._sectionsContainer.appendChild(titleElement); |
| } |
| for (const section of block.sections) { |
| this._sectionsContainer.appendChild(section.element); |
| if (index === focusedIndex) { |
| section.element.focus(); |
| } |
| index++; |
| } |
| } |
| 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.DOMNode} */ (node), true); |
| if (this._decorator) { |
| this._decorator.perform(); |
| this._decorator = null; |
| } |
| } |
| |
| /** |
| * @param {!SDK.DOMNode} node |
| * @param {boolean} rebuild |
| */ |
| _nodeStylesUpdatedForTest(node, rebuild) { |
| // For sniffing in tests. |
| } |
| |
| /** |
| * @param {!SDK.CSSMatchedStyles} matchedStyles |
| * @return {!Promise<!Array.<!SectionBlock>>} |
| */ |
| async _rebuildSectionsForMatchedStyleRules(matchedStyles) { |
| 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 section = new StylePropertiesSection(this, matchedStyles, style); |
| blocks.peekLast().sections.push(section); |
| } |
| 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.DOMNode} */ (node)); |
| |
| this.setUserOperation(false); |
| await this._createNewRuleInStyleSheet(styleSheetHeader); |
| } |
| |
| /** |
| * @param {?SDK.CSSStyleSheetHeader} styleSheetHeader |
| */ |
| async _createNewRuleInStyleSheet(styleSheetHeader) { |
| if (!styleSheetHeader) { |
| return; |
| } |
| |
| const text = (await styleSheetHeader.requestContent()).content || ''; |
| const lines = text.split('\n'); |
| const range = TextUtils.TextRange.createFromLocation(lines.length - 1, lines[lines.length - 1].length); |
| this._addBlankSection(this._sectionBlocks[0].sections[0], styleSheetHeader.id, range); |
| } |
| |
| /** |
| * @param {!StylePropertiesSection} insertAfterSection |
| * @param {string} styleSheetId |
| * @param {!TextUtils.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(); |
| } |
| this._noMatchesElement.classList.toggle('hidden', !!hasAnyVisibleBlock); |
| } |
| |
| /** |
| * @override |
| */ |
| willHide() { |
| this._swatchPopoverHelper.hide(); |
| super.willHide(); |
| } |
| |
| /** |
| * @return {!Array<!StylePropertiesSection>} |
| */ |
| allSections() { |
| 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 {!Element} |
| */ |
| _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(ls`Filter`, hbox, this._onFilterChanged.bind(this)); |
| UI.ARIAUtils.setAccessibleName(filterInput, Common.UIString('Filter Styles')); |
| filterContainerElement.appendChild(filterInput); |
| const toolbar = new UI.Toolbar('styles-pane-toolbar', hbox); |
| toolbar.makeToggledGray(); |
| toolbar.appendItemsAtLocation('styles-sidebarpane-toolbar'); |
| const toolbarPaneContainer = container.createChild('div', 'styles-sidebar-toolbar-pane-container'); |
| const toolbarPaneContent = toolbarPaneContainer.createChild('div', 'styles-sidebar-toolbar-pane'); |
| |
| return toolbarPaneContent; |
| } |
| |
| /** |
| * @param {?UI.Widget} widget |
| * @param {?UI.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.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; |
| } |
| } |
| } |
| } |
| |
| export const _maxLinkLength = 23; |
| |
| export class SectionBlock { |
| /** |
| * @param {?Element} titleElement |
| */ |
| constructor(titleElement) { |
| this._titleElement = titleElement; |
| this.sections = []; |
| } |
| |
| /** |
| * @param {!Protocol.DOM.PseudoType} pseudoType |
| * @return {!SectionBlock} |
| */ |
| static createPseudoTypeBlock(pseudoType) { |
| const separatorElement = createElement('div'); |
| separatorElement.className = 'sidebar-separator'; |
| separatorElement.textContent = Common.UIString('Pseudo ::%s element', pseudoType); |
| return new SectionBlock(separatorElement); |
| } |
| |
| /** |
| * @param {string} keyframesName |
| * @return {!SectionBlock} |
| */ |
| static createKeyframesBlock(keyframesName) { |
| const separatorElement = createElement('div'); |
| separatorElement.className = 'sidebar-separator'; |
| separatorElement.textContent = `@keyframes ${keyframesName}`; |
| return new SectionBlock(separatorElement); |
| } |
| |
| /** |
| * @param {!SDK.DOMNode} node |
| * @return {!Promise<!SectionBlock>} |
| */ |
| static async _createInheritedNodeBlock(node) { |
| const separatorElement = createElement('div'); |
| separatorElement.className = 'sidebar-separator'; |
| separatorElement.createTextChild(ls`Inherited from${' '}`); |
| const link = await Common.Linkifier.linkify(node, {preventKeyboardFocus: true}); |
| separatorElement.appendChild(link); |
| return new SectionBlock(separatorElement); |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| updateFilter() { |
| let hasAnyVisibleSection = false; |
| for (const section of this.sections) { |
| hasAnyVisibleSection |= section._updateFilter(); |
| } |
| if (this._titleElement) { |
| this._titleElement.classList.toggle('hidden', !hasAnyVisibleSection); |
| } |
| return !!hasAnyVisibleSection; |
| } |
| |
| /** |
| * @return {?Element} |
| */ |
| titleElement() { |
| return this._titleElement; |
| } |
| } |
| |
| export class StylePropertiesSection { |
| /** |
| * @param {!StylesSidebarPane} parentPane |
| * @param {!SDK.CSSMatchedStyles} matchedStyles |
| * @param {!SDK.CSSStyleDeclaration} style |
| */ |
| constructor(parentPane, matchedStyles, style) { |
| this._parentPane = parentPane; |
| this._style = style; |
| this._matchedStyles = matchedStyles; |
| this.editable = !!(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 = createElementWithClass('div', 'styles-section matched-styles monospace'); |
| this.element.tabIndex = -1; |
| UI.ARIAUtils.markAsTreeitem(this.element); |
| this.element.addEventListener('keydown', this._onKeyDown.bind(this), false); |
| this.element._section = this; |
| this._innerElement = this.element.createChild('div'); |
| |
| this._titleElement = this._innerElement.createChild('div', 'styles-section-title ' + (rule ? 'styles-selector' : '')); |
| |
| this.propertiesTreeOutline = new UI.TreeOutlineInShadow(); |
| this.propertiesTreeOutline.setFocusable(false); |
| this.propertiesTreeOutline.registerRequiredCSS('elements/stylesSectionTree.css'); |
| this.propertiesTreeOutline.element.classList.add('style-properties', 'matched-styles', 'monospace'); |
| this.propertiesTreeOutline.section = this; |
| this._innerElement.appendChild(this.propertiesTreeOutline.element); |
| |
| this._showAllButton = UI.createTextButton('', this._showAllItems.bind(this), 'styles-show-all'); |
| this._innerElement.appendChild(this._showAllButton); |
| |
| const selectorContainer = createElement('div'); |
| this._selectorElement = createElementWithClass('span', '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 = '}'; |
| |
| this._createHoverMenuToolbar(closeBrace); |
| |
| this._selectorElement.addEventListener('click', this._handleSelectorClick.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; |
| |
| 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.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'); |
| } |
| |
| this._hoverableSelectorsMode = false; |
| this._markSelectorMatches(); |
| this.onpopulate(); |
| } |
| |
| /** |
| * @param {!SDK.CSSMatchedStyles} matchedStyles |
| * @param {!Components.Linkifier} linkifier |
| * @param {?SDK.CSSRule} rule |
| * @return {!Node} |
| */ |
| static createRuleOriginNode(matchedStyles, linkifier, rule) { |
| if (!rule) { |
| return createTextNode(''); |
| } |
| |
| const ruleLocation = this._getRuleLocationFromCSSRule(rule); |
| |
| const header = rule.styleSheetId ? matchedStyles.cssModel().styleSheetHeaderForId(rule.styleSheetId) : null; |
| if (ruleLocation && rule.styleSheetId && header && !header.isAnonymousInlineStyleSheet()) { |
| return StylePropertiesSection._linkifyRuleLocation( |
| matchedStyles.cssModel(), linkifier, rule.styleSheetId, ruleLocation); |
| } |
| |
| if (rule.isUserAgent()) { |
| return createTextNode(Common.UIString('user agent stylesheet')); |
| } |
| if (rule.isInjected()) { |
| return createTextNode(Common.UIString('injected stylesheet')); |
| } |
| if (rule.isViaInspector()) { |
| return createTextNode(Common.UIString('via inspector')); |
| } |
| |
| if (header && header.ownerNode) { |
| const link = linkifyDeferredNodeReference(header.ownerNode, {preventKeyboardFocus: true}); |
| link.textContent = '<style>'; |
| return link; |
| } |
| |
| return createTextNode(''); |
| } |
| |
| /** |
| * @param {!SDK.CSSRule} rule |
| * @return {?TextUtils.TextRange} |
| */ |
| static _getRuleLocationFromCSSRule(rule) { |
| let ruleLocation = null; |
| if (rule instanceof SDK.CSSStyleRule) { |
| ruleLocation = rule.style.range; |
| } else if (rule instanceof SDK.CSSKeyframeRule) { |
| ruleLocation = rule.key().range; |
| } |
| return ruleLocation; |
| } |
| |
| /** |
| * @param {!SDK.CSSMatchedStyles} matchedStyles |
| * @param {?SDK.CSSRule} rule |
| */ |
| static tryNavigateToRuleLocation(matchedStyles, rule) { |
| if (!rule) { |
| return; |
| } |
| |
| const ruleLocation = this._getRuleLocationFromCSSRule(rule); |
| const header = rule.styleSheetId ? matchedStyles.cssModel().styleSheetHeaderForId(rule.styleSheetId) : null; |
| |
| if (ruleLocation && rule.styleSheetId && header && !header.isAnonymousInlineStyleSheet()) { |
| const matchingSelectorLocation = |
| this._getCSSSelectorLocation(matchedStyles.cssModel(), rule.styleSheetId, ruleLocation); |
| this._revealSelectorSource(matchingSelectorLocation, true); |
| } |
| } |
| |
| /** |
| * @param {!SDK.CSSModel} cssModel |
| * @param {!Components.Linkifier} linkifier |
| * @param {string} styleSheetId |
| * @param {!TextUtils.TextRange} ruleLocation |
| * @return {!Node} |
| */ |
| static _linkifyRuleLocation(cssModel, linkifier, styleSheetId, ruleLocation) { |
| const matchingSelectorLocation = this._getCSSSelectorLocation(cssModel, styleSheetId, ruleLocation); |
| return linkifier.linkifyCSSLocation(matchingSelectorLocation); |
| } |
| |
| /** |
| * @param {!SDK.CSSModel} cssModel |
| * @param {string} styleSheetId |
| * @param {!TextUtils.TextRange} ruleLocation |
| * @return {!SDK.CSSLocation} |
| */ |
| static _getCSSSelectorLocation(cssModel, styleSheetId, ruleLocation) { |
| const styleSheetHeader = cssModel.styleSheetHeaderForId(styleSheetId); |
| const lineNumber = styleSheetHeader.lineNumberInSource(ruleLocation.startLine); |
| const columnNumber = styleSheetHeader.columnNumberInSource(ruleLocation.startLine, ruleLocation.startColumn); |
| return new SDK.CSSLocation(styleSheetHeader, lineNumber, columnNumber); |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _onKeyDown(event) { |
| if (UI.isEditing() || !this.editable || event.altKey || event.ctrlKey || event.metaKey) { |
| return; |
| } |
| switch (event.key) { |
| case 'Enter': |
| case ' ': |
| this._startEditingAtFirstPosition(); |
| event.consume(true); |
| break; |
| default: |
| // Filter out non-printable key strokes. |
| if (event.key.length === 1) { |
| this.addNewBlankProperty(0).startEditing(); |
| } |
| break; |
| } |
| } |
| |
| /** |
| * @param {boolean} isHovered |
| */ |
| _setSectionHovered(isHovered) { |
| this.element.classList.toggle('styles-panel-hovered', isHovered); |
| this.propertiesTreeOutline.element.classList.toggle('styles-panel-hovered', isHovered); |
| if (this._hoverableSelectorsMode !== isHovered) { |
| this._hoverableSelectorsMode = isHovered; |
| this._markSelectorMatches(); |
| } |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _onMouseLeave(event) { |
| this._setSectionHovered(false); |
| this._parentPane._setActiveProperty(null); |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _onMouseMove(event) { |
| const hasCtrlOrMeta = UI.KeyboardShortcut.eventHasCtrlOrMeta(/** @type {!MouseEvent} */ (event)); |
| this._setSectionHovered(hasCtrlOrMeta); |
| |
| const treeElement = this.propertiesTreeOutline.treeElementFromEvent(event); |
| if (treeElement instanceof StylePropertyTreeElement) { |
| this._parentPane._setActiveProperty(/** @type {!StylePropertyTreeElement} */ (treeElement)); |
| } else { |
| this._parentPane._setActiveProperty(null); |
| } |
| if (!this._selectedSinceMouseDown && this.element.getComponentSelection().toString()) { |
| this._selectedSinceMouseDown = true; |
| } |
| } |
| |
| /** |
| * @param {!Element} container |
| */ |
| _createHoverMenuToolbar(container) { |
| if (!this.editable) { |
| return; |
| } |
| const items = []; |
| |
| const textShadowButton = new UI.ToolbarButton(Common.UIString('Add text-shadow'), 'largeicon-text-shadow'); |
| textShadowButton.addEventListener( |
| UI.ToolbarButton.Events.Click, this._onInsertShadowPropertyClick.bind(this, 'text-shadow')); |
| textShadowButton.element.tabIndex = -1; |
| items.push(textShadowButton); |
| |
| const boxShadowButton = new UI.ToolbarButton(Common.UIString('Add box-shadow'), 'largeicon-box-shadow'); |
| boxShadowButton.addEventListener( |
| UI.ToolbarButton.Events.Click, this._onInsertShadowPropertyClick.bind(this, 'box-shadow')); |
| boxShadowButton.element.tabIndex = -1; |
| items.push(boxShadowButton); |
| |
| const colorButton = new UI.ToolbarButton(Common.UIString('Add color'), 'largeicon-foreground-color'); |
| colorButton.addEventListener(UI.ToolbarButton.Events.Click, this._onInsertColorPropertyClick, this); |
| colorButton.element.tabIndex = -1; |
| items.push(colorButton); |
| |
| const backgroundButton = |
| new UI.ToolbarButton(Common.UIString('Add background-color'), 'largeicon-background-color'); |
| backgroundButton.addEventListener(UI.ToolbarButton.Events.Click, this._onInsertBackgroundColorPropertyClick, this); |
| backgroundButton.element.tabIndex = -1; |
| items.push(backgroundButton); |
| |
| let newRuleButton = null; |
| if (this._style.parentRule) { |
| newRuleButton = new UI.ToolbarButton(Common.UIString('Insert Style Rule Below'), 'largeicon-add'); |
| newRuleButton.addEventListener(UI.ToolbarButton.Events.Click, this._onNewRuleClick, this); |
| newRuleButton.element.tabIndex = -1; |
| items.push(newRuleButton); |
| } |
| |
| const sectionToolbar = new UI.Toolbar('sidebar-pane-section-toolbar', container); |
| for (let i = 0; i < items.length; ++i) { |
| sectionToolbar.appendToolbarItem(items[i]); |
| } |
| |
| const menuButton = new UI.ToolbarButton('', 'largeicon-menu'); |
| menuButton.element.tabIndex = -1; |
| sectionToolbar.appendToolbarItem(menuButton); |
| setItemsVisibility(items, false); |
| sectionToolbar.element.addEventListener('mouseenter', setItemsVisibility.bind(null, items, true)); |
| sectionToolbar.element.addEventListener('mouseleave', setItemsVisibility.bind(null, items, false)); |
| UI.ARIAUtils.markAsHidden(sectionToolbar.element); |
| |
| /** |
| * @param {!Array<!UI.ToolbarButton>} items |
| * @param {boolean} value |
| */ |
| function setItemsVisibility(items, value) { |
| for (let i = 0; i < items.length; ++i) { |
| items[i].setVisible(value); |
| } |
| menuButton.setVisible(!value); |
| } |
| } |
| |
| /** |
| * @return {!SDK.CSSStyleDeclaration} |
| */ |
| style() { |
| return this._style; |
| } |
| |
| /** |
| * @return {string} |
| */ |
| _headerText() { |
| const node = this._matchedStyles.nodeForStyle(this._style); |
| if (this._style.type === SDK.CSSStyleDeclaration.Type.Inline) { |
| return this._matchedStyles.isInherited(this._style) ? Common.UIString('Style Attribute') : 'element.style'; |
| } |
| if (this._style.type === SDK.CSSStyleDeclaration.Type.Attributes) { |
| return ls`${node.nodeNameInCorrectCase()}[Attributes Style]`; |
| } |
| return this._style.parentRule.selectorText(); |
| } |
| |
| _onMouseOutSelector() { |
| if (this._hoverTimer) { |
| clearTimeout(this._hoverTimer); |
| } |
| SDK.OverlayModel.hideDOMNodeHighlight(); |
| } |
| |
| _onMouseEnterSelector() { |
| if (this._hoverTimer) { |
| clearTimeout(this._hoverTimer); |
| } |
| this._hoverTimer = setTimeout(this._highlight.bind(this), 300); |
| } |
| |
| /** |
| * @param {string=} mode |
| */ |
| _highlight(mode = 'all') { |
| SDK.OverlayModel.hideDOMNodeHighlight(); |
| const node = this._parentPane.node(); |
| if (!node) { |
| return; |
| } |
| const selectorList = this._style.parentRule ? this._style.parentRule.selectorText() : undefined; |
| node.domModel().overlayModel().highlightInOverlay({node, selectorList}, mode); |
| } |
| |
| /** |
| * @return {?StylePropertiesSection} |
| */ |
| firstSibling() { |
| const parent = this.element.parentElement; |
| if (!parent) { |
| return null; |
| } |
| |
| let childElement = parent.firstChild; |
| while (childElement) { |
| if (childElement._section) { |
| return childElement._section; |
| } |
| childElement = childElement.nextSibling; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * @return {?StylePropertiesSection} |
| */ |
| lastSibling() { |
| const parent = this.element.parentElement; |
| if (!parent) { |
| return null; |
| } |
| |
| let childElement = parent.lastChild; |
| while (childElement) { |
| if (childElement._section) { |
| return childElement._section; |
| } |
| childElement = childElement.previousSibling; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * @return {?StylePropertiesSection} |
| */ |
| nextSibling() { |
| let curElement = this.element; |
| do { |
| curElement = curElement.nextSibling; |
| } while (curElement && !curElement._section); |
| |
| return curElement ? curElement._section : null; |
| } |
| |
| /** |
| * @return {?StylePropertiesSection} |
| */ |
| previousSibling() { |
| let curElement = this.element; |
| do { |
| curElement = curElement.previousSibling; |
| } while (curElement && !curElement._section); |
| |
| return curElement ? curElement._section : null; |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _onNewRuleClick(event) { |
| event.data.consume(); |
| const rule = this._style.parentRule; |
| const range = TextUtils.TextRange.createFromLocation(rule.style.range.endLine, rule.style.range.endColumn + 1); |
| this._parentPane._addBlankSection(this, /** @type {string} */ (rule.styleSheetId), range); |
| } |
| |
| /** |
| * @param {string} propertyName |
| * @param {!Common.Event} event |
| */ |
| _onInsertShadowPropertyClick(propertyName, event) { |
| event.data.consume(true); |
| const treeElement = this.addNewBlankProperty(); |
| treeElement.property.name = propertyName; |
| treeElement.property.value = '0 0 black'; |
| treeElement.updateTitle(); |
| const shadowSwatchPopoverHelper = ShadowSwatchPopoverHelper.forTreeElement(treeElement); |
| if (shadowSwatchPopoverHelper) { |
| shadowSwatchPopoverHelper.showPopover(); |
| } |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _onInsertColorPropertyClick(event) { |
| event.data.consume(true); |
| const treeElement = this.addNewBlankProperty(); |
| treeElement.property.name = 'color'; |
| treeElement.property.value = 'black'; |
| treeElement.updateTitle(); |
| const colorSwatch = ColorSwatchPopoverIcon.forTreeElement(treeElement); |
| if (colorSwatch) { |
| colorSwatch.showPopover(); |
| } |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _onInsertBackgroundColorPropertyClick(event) { |
| event.data.consume(true); |
| const treeElement = this.addNewBlankProperty(); |
| treeElement.property.name = 'background-color'; |
| treeElement.property.value = 'white'; |
| treeElement.updateTitle(); |
| const colorSwatch = ColorSwatchPopoverIcon.forTreeElement(treeElement); |
| if (colorSwatch) { |
| colorSwatch.showPopover(); |
| } |
| } |
| |
| /** |
| * @param {!SDK.CSSModel.Edit} edit |
| */ |
| _styleSheetEdited(edit) { |
| const rule = this._style.parentRule; |
| if (rule) { |
| rule.rebase(edit); |
| } else { |
| this._style.rebase(edit); |
| } |
| |
| this._updateMediaList(); |
| this._updateRuleOrigin(); |
| } |
| |
| /** |
| * @param {!Array.<!SDK.CSSMedia>} mediaRules |
| */ |
| _createMediaList(mediaRules) { |
| for (let i = mediaRules.length - 1; i >= 0; --i) { |
| const media = mediaRules[i]; |
| // Don't display trivial non-print media types. |
| if (!media.text.includes('(') && media.text !== 'print') { |
| continue; |
| } |
| const mediaDataElement = this._mediaListElement.createChild('div', 'media'); |
| const mediaContainerElement = mediaDataElement.createChild('span'); |
| const mediaTextElement = mediaContainerElement.createChild('span', 'media-text'); |
| switch (media.source) { |
| case SDK.CSSMedia.Source.LINKED_SHEET: |
| case SDK.CSSMedia.Source.INLINE_SHEET: |
| mediaTextElement.textContent = 'media="' + media.text + '"'; |
| break; |
| case SDK.CSSMedia.Source.MEDIA_RULE: |
| const decoration = mediaContainerElement.createChild('span'); |
| mediaContainerElement.insertBefore(decoration, mediaTextElement); |
| decoration.textContent = '@media '; |
| mediaTextElement.textContent = media.text; |
| if (media.styleSheetId) { |
| mediaDataElement.classList.add('editable-media'); |
| mediaTextElement.addEventListener( |
| 'click', this._handleMediaRuleClick.bind(this, media, mediaTextElement), false); |
| } |
| break; |
| case SDK.CSSMedia.Source.IMPORT_RULE: |
| mediaTextElement.textContent = '@import ' + media.text; |
| break; |
| } |
| } |
| } |
| |
| _updateMediaList() { |
| this._mediaListElement.removeChildren(); |
| if (this._style.parentRule && this._style.parentRule instanceof SDK.CSSStyleRule) { |
| this._createMediaList(this._style.parentRule.media); |
| } |
| } |
| |
| /** |
| * @param {string} propertyName |
| * @return {boolean} |
| */ |
| isPropertyInherited(propertyName) { |
| if (this._matchedStyles.isInherited(this._style)) { |
| // 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().isPropertyInherited(propertyName); |
| } |
| return false; |
| } |
| |
| /** |
| * @return {?StylePropertiesSection} |
| */ |
| nextEditableSibling() { |
| let curSection = this; |
| do { |
| curSection = curSection.nextSibling(); |
| } while (curSection && !curSection.editable); |
| |
| if (!curSection) { |
| curSection = this.firstSibling(); |
| while (curSection && !curSection.editable) { |
| curSection = curSection.nextSibling(); |
| } |
| } |
| |
| return (curSection && curSection.editable) ? curSection : null; |
| } |
| |
| /** |
| * @return {?StylePropertiesSection} |
| */ |
| previousEditableSibling() { |
| let curSection = this; |
| do { |
| curSection = curSection.previousSibling(); |
| } while (curSection && !curSection.editable); |
| |
| if (!curSection) { |
| curSection = this.lastSibling(); |
| while (curSection && !curSection.editable) { |
| curSection = curSection.previousSibling(); |
| } |
| } |
| |
| return (curSection && curSection.editable) ? curSection : null; |
| } |
| |
| /** |
| * @param {!StylePropertyTreeElement} editedTreeElement |
| */ |
| refreshUpdate(editedTreeElement) { |
| this._parentPane._refreshUpdate(this, editedTreeElement); |
| } |
| |
| /** |
| * @param {!StylePropertyTreeElement} editedTreeElement |
| */ |
| _updateVarFunctions(editedTreeElement) { |
| let child = this.propertiesTreeOutline.firstChild(); |
| while (child) { |
| if (child !== editedTreeElement) { |
| child.updateTitleIfComputedValueChanged(); |
| } |
| child = child.traverseNextTreeElement(false /* skipUnrevealed */, null /* stayWithin */, true /* dontPopulate */); |
| } |
| } |
| |
| /** |
| * @param {boolean} full |
| */ |
| update(full) { |
| this._selectorElement.textContent = this._headerText(); |
| this._markSelectorMatches(); |
| if (full) { |
| this.onpopulate(); |
| } else { |
| let child = this.propertiesTreeOutline.firstChild(); |
| while (child) { |
| child.setOverloaded(this._isPropertyOverloaded(child.property)); |
| child = |
| child.traverseNextTreeElement(false /* skipUnrevealed */, null /* stayWithin */, true /* dontPopulate */); |
| } |
| } |
| } |
| |
| /** |
| * @param {!Event=} event |
| */ |
| _showAllItems(event) { |
| if (event) { |
| event.consume(); |
| } |
| if (this._forceShowAll) { |
| return; |
| } |
| this._forceShowAll = true; |
| this.onpopulate(); |
| } |
| |
| onpopulate() { |
| this._parentPane._setActiveProperty(null); |
| this.propertiesTreeOutline.removeChildren(); |
| const style = this._style; |
| 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 = !!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 = ls`Show All Properties (${properties.length - count} more)`; |
| } else { |
| this._showAllButton.classList.add('hidden'); |
| } |
| } |
| |
| /** |
| * @param {!SDK.CSSProperty} property |
| * @return {boolean} |
| */ |
| _isPropertyOverloaded(property) { |
| return this._matchedStyles.propertyState(property) === SDK.CSSMatchedStyles.PropertyState.Overloaded; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| _updateFilter() { |
| let hasMatchingChild = false; |
| this._showAllItems(); |
| for (const child of this.propertiesTreeOutline.rootElement().children()) { |
| hasMatchingChild |= child._updateFilter(); |
| } |
| |
| const regex = this._parentPane.filterRegex(); |
| const hideRule = !hasMatchingChild && !!regex && !regex.test(this.element.deepTextContent()); |
| this.element.classList.toggle('hidden', hideRule); |
| if (!hideRule && this._style.parentRule) { |
| this._markSelectorHighlights(); |
| } |
| return !hideRule; |
| } |
| |
| _markSelectorMatches() { |
| const rule = this._style.parentRule; |
| if (!rule) { |
| return; |
| } |
| |
| this._mediaListElement.classList.toggle('media-matches', this._matchedStyles.mediaMatches(this._style)); |
| |
| const selectorTexts = rule.selectors.map(selector => selector.text); |
| const matchingSelectorIndexes = this._matchedStyles.matchingSelectors(/** @type {!SDK.CSSStyleRule} */ (rule)); |
| const matchingSelectors = /** @type {!Array<boolean>} */ (new Array(selectorTexts.length).fill(false)); |
| 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(); |
| } |
| |
| /** |
| * @param {!Array<string>} selectors |
| * @param {!Array<boolean>} matchingSelectors |
| * @return {!DocumentFragment} |
| */ |
| _renderHoverableSelectors(selectors, matchingSelectors) { |
| const fragment = createDocumentFragment(); |
| for (let i = 0; i < selectors.length; ++i) { |
| if (i) { |
| fragment.createTextChild(', '); |
| } |
| fragment.appendChild(this._createSelectorElement(selectors[i], matchingSelectors[i], i)); |
| } |
| return fragment; |
| } |
| |
| /** |
| * @param {string} text |
| * @param {boolean} isMatching |
| * @param {number=} navigationIndex |
| * @return {!Element} |
| */ |
| _createSelectorElement(text, isMatching, navigationIndex) { |
| const element = createElementWithClass('span', 'simple-selector'); |
| element.classList.toggle('selector-matches', isMatching); |
| if (typeof navigationIndex === 'number') { |
| element._selectorIndex = navigationIndex; |
| } |
| element.textContent = text; |
| return element; |
| } |
| |
| /** |
| * @param {!Array<string>} selectors |
| * @param {!Array<boolean>} matchingSelectors |
| * @return {!DocumentFragment} |
| */ |
| _renderSimplifiedSelectors(selectors, matchingSelectors) { |
| const fragment = 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() { |
| const selectors = this._selectorElement.getElementsByClassName('simple-selector'); |
| const regex = this._parentPane.filterRegex(); |
| for (let i = 0; i < selectors.length; ++i) { |
| const selectorMatchesFilter = !!regex && regex.test(selectors[i].textContent); |
| selectors[i].classList.toggle('filter-match', selectorMatchesFilter); |
| } |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| _checkWillCancelEditing() { |
| const willCauseCancelEditing = this._willCauseCancelEditing; |
| this._willCauseCancelEditing = false; |
| return willCauseCancelEditing; |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _handleSelectorContainerClick(event) { |
| if (this._checkWillCancelEditing() || !this.editable) { |
| return; |
| } |
| if (event.target === this._selectorContainer) { |
| this.addNewBlankProperty(0).startEditing(); |
| event.consume(true); |
| } |
| } |
| |
| /** |
| * @param {number=} index |
| * @return {!StylePropertyTreeElement} |
| */ |
| addNewBlankProperty(index = this.propertiesTreeOutline.rootElement().childCount()) { |
| const property = this._style.newBlankProperty(index); |
| const item = |
| new StylePropertyTreeElement(this._parentPane, this._matchedStyles, property, false, false, false, true); |
| this.propertiesTreeOutline.insertChild(item, property.index); |
| return item; |
| } |
| |
| _handleEmptySpaceMouseDown() { |
| this._willCauseCancelEditing = this._parentPane._isEditingStyle; |
| this._selectedSinceMouseDown = false; |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _handleEmptySpaceClick(event) { |
| if (!this.editable || this.element.hasSelection() || this._checkWillCancelEditing() || |
| this._selectedSinceMouseDown) { |
| return; |
| } |
| |
| if (event.target.classList.contains('header') || this.element.classList.contains('read-only') || |
| event.target.enclosingNodeOrSelfWithClass('media')) { |
| event.consume(); |
| return; |
| } |
| const deepTarget = event.deepElementFromPoint(); |
| if (deepTarget.treeElement) { |
| this.addNewBlankProperty(deepTarget.treeElement.property.index + 1).startEditing(); |
| } else { |
| this.addNewBlankProperty().startEditing(); |
| } |
| event.consume(true); |
| } |
| |
| /** |
| * @param {!SDK.CSSMedia} media |
| * @param {!Element} element |
| * @param {!Event} event |
| */ |
| _handleMediaRuleClick(media, element, event) { |
| if (UI.isBeingEdited(element)) { |
| return; |
| } |
| |
| if (UI.KeyboardShortcut.eventHasCtrlOrMeta(/** @type {!MouseEvent} */ (event)) && this.navigable) { |
| const location = media.rawLocation(); |
| if (!location) { |
| event.consume(true); |
| return; |
| } |
| const uiLocation = Bindings.cssWorkspaceBinding.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, media), this._editingMediaCancelled.bind(this, element), undefined, |
| this._editingMediaBlurHandler.bind(this)); |
| UI.InplaceEditor.startEditing(element, config); |
| |
| element.getComponentSelection().selectAllChildren(element); |
| this._parentPane.setEditingStyle(true); |
| const parentMediaElement = element.enclosingNodeOrSelfWithClass('media'); |
| parentMediaElement.classList.add('editing-media'); |
| |
| event.consume(true); |
| } |
| |
| /** |
| * @param {!Element} element |
| */ |
| _editingMediaFinished(element) { |
| this._parentPane.setEditingStyle(false); |
| const parentMediaElement = element.enclosingNodeOrSelfWithClass('media'); |
| parentMediaElement.classList.remove('editing-media'); |
| } |
| |
| /** |
| * @param {!Element} element |
| */ |
| _editingMediaCancelled(element) { |
| this._editingMediaFinished(element); |
| // Mark the selectors in group if necessary. |
| // This is overridden by BlankStylePropertiesSection. |
| this._markSelectorMatches(); |
| element.getComponentSelection().collapse(element, 0); |
| } |
| |
| /** |
| * @param {!Element} editor |
| * @param {!Event} blurEvent |
| * @return {boolean} |
| */ |
| _editingMediaBlurHandler(editor, blurEvent) { |
| return true; |
| } |
| |
| /** |
| * @param {!SDK.CSSMedia} media |
| * @param {!Element} element |
| * @param {string} newContent |
| * @param {string} oldContent |
| * @param {(!Elements.StylePropertyTreeElement.Context|undefined)} context |
| * @param {string} moveDirection |
| */ |
| _editingMediaCommitted(media, element, newContent, oldContent, context, moveDirection) { |
| this._parentPane.setEditingStyle(false); |
| this._editingMediaFinished(element); |
| |
| if (newContent) { |
| newContent = newContent.trim(); |
| } |
| |
| /** |
| * @param {boolean} success |
| * @this {StylePropertiesSection} |
| */ |
| function userCallback(success) { |
| 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); |
| this._parentPane.cssModel().setMediaText(media.styleSheetId, media.range, newContent).then(userCallback.bind(this)); |
| } |
| |
| _editingMediaTextCommittedForTest() { |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _handleSelectorClick(event) { |
| if (UI.KeyboardShortcut.eventHasCtrlOrMeta(/** @type {!MouseEvent} */ (event)) && this.navigable && |
| event.target.classList.contains('simple-selector')) { |
| this._navigateToSelectorSource(event.target._selectorIndex, true); |
| event.consume(true); |
| return; |
| } |
| if (this.element.hasSelection()) { |
| return; |
| } |
| this._startEditingAtFirstPosition(); |
| event.consume(true); |
| } |
| |
| /** |
| * @param {number} index |
| * @param {boolean} focus |
| */ |
| _navigateToSelectorSource(index, focus) { |
| const cssModel = this._parentPane.cssModel(); |
| const rule = this._style.parentRule; |
| const header = cssModel.styleSheetHeaderForId(/** @type {string} */ (rule.styleSheetId)); |
| if (!header) { |
| return; |
| } |
| const rawLocation = new SDK.CSSLocation(header, rule.lineNumberInSource(index), rule.columnNumberInSource(index)); |
| StylePropertiesSection._revealSelectorSource(rawLocation, focus); |
| } |
| |
| /** |
| * @param {!SDK.CSSLocation} rawLocation |
| * @param {boolean} focus |
| */ |
| static _revealSelectorSource(rawLocation, focus) { |
| const uiLocation = Bindings.cssWorkspaceBinding.rawLocationToUILocation(rawLocation); |
| if (uiLocation) { |
| Common.Revealer.reveal(uiLocation, !focus); |
| } |
| } |
| |
| _startEditingAtFirstPosition() { |
| if (!this.editable) { |
| return; |
| } |
| |
| if (!this._style.parentRule) { |
| this.moveEditorFromSelector('forward'); |
| return; |
| } |
| |
| this.startEditingSelector(); |
| } |
| |
| startEditingSelector() { |
| const element = this._selectorElement; |
| if (UI.isBeingEdited(element)) { |
| return; |
| } |
| |
| element.scrollIntoViewIfNeeded(false); |
| // Reset selector marks in group, and normalize whitespace. |
| element.textContent = element.textContent.replace(/\s+/g, ' ').trim(); |
| |
| const config = |
| new UI.InplaceEditor.Config(this.editingSelectorCommitted.bind(this), this.editingSelectorCancelled.bind(this)); |
| UI.InplaceEditor.startEditing(this._selectorElement, config); |
| |
| element.getComponentSelection().selectAllChildren(element); |
| this._parentPane.setEditingStyle(true); |
| if (element.classList.contains('simple-selector')) { |
| this._navigateToSelectorSource(0, false); |
| } |
| } |
| |
| /** |
| * @param {string} moveDirection |
| */ |
| moveEditorFromSelector(moveDirection) { |
| this._markSelectorMatches(); |
| |
| if (!moveDirection) { |
| return; |
| } |
| |
| if (moveDirection === 'forward') { |
| let firstChild = this.propertiesTreeOutline.firstChild(); |
| while (firstChild && firstChild.inherited()) { |
| firstChild = firstChild.nextSibling; |
| } |
| if (!firstChild) { |
| this.addNewBlankProperty().startEditing(); |
| } else { |
| firstChild.startEditing(firstChild.nameElement); |
| } |
| } else { |
| const previousSection = this.previousEditableSibling(); |
| if (!previousSection) { |
| return; |
| } |
| |
| previousSection.addNewBlankProperty().startEditing(); |
| } |
| } |
| |
| /** |
| * @param {!Element} element |
| * @param {string} newContent |
| * @param {string} oldContent |
| * @param {(!Elements.StylePropertyTreeElement.Context|undefined)} context |
| * @param {string} moveDirection |
| */ |
| editingSelectorCommitted(element, newContent, oldContent, context, moveDirection) { |
| 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._style.parentRule; |
| if (!rule) { |
| return; |
| } |
| |
| /** |
| * @this {StylePropertiesSection} |
| */ |
| function headerTextCommitted() { |
| 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)); |
| } |
| |
| /** |
| * @param {!SDK.CSSRule} rule |
| * @param {string} newContent |
| * @return {!Promise} |
| */ |
| _setHeaderText(rule, newContent) { |
| /** |
| * @param {!SDK.CSSStyleRule} rule |
| * @param {boolean} success |
| * @return {!Promise} |
| * @this {StylePropertiesSection} |
| */ |
| function onSelectorsUpdated(rule, success) { |
| if (!success) { |
| return Promise.resolve(); |
| } |
| return this._matchedStyles.recomputeMatchingSelectors(rule).then(updateSourceRanges.bind(this, rule)); |
| } |
| |
| /** |
| * @param {!SDK.CSSStyleRule} rule |
| * @this {StylePropertiesSection} |
| */ |
| function updateSourceRanges(rule) { |
| const doesAffectSelectedNode = this._matchedStyles.matchingSelectors(rule).length > 0; |
| this.propertiesTreeOutline.element.classList.toggle('no-affect', !doesAffectSelectedNode); |
| this._matchedStyles.resetActiveProperties(); |
| this._parentPane._refreshUpdate(this); |
| } |
| |
| console.assert(rule instanceof SDK.CSSStyleRule); |
| const oldSelectorRange = rule.selectorRange(); |
| if (!oldSelectorRange) { |
| return Promise.resolve(); |
| } |
| return rule.setSelectorText(newContent) |
| .then(onSelectorsUpdated.bind(this, /** @type {!SDK.CSSStyleRule} */ (rule), oldSelectorRange)); |
| } |
| |
| _editingSelectorCommittedForTest() { |
| } |
| |
| _updateRuleOrigin() { |
| this._selectorRefElement.removeChildren(); |
| this._selectorRefElement.appendChild(StylePropertiesSection.createRuleOriginNode( |
| this._matchedStyles, this._parentPane._linkifier, this._style.parentRule)); |
| } |
| |
| _editingSelectorEnded() { |
| this._parentPane.setEditingStyle(false); |
| } |
| |
| editingSelectorCancelled() { |
| this._editingSelectorEnded(); |
| |
| // Mark the selectors in group if necessary. |
| // This is overridden by BlankStylePropertiesSection. |
| this._markSelectorMatches(); |
| } |
| } |
| |
| StylePropertiesSection.MaxProperties = 50; |
| |
| export class BlankStylePropertiesSection extends StylePropertiesSection { |
| /** |
| * @param {!StylesSidebarPane} stylesPane |
| * @param {!SDK.CSSMatchedStyles} matchedStyles |
| * @param {string} defaultSelectorText |
| * @param {string} styleSheetId |
| * @param {!TextUtils.TextRange} ruleLocation |
| * @param {!SDK.CSSStyleDeclaration} insertAfterStyle |
| */ |
| constructor(stylesPane, matchedStyles, defaultSelectorText, styleSheetId, ruleLocation, insertAfterStyle) { |
| const cssModel = /** @type {!SDK.CSSModel} */ (stylesPane.cssModel()); |
| const rule = SDK.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) { |
| this._createMediaList(insertAfterStyle.parentRule.media); |
| } |
| this.element.classList.add('blank-section'); |
| } |
| |
| /** |
| * @return {!TextUtils.TextRange} |
| */ |
| _actualRuleLocation() { |
| const prefix = this._rulePrefix(); |
| const lines = prefix.split('\n'); |
| const editRange = new TextUtils.TextRange(0, 0, lines.length - 1, lines.peekLast().length); |
| return this._ruleLocation.rebaseAfterTextEdit(TextUtils.TextRange.createFromLocation(0, 0), editRange); |
| } |
| |
| /** |
| * @return {string} |
| */ |
| _rulePrefix() { |
| return this._ruleLocation.startLine === 0 && this._ruleLocation.startColumn === 0 ? '' : '\n\n'; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| get isBlank() { |
| return !this._normal; |
| } |
| |
| /** |
| * @override |
| * @param {!Element} element |
| * @param {string} newContent |
| * @param {string} oldContent |
| * @param {!Elements.StylePropertyTreeElement.Context|undefined} context |
| * @param {string} moveDirection |
| */ |
| editingSelectorCommitted(element, newContent, oldContent, context, moveDirection) { |
| if (!this.isBlank) { |
| super.editingSelectorCommitted(element, newContent, oldContent, context, moveDirection); |
| return; |
| } |
| |
| /** |
| * @param {?SDK.CSSStyleRule} newRule |
| * @return {!Promise} |
| * @this {BlankStylePropertiesSection} |
| */ |
| function onRuleAdded(newRule) { |
| if (!newRule) { |
| this.editingSelectorCancelled(); |
| this._editingSelectorCommittedForTest(); |
| return Promise.resolve(); |
| } |
| return this._matchedStyles.addNewRule(newRule, this._matchedStyles.node()) |
| .then(onAddedToCascade.bind(this, newRule)); |
| } |
| |
| /** |
| * @param {!SDK.CSSStyleRule} newRule |
| * @this {BlankStylePropertiesSection} |
| */ |
| function onAddedToCascade(newRule) { |
| const doesSelectorAffectSelectedNode = this._matchedStyles.matchingSelectors(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 + ' {}'; |
| cssModel.addRule(this._styleSheetId, ruleText, this._ruleLocation).then(onRuleAdded.bind(this)); |
| } |
| |
| /** |
| * @override |
| */ |
| editingSelectorCancelled() { |
| this._parentPane.setUserOperation(false); |
| if (!this.isBlank) { |
| super.editingSelectorCancelled(); |
| return; |
| } |
| |
| this._editingSelectorEnded(); |
| this._parentPane.removeSection(this); |
| } |
| |
| /** |
| * @param {!SDK.CSSRule} newRule |
| */ |
| _makeNormal(newRule) { |
| this.element.classList.remove('blank-section'); |
| this._style = newRule.style; |
| // FIXME: replace this instance by a normal StylePropertiesSection. |
| this._normal = true; |
| } |
| } |
| |
| export class KeyframePropertiesSection extends StylePropertiesSection { |
| /** |
| * @param {!StylesSidebarPane} stylesPane |
| * @param {!SDK.CSSMatchedStyles} matchedStyles |
| * @param {!SDK.CSSStyleDeclaration} style |
| */ |
| constructor(stylesPane, matchedStyles, style) { |
| super(stylesPane, matchedStyles, style); |
| this._selectorElement.className = 'keyframe-key'; |
| } |
| |
| /** |
| * @override |
| * @return {string} |
| */ |
| _headerText() { |
| return this._style.parentRule.key().text; |
| } |
| |
| /** |
| * @override |
| * @param {!SDK.CSSRule} rule |
| * @param {string} newContent |
| * @return {!Promise} |
| */ |
| _setHeaderText(rule, newContent) { |
| /** |
| * @param {boolean} success |
| * @this {KeyframePropertiesSection} |
| */ |
| function updateSourceRanges(success) { |
| if (!success) { |
| return; |
| } |
| this._parentPane._refreshUpdate(this); |
| } |
| |
| console.assert(rule instanceof SDK.CSSKeyframeRule); |
| const oldRange = rule.key().range; |
| if (!oldRange) { |
| return Promise.resolve(); |
| } |
| return rule.setKeyText(newContent).then(updateSourceRanges.bind(this)); |
| } |
| |
| /** |
| * @override |
| * @param {string} propertyName |
| * @return {boolean} |
| */ |
| isPropertyInherited(propertyName) { |
| return false; |
| } |
| |
| /** |
| * @override |
| * @param {!SDK.CSSProperty} property |
| * @return {boolean} |
| */ |
| _isPropertyOverloaded(property) { |
| return false; |
| } |
| |
| /** |
| * @override |
| */ |
| _markSelectorHighlights() { |
| } |
| |
| /** |
| * @override |
| */ |
| _markSelectorMatches() { |
| this._selectorElement.textContent = this._style.parentRule.key().text; |
| } |
| |
| /** |
| * @override |
| */ |
| _highlight() { |
| } |
| } |
| |
| export class CSSPropertyPrompt extends UI.TextPrompt { |
| /** |
| * @param {!StylePropertyTreeElement} treeElement |
| * @param {boolean} isEditingName |
| */ |
| constructor(treeElement, isEditingName) { |
| // Use the same callback both for applyItemCallback and acceptItemCallback. |
| super(); |
| this.initialize(this._buildPropertyCompletions.bind(this), UI.StyleValueDelimiters); |
| this._isColorAware = SDK.cssMetadata().isColorAwareProperty(treeElement.property.name); |
| /** @type {!Array<string>} */ |
| this._cssCompletions = []; |
| if (isEditingName) { |
| this._cssCompletions = SDK.cssMetadata().allProperties(); |
| if (!treeElement.node().isSVGNode()) { |
| this._cssCompletions = this._cssCompletions.filter(property => !SDK.cssMetadata().isSVGProperty(property)); |
| } |
| } else { |
| this._cssCompletions = SDK.cssMetadata().propertyValues(treeElement.nameElement.textContent); |
| } |
| |
| this._treeElement = treeElement; |
| this._isEditingName = isEditingName; |
| this._cssVariables = treeElement.matchedStyles().availableCSSVariables(treeElement.property.ownerStyle); |
| if (this._cssVariables.length < 1000) { |
| this._cssVariables.sort(String.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.isMac() ? 'Cmd' : 'Ctrl'; |
| if (cssValueText.match(/#[\da-f]{3,6}$/i)) { |
| this.setTitle(ls |
| `Increment/decrement with mousewheel or up/down keys. ${cmdOrCtrl}: R ±1, Shift: G ±1, Alt: B ±1`); |
| } else if (cssValueText.match(/\d+/)) { |
| this.setTitle(ls |
| `Increment/decrement with mousewheel or up/down keys. ${cmdOrCtrl}: ±100, Shift: ±10, Alt: ±0.1`); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @override |
| * @param {!Event} event |
| */ |
| onKeyDown(event) { |
| switch (event.key) { |
| case 'ArrowUp': |
| case 'ArrowDown': |
| case 'PageUp': |
| case 'PageDown': |
| if (!this.isSuggestBoxVisible() && this._handleNameOrValueUpDown(event)) { |
| event.preventDefault(); |
| return; |
| } |
| break; |
| case 'Enter': |
| if (event.shiftKey) { |
| return; |
| } |
| // Accept any available autocompletions and advance to the next field. |
| this.tabKeyPressed(); |
| event.preventDefault(); |
| return; |
| } |
| |
| super.onKeyDown(event); |
| } |
| |
| /** |
| * @override |
| * @param {!Event} event |
| */ |
| onMouseWheel(event) { |
| if (this._handleNameOrValueUpDown(event)) { |
| event.consume(true); |
| return; |
| } |
| super.onMouseWheel(event); |
| } |
| |
| /** |
| * @override |
| * @return {boolean} |
| */ |
| tabKeyPressed() { |
| this.acceptAutoComplete(); |
| |
| // Always tab to the next field. |
| return false; |
| } |
| |
| /** |
| * @param {!Event} event |
| * @return {boolean} |
| */ |
| _handleNameOrValueUpDown(event) { |
| /** |
| * @param {string} originalValue |
| * @param {string} replacementString |
| * @this {CSSPropertyPrompt} |
| */ |
| function finishHandler(originalValue, replacementString) { |
| // Synthesize property text disregarding any comments, custom whitespace etc. |
| this._treeElement.applyStyleText( |
| this._treeElement.nameElement.textContent + ': ' + this._treeElement.valueElement.textContent, false); |
| } |
| |
| /** |
| * @param {string} prefix |
| * @param {number} number |
| * @param {string} suffix |
| * @return {string} |
| * @this {CSSPropertyPrompt} |
| */ |
| function customNumberHandler(prefix, number, suffix) { |
| if (number !== 0 && !suffix.length && SDK.cssMetadata().isLengthProperty(this._treeElement.property.name)) { |
| suffix = 'px'; |
| } |
| return prefix + number + suffix; |
| } |
| |
| // Handle numeric value increment/decrement only at this point. |
| if (!this._isEditingName && this._treeElement.valueElement && |
| UI.handleElementValueModifications( |
| event, this._treeElement.valueElement, finishHandler.bind(this), this._isValueSuggestion.bind(this), |
| customNumberHandler.bind(this))) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * @param {string} word |
| * @return {boolean} |
| */ |
| _isValueSuggestion(word) { |
| if (!word) { |
| return false; |
| } |
| word = word.toLowerCase(); |
| return this._cssCompletions.indexOf(word) !== -1 || word.startsWith('--'); |
| } |
| |
| /** |
| * @param {string} expression |
| * @param {string} query |
| * @param {boolean=} force |
| * @return {!Promise<!UI.SuggestBox.Suggestions>} |
| */ |
| _buildPropertyCompletions(expression, query, force) { |
| const lowerQuery = query.toLowerCase(); |
| const editingVariable = !this._isEditingName && expression.trim().endsWith('var('); |
| if (!query && !force && !editingVariable && (this._isEditingName || expression)) { |
| return Promise.resolve([]); |
| } |
| |
| const prefixResults = []; |
| const anywhereResults = []; |
| if (!editingVariable) { |
| this._cssCompletions.forEach(completion => filterCompletions.call(this, completion, false /* variable */)); |
| } |
| if (this._isEditingName) { |
| const nameValuePresets = SDK.cssMetadata().nameValuePresets(this._treeElement.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'}); |
| } |
| 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(); |
| } |
| } |
| } |
| results.forEach(result => { |
| if (editingVariable) { |
| result.title = result.text; |
| result.text += ')'; |
| return; |
| } |
| const valuePreset = SDK.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}; |
| } |
| }); |
| if (this._isColorAware && !this._isEditingName) { |
| results.sort((a, b) => { |
| if (!!a.subtitleRenderer === !!b.subtitleRenderer) { |
| return 0; |
| } |
| return a.subtitleRenderer ? -1 : 1; |
| }); |
| } |
| return Promise.resolve(results); |
| |
| /** |
| * @param {string} completion |
| * @param {boolean} variable |
| * @param {boolean=} nameValue |
| * @this {CSSPropertyPrompt} |
| */ |
| function filterCompletions(completion, variable, nameValue) { |
| const index = completion.toLowerCase().indexOf(lowerQuery); |
| const result = {text: completion}; |
| if (variable) { |
| const computedValue = |
| this._treeElement.matchedStyles().computeCSSVariable(this._treeElement.property.ownerStyle, completion); |
| if (computedValue) { |
| const color = Common.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().propertyUsageWeight(completion) : 1; |
| prefixResults.push(result); |
| } else if (index > -1) { |
| anywhereResults.push(result); |
| } |
| } |
| |
| /** |
| * @param {!Common.Color} color |
| * @return {!Element} |
| */ |
| function swatchRenderer(color) { |
| const swatch = InlineEditor.ColorSwatch.create(); |
| swatch.hideText(true); |
| swatch.setColor(color); |
| swatch.style.pointerEvents = 'none'; |
| return swatch; |
| } |
| } |
| } |
| |
| export class StylesSidebarPropertyRenderer { |
| /** |
| * @param {?SDK.CSSRule} rule |
| * @param {?SDK.DOMNode} node |
| * @param {string} name |
| * @param {string} value |
| */ |
| constructor(rule, node, name, value) { |
| this._rule = rule; |
| this._node = node; |
| this._propertyName = name; |
| this._propertyValue = value; |
| /** @type {?function(string):!Node} */ |
| this._colorHandler = null; |
| /** @type {?function(string):!Node} */ |
| this._bezierHandler = null; |
| /** @type {?function(string, string):!Node} */ |
| this._shadowHandler = null; |
| /** @type {?function(string, string):!Node} */ |
| this._gridHandler = null; |
| /** @type {?function(string):!Node} */ |
| this._varHandler = createTextNode; |
| } |
| |
| /** |
| * @param {function(string):!Node} handler |
| */ |
| setColorHandler(handler) { |
| this._colorHandler = handler; |
| } |
| |
| /** |
| * @param {function(string):!Node} handler |
| */ |
| setBezierHandler(handler) { |
| this._bezierHandler = handler; |
| } |
| |
| /** |
| * @param {function(string, string):!Node} handler |
| */ |
| setShadowHandler(handler) { |
| this._shadowHandler = handler; |
| } |
| |
| /** |
| * @param {function(string, string):!Node} handler |
| */ |
| setGridHandler(handler) { |
| this._gridHandler = handler; |
| } |
| |
| /** |
| * @param {function(string):!Node} handler |
| */ |
| setVarHandler(handler) { |
| this._varHandler = handler; |
| } |
| |
| /** |
| * @return {!Element} |
| */ |
| renderName() { |
| const nameElement = createElement('span'); |
| nameElement.className = 'webkit-css-property'; |
| nameElement.textContent = this._propertyName; |
| nameElement.normalize(); |
| return nameElement; |
| } |
| |
| /** |
| * @return {!Element} |
| */ |
| renderValue() { |
| const valueElement = createElement('span'); |
| valueElement.className = 'value'; |
| if (!this._propertyValue) { |
| return valueElement; |
| } |
| |
| if (this._shadowHandler && (this._propertyName === 'box-shadow' || this._propertyName === 'text-shadow' || |
| this._propertyName === '-webkit-box-shadow') && |
| !SDK.CSSMetadata.VariableRegex.test(this._propertyValue)) { |
| valueElement.appendChild(this._shadowHandler(this._propertyValue, this._propertyName)); |
| valueElement.normalize(); |
| return valueElement; |
| } |
| |
| if (this._gridHandler && SDK.cssMetadata().isGridAreaDefiningProperty(this._propertyName)) { |
| valueElement.appendChild(this._gridHandler(this._propertyValue, this._propertyName)); |
| valueElement.normalize(); |
| return valueElement; |
| } |
| |
| const regexes = [SDK.CSSMetadata.VariableRegex, SDK.CSSMetadata.URLRegex]; |
| const processors = [this._varHandler, this._processURL.bind(this)]; |
| if (this._bezierHandler && SDK.cssMetadata().isBezierAwareProperty(this._propertyName)) { |
| regexes.push(UI.Geometry.CubicBezier.Regex); |
| processors.push(this._bezierHandler); |
| } |
| if (this._colorHandler && SDK.cssMetadata().isColorAwareProperty(this._propertyName)) { |
| regexes.push(Common.Color.Regex); |
| processors.push(this._colorHandler); |
| } |
| const results = TextUtils.TextUtils.splitStringByRegexes(this._propertyValue, regexes); |
| for (let i = 0; i < results.length; i++) { |
| const result = results[i]; |
| const processor = result.regexIndex === -1 ? createTextNode : processors[result.regexIndex]; |
| valueElement.appendChild(processor(result.value)); |
| } |
| valueElement.normalize(); |
| return valueElement; |
| } |
| |
| /** |
| * @param {string} text |
| * @return {!Node} |
| */ |
| _processURL(text) { |
| // Strip "url(" and ")" along with whitespace. |
| let url = text.substring(4, text.length - 1).trim(); |
| const isQuoted = /^'.*'$/.test(url) || /^".*"$/.test(url); |
| if (isQuoted) { |
| url = url.substring(1, url.length - 1); |
| } |
| const container = createDocumentFragment(); |
| container.createTextChild('url('); |
| let hrefUrl = null; |
| if (this._rule && this._rule.resourceURL()) { |
| hrefUrl = Common.ParsedURL.completeURL(this._rule.resourceURL(), url); |
| } else if (this._node) { |
| hrefUrl = this._node.resolveURL(url); |
| } |
| container.appendChild(Components.Linkifier.linkifyURL(hrefUrl || url, {text: url, preventClick: true})); |
| container.createTextChild(')'); |
| return container; |
| } |
| } |
| |
| /** |
| * @implements {UI.ToolbarItem.Provider} |
| */ |
| export class ButtonProvider { |
| constructor() { |
| this._button = new UI.ToolbarButton(Common.UIString('New Style Rule'), 'largeicon-add'); |
| this._button.addEventListener(UI.ToolbarButton.Events.Click, this._clicked, this); |
| const longclickTriangle = UI.Icon.create('largeicon-longclick-triangle', 'long-click-glyph'); |
| this._button.element.appendChild(longclickTriangle); |
| |
| new UI.LongClickController(this._button.element, this._longClicked.bind(this)); |
| UI.context.addFlavorChangeListener(SDK.DOMNode, onNodeChanged.bind(this)); |
| onNodeChanged.call(this); |
| |
| /** |
| * @this {ButtonProvider} |
| */ |
| function onNodeChanged() { |
| let node = UI.context.flavor(SDK.DOMNode); |
| node = node ? node.enclosingElementOrSelf() : null; |
| this._button.setEnabled(!!node); |
| } |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _clicked(event) { |
| StylesSidebarPane._instance._createNewRuleInViaInspectorStyleSheet(); |
| } |
| |
| /** |
| * @param {!Event} e |
| */ |
| _longClicked(e) { |
| StylesSidebarPane._instance._onAddButtonLongClick(e); |
| } |
| |
| /** |
| * @override |
| * @return {!UI.ToolbarItem} |
| */ |
| item() { |
| return this._button; |
| } |
| } |