Source code

Revision control

Copy as Markdown

Other Tools

/* 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);
}
);
});
}