366 lines
12 KiB
JavaScript
366 lines
12 KiB
JavaScript
/* 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.mjs");
|
|
const {
|
|
createElement,
|
|
createFactory,
|
|
} = require("resource://devtools/client/shared/vendor/react.mjs");
|
|
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);
|
|
}
|
|
);
|
|
});
|
|
}
|