Revision control

Copy as Markdown

Other Tools

/* 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",
});