summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/unifiedtoolbar/content/customizable-element.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/unifiedtoolbar/content/customizable-element.mjs')
-rw-r--r--comm/mail/components/unifiedtoolbar/content/customizable-element.mjs299
1 files changed, 299 insertions, 0 deletions
diff --git a/comm/mail/components/unifiedtoolbar/content/customizable-element.mjs b/comm/mail/components/unifiedtoolbar/content/customizable-element.mjs
new file mode 100644
index 0000000000..e503306fde
--- /dev/null
+++ b/comm/mail/components/unifiedtoolbar/content/customizable-element.mjs
@@ -0,0 +1,299 @@
+/* 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 CUSTOMIZABLE_ITEMS from "resource:///modules/CustomizableItemsDetails.mjs";
+
+const { EXTENSION_PREFIX } = ChromeUtils.importESModule(
+ "resource:///modules/CustomizableItems.sys.mjs"
+);
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
+});
+const browserActionFor = extensionId =>
+ lazy.ExtensionParent.apiManager.global.browserActionFor?.(
+ lazy.ExtensionParent.GlobalManager.getExtension(extensionId)
+ );
+
+/**
+ * Wrapper element for elements whose position can be customized.
+ *
+ * Template ID: #unifiedToolbarCustomizableElementTemplate
+ * Attributes:
+ * - item-id: ID of the customizable item this represents. Not observed.
+ * - disabled: Gets passed on to the live content.
+ */
+export default class CustomizableElement extends HTMLLIElement {
+ static get observedAttributes() {
+ return ["disabled", "tabindex"];
+ }
+
+ connectedCallback() {
+ if (this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ this.setAttribute("is", "customizable-element");
+
+ const template = document
+ .getElementById("unifiedToolbarCustomizableElementTemplate")
+ .content.cloneNode(true);
+
+ const itemId = this.getAttribute("item-id");
+
+ if (itemId.startsWith(EXTENSION_PREFIX)) {
+ const extensionId = itemId.slice(EXTENSION_PREFIX.length);
+ this.append(template);
+ this.#initializeForExtension(extensionId);
+ return;
+ }
+
+ const details = CUSTOMIZABLE_ITEMS.find(item => item.id === itemId);
+ if (!details) {
+ throw new Error(`Could not find definition for ${itemId}`);
+ }
+ this.append(template);
+ this.#initializeFromDetails(details).catch(console.error);
+ }
+
+ attributeChangedCallback(attribute) {
+ switch (attribute) {
+ case "disabled": {
+ const isDisabled = this.disabled;
+ for (const child of this.querySelector(".live-content")?.children ??
+ []) {
+ child.toggleAttribute("disabled", isDisabled);
+ }
+ break;
+ }
+ case "tabindex": {
+ const tabIndex = this.getAttribute("tabindex");
+ if (tabIndex === null) {
+ return;
+ }
+ if (this.details?.skipFocus && tabIndex !== "-1") {
+ this.removeAttribute("tabindex");
+ // Let the container know that an element that shouldn't be focused is
+ // currently marked with a tabindex instruction.
+ if (this.hasConnected) {
+ this.dispatchEvent(new CustomEvent("buttondisabled"));
+ }
+ return;
+ }
+ const tabIndexNumber = parseInt(tabIndex, 10);
+ for (const child of this.querySelector(".live-content")?.children ??
+ []) {
+ child.tabIndex = tabIndexNumber;
+ }
+ if (tabIndex !== "-1") {
+ this.removeAttribute("tabindex");
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Initialize the template contents from item details. Can't operate on the
+ * template directly due to being async.
+ *
+ * @param {CustomizableItemDetails} itemDetails
+ */
+ async #initializeFromDetails(itemDetails) {
+ if (this.details) {
+ return;
+ }
+ this.details = itemDetails;
+ this.classList.add(itemDetails.id);
+ if (Array.isArray(itemDetails.requiredModules)) {
+ await Promise.all(
+ itemDetails.requiredModules.map(module => {
+ return import(module); // eslint-disable-line no-unsanitized/method
+ })
+ );
+ }
+ if (itemDetails.templateId) {
+ const contentTemplate = document.getElementById(itemDetails.templateId);
+ this.querySelector(".live-content").append(
+ contentTemplate.content.cloneNode(true)
+ );
+ if (this.disabled) {
+ this.attributeChangedCallback("disabled");
+ }
+ }
+ if (itemDetails.skipFocus) {
+ this.classList.add("skip-focus");
+ }
+ if (this.hasAttribute("tabindex")) {
+ this.attributeChangedCallback("tabindex");
+ }
+ // We need to manually re-emit this event, since it might've been emitted
+ // after we cloned the template.
+ if (this.querySelector(".live-content button[disabled]")) {
+ this.dispatchEvent(new CustomEvent("buttondisabled"));
+ }
+ document.l10n.setAttributes(
+ this.querySelector(".preview-label"),
+ `${itemDetails.labelId}-label`
+ );
+ }
+
+ /**
+ * Initialize the contents of this customizable element for a button from an
+ * extension.
+ *
+ * @param {string} extensionId - ID of the extension the button is from.
+ */
+ async #initializeForExtension(extensionId) {
+ const extensionAction = browserActionFor(extensionId);
+ if (!extensionAction?.extension) {
+ return;
+ }
+ this.details = {
+ allowMultiple: false,
+ spaces: extensionAction.allowedSpaces ?? ["mail"],
+ };
+ if (!customElements.get("extension-action-button")) {
+ await import("./extension-action-button.mjs");
+ }
+ const { extension } = extensionAction;
+ this.classList.add("extension-action");
+ const extensionButton = document.createElement("button", {
+ is: "extension-action-button",
+ });
+ extensionButton.setAttribute("extension", extensionId);
+ this.querySelector(".live-content").append(extensionButton);
+ if (this.disabled) {
+ this.attributeChangedCallback("disabled");
+ }
+ if (this.hasAttribute("tabindex")) {
+ this.attributeChangedCallback("tabindex");
+ }
+ // We need to manually re-emit this event, since it might've been emitted
+ // before the button was attached to the DOM.
+ if (this.querySelector(".live-content button[disabled]")) {
+ this.dispatchEvent(new CustomEvent("buttondisabled"));
+ }
+ const previewLabel = this.querySelector(".preview-label");
+ const labelText = extension.name || extensionId;
+ previewLabel.textContent = labelText;
+ previewLabel.title = labelText;
+ const { IconDetails } = lazy.ExtensionParent;
+ if (extension.manifest.icons) {
+ let { icon } = IconDetails.getPreferredIcon(
+ extension.manifest.icons,
+ extension,
+ 16
+ );
+ let { icon: icon2x } = IconDetails.getPreferredIcon(
+ extension.manifest.icons,
+ extension,
+ 32
+ );
+ this.style.setProperty(
+ "--webextension-icon",
+ `url("${lazy.ExtensionParent.IconDetails.escapeUrl(icon)}")`
+ );
+ this.style.setProperty(
+ "--webextension-icon-2x",
+ `url("${lazy.ExtensionParent.IconDetails.escapeUrl(icon2x)}")`
+ );
+ }
+ }
+
+ /**
+ * Holds a reference to the palette this element belongs to.
+ *
+ * @type {CustomizationPalette}
+ */
+ get palette() {
+ const paletteClass = this.details.spaces?.length
+ ? "space-specific-palette"
+ : "generic-palette";
+ return this.getRootNode().querySelector(`.${paletteClass}`);
+ }
+
+ /**
+ * If multiple instances of this element are allowed in the same space.
+ *
+ * @type {boolean}
+ */
+ get allowMultiple() {
+ return Boolean(this.details?.allowMultiple);
+ }
+
+ /**
+ * Human readable label for the widget.
+ *
+ * @type {string}
+ */
+ get label() {
+ return this.querySelector(".preview-label").textContent;
+ }
+
+ /**
+ * Calls onTabSwitched on the first button contained in the live content.
+ * No-op if this item is disabled. Called by unified-toolbar's tab monitor.
+ *
+ * @param {TabInfo} tab - Tab that is now selected.
+ * @param {TabInfo} oldTab - Tab that was selected before.
+ */
+ onTabSwitched(tab, oldTab) {
+ if (this.disabled) {
+ return;
+ }
+ this.querySelector(".live-content button")?.onTabSwitched?.(tab, oldTab);
+ }
+
+ /**
+ * Calls onTabClosing on the first button contained in the live content.
+ * No-op if this item is disabled. Called by unified-toolbar's tab monitor.
+ *
+ * @param {TabInfo} tab - Tab that was closed.
+ */
+ onTabClosing(tab) {
+ if (this.disabled) {
+ return;
+ }
+ this.querySelector(".live-content button")?.onTabClosing?.(tab);
+ }
+
+ /**
+ * If this item can be added to all spaces.
+ *
+ * @type {boolean}
+ */
+ get allSpaces() {
+ return !this.details.spaces?.length;
+ }
+
+ /**
+ * If this item wants to provide its own context menu.
+ *
+ * @type {boolean}
+ */
+ get hasContextMenu() {
+ return Boolean(this.details?.hasContextMenu);
+ }
+
+ /**
+ * @type {boolean}
+ */
+ get disabled() {
+ return this.hasAttribute("disabled");
+ }
+
+ set disabled(value) {
+ this.toggleAttribute("disabled", value);
+ }
+
+ focus() {
+ this.querySelector(".live-content *:first-child")?.focus();
+ }
+}
+customElements.define("customizable-element", CustomizableElement, {
+ extends: "li",
+});