mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-01 00:32:11 +00:00
Bug 1529364
- Move the markup context menu into a separate module. r=rcaliman
Differential Revision: https://phabricator.services.mozilla.com/D20554 --HG-- rename : devtools/client/inspector/inspector.js => devtools/client/inspector/markup/markup-context-menu.js
This commit is contained in:
parent
1cc1d33847
commit
3021ef3b5f
@ -28,12 +28,7 @@ loader.lazyRequireGetter(this, "InspectorSearch", "devtools/client/inspector/ins
|
||||
loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/inspector/toolsidebar", true);
|
||||
loader.lazyRequireGetter(this, "MarkupView", "devtools/client/inspector/markup/markup");
|
||||
loader.lazyRequireGetter(this, "HighlightersOverlay", "devtools/client/inspector/shared/highlighters-overlay");
|
||||
loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
|
||||
loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
|
||||
loader.lazyRequireGetter(this, "MenuItem", "devtools/client/framework/menu-item");
|
||||
loader.lazyRequireGetter(this, "ExtensionSidebar", "devtools/client/inspector/extensions/extension-sidebar");
|
||||
loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
|
||||
loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
|
||||
loader.lazyRequireGetter(this, "saveScreenshot", "devtools/shared/screenshot/save");
|
||||
|
||||
loader.lazyImporter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
|
||||
@ -41,9 +36,6 @@ loader.lazyImporter(this, "DeferredTask", "resource://gre/modules/DeferredTask.j
|
||||
const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n");
|
||||
const INSPECTOR_L10N =
|
||||
new LocalizationHelper("devtools/client/locales/inspector.properties");
|
||||
loader.lazyGetter(this, "TOOLBOX_L10N", function() {
|
||||
return new LocalizationHelper("devtools/client/locales/toolbox.properties");
|
||||
});
|
||||
|
||||
// Sidebar dimensions
|
||||
const INITIAL_SIDEBAR_SIZE = 350;
|
||||
@ -125,12 +117,9 @@ function Inspector(toolbox) {
|
||||
// telemetry counts in the Grid Inspector are not double counted on reload.
|
||||
this.previousURL = this.target.url;
|
||||
|
||||
this.nodeMenuTriggerInfo = null;
|
||||
|
||||
this._clearSearchResultsLabel = this._clearSearchResultsLabel.bind(this);
|
||||
this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
|
||||
this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
|
||||
this._onContextMenu = this._onContextMenu.bind(this);
|
||||
this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
|
||||
this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
|
||||
|
||||
@ -1402,8 +1391,6 @@ Inspector.prototype = {
|
||||
|
||||
if (this._markupFrame) {
|
||||
this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
|
||||
this._markupFrame.contentWindow.removeEventListener("contextmenu",
|
||||
this._onContextMenu);
|
||||
}
|
||||
|
||||
if (this._search) {
|
||||
@ -1449,449 +1436,6 @@ Inspector.prototype = {
|
||||
return this._panelDestroyer;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the clipboard content if it is appropriate for pasting
|
||||
* into the current node's outer HTML, otherwise returns null.
|
||||
*/
|
||||
_getClipboardContentForPaste: function() {
|
||||
const content = clipboardHelper.getText();
|
||||
if (content && content.trim().length > 0) {
|
||||
return content;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
_onContextMenu: function(e) {
|
||||
if (!(e.originalTarget instanceof Element) ||
|
||||
e.originalTarget.closest("input[type=text]") ||
|
||||
e.originalTarget.closest("input:not([type])") ||
|
||||
e.originalTarget.closest("textarea")) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this._openMenu({
|
||||
screenX: e.screenX,
|
||||
screenY: e.screenY,
|
||||
target: e.target,
|
||||
});
|
||||
},
|
||||
|
||||
_openMenu: function({ 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 isSelectionElement = this.selection.isElementNode() &&
|
||||
!this.selection.isPseudoElementNode();
|
||||
const isEditableElement = isSelectionElement &&
|
||||
!this.selection.isAnonymousNode();
|
||||
const isDuplicatableElement = isSelectionElement &&
|
||||
!this.selection.isAnonymousNode() &&
|
||||
!this.selection.isRoot();
|
||||
const isScreenshotable = isSelectionElement &&
|
||||
this.selection.nodeFront.isTreeDisplayed;
|
||||
|
||||
const menu = new Menu();
|
||||
menu.append(new MenuItem({
|
||||
id: "node-menu-edithtml",
|
||||
label: INSPECTOR_L10N.getStr("inspectorHTMLEdit.label"),
|
||||
accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"),
|
||||
disabled: !isEditableElement,
|
||||
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.canAddHTMLChild(),
|
||||
click: () => this.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.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(isEditableElement),
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
type: "separator",
|
||||
}));
|
||||
|
||||
// Set the pseudo classes
|
||||
for (const name of ["hover", "active", "focus", "focus-within"]) {
|
||||
const menuitem = new MenuItem({
|
||||
id: "node-menu-pseudo-" + name,
|
||||
label: name,
|
||||
type: "checkbox",
|
||||
click: this.togglePseudoClass.bind(this, ":" + name),
|
||||
});
|
||||
|
||||
if (isSelectionElement) {
|
||||
const checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
|
||||
menuitem.checked = checked;
|
||||
} else {
|
||||
menuitem.disabled = true;
|
||||
}
|
||||
|
||||
menu.append(menuitem);
|
||||
}
|
||||
|
||||
menu.append(new MenuItem({
|
||||
type: "separator",
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"),
|
||||
submenu: this._getCopySubmenu(markupContainer, isSelectionElement),
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"),
|
||||
submenu: this._getPasteSubmenu(isEditableElement),
|
||||
}));
|
||||
|
||||
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.expandNode(),
|
||||
}));
|
||||
menu.append(new MenuItem({
|
||||
id: "node-menu-collapse",
|
||||
label: INSPECTOR_L10N.getStr("inspectorCollapseAll.label"),
|
||||
disabled: !isNodeWithChildren || !markupContainer.expanded,
|
||||
click: () => this.collapseAll(),
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
type: "separator",
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
id: "node-menu-scrollnodeintoview",
|
||||
label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"),
|
||||
accesskey:
|
||||
INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"),
|
||||
disabled: !isSelectionElement,
|
||||
click: () => this.scrollNodeIntoView(),
|
||||
}));
|
||||
menu.append(new MenuItem({
|
||||
id: "node-menu-screenshotnode",
|
||||
label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"),
|
||||
disabled: !isScreenshotable,
|
||||
click: () => this.screenshotNode().catch(console.error),
|
||||
}));
|
||||
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.nodeFront.customElementLocation) {
|
||||
menu.append(new MenuItem({
|
||||
type: "separator",
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
id: "node-menu-jumptodefinition",
|
||||
label: INSPECTOR_L10N.getStr("inspectorCustomElementDefinition.label"),
|
||||
click: () => this.jumpToCustomElementDefinition(),
|
||||
}));
|
||||
}
|
||||
|
||||
this.buildA11YMenuItem(menu);
|
||||
|
||||
const nodeLinkMenuItems = this._getNodeLinkMenuItems();
|
||||
if (nodeLinkMenuItems.filter(item => item.visible).length > 0) {
|
||||
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);
|
||||
return menu;
|
||||
},
|
||||
|
||||
buildA11YMenuItem: function(menu) {
|
||||
if (!(this.selection.isElementNode() || this.selection.isTextNode()) ||
|
||||
!Services.prefs.getBoolPref("devtools.accessibility.enabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showA11YPropsItem = new MenuItem({
|
||||
id: "node-menu-showaccessibilityproperties",
|
||||
label: INSPECTOR_L10N.getStr("inspectorShowAccessibilityProperties.label"),
|
||||
click: () => this.showAccessibilityProperties(),
|
||||
disabled: true,
|
||||
});
|
||||
// Only attempt to determine if a11y props menu item needs to be enabled if
|
||||
// AccessibilityFront is enabled.
|
||||
if (this.accessibilityFront.enabled) {
|
||||
this._updateA11YMenuItem(showA11YPropsItem);
|
||||
}
|
||||
|
||||
menu.append(showA11YPropsItem);
|
||||
},
|
||||
|
||||
_updateA11YMenuItem: async function(menuItem) {
|
||||
const hasMethod = await this.target.actorHasMethod("domwalker",
|
||||
"hasAccessibilityProperties");
|
||||
if (!hasMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasA11YProps = await this.walker.hasAccessibilityProperties(
|
||||
this.selection.nodeFront);
|
||||
if (hasA11YProps) {
|
||||
this._toolbox.doc.getElementById(menuItem.id).disabled = menuItem.disabled = false;
|
||||
}
|
||||
|
||||
this.emit("node-menu-updated");
|
||||
},
|
||||
|
||||
_getCopySubmenu: function(markupContainer, isSelectionElement) {
|
||||
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: !isSelectionElement,
|
||||
click: () => this.copyInnerHTML(),
|
||||
}));
|
||||
copySubmenu.append(new MenuItem({
|
||||
id: "node-menu-copyouter",
|
||||
label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
|
||||
accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
|
||||
disabled: !isSelectionElement,
|
||||
click: () => this.copyOuterHTML(),
|
||||
}));
|
||||
copySubmenu.append(new MenuItem({
|
||||
id: "node-menu-copyuniqueselector",
|
||||
label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
|
||||
accesskey:
|
||||
INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
|
||||
disabled: !isSelectionElement,
|
||||
click: () => this.copyUniqueSelector(),
|
||||
}));
|
||||
copySubmenu.append(new MenuItem({
|
||||
id: "node-menu-copycsspath",
|
||||
label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
|
||||
accesskey:
|
||||
INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
|
||||
disabled: !isSelectionElement,
|
||||
click: () => this.copyCssPath(),
|
||||
}));
|
||||
copySubmenu.append(new MenuItem({
|
||||
id: "node-menu-copyxpath",
|
||||
label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
|
||||
accesskey:
|
||||
INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
|
||||
disabled: !isSelectionElement,
|
||||
click: () => this.copyXPath(),
|
||||
}));
|
||||
copySubmenu.append(new MenuItem({
|
||||
id: "node-menu-copyimagedatauri",
|
||||
label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
|
||||
disabled: !isSelectionElement || !markupContainer ||
|
||||
!markupContainer.isPreviewable(),
|
||||
click: () => this.copyImageDataUri(),
|
||||
}));
|
||||
|
||||
return copySubmenu;
|
||||
},
|
||||
|
||||
_getPasteSubmenu: function(isEditableElement) {
|
||||
const isPasteable = isEditableElement && this._getClipboardContentForPaste();
|
||||
const disableAdjacentPaste = !isPasteable || this.selection.isRoot() ||
|
||||
this.selection.isBodyNode() || this.selection.isHeadNode();
|
||||
const disableFirstLastPaste = !isPasteable ||
|
||||
(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,
|
||||
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;
|
||||
},
|
||||
|
||||
_getAttributesSubmenu: function(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;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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: function() {
|
||||
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.target.chrome) {
|
||||
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];
|
||||
},
|
||||
|
||||
_initMarkup: function() {
|
||||
if (!this._markupFrame) {
|
||||
this._markupFrame = this.panelDoc.createElement("iframe");
|
||||
@ -1913,7 +1457,6 @@ Inspector.prototype = {
|
||||
|
||||
_onMarkupFrameLoad: function() {
|
||||
this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
|
||||
this._markupFrame.contentWindow.addEventListener("contextmenu", this._onContextMenu);
|
||||
this._markupFrame.contentWindow.focus();
|
||||
this._markupBox.style.visibility = "visible";
|
||||
this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
|
||||
@ -2028,241 +1571,6 @@ Inspector.prototype = {
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Show DOM properties
|
||||
*/
|
||||
showDOMProperties: function() {
|
||||
this._toolbox.openSplitConsole().then(() => {
|
||||
const panel = this._toolbox.getPanel("webconsole");
|
||||
const jsterm = panel.hud.jsterm;
|
||||
|
||||
jsterm.execute("inspect($0)");
|
||||
jsterm.focus();
|
||||
});
|
||||
},
|
||||
|
||||
jumpToCustomElementDefinition: function() {
|
||||
const node = this.selection.nodeFront;
|
||||
const { url, line } = node.customElementLocation;
|
||||
this._toolbox.viewSourceInDebugger(url, line, "show_custom_element");
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
useInConsole: function() {
|
||||
this._toolbox.openSplitConsole().then(() => {
|
||||
const panel = this._toolbox.getPanel("webconsole");
|
||||
const jsterm = panel.hud.jsterm;
|
||||
|
||||
const evalString = `{ let i = 0;
|
||||
while (window.hasOwnProperty("temp" + i) && i < 1000) {
|
||||
i++;
|
||||
}
|
||||
window["temp" + i] = $0;
|
||||
"temp" + i;
|
||||
}`;
|
||||
|
||||
const options = {
|
||||
selectedNodeActor: this.selection.nodeFront.actorID,
|
||||
};
|
||||
jsterm.requestEvaluation(evalString, options).then((res) => {
|
||||
jsterm.setInputValue(res.result);
|
||||
this.emit("console-var-ready");
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit the outerHTML of the selected Node.
|
||||
*/
|
||||
editHTML: function() {
|
||||
if (!this.selection.isNode()) {
|
||||
return;
|
||||
}
|
||||
if (this.markup) {
|
||||
this.markup.beginEditingOuterHTML(this.selection.nodeFront);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Paste the contents of the clipboard into the selected Node's outer HTML.
|
||||
*/
|
||||
pasteOuterHTML: function() {
|
||||
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);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Paste the contents of the clipboard into the selected Node's inner HTML.
|
||||
*/
|
||||
pasteInnerHTML: function() {
|
||||
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 as adjacent HTML to the selected Node.
|
||||
* @param position
|
||||
* The position as specified for Element.insertAdjacentHTML
|
||||
* (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
|
||||
*/
|
||||
pasteAdjacentHTML: function(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);
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy the innerHTML of the selected Node to the clipboard.
|
||||
*/
|
||||
copyInnerHTML: function() {
|
||||
if (!this.selection.isNode()) {
|
||||
return;
|
||||
}
|
||||
this._copyLongString(this.walker.innerHTML(this.selection.nodeFront));
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy the outerHTML of the selected Node to the clipboard.
|
||||
*/
|
||||
copyOuterHTML: function() {
|
||||
if (!this.selection.isNode()) {
|
||||
return;
|
||||
}
|
||||
const node = this.selection.nodeFront;
|
||||
|
||||
switch (node.nodeType) {
|
||||
case nodeConstants.ELEMENT_NODE :
|
||||
this._copyLongString(this.walker.outerHTML(node));
|
||||
break;
|
||||
case nodeConstants.COMMENT_NODE :
|
||||
this._getLongString(node.getNodeValue()).then(comment => {
|
||||
clipboardHelper.copyString("<!--" + comment + "-->");
|
||||
});
|
||||
break;
|
||||
case nodeConstants.DOCUMENT_TYPE_NODE :
|
||||
clipboardHelper.copyString(node.doctypeString);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy the data-uri for the currently selected image in the clipboard.
|
||||
*/
|
||||
copyImageDataUri: function() {
|
||||
const container = this.markup.getContainer(this.selection.nodeFront);
|
||||
if (container && container.isPreviewable()) {
|
||||
container.copyImageDataUri();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy the content of a longString (via a promise resolving a
|
||||
* LongStringActor) to the clipboard
|
||||
* @param {Promise} longStringActorPromise
|
||||
* promise expected to resolve a LongStringActor instance
|
||||
* @return {Promise} promise resolving (with no argument) when the
|
||||
* string is sent to the clipboard
|
||||
*/
|
||||
_copyLongString: function(longStringActorPromise) {
|
||||
return this._getLongString(longStringActorPromise).then(string => {
|
||||
clipboardHelper.copyString(string);
|
||||
}).catch(console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the content of a longString (via a promise resolving a LongStringActor)
|
||||
* @param {Promise} longStringActorPromise
|
||||
* promise expected to resolve a LongStringActor instance
|
||||
* @return {Promise} promise resolving with the retrieved string as argument
|
||||
*/
|
||||
_getLongString: function(longStringActorPromise) {
|
||||
return longStringActorPromise.then(longStringActor => {
|
||||
return longStringActor.string().then(string => {
|
||||
longStringActor.release().catch(console.error);
|
||||
return string;
|
||||
});
|
||||
}).catch(console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy a unique selector of the selected Node to the clipboard.
|
||||
*/
|
||||
copyUniqueSelector: function() {
|
||||
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 full CSS Path of the selected Node to the clipboard.
|
||||
*/
|
||||
copyCssPath: function() {
|
||||
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 XPath of the selected Node to the clipboard.
|
||||
*/
|
||||
copyXPath: function() {
|
||||
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);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initiate screenshot command on selected node.
|
||||
*/
|
||||
@ -2285,160 +1593,6 @@ Inspector.prototype = {
|
||||
await saveScreenshot(this.panelWin, args, screenshot);
|
||||
},
|
||||
|
||||
/**
|
||||
* Scroll the node into view.
|
||||
*/
|
||||
scrollNodeIntoView: function() {
|
||||
if (!this.selection.isNode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selection.nodeFront.scrollIntoView();
|
||||
},
|
||||
|
||||
/**
|
||||
* Duplicate the selected node
|
||||
*/
|
||||
duplicateNode: function() {
|
||||
const selection = this.selection;
|
||||
if (!selection.isElementNode() ||
|
||||
selection.isRoot() ||
|
||||
selection.isAnonymousNode() ||
|
||||
selection.isPseudoElementNode()) {
|
||||
return;
|
||||
}
|
||||
this.walker.duplicateNode(selection.nodeFront).catch(console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the selected node.
|
||||
*/
|
||||
deleteNode: function() {
|
||||
if (!this.selection.isNode() ||
|
||||
this.selection.isRoot()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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(this.selection.nodeFront);
|
||||
} else {
|
||||
// remove the node from content
|
||||
this.walker.removeNode(this.selection.nodeFront);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add attribute to node.
|
||||
* Used for node context menu and shouldn't be called directly.
|
||||
*/
|
||||
onAddAttribute: function() {
|
||||
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: function() {
|
||||
clipboardHelper.copyString(this.nodeMenuTriggerInfo.value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit attribute for node.
|
||||
* Used for node context menu and shouldn't be called directly.
|
||||
*/
|
||||
onEditAttribute: function() {
|
||||
const container = this.markup.getContainer(this.selection.nodeFront);
|
||||
container.editAttribute(this.nodeMenuTriggerInfo.name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove attribute from node.
|
||||
* Used for node context menu and shouldn't be called directly.
|
||||
*/
|
||||
onRemoveAttribute: function() {
|
||||
const container = this.markup.getContainer(this.selection.nodeFront);
|
||||
container.removeAttribute(this.nodeMenuTriggerInfo.name);
|
||||
},
|
||||
|
||||
expandNode: function() {
|
||||
this.markup.expandAll(this.selection.nodeFront);
|
||||
},
|
||||
|
||||
collapseAll: function() {
|
||||
this.markup.collapseAll(this.selection.nodeFront);
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is here for the benefit of the node-menu-link-follow menu item
|
||||
* in the inspector contextual-menu.
|
||||
*/
|
||||
onFollowLink: function() {
|
||||
const type = this.contextMenuTarget.dataset.type;
|
||||
const link = this.contextMenuTarget.dataset.link;
|
||||
|
||||
this.followAttributeLink(type, link);
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a type and link found in a node's attribute in the markup-view,
|
||||
* attempt to follow that link (which may result in opening a new tab, the
|
||||
* style editor or debugger).
|
||||
*/
|
||||
followAttributeLink: function(type, link) {
|
||||
if (!type || !link) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "uri" || type === "cssresource" || type === "jsresource") {
|
||||
// Open link in a new tab.
|
||||
this.inspector.resolveRelativeURL(
|
||||
link, this.selection.nodeFront).then(url => {
|
||||
if (type === "uri") {
|
||||
openContentLink(url);
|
||||
} else if (type === "cssresource") {
|
||||
return this.toolbox.viewSourceInStyleEditor(url);
|
||||
} else if (type === "jsresource") {
|
||||
return this.toolbox.viewSourceInDebugger(url);
|
||||
}
|
||||
return null;
|
||||
}).catch(console.error);
|
||||
} else if (type == "idref") {
|
||||
// Select the node in the same document.
|
||||
this.walker.document(this.selection.nodeFront).then(doc => {
|
||||
return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
|
||||
if (!node) {
|
||||
this.emit("idref-attribute-link-failed");
|
||||
return;
|
||||
}
|
||||
this.selection.setNodeFront(node);
|
||||
});
|
||||
}).catch(console.error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is here for the benefit of the node-menu-link-copy menu item
|
||||
* in the inspector contextual-menu.
|
||||
*/
|
||||
onCopyLink: function() {
|
||||
const link = this.contextMenuTarget.dataset.link;
|
||||
|
||||
this.copyAttributeLink(link);
|
||||
},
|
||||
|
||||
/**
|
||||
* This method is here for the benefit of copying links.
|
||||
*/
|
||||
copyAttributeLink: function(link) {
|
||||
this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
|
||||
clipboardHelper.copyString(url);
|
||||
}, console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an object containing the shared handler functions used in the box
|
||||
* model and grid React components.
|
||||
@ -2464,18 +1618,6 @@ Inspector.prototype = {
|
||||
toolbox.highlighter.highlight(nodeFront, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a value indicating whether a node can be deleted.
|
||||
*
|
||||
* @param {NodeFront} nodeFront
|
||||
* The node to test for deletion
|
||||
*/
|
||||
isDeletable(nodeFront) {
|
||||
return !(nodeFront.isDocumentElement ||
|
||||
nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
|
||||
nodeFront.isAnonymous);
|
||||
},
|
||||
|
||||
async inspectNodeActor(nodeActor, inspectFromAnnotation) {
|
||||
const nodeFront = await this.walker.gripToNodeFront({ actor: nodeActor });
|
||||
if (!nodeFront) {
|
||||
|
776
devtools/client/inspector/markup/markup-context-menu.js
Normal file
776
devtools/client/inspector/markup/markup-context-menu.js
Normal file
@ -0,0 +1,776 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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 Services = require("Services");
|
||||
const promise = require("promise");
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
|
||||
loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
|
||||
loader.lazyRequireGetter(this, "MenuItem", "devtools/client/framework/menu-item");
|
||||
loader.lazyRequireGetter(this, "copyLongString", "devtools/client/inspector/shared/utils", true);
|
||||
loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
|
||||
|
||||
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.target;
|
||||
this.telemetry = this.inspector.telemetry;
|
||||
this.toolbox = this.inspector.toolbox;
|
||||
this.walker = this.inspector.walker;
|
||||
}
|
||||
|
||||
show(event) {
|
||||
if (!(event.originalTarget instanceof Element) ||
|
||||
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.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() {
|
||||
if (!this.selection.isNode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
copyLongString(this.walker.innerHTML(this.selection.nodeFront));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the outerHTML of the selected Node to the clipboard.
|
||||
*/
|
||||
_copyOuterHTML() {
|
||||
if (!this.selection.isNode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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(this.selection.nodeFront);
|
||||
} else {
|
||||
// remove the node from content
|
||||
this.walker.removeNode(this.selection.nodeFront);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate the selected node
|
||||
*/
|
||||
_duplicateNode() {
|
||||
if (!this.selection.isElementNode() ||
|
||||
this.selection.isRoot() ||
|
||||
this.selection.isAnonymousNode() ||
|
||||
this.selection.isPseudoElementNode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.walker.duplicateNode(this.selection.nodeFront).catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the outerHTML of the selected Node.
|
||||
*/
|
||||
_editHTML() {
|
||||
if (!this.selection.isNode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.markup.beginEditingOuterHTML(this.selection.nodeFront);
|
||||
}
|
||||
|
||||
/**
|
||||
* Jumps to the custom element definition in the debugger.
|
||||
*/
|
||||
_jumpToCustomElementDefinition() {
|
||||
const { url, line } = this.selection.nodeFront.customElementLocation;
|
||||
this.toolbox.viewSourceInDebugger(url, line, "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 panel = this.toolbox.getPanel("webconsole");
|
||||
const jsterm = panel.hud.jsterm;
|
||||
|
||||
jsterm.execute("inspect($0)");
|
||||
jsterm.focus();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
_useInConsole() {
|
||||
this.toolbox.openSplitConsole().then(() => {
|
||||
const panel = this.toolbox.getPanel("webconsole");
|
||||
const jsterm = panel.hud.jsterm;
|
||||
|
||||
const evalString = `{ let i = 0;
|
||||
while (window.hasOwnProperty("temp" + i) && i < 1000) {
|
||||
i++;
|
||||
}
|
||||
window["temp" + i] = $0;
|
||||
"temp" + i;
|
||||
}`;
|
||||
|
||||
const options = {
|
||||
selectedNodeActor: this.selection.nodeFront.actorID,
|
||||
};
|
||||
jsterm.requestEvaluation(evalString, options).then((res) => {
|
||||
jsterm.setInputValue(res.result);
|
||||
this.inspector.emit("console-var-ready");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_buildA11YMenuItem(menu) {
|
||||
if (!(this.selection.isElementNode() || this.selection.isTextNode()) ||
|
||||
!Services.prefs.getBoolPref("devtools.accessibility.enabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const showA11YPropsItem = new MenuItem({
|
||||
id: "node-menu-showaccessibilityproperties",
|
||||
label: INSPECTOR_L10N.getStr("inspectorShowAccessibilityProperties.label"),
|
||||
click: () => this._showAccessibilityProperties(),
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
// Only attempt to determine if a11y props menu item needs to be enabled if
|
||||
// AccessibilityFront is enabled.
|
||||
if (this.inspector.accessibilityFront.enabled) {
|
||||
this._updateA11YMenuItem(showA11YPropsItem);
|
||||
}
|
||||
|
||||
menu.append(showA11YPropsItem);
|
||||
}
|
||||
|
||||
_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 > 0) {
|
||||
return content;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_getCopySubmenu(markupContainer, isSelectionElement) {
|
||||
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: !isSelectionElement,
|
||||
click: () => this._copyInnerHTML(),
|
||||
}));
|
||||
copySubmenu.append(new MenuItem({
|
||||
id: "node-menu-copyouter",
|
||||
label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
|
||||
accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
|
||||
disabled: !isSelectionElement,
|
||||
click: () => this._copyOuterHTML(),
|
||||
}));
|
||||
copySubmenu.append(new MenuItem({
|
||||
id: "node-menu-copyuniqueselector",
|
||||
label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
|
||||
accesskey:
|
||||
INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
|
||||
disabled: !isSelectionElement,
|
||||
click: () => this._copyUniqueSelector(),
|
||||
}));
|
||||
copySubmenu.append(new MenuItem({
|
||||
id: "node-menu-copycsspath",
|
||||
label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
|
||||
accesskey:
|
||||
INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
|
||||
disabled: !isSelectionElement,
|
||||
click: () => this._copyCssPath(),
|
||||
}));
|
||||
copySubmenu.append(new MenuItem({
|
||||
id: "node-menu-copyxpath",
|
||||
label: INSPECTOR_L10N.getStr("inspectorCopyXPath.label"),
|
||||
accesskey:
|
||||
INSPECTOR_L10N.getStr("inspectorCopyXPath.accesskey"),
|
||||
disabled: !isSelectionElement,
|
||||
click: () => this._copyXPath(),
|
||||
}));
|
||||
copySubmenu.append(new MenuItem({
|
||||
id: "node-menu-copyimagedatauri",
|
||||
label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
|
||||
disabled: !isSelectionElement || !markupContainer ||
|
||||
!markupContainer.isPreviewable(),
|
||||
click: () => this._copyImageDataUri(),
|
||||
}));
|
||||
|
||||
return copySubmenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.target.chrome) {
|
||||
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(isEditableElement) {
|
||||
const isPasteable = isEditableElement && this._getClipboardContentForPaste();
|
||||
const disableAdjacentPaste = !isPasteable ||
|
||||
this.selection.isRoot() ||
|
||||
this.selection.isBodyNode() ||
|
||||
this.selection.isHeadNode();
|
||||
const disableFirstLastPaste = !isPasteable ||
|
||||
(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,
|
||||
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;
|
||||
}
|
||||
|
||||
_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 isSelectionElement = this.selection.isElementNode() &&
|
||||
!this.selection.isPseudoElementNode();
|
||||
const isEditableElement = isSelectionElement &&
|
||||
!this.selection.isAnonymousNode();
|
||||
const isDuplicatableElement = isSelectionElement &&
|
||||
!this.selection.isAnonymousNode() &&
|
||||
!this.selection.isRoot();
|
||||
const isScreenshotable = isSelectionElement &&
|
||||
this.selection.nodeFront.isTreeDisplayed;
|
||||
|
||||
const menu = new Menu();
|
||||
menu.append(new MenuItem({
|
||||
id: "node-menu-edithtml",
|
||||
label: INSPECTOR_L10N.getStr("inspectorHTMLEdit.label"),
|
||||
accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"),
|
||||
disabled: !isEditableElement,
|
||||
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(isEditableElement),
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
type: "separator",
|
||||
}));
|
||||
|
||||
// Set the pseudo classes
|
||||
for (const name of ["hover", "active", "focus", "focus-within"]) {
|
||||
const menuitem = new MenuItem({
|
||||
id: "node-menu-pseudo-" + name,
|
||||
label: name,
|
||||
type: "checkbox",
|
||||
click: () => this.inspector.togglePseudoClass(":" + name),
|
||||
});
|
||||
|
||||
if (isSelectionElement) {
|
||||
const checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
|
||||
menuitem.checked = checked;
|
||||
} else {
|
||||
menuitem.disabled = true;
|
||||
}
|
||||
|
||||
menu.append(menuitem);
|
||||
}
|
||||
|
||||
menu.append(new MenuItem({
|
||||
type: "separator",
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"),
|
||||
submenu: this._getCopySubmenu(markupContainer, isSelectionElement),
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"),
|
||||
submenu: this._getPasteSubmenu(isEditableElement),
|
||||
}));
|
||||
|
||||
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),
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
type: "separator",
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
id: "node-menu-scrollnodeintoview",
|
||||
label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"),
|
||||
accesskey:
|
||||
INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"),
|
||||
disabled: !isSelectionElement,
|
||||
click: () => this.markup.scrollNodeIntoView(),
|
||||
}));
|
||||
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-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.nodeFront.customElementLocation) {
|
||||
menu.append(new MenuItem({
|
||||
type: "separator",
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem({
|
||||
id: "node-menu-jumptodefinition",
|
||||
label: INSPECTOR_L10N.getStr("inspectorCustomElementDefinition.label"),
|
||||
click: () => this._jumpToCustomElementDefinition(),
|
||||
}));
|
||||
}
|
||||
|
||||
this._buildA11YMenuItem(menu);
|
||||
|
||||
const nodeLinkMenuItems = this._getNodeLinkMenuItems();
|
||||
if (nodeLinkMenuItems.filter(item => item.visible).length > 0) {
|
||||
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);
|
||||
return menu;
|
||||
}
|
||||
|
||||
async _updateA11YMenuItem(menuItem) {
|
||||
const hasMethod = await this.target.actorHasMethod("domwalker",
|
||||
"hasAccessibilityProperties");
|
||||
if (!hasMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasA11YProps = await this.walker.hasAccessibilityProperties(
|
||||
this.selection.nodeFront);
|
||||
if (hasA11YProps) {
|
||||
this.toolbox.doc.getElementById(menuItem.id).disabled = menuItem.disabled = false;
|
||||
}
|
||||
|
||||
this.inspector.emit("node-menu-updated");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MarkupContextMenu;
|
@ -21,9 +21,14 @@ const MarkupReadOnlyContainer = require("devtools/client/inspector/markup/views/
|
||||
const MarkupTextContainer = require("devtools/client/inspector/markup/views/text-container");
|
||||
const RootContainer = require("devtools/client/inspector/markup/views/root-container");
|
||||
|
||||
loader.lazyRequireGetter(this, "MarkupContextMenu", "devtools/client/inspector/markup/markup-context-menu");
|
||||
loader.lazyRequireGetter(this, "SlottedNodeContainer", "devtools/client/inspector/markup/views/slotted-node-container");
|
||||
loader.lazyRequireGetter(this, "copyLongString", "devtools/client/inspector/shared/utils", true);
|
||||
loader.lazyRequireGetter(this, "getLongString", "devtools/client/inspector/shared/utils", true);
|
||||
loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
|
||||
loader.lazyRequireGetter(this, "HTMLTooltip", "devtools/client/shared/widgets/tooltip/HTMLTooltip", true);
|
||||
loader.lazyRequireGetter(this, "UndoStack", "devtools/client/shared/undo", true);
|
||||
loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
|
||||
|
||||
const INSPECTOR_L10N =
|
||||
new LocalizationHelper("devtools/client/locales/inspector.properties");
|
||||
@ -98,6 +103,7 @@ function MarkupView(inspector, frame, controllerWindow) {
|
||||
this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this);
|
||||
this._mutationObserver = this._mutationObserver.bind(this);
|
||||
this._onBlur = this._onBlur.bind(this);
|
||||
this._onContextMenu = this._onContextMenu.bind(this);
|
||||
this._onCopy = this._onCopy.bind(this);
|
||||
this._onCollapseAttributesPrefChange = this._onCollapseAttributesPrefChange.bind(this);
|
||||
this._onWalkerNodeStatesChanged = this._onWalkerNodeStatesChanged.bind(this);
|
||||
@ -113,6 +119,7 @@ function MarkupView(inspector, frame, controllerWindow) {
|
||||
// Listening to various events.
|
||||
this._elt.addEventListener("blur", this._onBlur, true);
|
||||
this._elt.addEventListener("click", this._onMouseClick);
|
||||
this._elt.addEventListener("contextmenu", this._onContextMenu);
|
||||
this._elt.addEventListener("mousemove", this._onMouseMove);
|
||||
this._elt.addEventListener("mouseout", this._onMouseOut);
|
||||
this._frame.addEventListener("focus", this._onFocus);
|
||||
@ -156,6 +163,14 @@ MarkupView.prototype = {
|
||||
|
||||
_selectedContainer: null,
|
||||
|
||||
get contextMenu() {
|
||||
if (!this._contextMenu) {
|
||||
this._contextMenu = new MarkupContextMenu(this);
|
||||
}
|
||||
|
||||
return this._contextMenu;
|
||||
},
|
||||
|
||||
get eventDetailsTooltip() {
|
||||
if (!this._eventDetailsTooltip) {
|
||||
// This tooltip will be attached to the toolbox document.
|
||||
@ -279,6 +294,10 @@ MarkupView.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
_onContextMenu: function(event) {
|
||||
this.contextMenu.show(event);
|
||||
},
|
||||
|
||||
/**
|
||||
* Executed on each mouse-move while a node is being dragged in the view.
|
||||
* Auto-scrolls the view to reveal nodes below the fold to drop the dragged
|
||||
@ -766,12 +785,73 @@ MarkupView.prototype = {
|
||||
|
||||
const selection = this.inspector.selection;
|
||||
if (selection.isNode()) {
|
||||
this.inspector.copyOuterHTML();
|
||||
this.copyOuterHTML();
|
||||
}
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy the outerHTML of the selected Node to the clipboard.
|
||||
*/
|
||||
copyOuterHTML: function() {
|
||||
if (!this.inspector.selection.isNode()) {
|
||||
return;
|
||||
}
|
||||
const node = this.inspector.selection.nodeFront;
|
||||
|
||||
switch (node.nodeType) {
|
||||
case nodeConstants.ELEMENT_NODE :
|
||||
copyLongString(this.walker.outerHTML(node));
|
||||
break;
|
||||
case nodeConstants.COMMENT_NODE :
|
||||
getLongString(node.getNodeValue()).then(comment => {
|
||||
clipboardHelper.copyString("<!--" + comment + "-->");
|
||||
});
|
||||
break;
|
||||
case nodeConstants.DOCUMENT_TYPE_NODE :
|
||||
clipboardHelper.copyString(node.doctypeString);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a type and link found in a node's attribute in the markup-view,
|
||||
* attempt to follow that link (which may result in opening a new tab, the
|
||||
* style editor or debugger).
|
||||
*/
|
||||
followAttributeLink: function(type, link) {
|
||||
if (!type || !link) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "uri" || type === "cssresource" || type === "jsresource") {
|
||||
// Open link in a new tab.
|
||||
this.inspector.inspector.resolveRelativeURL(
|
||||
link, this.inspector.selection.nodeFront).then(url => {
|
||||
if (type === "uri") {
|
||||
openContentLink(url);
|
||||
} else if (type === "cssresource") {
|
||||
return this.toolbox.viewSourceInStyleEditor(url);
|
||||
} else if (type === "jsresource") {
|
||||
return this.toolbox.viewSourceInDebugger(url);
|
||||
}
|
||||
return null;
|
||||
}).catch(console.error);
|
||||
} else if (type == "idref") {
|
||||
// Select the node in the same document.
|
||||
this.walker.document(this.inspector.selection.nodeFront).then(doc => {
|
||||
return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
|
||||
if (!node) {
|
||||
this.emit("idref-attribute-link-failed");
|
||||
return;
|
||||
}
|
||||
this.inspector.selection.setNodeFront(node);
|
||||
});
|
||||
}).catch(console.error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Register all key shortcuts.
|
||||
*/
|
||||
@ -820,8 +900,7 @@ MarkupView.prototype = {
|
||||
break;
|
||||
}
|
||||
case "markupView.scrollInto.key": {
|
||||
const selection = this._selectedContainer.node;
|
||||
this.inspector.scrollNodeIntoView(selection);
|
||||
this.scrollNodeIntoView();
|
||||
break;
|
||||
}
|
||||
// Generic keys
|
||||
@ -960,6 +1039,18 @@ MarkupView.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a value indicating whether a node can be deleted.
|
||||
*
|
||||
* @param {NodeFront} nodeFront
|
||||
* The node to test for deletion
|
||||
*/
|
||||
isDeletable(nodeFront) {
|
||||
return !(nodeFront.isDocumentElement ||
|
||||
nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
|
||||
nodeFront.isAnonymous);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a node from the DOM.
|
||||
* This is an undoable action.
|
||||
@ -970,7 +1061,7 @@ MarkupView.prototype = {
|
||||
* If set to true, focus the previous sibling, otherwise the next one.
|
||||
*/
|
||||
deleteNode: function(node, moveBackward) {
|
||||
if (!this.inspector.isDeletable(node)) {
|
||||
if (!this.isDeletable(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1017,6 +1108,17 @@ MarkupView.prototype = {
|
||||
}).catch(console.error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Scroll the node into view.
|
||||
*/
|
||||
scrollNodeIntoView() {
|
||||
if (!this.inspector.selection.isNode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.inspector.selection.nodeFront.scrollIntoView();
|
||||
},
|
||||
|
||||
/**
|
||||
* If an editable item is focused, select its container.
|
||||
*/
|
||||
@ -1952,6 +2054,7 @@ MarkupView.prototype = {
|
||||
|
||||
this._elt.removeEventListener("blur", this._onBlur, true);
|
||||
this._elt.removeEventListener("click", this._onMouseClick);
|
||||
this._elt.removeEventListener("contextmenu", this._onContextMenu);
|
||||
this._elt.removeEventListener("mousemove", this._onMouseMove);
|
||||
this._elt.removeEventListener("mouseout", this._onMouseOut);
|
||||
this._frame.removeEventListener("focus", this._onFocus);
|
||||
|
@ -9,6 +9,7 @@ DIRS += [
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'markup-context-menu.js',
|
||||
'markup.js',
|
||||
'utils.js',
|
||||
)
|
||||
|
@ -23,7 +23,7 @@ add_task(async function() {
|
||||
|
||||
info("Follow the link and wait for the new tab to open");
|
||||
const onTabOpened = once(gBrowser.tabContainer, "TabOpen");
|
||||
inspector.onFollowLink();
|
||||
inspector.markup.contextMenu._onFollowLink();
|
||||
const {target: tab} = await onTabOpened;
|
||||
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||
|
||||
@ -43,7 +43,7 @@ add_task(async function() {
|
||||
|
||||
info("Follow the link and wait for the new node to be selected");
|
||||
const onSelection = inspector.selection.once("new-node-front");
|
||||
inspector.onFollowLink();
|
||||
inspector.markup.contextMenu._onFollowLink();
|
||||
await onSelection;
|
||||
|
||||
ok(true, "A new node was selected");
|
||||
@ -59,8 +59,8 @@ add_task(async function() {
|
||||
});
|
||||
|
||||
info("Try to follow the link and check that no new node were selected");
|
||||
const onFailed = inspector.once("idref-attribute-link-failed");
|
||||
inspector.onFollowLink();
|
||||
const onFailed = inspector.markup.once("idref-attribute-link-failed");
|
||||
inspector.markup.contextMenu._onFollowLink();
|
||||
await onFailed;
|
||||
|
||||
ok(true, "The node selection failed");
|
||||
|
@ -23,7 +23,7 @@ add_task(async function() {
|
||||
|
||||
info("Follow the link and wait for the style-editor to open");
|
||||
const onStyleEditorReady = toolbox.once("styleeditor-ready");
|
||||
inspector.onFollowLink();
|
||||
inspector.markup.contextMenu._onFollowLink();
|
||||
await onStyleEditorReady;
|
||||
|
||||
// No real need to test that the editor opened on the right file here as this
|
||||
@ -44,7 +44,7 @@ add_task(async function() {
|
||||
|
||||
info("Follow the link and wait for the debugger to open");
|
||||
const onDebuggerReady = toolbox.once("jsdebugger-ready");
|
||||
inspector.onFollowLink();
|
||||
inspector.markup.contextMenu._onFollowLink();
|
||||
await onDebuggerReady;
|
||||
|
||||
// No real need to test that the debugger opened on the right file here as
|
||||
|
@ -99,7 +99,7 @@ async function followLinkWaitForNewNode(linkEl, isMetaClick, inspector) {
|
||||
}
|
||||
|
||||
async function followLinkNoNewNode(linkEl, isMetaClick, inspector) {
|
||||
const onFailed = inspector.once("idref-attribute-link-failed");
|
||||
const onFailed = inspector.markup.once("idref-attribute-link-failed");
|
||||
performMouseDown(linkEl, isMetaClick);
|
||||
await onFailed;
|
||||
|
||||
|
@ -45,9 +45,12 @@ add_task(async function() {
|
||||
await selectNode(divContainer.node, inspector);
|
||||
|
||||
info("Check the copied values for the various copy*Path helpers");
|
||||
await waitForClipboardPromise(() => inspector.copyXPath(), '//*[@id="el1"]');
|
||||
await waitForClipboardPromise(() => inspector.copyCssPath(), "div#el1");
|
||||
await waitForClipboardPromise(() => inspector.copyUniqueSelector(), "#el1");
|
||||
await waitForClipboardPromise(() => inspector.markup.contextMenu._copyXPath(),
|
||||
'//*[@id="el1"]');
|
||||
await waitForClipboardPromise(() => inspector.markup.contextMenu._copyCssPath(),
|
||||
"div#el1");
|
||||
await waitForClipboardPromise(() => inspector.markup.contextMenu._copyUniqueSelector(),
|
||||
"#el1");
|
||||
|
||||
info("Expand the div");
|
||||
await expandContainer(inspector, divContainer);
|
||||
@ -57,8 +60,10 @@ add_task(async function() {
|
||||
await selectNode(spanContainer.node, inspector);
|
||||
|
||||
info("Check the copied values for the various copy*Path helpers");
|
||||
await waitForClipboardPromise(() => inspector.copyXPath(), "/div/span[3]");
|
||||
await waitForClipboardPromise(() => inspector.copyCssPath(), "div#el1 span");
|
||||
await waitForClipboardPromise(() => inspector.copyUniqueSelector(),
|
||||
await waitForClipboardPromise(() => inspector.markup.contextMenu._copyXPath(),
|
||||
"/div/span[3]");
|
||||
await waitForClipboardPromise(() => inspector.markup.contextMenu._copyCssPath(),
|
||||
"div#el1 span");
|
||||
await waitForClipboardPromise(() => inspector.markup.contextMenu._copyUniqueSelector(),
|
||||
"#el1 > span:nth-child(3)");
|
||||
});
|
||||
|
@ -564,7 +564,7 @@ MarkupContainer.prototype = {
|
||||
const type = target.dataset.type;
|
||||
// Make container tabbable descendants not tabbable (by default).
|
||||
this.canFocus = false;
|
||||
this.markup.inspector.followAttributeLink(type, link);
|
||||
this.markup.followAttributeLink(type, link);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ const promise = require("promise");
|
||||
loader.lazyRequireGetter(this, "KeyCodes", "devtools/client/shared/keycodes", true);
|
||||
loader.lazyRequireGetter(this, "getCSSLexer", "devtools/shared/css/lexer", true);
|
||||
loader.lazyRequireGetter(this, "parseDeclarations", "devtools/shared/css/parsing-utils", true);
|
||||
loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
|
||||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
@ -82,6 +83,21 @@ function blurOnMultipleProperties(cssProperties) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the content of a longString (via a promise resolving a
|
||||
* LongStringActor) to the clipboard.
|
||||
*
|
||||
* @param {Promise} longStringActorPromise
|
||||
* promise expected to resolve a LongStringActor instance
|
||||
* @return {Promise} promise resolving (with no argument) when the
|
||||
* string is sent to the clipboard
|
||||
*/
|
||||
function copyLongString(longStringActorPromise) {
|
||||
return getLongString(longStringActorPromise).then(string => {
|
||||
clipboardHelper.copyString(string);
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a child element with a set of attributes.
|
||||
*
|
||||
@ -109,6 +125,22 @@ function createChild(parent, tagName, attributes = {}) {
|
||||
return elt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the content of a longString (via a promise resolving a LongStringActor).
|
||||
*
|
||||
* @param {Promise} longStringActorPromise
|
||||
* promise expected to resolve a LongStringActor instance
|
||||
* @return {Promise} promise resolving with the retrieved string as argument
|
||||
*/
|
||||
function getLongString(longStringActorPromise) {
|
||||
return longStringActorPromise.then(longStringActor => {
|
||||
return longStringActor.string().then(string => {
|
||||
longStringActor.release().catch(console.error);
|
||||
return string;
|
||||
});
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a selector of the Element Rep from the grip. This is based on the
|
||||
* getElements() function in our devtools-reps component for a ElementNode.
|
||||
@ -196,7 +228,9 @@ function translateNodeFrontToGrip(nodeFront) {
|
||||
exports.advanceValidate = advanceValidate;
|
||||
exports.appendText = appendText;
|
||||
exports.blurOnMultipleProperties = blurOnMultipleProperties;
|
||||
exports.copyLongString = copyLongString;
|
||||
exports.createChild = createChild;
|
||||
exports.getLongString = getLongString;
|
||||
exports.getSelectorFromGrip = getSelectorFromGrip;
|
||||
exports.promiseWarn = promiseWarn;
|
||||
exports.translateNodeFrontToGrip = translateNodeFrontToGrip;
|
||||
|
@ -36,7 +36,7 @@ add_task(async function() {
|
||||
const copyAttributeValue = getMenuItem("node-menu-copy-attribute");
|
||||
|
||||
info("Triggering 'Copy Attribute Value' and waiting for clipboard to copy the value");
|
||||
inspector.nodeMenuTriggerInfo = {
|
||||
inspector.markup.contextMenu.nodeMenuTriggerInfo = {
|
||||
type: "attribute",
|
||||
name: "data-edit",
|
||||
value: "the",
|
||||
@ -53,7 +53,7 @@ add_task(async function() {
|
||||
"23456789012345678901234567890123456789012345678901234567890123456789012" +
|
||||
"34567890123456789012345678901234567890123456789012345678901234567890123";
|
||||
|
||||
inspector.nodeMenuTriggerInfo = {
|
||||
inspector.markup.contextMenu.nodeMenuTriggerInfo = {
|
||||
type: "attribute",
|
||||
name: "data-edit",
|
||||
value: longAttribute,
|
||||
@ -67,7 +67,7 @@ add_task(async function() {
|
||||
const editAttribute = getMenuItem("node-menu-edit-attribute");
|
||||
|
||||
info("Triggering 'Edit Attribute' and waiting for mutation to occur");
|
||||
inspector.nodeMenuTriggerInfo = {
|
||||
inspector.markup.contextMenu.nodeMenuTriggerInfo = {
|
||||
type: "attribute",
|
||||
name: "data-edit",
|
||||
};
|
||||
@ -87,7 +87,7 @@ add_task(async function() {
|
||||
const removeAttribute = getMenuItem("node-menu-remove-attribute");
|
||||
|
||||
info("Triggering 'Remove Attribute' and waiting for mutation to occur");
|
||||
inspector.nodeMenuTriggerInfo = {
|
||||
inspector.markup.contextMenu.nodeMenuTriggerInfo = {
|
||||
type: "attribute",
|
||||
name: "data-remove",
|
||||
};
|
||||
|
@ -82,7 +82,7 @@ add_task(async function() {
|
||||
|
||||
async function checkTextBox(textBox, toolbox) {
|
||||
let textboxContextMenu = toolbox.doc.getElementById("toolbox-menu");
|
||||
ok(!textboxContextMenu, "The menu is closed");
|
||||
ok(!textboxContextMenu, "The menu is closed");
|
||||
|
||||
info("Simulating context click on the textbox and expecting the menu to open");
|
||||
const onContextMenu = toolbox.once("menu-open");
|
||||
|
@ -660,6 +660,6 @@ function openStyleContextMenuAndGetAllItems(view, target) {
|
||||
* @return An array of MenuItems
|
||||
*/
|
||||
function openContextMenuAndGetAllItems(inspector, options) {
|
||||
const menu = inspector._openMenu(options);
|
||||
const menu = inspector.markup.contextMenu._openMenu(options);
|
||||
return buildContextMenuItems(menu);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user