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 } = ChromeUtils.importESModule(
);
/**
* 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",
});