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/. */
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");
22 const INSPECTOR_L10N =
23 new LocalizationHelper("devtools/client/locales/inspector.properties");
26 * Context menu for the Markup view.
28 class MarkupContextMenu {
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;
41 this.inspector = null;
42 this.selection = null;
44 this.telemetry = null;
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")) {
57 event.stopPropagation();
58 event.preventDefault();
61 screenX: event.screenX,
62 screenY: event.screenY,
68 * This method is here for the benefit of copying links.
70 _copyAttributeLink(link) {
71 this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
72 clipboardHelper.copyString(url);
77 * Copy the full CSS Path of the selected Node to the clipboard.
80 if (!this.selection.isNode()) {
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);
91 * Copy the data-uri for the currently selected image in the clipboard.
94 const container = this.markup.getContainer(this.selection.nodeFront);
95 if (container && container.isPreviewable()) {
96 container.copyImageDataUri();
101 * Copy the innerHTML of the selected Node to the clipboard.
104 if (!this.selection.isNode()) {
108 copyLongString(this.walker.innerHTML(this.selection.nodeFront));
112 * Copy the outerHTML of the selected Node to the clipboard.
115 if (!this.selection.isNode()) {
119 this.markup.copyOuterHTML();
123 * Copy a unique selector of the selected Node to the clipboard.
125 _copyUniqueSelector() {
126 if (!this.selection.isNode()) {
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);
137 * Copy the XPath of the selected Node to the clipboard.
140 if (!this.selection.isNode()) {
144 this.telemetry.scalarSet("devtools.copy.xpath.opened", 1);
145 this.selection.nodeFront.getXPath().then(path => {
146 clipboardHelper.copyString(path);
147 }).catch(console.error);
151 * Delete the selected node.
154 if (!this.selection.isNode() ||
155 this.selection.isRoot()) {
159 // If the markup panel is active, use the markup panel to delete
160 // the node, making this an undoable action.
162 this.markup.deleteNode(this.selection.nodeFront);
164 // remove the node from content
165 this.walker.removeNode(this.selection.nodeFront);
170 * Duplicate the selected node
173 if (!this.selection.isElementNode() ||
174 this.selection.isRoot() ||
175 this.selection.isAnonymousNode() ||
176 this.selection.isPseudoElementNode()) {
180 this.walker.duplicateNode(this.selection.nodeFront).catch(console.error);
184 * Edit the outerHTML of the selected Node.
187 if (!this.selection.isNode()) {
191 this.markup.beginEditingOuterHTML(this.selection.nodeFront);
195 * Jumps to the custom element definition in the debugger.
197 _jumpToCustomElementDefinition() {
198 const { url, line } = this.selection.nodeFront.customElementLocation;
199 this.toolbox.viewSourceInDebugger(url, line, "show_custom_element");
203 * Add attribute to node.
204 * Used for node context menu and shouldn't be called directly.
207 const container = this.markup.getContainer(this.selection.nodeFront);
208 container.addAttribute();
212 * Copy attribute value for node.
213 * Used for node context menu and shouldn't be called directly.
215 _onCopyAttributeValue() {
216 clipboardHelper.copyString(this.nodeMenuTriggerInfo.value);
220 * This method is here for the benefit of the node-menu-link-copy menu item
221 * in the inspector contextual-menu.
224 this.copyAttributeLink(this.contextMenuTarget.dataset.link);
228 * Edit attribute for node.
229 * Used for node context menu and shouldn't be called directly.
232 const container = this.markup.getContainer(this.selection.nodeFront);
233 container.editAttribute(this.nodeMenuTriggerInfo.name);
237 * This method is here for the benefit of the node-menu-link-follow menu item
238 * in the inspector contextual-menu.
241 const type = this.contextMenuTarget.dataset.type;
242 const link = this.contextMenuTarget.dataset.link;
243 this.markup.followAttributeLink(type, link);
247 * Remove attribute from node.
248 * Used for node context menu and shouldn't be called directly.
250 _onRemoveAttribute() {
251 const container = this.markup.getContainer(this.selection.nodeFront);
252 container.removeAttribute(this.nodeMenuTriggerInfo.name);
256 * Paste the contents of the clipboard as adjacent HTML to the selected Node.
258 * @param {String} position
259 * The position as specified for Element.insertAdjacentHTML
260 * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
262 _pasteAdjacentHTML(position) {
263 const content = this._getClipboardContentForPaste();
265 return promise.reject("No clipboard content for paste");
268 const node = this.selection.nodeFront;
269 return this.markup.insertAdjacentHTMLToNode(node, position, content);
273 * Paste the contents of the clipboard into the selected Node's inner HTML.
276 const content = this._getClipboardContentForPaste();
278 return promise.reject("No clipboard content for paste");
281 const node = this.selection.nodeFront;
282 return this.markup.getNodeInnerHTML(node).then(oldContent => {
283 this.markup.updateNodeInnerHTML(node, content, oldContent);
288 * Paste the contents of the clipboard into the selected Node's outer HTML.
291 const content = this._getClipboardContentForPaste();
293 return promise.reject("No clipboard content for paste");
296 const node = this.selection.nodeFront;
297 return this.markup.getNodeOuterHTML(node).then(oldContent => {
298 this.markup.updateNodeOuterHTML(node, content, oldContent);
303 * Show Accessibility properties for currently selected node
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");
315 * Show DOM properties
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)");
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.
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) {
343 window["temp" + i] = $0;
348 selectedNodeActor: this.selection.nodeFront.actorID,
350 jsterm.requestEvaluation(evalString, options).then((res) => {
351 jsterm.setInputValue(res.result);
352 this.inspector.emit("console-var-ready");
357 _buildA11YMenuItem(menu) {
358 if (!(this.selection.isElementNode() || this.selection.isTextNode()) ||
359 !Services.prefs.getBoolPref("devtools.accessibility.enabled")) {
363 const showA11YPropsItem = new MenuItem({
364 id: "node-menu-showaccessibilityproperties",
365 label: INSPECTOR_L10N.getStr("inspectorShowAccessibilityProperties.label"),
366 click: () => this._showAccessibilityProperties(),
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);
376 menu.append(showA11YPropsItem);
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(),
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(),
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(),
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(),
417 return attributesSubmenu;
421 * Returns the clipboard content if it is appropriate for pasting
422 * into the current node's outer HTML, otherwise returns null.
424 _getClipboardContentForPaste() {
425 const content = clipboardHelper.getText();
426 if (content && content.trim().length > 0) {
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(),
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(),
448 copySubmenu.append(new MenuItem({
449 id: "node-menu-copyuniqueselector",
450 label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
452 INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
453 disabled: !isSelectionElement,
454 click: () => this._copyUniqueSelector(),
456 copySubmenu.append(new MenuItem({
457 id: "node-menu-copycsspath",
458 label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
460 INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
461 disabled: !isSelectionElement,
462 click: () => this._copyCssPath(),
464 copySubmenu.append(new MenuItem({
465 id: "node-menu-copyxpath",
466 label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
468 INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
469 disabled: !isSelectionElement,
470 click: () => this._copyXPath(),
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(),
484 * Link menu items can be shown or hidden depending on the context and
485 * selected node, and their labels can vary.
487 * @return {Array} list of visible menu items related to links.
489 _getNodeLinkMenuItems() {
490 const linkFollow = new MenuItem({
491 id: "node-menu-link-follow",
493 click: () => this._onFollowLink(),
495 const linkCopy = new MenuItem({
496 id: "node-menu-link-copy",
498 click: () => this._onCopyLink(),
501 // Get information about the right-clicked node.
502 const popupNode = this.contextMenuTarget;
503 if (!popupNode || !popupNode.classList.contains("link")) {
504 return [linkFollow, linkCopy];
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");
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);
533 return [linkFollow, linkCopy];
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(),
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(),
560 pasteSubmenu.append(new MenuItem({
561 id: "node-menu-pastebefore",
562 label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"),
564 INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"),
565 disabled: disableAdjacentPaste,
566 click: () => this._pasteAdjacentHTML("beforeBegin"),
568 pasteSubmenu.append(new MenuItem({
569 id: "node-menu-pasteafter",
570 label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"),
572 INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"),
573 disabled: disableAdjacentPaste,
574 click: () => this._pasteAdjacentHTML("afterEnd"),
576 pasteSubmenu.append(new MenuItem({
577 id: "node-menu-pastefirstchild",
578 label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"),
580 INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"),
581 disabled: disableFirstLastPaste,
582 click: () => this._pasteAdjacentHTML("afterBegin"),
584 pasteSubmenu.append(new MenuItem({
585 id: "node-menu-pastelastchild",
586 label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"),
588 INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"),
589 disabled: disableFirstLastPaste,
590 click: () => this._pasteAdjacentHTML("beforeEnd"),
596 _openMenu({ target, screenX = 0, screenY = 0 } = {}) {
597 if (this.selection.isSlotted()) {
598 // Slotted elements should not show any context menu.
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(),
626 menu.append(new MenuItem({
628 label: INSPECTOR_L10N.getStr("inspectorAddNode.label"),
629 accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"),
630 disabled: !this.inspector.canAddHTMLChild(),
631 click: () => this.inspector.addNode(),
633 menu.append(new MenuItem({
634 id: "node-menu-duplicatenode",
635 label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"),
636 disabled: !isDuplicatableElement,
637 click: () => this._duplicateNode(),
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(),
647 menu.append(new MenuItem({
648 label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"),
650 INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.accesskey"),
651 submenu: this._getAttributesSubmenu(isEditableElement),
654 menu.append(new MenuItem({
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,
664 click: () => this.inspector.togglePseudoClass(":" + name),
667 if (isSelectionElement) {
668 const checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
669 menuitem.checked = checked;
671 menuitem.disabled = true;
674 menu.append(menuitem);
677 menu.append(new MenuItem({
681 menu.append(new MenuItem({
682 label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"),
683 submenu: this._getCopySubmenu(markupContainer, isSelectionElement),
686 menu.append(new MenuItem({
687 label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"),
688 submenu: this._getPasteSubmenu(isEditableElement),
691 menu.append(new MenuItem({
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),
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),
710 menu.append(new MenuItem({
714 menu.append(new MenuItem({
715 id: "node-menu-scrollnodeintoview",
716 label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"),
718 INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"),
719 disabled: !isSelectionElement,
720 click: () => this.markup.scrollNodeIntoView(),
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),
728 menu.append(new MenuItem({
729 id: "node-menu-useinconsole",
730 label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"),
731 click: () => this._useInConsole(),
733 menu.append(new MenuItem({
734 id: "node-menu-showdomproperties",
735 label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"),
736 click: () => this._showDOMProperties(),
739 if (this.selection.nodeFront.customElementLocation) {
740 menu.append(new MenuItem({
744 menu.append(new MenuItem({
745 id: "node-menu-jumptodefinition",
746 label: INSPECTOR_L10N.getStr("inspectorCustomElementDefinition.label"),
747 click: () => this._jumpToCustomElementDefinition(),
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",
761 for (const menuitem of nodeLinkMenuItems) {
762 menu.append(menuitem);
765 menu.popup(screenX, screenY, this.toolbox);
769 async _updateA11YMenuItem(menuItem) {
770 const hasMethod = await this.target.actorHasMethod("domwalker",
771 "hasAccessibilityProperties");
776 const hasA11YProps = await this.walker.hasAccessibilityProperties(
777 this.selection.nodeFront);
779 this.toolbox.doc.getElementById(menuItem.id).disabled = menuItem.disabled = false;
782 this.inspector.emit("node-menu-updated");
786 module.exports = MarkupContextMenu;