/* 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 } = ChromeUtils.importESModule( "resource:///modules/CustomizableItems.sys.mjs" ); /** * Customization target where items can be placed, rearranged and removed. * Attributes: * - aria-label: Name of the target area. * - current-items: Comma separated item IDs currently in this area. When * changed initialize should be called. * Events: * - itemchange: Fired whenever the items inside the toolbar are added, moved or * removed. * - space: The space this target is in. */ class CustomizationTarget extends ListBoxSelection { contextMenuId = "customizationTargetMenu"; actionKey = "Delete"; canMoveItems = true; connectedCallback() { if (super.connectedCallback()) { return; } document .getElementById("customizationTargetForward") .addEventListener("command", this.#handleMenuForward); document .getElementById("customizationTargetBackward") .addEventListener("command", this.#handleMenuBackward); document .getElementById("customizationTargetRemove") .addEventListener("command", this.#handleMenuRemove); document .getElementById("customizationTargetRemoveEverywhere") .addEventListener("command", this.#handleMenuRemoveEverywhere); document .getElementById("customizationTargetAddEverywhere") .addEventListener("command", this.#handleMenuAddEverywhere); document .getElementById("customizationTargetStart") .addEventListener("command", this.#handleMenuStart); document .getElementById("customizationTargetEnd") .addEventListener("command", this.#handleMenuEnd); this.initialize(); } /** * Initialize the contents of the target from the current state. The relevant * state is passed in via the current-items attribute. */ initialize() { const itemIds = this.getAttribute("current-items").split(","); this.setItems(itemIds); } /** * Update the items in the target from an array of item IDs. * * @param {string[]} itemIds - ordered array of IDs of the items currently in * the target */ setItems(itemIds) { const childCount = this.children.length; const availableItems = getAvailableItemIdsForSpace( this.getAttribute("space"), true ); this.replaceChildren( ...itemIds.map(itemId => { const element = document.createElement("li", { is: "customizable-element", }); element.setAttribute("item-id", itemId); element.setAttribute("disabled", "disabled"); element.classList.toggle("collapsed", !availableItems.includes(itemId)); element.draggable = true; return element; }) ); if (childCount) { this.#onChange(); } } /** * Human-readable name of the customization target area. * * @type {string} */ get name() { return this.getAttribute("aria-label"); } handleContextMenu = event => { this.initializeContextMenu(event); const notForAllSpaces = !this.contextMenuFor.allSpaces; const removeEverywhereItem = document.getElementById( "customizationTargetRemoveEverywhere" ); const addEverywhereItem = document.getElementById( "customizationTargetAddEverywhere" ); addEverywhereItem.setAttribute("hidden", notForAllSpaces.toString()); removeEverywhereItem.setAttribute("hidden", notForAllSpaces.toString()); if (!notForAllSpaces) { const customization = this.getRootNode().host.closest( "unified-toolbar-customization" ); const itemId = this.contextMenuFor.getAttribute("item-id"); addEverywhereItem.disabled = !this.contextMenuFor.allowMultiple && customization.activeInAllSpaces(itemId); removeEverywhereItem.disabled = this.contextMenuFor.allowMultiple || !customization.activeInMultipleSpaces(itemId); } const isFirstElement = this.contextMenuFor === this.firstElementChild; const isLastElement = this.contextMenuFor === this.lastElementChild; document.getElementById("customizationTargetBackward").disabled = isFirstElement; document.getElementById("customizationTargetForward").disabled = isLastElement; document.getElementById("customizationTargetStart").disabled = isFirstElement; document.getElementById("customizationTargetEnd").disabled = isLastElement; }; /** * Event handler when the context menu item to move the item forward is * selected. */ #handleMenuForward = () => { if (this.contextMenuFor) { this.moveItemForward(this.contextMenuFor); } }; /** * Event handler when the context menu item to move the item backward is * selected. */ #handleMenuBackward = () => { if (this.contextMenuFor) { this.moveItemBackward(this.contextMenuFor); } }; /** * Event handler when the context menu item to remove the item is selected. */ #handleMenuRemove = () => { if (this.contextMenuFor) { this.primaryAction(this.contextMenuFor); } }; #handleMenuRemoveEverywhere = () => { if (this.contextMenuFor) { this.primaryAction(this.contextMenuFor); this.dispatchEvent( new CustomEvent("removeitem", { detail: { itemId: this.contextMenuFor.getAttribute("item-id"), }, bubbles: true, composed: true, }) ); } }; #handleMenuAddEverywhere = () => { if (this.contextMenuFor) { this.dispatchEvent( new CustomEvent("additem", { detail: { itemId: this.contextMenuFor.getAttribute("item-id"), }, bubbles: true, composed: true, }) ); } }; #handleMenuStart = () => { if (this.contextMenuFor) { this.moveItemToStart(this.contextMenuFor); } }; #handleMenuEnd = () => { if (this.contextMenuFor) { this.moveItemToEnd(this.contextMenuFor); } }; /** * Emit a change event. Should be called whenever items are added, moved or * removed from the target. */ #onChange() { const changeEvent = new Event("itemchange", { bubbles: true, // Make sure this bubbles out of the pane shadow root. composed: true, }); this.dispatchEvent(changeEvent); } /** * Adopt an item from another list into this one. * * @param {?CustomizableElement} item - Item from another list. */ #adoptItem(item) { item?.setAttribute("disabled", "disabled"); } moveItemForward(...args) { super.moveItemForward(...args); this.#onChange(); } moveItemBackward(...args) { super.moveItemBackward(...args); this.#onChange(); } moveItemToStart(...args) { super.moveItemToStart(...args); this.#onChange(); } moveItemToEnd(...args) { super.moveItemToEnd(...args); this.#onChange(); } handleDrop(itemId, sibling, afterSibling) { const item = super.handleDrop(itemId, sibling, afterSibling); if (item) { this.#adoptItem(item); this.#onChange(); } } handleDragSuccess(item) { super.handleDragSuccess(item); this.#onChange(); } /** * Return the item to its palette, removing it from this target. * * @param {CustomizableElement} item - The item to remove. */ primaryAction(item) { if (super.primaryAction(item)) { return; } item.palette.returnItem(item); this.#onChange(); } /** * Add an item to the end of this customization target. * * @param {CustomizableElement} item - The item to add. */ addItem(item) { if (!item) { return; } this.#adoptItem(item); this.append(item); this.#onChange(); } removeItemById(itemId) { const item = this.querySelector(`[item-id="${itemId}"]`); if (!item) { return; } this.primaryAction(item); } /** * Check if an item is currently used in this target. * * @param {string} itemId - Item ID of the item to check for. * @returns {boolean} If the item is currently used in this target. */ hasItem(itemId) { return Boolean(this.querySelector(`[item-id="${itemId}"]`)); } /** * IDs of the items currently in this target, in correct order including * duplicates. * * @type {string[]} */ get itemIds() { return Array.from(this.children, element => element.getAttribute("item-id") ); } /** * If the contents of this target differ from the currently saved * configuration. * * @type {boolean} */ get hasChanges() { return this.itemIds.join(",") !== this.getAttribute("current-items"); } } customElements.define("customization-target", CustomizationTarget, { extends: "ul", });