summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/rules/rules.js
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/inspector/rules/rules.js
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/inspector/rules/rules.js')
-rw-r--r--devtools/client/inspector/rules/rules.js1673
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;