diff options
Diffstat (limited to 'devtools/client/inspector/rules/rules.js')
-rw-r--r-- | devtools/client/inspector/rules/rules.js | 1673 |
1 files changed, 1673 insertions, 0 deletions
diff --git a/devtools/client/inspector/rules/rules.js b/devtools/client/inspector/rules/rules.js new file mode 100644 index 000000000..8c5ec7617 --- /dev/null +++ b/devtools/client/inspector/rules/rules.js @@ -0,0 +1,1673 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const promise = require("promise"); +const Services = require("Services"); +const {Task} = require("devtools/shared/task"); +const {Tools} = require("devtools/client/definitions"); +const {l10n} = require("devtools/shared/inspector/css-logic"); +const {ELEMENT_STYLE} = require("devtools/shared/specs/styles"); +const {OutputParser} = require("devtools/client/shared/output-parser"); +const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils"); +const {ElementStyle} = require("devtools/client/inspector/rules/models/element-style"); +const {Rule} = require("devtools/client/inspector/rules/models/rule"); +const {RuleEditor} = require("devtools/client/inspector/rules/views/rule-editor"); +const {gDevTools} = require("devtools/client/framework/devtools"); +const {getCssProperties} = require("devtools/shared/fronts/css-properties"); +const HighlightersOverlay = require("devtools/client/inspector/shared/highlighters-overlay"); +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, + VIEW_NODE_LOCATION_TYPE, +} = require("devtools/client/inspector/shared/node-types"); +const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu"); +const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay"); +const {createChild, promiseWarn, throttle} = require("devtools/client/inspector/shared/utils"); +const EventEmitter = require("devtools/shared/event-emitter"); +const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); +const clipboardHelper = require("devtools/shared/platform/clipboard"); +const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup"); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; +const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit"; +const PREF_ENABLE_MDN_DOCS_TOOLTIP = + "devtools.inspector.mdnDocsTooltip.enabled"; +const FILTER_CHANGED_TIMEOUT = 150; + +// This is used to parse user input when filtering. +const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/; +// This is used to parse the filter search value to see if the filter +// should be strict or not +const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/; + +/** + * Our model looks like this: + * + * ElementStyle: + * Responsible for keeping track of which properties are overridden. + * Maintains a list of Rule objects that apply to the element. + * Rule: + * Manages a single style declaration or rule. + * Responsible for applying changes to the properties in a rule. + * Maintains a list of TextProperty objects. + * TextProperty: + * Manages a single property from the authoredText attribute of the + * relevant declaration. + * Maintains a list of computed properties that come from this + * property declaration. + * Changes to the TextProperty are sent to its related Rule for + * application. + * + * View hierarchy mostly follows the model hierarchy. + * + * CssRuleView: + * Owns an ElementStyle and creates a list of RuleEditors for its + * Rules. + * RuleEditor: + * Owns a Rule object and creates a list of TextPropertyEditors + * for its TextProperties. + * Manages creation of new text properties. + * TextPropertyEditor: + * Owns a TextProperty object. + * Manages changes to the TextProperty. + * Can be expanded to display computed properties. + * Can mark a property disabled or enabled. + */ + +/** + * CssRuleView is a view of the style rules and declarations that + * apply to a given element. After construction, the 'element' + * property will be available with the user interface. + * + * @param {Inspector} inspector + * Inspector toolbox panel + * @param {Document} document + * The document that will contain the rule view. + * @param {Object} store + * The CSS rule view can use this object to store metadata + * that might outlast the rule view, particularly the current + * set of disabled properties. + * @param {PageStyleFront} pageStyle + * The PageStyleFront for communicating with the remote server. + */ +function CssRuleView(inspector, document, store, pageStyle) { + this.inspector = inspector; + this.styleDocument = document; + this.styleWindow = this.styleDocument.defaultView; + this.store = store || {}; + this.pageStyle = pageStyle; + + // Allow tests to override throttling behavior, as this can cause intermittents. + this.throttle = throttle; + + this.cssProperties = getCssProperties(inspector.toolbox); + + this._outputParser = new OutputParser(document, this.cssProperties); + + this._onAddRule = this._onAddRule.bind(this); + this._onContextMenu = this._onContextMenu.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onFilterStyles = this._onFilterStyles.bind(this); + this._onClearSearch = this._onClearSearch.bind(this); + this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this); + this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this); + + let doc = this.styleDocument; + this.element = doc.getElementById("ruleview-container-focusable"); + this.addRuleButton = doc.getElementById("ruleview-add-rule-button"); + this.searchField = doc.getElementById("ruleview-searchbox"); + this.searchClearButton = doc.getElementById("ruleview-searchinput-clear"); + this.pseudoClassPanel = doc.getElementById("pseudo-class-panel"); + this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle"); + this.hoverCheckbox = doc.getElementById("pseudo-hover-toggle"); + this.activeCheckbox = doc.getElementById("pseudo-active-toggle"); + this.focusCheckbox = doc.getElementById("pseudo-focus-toggle"); + + this.searchClearButton.hidden = true; + + this.shortcuts = new KeyShortcuts({ window: this.styleWindow }); + this._onShortcut = this._onShortcut.bind(this); + this.shortcuts.on("Escape", this._onShortcut); + this.shortcuts.on("Return", this._onShortcut); + this.shortcuts.on("Space", this._onShortcut); + this.shortcuts.on("CmdOrCtrl+F", this._onShortcut); + this.element.addEventListener("copy", this._onCopy); + this.element.addEventListener("contextmenu", this._onContextMenu); + this.addRuleButton.addEventListener("click", this._onAddRule); + this.searchField.addEventListener("input", this._onFilterStyles); + this.searchField.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu); + this.searchClearButton.addEventListener("click", this._onClearSearch); + this.pseudoClassToggle.addEventListener("click", + this._onTogglePseudoClassPanel); + this.hoverCheckbox.addEventListener("click", this._onTogglePseudoClass); + this.activeCheckbox.addEventListener("click", this._onTogglePseudoClass); + this.focusCheckbox.addEventListener("click", this._onTogglePseudoClass); + + this._handlePrefChange = this._handlePrefChange.bind(this); + this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this); + + this._prefObserver = new PrefObserver("devtools."); + this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged); + this._prefObserver.on(PREF_UA_STYLES, this._handlePrefChange); + this._prefObserver.on(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange); + this._prefObserver.on(PREF_ENABLE_MDN_DOCS_TOOLTIP, this._handlePrefChange); + + this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES); + this.enableMdnDocsTooltip = + Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP); + + // The popup will be attached to the toolbox document. + this.popup = new AutocompletePopup(inspector._toolbox.doc, { + autoSelect: true, + theme: "auto" + }); + + this._showEmpty(); + + this._contextmenu = new StyleInspectorMenu(this, { isRuleView: true }); + + // Add the tooltips and highlighters to the view + this.tooltips = new TooltipsOverlay(this); + this.tooltips.addToView(); + this.highlighters = new HighlightersOverlay(this); + this.highlighters.addToView(); + + EventEmitter.decorate(this); +} + +CssRuleView.prototype = { + // The element that we're inspecting. + _viewedElement: null, + + // Used for cancelling timeouts in the style filter. + _filterChangedTimeout: null, + + // Empty, unconnected element of the same type as this node, used + // to figure out how shorthand properties will be parsed. + _dummyElement: null, + + // Get the dummy elemenet. + get dummyElement() { + return this._dummyElement; + }, + + // Get the filter search value. + get searchValue() { + return this.searchField.value.toLowerCase(); + }, + + /** + * Get an instance of SelectorHighlighter (used to highlight nodes that match + * selectors in the rule-view). A new instance is only created the first time + * this function is called. The same instance will then be returned. + * + * @return {Promise} Resolves to the instance of the highlighter. + */ + getSelectorHighlighter: Task.async(function* () { + let utils = this.inspector.toolbox.highlighterUtils; + if (!utils.supportsCustomHighlighters()) { + return null; + } + + if (this.selectorHighlighter) { + return this.selectorHighlighter; + } + + try { + let h = yield utils.getHighlighterByType("SelectorHighlighter"); + this.selectorHighlighter = h; + return h; + } catch (e) { + // The SelectorHighlighter type could not be created in the + // current target. It could be an older server, or a XUL page. + return null; + } + }), + + /** + * Highlight/unhighlight all the nodes that match a given set of selectors + * inside the document of the current selected node. + * Only one selector can be highlighted at a time, so calling the method a + * second time with a different selector will first unhighlight the previously + * highlighted nodes. + * Calling the method a second time with the same selector will just + * unhighlight the highlighted nodes. + * + * @param {DOMNode} selectorIcon + * The icon that was clicked to toggle the selector. The + * class 'highlighted' will be added when the selector is + * highlighted. + * @param {String} selector + * The selector used to find nodes in the page. + */ + toggleSelectorHighlighter: function (selectorIcon, selector) { + if (this.lastSelectorIcon) { + this.lastSelectorIcon.classList.remove("highlighted"); + } + selectorIcon.classList.remove("highlighted"); + + this.unhighlightSelector().then(() => { + if (selector !== this.highlighters.selectorHighlighterShown) { + this.highlighters.selectorHighlighterShown = selector; + selectorIcon.classList.add("highlighted"); + this.lastSelectorIcon = selectorIcon; + this.highlightSelector(selector).then(() => { + this.emit("ruleview-selectorhighlighter-toggled", true); + }, e => console.error(e)); + } else { + this.highlighters.selectorHighlighterShown = null; + this.emit("ruleview-selectorhighlighter-toggled", false); + } + }, e => console.error(e)); + }, + + highlightSelector: Task.async(function* (selector) { + let node = this.inspector.selection.nodeFront; + + let highlighter = yield this.getSelectorHighlighter(); + if (!highlighter) { + return; + } + + yield highlighter.show(node, { + hideInfoBar: true, + hideGuides: true, + selector + }); + }), + + unhighlightSelector: Task.async(function* () { + let highlighter = yield this.getSelectorHighlighter(); + if (!highlighter) { + return; + } + + yield highlighter.hide(); + }), + + /** + * Get the type of a given node in the rule-view + * + * @param {DOMNode} node + * The node which we want information about + * @return {Object} The type information object contains the following props: + * - type {String} One of the VIEW_NODE_XXX_TYPE const in + * client/inspector/shared/node-types + * - value {Object} Depends on the type of the node + * returns null of the node isn't anything we care about + */ + getNodeInfo: function (node) { + if (!node) { + return null; + } + + let type, value; + let classes = node.classList; + let prop = getParentTextProperty(node); + + if (classes.contains("ruleview-propertyname") && prop) { + type = VIEW_NODE_PROPERTY_TYPE; + value = { + property: node.textContent, + value: getPropertyNameAndValue(node).value, + enabled: prop.enabled, + overridden: prop.overridden, + pseudoElement: prop.rule.pseudoElement, + sheetHref: prop.rule.domRule.href, + textProperty: prop + }; + } else if (classes.contains("ruleview-propertyvalue") && prop) { + type = VIEW_NODE_VALUE_TYPE; + value = { + property: getPropertyNameAndValue(node).name, + value: node.textContent, + enabled: prop.enabled, + overridden: prop.overridden, + pseudoElement: prop.rule.pseudoElement, + sheetHref: prop.rule.domRule.href, + textProperty: prop + }; + } else if (classes.contains("theme-link") && + !classes.contains("ruleview-rule-source") && prop) { + type = VIEW_NODE_IMAGE_URL_TYPE; + value = { + property: getPropertyNameAndValue(node).name, + value: node.parentNode.textContent, + url: node.href, + enabled: prop.enabled, + overridden: prop.overridden, + pseudoElement: prop.rule.pseudoElement, + sheetHref: prop.rule.domRule.href, + textProperty: prop + }; + } else if (classes.contains("ruleview-selector-unmatched") || + classes.contains("ruleview-selector-matched") || + classes.contains("ruleview-selectorcontainer") || + classes.contains("ruleview-selector") || + classes.contains("ruleview-selector-attribute") || + classes.contains("ruleview-selector-pseudo-class") || + classes.contains("ruleview-selector-pseudo-class-lock")) { + type = VIEW_NODE_SELECTOR_TYPE; + value = this._getRuleEditorForNode(node).selectorText.textContent; + } else if (classes.contains("ruleview-rule-source") || + classes.contains("ruleview-rule-source-label")) { + type = VIEW_NODE_LOCATION_TYPE; + let rule = this._getRuleEditorForNode(node).rule; + value = (rule.sheet && rule.sheet.href) ? rule.sheet.href : rule.title; + } else { + return null; + } + + return {type, value}; + }, + + /** + * Retrieve the RuleEditor instance that should be stored on + * the offset parent of the node + */ + _getRuleEditorForNode: function (node) { + if (!node.offsetParent) { + // some nodes don't have an offsetParent, but their parentNode does + node = node.parentNode; + } + return node.offsetParent._ruleEditor; + }, + + /** + * Context menu handler. + */ + _onContextMenu: function (event) { + this._contextmenu.show(event); + }, + + /** + * Callback for copy event. Copy the selected text. + * + * @param {Event} event + * copy event object. + */ + _onCopy: function (event) { + if (event) { + this.copySelection(event.target); + event.preventDefault(); + } + }, + + /** + * Copy the current selection. The current target is necessary + * if the selection is inside an input or a textarea + * + * @param {DOMNode} target + * DOMNode target of the copy action + */ + copySelection: function (target) { + try { + let text = ""; + + let nodeName = target && target.nodeName; + if (nodeName === "input" || nodeName == "textarea") { + let start = Math.min(target.selectionStart, target.selectionEnd); + let end = Math.max(target.selectionStart, target.selectionEnd); + let count = end - start; + text = target.value.substr(start, count); + } else { + text = this.styleWindow.getSelection().toString(); + + // Remove any double newlines. + text = text.replace(/(\r?\n)\r?\n/g, "$1"); + } + + clipboardHelper.copyString(text); + } catch (e) { + console.error(e); + } + }, + + /** + * A helper for _onAddRule that handles the case where the actor + * does not support as-authored styles. + */ + _onAddNewRuleNonAuthored: function () { + let elementStyle = this._elementStyle; + let element = elementStyle.element; + let rules = elementStyle.rules; + let pseudoClasses = element.pseudoClassLocks; + + this.pageStyle.addNewRule(element, pseudoClasses).then(options => { + let newRule = new Rule(elementStyle, options); + rules.push(newRule); + let editor = new RuleEditor(this, newRule); + newRule.editor = editor; + + // Insert the new rule editor after the inline element rule + if (rules.length <= 1) { + this.element.appendChild(editor.element); + } else { + for (let rule of rules) { + if (rule.domRule.type === ELEMENT_STYLE) { + let referenceElement = rule.editor.element.nextSibling; + this.element.insertBefore(editor.element, referenceElement); + break; + } + } + } + + // Focus and make the new rule's selector editable + editor.selectorText.click(); + elementStyle._changed(); + }); + }, + + /** + * Add a new rule to the current element. + */ + _onAddRule: function () { + let elementStyle = this._elementStyle; + let element = elementStyle.element; + let client = this.inspector.target.client; + let pseudoClasses = element.pseudoClassLocks; + + if (!client.traits.addNewRule) { + return; + } + + if (!this.pageStyle.supportsAuthoredStyles) { + // We're talking to an old server. + this._onAddNewRuleNonAuthored(); + return; + } + + // Adding a new rule with authored styles will cause the actor to + // emit an event, which will in turn cause the rule view to be + // updated. So, we wait for this update and for the rule creation + // request to complete, and then focus the new rule's selector. + let eventPromise = this.once("ruleview-refreshed"); + let newRulePromise = this.pageStyle.addNewRule(element, pseudoClasses); + promise.all([eventPromise, newRulePromise]).then((values) => { + let options = values[1]; + // Be sure the reference the correct |rules| here. + for (let rule of this._elementStyle.rules) { + if (options.rule === rule.domRule) { + rule.editor.selectorText.click(); + elementStyle._changed(); + break; + } + } + }); + }, + + /** + * Disables add rule button when needed + */ + refreshAddRuleButtonState: function () { + let shouldBeDisabled = !this._viewedElement || + !this.inspector.selection.isElementNode() || + this.inspector.selection.isAnonymousNode(); + this.addRuleButton.disabled = shouldBeDisabled; + }, + + setPageStyle: function (pageStyle) { + this.pageStyle = pageStyle; + }, + + /** + * Return {Boolean} true if the rule view currently has an input + * editor visible. + */ + get isEditing() { + return this.tooltips.isEditing || + this.element.querySelectorAll(".styleinspector-propertyeditor") + .length > 0; + }, + + _handlePrefChange: function (pref) { + if (pref === PREF_UA_STYLES) { + this.showUserAgentStyles = Services.prefs.getBoolPref(pref); + } + + // Reselect the currently selected element + let refreshOnPrefs = [PREF_UA_STYLES, PREF_DEFAULT_COLOR_UNIT]; + if (refreshOnPrefs.indexOf(pref) > -1) { + this.selectElement(this._viewedElement, true); + } + }, + + /** + * Update source links when pref for showing original sources changes + */ + _onSourcePrefChanged: function () { + if (this._elementStyle && this._elementStyle.rules) { + for (let rule of this._elementStyle.rules) { + if (rule.editor) { + rule.editor.updateSourceLink(); + } + } + this.inspector.emit("rule-view-sourcelinks-updated"); + } + }, + + /** + * Set the filter style search value. + * @param {String} value + * The search value. + */ + setFilterStyles: function (value = "") { + this.searchField.value = value; + this.searchField.focus(); + this._onFilterStyles(); + }, + + /** + * Called when the user enters a search term in the filter style search box. + */ + _onFilterStyles: function () { + if (this._filterChangedTimeout) { + clearTimeout(this._filterChangedTimeout); + } + + let filterTimeout = (this.searchValue.length > 0) ? + FILTER_CHANGED_TIMEOUT : 0; + this.searchClearButton.hidden = this.searchValue.length === 0; + + this._filterChangedTimeout = setTimeout(() => { + if (this.searchField.value.length > 0) { + this.searchField.setAttribute("filled", true); + } else { + this.searchField.removeAttribute("filled"); + } + + this.searchData = { + searchPropertyMatch: FILTER_PROP_RE.exec(this.searchValue), + searchPropertyName: this.searchValue, + searchPropertyValue: this.searchValue, + strictSearchValue: "", + strictSearchPropertyName: false, + strictSearchPropertyValue: false, + strictSearchAllValues: false + }; + + if (this.searchData.searchPropertyMatch) { + // Parse search value as a single property line and extract the + // property name and value. If the parsed property name or value is + // contained in backquotes (`), extract the value within the backquotes + // and set the corresponding strict search for the property to true. + if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[1])) { + this.searchData.strictSearchPropertyName = true; + this.searchData.searchPropertyName = + FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[1])[1]; + } else { + this.searchData.searchPropertyName = + this.searchData.searchPropertyMatch[1]; + } + + if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[2])) { + this.searchData.strictSearchPropertyValue = true; + this.searchData.searchPropertyValue = + FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[2])[1]; + } else { + this.searchData.searchPropertyValue = + this.searchData.searchPropertyMatch[2]; + } + + // Strict search for stylesheets will match the property line regex. + // Extract the search value within the backquotes to be used + // in the strict search for stylesheets in _highlightStyleSheet. + if (FILTER_STRICT_RE.test(this.searchValue)) { + this.searchData.strictSearchValue = + FILTER_STRICT_RE.exec(this.searchValue)[1]; + } + } else if (FILTER_STRICT_RE.test(this.searchValue)) { + // If the search value does not correspond to a property line and + // is contained in backquotes, extract the search value within the + // backquotes and set the flag to perform a strict search for all + // the values (selector, stylesheet, property and computed values). + let searchValue = FILTER_STRICT_RE.exec(this.searchValue)[1]; + this.searchData.strictSearchAllValues = true; + this.searchData.searchPropertyName = searchValue; + this.searchData.searchPropertyValue = searchValue; + this.searchData.strictSearchValue = searchValue; + } + + this._clearHighlight(this.element); + this._clearRules(); + this._createEditors(); + + this.inspector.emit("ruleview-filtered"); + + this._filterChangeTimeout = null; + }, filterTimeout); + }, + + /** + * Called when the user clicks on the clear button in the filter style search + * box. Returns true if the search box is cleared and false otherwise. + */ + _onClearSearch: function () { + if (this.searchField.value) { + this.setFilterStyles(""); + return true; + } + + return false; + }, + + destroy: function () { + this.isDestroyed = true; + this.clear(); + + this._dummyElement = null; + + this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged); + this._prefObserver.off(PREF_UA_STYLES, this._handlePrefChange); + this._prefObserver.off(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange); + this._prefObserver.destroy(); + + this._outputParser = null; + + // Remove context menu + if (this._contextmenu) { + this._contextmenu.destroy(); + this._contextmenu = null; + } + + this.tooltips.destroy(); + this.highlighters.destroy(); + + // Remove bound listeners + this.shortcuts.destroy(); + this.element.removeEventListener("copy", this._onCopy); + this.element.removeEventListener("contextmenu", this._onContextMenu); + this.addRuleButton.removeEventListener("click", this._onAddRule); + this.searchField.removeEventListener("input", this._onFilterStyles); + this.searchField.removeEventListener("contextmenu", + this.inspector.onTextBoxContextMenu); + this.searchClearButton.removeEventListener("click", this._onClearSearch); + this.pseudoClassToggle.removeEventListener("click", + this._onTogglePseudoClassPanel); + this.hoverCheckbox.removeEventListener("click", this._onTogglePseudoClass); + this.activeCheckbox.removeEventListener("click", this._onTogglePseudoClass); + this.focusCheckbox.removeEventListener("click", this._onTogglePseudoClass); + + this.searchField = null; + this.searchClearButton = null; + this.pseudoClassPanel = null; + this.pseudoClassToggle = null; + this.hoverCheckbox = null; + this.activeCheckbox = null; + this.focusCheckbox = null; + + this.inspector = null; + this.styleDocument = null; + this.styleWindow = null; + + if (this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + + if (this._elementStyle) { + this._elementStyle.destroy(); + } + + this.popup.destroy(); + }, + + /** + * Mark the view as selecting an element, disabling all interaction, and + * visually clearing the view after a few milliseconds to avoid confusion + * about which element's styles the rule view shows. + */ + _startSelectingElement: function () { + this.element.classList.add("non-interactive"); + }, + + /** + * Mark the view as no longer selecting an element, re-enabling interaction. + */ + _stopSelectingElement: function () { + this.element.classList.remove("non-interactive"); + }, + + /** + * Update the view with a new selected element. + * + * @param {NodeActor} element + * The node whose style rules we'll inspect. + * @param {Boolean} allowRefresh + * Update the view even if the element is the same as last time. + */ + selectElement: function (element, allowRefresh = false) { + let refresh = (this._viewedElement === element); + if (refresh && !allowRefresh) { + return promise.resolve(undefined); + } + + if (this.popup.isOpen) { + this.popup.hidePopup(); + } + + this.clear(false); + this._viewedElement = element; + + this.clearPseudoClassPanel(); + this.refreshAddRuleButtonState(); + + if (!this._viewedElement) { + this._stopSelectingElement(); + this._clearRules(); + this._showEmpty(); + this.refreshPseudoClassPanel(); + return promise.resolve(undefined); + } + + // To figure out how shorthand properties are interpreted by the + // engine, we will set properties on a dummy element and observe + // how their .style attribute reflects them as computed values. + let dummyElementPromise = promise.resolve(this.styleDocument).then(document => { + // ::before and ::after do not have a namespaceURI + let namespaceURI = this.element.namespaceURI || + document.documentElement.namespaceURI; + this._dummyElement = document.createElementNS(namespaceURI, + this.element.tagName); + }).then(null, promiseWarn); + + let elementStyle = new ElementStyle(element, this, this.store, + this.pageStyle, this.showUserAgentStyles); + this._elementStyle = elementStyle; + + this._startSelectingElement(); + + return dummyElementPromise.then(() => { + if (this._elementStyle === elementStyle) { + return this._populate(); + } + return undefined; + }).then(() => { + if (this._elementStyle === elementStyle) { + if (!refresh) { + this.element.scrollTop = 0; + } + this._stopSelectingElement(); + this._elementStyle.onChanged = () => { + this._changed(); + }; + } + }).then(null, e => { + if (this._elementStyle === elementStyle) { + this._stopSelectingElement(); + this._clearRules(); + } + console.error(e); + }); + }, + + /** + * Update the rules for the currently highlighted element. + */ + refreshPanel: function () { + // Ignore refreshes during editing or when no element is selected. + if (this.isEditing || !this._elementStyle) { + return promise.resolve(undefined); + } + + // Repopulate the element style once the current modifications are done. + let promises = []; + for (let rule of this._elementStyle.rules) { + if (rule._applyingModifications) { + promises.push(rule._applyingModifications); + } + } + + return promise.all(promises).then(() => { + return this._populate(); + }); + }, + + /** + * Clear the pseudo class options panel by removing the checked and disabled + * attributes for each checkbox. + */ + clearPseudoClassPanel: function () { + this.hoverCheckbox.checked = this.hoverCheckbox.disabled = false; + this.activeCheckbox.checked = this.activeCheckbox.disabled = false; + this.focusCheckbox.checked = this.focusCheckbox.disabled = false; + }, + + /** + * Update the pseudo class options for the currently highlighted element. + */ + refreshPseudoClassPanel: function () { + if (!this._elementStyle || !this.inspector.selection.isElementNode()) { + this.hoverCheckbox.disabled = true; + this.activeCheckbox.disabled = true; + this.focusCheckbox.disabled = true; + return; + } + + for (let pseudoClassLock of this._elementStyle.element.pseudoClassLocks) { + switch (pseudoClassLock) { + case ":hover": { + this.hoverCheckbox.checked = true; + break; + } + case ":active": { + this.activeCheckbox.checked = true; + break; + } + case ":focus": { + this.focusCheckbox.checked = true; + break; + } + } + } + }, + + _populate: function () { + let elementStyle = this._elementStyle; + return this._elementStyle.populate().then(() => { + if (this._elementStyle !== elementStyle || this.isDestroyed) { + return null; + } + + this._clearRules(); + let onEditorsReady = this._createEditors(); + this.refreshPseudoClassPanel(); + + // Notify anyone that cares that we refreshed. + return onEditorsReady.then(() => { + this.emit("ruleview-refreshed"); + }, e => console.error(e)); + }).then(null, promiseWarn); + }, + + /** + * Show the user that the rule view has no node selected. + */ + _showEmpty: function () { + if (this.styleDocument.getElementById("ruleview-no-results")) { + return; + } + + createChild(this.element, "div", { + id: "ruleview-no-results", + textContent: l10n("rule.empty") + }); + }, + + /** + * Clear the rules. + */ + _clearRules: function () { + this.element.innerHTML = ""; + }, + + /** + * Clear the rule view. + */ + clear: function (clearDom = true) { + this.lastSelectorIcon = null; + + if (clearDom) { + this._clearRules(); + } + this._viewedElement = null; + + if (this._elementStyle) { + this._elementStyle.destroy(); + this._elementStyle = null; + } + }, + + /** + * Called when the user has made changes to the ElementStyle. + * Emits an event that clients can listen to. + */ + _changed: function () { + this.emit("ruleview-changed"); + }, + + /** + * Text for header that shows above rules for this element + */ + get selectedElementLabel() { + if (this._selectedElementLabel) { + return this._selectedElementLabel; + } + this._selectedElementLabel = l10n("rule.selectedElement"); + return this._selectedElementLabel; + }, + + /** + * Text for header that shows above rules for pseudo elements + */ + get pseudoElementLabel() { + if (this._pseudoElementLabel) { + return this._pseudoElementLabel; + } + this._pseudoElementLabel = l10n("rule.pseudoElement"); + return this._pseudoElementLabel; + }, + + get showPseudoElements() { + if (this._showPseudoElements === undefined) { + this._showPseudoElements = + Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements"); + } + return this._showPseudoElements; + }, + + /** + * Creates an expandable container in the rule view + * + * @param {String} label + * The label for the container header + * @param {Boolean} isPseudo + * Whether or not the container will hold pseudo element rules + * @return {DOMNode} The container element + */ + createExpandableContainer: function (label, isPseudo = false) { + let header = this.styleDocument.createElementNS(HTML_NS, "div"); + header.className = this._getRuleViewHeaderClassName(true); + header.textContent = label; + + let twisty = this.styleDocument.createElementNS(HTML_NS, "span"); + twisty.className = "ruleview-expander theme-twisty"; + twisty.setAttribute("open", "true"); + + header.insertBefore(twisty, header.firstChild); + this.element.appendChild(header); + + let container = this.styleDocument.createElementNS(HTML_NS, "div"); + container.classList.add("ruleview-expandable-container"); + container.hidden = false; + this.element.appendChild(container); + + header.addEventListener("dblclick", () => { + this._toggleContainerVisibility(twisty, container, isPseudo, + !this.showPseudoElements); + }, false); + + twisty.addEventListener("click", () => { + this._toggleContainerVisibility(twisty, container, isPseudo, + !this.showPseudoElements); + }, false); + + if (isPseudo) { + this._toggleContainerVisibility(twisty, container, isPseudo, + this.showPseudoElements); + } + + return container; + }, + + /** + * Toggle the visibility of an expandable container + * + * @param {DOMNode} twisty + * Clickable toggle DOM Node + * @param {DOMNode} container + * Expandable container DOM Node + * @param {Boolean} isPseudo + * Whether or not the container will hold pseudo element rules + * @param {Boolean} showPseudo + * Whether or not pseudo element rules should be displayed + */ + _toggleContainerVisibility: function (twisty, container, isPseudo, + showPseudo) { + let isOpen = twisty.getAttribute("open"); + + if (isPseudo) { + this._showPseudoElements = !!showPseudo; + + Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements", + this.showPseudoElements); + + container.hidden = !this.showPseudoElements; + isOpen = !this.showPseudoElements; + } else { + container.hidden = !container.hidden; + } + + if (isOpen) { + twisty.removeAttribute("open"); + } else { + twisty.setAttribute("open", "true"); + } + }, + + _getRuleViewHeaderClassName: function (isPseudo) { + let baseClassName = "theme-gutter ruleview-header"; + return isPseudo ? baseClassName + " ruleview-expandable-header" : + baseClassName; + }, + + /** + * Creates editor UI for each of the rules in _elementStyle. + */ + _createEditors: function () { + // Run through the current list of rules, attaching + // their editors in order. Create editors if needed. + let lastInheritedSource = ""; + let lastKeyframes = null; + let seenPseudoElement = false; + let seenNormalElement = false; + let seenSearchTerm = false; + let container = null; + + if (!this._elementStyle.rules) { + return promise.resolve(); + } + + let editorReadyPromises = []; + for (let rule of this._elementStyle.rules) { + if (rule.domRule.system) { + continue; + } + + // Initialize rule editor + if (!rule.editor) { + rule.editor = new RuleEditor(this, rule); + editorReadyPromises.push(rule.editor.once("source-link-updated")); + } + + // Filter the rules and highlight any matches if there is a search input + if (this.searchValue && this.searchData) { + if (this.highlightRule(rule)) { + seenSearchTerm = true; + } else if (rule.domRule.type !== ELEMENT_STYLE) { + continue; + } + } + + // Only print header for this element if there are pseudo elements + if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) { + seenNormalElement = true; + let div = this.styleDocument.createElementNS(HTML_NS, "div"); + div.className = this._getRuleViewHeaderClassName(); + div.textContent = this.selectedElementLabel; + this.element.appendChild(div); + } + + let inheritedSource = rule.inheritedSource; + if (inheritedSource && inheritedSource !== lastInheritedSource) { + let div = this.styleDocument.createElementNS(HTML_NS, "div"); + div.className = this._getRuleViewHeaderClassName(); + div.textContent = inheritedSource; + lastInheritedSource = inheritedSource; + this.element.appendChild(div); + } + + if (!seenPseudoElement && rule.pseudoElement) { + seenPseudoElement = true; + container = this.createExpandableContainer(this.pseudoElementLabel, + true); + } + + let keyframes = rule.keyframes; + if (keyframes && keyframes !== lastKeyframes) { + lastKeyframes = keyframes; + container = this.createExpandableContainer(rule.keyframesName); + } + + if (container && (rule.pseudoElement || keyframes)) { + container.appendChild(rule.editor.element); + } else { + this.element.appendChild(rule.editor.element); + } + } + + if (this.searchValue && !seenSearchTerm) { + this.searchField.classList.add("devtools-style-searchbox-no-match"); + } else { + this.searchField.classList.remove("devtools-style-searchbox-no-match"); + } + + return promise.all(editorReadyPromises); + }, + + /** + * Highlight rules that matches the filter search value and returns a + * boolean indicating whether or not rules were highlighted. + * + * @param {Rule} rule + * The rule object we're highlighting if its rule selectors or + * property values match the search value. + * @return {Boolean} true if the rule was highlighted, false otherwise. + */ + highlightRule: function (rule) { + let isRuleSelectorHighlighted = this._highlightRuleSelector(rule); + let isStyleSheetHighlighted = this._highlightStyleSheet(rule); + let isHighlighted = isRuleSelectorHighlighted || isStyleSheetHighlighted; + + // Highlight search matches in the rule properties + for (let textProp of rule.textProps) { + if (!textProp.invisible && this._highlightProperty(textProp.editor)) { + isHighlighted = true; + } + } + + return isHighlighted; + }, + + /** + * Highlights the rule selector that matches the filter search value and + * returns a boolean indicating whether or not the selector was highlighted. + * + * @param {Rule} rule + * The Rule object. + * @return {Boolean} true if the rule selector was highlighted, + * false otherwise. + */ + _highlightRuleSelector: function (rule) { + let isSelectorHighlighted = false; + + let selectorNodes = [...rule.editor.selectorText.childNodes]; + if (rule.domRule.type === CSSRule.KEYFRAME_RULE) { + selectorNodes = [rule.editor.selectorText]; + } else if (rule.domRule.type === ELEMENT_STYLE) { + selectorNodes = []; + } + + // Highlight search matches in the rule selectors + for (let selectorNode of selectorNodes) { + let selector = selectorNode.textContent.toLowerCase(); + if ((this.searchData.strictSearchAllValues && + selector === this.searchData.strictSearchValue) || + (!this.searchData.strictSearchAllValues && + selector.includes(this.searchValue))) { + selectorNode.classList.add("ruleview-highlight"); + isSelectorHighlighted = true; + } + } + + return isSelectorHighlighted; + }, + + /** + * Highlights the stylesheet source that matches the filter search value and + * returns a boolean indicating whether or not the stylesheet source was + * highlighted. + * + * @return {Boolean} true if the stylesheet source was highlighted, false + * otherwise. + */ + _highlightStyleSheet: function (rule) { + let styleSheetSource = rule.title.toLowerCase(); + let isStyleSheetHighlighted = this.searchData.strictSearchValue ? + styleSheetSource === this.searchData.strictSearchValue : + styleSheetSource.includes(this.searchValue); + + if (isStyleSheetHighlighted) { + rule.editor.source.classList.add("ruleview-highlight"); + } + + return isStyleSheetHighlighted; + }, + + /** + * Highlights the rule properties and computed properties that match the + * filter search value and returns a boolean indicating whether or not the + * property or computed property was highlighted. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + * @return {Boolean} true if the property or computed property was + * highlighted, false otherwise. + */ + _highlightProperty: function (editor) { + let isPropertyHighlighted = this._highlightRuleProperty(editor); + let isComputedHighlighted = this._highlightComputedProperty(editor); + + // Expand the computed list if a computed property is highlighted and the + // property rule is not highlighted + if (!isPropertyHighlighted && isComputedHighlighted && + !editor.computed.hasAttribute("user-open")) { + editor.expandForFilter(); + } + + return isPropertyHighlighted || isComputedHighlighted; + }, + + /** + * Called when TextPropertyEditor is updated and updates the rule property + * highlight. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + */ + _updatePropertyHighlight: function (editor) { + if (!this.searchValue || !this.searchData) { + return; + } + + this._clearHighlight(editor.element); + + if (this._highlightProperty(editor)) { + this.searchField.classList.remove("devtools-style-searchbox-no-match"); + } + }, + + /** + * Highlights the rule property that matches the filter search value + * and returns a boolean indicating whether or not the property was + * highlighted. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + * @return {Boolean} true if the rule property was highlighted, + * false otherwise. + */ + _highlightRuleProperty: function (editor) { + // Get the actual property value displayed in the rule view + let propertyName = editor.prop.name.toLowerCase(); + let propertyValue = editor.valueSpan.textContent.toLowerCase(); + + return this._highlightMatches(editor.container, propertyName, + propertyValue); + }, + + /** + * Highlights the computed property that matches the filter search value and + * returns a boolean indicating whether or not the computed property was + * highlighted. + * + * @param {TextPropertyEditor} editor + * The rule property TextPropertyEditor object. + * @return {Boolean} true if the computed property was highlighted, false + * otherwise. + */ + _highlightComputedProperty: function (editor) { + let isComputedHighlighted = false; + + // Highlight search matches in the computed list of properties + editor._populateComputed(); + for (let computed of editor.prop.computed) { + if (computed.element) { + // Get the actual property value displayed in the computed list + let computedName = computed.name.toLowerCase(); + let computedValue = computed.parsedValue.toLowerCase(); + + isComputedHighlighted = this._highlightMatches(computed.element, + computedName, computedValue) ? true : isComputedHighlighted; + } + } + + return isComputedHighlighted; + }, + + /** + * Helper function for highlightRules that carries out highlighting the given + * element if the search terms match the property, and returns a boolean + * indicating whether or not the search terms match. + * + * @param {DOMNode} element + * The node to highlight if search terms match + * @param {String} propertyName + * The property name of a rule + * @param {String} propertyValue + * The property value of a rule + * @return {Boolean} true if the given search terms match the property, false + * otherwise. + */ + _highlightMatches: function (element, propertyName, propertyValue) { + let { + searchPropertyName, + searchPropertyValue, + searchPropertyMatch, + strictSearchPropertyName, + strictSearchPropertyValue, + strictSearchAllValues, + } = this.searchData; + let matches = false; + + // If the inputted search value matches a property line like + // `font-family: arial`, then check to make sure the name and value match. + // Otherwise, just compare the inputted search string directly against the + // name and value of the rule property. + let hasNameAndValue = searchPropertyMatch && + searchPropertyName && + searchPropertyValue; + let isMatch = (value, query, isStrict) => { + return isStrict ? value === query : query && value.includes(query); + }; + + if (hasNameAndValue) { + matches = + isMatch(propertyName, searchPropertyName, strictSearchPropertyName) && + isMatch(propertyValue, searchPropertyValue, strictSearchPropertyValue); + } else { + matches = + isMatch(propertyName, searchPropertyName, + strictSearchPropertyName || strictSearchAllValues) || + isMatch(propertyValue, searchPropertyValue, + strictSearchPropertyValue || strictSearchAllValues); + } + + if (matches) { + element.classList.add("ruleview-highlight"); + } + + return matches; + }, + + /** + * Clear all search filter highlights in the panel, and close the computed + * list if toggled opened + */ + _clearHighlight: function (element) { + for (let el of element.querySelectorAll(".ruleview-highlight")) { + el.classList.remove("ruleview-highlight"); + } + + for (let computed of element.querySelectorAll( + ".ruleview-computedlist[filter-open]")) { + computed.parentNode._textPropertyEditor.collapseForFilter(); + } + }, + + /** + * Called when the pseudo class panel button is clicked and toggles + * the display of the pseudo class panel. + */ + _onTogglePseudoClassPanel: function () { + if (this.pseudoClassPanel.hidden) { + this.pseudoClassToggle.setAttribute("checked", "true"); + this.hoverCheckbox.setAttribute("tabindex", "0"); + this.activeCheckbox.setAttribute("tabindex", "0"); + this.focusCheckbox.setAttribute("tabindex", "0"); + } else { + this.pseudoClassToggle.removeAttribute("checked"); + this.hoverCheckbox.setAttribute("tabindex", "-1"); + this.activeCheckbox.setAttribute("tabindex", "-1"); + this.focusCheckbox.setAttribute("tabindex", "-1"); + } + + this.pseudoClassPanel.hidden = !this.pseudoClassPanel.hidden; + }, + + /** + * Called when a pseudo class checkbox is clicked and toggles + * the pseudo class for the current selected element. + */ + _onTogglePseudoClass: function (event) { + let target = event.currentTarget; + this.inspector.togglePseudoClass(target.value); + }, + + /** + * Handle the keypress event in the rule view. + */ + _onShortcut: function (name, event) { + if (!event.target.closest("#sidebar-panel-ruleview")) { + return; + } + + if (name === "CmdOrCtrl+F") { + this.searchField.focus(); + event.preventDefault(); + } else if ((name === "Return" || name === "Space") && + this.element.classList.contains("non-interactive")) { + event.preventDefault(); + } else if (name === "Escape" && + event.target === this.searchField && + this._onClearSearch()) { + // Handle the search box's keypress event. If the escape key is pressed, + // clear the search box field. + event.preventDefault(); + event.stopPropagation(); + } + } + +}; + +/** + * Helper functions + */ + +/** + * Walk up the DOM from a given node until a parent property holder is found. + * For elements inside the computed property list, the non-computed parent + * property holder will be returned + * + * @param {DOMNode} node + * The node to start from + * @return {DOMNode} The parent property holder node, or null if not found + */ +function getParentTextPropertyHolder(node) { + while (true) { + if (!node || !node.classList) { + return null; + } + if (node.classList.contains("ruleview-property")) { + return node; + } + node = node.parentNode; + } +} + +/** + * For any given node, find the TextProperty it is in if any + * @param {DOMNode} node + * The node to start from + * @return {TextProperty} + */ +function getParentTextProperty(node) { + let parent = getParentTextPropertyHolder(node); + if (!parent) { + return null; + } + + let propValue = parent.querySelector(".ruleview-propertyvalue"); + if (!propValue) { + return null; + } + + return propValue.textProperty; +} + +/** + * Walker up the DOM from a given node until a parent property holder is found, + * and return the textContent for the name and value nodes. + * Stops at the first property found, so if node is inside the computed property + * list, the computed property will be returned + * + * @param {DOMNode} node + * The node to start from + * @return {Object} {name, value} + */ +function getPropertyNameAndValue(node) { + while (true) { + if (!node || !node.classList) { + return null; + } + // Check first for ruleview-computed since it's the deepest + if (node.classList.contains("ruleview-computed") || + node.classList.contains("ruleview-property")) { + return { + name: node.querySelector(".ruleview-propertyname").textContent, + value: node.querySelector(".ruleview-propertyvalue").textContent + }; + } + node = node.parentNode; + } +} + +function RuleViewTool(inspector, window) { + this.inspector = inspector; + this.document = window.document; + + this.view = new CssRuleView(this.inspector, this.document); + + this.clearUserProperties = this.clearUserProperties.bind(this); + this.refresh = this.refresh.bind(this); + this.onLinkClicked = this.onLinkClicked.bind(this); + this.onMutations = this.onMutations.bind(this); + this.onPanelSelected = this.onPanelSelected.bind(this); + this.onPropertyChanged = this.onPropertyChanged.bind(this); + this.onResized = this.onResized.bind(this); + this.onSelected = this.onSelected.bind(this); + this.onViewRefreshed = this.onViewRefreshed.bind(this); + + this.view.on("ruleview-changed", this.onPropertyChanged); + this.view.on("ruleview-refreshed", this.onViewRefreshed); + this.view.on("ruleview-linked-clicked", this.onLinkClicked); + + this.inspector.selection.on("detached-front", this.onSelected); + this.inspector.selection.on("new-node-front", this.onSelected); + this.inspector.selection.on("pseudoclass", this.refresh); + this.inspector.target.on("navigate", this.clearUserProperties); + this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected); + this.inspector.pageStyle.on("stylesheet-updated", this.refresh); + this.inspector.walker.on("mutations", this.onMutations); + this.inspector.walker.on("resize", this.onResized); + + this.onSelected(); +} + +RuleViewTool.prototype = { + isSidebarActive: function () { + if (!this.view) { + return false; + } + return this.inspector.sidebar.getCurrentTabID() == "ruleview"; + }, + + onSelected: function (event) { + // Ignore the event if the view has been destroyed, or if it's inactive. + // But only if the current selection isn't null. If it's been set to null, + // let the update go through as this is needed to empty the view on + // navigation. + if (!this.view) { + return; + } + + let isInactive = !this.isSidebarActive() && + this.inspector.selection.nodeFront; + if (isInactive) { + return; + } + + this.view.setPageStyle(this.inspector.pageStyle); + + if (!this.inspector.selection.isConnected() || + !this.inspector.selection.isElementNode()) { + this.view.selectElement(null); + return; + } + + if (!event || event == "new-node-front") { + let done = this.inspector.updating("rule-view"); + this.view.selectElement(this.inspector.selection.nodeFront) + .then(done, done); + } + }, + + refresh: function () { + if (this.isSidebarActive()) { + this.view.refreshPanel(); + } + }, + + clearUserProperties: function () { + if (this.view && this.view.store && this.view.store.userProperties) { + this.view.store.userProperties.clear(); + } + }, + + onPanelSelected: function () { + if (this.inspector.selection.nodeFront === this.view._viewedElement) { + this.refresh(); + } else { + this.onSelected(); + } + }, + + onLinkClicked: function (e, rule) { + let sheet = rule.parentStyleSheet; + + // Chrome stylesheets are not listed in the style editor, so show + // these sheets in the view source window instead. + if (!sheet || sheet.isSystem) { + let href = rule.nodeHref || rule.href; + let toolbox = gDevTools.getToolbox(this.inspector.target); + toolbox.viewSource(href, rule.line); + return; + } + + let location = promise.resolve(rule.location); + if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { + location = rule.getOriginalLocation(); + } + location.then(({ source, href, line, column }) => { + let target = this.inspector.target; + if (Tools.styleEditor.isTargetSupported(target)) { + gDevTools.showToolbox(target, "styleeditor").then(function (toolbox) { + let url = source || href; + toolbox.getCurrentPanel().selectStyleSheet(url, line, column); + }); + } + return; + }); + }, + + onPropertyChanged: function () { + this.inspector.markDirty(); + }, + + onViewRefreshed: function () { + this.inspector.emit("rule-view-refreshed"); + }, + + /** + * When markup mutations occur, if an attribute of the selected node changes, + * we need to refresh the view as that might change the node's styles. + */ + onMutations: function (mutations) { + for (let {type, target} of mutations) { + if (target === this.inspector.selection.nodeFront && + type === "attributes") { + this.refresh(); + break; + } + } + }, + + /** + * When the window gets resized, this may cause media-queries to match, and + * therefore, different styles may apply. + */ + onResized: function () { + this.refresh(); + }, + + destroy: function () { + this.inspector.walker.off("mutations", this.onMutations); + this.inspector.walker.off("resize", this.onResized); + this.inspector.selection.off("detached-front", this.onSelected); + this.inspector.selection.off("pseudoclass", this.refresh); + this.inspector.selection.off("new-node-front", this.onSelected); + this.inspector.target.off("navigate", this.clearUserProperties); + this.inspector.sidebar.off("ruleview-selected", this.onPanelSelected); + if (this.inspector.pageStyle) { + this.inspector.pageStyle.off("stylesheet-updated", this.refresh); + } + + this.view.off("ruleview-linked-clicked", this.onLinkClicked); + this.view.off("ruleview-changed", this.onPropertyChanged); + this.view.off("ruleview-refreshed", this.onViewRefreshed); + + this.view.destroy(); + + this.view = this.document = this.inspector = null; + } +}; + +exports.CssRuleView = CssRuleView; +exports.RuleViewTool = RuleViewTool; |