diff options
Diffstat (limited to 'devtools/client/webconsole/utils/context-menu.js')
-rw-r--r-- | devtools/client/webconsole/utils/context-menu.js | 368 |
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); + } + ); + }); +} |