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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
Directive,
noChange,
nothing,
directive,
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
/** @import { AttributePart } from "chrome://global/content/vendor/lit.all.mjs" */
/**
* @typedef {object} SettingElementConfig
* @property {string} [id] - The ID for the Setting, this should match the layout id
* @property {string} [l10nId] - The Fluent l10n ID for the setting
* @property {Record<string, string>} [l10nArgs] - An object containing l10n IDs and their values that will be translated with Fluent
* @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
* @property {string} [iconSrc] - A path to the icon for the control (if the control supports one)
* @property {string} [slot] - The named slot for the control
* @property {string} [supportPage] - The SUMO support page slug for the setting
* @property {string} [subcategory] - The sub-category slug used for direct linking to a setting from SUMO
*/
/**
* A Lit directive that applies all properties of an object to a DOM element.
*
* This directive interprets keys in the provided props object as follows:
* - Keys starting with `?` set or remove boolean attributes using `toggleAttribute`.
* - Keys starting with `.` set properties directly on the element.
* - Keys starting with `@` are currently not supported and will throw an error.
* - All other keys are applied as regular attributes using `setAttribute`.
*
* It avoids reapplying values that have not changed, but does not currently
* remove properties that were previously set and are no longer present in the new input.
*
* This directive is useful to "spread" an object of attributes/properties declaratively onto an
* element in a Lit template.
*/
class SpreadDirective extends Directive {
/**
* A record of previously applied properties to avoid redundant updates.
*
* @type {Record<string, unknown>}
*/
#prevProps = {};
/**
* Render nothing by default as all changes are made in update using DOM APIs
* on the element directly.
*
* @param {Record<string, unknown>} props The props to apply to this element.
*/
// eslint-disable-next-line no-unused-vars
render(props) {
return nothing;
}
/**
* Apply props to the element using DOM APIs, updating only changed values.
*
* @param {AttributePart} part - The part of the template this directive is bound to.
* @param {[Record<string, unknown>]} propsArray - An array with a single object containing props to apply.
* @returns {typeof noChange} - Indicates to Lit that no re-render is needed.
*/
update(part, [props]) {
// TODO: This doesn't clear any values that were set in previous calls if
// they are no longer present.
// It isn't entirely clear to me (mstriemer) what we should do if a prop is
// removed, or if the prop has changed from say ?foo to foo. By not
// implementing the auto-clearing hopefully the consumer will do something
// that fits their use case.
let el = part.element;
for (let [key, value] of Object.entries(props)) {
// Skip if the value hasn't changed since the last update.
if (value === this.#prevProps[key]) {
continue;
}
// Update the element based on the property key matching Lit's templates:
// ?key -> el.toggleAttribute(key, value)
// .key -> el.key = value
// key -> el.setAttribute(key, value)
if (key.startsWith("?")) {
el.toggleAttribute(key.slice(1), Boolean(value));
} else if (key.startsWith(".")) {
// @ts-ignore
el[key.slice(1)] = value;
} else if (key.startsWith("@")) {
throw new Error(
`Event listeners are not yet supported with spread (${key})`
);
} else {
el.setAttribute(key, String(value));
}
}
// Save current props for comparison in the next update.
this.#prevProps = props;
return noChange;
}
}
export const spread = directive(SpreadDirective);
export class SettingElement extends MozLitElement {
/**
* The default properties that the setting element accepts.
*
* @param {SettingElementConfig} config
*/
getCommonPropertyMapping(config) {
return {
id: config.id,
"data-l10n-id": config.l10nId ? config.l10nId : undefined,
"data-l10n-args": config.l10nArgs
? JSON.stringify(config.l10nArgs)
: undefined,
".iconSrc": config.iconSrc,
"data-subcategory": config.subcategory,
".supportPage":
config.supportPage != undefined ? config.supportPage : undefined,
slot: config.slot,
...config.controlAttrs,
};
}
}