/* 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 { EventEmitter } = ChromeUtils.importESModule( "resource://gre/modules/EventEmitter.sys.mjs" ); /** @import { SettingControlConfig } from "chrome://browser/content/preferences/widgets/setting-control.mjs" */ /** @import { SettingConfig, SettingValue } from "./Setting.mjs" */ /** * This is the interface for the async setting classes to implement. * * For the actual implementation see AsyncSettingMixin. */ export class AsyncSetting extends EventEmitter { static id = ""; /** @type {SettingConfig['controllingExtensionInfo']} */ static controllingExtensionInfo; /** @type {SettingValue} */ defaultValue = ""; defaultDisabled = false; defaultVisible = true; /** @type {Partial} */ defaultGetControlConfig = {}; /** * Emit a change event to notify listeners that the setting's data has * changed and should be updated. */ emitChange = () => { this.emit("change"); }; /** * Setup any external listeners that are required for managing this * setting's state. When the state needs to update the Setting.emitChange method should be called. * * @returns {ReturnType} Teardown function to clean up external listeners. */ setup() {} /** * Called before the setting values will be cached. You can start any shared * work here if you need the same value in multiple callbacks. * * @example * class Attendees extends AsyncSetting { * beforeRefresh() { * this.attendees = MeetingDb.getAttendees(); * } * get() { * return this.attendees; * } * async visible() { * return (await this.attendees).length; * } * } */ beforeRefresh() {} /** * Get the value of this setting. * * @abstract * @returns {Promise} */ async get() {} /** * Set the value of this setting. * * @abstract * @param {SettingValue} value The value from the input that triggered the update. * @returns {Promise} */ // eslint-disable-next-line no-unused-vars async set(value) {} /** * Whether the control should be disabled. * * @returns {Promise} */ async disabled() { return false; } /** * Whether the control should be visible. * * @returns {Promise} */ async visible() { return true; } /** * Override the initial control config. This will be spread into the * initial config, with this object taking precedence. * * @returns {Promise>} */ async getControlConfig() { return {}; } /** * Callback fired after a user has changed the setting's value. Useful for * recording telemetry. * * @param {SettingValue} value */ // eslint-disable-next-line no-unused-vars onUserChange(value) {} /** * Callback fired after a user has clicked a setting's control. * * @param {MouseEvent} event */ // eslint-disable-next-line no-unused-vars onUserClick(event) {} /** * Callback fired after a user has dismissed a message bar. * * @param {CustomEvent} event */ //eslint-disable-next-line no-unused-vars onMessageBarDismiss(event) {} /** * Callback fired after items in a reorderable list have been reordered. * This should be used to update the underlying data when the user * reorders items, such as updating preference values. * * @param {CustomEvent} event - reorder event with detail containing draggedElement, targetElement, position, draggedIndex, targetIndex */ //eslint-disable-next-line no-unused-vars onUserReorder(event) {} } /** * Wraps an AsyncSetting and adds caching of values to provide a synchronous * API to the Setting class. * * @implements {SettingConfig} */ export class AsyncSettingHandler { /** @type {AsyncSetting} */ asyncSetting; /** @type {Function} */ #emitChange; /** @type {string} */ pref; /** * Dependencies are not supported on AsyncSettings, but we include an empty * array for consistency with {@link SettingConfig}. * * @type {string[]} */ deps = []; /** @type {SettingConfig['controllingExtensionInfo']} */ controllingExtensionInfo; /** * @param {string} id * @param {typeof AsyncSetting} AsyncSettingClass */ constructor(id, AsyncSettingClass) { this.asyncSetting = new AsyncSettingClass(); this.id = id; this.controllingExtensionInfo = AsyncSettingClass.controllingExtensionInfo; this.#emitChange = () => {}; // Initialize cached values with defaults this.cachedValue = this.asyncSetting.defaultValue; this.cachedDisabled = this.asyncSetting.defaultDisabled; this.cachedVisible = this.asyncSetting.defaultVisible; this.cachedGetControlConfig = this.asyncSetting.defaultGetControlConfig; // Listen for change events from the async setting this.asyncSetting.on("change", () => this.refresh()); } /** * @param {() => any} emitChange * @returns {ReturnType} */ setup(emitChange) { let teardown = this.asyncSetting.setup(); this.#emitChange = emitChange; this.refresh(); return teardown; } /** * Called to trigger async tasks and re-cache values. */ async refresh() { this.asyncSetting.beforeRefresh(); [ this.cachedValue, this.cachedDisabled, this.cachedVisible, this.cachedGetControlConfig, ] = await Promise.all([ this.asyncSetting.get(), this.asyncSetting.disabled(), this.asyncSetting.visible(), this.asyncSetting.getControlConfig(), ]); this.#emitChange(); } /** * @returns {SettingValue} */ get() { return this.cachedValue; } /** * @param {any} value * @returns {Promise} */ set(value) { return this.asyncSetting.set(value); } /** * @returns {boolean} */ disabled() { return this.cachedDisabled; } /** * @returns {boolean} */ visible() { return this.cachedVisible; } /** * @param {SettingControlConfig} config * @returns {SettingControlConfig} */ getControlConfig(config) { return { ...config, ...this.cachedGetControlConfig, }; } /** * @param {SettingValue} value */ onUserChange(value) { return this.asyncSetting.onUserChange(value); } /** * @param {MouseEvent} event */ onUserClick(event) { this.asyncSetting.onUserClick(event); } /** * @param {CustomEvent} event */ onMessageBarDismiss(event) { this.asyncSetting.onMessageBarDismiss(event); } /** * @param {CustomEvent} event */ onUserReorder(event) { this.asyncSetting.onUserReorder(event); } }