diff options
Diffstat (limited to 'comm/mail/base/content/widgets/customizable-toolbar.js')
-rw-r--r-- | comm/mail/base/content/widgets/customizable-toolbar.js | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/comm/mail/base/content/widgets/customizable-toolbar.js b/comm/mail/base/content/widgets/customizable-toolbar.js new file mode 100644 index 0000000000..350e814716 --- /dev/null +++ b/comm/mail/base/content/widgets/customizable-toolbar.js @@ -0,0 +1,319 @@ +/* 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/. */ + +"use strict"; + +/* globals MozXULElement */ + +// Wrap in a block to prevent leaking to window scope. +{ + /** + * Extends the built-in `toolbar` element to allow it to be customized. + * + * @augments {MozXULElement} + */ + class CustomizableToolbar extends MozXULElement { + connectedCallback() { + if (this.delayConnectedCallback() || this._hasConnected) { + return; + } + this._hasConnected = true; + + this._toolbox = null; + this._newElementCount = 0; + + // Search for the toolbox palette in the toolbar binding because + // toolbars are constructed first. + let toolbox = this.toolbox; + if (!toolbox) { + return; + } + + if (!toolbox.palette) { + // Look to see if there is a toolbarpalette. + let node = toolbox.firstElementChild; + while (node) { + if (node.localName == "toolbarpalette") { + break; + } + node = node.nextElementSibling; + } + + if (!node) { + return; + } + + // Hold on to the palette but remove it from the document. + toolbox.palette = node; + toolbox.removeChild(node); + } + + // Build up our contents from the palette. + let currentSet = + this.getAttribute("currentset") || this.getAttribute("defaultset"); + + if (currentSet) { + this.currentSet = currentSet; + } + } + + /** + * Get the toolbox element connected to this toolbar. + * + * @returns {Element?} The toolbox element or null. + */ + get toolbox() { + if (this._toolbox) { + return this._toolbox; + } + + let toolboxId = this.getAttribute("toolboxid"); + if (toolboxId) { + let toolbox = document.getElementById(toolboxId); + if (!toolbox) { + let tbName = this.hasAttribute("toolbarname") + ? ` (${this.getAttribute("toolbarname")})` + : ""; + + throw new Error( + `toolbar ID ${this.id}${tbName}: toolboxid attribute '${toolboxId}' points to a toolbox that doesn't exist` + ); + } + this._toolbox = toolbox; + return this._toolbox; + } + + this._toolbox = + this.parentNode && this.parentNode.localName == "toolbox" + ? this.parentNode + : null; + + return this._toolbox; + } + + /** + * Sets the current set of items in the toolbar. + * + * @param {string} val - Comma-separated list of IDs or "__empty". + * @returns {string} Comma-separated list of IDs or "__empty". + */ + set currentSet(val) { + if (val == this.currentSet) { + return; + } + + // Build a cache of items in the toolbarpalette. + let palette = this.toolbox ? this.toolbox.palette : null; + let paletteChildren = palette ? palette.children : []; + + let paletteItems = {}; + + for (let item of paletteChildren) { + paletteItems[item.id] = item; + } + + let ids = val == "__empty" ? [] : val.split(","); + let children = this.children; + let nodeidx = 0; + let added = {}; + + // Iterate over the ids to use on the toolbar. + for (let id of ids) { + // Iterate over the existing nodes on the toolbar. nodeidx is the + // spot where we want to insert items. + let found = false; + for (let i = nodeidx; i < children.length; i++) { + let curNode = children[i]; + if (this._idFromNode(curNode) == id) { + // The node already exists. If i equals nodeidx, we haven't + // iterated yet, so the item is already in the right position. + // Otherwise, insert it here. + if (i != nodeidx) { + this.insertBefore(curNode, children[nodeidx]); + } + + added[curNode.id] = true; + nodeidx++; + found = true; + break; + } + } + if (found) { + // Move on to the next id. + continue; + } + + // The node isn't already on the toolbar, so add a new one. + let nodeToAdd = paletteItems[id] || this._getToolbarItem(id); + if (nodeToAdd && !(nodeToAdd.id in added)) { + added[nodeToAdd.id] = true; + this.insertBefore(nodeToAdd, children[nodeidx] || null); + nodeToAdd.setAttribute("removable", "true"); + nodeidx++; + } + } + + // Remove any leftover removable nodes. + for (let i = children.length - 1; i >= nodeidx; i--) { + let curNode = children[i]; + + let curNodeId = this._idFromNode(curNode); + // Skip over fixed items. + if (curNodeId && curNode.getAttribute("removable") == "true") { + if (palette) { + palette.appendChild(curNode); + } else { + this.removeChild(curNode); + } + } + } + } + + /** + * Gets the current set of items in the toolbar. + * + * @returns {string} Comma-separated list of IDs or "__empty". + */ + get currentSet() { + let node = this.firstElementChild; + let currentSet = []; + while (node) { + let id = this._idFromNode(node); + if (id) { + currentSet.push(id); + } + node = node.nextElementSibling; + } + + return currentSet.join(",") || "__empty"; + } + + /** + * Return the ID for a given toolbar item node, with special handling for + * some cases. + * + * @param {Element} node - Return the ID of this node. + * @returns {string} The ID of the node. + */ + _idFromNode(node) { + if (node.getAttribute("skipintoolbarset") == "true") { + return ""; + } + const specialItems = { + toolbarseparator: "separator", + toolbarspring: "spring", + toolbarspacer: "spacer", + }; + return specialItems[node.localName] || node.id; + } + + /** + * Returns a toolbar item based on the given ID. + * + * @param {string} id - The ID for the new toolbar item. + * @returns {Element?} The toolbar item corresponding to the ID, or null. + */ + _getToolbarItem(id) { + // Handle special cases. + if (["separator", "spring", "spacer"].includes(id)) { + let newItem = document.createXULElement("toolbar" + id); + // Due to timers resolution Date.now() can be the same for + // elements created in small timeframes. So ids are + // differentiated through a unique count suffix. + newItem.id = id + Date.now() + ++this._newElementCount; + if (id == "spring") { + newItem.flex = 1; + } + return newItem; + } + + let toolbox = this.toolbox; + if (!toolbox) { + return null; + } + + // Look for an item with the same id, as the item may be + // in a different toolbar. + let item = document.getElementById(id); + if ( + item && + item.parentNode && + item.parentNode.localName == "toolbar" && + item.parentNode.toolbox == toolbox + ) { + return item; + } + + if (toolbox.palette) { + // Attempt to locate an item with a matching ID within the palette. + let paletteItem = toolbox.palette.firstElementChild; + while (paletteItem) { + if (paletteItem.id == id) { + return paletteItem; + } + paletteItem = paletteItem.nextElementSibling; + } + } + return null; + } + + /** + * Insert an item into the toolbar. + * + * @param {string} id - The ID of the item to insert. + * @param {Element?} beforeElt - Optional element to insert the item before. + * @param {Element?} wrapper - Optional wrapper element. + * @returns {Element} The inserted item. + */ + insertItem(id, beforeElt, wrapper) { + let newItem = this._getToolbarItem(id); + if (!newItem) { + return null; + } + + let insertItem = newItem; + // Make sure added items are removable. + newItem.setAttribute("removable", "true"); + + // Wrap the item in another node if so inclined. + if (wrapper) { + wrapper.appendChild(newItem); + insertItem = wrapper; + } + + // Insert the palette item into the toolbar. + if (beforeElt) { + this.insertBefore(insertItem, beforeElt); + } else { + this.appendChild(insertItem); + } + return newItem; + } + + /** + * Determine whether the current set of toolbar items has custom + * interactive items or not. + * + * @param {string} currentSet - Comma-separated list of IDs or "__empty". + * @returns {boolean} Whether the current set has custom interactive items. + */ + hasCustomInteractiveItems(currentSet) { + if (currentSet == "__empty") { + return false; + } + + let defaultOrNoninteractive = (this.getAttribute("defaultset") || "") + .split(",") + .concat(["separator", "spacer", "spring"]); + + return currentSet + .split(",") + .some(item => !defaultOrNoninteractive.includes(item)); + } + } + + customElements.define("customizable-toolbar", CustomizableToolbar, { + extends: "toolbar", + }); +} |