tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

rule-editor.js (45201B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const { l10n } = require("resource://devtools/shared/inspector/css-logic.js");
      8 const {
      9  PSEUDO_CLASSES,
     10 } = require("resource://devtools/shared/css/constants.js");
     11 const {
     12  style: { ELEMENT_STYLE, PRES_HINTS },
     13 } = require("resource://devtools/shared/constants.js");
     14 const Rule = require("resource://devtools/client/inspector/rules/models/rule.js");
     15 const {
     16  InplaceEditor,
     17  editableField,
     18  editableItem,
     19 } = require("resource://devtools/client/shared/inplace-editor.js");
     20 const TextPropertyEditor = require("resource://devtools/client/inspector/rules/views/text-property-editor.js");
     21 const {
     22  createChild,
     23  blurOnMultipleProperties,
     24  promiseWarn,
     25 } = require("resource://devtools/client/inspector/shared/utils.js");
     26 const {
     27  parseNamedDeclarations,
     28  parsePseudoClassesAndAttributes,
     29  SELECTOR_ATTRIBUTE,
     30  SELECTOR_ELEMENT,
     31  SELECTOR_PSEUDO_CLASS,
     32 } = require("resource://devtools/shared/css/parsing-utils.js");
     33 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     34 const CssLogic = require("resource://devtools/shared/inspector/css-logic.js");
     35 
     36 loader.lazyRequireGetter(
     37  this,
     38  "Tools",
     39  "resource://devtools/client/definitions.js",
     40  true
     41 );
     42 loader.lazyRequireGetter(
     43  this,
     44  "PluralForm",
     45  "resource://devtools/shared/plural-form.js",
     46  true
     47 );
     48 
     49 const STYLE_INSPECTOR_PROPERTIES =
     50  "devtools/shared/locales/styleinspector.properties";
     51 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     52 const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
     53 
     54 const COMPONENT_PROPERTIES = "devtools/client/locales/components.properties";
     55 const COMPONENT_L10N = new LocalizationHelper(COMPONENT_PROPERTIES);
     56 
     57 loader.lazyGetter(this, "NEW_PROPERTY_NAME_INPUT_LABEL", function () {
     58  return STYLE_INSPECTOR_L10N.getStr("rule.newPropertyName.label");
     59 });
     60 
     61 const UNUSED_CSS_PROPERTIES_HIDE_THRESHOLD = 10;
     62 const INDENT_SIZE = 2;
     63 const INDENT_STR = " ".repeat(INDENT_SIZE);
     64 
     65 /**
     66 * RuleEditor is responsible for the following:
     67 *   Owns a Rule object and creates a list of TextPropertyEditors
     68 *     for its TextProperties.
     69 *   Manages creation of new text properties.
     70 */
     71 class RuleEditor extends EventEmitter {
     72  /**
     73   * @param {CssRuleView} ruleView
     74   *        The CssRuleView containg the document holding this rule editor.
     75   * @param {Rule} rule
     76   *        The Rule object we're editing.
     77   * @param {object} options
     78   * @param {Set} options.elementsWithPendingClicks
     79   * @param {Function} options.onShowUnusedCustomCssProperties
     80   * @param {boolean} options.shouldHideUnusedCustomCssProperties
     81   */
     82  constructor(ruleView, rule, options = {}) {
     83    super();
     84 
     85    this.ruleView = ruleView;
     86    this.doc = this.ruleView.styleDocument;
     87    this.toolbox = this.ruleView.inspector.toolbox;
     88    this.telemetry = this.toolbox.telemetry;
     89    this.rule = rule;
     90    this.options = options;
     91 
     92    this.isEditable = rule.isEditable();
     93    // Flag that blocks updates of the selector and properties when it is
     94    // being edited
     95    this.isEditing = false;
     96 
     97    this._onNewProperty = this._onNewProperty.bind(this);
     98    this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
     99    this._onSelectorDone = this._onSelectorDone.bind(this);
    100    this._locationChanged = this._locationChanged.bind(this);
    101    this.updateSourceLink = this.updateSourceLink.bind(this);
    102    this._onToolChanged = this._onToolChanged.bind(this);
    103    this._updateLocation = this._updateLocation.bind(this);
    104    this._onSourceClick = this._onSourceClick.bind(this);
    105    this._onShowUnusedCustomCssPropertiesButtonClick =
    106      this._onShowUnusedCustomCssPropertiesButtonClick.bind(this);
    107 
    108    this.rule.domRule.on("location-changed", this._locationChanged);
    109    this.toolbox.on("tool-registered", this._onToolChanged);
    110    this.toolbox.on("tool-unregistered", this._onToolChanged);
    111 
    112    this._create();
    113  }
    114  destroy() {
    115    for (const prop of this.rule.textProps) {
    116      prop.editor?.destroy();
    117    }
    118 
    119    this._unusedCssVariableDeclarations = null;
    120 
    121    if (this._showUnusedCustomCssPropertiesButton) {
    122      this._nullifyShowUnusedCustomCssProperties({ removeFromDom: false });
    123    }
    124 
    125    this.rule.domRule.off("location-changed");
    126    this.toolbox.off("tool-registered", this._onToolChanged);
    127    this.toolbox.off("tool-unregistered", this._onToolChanged);
    128 
    129    if (this._unsubscribeSourceMap) {
    130      this._unsubscribeSourceMap();
    131    }
    132  }
    133 
    134  get sourceMapURLService() {
    135    if (!this._sourceMapURLService) {
    136      // sourceMapURLService is a lazy getter in the toolbox.
    137      this._sourceMapURLService = this.toolbox.sourceMapURLService;
    138    }
    139 
    140    return this._sourceMapURLService;
    141  }
    142 
    143  get isSelectorEditable() {
    144    return (
    145      this.isEditable &&
    146      this.rule.domRule.type !== ELEMENT_STYLE &&
    147      this.rule.domRule.type !== CSSRule.KEYFRAME_RULE &&
    148      this.rule.domRule.className !== "CSSPositionTryRule"
    149    );
    150  }
    151 
    152  get showSelectorHighlighterButton() {
    153    return (
    154      this.rule.domRule.type !== CSSRule.KEYFRAME_RULE &&
    155      this.rule.domRule.className !== "CSSPositionTryRule"
    156    );
    157  }
    158 
    159  _create() {
    160    this.element = this.doc.createElement("div");
    161    this.element.className =
    162      "ruleview-rule devtools-monospace" +
    163      (this.rule.inherited ? " ruleview-rule-inherited" : "");
    164    this.element.dataset.ruleId = this.rule.domRule.actorID;
    165    this.element.setAttribute("uneditable", !this.isEditable);
    166    this.element.setAttribute("unmatched", this.rule.isUnmatched);
    167    this.element._ruleEditor = this;
    168 
    169    // Give a relative position for the inplace editor's measurement
    170    // span to be placed absolutely against.
    171    this.element.style.position = "relative";
    172 
    173    // Add the source link for supported rules. inline style and pres hints are not visible
    174    // in the StyleEditor, so don't show anything for such rule.
    175    if (
    176      this.rule.domRule.type !== ELEMENT_STYLE &&
    177      this.rule.domRule.type !== PRES_HINTS
    178    ) {
    179      this.source = createChild(this.element, "div", {
    180        class: "ruleview-rule-source theme-link",
    181      });
    182      this.source.addEventListener("click", this._onSourceClick);
    183 
    184      const sourceLabel = this.doc.createElement("a");
    185      sourceLabel.classList.add("ruleview-rule-source-label");
    186      this.source.appendChild(sourceLabel);
    187    }
    188    this.updateSourceLink();
    189 
    190    if (this.rule.domRule.ancestorData.length) {
    191      const ancestorsFrag = this.doc.createDocumentFragment();
    192      this.rule.domRule.ancestorData.forEach((ancestorData, index) => {
    193        const ancestorItem = this.doc.createElement("div");
    194        ancestorItem.setAttribute("role", "listitem");
    195        ancestorsFrag.append(ancestorItem);
    196        ancestorItem.setAttribute("data-ancestor-index", index);
    197        ancestorItem.classList.add("ruleview-rule-ancestor");
    198        if (ancestorData.type) {
    199          ancestorItem.classList.add(ancestorData.type);
    200        }
    201 
    202        // Indent each parent selector
    203        if (index) {
    204          createChild(ancestorItem, "span", {
    205            class: "ruleview-rule-indent",
    206            textContent: INDENT_STR.repeat(index),
    207          });
    208        }
    209 
    210        const selectorContainer = createChild(ancestorItem, "span", {
    211          class: "ruleview-rule-ancestor-selectorcontainer",
    212        });
    213 
    214        if (ancestorData.type == "container") {
    215          ancestorItem.classList.add("container-query", "has-tooltip");
    216 
    217          createChild(selectorContainer, "span", {
    218            class: "container-query-declaration",
    219            textContent: `@container${ancestorData.containerName ? " " + ancestorData.containerName : ""}`,
    220          });
    221 
    222          const jumpToNodeButton = createChild(selectorContainer, "button", {
    223            class: "open-inspector",
    224            title: l10n("rule.containerQuery.selectContainerButton.tooltip"),
    225          });
    226 
    227          let containerNodeFront;
    228          const getNodeFront = async () => {
    229            if (!containerNodeFront) {
    230              const res = await this.rule.domRule.getQueryContainerForNode(
    231                index,
    232                this.rule.inherited ||
    233                  this.ruleView.inspector.selection.nodeFront
    234              );
    235              containerNodeFront = res.node;
    236            }
    237            return containerNodeFront;
    238          };
    239 
    240          jumpToNodeButton.addEventListener("click", async () => {
    241            const front = await getNodeFront();
    242            if (!front) {
    243              return;
    244            }
    245            this.ruleView.inspector.selection.setNodeFront(front);
    246            await this.ruleView.inspector.highlighters.hideHighlighterType(
    247              this.ruleView.inspector.highlighters.TYPES.BOXMODEL
    248            );
    249          });
    250 
    251          ancestorItem.addEventListener("mouseenter", async () => {
    252            const front = await getNodeFront();
    253            if (!front) {
    254              return;
    255            }
    256 
    257            await this.ruleView.inspector.highlighters.showHighlighterTypeForNode(
    258              this.ruleView.inspector.highlighters.TYPES.BOXMODEL,
    259              front
    260            );
    261          });
    262          ancestorItem.addEventListener("mouseleave", async () => {
    263            await this.ruleView.inspector.highlighters.hideHighlighterType(
    264              this.ruleView.inspector.highlighters.TYPES.BOXMODEL
    265            );
    266          });
    267 
    268          createChild(selectorContainer, "span", {
    269            // Add a space between the container name (or @container if there's no name)
    270            // and the query so the title, which is computed from the DOM, displays correctly.
    271            textContent: " " + ancestorData.containerQuery,
    272          });
    273        } else if (ancestorData.type == "layer") {
    274          selectorContainer.append(
    275            this.doc.createTextNode(
    276              `@layer${ancestorData.value ? " " + ancestorData.value : ""}`
    277            )
    278          );
    279        } else if (ancestorData.type == "media") {
    280          selectorContainer.append(
    281            this.doc.createTextNode(`@media ${ancestorData.value}`)
    282          );
    283        } else if (ancestorData.type == "supports") {
    284          selectorContainer.append(
    285            this.doc.createTextNode(`@supports ${ancestorData.conditionText}`)
    286          );
    287        } else if (ancestorData.type == "import") {
    288          selectorContainer.append(
    289            this.doc.createTextNode(`@import ${ancestorData.value}`)
    290          );
    291        } else if (ancestorData.type == "scope") {
    292          let text = `@scope`;
    293          if (ancestorData.start) {
    294            text += ` (${ancestorData.start})`;
    295 
    296            if (ancestorData.end) {
    297              text += ` to (${ancestorData.end})`;
    298            }
    299          }
    300          selectorContainer.append(this.doc.createTextNode(text));
    301        } else if (ancestorData.type == "starting-style") {
    302          selectorContainer.append(this.doc.createTextNode(`@starting-style`));
    303        } else if (ancestorData.selectors) {
    304          ancestorData.selectors.forEach((selector, i) => {
    305            if (i !== 0) {
    306              createChild(selectorContainer, "span", {
    307                class: "ruleview-selector-separator",
    308                textContent: ", ",
    309              });
    310            }
    311 
    312            const selectorEl = createChild(selectorContainer, "span", {
    313              class: "ruleview-selector",
    314              textContent: selector,
    315            });
    316 
    317            const warningsContainer = this._createWarningsElementForSelector(
    318              i,
    319              ancestorData.selectorWarnings
    320            );
    321            if (warningsContainer) {
    322              selectorEl.append(warningsContainer);
    323            }
    324          });
    325        } else {
    326          // We shouldn't get here as `type` should only match to what can be set in
    327          // the StyleRuleActor form, but just in case, let's return an empty string.
    328          console.warn("Unknown ancestor data type:", ancestorData.type);
    329          return;
    330        }
    331 
    332        createChild(ancestorItem, "span", {
    333          class: "ruleview-ancestor-ruleopen",
    334          textContent: " {",
    335        });
    336      });
    337 
    338      // We can't use a proper "ol" as it will mess with selection copy text,
    339      // adding spaces on list item instead of the one we craft (.ruleview-rule-indent)
    340      this.ancestorDataEl = createChild(this.element, "div", {
    341        class: "ruleview-rule-ancestor-data theme-link",
    342        role: "list",
    343      });
    344      this.ancestorDataEl.append(ancestorsFrag);
    345    }
    346 
    347    this.ruleviewCodeEl = createChild(this.element, "div", {
    348      class: "ruleview-code",
    349    });
    350 
    351    const header = createChild(this.ruleviewCodeEl, "div", {});
    352 
    353    createChild(header, "span", {
    354      class: "ruleview-rule-indent",
    355      textContent: INDENT_STR.repeat(this.rule.domRule.ancestorData.length),
    356    });
    357 
    358    this.selectorText = createChild(header, "span", {
    359      class: "ruleview-selectors-container",
    360      tabindex: this.isSelectorEditable ? "0" : "-1",
    361    });
    362 
    363    if (
    364      this.rule.domRule.type === ELEMENT_STYLE ||
    365      this.rule.domRule.type === PRES_HINTS
    366    ) {
    367      this.selectorText.classList.add("alternative-selector");
    368    }
    369 
    370    if (this.isSelectorEditable) {
    371      this.selectorText.addEventListener("click", event => {
    372        // Clicks within the selector shouldn't propagate any further.
    373        event.stopPropagation();
    374      });
    375 
    376      editableField({
    377        element: this.selectorText,
    378        done: this._onSelectorDone,
    379        cssProperties: this.rule.cssProperties,
    380        // (Shift+)Tab will move the focus to the previous/next editable field (so property name,
    381        // or new property of the previous rule).
    382        focusEditableFieldAfterApply: true,
    383        focusEditableFieldContainerSelector: ".ruleview-rule",
    384        // We don't want Enter to trigger the next editable field, just to validate
    385        // what the user entered, close the editor, and focus the span so the user can
    386        // navigate with the keyboard as expected, unless the user has
    387        // devtools.inspector.rule-view.focusNextOnEnter set to true
    388        stopOnReturn: this.ruleView.inplaceEditorFocusNextOnEnter !== true,
    389      });
    390    } else {
    391      this.selectorText.classList.add("uneditable-selector");
    392    }
    393 
    394    if (this.rule.domRule.type !== CSSRule.KEYFRAME_RULE) {
    395      // This is a "normal" rule with a selector.
    396      let computedSelector = "";
    397      if (this.rule.domRule.selectors) {
    398        computedSelector = this.rule.domRule.computedSelector;
    399        // Otherwise, the rule is either inherited or inline, and selectors will
    400        // be computed on demand when the highlighter is requested.
    401      }
    402 
    403      if (this.showSelectorHighlighterButton) {
    404        const isHighlighted =
    405          this.ruleView.isSelectorHighlighted(computedSelector);
    406        // Handling of click events is delegated to CssRuleView.handleEvent()
    407        createChild(header, "button", {
    408          class:
    409            "ruleview-selectorhighlighter js-toggle-selector-highlighter" +
    410            (isHighlighted ? " highlighted" : ""),
    411          "aria-pressed": isHighlighted,
    412          // This is used in rules.js for the selector highlighter
    413          "data-computed-selector": computedSelector,
    414          title: l10n("rule.selectorHighlighter.tooltip"),
    415        });
    416      }
    417    }
    418 
    419    this.openBrace = createChild(header, "span", {
    420      class: "ruleview-ruleopen",
    421      textContent: " {",
    422    });
    423 
    424    // We can't use a proper "ol" as it will mess with selection copy text,
    425    // adding spaces on list item instead of the one we craft (.ruleview-rule-indent)
    426    this.propertyList = createChild(this.ruleviewCodeEl, "div", {
    427      class: "ruleview-propertylist",
    428      role: "list",
    429    });
    430 
    431    this.populate();
    432 
    433    this.closeBrace = createChild(this.ruleviewCodeEl, "div", {
    434      class: "ruleview-ruleclose",
    435      tabindex: this.isEditable ? "0" : "-1",
    436    });
    437 
    438    if (this.rule.domRule.ancestorData.length) {
    439      createChild(this.closeBrace, "span", {
    440        class: "ruleview-rule-indent",
    441        textContent: INDENT_STR.repeat(this.rule.domRule.ancestorData.length),
    442      });
    443    }
    444    this.closeBrace.append(this.doc.createTextNode("}"));
    445 
    446    if (this.rule.domRule.ancestorData.length) {
    447      let closingBracketsText = "";
    448      for (let i = this.rule.domRule.ancestorData.length - 1; i >= 0; i--) {
    449        if (i) {
    450          closingBracketsText += INDENT_STR.repeat(i);
    451        }
    452        closingBracketsText += "}\n";
    453      }
    454      createChild(this.ruleviewCodeEl, "div", {
    455        class: "ruleview-ancestor-ruleclose",
    456        textContent: closingBracketsText,
    457      });
    458    }
    459 
    460    if (this.isEditable) {
    461      // A newProperty editor should only be created when no editor was
    462      // previously displayed. Since the editors are cleared on blur,
    463      // check this.ruleview.isEditing on mousedown
    464      this._ruleViewIsEditing = false;
    465 
    466      this.ruleviewCodeEl.addEventListener("mousedown", () => {
    467        this._ruleViewIsEditing = this.ruleView.isEditing;
    468      });
    469 
    470      this.ruleviewCodeEl.addEventListener("click", () => {
    471        const selection = this.doc.defaultView.getSelection();
    472        if (selection.isCollapsed && !this._ruleViewIsEditing) {
    473          this.newProperty();
    474        }
    475        // Cleanup the _ruleViewIsEditing flag
    476        this._ruleViewIsEditing = false;
    477      });
    478 
    479      this.element.addEventListener("mousedown", () => {
    480        this.doc.defaultView.focus();
    481      });
    482 
    483      // Create a property editor when the close brace is clicked.
    484      editableItem({ element: this.closeBrace }, () => {
    485        this.newProperty();
    486      });
    487    }
    488  }
    489 
    490  /**
    491   * Returns the selector warnings element, or null if selector at selectorIndex
    492   * does not have any warning.
    493   *
    494   * @param {Integer} selectorIndex: The index of the selector we want to create the
    495   *        warnings for
    496   * @param {Array<object>} selectorWarnings: An array of object of the following shape:
    497   *        - {Integer} index: The index of the selector this applies to
    498   *        - {String} kind: Identifies the warning
    499   * @returns {Element|null}
    500   */
    501  _createWarningsElementForSelector(selectorIndex, selectorWarnings) {
    502    if (!selectorWarnings) {
    503      return null;
    504    }
    505 
    506    const warningKinds = [];
    507    for (const { index, kind } of selectorWarnings) {
    508      if (index !== selectorIndex) {
    509        continue;
    510      }
    511      warningKinds.push(kind);
    512    }
    513 
    514    if (!warningKinds.length) {
    515      return null;
    516    }
    517 
    518    const warningsContainer = this.doc.createElement("div");
    519    warningsContainer.classList.add(
    520      "ruleview-selector-warnings",
    521      "has-tooltip"
    522    );
    523 
    524    warningsContainer.setAttribute(
    525      "data-selector-warning-kind",
    526      warningKinds.join(",")
    527    );
    528 
    529    if (warningKinds.includes("UnconstrainedHas")) {
    530      warningsContainer.classList.add("slow");
    531    }
    532 
    533    return warningsContainer;
    534  }
    535 
    536  /**
    537   * Called when a tool is registered or unregistered.
    538   */
    539  _onToolChanged() {
    540    if (!this.source) {
    541      return;
    542    }
    543 
    544    // When the source editor is registered, update the source links
    545    // to be clickable; and if it is unregistered, update the links to
    546    // be unclickable.
    547    if (this.toolbox.isToolRegistered("styleeditor")) {
    548      this.source.removeAttribute("unselectable");
    549    } else {
    550      this.source.setAttribute("unselectable", "true");
    551    }
    552  }
    553 
    554  /**
    555   * Event handler called when a property changes on the
    556   * StyleRuleActor.
    557   */
    558  _locationChanged() {
    559    this.updateSourceLink();
    560  }
    561 
    562  _onSourceClick(e) {
    563    e.preventDefault();
    564    if (this.source.hasAttribute("unselectable")) {
    565      return;
    566    }
    567 
    568    const { inspector } = this.ruleView;
    569    if (Tools.styleEditor.isToolSupported(inspector.toolbox)) {
    570      inspector.toolbox.viewSourceInStyleEditorByResource(
    571        this.rule.sheet,
    572        this.rule.ruleLine,
    573        this.rule.ruleColumn
    574      );
    575    }
    576  }
    577 
    578  /**
    579   * Update the text of the source link to reflect whether we're showing
    580   * original sources or not.  This is a callback for
    581   * SourceMapURLService.subscribeByID, which see.
    582   *
    583   * @param {object | null} originalLocation
    584   *        The original position object (url/line/column) or null.
    585   */
    586  _updateLocation(originalLocation) {
    587    let displayURL = this.rule.sheet?.href;
    588    const constructed = this.rule.sheet?.constructed;
    589    let line = this.rule.ruleLine;
    590    if (originalLocation) {
    591      displayURL = originalLocation.url;
    592      line = originalLocation.line;
    593    }
    594 
    595    let sourceTextContent = CssLogic.shortSource({
    596      constructed,
    597      href: displayURL,
    598    });
    599 
    600    let displayLocation = displayURL ? displayURL : sourceTextContent;
    601    if (line > 0) {
    602      sourceTextContent += ":" + line;
    603      displayLocation += ":" + line;
    604    }
    605    const title = COMPONENT_L10N.getFormatStr(
    606      "frame.viewsourceinstyleeditor",
    607      displayLocation
    608    );
    609 
    610    const sourceLabel = this.element.querySelector(
    611      ".ruleview-rule-source-label"
    612    );
    613    sourceLabel.setAttribute("title", title);
    614    sourceLabel.setAttribute("href", displayURL);
    615    sourceLabel.textContent = sourceTextContent;
    616  }
    617 
    618  updateSourceLink() {
    619    if (this.source) {
    620      if (this.rule.isSystem) {
    621        const sourceLabel = this.element.querySelector(
    622          ".ruleview-rule-source-label"
    623        );
    624        const uaLabel = STYLE_INSPECTOR_L10N.getStr("rule.userAgentStyles");
    625        sourceLabel.textContent = uaLabel + " " + this.rule.title;
    626        sourceLabel.setAttribute("href", this.rule.sheet?.href);
    627      } else {
    628        this._updateLocation(null);
    629      }
    630 
    631      if (this.rule.sheet && !this.rule.isSystem) {
    632        // Only get the original source link if the rule isn't a system
    633        // rule and if it isn't an inline rule.
    634        if (this._unsubscribeSourceMap) {
    635          this._unsubscribeSourceMap();
    636        }
    637        this._unsubscribeSourceMap = this.sourceMapURLService.subscribeByID(
    638          this.rule.sheet.resourceId,
    639          this.rule.ruleLine,
    640          this.rule.ruleColumn,
    641          this._updateLocation
    642        );
    643      }
    644      // Set "unselectable" appropriately.
    645      this._onToolChanged();
    646    }
    647 
    648    Promise.resolve().then(() => {
    649      this.emit("source-link-updated");
    650    });
    651  }
    652 
    653  /**
    654   * Update the rule editor with the contents of the rule.
    655   *
    656   * @param {boolean} reset
    657   *        True to completely reset the rule editor before populating.
    658   */
    659  populate(reset) {
    660    // Clear out existing viewers.
    661    this.selectorText.replaceChildren();
    662 
    663    // If selector text comes from a css rule, highlight selectors that
    664    // actually match.  For custom selector text (such as for the 'element'
    665    // style, just show the text directly.
    666    if (
    667      this.rule.domRule.type === ELEMENT_STYLE ||
    668      this.rule.domRule.type === PRES_HINTS
    669    ) {
    670      this.selectorText.textContent = this.rule.selectorText;
    671    } else if (this.rule.domRule.type === CSSRule.KEYFRAME_RULE) {
    672      this.selectorText.textContent = this.rule.domRule.keyText;
    673    } else if (this.rule.domRule.className === "CSSPositionTryRule") {
    674      this.selectorText.textContent = this.rule.domRule.name;
    675    } else {
    676      this.rule.domRule.selectors.forEach((selector, i) => {
    677        this._populateSelector(selector, i);
    678      });
    679    }
    680 
    681    let focusedElSelector;
    682    if (reset) {
    683      // If we're going to reset the rule (i.e. if this is the `element` rule),
    684      // we want to restore the focus after the rule is populated.
    685      // So if this element contains the active element, retrieve its selector for later use.
    686      if (this.element.contains(this.doc.activeElement)) {
    687        focusedElSelector = CssLogic.findCssSelector(this.doc.activeElement);
    688      }
    689 
    690      this.propertyList.replaceChildren();
    691    }
    692 
    693    this._unusedCssVariableDeclarations =
    694      this._getUnusedCssVariableDeclarations();
    695    const hideUnusedCssVariableDeclarations =
    696      this._unusedCssVariableDeclarations.size >=
    697      // If the button was already displayed, hide unused variables if we have at least
    698      // one, even if it's less than the threshold
    699      (this._showUnusedCustomCssPropertiesButton
    700        ? 1
    701        : UNUSED_CSS_PROPERTIES_HIDE_THRESHOLD);
    702 
    703    // If we won't hide any variable, clear the Set of unused variables as it's used in
    704    // updateUnusedCssVariables and we might do unnecessary computation if we still
    705    // track variables which are actually visible.
    706    if (!hideUnusedCssVariableDeclarations) {
    707      this._unusedCssVariableDeclarations.clear();
    708    }
    709 
    710    for (const prop of this.rule.textProps) {
    711      if (hideUnusedCssVariableDeclarations && prop.isUnusedVariable) {
    712        continue;
    713      }
    714      if (!prop.editor && !prop.invisible) {
    715        const editor = new TextPropertyEditor(this, prop, {
    716          elementsWithPendingClicks: this.options.elementsWithPendingClicks,
    717        });
    718        this.propertyList.appendChild(editor.element);
    719      } else if (prop.editor) {
    720        // If an editor already existed, append it to the bottom now to make sure the
    721        // order of editors in the DOM follow the order of the rule's properties.
    722        this.propertyList.appendChild(prop.editor.element);
    723      }
    724    }
    725 
    726    if (hideUnusedCssVariableDeclarations) {
    727      if (!this._showUnusedCustomCssPropertiesButton) {
    728        this._showUnusedCustomCssPropertiesButton =
    729          this.doc.createElement("button");
    730        this._showUnusedCustomCssPropertiesButton.classList.add(
    731          "devtools-button",
    732          "devtools-button-standalone",
    733          "ruleview-show-unused-custom-css-properties"
    734        );
    735        this._showUnusedCustomCssPropertiesButton.addEventListener(
    736          "click",
    737          this._onShowUnusedCustomCssPropertiesButtonClick
    738        );
    739      }
    740      this.ruleviewCodeEl.insertBefore(
    741        this._showUnusedCustomCssPropertiesButton,
    742        this.closeBrace
    743      );
    744      this._updateShowUnusedCustomCssPropertiesButtonText();
    745    } else if (this._showUnusedCustomCssPropertiesButton) {
    746      this._nullifyShowUnusedCustomCssProperties();
    747    }
    748 
    749    // Set focus if the focus is still in the current document (avoid stealing
    750    // the focus, see Bug 1911627).
    751    if (this.doc.hasFocus() && focusedElSelector) {
    752      const elementToFocus = this.doc.querySelector(focusedElSelector);
    753      if (elementToFocus && this.element.contains(elementToFocus)) {
    754        // We need to wait for a tick for the focus to be properly set
    755        setTimeout(() => {
    756          elementToFocus.focus();
    757          this.ruleView.emitForTests("rule-editor-focus-reset");
    758        }, 0);
    759      }
    760    }
    761  }
    762 
    763  updateUnusedCssVariables() {
    764    if (
    765      !this._unusedCssVariableDeclarations ||
    766      !this._unusedCssVariableDeclarations.size
    767    ) {
    768      return;
    769    }
    770 
    771    // Store the list of what used to be unused
    772    const previouslyUnused = Array.from(this._unusedCssVariableDeclarations);
    773    // Then compute the list of unused variables again
    774    this._unusedCssVariableDeclarations =
    775      this._getUnusedCssVariableDeclarations();
    776 
    777    for (const prop of previouslyUnused) {
    778      if (this._unusedCssVariableDeclarations.has(prop)) {
    779        continue;
    780      }
    781 
    782      // The prop wasn't used, but now is, so let's show it
    783      this.showUnusedCssVariable(prop, {
    784        updateButton: false,
    785      });
    786    }
    787 
    788    this._updateShowUnusedCustomCssPropertiesButtonText();
    789  }
    790 
    791  /**
    792   * Create a TextPropertyEditor for TextProperty representing an unused CSS variable.
    793   *
    794   * @param {TextProperty} prop
    795   * @param {object} options
    796   * @param {boolean} options.updateButton
    797   * @returns {TextPropertyEditor|null} Returns null if passed TextProperty isn't found
    798   *          in the list of unused css variables
    799   */
    800  showUnusedCssVariable(prop, { updateButton = true } = {}) {
    801    if (prop.editor) {
    802      return null;
    803    }
    804 
    805    this._unusedCssVariableDeclarations.delete(prop);
    806 
    807    const editor = new TextPropertyEditor(this, prop, {
    808      elementsWithPendingClicks: this.options.elementsWithPendingClicks,
    809    });
    810    const declarationIndex = this.rule.textProps.indexOf(prop);
    811    // We need to insert the editor according to its index in the list of declarations.
    812    // So let's try to find the prop which is placed higher and is visible
    813    let nextSibling;
    814    for (let i = declarationIndex + 1; i < this.rule.textProps.length; i++) {
    815      const currentProp = this.rule.textProps[i];
    816      if (currentProp.editor) {
    817        nextSibling = currentProp.editor.element;
    818        break;
    819      }
    820    }
    821    // If we couldn't find nextSibling, that means that no declaration with higher index
    822    // is visible, so we can put the newly visible property at the end
    823    this.propertyList.insertBefore(editor.element, nextSibling || null);
    824 
    825    if (updateButton) {
    826      this._updateShowUnusedCustomCssPropertiesButtonText();
    827    }
    828 
    829    return editor;
    830  }
    831 
    832  /**
    833   * Returns a Set containing the list of unused CSS variable TextProperty which shouldn't
    834   * be visible.
    835   *
    836   * @returns {Set<TextProperty>}
    837   */
    838  _getUnusedCssVariableDeclarations() {
    839    const unusedCssVariableDeclarations = new Set();
    840 
    841    // No need to go through the declarations if we shouldn't hide unused custom properties
    842    if (!this.options.shouldHideUnusedCustomCssProperties) {
    843      return unusedCssVariableDeclarations;
    844    }
    845 
    846    // Compute a list of variables that will be visible, as there might be unused variables
    847    // that will be visible (e.g. if the user added one in the rules view)
    848    for (const prop of this.rule.textProps) {
    849      if (prop.isUnusedVariable) {
    850        unusedCssVariableDeclarations.add(prop);
    851      }
    852    }
    853 
    854    return unusedCssVariableDeclarations;
    855  }
    856 
    857  /**
    858   * Handle click on "Show X unused custom CSS properties" button
    859   *
    860   * @param {Event} e
    861   */
    862  _onShowUnusedCustomCssPropertiesButtonClick(e) {
    863    e.stopPropagation();
    864 
    865    this._nullifyShowUnusedCustomCssProperties();
    866 
    867    for (const prop of this._unusedCssVariableDeclarations) {
    868      if (!prop.invisible) {
    869        const editor = new TextPropertyEditor(this, prop, {
    870          elementsWithPendingClicks: this.options.elementsWithPendingClicks,
    871        });
    872        // Insert at the original declaration index
    873        this.propertyList.insertBefore(
    874          editor.element,
    875          this.propertyList.childNodes[this.rule.textProps.indexOf(prop)] ||
    876            null
    877        );
    878      }
    879    }
    880    if (typeof this.options.onShowUnusedCustomCssProperties === "function") {
    881      this.options.onShowUnusedCustomCssProperties();
    882    }
    883  }
    884 
    885  /**
    886   * Update the text for the "Show X unused custom CSS properties" button, or remove it
    887   * if there's no hidden custom properties anymore
    888   */
    889  _updateShowUnusedCustomCssPropertiesButtonText() {
    890    if (!this._showUnusedCustomCssPropertiesButton) {
    891      return;
    892    }
    893 
    894    const unusedVariablesCount = this._unusedCssVariableDeclarations.size;
    895    if (!unusedVariablesCount) {
    896      this._nullifyShowUnusedCustomCssProperties();
    897      return;
    898    }
    899 
    900    const label = PluralForm.get(
    901      unusedVariablesCount,
    902      STYLE_INSPECTOR_L10N.getStr("rule.showUnusedCssVariable")
    903    ).replace("#1", unusedVariablesCount);
    904 
    905    this._showUnusedCustomCssPropertiesButton.replaceChildren(label);
    906  }
    907 
    908  /**
    909   * Nullify this._showUnusedCustomCssPropertiesButton, remove its click event handler
    910   * and remove it from the DOM if `removeFromDom` is set to true.
    911   *
    912   * @param {object} [options]
    913   * @param {boolean} [options.removeFromDom]
    914   *        Should the button be removed from the DOM (defaults to true)
    915   */
    916  _nullifyShowUnusedCustomCssProperties({ removeFromDom = true } = {}) {
    917    if (!this._showUnusedCustomCssPropertiesButton) {
    918      return;
    919    }
    920 
    921    this._showUnusedCustomCssPropertiesButton.removeEventListener(
    922      "click",
    923      this._onShowUnusedCustomCssPropertiesButtonClick
    924    );
    925 
    926    if (removeFromDom) {
    927      this._showUnusedCustomCssPropertiesButton.remove();
    928    }
    929    this._showUnusedCustomCssPropertiesButton = null;
    930  }
    931 
    932  /**
    933   * Render a given rule selector in this.selectorText element
    934   *
    935   * @param {string} selector: The selector text to display
    936   * @param {number} selectorIndex: Its index in the rule
    937   */
    938  _populateSelector(selector, selectorIndex) {
    939    if (selectorIndex !== 0) {
    940      createChild(this.selectorText, "span", {
    941        class: "ruleview-selector-separator",
    942        textContent: ", ",
    943      });
    944    }
    945 
    946    const containerClass =
    947      "ruleview-selector " +
    948      (this.rule.matchedSelectorIndexes.includes(selectorIndex)
    949        ? "matched"
    950        : "unmatched");
    951 
    952    let selectorContainerTitle;
    953    if (
    954      typeof this.rule.selector.selectorsSpecificity?.[selectorIndex] !==
    955      "undefined"
    956    ) {
    957      // The specificity that we get from the platform is a single number that we
    958      // need to format into the common `(x,y,z)` specificity string.
    959      const specificity =
    960        this.rule.selector.selectorsSpecificity?.[selectorIndex];
    961      const a = Math.floor(specificity / (1024 * 1024));
    962      const b = Math.floor((specificity % (1024 * 1024)) / 1024);
    963      const c = specificity % 1024;
    964      selectorContainerTitle = STYLE_INSPECTOR_L10N.getFormatStr(
    965        "rule.selectorSpecificity.title",
    966        `(${a},${b},${c})`
    967      );
    968    }
    969    const selectorContainer = createChild(this.selectorText, "span", {
    970      class: containerClass,
    971      title: selectorContainerTitle,
    972    });
    973 
    974    const parsedSelector = parsePseudoClassesAndAttributes(selector);
    975 
    976    for (const selectorText of parsedSelector) {
    977      let selectorClass = "";
    978 
    979      switch (selectorText.type) {
    980        case SELECTOR_ATTRIBUTE:
    981          selectorClass = "ruleview-selector-attribute";
    982          break;
    983        case SELECTOR_ELEMENT:
    984          selectorClass = "ruleview-selector-element";
    985          break;
    986        case SELECTOR_PSEUDO_CLASS:
    987          selectorClass = PSEUDO_CLASSES.some(
    988            pseudo => selectorText.value === pseudo
    989          )
    990            ? "ruleview-selector-pseudo-class-lock"
    991            : "ruleview-selector-pseudo-class";
    992          break;
    993        default:
    994          break;
    995      }
    996 
    997      createChild(selectorContainer, "span", {
    998        textContent: selectorText.value,
    999        class: selectorClass,
   1000      });
   1001    }
   1002 
   1003    const warningsContainer = this._createWarningsElementForSelector(
   1004      selectorIndex,
   1005      this.rule.domRule.selectorWarnings
   1006    );
   1007    if (warningsContainer) {
   1008      selectorContainer.append(warningsContainer);
   1009    }
   1010  }
   1011 
   1012  /**
   1013   * Programatically add a new property to the rule.
   1014   *
   1015   * @param {string} name
   1016   *        Property name.
   1017   * @param {string} value
   1018   *        Property value.
   1019   * @param {string} priority
   1020   *        Property priority.
   1021   * @param {boolean} enabled
   1022   *        True if the property should be enabled.
   1023   * @param {TextProperty} siblingProp
   1024   *        Optional, property next to which the new property will be added.
   1025   * @return {TextProperty}
   1026   *        The new property
   1027   */
   1028  addProperty(name, value, priority, enabled, siblingProp) {
   1029    const prop = this.rule.createProperty(
   1030      name,
   1031      value,
   1032      priority,
   1033      enabled,
   1034      siblingProp
   1035    );
   1036    const index = this.rule.textProps.indexOf(prop);
   1037    const editor = new TextPropertyEditor(this, prop, {
   1038      elementsWithPendingClicks: this.options.elementsWithPendingClicks,
   1039    });
   1040 
   1041    // Insert this node before the DOM node that is currently at its new index
   1042    // in the property list.  There is currently one less node in the DOM than
   1043    // in the property list, so this causes it to appear after siblingProp.
   1044    // If there is no node at its index, as is the case where this is the last
   1045    // node being inserted, then this behaves as appendChild.
   1046    this.propertyList.insertBefore(
   1047      editor.element,
   1048      this.propertyList.children[index]
   1049    );
   1050 
   1051    return prop;
   1052  }
   1053 
   1054  /**
   1055   * Programatically add a list of new properties to the rule.  Focus the UI
   1056   * to the proper location after adding (either focus the value on the
   1057   * last property if it is empty, or create a new property and focus it).
   1058   *
   1059   * @param {Array} properties
   1060   *        Array of properties, which are objects with this signature:
   1061   *        {
   1062   *          name: {string},
   1063   *          value: {string},
   1064   *          priority: {string}
   1065   *        }
   1066   * @param {TextProperty} siblingProp
   1067   *        Optional, the property next to which all new props should be added.
   1068   */
   1069  addProperties(properties, siblingProp) {
   1070    if (!properties || !properties.length) {
   1071      return;
   1072    }
   1073 
   1074    let lastProp = siblingProp;
   1075    for (const p of properties) {
   1076      const isCommented = Boolean(p.commentOffsets);
   1077      const enabled = !isCommented;
   1078      lastProp = this.addProperty(
   1079        p.name,
   1080        p.value,
   1081        p.priority,
   1082        enabled,
   1083        lastProp
   1084      );
   1085    }
   1086 
   1087    // Either focus on the last value if incomplete, or start a new one.
   1088    if (lastProp && lastProp.value.trim() === "") {
   1089      lastProp.editor.valueSpan.click();
   1090    } else {
   1091      this.newProperty();
   1092    }
   1093  }
   1094 
   1095  /**
   1096   * Create a text input for a property name.  If a non-empty property
   1097   * name is given, we'll create a real TextProperty and add it to the
   1098   * rule.
   1099   */
   1100  newProperty() {
   1101    // If we're already creating a new property, ignore this.
   1102    if (!this.closeBrace.hasAttribute("tabindex")) {
   1103      return;
   1104    }
   1105 
   1106    // While we're editing a new property, it doesn't make sense to start a second new
   1107    // property editor, so disable focusing the close brace for now.
   1108    this.closeBrace.removeAttribute("tabindex");
   1109    // We also need to make the "Show Unused Variables" button non-focusable so hitting
   1110    // Tab while focused in the new property editor will move the focus to the next rule
   1111    // selector editor.
   1112    if (this._showUnusedCustomCssPropertiesButton) {
   1113      this._showUnusedCustomCssPropertiesButton.setAttribute("tabindex", "-1");
   1114    }
   1115 
   1116    this.newPropItem = createChild(this.propertyList, "div", {
   1117      class: "ruleview-property ruleview-newproperty",
   1118      role: "listitem",
   1119    });
   1120 
   1121    this.newPropSpan = createChild(this.newPropItem, "span", {
   1122      class: "ruleview-propertyname",
   1123      tabindex: "0",
   1124    });
   1125 
   1126    this.multipleAddedProperties = null;
   1127 
   1128    this.editor = new InplaceEditor({
   1129      element: this.newPropSpan,
   1130      done: this._onNewProperty,
   1131      // (Shift+)Tab will move the focus to the previous/next editable field
   1132      focusEditableFieldAfterApply: true,
   1133      focusEditableFieldContainerSelector: ".ruleview-rule",
   1134      destroy: this._newPropertyDestroy,
   1135      advanceChars: ":",
   1136      contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
   1137      popup: this.ruleView.popup,
   1138      cssProperties: this.rule.cssProperties,
   1139      inputAriaLabel: NEW_PROPERTY_NAME_INPUT_LABEL,
   1140      getCssVariables: () =>
   1141        this.rule.elementStyle.getAllCustomProperties(this.rule.pseudoElement),
   1142    });
   1143 
   1144    // Auto-close the input if multiple rules get pasted into new property.
   1145    this.editor.input.addEventListener(
   1146      "paste",
   1147      blurOnMultipleProperties(this.rule.cssProperties)
   1148    );
   1149  }
   1150 
   1151  /**
   1152   * Called when the new property input has been dismissed.
   1153   *
   1154   * @param {string} value
   1155   *        The value in the editor.
   1156   * @param {boolean} commit
   1157   *        True if the value should be committed.
   1158   */
   1159  _onNewProperty(value, commit) {
   1160    if (!value || !commit) {
   1161      return;
   1162    }
   1163 
   1164    // parseDeclarations allows for name-less declarations, but in the present
   1165    // case, we're creating a new declaration, it doesn't make sense to accept
   1166    // these entries
   1167    this.multipleAddedProperties = parseNamedDeclarations(
   1168      this.rule.cssProperties.isKnown,
   1169      value,
   1170      true
   1171    );
   1172 
   1173    // Blur the editor field now and deal with adding declarations later when
   1174    // the field gets destroyed (see _newPropertyDestroy)
   1175    this.editor.input.blur();
   1176 
   1177    this.telemetry.recordEvent("edit_rule", "ruleview");
   1178  }
   1179 
   1180  /**
   1181   * Called when the new property editor is destroyed.
   1182   * This is where the properties (type TextProperty) are actually being
   1183   * added, since we want to wait until after the inplace editor `destroy`
   1184   * event has been fired to keep consistent UI state.
   1185   */
   1186  _newPropertyDestroy() {
   1187    // We're done, make the close brace and "Show unused variable" button focusable again.
   1188    this.closeBrace.setAttribute("tabindex", "0");
   1189    if (this._showUnusedCustomCssPropertiesButton) {
   1190      this._showUnusedCustomCssPropertiesButton.removeAttribute("tabindex");
   1191    }
   1192 
   1193    this.propertyList.removeChild(this.newPropItem);
   1194    delete this.newPropItem;
   1195    delete this.newPropSpan;
   1196 
   1197    // If properties were added, we want to focus the proper element.
   1198    // If the last new property has no value, focus the value on it.
   1199    // Otherwise, start a new property and focus that field.
   1200    if (this.multipleAddedProperties && this.multipleAddedProperties.length) {
   1201      this.addProperties(this.multipleAddedProperties);
   1202    }
   1203  }
   1204 
   1205  /**
   1206   * Called when the selector's inplace editor is closed.
   1207   * Ignores the change if the user pressed escape, otherwise
   1208   * commits it.
   1209   *
   1210   * @param {string} value
   1211   *        The value contained in the editor.
   1212   * @param {boolean} commit
   1213   *        True if the change should be applied.
   1214   * @param {number} direction
   1215   *        The move focus direction number.
   1216   */
   1217  async _onSelectorDone(value, commit, direction) {
   1218    if (
   1219      !commit ||
   1220      this.isEditing ||
   1221      value === "" ||
   1222      value === this.rule.selectorText
   1223    ) {
   1224      return;
   1225    }
   1226 
   1227    const ruleView = this.ruleView;
   1228    const elementStyle = ruleView._elementStyle;
   1229    const element = elementStyle.element;
   1230 
   1231    this.isEditing = true;
   1232 
   1233    // Remove highlighter for the previous selector.
   1234    const computedSelector = this.rule.domRule.computedSelector;
   1235    if (this.ruleView.isSelectorHighlighted(computedSelector)) {
   1236      await this.ruleView.toggleSelectorHighlighter(
   1237        this.rule,
   1238        computedSelector
   1239      );
   1240    }
   1241 
   1242    try {
   1243      const response = await this.rule.domRule.modifySelector(element, value);
   1244 
   1245      // Modifying the selector might have removed the element (e.g. for pseudo element)
   1246      if (!element.actorID) {
   1247        return;
   1248      }
   1249 
   1250      // We recompute the list of applied styles, because editing a
   1251      // selector might cause this rule's position to change.
   1252      const applied = await elementStyle.pageStyle.getApplied(element, {
   1253        inherited: true,
   1254        matchedSelectors: true,
   1255        filter: elementStyle.showUserAgentStyles ? "ua" : undefined,
   1256      });
   1257 
   1258      // The element might have been removed while we were trying to get the applied declarations
   1259      if (!element.actorID) {
   1260        return;
   1261      }
   1262 
   1263      this.isEditing = false;
   1264 
   1265      const { ruleProps, isMatching } = response;
   1266      if (!ruleProps) {
   1267        // Notify for changes, even when nothing changes,
   1268        // just to allow tests being able to track end of this request.
   1269        ruleView.emit("ruleview-invalid-selector");
   1270        return;
   1271      }
   1272 
   1273      ruleProps.isUnmatched = !isMatching;
   1274      const newRule = new Rule(elementStyle, ruleProps);
   1275      const editor = new RuleEditor(ruleView, newRule);
   1276      const rules = elementStyle.rules;
   1277 
   1278      let newRuleIndex = applied.findIndex(r => r.rule == ruleProps.rule);
   1279      const oldIndex = rules.indexOf(this.rule);
   1280 
   1281      // If the selector no longer matches, then we leave the rule in
   1282      // the same relative position.
   1283      if (newRuleIndex === -1) {
   1284        newRuleIndex = oldIndex;
   1285      }
   1286 
   1287      // Remove the old rule and insert the new rule.
   1288      rules.splice(oldIndex, 1);
   1289      rules.splice(newRuleIndex, 0, newRule);
   1290      elementStyle._changed();
   1291      elementStyle.onRuleUpdated();
   1292 
   1293      // We install the new editor in place of the old -- you might
   1294      // think we would replicate the list-modification logic above,
   1295      // but that is complicated due to the way the UI installs
   1296      // pseudo-element rules and the like.
   1297      this.element.parentNode.replaceChild(editor.element, this.element);
   1298 
   1299      // As the rules elements will be replaced, and given that the inplace-editor doesn't
   1300      // wait for this `done` callback to be resolved, the focus management we do there
   1301      // will be useless as this specific code will usually happen later (and the focused
   1302      // element might be replaced).
   1303      // Because of this, we need to handle setting the focus ourselves from here.
   1304      editor._moveSelectorFocus(direction);
   1305    } catch (err) {
   1306      this.isEditing = false;
   1307      promiseWarn(err);
   1308    }
   1309  }
   1310 
   1311  /**
   1312   * Handle moving the focus change after a Tab keypress in the selector inplace editor.
   1313   *
   1314   * @param {number} direction
   1315   *        The move focus direction number.
   1316   */
   1317  _moveSelectorFocus(direction) {
   1318    if (!direction || direction === Services.focus.MOVEFOCUS_BACKWARD) {
   1319      return;
   1320    }
   1321 
   1322    if (this.rule.textProps.length) {
   1323      this.rule.textProps[0].editor.nameSpan.click();
   1324    } else {
   1325      this.propertyList.click();
   1326    }
   1327  }
   1328 }
   1329 
   1330 module.exports = RuleEditor;