771 lines
24 KiB
JavaScript
771 lines
24 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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
|
|
const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
|
|
const {
|
|
l10n,
|
|
} = require("resource://devtools/client/webconsole/utils/messages.js");
|
|
|
|
const { BrowserLoader } = ChromeUtils.importESModule(
|
|
"resource://devtools/shared/loader/browser-loader.sys.mjs"
|
|
);
|
|
const {
|
|
getAdHocFrontOrPrimitiveGrip,
|
|
} = require("resource://devtools/client/fronts/object.js");
|
|
|
|
const {
|
|
PREFS,
|
|
FILTERS,
|
|
} = require("resource://devtools/client/webconsole/constants.js");
|
|
|
|
const FirefoxDataProvider = require("resource://devtools/client/netmonitor/src/connector/firefox-data-provider.js");
|
|
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
|
|
});
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"START_IGNORE_ACTION",
|
|
"resource://devtools/client/shared/redux/middleware/ignore.js",
|
|
true
|
|
);
|
|
const ZoomKeys = require("resource://devtools/client/shared/zoom-keys.js");
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"TRACER_LOG_METHODS",
|
|
"resource://devtools/shared/specs/tracer.js",
|
|
true
|
|
);
|
|
|
|
const PREF_SIDEBAR_ENABLED = "devtools.webconsole.sidebarToggle";
|
|
const PREF_BROWSERTOOLBOX_SCOPE = "devtools.browsertoolbox.scope";
|
|
|
|
/**
|
|
* A WebConsoleUI instance is an interactive console initialized *per target*
|
|
* that displays console log data as well as provides an interactive terminal to
|
|
* manipulate the target's document content.
|
|
*
|
|
* The WebConsoleUI is responsible for the actual Web Console UI
|
|
* implementation.
|
|
*/
|
|
class WebConsoleUI {
|
|
/*
|
|
* @param {WebConsole} hud: The WebConsole owner object.
|
|
*/
|
|
constructor(hud) {
|
|
this.hud = hud;
|
|
this.hudId = this.hud.hudId;
|
|
this.isBrowserConsole = this.hud.isBrowserConsole;
|
|
|
|
this.isBrowserToolboxConsole =
|
|
this.hud.commands.descriptorFront.isBrowserProcessDescriptor &&
|
|
!this.isBrowserConsole;
|
|
|
|
this.window = this.hud.iframeWindow;
|
|
|
|
this._onPanelSelected = this._onPanelSelected.bind(this);
|
|
this._onChangeSplitConsoleState =
|
|
this._onChangeSplitConsoleState.bind(this);
|
|
this._onTargetAvailable = this._onTargetAvailable.bind(this);
|
|
this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
|
|
this._onResourceAvailable = this._onResourceAvailable.bind(this);
|
|
this._onNetworkResourceUpdated = this._onNetworkResourceUpdated.bind(this);
|
|
this._onScopePrefChanged = this._onScopePrefChanged.bind(this);
|
|
this._onShowConsoleEvaluation = this._onShowConsoleEvaluation.bind(this);
|
|
|
|
if (this.isBrowserConsole) {
|
|
Services.prefs.addObserver(
|
|
PREF_BROWSERTOOLBOX_SCOPE,
|
|
this._onScopePrefChanged
|
|
);
|
|
}
|
|
|
|
EventEmitter.decorate(this);
|
|
}
|
|
|
|
/**
|
|
* Initialize the WebConsoleUI instance.
|
|
* @return object
|
|
* A promise object that resolves once the frame is ready to use.
|
|
*/
|
|
init() {
|
|
if (this._initializer) {
|
|
return this._initializer;
|
|
}
|
|
|
|
this._initializer = (async () => {
|
|
this._initUI();
|
|
|
|
if (this.isBrowserConsole) {
|
|
// Bug 1605763:
|
|
// TargetCommand.startListening will start fetching additional targets
|
|
// and may overload the Browser Console with loads of targets and resources.
|
|
// We can call it from here, as `_attachTargets` is called after the UI is initialized.
|
|
// Bug 1642599:
|
|
// TargetCommand.startListening has to be called before:
|
|
// - `_attachTargets`, in order to set TargetCommand.watcherFront which is used by ResourceWatcher.watchResources.
|
|
// - `ConsoleCommands`, in order to set TargetCommand.targetFront which is wrapped by hud.currentTarget
|
|
await this.hud.commands.targetCommand.startListening();
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
await this.wrapper.init();
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
// Bug 1605763: It's important to call _attachTargets once the UI is initialized, as
|
|
// it may overload the Browser Console with many updates.
|
|
// It is also important to do it only after the wrapper is initialized,
|
|
// otherwise its `store` will be null while we already call a few dispatch methods
|
|
// from onResourceAvailable
|
|
await this._attachTargets();
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
// `_attachTargets` will process resources and throttle some actions
|
|
// Wait for these actions to be dispatched before reporting that the
|
|
// console is initialized. Otherwise `showToolbox` will resolve before
|
|
// all already existing console messages are displayed.
|
|
await this.wrapper.waitAsyncDispatches();
|
|
this._initNotifications();
|
|
})();
|
|
|
|
return this._initializer;
|
|
}
|
|
|
|
destroy() {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
this._destroyed = true;
|
|
|
|
this.React = this.ReactDOM = this.FrameView = null;
|
|
|
|
if (this.wrapper) {
|
|
this.wrapper.getStore()?.dispatch(START_IGNORE_ACTION);
|
|
this.wrapper.destroy();
|
|
}
|
|
|
|
if (this.jsterm) {
|
|
this.jsterm.destroy();
|
|
this.jsterm = null;
|
|
}
|
|
|
|
const { toolbox } = this.hud;
|
|
if (toolbox) {
|
|
toolbox.off("webconsole-selected", this._onPanelSelected);
|
|
toolbox.off("split-console", this._onChangeSplitConsoleState);
|
|
toolbox.off("select", this._onChangeSplitConsoleState);
|
|
toolbox.off(
|
|
"show-original-variable-mapping-warnings",
|
|
this._onShowConsoleEvaluation
|
|
);
|
|
}
|
|
|
|
if (this.isBrowserConsole) {
|
|
Services.prefs.removeObserver(
|
|
PREF_BROWSERTOOLBOX_SCOPE,
|
|
this._onScopePrefChanged
|
|
);
|
|
}
|
|
|
|
// Stop listening for targets
|
|
this.hud.commands.targetCommand.unwatchTargets({
|
|
types: this.hud.commands.targetCommand.ALL_TYPES,
|
|
onAvailable: this._onTargetAvailable,
|
|
onDestroyed: this._onTargetDestroyed,
|
|
});
|
|
|
|
const resourceCommand = this.hud.resourceCommand;
|
|
if (this._watchedResources) {
|
|
resourceCommand.unwatchResources(this._watchedResources, {
|
|
onAvailable: this._onResourceAvailable,
|
|
});
|
|
}
|
|
|
|
this.stopWatchingNetworkResources();
|
|
|
|
if (this.networkDataProvider) {
|
|
this.networkDataProvider.destroy();
|
|
this.networkDataProvider = null;
|
|
}
|
|
|
|
// Nullify `hud` last as it nullify also target which is used on destroy
|
|
this.window = this.hud = this.wrapper = null;
|
|
}
|
|
|
|
/**
|
|
* Clear the Web Console output.
|
|
*
|
|
* This method emits the "messages-cleared" notification.
|
|
*
|
|
* @param boolean clearStorage
|
|
* True if you want to clear the console messages storage associated to
|
|
* this Web Console.
|
|
* @param object event
|
|
* If the event exists, calls preventDefault on it.
|
|
*/
|
|
async clearOutput(clearStorage, event) {
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
if (this.wrapper) {
|
|
this.wrapper.dispatchMessagesClear();
|
|
}
|
|
|
|
if (clearStorage) {
|
|
await this.clearMessagesCache();
|
|
}
|
|
this.emitForTests("messages-cleared");
|
|
}
|
|
|
|
async clearMessagesCache() {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
// This can be called during console destruction and getAllFronts would reject in such case.
|
|
try {
|
|
const consoleFronts = await this.hud.commands.targetCommand.getAllFronts(
|
|
this.hud.commands.targetCommand.ALL_TYPES,
|
|
"console"
|
|
);
|
|
const promises = [];
|
|
for (const consoleFront of consoleFronts) {
|
|
promises.push(consoleFront.clearMessagesCacheAsync());
|
|
}
|
|
await Promise.all(promises);
|
|
this.emitForTests("messages-cache-cleared");
|
|
} catch (e) {
|
|
console.warn("Exception in clearMessagesCache", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove all of the private messages from the Web Console output.
|
|
*
|
|
* This method emits the "private-messages-cleared" notification.
|
|
*/
|
|
clearPrivateMessages() {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
this.wrapper.dispatchPrivateMessagesClear();
|
|
this.emitForTests("private-messages-cleared");
|
|
}
|
|
|
|
inspectObjectActor(objectActor) {
|
|
const { targetFront } = this.hud.commands.targetCommand;
|
|
this.wrapper.dispatchMessageAdd(
|
|
{
|
|
helperResult: {
|
|
type: "inspectObject",
|
|
object:
|
|
objectActor && objectActor.getGrip
|
|
? objectActor
|
|
: getAdHocFrontOrPrimitiveGrip(objectActor, targetFront),
|
|
},
|
|
},
|
|
true
|
|
);
|
|
return this.wrapper;
|
|
}
|
|
|
|
disableAllNetworkMessages() {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
this.wrapper.dispatchNetworkMessagesDisable();
|
|
}
|
|
|
|
getPanelWindow() {
|
|
return this.window;
|
|
}
|
|
|
|
logWarningAboutReplacedAPI() {
|
|
return this.hud.currentTarget.logWarningInPage(
|
|
l10n.getStr("ConsoleAPIDisabled"),
|
|
"ConsoleAPIDisabled"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Connect to the server using the remote debugging protocol.
|
|
*
|
|
* @private
|
|
* @return object
|
|
* A promise object that is resolved/reject based on the proxies connections.
|
|
*/
|
|
async _attachTargets() {
|
|
const { commands, resourceCommand } = this.hud;
|
|
this.networkDataProvider = new FirefoxDataProvider({
|
|
commands,
|
|
actions: {
|
|
updateRequest: (id, data) =>
|
|
this.wrapper.batchedRequestUpdates({ id, data }),
|
|
},
|
|
owner: this,
|
|
});
|
|
|
|
// Listen for all target types, including:
|
|
// - frames, in order to get the parent process target
|
|
// which is considered as a frame rather than a process.
|
|
// - workers, for similar reason. When we open a toolbox
|
|
// for just a worker, the top level target is a worker target.
|
|
// - processes, as we want to spawn additional proxies for them.
|
|
await commands.targetCommand.watchTargets({
|
|
types: this.hud.commands.targetCommand.ALL_TYPES,
|
|
onAvailable: this._onTargetAvailable,
|
|
onDestroyed: this._onTargetDestroyed,
|
|
});
|
|
|
|
this._watchedResources = [
|
|
resourceCommand.TYPES.CONSOLE_MESSAGE,
|
|
resourceCommand.TYPES.ERROR_MESSAGE,
|
|
resourceCommand.TYPES.PLATFORM_MESSAGE,
|
|
resourceCommand.TYPES.DOCUMENT_EVENT,
|
|
resourceCommand.TYPES.LAST_PRIVATE_CONTEXT_EXIT,
|
|
resourceCommand.TYPES.JSTRACER_TRACE,
|
|
resourceCommand.TYPES.JSTRACER_STATE,
|
|
];
|
|
|
|
// CSS Warnings are only enabled when the user explicitely requested to show them
|
|
// as it can slow down page load.
|
|
const shouldShowCssWarnings = this.wrapper.getFilterState(FILTERS.CSS);
|
|
if (shouldShowCssWarnings) {
|
|
this._watchedResources.push(resourceCommand.TYPES.CSS_MESSAGE);
|
|
}
|
|
|
|
await resourceCommand.watchResources(this._watchedResources, {
|
|
onAvailable: this._onResourceAvailable,
|
|
});
|
|
|
|
if (this.isBrowserConsole || this.isBrowserToolboxConsole) {
|
|
const shouldEnableNetworkMonitoring = Services.prefs.getBoolPref(
|
|
PREFS.UI.ENABLE_NETWORK_MONITORING
|
|
);
|
|
if (shouldEnableNetworkMonitoring) {
|
|
await this.startWatchingNetworkResources();
|
|
} else {
|
|
await this.stopWatchingNetworkResources();
|
|
}
|
|
} else {
|
|
// We should always watch for network resources in the webconsole
|
|
await this.startWatchingNetworkResources();
|
|
}
|
|
}
|
|
|
|
async startWatchingNetworkResources() {
|
|
const { commands, resourceCommand } = this.hud;
|
|
await resourceCommand.watchResources(
|
|
[
|
|
resourceCommand.TYPES.NETWORK_EVENT,
|
|
resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
|
|
],
|
|
{
|
|
onAvailable: this._onResourceAvailable,
|
|
onUpdated: this._onNetworkResourceUpdated,
|
|
}
|
|
);
|
|
|
|
// When opening a worker toolbox from about:debugging,
|
|
// we do not instantiate any Watcher actor yet and would throw here.
|
|
// But even once we do, we wouldn't support network inspection anyway.
|
|
if (commands.targetCommand.hasTargetWatcherSupport()) {
|
|
const networkFront = await commands.watcherFront.getNetworkParentActor();
|
|
// There is no way to view response bodies from the Browser Console, so do
|
|
// not waste the memory.
|
|
const saveBodies =
|
|
!this.isBrowserConsole &&
|
|
Services.prefs.getBoolPref(
|
|
"devtools.netmonitor.saveRequestAndResponseBodies"
|
|
);
|
|
await networkFront.setSaveRequestAndResponseBodies(saveBodies);
|
|
}
|
|
}
|
|
|
|
async stopWatchingNetworkResources() {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
await this.hud.resourceCommand.unwatchResources(
|
|
[
|
|
this.hud.resourceCommand.TYPES.NETWORK_EVENT,
|
|
this.hud.resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
|
|
],
|
|
{
|
|
onAvailable: this._onResourceAvailable,
|
|
onUpdated: this._onNetworkResourceUpdated,
|
|
}
|
|
);
|
|
}
|
|
|
|
handleDocumentEvent(resource) {
|
|
// Only consider top level document, and ignore remote iframes top document
|
|
if (!resource.targetFront.isTopLevel) {
|
|
return;
|
|
}
|
|
|
|
if (resource.name == "will-navigate") {
|
|
this.handleWillNavigate({
|
|
timeStamp: resource.time,
|
|
url: resource.newURI,
|
|
});
|
|
} else if (resource.name == "dom-complete") {
|
|
this.handleNavigated({
|
|
hasNativeConsoleAPI: resource.hasNativeConsoleAPI,
|
|
});
|
|
}
|
|
// For now, ignore all other DOCUMENT_EVENT's.
|
|
}
|
|
|
|
/**
|
|
* Handler for when the page is done loading.
|
|
*
|
|
* @param Boolean hasNativeConsoleAPI
|
|
* True if the `console` object is the native one and hasn't been overloaded by a custom
|
|
* object by the page itself.
|
|
*/
|
|
async handleNavigated({ hasNativeConsoleAPI }) {
|
|
// Updates instant evaluation on page navigation
|
|
this.wrapper.dispatchUpdateInstantEvaluationResultForCurrentExpression();
|
|
|
|
// Wait for completion of any async dispatch before notifying that the console
|
|
// is fully updated after a page reload
|
|
await this.wrapper.waitAsyncDispatches();
|
|
|
|
if (!hasNativeConsoleAPI) {
|
|
this.logWarningAboutReplacedAPI();
|
|
}
|
|
|
|
this.emit("reloaded");
|
|
}
|
|
|
|
handleWillNavigate({ timeStamp, url }) {
|
|
this.wrapper.dispatchTabWillNavigate({ timeStamp, url });
|
|
}
|
|
|
|
/**
|
|
* Called when the CSS Warning filter is enabled, in order to start observing for them in the backend.
|
|
*/
|
|
async watchCssMessages() {
|
|
const { resourceCommand } = this.hud;
|
|
if (this._watchedResources.includes(resourceCommand.TYPES.CSS_MESSAGE)) {
|
|
return;
|
|
}
|
|
await resourceCommand.watchResources([resourceCommand.TYPES.CSS_MESSAGE], {
|
|
onAvailable: this._onResourceAvailable,
|
|
});
|
|
this._watchedResources.push(resourceCommand.TYPES.CSS_MESSAGE);
|
|
}
|
|
|
|
// eslint-disable-next-line complexity
|
|
_onResourceAvailable(resources) {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
const { logMethod } = this.hud.commands.tracerCommand.getTracingOptions();
|
|
|
|
const messages = [];
|
|
for (const resource of resources) {
|
|
const { TYPES } = this.hud.resourceCommand;
|
|
if (resource.resourceType === TYPES.DOCUMENT_EVENT) {
|
|
this.handleDocumentEvent(resource);
|
|
continue;
|
|
}
|
|
if (resource.resourceType == TYPES.LAST_PRIVATE_CONTEXT_EXIT) {
|
|
// Private messages only need to be removed from the output in Browser Console/Browser Toolbox
|
|
// (but in theory this resource should only be send from parent process watchers)
|
|
if (this.isBrowserConsole || this.isBrowserToolboxConsole) {
|
|
this.clearPrivateMessages();
|
|
}
|
|
continue;
|
|
}
|
|
// Ignore messages forwarded from content processes if we're in fission browser toolbox.
|
|
if (
|
|
!this.wrapper ||
|
|
((resource.resourceType === TYPES.ERROR_MESSAGE ||
|
|
resource.resourceType === TYPES.CSS_MESSAGE) &&
|
|
resource.pageError?.isForwardedFromContentProcess &&
|
|
(this.isBrowserToolboxConsole || this.isBrowserConsole))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Don't show messages emitted from a private window before the Browser Console was
|
|
// opened to avoid leaking data from past usage of the browser (e.g. content message
|
|
// from now closed private tabs)
|
|
if (
|
|
(this.isBrowserToolboxConsole || this.isBrowserConsole) &&
|
|
resource.isAlreadyExistingResource &&
|
|
(resource.pageError?.private || resource.private)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
resource.resourceType === TYPES.JSTRACER_TRACE &&
|
|
logMethod != TRACER_LOG_METHODS.CONSOLE
|
|
) {
|
|
continue;
|
|
}
|
|
if (resource.resourceType === TYPES.NETWORK_EVENT_STACKTRACE) {
|
|
this.networkDataProvider?.onStackTraceAvailable(resource);
|
|
continue;
|
|
}
|
|
|
|
if (resource.resourceType === TYPES.NETWORK_EVENT) {
|
|
this.networkDataProvider?.onNetworkResourceAvailable(resource);
|
|
}
|
|
messages.push(resource);
|
|
}
|
|
this.wrapper.dispatchMessagesAdd(messages);
|
|
}
|
|
|
|
_onNetworkResourceUpdated(updates) {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
const messageUpdates = [];
|
|
for (const { resource } of updates) {
|
|
if (
|
|
resource.resourceType == this.hud.resourceCommand.TYPES.NETWORK_EVENT
|
|
) {
|
|
this.networkDataProvider?.onNetworkResourceUpdated(resource);
|
|
messageUpdates.push(resource);
|
|
}
|
|
}
|
|
this.wrapper.dispatchMessagesUpdate(messageUpdates);
|
|
}
|
|
|
|
/**
|
|
* Called any time a new target is available.
|
|
* i.e. it was already existing or has just been created.
|
|
*
|
|
* @private
|
|
*/
|
|
async _onTargetAvailable() {
|
|
// onTargetAvailable is a mandatory argument for watchTargets,
|
|
// we still define it solely for being able to use onTargetDestroyed.
|
|
}
|
|
|
|
_onTargetDestroyed({ targetFront, isModeSwitching }) {
|
|
// Don't try to do anything if the WebConsole is being destroyed
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
// We only want to remove messages from a target destroyed when we're switching mode
|
|
// in the Browser Console/Browser Toolbox Console.
|
|
// For regular cases, we want to keep the message history (the output will still be
|
|
// cleared when the top level target navigates, if "Persist Logs" isn't true, via handleWillNavigate)
|
|
if (isModeSwitching) {
|
|
this.wrapper.dispatchTargetMessagesRemove(targetFront);
|
|
}
|
|
}
|
|
|
|
_initUI() {
|
|
this.document = this.window.document;
|
|
this.rootElement = this.document.documentElement;
|
|
|
|
this.outputNode = this.document.getElementById("app-wrapper");
|
|
|
|
const { toolbox } = this.hud;
|
|
|
|
// Initialize module loader and load all the WebConsoleWrapper. The entire code-base
|
|
// doesn't need any extra privileges and runs entirely in content scope.
|
|
const browserLoader = BrowserLoader({
|
|
baseURI: "resource://devtools/client/webconsole/",
|
|
window: this.window,
|
|
});
|
|
// Expose `require` for the CustomFormatter ESM in order to allow it to load
|
|
// ObjectInspector, which are still CommonJS modules, via the same BrowserLoader instance.
|
|
this.window.browserLoaderRequire = browserLoader.require;
|
|
const WebConsoleWrapper = browserLoader.require(
|
|
"resource://devtools/client/webconsole/webconsole-wrapper.js"
|
|
);
|
|
|
|
this.wrapper = new WebConsoleWrapper(
|
|
this.outputNode,
|
|
this,
|
|
toolbox,
|
|
this.document
|
|
);
|
|
|
|
this._initShortcuts();
|
|
this._initOutputSyntaxHighlighting();
|
|
|
|
if (toolbox) {
|
|
toolbox.on("webconsole-selected", this._onPanelSelected);
|
|
toolbox.on("split-console", this._onChangeSplitConsoleState);
|
|
toolbox.on("select", this._onChangeSplitConsoleState);
|
|
}
|
|
}
|
|
|
|
_initOutputSyntaxHighlighting() {
|
|
// Given a DOM node, we syntax highlight identically to how the input field
|
|
// looks. See https://codemirror.net/demo/runmode.html;
|
|
const syntaxHighlightNode = node => {
|
|
const editor = this.jsterm && this.jsterm.editor;
|
|
if (node && editor) {
|
|
node.classList.add("cm-s-mozilla");
|
|
editor.CodeMirror.runMode(
|
|
node.textContent,
|
|
"application/javascript",
|
|
node
|
|
);
|
|
}
|
|
};
|
|
|
|
// Use a Custom Element to handle syntax highlighting to avoid
|
|
// dealing with refs or innerHTML from React.
|
|
const win = this.window;
|
|
win.customElements.define(
|
|
"syntax-highlighted",
|
|
class extends win.HTMLElement {
|
|
connectedCallback() {
|
|
if (!this.connected) {
|
|
this.connected = true;
|
|
syntaxHighlightNode(this);
|
|
|
|
// Highlight Again when the innerText changes
|
|
// We remove the listener before running codemirror mode and add
|
|
// it again to capture text changes
|
|
this.observer = new win.MutationObserver((mutations, observer) => {
|
|
observer.disconnect();
|
|
syntaxHighlightNode(this);
|
|
observer.observe(this, { childList: true });
|
|
});
|
|
|
|
this.observer.observe(this, { childList: true });
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
_initNotifications() {
|
|
if (this.hud.toolbox) {
|
|
this.wrapper.toggleOriginalVariableMappingEvaluationNotification(
|
|
!!this.hud.toolbox
|
|
.getPanel("jsdebugger")
|
|
?.shouldShowOriginalVariableMappingWarnings()
|
|
);
|
|
this.hud.toolbox.on(
|
|
"show-original-variable-mapping-warnings",
|
|
this._onShowConsoleEvaluation
|
|
);
|
|
}
|
|
}
|
|
|
|
_initShortcuts() {
|
|
const shortcuts = new KeyShortcuts({
|
|
window: this.window,
|
|
});
|
|
|
|
let clearShortcut;
|
|
if (lazy.AppConstants.platform === "macosx") {
|
|
const alternativaClearShortcut = l10n.getStr(
|
|
"webconsole.clear.alternativeKeyOSX"
|
|
);
|
|
shortcuts.on(alternativaClearShortcut, event =>
|
|
this.clearOutput(true, event)
|
|
);
|
|
clearShortcut = l10n.getStr("webconsole.clear.keyOSX");
|
|
} else {
|
|
clearShortcut = l10n.getStr("webconsole.clear.key");
|
|
}
|
|
|
|
shortcuts.on(clearShortcut, event => this.clearOutput(true, event));
|
|
|
|
if (this.isBrowserConsole) {
|
|
// Make sure keyboard shortcuts work immediately after opening
|
|
// the Browser Console (Bug 1461366).
|
|
this.window.focus();
|
|
shortcuts.on(
|
|
l10n.getStr("webconsole.close.key"),
|
|
this.window.close.bind(this.window)
|
|
);
|
|
|
|
ZoomKeys.register(this.window, shortcuts);
|
|
|
|
/* This is the same as DevelopmentHelpers.quickRestart, but it runs in all
|
|
* builds (even official). This allows a user to do a restart + session restore
|
|
* with Ctrl+Shift+J (open Browser Console) and then Ctrl+Alt+R (restart).
|
|
*/
|
|
shortcuts.on("CmdOrCtrl+Alt+R", () => {
|
|
this.hud.commands.targetCommand.reloadTopLevelTarget();
|
|
});
|
|
} else if (Services.prefs.getBoolPref(PREF_SIDEBAR_ENABLED)) {
|
|
shortcuts.on("Esc", () => {
|
|
this.wrapper.dispatchSidebarClose();
|
|
if (this.jsterm) {
|
|
this.jsterm.focus();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the focus to JavaScript input field when the web console tab is
|
|
* selected or when there is a split console present.
|
|
* @private
|
|
*/
|
|
_onPanelSelected() {
|
|
// We can only focus when we have the jsterm reference. This is fine because if the
|
|
// jsterm is not mounted yet, it will be focused in JSTerm's componentDidMount.
|
|
if (this.jsterm) {
|
|
this.jsterm.focus();
|
|
}
|
|
}
|
|
|
|
_onChangeSplitConsoleState() {
|
|
this.wrapper.dispatchSplitConsoleCloseButtonToggle();
|
|
}
|
|
|
|
_onScopePrefChanged() {
|
|
if (this.isBrowserConsole) {
|
|
this.hud.updateWindowTitle();
|
|
}
|
|
}
|
|
|
|
_onShowConsoleEvaluation(isOriginalVariableMappingEnabled) {
|
|
this.wrapper.toggleOriginalVariableMappingEvaluationNotification(
|
|
isOriginalVariableMappingEnabled
|
|
);
|
|
}
|
|
|
|
getInputCursor() {
|
|
return this.jsterm && this.jsterm.getSelectionStart();
|
|
}
|
|
|
|
getJsTermTooltipAnchor() {
|
|
return this.outputNode.querySelector(".CodeMirror-cursor");
|
|
}
|
|
|
|
attachRef(id, node) {
|
|
this[id] = node;
|
|
}
|
|
|
|
getSelectedNodeActorID() {
|
|
const inspectorSelection = this.hud.getInspectorSelection();
|
|
return inspectorSelection?.nodeFront?.actorID;
|
|
}
|
|
}
|
|
|
|
exports.WebConsoleUI = WebConsoleUI;
|