diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mail/components/unifiedtoolbar/content/customization-palette.mjs | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/comm/mail/components/unifiedtoolbar/content/customization-palette.mjs b/comm/mail/components/unifiedtoolbar/content/customization-palette.mjs new file mode 100644 index 0000000000..d3d0417f7e --- /dev/null +++ b/comm/mail/components/unifiedtoolbar/content/customization-palette.mjs @@ -0,0 +1,243 @@ +/* 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 ListBoxSelection from "./list-box-selection.mjs"; +import "./customizable-element.mjs"; // eslint-disable-line import/no-unassigned-import + +const { getAvailableItemIdsForSpace, MULTIPLE_ALLOWED_ITEM_IDS } = + ChromeUtils.importESModule("resource:///modules/CustomizableItems.sys.mjs"); + +/** + * Customization palette containing items that can be added to a customization + * target. + * Attributes: + * - space: ID of the space the widgets are for. "all" for space agnostic + * widgets. Not observed. + * - items-in-use: Comma-separated IDs of items that are in a target at the time + * this is initialized. When changed, initialize should be called. + */ +class CustomizationPalette extends ListBoxSelection { + contextMenuId = "customizationPaletteMenu"; + + /** + * If this palette contains items (even if those items are currently all in a + * target). + * + * @type {boolean} + */ + isEmpty = false; + + /** + * Array of item IDs allowed to be in this palette. + * + * @type {string[]} + */ + #allAvailableItems = []; + + /** + * If this palette contains items that can be added to all spaces. + * + * @type {boolean} + */ + #allSpaces = false; + + connectedCallback() { + if (super.connectedCallback()) { + return; + } + + this.#allSpaces = this.getAttribute("space") === "all"; + + if (this.#allSpaces) { + document + .getElementById("customizationPaletteAddEverywhere") + .addEventListener("command", this.#handleMenuAddEverywhere); + } + + this.initialize(); + } + + /** + * Initializes the contents of the palette from the current state. The + * relevant state is defined by the space and items-in-use attributes. + */ + initialize() { + const itemIds = this.getAttribute("items-in-use").split(","); + this.setItems(itemIds); + } + + /** + * Update the items currently removed from the palette with an array of item + * IDs. + * + * @param {string[]} itemIds - Array of item IDs currently being used in a + * target. + */ + setItems(itemIds) { + let space = this.getAttribute("space"); + if (space === "all") { + space = undefined; + } + const itemsInUse = new Set(itemIds); + this.#allAvailableItems = getAvailableItemIdsForSpace(space); + this.isEmpty = !this.#allAvailableItems.length; + const items = this.#allAvailableItems.filter( + itemId => !itemsInUse.has(itemId) || MULTIPLE_ALLOWED_ITEM_IDS.has(itemId) + ); + this.replaceChildren( + ...items.map(itemId => { + const element = document.createElement("li", { + is: "customizable-element", + }); + element.setAttribute("item-id", itemId); + element.draggable = true; + return element; + }) + ); + } + + /** + * Overwritten context menu handler. Before showing the menu, initializes the + * menu with items for all the target areas available. + * + * @param {MouseEvent} event + */ + handleContextMenu = event => { + const menu = document.getElementById(this.contextMenuId); + const targets = this.getRootNode().querySelectorAll( + '[is="customization-target"]' + ); + const addEverywhereItem = document.getElementById( + "customizationPaletteAddEverywhere" + ); + addEverywhereItem.setAttribute("hidden", (!this.#allSpaces).toString()); + const menuItems = Array.from(targets, target => { + const menuItem = document.createXULElement("menuitem"); + document.l10n.setAttributes(menuItem, "customize-palette-add-to", { + target: target.name, + }); + menuItem.addEventListener( + "command", + this.#makeAddToTargetHandler(target) + ); + return menuItem; + }); + menuItems.push(addEverywhereItem); + menu.replaceChildren(...menuItems); + this.initializeContextMenu(event); + }; + + #handleMenuAddEverywhere = () => { + if (this.contextMenuFor) { + this.primaryAction(this.contextMenuFor); + this.dispatchEvent( + new CustomEvent("additem", { + detail: { + itemId: this.contextMenuFor.getAttribute("item-id"), + }, + bubbles: true, + composed: true, + }) + ); + } + }; + + /** + * Generate a context menu item event handler that will add the right clicked + * item to the target. + * + * @param {CustomizationTarget} target + * @returns {function} Context menu item event handler curried with the given + * target. + */ + #makeAddToTargetHandler(target) { + return () => { + if (this.contextMenuFor) { + this.primaryAction(this.contextMenuFor, target); + } + }; + } + + handleDragSuccess(item) { + if (item.allowMultiple) { + return; + } + super.handleDragSuccess(item); + } + + handleDrop(itemId, sibling, afterSibling) { + if (this.querySelector(`li[item-id="${itemId}"]`)?.allowMultiple) { + return; + } + super.handleDrop(itemId, sibling, afterSibling); + } + + canAddElement(itemId) { + return ( + this.#allAvailableItems.includes(itemId) && + (super.canAddElement(itemId) || + this.querySelector(`li[item-id="${itemId}"]`).allowMultiple) + ); + } + + /** + * The primary action for the palette is to add the item to a customization + * target. Will pick the first target if none is provided. + * + * @param {CustomizableElement} item - Item to move to a target. + * @param {CustomizationTarget} [target] - The target to move the item to. + * Defaults to the first target in the root. + */ + primaryAction(item, target) { + if (!target) { + target = this.getRootNode().querySelector('[is="customization-target"]'); + } + if (item?.allowMultiple) { + target.addItem(item.cloneNode(true)); + return; + } + if (super.primaryAction(item)) { + return; + } + target.addItem(item); + } + + /** + * Returns the item to this palette from some other place. + * + * @param {CustomizableElement} item - Item to return to this palette. + */ + returnItem(item) { + if (item.allowMultiple) { + item.remove(); + return; + } + this.append(item); + } + + /** + * Filter the items in the palette for the given string based on their label. + * The comparison is done on the lower cased label, and the filter string is + * lower cased as well. + * + * @param {string} filterString - String to filter the items by. + */ + filterItems(filterString) { + const lowerFilterString = filterString.toLowerCase(); + for (const item of this.children) { + item.hidden = !item.label.toLowerCase().includes(lowerFilterString); + } + } + + addItemById(itemId) { + const item = this.querySelector(`[item-id="${itemId}"]`); + if (!item) { + return; + } + this.primaryAction(item); + } +} +customElements.define("customization-palette", CustomizationPalette, { + extends: "ul", +}); |