(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.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 = {
        "&": "&amp;",
        "<": "&lt;",
        ">": "&gt;",
        '"': "&quot;",
        "'": "&#39;",
        "/": "&#x2F;",
    };
    /** 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), "gi");
                filter = (node) => {
                    if (!node.title) {
                        return false;
                    }
                    // let text = escapeTitles ? node.title : extractHtmlText(node.title);
                    const text = node.title;
                    // `.match` instead of `.test` to get the capture groups
                    // const res = text.match(filterRegExp);
                    const res = filterRegExp.exec(text);
                    if (res && opts.highlight) {
                        let highlightString;
                        if (opts.fuzzy) {
                            highlightString = _markFuzzyMatchedChars(text, res, true);
                        }
                        else {
                            // #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
                            // Use some exotic characters to mark matches:
                            highlightString = text.replace(highlightRegExp, function (s) {
                                return START_MARKER + s + END_MARKER;
                            });
                        }
                        // now we can escape the title...
                        node.titleWithHighlight = escapeHtml(highlightString)
                            // ... and finally insert the desired `<mark>` tags
                            .replace(RE_START_MARKER, "<mark>")
                            .replace(RE_END_MARTKER, "</mark>");
                    }
                    return !!res;
                };
            }
            tree.filterMode = (_a = opts.mode) !== null && _a !== void 0 ? _a : "dim";
            // eslint-disable-next-line prefer-rest-params
            this.lastFilterArgs = arguments;
            tree.element.classList.toggle("wb-ext-filter-hide", !!hideMode);
            tree.element.classList.toggle("wb-ext-filter-dim", opts.mode === "dim");
            tree.element.classList.toggle("wb-ext-filter-hide-expanders", !!opts.hideExpanders);
            // Reset current filter
            tree.root.subMatchCount = 0;
            tree.visit((node) => {
                delete node.match;
                delete node.titleWithHighlight;
                node.subMatchCount = 0;
            });
            tree.setStatus(NodeStatusType.ok);
            // Adjust node.hide, .match, and .subMatchCount properties
            treeOpts.autoCollapse = false; // #528
            tree.visit((node) => {
                if (leavesOnly && node.children != null) {
                    return;
                }
                let res = filter(node);
                if (res === "skip") {
                    node.visit(function (c) {
                        c.match = undefined;
                    }, true);
                    return "skip";
                }
                let matchedByBranch = false;
                if ((matchBranch || res === "branch") && node.parent.match) {
                    res = true;
                    matchedByBranch = true;
                }
                if (res) {
                    count++;
                    node.match = count;
                    node.visitParents((p) => {
                        if (p !== node) {
                            p.subMatchCount += 1;
                        }
                        // Expand match (unless this is no real match, but only a node in a matched branch)
                        if (opts.autoExpand && !matchedByBranch && !p.expanded) {
                            p.setExpanded(true, {
                                noAnimation: true,
                                noEvents: true,
                            });
                            p._filterAutoExpanded = true;
                        }
                    }, true);
                }
            });
            treeOpts.autoCollapse = prevAutoCollapse;
            if (count === 0 && opts.noData && hideMode) {
                if (typeof opts.noData === "string") {
                    tree.root.setStatus(NodeStatusType.noData, { message: opts.noData });
                }
                else {
                    tree.root.setStatus(NodeStatusType.noData);
                }
            }
            // Redraw whole tree
            tree.logDebug(`Filter '${filter}' found ${count} nodes in ${Date.now() - start} ms.`);
            this._updatedConnectedControls();
            return count;
        }
        /**
         * [ext-filter] Dim or hide nodes.
         */
        filterNodes(filter, options) {
            return this._applyFilterNoUpdate(filter, options);
        }
        /**
         * [ext-filter] Dim or hide whole branches.
         * @deprecated Use {@link filterNodes} instead and set `options.matchBranch: true`.
         */
        filterBranches(filter, options) {
            assert(options.matchBranch === undefined, "filterBranches() is deprecated.");
            options.matchBranch = true;
            return this._applyFilterNoUpdate(filter, options);
        }
        /**
         * [ext-filter] Return the number of matched nodes.
         */
        countMatches() {
            let n = 0;
            this.tree.visit((node) => {
                if (node.match && !node.statusNodeType) {
                    n++;
                }
            });
            return n;
        }
        /**
         * [ext-filter] Re-apply current filter.
         */
        updateFilter() {
            var _a;
            const tree = this.tree;
            if (tree.filterMode &&
                this.lastFilterArgs &&
                ((_a = tree.options.filter) === null || _a === void 0 ? void 0 : _a.autoApply)) {
                // eslint-disable-next-line prefer-spread
                this._applyFilterNoUpdate.apply(this, this.lastFilterArgs);
            }
            else {
                tree.logWarn("updateFilter(): no filter active.");
            }
            this._updatedConnectedControls();
        }
        /**
         * [ext-filter] Reset the filter.
         */
        clearFilter() {
            const tree = this.tree;
            tree.enableUpdate(false);
            tree.setStatus(NodeStatusType.ok);
            // we also counted root node's subMatchCount
            delete tree.root.match;
            delete tree.root.subMatchCount;
            tree.visit((node) => {
                delete node.match;
                delete node.subMatchCount;
                delete node.titleWithHighlight;
                if (node._filterAutoExpanded && node.expanded) {
                    node.setExpanded(false, {
                        noAnimation: true,
                        noEvents: true,
                    });
                }
                delete node._filterAutoExpanded;
            });
            tree.filterMode = null;
            this.lastFilterArgs = null;
            tree.element.classList.remove(
            // "wb-ext-filter",
            "wb-ext-filter-dim", "wb-ext-filter-hide");
            this._updatedConnectedControls();
            tree.enableUpdate(true);
        }
    }
    /**
     * @description Marks the matching charecters of `text` either by `mark` or
     * by exotic*Chars (if `escapeTitles` is `true`) based on `matches`
     * which is an array of matching groups.
     * @param {string} text
     * @param {RegExpMatchArray} matches
     */
    function _markFuzzyMatchedChars(text, matches, escapeTitles = true) {
        const matchingIndices = [];
        // get the indices of matched characters (Iterate through `RegExpMatchArray`)
        for (let _matchingArrIdx = 1; _matchingArrIdx < matches.length; _matchingArrIdx++) {
            const _mIdx = 
            // get matching char index by cumulatively adding
            // the matched group length
            matches[_matchingArrIdx].length +
                (_matchingArrIdx === 1 ? 0 : 1) +
                (matchingIndices[matchingIndices.length - 1] || 0);
            matchingIndices.push(_mIdx);
        }
        // Map each `text` char to its position and store in `textPoses`.
        const textPoses = text.split("");
        if (escapeTitles) {
            // If escaping the title, then wrap the matching char within exotic chars
            matchingIndices.forEach(function (v) {
                textPoses[v] = START_MARKER + textPoses[v] + END_MARKER;
            });
        }
        else {
            // Otherwise, Wrap the matching chars within `mark`.
            matchingIndices.forEach(function (v) {
                textPoses[v] = "<mark>" + textPoses[v] + "</mark>";
            });
        }
        // Join back the modified `textPoses` to create final highlight markup.
        return textPoses.join("");
    }

    /*!
     * Wunderbaum - ext-keynav
     * 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 QUICKSEARCH_DELAY = 500;
    class KeynavExtension extends WunderbaumExtension {
        constructor(tree) {
            super(tree, "keynav", {});
        }
        _getEmbeddedInputElem(elem) {
            var _a;
            let input = null;
            if (elem && elem.type != null) {
                input = elem;
            }
            else {
                // ,[contenteditable]
                const ace = (_a = this.tree.getActiveColElem()) === null || _a === void 0 ? void 0 : _a.querySelector("input,select");
                if (ace) {
                    input = ace;
                }
            }
            return input;
        }
        // /* Return the current cell's embedded input that has keyboard focus. */
        // protected _getFocusedInputElem(): HTMLInputElement | null {
        //   const ace = this.tree
        //     .getActiveColElem()
        //     ?.querySelector<HTMLInputElement>("input:focus,select:focus");
        //   return ace || null;
        // }
        /* Return true if the current cell's embedded input has keyboard focus. */
        _isCurInputFocused() {
            var _a;
            const ace = (_a = this.tree
                .getActiveColElem()) === null || _a === void 0 ? void 0 : _a.querySelector("input:focus,select:focus");
            return !!ace;
        }
        onKeyEvent(data) {
            const event = data.event;
            const tree = this.tree;
            const opts = data.options;
            const activate = !event.ctrlKey || opts.autoActivate;
            const curInput = this._getEmbeddedInputElem(event.target);
            const inputHasFocus = curInput && this._isCurInputFocused();
            const navModeOption = opts.navigationModeOption;
            let focusNode, eventName = eventToString(event), node = data.node, handled = true;
            // tree.log(`onKeyEvent: ${eventName}, curInput`, curInput);
            if (!tree.isEnabled()) {
                // tree.logDebug(`onKeyEvent ignored for disabled tree: ${eventName}`);
                return false;
            }
            // Let callback prevent default processing
            if (tree._callEvent("keydown", data) === false) {
                return false;
            }
            // Let ext-edit trigger editing
            if (tree._callMethod("edit._preprocessKeyEvent", data) === false) {
                return false;
            }
            // Set focus to active (or first node) if no other node has the focus yet
            if (!node) {
                const currentNode = tree.getFocusNode() || tree.getActiveNode();
                const firstNode = tree.getFirstChild();
                if (!currentNode && firstNode && eventName === "ArrowDown") {
                    firstNode.logInfo("Keydown: activate first node.");
                    firstNode.setActive();
                    return;
                }
                focusNode = currentNode || firstNode;
                if (focusNode) {
                    focusNode.setFocus();
                    node = tree.getFocusNode();
                    node.logInfo("Keydown: force focus on active node.");
                }
            }
            const isColspan = node.isColspan();
            if (tree.isRowNav()) {
                // -----------------------------------------------------------------------
                // --- Row Mode ---
                // -----------------------------------------------------------------------
                if (inputHasFocus) {
                    // If editing an embedded input control, let the control handle all
                    // keys. Only Enter and Escape should apply / discard, but keep the
                    // keyboard focus.
                    switch (eventName) {
                        case "Enter":
                            curInput.blur();
                            tree.setFocus();
                            break;
                        case "Escape":
                            node._render();
                            tree.setFocus();
                            break;
                    }
                    return;
                }
                // --- Quick-Search
                if (opts.quicksearch &&
                    eventName.length === 1 &&
                    /^\w$/.test(eventName) &&
                    !curInput) {
                    // Allow to search for longer streaks if typed in quickly
                    const stamp = Date.now();
                    if (stamp - tree.lastQuicksearchTime > QUICKSEARCH_DELAY) {
                        tree.lastQuicksearchTerm = "";
                    }
                    tree.lastQuicksearchTime = stamp;
                    tree.lastQuicksearchTerm += eventName;
                    const matchNode = tree.findNextNode(tree.lastQuicksearchTerm, tree.getActiveNode());
                    if (matchNode) {
                        matchNode.setActive(true, { event: event });
                    }
                    event.preventDefault();
                    return;
                }
                // Pre-Evaluate expand/collapse action for LEFT/RIGHT
                switch (eventName) {
                    case "Enter":
                        if (node.isActive()) {
                            if (node.isExpanded()) {
                                eventName = "Subtract"; // callapse
                            }
                            else if (node.isExpandable(true)) {
                                eventName = "Add"; // expand
                            }
                        }
                        break;
                    case "ArrowLeft":
                        if (node.expanded) {
                            eventName = "Subtract"; // collapse
                        }
                        break;
                    case "ArrowRight":
                        if (!node.expanded && node.isExpandable(true)) {
                            eventName = "Add"; // expand
                        }
                        else if (navModeOption === NavModeEnum.startCell ||
                            navModeOption === NavModeEnum.startRow) {
                            event.preventDefault();
                            tree.setCellNav();
                            return false;
                        }
                        break;
                }
                // Standard navigation (row mode)
                switch (eventName) {
                    case "+":
                    case "Add":
                        // case "=": // 187: '+' @ Chrome, Safari
                        node.setExpanded(true);
                        break;
                    case "-":
                    case "Subtract":
                        node.setExpanded(false);
                        break;
                    case " ": // Space
                        // if (node.isPagingNode()) {
                        //   tree._triggerNodeEvent("clickPaging", ctx, event);
                        // } else
                        if (node.getOption("checkbox")) {
                            node.toggleSelected();
                        }
                        else {
                            node.setActive(true, { event: event });
                        }
                        break;
                    case "Enter":
                        node.setActive(true, { event: event });
                        break;
                    case "ArrowDown":
                    case "ArrowLeft":
                    case "ArrowRight":
                    case "ArrowUp":
                    case "Backspace":
                    case "End":
                    case "Home":
                    case "Control+End":
                    case "Control+Home":
                    case "Meta+ArrowDown":
                    case "Meta+ArrowUp":
                    case "PageDown":
                    case "PageUp":
                        node.navigate(eventName, { activate: activate, event: event });
                        break;
                    default:
                        handled = false;
                }
            }
            else {
                // -----------------------------------------------------------------------
                // --- Cell Mode ---
                // -----------------------------------------------------------------------
                // // Standard navigation (cell mode)
                // if (isCellEditMode && INPUT_BREAKOUT_KEYS.has(eventName)) {
                // }
                // const curInput = this._getEmbeddedInputElem(null);
                const curInputType = curInput ? curInput.type || curInput.tagName : "";
                // const inputHasFocus = curInput && this._isCurInputFocused();
                const inputCanFocus = curInput && curInputType !== "checkbox";
                if (inputHasFocus) {
                    if (eventName === "Escape") {
                        node.logDebug(`Reset focused input on Escape`);
                        // Discard changes and reset input validation state
                        curInput.setCustomValidity("");
                        node._render();
                        // Keep cell-nav mode
                        tree.setFocus();
                        tree.setColumn(tree.activeColIdx);
                        return;
                        // } else if (!INPUT_BREAKOUT_KEYS.has(eventName)) {
                    }
                    else if (eventName !== "Enter") {
                        if (curInput && curInput.checkValidity && !curInput.checkValidity()) {
                            // Invalid input: ignore all keys except Enter and Escape
                            node.logDebug(`Ignored ${eventName} inside invalid input`);
                            return false;
                        }
                        // Let current `<input>` handle it
                        node.logDebug(`Ignored ${eventName} inside focused input`);
                        return;
                    }
                    // const curInputType = curInput.type || curInput.tagName;
                    // const breakoutKeys = INPUT_KEYS[curInputType];
                    // if (!breakoutKeys.includes(eventName)) {
                    //   node.logDebug(`Ignored ${eventName} inside ${curInputType} input`);
                    //   return;
                    // }
                }
                else if (curInput) {
                    // On a cell that has an embedded, unfocused <input>
                    if (eventName.length === 1 && inputCanFocus) {
                        // Typing a single char
                        curInput.focus();
                        curInput.value = "";
                        node.logDebug(`Focus input: ${eventName}`);
                        return false;
                    }
                }
                if (eventName === "Tab") {
                    eventName = "ArrowRight";
                    handled = true;
                }
                else if (eventName === "Shift+Tab") {
                    eventName = tree.activeColIdx > 0 ? "ArrowLeft" : "";
                    handled = true;
                }
                switch (eventName) {
                    case "+":
                    case "Add":
                        // case "=": // 187: '+' @ Chrome, Safari
                        node.setExpanded(true);
                        break;
                    case "-":
                    case "Subtract":
                        node.setExpanded(false);
                        break;
                    case " ": // Space
                        if (tree.activeColIdx === 0 && node.getOption("checkbox")) {
                            node.toggleSelected();
                            handled = true;
                        }
                        else if (curInput && curInputType === "checkbox") {
                            curInput.click();
                            // toggleCheckbox(curInput)
                            // new Event("change")
                            // curInput.change
                            handled = true;
                        }
                        break;
                    case "F2":
                        if (curInput && !inputHasFocus && inputCanFocus) {
                            curInput.focus();
                            handled = true;
                        }
                        break;
                    case "Enter":
                        tree.setFocus(); // Blur prev. input if any
                        if ((tree.activeColIdx === 0 || isColspan) && node.isExpandable()) {
                            node.setExpanded(!node.isExpanded());
                            handled = true;
                        }
                        else if (curInput && !inputHasFocus && inputCanFocus) {
                            curInput.focus();
                            handled = true;
                        }
                        break;
                    case "Escape":
                        tree.setFocus(); // Blur prev. input if any
                        node.log(`keynav: focus tree...`);
                        if (tree.isCellNav() && navModeOption !== NavModeEnum.cell) {
                            node.log(`keynav: setCellNav(false)`);
                            tree.setCellNav(false); // row-nav mode
                            tree.setFocus(); //
                            handled = true;
                        }
                        break;
                    case "ArrowLeft":
                        tree.setFocus(); // Blur prev. input if any
                        if (isColspan && node.isExpanded()) {
                            node.setExpanded(false);
                        }
                        else if (!isColspan && tree.activeColIdx > 0) {
                            tree.setColumn(tree.activeColIdx - 1);
                        }
                        else if (navModeOption !== NavModeEnum.cell) {
                            tree.setCellNav(false); // row-nav mode
                        }
                        handled = true;
                        break;
                    case "ArrowRight":
                        tree.setFocus(); // Blur prev. input if any
                        if (isColspan && !node.isExpanded()) {
                            node.setExpanded();
                        }
                        else if (!isColspan &&
                            tree.activeColIdx < tree.columns.length - 1) {
                            tree.setColumn(tree.activeColIdx + 1);
                        }
                        handled = true;
                        break;
                    case "Home": // Generated by [Fn] + ArrowLeft on Mac
                        // case "Meta+ArrowLeft":
                        tree.setFocus(); // Blur prev. input if any
                        if (!isColspan && tree.activeColIdx > 0) {
                            tree.setColumn(0);
                        }
                        handled = true;
                        break;
                    case "End": // Generated by [Fn] + ArrowRight on Mac
                        // case "Meta+ArrowRight":
                        tree.setFocus(); // Blur prev. input if any
                        if (!isColspan && tree.activeColIdx < tree.columns.length - 1) {
                            tree.setColumn(tree.columns.length - 1);
                        }
                        handled = true;
                        break;
                    case "ArrowDown":
                    case "ArrowUp":
                    case "Backspace":
                    case "Control+End": // Generated by Control + [Fn] + ArrowRight on Mac
                    case "Control+Home": // Generated by Control + [Fn] + Arrowleft on Mac
                    case "Meta+ArrowDown": // [⌘] + ArrowDown on Mac
                    case "Meta+ArrowUp": // [⌘] + ArrowUp on Mac
                    case "PageDown": // Generated by [Fn] + ArrowDown on Mac
                    case "PageUp": // Generated by [Fn] + ArrowUp on Mac
                        node.navigate(eventName, { activate: activate, event: event });
                        // if (isCellEditMode) {
                        //   this._getEmbeddedInputElem(null, true); // set focus to input
                        // }
                        handled = true;
                        break;
                    default:
                        handled = false;
                }
            }
            if (handled) {
                event.preventDefault();
            }
            return;
        }
    }

    /*!
     * Wunderbaum - ext-logger
     * 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 LoggerExtension extends WunderbaumExtension {
        constructor(tree) {
            super(tree, "logger", {});
            this.ignoreEvents = new Set([
                "iconBadge",
                // "enhanceTitle",
                "render",
                "discard",
            ]);
            this.prefix = tree + ".ext-logger";
        }
        init() {
            const tree = this.tree;
            // this.ignoreEvents.add();
            if (tree.getOption("debugLevel") >= 4) {
                // const self = this;
                const ignoreEvents = this.ignoreEvents;
                const prefix = this.prefix;
                overrideMethod(tree, "callEvent", function (name, extra) {
                    /* eslint-disable prefer-rest-params */
                    if (ignoreEvents.has(name)) {
                        return tree._superApply(arguments);
                    }
                    const start = Date.now();
                    const res = tree._superApply(arguments);
                    tree.logDebug(`${prefix}: callEvent('${name}') took ${Date.now() - start} ms.`, arguments[1]);
                    return res;
                });
            }
        }
        onKeyEvent(data) {
            // this.tree.logInfo("onKeyEvent", eventToString(data.event), data);
            this.tree.logDebug(`${this.prefix}: onKeyEvent()`, data);
            return;
        }
    }

    /*!
     * Wunderbaum - ext-dnd
     * 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 nodeMimeType = "application/x-wunderbaum-node";
    class DndExtension extends WunderbaumExtension {
        constructor(tree) {
            super(tree, "dnd", {
                autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering
                // dropMarkerInsertOffsetX: -16, // Additional offset for drop-marker with hitMode = "before"/"after"
                // dropMarkerOffsetX: -24, // Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop)
                // #1021 `document.body` is not available yet
                // dropMarkerParent: "body", // Root Container used for drop marker (could be a shadow root)
                multiSource: false, // true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed
                effectAllowed: "all", // Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event)
                dropEffectDefault: "move", // Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed (override in drag, dragOver).
                guessDropEffect: true, // Calculate from `effectAllowed` and modifier keys)
                preventForeignNodes: false, // Prevent dropping nodes from different Wunderbaum trees
                preventLazyParents: true, // Prevent dropping items on unloaded lazy Wunderbaum tree nodes
                preventNonNodes: false, // Prevent dropping items other than Wunderbaum tree nodes
                preventRecursion: true, // Prevent dropping nodes on own descendants
                preventSameParent: false, // Prevent dropping nodes under same direct parent
                preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. (move only)
                serializeClipboardData: true, // Serialize node data to dataTransfer object
                scroll: true, // Enable auto-scrolling while dragging
                scrollSensitivity: 20, // Active top/bottom margin in pixel
                // scrollnterval: 50, // Generate event every 50 ms
                scrollSpeed: 5, // Scroll pixel per 50 ms
                // setTextTypeJson: false, // Allow dragging of nodes to different IE windows
                sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38
                // Events (drag support)
                dragStart: null, // Callback(sourceNode, data), return true, to enable dnd drag
                drag: null, // Callback(sourceNode, data)
                dragEnd: null, // Callback(sourceNode, data)
                // Events (drop support)
                dragEnter: null, // Callback(targetNode, data), return true, to enable dnd drop
                dragOver: null, // Callback(targetNode, data)
                dragExpand: null, // Callback(targetNode, data), return false to prevent autoExpand
                drop: null, // Callback(targetNode, data)
                dragLeave: null, // Callback(targetNode, data)
            });
            // public dropMarkerElem?: HTMLElement;
            this.srcNode = null;
            this.lastTargetNode = null;
            this.lastEnterStamp = 0;
            this.lastAllowedDropRegions = null;
            this.lastDropEffect = null;
            this.lastDropRegion = false;
            this.currentScrollDir = 0;
            this.applyScrollDirThrottled = throttle(this._applyScrollDir, 50);
        }
        init() {
            super.init();
            // Store the current scroll parent, which may be the tree
            // container, any enclosing div, or the document.
            // #761: scrollParent() always needs a container child
            // $temp = $("<span>").appendTo(this.$container);
            // this.$scrollParent = $temp.scrollParent();
            // $temp.remove();
            const tree = this.tree;
            const dndOpts = tree.options.dnd;
            // Enable drag support if dragStart() is specified:
            if (dndOpts.dragStart) {
                onEvent(tree.element, "dragstart drag dragend", this.onDragEvent.bind(this));
            }
            // Enable drop support if dragEnter() is specified:
            if (dndOpts.dragEnter) {
                onEvent(tree.element, "dragenter dragover dragleave drop", this.onDropEvent.bind(this));
            }
        }
        /** Cleanup classes after target node is no longer hovered. */
        _leaveNode() {
            // We remove the marker on dragenter from the previous target:
            const ltn = this.lastTargetNode;
            this.lastEnterStamp = 0;
            if (ltn) {
                ltn.setClass("wb-drop-target wb-drop-over wb-drop-after wb-drop-before", false);
                this.lastTargetNode = null;
            }
        }
        /** */
        unifyDragover(res) {
            if (res === false) {
                return false;
            }
            else if (res instanceof Set) {
                return res.size > 0 ? res : false;
            }
            else if (res === true) {
                return new Set(["over", "before", "after"]);
            }
            else if (typeof res === "string" || isArray(res)) {
                res = toSet(res);
                return res.size > 0 ? res : false;
            }
            throw new Error("Unsupported drop region definition: " + res);
        }
        /**
         * Calculates the drop region based on the drag event and the allowed drop regions.
         */
        _calcDropRegion(e, allowed) {
            const rowHeight = this.tree.options.rowHeightPx;
            const dy = e.offsetY;
            if (!allowed) {
                return false;
            }
            else if (allowed.size === 3) {
                return dy < 0.25 * rowHeight
                    ? "before"
                    : dy > 0.75 * rowHeight
                        ? "after"
                        : "over";
            }
            else if (allowed.size === 1 && allowed.has("over")) {
                return "over";
            }
            else {
                // Only 'before' and 'after':
                return dy > rowHeight / 2 ? "after" : "before";
            }
            // return "over";
        }
        /**
         * Guess drop effect (copy/link/move) using opinionated conventions.
         *
         * Default: dnd.dropEffectDefault
         */
        _guessDropEffect(e) {
            // const nativeDropEffect = e.dataTransfer?.dropEffect;
            var _a;
            // if (nativeDropEffect && nativeDropEffect !== "none") {
            //   return nativeDropEffect;
            // }
            const dndOpts = this.treeOpts.dnd;
            const ea = (_a = dndOpts.effectAllowed) !== null && _a !== void 0 ? _a : "all";
            const canCopy = ["all", "copy", "copyLink", "copyMove"].includes(ea);
            const canLink = ["all", "link", "copyLink", "linkMove"].includes(ea);
            const canMove = ["all", "move", "copyMove", "linkMove"].includes(ea);
            let res = dndOpts.dropEffectDefault;
            if (dndOpts.guessDropEffect) {
                if (isMac) {
                    if (e.altKey && canCopy) {
                        res = "copy";
                    }
                    if (e.metaKey && canMove) {
                        res = "move"; // command key
                    }
                    if (e.altKey && e.metaKey && canLink) {
                        res = "link";
                    }
                }
                else {
                    if (e.ctrlKey && canCopy) {
                        res = "copy";
                    }
                    if (e.shiftKey && canMove) {
                        res = "move";
                    }
                    if (e.altKey && canLink) {
                        res = "link";
                    }
                }
            }
            return res;
        }
        /** Don't allow void operation ('drop on self').*/
        _isVoidDrop(targetNode, srcNode, dropRegion) {
            // this.tree.logDebug(
            //   `_isVoidDrop: ${srcNode} -> ${dropRegion} ${targetNode}`
            // );
            // TODO: should be checked on  move only
            if (!this.treeOpts.dnd.preventVoidMoves || !srcNode) {
                return false;
            }
            if ((dropRegion === "before" && targetNode === srcNode.getNextSibling()) ||
                (dropRegion === "after" && targetNode === srcNode.getPrevSibling())) {
                // this.tree.logDebug("Prevented before/after self");
                return true;
            }
            // Don't allow dropping nodes on own parent (or self)
            return srcNode === targetNode || srcNode.parent === targetNode;
        }
        /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
        _applyScrollDir() {
            if (this.isDragging() && this.currentScrollDir) {
                const dndOpts = this.tree.options.dnd;
                const sp = this.tree.element; // scroll parent
                const scrollTop = sp.scrollTop;
                if (this.currentScrollDir < 0) {
                    sp.scrollTop = Math.max(0, scrollTop - dndOpts.scrollSpeed);
                }
                else if (this.currentScrollDir > 0) {
                    sp.scrollTop = scrollTop + dndOpts.scrollSpeed;
                }
            }
        }
        /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
        _autoScroll(viewportY) {
            const tree = this.tree;
            const dndOpts = tree.options.dnd;
            const sensitivity = dndOpts.scrollSensitivity;
            const sp = tree.element; // scroll parent
            const headerHeight = tree.headerElement.clientHeight; // May be 0
            // const height = sp.clientHeight - headerHeight;
            // const height = sp.offsetHeight + headerHeight;
            const height = sp.offsetHeight;
            const scrollTop = sp.scrollTop;
            // tree.logDebug(
            //   `autoScroll: height=${height}, scrollTop=${scrollTop}, viewportY=${viewportY}`
            // );
            this.currentScrollDir = 0;
            if (scrollTop > 0 &&
                viewportY > 0 &&
                viewportY <= sensitivity + headerHeight) {
                // Mouse in top 20px area: scroll up
                // sp.scrollTop = Math.max(0, scrollTop - dndOpts.scrollSpeed);
                this.currentScrollDir = -1;
            }
            else if (scrollTop < sp.scrollHeight - height &&
                viewportY >= height - sensitivity) {
                // Mouse in bottom 20px area: scroll down
                // sp.scrollTop = scrollTop + dndOpts.scrollSpeed;
                this.currentScrollDir = 1;
            }
            if (this.currentScrollDir) {
                this.applyScrollDirThrottled();
            }
            return sp.scrollTop - scrollTop;
        }
        /** Return true if a drag operation currently in progress. */
        isDragging() {
            return !!this.srcNode;
        }
        /**
         * Handle dragstart, drag and dragend events for the source node.
         */
        onDragEvent(e) {
            var _a;
            // const tree = this.tree;
            const dndOpts = this.treeOpts.dnd;
            const srcNode = Wunderbaum.getNode(e);
            if (!srcNode) {
                this.tree.logWarn(`onDragEvent.${e.type}: no node`);
                return;
            }
            if (["dragstart", "dragend"].includes(e.type)) {
                this.tree.logDebug(`onDragEvent.${e.type} srcNode: ${srcNode}`, e);
            }
            // --- dragstart ---
            if (e.type === "dragstart") {
                // Set a default definition of allowed effects
                e.dataTransfer.effectAllowed = dndOpts.effectAllowed; //"copyMove"; // "all";
                if (srcNode.isEditingTitle()) {
                    srcNode.logDebug("Prevented dragging node in edit mode.");
                    e.preventDefault();
                    return false;
                }
                // Let user cancel the drag operation, override effectAllowed, etc.:
                const res = srcNode._callEvent("dnd.dragStart", { event: e });
                if (!res) {
                    e.preventDefault();
                    return false;
                }
                const nodeData = srcNode.toDict(true, (n) => {
                    // We don't want to re-use the key on drop:
                    n._orgKey = n.key;
                    delete n.key;
                });
                nodeData._treeId = srcNode.tree.id;
                if (dndOpts.serializeClipboardData) {
                    if (typeof dndOpts.serializeClipboardData === "function") {
                        e.dataTransfer.setData(nodeMimeType, dndOpts.serializeClipboardData(nodeData, srcNode));
                    }
                    else {
                        e.dataTransfer.setData(nodeMimeType, JSON.stringify(nodeData));
                    }
                }
                // e.dataTransfer!.setData("text/html", $(node.span).html());
                if (!((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.types.includes("text/plain"))) {
                    e.dataTransfer.setData("text/plain", srcNode.title);
                }
                this.srcNode = srcNode;
                setTimeout(() => {
                    // Decouple this call, so the CSS is applied to the node, but not to
                    // the system generated drag image
                    srcNode.setClass("wb-drag-source");
                }, 0);
                // --- drag ---
            }
            else if (e.type === "drag") {
                if (dndOpts.drag) {
                    srcNode._callEvent("dnd.drag", { event: e });
                }
                // --- dragend ---
            }
            else if (e.type === "dragend") {
                srcNode.setClass("wb-drag-source", false);
                this.srcNode = null;
                if (this.lastTargetNode) {
                    this._leaveNode();
                }
                srcNode._callEvent("dnd.dragEnd", { event: e });
            }
            return true;
        }
        /**
         * Handle dragenter, dragover, dragleave, drop events.
         */
        onDropEvent(e) {
            var _a;
            // const isLink = event.dataTransfer.types.includes("text/uri-list");
            const srcNode = this.srcNode;
            const srcTree = srcNode ? srcNode.tree : null;
            const targetNode = Wunderbaum.getNode(e);
            const dndOpts = this.treeOpts.dnd;
            const dt = e.dataTransfer;
            const dropRegion = this._calcDropRegion(e, this.lastAllowedDropRegions);
            /** Helper to log a message if predicate is false. */
            const _t = (pred, msg) => {
                if (pred) {
                    this.tree.log(`Prevented drop operation (${msg}).`);
                }
                return pred;
            };
            if (!targetNode) {
                this._leaveNode();
                return;
            }
            if (["drop"].includes(e.type)) {
                this.tree.logDebug(`onDropEvent.${e.type} targetNode: ${targetNode}, ea: ${dt === null || dt === void 0 ? void 0 : dt.effectAllowed}, ` +
                    `de: ${dt === null || dt === void 0 ? void 0 : dt.dropEffect}, cy: ${e.offsetY}, r: ${dropRegion}, srcNode: ${srcNode}`, e);
            }
            // --- dragenter ---
            if (e.type === "dragenter") {
                // this.tree.logWarn(` onDropEvent.${e.type} targetNode: ${targetNode}`, e);
                this.lastAllowedDropRegions = null;
                // `dragleave` is not reliable with event delegation, so we generate it
                // from dragenter:
                if (this.lastTargetNode && this.lastTargetNode !== targetNode) {
                    this._leaveNode();
                }
                this.lastTargetNode = targetNode;
                this.lastEnterStamp = Date.now();
                if (
                // Don't drop on status node:
                _t(targetNode.isStatusNode(), "is status node") ||
                    // Prevent dropping nodes from different Wunderbaum trees:
                    _t(dndOpts.preventForeignNodes && targetNode.tree !== srcTree, "preventForeignNodes") ||
                    // Prevent dropping items on unloaded lazy Wunderbaum tree nodes:
                    _t(dndOpts.preventLazyParents && !targetNode.isLoaded(), "preventLazyParents") ||
                    // Prevent dropping items other than Wunderbaum tree nodes:
                    _t(dndOpts.preventNonNodes && !srcNode, "preventNonNodes") ||
                    // Prevent dropping nodes on own descendants:
                    _t(dndOpts.preventRecursion && (srcNode === null || srcNode === void 0 ? void 0 : srcNode.isAncestorOf(targetNode)), "preventRecursion") ||
                    // Prevent dropping nodes under same direct parent:
                    _t(dndOpts.preventSameParent &&
                        srcNode &&
                        targetNode.parent === srcNode.parent, "preventSameParent") ||
                    // Don't allow void operation ('drop on self'): TODO: should be checked on  move only
                    _t(dndOpts.preventVoidMoves && targetNode === srcNode, "preventVoidMoves")) {
                    dt.dropEffect = "none";
                    // this.tree.log("Prevented drop operation");
                    return true; // Prevent drop operation
                }
                // User may return a set of regions (or `false` to prevent drop)
                // Figure out a drop effect (copy/link/move) using opinated conventions.
                dt.dropEffect = this._guessDropEffect(e) || "none";
                let regionSet = targetNode._callEvent("dnd.dragEnter", {
                    event: e,
                    sourceNode: srcNode,
                });
                //
                regionSet = this.unifyDragover(regionSet);
                if (!regionSet) {
                    dt.dropEffect = "none";
                    return true; // Prevent drop operation
                }
                this.lastAllowedDropRegions = regionSet;
                this.lastDropEffect = dt.dropEffect;
                const region = this._calcDropRegion(e, this.lastAllowedDropRegions);
                targetNode.setClass("wb-drop-target");
                targetNode.setClass("wb-drop-over", region === "over");
                targetNode.setClass("wb-drop-before", region === "before");
                targetNode.setClass("wb-drop-after", region === "after");
                e.preventDefault(); // Allow drop (Drop operation is denied by default)
                return false;
                // --- dragover ---
            }
            else if (e.type === "dragover") {
                const viewportY = e.clientY - this.tree.element.offsetTop;
                this._autoScroll(viewportY);
                dt.dropEffect = this._guessDropEffect(e) || "none";
                targetNode._callEvent("dnd.dragOver", { event: e, sourceNode: srcNode });
                const region = this._calcDropRegion(e, this.lastAllowedDropRegions);
                this.lastDropRegion = region;
                this.lastDropEffect = dt.dropEffect;
                if (dndOpts.autoExpandMS > 0 &&
                    targetNode.isExpandable(true) &&
                    !targetNode._isLoading &&
                    Date.now() - this.lastEnterStamp > dndOpts.autoExpandMS &&
                    targetNode._callEvent("dnd.dragExpand", {
                        event: e,
                        sourceNode: srcNode,
                    }) !== false) {
                    targetNode.setExpanded();
                }
                if (!region || this._isVoidDrop(targetNode, srcNode, region)) {
                    return; // We already rejected in dragenter
                }
                targetNode.setClass("wb-drop-over", region === "over");
                targetNode.setClass("wb-drop-before", region === "before");
                targetNode.setClass("wb-drop-after", region === "after");
                e.preventDefault(); // Allow drop (Drop operation is denied by default)
                return false;
                // --- dragleave ---
            }
            else if (e.type === "dragleave") {
                // NOTE: we cannot trust this event, since it is always fired,
                // Instead we remove the marker on dragenter
                targetNode._callEvent("dnd.dragLeave", { event: e, sourceNode: srcNode });
                // --- drop ---
            }
            else if (e.type === "drop") {
                e.stopPropagation(); // prevent browser from opening links?
                e.preventDefault(); // #69 prevent iOS browser from opening links
                this._leaveNode();
                const region = this.lastDropRegion;
                let nodeData = (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.getData(nodeMimeType);
                nodeData = nodeData ? JSON.parse(nodeData) : null;
                const srcNode = this.srcNode;
                const lastDropEffect = this.lastDropEffect;
                setTimeout(() => {
                    // Decouple this call, because drop actions may prevent the dragend event
                    // from being fired on some browsers
                    targetNode._callEvent("dnd.drop", {
                        event: e,
                        region: region,
                        suggestedDropMode: region === "over" ? "appendChild" : region,
                        suggestedDropEffect: lastDropEffect,
                        // suggestedDropEffect: e.dataTransfer?.dropEffect,
                        sourceNode: srcNode,
                        sourceNodeData: nodeData,
                    });
                }, 10);
            }
            return false;
        }
    }

    /*!
     * Wunderbaum - drag_observer
     * 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)
     */
    /**
     * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
     */
    class DragObserver {
        constructor(opts) {
            this.start = {
                event: null,
                x: 0,
                y: 0,
                altKey: false,
                ctrlKey: false,
                metaKey: false,
                shiftKey: false,
            };
            this.dragElem = null;
            this.dragging = false;
            this.customData = {};
            // TODO: touch events
            this.events = ["mousedown", "mouseup", "mousemove", "keydown"];
            if (!opts.root) {
                throw new Error("Missing `root` option.");
            }
            this.opts = Object.assign({ thresh: 5 }, opts);
            this.root = opts.root;
            this._handler = this.handleEvent.bind(this);
            this.events.forEach((type) => {
                this.root.addEventListener(type, this._handler);
            });
        }
        /** Unregister all event listeners. */
        disconnect() {
            this.events.forEach((type) => {
                this.root.removeEventListener(type, this._handler);
            });
        }
        getDragElem() {
            return this.dragElem;
        }
        isDragging() {
            return this.dragging;
        }
        stopDrag(cb_event) {
            if (this.dragging && this.opts.dragstop && cb_event) {
                cb_event.type = "dragstop";
                try {
                    this.opts.dragstop(cb_event);
                }
                catch (err) {
                    console.error("dragstop error", err); // eslint-disable-line no-console
                }
            }
            this.dragElem = null;
            this.dragging = false;
            this.start.event = null;
            this.customData = {};
        }
        handleEvent(e) {
            const type = e.type;
            const opts = this.opts;
            const cb_event = {
                type: e.type,
                startEvent: type === "mousedown" ? e : this.start.event,
                event: e,
                customData: this.customData,
                dragElem: this.dragElem,
                dx: e.pageX - this.start.x,
                dy: e.pageY - this.start.y,
                apply: undefined,
            };
            // console.log("handleEvent", type, cb_event);
            switch (type) {
                case "keydown":
                    this.stopDrag(cb_event);
                    break;
                case "mousedown":
                    if (this.dragElem) {
                        this.stopDrag(cb_event);
                        break;
                    }
                    if (opts.selector) {
                        let elem = e.target;
                        if (elem.matches(opts.selector)) {
                            this.dragElem = elem;
                        }
                        else {
                            elem = elem.closest(opts.selector);
                            if (elem) {
                                this.dragElem = elem;
                            }
                            else {
                                break; // no event delegation selector matched
                            }
                        }
                    }
                    this.start.event = e;
                    this.start.x = e.pageX;
                    this.start.y = e.pageY;
                    this.start.altKey = e.altKey;
                    this.start.ctrlKey = e.ctrlKey;
                    this.start.metaKey = e.metaKey;
                    this.start.shiftKey = e.shiftKey;
                    break;
                case "mousemove":
                    // TODO: debounce/throttle?
                    // TODO: horizontal mode: ignore if dx unchanged
                    if (!this.dragElem) {
                        break;
                    }
                    if (!this.dragging) {
                        if (opts.thresh) {
                            const dist2 = cb_event.dx * cb_event.dx + cb_event.dy * cb_event.dy;
                            if (dist2 < opts.thresh * opts.thresh) {
                                break;
                            }
                        }
                        cb_event.type = "dragstart";
                        if (opts.dragstart(cb_event) === false) {
                            this.stopDrag(cb_event);
                            break;
                        }
                        this.dragging = true;
                    }
                    if (this.dragging && this.opts.drag) {
                        cb_event.type = "drag";
                        this.opts.drag(cb_event);
                    }
                    break;
                case "mouseup":
                    if (!this.dragging) {
                        this.stopDrag(cb_event);
                        break;
                    }
                    if (e.button === 0) {
                        cb_event.apply = true;
                    }
                    else {
                        cb_event.apply = false;
                    }
                    this.stopDrag(cb_event);
                    break;
            }
        }
    }

    /*!
     * Wunderbaum - common
     * 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 DEFAULT_DEBUGLEVEL = 3; // Replaced by rollup script
    /**
     * Fixed height of a row in pixel. Must match the SCSS variable `$row-outer-height`.
     */
    const DEFAULT_ROW_HEIGHT = 22;
    /**
     * Fixed width of node icons in pixel. Must match the SCSS variable `$icon-outer-width`.
     */
    const ICON_WIDTH = 20;
    /**
     * Adjust the width of the title span, so overflow ellipsis work.
     * (2 x `$col-padding-x` + 3px rounding errors).
     */
    const TITLE_SPAN_PAD_Y = 7;
    /** Render row markup for N nodes above and below the visible viewport. */
    const RENDER_MAX_PREFETCH = 5;
    /** Minimum column width if not set otherwise. */
    const DEFAULT_MIN_COL_WIDTH = 4;
    /** Regular expression to detect if a string describes an image URL (in contrast
     * to a class name). Strings are considered image urls if they contain '.' or '/'.
     */
    const TEST_IMG = new RegExp(/\.|\//);
    // export const RECURSIVE_REQUEST_ERROR = "$recursive_request";
    // export const INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid";
    /**
     * Default node icons for icon libraries
     *
     *  - 'bootstrap': {@link https://icons.getbootstrap.com}
     *  - 'fontawesome6' {@link https://fontawesome.com/icons}
     *
     */
    const iconMaps = {
        bootstrap: {
            error: "bi bi-exclamation-triangle",
            // loading: "bi bi-hourglass-split wb-busy",
            loading: "bi bi-chevron-right wb-busy",
            // loading: "bi bi-arrow-repeat wb-spin",
            // loading: '<div class="spinner-border spinner-border-sm" role="status"> <span class="visually-hidden">Loading...</span> </div>',
            // noData: "bi bi-search",
            noData: "bi bi-question-circle",
            expanderExpanded: "bi bi-chevron-down",
            // expanderExpanded: "bi bi-dash-square",
            expanderCollapsed: "bi bi-chevron-right",
            // expanderCollapsed: "bi bi-plus-square",
            expanderLazy: "bi bi-chevron-right wb-helper-lazy-expander",
            // expanderLazy: "bi bi-chevron-bar-right",
            checkChecked: "bi bi-check-square",
            checkUnchecked: "bi bi-square",
            checkUnknown: "bi bi-dash-square-dotted",
            radioChecked: "bi bi-circle-fill",
            radioUnchecked: "bi bi-circle",
            radioUnknown: "bi bi-record-circle",
            folder: "bi bi-folder2",
            folderOpen: "bi bi-folder2-open",
            folderLazy: "bi bi-folder-symlink",
            doc: "bi bi-file-earmark",
            colSortable: "bi bi-chevron-expand",
            // colSortable: "bi bi-arrow-down-up",
            // colSortAsc: "bi bi-chevron-down",
            // colSortDesc: "bi bi-chevron-up",
            colSortAsc: "bi bi-arrow-down",
            colSortDesc: "bi bi-arrow-up",
            colFilter: "bi bi-filter-circle",
            colFilterActive: "bi bi-filter-circle-fill wb-helper-invalid",
            colMenu: "bi bi-three-dots-vertical",
        },
        fontawesome6: {
            error: "fa-solid fa-triangle-exclamation",
            loading: "fa-solid fa-chevron-right fa-beat",
            noData: "fa-solid fa-circle-question",
            expanderExpanded: "fa-solid fa-chevron-down",
            expanderCollapsed: "fa-solid fa-chevron-right",
            expanderLazy: "fa-solid fa-chevron-right wb-helper-lazy-expander",
            checkChecked: "fa-regular fa-square-check",
            checkUnchecked: "fa-regular fa-square",
            checkUnknown: "fa-regular fa-square-minus",
            radioChecked: "fa-solid fa-circle",
            radioUnchecked: "fa-regular fa-circle",
            radioUnknown: "fa-regular fa-circle-question",
            folder: "fa-solid fa-folder-closed",
            folderOpen: "fa-regular fa-folder-open",
            folderLazy: "fa-solid fa-folder-plus",
            doc: "fa-regular fa-file",
            colSortable: "fa-solid fa-fw fa-sort",
            colSortAsc: "fa-solid fa-fw fa-sort-up",
            colSortDesc: "fa-solid fa-fw fa-sort-down",
            colFilter: "fa-solid fa-fw fa-filter",
            colFilterActive: "fa-solid fa-fw fa-filter wb-helper-invalid",
            colMenu: "fa-solid fa-fw fa-ellipsis-v",
        },
    };
    /** Dict keys that are evaluated by source loader (others are added to `tree.data` instead). */
    const RESERVED_TREE_SOURCE_KEYS = new Set([
        "_format", // reserved for future use
        "_keyMap", // Used for compressed data format
        "_positional", // Used for compressed data format
        "_typeList", // Used for compressed data format @deprecated
        "_valueMap", // Used for compressed data format
        "_version", // reserved for future use
        "children",
        "columns",
        "types",
    ]);
    // /** Key codes that trigger grid navigation, even when inside an input element. */
    // export const INPUT_BREAKOUT_KEYS: Set<string> = new Set([
    //   // "ArrowDown",
    //   // "ArrowUp",
    //   "Enter",
    //   "Escape",
    // ]);
    /** Map `KeyEvent.key` to navigation action. */
    const KEY_TO_NAVIGATION_MAP = {
        ArrowDown: "down",
        ArrowLeft: "left",
        ArrowRight: "right",
        ArrowUp: "up",
        Backspace: "parent",
        End: "lastCol",
        Home: "firstCol",
        "Control+End": "last",
        "Control+Home": "first",
        "Meta+ArrowDown": "last", // macOs
        "Meta+ArrowUp": "first", // macOs
        PageDown: "pageDown",
        PageUp: "pageUp",
    };
    /** Return a callback that returns true if the node title matches the string
     * or regular expression.
     * @see {@link WunderbaumNode.findAll}
     */
    function makeNodeTitleMatcher(match) {
        if (match instanceof RegExp) {
            return function (node) {
                return match.test(node.title);
            };
        }
        assert(typeof match === "string", `Expected a string or RegExp: ${match}`);
        // s = escapeRegex(s.toLowerCase());
        return function (node) {
            return node.title === match;
            // console.log("match " + node, node.title.toLowerCase().indexOf(match))
            // return node.title.toLowerCase().indexOf(match) >= 0;
        };
    }
    /** Return a callback that returns true if the node title starts with a string (case-insensitive). */
    function makeNodeTitleStartMatcher(s) {
        s = escapeRegex(s);
        const reMatch = new RegExp("^" + s, "i");
        return function (node) {
            return reMatch.test(node.title);
        };
    }
    /** Compare two nodes by title (case-insensitive). */
    function nodeTitleSorter(a, b) {
        const x = a.title.toLowerCase();
        const y = b.title.toLowerCase();
        return x === y ? 0 : x > y ? 1 : -1;
    }
    /**
     * Convert 'flat' to 'nested' format.
     *
     * Flat node entry format:
     *    [PARENT_IDX, {KEY_VALUE_ARGS}]
     * or, if N _positional re defined:
     *    [PARENT_IDX, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., POSITIONAL_ARG_N]
     * Even if _positional additional are defined, KEY_VALUE_ARGS can be appended:
     *    [PARENT_IDX, POSITIONAL_ARG_1, ..., {KEY_VALUE_ARGS}]
     *
     * 1. Parent-referencing list is converted to a list of nested dicts with
     *    optional `children` properties.
     * 2. `[POSITIONAL_ARGS]` are added as dict attributes.
     */
    function unflattenSource(source) {
        var _a, _b, _c;
        const { _format, _keyMap = {}, _positional = [], children } = source;
        const _positionalCount = _positional.length;
        if (_format !== "flat") {
            throw new Error(`Expected source._format: "flat", but got ${_format}`);
        }
        if (_positionalCount && _positional.includes("children")) {
            throw new Error(`source._positional must not include "children": ${_positional}`);
        }
        let longToShort = _keyMap;
        if (_keyMap.t) {
            // Inverse keyMap was used (pre 0.7.0)
            // TODO: raise Error on final 1.x release
            const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`;
            console.warn(msg); // eslint-disable-line no-console
            longToShort = {};
            for (const [key, value] of Object.entries(_keyMap)) {
                longToShort[value] = key;
            }
        }
        const positionalShort = _positional.map((e) => { var _a; return (_a = longToShort[e]) !== null && _a !== void 0 ? _a : e; });
        const newChildren = [];
        const keyToNodeMap = {};
        const indexToNodeMap = {};
        const keyAttrName = (_a = longToShort["key"]) !== null && _a !== void 0 ? _a : "key";
        const childrenAttrName = (_b = longToShort["children"]) !== null && _b !== void 0 ? _b : "children";
        for (const [index, nodeTuple] of children.entries()) {
            // Node entry format:
            //   [PARENT_ID, [POSITIONAL_ARGS]]
            // or
            //   [PARENT_ID, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., {KEY_VALUE_ARGS}]
            let kwargs;
            const [parentId, ...args] = nodeTuple;
            if (args.length === _positionalCount) {
                kwargs = {};
            }
            else if (args.length === _positionalCount + 1) {
                kwargs = args.pop();
                if (typeof kwargs !== "object") {
                    throw new Error(`unflattenSource: Expected dict as last tuple element: ${nodeTuple}`);
                }
            }
            else {
                throw new Error(`unflattenSource: unexpected tuple length: ${nodeTuple}`);
            }
            // Free up some memory as we go
            nodeTuple[1] = null;
            if (nodeTuple[2] != null) {
                nodeTuple[2] = null;
            }
            // We keep `kwargs` as our new node definition. Then we add all positional
            // values to this object:
            args.forEach((val, positionalIdx) => {
                kwargs[positionalShort[positionalIdx]] = val;
            });
            args.length = 0;
            // Find the parent node. `null` means 'toplevel'. PARENT_ID may be the numeric
            // index of the source.children list. If PARENT_ID is a string, we search
            // a parent with node.key of this value.
            indexToNodeMap[index] = kwargs;
            const key = kwargs[keyAttrName];
            if (key != null) {
                keyToNodeMap[key] = kwargs;
            }
            let parentNode = null;
            if (parentId === null) ;
            else if (typeof parentId === "number") {
                parentNode = indexToNodeMap[parentId];
                if (parentNode === undefined) {
                    throw new Error(`unflattenSource: Could not find parent node by index: ${parentId}.`);
                }
            }
            else {
                parentNode = keyToNodeMap[parentId];
                if (parentNode === undefined) {
                    throw new Error(`unflattenSource: Could not find parent node by key: ${parentId}`);
                }
            }
            if (parentNode) {
                (_c = parentNode[childrenAttrName]) !== null && _c !== void 0 ? _c : (parentNode[childrenAttrName] = []);
                parentNode[childrenAttrName].push(kwargs);
            }
            else {
                newChildren.push(kwargs);
            }
        }
        source.children = newChildren;
    }
    /**
     * Decompresses the source data by
     * - converting from 'flat' to 'nested' format
     * - expanding short alias names to long names (if defined in _keyMap)
     * - resolving value indexes to value strings (if defined in _valueMap)
     *
     * @param source - The source object to be decompressed.
     * @returns void
     */
    function decompressSourceData(source) {
        let { _format, _version = 1, _keyMap, _valueMap } = source;
        assert(_version === 1, `Expected file version 1 instead of ${_version}`);
        let longToShort = _keyMap;
        let shortToLong = {};
        if (longToShort) {
            for (const [key, value] of Object.entries(longToShort)) {
                shortToLong[value] = key;
            }
        }
        // Fallback for old format (pre 0.7.0, using _keyMap in reverse direction)
        // TODO: raise Error on final 1.x release
        if (longToShort && longToShort.t) {
            const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`;
            console.warn(msg); // eslint-disable-line no-console
            [longToShort, shortToLong] = [shortToLong, longToShort];
        }
        // Fallback for old format (pre 0.7.0, using _typeList instead of _valueMap)
        // TODO: raise Error on final 1.x release
        if (source._typeList != null) {
            const msg = `source._typeList is deprecated since v0.7.0: use source._valueMap: {"type": [...]} instead.`;
            if (_valueMap != null) {
                throw new Error(msg);
            }
            else {
                console.warn(msg); // eslint-disable-line no-console
                _valueMap = { type: source._typeList };
                delete source._typeList;
            }
        }
        if (_format === "flat") {
            unflattenSource(source);
        }
        delete source._format;
        delete source._version;
        delete source._keyMap;
        delete source._valueMap;
        delete source._positional;
        function _iter(childList) {
            for (const node of childList) {
                // Iterate over a list of names, because we modify inside the loop
                // (for ... of ... does not allow this)
                Object.getOwnPropertyNames(node).forEach((propName) => {
                    const value = node[propName];
                    // Replace short names with long names if defined in _keyMap
                    let longName = propName;
                    if (_keyMap && shortToLong[propName] != null) {
                        longName = shortToLong[propName];
                        if (longName !== propName) {
                            node[longName] = value;
                            delete node[propName];
                        }
                    }
                    // Replace type index with type name if defined in _valueMap
                    if (_valueMap &&
                        typeof value === "number" &&
                        _valueMap[longName] != null) {
                        const newValue = _valueMap[longName][value];
                        if (newValue == null) {
                            throw new Error(`Expected valueMap[${longName}][${value}] entry in [${_valueMap[longName]}]`);
                        }
                        node[longName] = newValue;
                    }
                });
                // Recursion
                if (node.children) {
                    _iter(node.children);
                }
            }
        }
        if (_keyMap || _valueMap) {
            _iter(source.children);
        }
    }

    /*!
     * Wunderbaum - ext-grid
     * 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 GridExtension extends WunderbaumExtension {
        constructor(tree) {
            super(tree, "grid", {
            // throttle: 200,
            });
            this.observer = new DragObserver({
                root: window.document,
                selector: "span.wb-col-resizer-active",
                thresh: 4,
                // throttle: 400,
                dragstart: (e) => {
                    const info = Wunderbaum.getEventInfo(e.startEvent);
                    const colDef = info.colDef;
                    const allow = colDef &&
                        this.tree.element.contains(e.dragElem) &&
                        toBool(colDef.resizable, tree.options.columnsResizable, false);
                    // this.tree.log("dragstart", colDef, e, info);
                    this.tree.element.classList.toggle("wb-col-resizing", !!allow);
                    info.colElem.classList.toggle("wb-col-resizing", !!allow);
                    // We start dagging, so we remember the actual width in *pixels*
                    // (which may be 'auto' or '100%').
                    // Since we we re-create the markup on each update, we also cannot store
                    // the original event or DOM element, but only the colDef object.
                    if (allow) {
                        // Store initial target column infos in customData
                        e.customData.colDef = colDef;
                        e.customData.orgCustomWidthPx = colDef.customWidthPx;
                        const curWidthPx = Number.parseInt(info.colElem.style.width, 10);
                        e.customData.orgWidthPx = curWidthPx;
                        // Set custom width to current width, so that we can modify it
                        colDef.customWidthPx = curWidthPx;
                        // this.tree.log(
                        //   `dragstart customWidthPx=${colDef.customWidthPx}`,
                        //   e,
                        //   info
                        // );
                        this.tree.update(ChangeType.colStructure);
                        // this.tree.log(
                        //   `dragstart 2 customWidthPx=${colDef.customWidthPx}`,
                        //   e,
                        //   info
                        // );
                    }
                    return allow;
                },
                drag: (e) => {
                    // TODO: throttle
                    return this.handleDrag(e);
                },
                dragstop: (e) => {
                    return this.handleDrag(e);
                },
            });
        }
        init() {
            super.init();
        }
        /**
         * Hanldes drag and sragstop events for column resizing.
         */
        handleDrag(e) {
            const custom = e.customData;
            const colDef = custom.colDef;
            // this.tree.log(`${e.type} (dx=${e.dx})`, e, info);
            if (e.type === "dragstop" || e.type === "drag") {
                this.tree.element.classList.remove("wb-col-resizing");
                // info.colElem!.classList.remove("wb-col-resizing");
                if (e.apply || e.type === "drag") {
                    const minWidth = toPixel(colDef.minWidth, DEFAULT_MIN_COL_WIDTH);
                    const newWidth = Math.max(minWidth, custom.orgWidthPx + e.dx);
                    colDef.customWidthPx = newWidth;
                    // this.tree.log(
                    //   `${e.type} minWidth=${minWidth}, newWidth=${newWidth}`,
                    //   colDef
                    // );
                }
                else {
                    // Drag was cancelled
                    this.tree.log("Column resize cancelled", e);
                    colDef.customWidthPx = custom.orgCustomWidthPx; // Restore original width or undefined
                }
                this.tree.update(ChangeType.colStructure);
            }
        }
    }

    /*!
     * Wunderbaum - deferred
     * 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)
     */
    /**
     * Implement a ES6 Promise, that exposes a resolve() and reject() method.
     *
     * Loosely mimics {@link https://api.jquery.com/category/deferred-object/ | jQuery.Deferred}.
     * Example:
     * ```js
     * function foo() {
     *   let dfd = new Deferred(),
     *   ...
     *   dfd.resolve('foo')
     *   ...
     *   return dfd.promise();
     * }
     * ```
     */
    class Deferred {
        constructor() {
            this._promise = new Promise((resolve, reject) => {
                this._resolve = resolve;
                this._reject = reject;
            });
        }
        /** Resolve the Promise. */
        resolve(value) {
            this._resolve(value);
        }
        /** Reject the Promise. */
        reject(reason) {
            this._reject(reason);
        }
        /** Return the native Promise instance.*/
        promise() {
            return this._promise;
        }
        /** Call Promise.then on the embedded promise instance.*/
        then(cb) {
            return this._promise.then(cb);
        }
        /** Call Promise.catch on the embedded promise instance.*/
        catch(cb) {
            return this._promise.catch(cb);
        }
        /** Call Promise.finally on the embedded promise instance.*/
        finally(cb) {
            return this._promise.finally(cb);
        }
    }

    /*!
     * Wunderbaum - wunderbaum_node
     * 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)
     */
    /** WunderbaumNode properties that can be passed with source data.
     * (Any other source properties will be stored as `node.data.PROP`.)
     */
    const NODE_PROPS = new Set([
        "checkbox",
        "classes",
        "expanded",
        "icon",
        "iconTooltip",
        "key",
        "lazy",
        "_partsel",
        "radiogroup",
        "refKey",
        "selected",
        "statusNodeType",
        "title",
        "tooltip",
        "type",
        "unselectable",
    ]);
    /** WunderbaumNode properties that will be returned by `node.toDict()`.)
     */
    const NODE_DICT_PROPS = new Set(NODE_PROPS);
    NODE_DICT_PROPS.delete("_partsel");
    NODE_DICT_PROPS.delete("unselectable");
    // /** Node properties that are of type bool (or boolean & string).
    //  *  When parsing, we accept 0 for false and 1 for true for better JSON compression.
    //  */
    // export const NODE_BOOL_PROPS: Set<string> = new Set([
    //   "checkbox",
    //   "colspan",
    //   "expanded",
    //   "icon",
    //   "iconTooltip",
    //   "radiogroup",
    //   "selected",
    //   "tooltip",
    //   "unselectable",
    // ]);
    /**
     * A single tree node.
     *
     * **NOTE:** <br>
     * Generally you should not modify properties directly, since this may break
     * the internal bookkeeping.
     */
    class WunderbaumNode {
        constructor(tree, parent, data) {
            var _a, _b;
            /** Reference key. Unlike {@link key}, a `refKey` may occur multiple
             * times within a tree (in this case we have 'clone nodes').
             * @see Use {@link setKey} to modify.
             */
            this.refKey = undefined;
            /**
             * Array of child nodes (null for leaf nodes).
             * For lazy nodes, this is `null` or ùndefined` until the children are loaded
             * and leaf nodes may be `[]` (empty array).
             * @see {@link hasChildren}, {@link addChildren}, {@link lazy}.
             */
            this.children = null;
            /** Additional classes added to `div.wb-row`.
             * @see {@link hasClass}, {@link setClass}. */
            this.classes = null; //new Set<string>();
            /** Custom data that was passed to the constructor */
            this.data = {};
            this._isLoading = false;
            this._requestId = 0;
            this._errorInfo = null;
            this._partsel = false;
            this._partload = false;
            this.subMatchCount = 0;
            this._rowIdx = 0;
            this._rowElem = undefined;
            assert(!parent || parent.tree === tree, `Invalid parent: ${parent}`);
            assert(!data.children, "'children' not allowed here");
            this.tree = tree;
            this.parent = parent;
            this.key = "" + ((_a = data.key) !== null && _a !== void 0 ? _a : ++WunderbaumNode.sequence);
            this.title = "" + ((_b = data.title) !== null && _b !== void 0 ? _b : "<" + this.key + ">");
            this.expanded = !!data.expanded;
            this.lazy = !!data.lazy;
            // We set the following node properties only if a matching data value is
            // passed
            data.refKey != null ? (this.refKey = "" + data.refKey) : 0;
            data.type != null ? (this.type = "" + data.type) : 0;
            data.icon != null ? (this.icon = intToBool(data.icon)) : 0;
            data.tooltip != null ? (this.tooltip = intToBool(data.tooltip)) : 0;
            data.iconTooltip != null
                ? (this.iconTooltip = intToBool(data.iconTooltip))
                : 0;
            data.statusNodeType != null
                ? (this.statusNodeType = ("" + data.statusNodeType))
                : 0;
            data.colspan != null ? (this.colspan = !!data.colspan) : 0;
            // Selection
            data.checkbox != null ? intToBool(data.checkbox) : 0;
            data.radiogroup != null ? (this.radiogroup = !!data.radiogroup) : 0;
            data.selected != null ? (this.selected = !!data.selected) : 0;
            data.unselectable != null ? (this.unselectable = !!data.unselectable) : 0;
            if (data.classes) {
                this.setClass(data.classes);
            }
            // Store custom fields as `node.data`
            for (const [key, value] of Object.entries(data)) {
                if (!NODE_PROPS.has(key)) {
                    this.data[key] = value;
                }
            }
            if (parent && !this.statusNodeType) {
                // Don't register root node or status nodes
                tree._registerNode(this);
            }
        }
        /**
         * Return readable string representation for this instance.
         * @internal
         */
        toString() {
            return `WunderbaumNode@${this.key}<'${this.title}'>`;
        }
        /**
         * Iterate all descendant nodes depth-first, pre-order using `for ... of ...` syntax.
         * More concise, but slightly slower than {@link WunderbaumNode.visit}.
         *
         * Example:
         * ```js
         * for(const n of node) {
         *   ...
         * }
         * ```
         */
        *[Symbol.iterator]() {
            // let node: WunderbaumNode | null = this;
            const cl = this.children;
            if (cl) {
                for (let i = 0, l = cl.length; i < l; i++) {
                    const n = cl[i];
                    yield n;
                    if (n.children) {
                        yield* n;
                    }
                }
                // Slower:
                // for (let node of this.children) {
                //   yield node;
                //   yield* node : 0;
                // }
            }
        }
        // /** Return an option value. */
        // protected _getOpt(
        //   name: string,
        //   nodeObject: any = null,
        //   treeOptions: any = null,
        //   defaultValue: any = null
        // ): any {
        //   return evalOption(
        //     name,
        //     this,
        //     nodeObject || this,
        //     treeOptions || this.tree.options,
        //     defaultValue
        //   );
        // }
        /** Call event handler if defined in tree.options.
         * Example:
         * ```js
         * node._callEvent("edit.beforeEdit", {foo: 42})
         * ```
         */
        _callEvent(type, extra) {
            var _a;
            return (_a = this.tree) === null || _a === void 0 ? void 0 : _a._callEvent(type, extend({
                node: this,
                typeInfo: this.type ? this.tree.types[this.type] : {},
            }, extra));
        }
        /**
         * Append (or insert) a list of child nodes.
         *
         * Tip: pass `{ before: 0 }` to prepend new nodes as first children.
         *
         * @returns first child added
         */
        addChildren(nodeData, options) {
            const tree = this.tree;
            let { before = null, applyMinExpanLevel = true, _level } = options !== null && options !== void 0 ? options : {};
            // let { before, loadLazy=true, _level } = options ?? {};
            // const isTopCall = _level == null;
            _level !== null && _level !== void 0 ? _level : (_level = this.getLevel());
            const nodeList = [];
            try {
                tree.enableUpdate(false);
                if (isPlainObject(nodeData)) {
                    nodeData = [nodeData];
                }
                const forceExpand = applyMinExpanLevel && _level < tree.options.minExpandLevel;
                for (const child of nodeData) {
                    const subChildren = child.children;
                    delete child.children;
                    const n = new WunderbaumNode(tree, this, child);
                    if (forceExpand && !n.isUnloaded()) {
                        n.expanded = true;
                    }
                    nodeList.push(n);
                    if (subChildren) {
                        n.addChildren(subChildren, { _level: _level + 1 });
                    }
                }
                if (!this.children) {
                    this.children = nodeList;
                }
                else if (before == null || this.children.length === 0) {
                    this.children = this.children.concat(nodeList);
                }
                else {
                    // Returns null if before is not a direct child:
                    before = this.findDirectChild(before);
                    const pos = this.children.indexOf(before);
                    assert(pos >= 0, `options.before must be a direct child of ${this}`);
                    // insert nodeList after children[pos]
                    this.children.splice(pos, 0, ...nodeList);
                }
                // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null);
                tree.update(ChangeType.structure);
            }
            finally {
                // if (tree.options.selectMode === "hier") {
                //   if (this.parent && this.parent.children) {
                //     this.fixSelection3FromEndNodes();
                //   } else {
                //     // may happen when loading __root__;
                //   }
                // }
                tree.enableUpdate(true);
            }
            // if(isTopCall && loadLazy){
            //   this.logWarn("addChildren(): loadLazy is not yet implemented.")
            // }
            return nodeList[0];
        }
        /**
         * Append or prepend a node, or append a child node.
         *
         * This a convenience function that calls addChildren()
         *
         * @param nodeData node definition
         * @param [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child')
         * @returns new node
         */
        addNode(nodeData, mode = "appendChild") {
            if (mode === "over") {
                mode = "appendChild"; // compatible with drop region
            }
            switch (mode) {
                case "after":
                    return this.parent.addChildren(nodeData, {
                        before: this.getNextSibling(),
                    });
                case "before":
                    return this.parent.addChildren(nodeData, { before: this });
                case "prependChild":
                    // Insert before the first child if any
                    // let insertBefore = this.children ? this.children[0] : undefined;
                    return this.addChildren(nodeData, { before: 0 });
                case "appendChild":
                    return this.addChildren(nodeData);
            }
            assert(false, `Invalid mode: ${mode}`);
            return undefined;
        }
        /**
         * Apply a modification (or navigation) operation.
         *
         * @see {@link Wunderbaum.applyCommand}
         */
        applyCommand(cmd, options) {
            return this.tree.applyCommand(cmd, this, options);
        }
        /**
         * Collapse all expanded sibling nodes if any.
         * (Automatically called when `autoCollapse` is true.)
         */
        collapseSiblings(options) {
            for (const node of this.parent.children) {
                if (node !== this && node.expanded) {
                    node.setExpanded(false, options);
                }
            }
        }
        /**
         * Add/remove one or more classes to `<div class='wb-row'>`.
         *
         * This also maintains `node.classes`, so the class will survive a re-render.
         *
         * @param className one or more class names. Multiple classes can be passed
         *     as space-separated string, array of strings, or set of strings.
         */
        setClass(className, flag = true) {
            const cnSet = toSet(className);
            if (flag) {
                if (this.classes === null) {
                    this.classes = new Set();
                }
                cnSet.forEach((cn) => {
                    var _a;
                    this.classes.add(cn);
                    (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.classList.toggle(cn, flag);
                });
            }
            else {
                if (this.classes === null) {
                    return;
                }
                cnSet.forEach((cn) => {
                    var _a;
                    this.classes.delete(cn);
                    (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.classList.toggle(cn, flag);
                });
                if (this.classes.size === 0) {
                    this.classes = null;
                }
            }
        }
        /** Start editing this node's title. */
        startEditTitle() {
            this.tree._callMethod("edit.startEditTitle", this);
        }
        /**
         * Call `setExpanded()` on all descendant nodes.
         *
         * @param flag true to expand, false to collapse.
         * @param options Additional options.
         * @see {@link Wunderbaum.expandAll}
         * @see {@link WunderbaumNode.setExpanded}
         */
        async expandAll(flag = true, options) {
            const tree = this.tree;
            const { collapseOthers, deep, depth, force, keepActiveNodeVisible = true, loadLazy, resetLazy, } = options !== null && options !== void 0 ? options : {};
            // limit expansion level to `depth` (or tree.minExpandLevel). Default: unlimited
            const treeLevel = this.tree.options.minExpandLevel || null; // 0 -> null
            const minLevel = depth !== null && depth !== void 0 ? depth : (force ? null : treeLevel);
            const expandOpts = {
                deep: deep,
                force: force,
                loadLazy: loadLazy,
                resetLazy: resetLazy,
                scrollIntoView: false, // don't scroll every node while iterating
            };
            this.logInfo(`expandAll(${flag}, depth=${depth}, minLevel=${minLevel})`);
            assert(!(flag && deep != null && !collapseOthers), "Expanding with `deep` option is not supported (implied by the `depth` option).");
            // Expand all direct children in parallel:
            async function _iter(n, level) {
                var _a;
                // n.logInfo(`  _iter(level=${level})`);
                const promises = [];
                (_a = n.children) === null || _a === void 0 ? void 0 : _a.forEach((cn) => {
                    if (flag) {
                        if (!cn.expanded &&
                            (minLevel == null || level < minLevel) &&
                            (cn.children || (loadLazy && cn.lazy))) {
                            // Node is collapsed and may be expanded (i.e. has children or is lazy)
                            // Expanding may be async, so we store the promise.
                            // Also the recursion is delayed until expansion finished.
                            const p = cn.setExpanded(true, expandOpts);
                            promises.push(p);
                            if (depth == null) {
                                p.then(async () => {
                                    await _iter(cn, level + 1);
                                });
                            }
                        }
                        else {
                            // We don't expand the node, but still visit descendants.
                            // There we may find lazy nodes, so we
                            promises.push(_iter(cn, level + 1));
                        }
                    }
                    else {
                        // Collapsing is always synchronous, so no promises required
                        // Do not collapse until minExpandLevel
                        if (minLevel == null || level >= minLevel) {
                            cn.setExpanded(false, expandOpts);
                        }
                        if ((minLevel != null && level < minLevel) || deep) {
                            _iter(cn, level + 1); // recursion, even if cn was already collapsed
                        }
                    }
                });
                return new Promise((resolve) => {
                    Promise.all(promises).then(() => {
                        resolve(true);
                    });
                });
            }
            const tag = tree.logTime(`${this}.expandAll(${flag}, depth=${depth})`);
            try {
                tree.enableUpdate(false);
                await _iter(this, 0);
                if (collapseOthers) {
                    assert(flag, "Option `collapseOthers` requires flag=true");
                    assert(minLevel != null, "Option `collapseOthers` requires `depth` or `minExpandLevel`");
                    this.expandAll(false, { depth: minLevel });
                }
            }
            finally {
                tree.enableUpdate(true);
                tree.logTimeEnd(tag);
            }
            if (tree.activeNode && keepActiveNodeVisible) {
                tree.activeNode.scrollIntoView();
            }
        }
        /**
         * Find all descendant nodes that match condition (excluding self).
         *
         * If `match` is a string, search for exact node title.
         * If `match` is a RegExp expression, apply it to node.title, using
         * [RegExp.test()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test).
         * If `match` is a callback, match all nodes for that the callback(node) returns true.
         *
         * Returns an empty array if no nodes were found.
         *
         * Examples:
         * ```js
         * // Match all node titles that match exactly 'Joe':
         * nodeList = node.findAll("Joe")
         * // Match all node titles that start with 'Joe' case sensitive:
         * nodeList = node.findAll(/^Joe/)
         * // Match all node titles that contain 'oe', case insensitive:
         * nodeList = node.findAll(/oe/i)
         * // Match all nodes with `data.price` >= 99:
         * nodeList = node.findAll((n) => {
         *   return n.data.price >= 99;
         * })
         * ```
         */
        findAll(match) {
            const matcher = typeof match === "function" ? match : makeNodeTitleMatcher(match);
            const res = [];
            this.visit((n) => {
                if (matcher(n)) {
                    res.push(n);
                }
            });
            return res;
        }
        /** Return the direct child with a given key, index or null. */
        findDirectChild(ptr) {
            const cl = this.children;
            if (!cl) {
                return null;
            }
            if (typeof ptr === "string") {
                for (let i = 0, l = cl.length; i < l; i++) {
                    if (cl[i].key === ptr) {
                        return cl[i];
                    }
                }
            }
            else if (typeof ptr === "number") {
                return cl[ptr];
            }
            else if (ptr.parent === this) {
                // Return null if `ptr` is not a direct child
                return ptr;
            }
            return null;
        }
        /**
         * Find first descendant node that matches condition (excluding self) or null.
         *
         * @see {@link WunderbaumNode.findAll} for examples.
         */
        findFirst(match) {
            const matcher = typeof match === "function" ? match : makeNodeTitleMatcher(match);
            let res = null;
            this.visit((n) => {
                if (matcher(n)) {
                    res = n;
                    return false;
                }
            });
            return res;
        }
        /** Find a node relative to self.
         *
         * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()}
         */
        findRelatedNode(where, includeHidden = false) {
            return this.tree.findRelatedNode(this, where, includeHidden);
        }
        /**
         * Iterator version of {@link WunderbaumNode.format}.
         */
        *format_iter(name_cb, connectors) {
            connectors !== null && connectors !== void 0 ? connectors : (connectors = ["    ", " |  ", " ╰─ ", " ├─ "]);
            name_cb !== null && name_cb !== void 0 ? name_cb : (name_cb = (node) => "" + node);
            function _is_last(node) {
                const ca = node.parent.children;
                return node === ca[ca.length - 1];
            }
            const _format_line = (node) => {
                // https://www.measurethat.net/Benchmarks/Show/12196/0/arr-unshift-vs-push-reverse-small-array
                const parts = [name_cb(node)];
                parts.unshift(connectors[_is_last(node) ? 2 : 3]);
                let p = node.parent;
                while (p && p !== this) {
                    // `this` is the top node
                    parts.unshift(connectors[_is_last(p) ? 0 : 1]);
                    p = p.parent;
                }
                return parts.join("");
            };
            yield name_cb(this);
            for (const node of this) {
                yield _format_line(node);
            }
        }
        /**
         * Return a multiline string representation of a node/subnode hierarchy.
         * Mostly useful for debugging.
         *
         * Example:
         * ```js
         * console.info(tree.getActiveNode().format((n)=>n.title));
         * ```
         * logs
         * ```
         * Books
         *  ├─ Art of War
         *  ╰─ Don Quixote
         * ```
         * @see {@link WunderbaumNode.format_iter}
         */
        format(name_cb, connectors) {
            const a = [];
            for (const line of this.format_iter(name_cb, connectors)) {
                a.push(line);
            }
            return a.join("\n");
        }
        /** Return the `<span class='wb-col'>` element with a given index or id.
         * @returns {WunderbaumNode | null}
         */
        getColElem(colIdx) {
            var _a;
            if (typeof colIdx === "string") {
                colIdx = this.tree.columns.findIndex((value) => value.id === colIdx);
            }
            const colElems = (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.querySelectorAll("span.wb-col");
            return colElems ? colElems[colIdx] : null;
        }
        /**
         * Return all nodes with the same refKey.
         *
         * @param includeSelf Include this node itself.
         * @see {@link Wunderbaum.findByRefKey}
         */
        getCloneList(includeSelf = false) {
            if (!this.refKey) {
                return [];
            }
            const clones = this.tree.findByRefKey(this.refKey);
            if (includeSelf) {
                return clones;
            }
            return [...clones].filter((n) => n !== this);
        }
        /** Return the first child node or null.
         * @returns {WunderbaumNode | null}
         */
        getFirstChild() {
            return this.children ? this.children[0] : null;
        }
        /** Return the last child node or null.
         * @returns {WunderbaumNode | null}
         */
        getLastChild() {
            return this.children ? this.children[this.children.length - 1] : null;
        }
        /** Return node depth (starting with 1 for top level nodes). */
        getLevel() {
            let i = 0, p = this.parent;
            while (p) {
                i++;
                p = p.parent;
            }
            return i;
        }
        /** Return the successive node (under the same parent) or null. */
        getNextSibling() {
            const ac = this.parent.children;
            const idx = ac.indexOf(this);
            return ac[idx + 1] || null;
        }
        /** Return the parent node (null for the system root node). */
        getParent() {
            // TODO: return null for top-level nodes?
            return this.parent;
        }
        /** Return an array of all parent nodes (top-down).
         * @param includeRoot Include the invisible system root node.
         * @param includeSelf Include the node itself.
         */
        getParentList(includeRoot = false, includeSelf = false) {
            const l = [];
            let dtn = includeSelf ? this : this.parent;
            while (dtn) {
                if (includeRoot || dtn.parent) {
                    l.unshift(dtn);
                }
                dtn = dtn.parent;
            }
            return l;
        }
        /** Return a string representing the hierachical node path, e.g. "a/b/c".
         * @param includeSelf
         * @param part property name or callback
         * @param separator
         */
        getPath(includeSelf = true, part = "title", separator = "/") {
            // includeSelf = includeSelf !== false;
            // part = part || "title";
            // separator = separator || "/";
            let val;
            const path = [];
            const isFunc = typeof part === "function";
            this.visitParents((n) => {
                if (n.parent) {
                    val = isFunc
                        ? part(n)
                        : n[part];
                    path.unshift(val);
                }
                return undefined; // TODO remove this line
            }, includeSelf);
            return path.join(separator);
        }
        /** Return the preceeding node (under the same parent) or null. */
        getPrevSibling() {
            const ac = this.parent.children;
            const idx = ac.indexOf(this);
            return ac[idx - 1] || null;
        }
        /** Return true if node has children.
         * Return undefined if not sure, i.e. the node is lazy and not yet loaded.
         */
        hasChildren() {
            if (this.lazy) {
                if (this.children == null) {
                    return undefined; // null or undefined: Not yet loaded
                }
                else if (this.children.length === 0) {
                    return false; // Loaded, but response was empty
                }
                else if (this.children.length === 1 &&
                    this.children[0].isStatusNode()) {
                    return undefined; // Currently loading or load error
                }
                return true; // One or more child nodes
            }
            return !!(this.children && this.children.length);
        }
        /** Return true if node has className set. */
        hasClass(className) {
            return this.classes ? this.classes.has(className) : false;
        }
        /** Return true if node ist the currently focused node. @since 0.9.0 */
        hasFocus() {
            return this.tree.focusNode === this;
        }
        /** Return true if this node is the currently active tree node. */
        isActive() {
            return this.tree.activeNode === this;
        }
        /** Return true if this node is a direct or indirect parent of `other`.
         * @see {@link WunderbaumNode.isParentOf}
         */
        isAncestorOf(other) {
            return other && other.isDescendantOf(this);
        }
        /** Return true if this node is a **direct** subnode of `other`.
         * @see {@link WunderbaumNode.isDescendantOf}
         */
        isChildOf(other) {
            return other && this.parent === other;
        }
        /** Return true if this node's refKey is used by at least one other node.
         */
        isClone() {
            return !!this.refKey && this.tree.findByRefKey(this.refKey).length > 1;
        }
        /** Return true if this node's title spans all columns, i.e. the node has no
         * grid cells.
         */
        isColspan() {
            return !!this.getOption("colspan");
        }
        /** Return true if this node is a direct or indirect subnode of `other`.
         * @see {@link WunderbaumNode.isChildOf}
         */
        isDescendantOf(other) {
            if (!other || other.tree !== this.tree) {
                return false;
            }
            let p = this.parent;
            while (p) {
                if (p === other) {
                    return true;
                }
                if (p === p.parent) {
                    error(`Recursive parent link: ${p}`);
                }
                p = p.parent;
            }
            return false;
        }
        /** Return true if this node has children, i.e. the node is generally expandable.
         * If `andCollapsed` is set, we also check if this node is collapsed, i.e.
         * an expand operation is currently possible.
         */
        isExpandable(andCollapsed = false) {
            // `false` is never expandable (unoffical)
            if ((andCollapsed && this.expanded) || this.children === false) {
                return false;
            }
            if (this.children == null) {
                return !!this.lazy; // null or undefined can trigger lazy load
            }
            if (this.children.length === 0) {
                return !!this.tree.options.emptyChildListExpandable;
            }
            return true;
        }
        /** Return true if _this_ node is currently in edit-title mode.
         *
         * See {@link WunderbaumNode.startEditTitle}.
         */
        isEditingTitle() {
            return this.tree._callMethod("edit.isEditingTitle", this);
        }
        /** Return true if this node is currently expanded. */
        isExpanded() {
            return !!this.expanded;
        }
        /** Return true if this node is the first node of its parent's children. */
        isFirstSibling() {
            const p = this.parent;
            return !p || p.children[0] === this;
        }
        /** Return true if this node is the last node of its parent's children. */
        isLastSibling() {
            const p = this.parent;
            return !p || p.children[p.children.length - 1] === this;
        }
        /** Return true if this node is lazy (even if data was already loaded) */
        isLazy() {
            return !!this.lazy;
        }
        /** Return true if node is lazy and loaded. For non-lazy nodes always return true. */
        isLoaded() {
            return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node
        }
        /** Return true if node is currently loading, i.e. a GET request is pending. */
        isLoading() {
            return this._isLoading;
        }
        /** Return true if this node is a temporarily generated status node of type 'paging'. */
        isPagingNode() {
            return this.statusNodeType === "paging";
        }
        /** Return true if this node is a **direct** parent of `other`.
         * @see {@link WunderbaumNode.isAncestorOf}
         */
        isParentOf(other) {
            return other && other.parent === this;
        }
        /** Return true if this node is partially loaded. @experimental  */
        isPartload() {
            return !!this._partload;
        }
        /** Return true if this node is partially selected (tri-state). */
        isPartsel() {
            return !this.selected && !!this._partsel;
        }
        /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
        isRadio() {
            return !!this.parent.radiogroup || this.getOption("checkbox") === "radio";
        }
        /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
        isRendered() {
            return !!this._rowElem;
        }
        /** Return true if this node is the (invisible) system root node.
         * @see {@link WunderbaumNode.isTopLevel}
         */
        isRootNode() {
            return this.tree.root === this;
        }
        /** Return true if this node is selected, i.e. the checkbox is set.
         * `undefined` if partly selected (tri-state), false otherwise.
         */
        isSelected() {
            return this.selected ? true : this._partsel ? undefined : false;
        }
        /** Return true if this node is a temporarily generated system node like
         * 'loading', 'paging', or 'error' (node.statusNodeType contains the type).
         */
        isStatusNode() {
            return !!this.statusNodeType;
        }
        /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node. */
        isTopLevel() {
            return this.tree.root === this.parent;
        }
        /** Return true if node is marked lazy but not yet loaded.
         * For non-lazy nodes always return false.
         */
        isUnloaded() {
            // Also checks if the only child is a status node:
            return this.hasChildren() === undefined;
        }
        /** Return true if all parent nodes are expanded. Note: this does not check
         * whether the node is scrolled into the visible part of the screen or viewport.
         */
        isVisible() {
            const hasFilter = this.tree.filterMode === "hide";
            const parents = this.getParentList(false, false);
            // TODO: check $(n.span).is(":visible")
            // i.e. return false for nodes (but not parents) that are hidden
            // by a filter
            if (hasFilter && !this.match && !this.subMatchCount) {
                // this.debug( "isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")" );
                return false;
            }
            for (let i = 0, l = parents.length; i < l; i++) {
                const n = parents[i];
                if (!n.expanded) {
                    // this.debug("isVisible: HIDDEN (parent collapsed)");
                    return false;
                }
                // if (hasFilter && !n.match && !n.subMatchCount) {
                // 	this.debug("isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")");
                // 	return false;
                // }
            }
            // this.debug("isVisible: VISIBLE");
            return true;
        }
        _loadSourceObject(source, level) {
            var _a;
            const tree = this.tree;
            level !== null && level !== void 0 ? level : (level = this.getLevel());
            // Let caller modify the parsed JSON response:
            const res = this._callEvent("receive", { response: source });
            if (res != null) {
                source = res;
            }
            if (isArray(source)) {
                source = { children: source };
            }
            assert(isPlainObject(source), `Expected an array or plain object: ${source}`);
            const format = (_a = source.format) !== null && _a !== void 0 ? _a : "nested";
            assert(format === "nested" || format === "flat", `Expected source.format = 'nested' or 'flat': ${format}`);
            // Pre-rocess for 'nested' or 'flat' format
            decompressSourceData(source);
            assert(source.children, "If `source` is an object, it must have a `children` property");
            if (source.types) {
                tree.logInfo("Redefine types", source.columns);
                tree.setTypes(source.types, false);
                delete source.types;
            }
            if (source.columns) {
                tree.logInfo("Redefine columns", source.columns);
                tree.columns = source.columns;
                delete source.columns;
                tree.update(ChangeType.colStructure);
            }
            this.addChildren(source.children);
            // Add extra data to `tree.data`
            for (const [key, value] of Object.entries(source)) {
                if (!RESERVED_TREE_SOURCE_KEYS.has(key)) {
                    tree.data[key] = value;
                    // tree.logDebug(`Add source.${key} to tree.data.${key}`);
                }
            }
            if (tree.options.selectMode === "hier") {
                this.fixSelection3FromEndNodes();
            }
            // Allow to un-sort nodes after sorting
            this.resetNativeChildOrder();
            this._callEvent("load");
        }
        async _fetchWithOptions(source) {
            var _a, _b;
            // Either a URL string or an object with a `.url` property.
            let url, params, body, options, rest;
            let fetchOpts = {};
            if (typeof source === "string") {
                // source is a plain URL string: assume GET request
                url = source;
                fetchOpts.method = "GET";
            }
            else if (isPlainObject(source)) {
                // source is a plain object with `.url` property.
                ({ url, params, body, options, ...rest } = source);
                assert(!rest || Object.keys(rest).length === 0, `Unexpected source properties: ${Object.keys(rest)}. Use 'options' instead.`);
                assert(typeof url === "string", `expected source.url as string`);
                if (isPlainObject(options)) {
                    fetchOpts = options;
                }
                if (isPlainObject(body)) {
                    // we also accept 'body' as object...
                    assert(!fetchOpts.body, "options.body should be passed as source.body");
                    fetchOpts.body = JSON.stringify(fetchOpts.body);
                    (_a = fetchOpts.method) !== null && _a !== void 0 ? _a : (fetchOpts.method = "POST"); // set default
                }
                if (isPlainObject(params)) {
                    url += "?" + new URLSearchParams(params);
                    (_b = fetchOpts.method) !== null && _b !== void 0 ? _b : (fetchOpts.method = "GET"); // set default
                }
            }
            else {
                url = ""; // keep linter happy
                error(`Unsupported source format: ${source}`);
            }
            this.setStatus(NodeStatusType.loading);
            const response = await fetch(url, fetchOpts);
            if (!response.ok) {
                error(`GET ${url} returned ${response.status}, ${response}`);
            }
            return await response.json();
        }
        /** Download  data from the cloud, then call `.update()`. */
        async load(source) {
            const tree = this.tree;
            const requestId = Date.now();
            const prevParent = this.parent;
            const start = Date.now();
            let elap = 0, elapLoad = 0, elapProcess = 0;
            // Check for overlapping requests
            if (this._requestId) {
                this.logWarn(`Recursive load request #${requestId} while #${this._requestId} is pending. ` +
                    "The previous request will be ignored.");
            }
            this._requestId = requestId;
            // const timerLabel = tree.logTime(this + ".load()");
            try {
                const url = typeof source === "string" ? source : source.url;
                if (!url) {
                    // An array or a plain object (that does NOT contain a `.url` property)
                    // will be treated as native Wunderbaum data
                    if (typeof source.then === "function") {
                        const msg = tree.logTime(`Resolve thenable ${source}`);
                        source = await Promise.resolve(source);
                        tree.logTimeEnd(msg);
                    }
                    this._loadSourceObject(source);
                    elapProcess = Date.now() - start;
                }
                else {
                    // Either a URL string or an object with a `.url` property.
                    const data = await this._fetchWithOptions(source);
                    elapLoad = Date.now() - start;
                    if (this._requestId && this._requestId > requestId) {
                        this.logWarn(`Ignored load response #${requestId} because #${this._requestId} is pending.`);
                        return;
                    }
                    else {
                        this.logDebug(`Received response for load request #${requestId}`);
                    }
                    if (this.parent === null && prevParent !== null) {
                        this.logWarn("Lazy parent node was removed while loading: discarding response.");
                        return;
                    }
                    this.setStatus(NodeStatusType.ok);
                    // if (data.columns) {
                    //   tree.logInfo("Re-define columns", data.columns);
                    //   util.assert(!this.parent);
                    //   tree.columns = data.columns;
                    //   delete data.columns;
                    //   tree.updateColumns({ calculateCols: false });
                    // }
                    const startProcess = Date.now();
                    this._loadSourceObject(data);
                    elapProcess = Date.now() - startProcess;
                }
            }
            catch (error) {
                this.logError("Error during load()", source, error);
                this._callEvent("error", { error: error });
                this.setStatus(NodeStatusType.error, { message: "" + error });
                throw error;
            }
            finally {
                this._requestId = 0;
                elap = Date.now() - start;
                if (tree.options.debugLevel >= 3) {
                    tree.logInfo(`Load source took ${elap / 1000} seconds ` +
                        `(transfer: ${elapLoad / 1000}s, ` +
                        `processing: ${elapProcess / 1000}s)`);
                }
            }
        }
        /**
         * Load content of a lazy node.
         * If the node is already loaded, nothing happens.
         * @param [forceReload=false] If true, reload even if already loaded.
         */
        async loadLazy(forceReload = false) {
            const wasExpanded = this.expanded;
            assert(this.lazy, "load() requires a lazy node");
            if (!forceReload && !this.isUnloaded()) {
                return; // Already loaded: nothing to do
            }
            if (this.isLoading()) {
                this.logWarn("loadLazy() called while already loading: ignored.");
                return; // Already loading: prevent duplicate requests
            }
            if (this.isLoaded()) {
                this.resetLazy(); // Also collapses if currently expanded
            }
            // `lazyLoad` may be long-running, so mark node as loading now. `this.load()`
            // will reset the status later.
            this.setStatus(NodeStatusType.loading);
            try {
                const source = await this._callEvent("lazyLoad");
                if (source === false) {
                    this.setStatus(NodeStatusType.ok);
                    return;
                }
                assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}`, or false.");
                await this.load(source);
                this.setStatus(NodeStatusType.ok); // Also resets `this._isLoading`
                if (wasExpanded) {
                    this.expanded = true;
                    this.tree.update(ChangeType.structure);
                }
                else {
                    this.update(); // Fix expander icon to 'loaded'
                }
            }
            catch (e) {
                this.logError("Error during loadLazy()", e);
                this._callEvent("error", { error: e });
                // Also resets `this._isLoading`:
                this.setStatus(NodeStatusType.error, { message: "" + e });
            }
            return;
        }
        /** Write to `console.log` with node name as prefix if opts.debugLevel >= 4.
         * @see {@link WunderbaumNode.logDebug}
         */
        log(...args) {
            if (this.tree.options.debugLevel >= 4) {
                console.log(this.toString(), ...args); // eslint-disable-line no-console
            }
        }
        /** Write to `console.debug` with node name as prefix if opts.debugLevel >= 4
         * and browser console level includes debug/verbose messages.
         * @see {@link WunderbaumNode.log}
         */
        logDebug(...args) {
            if (this.tree.options.debugLevel >= 4) {
                console.debug(this.toString(), ...args); // eslint-disable-line no-console
            }
        }
        /** Write to `console.error` with node name as prefix if opts.debugLevel >= 1. */
        logError(...args) {
            if (this.tree.options.debugLevel >= 1) {
                console.error(this.toString(), ...args); // eslint-disable-line no-console
            }
        }
        /** Write to `console.info` with node name as prefix if opts.debugLevel >= 3. */
        logInfo(...args) {
            if (this.tree.options.debugLevel >= 3) {
                console.info(this.toString(), ...args); // eslint-disable-line no-console
            }
        }
        /** Write to `console.warn` with node name as prefix if opts.debugLevel >= 2. */
        logWarn(...args) {
            if (this.tree.options.debugLevel >= 2) {
                console.warn(this.toString(), ...args); // eslint-disable-line no-console
            }
        }
        /** Expand all parents and optionally scroll into visible area as neccessary.
         * Promise is resolved, when lazy loading and animations are done.
         * @param {object} [options] passed to `setExpanded()`.
         *     Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
         */
        async makeVisible(options) {
            let i;
            const dfd = new Deferred();
            const deferreds = [];
            const parents = this.getParentList(false, false);
            const len = parents.length;
            const noAnimation = getOption(options, "noAnimation", false);
            const scroll = getOption(options, "scrollIntoView", true);
            // Expand bottom-up, so only the top node is animated
            for (i = len - 1; i >= 0; i--) {
                // self.debug("pushexpand" + parents[i]);
                const seOpts = { noAnimation: noAnimation };
                deferreds.push(parents[i].setExpanded(true, seOpts));
            }
            Promise.all(deferreds).then(() => {
                // All expands have finished
                // self.debug("expand DONE", scroll);
                // Note: this.tree may be none when switching demo trees
                if (scroll && this.tree) {
                    // Make sure markup and _rowIdx is updated before we do the scroll calculations
                    this.tree.updatePendingModifications();
                    this.scrollIntoView().then(() => {
                        // self.debug("scroll DONE");
                        dfd.resolve();
                    });
                }
                else {
                    dfd.resolve();
                }
            });
            return dfd.promise();
        }
        /** Move this node to targetNode. */
        moveTo(targetNode, mode = "appendChild", map) {
            if (mode === "over") {
                mode = "appendChild"; // compatible with drop region
            }
            if (mode === "prependChild") {
                if (targetNode.children && targetNode.children.length) {
                    mode = "before";
                    targetNode = targetNode.children[0];
                }
                else {
                    mode = "appendChild";
                }
            }
            let pos;
            const tree = this.tree;
            const prevParent = this.parent;
            const targetParent = mode === "appendChild" ? targetNode : targetNode.parent;
            if (this === targetNode) {
                return;
            }
            else if (!this.parent) {
                error("Cannot move system root");
            }
            else if (targetParent.isDescendantOf(this)) {
                error("Cannot move a node to its own descendant");
            }
            if (targetParent !== prevParent) {
                prevParent.triggerModifyChild("remove", this);
            }
            // Unlink this node from current parent
            if (this.parent.children.length === 1) {
                if (this.parent === targetParent) {
                    return; // #258
                }
                this.parent.children = this.parent.lazy ? [] : null;
                this.parent.expanded = false;
            }
            else {
                pos = this.parent.children.indexOf(this);
                assert(pos >= 0, "invalid source parent");
                this.parent.children.splice(pos, 1);
            }
            // Insert this node to target parent's child list
            this.parent = targetParent;
            if (targetParent.hasChildren()) {
                switch (mode) {
                    case "appendChild":
                        // Append to existing target children
                        targetParent.children.push(this);
                        break;
                    case "before":
                        // Insert this node before target node
                        pos = targetParent.children.indexOf(targetNode);
                        assert(pos >= 0, "invalid target parent");
                        targetParent.children.splice(pos, 0, this);
                        break;
                    case "after":
                        // Insert this node after target node
                        pos = targetParent.children.indexOf(targetNode);
                        assert(pos >= 0, "invalid target parent");
                        targetParent.children.splice(pos + 1, 0, this);
                        break;
                    default:
                        error(`Invalid mode '${mode}'.`);
                }
            }
            else {
                targetParent.children = [this];
            }
            // Let caller modify the nodes
            if (map) {
                targetNode.visit(map, true);
            }
            if (targetParent === prevParent) {
                targetParent.triggerModifyChild("move", this);
            }
            else {
                // prevParent.triggerModifyChild("remove", this);
                targetParent.triggerModifyChild("add", this);
            }
            // Handle cross-tree moves
            if (tree !== targetNode.tree) {
                // Fix node.tree for all source nodes
                // 	util.assert(false, "Cross-tree move is not yet implemented.");
                this.logWarn("Cross-tree moveTo is experimental!");
                this.visit((n) => {
                    // TODO: fix selection state and activation, ...
                    n.tree = targetNode.tree;
                }, true);
            }
            // Make sure we update async, because discarding the markup would prevent
            // DragAndDrop to generate a dragend event on the source node
            setTimeout(() => {
                // Even indentation may have changed:
                tree.update(ChangeType.any);
            }, 0);
            // TODO: fix selection state
            // TODO: fix active state
        }
        /** Set focus relative to this node and optionally activate.
         *
         * 'left' collapses the node if it is expanded, or move to the parent
         * otherwise.
         * 'right' expands the node if it is collapsed, or move to the first
         * child otherwise.
         *
         * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
         *   (Alternatively the `event.key` that would normally trigger this move,
         *   e.g. `ArrowLeft` = 'left'.
         * @param options
         */
        async navigate(where, options) {
            var _a;
            // Allow to pass 'ArrowLeft' instead of 'left'
            const navType = ((_a = KEY_TO_NAVIGATION_MAP[where]) !== null && _a !== void 0 ? _a : where);
            // Otherwise activate or focus the related node
            const node = this.findRelatedNode(navType);
            if (!node) {
                this.logWarn(`Could not find related node '${where}'.`);
                return Promise.resolve(this);
            }
            // setFocus/setActive will scroll later (if autoScroll is specified)
            try {
                node.makeVisible({ scrollIntoView: false });
            }
            catch (e) {
                // ignore
            }
            node.setFocus();
            if ((options === null || options === void 0 ? void 0 : options.activate) === false) {
                return Promise.resolve(this);
            }
            return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event });
        }
        /** Delete this node and all descendants. */
        remove() {
            const tree = this.tree;
            const pos = this.parent.children.indexOf(this);
            this.triggerModify("remove");
            this.parent.children.splice(pos, 1);
            this.visit((n) => {
                n.removeMarkup();
                tree._unregisterNode(n);
            }, true);
            tree.update(ChangeType.structure);
        }
        /** Remove all descendants of this node. */
        removeChildren() {
            var _a, _b;
            const tree = this.tree;
            if (!this.children) {
                return;
            }
            if ((_a = tree.activeNode) === null || _a === void 0 ? void 0 : _a.isDescendantOf(this)) {
                tree.activeNode.setActive(false); // TODO: don't fire events
            }
            if ((_b = tree.focusNode) === null || _b === void 0 ? void 0 : _b.isDescendantOf(this)) {
                tree._setFocusNode(null);
            }
            // TODO: persist must take care to clear select and expand cookies
            // Unlink children to support GC
            // TODO: also delete this.children (not possible using visit())
            this.triggerModifyChild("remove", null);
            this.visit((n) => {
                tree._unregisterNode(n);
            });
            if (this.lazy) {
                // 'undefined' would be interpreted as 'not yet loaded' for lazy nodes
                this.children = [];
            }
            else {
                this.children = null;
            }
            // util.assert(this.parent); // don't call this for root node
            if (!this.isRootNode()) {
                this.expanded = false;
            }
            this.tree.update(ChangeType.structure);
        }
        /** Remove all HTML markup from the DOM. */
        removeMarkup() {
            if (this._rowElem) {
                delete this._rowElem._wb_node;
                this._rowElem.remove();
                this._rowElem = undefined;
            }
        }
        _getRenderInfo() {
            const allColInfosById = {};
            const renderColInfosById = {};
            const isColspan = this.isColspan();
            const colElems = this._rowElem
                ? (this._rowElem.querySelectorAll("span.wb-col"))
                : null;
            let idx = 0;
            for (const col of this.tree.columns) {
                allColInfosById[col.id] = {
                    id: col.id,
                    idx: idx,
                    elem: colElems ? colElems[idx] : null,
                    info: col,
                };
                // renderColInfosById only contains columns that need rendering:
                if (!isColspan && col.id !== "*") {
                    renderColInfosById[col.id] = allColInfosById[col.id];
                }
                idx++;
            }
            return {
                allColInfosById: allColInfosById,
                renderColInfosById: renderColInfosById,
            };
        }
        _createIcon(parentElem, replaceChild, showLoading) {
            const iconElem = this.tree._createNodeIcon(this, showLoading, true);
            if (iconElem) {
                if (replaceChild) {
                    parentElem.replaceChild(iconElem, replaceChild);
                }
                else {
                    parentElem.appendChild(iconElem);
                }
            }
            return iconElem;
        }
        /**
         * Create a whole new `<div class="wb-row">` element.
         * @see {@link WunderbaumNode._render}
         */
        _render_markup(opts) {
            const tree = this.tree;
            const treeOptions = tree.options;
            const rowHeight = treeOptions.rowHeightPx;
            const checkbox = this.getOption("checkbox");
            const columns = tree.columns;
            const level = this.getLevel();
            const activeColIdx = tree.isRowNav() ? null : tree.activeColIdx;
            let elem;
            let rowDiv = this._rowElem;
            let checkboxSpan = null;
            let expanderSpan = null;
            const isNew = !rowDiv;
            assert(isNew, "Expected unrendered node");
            assert(!isNew || (opts && opts.after), "opts.after expected, unless updating");
            assert(!this.isRootNode(), "Root node not allowed");
            rowDiv = document.createElement("div");
            rowDiv.classList.add("wb-row");
            rowDiv.style.top = this._rowIdx * rowHeight + "px";
            this._rowElem = rowDiv;
            // Attach a node reference to the DOM Element:
            rowDiv._wb_node = this;
            const nodeElem = document.createElement("span");
            nodeElem.classList.add("wb-node", "wb-col");
            rowDiv.appendChild(nodeElem);
            let ofsTitlePx = 0;
            if (checkbox) {
                checkboxSpan = document.createElement("i");
                checkboxSpan.classList.add("wb-checkbox");
                if (checkbox === "radio" || this.parent.radiogroup) {
                    checkboxSpan.classList.add("wb-radio");
                }
                nodeElem.appendChild(checkboxSpan);
                ofsTitlePx += ICON_WIDTH;
            }
            for (let i = level - 1; i > 0; i--) {
                elem = document.createElement("i");
                elem.classList.add("wb-indent");
                nodeElem.appendChild(elem);
                ofsTitlePx += ICON_WIDTH;
            }
            if (!treeOptions.minExpandLevel || level > treeOptions.minExpandLevel) {
                expanderSpan = document.createElement("i");
                expanderSpan.classList.add("wb-expander");
                nodeElem.appendChild(expanderSpan);
                ofsTitlePx += ICON_WIDTH;
            }
            // Render the icon (show a 'loading' icon if we do not have an expander that
            // we would prefer).
            const iconSpan = this._createIcon(nodeElem, null, !expanderSpan);
            if (iconSpan) {
                ofsTitlePx += ICON_WIDTH;
            }
            const titleSpan = document.createElement("span");
            titleSpan.classList.add("wb-title");
            nodeElem.appendChild(titleSpan);
            // this._callEvent("enhanceTitle", { titleSpan: titleSpan });
            // Store the width of leading icons with the node, so we can calculate
            // the width of the embedded title span later
            nodeElem._ofsTitlePx = ofsTitlePx;
            // Support HTML5 drag-n-drop
            if (tree.options.dnd.dragStart) {
                nodeElem.draggable = true;
            }
            // Render columns
            const isColspan = this.isColspan();
            if (!isColspan && columns.length > 1) {
                let colIdx = 0;
                for (const col of columns) {
                    colIdx++;
                    let colElem;
                    if (col.id === "*") {
                        colElem = nodeElem;
                    }
                    else {
                        colElem = document.createElement("span");
                        colElem.classList.add("wb-col");
                        rowDiv.appendChild(colElem);
                    }
                    if (colIdx === activeColIdx) {
                        colElem.classList.add("wb-active");
                    }
                    // Add classes from `columns` definition to `<div.wb-col>` cells
                    col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0;
                    colElem.style.left = col._ofsPx + "px";
                    colElem.style.width = col._widthPx + "px";
                    if (isNew && col.html) {
                        if (typeof col.html === "string") {
                            colElem.innerHTML = col.html;
                        }
                    }
                }
            }
            // Attach to DOM as late as possible
            const after = opts ? opts.after : "last";
            switch (after) {
                case "first":
                    tree.nodeListElement.prepend(rowDiv);
                    break;
                case "last":
                    tree.nodeListElement.appendChild(rowDiv);
                    break;
                default:
                    opts.after.after(rowDiv);
            }
            // Now go on and fill in data and update classes
            opts.isNew = true;
            this._render_data(opts);
        }
        /**
         * Render `node.title`, `.icon` into an existing row.
         *
         * @see {@link WunderbaumNode._render}
         */
        _render_data(opts) {
            assert(this._rowElem, "No _rowElem");
            const tree = this.tree;
            const treeOptions = tree.options;
            const rowDiv = this._rowElem;
            const isNew = !!opts.isNew; // Called by _render_markup()?
            const preventScroll = !!opts.preventScroll;
            const columns = tree.columns;
            const isColspan = this.isColspan();
            // Row markup already exists
            const nodeElem = rowDiv.querySelector("span.wb-node");
            const titleSpan = nodeElem.querySelector("span.wb-title");
            const scrollTop = tree.element.scrollTop;
            if (this.titleWithHighlight) {
                titleSpan.innerHTML = this.titleWithHighlight;
            }
            else {
                titleSpan.textContent = this.title; // TODO: this triggers scroll events
            }
            const tooltip = this.getOption("tooltip", false);
            if (tooltip) {
                titleSpan.title = tooltip === true ? this.title : tooltip;
            }
            // NOTE: At least on Safari, this render call triggers a scroll event
            // probably when a focused input is replaced.
            if (preventScroll) {
                tree.element.scrollTop = scrollTop;
            }
            // Set the width of the title span, so overflow ellipsis work
            if (!treeOptions.skeleton) {
                if (isColspan) {
                    const vpWidth = tree.element.clientWidth;
                    titleSpan.style.width =
                        vpWidth - nodeElem._ofsTitlePx - TITLE_SPAN_PAD_Y + "px";
                }
                else {
                    titleSpan.style.width =
                        columns[0]._widthPx -
                            nodeElem._ofsTitlePx -
                            TITLE_SPAN_PAD_Y +
                            "px";
                }
            }
            // Update row classes
            opts.isDataChange = true;
            this._render_status(opts);
            // Let user modify the result
            if (this.statusNodeType) {
                this._callEvent("renderStatusNode", {
                    isNew: isNew,
                    nodeElem: nodeElem,
                    isColspan: isColspan,
                });
            }
            else if (this.parent) {
                // Skip root node
                const renderInfo = this._getRenderInfo();
                this._callEvent("render", {
                    isNew: isNew,
                    nodeElem: nodeElem,
                    isColspan: isColspan,
                    allColInfosById: renderInfo.allColInfosById,
                    renderColInfosById: renderInfo.renderColInfosById,
                });
            }
        }
        /**
         * Update row classes to reflect active, focuses, etc.
         * @see {@link WunderbaumNode._render}
         */
        _render_status(opts) {
            // this.log("_render_status", opts);
            const tree = this.tree;
            const iconMap = tree.iconMap;
            const treeOptions = tree.options;
            const typeInfo = this.type ? tree.types[this.type] : null;
            const rowDiv = this._rowElem;
            // Row markup already exists
            const nodeElem = rowDiv.querySelector("span.wb-node");
            const expanderSpan = nodeElem.querySelector("i.wb-expander");
            const checkboxSpan = nodeElem.querySelector("i.wb-checkbox");
            const rowClasses = ["wb-row"];
            this.expanded ? rowClasses.push("wb-expanded") : 0;
            this.lazy ? rowClasses.push("wb-lazy") : 0;
            this.selected ? rowClasses.push("wb-selected") : 0;
            this._partsel ? rowClasses.push("wb-partsel") : 0;
            this === tree.activeNode ? rowClasses.push("wb-active") : 0;
            this === tree.focusNode ? rowClasses.push("wb-focus") : 0;
            this._errorInfo ? rowClasses.push("wb-error") : 0;
            this._isLoading ? rowClasses.push("wb-loading") : 0;
            this.isColspan() ? rowClasses.push("wb-colspan") : 0;
            this.statusNodeType
                ? rowClasses.push("wb-status-" + this.statusNodeType)
                : 0;
            this.match ? rowClasses.push("wb-match") : 0;
            this.subMatchCount ? rowClasses.push("wb-submatch") : 0;
            treeOptions.skeleton ? rowClasses.push("wb-skeleton") : 0;
            // Replace previous classes:
            rowDiv.className = rowClasses.join(" ");
            // Add classes from `node.classes`
            this.classes ? rowDiv.classList.add(...this.classes) : 0;
            // Add classes from `tree.types[node.type]`
            if (typeInfo && typeInfo.classes) {
                rowDiv.classList.add(...typeInfo.classes);
            }
            if (expanderSpan) {
                let image = null;
                if (this._isLoading) {
                    image = iconMap.loading;
                }
                else if (this.isExpandable(false)) {
                    if (this.expanded) {
                        image = iconMap.expanderExpanded;
                    }
                    else {
                        image = iconMap.expanderCollapsed;
                    }
                }
                else if (this.lazy && this.children == null) {
                    image = iconMap.expanderLazy;
                }
                if (image == null) {
                    expanderSpan.classList.add("wb-indent");
                }
                else if (TEST_IMG.test(image)) {
                    expanderSpan.style.backgroundImage = `url('${image}')`;
                }
                else {
                    expanderSpan.className = "wb-expander " + image;
                }
            }
            if (checkboxSpan) {
                let cbclass = "wb-checkbox ";
                if (this.isRadio()) {
                    cbclass += "wb-radio ";
                    if (this.selected) {
                        cbclass += iconMap.radioChecked;
                        // } else if (this._partsel) {
                        //   cbclass += iconMap.radioUnknown;
                    }
                    else {
                        cbclass += iconMap.radioUnchecked;
                    }
                }
                else {
                    if (this.selected) {
                        cbclass += iconMap.checkChecked;
                    }
                    else if (this._partsel) {
                        cbclass += iconMap.checkUnknown;
                    }
                    else {
                        cbclass += iconMap.checkUnchecked;
                    }
                }
                checkboxSpan.className = cbclass;
            }
            // Fix active cell in cell-nav mode
            if (!opts.isNew) {
                let i = 0;
                for (const colSpan of rowDiv.children) {
                    colSpan.classList.toggle("wb-active", i++ === tree.activeColIdx);
                    colSpan.classList.remove("wb-error", "wb-invalid");
                }
                // Update icon (if not opts.isNew, which would rebuild markup anyway)
                const iconSpan = nodeElem.querySelector("i.wb-icon");
                if (iconSpan) {
                    this._createIcon(nodeElem, iconSpan, !expanderSpan);
                }
            }
            // Adjust column width
            if (opts.resizeCols !== false && !this.isColspan()) {
                const colElems = rowDiv.querySelectorAll("span.wb-col");
                let idx = 0;
                let ofs = 0;
                for (const colDef of this.tree.columns) {
                    const colElem = colElems[idx];
                    colElem.style.left = `${ofs}px`;
                    colElem.style.width = `${colDef._widthPx}px`;
                    idx++;
                    ofs += colDef._widthPx;
                }
            }
        }
        /*
         * Create or update node's markup.
         *
         * `options.change` defaults to ChangeType.data, which updates the title,
         * icon, and status. It also triggers the `render` event, that lets the user
         * create or update the content of embeded cell elements.
         *
         * If only the status or other class-only modifications have changed,
         * `options.change` should be set to ChangeType.status instead for best
         * efficiency.
         *
         * Calling `update()` is almost always a better alternative.
         * @see {@link WunderbaumNode.update}
         */
        _render(options) {
            // this.log("render", options);
            const opts = Object.assign({ change: ChangeType.data }, options);
            if (!this._rowElem) {
                opts.change = ChangeType.row;
            }
            switch (opts.change) {
                case "status":
                    this._render_status(opts);
                    break;
                case "data":
                    this._render_data(opts);
                    break;
                case "row":
                    // _rowElem is not yet created (asserted in _render_markup)
                    this._render_markup(opts);
                    break;
                default:
                    error(`Invalid change type '${opts.change}'.`);
            }
        }
        /**
         * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad
         * event is triggered on next expand.
         */
        resetLazy() {
            this.removeChildren();
            this.expanded = false;
            this.lazy = true;
            this.children = null;
            this.tree.update(ChangeType.structure);
        }
        /** Convert node (or whole branch) into a plain object.
         *
         * The result is compatible with node.addChildren().
         *
         * @param recursive include child nodes
         * @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 Wunderbaum.toDictArray}.
         */
        toDict(recursive = false, callback) {
            const dict = {};
            NODE_DICT_PROPS.forEach((propName) => {
                const val = this[propName];
                if (val instanceof Set) {
                    // Convert Set to string (or skip if set is empty)
                    val.size
                        ? (dict[propName] = Array.prototype.join.call(val.keys(), " "))
                        : 0;
                }
                else if (val || val === false || val === 0) {
                    dict[propName] = val;
                }
            });
            if (!isEmptyObject(this.data)) {
                dict.data = extend({}, this.data);
                if (isEmptyObject(dict.data)) {
                    delete dict.data;
                }
            }
            if (callback) {
                const res = callback(dict, this);
                if (res === false) {
                    // Note: a return value of `false` is only used internally
                    return false; // Don't include this node nor its children
                }
                if (res === "skip") {
                    recursive = false; // Include this node, but not the children
                }
            }
            if (recursive) {
                if (isArray(this.children)) {
                    dict.children = [];
                    for (let i = 0, l = this.children.length; i < l; i++) {
                        const node = this.children[i];
                        if (!node.isStatusNode()) {
                            // Note: a return value of `false` is only used internally
                            const res = node.toDict(true, callback);
                            if (res !== false) {
                                dict.children.push(res);
                            }
                        }
                    }
                }
            }
            return dict;
        }
        /** Return an option value that has a default, but may be overridden by a
         * callback or a node instance attribute.
         *
         * Evaluation sequence:
         *
         * - If `tree.options.<name>` is a callback that returns something, use that.
         * - Else if `node.<name>` is defined, use that.
         * - Else if `tree.types[<node.type>]` is a value, use that.
         * - Else if `tree.options.<name>` is a value, use that.
         * - Else use `defaultValue`.
         *
         * @param name name of the option property (on node and tree)
         * @param defaultValue return this if nothing else matched
         * {@link Wunderbaum.getOption|Wunderbaum.getOption}
         */
        getOption(name, defaultValue) {
            const tree = this.tree;
            let opts = tree.options;
            // Lookup `name` in options dict
            if (name.indexOf(".") >= 0) {
                [opts, name] = name.split(".");
            }
            const value = opts[name]; // ?? defaultValue;
            // A callback resolver always takes precedence
            if (typeof value === "function") {
                const res = value.call(tree, {
                    type: "resolve",
                    tree: tree,
                    node: this,
                    // typeInfo: this.type ? tree.types[this.type] : {},
                });
                if (res !== undefined) {
                    return res;
                }
            }
            // If this node has an explicit local setting, use it:
            if (this[name] !== undefined) {
                return this[name];
            }
            // Use value from type definition if defined
            const typeInfo = this.type ? tree.types[this.type] : undefined;
            const res = typeInfo ? typeInfo[name] : undefined;
            if (res !== undefined) {
                return res;
            }
            // Use value from value options dict, fallback do default
            return value !== null && value !== void 0 ? value : defaultValue;
        }
        /** Make sure that this node is visible in the viewport.
         * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo}
         */
        async scrollIntoView(options) {
            const opts = Object.assign({ node: this }, options);
            return this.tree.scrollTo(opts);
        }
        /**
         * Activate this node, deactivate previous, send events, activate column and
         * scroll into viewport.
         */
        async setActive(flag = true, options) {
            const tree = this.tree;
            const prev = tree.getActiveNode();
            const retrigger = options === null || options === void 0 ? void 0 : options.retrigger; // Default: false
            const focusTree = options === null || options === void 0 ? void 0 : options.focusTree; // Default: false
            // const focusNode = options?.focusNode !== false; // Default: true
            const noEvents = options === null || options === void 0 ? void 0 : options.noEvents; // Default: false
            const orgEvent = options === null || options === void 0 ? void 0 : options.event; // Default: null
            const colIdx = options === null || options === void 0 ? void 0 : options.colIdx; // Default: null
            const edit = options === null || options === void 0 ? void 0 : options.edit; // Default: false
            // util.assert(!colIdx || tree.isCellNav(), "colIdx requires cellNav");
            assert(!edit || colIdx != null, "edit requires colIdx");
            if (!noEvents) {
                if (flag) {
                    if (prev !== this || retrigger) {
                        if ((prev === null || prev === void 0 ? void 0 : prev._callEvent("deactivate", {
                            nextNode: this,
                            event: orgEvent,
                        })) === false ||
                            this._callEvent("beforeActivate", {
                                prevNode: prev,
                                event: orgEvent,
                            }) === false) {
                            return;
                        }
                        tree._setActiveNode(null);
                        prev === null || prev === void 0 ? void 0 : prev.update(ChangeType.status);
                    }
                }
                else if (prev === this || retrigger) {
                    this._callEvent("deactivate", { nextNode: null, event: orgEvent });
                }
            }
            if (prev !== this) {
                if (flag) {
                    tree._setActiveNode(this);
                }
                prev === null || prev === void 0 ? void 0 : prev.update(ChangeType.status);
                this.update(ChangeType.status);
            }
            return this.makeVisible().then(() => {
                if (flag) {
                    if (focusTree || edit) {
                        tree.setFocus();
                        tree._setFocusNode(this);
                        tree.focusNode.setFocus();
                    }
                    // if (focusNode || edit) {
                    //   tree.focusNode = this;
                    //   tree.focusNode.setFocus();
                    // }
                    if (colIdx != null && tree.isCellNav()) {
                        tree.setColumn(colIdx, { edit: edit });
                    }
                    if (!noEvents) {
                        this._callEvent("activate", { prevNode: prev, event: orgEvent });
                    }
                }
            });
        }
        /**
         * Expand or collapse this node.
         */
        async setExpanded(flag = true, options) {
            const { force, scrollIntoView, immediate, resetLazy } = options !== null && options !== void 0 ? options : {};
            const sendEvents = !(options === null || options === void 0 ? void 0 : options.noEvents); // Default: send events
            if (!flag &&
                this.isExpanded() &&
                this.getLevel() <= this.tree.getOption("minExpandLevel") &&
                !force) {
                this.logDebug("Ignored collapse request below minExpandLevel.");
                return;
            }
            if (!flag === !this.expanded) {
                return; // Nothing to do
            }
            if (sendEvents &&
                this._callEvent("beforeExpand", { flag: flag }) === false) {
                return;
            }
            // this.log("setExpanded()");
            if (flag && this.getOption("autoCollapse")) {
                this.collapseSiblings(options);
            }
            if (flag && this.lazy && this.children == null) {
                await this.loadLazy();
            }
            else if (!flag && resetLazy && this.lazy && this.children) {
                this.resetLazy();
            }
            this.expanded = flag;
            const updateOpts = { immediate: immediate };
            // const updateOpts = { immediate: !!util.getOption(options, "immediate") };
            this.tree.update(ChangeType.structure, updateOpts);
            if (flag && scrollIntoView) {
                const lastChild = this.getLastChild();
                if (lastChild) {
                    this.tree.updatePendingModifications();
                    lastChild.scrollIntoView({ topNode: this });
                }
            }
            if (sendEvents) {
                this._callEvent("expand", { flag: flag });
            }
        }
        /**
         * Set keyboard focus here.
         * @see {@link setActive}
         */
        setFocus(flag = true) {
            assert(!!flag, "Blur is not yet implemented");
            const prev = this.tree.focusNode;
            this.tree._setFocusNode(this);
            prev === null || prev === void 0 ? void 0 : prev.update();
            this.update();
        }
        /** Set a new icon path or class. */
        setIcon(icon) {
            this.icon = icon;
            this.update();
        }
        /** Change node's {@link key} and/or {@link refKey}.  */
        setKey(key, refKey) {
            throw new Error("Not yet implemented");
        }
        /**
         * Trigger a repaint, typically after a status or data change.
         *
         * `change` defaults to 'data', which handles modifcations of title, icon,
         * and column content. It can be reduced to 'ChangeType.status' if only
         * active/focus/selected state has changed.
         *
         * This method will eventually call  {@link WunderbaumNode._render} with
         * default options, but may be more consistent with the tree's
         * {@link Wunderbaum.update} API.
         */
        update(change = ChangeType.data) {
            assert(change === ChangeType.status || change === ChangeType.data, `Invalid change type ${change}`);
            this.tree.update(change, this);
        }
        /**
         * Return an array of selected nodes.
         * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier')
         */
        getSelectedNodes(stopOnParents = false) {
            const nodeList = [];
            this.visit((node) => {
                if (node.selected) {
                    nodeList.push(node);
                    if (stopOnParents === true) {
                        return "skip"; // stop processing this branch
                    }
                }
            });
            return nodeList;
        }
        /** Toggle the check/uncheck state. */
        toggleSelected(options) {
            let flag = this.isSelected();
            if (flag === undefined && !this.isRadio()) {
                flag = this._anySelectable();
            }
            else {
                flag = !flag;
            }
            return this.setSelected(flag, options);
        }
        /** Return true if at least on selectable descendant end-node is unselected. @internal */
        _anySelectable() {
            let found = false;
            this.visit((node) => {
                if (node.selected === false &&
                    !node.unselectable &&
                    !node.hasChildren() &&
                    !node.parent.radiogroup) {
                    found = true;
                    return false; // Stop iteration
                }
            });
            return found;
        }
        /* Apply selection state to a single node. */
        _changeSelectStatusProps(state) {
            let changed = false;
            switch (state) {
                case false:
                    changed = this.selected || this._partsel;
                    this.selected = false;
                    this._partsel = false;
                    break;
                case true:
                    changed = !this.selected || !this._partsel;
                    this.selected = true;
                    this._partsel = true;
                    break;
                case undefined:
                    changed = this.selected || !this._partsel;
                    this.selected = false;
                    // #110: end nodess cannot have a `_partsel` flag
                    this._partsel = this.hasChildren() ? true : false;
                    break;
                default:
                    error(`Invalid state: ${state}`);
            }
            if (changed) {
                this.update();
            }
            return changed;
        }
        /**
         * Fix selection status, after this node was (de)selected in `selectMode: 'hier'`.
         * This includes (de)selecting all descendants.
         */
        fixSelection3AfterClick(opts) {
            const force = !!(opts === null || opts === void 0 ? void 0 : opts.force);
            const flag = this.isSelected();
            this.visit((node) => {
                if (node.radiogroup) {
                    return "skip"; // Don't (de)select this branch
                }
                if (force || !node.getOption("unselectable")) {
                    node._changeSelectStatusProps(flag);
                }
            });
            this.fixSelection3FromEndNodes();
        }
        /**
         * Fix selection status for multi-hier mode.
         * Only end-nodes are considered to update the descendants branch and parents.
         * Should be called after this node has loaded new children or after
         * children have been modified using the API.
         */
        fixSelection3FromEndNodes(opts) {
            const force = !!(opts === null || opts === void 0 ? void 0 : opts.force);
            assert(this.tree.options.selectMode === "hier", "expected selectMode 'hier'");
            // Visit all end nodes and adjust their parent's `selected` and `_partsel`
            // attributes. Return selection state true, false, or undefined.
            const _walk = (node) => {
                let state;
                const children = node.children;
                if (children && children.length) {
                    // check all children recursively
                    let allSelected = true;
                    let someSelected = false;
                    for (let i = 0, l = children.length; i < l; i++) {
                        const child = children[i];
                        // the selection state of a node is not relevant; we need the end-nodes
                        const s = _walk(child);
                        if (s !== false) {
                            someSelected = true;
                        }
                        if (s !== true) {
                            allSelected = false;
                        }
                    }
                    state = allSelected ? true : someSelected ? undefined : false;
                }
                else {
                    // This is an end-node: simply report the status
                    state = !!node.selected;
                }
                // #939: Keep a `_partsel` flag that was explicitly set on a lazy node
                if (node._partsel &&
                    !node.selected &&
                    node.lazy &&
                    node.children == null) {
                    state = undefined;
                }
                if (force || !node.getOption("unselectable")) {
                    node._changeSelectStatusProps(state);
                }
                return state;
            };
            _walk(this);
            // Update parent's state
            this.visitParents((node) => {
                let state;
                const children = node.children;
                let allSelected = true;
                let someSelected = false;
                for (let i = 0, l = children.length; i < l; i++) {
                    const child = children[i];
                    state = !!child.selected;
                    // When fixing the parents, we trust the sibling status (i.e. we don't recurse)
                    if (state || child._partsel) {
                        someSelected = true;
                    }
                    if (!state) {
                        allSelected = false;
                    }
                }
                state = allSelected ? true : someSelected ? undefined : false;
                node._changeSelectStatusProps(state);
            });
        }
        /** Modify the check/uncheck state. */
        setSelected(flag = true, options) {
            const tree = this.tree;
            const sendEvents = !(options === null || options === void 0 ? void 0 : options.noEvents); // Default: send events
            const prev = this.isSelected();
            const isRadio = this.parent && this.parent.radiogroup;
            const selectMode = tree.options.selectMode;
            const canSelect = (options === null || options === void 0 ? void 0 : options.force) || !this.getOption("unselectable");
            flag = !!flag;
            // this.logDebug(`setSelected(${flag})`, this);
            if (!canSelect) {
                return prev;
            }
            if ((options === null || options === void 0 ? void 0 : options.propagateDown) && selectMode === "multi") {
                tree.runWithDeferredUpdate(() => {
                    this.visit((node) => {
                        node.setSelected(flag);
                    });
                });
                return prev;
            }
            if (flag === prev ||
                (sendEvents && this._callEvent("beforeSelect", { flag: flag }) === false)) {
                return prev;
            }
            tree.runWithDeferredUpdate(() => {
                if (isRadio) {
                    // Radiobutton Group
                    if (!flag && !(options === null || options === void 0 ? void 0 : options.force)) {
                        return prev; // don't uncheck radio buttons
                    }
                    for (const sibling of this.parent.children) {
                        sibling.selected = sibling === this;
                    }
                }
                else {
                    this.selected = flag;
                    if (selectMode === "hier") {
                        this.fixSelection3AfterClick();
                    }
                    else if (selectMode === "single") {
                        tree.visit((n) => {
                            n.selected = false;
                        });
                    }
                }
            });
            if (sendEvents) {
                this._callEvent("select", { flag: flag });
            }
            return prev;
        }
        /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */
        setStatus(status, options) {
            const tree = this.tree;
            const message = options === null || options === void 0 ? void 0 : options.message;
            const details = options === null || options === void 0 ? void 0 : options.details;
            let statusNode = null;
            const _clearStatusNode = () => {
                // Remove dedicated dummy node, if any
                const children = this.children;
                if (children && children.length && children[0].isStatusNode()) {
                    children[0].remove();
                }
            };
            const _setStatusNode = (data) => {
                // Create/modify the dedicated dummy node for 'loading...' or
                // 'error!' status. (only called for direct child of the invisible
                // system root)
                const children = this.children;
                const firstChild = children ? children[0] : null;
                assert(data.statusNodeType, "Not a status node");
                assert(!firstChild || !firstChild.isStatusNode(), "Child must not be a status node");
                statusNode = this.addNode(data, "prependChild");
                statusNode.match = -1; // Mark as 'match' to avoid hiding
                tree.update(ChangeType.structure);
                return statusNode;
            };
            _clearStatusNode();
            switch (status) {
                case "ok":
                    this._isLoading = false;
                    this._errorInfo = null;
                    break;
                case "loading":
                    this._isLoading = true;
                    this._errorInfo = null;
                    if (this.parent) {
                        this.update(ChangeType.status);
                    }
                    else {
                        // If this is the invisible root, add a visible top-level node
                        _setStatusNode({
                            statusNodeType: status,
                            title: tree.options.strings.loading +
                                (message ? " (" + message + ")" : ""),
                            checkbox: false,
                            colspan: true,
                            tooltip: details,
                        });
                    }
                    // this.update();
                    break;
                case "error":
                    _setStatusNode({
                        statusNodeType: status,
                        title: tree.options.strings.loadError +
                            (message ? " (" + message + ")" : ""),
                        checkbox: false,
                        colspan: true,
                        // classes: "wb-center",
                        tooltip: details,
                    });
                    this._isLoading = false;
                    this._errorInfo = { message: message, details: details };
                    break;
                case "noData":
                    _setStatusNode({
                        statusNodeType: status,
                        title: message || tree.options.strings.noData,
                        checkbox: false,
                        colspan: true,
                        tooltip: details,
                    });
                    this._isLoading = false;
                    this._errorInfo = null;
                    break;
                default:
                    error("invalid node status " + status);
            }
            tree.update(ChangeType.structure);
            return statusNode;
        }
        /** Rename this node. */
        setTitle(title) {
            this.title = title;
            this.update();
            // this.triggerModify("rename"); // TODO
        }
        /** Set the node tooltip. */
        setTooltip(tooltip) {
            this.tooltip = tooltip;
            this.update();
        }
        _sortChildren(cmp, deep) {
            const cl = this.children;
            if (!cl) {
                return;
            }
            cl.sort(cmp);
            if (deep) {
                for (let i = 0, l = cl.length; i < l; i++) {
                    if (cl[i].children) {
                        cl[i]._sortChildren(cmp, deep);
                    }
                }
            }
        }
        /**
         * Sort child 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
         */
        sortChildren(cmp = nodeTitleSorter, deep = false) {
            this._sortChildren(cmp || nodeTitleSorter, deep);
            this.tree.update(ChangeType.structure);
            // this.triggerModify("sort"); // TODO
        }
        /**
         * Renumber nodes `_nativeIndex`. This is useful to allow to restore the
         * order after sorting a column.
         * This method is automatically called after loading new child nodes.
         * @since 0.11.0
         */
        resetNativeChildOrder(options) {
            const { recursive = true, propName = "_nativeIndex" } = options !== null && options !== void 0 ? options : {};
            if (this.children) {
                this.children.forEach((child, i) => {
                    child.data[propName] = i;
                    if (recursive && child.children) {
                        child.resetNativeChildOrder(options);
                    }
                });
            }
        }
        /**
         * Convenience method to implement column sorting.
         * @since 0.11.0
         */
        sortByProperty(options) {
            var _a, _b, _c;
            const { caseInsensitive = true, deep = true, nativeOrderPropName = "_nativeIndex", updateColInfo = false, } = options;
            let order;
            let colDef;
            if (updateColInfo) {
                colDef = this.tree["_columnsById"][options.colId];
                assert(colDef, `Invalid colId specified: ${options.colId}`);
                order =
                    (_a = options.order) !== null && _a !== void 0 ? _a : rotate(colDef.sortOrder, ["asc", "desc", undefined]);
                for (const col of this.tree.columns) {
                    col.sortOrder = col === colDef ? order : undefined;
                }
                this.tree.update(ChangeType.colStructure);
            }
            else {
                order = (_b = options.order) !== null && _b !== void 0 ? _b : "asc";
            }
            let propName = (_c = options.propName) !== null && _c !== void 0 ? _c : (options.colId || "");
            if (propName === "*") {
                propName = "title";
            }
            if (order == null) {
                propName = nativeOrderPropName;
                order = "asc";
            }
            this.logDebug(`sortByProperty(), propName=${propName}, ${order}`, options);
            assert(propName, "No property name specified");
            const cmp = (a, b) => {
                let av, bv;
                if (NODE_DICT_PROPS.has(propName)) {
                    av = a[propName];
                    bv = b[propName];
                }
                else {
                    av = a.data[propName];
                    bv = b.data[propName];
                }
                if (av == null && bv == null) {
                    return 0;
                }
                if (av == null) {
                    av = typeof bv === "string" ? "" : 0;
                }
                else if (typeof av === "boolean") {
                    av = av ? 1 : 0;
                }
                if (bv == null) {
                    bv = typeof av === "string" ? "" : 0;
                }
                else if (typeof bv === "boolean") {
                    bv = bv ? 1 : 0;
                }
                if (caseInsensitive) {
                    if (typeof av === "string") {
                        av = av.toLowerCase();
                    }
                    if (typeof bv === "string") {
                        bv = bv.toLowerCase();
                    }
                }
                if (order === "desc") {
                    return av === bv ? 0 : av > bv ? -1 : 1;
                }
                return av === bv ? 0 : av > bv ? 1 : -1;
            };
            return this.sortChildren(cmp, deep);
        }
        /**
         * Trigger `modifyChild` event on a parent to signal that a child was modified.
         * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ...
         */
        triggerModifyChild(operation, child, extra) {
            this.logDebug(`modifyChild(${operation})`, extra, child);
            if (!this.tree.options.modifyChild) {
                return;
            }
            if (child && child.parent !== this) {
                error("child " + child + " is not a child of " + this);
            }
            this._callEvent("modifyChild", extend({ operation: operation, child: child }, extra));
        }
        /**
         * Trigger `modifyChild` event on node.parent(!).
         * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ...
         * @param {object} [extra]
         */
        triggerModify(operation, extra) {
            // if (!this.parent) {
            //   return;
            // }
            this.parent.triggerModifyChild(operation, this, extra);
        }
        /**
         * Call `callback(node)` for all descendant nodes in hierarchical order (depth-first, pre-order).
         *
         * Stop iteration, if fn() returns false. Skip current branch, if fn()
         * returns "skip".<br>
         * Return false if iteration was stopped.
         *
         * @param {function} callback the callback function.
         *     Return false to stop iteration, return "skip" to skip this node and
         *     its children only.
         * @see {@link IterableIterator<WunderbaumNode>}, {@link Wunderbaum.visit}.
         */
        visit(callback, includeSelf = false) {
            let res = true;
            const children = this.children;
            if (includeSelf === true) {
                res = callback(this);
                if (res === false || res === "skip") {
                    return res;
                }
            }
            if (children) {
                for (let i = 0, l = children.length; i < l; i++) {
                    res = children[i].visit(callback, true);
                    if (res === false) {
                        break;
                    }
                }
            }
            return res;
        }
        /** Call fn(node) for all parent nodes, bottom-up, including invisible system root.<br>
         * Stop iteration, if callback() returns false.<br>
         * Return false if iteration was stopped.
         *
         * @param callback the callback function. Return false to stop iteration
         */
        visitParents(callback, includeSelf = false) {
            if (includeSelf && callback(this) === false) {
                return false;
            }
            let p = this.parent;
            while (p) {
                if (callback(p) === false) {
                    return false;
                }
                p = p.parent;
            }
            return true;
        }
        /**
         * Call fn(node) for all sibling nodes.<br>
         * Stop iteration, if fn() returns false.<br>
         * Return false if iteration was stopped.
         *
         * @param callback the callback function.
         *     Return false to stop iteration.
         * @param includeSelf include this node in the iteration.
         */
        visitSiblings(callback, includeSelf = false) {
            const ac = this.parent.children;
            for (let i = 0, l = ac.length; i < l; i++) {
                const n = ac[i];
                if (includeSelf || n !== this) {
                    if (callback(n) === false) {
                        return false;
                    }
                }
            }
            return true;
        }
        /**
         * [ext-filter] Return true if this node is matched by current filter (or no filter is active).
         */
        isMatched() {
            return !(this.tree.filterMode && !this.match);
        }
    }
    WunderbaumNode.sequence = 0;

    /*!
     * Wunderbaum - ext-edit
     * 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";
    class EditExtension extends WunderbaumExtension {
        constructor(tree) {
            super(tree, "edit", {
                debounce: 100,
                minlength: 1,
                maxlength: null,
                trigger: [], //["clickActive", "F2", "macEnter"],
                trim: true,
                select: true,
                slowClickDelay: 1000, // Handle 'clickActive' only if last click is less than this old (0: always)
                validity: true, //"Please enter a title",
                // --- Events ---
                // (note: there is also the `tree.change` event.)
                beforeEdit: null,
                edit: null,
                apply: null,
            });
            this.curEditNode = null;
            this.relatedNode = null;
            this.debouncedOnChange = debounce(this._onChange.bind(this), this.getPluginOption("debounce"));
        }
        /*
         * Call an event handler, while marking the current node cell 'busy'.
         * Deal with returned promises and ValidationError.
         * Convert a ValidationError into a input.setCustomValidity() call and vice versa.
         */
        async _applyChange(eventName, node, colElem, inputElem, extra) {
            node.log(`_applyChange(${eventName})`, extra);
            colElem.classList.add("wb-busy");
            colElem.classList.remove("wb-error", "wb-invalid");
            inputElem.setCustomValidity("");
            // Call event handler either ('change' or 'edit.appy'), which may return a
            // promise or a scalar value or throw a ValidationError.
            return new Promise((resolve, reject) => {
                const res = node._callEvent(eventName, extra);
                // normalize to promise, even if a scalar value was returned and await it
                Promise.resolve(res)
                    .then((res) => {
                    resolve(res);
                })
                    .catch((err) => {
                    reject(err);
                });
            })
                .then((res) => {
                if (!inputElem.checkValidity()) {
                    // Native validation failed or handler called 'inputElem.setCustomValidity()'
                    node.logWarn("inputElem.checkValidity() failed: throwing....");
                    throw new ValidationError(inputElem.validationMessage);
                }
                return res;
            })
                .catch((err) => {
                if (err instanceof ValidationError) {
                    node.logWarn("catched ", err);
                    colElem.classList.add("wb-invalid");
                    if (inputElem.setCustomValidity && !inputElem.validationMessage) {
                        inputElem.setCustomValidity(err.message);
                    }
                    if (inputElem.validationMessage) {
                        inputElem.reportValidity();
                    }
                    // throw err;
                }
                else {
                    node.logError(`Error in ${eventName} event handler (throw e.util.ValidationError instead if this was intended)`, err);
                    colElem.classList.add("wb-error");
                    throw err;
                }
            })
                .finally(() => {
                colElem.classList.remove("wb-busy");
            });
        }
        /*
         * Called for when a control that is embedded in a cell fires a `change` event.
         */
        _onChange(e) {
            const info = Wunderbaum.getEventInfo(e);
            const node = info.node;
            const colElem = info.colElem;
            if (!node || info.colIdx === 0) {
                this.tree.log("Ignored change event for removed element or node title");
                return;
            }
            // See also WbChangeEventType
            this._applyChange("change", node, colElem, e.target, {
                info: info,
                event: e,
                inputElem: e.target,
                inputValue: Wunderbaum.util.getValueFromElem(e.target),
                inputValid: e.target.checkValidity(),
            });
        }
        init() {
            super.init();
            onEvent(this.tree.element, "change", //"change input",
            ".contenteditable,input,textarea,select", 
            // #61: we must not debounce the `change`, event.target may be reset to null
            // when the debounced handler is called.
            // (e) => {
            //   this.debouncedOnChange(e);
            // }
            (e) => this._onChange(e));
        }
        /* Called by ext_keynav to pre-process input. */
        _preprocessKeyEvent(data) {
            const event = data.event;
            const eventName = eventToString(event);
            const tree = this.tree;
            const trigger = this.getPluginOption("trigger");
            // const inputElem =
            //   event.target && event.target.closest("input,[contenteditable]");
            // tree.logDebug(`_preprocessKeyEvent: ${eventName}, editing:${this.isEditingTitle()}`);
            // --- Title editing: apply/discard ---
            // if (inputElem) {
            if (this.isEditingTitle()) {
                switch (eventName) {
                    case "Enter":
                        this._stopEditTitle(true, { event: event });
                        return false;
                    case "Escape":
                        this._stopEditTitle(false, { event: event });
                        return false;
                }
                // If the event target is an input element or `contenteditable="true"`,
                // we ignore it as navigation command
                return false;
            }
            // --- Trigger title editing
            if (tree.isRowNav() || tree.activeColIdx === 0) {
                switch (eventName) {
                    case "Enter":
                        if (trigger.indexOf("macEnter") >= 0 && isMac) {
                            this.startEditTitle();
                            return false;
                        }
                        break;
                    case "F2":
                        if (trigger.indexOf("F2") >= 0) {
                            this.startEditTitle();
                            return false;
                        }
                        break;
                }
                return true;
            }
            return true;
        }
        /** Return true if a title is currently being edited. */
        isEditingTitle(node) {
            return node ? this.curEditNode === node : !!this.curEditNode;
        }
        /** Start renaming, i.e. replace the title with an embedded `<input>`. */
        startEditTitle(node) {
            node = node !== null && node !== void 0 ? node : this.tree.getActiveNode();
            const validity = this.getPluginOption("validity");
            const select = this.getPluginOption("select");
            if (!node) {
                return;
            }
            if (node.isStatusNode()) {
                node.logWarn("Cannot edit status node.");
                return;
            }
            this.tree.logDebug(`startEditTitle(node=${node})`);
            let inputHtml = node._callEvent("edit.beforeEdit");
            if (inputHtml === false) {
                node.logDebug("beforeEdit canceled operation.");
                return;
            }
            // `beforeEdit(e)` may return an input HTML string. Otherwise use a default
            // (we also treat a `true` return value as 'use default'):
            if (inputHtml === true || !inputHtml) {
                const title = escapeHtml(node.title);
                let opt = this.getPluginOption("maxlength");
                const maxlength = opt ? ` maxlength="${opt}"` : "";
                opt = this.getPluginOption("minlength");
                const minlength = opt ? ` minlength="${opt}"` : "";
                const required = opt > 0 ? " required" : "";
                inputHtml =
                    `<input type=text class="wb-input-edit" tabindex=-1 value="${title}" ` +
                        `autocorrect="off"${required}${minlength}${maxlength} >`;
            }
            const titleSpan = node
                .getColElem(0)
                .querySelector(".wb-title");
            titleSpan.innerHTML = inputHtml;
            const inputElem = titleSpan.firstElementChild;
            if (validity) {
                // Permanently apply input validations (CSS and tooltip)
                inputElem.addEventListener("keydown", (e) => {
                    inputElem.setCustomValidity("");
                    if (!inputElem.reportValidity()) {
                        node.logWarn(`Invalid input: '${inputElem.value}'`);
                    }
                });
            }
            inputElem.focus();
            if (select) {
                inputElem.select();
            }
            this.curEditNode = node;
            node._callEvent("edit.edit", {
                inputElem: inputElem,
            });
        }
        /**
         *
         * @param apply
         * @returns
         */
        stopEditTitle(apply) {
            return this._stopEditTitle(apply, {});
        }
        /*
         *
         * @param apply
         * @param opts.canKeepOpen
         */
        _stopEditTitle(apply, options) {
            var _a;
            options !== null && options !== void 0 ? options : (options = {});
            const focusElem = document.activeElement;
            let newValue = focusElem ? getValueFromElem(focusElem) : null;
            const node = this.curEditNode;
            const forceClose = !!options.forceClose;
            const validity = this.getPluginOption("validity");
            if (newValue && this.getPluginOption("trim")) {
                newValue = newValue.trim();
            }
            if (!node) {
                this.tree.logDebug("stopEditTitle: not in edit mode.");
                return;
            }
            node.logDebug(`stopEditTitle(${apply})`, options, focusElem, newValue);
            if (apply && newValue !== null && newValue !== node.title) {
                const errMsg = focusElem.validationMessage;
                if (errMsg) {
                    // input element's native validation failed
                    throw new Error(`Input validation failed for "${newValue}": ${errMsg}.`);
                }
                const colElem = node.getColElem(0);
                this._applyChange("edit.apply", node, colElem, focusElem, {
                    oldValue: node.title,
                    newValue: newValue,
                    inputElem: focusElem,
                    inputValid: focusElem.checkValidity(),
                }).then((value) => {
                    var _a;
                    const errMsg = focusElem.validationMessage;
                    if (validity && errMsg && value !== false) {
                        // Handler called 'inputElem.setCustomValidity()' to signal error
                        throw new Error(`Edit apply validation failed for "${newValue}": ${errMsg}.`);
                    }
                    // Discard the embedded `<input>`
                    // node.logDebug("applyChange:", value, forceClose)
                    if (!forceClose && value === false) {
                        // Keep open
                        return;
                    }
                    node === null || node === void 0 ? void 0 : node.setTitle(newValue);
                    // NOTE: At least on Safari, this render call triggers a scroll event
                    // probably because the focused input is replaced.
                    (_a = this.curEditNode) === null || _a === void 0 ? void 0 : _a._render({ preventScroll: true });
                    this.curEditNode = null;
                    this.relatedNode = null;
                    this.tree.setFocus(); // restore focus that was in the input element
                });
                // .catch((err) => {
                //   node.logError(err);
                // });
                // Trigger 'change' event for embedded `<input>`
                // focusElem.blur();
            }
            else {
                // Discard the embedded `<input>`
                // NOTE: At least on Safari, this render call triggers a scroll event
                // probably because the focused input is replaced.
                (_a = this.curEditNode) === null || _a === void 0 ? void 0 : _a._render({ preventScroll: true });
                this.curEditNode = null;
                this.relatedNode = null;
                // We discarded the <input>, so we have to acquire keyboard focus again
                this.tree.setFocus();
            }
        }
        /**
         * Create a new child or sibling node and start edit mode.
         */
        createNode(mode = "after", node, init) {
            const tree = this.tree;
            node = node !== null && node !== void 0 ? node : tree.getActiveNode();
            assert(node, "No node was passed, or no node is currently active.");
            // const validity = this.getPluginOption("validity");
            mode = mode || "prependChild";
            if (init == null) {
                init = { title: "" };
            }
            else if (typeof init === "string") {
                init = { title: init };
            }
            else {
                assert(isPlainObject(init), `Expected a plain object: ${init}`);
            }
            // Make sure node is expanded (and loaded) in 'child' mode
            if ((mode === "prependChild" || mode === "appendChild") &&
                (node === null || node === void 0 ? void 0 : node.isExpandable(true))) {
                node.setExpanded().then(() => {
                    this.createNode(mode, node, init);
                });
                return;
            }
            const newNode = node.addNode(init, mode);
            newNode.setClass("wb-edit-new");
            this.relatedNode = node;
            // Don't filter new nodes:
            newNode.match = -1;
            newNode.makeVisible({ noAnimation: true }).then(() => {
                this.startEditTitle(newNode);
            });
        }
    }

    /*!
     * wunderbaum.ts
     *
     * A treegrid control.
     *
     * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de).
     * https://github.com/mar10/wunderbaum
     *
     * Released under the MIT license.
     * @version v0.13.0
     * @date Sat, 08 Mar 2025 14:16:31 GMT
     */
    // import "./wunderbaum.scss";
    class WbSystemRoot extends WunderbaumNode {
        constructor(tree) {
            super(tree, null, {
                key: "__root__",
                title: tree.id,
            });
        }
        toString() {
            return `WbSystemRoot@${this.key}<'${this.tree.id}'>`;
        }
    }
    /**
     * A persistent plain object or array.
     *
     * See also {@link WunderbaumOptions}.
     */
    class Wunderbaum {
        /** Currently active node if any.
         * Use {@link WunderbaumNode.setActive|setActive} to modify.
         */
        get activeNode() {
            var _a;
            // Check for deleted node, i.e. node.tree === null
            return ((_a = this._activeNode) === null || _a === void 0 ? void 0 : _a.tree) ? this._activeNode : null;
        }
        /** Current node hat has keyboard focus if any.
         * Use {@link WunderbaumNode.setFocus|setFocus()} to modify.
         */
        get focusNode() {
            var _a;
            // Check for deleted node, i.e. node.tree === null
            return ((_a = this._focusNode) === null || _a === void 0 ? void 0 : _a.tree) ? this._focusNode : null;
        }
        constructor(options) {
            this.enabled = true;
            /** Contains additional data that was sent as response to an Ajax source load request. */
            this.data = {};
            this.extensionList = [];
            this.extensions = {};
            this.keyMap = new Map();
            this.refKeyMap = new Map();
            this.treeRowCount = 0;
            this._disableUpdateCount = 0;
            this._disableUpdateIgnoreCount = 0;
            this._activeNode = null;
            this._focusNode = null;
            /** Shared properties, referenced by `node.type`. */
            this.types = {};
            /** List of column definitions. */
            this.columns = []; // any[] = [];
            this._columnsById = {};
            // Modification Status
            this.pendingChangeTypes = new Set();
            /** Expose some useful methods of the util.ts module as `tree._util`. */
            this._util = util;
            // --- SELECT ---
            // /** @internal */
            // public selectRangeAnchor: WunderbaumNode | null = null;
            // --- BREADCRUMB ---
            /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
            this.breadcrumb = null;
            // --- FILTER ---
            /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */
            this.filterMode = null;
            // --- KEYNAV ---
            /** @internal Use `setColumn()`/`getActiveColElem()` to access. */
            this.activeColIdx = 0;
            /** @internal */
            this._cellNavMode = false;
            /** @internal */
            this.lastQuicksearchTime = 0;
            /** @internal */
            this.lastQuicksearchTerm = "";
            // --- EDIT ---
            this.lastClickTime = 0;
            const opts = (this.options = extend({
                id: null,
                source: null, // URL for GET/PUT, Ajax options, or callback
                element: null, // <div class="wunderbaum">
                debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
                header: null, // Show/hide header (pass bool or string)
                // headerHeightPx: ROW_HEIGHT,
                rowHeightPx: DEFAULT_ROW_HEIGHT,
                iconMap: "bootstrap",
                columns: null,
                types: null,
                // escapeTitles: true,
                enabled: true,
                fixedCol: false,
                showSpinner: false,
                checkbox: false,
                minExpandLevel: 0,
                emptyChildListExpandable: false,
                // updateThrottleWait: 200,
                skeleton: false,
                connectTopBreadcrumb: null,
                selectMode: "multi", // SelectModeType
                // --- KeyNav ---
                navigationModeOption: null, // NavModeEnum,
                quicksearch: true,
                // --- Events ---
                iconBadge: null,
                change: null,
                // enhanceTitle: null,
                error: null,
                receive: null,
                // --- Strings ---
                strings: {
                    loadError: "Error",
                    loading: "Loading...",
                    noData: "No data",
                    breadcrumbDelimiter: " » ",
                    queryResult: "Found ${matches} of ${count}",
                    noMatch: "No results",
                    matchIndex: "${match} of ${matches}",
                },
            }, options));
            const readyDeferred = new Deferred();
            this.ready = readyDeferred.promise();
            let readyOk = false;
            this.ready
                .then(() => {
                readyOk = true;
                try {
                    this._callEvent("init");
                }
                catch (error) {
                    // We re-raise in the reject handler, but Chrome resets the stack
                    // frame then, so we log it here:
                    this.logError("Exception inside `init(e)` event:", error);
                }
            })
                .catch((err) => {
                if (readyOk) {
                    // Error occurred in `init` handler. We can re-raise, but Chrome
                    // resets the stack frame.
                    throw err;
                }
                else {
                    // Error in load process
                    this._callEvent("init", { error: err });
                }
            });
            this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
            this.root = new WbSystemRoot(this);
            this._registerExtension(new KeynavExtension(this));
            this._registerExtension(new EditExtension(this));
            this._registerExtension(new FilterExtension(this));
            this._registerExtension(new DndExtension(this));
            this._registerExtension(new GridExtension(this));
            this._registerExtension(new LoggerExtension(this));
            this._updateViewportThrottled = adaptiveThrottle(this._updateViewportImmediately.bind(this), {});
            // --- Evaluate options
            this.columns = opts.columns;
            delete opts.columns;
            if (!this.columns || !this.columns.length) {
                const title = typeof opts.header === "string" ? opts.header : this.id;
                this.columns = [{ id: "*", title: title, width: "*" }];
            }
            if (opts.types) {
                this.setTypes(opts.types, true);
            }
            delete opts.types;
            // --- Create Markup
            this.element = elemFromSelector(opts.element);
            assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
            this.element.classList.add("wunderbaum");
            if (!this.element.getAttribute("tabindex")) {
                this.element.tabIndex = 0;
            }
            if (opts.rowHeightPx !== DEFAULT_ROW_HEIGHT) {
                this.element.style.setProperty("--wb-row-outer-height", opts.rowHeightPx + "px");
                this.element.style.setProperty("--wb-row-inner-height", opts.rowHeightPx - 2 + "px");
            }
            // Attach tree instance to <div>
            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 = "<span class='wb-col'></span>".repeat(this.columns.length);
                this.element.innerHTML = `
        <div class='wb-header'>
          <div class='wb-row'>
            ${coldivs}
          </div>
        </div>`;
                if (!wantHeader) {
                    const he = this.element.querySelector("div.wb-header");
                    he.style.display = "none";
                }
            }
            //
            this.element.innerHTML += `
      <div class="wb-list-container">
        <div class="wb-node-list"></div>
      </div>`;
            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 (opts.source) {
                if (opts.showSpinner) {
                    this.nodeListElement.innerHTML = `<progress class='spinner'>${opts.strings.loading}</progress>`;
                }
                this.load(opts.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.element.addEventListener("scroll", (e) => {
                // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e);
                this.update(ChangeType.scroll);
            });
            this.resizeObserver = new ResizeObserver((entries) => {
                // this.log("ResizeObserver: Size changed", entries);
                this.update(ChangeType.resize);
            });
            this.resizeObserver.observe(this.element);
            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._selectRange(info) === false) {
                //   return;
                // }
                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.colIdx >= 0) {
                        node.setActive(true, { colIdx: info.colIdx, event: e });
                    }
                    else {
                        node.setActive(true, { event: e });
                    }
                    if (info.region === NodeRegion.expander) {
                        node.setExpanded(!node.isExpanded(), {
                            scrollIntoView: options.scrollIntoViewOnExpandClick !== false,
                        });
                    }
                    else if (info.region === NodeRegion.checkbox) {
                        node.toggleSelected();
                    }
                }
                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 (opts.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.
         */
        get iconMap() {
            const map = this.options.iconMap;
            if (typeof map === "string") {
                return iconMaps[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();
            }
        }
        /** Add node to tree's bookkeeping data structures. */
        _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. */
        _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 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
         * tree.runWithDeferredUpdate(() => {
         *   return someFuncThatWouldUpdateManyNodes();
         * });
         * ```
         */
        runWithDeferredUpdate(func, hint = null) {
            try {
                this.enableUpdate(false);
                const res = func();
                assert(!(res instanceof Promise), `Promise return not allowed: ${res}`);
                return res;
            }
            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 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);
        }
        /**
         * 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 Wunderbaum.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 Wunderbaum.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 Wunderbaum.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 Wunderbaum.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
            }
        }
        /** 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. <br>
         * 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;
            let expandedKeys = undefined;
            if (options.expandedKeys !== false) {
                expandedKeys = [];
                for (const node of this) {
                    if (node.expanded) {
                        expandedKeys.push(node.key);
                    }
                }
            }
            const state = {
                activeKey: (_b = (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null,
                activeColIdx: this.activeColIdx,
                selectedKeys: options.selectedKeys === false
                    ? undefined
                    : this.getSelectedNodes().flatMap((n) => n.key),
                expandedKeys: expandedKeys,
            };
            return state;
        }
        /** Apply selection/expansion/activation status. @experimental */
        setState(state, options) {
            this.runWithDeferredUpdate(() => {
                var _a, _b;
                if (state.selectedKeys) {
                    this.selectAll(false);
                    for (const key of state.selectedKeys) {
                        (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setSelected(true);
                    }
                }
                if (state.expandedKeys) {
                    for (const key of state.expandedKeys) {
                        (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setExpanded(true);
                    }
                }
                if (state.activeKey) {
                    this.setActiveNode(state.activeKey);
                }
                if (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
         */
        sortChildren(cmp = nodeTitleSorter, deep = false) {
            this.root.sortChildren(cmp, deep);
        }
        /**
         * Convenience method to implement column sorting.
         * @see {@link WunderbaumNode.sortByProperty}.
         * @since 0.11.0
         */
        sortByProperty(options) {
            this.root.sortByProperty(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. "<num>px" or <int>).`);
                }
            }
            // 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 = "<span class='wb-col'></span>".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 `<div.wb-col>` 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 = `<i data-command=menu class="wb-col-icon ${iconClass}"></i>`;
                    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 = `<i data-command=sort class="wb-col-icon ${iconClass}"></i>`;
                    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 = `<i data-command=filter class="wb-col-icon ${iconClass}"></i>`;
                    addMarkup += icon;
                }
                // Add resizer to all but the last column
                if (i < colCount - 1) {
                    if (toBool(col.resizable, this.options.columnsResizable, false)) {
                        addMarkup +=
                            '<span class="wb-col-resizer wb-col-resizer-active"></span>';
                    }
                    else {
                        addMarkup += '<span class="wb-col-resizer"></span>';
                    }
                }
                // Create column header
                const title = escapeHtml(col.title || col.id);
                colElem.innerHTML = `<span class="wb-col-title"${tooltip}>${title}</span>${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 (icon.indexOf("<") >= 0) {
                // HTML
                iconElem = elemFromHtml(icon);
            }
            else if (TEST_IMG.test(icon)) {
                // Image URL
                iconElem = elemFromHtml(`<i class="wb-icon" style="background-image: url('${icon}');">`);
            }
            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(`<span class="wb-badge${classes}"${tooltip}>${escapeHtml(badge)}</span>`);
                }
                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 = "&nbsp;";
            }
        }
        /**
         * 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 = (<any>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 {@link IterableIterator<WunderbaumNode>}, {@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.<br>
         * 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.
         */
        load(source) {
            this.clear();
            return this.root.load(source);
        }
        /**
         * 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.13.0"; // Set to semver by 'grunt release'
    /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
    Wunderbaum.util = util;

    exports.Wunderbaum = Wunderbaum;

}));
