tor-browser

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

markup-context-menu.js (27773B)


      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 {
      8  PSEUDO_CLASSES,
      9 } = require("resource://devtools/shared/css/constants.js");
     10 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     11 
     12 loader.lazyRequireGetter(
     13  this,
     14  "Menu",
     15  "resource://devtools/client/framework/menu.js"
     16 );
     17 loader.lazyRequireGetter(
     18  this,
     19  "MenuItem",
     20  "resource://devtools/client/framework/menu-item.js"
     21 );
     22 loader.lazyRequireGetter(
     23  this,
     24  "clipboardHelper",
     25  "resource://devtools/shared/platform/clipboard.js"
     26 );
     27 
     28 loader.lazyGetter(this, "TOOLBOX_L10N", function () {
     29  return new LocalizationHelper("devtools/client/locales/toolbox.properties");
     30 });
     31 
     32 const INSPECTOR_L10N = new LocalizationHelper(
     33  "devtools/client/locales/inspector.properties"
     34 );
     35 
     36 /**
     37 * Context menu for the Markup view.
     38 */
     39 class MarkupContextMenu {
     40  constructor(markup) {
     41    this.markup = markup;
     42    this.inspector = markup.inspector;
     43    this.selection = this.inspector.selection;
     44    this.target = this.inspector.currentTarget;
     45    this.telemetry = this.inspector.telemetry;
     46    this.toolbox = this.inspector.toolbox;
     47    this.walker = this.inspector.walker;
     48  }
     49 
     50  destroy() {
     51    this.markup = null;
     52    this.inspector = null;
     53    this.selection = null;
     54    this.target = null;
     55    this.telemetry = null;
     56    this.toolbox = null;
     57    this.walker = null;
     58  }
     59 
     60  show(event) {
     61    if (
     62      !Element.isInstance(event.originalTarget) ||
     63      event.originalTarget.closest("input[type=text]") ||
     64      event.originalTarget.closest("input:not([type])") ||
     65      event.originalTarget.closest("textarea")
     66    ) {
     67      return;
     68    }
     69 
     70    event.stopPropagation();
     71    event.preventDefault();
     72 
     73    this._openMenu({
     74      screenX: event.screenX,
     75      screenY: event.screenY,
     76      target: event.target,
     77    });
     78  }
     79 
     80  /**
     81   * This method is here for the benefit of copying links.
     82   */
     83  _copyAttributeLink(link) {
     84    this.inspector.inspectorFront
     85      .resolveRelativeURL(link, this.selection.nodeFront)
     86      .then(url => {
     87        clipboardHelper.copyString(url);
     88      }, console.error);
     89  }
     90 
     91  /**
     92   * Copy the full CSS Path of the selected Node to the clipboard.
     93   */
     94  _copyCssPath() {
     95    if (!this.selection.isNode()) {
     96      return;
     97    }
     98 
     99    this.selection.nodeFront
    100      .getCssPath()
    101      .then(path => {
    102        clipboardHelper.copyString(path);
    103      })
    104      .catch(console.error);
    105  }
    106 
    107  /**
    108   * Copy the data-uri for the currently selected image in the clipboard.
    109   */
    110  _copyImageDataUri() {
    111    const container = this.markup.getContainer(this.selection.nodeFront);
    112    if (container && container.isPreviewable()) {
    113      container.copyImageDataUri();
    114    }
    115  }
    116 
    117  /**
    118   * Copy the innerHTML of the selected Node to the clipboard.
    119   */
    120  _copyInnerHTML() {
    121    this.markup.copyInnerHTML();
    122  }
    123 
    124  /**
    125   * Copy the outerHTML of the selected Node to the clipboard.
    126   */
    127  _copyOuterHTML() {
    128    this.markup.copyOuterHTML();
    129  }
    130 
    131  /**
    132   * Copy a unique selector of the selected Node to the clipboard.
    133   */
    134  _copyUniqueSelector() {
    135    if (!this.selection.isNode()) {
    136      return;
    137    }
    138 
    139    this.selection.nodeFront
    140      .getUniqueSelector()
    141      .then(selector => {
    142        clipboardHelper.copyString(selector);
    143      })
    144      .catch(console.error);
    145  }
    146 
    147  /**
    148   * Copy the XPath of the selected Node to the clipboard.
    149   */
    150  _copyXPath() {
    151    if (!this.selection.isNode()) {
    152      return;
    153    }
    154 
    155    this.selection.nodeFront
    156      .getXPath()
    157      .then(path => {
    158        clipboardHelper.copyString(path);
    159      })
    160      .catch(console.error);
    161  }
    162 
    163  /**
    164   * Delete the selected node.
    165   */
    166  _deleteNode() {
    167    if (!this.selection.isNode() || this.selection.isRoot()) {
    168      return;
    169    }
    170 
    171    const nodeFront = this.selection.nodeFront;
    172 
    173    // If the markup panel is active, use the markup panel to delete
    174    // the node, making this an undoable action.
    175    if (this.markup) {
    176      this.markup.deleteNode(nodeFront);
    177    } else {
    178      // remove the node from content
    179      nodeFront.walkerFront.removeNode(nodeFront);
    180    }
    181  }
    182 
    183  /**
    184   * Duplicate the selected node
    185   */
    186  _duplicateNode() {
    187    if (
    188      !this.selection.isElementNode() ||
    189      this.selection.isRoot() ||
    190      this.selection.isNativeAnonymousNode() ||
    191      this.selection.isPseudoElementNode()
    192    ) {
    193      return;
    194    }
    195 
    196    const nodeFront = this.selection.nodeFront;
    197    nodeFront.walkerFront.duplicateNode(nodeFront).catch(console.error);
    198  }
    199 
    200  /**
    201   * Edit the outerHTML of the selected Node.
    202   */
    203  _editHTML() {
    204    if (!this.selection.isNode()) {
    205      return;
    206    }
    207    this.markup.beginEditingHTML(this.selection.nodeFront);
    208  }
    209 
    210  /**
    211   * Jumps to the custom element definition in the debugger.
    212   */
    213  _jumpToCustomElementDefinition() {
    214    const { url, line, column } =
    215      this.selection.nodeFront.customElementLocation;
    216    this.toolbox.viewSourceInDebugger(
    217      url,
    218      line,
    219      column,
    220      null,
    221      "show_custom_element"
    222    );
    223  }
    224 
    225  /**
    226   * Add attribute to node.
    227   * Used for node context menu and shouldn't be called directly.
    228   */
    229  _onAddAttribute() {
    230    const container = this.markup.getContainer(this.selection.nodeFront);
    231    container.addAttribute();
    232  }
    233 
    234  /**
    235   * Copy attribute value for node.
    236   * Used for node context menu and shouldn't be called directly.
    237   */
    238  _onCopyAttributeValue() {
    239    clipboardHelper.copyString(this.nodeMenuTriggerInfo.value);
    240  }
    241 
    242  /**
    243   * This method is here for the benefit of the node-menu-link-copy menu item
    244   * in the inspector contextual-menu.
    245   */
    246  _onCopyLink() {
    247    this._copyAttributeLink(this.contextMenuTarget.dataset.link);
    248  }
    249 
    250  /**
    251   * Edit attribute for node.
    252   * Used for node context menu and shouldn't be called directly.
    253   */
    254  _onEditAttribute() {
    255    const container = this.markup.getContainer(this.selection.nodeFront);
    256    container.editAttribute(this.nodeMenuTriggerInfo.name);
    257  }
    258 
    259  /**
    260   * This method is here for the benefit of the node-menu-link-follow menu item
    261   * in the inspector contextual-menu.
    262   */
    263  _onFollowLink() {
    264    const type = this.contextMenuTarget.dataset.type;
    265    const link = this.contextMenuTarget.dataset.link;
    266    this.markup.followAttributeLink(type, link);
    267  }
    268 
    269  /**
    270   * Remove attribute from node.
    271   * Used for node context menu and shouldn't be called directly.
    272   */
    273  _onRemoveAttribute() {
    274    const container = this.markup.getContainer(this.selection.nodeFront);
    275    container.removeAttribute(this.nodeMenuTriggerInfo.name);
    276  }
    277 
    278  /**
    279   * Paste the contents of the clipboard as adjacent HTML to the selected Node.
    280   *
    281   * @param  {string} position
    282   *         The position as specified for Element.insertAdjacentHTML
    283   *         (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
    284   */
    285  _pasteAdjacentHTML(position) {
    286    const content = this._getClipboardContentForPaste();
    287    if (!content) {
    288      return Promise.reject("No clipboard content for paste");
    289    }
    290 
    291    const node = this.selection.nodeFront;
    292    return this.markup.insertAdjacentHTMLToNode(node, position, content);
    293  }
    294 
    295  /**
    296   * Paste the contents of the clipboard into the selected Node's inner HTML.
    297   */
    298  _pasteInnerHTML() {
    299    const content = this._getClipboardContentForPaste();
    300    if (!content) {
    301      return Promise.reject("No clipboard content for paste");
    302    }
    303 
    304    const node = this.selection.nodeFront;
    305    return this.markup.getNodeInnerHTML(node).then(oldContent => {
    306      this.markup.updateNodeInnerHTML(node, content, oldContent);
    307    });
    308  }
    309 
    310  /**
    311   * Paste the contents of the clipboard into the selected Node's outer HTML.
    312   */
    313  _pasteOuterHTML() {
    314    const content = this._getClipboardContentForPaste();
    315    if (!content) {
    316      return Promise.reject("No clipboard content for paste");
    317    }
    318 
    319    const node = this.selection.nodeFront;
    320    return this.markup.getNodeOuterHTML(node).then(oldContent => {
    321      this.markup.updateNodeOuterHTML(node, content, oldContent);
    322    });
    323  }
    324 
    325  /**
    326   * Show Accessibility properties for currently selected node
    327   */
    328  async _showAccessibilityProperties() {
    329    const a11yPanel = await this.toolbox.selectTool("accessibility");
    330    // Select the accessible object in the panel and wait for the event that
    331    // tells us it has been done.
    332    const onSelected = a11yPanel.once("new-accessible-front-selected");
    333    a11yPanel.selectAccessibleForNode(
    334      this.selection.nodeFront,
    335      "inspector-context-menu"
    336    );
    337    await onSelected;
    338  }
    339 
    340  /**
    341   * Show DOM properties
    342   */
    343  _showDOMProperties() {
    344    this.toolbox.openSplitConsole().then(() => {
    345      const { hud } = this.toolbox.getPanel("webconsole");
    346      hud.ui.wrapper.dispatchEvaluateExpression("inspect($0, true)");
    347    });
    348  }
    349 
    350  /**
    351   * Use in Console.
    352   *
    353   * Takes the currently selected node in the inspector and assigns it to a
    354   * temp variable on the content window.  Also opens the split console and
    355   * autofills it with the temp variable.
    356   */
    357  async _useInConsole() {
    358    await this.toolbox.openSplitConsole();
    359    const { hud } = this.toolbox.getPanel("webconsole");
    360 
    361    const evalString = `{ let i = 0;
    362      while (window.hasOwnProperty("temp" + i) && i < 1000) {
    363        i++;
    364      }
    365      window["temp" + i] = $0;
    366      "temp" + i;
    367    }`;
    368 
    369    const res = await this.toolbox.commands.scriptCommand.execute(evalString, {
    370      selectedNodeActor: this.selection.nodeFront.actorID,
    371      // Prevent any type of breakpoint when evaluating this code
    372      disableBreaks: true,
    373      // Ensure always overriding "$0" console command, even if the page implements its own "$0" variable.
    374      preferConsoleCommandsOverLocalSymbols: true,
    375    });
    376    hud.setInputValue(res.result);
    377    this.inspector.emit("console-var-ready");
    378  }
    379 
    380  _getAttributesSubmenu(isEditableElement) {
    381    const attributesSubmenu = new Menu();
    382    const nodeInfo = this.nodeMenuTriggerInfo;
    383    const isAttributeClicked =
    384      isEditableElement && nodeInfo && nodeInfo.type === "attribute";
    385 
    386    attributesSubmenu.append(
    387      new MenuItem({
    388        id: "node-menu-add-attribute",
    389        label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"),
    390        accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"),
    391        disabled: !isEditableElement,
    392        click: () => this._onAddAttribute(),
    393      })
    394    );
    395    attributesSubmenu.append(
    396      new MenuItem({
    397        id: "node-menu-copy-attribute",
    398        label: INSPECTOR_L10N.getFormatStr(
    399          "inspectorCopyAttributeValue.label",
    400          isAttributeClicked ? `${nodeInfo.value}` : ""
    401        ),
    402        accesskey: INSPECTOR_L10N.getStr(
    403          "inspectorCopyAttributeValue.accesskey"
    404        ),
    405        disabled: !isAttributeClicked,
    406        click: () => this._onCopyAttributeValue(),
    407      })
    408    );
    409    attributesSubmenu.append(
    410      new MenuItem({
    411        id: "node-menu-edit-attribute",
    412        label: INSPECTOR_L10N.getFormatStr(
    413          "inspectorEditAttribute.label",
    414          isAttributeClicked ? `${nodeInfo.name}` : ""
    415        ),
    416        accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"),
    417        disabled: !isAttributeClicked,
    418        click: () => this._onEditAttribute(),
    419      })
    420    );
    421    attributesSubmenu.append(
    422      new MenuItem({
    423        id: "node-menu-remove-attribute",
    424        label: INSPECTOR_L10N.getFormatStr(
    425          "inspectorRemoveAttribute.label",
    426          isAttributeClicked ? `${nodeInfo.name}` : ""
    427        ),
    428        accesskey: INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"),
    429        disabled: !isAttributeClicked,
    430        click: () => this._onRemoveAttribute(),
    431      })
    432    );
    433 
    434    return attributesSubmenu;
    435  }
    436 
    437  /**
    438   * Returns the clipboard content if it is appropriate for pasting
    439   * into the current node's outer HTML, otherwise returns null.
    440   */
    441  _getClipboardContentForPaste() {
    442    const content = clipboardHelper.getText();
    443    if (content && content.trim().length) {
    444      return content;
    445    }
    446    return null;
    447  }
    448 
    449  _getCopySubmenu(markupContainer, isElement, isFragment) {
    450    const copySubmenu = new Menu();
    451    copySubmenu.append(
    452      new MenuItem({
    453        id: "node-menu-copyinner",
    454        label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"),
    455        accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"),
    456        disabled: !isElement && !isFragment,
    457        click: () => this._copyInnerHTML(),
    458      })
    459    );
    460    copySubmenu.append(
    461      new MenuItem({
    462        id: "node-menu-copyouter",
    463        label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
    464        accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
    465        disabled: !isElement,
    466        click: () => this._copyOuterHTML(),
    467      })
    468    );
    469    copySubmenu.append(
    470      new MenuItem({
    471        id: "node-menu-copyuniqueselector",
    472        label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
    473        accesskey: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
    474        disabled: !isElement,
    475        click: () => this._copyUniqueSelector(),
    476      })
    477    );
    478    copySubmenu.append(
    479      new MenuItem({
    480        id: "node-menu-copycsspath",
    481        label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
    482        accesskey: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
    483        disabled: !isElement,
    484        click: () => this._copyCssPath(),
    485      })
    486    );
    487    copySubmenu.append(
    488      new MenuItem({
    489        id: "node-menu-copyxpath",
    490        label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
    491        accesskey: INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
    492        disabled: !isElement,
    493        click: () => this._copyXPath(),
    494      })
    495    );
    496    copySubmenu.append(
    497      new MenuItem({
    498        id: "node-menu-copyimagedatauri",
    499        label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
    500        disabled:
    501          !isElement || !markupContainer || !markupContainer.isPreviewable(),
    502        click: () => this._copyImageDataUri(),
    503      })
    504    );
    505 
    506    return copySubmenu;
    507  }
    508 
    509  _getDOMBreakpointSubmenu(isElement) {
    510    const menu = new Menu();
    511    const mutationBreakpoints = this.selection.nodeFront.mutationBreakpoints;
    512 
    513    menu.append(
    514      new MenuItem({
    515        id: "node-menu-mutation-breakpoint-subtree",
    516        checked: mutationBreakpoints.subtree,
    517        click: () => this.markup.toggleMutationBreakpoint("subtree"),
    518        disabled: !isElement,
    519        label: INSPECTOR_L10N.getStr("inspectorSubtreeModification.label"),
    520        type: "checkbox",
    521      })
    522    );
    523 
    524    menu.append(
    525      new MenuItem({
    526        id: "node-menu-mutation-breakpoint-attribute",
    527        checked: mutationBreakpoints.attribute,
    528        click: () => this.markup.toggleMutationBreakpoint("attribute"),
    529        disabled: !isElement,
    530        label: INSPECTOR_L10N.getStr("inspectorAttributeModification.label"),
    531        type: "checkbox",
    532      })
    533    );
    534 
    535    menu.append(
    536      new MenuItem({
    537        checked: mutationBreakpoints.removal,
    538        click: () => this.markup.toggleMutationBreakpoint("removal"),
    539        disabled: !isElement,
    540        label: INSPECTOR_L10N.getStr("inspectorNodeRemoval.label"),
    541        type: "checkbox",
    542      })
    543    );
    544 
    545    return menu;
    546  }
    547 
    548  /**
    549   * Link menu items can be shown or hidden depending on the context and
    550   * selected node, and their labels can vary.
    551   *
    552   * @return {Array} list of visible menu items related to links.
    553   */
    554  _getNodeLinkMenuItems() {
    555    const linkFollow = new MenuItem({
    556      id: "node-menu-link-follow",
    557      visible: false,
    558      click: () => this._onFollowLink(),
    559    });
    560    const linkCopy = new MenuItem({
    561      id: "node-menu-link-copy",
    562      visible: false,
    563      click: () => this._onCopyLink(),
    564    });
    565 
    566    // Get information about the right-clicked node.
    567    const popupNode = this.contextMenuTarget;
    568    if (!popupNode || !popupNode.classList.contains("link")) {
    569      return [linkFollow, linkCopy];
    570    }
    571 
    572    const type = popupNode.dataset.type;
    573    if (type === "uri" || type === "cssresource" || type === "jsresource") {
    574      // Links can't be opened in new tabs in the browser toolbox.
    575      if (type === "uri" && !this.toolbox.isBrowserToolbox) {
    576        linkFollow.visible = true;
    577        linkFollow.label = INSPECTOR_L10N.getStr(
    578          "inspector.menu.openUrlInNewTab.label"
    579        );
    580      } else if (type === "cssresource") {
    581        linkFollow.visible = true;
    582        linkFollow.label = TOOLBOX_L10N.getStr(
    583          "toolbox.viewCssSourceInStyleEditor.label"
    584        );
    585      } else if (type === "jsresource") {
    586        linkFollow.visible = true;
    587        linkFollow.label = TOOLBOX_L10N.getStr(
    588          "toolbox.viewJsSourceInDebugger.label"
    589        );
    590      }
    591 
    592      linkCopy.visible = true;
    593      linkCopy.label = INSPECTOR_L10N.getStr(
    594        "inspector.menu.copyUrlToClipboard.label"
    595      );
    596    } else if (type === "idref") {
    597      linkFollow.visible = true;
    598      linkFollow.label = INSPECTOR_L10N.getFormatStr(
    599        "inspector.menu.selectElement.label",
    600        popupNode.dataset.link
    601      );
    602    }
    603 
    604    return [linkFollow, linkCopy];
    605  }
    606 
    607  _getPasteSubmenu(isElement, isFragment, isAnonymous) {
    608    const isPasteable =
    609      !isAnonymous &&
    610      (isFragment || isElement) &&
    611      this._getClipboardContentForPaste();
    612    const disableAdjacentPaste =
    613      !isPasteable ||
    614      !isElement ||
    615      this.selection.isRoot() ||
    616      this.selection.isBodyNode() ||
    617      this.selection.isHeadNode();
    618    const disableFirstLastPaste =
    619      !isPasteable ||
    620      !isElement ||
    621      (this.selection.isHTMLNode() && this.selection.isRoot());
    622 
    623    const pasteSubmenu = new Menu();
    624    pasteSubmenu.append(
    625      new MenuItem({
    626        id: "node-menu-pasteinnerhtml",
    627        label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"),
    628        accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"),
    629        disabled: !isPasteable,
    630        click: () => this._pasteInnerHTML(),
    631      })
    632    );
    633    pasteSubmenu.append(
    634      new MenuItem({
    635        id: "node-menu-pasteouterhtml",
    636        label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"),
    637        accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"),
    638        disabled: !isPasteable || !isElement,
    639        click: () => this._pasteOuterHTML(),
    640      })
    641    );
    642    pasteSubmenu.append(
    643      new MenuItem({
    644        id: "node-menu-pastebefore",
    645        label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"),
    646        accesskey: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"),
    647        disabled: disableAdjacentPaste,
    648        click: () => this._pasteAdjacentHTML("beforeBegin"),
    649      })
    650    );
    651    pasteSubmenu.append(
    652      new MenuItem({
    653        id: "node-menu-pasteafter",
    654        label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"),
    655        accesskey: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"),
    656        disabled: disableAdjacentPaste,
    657        click: () => this._pasteAdjacentHTML("afterEnd"),
    658      })
    659    );
    660    pasteSubmenu.append(
    661      new MenuItem({
    662        id: "node-menu-pastefirstchild",
    663        label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"),
    664        accesskey: INSPECTOR_L10N.getStr(
    665          "inspectorHTMLPasteFirstChild.accesskey"
    666        ),
    667        disabled: disableFirstLastPaste,
    668        click: () => this._pasteAdjacentHTML("afterBegin"),
    669      })
    670    );
    671    pasteSubmenu.append(
    672      new MenuItem({
    673        id: "node-menu-pastelastchild",
    674        label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"),
    675        accesskey: INSPECTOR_L10N.getStr(
    676          "inspectorHTMLPasteLastChild.accesskey"
    677        ),
    678        disabled: disableFirstLastPaste,
    679        click: () => this._pasteAdjacentHTML("beforeEnd"),
    680      })
    681    );
    682 
    683    return pasteSubmenu;
    684  }
    685 
    686  _getPseudoClassSubmenu() {
    687    const menu = new Menu();
    688    const enabled = this.inspector.canTogglePseudoClassForSelectedNode();
    689 
    690    // Set the pseudo classes
    691    for (const name of PSEUDO_CLASSES) {
    692      const menuitem = new MenuItem({
    693        id: "node-menu-pseudo-" + name.substr(1),
    694        label: name.substr(1),
    695        type: "checkbox",
    696        click: () => this.inspector.togglePseudoClass(name),
    697      });
    698 
    699      if (enabled) {
    700        const checked = this.selection.nodeFront.hasPseudoClassLock(name);
    701        menuitem.checked = checked;
    702      } else {
    703        menuitem.disabled = true;
    704      }
    705 
    706      menu.append(menuitem);
    707    }
    708 
    709    return menu;
    710  }
    711 
    712  _getEditMarkupString() {
    713    if (this.selection.isHTMLNode()) {
    714      return "inspectorHTMLEdit";
    715    } else if (this.selection.isSVGNode()) {
    716      return "inspectorSVGEdit";
    717    } else if (this.selection.isMathMLNode()) {
    718      return "inspectorMathMLEdit";
    719    }
    720    return "inspectorXMLEdit";
    721  }
    722 
    723  _openMenu({ target, screenX = 0, screenY = 0 } = {}) {
    724    if (this.selection.isSlotted()) {
    725      // Slotted elements should not show any context menu.
    726      return null;
    727    }
    728 
    729    const markupContainer = this.markup.getContainer(this.selection.nodeFront);
    730 
    731    this.contextMenuTarget = target;
    732    this.nodeMenuTriggerInfo =
    733      markupContainer && markupContainer.editor.getInfoAtNode(target);
    734 
    735    const isFragment = this.selection.isDocumentFragmentNode();
    736    const isAnonymous = this.selection.isNativeAnonymousNode();
    737    const isElement =
    738      this.selection.isElementNode() && !this.selection.isPseudoElementNode();
    739    const isDuplicatableElement =
    740      isElement && !isAnonymous && !this.selection.isRoot();
    741    const isScreenshotable =
    742      isElement && this.selection.nodeFront.isTreeDisplayed;
    743 
    744    const menu = new Menu({ id: "markup-context-menu" });
    745    menu.append(
    746      new MenuItem({
    747        id: "node-menu-edithtml",
    748        label: INSPECTOR_L10N.getStr(`${this._getEditMarkupString()}.label`),
    749        accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"),
    750        disabled: isAnonymous || (!isElement && !isFragment),
    751        click: () => this._editHTML(),
    752      })
    753    );
    754    menu.append(
    755      new MenuItem({
    756        id: "node-menu-add",
    757        label: INSPECTOR_L10N.getStr("inspectorAddNode.label"),
    758        accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"),
    759        disabled: !this.inspector.canAddHTMLChild(),
    760        click: () => this.inspector.addNode(),
    761      })
    762    );
    763    menu.append(
    764      new MenuItem({
    765        id: "node-menu-duplicatenode",
    766        label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"),
    767        disabled: !isDuplicatableElement,
    768        click: () => this._duplicateNode(),
    769      })
    770    );
    771    menu.append(
    772      new MenuItem({
    773        id: "node-menu-delete",
    774        label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"),
    775        accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"),
    776        disabled: !this.markup.isDeletable(this.selection.nodeFront),
    777        click: () => this._deleteNode(),
    778      })
    779    );
    780 
    781    menu.append(
    782      new MenuItem({
    783        label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"),
    784        accesskey: INSPECTOR_L10N.getStr(
    785          "inspectorAttributesSubmenu.accesskey"
    786        ),
    787        submenu: this._getAttributesSubmenu(isElement && !isAnonymous),
    788      })
    789    );
    790 
    791    menu.append(
    792      new MenuItem({
    793        type: "separator",
    794      })
    795    );
    796 
    797    if (this.selection.nodeFront.mutationBreakpoints) {
    798      menu.append(
    799        new MenuItem({
    800          label: INSPECTOR_L10N.getStr("inspectorBreakpointSubmenu.label"),
    801          // FIXME(bug 1598952): This doesn't work in shadow trees at all, but
    802          // we still display the active menu. Also, this should probably be
    803          // enabled for ShadowRoot, at least the non-attribute breakpoints.
    804          submenu: this._getDOMBreakpointSubmenu(isElement),
    805          id: "node-menu-mutation-breakpoint",
    806        })
    807      );
    808    }
    809 
    810    menu.append(
    811      new MenuItem({
    812        id: "node-menu-useinconsole",
    813        label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"),
    814        click: () => this._useInConsole(),
    815      })
    816    );
    817 
    818    menu.append(
    819      new MenuItem({
    820        id: "node-menu-showdomproperties",
    821        label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"),
    822        click: () => this._showDOMProperties(),
    823      })
    824    );
    825 
    826    if (this.selection.isElementNode() || this.selection.isTextNode()) {
    827      menu.append(
    828        new MenuItem({
    829          id: "node-menu-showaccessibilityproperties",
    830          label: INSPECTOR_L10N.getStr(
    831            "inspectorShowAccessibilityProperties.label"
    832          ),
    833          click: () => this._showAccessibilityProperties(),
    834        })
    835      );
    836    }
    837 
    838    if (this.selection.nodeFront.customElementLocation) {
    839      menu.append(
    840        new MenuItem({
    841          id: "node-menu-jumptodefinition",
    842          label: INSPECTOR_L10N.getStr(
    843            "inspectorCustomElementDefinition.label"
    844          ),
    845          click: () => this._jumpToCustomElementDefinition(),
    846        })
    847      );
    848    }
    849 
    850    menu.append(
    851      new MenuItem({
    852        type: "separator",
    853      })
    854    );
    855 
    856    menu.append(
    857      new MenuItem({
    858        label: INSPECTOR_L10N.getStr("inspectorPseudoClassSubmenu.label"),
    859        submenu: this._getPseudoClassSubmenu(),
    860      })
    861    );
    862 
    863    menu.append(
    864      new MenuItem({
    865        id: "node-menu-screenshotnode",
    866        label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"),
    867        disabled: !isScreenshotable,
    868        click: () => this.inspector.screenshotNode().catch(console.error),
    869      })
    870    );
    871 
    872    menu.append(
    873      new MenuItem({
    874        id: "node-menu-scrollnodeintoview",
    875        label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"),
    876        accesskey: INSPECTOR_L10N.getStr(
    877          "inspectorScrollNodeIntoView.accesskey"
    878        ),
    879        disabled: !this.inspector.selection.supportsScrollIntoView(),
    880        click: () => this.markup.scrollNodeIntoView(),
    881      })
    882    );
    883 
    884    menu.append(
    885      new MenuItem({
    886        type: "separator",
    887      })
    888    );
    889 
    890    menu.append(
    891      new MenuItem({
    892        label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"),
    893        submenu: this._getCopySubmenu(markupContainer, isElement, isFragment),
    894      })
    895    );
    896 
    897    menu.append(
    898      new MenuItem({
    899        label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"),
    900        submenu: this._getPasteSubmenu(isElement, isFragment, isAnonymous),
    901      })
    902    );
    903 
    904    menu.append(
    905      new MenuItem({
    906        type: "separator",
    907      })
    908    );
    909 
    910    const isNodeWithChildren =
    911      this.selection.isNode() && markupContainer.hasChildren;
    912    menu.append(
    913      new MenuItem({
    914        id: "node-menu-expand",
    915        label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"),
    916        disabled: !isNodeWithChildren,
    917        click: () => this.markup.expandAll(this.selection.nodeFront),
    918      })
    919    );
    920    menu.append(
    921      new MenuItem({
    922        id: "node-menu-collapse",
    923        label: INSPECTOR_L10N.getStr("inspectorCollapseAll.label"),
    924        disabled: !isNodeWithChildren || !markupContainer.expanded,
    925        click: () => this.markup.collapseAll(this.selection.nodeFront),
    926      })
    927    );
    928 
    929    const nodeLinkMenuItems = this._getNodeLinkMenuItems();
    930    if (nodeLinkMenuItems.filter(item => item.visible).length) {
    931      menu.append(
    932        new MenuItem({
    933          id: "node-menu-link-separator",
    934          type: "separator",
    935        })
    936      );
    937    }
    938 
    939    for (const menuitem of nodeLinkMenuItems) {
    940      menu.append(menuitem);
    941    }
    942 
    943    menu.popup(screenX, screenY, this.toolbox.doc);
    944    return menu;
    945  }
    946 }
    947 
    948 module.exports = MarkupContextMenu;