summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/utils/context-menu.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/utils/context-menu.js')
-rw-r--r--devtools/client/webconsole/utils/context-menu.js317
1 files changed, 317 insertions, 0 deletions
diff --git a/devtools/client/webconsole/utils/context-menu.js b/devtools/client/webconsole/utils/context-menu.js
new file mode 100644
index 0000000000..e7f41e3697
--- /dev/null
+++ b/devtools/client/webconsole/utils/context-menu.js
@@ -0,0 +1,317 @@
+/* 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 Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
+const { MESSAGE_SOURCE } = require("devtools/client/webconsole/constants");
+
+const clipboardHelper = require("devtools/shared/platform/clipboard");
+const { l10n } = require("devtools/client/webconsole/utils/messages");
+const actions = require("devtools/client/webconsole/actions/index");
+
+loader.lazyRequireGetter(this, "saveAs", "devtools/shared/DevToolsUtils", true);
+loader.lazyRequireGetter(
+ this,
+ "openContentLink",
+ "devtools/client/shared/link",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getElementText",
+ "devtools/client/webconsole/utils/clipboard",
+ true
+);
+
+/**
+ * Create a Menu instance for the webconsole.
+ *
+ * @param {Event} context menu event
+ * {Object} message (optional) message object containing metadata such as:
+ * - {String} source
+ * - {String} request
+ * @param {Object} options
+ * - {Actions} bound actions
+ * - {WebConsoleWrapper} wrapper instance used for accessing properties like the store
+ * and window.
+ */
+function createContextMenu(event, message, webConsoleWrapper) {
+ const { target } = event;
+ const { parentNode, toolbox, hud } = webConsoleWrapper;
+ const store = webConsoleWrapper.getStore();
+ const { dispatch } = store;
+
+ const messageEl = target.closest(".message");
+ const clipboardText = getElementText(messageEl);
+
+ const linkEl = target.closest("a[href]");
+ const url = linkEl && linkEl.href;
+
+ const messageVariable = target.closest(".objectBox");
+ // Ensure that console.group and console.groupCollapsed commands are not captured
+ const variableText =
+ messageVariable &&
+ !messageEl.classList.contains("startGroup") &&
+ !messageEl.classList.contains("startGroupCollapsed")
+ ? messageVariable.textContent
+ : null;
+
+ // Retrieve closes actor id from the DOM.
+ const actorEl =
+ target.closest("[data-link-actor-id]") ||
+ target.querySelector("[data-link-actor-id]");
+ const actor = actorEl ? actorEl.dataset.linkActorId : null;
+
+ const rootObjectInspector = target.closest(".object-inspector");
+ const rootActor = rootObjectInspector
+ ? rootObjectInspector.querySelector("[data-link-actor-id]")
+ : null;
+ // We can have object which are not displayed inside an ObjectInspector (e.g. Errors),
+ // so let's default to `actor`.
+ const rootActorId = rootActor ? rootActor.dataset.linkActorId : actor;
+
+ const elementNode =
+ target.closest(".objectBox-node") || target.closest(".objectBox-textNode");
+ const isConnectedElement =
+ elementNode && elementNode.querySelector(".open-inspector") !== null;
+
+ const win = parentNode.ownerDocument.defaultView;
+ const selection = win.getSelection();
+
+ const { source, request, messageId } = message || {};
+
+ const menu = new Menu({ id: "webconsole-menu" });
+
+ // Copy URL for a network request.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-copy-url",
+ label: l10n.getStr("webconsole.menu.copyURL.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyURL.accesskey"),
+ visible: source === MESSAGE_SOURCE.NETWORK,
+ click: () => {
+ if (!request) {
+ return;
+ }
+ clipboardHelper.copyString(request.url);
+ },
+ })
+ );
+
+ if (toolbox && request) {
+ // Open Network message in the Network panel.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-open-in-network-panel",
+ label: l10n.getStr("webconsole.menu.openInNetworkPanel.label"),
+ accesskey: l10n.getStr("webconsole.menu.openInNetworkPanel.accesskey"),
+ visible: source === MESSAGE_SOURCE.NETWORK,
+ click: () => dispatch(actions.openNetworkPanel(message.messageId)),
+ })
+ );
+ // Resend Network message.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-resend-network-request",
+ label: l10n.getStr("webconsole.menu.resendNetworkRequest.label"),
+ accesskey: l10n.getStr(
+ "webconsole.menu.resendNetworkRequest.accesskey"
+ ),
+ visible: source === MESSAGE_SOURCE.NETWORK,
+ click: () => dispatch(actions.resendNetworkRequest(messageId)),
+ })
+ );
+ }
+
+ // Open URL in a new tab for a network request.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-open-url",
+ label: l10n.getStr("webconsole.menu.openURL.label"),
+ accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"),
+ visible: source === MESSAGE_SOURCE.NETWORK,
+ click: () => {
+ if (!request) {
+ return;
+ }
+ openContentLink(request.url);
+ },
+ })
+ );
+
+ // Open DOM node in the Inspector panel.
+ const contentDomReferenceEl = target.closest(
+ "[data-link-content-dom-reference]"
+ );
+ if (isConnectedElement && contentDomReferenceEl) {
+ const contentDomReference = contentDomReferenceEl.getAttribute(
+ "data-link-content-dom-reference"
+ );
+
+ menu.append(
+ new MenuItem({
+ id: "console-menu-open-node",
+ label: l10n.getStr("webconsole.menu.openNodeInInspector.label"),
+ accesskey: l10n.getStr("webconsole.menu.openNodeInInspector.accesskey"),
+ disabled: false,
+ click: () =>
+ dispatch(
+ actions.openNodeInInspector(JSON.parse(contentDomReference))
+ ),
+ })
+ );
+ }
+
+ // Store as global variable.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-store",
+ label: l10n.getStr("webconsole.menu.storeAsGlobalVar.label"),
+ accesskey: l10n.getStr("webconsole.menu.storeAsGlobalVar.accesskey"),
+ disabled: !actor,
+ click: () => dispatch(actions.storeAsGlobal(actor)),
+ })
+ );
+
+ // Copy message or grip.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-copy",
+ label: l10n.getStr("webconsole.menu.copyMessage.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyMessage.accesskey"),
+ // Disabled if there is no selection and no message element available to copy.
+ disabled: selection.isCollapsed && !clipboardText,
+ click: () => {
+ if (selection.isCollapsed) {
+ // If the selection is empty/collapsed, copy the text content of the
+ // message for which the context menu was opened.
+ clipboardHelper.copyString(clipboardText);
+ } else {
+ clipboardHelper.copyString(selection.toString());
+ }
+ },
+ })
+ );
+
+ // Copy message object.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-copy-object",
+ label: l10n.getStr("webconsole.menu.copyObject.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyObject.accesskey"),
+ // Disabled if there is no actor and no variable text associated.
+ disabled: !actor && !variableText,
+ click: () => dispatch(actions.copyMessageObject(actor, variableText)),
+ })
+ );
+
+ // Select all.
+ menu.append(
+ new MenuItem({
+ id: "console-menu-select",
+ label: l10n.getStr("webconsole.menu.selectAll.label"),
+ accesskey: l10n.getStr("webconsole.menu.selectAll.accesskey"),
+ disabled: false,
+ click: () => {
+ const webconsoleOutput = parentNode.querySelector(".webconsole-output");
+ selection.selectAllChildren(webconsoleOutput);
+ },
+ })
+ );
+
+ const exportSubmenu = new Menu({
+ id: "export-submenu",
+ });
+
+ // Export to clipboard
+ exportSubmenu.append(
+ new MenuItem({
+ id: "console-menu-export-clipboard",
+ label: l10n.getStr("webconsole.menu.exportSubmenu.exportCliboard.label"),
+ disabled: false,
+ click: () => {
+ const webconsoleOutput = parentNode.querySelector(".webconsole-output");
+ clipboardHelper.copyString(getElementText(webconsoleOutput));
+ },
+ })
+ );
+
+ // Export to file
+ exportSubmenu.append(
+ new MenuItem({
+ id: "console-menu-export-file",
+ label: l10n.getStr("webconsole.menu.exportSubmenu.exportFile.label"),
+ disabled: false,
+ // Note: not async, but returns a promise for the actual save.
+ click: () => {
+ const date = new Date();
+ const suggestedName =
+ `console-export-${date.getFullYear()}-` +
+ `${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-` +
+ `${date.getMinutes()}-${date.getSeconds()}.txt`;
+ const webconsoleOutput = parentNode.querySelector(".webconsole-output");
+ const data = new TextEncoder().encode(getElementText(webconsoleOutput));
+ return saveAs(window, data, suggestedName);
+ },
+ })
+ );
+
+ menu.append(
+ new MenuItem({
+ id: "console-menu-export",
+ label: l10n.getStr("webconsole.menu.exportSubmenu.label"),
+ disabled: false,
+ submenu: exportSubmenu,
+ })
+ );
+
+ // Open object in sidebar.
+ const shouldOpenSidebar = store.getState().prefs.sidebarToggle;
+ if (shouldOpenSidebar) {
+ menu.append(
+ new MenuItem({
+ id: "console-menu-open-sidebar",
+ label: l10n.getStr("webconsole.menu.openInSidebar.label1"),
+ accesskey: l10n.getStr("webconsole.menu.openInSidebar.accesskey"),
+ disabled: !rootActorId,
+ click: () => dispatch(actions.openSidebar(messageId, rootActorId)),
+ })
+ );
+ }
+
+ if (url) {
+ menu.append(
+ new MenuItem({
+ id: "console-menu-open-url",
+ label: l10n.getStr("webconsole.menu.openURL.label"),
+ accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"),
+ click: () =>
+ openContentLink(url, {
+ inBackground: true,
+ relatedToCurrent: true,
+ }),
+ })
+ );
+ menu.append(
+ new MenuItem({
+ id: "console-menu-copy-url",
+ label: l10n.getStr("webconsole.menu.copyURL.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyURL.accesskey"),
+ click: () => clipboardHelper.copyString(url),
+ })
+ );
+ }
+
+ // Emit the "menu-open" event for testing.
+ const { screenX, screenY } = event;
+ menu.once("open", () => webConsoleWrapper.emitForTests("menu-open"));
+ menu.popup(screenX, screenY, hud.chromeWindow.document);
+
+ return menu;
+}
+
+exports.createContextMenu = createContextMenu;