setting-element.mjs (5109B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { 6 Directive, 7 noChange, 8 nothing, 9 directive, 10 } from "chrome://global/content/vendor/lit.all.mjs"; 11 12 import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; 13 14 /** @import { AttributePart } from "chrome://global/content/vendor/lit.all.mjs" */ 15 16 /** 17 * @typedef {object} SettingElementConfig 18 * @property {string} [id] - The ID for the Setting, this should match the layout id 19 * @property {string} [l10nId] - The Fluent l10n ID for the setting 20 * @property {Record<string, string>} [l10nArgs] - An object containing l10n IDs and their values that will be translated with Fluent 21 * @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 22 * @property {string} [iconSrc] - A path to the icon for the control (if the control supports one) 23 * @property {string} [slot] - The named slot for the control 24 * @property {string} [supportPage] - The SUMO support page slug for the setting 25 * @property {string} [subcategory] - The sub-category slug used for direct linking to a setting from SUMO 26 */ 27 28 /** 29 * A Lit directive that applies all properties of an object to a DOM element. 30 * 31 * This directive interprets keys in the provided props object as follows: 32 * - Keys starting with `?` set or remove boolean attributes using `toggleAttribute`. 33 * - Keys starting with `.` set properties directly on the element. 34 * - Keys starting with `@` are currently not supported and will throw an error. 35 * - All other keys are applied as regular attributes using `setAttribute`. 36 * 37 * It avoids reapplying values that have not changed, but does not currently 38 * remove properties that were previously set and are no longer present in the new input. 39 * 40 * This directive is useful to "spread" an object of attributes/properties declaratively onto an 41 * element in a Lit template. 42 */ 43 class SpreadDirective extends Directive { 44 /** 45 * A record of previously applied properties to avoid redundant updates. 46 * 47 * @type {Record<string, unknown>} 48 */ 49 #prevProps = {}; 50 51 /** 52 * Render nothing by default as all changes are made in update using DOM APIs 53 * on the element directly. 54 * 55 * @param {Record<string, unknown>} props The props to apply to this element. 56 */ 57 // eslint-disable-next-line no-unused-vars 58 render(props) { 59 return nothing; 60 } 61 62 /** 63 * Apply props to the element using DOM APIs, updating only changed values. 64 * 65 * @param {AttributePart} part - The part of the template this directive is bound to. 66 * @param {[Record<string, unknown>]} propsArray - An array with a single object containing props to apply. 67 * @returns {typeof noChange} - Indicates to Lit that no re-render is needed. 68 */ 69 update(part, [props]) { 70 // TODO: This doesn't clear any values that were set in previous calls if 71 // they are no longer present. 72 // It isn't entirely clear to me (mstriemer) what we should do if a prop is 73 // removed, or if the prop has changed from say ?foo to foo. By not 74 // implementing the auto-clearing hopefully the consumer will do something 75 // that fits their use case. 76 77 let el = part.element; 78 79 for (let [key, value] of Object.entries(props)) { 80 // Skip if the value hasn't changed since the last update. 81 if (value === this.#prevProps[key]) { 82 continue; 83 } 84 85 // Update the element based on the property key matching Lit's templates: 86 // ?key -> el.toggleAttribute(key, value) 87 // .key -> el.key = value 88 // key -> el.setAttribute(key, value) 89 if (key.startsWith("?")) { 90 el.toggleAttribute(key.slice(1), Boolean(value)); 91 } else if (key.startsWith(".")) { 92 // @ts-ignore 93 el[key.slice(1)] = value; 94 } else if (key.startsWith("@")) { 95 throw new Error( 96 `Event listeners are not yet supported with spread (${key})` 97 ); 98 } else { 99 el.setAttribute(key, String(value)); 100 } 101 } 102 103 // Save current props for comparison in the next update. 104 this.#prevProps = props; 105 106 return noChange; 107 } 108 } 109 110 export const spread = directive(SpreadDirective); 111 112 export class SettingElement extends MozLitElement { 113 /** 114 * The default properties that the setting element accepts. 115 * 116 * @param {SettingElementConfig} config 117 */ 118 getCommonPropertyMapping(config) { 119 return { 120 id: config.id, 121 "data-l10n-id": config.l10nId ? config.l10nId : undefined, 122 "data-l10n-args": config.l10nArgs 123 ? JSON.stringify(config.l10nArgs) 124 : undefined, 125 ".iconSrc": config.iconSrc, 126 "data-subcategory": config.subcategory, 127 ".supportPage": 128 config.supportPage != undefined ? config.supportPage : undefined, 129 slot: config.slot, 130 ...config.controlAttrs, 131 }; 132 } 133 }