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.js368
1 files changed, 368 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..7996194d56
--- /dev/null
+++ b/devtools/client/webconsole/utils/context-menu.js
@@ -0,0 +1,368 @@
+/* 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("resource://devtools/client/framework/menu.js");
+const MenuItem = require("resource://devtools/client/framework/menu-item.js");
+
+const {
+ MESSAGE_SOURCE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const clipboardHelper = require("resource://devtools/shared/platform/clipboard.js");
+const {
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const actions = require("resource://devtools/client/webconsole/actions/index.js");
+
+loader.lazyRequireGetter(
+ this,
+ "saveAs",
+ "resource://devtools/shared/DevToolsUtils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "openContentLink",
+ "resource://devtools/client/shared/link.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getElementText",
+ "resource://devtools/client/webconsole/utils/clipboard.js",
+ 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)),
+ })
+ );
+
+ // Export to clipboard
+ menu.append(
+ new MenuItem({
+ id: "console-menu-export-clipboard",
+ label: l10n.getStr("webconsole.menu.copyAllMessages.label"),
+ accesskey: l10n.getStr("webconsole.menu.copyAllMessages.accesskey"),
+ disabled: false,
+ async click() {
+ const outputText = await getUnvirtualizedConsoleOutputText(
+ webConsoleWrapper
+ );
+ clipboardHelper.copyString(outputText);
+ },
+ })
+ );
+
+ // Export to file
+ menu.append(
+ new MenuItem({
+ id: "console-menu-export-file",
+ label: l10n.getStr("webconsole.menu.saveAllMessagesFile.label"),
+ accesskey: l10n.getStr("webconsole.menu.saveAllMessagesFile.accesskey"),
+ disabled: false,
+ // Note: not async, but returns a promise for the actual save.
+ click: async () => {
+ const date = new Date();
+ const suggestedName =
+ `console-export-${date.getFullYear()}-` +
+ `${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-` +
+ `${date.getMinutes()}-${date.getSeconds()}.txt`;
+ const outputText = await getUnvirtualizedConsoleOutputText(
+ webConsoleWrapper
+ );
+ const data = new TextEncoder().encode(outputText);
+ saveAs(window, data, suggestedName);
+ },
+ })
+ );
+
+ // 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;
+
+/**
+ * Returns the whole text content of the console output.
+ * We're creating a new ConsoleOutput using the current store, turning off virtualization
+ * so we can have access to all the messages.
+ *
+ * @param {WebConsoleWrapper} webConsoleWrapper
+ * @returns Promise<String>
+ */
+async function getUnvirtualizedConsoleOutputText(webConsoleWrapper) {
+ return new Promise(resolve => {
+ const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js");
+ const {
+ createElement,
+ createFactory,
+ } = require("resource://devtools/client/shared/vendor/react.js");
+ const ConsoleOutput = createFactory(
+ require("resource://devtools/client/webconsole/components/Output/ConsoleOutput.js")
+ );
+ const {
+ Provider,
+ createProvider,
+ } = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+ const { parentNode, toolbox } = webConsoleWrapper;
+ const doc = parentNode.ownerDocument;
+
+ // Create an element that won't impact the layout of the console
+ const singleUseElement = doc.createElement("section");
+ singleUseElement.classList.add("clipboard-only");
+ doc.body.append(singleUseElement);
+
+ const consoleOutput = ConsoleOutput({
+ serviceContainer: {
+ ...webConsoleWrapper.getServiceContainer(),
+ preventStacktraceInitialRenderDelay: true,
+ },
+ disableVirtualization: true,
+ });
+
+ ReactDOM.render(
+ createElement(
+ Provider,
+ {
+ store: webConsoleWrapper.getStore(),
+ },
+ toolbox
+ ? createElement(
+ createProvider(toolbox.commands.targetCommand.storeId),
+ { store: toolbox.commands.targetCommand.store },
+ consoleOutput
+ )
+ : consoleOutput
+ ),
+ singleUseElement,
+ () => {
+ resolve(getElementText(singleUseElement));
+ singleUseElement.remove();
+ ReactDOM.unmountComponentAtNode(singleUseElement);
+ }
+ );
+ });
+}