/* 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. * * @extends {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. * * @return {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". * @return {string} Comma-separated list of IDs or "__empty". */ set currentSet(val) { if (val == this.currentSet) { return val; } // 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); } } } return val; } /** * Gets the current set of items in the toolbar. * * @return {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. * @return {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. * @return {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. * @return {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". * @return {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", }); }