@trunkjs/browser-utils 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,51 @@
1
+ ## 1.0.12 (2025-08-11)
2
+
3
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
4
+
5
+ ## 1.0.11 (2025-08-11)
6
+
7
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
8
+
9
+ ## 1.0.10 (2025-08-11)
10
+
11
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
12
+
13
+ ## 1.0.9 (2025-08-11)
14
+
15
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
16
+
17
+ ## 1.0.8 (2025-08-11)
18
+
19
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
20
+
21
+ ## 1.0.7 (2025-08-07)
22
+
23
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
24
+
25
+ ## 1.0.6 (2025-08-07)
26
+
27
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
28
+
29
+ ## 1.0.5 (2025-08-07)
30
+
31
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
32
+
33
+ ## 1.0.4 (2025-08-07)
34
+
35
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
36
+
37
+ ## 1.0.3 (2025-08-07)
38
+
39
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
40
+
41
+ ## 1.0.2 (2025-08-07)
42
+
43
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
44
+
45
+ ## 1.0.1 (2025-08-07)
46
+
47
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
48
+
49
+ # 1.0.0 (2025-08-07)
50
+
51
+ This was a version bump only for browser-utils to align it with other projects, there were no code changes.
package/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # @trunkjs/browser-utils
2
+
3
+ A small collection of browser-focused, framework-agnostic utilities that make common DOM and timing tasks easier. All utilities are written in TypeScript and ship as ES modules.
4
+
5
+ Exports:
6
+ - create_element: Minimal DOM element factory
7
+ - Debouncer: Debounce helper with optional max-delay
8
+ - Stopwatch: Lightweight performance timer with lap logging
9
+ - waitFor, waitForDomContentLoaded, waitForLoad, sleep, waitForAnimationEnd: Promise-based event and timing helpers
10
+ - LoggingMixin: Lightweight logging mixin for Custom Elements
11
+
12
+ ## Installation
13
+
14
+ Within this monorepo, the package is consumed via path aliases. If you publish or consume it externally, import from the package name:
15
+
16
+ ```ts
17
+ import {
18
+ create_element,
19
+ Debouncer,
20
+ Stopwatch,
21
+ waitFor,
22
+ waitForDomContentLoaded,
23
+ waitForLoad,
24
+ sleep,
25
+ waitForAnimationEnd,
26
+ LoggingMixin,
27
+ } from '@trunkjs/browser-utils';
28
+ ```
29
+
30
+ Note: Several utilities depend on browser globals (window, document, performance). Use them in browser-like environments.
31
+
32
+ ## Quick start
33
+
34
+ ### Create and append an element
35
+
36
+ ```ts
37
+ import { create_element } from '@trunkjs/browser-utils';
38
+
39
+ const card = create_element('section', { class: 'card', 'aria-live': 'polite' }, [
40
+ create_element('h2', {}, 'Title'),
41
+ create_element('p', {}, 'This is a paragraph.'),
42
+ create_element('button', { disabled: true }, 'Disabled'),
43
+ ]);
44
+
45
+ document.body.appendChild(card);
46
+ ```
47
+
48
+ Notes:
49
+ - Attributes with value true create boolean attributes (e.g. disabled becomes disabled="").
50
+ - Attributes with null or undefined are omitted.
51
+
52
+ ### Debounce input handling
53
+
54
+ ```ts
55
+ import { Debouncer } from '@trunkjs/browser-utils';
56
+
57
+ const debouncer = new Debouncer(300, 2000); // 300ms debounce, 2s max delay
58
+ const input = document.querySelector('input[type="search"]')!;
59
+
60
+ input.addEventListener('input', async () => {
61
+ await debouncer.wait(); // resets while typing; fires within 2s max
62
+ // Trigger the expensive operation here
63
+ console.log('Searching for:', input.value);
64
+ });
65
+ ```
66
+
67
+ Alternatively, execute a function directly:
68
+
69
+ ```ts
70
+ const saveDebouncer = new Debouncer(500);
71
+ window.addEventListener('resize', () => {
72
+ saveDebouncer.debounce(() => {
73
+ console.log('Resized!');
74
+ });
75
+ });
76
+ ```
77
+
78
+ ### Measure performance with Stopwatch
79
+
80
+ ```ts
81
+ import { Stopwatch } from '@trunkjs/browser-utils';
82
+
83
+ const sw = new Stopwatch('Render');
84
+ // ... do stuff
85
+ sw.lap('after step 1');
86
+ // ... do more stuff
87
+ sw.lap('after step 2');
88
+ console.debug('Total ms:', sw.stop());
89
+ ```
90
+
91
+ ### Enable conditional logging in custom elements (LoggingMixin)
92
+
93
+ ```ts
94
+ import { LoggingMixin } from '@trunkjs/browser-utils';
95
+
96
+ class MyEl extends LoggingMixin(HTMLElement) {
97
+ connectedCallback() {
98
+ // Only prints if the element has a truthy debug attribute
99
+ this.log('connected');
100
+ // Always prints
101
+ this.warn('Heads up');
102
+ this.error('Something went wrong?');
103
+ }
104
+ }
105
+
106
+ customElements.define('my-el', MyEl);
107
+
108
+ // <my-el debug></my-el> // enables debug logging
109
+ // <my-el debug="false"></my-el> // disables debug logging
110
+ // <my-el></my-el> // debug logging disabled by default
111
+ ```
112
+
113
+ Tip:
114
+ - If you toggle the debug attribute at runtime, call el.invalidateDebugCache() so the mixin re-evaluates the attribute on the next log/warn/error call.
115
+
116
+ ### Promise-based event helpers
117
+
118
+ ```ts
119
+ import { waitFor, waitForDomContentLoaded, waitForLoad, sleep, waitForAnimationEnd } from '@trunkjs/browser-utils';
120
+
121
+ await waitForDomContentLoaded(); // resolves when DOM is ready
122
+ await waitForLoad(); // resolves when all resources are loaded
123
+
124
+ // Wait for a specific event
125
+ const clickEvent = await waitFor<MouseEvent>(document.getElementById('btn')!, 'click');
126
+
127
+ // Pause execution
128
+ await sleep(250);
129
+
130
+ // Wait for CSS animation to finish
131
+ await waitForAnimationEnd(document.querySelector('.animate')!);
132
+ ```
133
+
134
+ ## API Reference
135
+
136
+ ### create_element(tag, attrs?, children?)
137
+
138
+ - Signature:
139
+ - create_element(tag: string, attrs?: Record<string, string | true | null | undefined>, children?: (Node | string)[] | string | Node): HTMLElement
140
+ - Behavior:
141
+ - Creates an element, sets provided attributes, and appends children.
142
+ - children can be a single Node/string or an array.
143
+ - Attribute values:
144
+ - true → renders as a boolean attribute with an empty value (e.g. disabled="")
145
+ - null/undefined → attribute is omitted
146
+ - string → sets the attribute to the given string
147
+
148
+ Example:
149
+ ```ts
150
+ create_element('input', { type: 'checkbox', checked: true });
151
+ ```
152
+
153
+ ### class Debouncer
154
+
155
+ - constructor(delay: number, max_delay: number | false = false)
156
+ - delay: debounce window in ms
157
+ - max_delay: maximum time a call may be deferred. If false, no max is applied.
158
+ - wait(): Promise<true>
159
+ - Returns a promise that resolves after delay ms since the last call.
160
+ - If max_delay is set and exceeded, the pending timer is not cleared and will resolve soon.
161
+ - debounce(callback: () => void): void
162
+ - Schedules callback to run after delay ms since the last call.
163
+
164
+ Use cases:
165
+ - User input throttling (search boxes)
166
+ - Resize or scroll handlers
167
+ - Preventing excessive network calls
168
+
169
+ ### class Stopwatch
170
+
171
+ - constructor(label: string, enabled: boolean = true)
172
+ - lap(msg?: string): void
173
+ - Logs a lap line to console.debug in format: [label] msg +Xs
174
+ - elapsed(): number
175
+ - Milliseconds since start
176
+ - reset(): void
177
+ - stop(): number
178
+ - Stops the stopwatch and returns elapsed ms
179
+ - start(): void
180
+ - Starts (or restarts) and resets timings
181
+ - isRunning(): boolean
182
+
183
+ Notes:
184
+ - Uses performance.now() for high-resolution timing
185
+ - If enabled is false, lap is a no-op
186
+
187
+ ### LoggingMixin(Base)
188
+
189
+ - Signature:
190
+ - LoggingMixin<TBase extends abstract new (...args: any[]) => object>(Base: TBase): class extends Base
191
+ - Adds methods and properties to your Custom Element base class:
192
+ - log(...args: any[]): void
193
+ - Logs to console.log only when debug is enabled.
194
+ - warn(...args: any[]): void
195
+ - Always logs to console.warn (independent of debug).
196
+ - error(...args: any[]): void
197
+ - Always logs to console.error (independent of debug).
198
+ - get _debug(): boolean
199
+ - Indicates whether debug logging is enabled for the instance.
200
+ - invalidateDebugCache(): void
201
+ - Clears the cached debug flag. Call this after changing the debug attribute dynamically.
202
+
203
+ How debug is determined:
204
+ - On first call to log/warn/error, the mixin checks the element’s debug attribute and caches the result.
205
+ - The attribute is considered truthy if present and not one of: "false", "0", "off", "no".
206
+ - Examples:
207
+ - <my-el debug></my-el> → debug ON
208
+ - <my-el debug="true"></my-el> → debug ON
209
+ - <my-el debug="false"></my-el> → debug OFF
210
+ - <my-el></my-el> → debug OFF
211
+ - If you toggle the attribute at runtime, call invalidateDebugCache() so the next call re-evaluates it.
212
+
213
+ Usage:
214
+ ```ts
215
+ class MyEl extends LoggingMixin(HTMLElement) {
216
+ connectedCallback() {
217
+ this.log('connected'); // prints only if debug is ON
218
+ this.warn('warning'); // always prints
219
+ this.error('error'); // always prints
220
+ }
221
+ }
222
+ ```
223
+
224
+ ### waitFor<T>(target, eventName, options?)
225
+
226
+ - Signature:
227
+ - waitFor<T>(target: EventTarget, eventName: string, options?: AddEventListenerOptions): Promise<T>
228
+ - Resolves with the event object T upon first occurrence.
229
+
230
+ ### waitForDomContentLoaded()
231
+
232
+ - Resolves once the DOM is ready (DOMContentLoaded).
233
+ - If the document is already past loading, resolves immediately.
234
+
235
+ ### waitForLoad()
236
+
237
+ - Resolves once the window load event fires.
238
+ - If the document is already complete, resolves immediately.
239
+
240
+ ### sleep(ms)
241
+
242
+ - Signature: sleep(ms: number): Promise<void>
243
+ - Resolves after the given delay.
244
+
245
+ ### waitForAnimationEnd(element)
246
+
247
+ - Signature: waitForAnimationEnd(element: HTMLElement): Promise<AnimationEvent>
248
+ - Resolves when the element fires animationend.
249
+
250
+ ## Building
251
+
252
+ Run `nx build browser-utils` to build the library.
253
+
254
+ The library is configured for:
255
+ - ES module output
256
+ - TypeScript declaration generation via vite-plugin-dts
257
+ - Vite rollup bundling
258
+
259
+ ## Running unit tests
260
+
261
+ Run `nx test browser-utils` to execute the unit tests via Vitest.
262
+
263
+ ## Notes and caveats
264
+
265
+ - Environment: Some utilities require browser globals (window, document, performance). Use them in a DOM-capable environment (browser or a DOM-enabled test runner).
266
+ - Types: Debouncer uses NodeJS.Timeout type in typings; in browsers, the runtime value is still a timeout handle and works as expected.
267
+ - LoggingMixin: Designed for Custom Elements (HTMLElement or frameworks like Lit’s ReactiveElement). The debug attribute is cached for performance; call invalidateDebugCache() after toggling it dynamically. warn and error always log regardless of debug state.
package/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './lib/create-element';
2
+ export * from './lib/Debouncer';
3
+ export * from './lib/get-error-location';
4
+ export * from './lib/Stopwatch';
5
+ export * from './lib/wait-for';
6
+ export * from './mixins/LoggingMixin';
package/index.js ADDED
@@ -0,0 +1,162 @@
1
+ var h = Object.defineProperty;
2
+ var l = (e) => {
3
+ throw TypeError(e);
4
+ };
5
+ var f = (e, t, n) => t in e ? h(e, t, { enumerable: !0, configurable: !0, writable: !0, value: n }) : e[t] = n;
6
+ var r = (e, t, n) => f(e, typeof t != "symbol" ? t + "" : t, n), c = (e, t, n) => t.has(e) || l("Cannot " + n);
7
+ var a = (e, t, n) => (c(e, t, "read from private field"), n ? n.call(e) : t.get(e)), m = (e, t, n) => t.has(e) ? l("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(e) : t.set(e, n), u = (e, t, n, i) => (c(e, t, "write to private field"), i ? i.call(e, n) : t.set(e, n), n);
8
+ function b(e, t = {}, n = []) {
9
+ Array.isArray(n) || (n = [n]);
10
+ const i = document.createElement(e);
11
+ for (const s in t)
12
+ t[s] !== null && t[s] !== void 0 && i.setAttribute(s, t[s] !== !0 ? t[s] : "");
13
+ for (const s of n)
14
+ i.append(typeof s == "string" ? document.createTextNode(s) : s);
15
+ return i;
16
+ }
17
+ class w {
18
+ /**
19
+ *
20
+ * @param delay Debounce delay in milliseconds
21
+ * @param max_delay Maximum delay in milliseconds, if false then no maximum delay is applied
22
+ */
23
+ constructor(t, n = !1) {
24
+ r(this, "timeout", null);
25
+ r(this, "startTimeWithMs", 0);
26
+ this.delay = t, this.max_delay = n;
27
+ }
28
+ async wait() {
29
+ return this.startTimeWithMs === 0 && (this.startTimeWithMs = Date.now()), this.timeout && (this.max_delay === !1 || this.startTimeWithMs + this.max_delay > Date.now()) && clearTimeout(this.timeout), new Promise((t) => {
30
+ this.timeout = setTimeout(() => {
31
+ this.startTimeWithMs = 0, t(!0);
32
+ }, this.delay);
33
+ });
34
+ }
35
+ debounce(t) {
36
+ this.timeout && clearTimeout(this.timeout), this.timeout = setTimeout(() => {
37
+ t();
38
+ }, this.delay);
39
+ }
40
+ }
41
+ function p(e) {
42
+ if (typeof e.lineNumber == "number")
43
+ return {
44
+ file: e.fileName || e.sourceURL,
45
+ line: e.lineNumber,
46
+ column: e.columnNumber ?? void 0
47
+ };
48
+ if (typeof e.line == "number")
49
+ return {
50
+ file: e.sourceURL,
51
+ line: e.line,
52
+ column: e.column
53
+ };
54
+ const n = String(e.stack || e.message || "").split(`
55
+ `), i = /(.*?)(?:\(|@)?(.*?):(\d+):(\d+)\)?$/;
56
+ for (const s of n) {
57
+ const o = s.match(i);
58
+ if (o)
59
+ return { file: o[2], line: +o[3], column: +o[4] };
60
+ }
61
+ return { file: e.fileName || e.sourceURL };
62
+ }
63
+ class L {
64
+ constructor(t, n = !0) {
65
+ r(this, "label");
66
+ r(this, "last");
67
+ r(this, "startTime");
68
+ r(this, "running", !1);
69
+ r(this, "enabled");
70
+ this.label = t, this.enabled = n, this.startTime = this.last = performance.now(), this.running = !0;
71
+ }
72
+ lap(t = "") {
73
+ if (!this.enabled) return;
74
+ const n = performance.now(), i = (n - this.last) / 1e3;
75
+ this.last = n, console.debug(`[${this.label}] ${t} +${i.toFixed(3)}s`);
76
+ }
77
+ elapsed() {
78
+ return performance.now() - this.startTime;
79
+ }
80
+ reset() {
81
+ this.startTime = this.last = performance.now();
82
+ }
83
+ stop() {
84
+ return this.running = !1, this.elapsed();
85
+ }
86
+ start() {
87
+ this.running = !0, this.reset();
88
+ }
89
+ isRunning() {
90
+ return this.running;
91
+ }
92
+ }
93
+ function T(e, t, n) {
94
+ return new Promise((i, s) => {
95
+ const o = (d) => {
96
+ e.removeEventListener(t, o, n), i(d);
97
+ };
98
+ e.addEventListener(t, o, n);
99
+ });
100
+ }
101
+ function y() {
102
+ return document.readyState === "loading" ? new Promise((e) => {
103
+ document.addEventListener("DOMContentLoaded", () => e());
104
+ }) : Promise.resolve();
105
+ }
106
+ function v() {
107
+ return document.readyState === "complete" ? Promise.resolve() : new Promise((e) => {
108
+ window.addEventListener("load", () => e());
109
+ });
110
+ }
111
+ function E(e) {
112
+ return new Promise((t) => setTimeout(t, e));
113
+ }
114
+ function x(e) {
115
+ return new Promise((t) => {
116
+ const n = (i) => {
117
+ e.removeEventListener("animationend", n), t(i);
118
+ };
119
+ e.addEventListener("animationend", n);
120
+ });
121
+ }
122
+ function M(e) {
123
+ var n;
124
+ class t extends e {
125
+ constructor() {
126
+ super(...arguments);
127
+ m(this, n, null);
128
+ }
129
+ /**
130
+ * Clears the cached debug flag so the attribute will be checked again
131
+ * on the next log/warn/error call.
132
+ */
133
+ invalidateDebugCache() {
134
+ u(this, n, null);
135
+ }
136
+ get _debug() {
137
+ return a(this, n) !== null ? a(this, n) : (this instanceof HTMLElement && u(this, n, this.hasAttribute("debug") && !["false", "0", "off", "no"].includes(this.getAttribute("debug") || "")), a(this, n));
138
+ }
139
+ log(...o) {
140
+ this._debug && console.log("[LOG]", this, ...o);
141
+ }
142
+ warn(...o) {
143
+ console.warn("[WARN]", this, ...o);
144
+ }
145
+ error(...o) {
146
+ console.error("[ERROR]", this, ...o);
147
+ }
148
+ }
149
+ return n = new WeakMap(), t;
150
+ }
151
+ export {
152
+ w as Debouncer,
153
+ M as LoggingMixin,
154
+ L as Stopwatch,
155
+ b as create_element,
156
+ p as getErrorLocation,
157
+ E as sleep,
158
+ T as waitFor,
159
+ x as waitForAnimationEnd,
160
+ y as waitForDomContentLoaded,
161
+ v as waitForLoad
162
+ };
@@ -0,0 +1,14 @@
1
+ export declare class Debouncer {
2
+ private delay;
3
+ private max_delay;
4
+ private timeout;
5
+ private startTimeWithMs;
6
+ /**
7
+ *
8
+ * @param delay Debounce delay in milliseconds
9
+ * @param max_delay Maximum delay in milliseconds, if false then no maximum delay is applied
10
+ */
11
+ constructor(delay: number, max_delay?: number | false);
12
+ wait(): Promise<unknown>;
13
+ debounce(callback: () => void): void;
14
+ }
@@ -0,0 +1,14 @@
1
+ export declare class Stopwatch {
2
+ private label;
3
+ private last;
4
+ private startTime;
5
+ private running;
6
+ private enabled;
7
+ constructor(label: string, enabled?: boolean);
8
+ lap(msg?: string): void;
9
+ elapsed(): number;
10
+ reset(): void;
11
+ stop(): number;
12
+ start(): void;
13
+ isRunning(): boolean;
14
+ }
@@ -0,0 +1,3 @@
1
+ type Attrs = Record<string, string | true | null | undefined>;
2
+ export declare function create_element(tag: string, attrs?: Attrs, children?: (Node | string)[] | string | Node): HTMLElement;
3
+ export {};
@@ -0,0 +1,13 @@
1
+ export interface ErrorLocation {
2
+ file?: string;
3
+ line?: number;
4
+ column?: number;
5
+ }
6
+ export declare function getErrorLocation(err: Error & {
7
+ lineNumber?: number;
8
+ columnNumber?: number;
9
+ fileName?: string;
10
+ sourceURL?: string;
11
+ line?: number;
12
+ column?: number;
13
+ }): ErrorLocation;
@@ -0,0 +1,5 @@
1
+ export declare function waitFor<T>(target: EventTarget, eventName: string, options?: AddEventListenerOptions): Promise<T>;
2
+ export declare function waitForDomContentLoaded(): Promise<void>;
3
+ export declare function waitForLoad(): Promise<void>;
4
+ export declare function sleep(ms: number): Promise<void>;
5
+ export declare function waitForAnimationEnd(element: HTMLElement): Promise<AnimationEvent>;
@@ -0,0 +1,34 @@
1
+ type Constructor<T = object> = abstract new (...args: any[]) => T;
2
+ /**
3
+ * LoggingMixin
4
+ *
5
+ * A lightweight logging mixin that can be applied to any Custom Element base class
6
+ * (e.g., HTMLElement, LitElement/ReactiveElement, etc.). It provides log, warn,
7
+ * and error methods that only output when debugging is enabled.
8
+ *
9
+ * How it decides whether to log:
10
+ * - On the first call to any of the logging methods (log/warn/error), the mixin
11
+ * checks the 'debug' attribute on the element instance and caches the result
12
+ * (ressourcenschonende Attributabfrage mit Caching).
13
+ * - If the 'debug' attribute is present, it is considered truthy unless explicitly
14
+ *
15
+ * Example:
16
+ * class MyElement extends LoggingMixin(HTMLElement) {}
17
+ * // or with Lit:
18
+ * class MyLitEl extends LoggingMixin(ReactiveElement) {}
19
+ *
20
+ * <my-element debug></my-element> // enables debug logging
21
+ */
22
+ export declare function LoggingMixin<TBase extends Constructor<object>>(Base: TBase): (abstract new (...args: any[]) => {
23
+ "__#1700@#debugCached": boolean | null;
24
+ /**
25
+ * Clears the cached debug flag so the attribute will be checked again
26
+ * on the next log/warn/error call.
27
+ */
28
+ invalidateDebugCache(): void;
29
+ readonly _debug: boolean | null;
30
+ log(...args: any[]): void;
31
+ warn(...args: any[]): void;
32
+ error(...args: any[]): void;
33
+ }) & TBase;
34
+ export {};
package/package.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@trunkjs/browser-utils",
3
+ "version": "1.0.12",
4
+ "main": "./index.js",
5
+ "dependencies": {},
6
+ "type": "module",
7
+ "types": "./index.d.ts"
8
+ }