summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutconfig/content
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/aboutconfig/content
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/aboutconfig/content')
-rw-r--r--toolkit/components/aboutconfig/content/aboutconfig.css290
-rw-r--r--toolkit/components/aboutconfig/content/aboutconfig.html108
-rw-r--r--toolkit/components/aboutconfig/content/aboutconfig.js696
-rw-r--r--toolkit/components/aboutconfig/content/background.svg4
-rw-r--r--toolkit/components/aboutconfig/content/toggle.svg7
5 files changed, 1105 insertions, 0 deletions
diff --git a/toolkit/components/aboutconfig/content/aboutconfig.css b/toolkit/components/aboutconfig/content/aboutconfig.css
new file mode 100644
index 0000000000..b1ffe12a86
--- /dev/null
+++ b/toolkit/components/aboutconfig/content/aboutconfig.css
@@ -0,0 +1,290 @@
+/* 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/. */
+
+:root {
+ --prefs-table-border-width: 1px;
+ --prefs-table-border: var(--prefs-table-border-width) solid var(--in-content-box-border-color);
+}
+
+.hidden {
+ display: none;
+}
+
+.table-shown > #show-all,
+.table-shown > .config-background-wrapper {
+ display: none;
+}
+
+.config-background {
+ background: url("chrome://global/content/aboutconfig/background.svg") no-repeat;
+ height: 182px;
+ margin: 32px auto;
+ width: 235px;
+}
+
+.config-help-text {
+ text-align: center;
+}
+
+.title {
+ background-image: url("chrome://global/skin/icons/warning.svg");
+ fill: #fcd100;
+}
+
+#toolbar {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ box-sizing: border-box;
+ width: 100%;
+ background-color: var(--in-content-page-background);
+ padding: 10px;
+ padding-bottom: 0;
+ min-width: 644px;
+ display: flex;
+}
+
+.checkbox-container {
+ /* Center align and get rid of whitespace. */
+ display: inline-flex;
+ align-items: center;
+ margin-inline-start: 1ch;
+}
+
+#about-config-search {
+ -moz-context-properties: fill, fill-opacity;
+ fill: currentColor;
+ box-sizing: border-box;
+ flex-grow: 1;
+ background-image: url("chrome://global/skin/icons/search-glass.svg");
+ background-repeat: no-repeat;
+ background-position: 8px center;
+ background-size: 16px;
+ /* Set horizontal margin to 0 to ensure alignment with table. */
+ margin-inline: 0;
+ text-align: match-parent;
+ /* All prefs must be left-to-right. */
+ direction: ltr;
+}
+
+@media not (prefers-contrast) {
+ #about-config-search {
+ fill-opacity: 0.4;
+ }
+}
+
+#about-config-search:placeholder-shown {
+ /* Display the placeholder in its natural directionality,
+ * even if the user changes the text direction manually
+ * (e.g. via RightCtrl+Shift). */
+ direction: inherit;
+}
+
+:root:dir(ltr) #about-config-search {
+ /* Be explicit about padding direction since
+ * `about-config-search` is forced to be LTR. */
+ padding-left: 32px;
+}
+
+:root:dir(rtl) #about-config-search {
+ background-position-x: right 8px;
+ padding-right: 32px;
+}
+
+#show-all {
+ display: block;
+ margin: 10px auto;
+}
+
+#prefs {
+ background-color: var(--in-content-box-background);
+ color: var(--in-content-text-color);
+ margin: 10px;
+ table-layout: fixed;
+ width: calc(100% - 20px);
+ min-width: 644px;
+ /* To stay consistent with about:preferences (664px - 20px margin). */
+ border: var(--prefs-table-border);
+ border-radius: 4px;
+ border-spacing: 0;
+}
+
+#prefs > tr.odd {
+ background-color: var(--in-content-box-background-odd);
+}
+
+#prefs > tr:hover {
+ background-color: var(--in-content-item-hover);
+ color: var(--in-content-item-hover-text);
+}
+
+#prefs > tr.has-user-value {
+ font-weight: bold;
+}
+
+#prefs > tr.locked {
+ opacity: 0.4;
+ background-image: url("chrome://global/skin/icons/security.svg");
+ background-repeat: no-repeat;
+ background-position: 9px center;
+ background-size: 16px 16px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+#prefs > tr.locked:dir(rtl) {
+ background-position-x: right 9px;
+}
+
+#prefs > tr > td,
+#prefs > tr > th {
+ padding: 4px;
+ font-weight: inherit;
+}
+
+#prefs > tr > th {
+ direction: ltr;
+ text-align: match-parent;
+}
+
+#prefs > tr:dir(ltr) > th {
+ /* Be explicit about padding direction since `th` is forced to be LTR. */
+ padding-left: 30px;
+}
+
+#prefs > tr:dir(rtl) > th {
+ padding-right: 30px;
+}
+
+#prefs > tr.deleted > th > span {
+ font-weight: bold;
+ color: var(--text-color-deemphasized);
+}
+
+#prefs > tr > td.cell-edit,
+#prefs > tr > td.cell-reset {
+ width: 40px;
+ padding: 0;
+}
+
+.cell-value {
+ overflow-wrap: anywhere;
+ white-space: pre-wrap;
+ word-break: break-all;
+}
+
+tr:not(.deleted) > .cell-value {
+ /* Always display the text in the value cell using left-to-right rules, but
+ align it according to the page direction. This doesn't apply to the radio
+ buttons shown for deleted preferences. */
+ direction: ltr;
+ text-align: match-parent;
+}
+
+:root:dir(ltr) tr:not(.deleted) > .cell-value > #form-edit {
+ /* Make the text in the form stay in the same place as before editing the pref. */
+ margin-left: -8px;
+}
+
+:root:dir(rtl) tr:not(.deleted) > .cell-value > #form-edit {
+ margin-right: -8px;
+}
+
+#form-edit > label {
+ /* Make the radiobutton's text wrap to a new line along with
+ the radiobutton itself, when space is constrained. */
+ display: inline-block;
+ margin-inline-end: 30px;
+}
+
+#form-edit > label:last-of-type {
+ margin-inline-end: 0;
+}
+
+#form-edit > label > :is(input[type="radio"], span) {
+ vertical-align: middle;
+}
+
+td.cell-value > form > input[type="text"],
+td.cell-value > form > input[type="number"] {
+ appearance: textfield;
+ margin: 0;
+ width: 100%;
+ box-sizing: border-box;
+ /* Align the text inside the input field in the same way as the table cell,
+ for both the left-to-right and right-to-left directions. */
+ text-align: match-parent;
+}
+
+.button-add,
+.button-save,
+.button-edit,
+.button-toggle,
+.button-delete,
+.button-reset {
+ -moz-context-properties: fill;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 16px;
+ fill: currentColor;
+ min-width: auto;
+ width: 32px;
+}
+
+.button-add {
+ background-image: url("chrome://global/skin/icons/plus.svg");
+}
+
+.button-save {
+ background-image: url("chrome://global/skin/icons/check.svg");
+}
+
+.button-edit {
+ background-image: url("chrome://global/skin/icons/edit.svg");
+}
+
+.button-toggle {
+ background-image: url("chrome://global/content/aboutconfig/toggle.svg");
+}
+
+.button-delete {
+ background-image: url("chrome://global/skin/icons/delete.svg");
+}
+
+.button-reset {
+ background-image: url("chrome://global/skin/icons/undo.svg");
+}
+
+.button-reset:dir(rtl) {
+ transform: scaleX(-1);
+}
+
+/* The ::before creates a blank space between the last visible pref and the add row. */
+#prefs[has-visible-prefs] > .add > th::before,
+#prefs[has-visible-prefs] > .add > td::before {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0;
+ /* Make it wider by the border width so the border-inline is hidden. */
+ inset-inline: calc(var(--prefs-table-border-width) * -1);
+ height: 12px;
+ background-color: var(--in-content-page-background);
+ /* This is visually the top border on the add row. */
+ border-bottom: var(--prefs-table-border);
+}
+
+#prefs[has-visible-prefs] > .add > th,
+#prefs[has-visible-prefs] > .add > td {
+ /* This is the border underneath the last existing pref row. */
+ border-top: var(--prefs-table-border);
+ padding-top: 14px;
+ position: relative;
+}
+
+@media (prefers-contrast) {
+ #prefs > tr.deleted:hover > th > span {
+ color: inherit;
+ }
+}
diff --git a/toolkit/components/aboutconfig/content/aboutconfig.html b/toolkit/components/aboutconfig/content/aboutconfig.html
new file mode 100644
index 0000000000..8962cdd86d
--- /dev/null
+++ b/toolkit/components/aboutconfig/content/aboutconfig.html
@@ -0,0 +1,108 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome:; object-src 'none'"
+ />
+ <meta charset="utf-8" />
+ <meta name="color-scheme" content="light dark" />
+ <link
+ rel="stylesheet"
+ media="screen, projection"
+ type="text/css"
+ href="chrome://global/skin/in-content/common.css"
+ />
+ <link
+ rel="stylesheet"
+ media="screen, projection"
+ type="text/css"
+ href="chrome://global/skin/in-content/info-pages.css"
+ title="infop"
+ />
+ <link
+ rel="stylesheet"
+ type="text/css"
+ href="chrome://global/content/aboutconfig/aboutconfig.css"
+ />
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="toolkit/about/config.ftl" />
+ <link rel="icon" href="chrome://global/skin/icons/settings.svg" />
+ <script src="chrome://global/content/aboutconfig/aboutconfig.js"></script>
+ <title data-l10n-id="about-config-page-title"></title>
+ </head>
+ <body>
+ <div
+ class="container"
+ role="alertdialog"
+ aria-labelledby="warningTitle"
+ aria-describedby="warningDescription"
+ >
+ <div class="title">
+ <h1
+ id="warningTitle"
+ class="title-text"
+ data-l10n-id="about-config-intro-warning-title"
+ ></h1>
+ </div>
+
+ <div class="description">
+ <p
+ id="warningDescription"
+ data-l10n-id="about-config-intro-warning-text"
+ ></p>
+ </div>
+
+ <div class="toggle-container-with-text">
+ <input type="checkbox" id="showWarningNextTime" checked />
+ <label
+ for="showWarningNextTime"
+ data-l10n-id="about-config-intro-warning-checkbox"
+ ></label>
+ </div>
+
+ <div class="button-container">
+ <button
+ id="warningButton"
+ class="primary"
+ data-l10n-id="about-config-intro-warning-button"
+ ></button>
+ </div>
+ </div>
+
+ <template id="main">
+ <div id="toolbar">
+ <!-- Use a unique ID to prevent showing autocomplete results from other
+ browser pages with similarly named fields. -->
+ <input
+ type="text"
+ id="about-config-search"
+ data-l10n-id="about-config-search-input1"
+ />
+ <label class="checkbox-container">
+ <input type="checkbox" id="about-config-show-only-modified" />
+ <span data-l10n-id="about-config-show-only-modified"></span>
+ </label>
+ </div>
+
+ <table id="prefs"></table>
+
+ <div class="config-background-wrapper">
+ <button
+ id="show-all"
+ class="ghost-button"
+ data-l10n-id="about-config-show-all"
+ ></button>
+ <div class="config-background"></div>
+ <p
+ class="config-help-text"
+ data-l10n-id="about-config-caution-text"
+ ></p>
+ </div>
+ </template>
+ </body>
+</html>
diff --git a/toolkit/components/aboutconfig/content/aboutconfig.js b/toolkit/components/aboutconfig/content/aboutconfig.js
new file mode 100644
index 0000000000..e11021ed64
--- /dev/null
+++ b/toolkit/components/aboutconfig/content/aboutconfig.js
@@ -0,0 +1,696 @@
+/* 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/. */
+
+const { DeferredTask } = ChromeUtils.importESModule(
+ "resource://gre/modules/DeferredTask.sys.mjs"
+);
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+const SEARCH_TIMEOUT_MS = 100;
+const SEARCH_AUTO_MIN_CRARACTERS = 3;
+
+const GETTERS_BY_PREF_TYPE = {
+ [Ci.nsIPrefBranch.PREF_BOOL]: "getBoolPref",
+ [Ci.nsIPrefBranch.PREF_INT]: "getIntPref",
+ [Ci.nsIPrefBranch.PREF_STRING]: "getStringPref",
+};
+
+const STRINGS_ADD_BY_TYPE = {
+ Boolean: "about-config-pref-add-type-boolean",
+ Number: "about-config-pref-add-type-number",
+ String: "about-config-pref-add-type-string",
+};
+
+// Fluent limits the maximum length of placeables.
+const MAX_PLACEABLE_LENGTH = 2500;
+
+let gDefaultBranch = Services.prefs.getDefaultBranch("");
+let gFilterPrefsTask = new DeferredTask(
+ () => filterPrefs(),
+ SEARCH_TIMEOUT_MS,
+ 0
+);
+
+/**
+ * Maps the name of each preference in the back-end to its PrefRow object,
+ * separating the preferences that actually exist. This is as an optimization to
+ * avoid querying the preferences service each time the list is filtered.
+ */
+let gExistingPrefs = new Map();
+let gDeletedPrefs = new Map();
+
+/**
+ * Also cache several values to improve the performance of common use cases.
+ */
+let gSortedExistingPrefs = null;
+let gSearchInput = null;
+let gShowOnlyModifiedCheckbox = null;
+let gPrefsTable = null;
+
+/**
+ * Reference to the PrefRow currently being edited, if any.
+ */
+let gPrefInEdit = null;
+
+/**
+ * Lowercase substring that should be contained in the preference name.
+ */
+let gFilterString = null;
+
+/**
+ * RegExp that should be matched to the preference name.
+ */
+let gFilterPattern = null;
+
+/**
+ * True if we were requested to show all preferences.
+ */
+let gFilterShowAll = false;
+
+class PrefRow {
+ constructor(name, opts) {
+ this.name = name;
+ this.value = true;
+ this.hidden = false;
+ this.odd = false;
+ this.editing = false;
+ this.isAddRow = opts && opts.isAddRow;
+ this.refreshValue();
+ }
+
+ refreshValue() {
+ let prefType = Services.prefs.getPrefType(this.name);
+
+ // If this preference has been deleted, we keep its last known value.
+ if (prefType == Ci.nsIPrefBranch.PREF_INVALID) {
+ this.hasDefaultValue = false;
+ this.hasUserValue = false;
+ this.isLocked = false;
+ if (gExistingPrefs.has(this.name)) {
+ gExistingPrefs.delete(this.name);
+ gSortedExistingPrefs = null;
+ }
+ gDeletedPrefs.set(this.name, this);
+ return;
+ }
+
+ if (!gExistingPrefs.has(this.name)) {
+ gExistingPrefs.set(this.name, this);
+ gSortedExistingPrefs = null;
+ }
+ gDeletedPrefs.delete(this.name);
+
+ try {
+ this.value = gDefaultBranch[GETTERS_BY_PREF_TYPE[prefType]](this.name);
+ this.hasDefaultValue = true;
+ } catch (ex) {
+ this.hasDefaultValue = false;
+ }
+ this.hasUserValue = Services.prefs.prefHasUserValue(this.name);
+ this.isLocked = Services.prefs.prefIsLocked(this.name);
+
+ try {
+ if (this.hasUserValue) {
+ // This can throw for locked preferences without a default value.
+ this.value = Services.prefs[GETTERS_BY_PREF_TYPE[prefType]](this.name);
+ } else if (/^chrome:\/\/.+\/locale\/.+\.properties/.test(this.value)) {
+ // We don't know which preferences should be read using getComplexValue,
+ // so we use a heuristic to determine if this is a localized preference.
+ // This can throw if there is no value in the localized files.
+ this.value = Services.prefs.getComplexValue(
+ this.name,
+ Ci.nsIPrefLocalizedString
+ ).data;
+ }
+ } catch (ex) {
+ this.value = "";
+ }
+ }
+
+ get type() {
+ return this.value.constructor.name;
+ }
+
+ get exists() {
+ return this.hasDefaultValue || this.hasUserValue;
+ }
+
+ get matchesFilter() {
+ if (!this.matchesModifiedFilter) {
+ return false;
+ }
+
+ return (
+ gFilterShowAll ||
+ (gFilterPattern && gFilterPattern.test(this.name)) ||
+ (gFilterString && this.name.toLowerCase().includes(gFilterString))
+ );
+ }
+
+ get matchesModifiedFilter() {
+ const onlyShowModified = gShowOnlyModifiedCheckbox.checked;
+ return !onlyShowModified || this.hasUserValue;
+ }
+
+ /**
+ * Returns a reference to the table row element to be added to the document,
+ * constructing and initializing it the first time this method is called.
+ */
+ getElement() {
+ if (this._element) {
+ return this._element;
+ }
+
+ this._element = document.createElement("tr");
+ this._element._pref = this;
+
+ let nameCell = document.createElement("th");
+ let nameCellSpan = document.createElement("span");
+ nameCell.appendChild(nameCellSpan);
+ this._element.append(
+ nameCell,
+ (this.valueCell = document.createElement("td")),
+ (this.editCell = document.createElement("td")),
+ (this.resetCell = document.createElement("td"))
+ );
+ this.editCell.appendChild(
+ (this.editButton = document.createElement("button"))
+ );
+ delete this.resetButton;
+
+ nameCell.setAttribute("scope", "row");
+ this.valueCell.className = "cell-value";
+ this.editCell.className = "cell-edit";
+ this.resetCell.className = "cell-reset";
+
+ // Add <wbr> behind dots to prevent line breaking in random mid-word places.
+ let parts = this.name.split(".");
+ for (let i = 0; i < parts.length - 1; i++) {
+ nameCellSpan.append(parts[i] + ".", document.createElement("wbr"));
+ }
+ nameCellSpan.append(parts[parts.length - 1]);
+
+ this.refreshElement();
+
+ return this._element;
+ }
+
+ refreshElement() {
+ if (!this._element) {
+ // No need to update if this preference was never added to the table.
+ return;
+ }
+
+ if (this.exists && !this.editing) {
+ // We need to place the text inside a "span" element to ensure that the
+ // text copied to the clipboard includes all whitespace.
+ let span = document.createElement("span");
+ span.textContent = this.value;
+ // We additionally need to wrap this with another "span" element to convey
+ // the state to screen readers without affecting the visual presentation.
+ span.setAttribute("aria-hidden", "true");
+ let outerSpan = document.createElement("span");
+ if (this.type == "String" && this.value.length > MAX_PLACEABLE_LENGTH) {
+ // If the value is too long for localization, don't include the state.
+ // Since the preferences system is designed to store short values, this
+ // case happens very rarely, thus we keep the same DOM structure for
+ // consistency even though we could avoid the extra "span" element.
+ outerSpan.setAttribute("aria-label", this.value);
+ } else {
+ let spanL10nId = this.hasUserValue
+ ? "about-config-pref-accessible-value-custom"
+ : "about-config-pref-accessible-value-default";
+ document.l10n.setAttributes(outerSpan, spanL10nId, {
+ value: "" + this.value,
+ });
+ }
+ outerSpan.appendChild(span);
+ this.valueCell.textContent = "";
+ this.valueCell.append(outerSpan);
+ if (this.type == "Boolean") {
+ document.l10n.setAttributes(
+ this.editButton,
+ "about-config-pref-toggle-button"
+ );
+ this.editButton.className = "button-toggle semi-transparent";
+ } else {
+ document.l10n.setAttributes(
+ this.editButton,
+ "about-config-pref-edit-button"
+ );
+ this.editButton.className = "button-edit semi-transparent";
+ }
+ this.editButton.removeAttribute("form");
+ delete this.inputField;
+ } else {
+ this.valueCell.textContent = "";
+ // The form is needed for the validation report to appear, but we need to
+ // prevent the associated button from reloading the page.
+ let form = document.createElement("form");
+ form.addEventListener("submit", event => event.preventDefault());
+ form.id = "form-edit";
+ if (this.editing) {
+ this.inputField = document.createElement("input");
+ this.inputField.value = this.value;
+ if (this.type == "Number") {
+ this.inputField.type = "number";
+ this.inputField.required = true;
+ this.inputField.min = -2147483648;
+ this.inputField.max = 2147483647;
+ } else {
+ this.inputField.type = "text";
+ }
+ form.appendChild(this.inputField);
+ document.l10n.setAttributes(
+ this.editButton,
+ "about-config-pref-save-button"
+ );
+ this.editButton.className = "primary button-save semi-transparent";
+ } else {
+ delete this.inputField;
+ for (let type of ["Boolean", "Number", "String"]) {
+ let radio = document.createElement("input");
+ radio.type = "radio";
+ radio.name = "type";
+ radio.value = type;
+ radio.checked = this.type == type;
+ let radioSpan = document.createElement("span");
+ document.l10n.setAttributes(radioSpan, STRINGS_ADD_BY_TYPE[type]);
+ let radioLabel = document.createElement("label");
+ radioLabel.append(radio, radioSpan);
+ form.appendChild(radioLabel);
+ }
+ form.addEventListener("click", event => {
+ if (event.target.name != "type") {
+ return;
+ }
+ let type = event.target.value;
+ if (this.type != type) {
+ if (type == "Boolean") {
+ this.value = true;
+ } else if (type == "Number") {
+ this.value = 0;
+ } else {
+ this.value = "";
+ }
+ }
+ });
+ document.l10n.setAttributes(
+ this.editButton,
+ "about-config-pref-add-button"
+ );
+ this.editButton.className = "button-add semi-transparent";
+ }
+ this.valueCell.appendChild(form);
+ this.editButton.setAttribute("form", "form-edit");
+ }
+ this.editButton.disabled = this.isLocked;
+ if (!this.isLocked && this.hasUserValue) {
+ if (!this.resetButton) {
+ this.resetButton = document.createElement("button");
+ this.resetCell.appendChild(this.resetButton);
+ }
+ if (!this.hasDefaultValue) {
+ document.l10n.setAttributes(
+ this.resetButton,
+ "about-config-pref-delete-button"
+ );
+ this.resetButton.className =
+ "button-delete ghost-button semi-transparent";
+ } else {
+ document.l10n.setAttributes(
+ this.resetButton,
+ "about-config-pref-reset-button"
+ );
+ this.resetButton.className =
+ "button-reset ghost-button semi-transparent";
+ }
+ } else if (this.resetButton) {
+ this.resetButton.remove();
+ delete this.resetButton;
+ }
+
+ this.refreshClass();
+ }
+
+ refreshClass() {
+ if (!this._element) {
+ // No need to update if this preference was never added to the table.
+ return;
+ }
+
+ let className;
+ if (this.hidden) {
+ className = "hidden";
+ } else {
+ className =
+ (this.hasUserValue ? "has-user-value " : "") +
+ (this.isLocked ? "locked " : "") +
+ (this.exists ? "" : "deleted ") +
+ (this.isAddRow ? "add " : "") +
+ (this.odd ? "odd " : "");
+ }
+
+ if (this._lastClassName !== className) {
+ this._element.className = this._lastClassName = className;
+ }
+ }
+
+ edit() {
+ if (gPrefInEdit) {
+ gPrefInEdit.endEdit();
+ }
+ gPrefInEdit = this;
+ this.editing = true;
+ this.refreshElement();
+ // The type=number input isn't selected unless it's focused first.
+ this.inputField.focus();
+ this.inputField.select();
+ }
+
+ toggle() {
+ Services.prefs.setBoolPref(this.name, !this.value);
+ }
+
+ editOrToggle() {
+ if (this.type == "Boolean") {
+ this.toggle();
+ } else {
+ this.edit();
+ }
+ }
+
+ save() {
+ if (this.type == "Number") {
+ if (!this.inputField.reportValidity()) {
+ return;
+ }
+ Services.prefs.setIntPref(this.name, parseInt(this.inputField.value));
+ } else {
+ Services.prefs.setStringPref(this.name, this.inputField.value);
+ }
+ this.refreshValue();
+ this.endEdit();
+ this.editButton.focus();
+ }
+
+ endEdit() {
+ this.editing = false;
+ this.refreshElement();
+ gPrefInEdit = null;
+ }
+}
+
+let gPrefObserverRegistered = false;
+let gPrefObserver = {
+ observe(subject, topic, data) {
+ let pref = gExistingPrefs.get(data) || gDeletedPrefs.get(data);
+ if (pref) {
+ pref.refreshValue();
+ if (!pref.editing) {
+ pref.refreshElement();
+ }
+ return;
+ }
+
+ let newPref = new PrefRow(data);
+ if (newPref.matchesFilter) {
+ document.getElementById("prefs").appendChild(newPref.getElement());
+ }
+ },
+};
+
+if (!Preferences.get("browser.aboutConfig.showWarning")) {
+ // When showing the filtered preferences directly, remove the warning elements
+ // immediately to prevent flickering, but wait to filter the preferences until
+ // the value of the textbox has been restored from previous sessions.
+ document.addEventListener("DOMContentLoaded", loadPrefs, { once: true });
+ window.addEventListener(
+ "load",
+ () => {
+ if (document.getElementById("about-config-search").value) {
+ filterPrefs();
+ }
+ },
+ { once: true }
+ );
+} else {
+ document.addEventListener("DOMContentLoaded", function () {
+ let warningButton = document.getElementById("warningButton");
+ warningButton.addEventListener("click", onWarningButtonClick);
+ warningButton.focus({ focusVisible: false });
+ });
+}
+
+function onWarningButtonClick() {
+ Services.prefs.setBoolPref(
+ "browser.aboutConfig.showWarning",
+ document.getElementById("showWarningNextTime").checked
+ );
+ loadPrefs();
+}
+
+function loadPrefs() {
+ [...document.styleSheets].find(s => s.title == "infop").disabled = true;
+
+ let { content } = document.getElementById("main");
+ document.body.textContent = "";
+ document.body.appendChild(content);
+
+ let search = (gSearchInput = document.getElementById("about-config-search"));
+ let prefs = (gPrefsTable = document.getElementById("prefs"));
+ let showAll = document.getElementById("show-all");
+ gShowOnlyModifiedCheckbox = document.getElementById(
+ "about-config-show-only-modified"
+ );
+ search.focus();
+ gShowOnlyModifiedCheckbox.checked = false;
+
+ for (let name of Services.prefs.getChildList("")) {
+ new PrefRow(name);
+ }
+
+ search.addEventListener("keypress", event => {
+ if (event.key == "Escape") {
+ // The ESC key returns immediately to the initial empty page.
+ search.value = "";
+ gFilterPrefsTask.disarm();
+ filterPrefs();
+ } else if (event.key == "Enter") {
+ // The Enter key filters immediately even if the search string is short.
+ gFilterPrefsTask.disarm();
+ filterPrefs({ shortString: true });
+ }
+ });
+
+ search.addEventListener("input", () => {
+ // We call "disarm" to restart the timer at every input.
+ gFilterPrefsTask.disarm();
+ if (search.value.trim().length < SEARCH_AUTO_MIN_CRARACTERS) {
+ // Return immediately to the empty page if the search string is short.
+ filterPrefs();
+ } else {
+ gFilterPrefsTask.arm();
+ }
+ });
+
+ gShowOnlyModifiedCheckbox.addEventListener("change", () => {
+ // This checkbox:
+ // - Filters results to only modified prefs when search query is entered
+ // - Shows all modified prefs, in show all mode, and after initial checkbox click
+ let tableHidden = !document.body.classList.contains("table-shown");
+ filterPrefs({
+ showAll:
+ gFilterShowAll || (gShowOnlyModifiedCheckbox.checked && tableHidden),
+ });
+ });
+
+ showAll.addEventListener("click", event => {
+ search.focus();
+ search.value = "";
+ gFilterPrefsTask.disarm();
+ filterPrefs({ showAll: true });
+ });
+
+ function shouldBeginEdit(event) {
+ if (
+ event.target.localName != "button" &&
+ event.target.localName != "input"
+ ) {
+ let row = event.target.closest("tr");
+ return row && row._pref.exists;
+ }
+ return false;
+ }
+
+ // Disable double/triple-click text selection since that triggers edit/toggle.
+ prefs.addEventListener("mousedown", event => {
+ if (event.detail > 1 && shouldBeginEdit(event)) {
+ event.preventDefault();
+ }
+ });
+
+ prefs.addEventListener("click", event => {
+ if (event.detail == 2 && shouldBeginEdit(event)) {
+ event.target.closest("tr")._pref.editOrToggle();
+ return;
+ }
+
+ if (event.target.localName != "button") {
+ return;
+ }
+
+ let pref = event.target.closest("tr")._pref;
+ let button = event.target.closest("button");
+
+ if (button.classList.contains("button-add")) {
+ pref.isAddRow = false;
+ Preferences.set(pref.name, pref.value);
+ if (pref.type == "Boolean") {
+ pref.refreshClass();
+ } else {
+ pref.edit();
+ }
+ } else if (
+ button.classList.contains("button-toggle") ||
+ button.classList.contains("button-edit")
+ ) {
+ pref.editOrToggle();
+ } else if (button.classList.contains("button-save")) {
+ pref.save();
+ } else {
+ // This is "button-reset" or "button-delete".
+ pref.editing = false;
+ Services.prefs.clearUserPref(pref.name);
+ pref.editButton.focus();
+ }
+ });
+
+ window.addEventListener("keypress", event => {
+ if (event.target != search && event.key == "Escape" && gPrefInEdit) {
+ gPrefInEdit.endEdit();
+ }
+ });
+}
+
+function filterPrefs(options = {}) {
+ if (gPrefInEdit) {
+ gPrefInEdit.endEdit();
+ }
+ gDeletedPrefs.clear();
+
+ let searchName = gSearchInput.value.trim();
+ if (searchName.length < SEARCH_AUTO_MIN_CRARACTERS && !options.shortString) {
+ searchName = "";
+ }
+
+ gFilterString = searchName.toLowerCase();
+ gFilterShowAll = !!options.showAll;
+
+ gFilterPattern = null;
+ if (gFilterString.includes("*")) {
+ gFilterPattern = new RegExp(gFilterString.replace(/\*+/g, ".*"), "i");
+ gFilterString = "";
+ }
+
+ let showResults = gFilterString || gFilterPattern || gFilterShowAll;
+ document.body.classList.toggle("table-shown", showResults);
+
+ let prefArray = [];
+ if (showResults) {
+ if (!gSortedExistingPrefs) {
+ gSortedExistingPrefs = [...gExistingPrefs.values()];
+ gSortedExistingPrefs.sort((a, b) => a.name > b.name);
+ }
+ prefArray = gSortedExistingPrefs;
+ }
+
+ // The slowest operations tend to be the addition and removal of DOM nodes, so
+ // this algorithm tries to reduce removals by hiding nodes instead. This
+ // happens frequently when the set narrows while typing preference names. We
+ // iterate the nodes already in the table in parallel to those we want to
+ // show, because the two lists are sorted and they will often match already.
+ let fragment = null;
+ let indexInArray = 0;
+ let elementInTable = gPrefsTable.firstElementChild;
+ let odd = false;
+ let hasVisiblePrefs = false;
+ while (indexInArray < prefArray.length || elementInTable) {
+ // For efficiency, filter the array while we are iterating.
+ let prefInArray = prefArray[indexInArray];
+ if (prefInArray) {
+ if (!prefInArray.matchesFilter) {
+ indexInArray++;
+ continue;
+ }
+ prefInArray.hidden = false;
+ prefInArray.odd = odd;
+ }
+
+ let prefInTable = elementInTable && elementInTable._pref;
+ if (!prefInTable) {
+ // We're at the end of the table, we just have to insert all the matching
+ // elements that remain in the array. We can use a fragment to make the
+ // insertions faster, which is useful during the initial filtering.
+ if (!fragment) {
+ fragment = document.createDocumentFragment();
+ }
+ fragment.appendChild(prefInArray.getElement());
+ } else if (prefInTable == prefInArray) {
+ // We got two matching elements, we just need to update the visibility.
+ elementInTable = elementInTable.nextElementSibling;
+ } else if (prefInArray && prefInArray.name < prefInTable.name) {
+ // The iteration in the table is ahead of the iteration in the array.
+ // Insert or move the array element, and advance the array index.
+ gPrefsTable.insertBefore(prefInArray.getElement(), elementInTable);
+ } else {
+ // The iteration in the array is ahead of the iteration in the table.
+ // Hide the element in the table, and advance to the next element.
+ let nextElementInTable = elementInTable.nextElementSibling;
+ if (!prefInTable.exists) {
+ // Remove rows for deleted preferences, or temporary addition rows.
+ elementInTable.remove();
+ } else {
+ // Keep the element for the next filtering if the preference exists.
+ prefInTable.hidden = true;
+ prefInTable.refreshClass();
+ }
+ elementInTable = nextElementInTable;
+ continue;
+ }
+
+ prefInArray.refreshClass();
+ odd = !odd;
+ indexInArray++;
+ hasVisiblePrefs = true;
+ }
+
+ if (fragment) {
+ gPrefsTable.appendChild(fragment);
+ }
+
+ gPrefsTable.toggleAttribute("has-visible-prefs", hasVisiblePrefs);
+
+ if (searchName && !gExistingPrefs.has(searchName)) {
+ let addPrefRow = new PrefRow(searchName, { isAddRow: true });
+ addPrefRow.odd = odd;
+ gPrefsTable.appendChild(addPrefRow.getElement());
+ }
+
+ // We only start observing preference changes after the first search is done,
+ // so that newly added preferences won't appear while the page is still empty.
+ if (!gPrefObserverRegistered) {
+ gPrefObserverRegistered = true;
+ Services.prefs.addObserver("", gPrefObserver);
+ window.addEventListener(
+ "unload",
+ () => {
+ Services.prefs.removeObserver("", gPrefObserver);
+ },
+ { once: true }
+ );
+ }
+}
diff --git a/toolkit/components/aboutconfig/content/background.svg b/toolkit/components/aboutconfig/content/background.svg
new file mode 100644
index 0000000000..d03d081ee5
--- /dev/null
+++ b/toolkit/components/aboutconfig/content/background.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"><linearGradient id="a"><stop offset="0" stop-color="#ccfbff"/><stop offset="1" stop-color="#c9e4ff"/></linearGradient><linearGradient id="b" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="6.5576" x2="120.5576" href="#a" y1="318.3719" y2="152.8721"/><linearGradient id="c" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="37.205" x2="151.205" href="#a" y1="339.4831" y2="173.9827"/><linearGradient id="d"><stop offset="0" stop-color="#00c8d7"/><stop offset="1" stop-color="#0a84ff"/></linearGradient><linearGradient id="e" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="-51.3427" x2="242.3216" href="#d" y1="411.209" y2="56.9062"/><linearGradient id="f" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="98.7014" x2="116.0054" href="#d" y1="238.5525" y2="212.9205"/><linearGradient id="g" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="70.9844" x2="164.4848" href="#d" y1="291.4737" y2="152.9739"/><linearGradient id="h" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="53.6667" x2="192.6669" href="#a" y1="337.889" y2="159.389"/><linearGradient id="i" gradientTransform="matrix(1 0 0 -1 0 320)" gradientUnits="userSpaceOnUse" x1="16.7153" x2="201.7151" href="#d" y1="374.8648" y2="156.3646"/><g fill="#eaeaee"><path d="m174.2 162.1c16.4-3.1 26.8-7.5 26.8-12.5 0-9.3-35.7-16.8-79.8-16.8s-79.8 7.5-79.8 16.8c0 7.8 25.2 14.3 59.3 16.2-1.5 1.2-2.3 2.5-2.3 3.8 0 6.5 19.1 11.7 42.8 11.7s42.8-5.3 42.8-11.7c-.1-2.8-3.7-5.4-9.8-7.5z"/><ellipse cx="203.2" cy="165.4" rx="12.3" ry="4"/><ellipse cx="78.9" cy="171.1" rx="9.5" ry="3.3"/><ellipse cx="207.9" cy="156.9" rx="4.8" ry="2"/><path d="m53.6 93.2h123.4c.6 0 1.1-.5 1.1-1.1s-.5-1.1-1.1-1.1h-123.4c-.6 0-1.1.5-1.1 1.1s.5 1.1 1.1 1.1zm16.7-5.6h30.9c.3 0 .6-.3.6-.6s-.3-.6-.6-.6h-30.9c-.3 0-.6.3-.6.6s.3.6.6.6zm-38.9 10.5h13.4c.3 0 .6-.3.6-.6s-.3-.6-.6-.6h-13.4c-.3 0-.6.3-.6.6s.3.6.6.6zm22.3-1.1c-.3 0-.6.3-.6.6s.3.6.6.6h3.3c.3 0 .6-.3.6-.6s-.3-.6-.6-.6zm123.1.5c0 .3.3.6.6.6h3.3c.3 0 .6-.3.6-.6s-.3-.5-.6-.5h-3.3c-.3 0-.6.2-.6.5zm8.4.6h1.1c.3 0 .6-.3.6-.6s-.3-.6-.6-.6h-1.1c-.3 0-.6.3-.6.6s.3.6.6.6zm-122.6-1.1h-1.1c-.3 0-.6.3-.6.6s.3.6.6.6h1.1c.3 0 .6-.3.6-.6 0-.4-.3-.6-.6-.6zm133.7 1.1h13.4c.3 0 .6-.3.6-.6s-.3-.5-.6-.5h-13.4c-.3 0-.6.3-.6.6s.3.5.6.5zm-27.3-.6c0-.3-.3-.6-.6-.6h-13.4c-.3 0-.6.3-.6.6s.3.6.6.6h13.4c.4 0 .6-.3.6-.6zm52.5 29.2h-56.7c-.6 0-1.1.5-1.1 1.1s.5 1.1 1.1 1.1h56.7c.6 0 1.1-.5 1.1-1.1s-.5-1.1-1.1-1.1zm-162 0h-50.6c-.6 0-1.1.5-1.1 1.1s.5 1.1 1.1 1.1h50.7c.6 0 1.1-.5 1.1-1.1s-.6-1.1-1.2-1.1z"/></g><path d="m163.4 155.3-12.3-103.2c-.3-2.1-2-3.7-4.2-3.7h-62c-2.4 0-4.1 1.8-4.6 4l-19.3 92.3c-.3 1.2.1 2.5.9 3.5s2 1.5 3.3 1.5h27.1l.8 6.5c.3 2.1 2 3.7 4.2 3.7h62c1.2 0 2.3-.5 3.1-1.4s1.1-2 1-3.2z" fill="#fff"/><path d="m94.7 144.1-10-83.1-17.3 83.1z" fill="url(#b)"/><path d="m86.9 54.2 12 100.2h59l-11.9-100.2z" fill="url(#c)"/><path d="m161.1 155.7-12.3-103.2c-.1-.8-.7-1.3-1.5-1.3h-62c-.9 0-1.6.5-1.8 1.3l-19.4 92.9c-.1.4 0 .9.3 1.3.3.3.7.6 1.2.6h29.4l1.1 8.9c.1.8.7 1.3 1.5 1.3h62c.4 0 .8-.2 1.1-.5.3-.5.5-.9.4-1.3zm-79.3-80.4 8 68.9h-22.4c0-.1 14.4-68.9 14.4-68.9zm17.1 79.1-12-100.2h59l12 100.2z" fill="url(#e)"/><path d="m91 57.7 11.3 92.5h51.9l-11.3-92.5z" fill="#f9f9fa"/><path d="m108.3 91.1c-2.2-.1-4 1.6-4.2 3.8-.1 2.3 1.5 4.2 3.6 4.4.8.1 1.5-.1 2.1-.5.4-1.5 1-3.6 1.6-5.6-.6-1.2-1.8-2-3.1-2.1z" fill="url(#f)"/><path d="m143 105.5-11.9-5.2c-.5-.2-1.2-.2-1.6.2l-5.8 4.8-.1-.1-6.8-6.2 10.4-10.3c.6-.6.7-1.7.1-2.4s-1.6-.7-2.3-.1l-10.1 10c1.2-4.1 3-10.4 4.3-14.8.3-.9-.2-1.8-1.1-2.1-.8-.3-1.7.2-2 1.1 0 0-.7 2.5-1.7 5.6-.4 1.2-.7 2.5-1.1 3.9-2.9 9.9-2.8 10.1-2.8 10.6 0 .3.2.6.3.8l.1.1c.2.3.4.6.6.8l8.2 7.5.2.2.1.1 5.2 4.8-5.5 9.9-1.4 2.6c-.4.8-.2 1.8.6 2.3.3.2.6.3.9.2.5 0 1-.3 1.3-.8l7.6-13.7c.4-.7.3-1.6-.3-2.1l-4.3-4.1.1-.1 1.7-1.4 4.7-3.9 7.8 3.4 3.2 1.4c.2.1.5.1.7.1.6 0 1.1-.4 1.4-1 .5-.8.2-1.8-.7-2.1z" fill="url(#g)"/><path d="m139.2 30h-27.2c-2.4-4.4-8.4-14-15.9-15.4-10-1.9-12 8-12 8s-6.6-17.2-23.4-15c-18.5 2.5-10.2 21.7-10 22.4h-28.3c-.7 0-1.3.6-1.3 1.3s.6 1.3 1.3 1.3h116.8c.7 0 1.3-.6 1.3-1.3s-.6-1.3-1.3-1.3z" fill="#fff"/><path d="m138.4 27.6h-7.3c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h7.3c.4 0 .7.3.7.7s-.4.7-.7.7zm-19.1 0h-1.3c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h1.3c.4 0 .7.3.7.7-.1.4-.4.7-.7.7zm-68.1-.8h-2.1c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h1.1c-.1-.2-.2-.5-.3-.8s.1-.7.4-.8.7.1.8.4c.3 1 .6 1.6.6 1.6.1.2.1.4 0 .6 0 .3-.2.4-.5.4zm-12.6 0h-15.7c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h15.7c.4 0 .7.3.7.7s-.3.7-.7.7zm74.6 0c-.2 0-.5-.1-.6-.3-.4-.8-1.1-1.9-2-3.3-.2-.3-.1-.7.2-.9s.7-.1.9.2c.9 1.4 1.7 2.6 2.1 3.4.2.3.1.7-.3.9-.1-.1-.2 0-.3 0zm-63.8-6.8c-.3 0-.6-.2-.6-.6-.1-.4-.1-.9-.1-1.3s.2-.7.6-.7.7.2.7.6.1.9.1 1.3c0 .3-.3.7-.7.7zm34.1-2.9c-.2 0-.5-.1-.6-.3-.2-.3-.4-.7-.6-1.1-.2-.3-.1-.7.2-.9s.7-.1.9.2c.2.4.5.8.7 1.2.2.3.1.7-.3.9-.1 0-.2 0-.3 0zm20.8-1.6c-.2 0-.3-.1-.4-.2-2.5-2.1-5-3.5-7.4-3.9-2.4-.5-4.5-.2-6.3.7-.3.2-.7 0-.9-.3s0-.7.3-.9c2-1 4.4-1.3 7.1-.8s5.4 1.9 8.1 4.2c.3.2.3.6.1.9-.2.2-.4.3-.6.3zm-24.5-3.7c-.2 0-.4-.1-.5-.2-.9-1-1.8-1.9-2.7-2.7-.3-.2-.3-.6-.1-.9s.6-.3.9-.1c1 .8 2 1.8 2.9 2.8.2.3.2.7-.1.9-.1.1-.3.2-.4.2zm-27-3.9c-.2 0-.4-.1-.5-.2-.3-.3-.3-.7 0-.9 2.1-1.9 5.1-3.1 8.8-3.6 2.3-.3 4.5-.3 6.6.1.4.1.6.4.5.8s-.4.6-.8.5c-2-.3-4.1-.4-6.2-.1-3.5.5-6.2 1.6-8.1 3.3 0 .1-.2.1-.3.1z" fill="#eaeaee"/><path d="m233.4 63.7h-15.4c-1.3-2.5-4.7-7.8-8.9-8.6-5.6-1.1-6.7 4.5-6.7 4.5s-3.7-9.6-13.1-8.4c-10.5 1.4-5.6 12.5-5.6 12.5h-15.8.5-.2c-.7 0-1.3.6-1.3 1.3s.6 1.3 1.3 1.3h65.2c.7 0 1.3-.6 1.3-1.3s-.6-1.3-1.3-1.3z" fill="#fff"/><path d="m184.2 61.9h-15.8c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h15.8c.4 0 .7.3.7.7s-.3.7-.7.7zm48.6-.2h-.6c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h.6c.4 0 .7.3.7.7s-.3.7-.7.7zm-5.8 0h-4c-.4 0-.7-.3-.7-.7s.3-.7.7-.7h3.9c.4 0 .7.3.7.7s-.3.7-.6.7zm-24.2-3.9c-.3 0-.5-.2-.6-.4s-.1-.4.2-1.1c.6-1.8 2.3-4.5 5.8-4.5.5 0 1 0 1.5.1 2 .4 3.9 1.5 5.9 3.4.3.3.3.7 0 .9-.3.3-.7.3-.9 0-1.8-1.7-3.5-2.7-5.2-3-.4-.1-.8-.1-1.2-.1-3.8 0-4.6 3.9-4.7 4.1-.2.4-.5.6-.8.6zm-18.3-5.9c-.1 0-.3-.1-.4-.2-.3-.2-.3-.6-.1-.9.9-1 2.1-1.8 3.6-2.3.3-.1.7.1.8.4s-.1.7-.4.8c-1.3.4-2.3 1-3 1.9-.1.2-.3.3-.5.3zm9.8-2.2c-.1 0-.1 0-.2 0-.4-.1-.8-.2-1.2-.3s-.6-.4-.6-.7.4-.6.7-.6c.5.1.9.2 1.4.3.3.1.6.5.5.8 0 .3-.3.5-.6.5z" fill="#eaeaee"/><path d="m128.8 68.2h-21c-1.7 0-3-1.3-3-3 0-1.6 1.3-3 3-3h21c1.6 0 3 1.3 3 3 0 1.6-1.3 3-3 3z" fill="url(#h)"/><path d="m128.8 68.8h-21c-2 0-3.6-1.6-3.6-3.6s1.6-3.6 3.6-3.6h21c2 0 3.6 1.6 3.6 3.6 0 1.9-1.6 3.6-3.6 3.6zm-21-6c-1.3 0-2.4 1.1-2.4 2.4s1.1 2.4 2.4 2.4h21c1.3 0 2.4-1.1 2.4-2.4s-1.1-2.4-2.4-2.4z" fill="url(#i)"/></svg>
diff --git a/toolkit/components/aboutconfig/content/toggle.svg b/toolkit/components/aboutconfig/content/toggle.svg
new file mode 100644
index 0000000000..73f45b079e
--- /dev/null
+++ b/toolkit/components/aboutconfig/content/toggle.svg
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="context-fill">
+ <path d="M15 9H.5c-.39.39-.39 1.61 0 2l4.793 4.707a1 1 0 0 0 1.414-1.414L3.414 11H15a1 1 0 0 0 0-2zM1 6.988h14.5c.39-.39.39-1.61 0-2L10.707.28a1 1 0 0 0-1.414 1.414l3.293 3.293H1a1 1 0 0 0 0 2z"/>
+</svg>
+