From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../components/aboutconfig/content/aboutconfig.css | 290 +++++++++ .../aboutconfig/content/aboutconfig.html | 108 ++++ .../components/aboutconfig/content/aboutconfig.js | 696 +++++++++++++++++++++ .../components/aboutconfig/content/background.svg | 4 + toolkit/components/aboutconfig/content/toggle.svg | 7 + 5 files changed, 1105 insertions(+) create mode 100644 toolkit/components/aboutconfig/content/aboutconfig.css create mode 100644 toolkit/components/aboutconfig/content/aboutconfig.html create mode 100644 toolkit/components/aboutconfig/content/aboutconfig.js create mode 100644 toolkit/components/aboutconfig/content/background.svg create mode 100644 toolkit/components/aboutconfig/content/toggle.svg (limited to 'toolkit/components/aboutconfig/content') 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 @@ + + + + + + + + + + + + + + + + + + +
+
+

+
+ +
+

+
+ +
+ + +
+ +
+ +
+
+ + + + 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 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 @@ + + 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 @@ + + + + + -- cgit v1.2.3