/* 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";
/** * A tree widget with keyboard navigation and collapsable structure. * * @param {Node} node * The container element for the tree widget. * @param {Object} options * - emptyText {string}: text to display when no entries in the table. * - defaultType {string}: The default type of the tree items. For ex. * 'js' * - sorted {boolean}: Defaults to true. If true, tree items are kept in * lexical order. If false, items will be kept in insertion order. * - contextMenuId {string}: ID of context menu to be displayed on * tree items.
*/ function TreeWidget(node, options = {}) {
EventEmitter.decorate(this);
if (this.emptyText) { this.setPlaceholderText(this.emptyText);
} // A map to hold all the passed attachment to each leaf in the tree. this.attachments = new Map();
}
/** * Select any node in the tree. * * @param {array} ids * An array of ids leading upto the selected item
*/
set selectedItem(ids) { if (this._selectedLabel) { this._selectedLabel.classList.remove("theme-selected");
} const currentSelected = this._selectedLabel; if (ids == -1) { this._selectedLabel = this._selectedItem = null; return;
} if (!Array.isArray(ids)) { return;
} this._selectedLabel = this.root.setSelectedItem(ids); if (!this._selectedLabel) { this._selectedItem = null;
} else { if (currentSelected != this._selectedLabel) { this.ensureSelectedVisible();
} this._selectedItem = ids; this.emit( "select", this._selectedItem, this.attachments.get(JSON.stringify(ids))
);
}
},
/** * Gets the selected item in the tree. * * @return {array} * An array of ids leading upto the selected item
*/
get selectedItem() { returnthis._selectedItem;
},
/** * Returns if the passed array corresponds to the selected item in the tree. * * @return {array} * An array of ids leading upto the requested item
*/
isSelected(item) { if (!this._selectedItem || this._selectedItem.length != item.length) { returnfalse;
}
for (let i = 0; i < this._selectedItem.length; i++) { if (this._selectedItem[i] != item[i]) { returnfalse;
}
}
/** * Sets up the root container of the TreeWidget.
*/
setupRoot() { this.root = new TreeItem(this.document); if (this.contextMenuId) { this.root.children.addEventListener("contextmenu", event => { // Call stopPropagation() and preventDefault() here so that avoid to show default // context menu in about:devtools-toolbox. See Bug 1515265.
event.stopPropagation();
event.preventDefault(); const menu = this.document.getElementById(this.contextMenuId);
menu.openPopupAtScreen(event.screenX, event.screenY, true);
});
}
this._parent.appendChild(this.root.children);
this.root.children.addEventListener("mousedown", e => this.onClick(e)); this.root.children.addEventListener("keydown", e => this.onKeydown(e));
},
/** * Sets the text to be shown when no node is present in the tree. * The placeholder will be hidden if text is empty.
*/
setPlaceholderText(text) { this.placeholder.textContent = text; if (text) { this.placeholder.removeAttribute("hidden");
} else { this.placeholder.setAttribute("hidden", "true");
}
},
/** * Select any node in the tree. * * @param {array} id * An array of ids leading upto the selected item
*/
selectItem(id) { this.selectedItem = id;
},
/** * Selects the next visible item in the tree.
*/
selectNextItem() { const next = this.getNextVisibleItem(); if (next) { this.selectedItem = next;
}
},
/** * Selects the previos visible item in the tree
*/
selectPreviousItem() { const prev = this.getPreviousVisibleItem(); if (prev) { this.selectedItem = prev;
}
},
/** * Returns the next visible item in the tree
*/
getNextVisibleItem() {
let node = this._selectedLabel; if (node.hasAttribute("expanded") && node.nextSibling.firstChild) { return JSON.parse(node.nextSibling.firstChild.getAttribute("data-id"));
}
node = node.parentNode; if (node.nextSibling) { return JSON.parse(node.nextSibling.getAttribute("data-id"));
}
node = node.parentNode; while (node.parentNode && node != this.root.children) { if (node.parentNode?.nextSibling) { return JSON.parse(node.parentNode.nextSibling.getAttribute("data-id"));
}
node = node.parentNode;
} returnnull;
},
/** * Returns the previous visible item in the tree
*/
getPreviousVisibleItem() {
let node = this._selectedLabel.parentNode; if (node.previousSibling) {
node = node.previousSibling.firstChild; while (node.hasAttribute("expanded") && !node.hasAttribute("empty")) { if (!node.nextSibling.lastChild) { break;
}
node = node.nextSibling.lastChild.firstChild;
} return JSON.parse(node.parentNode.getAttribute("data-id"));
}
node = node.parentNode; if (node.parentNode && node != this.root.children) {
node = node.parentNode; while (node.hasAttribute("expanded") && !node.hasAttribute("empty")) { if (!node.nextSibling.firstChild) { break;
}
node = node.nextSibling.firstChild.firstChild;
} return JSON.parse(node.getAttribute("data-id"));
} returnnull;
},
clearSelection() { this.selectedItem = -1;
},
/** * Adds an item in the tree. The item can be added as a child to any node in * the tree. The method will also create any subnode not present in the * process. * * @param {[string|object]} items * An array of either string or objects where each increasing index * represents an item corresponding to an equivalent depth in the tree. * Each array element can be either just a string with the value as the * id of of that item as well as the display value, or it can be an * object with the following propeties: * - id {string} The id of the item * - label {string} The display value of the item * - node {DOMNode} The dom node if you want to insert some custom * element as the item. The label property is not used in this * case * - attachment {object} Any object to be associated with this item. * - type {string} The type of this particular item. If this is null, * then defaultType will be used. * For example, if items = ["foo", "bar", { id: "id1", label: "baz" }] * and the tree is empty, then the following hierarchy will be created * in the tree: * foo * └ bar * └ baz * Passing the string id instead of the complete object helps when you * are simply adding children to an already existing node and you know * its id.
*/
add(items) { this.root.add(items, this.defaultType, this.sorted); for (let i = 0; i < items.length; i++) { if (items[i].attachment) { this.attachments.set(
JSON.stringify(items.slice(0, i + 1).map(item => item.id || item)),
items[i].attachment
);
}
} // Empty the empty-tree-text this.setPlaceholderText("");
},
/** * Check if an item exists. * * @param {array} item * The array of ids leading up to the item.
*/
exists(item) {
let bookmark = this.root;
for (const id of item) { if (bookmark.items.has(id)) {
bookmark = bookmark.items.get(id);
} else { returnfalse;
}
} returntrue;
},
/** * Removes the specified item and all of its child items from the tree. * * @param {array} item * The array of ids leading up to the item.
*/
remove(item) { this.root.remove(item); this.attachments.delete(JSON.stringify(item)); // Display the empty tree text if (this.root.items.size == 0 && this.emptyText) { this.setPlaceholderText(this.emptyText);
}
},
/** * Removes all of the child nodes from this tree.
*/
clear() { this.root.remove(); this.setupRoot(); this.attachments.clear(); if (this.emptyText) { this.setPlaceholderText(this.emptyText);
}
},
/** * Expands the tree completely
*/
expandAll() { this.root.expandAll();
},
/** * Collapses the tree completely
*/
collapseAll() { this.root.collapseAll();
},
/** * Click handler for the tree. Used to select, open and close the tree nodes.
*/
onClick(event) {
let target = event.originalTarget; while (target && !target.classList.contains("tree-widget-item")) { if (target == this.root.children) { return;
}
target = target.parentNode;
} if (!target) { return;
}
if (target.hasAttribute("expanded")) {
target.removeAttribute("expanded");
} else {
target.setAttribute("expanded", "true");
}
/** * Keydown handler for this tree. Used to select next and previous visible * items, as well as collapsing and expanding any item.
*/
onKeydown(event) { switch (event.keyCode) { case KeyCodes.DOM_VK_UP: this.selectPreviousItem(); break;
case KeyCodes.DOM_VK_DOWN: this.selectNextItem(); break;
case KeyCodes.DOM_VK_RIGHT: if (this._selectedLabel.hasAttribute("expanded")) { this.selectNextItem();
} else { this._selectedLabel.setAttribute("expanded", "true");
} break;
case KeyCodes.DOM_VK_LEFT: if ( this._selectedLabel.hasAttribute("expanded") &&
!this._selectedLabel.hasAttribute("empty")
) { this._selectedLabel.removeAttribute("expanded");
} else { this.selectPreviousItem();
} break;
default: return;
}
event.preventDefault();
},
/** * Scrolls the viewport of the tree so that the selected item is always * visible.
*/
ensureSelectedVisible() { const { top, bottom } = this._selectedLabel.getBoundingClientRect(); const height = this.root.children.parentNode.clientHeight; if (top < 0) { this._selectedLabel.scrollIntoView();
} elseif (bottom > height) { this._selectedLabel.scrollIntoView(false);
}
},
};
module.exports.TreeWidget = TreeWidget;
/** * Any item in the tree. This can be an empty leaf node also. * * @param {HTMLDocument} document * The document element used for creating new nodes. * @param {TreeItem} parent * The parent item for this item. * @param {string|DOMElement} label * Either the dom node to be used as the item, or the string to be * displayed for this node in the tree * @param {string} type * The type of the current node. For ex. "js"
*/ function TreeItem(document, parent, label, type) { this.document = document; this.node = this.document.createElementNS(HTML_NS, "li"); this.node.setAttribute("tabindex", "0"); this.isRoot = !parent; this.parent = parent; if (this.parent) { this.level = this.parent.level + 1;
} if (label) { this.label = this.document.createElementNS(HTML_NS, "div"); this.label.setAttribute("empty", "true"); this.label.setAttribute("level", this.level); this.label.className = "tree-widget-item"; if (type) { this.label.setAttribute("type", type);
} if (typeof label == "string") { this.label.textContent = label;
} else { this.label.appendChild(label);
} this.node.appendChild(this.label);
} this.children = this.document.createElementNS(HTML_NS, "ul"); if (this.isRoot) { this.children.className = "tree-widget-container";
} else { this.children.className = "tree-widget-children";
} this.node.appendChild(this.children); this.items = new Map();
}
TreeItem.prototype = {
items: null,
isSelected: false,
expanded: false,
isRoot: false,
parent: null,
children: null,
level: 0,
/** * Adds the item to the sub tree contained by this node. The item to be * inserted can be a direct child of this node, or further down the tree. * * @param {array} items * Same as TreeWidget.add method's argument * @param {string} defaultType * The default type of the item to be used when items[i].type is null * @param {boolean} sorted * true if the tree items are inserted in a lexically sorted manner. * Otherwise, false if the item are to be appended to their parent.
*/
add(items, defaultType, sorted) { if (items.length == this.level) { // This is the exit condition of recursive TreeItem.add calls return;
} // Get the id and label corresponding to this level inside the tree. const id = items[this.level].id || items[this.level]; if (this.items.has(id)) { // An item with same id already exists, thus calling the add method of // that child to add the passed node at correct position. this.items.get(id).add(items, defaultType, sorted); return;
} // No item with the id `id` exists, so we create one and call the add // method of that item. // The display string of the item can be the label, the id, or the item // itself if its a plain string.
let label =
items[this.level].label || items[this.level].id || items[this.level]; const node = items[this.level].node; if (node) { // The item is supposed to be a DOMNode, so we fetch the textContent in // order to find the correct sorted location of this new item.
label = node.textContent;
} const treeItem = new TreeItem( this.document, this,
node || label,
items[this.level].type || defaultType
);
if (sorted) { // Inserting this newly created item at correct position const nextSibling = [...this.items.values()].find(child => { return child.label.textContent >= label;
});
if (this.label) { this.label.removeAttribute("empty");
} this.items.set(id, treeItem);
},
/** * If this item is to be removed, then removes this item and thus all of its * subtree. Otherwise, call the remove method of appropriate child. This * recursive method goes on till we have reached the end of the branch or the * current item is to be removed. * * @param {array} items * Ids of items leading up to the item to be removed.
*/
remove(items = []) { const id = items.shift(); if (id && this.items.has(id)) { const deleted = this.items.get(id); if (!items.length) { this.items.delete(id);
} if (this.items.size == 0) { this.label.setAttribute("empty", "true");
}
deleted.remove(items);
} elseif (!id) { this.destroy();
}
},
/** * If this item is to be selected, then selected and expands the item. * Otherwise, if a child item is to be selected, just expands this item. * * @param {array} items * Ids of items leading up to the item to be selected.
*/
setSelectedItem(items) { if (!items[this.level]) { this.label.classList.add("theme-selected"); this.label.setAttribute("expanded", "true"); returnthis.label;
} if (this.items.has(items[this.level])) { const label = this.items.get(items[this.level]).setSelectedItem(items); if (label && this.label) { this.label.setAttribute("expanded", true);
} return label;
} returnnull;
},
/** * Collapses this item and all of its sub tree items
*/
collapseAll() { if (this.label) { this.label.removeAttribute("expanded");
} for (const child of this.items.values()) {
child.collapseAll();
}
},
/** * Expands this item and all of its sub tree items
*/
expandAll() { if (this.label) { this.label.setAttribute("expanded", "true");
} for (const child of this.items.values()) {
child.expandAll();
}
},
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.