summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/unifiedtoolbar/content/customization-palette.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/unifiedtoolbar/content/customization-palette.mjs')
-rw-r--r--comm/mail/components/unifiedtoolbar/content/customization-palette.mjs243
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",
+});