wunderbaum
Version:
JavaScript tree/grid/treegrid control.
1,446 lines (1,441 loc) • 322 kB
JavaScript
/*!
* Wunderbaum - debounce.ts
* Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
* v0.13.0, Sat, 08 Mar 2025 14:16:31 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.13.0, Sat, 08 Mar 2025 14:16:31 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 $("<div/>").html(s).text();
}
return s;
}
/**
* Read the value from an HTML input element.
*
* If a `<span class="wb-col">` 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 `<input>` or `<select>` element. Also a parent `span.wb-col` is accepted.
* @param coerce pass true to convert date/time inputs to `Date`.
* @returns the value
*/
function getValueFromElem(elem, coerce = false) {
const tag = elem.tagName;
let value = null;
if (tag === "SPAN" && elem.classList.contains("wb-col")) {
const span = elem;
const embeddedInput = span.querySelector("input,select");
if (embeddedInput) {
return getValueFromElem(embeddedInput, coerce);
}
span.innerText = "" + value;
}
else if (tag === "INPUT") {
const input = elem;
const type = input.type;
switch (type) {
case "button":
case "reset":
case "submit":
case "image":
break;
case "checkbox":
value = input.indeterminate ? null : input.checked;
break;
case "date":
case "datetime":
case "datetime-local":
case "month":
case "time":
case "week":
value = coerce ? input.valueAsDate : input.value;
break;
case "number":
case "range":
value = input.valueAsNumber;
break;
case "radio":
{
const name = input.name;
const checked = input.parentElement.querySelector(`input[name="${name}"]:checked`);
value = checked ? checked.value : undefined;
}
break;
case "text":
default:
value = input.value;
}
}
else if (tag === "SELECT") {
const select = elem;
value = select.value;
}
return value;
}
/**
* Set the value of an HTML input element.
*
* If a `<span class="wb-col">` is passed, the first child input is used.
* Depending on the target element type, `value` is interpreted accordingly.
* For example a checkbox is set to checked, unchecked, or indeterminate if the
* value is truethy, falsy, or `null`.
* For datetime input control a numerical value is assumed, etc.
*
* Common use case: update embedded input controls in a `render` event handler:
*
* ```ts
* render: (e) => {
* // e.node.log(e.type, e, e.node.data);
*
* for (const col of Object.values(e.renderColInfosById)) {
* switch (col.id) {
* default:
* // Assumption: we named column.id === node.data.NAME
* util.setValueToElem(col.elem, e.node.data[col.id]);
* break;
* }
* }
* },
* ```
*
* @param elem `<input>` or `<select>` element Also a parent `span.wb-col` is accepted.
* @param value a value that matches the target element.
*/
function setValueToElem(elem, value) {
const tag = elem.tagName;
if (tag === "SPAN" && elem.classList.contains("wb-col")) {
const span = elem;
const embeddedInput = span.querySelector("input,select");
if (embeddedInput) {
return setValueToElem(embeddedInput, value);
}
// No embedded input: simply write as escaped html
span.innerText = "" + value;
}
else if (tag === "INPUT") {
const input = elem;
const type = input.type;
switch (type) {
case "checkbox":
// An explicit `null` value is interpreted as 'indeterminate'.
// `undefined` is interpreted as 'unchecked'
input.indeterminate = value === null;
input.checked = !!value;
break;
case "date":
case "month":
case "time":
case "week":
case "datetime":
case "datetime-local":
input.valueAsDate = new Date(value);
break;
case "number":
case "range":
if (value == null) {
input.value = value;
}
else {
input.valueAsNumber = value;
}
break;
case "radio":
error(`Not yet implemented: ${type}`);
// const name = input.name;
// const checked = input.parentElement!.querySelector(
// `input[name="${name}"]:checked`
// );
// value = checked ? (<HTMLInputElement>checked).value : undefined;
break;
case "button":
case "reset":
case "submit":
case "image":
break;
case "text":
default:
input.value = value !== null && value !== void 0 ? value : "";
}
}
else if (tag === "SELECT") {
const select = elem;
if (value == null) {
select.selectedIndex = -1;
}
else {
select.value = value;
}
}
}
/** Show/hide element by setting the `display` style to 'none'. */
function setElemDisplay(elem, flag) {
const style = elemFromSelector(elem).style;
if (flag) {
if (style.display === "none") {
style.display = "";
}
}
else if (style.display === "") {
style.display = "none";
}
}
/** Create and return an unconnected `HTMLElement` from a HTML string. */
function elemFromHtml(html) {
const t = document.createElement("template");
t.innerHTML = html.trim();
return t.content.firstElementChild;
}
const _IGNORE_KEYS = new Set(["Alt", "Control", "Meta", "Shift"]);
/** Return a HtmlElement from selector or cast an existing element. */
function elemFromSelector(obj) {
if (!obj) {
return null; //(null as unknown) as HTMLElement;
}
if (typeof obj === "string") {
return document.querySelector(obj);
}
return obj;
}
/**
* Return a canonical descriptive string for a keyboard or mouse event.
*
* The result also contains a prefix for modifiers if any, for example
* `"x"`, `"F2"`, `"Control+Home"`, or `"Shift+clickright"`.
* This is especially useful in `switch` statements, to make sure that modifier
* keys are considered and handled correctly:
* ```ts
* const eventName = util.eventToString(e);
* switch (eventName) {
* case "+":
* case "Add":
* ...
* break;
* case "Enter":
* case "End":
* case "Control+End":
* case "Meta+ArrowDown":
* case "PageDown":
* ...
* break;
* }
* ```
*/
function eventToString(event) {
const key = event.key;
const et = event.type;
const s = [];
if (event.altKey) {
s.push("Alt");
}
if (event.ctrlKey) {
s.push("Control");
}
if (event.metaKey) {
s.push("Meta");
}
if (event.shiftKey) {
s.push("Shift");
}
if (et === "click" || et === "dblclick") {
s.push(MOUSE_BUTTONS[event.button] + et);
}
else if (et === "wheel") {
s.push(et);
// } else if (!IGNORE_KEYCODES[key]) {
// s.push(
// SPECIAL_KEYCODES[key] ||
// String.fromCharCode(key).toLowerCase()
// );
}
else if (!_IGNORE_KEYS.has(key)) {
s.push(key);
}
return s.join("+");
}
/**
* Copy allproperties from one or more source objects to a target object.
*
* @returns the modified target object.
*/
// TODO: use Object.assign()? --> https://stackoverflow.com/a/42740894
// TODO: support deep merge --> https://stackoverflow.com/a/42740894
function extend(...args) {
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg == null) {
continue;
}
for (const key in arg) {
if (Object.prototype.hasOwnProperty.call(arg, key)) {
args[0][key] = arg[key];
}
}
}
return args[0];
}
/** Return true if `obj` is of type `array`. */
function isArray(obj) {
return Array.isArray(obj);
}
/** Return true if `obj` is of type `Object` and has no properties. */
function isEmptyObject(obj) {
return Object.keys(obj).length === 0 && obj.constructor === Object;
}
/** Return true if `obj` is of type `function`. */
function isFunction(obj) {
return typeof obj === "function";
}
/** Return true if `obj` is of type `Object`. */
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === "[object Object]";
}
/** A dummy function that does nothing ('no operation'). */
function noop(...args) { }
function onEvent(rootTarget, eventNames, selectorOrHandler, handlerOrNone) {
let selector, handler;
rootTarget = elemFromSelector(rootTarget);
// rootTarget = eventTargetFromSelector<EventTarget>(rootTarget)!;
if (handlerOrNone) {
selector = selectorOrHandler;
handler = handlerOrNone;
}
else {
selector = "";
handler = selectorOrHandler;
}
eventNames.split(" ").forEach((evn) => {
rootTarget.addEventListener(evn, function (e) {
if (!selector) {
return handler(e); // no event delegation
}
else if (e.target) {
let elem = e.target;
if (elem.matches(selector)) {
return handler(e);
}
elem = elem.closest(selector);
if (elem) {
return handler(e);
}
}
});
});
}
/** Return a wrapped handler method, that provides `this._super` and `this._superApply`.
*
* ```ts
// Implement `opts.createNode` event to add the 'draggable' attribute
overrideMethod(ctx.options, "createNode", (event, data) => {
// Default processing if any
this._super.apply(this, event, data);
// Add 'draggable' attribute
data.node.span.draggable = true;
});
```
*/
function overrideMethod(instance, methodName, handler, ctx) {
let prevSuper, prevSuperApply;
const self = ctx || instance;
const prevFunc = instance[methodName];
const _super = (...args) => {
return prevFunc.apply(self, args);
};
const _superApply = (argsArray) => {
return prevFunc.apply(self, argsArray);
};
const wrapper = (...args) => {
try {
prevSuper = self._super;
prevSuperApply = self._superApply;
self._super = _super;
self._superApply = _superApply;
return handler.apply(self, args);
}
finally {
self._super = prevSuper;
self._superApply = prevSuperApply;
}
};
instance[methodName] = wrapper;
}
/** Run function after ms milliseconds and return a promise that resolves when done. */
function setTimeoutPromise(callback, ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
resolve(callback.apply(this));
}
catch (err) {
reject(err);
}
}, ms);
});
}
/**
* Wait `ms` microseconds.
*
* Example:
* ```js
* await sleep(1000);
* ```
* @param ms duration
* @returns
*/
async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Set or rotate checkbox status with support for tri-state.
*
* An initial 'indeterminate' state becomes 'checked' on the first call.
*
* If the input element has the class 'wb-tristate' assigned, the sequence is:<br>
* 'indeterminate' -> 'checked' -> 'unchecked' -> 'indeterminate' -> ...<br>
* Otherwise we toggle like <br>
* 'checked' -> 'unchecked' -> 'checked' -> ...
*/
function toggleCheckbox(element, value, tristate) {
const input = elemFromSelector(element);
assert(input.type === "checkbox", `Expected a checkbox: ${input.type}`);
tristate !== null && tristate !== void 0 ? tristate : (tristate = input.classList.contains("wb-tristate") || input.indeterminate);
if (value === undefined) {
const curValue = input.indeterminate ? null : input.checked;
switch (curValue) {
case true:
value = false;
break;
case false:
value = tristate ? null : true;
break;
case null:
value = true;
break;
}
}
input.indeterminate = value == null;
input.checked = !!value;
}
/**
* Return `opts.NAME` if opts is valid and
*
* @param opts dict, object, or null
* @param name option name (use dot notation to access extension option, e.g. `filter.mode`)
* @param defaultValue returned when `opts` is not an object, or does not have a NAME property
*/
function getOption(opts, name, defaultValue = undefined) {
let ext;
// Lookup `name` in options dict
if (opts && name.indexOf(".") >= 0) {
[ext, name] = name.split(".");
opts = opts[ext];
}
const value = opts ? opts[name] : null;
// Use value from value options dict, fallback do default
return value !== null && value !== void 0 ? value : defaultValue;
}
/** Return the next value from a list of values (rotating). @since 0.11 */
function rotate(value, values) {
const idx = values.indexOf(value);
return values[(idx + 1) % values.length];
}
/** Convert an Array or space-separated string to a Set. */
function toSet(val) {
if (val instanceof Set) {
return val;
}
if (typeof val === "string") {
const set = new Set();
for (const c of val.split(" ")) {
set.add(c.trim());
}
return set;
}
if (Array.isArray(val)) {
return new Set(val);
}
throw new Error("Cannot convert to Set<string>: " + val);
}
/** Convert a pixel string to number.
* We accept a number or a string like '123px'. If undefined, the first default
* value that is a number or a string ending with 'px' is returned.
*
* Example:
* ```js
* let x = undefined;
* let y = "123px";
* const width = util.toPixel(x, y, 100); // returns 123
* ```
*/
function toPixel(...defaults) {
for (const d of defaults) {
if (typeof d === "number") {
return d;
}
if (typeof d === "string" && d.endsWith("px")) {
return parseInt(d, 10);
}
assert(d == null, `Expected a number or string like '123px': ${d}`);
}
throw new Error(`Expected a string like '123px': ${defaults}`);
}
/** Return the the boolean value of the first non-null element.
* Example:
* ```js
* const opts = { flag: true };
* const value = util.toBool(opts.foo, opts.flag, false); // returns true
* ```
*/
function toBool(...boolDefaults) {
for (const d of boolDefaults) {
if (d != null) {
return !!d;
}
}
throw new Error("No default boolean value provided");
}
/**
* Return `val` unless `val` is a number in which case we convert to boolean.
* This is useful when a boolean value is stored as a 0/1 (e.g. in JSON) and
* we still want to maintain string values. null and undefined are returned as
* is. E.g. `checkbox` may be boolean or 'radio'.
*/
function intToBool(val) {
return typeof val === "number" ? !!val : val;
}
// /** Check if a string is contained in an Array or Set. */
// export function isAnyOf(s: string, items: Array<string>|Set<string>): boolean {
// return Array.prototype.includes.call(items, s)
// }
// /** Check if an Array or Set has at least one matching entry. */
// export function hasAnyOf(container: Array<string>|Set<string>, items: Array<string>): boolean {
// if (Array.isArray(container)) {
// return container.some(v => )
// }
// return container.some(v => {})
// // const container = toSet(items);
// // const itemSet = toSet(items);
// // Array.prototype.includes
// // throw new Error("Cannot convert to Set<string>: " + val);
// }
/** Return a canonical string representation for an object's type (e.g. 'array', 'number', ...). */
function type(obj) {
return Object.prototype.toString
.call(obj)
.replace(/^\[object (.+)\]$/, "$1")
.toLowerCase();
}
/**
* Return a function that can be called instead of `callback`, but guarantees
* a limited execution rate.
* The execution rate is calculated based on the runtime duration of the
* previous call.
* Example:
* ```js
* throttledFoo = util.adaptiveThrottle(foo.bind(this), {});
* throttledFoo();
* throttledFoo();
* ```
*/
function adaptiveThrottle(callback, options) {
const opts = Object.assign({
minDelay: 16,
defaultDelay: 200,
maxDelay: 5000,
delayFactor: 2.0,
}, options);
const minDelay = Math.max(16, +opts.minDelay);
const maxDelay = +opts.maxDelay;
let waiting = 0; // Initially, we're not waiting
let pendingArgs = null;
let pendingTimer = null;
const throttledFn = (...args) => {
if (waiting) {
pendingArgs = args;
// console.log(`adaptiveThrottle() queing request #${waiting}...`, args);
waiting += 1;
}
else {
// Prevent invocations while running or blocking
waiting = 1;
const useArgs = args; // pendingArgs || args;
pendingArgs = null;
// console.log(`adaptiveThrottle() execute...`, useArgs);
const start = Date.now();
try {
callback.apply(this, useArgs);
}
catch (error) {
console.error(error); // eslint-disable-line no-console
}
const elap = Date.now() - start;
const curDelay = Math.min(Math.max(minDelay, elap * opts.delayFactor), maxDelay);
const useDelay = Math.max(minDelay, curDelay - elap);
// console.log(
// `adaptiveThrottle() calling worker took ${elap}ms. delay = ${curDelay}ms, using ${useDelay}ms`,
// pendingArgs
// );
pendingTimer = setTimeout(() => {
// Unblock, and trigger pending requests if any
// const skipped = waiting - 1;
pendingTimer = null;
waiting = 0; // And allow future invocations
if (pendingArgs != null) {
// There was another request while running or waiting
// console.log(
// `adaptiveThrottle() re-trigger (missed ${skipped})...`,
// pendingArgs
// );
throttledFn.apply(this, pendingArgs);
}
}, useDelay);
}
};
throttledFn.cancel = () => {
if (pendingTimer) {
clearTimeout(pendingTimer);
pendingTimer = null;
}
pendingArgs = null;
waiting = 0;
};
throttledFn.pending = () => {
return !!pendingTimer;
};
throttledFn.flush = () => {
throw new Error("Not implemented");
};
return throttledFn;
}
var util = /*#__PURE__*/Object.freeze({
__proto__: null,
Deferred: Deferred$1,
MAX_INT: MAX_INT,
MOUSE_BUTTONS: MOUSE_BUTTONS,
ValidationError: ValidationError,
adaptiveThrottle: adaptiveThrottle,
assert: assert,
debounce: debounce,
documentReady: documentReady,
documentReadyPromise: documentReadyPromise,
each: each,
elemFromHtml: elemFromHtml,
elemFromSelector: elemFromSelector,
error: error,
escapeHtml: escapeHtml,
escapeRegex: escapeRegex,
escapeTooltip: escapeTooltip,
eventToString: eventToString,
extend: extend,
extractHtmlText: extractHtmlText,
getOption: getOption,
getValueFromElem: getValueFromElem,
intToBool: intToBool,
isArray: isArray,
isEmptyObject: isEmptyObject,
isFunction: isFunction,
isMac: isMac,
isPlainObject: isPlainObject,
noop: noop,
onEvent: onEvent,
overrideMethod: overrideMethod,
rotate: rotate,
setElemDisplay: setElemDisplay,
setTimeoutPromise: setTimeoutPromise,
setValueToElem: setValueToElem,
sleep: sleep,
throttle: throttle,
toBool: toBool,
toPixel: toPixel,
toSet: toSet,
toggleCheckbox: toggleCheckbox,
type: type
});
/*!
* Wunderbaum - types
* Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
* v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
*/
/**
* Possible values for {@link WunderbaumNode.update} and {@link Wunderbaum.update}.
*/
var ChangeType;
(function (ChangeType) {
/** Re-render the whole viewport, headers, and all rows. */
ChangeType["any"] = "any";
/** A node's title, icon, columns, or status have changed. Update the existing row markup. */
ChangeType["data"] = "data";
/** The `tree.columns` definition has changed beyond simple width adjustments. */
ChangeType["colStructure"] = "colStructure";
/** The viewport/window was resized. Adjust layout attributes for all elements. */
ChangeType["resize"] = "resize";
/** A node's definition has changed beyond status and data. Re-render the whole row's markup. */
ChangeType["row"] = "row";
/** Nodes have been added, removed, etc. Update markup. */
ChangeType["structure"] = "structure";
/** A node's status has changed. Update current row's classes, to reflect active, selected, ... */
ChangeType["status"] = "status";
/** Vertical scroll event. Update the 'top' property of all rows. */
ChangeType["scroll"] = "scroll";
})(ChangeType || (ChangeType = {}));
/** @internal */
var RenderFlag;
(function (RenderFlag) {
RenderFlag["clearMarkup"] = "clearMarkup";
RenderFlag["header"] = "header";
RenderFlag["redraw"] = "redraw";
RenderFlag["scroll"] = "scroll";
})(RenderFlag || (RenderFlag = {}));
/** Possible values for {@link WunderbaumNode.setStatus}. */
var NodeStatusType;
(function (NodeStatusType) {
NodeStatusType["ok"] = "ok";
NodeStatusType["loading"] = "loading";
NodeStatusType["error"] = "error";
NodeStatusType["noData"] = "noData";
NodeStatusType["paging"] = "paging";
})(NodeStatusType || (NodeStatusType = {}));
/** Define the subregion of a node, where an event occurred. */
var NodeRegion;
(function (NodeRegion) {
NodeRegion["unknown"] = "";
NodeRegion["checkbox"] = "checkbox";
NodeRegion["column"] = "column";
NodeRegion["expander"] = "expander";
NodeRegion["icon"] = "icon";
NodeRegion["prefix"] = "prefix";
NodeRegion["title"] = "title";
})(NodeRegion || (NodeRegion = {}));
/** Initial navigation mode and possible transition. */
var NavModeEnum;
(function (NavModeEnum) {
/** Start with row mode, but allow cell-nav mode */
NavModeEnum["startRow"] = "startRow";
/** Cell-nav mode only */
NavModeEnum["cell"] = "cell";
/** Start in cell-nav mode, but allow row mode */
NavModeEnum["startCell"] = "startCell";
/** Row mode only */
NavModeEnum["row"] = "row";
})(NavModeEnum || (NavModeEnum = {}));
/*!
* Wunderbaum - wb_extension_base
* Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
* v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
*/
class WunderbaumExtension {
constructor(tree, id, defaults) {
this.enabled = true;
this.tree = tree;
this.id = id;
this.treeOpts = tree.options;
const opts = tree.options;
if (this.treeOpts[id] === undefined) {
opts[id] = this.extensionOpts = extend({}, defaults);
}
else {
// TODO: do we break existing object instance references here?
this.extensionOpts = extend({}, defaults, opts[id]);
opts[id] = this.extensionOpts;
}
this.enabled = this.getPluginOption("enabled", true);
}
/** Called on tree (re)init after all extensions are added, but before loading.*/
init() {
this.tree.element.classList.add("wb-ext-" + this.id);
}
// protected callEvent(type: string, extra?: any): any {
// let func = this.extensionOpts[type];
// if (func) {
// return func.call(
// this.tree,
// util.extend(
// {
// event: this.id + "." + type,
// },
// extra
// )
// );
// }
// }
getPluginOption(name, defaultValue) {
var _a;
return (_a = this.extensionOpts[name]) !== null && _a !== void 0 ? _a : defaultValue;
}
setPluginOption(name, value) {
this.extensionOpts[name] = value;
}
setEnabled(flag = true) {
return this.setPluginOption("enabled", !!flag);
// this.enabled = !!flag;
}
onKeyEvent(data) {
return;
}
onRender(data) {
return;
}
}
/*!
* Wunderbaum - ext-filter
* Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license.
* v0.13.0, Sat, 08 Mar 2025 14:16:31 GMT (https://github.com/mar10/wunderbaum)
*/
const START_MARKER = "\uFFF7";
const END_MARKER = "\uFFF8";
const RE_START_MARKER = new RegExp(escapeRegex(START_MARKER), "g");
const RE_END_MARTKER = new RegExp(escapeRegex(END_MARKER), "g");
class FilterExtension extends WunderbaumExtension {
constructor(tree) {
super(tree, "filter", {
autoApply: true, // Re-apply last filter if lazy data is loaded
autoExpand: false, // Expand all branches that contain matches while filtered
matchBranch: false, // Whether to implicitly match all children of matched nodes
connect: null, // Element or selector of an input control for filter query strings
fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
highlight: true, // Highlight matches by wrapping inside <mark> tags
leavesOnly: false, // Match end nodes only
mode: "dim", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
noData: true, // Display a 'no data' status node if result is empty
});
this.queryInput = null;
this.prevButton = null;
this.nextButton = null;
this.modeButton = null;
this.matchInfoElem = null;
this.lastFilterArgs = null;
}
init() {
super.init();
const connect = this.getPluginOption("connect");
if (connect) {
this._connectControls();
}
}
setPluginOption(name, value) {
super.setPluginOption(name, value);
switch (name) {
case "mode":
this.tree.filterMode =
value === "hide" ? "hide" : value === "mark" ? "mark" : "dim";
this.tree.updateFilter();
break;
}
}
_updatedConnectedControls() {
var _a;
const filterActive = this.tree.filterMode !== null;
const activeNode = this.tree.getActiveNode();
const matchCount = filterActive ? this.countMatches() : 0;
const strings = this.treeOpts.strings;
let matchIdx = "?";
if (this.matchInfoElem) {
if (filterActive) {
let info;
if (matchCount === 0) {
info = strings.noMatch;
}
else if (activeNode && activeNode.match >= 1) {
matchIdx = (_a = activeNode.match) !== null && _a !== void 0 ? _a : "?";
info = strings.matchIndex;
}
else {
info = strings.queryResult;
}
info = info
.replace("${count}", this.tree.count().toLocaleString())
.replace("${match}", "" + matchIdx)
.replace("${matches}", matchCount.toLocaleString());
this.matchInfoElem.textContent = info;
}
else {
this.matchInfoElem.textContent = "";
}
}
if (this.nextButton instanceof HTMLButtonElement) {
this.nextButton.disabled = !matchCount;
}
if (this.prevButton instanceof HTMLButtonElement) {
this.prevButton.disabled = !matchCount;
}
if (this.modeButton) {
this.modeButton.disabled = !filterActive;
this.modeButton.classList.toggle("wb-filter-hide", this.tree.filterMode === "hide");
}
}
_connectControls() {
const tree = this.tree;
const connect = this.getPluginOption("connect");
if (!connect) {
return;
}
this.queryInput = elemFromSelector(connect.inputElem);
if (!this.queryInput) {
throw new Error(`Invalid 'filter.connect' option: ${connect.inputElem}.`);
}
this.prevButton = elemFromSelector(connect.prevButton);
this.nextButton = elemFromSelector(connect.nextButton);
this.modeButton = elemFromSelector(connect.modeButton);
this.matchInfoElem = elemFromSelector(connect.matchInfoElem);
if (this.prevButton) {
onEvent(this.prevButton, "click", () => {
tree.findRelatedNode(tree.getActiveNode() || tree.getFirstChild(), "prevMatch");
this._updatedConnectedControls();
});
}
if (this.nextButton) {
onEvent(this.nextButton, "click", () => {
tree.findRelatedNode(tree.getActiveNode() || tree.getFirstChild(), "nextMatch");
this._updatedConnectedControls();
});
}
if (this.modeButton) {
onEvent(this.modeButton, "click", (e) => {
if (!this.tree.filterMode) {
return;
}
this.setPluginOption("mode", tree.filterMode === "dim" ? "hide" : "dim");
});
}
onEvent(this.queryInput, "input", debounce((e) => {
this.filterNodes(this.queryInput.value.trim(), {});
}, 700));
this._updatedConnectedControls();
}
_applyFilterNoUpdate(filter, _opts) {
return this.tree.runWithDeferredUpdate(() => {
return this._applyFilterImpl(filter, _opts);
});
}
_applyFilterImpl(filter, _opts) {
var _a;
let //temp,
count = 0;
const start = Date.now();
const tree = this.tree;
const treeOpts = tree.options;
const prevAutoCollapse = treeOpts.autoCollapse;
// Use default options from `tree.options.filter`, but allow to override them
const opts = extend({}, treeOpts.filter, _opts);
const hideMode = opts.mode === "hide";
const matchBranch = !!opts.matchBranch;
const leavesOnly = !!opts.leavesOnly && !matchBranch;
let filterRegExp;
let highlightRegExp;
// Default to 'match title substring (case insensitive)'
if (typeof filter === "string" || filter instanceof RegExp) {
if (filter === "") {
tree.logInfo("Passing an empty string as a filter is handled as clearFilter().");
this.clearFilter();
return 0;
}
if (opts.fuzzy) {
assert(typeof filter === "string", "fuzzy filter must be a string");
// See https://codereview.stackexchange.com/questions/23899/faster-javascript-fuzzy-string-matching-function/23905#23905
// and http://www.quora.com/How-is-the-fuzzy-search-algorithm-in-Sublime-Text-designed
// and http://www.dustindiaz.com/autocomplete-fuzzy-matching
const matchReString = filter
.split("")
// Escaping the `filter` will not work because,
// it gets further split into individual characters. So,
// escape each character after splitting
.map(escapeRegex)
.reduce(function (a, b) {
// create capture groups for parts that comes before
// the character
return a + "([^" + b + "]*)" + b;
}, "");
filterRegExp = new RegExp(matchReString, "i");
// highlightRegExp = new RegExp(escapeRegex(filter), "gi");
}
else if (filter instanceof RegExp) {
filterRegExp = filter;
highlightRegExp = filter;
}
else {
const matchReString = escapeRegex(filter); // make sure a '.' is treated literally
filterRegExp = new RegExp(matchReString, "i");
highlightRegExp = new RegExp(matchReString, "gi");
}
tree.logDebug(`Filtering nodes by '${filterRegExp}'`);
// const re = new RegExp(match, "i");
// const reHighlight = new RegExp(escapeRegex(filter),