697 lines
21 KiB
JavaScript
697 lines
21 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
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;
|
|
this.inputField.ariaLabel = this.name;
|
|
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 || this.isAddRow ? "" : "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", () => {
|
|
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 }
|
|
);
|
|
}
|
|
}
|