diff options
Diffstat (limited to 'devtools/client/inspector/markup/markup-context-menu.js')
-rw-r--r-- | devtools/client/inspector/markup/markup-context-menu.js | 951 |
1 files changed, 951 insertions, 0 deletions
diff --git a/devtools/client/inspector/markup/markup-context-menu.js b/devtools/client/inspector/markup/markup-context-menu.js new file mode 100644 index 0000000000..38ee79d6ba --- /dev/null +++ b/devtools/client/inspector/markup/markup-context-menu.js @@ -0,0 +1,951 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + PSEUDO_CLASSES, +} = require("resource://devtools/shared/css/constants.js"); +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); + +loader.lazyRequireGetter( + this, + "Menu", + "resource://devtools/client/framework/menu.js" +); +loader.lazyRequireGetter( + this, + "MenuItem", + "resource://devtools/client/framework/menu-item.js" +); +loader.lazyRequireGetter( + this, + "clipboardHelper", + "resource://devtools/shared/platform/clipboard.js" +); + +loader.lazyGetter(this, "TOOLBOX_L10N", function () { + return new LocalizationHelper("devtools/client/locales/toolbox.properties"); +}); + +const INSPECTOR_L10N = new LocalizationHelper( + "devtools/client/locales/inspector.properties" +); + +/** + * Context menu for the Markup view. + */ +class MarkupContextMenu { + constructor(markup) { + this.markup = markup; + this.inspector = markup.inspector; + this.selection = this.inspector.selection; + this.target = this.inspector.currentTarget; + this.telemetry = this.inspector.telemetry; + this.toolbox = this.inspector.toolbox; + this.walker = this.inspector.walker; + } + + destroy() { + this.markup = null; + this.inspector = null; + this.selection = null; + this.target = null; + this.telemetry = null; + this.toolbox = null; + this.walker = null; + } + + show(event) { + if ( + !Element.isInstance(event.originalTarget) || + event.originalTarget.closest("input[type=text]") || + event.originalTarget.closest("input:not([type])") || + event.originalTarget.closest("textarea") + ) { + return; + } + + event.stopPropagation(); + event.preventDefault(); + + this._openMenu({ + screenX: event.screenX, + screenY: event.screenY, + target: event.target, + }); + } + + /** + * This method is here for the benefit of copying links. + */ + _copyAttributeLink(link) { + this.inspector.inspectorFront + .resolveRelativeURL(link, this.selection.nodeFront) + .then(url => { + clipboardHelper.copyString(url); + }, console.error); + } + + /** + * Copy the full CSS Path of the selected Node to the clipboard. + */ + _copyCssPath() { + if (!this.selection.isNode()) { + return; + } + + this.telemetry.scalarSet("devtools.copy.full.css.selector.opened", 1); + this.selection.nodeFront + .getCssPath() + .then(path => { + clipboardHelper.copyString(path); + }) + .catch(console.error); + } + + /** + * Copy the data-uri for the currently selected image in the clipboard. + */ + _copyImageDataUri() { + const container = this.markup.getContainer(this.selection.nodeFront); + if (container && container.isPreviewable()) { + container.copyImageDataUri(); + } + } + + /** + * Copy the innerHTML of the selected Node to the clipboard. + */ + _copyInnerHTML() { + this.markup.copyInnerHTML(); + } + + /** + * Copy the outerHTML of the selected Node to the clipboard. + */ + _copyOuterHTML() { + this.markup.copyOuterHTML(); + } + + /** + * Copy a unique selector of the selected Node to the clipboard. + */ + _copyUniqueSelector() { + if (!this.selection.isNode()) { + return; + } + + this.telemetry.scalarSet("devtools.copy.unique.css.selector.opened", 1); + this.selection.nodeFront + .getUniqueSelector() + .then(selector => { + clipboardHelper.copyString(selector); + }) + .catch(console.error); + } + + /** + * Copy the XPath of the selected Node to the clipboard. + */ + _copyXPath() { + if (!this.selection.isNode()) { + return; + } + + this.telemetry.scalarSet("devtools.copy.xpath.opened", 1); + this.selection.nodeFront + .getXPath() + .then(path => { + clipboardHelper.copyString(path); + }) + .catch(console.error); + } + + /** + * Delete the selected node. + */ + _deleteNode() { + if (!this.selection.isNode() || this.selection.isRoot()) { + return; + } + + const nodeFront = this.selection.nodeFront; + + // If the markup panel is active, use the markup panel to delete + // the node, making this an undoable action. + if (this.markup) { + this.markup.deleteNode(nodeFront); + } else { + // remove the node from content + nodeFront.walkerFront.removeNode(nodeFront); + } + } + + /** + * Duplicate the selected node + */ + _duplicateNode() { + if ( + !this.selection.isElementNode() || + this.selection.isRoot() || + this.selection.isAnonymousNode() || + this.selection.isPseudoElementNode() + ) { + return; + } + + const nodeFront = this.selection.nodeFront; + nodeFront.walkerFront.duplicateNode(nodeFront).catch(console.error); + } + + /** + * Edit the outerHTML of the selected Node. + */ + _editHTML() { + if (!this.selection.isNode()) { + return; + } + this.markup.beginEditingHTML(this.selection.nodeFront); + } + + /** + * Jumps to the custom element definition in the debugger. + */ + _jumpToCustomElementDefinition() { + const { url, line, column } = + this.selection.nodeFront.customElementLocation; + this.toolbox.viewSourceInDebugger( + url, + line, + column, + null, + "show_custom_element" + ); + } + + /** + * Add attribute to node. + * Used for node context menu and shouldn't be called directly. + */ + _onAddAttribute() { + const container = this.markup.getContainer(this.selection.nodeFront); + container.addAttribute(); + } + + /** + * Copy attribute value for node. + * Used for node context menu and shouldn't be called directly. + */ + _onCopyAttributeValue() { + clipboardHelper.copyString(this.nodeMenuTriggerInfo.value); + } + + /** + * This method is here for the benefit of the node-menu-link-copy menu item + * in the inspector contextual-menu. + */ + _onCopyLink() { + this._copyAttributeLink(this.contextMenuTarget.dataset.link); + } + + /** + * Edit attribute for node. + * Used for node context menu and shouldn't be called directly. + */ + _onEditAttribute() { + const container = this.markup.getContainer(this.selection.nodeFront); + container.editAttribute(this.nodeMenuTriggerInfo.name); + } + + /** + * This method is here for the benefit of the node-menu-link-follow menu item + * in the inspector contextual-menu. + */ + _onFollowLink() { + const type = this.contextMenuTarget.dataset.type; + const link = this.contextMenuTarget.dataset.link; + this.markup.followAttributeLink(type, link); + } + + /** + * Remove attribute from node. + * Used for node context menu and shouldn't be called directly. + */ + _onRemoveAttribute() { + const container = this.markup.getContainer(this.selection.nodeFront); + container.removeAttribute(this.nodeMenuTriggerInfo.name); + } + + /** + * Paste the contents of the clipboard as adjacent HTML to the selected Node. + * + * @param {String} position + * The position as specified for Element.insertAdjacentHTML + * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd"). + */ + _pasteAdjacentHTML(position) { + const content = this._getClipboardContentForPaste(); + if (!content) { + return Promise.reject("No clipboard content for paste"); + } + + const node = this.selection.nodeFront; + return this.markup.insertAdjacentHTMLToNode(node, position, content); + } + + /** + * Paste the contents of the clipboard into the selected Node's inner HTML. + */ + _pasteInnerHTML() { + const content = this._getClipboardContentForPaste(); + if (!content) { + return Promise.reject("No clipboard content for paste"); + } + + const node = this.selection.nodeFront; + return this.markup.getNodeInnerHTML(node).then(oldContent => { + this.markup.updateNodeInnerHTML(node, content, oldContent); + }); + } + + /** + * Paste the contents of the clipboard into the selected Node's outer HTML. + */ + _pasteOuterHTML() { + const content = this._getClipboardContentForPaste(); + if (!content) { + return Promise.reject("No clipboard content for paste"); + } + + const node = this.selection.nodeFront; + return this.markup.getNodeOuterHTML(node).then(oldContent => { + this.markup.updateNodeOuterHTML(node, content, oldContent); + }); + } + + /** + * Show Accessibility properties for currently selected node + */ + async _showAccessibilityProperties() { + const a11yPanel = await this.toolbox.selectTool("accessibility"); + // Select the accessible object in the panel and wait for the event that + // tells us it has been done. + const onSelected = a11yPanel.once("new-accessible-front-selected"); + a11yPanel.selectAccessibleForNode( + this.selection.nodeFront, + "inspector-context-menu" + ); + await onSelected; + } + + /** + * Show DOM properties + */ + _showDOMProperties() { + this.toolbox.openSplitConsole().then(() => { + const { hud } = this.toolbox.getPanel("webconsole"); + hud.ui.wrapper.dispatchEvaluateExpression("inspect($0, true)"); + }); + } + + /** + * Use in Console. + * + * Takes the currently selected node in the inspector and assigns it to a + * temp variable on the content window. Also opens the split console and + * autofills it with the temp variable. + */ + async _useInConsole() { + await this.toolbox.openSplitConsole(); + const { hud } = this.toolbox.getPanel("webconsole"); + + const evalString = `{ let i = 0; + while (window.hasOwnProperty("temp" + i) && i < 1000) { + i++; + } + window["temp" + i] = $0; + "temp" + i; + }`; + + const res = await this.toolbox.commands.scriptCommand.execute(evalString, { + selectedNodeActor: this.selection.nodeFront.actorID, + }); + hud.setInputValue(res.result); + this.inspector.emit("console-var-ready"); + } + + _getAttributesSubmenu(isEditableElement) { + const attributesSubmenu = new Menu(); + const nodeInfo = this.nodeMenuTriggerInfo; + const isAttributeClicked = + isEditableElement && nodeInfo && nodeInfo.type === "attribute"; + + attributesSubmenu.append( + new MenuItem({ + id: "node-menu-add-attribute", + label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"), + disabled: !isEditableElement, + click: () => this._onAddAttribute(), + }) + ); + attributesSubmenu.append( + new MenuItem({ + id: "node-menu-copy-attribute", + label: INSPECTOR_L10N.getFormatStr( + "inspectorCopyAttributeValue.label", + isAttributeClicked ? `${nodeInfo.value}` : "" + ), + accesskey: INSPECTOR_L10N.getStr( + "inspectorCopyAttributeValue.accesskey" + ), + disabled: !isAttributeClicked, + click: () => this._onCopyAttributeValue(), + }) + ); + attributesSubmenu.append( + new MenuItem({ + id: "node-menu-edit-attribute", + label: INSPECTOR_L10N.getFormatStr( + "inspectorEditAttribute.label", + isAttributeClicked ? `${nodeInfo.name}` : "" + ), + accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"), + disabled: !isAttributeClicked, + click: () => this._onEditAttribute(), + }) + ); + attributesSubmenu.append( + new MenuItem({ + id: "node-menu-remove-attribute", + label: INSPECTOR_L10N.getFormatStr( + "inspectorRemoveAttribute.label", + isAttributeClicked ? `${nodeInfo.name}` : "" + ), + accesskey: INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"), + disabled: !isAttributeClicked, + click: () => this._onRemoveAttribute(), + }) + ); + + return attributesSubmenu; + } + + /** + * Returns the clipboard content if it is appropriate for pasting + * into the current node's outer HTML, otherwise returns null. + */ + _getClipboardContentForPaste() { + const content = clipboardHelper.getText(); + if (content && content.trim().length) { + return content; + } + return null; + } + + _getCopySubmenu(markupContainer, isElement, isFragment) { + const copySubmenu = new Menu(); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copyinner", + label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"), + disabled: !isElement && !isFragment, + click: () => this._copyInnerHTML(), + }) + ); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copyouter", + label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"), + disabled: !isElement, + click: () => this._copyOuterHTML(), + }) + ); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copyuniqueselector", + label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"), + disabled: !isElement, + click: () => this._copyUniqueSelector(), + }) + ); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copycsspath", + label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"), + disabled: !isElement, + click: () => this._copyCssPath(), + }) + ); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copyxpath", + label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"), + disabled: !isElement, + click: () => this._copyXPath(), + }) + ); + copySubmenu.append( + new MenuItem({ + id: "node-menu-copyimagedatauri", + label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"), + disabled: + !isElement || !markupContainer || !markupContainer.isPreviewable(), + click: () => this._copyImageDataUri(), + }) + ); + + return copySubmenu; + } + + _getDOMBreakpointSubmenu(isElement) { + const menu = new Menu(); + const mutationBreakpoints = this.selection.nodeFront.mutationBreakpoints; + + menu.append( + new MenuItem({ + id: "node-menu-mutation-breakpoint-subtree", + checked: mutationBreakpoints.subtree, + click: () => this.markup.toggleMutationBreakpoint("subtree"), + disabled: !isElement, + label: INSPECTOR_L10N.getStr("inspectorSubtreeModification.label"), + type: "checkbox", + }) + ); + + menu.append( + new MenuItem({ + id: "node-menu-mutation-breakpoint-attribute", + checked: mutationBreakpoints.attribute, + click: () => this.markup.toggleMutationBreakpoint("attribute"), + disabled: !isElement, + label: INSPECTOR_L10N.getStr("inspectorAttributeModification.label"), + type: "checkbox", + }) + ); + + menu.append( + new MenuItem({ + checked: mutationBreakpoints.removal, + click: () => this.markup.toggleMutationBreakpoint("removal"), + disabled: !isElement, + label: INSPECTOR_L10N.getStr("inspectorNodeRemoval.label"), + type: "checkbox", + }) + ); + + return menu; + } + + /** + * Link menu items can be shown or hidden depending on the context and + * selected node, and their labels can vary. + * + * @return {Array} list of visible menu items related to links. + */ + _getNodeLinkMenuItems() { + const linkFollow = new MenuItem({ + id: "node-menu-link-follow", + visible: false, + click: () => this._onFollowLink(), + }); + const linkCopy = new MenuItem({ + id: "node-menu-link-copy", + visible: false, + click: () => this._onCopyLink(), + }); + + // Get information about the right-clicked node. + const popupNode = this.contextMenuTarget; + if (!popupNode || !popupNode.classList.contains("link")) { + return [linkFollow, linkCopy]; + } + + const type = popupNode.dataset.type; + if (type === "uri" || type === "cssresource" || type === "jsresource") { + // Links can't be opened in new tabs in the browser toolbox. + if (type === "uri" && !this.toolbox.isBrowserToolbox) { + linkFollow.visible = true; + linkFollow.label = INSPECTOR_L10N.getStr( + "inspector.menu.openUrlInNewTab.label" + ); + } else if (type === "cssresource") { + linkFollow.visible = true; + linkFollow.label = TOOLBOX_L10N.getStr( + "toolbox.viewCssSourceInStyleEditor.label" + ); + } else if (type === "jsresource") { + linkFollow.visible = true; + linkFollow.label = TOOLBOX_L10N.getStr( + "toolbox.viewJsSourceInDebugger.label" + ); + } + + linkCopy.visible = true; + linkCopy.label = INSPECTOR_L10N.getStr( + "inspector.menu.copyUrlToClipboard.label" + ); + } else if (type === "idref") { + linkFollow.visible = true; + linkFollow.label = INSPECTOR_L10N.getFormatStr( + "inspector.menu.selectElement.label", + popupNode.dataset.link + ); + } + + return [linkFollow, linkCopy]; + } + + _getPasteSubmenu(isElement, isFragment, isAnonymous) { + const isPasteable = + !isAnonymous && + (isFragment || isElement) && + this._getClipboardContentForPaste(); + const disableAdjacentPaste = + !isPasteable || + !isElement || + this.selection.isRoot() || + this.selection.isBodyNode() || + this.selection.isHeadNode(); + const disableFirstLastPaste = + !isPasteable || + !isElement || + (this.selection.isHTMLNode() && this.selection.isRoot()); + + const pasteSubmenu = new Menu(); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pasteinnerhtml", + label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"), + disabled: !isPasteable, + click: () => this._pasteInnerHTML(), + }) + ); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pasteouterhtml", + label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"), + disabled: !isPasteable || !isElement, + click: () => this._pasteOuterHTML(), + }) + ); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pastebefore", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"), + disabled: disableAdjacentPaste, + click: () => this._pasteAdjacentHTML("beforeBegin"), + }) + ); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pasteafter", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"), + disabled: disableAdjacentPaste, + click: () => this._pasteAdjacentHTML("afterEnd"), + }) + ); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pastefirstchild", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"), + accesskey: INSPECTOR_L10N.getStr( + "inspectorHTMLPasteFirstChild.accesskey" + ), + disabled: disableFirstLastPaste, + click: () => this._pasteAdjacentHTML("afterBegin"), + }) + ); + pasteSubmenu.append( + new MenuItem({ + id: "node-menu-pastelastchild", + label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"), + accesskey: INSPECTOR_L10N.getStr( + "inspectorHTMLPasteLastChild.accesskey" + ), + disabled: disableFirstLastPaste, + click: () => this._pasteAdjacentHTML("beforeEnd"), + }) + ); + + return pasteSubmenu; + } + + _getPseudoClassSubmenu(isElement) { + const menu = new Menu(); + + // Set the pseudo classes + for (const name of PSEUDO_CLASSES) { + const menuitem = new MenuItem({ + id: "node-menu-pseudo-" + name.substr(1), + label: name.substr(1), + type: "checkbox", + click: () => this.inspector.togglePseudoClass(name), + }); + + if (isElement) { + const checked = this.selection.nodeFront.hasPseudoClassLock(name); + menuitem.checked = checked; + } else { + menuitem.disabled = true; + } + + menu.append(menuitem); + } + + return menu; + } + + _getEditMarkupString() { + if (this.selection.isHTMLNode()) { + return "inspectorHTMLEdit"; + } else if (this.selection.isSVGNode()) { + return "inspectorSVGEdit"; + } else if (this.selection.isMathMLNode()) { + return "inspectorMathMLEdit"; + } + return "inspectorXMLEdit"; + } + + _openMenu({ target, screenX = 0, screenY = 0 } = {}) { + if (this.selection.isSlotted()) { + // Slotted elements should not show any context menu. + return null; + } + + const markupContainer = this.markup.getContainer(this.selection.nodeFront); + + this.contextMenuTarget = target; + this.nodeMenuTriggerInfo = + markupContainer && markupContainer.editor.getInfoAtNode(target); + + const isFragment = this.selection.isDocumentFragmentNode(); + const isAnonymous = this.selection.isAnonymousNode(); + const isElement = + this.selection.isElementNode() && !this.selection.isPseudoElementNode(); + const isDuplicatableElement = + isElement && !isAnonymous && !this.selection.isRoot(); + const isScreenshotable = + isElement && this.selection.nodeFront.isTreeDisplayed; + + const menu = new Menu(); + menu.append( + new MenuItem({ + id: "node-menu-edithtml", + label: INSPECTOR_L10N.getStr(`${this._getEditMarkupString()}.label`), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"), + disabled: isAnonymous || (!isElement && !isFragment), + click: () => this._editHTML(), + }) + ); + menu.append( + new MenuItem({ + id: "node-menu-add", + label: INSPECTOR_L10N.getStr("inspectorAddNode.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"), + disabled: !this.inspector.canAddHTMLChild(), + click: () => this.inspector.addNode(), + }) + ); + menu.append( + new MenuItem({ + id: "node-menu-duplicatenode", + label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"), + disabled: !isDuplicatableElement, + click: () => this._duplicateNode(), + }) + ); + menu.append( + new MenuItem({ + id: "node-menu-delete", + label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"), + accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"), + disabled: !this.markup.isDeletable(this.selection.nodeFront), + click: () => this._deleteNode(), + }) + ); + + menu.append( + new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"), + accesskey: INSPECTOR_L10N.getStr( + "inspectorAttributesSubmenu.accesskey" + ), + submenu: this._getAttributesSubmenu(isElement && !isAnonymous), + }) + ); + + menu.append( + new MenuItem({ + type: "separator", + }) + ); + + if ( + Services.prefs.getBoolPref( + "devtools.markup.mutationBreakpoints.enabled" + ) && + this.selection.nodeFront.mutationBreakpoints + ) { + menu.append( + new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorBreakpointSubmenu.label"), + // FIXME(bug 1598952): This doesn't work in shadow trees at all, but + // we still display the active menu. Also, this should probably be + // enabled for ShadowRoot, at least the non-attribute breakpoints. + submenu: this._getDOMBreakpointSubmenu(isElement), + id: "node-menu-mutation-breakpoint", + }) + ); + } + + menu.append( + new MenuItem({ + id: "node-menu-useinconsole", + label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"), + click: () => this._useInConsole(), + }) + ); + + menu.append( + new MenuItem({ + id: "node-menu-showdomproperties", + label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"), + click: () => this._showDOMProperties(), + }) + ); + + if (this.selection.isElementNode() || this.selection.isTextNode()) { + menu.append( + new MenuItem({ + id: "node-menu-showaccessibilityproperties", + label: INSPECTOR_L10N.getStr( + "inspectorShowAccessibilityProperties.label" + ), + click: () => this._showAccessibilityProperties(), + }) + ); + } + + if (this.selection.nodeFront.customElementLocation) { + menu.append( + new MenuItem({ + id: "node-menu-jumptodefinition", + label: INSPECTOR_L10N.getStr( + "inspectorCustomElementDefinition.label" + ), + click: () => this._jumpToCustomElementDefinition(), + }) + ); + } + + menu.append( + new MenuItem({ + type: "separator", + }) + ); + + menu.append( + new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorPseudoClassSubmenu.label"), + submenu: this._getPseudoClassSubmenu(isElement), + }) + ); + + menu.append( + new MenuItem({ + id: "node-menu-screenshotnode", + label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"), + disabled: !isScreenshotable, + click: () => this.inspector.screenshotNode().catch(console.error), + }) + ); + + menu.append( + new MenuItem({ + id: "node-menu-scrollnodeintoview", + label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"), + accesskey: INSPECTOR_L10N.getStr( + "inspectorScrollNodeIntoView.accesskey" + ), + disabled: !isElement, + click: () => this.markup.scrollNodeIntoView(), + }) + ); + + menu.append( + new MenuItem({ + type: "separator", + }) + ); + + menu.append( + new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"), + submenu: this._getCopySubmenu(markupContainer, isElement, isFragment), + }) + ); + + menu.append( + new MenuItem({ + label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"), + submenu: this._getPasteSubmenu(isElement, isFragment, isAnonymous), + }) + ); + + menu.append( + new MenuItem({ + type: "separator", + }) + ); + + const isNodeWithChildren = + this.selection.isNode() && markupContainer.hasChildren; + menu.append( + new MenuItem({ + id: "node-menu-expand", + label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"), + disabled: !isNodeWithChildren, + click: () => this.markup.expandAll(this.selection.nodeFront), + }) + ); + menu.append( + new MenuItem({ + id: "node-menu-collapse", + label: INSPECTOR_L10N.getStr("inspectorCollapseAll.label"), + disabled: !isNodeWithChildren || !markupContainer.expanded, + click: () => this.markup.collapseAll(this.selection.nodeFront), + }) + ); + + const nodeLinkMenuItems = this._getNodeLinkMenuItems(); + if (nodeLinkMenuItems.filter(item => item.visible).length) { + menu.append( + new MenuItem({ + id: "node-menu-link-separator", + type: "separator", + }) + ); + } + + for (const menuitem of nodeLinkMenuItems) { + menu.append(menuitem); + } + + menu.popup(screenX, screenY, this.toolbox.doc); + return menu; + } +} + +module.exports = MarkupContextMenu; |