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;