/* 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 { gDevTools, } = require("resource://devtools/client/framework/devtools.js"); const { getFormattedSize, } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); const l10n = new Localization(["devtools/client/toolbox-options.ftl"], true); loader.lazyRequireGetter( this, "openDocLink", "resource://devtools/client/shared/link.js", true ); loader.lazyRequireGetter( this, "findCssSelector", "resource://devtools/shared/inspector/css-logic.js", true ); const lazy = {}; ChromeUtils.defineLazyGetter(lazy, "LocalFile", () => Components.Constructor("@mozilla.org/file/local;1", "nsIFile", "initWithPath") ); ChromeUtils.defineESModuleGetters(lazy, { FileUtils: "resource://gre/modules/FileUtils.sys.mjs", LocalModeMappings: "resource://devtools/client/framework/LocalModeMappings.sys.mjs", }); const NETMONITOR_BODY_LIMIT_PREF = "devtools.netmonitor.bodyLimit"; function GetPref(name) { const type = Services.prefs.getPrefType(name); switch (type) { case Services.prefs.PREF_STRING: return Services.prefs.getCharPref(name); case Services.prefs.PREF_INT: return Services.prefs.getIntPref(name); case Services.prefs.PREF_BOOL: return Services.prefs.getBoolPref(name); default: throw new Error("Unknown type"); } } function SetPref(name, value) { const type = Services.prefs.getPrefType(name); switch (type) { case Services.prefs.PREF_STRING: return Services.prefs.setCharPref(name, value); case Services.prefs.PREF_INT: return Services.prefs.setIntPref(name, value); case Services.prefs.PREF_BOOL: return Services.prefs.setBoolPref(name, value); default: throw new Error("Unknown type"); } } function InfallibleGetBoolPref(key) { try { return Services.prefs.getBoolPref(key); } catch (ex) { return true; } } /** * Represents the Options Panel in the Toolbox. */ class OptionsPanel extends EventEmitter { constructor(iframeWindow, toolbox, commands) { super(); this.panelDoc = iframeWindow.document; this.panelWin = iframeWindow; this.toolbox = toolbox; this.commands = commands; this.telemetry = toolbox.telemetry; this.setupToolsList = this.setupToolsList.bind(this); this.disableJSNode = this.panelDoc.getElementById( "devtools-disable-javascript" ); this.#addListeners(); } get target() { return this.toolbox.target; } async open() { this.setupToolsList(); this.setupToolbarButtonsList(); this.setupThemeList(); this.setupAdditionalOptions(); await this.populatePreferences(); this.#setupLocalMode(); this.setupNetworkBodySizeLimit(); return this; } #observedPreferences = [ "devtools.cache.disabled", "devtools.theme", "devtools.source-map.client-service.enabled", "devtools.toolbox.splitconsole.enabled", NETMONITOR_BODY_LIMIT_PREF, ]; #addListeners() { for (const prefName of this.#observedPreferences) { Services.prefs.addObserver(prefName, this.#prefChanged); } gDevTools.on("theme-registered", this.#themeRegistered); gDevTools.on("theme-unregistered", this.#themeUnregistered); // Refresh the tools list when a new tool or webextension has been // registered to the toolbox. this.toolbox.on("tool-registered", this.setupToolsList); this.toolbox.on("webextension-registered", this.setupToolsList); // Refresh the tools list when a new tool or webextension has been // unregistered from the toolbox. this.toolbox.on("tool-unregistered", this.setupToolsList); this.toolbox.on("webextension-unregistered", this.setupToolsList); lazy.LocalModeMappings.on("updated", this.#updateLocalModeMappings); } #removeListeners() { for (const prefName of this.#observedPreferences) { Services.prefs.removeObserver(prefName, this.#prefChanged); } this.toolbox.off("tool-registered", this.setupToolsList); this.toolbox.off("tool-unregistered", this.setupToolsList); this.toolbox.off("webextension-registered", this.setupToolsList); this.toolbox.off("webextension-unregistered", this.setupToolsList); lazy.LocalModeMappings.off("updated", this.#updateLocalModeMappings); gDevTools.off("theme-registered", this.#themeRegistered); gDevTools.off("theme-unregistered", this.#themeUnregistered); } #prefChanged = (subject, topic, prefName) => { if (prefName === "devtools.cache.disabled") { const cacheDisabled = GetPref(prefName); const cbx = this.panelDoc.getElementById("devtools-disable-cache"); cbx.checked = cacheDisabled; } else if (prefName === "devtools.theme") { this.updateCurrentTheme(); } else if (prefName === "devtools.source-map.client-service.enabled") { this.updateSourceMapPref(); } else if (prefName === "devtools.toolbox.splitconsole.enabled") { this.toolbox.updateIsSplitConsoleEnabled(); } else if (prefName === NETMONITOR_BODY_LIMIT_PREF) { this.updateNetworkBodySizeLimit(); } }; #themeRegistered = () => { this.setupThemeList(); }; #themeUnregistered = theme => { const themeBox = this.panelDoc.getElementById("devtools-theme-box"); const themeInput = themeBox.querySelector(`[value=${theme.id}]`); if (themeInput) { themeInput.parentNode.remove(); } }; async setupToolbarButtonsList() { // Ensure the toolbox is open, and the buttons are all set up. await this.toolbox.isOpen; const enabledToolbarButtonsBox = this.panelDoc.getElementById( "enabled-toolbox-buttons-box" ); const toolbarButtons = this.toolbox.toolbarButtons; if (!toolbarButtons) { console.warn("The command buttons weren't initiated yet."); return; } const onCheckboxClick = checkbox => { const commandButton = toolbarButtons.filter( toggleableButton => toggleableButton.id === checkbox.id )[0]; Services.prefs.setBoolPref( commandButton.visibilityswitch, checkbox.checked ); this.toolbox.updateToolboxButtonsVisibility(); }; const createCommandCheckbox = button => { const checkboxLabel = this.panelDoc.createElement("label"); const checkboxSpanLabel = this.panelDoc.createElement("span"); checkboxSpanLabel.textContent = button.description; const checkboxInput = this.panelDoc.createElement("input"); checkboxInput.setAttribute("type", "checkbox"); checkboxInput.setAttribute("id", button.id); if (Services.prefs.getBoolPref(button.visibilityswitch, true)) { checkboxInput.setAttribute("checked", true); } checkboxInput.addEventListener( "change", onCheckboxClick.bind(this, checkboxInput) ); checkboxLabel.appendChild(checkboxInput); checkboxLabel.appendChild(checkboxSpanLabel); return checkboxLabel; }; for (const button of toolbarButtons) { if (!button.isToolSupported(this.toolbox)) { continue; } enabledToolbarButtonsBox.appendChild(createCommandCheckbox(button)); } } setupToolsList() { const defaultToolsBox = this.panelDoc.getElementById("default-tools-box"); const additionalToolsBox = this.panelDoc.getElementById( "additional-tools-box" ); const toolsNotSupportedLabel = this.panelDoc.getElementById( "tools-not-supported-label" ); let atleastOneToolNotSupported = false; // Signal tool registering/unregistering globally (for the tools registered // globally) and per toolbox (for the tools registered to a single toolbox). // This event handler expect this to be binded to the related checkbox element. const onCheckboxClick = function (telemetry, tool) { // Set the kill switch pref boolean to true Services.prefs.setBoolPref(tool.visibilityswitch, this.checked); if (!tool.isWebExtension) { gDevTools.emit( this.checked ? "tool-registered" : "tool-unregistered", tool.id ); // Record which tools were registered and unregistered. Glean.devtoolsTool.registered[tool.id].set(this.checked); } }; const createToolCheckbox = tool => { const checkboxLabel = this.panelDoc.createElement("label"); const checkboxInput = this.panelDoc.createElement("input"); checkboxInput.setAttribute("type", "checkbox"); checkboxInput.setAttribute("id", tool.id); checkboxInput.setAttribute("title", tool.tooltip || ""); const checkboxSpanLabel = this.panelDoc.createElement("span"); if (tool.isToolSupported(this.toolbox)) { checkboxSpanLabel.textContent = tool.label; } else { atleastOneToolNotSupported = true; checkboxSpanLabel.textContent = l10n.formatValueSync( "options-tool-not-supported-marker", { toolLabel: tool.label } ); checkboxInput.setAttribute("data-unsupported", "true"); checkboxInput.setAttribute("disabled", "true"); } if (InfallibleGetBoolPref(tool.visibilityswitch)) { checkboxInput.setAttribute("checked", "true"); } checkboxInput.addEventListener( "change", onCheckboxClick.bind(checkboxInput, this.telemetry, tool) ); checkboxLabel.appendChild(checkboxInput); checkboxLabel.appendChild(checkboxSpanLabel); // We shouldn't have deprecated tools anymore, but we might have one in the future, // when migrating the storage inspector to the application panel (Bug 1681059). // Let's keep this code for now so we keep the l10n property around and avoid // unnecessary translation work if we need it again in the future. if (tool.deprecated) { const deprecationURL = this.panelDoc.createElement("a"); deprecationURL.title = deprecationURL.href = tool.deprecationURL; deprecationURL.textContent = l10n.formatValueSync( "options-deprecation-notice" ); // Cannot use a real link when we are in the Browser Toolbox. deprecationURL.addEventListener("click", e => { e.preventDefault(); openDocLink(tool.deprecationURL, { relatedToCurrent: true }); }); const checkboxSpanDeprecated = this.panelDoc.createElement("span"); checkboxSpanDeprecated.className = "deprecation-notice"; checkboxLabel.appendChild(checkboxSpanDeprecated); checkboxSpanDeprecated.appendChild(deprecationURL); } return checkboxLabel; }; // Clean up any existent default tools content. for (const label of defaultToolsBox.querySelectorAll("label")) { label.remove(); } // Populating the default tools lists const toggleableTools = gDevTools.getDefaultTools().filter(tool => { return tool.visibilityswitch && !tool.hiddenInOptions; }); const fragment = this.panelDoc.createDocumentFragment(); for (const tool of toggleableTools) { fragment.appendChild(createToolCheckbox(tool)); } const toolsNotSupportedLabelNode = this.panelDoc.getElementById( "tools-not-supported-label" ); defaultToolsBox.insertBefore(fragment, toolsNotSupportedLabelNode); // Clean up any existent additional tools content. for (const label of additionalToolsBox.querySelectorAll("label")) { label.remove(); } // Populating the additional tools list. let atleastOneAddon = false; for (const tool of gDevTools.getAdditionalTools()) { atleastOneAddon = true; additionalToolsBox.appendChild(createToolCheckbox(tool)); } // Populating the additional tools that came from the installed WebExtension add-ons. for (const { uuid, name, pref } of this.toolbox.listWebExtensions()) { atleastOneAddon = true; additionalToolsBox.appendChild( createToolCheckbox({ isWebExtension: true, // Use the preference as the unified webextensions tool id. id: `webext-${uuid}`, tooltip: name, label: name, // Disable the devtools extension using the given pref name: // the toolbox options for the WebExtensions are not related to a single // tool (e.g. a devtools panel created from the extension devtools_page) // but to the entire devtools part of a webextension which is enabled // by the Addon Manager (but it may be disabled by its related // devtools about:config preference), and so the following visibilityswitch: pref, // Only local tabs are currently supported as targets. isToolSupported: toolbox => toolbox.commands.descriptorFront.isLocalTab, }) ); } if (!atleastOneAddon) { additionalToolsBox.style.display = "none"; } else { additionalToolsBox.style.display = ""; } if (!atleastOneToolNotSupported) { toolsNotSupportedLabel.style.display = "none"; } else { toolsNotSupportedLabel.style.display = ""; } this.panelWin.focus(); } setupThemeList() { const themeBox = this.panelDoc.getElementById("devtools-theme-box"); const themeLabels = themeBox.querySelectorAll("label"); for (const label of themeLabels) { label.remove(); } const createThemeOption = theme => { const inputLabel = this.panelDoc.createElement("label"); const inputRadio = this.panelDoc.createElement("input"); inputRadio.setAttribute("type", "radio"); inputRadio.setAttribute("value", theme.id); inputRadio.setAttribute("name", "devtools-theme-item"); inputRadio.addEventListener("change", function (e) { SetPref(themeBox.getAttribute("data-pref"), e.target.value); }); const inputSpanLabel = this.panelDoc.createElement("span"); inputSpanLabel.textContent = theme.label; inputLabel.appendChild(inputRadio); inputLabel.appendChild(inputSpanLabel); return inputLabel; }; // Populating the default theme list themeBox.appendChild( createThemeOption({ id: "auto", label: l10n.formatValueSync("options-auto-theme-label"), }) ); const themes = gDevTools.getThemeDefinitionArray(); for (const theme of themes) { themeBox.appendChild(createThemeOption(theme)); } this.updateCurrentTheme(); } /** * Add extra checkbox options bound to a boolean preference. */ setupAdditionalOptions() { const prefDefinitions = [ { pref: "devtools.custom-formatters.enabled", l10nLabelId: "options-enable-custom-formatters-label", l10nTooltipId: "options-enable-custom-formatters-tooltip", id: "devtools-custom-formatters", parentId: "context-options", }, ]; const createPreferenceOption = ({ pref, label, l10nLabelId, l10nTooltipId, id, onChange, }) => { const inputLabel = this.panelDoc.createElement("label"); if (l10nTooltipId) { this.panelDoc.l10n.setAttributes(inputLabel, l10nTooltipId); } const checkbox = this.panelDoc.createElement("input"); checkbox.setAttribute("type", "checkbox"); if (GetPref(pref)) { checkbox.setAttribute("checked", "checked"); } checkbox.setAttribute("id", id); checkbox.addEventListener("change", e => { SetPref(pref, e.target.checked); if (onChange) { onChange(e.target.checked); } }); const inputSpanLabel = this.panelDoc.createElement("span"); if (l10nLabelId) { this.panelDoc.l10n.setAttributes(inputSpanLabel, l10nLabelId); } else if (label) { inputSpanLabel.textContent = label; } inputLabel.appendChild(checkbox); inputLabel.appendChild(inputSpanLabel); return inputLabel; }; for (const prefDefinition of prefDefinitions) { const parent = this.panelDoc.getElementById(prefDefinition.parentId); // We want to insert the new definition after the last existing // definition, but before any other element. // For example in the "Advanced Settings" column there's indeed a // text at the end, and we want that it stays at the end. // The reference element can be `null` if there's no label or if there's // no element after the last label. But that's OK and it will do what we // want. const referenceElement = parent.querySelector("label:last-of-type + *"); parent.insertBefore( createPreferenceOption(prefDefinition), referenceElement ); } } async populatePreferences() { const prefCheckboxes = this.panelDoc.querySelectorAll( "input[type=checkbox][data-pref]" ); for (const prefCheckbox of prefCheckboxes) { if (GetPref(prefCheckbox.getAttribute("data-pref"))) { prefCheckbox.setAttribute("checked", true); } prefCheckbox.addEventListener("change", e => { const checkbox = e.target; SetPref(checkbox.getAttribute("data-pref"), checkbox.checked); if (checkbox.hasAttribute("data-force-reload")) { this.commands.targetCommand.reloadTopLevelTarget(); } }); } // Themes radio inputs are handled in setupThemeList const prefRadiogroups = this.panelDoc.querySelectorAll( ".radiogroup[data-pref]:not(#devtools-theme-box)" ); for (const radioGroup of prefRadiogroups) { const selectedValue = GetPref(radioGroup.getAttribute("data-pref")); for (const radioInput of radioGroup.querySelectorAll( "input[type=radio]" )) { if (radioInput.getAttribute("value") == selectedValue) { radioInput.setAttribute("checked", true); } radioInput.addEventListener("change", function (e) { SetPref(radioGroup.getAttribute("data-pref"), e.target.value); }); } } const prefSelects = this.panelDoc.querySelectorAll("select[data-pref]"); for (const prefSelect of prefSelects) { const pref = GetPref(prefSelect.getAttribute("data-pref")); const options = [...prefSelect.options]; options.some(function (option) { const value = option.value; // non strict check to allow int values. if (value == pref) { prefSelect.selectedIndex = options.indexOf(option); return true; } return false; }); prefSelect.addEventListener("change", function (e) { const select = e.target; SetPref( select.getAttribute("data-pref"), select.options[select.selectedIndex].value ); }); } if (this.commands.descriptorFront.isTabDescriptor) { const isJavascriptEnabled = await this.commands.targetConfigurationCommand.isJavascriptEnabled(); this.disableJSNode.checked = !isJavascriptEnabled; this.disableJSNode.addEventListener("click", this.#disableJSClicked); } else { // Hide the checkbox and label this.disableJSNode.parentNode.style.display = "none"; } } setupNetworkBodySizeLimit() { const input = this.panelDoc.querySelector("#netmonitor-body-limit"); // Int preferences at capped at PR_INT32_MAX == ~2 GB const maxValue = 2147483647; const editElement = this.panelDoc.querySelector( `.netmonitor-body-limit-button` ); editElement.addEventListener("click", this.editNetworkBodySizeLimit); const valueElement = this.panelDoc.querySelector( `#netmonitor-body-limit-value` ); const editSection = this.panelDoc.querySelector( "#netmonitor-body-limit-edit" ); input.addEventListener("keydown", function (event) { if (event.key == "Escape") { editSection.classList.remove("active"); // Prevents showing the split console event.preventDefault(); event.stopPropagation(); valueElement.classList.remove("hidden"); editElement.classList.remove("hidden"); input.setCustomValidity(""); const hasCustomValue = Services.prefs.prefHasUserValue( NETMONITOR_BODY_LIMIT_PREF ); resetElement.classList.toggle("hidden", !hasCustomValue); } else if (event.key == "Enter") { setValue(); // Prevents submitting the form and navigating the options document // to self-reload event.preventDefault(); } }); input.addEventListener("input", function () { const limit = parseInt(input.value, 10); if (limit < 0 || limit > maxValue) { input.setCustomValidity("invalid"); } else { input.setCustomValidity(""); } }); function setValue() { editSection.classList.remove("active"); valueElement.classList.remove("hidden"); editElement.classList.remove("hidden"); input.setCustomValidity(""); // Check mismatch as parseInt accepts strings like "1000xxx" and returns 1000 if (!input.validity.patternMismatch) { // If this is a valid number, clamp to the closest valid number const limit = Math.min( Math.max(parseInt(input.value, 10), 0), maxValue ); SetPref(NETMONITOR_BODY_LIMIT_PREF, limit); } const hasCustomValue = Services.prefs.prefHasUserValue( NETMONITOR_BODY_LIMIT_PREF ); resetElement.classList.toggle("hidden", !hasCustomValue); } function reset() { editSection.classList.remove("active"); valueElement.classList.remove("hidden"); editElement.classList.remove("hidden"); // Reset to the default value Services.prefs.clearUserPref(NETMONITOR_BODY_LIMIT_PREF); resetElement.classList.add("hidden"); } const setElement = this.panelDoc.querySelector( `.netmonitor-body-limit-set` ); setElement.addEventListener("click", setValue); const resetElement = this.panelDoc.querySelector( `.netmonitor-body-limit-restore-default` ); resetElement.addEventListener("click", reset); const hasCustomValue = Services.prefs.prefHasUserValue( NETMONITOR_BODY_LIMIT_PREF ); resetElement.classList.toggle("hidden", !hasCustomValue); this.updateNetworkBodySizeLimit(); } updateNetworkBodySizeLimit() { const valueElement = this.panelDoc.querySelector( `#netmonitor-body-limit-value` ); const value = GetPref(NETMONITOR_BODY_LIMIT_PREF); valueElement.textContent = value == 0 ? l10n.formatValueSync("options-netmonitor-body-limit-unlimited-label") : getFormattedSize(value); } /** * Show a form to edit the network maximum body size limit. * Including an input and a save button which will replace the read-only display. */ editNetworkBodySizeLimit = () => { const valueElement = this.panelDoc.querySelector( `#netmonitor-body-limit-value` ); valueElement.classList.add("hidden"); const editElement = this.panelDoc.querySelector( `.netmonitor-body-limit-button` ); editElement.classList.add("hidden"); const editSection = this.panelDoc.querySelector( "#netmonitor-body-limit-edit" ); editSection.classList.add("active"); const resetElement = this.panelDoc.querySelector( `.netmonitor-body-limit-restore-default` ); resetElement.classList.add("hidden"); const input = this.panelDoc.querySelector("#netmonitor-body-limit"); input.value = GetPref(NETMONITOR_BODY_LIMIT_PREF); input.focus(); }; updateCurrentTheme() { const currentTheme = GetPref("devtools.theme"); const themeBox = this.panelDoc.getElementById("devtools-theme-box"); const themeRadioInput = themeBox.querySelector(`[value=${currentTheme}]`); if (themeRadioInput) { themeRadioInput.checked = true; } else { // If the current theme does not exist anymore, switch to auto theme const autoThemeInputRadio = themeBox.querySelector("[value=auto]"); autoThemeInputRadio.checked = true; } } #setupLocalMode() { if (!this.commands.descriptorFront.isLocalTab) { const notice = this.panelDoc.querySelector( `.local-mode-only-work-locally` ); notice.classList.remove("hidden"); } const newButton = this.panelDoc.querySelector(`.local-mode-new-mapping`); newButton.addEventListener("click", this.#newLocalModeMapping); this.#updateLocalModeMappings(); } #newLocalModeMapping = async event => { event.preventDefault(); event.stopPropagation(); const origin = lazy.LocalModeMappings.getNextAvailableOrigin(); const path = await this.#chooseLocalModePath(origin); this.#focusLocalModeLastMapping = true; lazy.LocalModeMappings.createNewMapping(origin, path); }; /** * Create all the DOM Elements to control one "Local Mode" mapping. * * @param {string} origin * Mapping's https origin. e.g. firefox.location * @param {string} path * Absolute path from where the origin should be loaded locally * @param {boolean} disabled * Is the mapping currently disabled. * @param {string} prefPrefix * Preference prefix for this specific mapping * e.g. "devtools.local-mode.mappings.0." * @param {Array} mappings * List of all the mappings. * See `LocalModeMappings.getAllMappings()` * @return {DOMElement} * The
  • element rendering this mapping. */ #createLocalModeMappingDOM(origin, path, disabled, prefPrefix, mappings) { const el = this.panelDoc.createElement("li"); // Expose the prefix for the notification bar to easily spot the newly created mapping el.setAttribute("data-pref-prefix", prefPrefix); el.classList.toggle("disabled", disabled); const originLine = this.panelDoc.createElement("div"); originLine.classList.add("local-mode-origin-line"); const inputId = "origin-" + prefPrefix.replace(/\./g, "-"); const originLabel = this.panelDoc.createElement("label"); originLabel.setAttribute("data-l10n-id", "options-local-mode-domain-label"); originLabel.setAttribute("for", inputId); const originValueContainer = this.panelDoc.createElement("div"); const originPrefixLabel = this.panelDoc.createElement("span"); originPrefixLabel.textContent = "http(s)://"; const originElement = this.panelDoc.createElement("input"); originElement.id = inputId; originElement.classList.add("local-mode-origin-input"); originElement.setAttribute( "data-l10n-id", "options-local-mode-origin-input" ); originElement.setAttribute("type", "text"); originElement.setAttribute("value", origin); originElement.toggleAttribute("disabled", disabled); originElement.addEventListener("keypress", event => { if (event.key == "Enter") { // Cancel the enter keypress as it would something trigger a click // on the first open element and always try to navigate to first mapping URL event.preventDefault(); originElement.blur(); } }); originElement.addEventListener("input", event => { const newOrigin = event.target.value; if (!newOrigin) { originError.textContent = ""; originElement.setCustomValidity(""); return; } // Check if we may override another mapping if ( newOrigin != origin && mappings.some(mapping => mapping.origin == newOrigin) ) { originElement.setCustomValidity("invalid"); originError.textContent = l10n.formatValueSync( "options-local-mode-origin-conflict" ); } else if (!URL.canParse(`https://${newOrigin}`)) { originElement.setCustomValidity("invalid"); originError.textContent = l10n.formatValueSync( "options-local-mode-origin-invalid" ); } else { originError.textContent = ""; originElement.setCustomValidity(""); } }); originElement.addEventListener("blur", event => { const newOrigin = event.target.value; originError.textContent = ""; originElement.setCustomValidity(""); if (newOrigin == origin) { return; } // In case of empty or invalid input, reverts back to initial value on blur if ( !newOrigin || mappings.some(mapping => mapping.origin == newOrigin) || !URL.canParse(`https://${newOrigin}`) ) { // Reset back to previous value event.target.value = origin; return; } // Disable DOM updates on preferences changes as it would make us lose the focus // and no update are needed as we can simply update the local `origin` variable this.#ignoreLocalModeChanges = false; // And create a new one with the new origin Services.prefs.setStringPref(prefPrefix + "origin", newOrigin); origin = newOrigin; this.#ignoreLocalModeChanges = false; }); const originError = this.panelDoc.createElement("span"); originError.classList.add("local-mode-origin-error"); const openButton = this.panelDoc.createElement("button"); openButton.id = "navigate-" + prefPrefix; openButton.classList.add( "devtools-button", "local-mode-mapping-navigate-to" ); openButton.setAttribute("data-l10n-id", "options-local-mode-navigate-to"); openButton.toggleAttribute("disabled", disabled); openButton.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); this.commands.targetCommand.navigateTo("https://" + origin); }); originValueContainer.append( originElement, originPrefixLabel, originElement, openButton, originError ); originLine.append(originLabel, originValueContainer); const folderLine = this.panelDoc.createElement("div"); folderLine.classList.add("local-mode-folder-line"); if (disabled) { folderLine.classList.add("disabled"); } const folderLabel = this.panelDoc.createElement("label"); folderLabel.setAttribute("data-l10n-id", "options-local-mode-folder-label"); const inputContainer2 = this.panelDoc.createElement("div"); const folderLinkElement = this.panelDoc.createElement("a"); folderLinkElement.id = "link-" + prefPrefix; folderLinkElement.href = "file://" + path; folderLinkElement.textContent = path; folderLinkElement.addEventListener("click", function (event) { // Request the OS to open the folder in the default file explorer app new lazy.LocalFile(path).reveal(); // Prevent Cmd/Shift+click from opening the file:// URL in a tab event.preventDefault(); }); // If the path is invalid, replace the link with a label + link // to warn the user about that. let pathExists = false; try { pathExists = new lazy.FileUtils.File(path).exists(); } catch (e) {} let folderError = ""; if (!pathExists) { folderError = this.panelDoc.createElement("span"); folderError.classList.add("local-mode-folder-error"); folderError.textContent = l10n.formatValueSync( "options-local-mode-folder-invalid" ); } const folderChooserElement = this.panelDoc.createElement("button"); folderChooserElement.id = "choose-folder-" + prefPrefix; folderChooserElement.classList.add( "devtools-button", "local-mode-mapping-choose-folder" ); folderChooserElement.setAttribute( "data-l10n-id", "options-local-mode-choose-folder" ); folderChooserElement.toggleAttribute("disabled", disabled); folderChooserElement.addEventListener("click", async event => { event.preventDefault(); event.stopPropagation(); const newPath = await this.#chooseLocalModePath(origin, path); Services.prefs.setStringPref(prefPrefix + "path", newPath); }); inputContainer2.append(folderLinkElement, folderChooserElement); folderLine.append(folderLabel, inputContainer2); if (folderError) { folderLine.append(folderError); } const footerEl = this.panelDoc.createElement("footer"); const toggleButton = this.panelDoc.createElement("button"); toggleButton.id = "toggle-" + prefPrefix; toggleButton.setAttribute("data-l10n-id", "options-local-mode-toggle"); toggleButton.classList.add("devtools-button", "local-mode-mapping-toggle"); toggleButton.textContent = l10n.formatValueSync( disabled ? "options-local-mode-toggle-enable" : "options-local-mode-toggle-disable" ); toggleButton.addEventListener("click", event => { event.preventDefault(); Services.prefs.setBoolPref(prefPrefix + "disabled", !disabled); }); const removeButton = this.panelDoc.createElement("button"); removeButton.classList.add("devtools-button", "local-mode-mapping-remove"); removeButton.append("Remove local mapping"); removeButton.addEventListener("click", event => { event.preventDefault(); const message = l10n.formatValueSync( "options-local-mode-confirm-deletion", { mappingOrigin: origin } ); if (!this.panelDoc.defaultView.confirm(message)) { return; } Services.prefs.clearUserPref(prefPrefix + "origin"); Services.prefs.clearUserPref(prefPrefix + "path"); Services.prefs.clearUserPref(prefPrefix + "disabled"); }); footerEl.append(toggleButton, removeButton); el.append(originLine, folderLine, footerEl); return el; } // Internal flag to avoid updated Local Mode mappings when receiving // a preferences update notification #ignoreLocalModeChanges = false; // Should we focus the last displayed mapping on next local mode mappings update #focusLocalModeLastMapping = false; /** * Update the list of all local mode mappings on startup, or when preferences * are updated. */ #updateLocalModeMappings = async () => { // When the UI updates the prefs, we may not want to update the DOM if (this.#ignoreLocalModeChanges) { return; } const mappingsElement = this.panelDoc.querySelector(`#local-mode-mappings`); const elements = []; const mappings = lazy.LocalModeMappings.getAllMappings(); for (const { origin, path, disabled, prefPrefix } of mappings) { elements.push( this.#createLocalModeMappingDOM( origin, path, disabled, prefPrefix, mappings ) ); } // As we are about to wipe and recreate all the mappings, // try to save and restore the currently focused element via their ID let focusedId = ""; const { activeElement } = this.panelDoc; if (activeElement?.id && mappingsElement.contains(activeElement)) { focusedId = activeElement.id; } mappingsElement.replaceChildren(...elements); if (this.#focusLocalModeLastMapping) { const lastMappingOriginInput = mappingsElement.querySelector( "li:last-of-type .local-mode-origin-input" ); if (lastMappingOriginInput) { lastMappingOriginInput.focus(); lastMappingOriginInput.select(); } this.#focusLocalModeLastMapping = false; } else if (focusedId) { const elementToFocus = this.panelDoc.getElementById(focusedId); if (elementToFocus) { elementToFocus.focus(); } } }; /** * Helper to choose a local folder path for a given local mode origin. * * @param {string} origin * @param {string} existingPath * If picking a folder for an existing mapping, the absolute * path to the current folder associated with this mapping. * @return {promise} * Absolute path to the local folder */ #chooseLocalModePath(origin, existingPath) { const FilePicker = Cc["@mozilla.org/filepicker;1"].createInstance( Ci.nsIFilePicker ); FilePicker.init( this.panelWin.browsingContext, l10n.formatValueSync("options-local-mode-choose-folder-picker-title", { url: "https://" + origin, }), FilePicker.modeGetFolder ); // Try to display the existing path for this mapping, if valid and exists try { const file = new lazy.FileUtils.File(existingPath); if (file.exists()) { FilePicker.displayDirectory = file; } } catch (e) {} return new Promise((resolve, reject) => { FilePicker.open(rv => { if (rv == FilePicker.returnOK) { resolve(FilePicker.file.path); } else { reject(); } }); }); } updateSourceMapPref() { const prefName = "devtools.source-map.client-service.enabled"; const enabled = GetPref(prefName); const box = this.panelDoc.querySelector(`[data-pref="${prefName}"]`); box.checked = enabled; } /** * Disables JavaScript for the currently loaded tab. We force a page refresh * here because setting browsingContext.allowJavascript to true fails to block * JS execution from event listeners added using addEventListener(), AJAX * calls and timers. The page refresh prevents these things from being added * in the first place. * * @param {Event} event * The event sent by checking / unchecking the disable JS checkbox. */ #disableJSClicked = event => { const checked = event.target.checked; this.commands.targetConfigurationCommand.updateConfiguration({ javascriptEnabled: !checked, }); }; destroy() { if (this.destroyed) { return; } this.destroyed = true; this.#removeListeners(); this.disableJSNode.removeEventListener("click", this.#disableJSClicked); this.panelWin = this.panelDoc = this.disableJSNode = this.toolbox = null; } } exports.OptionsPanel = OptionsPanel;