this.element._wb_tree = this;
// Create header markup, or take it from the existing html
this.headerElement =
this.element.querySelector("div.wb-header");
const wantHeader = opts.header == null ? this.columns.length > 1 : !!opts.header;
if (this.headerElement) {
// User existing header markup to define `this.columns`
assert(!this.columns, "`opts.columns` must not be set if table markup already contains a header");
this.columns = [];
const rowElement = this.headerElement.querySelector("div.wb-row");
for (const colDiv of rowElement.querySelectorAll("div")) {
this.columns.push({
id: colDiv.dataset.id || `col_${this.columns.length}`,
// id: colDiv.dataset.id || null,
title: "" + colDiv.textContent,
// text: "" + colDiv.textContent,
width: "*", // TODO: read from header span
});
}
}
else {
// We need a row div, the rest will be computed from `this.columns`
const coldivs = "
".repeat(this.columns.length);
this.element.innerHTML = `
`;
if (!wantHeader) {
const he = this.element.querySelector("div.wb-header");
he.style.display = "none";
}
}
//
this.element.innerHTML += `
`;
this.listContainerElement = this.element.querySelector("div.wb-list-container");
this.nodeListElement =
this.listContainerElement.querySelector("div.wb-node-list");
this.headerElement =
this.element.querySelector("div.wb-header");
this.element.classList.toggle("wb-grid", this.columns.length > 1);
if (this.options.connectTopBreadcrumb) {
this.breadcrumb = elemFromSelector(this.options.connectTopBreadcrumb);
assert(!this.breadcrumb || this.breadcrumb.innerHTML != null, `Invalid 'connectTopBreadcrumb' option: ${this.breadcrumb}.`);
this.breadcrumb.addEventListener("click", (e) => {
// const node = Wunderbaum.getNode(e)!;
const elem = e.target;
if (elem && elem.matches("a.wb-breadcrumb")) {
const node = this.keyMap.get(elem.dataset.key);
node === null || node === void 0 ? void 0 : node.setActive();
e.preventDefault();
}
});
}
this._initExtensions();
// --- apply initial options
["enabled", "fixedCol"].forEach((optName) => {
if (opts[optName] != null) {
this.setOption(optName, opts[optName]);
}
});
// --- Load initial data
if (initOptions.source) {
if (opts.showSpinner) {
this.nodeListElement.innerHTML = `
`;
}
this.load(initOptions.source)
.then(() => {
// The source may have defined columns, so we may adjust the nav mode
if (opts.navigationModeOption == null) {
if (this.isGrid()) {
this.setNavigationOption(NavModeEnum.cell);
}
else {
this.setNavigationOption(NavModeEnum.row);
}
}
else {
this.setNavigationOption(opts.navigationModeOption);
}
this.update(ChangeType.structure, { immediate: true });
readyDeferred.resolve();
})
.catch((error) => {
readyDeferred.reject(error);
})
.finally(() => {
var _a;
(_a = this.element.querySelector("progress.spinner")) === null || _a === void 0 ? void 0 : _a.remove();
this.element.classList.remove("wb-initializing");
});
}
else {
readyDeferred.resolve();
}
// Async mode is sometimes required, because this.element.clientWidth
// has a wrong value at start???
this.update(ChangeType.any);
// --- Bind listeners
this._registerEventHandlers();
this.resizeObserver = new ResizeObserver((entries) => {
// this.log("ResizeObserver: Size changed", entries);
this.update(ChangeType.resize);
});
this.resizeObserver.observe(this.element);
}
_registerEventHandlers() {
this.element.addEventListener("scroll", (e) => {
// this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
this.update(ChangeType.scroll);
});
onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => {
var _a, _b;
const info = Wunderbaum.getEventInfo(e);
const command = (_b = (_a = e.target) === null || _a === void 0 ? void 0 : _a.dataset) === null || _b === void 0 ? void 0 : _b.command;
this._callEvent("buttonClick", {
event: e,
info: info,
command: command,
});
});
onEvent(this.nodeListElement, "click", "div.wb-row", (e) => {
const info = Wunderbaum.getEventInfo(e);
const node = info.node;
const mouseEvent = e;
// this.log("click", info);
if (this._callEvent("click", { event: e, node: node, info: info }) === false) {
this.lastClickTime = Date.now();
return false;
}
if (node) {
if (mouseEvent.ctrlKey) {
node.toggleSelected();
return;
}
// Edit title if 'clickActive' is triggered:
const trigger = this.getOption("edit.trigger");
const slowClickDelay = this.getOption("edit.slowClickDelay");
if (trigger.indexOf("clickActive") >= 0 &&
info.region === "title" &&
node.isActive() &&
(!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay)) {
node.startEditTitle();
}
if (info.region === NodeRegion.expander) {
node.setExpanded(!node.isExpanded(), {
scrollIntoView: this.options.scrollIntoViewOnExpandClick !== false,
});
}
else if (info.region === NodeRegion.checkbox) {
node.toggleSelected();
}
else {
if (info.colIdx >= 0) {
node.setActive(true, { colIdx: info.colIdx, event: e });
}
else {
node.setActive(true, { event: e });
}
}
}
this.lastClickTime = Date.now();
});
onEvent(this.nodeListElement, "dblclick", "div.wb-row", (e) => {
const info = Wunderbaum.getEventInfo(e);
const node = info.node;
// this.log("dblclick", info, e);
if (this._callEvent("dblclick", { event: e, node: node, info: info }) ===
false) {
return false;
}
if (node &&
info.colIdx === 0 &&
node.isExpandable() &&
info.region !== NodeRegion.expander) {
this._callMethod("edit._stopEditTitle");
node.setExpanded(!node.isExpanded());
}
});
onEvent(this.element, "keydown", (e) => {
const info = Wunderbaum.getEventInfo(e);
const eventName = eventToString(e);
const node = info.node || this.getFocusNode();
this._callHook("onKeyEvent", {
event: e,
node: node,
info: info,
eventName: eventName,
});
});
onEvent(this.element, "focusin focusout", (e) => {
const flag = e.type === "focusin";
const targetNode = Wunderbaum.getNode(e);
this._callEvent("focus", { flag: flag, event: e });
if (flag && this.isRowNav() && !this.isEditingTitle()) {
if (this.options.navigationModeOption === NavModeEnum.row) {
targetNode === null || targetNode === void 0 ? void 0 : targetNode.setActive();
}
else {
this.setCellNav();
}
}
if (!flag) {
this._callMethod("edit._stopEditTitle", true, {
event: e,
forceClose: true,
});
}
});
}
/**
* Return a Wunderbaum instance, from element, id, index, or event.
*
* ```js
* getTree(); // Get first Wunderbaum instance on page
* getTree(1); // Get second Wunderbaum instance on page
* getTree(event); // Get tree for this mouse- or keyboard event
* getTree("foo"); // Get tree for this `tree.options.id`
* getTree("#tree"); // Get tree for first matching element selector
* ```
*/
static getTree(el) {
if (el instanceof Wunderbaum) {
return el;
}
else if (el instanceof WunderbaumNode) {
return el.tree;
}
if (el === undefined) {
el = 0; // get first tree
}
if (typeof el === "number") {
el = document.querySelectorAll(".wunderbaum")[el]; // el was an integer: return nth element
}
else if (typeof el === "string") {
// Search all trees for matching ID
for (const treeElem of document.querySelectorAll(".wunderbaum")) {
const tree = treeElem._wb_tree;
if (tree && tree.id === el) {
return tree;
}
}
// Search by selector
el = document.querySelector(el);
if (!el) {
return null;
}
}
else if (el.target) {
el = el.target;
}
assert(el instanceof Element, `Invalid el type: ${el}`);
if (!el.matches(".wunderbaum")) {
el = el.closest(".wunderbaum");
}
if (el && el._wb_tree) {
return el._wb_tree;
}
return null;
}
/**
* Return the icon-function -> icon-definition mapping.
* @deprecated Use {@link Wunderbaum.iconMaps}
*/
get iconMap() {
const map = this.options.iconMap;
if (typeof map === "string") {
return defaultIconMaps[map];
}
return map;
}
/**
* Return a WunderbaumNode instance from element or event.
*/
static getNode(el) {
if (!el) {
return null;
}
else if (el instanceof WunderbaumNode) {
return el;
}
else if (el.target !== undefined) {
el = el.target; // el was an Event
}
// `el` is a DOM element
// let nodeElem = obj.closest("div.wb-row");
while (el) {
if (el._wb_node) {
return el._wb_node;
}
el = el.parentElement; //.parentNode;
}
return null;
}
/**
* Iterate all descendant nodes depth-first, pre-order using `for ... of ...` syntax.
* More concise, but slightly slower than {@link Wunderbaum.visit}.
*
* Example:
* ```js
* for(const node of tree) {
* ...
* }
* ```
*/
*[Symbol.iterator]() {
yield* this.root;
}
/** @internal */
_registerExtension(extension) {
this.extensionList.push(extension);
this.extensions[extension.id] = extension;
// this.extensionMap.set(extension.id, extension);
}
/** Called on tree (re)init after markup is created, before loading. */
_initExtensions() {
for (const ext of this.extensionList) {
ext.init();
}
}
/**
* Calculate a *stable*, unique key for a node from its refKey (or title).
* We also add information from the parent, because a refKey may occur multiple
* times in a tree (but not as child of the same parent).
* @internal
*/
_calculateKey(data, parent) {
if (data.key) {
// Always use an explicitly passed key
return data.key;
}
// Auto-keys are optional, use a monotonic counter by default:
if (!this.options.autoKeys) {
return "" + ++WunderbaumNode.sequence;
}
// Add the parent's key to the hash. Assuming this was generated by the
// same algorithm, this should incorporate the whole path:
const s = (parent ? parent.key : "") + (data.refKey || data.title);
// 32-bit has a high probability of collisions, so we pump up to 64-bit
// https://security.stackexchange.com/q/209882/207588
const h1 = murmurHash3(s, true);
let key = "id_" + h1 + murmurHash3(h1 + s, true);
// Check for collisions
// (Most likely if the same title occurs multiple in the same parent).
const existingNode = this.keyMap.get(key);
if (existingNode) {
key += "." + ++Wunderbaum.sequence;
this.logWarn(`Node with existing key: '${existingNode}', using ${key}.`, data);
}
return key;
}
/** Add node to tree's bookkeeping data structures. @internal */
_registerNode(node) {
const key = node.key;
assert(key != null, `Missing key: '${node}'.`);
assert(!this.keyMap.has(key), `Duplicate key: '${key}': ${node}.`);
this.keyMap.set(key, node);
const rk = node.refKey;
if (rk != null) {
const rks = this.refKeyMap.get(rk); // Set of nodes with this refKey
if (rks) {
rks.add(node);
}
else {
this.refKeyMap.set(rk, new Set([node]));
}
}
}
/** Remove node from tree's bookkeeping data structures. @internal */
_unregisterNode(node) {
// Remove refKey reference from map (if any)
const rk = node.refKey;
if (rk != null) {
const rks = this.refKeyMap.get(rk);
if (rks && rks.delete(node) && !rks.size) {
// We just removed the last element
this.refKeyMap.delete(rk);
}
}
// Remove key reference from map
this.keyMap.delete(node.key);
// Mark as disposed
node.tree = null;
node.parent = null;
// Remove HTML markup
node.removeMarkup();
}
/** Call all hook methods of all registered extensions.*/
_callHook(hook, data = {}) {
let res;
const d = extend({}, { tree: this, options: this.options, result: undefined }, data);
for (const ext of this.extensionList) {
res = ext[hook].call(ext, d);
if (res === false) {
break;
}
if (d.result !== undefined) {
res = d.result;
}
}
return res;
}
/**
* Call tree method or extension method if defined.
*
* Example:
* ```js
* tree._callMethod("edit.startEdit", "arg1", "arg2")
* ```
*/
_callMethod(name, ...args) {
const [p, n] = name.split(".");
const obj = n ? this.extensions[p] : this;
const func = obj[n];
if (func) {
return func.apply(obj, args);
}
else {
this.logError(`Calling undefined method '${name}()'.`);
}
}
/**
* Call event handler if defined in tree or tree.EXTENSION options.
*
* Example:
* ```js
* tree._callEvent("edit.beforeEdit", {foo: 42})
* ```
*/
_callEvent(type, extra) {
const [p, n] = type.split(".");
const opts = this.options;
const func = n ? opts[p][n] : opts[p];
if (func) {
return func.call(this, extend({ type: type, tree: this, util: this._util }, extra));
// } else {
// this.logError(`Triggering undefined event '${type}'.`)
}
}
/** Return the node for given row index. */
_getNodeByRowIdx(idx) {
// TODO: start searching from active node (reverse)
let node = null;
this.visitRows((n) => {
if (n._rowIdx === idx) {
node = n;
return false;
}
});
return node;
}
/** Return the topmost visible node in the viewport.
* @param complete If `false`, the node is considered visible if at least one
* pixel is visible.
*/
getTopmostVpNode(complete = true) {
const rowHeight = this.options.rowHeightPx;
const gracePx = 1; // ignore subpixel scrolling
const scrollParent = this.element;
// const headerHeight = this.headerElement.clientHeight; // May be 0
const scrollTop = scrollParent.scrollTop; // + headerHeight;
let topIdx;
if (complete) {
topIdx = Math.ceil((scrollTop - gracePx) / rowHeight);
}
else {
topIdx = Math.floor(scrollTop / rowHeight);
}
return this._getNodeByRowIdx(topIdx);
}
/** Return the lowest visible node in the viewport. */
getLowestVpNode(complete = true) {
const rowHeight = this.options.rowHeightPx;
const scrollParent = this.element;
const headerHeight = this.headerElement.clientHeight; // May be 0
const scrollTop = scrollParent.scrollTop;
const clientHeight = scrollParent.clientHeight - headerHeight;
let bottomIdx;
if (complete) {
bottomIdx = Math.floor((scrollTop + clientHeight) / rowHeight) - 1;
}
else {
bottomIdx = Math.ceil((scrollTop + clientHeight) / rowHeight) - 1;
}
bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
return this._getNodeByRowIdx(bottomIdx);
}
/** Return preceding visible node in the viewport. */
_getPrevNodeInView(node, ofs = 1) {
this.visitRows((n) => {
node = n;
if (ofs-- <= 0) {
return false;
}
}, { reverse: true, start: node || this.getActiveNode() });
return node;
}
/** Return following visible node in the viewport. */
_getNextNodeInView(node, options) {
let ofs = (options === null || options === void 0 ? void 0 : options.ofs) || 1;
const reverse = !!(options === null || options === void 0 ? void 0 : options.reverse);
this.visitRows((n) => {
node = n;
if ((options === null || options === void 0 ? void 0 : options.cb) && options.cb(n)) {
return false;
}
if (ofs-- <= 0) {
return false;
}
}, { reverse: reverse, start: node || this.getActiveNode() });
return node;
}
/**
* Append (or insert) a list of toplevel nodes.
*
* @see {@link WunderbaumNode.addChildren}
*/
addChildren(nodeData, options) {
return this.root.addChildren(nodeData, options);
}
/**
* Apply a modification or navigation operation.
*
* Most of these commands simply map to a node or tree method.
* This method is especially useful when implementing keyboard mapping,
* context menus, or external buttons.
*
* Valid commands:
* - 'moveUp', 'moveDown'
* - 'indent', 'outdent'
* - 'remove'
* - 'edit', 'addChild', 'addSibling': (reqires ext-edit extension)
* - 'cut', 'copy', 'paste': (use an internal singleton 'clipboard')
* - 'down', 'first', 'last', 'left', 'parent', 'right', 'up': navigate
*
*/
applyCommand(cmd, nodeOrOpts, options) {
let // clipboard,
node, refNode;
// options = $.extend(
// { setActive: true, clipboard: CLIPBOARD },
// options_
// );
if (nodeOrOpts instanceof WunderbaumNode) {
node = nodeOrOpts;
}
else {
node = this.getActiveNode();
assert(options === undefined, `Unexpected options: ${options}`);
options = nodeOrOpts;
}
// clipboard = options.clipboard;
switch (cmd) {
// Sorting and indentation:
case "moveUp":
refNode = node.getPrevSibling();
if (refNode) {
node.moveTo(refNode, "before");
node.setActive();
}
break;
case "moveDown":
refNode = node.getNextSibling();
if (refNode) {
node.moveTo(refNode, "after");
node.setActive();
}
break;
case "indent":
refNode = node.getPrevSibling();
if (refNode) {
node.moveTo(refNode, "appendChild");
refNode.setExpanded();
node.setActive();
}
break;
case "outdent":
if (!node.isTopLevel()) {
node.moveTo(node.getParent(), "after");
node.setActive();
}
break;
// Remove:
case "remove":
refNode = node.getPrevSibling() || node.getParent();
node.remove();
if (refNode) {
refNode.setActive();
}
break;
// Add, edit (requires ext-edit):
case "addChild":
this._callMethod("edit.createNode", "prependChild");
break;
case "addSibling":
this._callMethod("edit.createNode", "after");
break;
case "rename":
node.startEditTitle();
break;
// Simple clipboard simulation:
// case "cut":
// clipboard = { mode: cmd, data: node };
// break;
// case "copy":
// clipboard = {
// mode: cmd,
// data: node.toDict(function(d, n) {
// delete d.key;
// }),
// };
// break;
// case "clear":
// clipboard = null;
// break;
// case "paste":
// if (clipboard.mode === "cut") {
// // refNode = node.getPrevSibling();
// clipboard.data.moveTo(node, "child");
// clipboard.data.setActive();
// } else if (clipboard.mode === "copy") {
// node.addChildren(clipboard.data).setActive();
// }
// break;
// Navigation commands:
case "down":
case "first":
case "last":
case "left":
case "nextMatch":
case "pageDown":
case "pageUp":
case "parent":
case "prevMatch":
case "right":
case "up":
return node.navigate(cmd);
default:
error(`Unhandled command: '${cmd}'`);
}
}
/** Delete all nodes. */
clear() {
this.root.removeChildren();
this.root.children = null;
this.keyMap.clear();
this.refKeyMap.clear();
this.treeRowCount = 0;
this._activeNode = null;
this._focusNode = null;
// this.types = {};
// this. columns =[];
// this._columnsById = {};
// Modification Status
// this.changedSince = 0;
// this.changes.clear();
// this.changedNodes.clear();
// // --- FILTER ---
// public filterMode: FilterModeType = null;
// // --- KEYNAV ---
// public activeColIdx = 0;
// public cellNavMode = false;
// public lastQuicksearchTime = 0;
// public lastQuicksearchTerm = "";
this.update(ChangeType.structure);
}
/**
* Clear nodes and markup and detach events and observers.
*
* This method may be useful to free up resources before re-creating a tree
* on an existing div, for example in unittest suites.
* Note that this Wunderbaum instance becomes unusable afterwards.
*/
destroy() {
this.logInfo("destroy()...");
this.clear();
this.resizeObserver.disconnect();
this.element.innerHTML = "";
// Remove all event handlers
this.element.outerHTML = this.element.outerHTML; // eslint-disable-line
}
/**
* Return `tree.option.NAME` (also resolving if this is a callback).
*
* See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()}
* to evaluate `node.NAME` setting and `tree.types[node.type].NAME`.
*
* @param name option name (use dot notation to access extension option, e.g.
* `filter.mode`)
*/
getOption(name, defaultValue) {
let ext;
let opts = this.options;
// Lookup `name` in options dict
if (name.indexOf(".") >= 0) {
[ext, name] = name.split(".");
opts = opts[ext];
}
let value = opts[name];
// A callback resolver always takes precedence
if (typeof value === "function") {
value = value({ type: "resolve", tree: this });
}
// Use value from value options dict, fallback do default
// console.info(name, value, opts)
return value !== null && value !== void 0 ? value : defaultValue;
}
/**
* Set tree option.
* Use dot notation to set plugin option, e.g. "filter.mode".
*/
setOption(name, value) {
// this.log(`setOption(${name}, ${value})`);
if (name.indexOf(".") >= 0) {
const parts = name.split(".");
const ext = this.extensions[parts[0]];
ext.setPluginOption(parts[1], value);
return;
}
this.options[name] = value;
switch (name) {
case "checkbox":
this.update(ChangeType.any);
break;
case "enabled":
this.setEnabled(!!value);
break;
case "fixedCol":
this.element.classList.toggle("wb-fixed-col", !!value);
break;
}
}
/** Return true if the tree (or one of its nodes) has the input focus. */
hasFocus() {
return this.element.contains(document.activeElement);
}
/**
* Return true if the tree displays a header. Grids have a header unless the
* `header` option is set to `false`. Plain trees have a header if the `header`
* option is a string or `true`.
*/
hasHeader() {
const header = this.options.header;
return this.isGrid() ? header !== false : !!header;
}
/** Run code, but defer rendering of viewport until done.
*
* ```js
* const res = tree.runWithDeferredUpdate(() => {
* return someFunctionThatWouldUpdateManyNodes();
* });
* ```
*/
runWithDeferredUpdate(func) {
try {
this.enableUpdate(false);
const res = func();
assert(!(res instanceof Promise), `Promise return not allowed (see 'runWithDeferredUpdateAsync()'): ${res}`);
return res;
}
finally {
this.enableUpdate(true);
}
}
/** Run code, but defer rendering of viewport until done.
*
* ```js
* const res = await tree.runWithDeferredUpdate(async () => {
* return someAsyncFunctionThatWouldUpdateManyNodes();
* });
* ```
*/
async runWithDeferredUpdateAsync(func) {
try {
this.enableUpdate(false);
return await func();
}
finally {
this.enableUpdate(true);
}
}
/** Recursively expand all expandable nodes (triggers lazy load if needed). */
async expandAll(flag = true, options) {
await this.root.expandAll(flag, options);
}
/** Recursively select all nodes. */
selectAll(flag = true) {
return this.root.setSelected(flag, { propagateDown: true });
}
/** Toggle select all nodes. */
toggleSelect() {
this.selectAll(this.root._anySelectable());
}
/**
* Return an array of selected nodes.
* @param stopOnParents only return the topmost selected node (useful with selectMode 'hier')
*/
getSelectedNodes(stopOnParents = false) {
return this.root.getSelectedNodes(stopOnParents);
}
/**
* Return an array of refKey values.
*
* RefKeys are unique identifiers for a node data, and are used to identify
* clones.
* If more than one node has the same refKey, it is only returned once.
* @param selected if true, only return refKeys of selected nodes.
*/
getRefKeys(selected = false) {
return this.root.getRefKeys(selected);
}
/*
* Return an array of selected nodes.
*/
_selectRange(eventInfo) {
this.logDebug("_selectRange", eventInfo);
error("Not yet implemented.");
// const mode = this.options.selectMode!;
// if (mode !== "multi") {
// this.logDebug(`Range selection only available for selectMode 'multi'`);
// return;
// }
// if (eventInfo.canonicalName === "Meta+click") {
// eventInfo.node?.toggleSelected();
// return false; // don't
// } else if (eventInfo.canonicalName === "Shift+click") {
// let from = this.activeNode;
// let to = eventInfo.node;
// if (!from || !to || from === to) {
// return;
// }
// this.runWithDeferredUpdate(() => {
// this.visitRows(
// (node) => {
// node.setSelected();
// },
// {
// includeHidden: true,
// includeSelf: false,
// start: from,
// reverse: from!._rowIdx! > to!._rowIdx!,
// }
// );
// });
// return false;
// }
}
/** Return the number of nodes in the data model.
* @param visible if true, nodes that are hidden due to collapsed parents are ignored.
*/
count(visible = false) {
return visible ? this.treeRowCount : this.keyMap.size;
}
/** Return the number of *unique* nodes in the data model, i.e. unique `node.refKey`.
*/
countUnique() {
return this.refKeyMap.size;
}
/** @internal sanity check. */
_check() {
let i = 0;
this.visit((n) => {
i++;
});
if (this.keyMap.size !== i) {
this.logWarn(`_check failed: ${this.keyMap.size} !== ${i}`);
}
// util.assert(this.keyMap.size === i);
}
/**
* Find all nodes that match condition.
*
* @param match title string to search for, or a
* callback function that returns `true` if a node is matched.
* @see {@link WunderbaumNode.findAll}
*/
findAll(match) {
return this.root.findAll(match);
}
/**
* Find all nodes with a given _refKey_ (aka a list of clones).
*
* @param refKey a `node.refKey` value to search for.
* @returns an array of matching nodes with at least two element or `[]`
* if nothing found.
*
* @see {@link WunderbaumNode.getCloneList}
*/
findByRefKey(refKey) {
const clones = this.refKeyMap.get(refKey);
return clones ? Array.from(clones) : [];
}
/**
* Find first node that matches condition.
*
* @param match title string to search for, or a
* callback function that returns `true` if a node is matched.
* @see {@link WunderbaumNode.findFirst}
*/
findFirst(match) {
return this.root.findFirst(match);
}
/**
* Find first node that matches condition.
*
* @see {@link WunderbaumNode.findFirst}
*
*/
findKey(key) {
return this.keyMap.get(key) || null;
}
/**
* Find the next visible node that starts with `match`, starting at `startNode`
* and wrap-around at the end.
* Used by quicksearch and keyboard navigation.
*/
findNextNode(match, startNode, reverse = false) {
//, visibleOnly) {
let res = null;
const firstNode = this.getFirstChild();
// Last visible node (calculation is expensive, so do only if we need it):
const lastNode = reverse ? this.findRelatedNode(firstNode, "last") : null;
const matcher = typeof match === "string" ? makeNodeTitleStartMatcher(match) : match;
startNode = startNode || (reverse ? lastNode : firstNode);
function _checkNode(n) {
// console.log("_check " + n)
if (matcher(n)) {
res = n;
}
if (res || n === startNode) {
return false;
}
}
this.visitRows(_checkNode, {
start: startNode,
includeSelf: false,
reverse: reverse,
});
// Wrap around search
if (!res && startNode !== firstNode) {
this.visitRows(_checkNode, {
start: reverse ? lastNode : firstNode,
includeSelf: true,
reverse: reverse,
});
}
return res;
}
/**
* Find a node relative to another node.
*
* @param node
* @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
* (Alternatively the keyCode that would normally trigger this move,
* e.g. `$.ui.keyCode.LEFT` = 'left'.
* @param includeHidden Not yet implemented
*/
findRelatedNode(node, where, includeHidden = false) {
const rowHeight = this.options.rowHeightPx;
let res = null;
const pageSize = Math.floor(this.listContainerElement.clientHeight / rowHeight);
switch (where) {
case "parent":
if (node.parent && node.parent.parent) {
res = node.parent;
}
break;
case "first":
// First visible node
this.visit((n) => {
if (n.isVisible()) {
res = n;
return false;
}
});
break;
case "last":
this.visit((n) => {
// last visible node
if (n.isVisible()) {
res = n;
}
});
break;
case "left":
if (node.parent && node.parent.parent) {
res = node.parent;
}
// if (node.expanded) {
// node.setExpanded(false);
// } else if (node.parent && node.parent.parent) {
// res = node.parent;
// }
break;
case "right":
if (node.children && node.children.length) {
res = node.children[0];
}
// if (this.cellNavMode) {
// throw new Error("Not implemented");
// } else {
// if (!node.expanded && (node.children || node.lazy)) {
// node.setExpanded();
// res = node;
// } else if (node.children && node.children.length) {
// res = node.children[0];
// }
// }
break;
case "up":
res = this._getNextNodeInView(node, { reverse: true });
break;
case "down":
res = this._getNextNodeInView(node);
break;
case "pageDown":
{
const bottomNode = this.getLowestVpNode();
// this.logDebug(`${where}(${node}) -> ${bottomNode}`);
if (node._rowIdx < bottomNode._rowIdx) {
res = bottomNode;
}
else {
res = this._getNextNodeInView(node, {
reverse: false,
ofs: pageSize,
});
}
}
break;
case "pageUp":
if (node._rowIdx === 0) {
res = node;
}
else {
const topNode = this.getTopmostVpNode();
// this.logDebug(`${where}(${node}) -> ${topNode}`);
if (node._rowIdx > topNode._rowIdx) {
res = topNode;
}
else {
res = this._getNextNodeInView(node, {
reverse: true,
ofs: pageSize,
});
}
}
break;
case "prevMatch":
// fallthrough
case "nextMatch":
if (!this.isFilterActive) {
this.logWarn(`${where}: Filter is not active.`);
break;
}
res = this.findNextNode((n) => n.isMatched(), node, where === "prevMatch");
res === null || res === void 0 ? void 0 : res.setActive();
break;
default:
this.logWarn("Unknown relation '" + where + "'.");
}
return res;
}
/**
* Iterator version of {@link Wunderbaum.format}.
*/
*format_iter(name_cb, connectors) {
yield* this.root.format_iter(name_cb, connectors);
}
/**
* Return multiline string representation of the node hierarchy.
* Mostly useful for debugging.
*
* Example:
* ```js
* console.info(tree.format((n)=>n.title));
* ```
* logs
* ```
* Playground
* ├─ Books
* | ├─ Art of War
* | ╰─ Don Quixote
* ├─ Music
* ...
* ```
*
* @see {@link Wunderbaum.format_iter} and {@link WunderbaumNode.format}.
*/
format(name_cb, connectors) {
return this.root.format(name_cb, connectors);
}
/**
* Always returns null (so a tree instance behaves as `tree.root`).
*/
get parent() {
return null;
}
/**
* Return a list of top-level nodes.
*/
get children() {
return this.root.children || [];
}
/**
* Return the active cell (`span.wb-col`) of the currently active node or null.
*/
getActiveColElem() {
if (this.activeNode && this.activeColIdx >= 0) {
return this.activeNode.getColElem(this.activeColIdx);
}
return null;
}
/**
* Return the currently active node or null (alias for `tree.activeNode`).
* Alias for {@link Wunderbaum.activeNode}.
*
* @see {@link WunderbaumNode.setActive}
* @see {@link WunderbaumNode.isActive}
* @see {@link Wunderbaum.activeNode}
* @see {@link Wunderbaum.focusNode}
*/
getActiveNode() {
return this.activeNode;
}
/**
* Return the first top level node if any (not the invisible root node).
*/
getFirstChild() {
return this.root.getFirstChild();
}
/**
* Return the last top level node if any (not the invisible root node).
*/
getLastChild() {
return this.root.getLastChild();
}
/**
* Return the node that currently has keyboard focus or null.
* Alias for {@link Wunderbaum.focusNode}.
* @see {@link WunderbaumNode.setFocus}
* @see {@link WunderbaumNode.hasFocus}
* @see {@link Wunderbaum.activeNode}
* @see {@link Wunderbaum.focusNode}
*/
getFocusNode() {
return this.focusNode;
}
/** Return a {node: WunderbaumNode, region: TYPE} object for a mouse event.
*
* @param {Event} event Mouse event, e.g. click, ...
* @returns {object} Return a {node: WunderbaumNode, region: TYPE} object
* TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
*/
static getEventInfo(event) {
const target = event.target;
const cl = target.classList;
const parentCol = target.closest("span.wb-col");
const node = Wunderbaum.getNode(target);
const tree = node ? node.tree : Wunderbaum.getTree(event);
const res = {
event: event,
canonicalName: eventToString(event),
tree: tree,
node: node,
region: NodeRegion.unknown,
colDef: undefined,
colIdx: -1,
colId: undefined,
colElem: parentCol,
};
if (cl.contains("wb-title")) {
res.region = NodeRegion.title;
}
else if (cl.contains("wb-expander")) {
res.region = node.isExpandable()
? NodeRegion.expander
: NodeRegion.prefix;
}
else if (cl.contains("wb-checkbox")) {
res.region = NodeRegion.checkbox;
}
else if (cl.contains("wb-icon")) {
//|| cl.contains("wb-custom-icon")) {
res.region = NodeRegion.icon;
}
else if (cl.contains("wb-node")) {
res.region = NodeRegion.title;
}
else if (parentCol) {
res.region = NodeRegion.column;
const idx = Array.prototype.indexOf.call(parentCol.parentNode.children, parentCol);
res.colIdx = idx;
}
else if (cl.contains("wb-row")) {
// Plain tree
res.region = NodeRegion.title;
}
else {
// Somewhere near the title
if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) {
tree === null || tree === void 0 ? void 0 : tree.logWarn("getEventInfo(): not found", event, res);
}
return res;
}
if (res.colIdx === -1) {
res.colIdx = 0;
}
res.colDef = tree === null || tree === void 0 ? void 0 : tree.columns[res.colIdx];
res.colDef != null ? (res.colId = res.colDef.id) : 0;
// this.log("Event", event, res);
return res;
}
/**
* Return readable string representation for this instance.
* @internal
*/
toString() {
return `Wunderbaum<'${this.id}'>`;
}
/** Return true if any node title or grid cell is currently beeing edited.
*
* See also {@link isEditingTitle}.
*/
isEditing() {
const focusElem = this.nodeListElement.querySelector("input:focus,select:focus");
return !!focusElem;
}
/** Return true if any node is currently in edit-title mode.
*
* See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}.
*/
isEditingTitle() {
return this._callMethod("edit.isEditingTitle");
}
/**
* Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
*/
isLoading() {
let res = false;
this.root.visit((n) => {
// also visit rootNode
if (n._isLoading || n._requestId) {
res = true;
return false;
}
}, true);
return res;
}
/** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4.
* @see {@link logDebug}
*/
log(...args) {
if (this.options.debugLevel >= 4) {
console.log(this.toString(), ...args); // eslint-disable-line no-console
}
}
/** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4.
* and browser console level includes debug/verbose messages.
* @see {@link log}
*/
logDebug(...args) {
if (this.options.debugLevel >= 4) {
console.debug(this.toString(), ...args); // eslint-disable-line no-console
}
}
/** Write to `console.error` with tree name as prefix. */
logError(...args) {
if (this.options.debugLevel >= 1) {
console.error(this.toString(), ...args); // eslint-disable-line no-console
}
}
/** Write to `console.info` with tree name as prefix if opts.debugLevel >= 3. */
logInfo(...args) {
if (this.options.debugLevel >= 3) {
console.info(this.toString(), ...args); // eslint-disable-line no-console
}
}
/** @internal */
logTime(label) {
if (this.options.debugLevel >= 4) {
console.time(this + ": " + label); // eslint-disable-line no-console
}
return label;
}
/** @internal */
logTimeEnd(label) {
if (this.options.debugLevel >= 4) {
console.timeEnd(this + ": " + label); // eslint-disable-line no-console
}
}
/** Write to `console.warn` with tree name as prefix with if opts.debugLevel >= 2. */
logWarn(...args) {
if (this.options.debugLevel >= 2) {
console.warn(this.toString(), ...args); // eslint-disable-line no-console
}
}
/** Emit a warning for deprecated methods. @internal */
logDeprecate(method, options) {
if (this.options.debugLevel >= 2) {
let msg = `${this}: ${method} is deprecated`;
if (options === null || options === void 0 ? void 0 : options.since) {
msg += ` since ${options.since}`;
}
if (options === null || options === void 0 ? void 0 : options.hint) {
msg += ` (${options.since})`;
}
console.warn(msg + "."); // eslint-disable-line no-console
}
}
/** Reset column widths to default. @since 0.10.0 */
resetColumns() {
this.columns.forEach((col) => {
delete col.customWidthPx;
});
this.update(ChangeType.colStructure);
}
// /** Renumber nodes `_nativeIndex`. @see {@link WunderbaumNode.resetNativeChildOrder} */
// resetNativeChildOrder(options?: ResetOrderOptions) {
// this.root.resetNativeChildOrder(options);
// }
/**
* Make sure that this node is vertically scrolled into the viewport.
*
* Nodes that are above the visible area become the top row, nodes that are
* below the viewport become the bottom row.
*/
scrollTo(nodeOrOpts) {
const PADDING = 2; // leave some pixels between viewport bounds
let node;
// WunderbaumNode;
let options;
if (nodeOrOpts instanceof WunderbaumNode) {
node = nodeOrOpts;
}
else {
options = nodeOrOpts;
node = options.node;
}
assert(node && node._rowIdx != null, `Invalid node: ${node}`);
const rowHeight = this.options.rowHeightPx;
const scrollParent = this.element;
const headerHeight = this.headerElement.clientHeight; // May be 0
const scrollTop = scrollParent.scrollTop;
const vpHeight = scrollParent.clientHeight;
const rowTop = node._rowIdx * rowHeight + headerHeight;
const vpTop = headerHeight;
const vpRowTop = rowTop - scrollTop;
const vpRowBottom = vpRowTop + rowHeight;
const topNode = options === null || options === void 0 ? void 0 : options.topNode;
// this.log( `scrollTo(${node.title}), vpTop:${vpTop}px, scrollTop:${scrollTop}, vpHeight:${vpHeight}, rowTop:${rowTop}, vpRowTop:${vpRowTop}`, nodeOrOpts , options);
let newScrollTop = null;
if (vpRowTop >= vpTop) {
if (vpRowBottom <= vpHeight) ;
else {
// Node is below viewport
// this.log("Below viewport");
newScrollTop = rowTop + rowHeight - vpHeight + PADDING; // leave some pixels between viewport bounds
}
}
else {
// Node is above viewport
// this.log("Above viewport");
newScrollTop = rowTop - vpTop - PADDING; // leave some pixels between viewport bounds
}
if (newScrollTop != null) {
this.log(`scrollTo(${rowTop}): ${scrollTop} => ${newScrollTop}`);
scrollParent.scrollTop = newScrollTop;
if (topNode) {
// Make sure the topNode is always visible
this.scrollTo(topNode);
}
// this.update(ChangeType.scroll);
}
}
/**
* Make sure that this node is horizontally scrolled into the viewport.
* Called by {@link setColumn}.
*/
scrollToHorz() {
// const PADDING = 1;
const fixedWidth = this.columns[0]._widthPx;
const vpWidth = this.element.clientWidth;
const scrollLeft = this.element.scrollLeft;
const colElem = this.getActiveColElem();
const colLeft = Number.parseInt(colElem === null || colElem === void 0 ? void 0 : colElem.style.left, 10);
const colRight = colLeft + Number.parseInt(colElem === null || colElem === void 0 ? void 0 : colElem.style.width, 10);
let newLeft = scrollLeft;
if (colLeft - scrollLeft < fixedWidth) {
// The current column is scrolled behind the left fixed column
newLeft = colLeft - fixedWidth;
}
else if (colRight - scrollLeft > vpWidth) {
// The current column is scrolled outside the right side
newLeft = colRight - vpWidth;
}
newLeft = Math.max(0, newLeft);
// util.assert(node._rowIdx != null);
this.log(`scrollToHorz(${this.activeColIdx}): ${colLeft}..${colRight}, fixedOfs=${fixedWidth}, vpWidth=${vpWidth}, curLeft=${scrollLeft} -> ${newLeft}`);
this.element.scrollLeft = newLeft;
// this.update(ChangeType.scroll);
}
/**
* Set column #colIdx to 'active'.
*
* This higlights the column header and -cells by adding the `wb-active`
* class to all grid cells of the active column.
* Available in cell-nav mode only.
*
* If _options.edit_ is true, the embedded input element is focused, or if
* colIdx is 0, the node title is put into edit mode.
*/
setColumn(colIdx, options) {
var _a, _b, _c;
const edit = options === null || options === void 0 ? void 0 : options.edit;
const scroll = (options === null || options === void 0 ? void 0 : options.scrollIntoView) !== false;
assert(this.isCellNav(), "Expected cellNav mode");
if (typeof colIdx === "string") {
const cid = colIdx;
colIdx = this.columns.findIndex((c) => c.id === colIdx);
assert(colIdx >= 0, `Invalid colId: ${cid}`);
}
assert(0 <= colIdx && colIdx < this.columns.length, `Invalid colIdx: ${colIdx}`);
this.activeColIdx = colIdx;
// Update `wb-active` class for all headers
if (this.hasHeader()) {
for (const rowDiv of this.headerElement.children) {
let i = 0;
for (const colDiv of rowDiv.children) {
colDiv.classList.toggle("wb-active", i++ === colIdx);
}
}
}
(_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.update(ChangeType.status);
// Update `wb-active` class for all cell spans
for (const rowDiv of this.nodeListElement.children) {
let i = 0;
for (const colDiv of rowDiv.children) {
colDiv.classList.toggle("wb-active", i++ === colIdx);
}
}
// Horizontically scroll into view
if (scroll || edit) {
this.scrollToHorz();
}
if (edit && this.activeNode) {
// this.activeNode.setFocus(); // Blur prev. input if any
if (colIdx === 0) {
this.activeNode.startEditTitle();
}
else {
(_c = (_b = this.getActiveColElem()) === null || _b === void 0 ? void 0 : _b.querySelector("input,select")) === null || _c === void 0 ? void 0 : _c.focus();
}
}
}
/* Set or remove keyboard focus to the tree container. @internal */
_setActiveNode(node) {
this._activeNode = node;
}
/** Set or remove keyboard focus to the tree container. */
setActiveNode(key, flag = true, options) {
var _a;
(_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setActive(flag, options);
}
/** Set or remove keyboard focus to the tree container. */
setFocus(flag = true) {
if (flag) {
this.element.focus();
}
else {
this.element.blur();
}
}
/* Set or remove keyboard focus to the tree container. @internal */
_setFocusNode(node) {
this._focusNode = node;
}
/** Return the current selection/expansion/activation status. @experimental */
getState(options = {}) {
var _a, _b;
const { activeKey = true, expandedKeys = false, selectedKeys = false, } = options;
const expandSet = new Set();
if (expandedKeys) {
for (const node of this) {
if (node.isExpanded() && node.hasChildren()) {
expandSet.add(node.key);
}
}
}
// Parents of active node are always expanded
if (activeKey && this.activeNode) {
this.activeNode.visitParents((n) => {
if (n.parent) {
expandSet.add(n.key);
}
}, false);
}
const state = {
expandedKeys: expandSet.size ? Array.from(expandSet) : undefined,
activeKey: (_b = (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null,
activeColIdx: this.activeColIdx,
selectedKeys: selectedKeys
? this.getSelectedNodes().flatMap((n) => n.key)
: undefined,
};
return state;
}
/** Apply selection/expansion/activation status. @experimental */
async setState(state, options = {}) {
const { expandLazy = true } = options;
return this.runWithDeferredUpdateAsync(async () => {
var _a, _b;
if (state.expandedKeys && state.expandedKeys.length) {
if (expandLazy) {
// Expand all keys recursively, even if they are not in the tree yet
await this._loadLazyNodes(state.expandedKeys, {
expand: true,
noEvents: true,
});
}
else {
for (const key of state.expandedKeys) {
(_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setExpanded(true);
}
}
}
if (state.activeKey) {
this.setActiveNode(state.activeKey);
}
if (state.selectedKeys) {
this.selectAll(false);
for (const key of state.selectedKeys) {
(_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setSelected(true);
}
}
if (this.isCellNav() && state.activeColIdx != null) {
this.setColumn(state.activeColIdx);
}
});
}
update(change, node, options) {
// this.log(`update(${change}) node=${node}`);
if (!(node instanceof WunderbaumNode)) {
options = node;
node = undefined;
}
const immediate = !!getOption(options, "immediate");
const RF = RenderFlag;
const pending = this.pendingChangeTypes;
if (this._disableUpdateCount) {
// Assuming that we redraw all when enableUpdate() is re-enabled.
// this.log(
// `IGNORED update(${change}) node=${node} (disable level ${this._disableUpdateCount})`
// );
this._disableUpdateIgnoreCount++;
return;
}
switch (change) {
case ChangeType.any:
case ChangeType.colStructure:
pending.add(RF.header);
pending.add(RF.clearMarkup);
pending.add(RF.redraw);
pending.add(RF.scroll);
break;
case ChangeType.resize:
// case ChangeType.colWidth:
pending.add(RF.header);
pending.add(RF.redraw);
break;
case ChangeType.structure:
pending.add(RF.redraw);
break;
case ChangeType.scroll:
pending.add(RF.scroll);
break;
case ChangeType.row:
case ChangeType.data:
case ChangeType.status:
assert(node, `Option '${change}' requires a node.`);
// Single nodes are immediately updated if already inside the viewport
// (otherwise we can ignore)
if (node._rowElem) {
node._render({ change: change });
}
break;
default:
error(`Invalid change type '${change}'.`);
}
if (change === ChangeType.colStructure) {
const isGrid = this.isGrid();
this.element.classList.toggle("wb-grid", isGrid);
if (!isGrid && this.isCellNav()) {
this.setCellNav(false);
}
}
if (pending.size > 0) {
if (immediate) {
this._updateViewportImmediately();
}
else {
this._updateViewportThrottled();
}
}
}
/** Disable mouse and keyboard interaction (return prev. state). */
setEnabled(flag = true) {
const prev = this.enabled;
this.enabled = !!flag;
this.element.classList.toggle("wb-disabled", !flag);
return prev;
}
/** Return false if tree is disabled. */
isEnabled() {
return this.enabled;
}
/** Return true if tree has more than one column, i.e. has additional data columns. */
isGrid() {
return this.columns && this.columns.length > 1;
}
/** Return true if cell-navigation mode is active. */
isCellNav() {
return !!this._cellNavMode;
}
/** Return true if row-navigation mode is active. */
isRowNav() {
return !this._cellNavMode;
}
/** Set the tree's navigation mode. */
setCellNav(flag = true) {
var _a;
const prev = this._cellNavMode;
// if (flag === prev) {
// return;
// }
this._cellNavMode = !!flag;
if (flag && !prev) {
// switch from row to cell mode
this.setColumn(0);
}
this.element.classList.toggle("wb-cell-mode", flag);
(_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.update(ChangeType.status);
}
/** Set the tree's navigation mode option. */
setNavigationOption(mode, reset = false) {
if (!this.isGrid() && mode !== NavModeEnum.row) {
this.logWarn("Plain trees only support row navigation mode.");
return;
}
this.options.navigationModeOption = mode;
switch (mode) {
case NavModeEnum.cell:
this.setCellNav(true);
break;
case NavModeEnum.row:
this.setCellNav(false);
break;
case NavModeEnum.startCell:
if (reset) {
this.setCellNav(true);
}
break;
case NavModeEnum.startRow:
if (reset) {
this.setCellNav(false);
}
break;
default:
error(`Invalid mode '${mode}'.`);
}
}
/** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */
setStatus(status, options) {
return this.root.setStatus(status, options);
}
/** Add or redefine node type definitions. */
setTypes(types, replace = true) {
assert(isPlainObject(types), `Expected plain objext: ${types}`);
if (replace) {
this.types = types;
}
else {
extend(this.types, types);
}
// Convert `TYPE.classes` to a Set
for (const t of Object.values(this.types)) {
if (t.classes) {
t.classes = toSet(t.classes);
}
}
}
/**
* Sort nodes list by title or custom criteria.
* @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
* (defaults to sorting by title).
* @param {boolean} deep pass true to sort all descendant nodes recursively
* @deprecated use {@link sort}
*/
sortChildren(cmp = nodeTitleSorter, deep = false) {
this.logDeprecate("sortChildren()", { since: "0.14.0" });
return this.sort({
cmp: cmp ? cmp : undefined,
deep: deep,
propName: "title",
});
}
/**
* Convenience method to implement column sorting.
* @see {@link WunderbaumNode.sortByProperty}.
* @since 0.11.0
* @deprecated use {@link sort}
*/
sortByProperty(options) {
this.logDeprecate("sortByProperty()", { since: "0.14.0" });
this.root.sortByProperty(options);
}
/**
* Sort nodes list by title or custom criteria.
* @since 0.14.0
*/
sort(options) {
this.root.sort(options);
}
/** Convert tree to an array of plain objects.
*
* @param callback is called for every node, in order to allow
* modifications.
* Return `false` to ignore this node or `"skip"` to include this node
* without its children.
* @see {@link WunderbaumNode.toDict}.
*/
toDictArray(callback) {
var _a;
const res = this.root.toDict(true, callback);
return (_a = res.children) !== null && _a !== void 0 ? _a : [];
}
/**
* Update column headers and column width.
* Return true if at least one column width changed.
*/
// _updateColumnWidths(options?: UpdateColumnsOptions): boolean {
_updateColumnWidths() {
// options = Object.assign({ updateRows: true, renderMarkup: false }, options);
const defaultMinWidth = 4;
const vpWidth = this.element.clientWidth;
// Shorten last column width to avoid h-scrollbar
// (otherwise resizbing the demo would display a void scrollbar?)
const FIX_ADJUST_LAST_COL = 1;
const columns = this.columns;
const col0 = columns[0];
let totalWidth = 0;
let totalWeight = 0;
let fixedWidth = 0;
let modified = false;
// this.element.classList.toggle("wb-grid", isGrid);
// if (!isGrid && this.isCellNav()) {
// this.setCellNav(false);
// }
// if (options.calculateCols) {
if (col0.id !== "*") {
throw new Error(`First column must have id '*': got '${col0.id}'.`);
}
// Gather width definitions
this._columnsById = {};
for (const col of columns) {
this._columnsById[col.id] = col;
const cw = col.customWidthPx ? `${col.customWidthPx}px` : col.width;
if (col.id === "*" && col !== col0) {
throw new Error(`Column id '*' must be defined only once: '${col.title}'.`);
}
if (!cw || cw === "*") {
col._weight = 1.0;
totalWeight += 1.0;
}
else if (typeof cw === "number") {
col._weight = cw;
totalWeight += cw;
}
else if (typeof cw === "string" && cw.endsWith("px")) {
col._weight = 0;
const px = parseFloat(cw.slice(0, -2));
if (col._widthPx != px) {
modified = true;
col._widthPx = px;
}
fixedWidth += px;
}
else {
error(`Invalid column width: ${cw} (expected string ending with 'px' or number, e.g. "
px" or ).`);
}
}
// Share remaining space between non-fixed columns
const restPx = Math.max(0, vpWidth - fixedWidth);
let ofsPx = 0;
for (const col of columns) {
let minWidth;
if (col._weight) {
const cmw = col.minWidth;
if (typeof cmw === "number") {
minWidth = cmw;
}
else if (typeof cmw === "string" && cmw.endsWith("px")) {
minWidth = parseFloat(cmw.slice(0, -2));
}
else {
minWidth = defaultMinWidth;
}
const px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
if (col._widthPx != px) {
modified = true;
col._widthPx = px;
}
}
col._ofsPx = ofsPx;
ofsPx += col._widthPx;
}
columns[columns.length - 1]._widthPx -= FIX_ADJUST_LAST_COL;
totalWidth = ofsPx - FIX_ADJUST_LAST_COL;
const tw = `${totalWidth}px`;
this.headerElement.style.width = tw;
this.listContainerElement.style.width = tw;
// }
// Every column has now a calculated `_ofsPx` and `_widthPx`
// this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
// console.trace();
// util.error("BREAK");
// if (modified) {
// this._renderHeaderMarkup();
// if (options.renderMarkup) {
// this.update(ChangeType.header, { removeMarkup: true });
// } else if (options.updateRows) {
// this._updateRows();
// }
// }
return modified;
}
// protected _insertIcon(icon: string, elem: HTMLElement) {
// const iconElem = document.createElement("i");
// iconElem.className = icon;
// elem.appendChild(iconElem);
// }
/** Create/update header markup from `this.columns` definition.
* @internal
*/
_renderHeaderMarkup() {
assert(this.headerElement, "Expected a headerElement");
const wantHeader = this.hasHeader();
setElemDisplay(this.headerElement, wantHeader);
if (!wantHeader) {
return;
}
const iconMap = this.iconMap;
const colCount = this.columns.length;
const headerRow = this.headerElement.querySelector(".wb-row");
assert(headerRow, "Expected a row in header element");
headerRow.innerHTML = "".repeat(colCount);
for (let i = 0; i < colCount; i++) {
const col = this.columns[i];
const colElem = headerRow.children[i];
colElem.style.left = col._ofsPx + "px";
colElem.style.width = col._widthPx + "px";
// Add classes from `columns` definition to `` cells
if (typeof col.headerClasses === "string") {
col.headerClasses
? colElem.classList.add(...col.headerClasses.split(" "))
: 0;
}
else {
col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0;
}
// Add tooltip to column title
let tooltip = "";
if (col.tooltip) {
tooltip = escapeTooltip(col.tooltip);
tooltip = ` title="${tooltip}"`;
}
// Add column header icons
let addMarkup = "";
// NOTE: we use CSS float: right to align icons, so they must be added in
// reverse order
if (toBool(col.menu, this.options.columnsMenu, false)) {
const iconClass = "wb-col-icon-menu " + iconMap.colMenu;
const icon = ``;
addMarkup += icon;
}
if (toBool(col.sortable, this.options.columnsSortable, false)) {
let iconClass = "wb-col-icon-sort " + iconMap.colSortable;
if (col.sortOrder) {
iconClass += `wb-col-sort-${col.sortOrder}`;
iconClass +=
col.sortOrder === "asc" ? iconMap.colSortAsc : iconMap.colSortDesc;
}
const icon = ``;
addMarkup += icon;
}
if (toBool(col.filterable, this.options.columnsFilterable, false)) {
colElem.classList.toggle("wb-col-filter", !!col.filterActive);
let iconClass = "wb-col-icon-filter " + iconMap.colFilter;
if (col.filterActive) {
iconClass += iconMap.colFilterActive;
}
const icon = ``;
addMarkup += icon;
}
// Add resizer to all but the last column
if (i < colCount - 1) {
if (toBool(col.resizable, this.options.columnsResizable, false)) {
addMarkup +=
'';
}
else {
addMarkup += '';
}
}
// Create column header
const title = escapeHtml(col.title || col.id);
colElem.innerHTML = `${title}${addMarkup}`;
// Highlight active column
if (this.isCellNav()) {
colElem.classList.toggle("wb-active", i === this.activeColIdx);
}
}
}
/**
* Render pending changes that were scheduled using {@link WunderbaumNode.update} if any.
*
* This is hardly ever neccessary, since we normally either
* - call `update(ChangeType.TYPE)` (async, throttled), or
* - call `update(ChangeType.TYPE, {immediate: true})` (synchronous)
*
* `updatePendingModifications()` will only force immediate execution of
* pending async changes if any.
*/
updatePendingModifications() {
if (this.pendingChangeTypes.size > 0) {
this._updateViewportImmediately();
}
}
/** @internal */
_createNodeIcon(node, showLoading, showBadge) {
const iconMap = this.iconMap;
let iconElem;
let icon = node.getOption("icon");
if (node._errorInfo) {
icon = iconMap.error;
}
else if (node._isLoading && showLoading) {
// Status nodes, or nodes without expander (< minExpandLevel) should
// display the 'loading' status with the i.wb-icon span
icon = iconMap.loading;
}
if (icon === false) {
return null; // explicitly disabled: don't try default icons
}
if (typeof icon === "string") ;
else if (node.statusNodeType) {
icon = iconMap[node.statusNodeType];
}
else if (node.expanded) {
icon = iconMap.folderOpen;
}
else if (node.children) {
icon = iconMap.folder;
}
else if (node.lazy) {
icon = iconMap.folderLazy;
}
else {
icon = iconMap.doc;
}
if (!icon) {
iconElem = document.createElement("i");
iconElem.className = "wb-icon";
}
else if (TEST_HTML.test(icon)) {
iconElem = elemFromHtml(icon);
}
else if (TEST_FILE_PATH.test(icon)) {
iconElem = elemFromHtml(``);
}
else {
// Class name
iconElem = document.createElement("i");
iconElem.className = "wb-icon " + icon;
}
// Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement
const cbRes = showBadge && node._callEvent("iconBadge", { iconSpan: iconElem });
let badge = null;
if (cbRes != null && cbRes !== false) {
let classes = "";
let tooltip = "";
if (isPlainObject(cbRes)) {
badge = "" + cbRes.badge;
classes = cbRes.badgeClass ? " " + cbRes.badgeClass : "";
tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : "";
}
else if (typeof cbRes === "number") {
badge = "" + cbRes;
}
else {
badge = cbRes; // string or HTMLSpanElement
}
if (typeof badge === "string") {
badge = elemFromHtml(`${escapeHtml(badge)}`);
}
if (badge) {
iconElem.append(badge);
}
}
return iconElem;
}
_updateTopBreadcrumb() {
const breadcrumb = this.breadcrumb;
const topmost = this.getTopmostVpNode(true);
const parentList = topmost === null || topmost === void 0 ? void 0 : topmost.getParentList(false, false);
if (parentList === null || parentList === void 0 ? void 0 : parentList.length) {
breadcrumb.innerHTML = "";
for (const n of topmost.getParentList(false, false)) {
const icon = this._createNodeIcon(n, false, false);
if (icon) {
breadcrumb.append(icon, " ");
}
const part = document.createElement("a");
part.textContent = n.title;
part.href = "#";
part.classList.add("wb-breadcrumb");
part.dataset.key = n.key;
breadcrumb.append(part, this.options.strings.breadcrumbDelimiter);
}
}
else {
breadcrumb.innerHTML = " ";
}
}
/**
* This is the actual update method, which is wrapped inside a throttle method.
* It calls `updateColumns()` and `_updateRows()`.
*
* This protected method should not be called directly but via
* {@link WunderbaumNode.update}`, {@link Wunderbaum.update},
* or {@link Wunderbaum.updatePendingModifications}.
* @internal
*/
_updateViewportImmediately() {
if (this._disableUpdateCount) {
this.log(`_updateViewportImmediately() IGNORED (disable level: ${this._disableUpdateCount}).`);
this._disableUpdateIgnoreCount++;
return;
}
if (this._updateViewportThrottled.pending()) {
// this.logWarn(`_updateViewportImmediately() cancel pending timer.`);
this._updateViewportThrottled.cancel();
}
// Shorten container height to avoid v-scrollbar
const FIX_ADJUST_HEIGHT = 1;
const RF = RenderFlag;
const pending = new Set(this.pendingChangeTypes);
this.pendingChangeTypes.clear();
const scrollOnly = pending.has(RF.scroll) && pending.size === 1;
if (scrollOnly) {
this._updateRows({ newNodesOnly: true });
// this.log("_updateViewportImmediately(): scroll only.");
}
else {
this.log("_updateViewportImmediately():", pending);
if (this.options.adjustHeight !== false) {
let height = this.listContainerElement.clientHeight;
const headerHeight = this.headerElement.clientHeight; // May be 0
const wantHeight = this.element.clientHeight - headerHeight - FIX_ADJUST_HEIGHT;
if (Math.abs(height - wantHeight) > 1.0) {
// this.log("resize", height, wantHeight);
this.listContainerElement.style.height = wantHeight + "px";
height = wantHeight;
}
}
// console.profile(`_updateViewportImmediately()`)
if (pending.has(RF.clearMarkup)) {
this.visit((n) => {
n.removeMarkup();
});
}
// let widthModified = false;
if (pending.has(RF.header)) {
// widthModified = this._updateColumnWidths();
this._updateColumnWidths();
this._renderHeaderMarkup();
}
this._updateRows();
// console.profileEnd(`_updateViewportImmediately()`)
}
if (this.breadcrumb) {
this._updateTopBreadcrumb();
}
this._callEvent("update");
}
// /**
// * Assert that TR order matches the natural node order
// * @internal
// */
// protected _validateRows(): boolean {
// let trs = this.nodeListElement.childNodes;
// let i = 0;
// let prev = -1;
// let ok = true;
// trs.forEach((element) => {
// const tr = element as HTMLTableRowElement;
// const top = Number.parseInt(tr.style.top);
// const n = (tr)._wb_node;
// // if (i < 4) {
// // console.info(
// // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'`
// // );
// // }
// if (prev >= 0 && top !== prev + ROW_HEIGHT) {
// n.logWarn(
// `TR order mismatch at index ${i}: top=${top}px != ${
// prev + ROW_HEIGHT
// }`
// );
// // throw new Error("fault");
// ok = false;
// }
// prev = top;
// i++;
// });
// return ok;
// }
/*
* - Traverse all *visible* nodes of the whole tree, i.e. skip collapsed nodes.
* - Store count of rows to `tree.treeRowCount`.
* - Renumber `node._rowIdx` for all visible nodes.
* - Calculate the index range that must be rendered to fill the viewport
* (including upper and lower prefetch)
* -
*/
_updateRows(options) {
// const label = this.logTime("_updateRows");
// this.log("_updateRows", opts)
options = Object.assign({ newNodesOnly: false }, options);
const newNodesOnly = !!options.newNodesOnly;
const rowHeight = this.options.rowHeightPx;
const vpHeight = this.element.clientHeight;
const prefetch = RENDER_MAX_PREFETCH;
// const grace_prefetch = RENDER_MAX_PREFETCH - RENDER_MIN_PREFETCH;
const ofs = this.element.scrollTop;
let startIdx = Math.max(0, ofs / rowHeight - prefetch);
startIdx = Math.floor(startIdx);
// Make sure start is always even, so the alternating row colors don't
// change when scrolling:
if (startIdx % 2) {
startIdx--;
}
let endIdx = Math.max(0, (ofs + vpHeight) / rowHeight + prefetch);
endIdx = Math.ceil(endIdx);
// this.debug("render", opts);
const obsoleteNodes = new Set();
this.nodeListElement.childNodes.forEach((elem) => {
if (elem._wb_node) {
obsoleteNodes.add(elem._wb_node);
}
});
let idx = 0;
let top = 0;
let modified = false;
let prevElem = "first";
this.visitRows(function (node) {
// node.log("visit")
const rowDiv = node._rowElem;
// Renumber all expanded nodes
if (node._rowIdx !== idx) {
node._rowIdx = idx;
modified = true;
}
if (idx < startIdx || idx > endIdx) {
// row is outside viewport bounds
if (rowDiv) {
prevElem = rowDiv;
}
}
else if (rowDiv && newNodesOnly) {
obsoleteNodes.delete(node);
// no need to update existing node markup
rowDiv.style.top = idx * rowHeight + "px";
prevElem = rowDiv;
}
else {
obsoleteNodes.delete(node);
// Create new markup
if (rowDiv) {
rowDiv.style.top = idx * rowHeight + "px";
}
node._render({ top: top, after: prevElem });
// node.log("render", top, prevElem, "=>", node._rowElem);
prevElem = node._rowElem;
}
idx++;
top += rowHeight;
});
this.treeRowCount = idx;
for (const n of obsoleteNodes) {
n._callEvent("discard");
n.removeMarkup();
}
// Resize tree container
this.nodeListElement.style.height = `${top}px`;
// this.log(
// `_updateRows(scrollOfs:${ofs}, ${startIdx}..${endIdx})`,
// this.nodeListElement.style.height
// );
// this.logTimeEnd(label);
// this._validateRows();
return modified;
}
/**
* Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order).
* @see `wb_node.WunderbaumNode.IterableIterator`
* @see {@link WunderbaumNode.visit}.
*
* @param {function} callback the callback function.
* Return false to stop iteration, return "skip" to skip this node and
* children only.
* @returns {boolean} false, if the iterator was stopped.
*/
visit(callback) {
return this.root.visit(callback, false);
}
/**
* Call callback(node) for all nodes in vertical order, top down (or bottom up).
*
* Note that this considers expansion state, i.e. filtered nodes and children
* of collapsed nodes are skipped, unless `includeHidden` is set.
*
* Stop iteration if callback() returns false.
* Return false if iteration was stopped.
*
* @returns {boolean} false if iteration was canceled
*/
visitRows(callback, options) {
if (!this.root.hasChildren()) {
return false;
}
if (options && options.reverse) {
delete options.reverse;
return this._visitRowsUp(callback, options);
}
options = options || {};
let i, nextIdx, parent, res, siblings, stopNode, siblingOfs = 0, skipFirstNode = options.includeSelf === false, node = options.start || this.root.children[0];
const includeHidden = !!options.includeHidden;
const checkFilter = !includeHidden && this.filterMode === "hide";
parent = node.parent;
while (parent) {
// visit siblings
siblings = parent.children;
nextIdx = siblings.indexOf(node) + siblingOfs;
assert(nextIdx >= 0, `Could not find ${node} in parent's children: ${parent}`);
for (i = nextIdx; i < siblings.length; i++) {
node = siblings[i];
if (node === stopNode) {
return false;
}
if (checkFilter &&
!node.statusNodeType &&
!node.match &&
!node.subMatchCount) {
continue;
}
if (!skipFirstNode && callback(node) === false) {
return false;
}
skipFirstNode = false;
// Dive into node's child nodes
if (node.children &&
node.children.length &&
(includeHidden || node.expanded)) {
res = node.visit((n) => {
if (n === stopNode) {
return false;
}
if (checkFilter && !n.match && !n.subMatchCount) {
return "skip";
}
if (callback(n) === false) {
return false;
}
if (!includeHidden && n.children && !n.expanded) {
return "skip";
}
}, false);
if (res === false) {
return false;
}
}
}
// Visit parent nodes (bottom up)
node = parent;
parent = parent.parent;
siblingOfs = 1; //
if (!parent && options.wrap) {
this.logDebug("visitRows(): wrap around");
assert(options.start, "`wrap` option requires `start`");
stopNode = options.start;
options.wrap = false;
parent = this.root;
siblingOfs = 0;
}
}
return true;
}
/**
* Call fn(node) for all nodes in vertical order, bottom up.
* @internal
*/
_visitRowsUp(callback, options) {
let children, idx, parent, node = options.start || this.root.children[0];
const includeHidden = !!options.includeHidden;
if (options.includeSelf !== false) {
if (callback(node) === false) {
return false;
}
}
while (true) {
parent = node.parent;
children = parent.children;
if (children[0] === node) {
// If this is already the first sibling, goto parent
node = parent;
if (!node.parent) {
break; // first node of the tree
}
children = parent.children;
}
else {
// Otherwise, goto prev. sibling
idx = children.indexOf(node);
node = children[idx - 1];
// If the prev. sibling has children, follow down to last descendant
while ((includeHidden || node.expanded) &&
node.children &&
node.children.length) {
children = node.children;
parent = node;
node = children[children.length - 1];
}
}
// Skip invisible
if (!includeHidden && !node.isVisible()) {
continue;
}
if (callback(node) === false) {
return false;
}
}
return true;
}
/**
* Reload the tree with a new source.
*
* Previous data is cleared. Note that also column- and type defintions may
* be passed with the `source` object.
* @see {@link Wunderbaum.reload} for a shortcut to reload the last ajax request
* and restore the previous state.
*/
async load(source) {
this.clear();
this._initialSource = source;
return this.root.load(source);
}
/** Reload the tree and optionally restore state.
* Source defaults to last ajax url if any.
* Restoring the active node requires stable keys
* @see {@link WunderbaumOptions.autoKeys}
* @see {@link Wunderbaum.load}
* @experimental
*/
async reload(options = {}) {
const { source = this._initialSource, reactivate = true } = options;
if (!source) {
this.logWarn("No previous ajax source to reload.");
return;
}
if (!reactivate) {
return this.load(source);
}
const state = this.getState();
await this.load(source);
return this.setState(state);
}
/**
* Make sure that all nodes in the given keyList are accessible.
* This may include loading lazy parent nodes.
* Recursively load (and optionally expand) all requested node paths.
*/
async _loadLazyNodes(keyList, options = {}) {
const { expand = true } = options;
const keySet = new Set(keyList);
// Make sure that all parent nodes are loaded (and expand if requested)
while (keySet.size > 0) {
const pendingNodes = [];
const curSet = new Set(keySet);
for (const key of curSet) {
const node = this.findKey(key);
if (!node) {
continue; // key not yet found (need to load lazy parent?)
}
keySet.delete(key);
if (expand) {
pendingNodes.push(node.setExpanded(true));
}
else if (node.isUnloaded()) {
pendingNodes.push(node.loadLazy());
}
if (node._rowElem) {
node._render(); // show spinner even is update is suppressed
}
}
if (pendingNodes.length === 0) {
// will not load any more nodes, so if if there are still keys
// left in the set, we will never find them
this.logWarn(`Could not expand ${keySet.size} nodes:`, keySet);
break;
}
await Promise.allSettled(pendingNodes);
}
}
/**
* Disable render requests during operations that would trigger many updates.
*
* ```js
* try {
* tree.enableUpdate(false);
* // ... (long running operation that would trigger many updates)
* foo();
* // ... NOTE: make sure that async operations have finished, e.g.
* await foo();
* } finally {
* tree.enableUpdate(true);
* }
* ```
*/
enableUpdate(flag) {
/*
5 7 9 20 25 30
1 >-------------------------------------<
2 >--------------------<
3 >--------------------------<
*/
if (flag) {
assert(this._disableUpdateCount > 0, "enableUpdate(true) was called too often");
this._disableUpdateCount--;
// this.logDebug(
// `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
// );
if (this._disableUpdateCount === 0) {
this.logDebug(`enableUpdate(): active again. Re-painting to catch up with ${this._disableUpdateIgnoreCount} ignored update requests...`);
this._disableUpdateIgnoreCount = 0;
this.update(ChangeType.any, { immediate: true });
}
}
else {
this._disableUpdateCount++;
// this.logDebug(
// `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
// );
// this._disableUpdate = Date.now();
}
// return !flag; // return previous value
}
/* ---------------------------------------------------------------------------
* FILTER
* -------------------------------------------------------------------------*/
/**
* Dim or hide unmatched nodes.
* @param filter a string to match against node titles, or a callback function.
* @param options filter options. Defaults to the `tree.options.filter` settings.
* @returns the number of nodes that match the filter.
* @example
* ```ts
* tree.filterNodes("foo", {mode: 'dim', fuzzy: true});
* // or pass a callback
* tree.filterNodes((node) => { return node.data.foo === true }, {mode: 'hide'});
* ```
*/
filterNodes(filter, options) {
return this.extensions.filter.filterNodes(filter, options);
}
/**
* Return the number of nodes that match the current filter.
* @see {@link Wunderbaum.filterNodes}
* @since 0.9.0
*/
countMatches() {
return this.extensions.filter.countMatches();
}
/**
* Dim or hide whole branches.
* @deprecated Use {@link filterNodes} instead and set `options.matchBranch: true`.
*/
filterBranches(filter, options) {
return this.extensions.filter.filterBranches(filter, options);
}
/**
* Reset the filter.
*/
clearFilter() {
return this.extensions.filter.clearFilter();
}
/**
* Return true if a filter is currently applied.
*/
isFilterActive() {
return !!this.filterMode;
}
/**
* Re-apply current filter.
*/
updateFilter() {
return this.extensions.filter.updateFilter();
}
}
Wunderbaum.sequence = 0;
/** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
Wunderbaum.version = "v0.14.1"; // Set to semver by 'grunt release'
/** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
Wunderbaum.util = util;
/** A map of default iconMaps.
* May be used as default, when passing partial icon definition maps:
* ```js
* const tree = new mar10.Wunderbaum({
* ...
* iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, {
* folder: "bi bi-archive",
* }),
* });
* ```
*/
Wunderbaum.iconMaps = defaultIconMaps;
export { Wunderbaum };
================================================
FILE: dist/wunderbaum.umd.js
================================================
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.mar10 = {}));
})(this, (function (exports) { 'use strict';
/*!
* Wunderbaum - debounce.ts
* Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
* v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
*/
/*
* debounce & throttle, taken from https://github.com/lodash/lodash v4.17.21
* MIT License: https://raw.githubusercontent.com/lodash/lodash/4.17.21-npm/LICENSE
* Modified for TypeScript type annotations.
*/
/* --- */
/** Detect free variable `global` from Node.js. */
const freeGlobal = typeof global === "object" &&
global !== null &&
global.Object === Object &&
global;
/** Detect free variable `globalThis` */
const freeGlobalThis = typeof globalThis === "object" &&
globalThis !== null &&
globalThis.Object == Object &&
globalThis;
/** Detect free variable `self`. */
const freeSelf = typeof self === "object" && self !== null && self.Object === Object && self;
/** Used as a reference to the global object. */
const root = freeGlobalThis || freeGlobal || freeSelf || Function("return this")();
/**
* Checks if `value` is the
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
*
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
* @example
*
* isObject({})
* // => true
*
* isObject([1, 2, 3])
* // => true
*
* isObject(Function)
* // => true
*
* isObject(null)
* // => false
*/
function isObject(value) {
const type = typeof value;
return value != null && (type === "object" || type === "function");
}
/**
* Creates a debounced function that delays invoking `func` until after `wait`
* milliseconds have elapsed since the last time the debounced function was
* invoked, or until the next browser frame is drawn. The debounced function
* comes with a `cancel` method to cancel delayed `func` invocations and a
* `flush` method to immediately invoke them. Provide `options` to indicate
* whether `func` should be invoked on the leading and/or trailing edge of the
* `wait` timeout. The `func` is invoked with the last arguments provided to the
* debounced function. Subsequent calls to the debounced function return the
* result of the last `func` invocation.
*
* **Note:** If `leading` and `trailing` options are `true`, `func` is
* invoked on the trailing edge of the timeout only if the debounced function
* is invoked more than once during the `wait` timeout.
*
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
* until the next tick, similar to `setTimeout` with a timeout of `0`.
*
* If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
* invocation will be deferred until the next frame is drawn (typically about
* 16ms).
*
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
* for details over the differences between `debounce` and `throttle`.
*
* @since 0.1.0
* @category Function
* @param {Function} func The function to debounce.
* @param {number} [wait=0]
* The number of milliseconds to delay; if omitted, `requestAnimationFrame` is
* used (if available).
* @param [options={}] The options object.
* @returns {Function} Returns the new debounced function.
* @example
*
* // Avoid costly calculations while the window size is in flux.
* jQuery(window).on('resize', debounce(calculateLayout, 150))
*
* // Invoke `sendMail` when clicked, debouncing subsequent calls.
* jQuery(element).on('click', debounce(sendMail, 300, {
* 'leading': true,
* 'trailing': false
* }))
*
* // Ensure `batchLog` is invoked once after 1 second of debounced calls.
* const debounced = debounce(batchLog, 250, { 'maxWait': 1000 })
* const source = new EventSource('/stream')
* jQuery(source).on('message', debounced)
*
* // Cancel the trailing debounced invocation.
* jQuery(window).on('popstate', debounced.cancel)
*
* // Check for pending invocations.
* const status = debounced.pending() ? "Pending..." : "Ready"
*/
function debounce(func, wait = 0, options = {}) {
let lastArgs, lastThis, maxWait, result, timerId, lastCallTime;
let lastInvokeTime = 0;
let leading = false;
let maxing = false;
let trailing = true;
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
const useRAF = !wait && wait !== 0 && typeof root.requestAnimationFrame === "function";
if (typeof func !== "function") {
throw new TypeError("Expected a function");
}
wait = +wait || 0;
if (isObject(options)) {
leading = !!options.leading;
maxing = "maxWait" in options;
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait;
trailing = "trailing" in options ? !!options.trailing : trailing;
}
function invokeFunc(time) {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
function startTimer(pendingFunc, wait) {
if (useRAF) {
root.cancelAnimationFrame(timerId);
return root.requestAnimationFrame(pendingFunc);
}
return setTimeout(pendingFunc, wait);
}
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id);
}
clearTimeout(id);
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
const timeWaiting = wait - timeSinceLastCall;
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (lastCallTime === undefined ||
timeSinceLastCall >= wait ||
timeSinceLastCall < 0 ||
(maxing && timeSinceLastInvoke >= maxWait));
}
function timerExpired() {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time));
}
function trailingEdge(time) {
timerId = undefined;
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now());
}
function pending() {
return timerId !== undefined;
}
function debounced(...args) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
// eslint-disable-next-line @typescript-eslint/no-this-alias
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
debounced.pending = pending;
return debounced;
}
/**
* Creates a throttled function that only invokes `func` at most once per
* every `wait` milliseconds (or once per browser frame). The throttled function
* comes with a `cancel` method to cancel delayed `func` invocations and a
* `flush` method to immediately invoke them. Provide `options` to indicate
* whether `func` should be invoked on the leading and/or trailing edge of the
* `wait` timeout. The `func` is invoked with the last arguments provided to the
* throttled function. Subsequent calls to the throttled function return the
* result of the last `func` invocation.
*
* **Note:** If `leading` and `trailing` options are `true`, `func` is
* invoked on the trailing edge of the timeout only if the throttled function
* is invoked more than once during the `wait` timeout.
*
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
* until the next tick, similar to `setTimeout` with a timeout of `0`.
*
* If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
* invocation will be deferred until the next frame is drawn (typically about
* 16ms).
*
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
* for details over the differences between `throttle` and `debounce`.
*
* @since 0.1.0
* @category Function
* @param {Function} func The function to throttle.
* @param {number} [wait=0]
* The number of milliseconds to throttle invocations to; if omitted,
* `requestAnimationFrame` is used (if available).
* @param [options={}] The options object.
* @returns {Function} Returns the new throttled function.
* @example
*
* // Avoid excessively updating the position while scrolling.
* jQuery(window).on('scroll', throttle(updatePosition, 100))
*
* // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
* const throttled = throttle(renewToken, 300000, { 'trailing': false })
* jQuery(element).on('click', throttled)
*
* // Cancel the trailing throttled invocation.
* jQuery(window).on('popstate', throttled.cancel)
*/
function throttle(func, wait = 0, options = {}) {
let leading = true;
let trailing = true;
if (typeof func !== "function") {
throw new TypeError("Expected a function");
}
if (isObject(options)) {
leading = "leading" in options ? !!options.leading : leading;
trailing = "trailing" in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
leading,
trailing,
maxWait: wait,
});
}
/*!
* Wunderbaum - util
* Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
* v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum)
*/
/** @module util */
/** Readable names for `MouseEvent.button` */
const MOUSE_BUTTONS = {
0: "",
1: "left",
2: "middle",
3: "right",
4: "back",
5: "forward",
};
const MAX_INT = 9007199254740991;
const userInfo = _getUserInfo();
/**True if the client is using a macOS platform. */
const isMac = userInfo.isMac;
const REX_HTML = /[&<>"'/]/g; // Escape those characters
const REX_TOOLTIP = /[<>"'/]/g; // Don't escape `&` in tooltips
const ENTITY_MAP = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
"/": "/",
};
/** A generic error that can be thrown to indicate a validation error when
* handling the `apply` event for a node title or the `change` event for a
* grid cell.
*/
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
/**
* A ES6 Promise, that exposes the resolve()/reject() methods.
*
* TODO: See [Promise.withResolvers()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers#description)
* , a proposed standard, but not yet implemented in any browser.
*/
let Deferred$1 = class Deferred {
constructor() {
this.thens = [];
this.catches = [];
this.status = "";
}
resolve(value) {
if (this.status) {
throw new Error("already settled");
}
this.status = "resolved";
this.resolvedValue = value;
this.thens.forEach((t) => t(value));
this.thens = []; // Avoid memleaks.
}
reject(error) {
if (this.status) {
throw new Error("already settled");
}
this.status = "rejected";
this.rejectedError = error;
this.catches.forEach((c) => c(error));
this.catches = []; // Avoid memleaks.
}
then(cb) {
if (status === "resolved") {
cb(this.resolvedValue);
}
else {
this.thens.unshift(cb);
}
}
catch(cb) {
if (this.status === "rejected") {
cb(this.rejectedError);
}
else {
this.catches.unshift(cb);
}
}
promise() {
return {
then: this.then,
catch: this.catch,
};
}
};
/**Throw an `Error` if `cond` is falsey. */
function assert(cond, msg) {
if (!cond) {
msg = msg || "Assertion failed.";
throw new Error(msg);
}
}
function _getUserInfo() {
const nav = navigator;
// const ua = nav.userAgentData;
const res = {
isMac: /Mac/.test(nav.platform),
};
return res;
}
/** Run `callback` when document was loaded. */
function documentReady(callback) {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", callback);
}
else {
callback();
}
}
/** Resolve when document was loaded. */
function documentReadyPromise() {
return new Promise((resolve) => {
documentReady(resolve);
});
}
/**
* Iterate over Object properties or array elements.
*
* @param obj `Object`, `Array` or null
* @param callback called for every item.
* `this` also contains the item.
* Return `false` to stop the iteration.
*/
function each(obj, callback) {
if (obj == null) {
// accept `null` or `undefined`
return obj;
}
const length = obj.length;
let i = 0;
if (typeof length === "number") {
for (; i < length; i++) {
if (callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
}
else {
for (const k in obj) {
if (callback.call(obj[i], k, obj[k]) === false) {
break;
}
}
}
return obj;
}
/** Shortcut for `throw new Error(msg)`. */
function error(msg) {
throw new Error(msg);
}
/** Convert `<`, `>`, `&`, `"`, `'`, and `/` to the equivalent entities. */
function escapeHtml(s) {
return ("" + s).replace(REX_HTML, function (s) {
return ENTITY_MAP[s];
});
}
// export function escapeRegExp(s: string) {
// return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
// }
/**Convert a regular expression string by escaping special characters (e.g. `"$"` -> `"\$"`) */
function escapeRegex(s) {
return ("" + s).replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
}
/** Convert `<`, `>`, `"`, `'`, and `/` (but not `&`) to the equivalent entities. */
function escapeTooltip(s) {
return ("" + s).replace(REX_TOOLTIP, function (s) {
return ENTITY_MAP[s];
});
}
/** TODO */
function extractHtmlText(s) {
if (s.indexOf(">") >= 0) {
error("Not implemented");
// return $("").html(s).text();
}
return s;
}
/**
* Read the value from an HTML input element.
*
* If a `` is passed, the first child input is used.
* Depending on the target element type, `value` is interpreted accordingly.
* For example for a checkbox, a value of true, false, or null is returned if
* the element is checked, unchecked, or indeterminate.
* For datetime input control a numerical value is assumed, etc.
*
* Common use case: store the new user input in a `change` event handler:
*
* ```ts
* change: (e) => {
* const tree = e.tree;
* const node = e.node;
* // Read the value from the input control that triggered the change event:
* let value = tree.getValueFromElem(e.element);
* // and store it to the node model (assuming the column id matches the property name)
* node.data[e.info.colId] = value;
* },
* ```
* @param elem `` or `