Bug 1529845 - Add a destroy for the MarkupContextMenu. r=rcaliman
[gecko.git] / devtools / client / inspector / markup / markup-context-menu.js
blobc8d4849a382a29a47ff998149f0dd98aaefe8beb
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 "use strict";
9 const Services = require("Services");
10 const promise = require("promise");
11 const { LocalizationHelper } = require("devtools/shared/l10n");
13 loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
14 loader.lazyRequireGetter(this, "MenuItem", "devtools/client/framework/menu-item");
15 loader.lazyRequireGetter(this, "copyLongString", "devtools/client/inspector/shared/utils", true);
16 loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
18 loader.lazyGetter(this, "TOOLBOX_L10N", function() {
19   return new LocalizationHelper("devtools/client/locales/toolbox.properties");
20 });
22 const INSPECTOR_L10N =
23   new LocalizationHelper("devtools/client/locales/inspector.properties");
25 /**
26  * Context menu for the Markup view.
27  */
28 class MarkupContextMenu {
29   constructor(markup) {
30     this.markup = markup;
31     this.inspector = markup.inspector;
32     this.selection = this.inspector.selection;
33     this.target = this.inspector.target;
34     this.telemetry = this.inspector.telemetry;
35     this.toolbox = this.inspector.toolbox;
36     this.walker = this.inspector.walker;
37   }
39   destroy() {
40     this.markup = null;
41     this.inspector = null;
42     this.selection = null;
43     this.target = null;
44     this.telemetry = null;
45     this.toolbox = null;
46     this.walker = null;
47   }
49   show(event) {
50     if (!(event.originalTarget instanceof Element) ||
51         event.originalTarget.closest("input[type=text]") ||
52         event.originalTarget.closest("input:not([type])") ||
53         event.originalTarget.closest("textarea")) {
54       return;
55     }
57     event.stopPropagation();
58     event.preventDefault();
60     this._openMenu({
61       screenX: event.screenX,
62       screenY: event.screenY,
63       target: event.target,
64     });
65   }
67   /**
68    * This method is here for the benefit of copying links.
69    */
70   _copyAttributeLink(link) {
71     this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
72       clipboardHelper.copyString(url);
73     }, console.error);
74   }
76   /**
77    * Copy the full CSS Path of the selected Node to the clipboard.
78    */
79   _copyCssPath() {
80     if (!this.selection.isNode()) {
81       return;
82     }
84     this.telemetry.scalarSet("devtools.copy.full.css.selector.opened", 1);
85     this.selection.nodeFront.getCssPath().then(path => {
86       clipboardHelper.copyString(path);
87     }).catch(console.error);
88   }
90   /**
91    * Copy the data-uri for the currently selected image in the clipboard.
92    */
93   _copyImageDataUri() {
94     const container = this.markup.getContainer(this.selection.nodeFront);
95     if (container && container.isPreviewable()) {
96       container.copyImageDataUri();
97     }
98   }
100   /**
101    * Copy the innerHTML of the selected Node to the clipboard.
102    */
103   _copyInnerHTML() {
104     if (!this.selection.isNode()) {
105       return;
106     }
108     copyLongString(this.walker.innerHTML(this.selection.nodeFront));
109   }
111   /**
112    * Copy the outerHTML of the selected Node to the clipboard.
113    */
114   _copyOuterHTML() {
115     if (!this.selection.isNode()) {
116       return;
117     }
119     this.markup.copyOuterHTML();
120   }
122   /**
123    * Copy a unique selector of the selected Node to the clipboard.
124    */
125   _copyUniqueSelector() {
126     if (!this.selection.isNode()) {
127       return;
128     }
130     this.telemetry.scalarSet("devtools.copy.unique.css.selector.opened", 1);
131     this.selection.nodeFront.getUniqueSelector().then(selector => {
132       clipboardHelper.copyString(selector);
133     }).catch(console.error);
134   }
136   /**
137    * Copy the XPath of the selected Node to the clipboard.
138    */
139   _copyXPath() {
140     if (!this.selection.isNode()) {
141       return;
142     }
144     this.telemetry.scalarSet("devtools.copy.xpath.opened", 1);
145     this.selection.nodeFront.getXPath().then(path => {
146       clipboardHelper.copyString(path);
147     }).catch(console.error);
148   }
150   /**
151    * Delete the selected node.
152    */
153   _deleteNode() {
154     if (!this.selection.isNode() ||
155          this.selection.isRoot()) {
156       return;
157     }
159     // If the markup panel is active, use the markup panel to delete
160     // the node, making this an undoable action.
161     if (this.markup) {
162       this.markup.deleteNode(this.selection.nodeFront);
163     } else {
164       // remove the node from content
165       this.walker.removeNode(this.selection.nodeFront);
166     }
167   }
169   /**
170    * Duplicate the selected node
171    */
172   _duplicateNode() {
173     if (!this.selection.isElementNode() ||
174         this.selection.isRoot() ||
175         this.selection.isAnonymousNode() ||
176         this.selection.isPseudoElementNode()) {
177       return;
178     }
180     this.walker.duplicateNode(this.selection.nodeFront).catch(console.error);
181   }
183   /**
184    * Edit the outerHTML of the selected Node.
185    */
186   _editHTML() {
187     if (!this.selection.isNode()) {
188       return;
189     }
191     this.markup.beginEditingOuterHTML(this.selection.nodeFront);
192   }
194   /**
195    * Jumps to the custom element definition in the debugger.
196    */
197   _jumpToCustomElementDefinition() {
198     const { url, line } = this.selection.nodeFront.customElementLocation;
199     this.toolbox.viewSourceInDebugger(url, line, "show_custom_element");
200   }
202   /**
203    * Add attribute to node.
204    * Used for node context menu and shouldn't be called directly.
205    */
206   _onAddAttribute() {
207     const container = this.markup.getContainer(this.selection.nodeFront);
208     container.addAttribute();
209   }
211   /**
212    * Copy attribute value for node.
213    * Used for node context menu and shouldn't be called directly.
214    */
215   _onCopyAttributeValue() {
216     clipboardHelper.copyString(this.nodeMenuTriggerInfo.value);
217   }
219   /**
220    * This method is here for the benefit of the node-menu-link-copy menu item
221    * in the inspector contextual-menu.
222    */
223   _onCopyLink() {
224     this.copyAttributeLink(this.contextMenuTarget.dataset.link);
225   }
227   /**
228    * Edit attribute for node.
229    * Used for node context menu and shouldn't be called directly.
230    */
231   _onEditAttribute() {
232     const container = this.markup.getContainer(this.selection.nodeFront);
233     container.editAttribute(this.nodeMenuTriggerInfo.name);
234   }
236   /**
237    * This method is here for the benefit of the node-menu-link-follow menu item
238    * in the inspector contextual-menu.
239    */
240   _onFollowLink() {
241     const type = this.contextMenuTarget.dataset.type;
242     const link = this.contextMenuTarget.dataset.link;
243     this.markup.followAttributeLink(type, link);
244   }
246   /**
247    * Remove attribute from node.
248    * Used for node context menu and shouldn't be called directly.
249    */
250   _onRemoveAttribute() {
251     const container = this.markup.getContainer(this.selection.nodeFront);
252     container.removeAttribute(this.nodeMenuTriggerInfo.name);
253   }
255   /**
256    * Paste the contents of the clipboard as adjacent HTML to the selected Node.
257    *
258    * @param  {String} position
259    *         The position as specified for Element.insertAdjacentHTML
260    *         (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
261    */
262   _pasteAdjacentHTML(position) {
263     const content = this._getClipboardContentForPaste();
264     if (!content) {
265       return promise.reject("No clipboard content for paste");
266     }
268     const node = this.selection.nodeFront;
269     return this.markup.insertAdjacentHTMLToNode(node, position, content);
270   }
272   /**
273    * Paste the contents of the clipboard into the selected Node's inner HTML.
274    */
275   _pasteInnerHTML() {
276     const content = this._getClipboardContentForPaste();
277     if (!content) {
278       return promise.reject("No clipboard content for paste");
279     }
281     const node = this.selection.nodeFront;
282     return this.markup.getNodeInnerHTML(node).then(oldContent => {
283       this.markup.updateNodeInnerHTML(node, content, oldContent);
284     });
285   }
287   /**
288    * Paste the contents of the clipboard into the selected Node's outer HTML.
289    */
290   _pasteOuterHTML() {
291     const content = this._getClipboardContentForPaste();
292     if (!content) {
293       return promise.reject("No clipboard content for paste");
294     }
296     const node = this.selection.nodeFront;
297     return this.markup.getNodeOuterHTML(node).then(oldContent => {
298       this.markup.updateNodeOuterHTML(node, content, oldContent);
299     });
300   }
302   /**
303    * Show Accessibility properties for currently selected node
304    */
305   async _showAccessibilityProperties() {
306     const a11yPanel = await this.toolbox.selectTool("accessibility");
307     // Select the accessible object in the panel and wait for the event that
308     // tells us it has been done.
309     const onSelected = a11yPanel.once("new-accessible-front-selected");
310     a11yPanel.selectAccessibleForNode(this.selection.nodeFront, "inspector-context-menu");
311     await onSelected;
312   }
314   /**
315    * Show DOM properties
316    */
317   _showDOMProperties() {
318     this.toolbox.openSplitConsole().then(() => {
319       const panel = this.toolbox.getPanel("webconsole");
320       const jsterm = panel.hud.jsterm;
322       jsterm.execute("inspect($0)");
323       jsterm.focus();
324     });
325   }
327   /**
328    * Use in Console.
329    *
330    * Takes the currently selected node in the inspector and assigns it to a
331    * temp variable on the content window.  Also opens the split console and
332    * autofills it with the temp variable.
333    */
334   _useInConsole() {
335     this.toolbox.openSplitConsole().then(() => {
336       const panel = this.toolbox.getPanel("webconsole");
337       const jsterm = panel.hud.jsterm;
339       const evalString = `{ let i = 0;
340         while (window.hasOwnProperty("temp" + i) && i < 1000) {
341           i++;
342         }
343         window["temp" + i] = $0;
344         "temp" + i;
345       }`;
347       const options = {
348         selectedNodeActor: this.selection.nodeFront.actorID,
349       };
350       jsterm.requestEvaluation(evalString, options).then((res) => {
351         jsterm.setInputValue(res.result);
352         this.inspector.emit("console-var-ready");
353       });
354     });
355   }
357   _buildA11YMenuItem(menu) {
358     if (!(this.selection.isElementNode() || this.selection.isTextNode()) ||
359         !Services.prefs.getBoolPref("devtools.accessibility.enabled")) {
360       return;
361     }
363     const showA11YPropsItem = new MenuItem({
364       id: "node-menu-showaccessibilityproperties",
365       label: INSPECTOR_L10N.getStr("inspectorShowAccessibilityProperties.label"),
366       click: () => this._showAccessibilityProperties(),
367       disabled: true,
368     });
370     // Only attempt to determine if a11y props menu item needs to be enabled if
371     // AccessibilityFront is enabled.
372     if (this.inspector.accessibilityFront.enabled) {
373       this._updateA11YMenuItem(showA11YPropsItem);
374     }
376     menu.append(showA11YPropsItem);
377   }
379   _getAttributesSubmenu(isEditableElement) {
380     const attributesSubmenu = new Menu();
381     const nodeInfo = this.nodeMenuTriggerInfo;
382     const isAttributeClicked = isEditableElement && nodeInfo &&
383                               nodeInfo.type === "attribute";
385     attributesSubmenu.append(new MenuItem({
386       id: "node-menu-add-attribute",
387       label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"),
388       accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"),
389       disabled: !isEditableElement,
390       click: () => this._onAddAttribute(),
391     }));
392     attributesSubmenu.append(new MenuItem({
393       id: "node-menu-copy-attribute",
394       label: INSPECTOR_L10N.getFormatStr("inspectorCopyAttributeValue.label",
395                                         isAttributeClicked ? `${nodeInfo.value}` : ""),
396       accesskey: INSPECTOR_L10N.getStr("inspectorCopyAttributeValue.accesskey"),
397       disabled: !isAttributeClicked,
398       click: () => this._onCopyAttributeValue(),
399     }));
400     attributesSubmenu.append(new MenuItem({
401       id: "node-menu-edit-attribute",
402       label: INSPECTOR_L10N.getFormatStr("inspectorEditAttribute.label",
403                                         isAttributeClicked ? `${nodeInfo.name}` : ""),
404       accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"),
405       disabled: !isAttributeClicked,
406       click: () => this._onEditAttribute(),
407     }));
408     attributesSubmenu.append(new MenuItem({
409       id: "node-menu-remove-attribute",
410       label: INSPECTOR_L10N.getFormatStr("inspectorRemoveAttribute.label",
411                                         isAttributeClicked ? `${nodeInfo.name}` : ""),
412       accesskey: INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"),
413       disabled: !isAttributeClicked,
414       click: () => this._onRemoveAttribute(),
415     }));
417     return attributesSubmenu;
418   }
420   /**
421    * Returns the clipboard content if it is appropriate for pasting
422    * into the current node's outer HTML, otherwise returns null.
423    */
424   _getClipboardContentForPaste() {
425     const content = clipboardHelper.getText();
426     if (content && content.trim().length > 0) {
427       return content;
428     }
429     return null;
430   }
432   _getCopySubmenu(markupContainer, isSelectionElement) {
433     const copySubmenu = new Menu();
434     copySubmenu.append(new MenuItem({
435       id: "node-menu-copyinner",
436       label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"),
437       accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"),
438       disabled: !isSelectionElement,
439       click: () => this._copyInnerHTML(),
440     }));
441     copySubmenu.append(new MenuItem({
442       id: "node-menu-copyouter",
443       label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
444       accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
445       disabled: !isSelectionElement,
446       click: () => this._copyOuterHTML(),
447     }));
448     copySubmenu.append(new MenuItem({
449       id: "node-menu-copyuniqueselector",
450       label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
451       accesskey:
452         INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
453       disabled: !isSelectionElement,
454       click: () => this._copyUniqueSelector(),
455     }));
456     copySubmenu.append(new MenuItem({
457       id: "node-menu-copycsspath",
458       label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
459       accesskey:
460         INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
461       disabled: !isSelectionElement,
462       click: () => this._copyCssPath(),
463     }));
464     copySubmenu.append(new MenuItem({
465       id: "node-menu-copyxpath",
466       label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
467       accesskey:
468         INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
469       disabled: !isSelectionElement,
470       click: () => this._copyXPath(),
471     }));
472     copySubmenu.append(new MenuItem({
473       id: "node-menu-copyimagedatauri",
474       label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
475       disabled: !isSelectionElement || !markupContainer ||
476                 !markupContainer.isPreviewable(),
477       click: () => this._copyImageDataUri(),
478     }));
480     return copySubmenu;
481   }
483   /**
484    * Link menu items can be shown or hidden depending on the context and
485    * selected node, and their labels can vary.
486    *
487    * @return {Array} list of visible menu items related to links.
488    */
489   _getNodeLinkMenuItems() {
490     const linkFollow = new MenuItem({
491       id: "node-menu-link-follow",
492       visible: false,
493       click: () => this._onFollowLink(),
494     });
495     const linkCopy = new MenuItem({
496       id: "node-menu-link-copy",
497       visible: false,
498       click: () => this._onCopyLink(),
499     });
501     // Get information about the right-clicked node.
502     const popupNode = this.contextMenuTarget;
503     if (!popupNode || !popupNode.classList.contains("link")) {
504       return [linkFollow, linkCopy];
505     }
507     const type = popupNode.dataset.type;
508     if ((type === "uri" || type === "cssresource" || type === "jsresource")) {
509       // Links can't be opened in new tabs in the browser toolbox.
510       if (type === "uri" && !this.target.chrome) {
511         linkFollow.visible = true;
512         linkFollow.label = INSPECTOR_L10N.getStr(
513           "inspector.menu.openUrlInNewTab.label");
514       } else if (type === "cssresource") {
515         linkFollow.visible = true;
516         linkFollow.label = TOOLBOX_L10N.getStr(
517           "toolbox.viewCssSourceInStyleEditor.label");
518       } else if (type === "jsresource") {
519         linkFollow.visible = true;
520         linkFollow.label = TOOLBOX_L10N.getStr(
521           "toolbox.viewJsSourceInDebugger.label");
522       }
524       linkCopy.visible = true;
525       linkCopy.label = INSPECTOR_L10N.getStr(
526         "inspector.menu.copyUrlToClipboard.label");
527     } else if (type === "idref") {
528       linkFollow.visible = true;
529       linkFollow.label = INSPECTOR_L10N.getFormatStr(
530         "inspector.menu.selectElement.label", popupNode.dataset.link);
531     }
533     return [linkFollow, linkCopy];
534   }
536   _getPasteSubmenu(isEditableElement) {
537     const isPasteable = isEditableElement && this._getClipboardContentForPaste();
538     const disableAdjacentPaste = !isPasteable ||
539                                  this.selection.isRoot() ||
540                                  this.selection.isBodyNode() ||
541                                  this.selection.isHeadNode();
542     const disableFirstLastPaste = !isPasteable ||
543       (this.selection.isHTMLNode() && this.selection.isRoot());
545     const pasteSubmenu = new Menu();
546     pasteSubmenu.append(new MenuItem({
547       id: "node-menu-pasteinnerhtml",
548       label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"),
549       accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"),
550       disabled: !isPasteable,
551       click: () => this._pasteInnerHTML(),
552     }));
553     pasteSubmenu.append(new MenuItem({
554       id: "node-menu-pasteouterhtml",
555       label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"),
556       accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"),
557       disabled: !isPasteable,
558       click: () => this._pasteOuterHTML(),
559     }));
560     pasteSubmenu.append(new MenuItem({
561       id: "node-menu-pastebefore",
562       label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"),
563       accesskey:
564         INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"),
565       disabled: disableAdjacentPaste,
566       click: () => this._pasteAdjacentHTML("beforeBegin"),
567     }));
568     pasteSubmenu.append(new MenuItem({
569       id: "node-menu-pasteafter",
570       label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"),
571       accesskey:
572         INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"),
573       disabled: disableAdjacentPaste,
574       click: () => this._pasteAdjacentHTML("afterEnd"),
575     }));
576     pasteSubmenu.append(new MenuItem({
577       id: "node-menu-pastefirstchild",
578       label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"),
579       accesskey:
580         INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"),
581       disabled: disableFirstLastPaste,
582       click: () => this._pasteAdjacentHTML("afterBegin"),
583     }));
584     pasteSubmenu.append(new MenuItem({
585       id: "node-menu-pastelastchild",
586       label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"),
587       accesskey:
588         INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"),
589       disabled: disableFirstLastPaste,
590       click: () => this._pasteAdjacentHTML("beforeEnd"),
591     }));
593     return pasteSubmenu;
594   }
596   _openMenu({ target, screenX = 0, screenY = 0 } = {}) {
597     if (this.selection.isSlotted()) {
598       // Slotted elements should not show any context menu.
599       return null;
600     }
602     const markupContainer = this.markup.getContainer(this.selection.nodeFront);
604     this.contextMenuTarget = target;
605     this.nodeMenuTriggerInfo = markupContainer &&
606       markupContainer.editor.getInfoAtNode(target);
608     const isSelectionElement = this.selection.isElementNode() &&
609                              !this.selection.isPseudoElementNode();
610     const isEditableElement = isSelectionElement &&
611                             !this.selection.isAnonymousNode();
612     const isDuplicatableElement = isSelectionElement &&
613                                 !this.selection.isAnonymousNode() &&
614                                 !this.selection.isRoot();
615     const isScreenshotable = isSelectionElement &&
616                            this.selection.nodeFront.isTreeDisplayed;
618     const menu = new Menu();
619     menu.append(new MenuItem({
620       id: "node-menu-edithtml",
621       label: INSPECTOR_L10N.getStr("inspectorHTMLEdit.label"),
622       accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"),
623       disabled: !isEditableElement,
624       click: () => this._editHTML(),
625     }));
626     menu.append(new MenuItem({
627       id: "node-menu-add",
628       label: INSPECTOR_L10N.getStr("inspectorAddNode.label"),
629       accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"),
630       disabled: !this.inspector.canAddHTMLChild(),
631       click: () => this.inspector.addNode(),
632     }));
633     menu.append(new MenuItem({
634       id: "node-menu-duplicatenode",
635       label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"),
636       disabled: !isDuplicatableElement,
637       click: () => this._duplicateNode(),
638     }));
639     menu.append(new MenuItem({
640       id: "node-menu-delete",
641       label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"),
642       accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"),
643       disabled: !this.markup.isDeletable(this.selection.nodeFront),
644       click: () => this._deleteNode(),
645     }));
647     menu.append(new MenuItem({
648       label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"),
649       accesskey:
650         INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.accesskey"),
651       submenu: this._getAttributesSubmenu(isEditableElement),
652     }));
654     menu.append(new MenuItem({
655       type: "separator",
656     }));
658     // Set the pseudo classes
659     for (const name of ["hover", "active", "focus", "focus-within"]) {
660       const menuitem = new MenuItem({
661         id: "node-menu-pseudo-" + name,
662         label: name,
663         type: "checkbox",
664         click: () => this.inspector.togglePseudoClass(":" + name),
665       });
667       if (isSelectionElement) {
668         const checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
669         menuitem.checked = checked;
670       } else {
671         menuitem.disabled = true;
672       }
674       menu.append(menuitem);
675     }
677     menu.append(new MenuItem({
678       type: "separator",
679     }));
681     menu.append(new MenuItem({
682       label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"),
683       submenu: this._getCopySubmenu(markupContainer, isSelectionElement),
684     }));
686     menu.append(new MenuItem({
687       label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"),
688       submenu: this._getPasteSubmenu(isEditableElement),
689     }));
691     menu.append(new MenuItem({
692       type: "separator",
693     }));
695     const isNodeWithChildren = this.selection.isNode() &&
696                              markupContainer.hasChildren;
697     menu.append(new MenuItem({
698       id: "node-menu-expand",
699       label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"),
700       disabled: !isNodeWithChildren,
701       click: () => this.markup.expandAll(this.selection.nodeFront),
702     }));
703     menu.append(new MenuItem({
704       id: "node-menu-collapse",
705       label: INSPECTOR_L10N.getStr("inspectorCollapseAll.label"),
706       disabled: !isNodeWithChildren || !markupContainer.expanded,
707       click: () => this.markup.collapseAll(this.selection.nodeFront),
708     }));
710     menu.append(new MenuItem({
711       type: "separator",
712     }));
714     menu.append(new MenuItem({
715       id: "node-menu-scrollnodeintoview",
716       label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"),
717       accesskey:
718         INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"),
719       disabled: !isSelectionElement,
720       click: () => this.markup.scrollNodeIntoView(),
721     }));
722     menu.append(new MenuItem({
723       id: "node-menu-screenshotnode",
724       label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"),
725       disabled: !isScreenshotable,
726       click: () => this.inspector.screenshotNode().catch(console.error),
727     }));
728     menu.append(new MenuItem({
729       id: "node-menu-useinconsole",
730       label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"),
731       click: () => this._useInConsole(),
732     }));
733     menu.append(new MenuItem({
734       id: "node-menu-showdomproperties",
735       label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"),
736       click: () => this._showDOMProperties(),
737     }));
739     if (this.selection.nodeFront.customElementLocation) {
740       menu.append(new MenuItem({
741         type: "separator",
742       }));
744       menu.append(new MenuItem({
745         id: "node-menu-jumptodefinition",
746         label: INSPECTOR_L10N.getStr("inspectorCustomElementDefinition.label"),
747         click: () => this._jumpToCustomElementDefinition(),
748       }));
749     }
751     this._buildA11YMenuItem(menu);
753     const nodeLinkMenuItems = this._getNodeLinkMenuItems();
754     if (nodeLinkMenuItems.filter(item => item.visible).length > 0) {
755       menu.append(new MenuItem({
756         id: "node-menu-link-separator",
757         type: "separator",
758       }));
759     }
761     for (const menuitem of nodeLinkMenuItems) {
762       menu.append(menuitem);
763     }
765     menu.popup(screenX, screenY, this.toolbox);
766     return menu;
767   }
769   async _updateA11YMenuItem(menuItem) {
770     const hasMethod = await this.target.actorHasMethod("domwalker",
771                                                        "hasAccessibilityProperties");
772     if (!hasMethod) {
773       return;
774     }
776     const hasA11YProps = await this.walker.hasAccessibilityProperties(
777       this.selection.nodeFront);
778     if (hasA11YProps) {
779       this.toolbox.doc.getElementById(menuItem.id).disabled = menuItem.disabled = false;
780     }
782     this.inspector.emit("node-menu-updated");
783   }
786 module.exports = MarkupContextMenu;