UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

1,525 lines (1,337 loc) 79 kB
/* * 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. */ Elements.StylesSidebarPane = class extends Elements.ElementsSidebarPane { constructor() { super(); this.setMinimumSize(96, 26); this.registerRequiredCSS('elements/stylesSidebarPane.css'); this.element.tabIndex = -1; 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(Elements.StylesSidebarPane._maxLinkLength, /* useLinkDecorator */ true); /** @type {?Elements.StylePropertyHighlighter} */ this._decorator = null; this._userOperation = false; this._isEditingStyle = false; /** @type {?RegExp} */ this._filterRegex = null; this.contentElement.classList.add('styles-pane'); /** @type {!Array<!Elements.SectionBlock>} */ this._sectionBlocks = []; Elements.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('label', 'dt-icon-label'); exclamationElement.className = 'exclamation-mark'; if (!Elements.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 Elements.StylePropertyHighlighter(this, cssProperty); this._decorator.perform(); this.update(); } forceUpdate() { 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() { // 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 {!Elements.StylePropertiesSection} editedSection * @param {!Elements.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 {Elements.StylesSidebarPane} */ function validateStyles(matchedStyles) { return matchedStyles && matchedStyles.node() === this.node() ? matchedStyles : null; } } /** * @param {boolean} editing */ setEditingStyle(editing) { if (this._isEditingStyle === editing) return; this.contentElement.classList.toggle('is-editing-style', editing); this._isEditingStyle = editing; } /** * @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 {?SDK.CSSMatchedStyles} matchedStyles * @return {!Promise} */ async _innerRebuildUpdate(matchedStyles) { 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 = Elements.SectionBlock.createPseudoTypeBlock(pseudoType); for (const style of matchedStyles.pseudoStyles(pseudoType)) { const section = new Elements.StylePropertiesSection(this, matchedStyles, style); block.sections.push(section); } this._sectionBlocks.push(block); } for (const keyframesRule of matchedStyles.keyframes()) { const block = Elements.SectionBlock.createKeyframesBlock(keyframesRule.name().text); for (const keyframe of keyframesRule.keyframes()) block.sections.push(new Elements.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.<!Elements.SectionBlock>>} */ async _rebuildSectionsForMatchedStyleRules(matchedStyles) { const blocks = [new Elements.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 Elements.SectionBlock._createInheritedNodeBlock(lastParentNode); blocks.push(block); } const section = new Elements.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() || ''; 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 {!Elements.StylePropertiesSection} insertAfterSection * @param {string} styleSheetId * @param {!TextUtils.TextRange} ruleLocation */ _addBlankSection(insertAfterSection, styleSheetId, ruleLocation) { const node = this.node(); const blankSection = new Elements.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 {!Elements.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<!Elements.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 = Elements.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.appendLocationItems('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 {!Elements.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; } } } }; Elements.StylesSidebarPane._maxLinkLength = 30; Elements.SectionBlock = class { /** * @param {?Element} titleElement */ constructor(titleElement) { this._titleElement = titleElement; this.sections = []; } /** * @param {!Protocol.DOM.PseudoType} pseudoType * @return {!Elements.SectionBlock} */ static createPseudoTypeBlock(pseudoType) { const separatorElement = createElement('div'); separatorElement.className = 'sidebar-separator'; separatorElement.textContent = Common.UIString('Pseudo ::%s element', pseudoType); return new Elements.SectionBlock(separatorElement); } /** * @param {string} keyframesName * @return {!Elements.SectionBlock} */ static createKeyframesBlock(keyframesName) { const separatorElement = createElement('div'); separatorElement.className = 'sidebar-separator'; separatorElement.textContent = Common.UIString('@keyframes ' + keyframesName); return new Elements.SectionBlock(separatorElement); } /** * @param {!SDK.DOMNode} node * @return {!Promise<!Elements.SectionBlock>} */ static async _createInheritedNodeBlock(node) { const separatorElement = createElement('div'); separatorElement.className = 'sidebar-separator'; separatorElement.createTextChild(Common.UIString('Inherited from') + ' '); const link = await Common.Linkifier.linkify(node); separatorElement.appendChild(link); return new Elements.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; } }; Elements.StylePropertiesSection = class { /** * @param {!Elements.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._editing = false; 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('mouseleave', this._onMouseOutSelector.bind(this), false); const openBrace = createElement('span'); openBrace.textContent = ' {'; selectorContainer.appendChild(openBrace); 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._setSectionHovered.bind(this, false), 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 Elements.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'); } const throttler = new Common.Throttler(100); this._scheduleHeightUpdate = () => throttler.schedule(this._manuallySetHeight.bind(this)); 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(''); let ruleLocation; if (rule instanceof SDK.CSSStyleRule) ruleLocation = rule.style.range; else if (rule instanceof SDK.CSSKeyframeRule) ruleLocation = rule.key().range; const header = rule.styleSheetId ? matchedStyles.cssModel().styleSheetHeaderForId(rule.styleSheetId) : null; if (ruleLocation && rule.styleSheetId && header && !header.isAnonymousInlineStyleSheet()) { return Elements.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 = Elements.DOMLinkifier.linkifyDeferredNodeReference(header.ownerNode); link.textContent = '<style>…</style>'; return link; } return createTextNode(''); } /** * @param {!SDK.CSSModel} cssModel * @param {!Components.Linkifier} linkifier * @param {string} styleSheetId * @param {!TextUtils.TextRange} ruleLocation * @return {!Node} */ static _linkifyRuleLocation(cssModel, linkifier, styleSheetId, ruleLocation) { const styleSheetHeader = cssModel.styleSheetHeaderForId(styleSheetId); const lineNumber = styleSheetHeader.lineNumberInSource(ruleLocation.startLine); const columnNumber = styleSheetHeader.columnNumberInSource(ruleLocation.startLine, ruleLocation.startColumn); const matchingSelectorLocation = new SDK.CSSLocation(styleSheetHeader, lineNumber, columnNumber); return linkifier.linkifyCSSLocation(matchingSelectorLocation); } /** * @param {!Event} event */ _onKeyDown(event) { if (this._editing || !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 */ _onMouseMove(event) { const hasCtrlOrMeta = UI.KeyboardShortcut.eventHasCtrlOrMeta(/** @type {!MouseEvent} */ (event)); this._setSectionHovered(hasCtrlOrMeta); } /** * @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.call(this, items, false); sectionToolbar.element.addEventListener('mouseenter', setItemsVisibility.bind(this, items, true)); sectionToolbar.element.addEventListener('mouseleave', setItemsVisibility.bind(this, items, false)); UI.ARIAUtils.markAsHidden(sectionToolbar.element); /** * @param {!Array<!UI.ToolbarButton>} items * @param {boolean} value * @this {Elements.StylePropertiesSection} */ function setItemsVisibility(items, value) { for (let i = 0; i < items.length; ++i) items[i].setVisible(value); menuButton.setVisible(!value); if (this._isSASSStyle()) newRuleButton.setVisible(false); } } /** * @return {boolean} */ _isSASSStyle() { const header = this._style.styleSheetId ? this._style.cssModel().styleSheetHeaderForId(this._style.styleSheetId) : null; if (!header) return false; const sourceMap = header.cssModel().sourceMapManager().sourceMapForClient(header); return sourceMap ? sourceMap.editable() : false; } /** * @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 node.nodeNameInCorrectCase() + '[' + Common.UIString('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); } _highlight() { SDK.OverlayModel.hideDOMNodeHighlight(); const node = this._parentPane.node(); if (!node) return; const selectors = this._style.parentRule ? this._style.parentRule.selectorText() : undefined; node.domModel().overlayModel().highlightDOMNodeWithConfig( node.id, {mode: 'all', showInfo: undefined, selectors: selectors}); } /** * @return {?Elements.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 {?Elements.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 {?Elements.StylePropertiesSection} */ nextSibling() { let curElement = this.element; do curElement = curElement.nextSibling; while (curElement && !curElement._section); return curElement ? curElement._section : null; } /** * @return {?Elements.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 = Elements.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 = Elements.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 = Elements.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 {?Elements.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 {?Elements.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 {!Elements.StylePropertyTreeElement} editedTreeElement */ refreshUpdate(editedTreeElement) { this._parentPane._refreshUpdate(this, editedTreeElement); } /** * @param {!Elements.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.propertiesTreeOutline.removeChildren(); const style = this._style; let count = 0; const properties = style.leadingProperties(); const maxProperties = Elements.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); const item = new Elements.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 {!Elements.StylePropertyTreeElement} */ addNewBlankProperty(index = this.propertiesTreeOutline.rootElement().childCount()) { const property = this._style.newBlankProperty(index); const item = new Elements.StylePropertyTreeElement( this._parentPane, this._matchedStyles, property, false, false, false, true); this.propertiesTreeOutline.insertChild(item, property.index); return item; } _handleEmptySpaceMouseDown() { this._willCauseCancelEditing = this._parentPane._isEditingStyle; } /** * @param {!Event} event */ _handleEmpt