Source code
Revision control
Copy as Markdown
Other Tools
/* 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
import { AsyncSetting } from "chrome://global/content/preferences/AsyncSetting.mjs";
import { Preference } from "chrome://global/content/preferences/Preference.mjs";
import { Setting } from "chrome://global/content/preferences/Setting.mjs";
/** @import {PreferenceConfigInfo} from "chrome://global/content/preferences/Preference.mjs" */
/** @import {PreferenceSettingDepsMap} from "chrome://global/content/preferences/Setting.mjs" */
/**
* @callback PreferenceSettingVisibleFunction
* @param {PreferenceSettingDepsMap} deps
* @param {Setting} setting
* @returns {boolean | string | undefined} If truthy shows the setting in the UI, or hides it if not
*/
/**
* Gets the value of a {@link PreferencesSettingsConfig}.
*
* @callback PreferenceSettingGetter
* @param {string | number} val - The value that was retrieved from the preferences backend
* @param {PreferenceSettingDepsMap} deps
* @param {Setting} setting
* @returns {any} - The value to set onto the setting
*/
/**
* Sets the value of a {@link PreferencesSettingsConfig}.
*
* @callback PreferenceSettingSetter
* @param {string | undefined} val - The value/pressed/checked from the input (control) associated with the setting
* @param {PreferenceSettingDepsMap} deps
* @param {Setting} setting
* @returns {void}
*/
/**
* @callback PreferencesSettingOnUserChangeFunction
* @param {string} val - The value/pressed/checked from the input of the control associated with the setting
* @param {PreferenceSettingDepsMap} deps
* @param {Setting} setting
* @returns {void}
*/
/**
* @callback PreferencesSettingConfigDisabledFunction
* @param {PreferenceSettingDepsMap} deps
* @param {Setting} setting
* @returns {boolean}
*/
/**
* @callback PreferencesSettingGetControlConfigFunction
* @param {PreferencesSettingsConfig} config
* @param {PreferenceSettingDepsMap} deps
* @param {Setting} setting
* @returns {PreferencesSettingsConfig | undefined}
*/
/**
* @callback PreferencesSettingConfigTeardownFunction
* @returns {void}
*/
/**
* @callback PreferencesSettingConfigSetupFunction
* @param {Function} emitChange
* @param {PreferenceSettingDepsMap} deps
* @param {Setting} setting
* @returns {PreferencesSettingConfigTeardownFunction | void}
*/
/**
* @callback PreferencesSettingConfigOnUserClickFunction
* @param {Event} event
* @returns {void}
*/
/**
* @typedef {object} PreferencesSettingsConfig
* @property {string} id - The ID for the Setting, this should match the layout id
* @property {string} [l10nId]
* @property {string} [pref] - A {@link Services.prefs} id that will be used as the backend if it is provided
* @property {PreferenceSettingVisibleFunction} [visible] - Function to determine if a setting is visible in the UI
* @property {PreferenceSettingGetter} [get] - Function to get the value of the setting. Optional if {@link PreferencesSettingsConfig#pref} is set.
* @property {PreferenceSettingSetter} [set] - Function to set the value of the setting. Optional if {@link PreferencesSettingsConfig#pref} is set.
* @property {PreferencesSettingGetControlConfigFunction} [getControlConfig] - Function that allows the setting to modify its layout, this is intended to be used to provide the options, {@link PreferencesSettingsConfig#l10nId} or {@link PreferencesSettingsConfig#l10nArgs} data if necessary, but technically it can change anything (that doesn't mean it will have any effect though).
* @property {PreferencesSettingOnUserChangeFunction} [onUserChange] - A function that will be called when the setting
* has been modified by the user, it is passed the value/pressed/checked from its input. NOTE: This should be used for
* additional work that needs to happen, such as recording telemetry.
* If you want to set the value of the Setting then use the {@link PreferencesSettingsConfig.set} function.
* @property {Array<PreferencesSettingsConfig> | undefined} [items]
* @property {string | undefined} [control]
* @property {PreferencesSettingConfigSetupFunction} [setup] - A function to be called to register listeners for
* the setting. It should return a {@link PreferencesSettingConfigTeardownFunction} function to
* remove the listeners if necessary. This should emit change events when the setting has changed to
* ensure the UI stays in sync if possible.
* @property {PreferencesSettingConfigDisabledFunction} [disabled] - A function to determine if a setting should be disabled
* @property {PreferencesSettingConfigOnUserClickFunction} [onUserClick] - A function that will be called when a setting has been
* clicked, the element name must be included in the CLICK_HANDLERS array
* in {@link file://./../../browser/components/preferences/widgets/setting-group/setting-group.mjs}. This should be
* used for controls that aren’t regular form controls but instead perform an action when clicked, like a button or link.
* @property {Array<string> | void} [deps] - An array of setting IDs that this setting depends on, when these settings change this setting will emit a change event to update the UI
* @property {Record<string, any>} [controlAttrs] - An object of additional attributes to be set on the control. These can be used to further customize the control for example a message bar of the warning type, or what dialog a button should open
*/
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
});
const domContentLoadedPromise = new Promise(resolve => {
window.addEventListener("DOMContentLoaded", resolve, {
capture: true,
once: true,
});
});
export const Preferences = {
/**
* @type {Record<string, Preference>}
*/
_all: {},
/**
* @type {Map<string, Setting>}
*/
_settings: new Map(),
/**
* @param {PreferenceConfigInfo} prefInfo
*/
_add(prefInfo) {
if (this._all[prefInfo.id]) {
throw new Error(`preference with id '${prefInfo.id}' already added`);
}
const pref = new Preference(prefInfo);
this._all[pref.id] = pref;
domContentLoadedPromise.then(() => {
if (!this.updateQueued) {
pref.updateElements();
}
});
return pref;
},
/**
* @param {PreferenceConfigInfo} prefInfo
* @returns {Preference}
*/
add(prefInfo) {
const pref = this._add(prefInfo);
return pref;
},
/**
* @param {Array<PreferenceConfigInfo>} prefInfos
*/
addAll(prefInfos) {
prefInfos.map(prefInfo => this._add(prefInfo));
},
/**
* @param {string} id
* @returns {Preference | null}
*/
get(id) {
return this._all[id] || null;
},
/**
* @returns {Array<PreferenceConfigInfo>}
*/
getAll() {
return Object.values(this._all);
},
/**
* A configuration object that adds an element (control) associated with a pref,
* that includes all of the configuration for the control
* such as its Fluent strings, support page, subcategory etc.
*
* @param {PreferencesSettingsConfig} settingConfig
*/
addSetting(settingConfig) {
this._settings.set(
settingConfig.id,
new Setting(settingConfig.id, settingConfig)
);
},
/**
* @param {string} settingId
* @returns {Setting | undefined}
*/
getSetting(settingId) {
return this._settings.get(settingId);
},
defaultBranch: Services.prefs.getDefaultBranch(""),
get type() {
return document.documentElement.getAttribute("type") || "";
},
get instantApply() {
// The about:preferences page forces instantApply.
// TODO: Remove forceEnableInstantApply in favor of always applying in a
if (this._instantApplyForceEnabled) {
return true;
}
// Dialogs of type="child" are never instantApply.
return this.type !== "child";
},
_instantApplyForceEnabled: false,
// Override the computed value of instantApply for this window.
forceEnableInstantApply() {
this._instantApplyForceEnabled = true;
},
observe(subject, topic, data) {
const pref = this._all[data];
if (pref) {
pref.value = pref.valueFromPreferences;
}
},
updateQueued: false,
queueUpdateOfAllElements() {
if (this.updateQueued) {
return;
}
this.updateQueued = true;
Services.tm.dispatchToMainThread(() => {
let startTime = ChromeUtils.now();
const elements = document.querySelectorAll("[preference]");
for (const element of elements) {
const id = element.getAttribute("preference");
let preference = this.get(id);
if (!preference) {
console.error(`Missing preference for ID ${id}`);
continue;
}
preference.setElementValue(element);
}
ChromeUtils.addProfilerMarker(
"Preferences",
{ startTime },
`updateAllElements: ${elements.length} preferences updated`
);
this.updateQueued = false;
});
},
onUnload() {
this._settings.forEach(setting => setting?.destroy?.());
Services.prefs.removeObserver("", this);
},
QueryInterface: ChromeUtils.generateQI(["nsITimerCallback", "nsIObserver"]),
_deferredValueUpdateElements: new Set(),
writePreferences(aFlushToDisk) {
// Write all values to preferences.
if (this._deferredValueUpdateElements.size) {
this._finalizeDeferredElements();
}
const preferences = Preferences.getAll();
for (const preference of preferences) {
preference.batching = true;
preference.valueFromPreferences = preference.value;
preference.batching = false;
}
if (aFlushToDisk) {
Services.prefs.savePrefFile(null);
}
},
getPreferenceElement(aStartElement) {
let temp = aStartElement;
while (
temp &&
temp.nodeType == Node.ELEMENT_NODE &&
!temp.hasAttribute("preference")
) {
temp = temp.parentNode;
}
return temp && temp.nodeType == Node.ELEMENT_NODE ? temp : aStartElement;
},
_deferredValueUpdate(aElement) {
delete aElement._deferredValueUpdateTask;
const prefID = aElement.getAttribute("preference");
const preference = Preferences.get(prefID);
const prefVal = preference.getElementValue(aElement);
preference.value = prefVal;
this._deferredValueUpdateElements.delete(aElement);
},
_finalizeDeferredElements() {
for (const el of this._deferredValueUpdateElements) {
if (el._deferredValueUpdateTask) {
el._deferredValueUpdateTask.finalize();
}
}
},
userChangedValue(aElement) {
const element = this.getPreferenceElement(aElement);
if (element.hasAttribute("preference")) {
if (element.getAttribute("delayprefsave") != "true") {
const preference = Preferences.get(element.getAttribute("preference"));
const prefVal = preference.getElementValue(element);
preference.value = prefVal;
} else {
if (!element._deferredValueUpdateTask) {
element._deferredValueUpdateTask = new lazy.DeferredTask(
this._deferredValueUpdate.bind(this, element),
1000
);
this._deferredValueUpdateElements.add(element);
} else {
// Each time the preference is changed, restart the delay.
element._deferredValueUpdateTask.disarm();
}
element._deferredValueUpdateTask.arm();
}
}
},
onCommand(event) {
// This "command" event handler tracks changes made to preferences by
// the user in this window.
if (event.sourceEvent) {
event = event.sourceEvent;
}
this.userChangedValue(event.target);
},
onChange(event) {
// This "change" event handler tracks changes made to preferences by
// the user in this window.
this.userChangedValue(event.target);
},
onInput(event) {
// This "input" event handler tracks changes made to preferences by
// the user in this window.
this.userChangedValue(event.target);
},
_fireEvent(aEventName, aTarget) {
try {
const event = new CustomEvent(aEventName, {
bubbles: true,
cancelable: true,
});
return aTarget.dispatchEvent(event);
} catch (e) {
console.error(e);
}
return false;
},
onDialogAccept(event) {
let dialog = document.querySelector("dialog");
if (!this._fireEvent("beforeaccept", dialog)) {
event.preventDefault();
return false;
}
this.writePreferences(true);
return true;
},
close(event) {
if (Preferences.instantApply) {
window.close();
}
event.stopPropagation();
event.preventDefault();
},
handleEvent(event) {
switch (event.type) {
case "toggle":
case "change":
return this.onChange(event);
case "command":
return this.onCommand(event);
case "dialogaccept":
return this.onDialogAccept(event);
case "input":
return this.onInput(event);
case "unload":
return this.onUnload(event);
default:
return undefined;
}
},
_syncFromPrefListeners: new WeakMap(),
_syncToPrefListeners: new WeakMap(),
addSyncFromPrefListener(aElement, callback) {
this._syncFromPrefListeners.set(aElement, callback);
if (this.updateQueued) {
return;
}
// Make sure elements are updated correctly with the listener attached.
let elementPref = aElement.getAttribute("preference");
if (elementPref) {
let pref = this.get(elementPref);
if (pref) {
pref.updateElements();
}
}
},
addSyncToPrefListener(aElement, callback) {
this._syncToPrefListeners.set(aElement, callback);
if (this.updateQueued) {
return;
}
// Make sure elements are updated correctly with the listener attached.
let elementPref = aElement.getAttribute("preference");
if (elementPref) {
let pref = this.get(elementPref);
if (pref) {
pref.updateElements();
}
}
},
removeSyncFromPrefListener(aElement) {
this._syncFromPrefListeners.delete(aElement);
},
removeSyncToPrefListener(aElement) {
this._syncToPrefListeners.delete(aElement);
},
AsyncSetting,
Preference,
Setting,
};
Services.prefs.addObserver("", Preferences);
window.addEventListener("toggle", Preferences);
window.addEventListener("change", Preferences);
window.addEventListener("command", Preferences);
window.addEventListener("dialogaccept", Preferences);
window.addEventListener("input", Preferences);
window.addEventListener("select", Preferences);
window.addEventListener("unload", Preferences, { once: true });