| /* |
| * 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'); |
| |
| 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._sectionsContainer = this.contentElement.createChild('div'); |
| 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; |
| |
| /** @type {?Elements.StylePropertyTreeElement} */ |
| this._mouseDownTreeElement = null; |
| this._mouseDownTreeElementIsName = false; |
| this._mouseDownTreeElementIsValue = false; |
| |
| 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); |
| } |
| |
| /** |
| * @param {!SDK.CSSProperty} property |
| * @return {!Element} |
| */ |
| static createExclamationMark(property) { |
| var 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); |
| } |
| |
| var 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; |
| |
| var 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 |
| * @param {string} activeClassName |
| * @return {!Element} |
| */ |
| static createPropertyFilterElement(placeholder, container, filterCallback, activeClassName) { |
| var input = createElementWithClass('input'); |
| input.placeholder = placeholder; |
| |
| function searchHandler() { |
| var regex = input.value ? new RegExp(input.value.escapeForRegExp(), 'i') : null; |
| filterCallback(regex); |
| container.classList.toggle(activeClassName, !!input.value); |
| } |
| 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 |
| */ |
| _onAddButtonLongClick(event) { |
| var cssModel = this.cssModel(); |
| if (!cssModel) |
| return; |
| var headers = cssModel.styleSheetHeaders().filter(styleSheetResourceHeader); |
| |
| /** @type {!Array.<{text: string, handler: function()}>} */ |
| var contextMenuDescriptors = []; |
| for (var i = 0; i < headers.length; ++i) { |
| var header = headers[i]; |
| var handler = this._createNewRuleInStyleSheet.bind(this, header); |
| contextMenuDescriptors.push({text: Bindings.displayNameForURL(header.resourceURL()), handler: handler}); |
| } |
| |
| contextMenuDescriptors.sort(compareDescriptors); |
| |
| var contextMenu = new UI.ContextMenu(event); |
| for (var i = 0; i < contextMenuDescriptors.length; ++i) { |
| var descriptor = contextMenuDescriptors[i]; |
| contextMenu.appendItem(descriptor.text, descriptor.handler); |
| } |
| if (!contextMenu.isEmpty()) |
| contextMenu.appendSeparator(); |
| contextMenu.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 |
| */ |
| _refreshUpdate(editedSection) { |
| var node = this.node(); |
| if (!node) |
| return; |
| |
| var fullRefresh = Runtime.experiments.isEnabled('liveSASS'); |
| for (var section of this.allSections()) { |
| if (section.isBlank) |
| continue; |
| section.update(fullRefresh || 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() { |
| var 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() { |
| var 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) { |
| var edit = event && event.data ? /** @type {?SDK.CSSModel.Edit} */ (event.data.edit) : null; |
| if (edit) { |
| for (var section of this.allSections()) |
| section._styleSheetEdited(edit); |
| return; |
| } |
| |
| if (this._userOperation || this._isEditingStyle) |
| return; |
| |
| this._resetCache(); |
| this.update(); |
| } |
| |
| /** |
| * @param {?SDK.CSSMatchedStyles} matchedStyles |
| */ |
| _innerRebuildUpdate(matchedStyles) { |
| this._linkifier.reset(); |
| this._sectionsContainer.removeChildren(); |
| this._sectionBlocks = []; |
| |
| var node = this.node(); |
| if (!matchedStyles || !node) |
| return; |
| |
| this._sectionBlocks = this._rebuildSectionsForMatchedStyleRules(matchedStyles); |
| var pseudoTypes = []; |
| var keys = new Set(matchedStyles.pseudoStyles().keys()); |
| if (keys.delete(Protocol.DOM.PseudoType.Before)) |
| pseudoTypes.push(Protocol.DOM.PseudoType.Before); |
| pseudoTypes = pseudoTypes.concat(keys.valuesArray().sort()); |
| for (var pseudoType of pseudoTypes) { |
| var block = Elements.SectionBlock.createPseudoTypeBlock(pseudoType); |
| var styles = |
| /** @type {!Array<!SDK.CSSStyleDeclaration>} */ (matchedStyles.pseudoStyles().get(pseudoType)); |
| for (var style of styles) { |
| var section = new Elements.StylePropertiesSection(this, matchedStyles, style); |
| block.sections.push(section); |
| } |
| this._sectionBlocks.push(block); |
| } |
| |
| for (var keyframesRule of matchedStyles.keyframes()) { |
| var block = Elements.SectionBlock.createKeyframesBlock(keyframesRule.name().text); |
| for (var keyframe of keyframesRule.keyframes()) |
| block.sections.push(new Elements.KeyframePropertiesSection(this, matchedStyles, keyframe.style)); |
| this._sectionBlocks.push(block); |
| } |
| |
| for (var block of this._sectionBlocks) { |
| var titleElement = block.titleElement(); |
| if (titleElement) |
| this._sectionsContainer.appendChild(titleElement); |
| for (var section of block.sections) |
| this._sectionsContainer.appendChild(section.element); |
| } |
| |
| if (this._filterRegex) |
| this._updateFilter(); |
| |
| this._nodeStylesUpdatedForTest(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 {!Array.<!Elements.SectionBlock>} |
| */ |
| _rebuildSectionsForMatchedStyleRules(matchedStyles) { |
| var blocks = [new Elements.SectionBlock(null)]; |
| var lastParentNode = null; |
| for (var style of matchedStyles.nodeStyles()) { |
| var parentNode = matchedStyles.isInherited(style) ? matchedStyles.nodeForStyle(style) : null; |
| if (parentNode && parentNode !== lastParentNode) { |
| lastParentNode = parentNode; |
| var block = Elements.SectionBlock.createInheritedNodeBlock(lastParentNode); |
| blocks.push(block); |
| } |
| |
| var section = new Elements.StylePropertiesSection(this, matchedStyles, style); |
| blocks.peekLast().sections.push(section); |
| } |
| return blocks; |
| } |
| |
| async _createNewRuleInViaInspectorStyleSheet() { |
| var cssModel = this.cssModel(); |
| var node = this.node(); |
| if (!cssModel || !node) |
| return; |
| this._userOperation = true; |
| |
| var styleSheetHeader = await cssModel.requestViaInspectorStylesheet(/** @type {!SDK.DOMNode} */ (node)); |
| |
| this._userOperation = false; |
| this._createNewRuleInStyleSheet(styleSheetHeader); |
| } |
| |
| /** |
| * @param {?SDK.CSSStyleSheetHeader} styleSheetHeader |
| */ |
| _createNewRuleInStyleSheet(styleSheetHeader) { |
| if (!styleSheetHeader) |
| return; |
| styleSheetHeader.requestContent().then(onStyleSheetContent.bind(this, styleSheetHeader.id)); |
| |
| /** |
| * @param {string} styleSheetId |
| * @param {?string} text |
| * @this {Elements.StylesSidebarPane} |
| */ |
| function onStyleSheetContent(styleSheetId, text) { |
| text = text || ''; |
| var lines = text.split('\n'); |
| var range = TextUtils.TextRange.createFromLocation(lines.length - 1, lines[lines.length - 1].length); |
| this._addBlankSection(this._sectionBlocks[0].sections[0], styleSheetId, range); |
| } |
| } |
| |
| /** |
| * @param {!Elements.StylePropertiesSection} insertAfterSection |
| * @param {string} styleSheetId |
| * @param {!TextUtils.TextRange} ruleLocation |
| */ |
| _addBlankSection(insertAfterSection, styleSheetId, ruleLocation) { |
| var node = this.node(); |
| var blankSection = new Elements.BlankStylePropertiesSection( |
| this, insertAfterSection._matchedStyles, node ? Components.DOMPresentationUtils.simpleSelector(node) : '', |
| styleSheetId, ruleLocation, insertAfterSection._style); |
| |
| this._sectionsContainer.insertBefore(blankSection.element, insertAfterSection.element.nextSibling); |
| |
| for (var block of this._sectionBlocks) { |
| var 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 (var block of this._sectionBlocks) { |
| var index = block.sections.indexOf(section); |
| if (index === -1) |
| continue; |
| block.sections.splice(index, 1); |
| section.element.remove(); |
| } |
| } |
| |
| /** |
| * @return {?RegExp} |
| */ |
| filterRegex() { |
| return this._filterRegex; |
| } |
| |
| _updateFilter() { |
| for (var block of this._sectionBlocks) |
| block.updateFilter(); |
| } |
| |
| /** |
| * @override |
| */ |
| willHide() { |
| this._swatchPopoverHelper.hide(); |
| super.willHide(); |
| } |
| |
| /** |
| * @return {!Array<!Elements.StylePropertiesSection>} |
| */ |
| allSections() { |
| var sections = []; |
| for (var 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() { |
| var container = this.contentElement.createChild('div', 'styles-sidebar-pane-toolbar-container'); |
| var hbox = container.createChild('div', 'hbox styles-sidebar-pane-toolbar'); |
| var filterContainerElement = hbox.createChild('div', 'styles-sidebar-pane-filter-box'); |
| var filterInput = Elements.StylesSidebarPane.createPropertyFilterElement( |
| Common.UIString('Filter'), hbox, this._onFilterChanged.bind(this), 'styles-filter-engaged'); |
| UI.ARIAUtils.setAccessibleName(filterInput, Common.UIString('Filter Styles')); |
| filterContainerElement.appendChild(filterInput); |
| var toolbar = new UI.Toolbar('styles-pane-toolbar', hbox); |
| toolbar.makeToggledGray(); |
| toolbar.appendLocationItems('styles-sidebarpane-toolbar'); |
| var toolbarPaneContainer = container.createChild('div', 'styles-sidebar-toolbar-pane-container'); |
| var 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); |
| |
| var 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) { |
| var 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) { |
| var separatorElement = createElement('div'); |
| separatorElement.className = 'sidebar-separator'; |
| separatorElement.textContent = Common.UIString('@keyframes ' + keyframesName); |
| return new Elements.SectionBlock(separatorElement); |
| } |
| |
| /** |
| * @param {!SDK.DOMNode} node |
| * @return {!Elements.SectionBlock} |
| */ |
| static createInheritedNodeBlock(node) { |
| var separatorElement = createElement('div'); |
| separatorElement.className = 'sidebar-separator'; |
| var link = Components.DOMPresentationUtils.linkifyNodeReference(node); |
| separatorElement.createTextChild(Common.UIString('Inherited from') + ' '); |
| separatorElement.appendChild(link); |
| return new Elements.SectionBlock(separatorElement); |
| } |
| |
| updateFilter() { |
| var hasAnyVisibleSection = false; |
| for (var section of this.sections) |
| hasAnyVisibleSection |= section._updateFilter(); |
| if (this._titleElement) |
| this._titleElement.classList.toggle('hidden', !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; |
| /** @type {?function(!Elements.StylePropertiesSection)} */ |
| this._afterUpdate = null; |
| this._willCauseCancelEditing = false; |
| |
| var rule = style.parentRule; |
| this.element = createElementWithClass('div', 'styles-section matched-styles monospace'); |
| 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.registerRequiredCSS('elements/stylesSectionTree.css'); |
| this.propertiesTreeOutline.element.classList.add('style-properties', 'matched-styles', 'monospace'); |
| this.propertiesTreeOutline.section = this; |
| this._innerElement.appendChild(this.propertiesTreeOutline.element); |
| |
| var 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); |
| |
| var openBrace = createElement('span'); |
| openBrace.textContent = ' {'; |
| selectorContainer.appendChild(openBrace); |
| selectorContainer.addEventListener('mousedown', this._handleEmptySpaceMouseDown.bind(this), false); |
| selectorContainer.addEventListener('click', this._handleSelectorContainerClick.bind(this), false); |
| |
| var 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) { |
| var 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'); |
| } |
| |
| var 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(''); |
| |
| var ruleLocation; |
| if (rule instanceof SDK.CSSStyleRule) |
| ruleLocation = rule.style.range; |
| else if (rule instanceof SDK.CSSKeyframeRule) |
| ruleLocation = rule.key().range; |
| |
| var 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) { |
| var link = Components.DOMPresentationUtils.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) { |
| var styleSheetHeader = cssModel.styleSheetHeaderForId(styleSheetId); |
| var lineNumber = styleSheetHeader.lineNumberInSource(ruleLocation.startLine); |
| var columnNumber = styleSheetHeader.columnNumberInSource(ruleLocation.startLine, ruleLocation.startColumn); |
| var matchingSelectorLocation = new SDK.CSSLocation(styleSheetHeader, lineNumber, columnNumber); |
| return linkifier.linkifyCSSLocation(matchingSelectorLocation); |
| } |
| |
| /** |
| * @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) { |
| var hasCtrlOrMeta = UI.KeyboardShortcut.eventHasCtrlOrMeta(/** @type {!MouseEvent} */ (event)); |
| this._setSectionHovered(hasCtrlOrMeta); |
| } |
| |
| /** |
| * @param {!Element} container |
| */ |
| _createHoverMenuToolbar(container) { |
| if (!this.editable) |
| return; |
| var items = []; |
| |
| var textShadowButton = new UI.ToolbarButton(Common.UIString('Add text-shadow'), 'largeicon-text-shadow'); |
| textShadowButton.addEventListener( |
| UI.ToolbarButton.Events.Click, this._onInsertShadowPropertyClick.bind(this, 'text-shadow')); |
| items.push(textShadowButton); |
| |
| var boxShadowButton = new UI.ToolbarButton(Common.UIString('Add box-shadow'), 'largeicon-box-shadow'); |
| boxShadowButton.addEventListener( |
| UI.ToolbarButton.Events.Click, this._onInsertShadowPropertyClick.bind(this, 'box-shadow')); |
| items.push(boxShadowButton); |
| |
| var colorButton = new UI.ToolbarButton(Common.UIString('Add color'), 'largeicon-foreground-color'); |
| colorButton.addEventListener(UI.ToolbarButton.Events.Click, this._onInsertColorPropertyClick, this); |
| items.push(colorButton); |
| |
| var backgroundButton = new UI.ToolbarButton(Common.UIString('Add background-color'), 'largeicon-background-color'); |
| backgroundButton.addEventListener(UI.ToolbarButton.Events.Click, this._onInsertBackgroundColorPropertyClick, this); |
| items.push(backgroundButton); |
| |
| var 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); |
| items.push(newRuleButton); |
| } |
| |
| var sectionToolbar = new UI.Toolbar('sidebar-pane-section-toolbar', container); |
| for (var i = 0; i < items.length; ++i) |
| sectionToolbar.appendToolbarItem(items[i]); |
| |
| var menuButton = new UI.ToolbarButton(Common.UIString('More tools\u2026'), 'largeicon-menu'); |
| 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)); |
| |
| /** |
| * @param {!Array<!UI.ToolbarButton>} items |
| * @param {boolean} value |
| * @this {Elements.StylePropertiesSection} |
| */ |
| function setItemsVisibility(items, value) { |
| for (var i = 0; i < items.length; ++i) |
| items[i].setVisible(value); |
| menuButton.setVisible(!value); |
| if (this._isSASSStyle()) |
| newRuleButton.setVisible(false); |
| } |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| _isSASSStyle() { |
| var header = |
| this._style.styleSheetId ? this._style.cssModel().styleSheetHeaderForId(this._style.styleSheetId) : null; |
| if (!header) |
| return false; |
| var sourceMap = header.cssModel().sourceMapManager().sourceMapForClient(header); |
| return sourceMap ? sourceMap.editable() : false; |
| } |
| |
| /** |
| * @return {!SDK.CSSStyleDeclaration} |
| */ |
| style() { |
| return this._style; |
| } |
| |
| /** |
| * @return {string} |
| */ |
| _headerText() { |
| var 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(); |
| var node = this._parentPane.node(); |
| var 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() { |
| var parent = this.element.parentElement; |
| if (!parent) |
| return null; |
| |
| var childElement = parent.firstChild; |
| while (childElement) { |
| if (childElement._section) |
| return childElement._section; |
| childElement = childElement.nextSibling; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * @return {?Elements.StylePropertiesSection} |
| */ |
| lastSibling() { |
| var parent = this.element.parentElement; |
| if (!parent) |
| return null; |
| |
| var childElement = parent.lastChild; |
| while (childElement) { |
| if (childElement._section) |
| return childElement._section; |
| childElement = childElement.previousSibling; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * @return {?Elements.StylePropertiesSection} |
| */ |
| nextSibling() { |
| var curElement = this.element; |
| do |
| curElement = curElement.nextSibling; |
| while (curElement && !curElement._section); |
| |
| return curElement ? curElement._section : null; |
| } |
| |
| /** |
| * @return {?Elements.StylePropertiesSection} |
| */ |
| previousSibling() { |
| var 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(); |
| var rule = this._style.parentRule; |
| var 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); |
| var treeElement = this.addNewBlankProperty(); |
| treeElement.property.name = propertyName; |
| treeElement.property.value = '0 0 black'; |
| treeElement.updateTitle(); |
| var shadowSwatchPopoverHelper = Elements.ShadowSwatchPopoverHelper.forTreeElement(treeElement); |
| if (shadowSwatchPopoverHelper) |
| shadowSwatchPopoverHelper.showPopover(); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _onInsertColorPropertyClick(event) { |
| event.data.consume(true); |
| var treeElement = this.addNewBlankProperty(); |
| treeElement.property.name = 'color'; |
| treeElement.property.value = 'black'; |
| treeElement.updateTitle(); |
| var colorSwatch = Elements.ColorSwatchPopoverIcon.forTreeElement(treeElement); |
| if (colorSwatch) |
| colorSwatch.showPopover(); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _onInsertBackgroundColorPropertyClick(event) { |
| event.data.consume(true); |
| var treeElement = this.addNewBlankProperty(); |
| treeElement.property.name = 'background-color'; |
| treeElement.property.value = 'white'; |
| treeElement.updateTitle(); |
| var colorSwatch = Elements.ColorSwatchPopoverIcon.forTreeElement(treeElement); |
| if (colorSwatch) |
| colorSwatch.showPopover(); |
| } |
| |
| /** |
| * @param {!SDK.CSSModel.Edit} edit |
| */ |
| _styleSheetEdited(edit) { |
| var 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 (var i = mediaRules.length - 1; i >= 0; --i) { |
| var media = mediaRules[i]; |
| // Don't display trivial non-print media types. |
| if (!media.text.includes('(') && media.text !== 'print') |
| continue; |
| var mediaDataElement = this._mediaListElement.createChild('div', 'media'); |
| var mediaContainerElement = mediaDataElement.createChild('span'); |
| var 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: |
| var 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() { |
| var 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() { |
| var 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 {boolean} full |
| */ |
| update(full) { |
| this._selectorElement.textContent = this._headerText(); |
| this._markSelectorMatches(); |
| if (full) { |
| this.propertiesTreeOutline.removeChildren(); |
| this.onpopulate(); |
| } else { |
| var child = this.propertiesTreeOutline.firstChild(); |
| while (child) { |
| child.setOverloaded(this._isPropertyOverloaded(child.property)); |
| child = child.traverseNextTreeElement(false, null, true); |
| } |
| } |
| this.afterUpdate(); |
| } |
| |
| afterUpdate() { |
| if (this._afterUpdate) { |
| this._afterUpdate(this); |
| this._afterUpdate = null; |
| this._afterUpdateFinishedForTest(); |
| } |
| } |
| |
| _afterUpdateFinishedForTest() { |
| } |
| |
| onpopulate() { |
| var style = this._style; |
| for (var property of style.leadingProperties()) { |
| var isShorthand = !!style.longhandProperties(property.name).length; |
| var inherited = this.isPropertyInherited(property.name); |
| var overloaded = this._isPropertyOverloaded(property); |
| var item = new Elements.StylePropertyTreeElement( |
| this._parentPane, this._matchedStyles, property, isShorthand, inherited, overloaded); |
| this.propertiesTreeOutline.appendChild(item); |
| } |
| } |
| |
| /** |
| * @param {!SDK.CSSProperty} property |
| * @return {boolean} |
| */ |
| _isPropertyOverloaded(property) { |
| return this._matchedStyles.propertyState(property) === SDK.CSSMatchedStyles.PropertyState.Overloaded; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| _updateFilter() { |
| var hasMatchingChild = false; |
| for (var child of this.propertiesTreeOutline.rootElement().children()) |
| hasMatchingChild |= child._updateFilter(); |
| |
| var regex = this._parentPane.filterRegex(); |
| var hideRule = !hasMatchingChild && !!regex && !regex.test(this.element.deepTextContent()); |
| this.element.classList.toggle('hidden', hideRule); |
| if (!hideRule && this._style.parentRule) |
| this._markSelectorHighlights(); |
| return !hideRule; |
| } |
| |
| _markSelectorMatches() { |
| var rule = this._style.parentRule; |
| if (!rule) |
| return; |
| |
| this._mediaListElement.classList.toggle('media-matches', this._matchedStyles.mediaMatches(this._style)); |
| |
| var selectorTexts = rule.selectors.map(selector => selector.text); |
| var matchingSelectorIndexes = this._matchedStyles.matchingSelectors(/** @type {!SDK.CSSStyleRule} */ (rule)); |
| var matchingSelectors = /** @type {!Array<boolean>} */ (new Array(selectorTexts.length).fill(false)); |
| for (var matchingIndex of matchingSelectorIndexes) |
| matchingSelectors[matchingIndex] = true; |
| |
| if (this._parentPane._isEditingStyle) |
| return; |
| |
| var 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) { |
| var fragment = createDocumentFragment(); |
| for (var 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) { |
| var 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) { |
| var fragment = createDocumentFragment(); |
| var currentMatching = false; |
| var text = ''; |
| for (var 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() { |
| var selectors = this._selectorElement.getElementsByClassName('simple-selector'); |
| var regex = this._parentPane.filterRegex(); |
| for (var i = 0; i < selectors.length; ++i) { |
| var selectorMatchesFilter = !!regex && regex.test(selectors[i].textContent); |
| selectors[i].classList.toggle('filter-match', selectorMatchesFilter); |
| } |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| _checkWillCancelEditing() { |
| var 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) { |
| var property = this._style.newBlankProperty(index); |
| var item = |
| new Elements.StylePropertyTreeElement(this._parentPane, this._matchedStyles, property, false, false, false); |
| index = property.index; |
| this.propertiesTreeOutline.insertChild(item, index); |
| item.listItemElement.textContent = ''; |
| item._newProperty = true; |
| item.updateTitle(); |
| return item; |
| } |
| |
| _handleEmptySpaceMouseDown() { |
| this._willCauseCancelEditing = this._parentPane._isEditingStyle; |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _handleEmptySpaceClick(event) { |
| if (!this.editable || this.element.hasSelection() || this._checkWillCancelEditing()) |
| return; |
| |
| if (event.target.classList.contains('header') || this.element.classList.contains('read-only') || |
| event.target.enclosingNodeOrSelfWithClass('media')) { |
| event.consume(); |
| return; |
| } |
| 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) { |
| var location = media.rawLocation(); |
| if (!location) { |
| event.consume(true); |
| return; |
| } |
| var uiLocation = Bindings.cssWorkspaceBinding.rawLocationToUILocation(location); |
| if (uiLocation) |
| Common.Revealer.reveal(uiLocation); |
| event.consume(true); |
| return; |
| } |
| |
| if (!this.editable || this._isSASSStyle()) |
| return; |
| |
| var 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); |
| this._startEditing(); |
| |
| element.getComponentSelection().selectAllChildren(element); |
| this._parentPane.setEditingStyle(true); |
| var parentMediaElement = element.enclosingNodeOrSelfWithClass('media'); |
| parentMediaElement.classList.add('editing-media'); |
| |
| event.consume(true); |
| } |
| |
| /** |
| * @param {!Element} element |
| */ |
| _editingMediaFinished(element) { |
| this._parentPane.setEditingStyle(false); |
| var parentMediaElement = element.enclosingNodeOrSelfWithClass('media'); |
| parentMediaElement.classList.remove('editing-media'); |
| this._stopEditing(); |
| } |
| |
| /** |
| * @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 {Elements.StylePropertiesSection} |
| */ |
| function userCallback(success) { |
| if (success) { |
| this._matchedStyles.resetActiveProperties(); |
| this._parentPane._refreshUpdate(this); |
| } |
| this._parentPane._userOperation = false; |
| this._editingMediaTextCommittedForTest(); |
| } |
| |
| // This gets deleted in finishOperation(), which is called both on success and failure. |
| this._parentPane._userOperation = 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; |
| } |
| this._startEditingOnMouseEvent(); |
| event.consume(true); |
| } |
| |
| /** |
| * @param {number} index |
| * @param {boolean} focus |
| */ |
| _navigateToSelectorSource(index, focus) { |
| var cssModel = this._parentPane.cssModel(); |
| var rule = this._style.parentRule; |
| var header = cssModel.styleSheetHeaderForId(/** @type {string} */ (rule.styleSheetId)); |
| if (!header) |
| return; |
| var rawLocation = new SDK.CSSLocation(header, rule.lineNumberInSource(index), rule.columnNumberInSource(index)); |
| var uiLocation = Bindings.cssWorkspaceBinding.rawLocationToUILocation(rawLocation); |
| if (uiLocation) |
| Common.Revealer.reveal(uiLocation, !focus); |
| } |
| |
| _startEditingOnMouseEvent() { |
| if (!this.editable || this._isSASSStyle()) |
| return; |
| |
| var rule = this._style.parentRule; |
| if (!rule && !this.propertiesTreeOutline.rootElement().childCount()) { |
| this.addNewBlankProperty().startEditing(); |
| return; |
| } |
| |
| if (!rule) |
| return; |
| |
| this.startEditingSelector(); |
| } |
| |
| startEditingSelector() { |
| var element = this._selectorElement; |
| if (UI.isBeingEdited(element)) |
| return; |
| |
| element.scrollIntoViewIfNeeded(false); |
| element.textContent = element.textContent; // Reset selector marks in group. |
| |
| var config = |
| new UI.InplaceEditor.Config(this.editingSelectorCommitted.bind(this), this.editingSelectorCancelled.bind(this)); |
| UI.InplaceEditor.startEditing(this._selectorElement, config); |
| this._startEditing(); |
| |
| 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') { |
| var firstChild = this.propertiesTreeOutline.firstChild(); |
| while (firstChild && firstChild.inherited()) |
| firstChild = firstChild.nextSibling; |
| if (!firstChild) |
| this.addNewBlankProperty().startEditing(); |
| else |
| firstChild.startEditing(firstChild.nameElement); |
| } else { |
| var 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; |
| } |
| var rule = this._style.parentRule; |
| if (!rule) |
| return; |
| |
| /** |
| * @this {Elements.StylePropertiesSection} |
| */ |
| function headerTextCommitted() { |
| this._parentPane._userOperation = false; |
| this._moveEditorFromSelector(moveDirection); |
| this._editingSelectorCommittedForTest(); |
| } |
| |
| // This gets deleted in finishOperationAndMoveEditor(), which is called both on success and failure. |
| this._parentPane._userOperation = 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 {Elements.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 {Elements.StylePropertiesSection} |
| */ |
| function updateSourceRanges(rule) { |
| var 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); |
| var 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(Elements.StylePropertiesSection.createRuleOriginNode( |
| this._matchedStyles, this._parentPane._linkifier, this._style.parentRule)); |
| } |
| |
| _editingSelectorEnded() { |
| this._parentPane.setEditingStyle(false); |
| this._stopEditing(); |
| } |
| |
| editingSelectorCancelled() { |
| this._editingSelectorEnded(); |
| |
| // Mark the selectors in group if necessary. |
| // This is overridden by BlankStylePropertiesSection. |
| this._markSelectorMatches(); |
| } |
| |
| _startEditing() { |
| this._manuallySetHeight(); |
| this.element.addEventListener('input', this._scheduleHeightUpdate, true); |
| } |
| |
| /** |
| * @return {!Promise} |
| */ |
| _manuallySetHeight() { |
| this.element.style.height = (this._innerElement.clientHeight + 1) + 'px'; |
| this.element.style.contain = 'strict'; |
| return Promise.resolve(); |
| } |
| |
| _stopEditing() { |
| this.element.style.removeProperty('height'); |
| this.element.style.removeProperty('contain'); |
| this.element.removeEventListener('input', this._scheduleHeightUpdate, true); |
| } |
| }; |
| |
| Elements.BlankStylePropertiesSection = class extends Elements.StylePropertiesSection { |
| /** |
| * @param {!Elements.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) { |
| var cssModel = /** @type {!SDK.CSSModel} */ (stylesPane.cssModel()); |
| var 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(Elements.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() { |
| var prefix = this._rulePrefix(); |
| var lines = prefix.split('\n'); |
| var 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 {Elements.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 {Elements.BlankStylePropertiesSection} |
| */ |
| function onAddedToCascade(newRule) { |
| var doesSelectorAffectSelectedNode = this._matchedStyles.matchingSelectors(newRule).length > 0; |
| this._makeNormal(newRule); |
| |
| if (!doesSelectorAffectSelectedNode) |
| this.propertiesTreeOutline.element.classList.add('no-affect'); |
| |
| this._updateRuleOrigin(); |
| if (this.element.parentElement) // Might have been detached already. |
| this._moveEditorFromSelector(moveDirection); |
| |
| this._parentPane._userOperation = false; |
| this._editingSelectorEnded(); |
| this._markSelectorMatches(); |
| |
| this._editingSelectorCommittedForTest(); |
| } |
| |
| if (newContent) |
| newContent = newContent.trim(); |
| this._parentPane._userOperation = true; |
| |
| var cssModel = this._parentPane.cssModel(); |
| var ruleText = this._rulePrefix() + newContent + ' {}'; |
| cssModel.addRule(this._styleSheetId, ruleText, this._ruleLocation).then(onRuleAdded.bind(this)); |
| } |
| |
| /** |
| * @override |
| */ |
| editingSelectorCancelled() { |
| this._parentPane._userOperation = 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 Elements.StylePropertiesSection. |
| this._normal = true; |
| } |
| }; |
| |
| Elements.KeyframePropertiesSection = class extends Elements.StylePropertiesSection { |
| /** |
| * @param {!Elements.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 {Elements.KeyframePropertiesSection} |
| */ |
| function updateSourceRanges(success) { |
| if (!success) |
| return; |
| this._parentPane._refreshUpdate(this); |
| } |
| |
| console.assert(rule instanceof SDK.CSSKeyframeRule); |
| var 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() { |
| } |
| }; |
| |
| Elements.StylePropertyTreeElement = class extends UI.TreeElement { |
| /** |
| * @param {!Elements.StylesSidebarPane} stylesPane |
| * @param {!SDK.CSSMatchedStyles} matchedStyles |
| * @param {!SDK.CSSProperty} property |
| * @param {boolean} isShorthand |
| * @param {boolean} inherited |
| * @param {boolean} overloaded |
| */ |
| constructor(stylesPane, matchedStyles, property, isShorthand, inherited, overloaded) { |
| // Pass an empty title, the title gets made later in onattach. |
| super('', isShorthand); |
| this._style = property.ownerStyle; |
| this._matchedStyles = matchedStyles; |
| this.property = property; |
| this._inherited = inherited; |
| this._overloaded = overloaded; |
| this.selectable = false; |
| this._parentPane = stylesPane; |
| this.isShorthand = isShorthand; |
| this._applyStyleThrottler = new Common.Throttler(0); |
| this._newProperty = false; |
| this._expandedDueToFilter = false; |
| this.valueElement = null; |
| this.nameElement = null; |
| this._expandElement = null; |
| this._originalPropertyText = ''; |
| this._prompt = null; |
| this._propertyHasBeenEditedIncrementally = false; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| _editable() { |
| return !!(this._style.styleSheetId && this._style.range); |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| inherited() { |
| return this._inherited; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| overloaded() { |
| return this._overloaded; |
| } |
| |
| /** |
| * @param {boolean} x |
| */ |
| setOverloaded(x) { |
| if (x === this._overloaded) |
| return; |
| this._overloaded = x; |
| this._updateState(); |
| } |
| |
| get name() { |
| return this.property.name; |
| } |
| |
| get value() { |
| return this.property.value; |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| _updateFilter() { |
| var regex = this._parentPane.filterRegex(); |
| var matches = !!regex && (regex.test(this.property.name) || regex.test(this.property.value)); |
| this.listItemElement.classList.toggle('filter-match', matches); |
| |
| this.onpopulate(); |
| var hasMatchingChildren = false; |
| for (var i = 0; i < this.childCount(); ++i) |
| hasMatchingChildren |= this.childAt(i)._updateFilter(); |
| |
| if (!regex) { |
| if (this._expandedDueToFilter) |
| this.collapse(); |
| this._expandedDueToFilter = false; |
| } else if (hasMatchingChildren && !this.expanded) { |
| this.expand(); |
| this._expandedDueToFilter = true; |
| } else if (!hasMatchingChildren && this.expanded && this._expandedDueToFilter) { |
| this.collapse(); |
| this._expandedDueToFilter = false; |
| } |
| return matches; |
| } |
| |
| /** |
| * @param {string} text |
| * @return {!Node} |
| */ |
| _processColor(text) { |
| // We can be called with valid non-color values of |text| (like 'none' from border style) |
| var color = Common.Color.parse(text); |
| if (!color) |
| return createTextNode(text); |
| |
| if (!this._editable()) { |
| var swatch = InlineEditor.ColorSwatch.create(); |
| swatch.setColor(color); |
| return swatch; |
| } |
| |
| var swatchPopoverHelper = this._parentPane._swatchPopoverHelper; |
| var swatch = InlineEditor.ColorSwatch.create(); |
| swatch.setColor(color); |
| swatch.setFormat(Common.Color.detectColorFormat(swatch.color())); |
| var swatchIcon = new Elements.ColorSwatchPopoverIcon(this, swatchPopoverHelper, swatch); |
| |
| /** |
| * @param {?Array<string>} backgroundColors |
| */ |
| function computedCallback(backgroundColors) { |
| // TODO(aboxhall): distinguish between !backgroundColors (no text) and |
| // !backgroundColors.length (no computed bg color) |
| if (!backgroundColors || !backgroundColors.length) |
| return; |
| // TODO(samli): figure out what to do in the case of multiple background colors (i.e. gradients) |
| var bgColorText = backgroundColors[0]; |
| var bgColor = Common.Color.parse(bgColorText); |
| if (!bgColor) |
| return; |
| |
| // If we have a semi-transparent background color over an unknown |
| // background, draw the line for the "worst case" scenario: where |
| // the unknown background is the same color as the text. |
| if (bgColor.hasAlpha) { |
| var blendedRGBA = []; |
| Common.Color.blendColors(bgColor.rgba(), color.rgba(), blendedRGBA); |
| bgColor = new Common.Color(blendedRGBA, Common.Color.Format.RGBA); |
| } |
| |
| swatchIcon.setContrastColor(bgColor); |
| } |
| |
| if (Runtime.experiments.isEnabled('colorContrastRatio') && this.property.name === 'color' && |
| this._parentPane.cssModel() && this.node()) { |
| var cssModel = this._parentPane.cssModel(); |
| cssModel.backgroundColorsPromise(this.node().id).then(computedCallback); |
| } |
| |
| return swatch; |
| } |
| |
| /** |
| * @return {string} |
| */ |
| renderedPropertyText() { |
| return this.nameElement.textContent + ': ' + this.valueElement.textContent; |
| } |
| |
| /** |
| * @param {string} text |
| * @return {!Node} |
| */ |
| _processBezier(text) { |
| if (!this._editable() || !UI.Geometry.CubicBezier.parse(text)) |
| return createTextNode(text); |
| var swatchPopoverHelper = this._parentPane._swatchPopoverHelper; |
| var swatch = InlineEditor.BezierSwatch.create(); |
| swatch.setBezierText(text); |
| new Elements.BezierPopoverIcon(this, swatchPopoverHelper, swatch); |
| return swatch; |
| } |
| |
| /** |
| * @param {string} propertyValue |
| * @param {string} propertyName |
| * @return {!Node} |
| */ |
| _processShadow(propertyValue, propertyName) { |
| if (!this._editable()) |
| return createTextNode(propertyValue); |
| var shadows; |
| if (propertyName === 'text-shadow') |
| shadows = InlineEditor.CSSShadowModel.parseTextShadow(propertyValue); |
| else |
| shadows = InlineEditor.CSSShadowModel.parseBoxShadow(propertyValue); |
| if (!shadows.length) |
| return createTextNode(propertyValue); |
| var container = createDocumentFragment(); |
| var swatchPopoverHelper = this._parentPane._swatchPopoverHelper; |
| for (var i = 0; i < shadows.length; i++) { |
| if (i !== 0) |
| container.appendChild(createTextNode(', ')); // Add back commas and spaces between each shadow. |
| // TODO(flandy): editing the property value should use the original value with all spaces. |
| var cssShadowSwatch = InlineEditor.CSSShadowSwatch.create(); |
| cssShadowSwatch.setCSSShadow(shadows[i]); |
| new Elements.ShadowSwatchPopoverHelper(this, swatchPopoverHelper, cssShadowSwatch); |
| var colorSwatch = cssShadowSwatch.colorSwatch(); |
| if (colorSwatch) |
| new Elements.ColorSwatchPopoverIcon(this, swatchPopoverHelper, colorSwatch); |
| container.appendChild(cssShadowSwatch); |
| } |
| return container; |
| } |
| |
| _updateState() { |
| if (!this.listItemElement) |
| return; |
| |
| if (this._style.isPropertyImplicit(this.name)) |
| this.listItemElement.classList.add('implicit'); |
| else |
| this.listItemElement.classList.remove('implicit'); |
| |
| var hasIgnorableError = |
| !this.property.parsedOk && Elements.StylesSidebarPane.ignoreErrorsForProperty(this.property); |
| if (hasIgnorableError) |
| this.listItemElement.classList.add('has-ignorable-error'); |
| else |
| this.listItemElement.classList.remove('has-ignorable-error'); |
| |
| if (this.inherited()) |
| this.listItemElement.classList.add('inherited'); |
| else |
| this.listItemElement.classList.remove('inherited'); |
| |
| if (this.overloaded()) |
| this.listItemElement.classList.add('overloaded'); |
| else |
| this.listItemElement.classList.remove('overloaded'); |
| |
| if (this.property.disabled) |
| this.listItemElement.classList.add('disabled'); |
| else |
| this.listItemElement.classList.remove('disabled'); |
| } |
| |
| /** |
| * @return {?SDK.DOMNode} |
| */ |
| node() { |
| return this._parentPane.node(); |
| } |
| |
| /** |
| * @return {!Elements.StylesSidebarPane} |
| */ |
| parentPane() { |
| return this._parentPane; |
| } |
| |
| /** |
| * @return {?Elements.StylePropertiesSection} |
| */ |
| section() { |
| return this.treeOutline && this.treeOutline.section; |
| } |
| |
| _updatePane() { |
| var section = this.section(); |
| if (section && section._parentPane) |
| section._parentPane._refreshUpdate(section); |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _toggleEnabled(event) { |
| var disabled = !event.target.checked; |
| var oldStyleRange = this._style.range; |
| if (!oldStyleRange) |
| return; |
| |
| /** |
| * @param {boolean} success |
| * @this {Elements.StylePropertyTreeElement} |
| */ |
| function callback(success) { |
| this._parentPane._userOperation = false; |
| |
| if (!success) |
| return; |
| this._matchedStyles.resetActiveProperties(); |
| this._updatePane(); |
| this.styleTextAppliedForTest(); |
| } |
| |
| event.consume(); |
| this._parentPane._userOperation = true; |
| this.property.setDisabled(disabled).then(callback.bind(this)); |
| } |
| |
| /** |
| * @override |
| */ |
| onpopulate() { |
| // Only populate once and if this property is a shorthand. |
| if (this.childCount() || !this.isShorthand) |
| return; |
| |
| var longhandProperties = this._style.longhandProperties(this.name); |
| for (var i = 0; i < longhandProperties.length; ++i) { |
| var name = longhandProperties[i].name; |
| var inherited = false; |
| var overloaded = false; |
| |
| var section = this.section(); |
| if (section) { |
| inherited = section.isPropertyInherited(name); |
| overloaded = |
| this._matchedStyles.propertyState(longhandProperties[i]) === SDK.CSSMatchedStyles.PropertyState.Overloaded; |
| } |
| |
| var item = new Elements.StylePropertyTreeElement( |
| this._parentPane, this._matchedStyles, longhandProperties[i], false, inherited, overloaded); |
| this.appendChild(item); |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| onattach() { |
| this.updateTitle(); |
| |
| this.listItemElement.addEventListener('mousedown', this._mouseDown.bind(this)); |
| this.listItemElement.addEventListener('mouseup', this._resetMouseDownElement.bind(this)); |
| this.listItemElement.addEventListener('click', this._mouseClick.bind(this)); |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _mouseDown(event) { |
| if (this._parentPane) { |
| this._parentPane._mouseDownTreeElement = this; |
| this._parentPane._mouseDownTreeElementIsName = |
| this.nameElement && this.nameElement.isSelfOrAncestor(event.target); |
| this._parentPane._mouseDownTreeElementIsValue = |
| this.valueElement && this.valueElement.isSelfOrAncestor(event.target); |
| } |
| } |
| |
| _resetMouseDownElement() { |
| if (this._parentPane) { |
| this._parentPane._mouseDownTreeElement = null; |
| this._parentPane._mouseDownTreeElementIsName = false; |
| this._parentPane._mouseDownTreeElementIsValue = false; |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| onexpand() { |
| this._updateExpandElement(); |
| } |
| |
| /** |
| * @override |
| */ |
| oncollapse() { |
| this._updateExpandElement(); |
| } |
| |
| _updateExpandElement() { |
| if (this.expanded) |
| this._expandElement.setIconType('smallicon-triangle-down'); |
| else |
| this._expandElement.setIconType('smallicon-triangle-right'); |
| } |
| |
| updateTitle() { |
| this._updateState(); |
| this._expandElement = UI.Icon.create('smallicon-triangle-right', 'expand-icon'); |
| |
| var propertyRenderer = |
| new Elements.StylesSidebarPropertyRenderer(this._style.parentRule, this.node(), this.name, this.value); |
| if (this.property.parsedOk) { |
| propertyRenderer.setColorHandler(this._processColor.bind(this)); |
| propertyRenderer.setBezierHandler(this._processBezier.bind(this)); |
| propertyRenderer.setShadowHandler(this._processShadow.bind(this)); |
| } |
| |
| this.listItemElement.removeChildren(); |
| this.nameElement = propertyRenderer.renderName(); |
| this.valueElement = propertyRenderer.renderValue(); |
| if (!this.treeOutline) |
| return; |
| |
| var indent = Common.moduleSetting('textEditorIndent').get(); |
| this.listItemElement.createChild('span', 'styles-clipboard-only') |
| .createTextChild(indent + (this.property.disabled ? '/* ' : '')); |
| this.listItemElement.appendChild(this.nameElement); |
| this.listItemElement.createTextChild(': '); |
| this.listItemElement.appendChild(this._expandElement); |
| this.listItemElement.appendChild(this.valueElement); |
| this.listItemElement.createTextChild(';'); |
| if (this.property.disabled) |
| this.listItemElement.createChild('span', 'styles-clipboard-only').createTextChild(' */'); |
| |
| if (!this.property.parsedOk) { |
| // Avoid having longhands under an invalid shorthand. |
| this.listItemElement.classList.add('not-parsed-ok'); |
| |
| // Add a separate exclamation mark IMG element with a tooltip. |
| this.listItemElement.insertBefore( |
| Elements.StylesSidebarPane.createExclamationMark(this.property), this.listItemElement.firstChild); |
| } |
| if (!this.property.activeInStyle()) |
| this.listItemElement.classList.add('inactive'); |
| this._updateFilter(); |
| |
| if (this.property.parsedOk && this.section() && this.parent.root) { |
| var enabledCheckboxElement = createElement('input'); |
| enabledCheckboxElement.className = 'enabled-button'; |
| enabledCheckboxElement.type = 'checkbox'; |
| enabledCheckboxElement.checked = !this.property.disabled; |
| enabledCheckboxElement.addEventListener('click', this._toggleEnabled.bind(this), false); |
| this.listItemElement.insertBefore(enabledCheckboxElement, this.listItemElement.firstChild); |
| } |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _mouseClick(event) { |
| if (event.target.hasSelection()) |
| return; |
| |
| event.consume(true); |
| |
| if (event.target === this.listItemElement) { |
| var section = this.section(); |
| if (!section || !section.editable) |
| return; |
| |
| if (section._checkWillCancelEditing()) |
| return; |
| section.addNewBlankProperty(this.property.index + 1).startEditing(); |
| return; |
| } |
| |
| if (UI.KeyboardShortcut.eventHasCtrlOrMeta(/** @type {!MouseEvent} */ (event)) && this.section().navigable) { |
| this._navigateToSource(/** @type {!Element} */ (event.target)); |
| return; |
| } |
| |
| this.startEditing(/** @type {!Element} */ (event.target)); |
| } |
| |
| /** |
| * @param {!Element} element |
| * @param {boolean=} omitFocus |
| */ |
| _navigateToSource(element, omitFocus) { |
| if (!this.section().navigable) |
| return; |
| var propertyNameClicked = element === this.nameElement; |
| var uiLocation = Bindings.cssWorkspaceBinding.propertyUILocation(this.property, propertyNameClicked); |
| if (uiLocation) |
| Common.Revealer.reveal(uiLocation, omitFocus); |
| } |
| |
| /** |
| * @param {?Element=} selectElement |
| */ |
| startEditing(selectElement) { |
| // FIXME: we don't allow editing of longhand properties under a shorthand right now. |
| if (this.parent.isShorthand) |
| return; |
| |
| if (selectElement === this._expandElement) |
| return; |
| |
| var section = this.section(); |
| if (section && !section.editable) |
| return; |
| |
| if (selectElement) { |
| selectElement = selectElement.enclosingNodeOrSelfWithClass('webkit-css-property') || |
| selectElement.enclosingNodeOrSelfWithClass('value'); |
| } |
| if (!selectElement) |
| selectElement = this.nameElement; |
| |
| if (UI.isBeingEdited(selectElement)) |
| return; |
| |
| var isEditingName = selectElement === this.nameElement; |
| if (!isEditingName) |
| this.valueElement.textContent = restoreURLs(this.valueElement.textContent, this.value); |
| |
| /** |
| * @param {string} fieldValue |
| * @param {string} modelValue |
| * @return {string} |
| */ |
| function restoreURLs(fieldValue, modelValue) { |
| const urlRegex = /\b(url\([^)]*\))/g; |
| var splitFieldValue = fieldValue.split(urlRegex); |
| if (splitFieldValue.length === 1) |
| return fieldValue; |
| var modelUrlRegex = new RegExp(urlRegex); |
| for (var i = 1; i < splitFieldValue.length; i += 2) { |
| var match = modelUrlRegex.exec(modelValue); |
| if (match) |
| splitFieldValue[i] = match[0]; |
| } |
| return splitFieldValue.join(''); |
| } |
| |
| /** @type {!Elements.StylePropertyTreeElement.Context} */ |
| var context = { |
| expanded: this.expanded, |
| hasChildren: this.isExpandable(), |
| isEditingName: isEditingName, |
| previousContent: selectElement.textContent |
| }; |
| |
| // Lie about our children to prevent expanding on double click and to collapse shorthands. |
| this.setExpandable(false); |
| |
| if (selectElement.parentElement) |
| selectElement.parentElement.classList.add('child-editing'); |
| selectElement.textContent = selectElement.textContent; // remove color swatch and the like |
| |
| /** |
| * @param {!Elements.StylePropertyTreeElement.Context} context |
| * @param {!Event} event |
| * @this {Elements.StylePropertyTreeElement} |
| */ |
| function pasteHandler(context, event) { |
| var data = event.clipboardData.getData('Text'); |
| if (!data) |
| return; |
| var colonIdx = data.indexOf(':'); |
| if (colonIdx < 0) |
| return; |
| var name = data.substring(0, colonIdx).trim(); |
| var value = data.substring(colonIdx + 1).trim(); |
| |
| event.preventDefault(); |
| |
| if (!('originalName' in context)) { |
| context.originalName = this.nameElement.textContent; |
| context.originalValue = this.valueElement.textContent; |
| } |
| this.property.name = name; |
| this.property.value = value; |
| this.nameElement.textContent = name; |
| this.valueElement.textContent = value; |
| this.nameElement.normalize(); |
| this.valueElement.normalize(); |
| |
| this._editingCommitted(event.target.textContent, context, 'forward'); |
| } |
| |
| /** |
| * @param {!Elements.StylePropertyTreeElement.Context} context |
| * @param {!Event} event |
| * @this {Elements.StylePropertyTreeElement} |
| */ |
| function blurListener(context, event) { |
| var treeElement = this._parentPane._mouseDownTreeElement; |
| var moveDirection = ''; |
| if (treeElement === this) { |
| if (isEditingName && this._parentPane._mouseDownTreeElementIsValue) |
| moveDirection = 'forward'; |
| if (!isEditingName && this._parentPane._mouseDownTreeElementIsName) |
| moveDirection = 'backward'; |
| } |
| var text = event.target.textContent; |
| if (!context.isEditingName) |
| text = this.value || text; |
| this._editingCommitted(text, context, moveDirection); |
| } |
| |
| this._originalPropertyText = this.property.propertyText; |
| |
| this._parentPane.setEditingStyle(true); |
| if (selectElement.parentElement) |
| selectElement.parentElement.scrollIntoViewIfNeeded(false); |
| |
| var cssCompletions = []; |
| if (isEditingName) { |
| cssCompletions = SDK.cssMetadata().allProperties(); |
| cssCompletions = |
| cssCompletions.filter(property => SDK.cssMetadata().isSVGProperty(property) === this.node().isSVGNode()); |
| } else { |
| cssCompletions = SDK.cssMetadata().propertyValues(this.nameElement.textContent); |
| } |
| var cssVariables = this._matchedStyles.cssVariables().sort(String.naturalOrderComparator); |
| |
| this._prompt = new Elements.StylesSidebarPane.CSSPropertyPrompt(cssCompletions, cssVariables, this, isEditingName); |
| this._prompt.setAutocompletionTimeout(0); |
| if (section) |
| section._startEditing(); |
| |
| // Do not live-edit "content" property of pseudo elements. crbug.com/433889 |
| if (!isEditingName && (!this._parentPane.node().pseudoType() || this.name !== 'content')) |
| this._prompt.addEventListener(UI.TextPrompt.Events.TextChanged, this._applyFreeFlowStyleTextEdit.bind(this)); |
| |
| var proxyElement = this._prompt.attachAndStartEditing(selectElement, blurListener.bind(this, context)); |
| this._navigateToSource(selectElement, true); |
| |
| proxyElement.addEventListener('keydown', this._editingNameValueKeyDown.bind(this, context), false); |
| proxyElement.addEventListener('keypress', this._editingNameValueKeyPress.bind(this, context), false); |
| if (isEditingName) |
| proxyElement.addEventListener('paste', pasteHandler.bind(this, context), false); |
| |
| selectElement.getComponentSelection().selectAllChildren(selectElement); |
| } |
| |
| /** |
| * @param {!Elements.StylePropertyTreeElement.Context} context |
| * @param {!Event} event |
| */ |
| _editingNameValueKeyDown(context, event) { |
| if (event.handled) |
| return; |
| |
| var result; |
| |
| if (isEnterKey(event)) { |
| event.preventDefault(); |
| result = 'forward'; |
| } else if (event.keyCode === UI.KeyboardShortcut.Keys.Esc.code || event.key === 'Escape') { |
| result = 'cancel'; |
| } else if ( |
| !context.isEditingName && this._newProperty && event.keyCode === UI.KeyboardShortcut.Keys.Backspace.code) { |
| // For a new property, when Backspace is pressed at the beginning of new property value, move back to the property name. |
| var selection = event.target.getComponentSelection(); |
| if (selection.isCollapsed && !selection.focusOffset) { |
| event.preventDefault(); |
| result = 'backward'; |
| } |
| } else if (event.key === 'Tab') { |
| result = event.shiftKey ? 'backward' : 'forward'; |
| event.preventDefault(); |
| } |
| |
| if (result) { |
| switch (result) { |
| case 'cancel': |
| this.editingCancelled(null, context); |
| break; |
| case 'forward': |
| case 'backward': |
| this._editingCommitted(event.target.textContent, context, result); |
| break; |
| } |
| |
| event.consume(); |
| return; |
| } |
| } |
| |
| /** |
| * @param {!Elements.StylePropertyTreeElement.Context} context |
| * @param {!Event} event |
| */ |
| _editingNameValueKeyPress(context, event) { |
| /** |
| * @param {string} text |
| * @param {number} cursorPosition |
| * @return {boolean} |
| */ |
| function shouldCommitValueSemicolon(text, cursorPosition) { |
| // FIXME: should this account for semicolons inside comments? |
| var openQuote = ''; |
| for (var i = 0; i < cursorPosition; ++i) { |
| var ch = text[i]; |
| if (ch === '\\' && openQuote !== '') |
| ++i; // skip next character inside string |
| else if (!openQuote && (ch === '"' || ch === '\'')) |
| openQuote = ch; |
| else if (openQuote === ch) |
| openQuote = ''; |
| } |
| return !openQuote; |
| } |
| |
| var keyChar = String.fromCharCode(event.charCode); |
| var isFieldInputTerminated = |
| (context.isEditingName ? keyChar === ':' : |
| keyChar === ';' && |
| shouldCommitValueSemicolon(event.target.textContent, event.target.selectionLeftOffset())); |
| if (isFieldInputTerminated) { |
| // Enter or colon (for name)/semicolon outside of string (for value). |
| event.consume(true); |
| this._editingCommitted(event.target.textContent, context, 'forward'); |
| return; |
| } |
| } |
| |
| _applyFreeFlowStyleTextEdit() { |
| var valueText = this._prompt.textWithCurrentSuggestion(); |
| if (valueText.indexOf(';') === -1) |
| this.applyStyleText(this.nameElement.textContent + ': ' + valueText, false); |
| } |
| |
| kickFreeFlowStyleEditForTest() { |
| this._applyFreeFlowStyleTextEdit(); |
| } |
| |
| /** |
| * @param {!Elements.StylePropertyTreeElement.Context} context |
| */ |
| editingEnded(context) { |
| this._resetMouseDownElement(); |
| |
| this.setExpandable(context.hasChildren); |
| if (context.expanded) |
| this.expand(); |
| var editedElement = context.isEditingName ? this.nameElement : this.valueElement; |
| // The proxyElement has been deleted, no need to remove listener. |
| if (editedElement.parentElement) |
| editedElement.parentElement.classList.remove('child-editing'); |
| |
| this._parentPane.setEditingStyle(false); |
| } |
| |
| /** |
| * @param {?Element} element |
| * @param {!Elements.StylePropertyTreeElement.Context} context |
| */ |
| editingCancelled(element, context) { |
| this._removePrompt(); |
| this._revertStyleUponEditingCanceled(); |
| // This should happen last, as it clears the info necessary to restore the property value after [Page]Up/Down changes. |
| this.editingEnded(context); |
| } |
| |
| _revertStyleUponEditingCanceled() { |
| if (this._propertyHasBeenEditedIncrementally) { |
| this.applyStyleText(this._originalPropertyText, false); |
| this._originalPropertyText = ''; |
| } else if (this._newProperty) { |
| this.treeOutline.removeChild(this); |
| } else { |
| this.updateTitle(); |
| } |
| } |
| |
| /** |
| * @param {string} moveDirection |
| * @return {?Elements.StylePropertyTreeElement} |
| */ |
| _findSibling(moveDirection) { |
| var target = this; |
| do |
| target = (moveDirection === 'forward' ? target.nextSibling : target.previousSibling); |
| while (target && target.inherited()); |
| |
| return target; |
| } |
| |
| /** |
| * @param {string} userInput |
| * @param {!Elements.StylePropertyTreeElement.Context} context |
| * @param {string} moveDirection |
| */ |
| _editingCommitted(userInput, context, moveDirection) { |
| this._removePrompt(); |
| this.editingEnded(context); |
| var isEditingName = context.isEditingName; |
| |
| // Determine where to move to before making changes |
| var createNewProperty, moveToSelector; |
| var isDataPasted = 'originalName' in context; |
| var isDirtyViaPaste = isDataPasted && (this.nameElement.textContent !== context.originalName || |
| this.valueElement.textContent !== context.originalValue); |
| var isPropertySplitPaste = isDataPasted && isEditingName && this.valueElement.textContent !== context.originalValue; |
| var moveTo = this; |
| var moveToOther = (isEditingName ^ (moveDirection === 'forward')); |
| var abandonNewProperty = this._newProperty && !userInput && (moveToOther || isEditingName); |
| if (moveDirection === 'forward' && (!isEditingName || isPropertySplitPaste) || |
| moveDirection === 'backward' && isEditingName) { |
| moveTo = moveTo._findSibling(moveDirection); |
| if (!moveTo) { |
| if (moveDirection === 'forward' && (!this._newProperty || userInput)) |
| createNewProperty = true; |
| else if (moveDirection === 'backward') |
| moveToSelector = true; |
| } |
| } |
| |
| // Make the Changes and trigger the moveToNextCallback after updating. |
| var moveToIndex = moveTo && this.treeOutline ? this.treeOutline.rootElement().indexOfChild(moveTo) : -1; |
| var blankInput = userInput.isWhitespace(); |
| var shouldCommitNewProperty = this._newProperty && |
| (isPropertySplitPaste || moveToOther || (!moveDirection && !isEditingName) || (isEditingName && blankInput)); |
| var section = /** @type {!Elements.StylePropertiesSection} */ (this.section()); |
| if (((userInput !== context.previousContent || isDirtyViaPaste) && !this._newProperty) || shouldCommitNewProperty) { |
| section._afterUpdate = moveToNextCallback.bind(this, this._newProperty, !blankInput, section); |
| var propertyText; |
| if (blankInput || (this._newProperty && this.valueElement.textContent.isWhitespace())) { |
| propertyText = ''; |
| } else { |
| if (isEditingName) |
| propertyText = userInput + ': ' + this.property.value; |
| else |
| propertyText = this.property.name + ': ' + userInput; |
| } |
| this.applyStyleText(propertyText, true); |
| } else { |
| if (isEditingName) |
| this.property.name = userInput; |
| else |
| this.property.value = userInput; |
| if (!isDataPasted && !this._newProperty) |
| this.updateTitle(); |
| moveToNextCallback.call(this, this._newProperty, false, section); |
| } |
| |
| /** |
| * The Callback to start editing the next/previous property/selector. |
| * @param {boolean} alreadyNew |
| * @param {boolean} valueChanged |
| * @param {!Elements.StylePropertiesSection} section |
| * @this {Elements.StylePropertyTreeElement} |
| */ |
| function moveToNextCallback(alreadyNew, valueChanged, section) { |
| if (!moveDirection) |
| return; |
| |
| // User just tabbed through without changes. |
| if (moveTo && moveTo.parent) { |
| moveTo.startEditing(!isEditingName ? moveTo.nameElement : moveTo.valueElement); |
| return; |
| } |
| |
| // User has made a change then tabbed, wiping all the original treeElements. |
| // Recalculate the new treeElement for the same property we were going to edit next. |
| if (moveTo && !moveTo.parent) { |
| var rootElement = section.propertiesTreeOutline.rootElement(); |
| if (moveDirection === 'forward' && blankInput && !isEditingName) |
| --moveToIndex; |
| if (moveToIndex >= rootElement.childCount() && !this._newProperty) { |
| createNewProperty = true; |
| } else { |
| var treeElement = moveToIndex >= 0 ? rootElement.childAt(moveToIndex) : null; |
| if (treeElement) { |
| var elementToEdit = |
| !isEditingName || isPropertySplitPaste ? treeElement.nameElement : treeElement.valueElement; |
| if (alreadyNew && blankInput) |
| elementToEdit = moveDirection === 'forward' ? treeElement.nameElement : treeElement.valueElement; |
| treeElement.startEditing(elementToEdit); |
| return; |
| } else if (!alreadyNew) { |
| moveToSelector = true; |
| } |
| } |
| } |
| |
| // Create a new attribute in this section (or move to next editable selector if possible). |
| if (createNewProperty) { |
| if (alreadyNew && !valueChanged && (isEditingName ^ (moveDirection === 'backward'))) |
| return; |
| |
| section.addNewBlankProperty().startEditing(); |
| return; |
| } |
| |
| if (abandonNewProperty) { |
| moveTo = this._findSibling(moveDirection); |
| var sectionToEdit = (moveTo || moveDirection === 'backward') ? section : section.nextEditableSibling(); |
| if (sectionToEdit) { |
| if (sectionToEdit.style().parentRule) |
| sectionToEdit.startEditingSelector(); |
| else |
| sectionToEdit._moveEditorFromSelector(moveDirection); |
| } |
| return; |
| } |
| |
| if (moveToSelector) { |
| if (section.style().parentRule) |
| section.startEditingSelector(); |
| else |
| section._moveEditorFromSelector(moveDirection); |
| } |
| } |
| } |
| |
| _removePrompt() { |
| // BUG 53242. This cannot go into editingEnded(), as it should always happen first for any editing outcome. |
| if (this._prompt) { |
| this._prompt.detach(); |
| this._prompt = null; |
| } |
| var section = this.section(); |
| if (section) |
| section._stopEditing(); |
| } |
| |
| styleTextAppliedForTest() { |
| } |
| |
| /** |
| * @param {string} styleText |
| * @param {boolean} majorChange |
| */ |
| applyStyleText(styleText, majorChange) { |
| this._applyStyleThrottler.schedule(this._innerApplyStyleText.bind(this, styleText, majorChange)); |
| } |
| |
| /** |
| * @param {string} styleText |
| * @param {boolean} majorChange |
| * @return {!Promise.<undefined>} |
| */ |
| _innerApplyStyleText(styleText, majorChange) { |
| if (!this.treeOutline) |
| return Promise.resolve(); |
| |
| var oldStyleRange = this._style.range; |
| if (!oldStyleRange) |
| return Promise.resolve(); |
| |
| styleText = styleText.replace(/\s/g, ' ').trim(); // Replace with whitespace. |
| if (!styleText.length && majorChange && this._newProperty && !this._propertyHasBeenEditedIncrementally) { |
| // The user deleted everything and never applied a new property value via Up/Down scrolling/live editing, so remove the tree element and update. |
| var section = this.section(); |
| this.parent.removeChild(this); |
| section.afterUpdate(); |
| return Promise.resolve(); |
| } |
| |
| var currentNode = this._parentPane.node(); |
| this._parentPane._userOperation = true; |
| |
| /** |
| * @param {boolean} success |
| * @this {Elements.StylePropertyTreeElement} |
| */ |
| function callback(success) { |
| this._parentPane._userOperation = false; |
| |
| if (!success) { |
| if (majorChange) { |
| // It did not apply, cancel editing. |
| this._revertStyleUponEditingCanceled(); |
| } |
| this.styleTextAppliedForTest(); |
| return; |
| } |
| |
| this._matchedStyles.resetActiveProperties(); |
| this._propertyHasBeenEditedIncrementally = true; |
| this.property = this._style.propertyAt(this.property.index); |
| |
| // We are happy to update UI if user is not editing. |
| if (!this._parentPane._isEditingStyle && currentNode === this.node()) |
| this._updatePane(); |
| |
| this.styleTextAppliedForTest(); |
| } |
| |
| // Append a ";" if the new text does not end in ";". |
| // FIXME: this does not handle trailing comments. |
| if (styleText.length && !/;\s*$/.test(styleText)) |
| styleText += ';'; |
| var overwriteProperty = !this._newProperty || this._propertyHasBeenEditedIncrementally; |
| return this.property.setText(styleText, majorChange, overwriteProperty).then(callback.bind(this)); |
| } |
| |
| /** |
| * @override |
| * @return {boolean} |
| */ |
| ondblclick() { |
| return true; // handled |
| } |
| |
| /** |
| * @override |
| * @param {!Event} event |
| * @return {boolean} |
| */ |
| isEventWithinDisclosureTriangle(event) { |
| return event.target === this._expandElement; |
| } |
| }; |
| |
| /** @typedef {{expanded: boolean, hasChildren: boolean, isEditingName: boolean, previousContent: string}} */ |
| Elements.StylePropertyTreeElement.Context; |
| |
| Elements.StylesSidebarPane.CSSPropertyPrompt = class extends UI.TextPrompt { |
| /** |
| * @param {!Array<string>} cssCompletions |
| * @param {!Array<string>} cssVariables |
| * @param {!Elements.StylePropertyTreeElement} treeElement |
| * @param {boolean} isEditingName |
| */ |
| constructor(cssCompletions, cssVariables, treeElement, isEditingName) { |
| // Use the same callback both for applyItemCallback and acceptItemCallback. |
| super(); |
| this.initialize(this._buildPropertyCompletions.bind(this), UI.StyleValueDelimiters); |
| this._cssCompletions = cssCompletions; |
| this._cssVariables = cssVariables; |
| this._treeElement = treeElement; |
| this._isEditingName = isEditingName; |
| |
| 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) { |
| var cssValueText = treeElement.valueElement.textContent; |
| if (cssValueText.match(/#[\da-f]{3,6}$/i)) { |
| this.setTitle(Common.UIString( |
| 'Increment/decrement with mousewheel or up/down keys. %s: R ±1, Shift: G ±1, Alt: B ±1', |
| Host.isMac() ? 'Cmd' : 'Ctrl')); |
| } else if (cssValueText.match(/\d+/)) { |
| this.setTitle(Common.UIString( |
| 'Increment/decrement with mousewheel or up/down keys. %s: ±100, Shift: ±10, Alt: ±0.1', |
| Host.isMac() ? 'Cmd' : 'Ctrl')); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @override |
| * @param {!Event} event |
| */ |
| onKeyDown(event) { |
| switch (event.key) { |
| case 'ArrowUp': |
| case 'ArrowDown': |
| case 'PageUp': |
| case 'PageDown': |
| if (this._handleNameOrValueUpDown(event)) { |
| event.preventDefault(); |
| return; |
| } |
| break; |
| case 'Enter': |
| // Accept any available autocompletions and advance to the next field. |
| if (this.textWithCurrentSuggestion() !== this.text()) { |
| this.tabKeyPressed(); |
| return; |
| } |
| break; |
| } |
| |
| 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 {Elements.StylesSidebarPane.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 {Elements.StylesSidebarPane.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) { |
| var lowerQuery = query.toLowerCase(); |
| var editingVariable = !this._isEditingName && expression.trim().endsWith('var('); |
| if (!query && !force && !editingVariable && (this._isEditingName || expression)) |
| return Promise.resolve([]); |
| |
| var prefixResults = []; |
| var anywhereResults = []; |
| if (!editingVariable) |
| this._cssCompletions.forEach(filterCompletions.bind(this)); |
| if (this._isEditingName || editingVariable) |
| this._cssVariables.forEach(filterCompletions.bind(this)); |
| |
| var results = prefixResults.concat(anywhereResults); |
| if (!this._isEditingName && !results.length && query.length > 1 && '!important'.startsWith(lowerQuery)) |
| results.push({text: '!important'}); |
| var userEnteredText = query.replace('-', ''); |
| if (userEnteredText && (userEnteredText === userEnteredText.toUpperCase())) { |
| for (var i = 0; i < results.length; ++i) { |
| if (!results[i].text.startsWith('--')) |
| results[i].text = results[i].text.toUpperCase(); |
| } |
| } |
| if (editingVariable) |
| results.forEach(result => result.text += ')'); |
| return Promise.resolve(results); |
| |
| /** |
| * @param {string} completion |
| * @this {Elements.StylesSidebarPane.CSSPropertyPrompt} |
| */ |
| function filterCompletions(completion) { |
| var index = completion.toLowerCase().indexOf(lowerQuery); |
| if (index === 0) { |
| var priority = this._isEditingName ? SDK.cssMetadata().propertyUsageWeight(completion) : 1; |
| prefixResults.push({text: completion, priority: priority}); |
| } else if (index > -1) { |
| anywhereResults.push({text: completion}); |
| } |
| } |
| } |
| }; |
| |
| Elements.StylesSidebarPropertyRenderer = class { |
| /** |
| * @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; |
| } |
| |
| /** |
| * @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; |
| } |
| |
| /** |
| * @return {!Element} |
| */ |
| renderName() { |
| var nameElement = createElement('span'); |
| nameElement.className = 'webkit-css-property'; |
| nameElement.textContent = this._propertyName; |
| nameElement.normalize(); |
| return nameElement; |
| } |
| |
| /** |
| * @return {!Element} |
| */ |
| renderValue() { |
| var 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; |
| } |
| |
| var regexes = [SDK.CSSMetadata.VariableRegex, SDK.CSSMetadata.URLRegex]; |
| var processors = [createTextNode, 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); |
| } |
| var results = TextUtils.TextUtils.splitStringByRegexes(this._propertyValue, regexes); |
| for (var i = 0; i < results.length; i++) { |
| var result = results[i]; |
| var 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. |
| var url = text.substring(4, text.length - 1).trim(); |
| var isQuoted = /^'.*'$/.test(url) || /^".*"$/.test(url); |
| if (isQuoted) |
| url = url.substring(1, url.length - 1); |
| var container = createDocumentFragment(); |
| container.createTextChild('url('); |
| var 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} |
| */ |
| Elements.StylesSidebarPane.ButtonProvider = class { |
| constructor() { |
| this._button = new UI.ToolbarButton(Common.UIString('New Style Rule'), 'largeicon-add'); |
| this._button.addEventListener(UI.ToolbarButton.Events.Click, this._clicked, this); |
| var 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 {Elements.StylesSidebarPane.ButtonProvider} |
| */ |
| function onNodeChanged() { |
| var node = UI.context.flavor(SDK.DOMNode); |
| node = node ? node.enclosingElementOrSelf() : null; |
| this._button.setEnabled(!!node); |
| } |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _clicked(event) { |
| Elements.StylesSidebarPane._instance._createNewRuleInViaInspectorStyleSheet(); |
| } |
| |
| /** |
| * @param {!Event} e |
| */ |
| _longClicked(e) { |
| Elements.StylesSidebarPane._instance._onAddButtonLongClick(e); |
| } |
| |
| /** |
| * @override |
| * @return {!UI.ToolbarItem} |
| */ |
| item() { |
| return this._button; |
| } |
| }; |