/*! * Wunderbaum - util * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ /** @module util */ import { DebouncedFunction, debounce, throttle } from "./debounce"; export { debounce, throttle }; /** Readable names for `MouseEvent.button` */ export const MOUSE_BUTTONS: { [key: number]: string } = { 0: "", 1: "left", 2: "middle", 3: "right", 4: "back", 5: "forward", }; export const MAX_INT = 9007199254740991; const userInfo = _getUserInfo(); /**True if the client is using a macOS platform. */ export const isMac = userInfo.isMac; const REX_HTML = /[&<>"'/]/g; // Escape those characters const REX_TOOLTIP = /[<>"'/]/g; // Don't escape `&` in tooltips const ENTITY_MAP: { [key: string]: string } = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/", }; export type NotPromise = T extends Promise ? never : T; export type FunctionType = (...args: any[]) => any; export type EventCallbackType = (e: Event) => boolean | void; type PromiseCallbackType = (val: any) => void; /** 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. */ export class ValidationError extends Error { constructor(message: string) { 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. */ export class Deferred { private thens: PromiseCallbackType[] = []; private catches: PromiseCallbackType[] = []; private status = ""; private resolvedValue: any; private rejectedError: any; constructor() {} resolve(value?: any) { 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?: any) { 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: any) { if (status === "resolved") { cb(this.resolvedValue); } else { this.thens.unshift(cb); } } catch(cb: any) { 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. */ export function assert(cond: any, msg: string) { 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. */ export function documentReady(callback: () => void): void { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", callback); } else { callback(); } } /** Resolve when document was loaded. */ export function documentReadyPromise(): Promise { 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. */ export function each( obj: any, callback: (index: number | string, item: any) => void | boolean ): any { 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)`. */ export function error(msg: string) { throw new Error(msg); } /** Convert `<`, `>`, `&`, `"`, `'`, and `/` to the equivalent entities. */ export function escapeHtml(s: string): string { 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. `"$"` -> `"\$"`) */ export function escapeRegex(s: string) { return ("" + s).replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); } /** Convert `<`, `>`, `"`, `'`, and `/` (but not `&`) to the equivalent entities. */ export function escapeTooltip(s: string): string { return ("" + s).replace(REX_TOOLTIP, function (s) { return ENTITY_MAP[s]; }); } /** TODO */ export function extractHtmlText(s: string) { if (s.indexOf(">") >= 0) { error("Not implemented"); // return $("
").html(s).text(); } return s; } /** * Read the value from an HTML input element. * * If a `` is passed, the first child input is used. * Depending on the target element type, `value` is interpreted accordingly. * For example for a checkbox, a value of true, false, or null is returned if * the element is checked, unchecked, or indeterminate. * For datetime input control a numerical value is assumed, etc. * * Common use case: store the new user input in a `change` event handler: * * ```ts * change: (e) => { * const tree = e.tree; * const node = e.node; * // Read the value from the input control that triggered the change event: * let value = tree.getValueFromElem(e.element); * // and store it to the node model (assuming the column id matches the property name) * node.data[e.info.colId] = value; * }, * ``` * @param elem `` or `` or `