setting-group.mjs (5012B)
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 { html } from "chrome://global/content/vendor/lit.all.mjs"; 6 import { 7 SettingElement, 8 spread, 9 } from "chrome://browser/content/preferences/widgets/setting-element.mjs"; 10 11 /** @import { SettingElementConfig } from "chrome://browser/content/preferences/widgets/setting-element.mjs" */ 12 /** @import { SettingControlConfig, SettingControlEvent } from "../setting-control/setting-control.mjs" */ 13 /** @import { Preferences } from "chrome://global/content/preferences/Preferences.mjs" */ 14 15 /** 16 * @typedef {object} SettingGroupConfigExtensions 17 * @property {SettingControlConfig[]} items Array of SettingControlConfigs to render. 18 * @property {number} [headingLevel] A heading level to create the legend as (1-6). 19 * @property {boolean} [inProgress] 20 * Hide this section unless the browser.settings-redesign.enabled or 21 * browser.settings-redesign.<groupid>.enabled prefs are true. 22 */ 23 /** @typedef {SettingElementConfig & SettingGroupConfigExtensions} SettingGroupConfig */ 24 25 const CLICK_HANDLERS = new Set([ 26 "dialog-button", 27 "moz-box-button", 28 "moz-box-item", 29 "moz-box-link", 30 "moz-button", 31 "moz-box-group", 32 "moz-message-bar", 33 ]); 34 35 /** 36 * Enumish of attribute names used for changing setting-group and groupbox 37 * visibilities based on the visibility of child setting-controls. 38 */ 39 const HiddenAttr = Object.freeze({ 40 /** Attribute used to hide elements without using the hidden attribute. */ 41 Self: "data-hidden-by-setting-group", 42 /** Attribute used to signal that this element should not be searchable. */ 43 Search: "data-hidden-from-search", 44 }); 45 46 export class SettingGroup extends SettingElement { 47 constructor() { 48 super(); 49 50 /** 51 * @type {Preferences['getSetting'] | undefined} 52 */ 53 this.getSetting = undefined; 54 55 /** 56 * @type {SettingGroupConfig | undefined} 57 */ 58 this.config = undefined; 59 } 60 61 static properties = { 62 config: { type: Object }, 63 groupId: { type: String }, 64 getSetting: { type: Function }, 65 }; 66 67 static queries = { 68 controlEls: { all: "setting-control" }, 69 }; 70 71 createRenderRoot() { 72 return this; 73 } 74 75 async handleVisibilityChange() { 76 await this.updateComplete; 77 // @ts-expect-error bug 1997478 78 let hasVisibleControls = [...this.controlEls].some(el => !el.hidden); 79 let groupbox = /** @type {XULElement} */ (this.closest("groupbox")); 80 if (hasVisibleControls) { 81 if (this.hasAttribute(HiddenAttr.Self)) { 82 this.removeAttribute(HiddenAttr.Self); 83 this.removeAttribute(HiddenAttr.Search); 84 } 85 if (groupbox && groupbox.hasAttribute(HiddenAttr.Self)) { 86 groupbox.removeAttribute(HiddenAttr.Search); 87 groupbox.removeAttribute(HiddenAttr.Self); 88 } 89 } else { 90 this.setAttribute(HiddenAttr.Self, ""); 91 this.setAttribute(HiddenAttr.Search, "true"); 92 if (groupbox && !groupbox.hasAttribute(HiddenAttr.Search)) { 93 groupbox.setAttribute(HiddenAttr.Search, "true"); 94 groupbox.setAttribute(HiddenAttr.Self, ""); 95 } 96 } 97 } 98 99 async getUpdateComplete() { 100 let result = await super.getUpdateComplete(); 101 // @ts-expect-error bug 1997478 102 await Promise.all([...this.controlEls].map(el => el.updateComplete)); 103 return result; 104 } 105 106 /** 107 * Notify child controls when their input has fired an event. When controls 108 * are nested the parent receives events for the nested controls, so this is 109 * actually easier to manage here; it also registers fewer listeners. 110 * 111 * @param {SettingControlEvent<InputEvent>} e 112 */ 113 onChange(e) { 114 let inputEl = e.target; 115 inputEl.control?.onChange(inputEl); 116 } 117 118 /** 119 * Notify child controls when their input has been clicked. When controls 120 * are nested the parent receives events for the nested controls, so this is 121 * actually easier to manage here; it also registers fewer listeners. 122 * 123 * @param {SettingControlEvent<MouseEvent>} e 124 */ 125 onClick(e) { 126 let inputEl = e.target; 127 if (!CLICK_HANDLERS.has(inputEl.localName)) { 128 return; 129 } 130 inputEl.control?.onClick(e); 131 } 132 133 /** 134 * @param {SettingControlConfig} item 135 */ 136 itemTemplate(item) { 137 let setting = this.getSetting(item.id); 138 return html`<setting-control 139 .setting=${setting} 140 .config=${item} 141 .getSetting=${this.getSetting} 142 ></setting-control>`; 143 } 144 145 render() { 146 if (!this.config) { 147 return ""; 148 } 149 return html`<moz-fieldset 150 .headingLevel=${this.config.headingLevel} 151 @change=${this.onChange} 152 @toggle=${this.onChange} 153 @click=${this.onClick} 154 @visibility-change=${this.handleVisibilityChange} 155 ${spread(this.getCommonPropertyMapping(this.config))} 156 >${this.config.items.map(item => this.itemTemplate(item))}</moz-fieldset 157 >`; 158 } 159 } 160 customElements.define("setting-group", SettingGroup);