@brandup/ui 2.0.1 → 2.0.3

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.
Files changed (63) hide show
  1. package/README.md +12 -7
  2. package/dist/cjs/constants.js +14 -0
  3. package/dist/cjs/constants.js.map +1 -0
  4. package/dist/cjs/dom/bind-each.js +90 -0
  5. package/dist/cjs/dom/bind-each.js.map +1 -0
  6. package/dist/cjs/dom/bind.js +29 -0
  7. package/dist/cjs/dom/bind.js.map +1 -0
  8. package/dist/cjs/dom/binding-cleanup.js +162 -0
  9. package/dist/cjs/dom/binding-cleanup.js.map +1 -0
  10. package/dist/cjs/dom/dom.js +184 -0
  11. package/dist/cjs/dom/dom.js.map +1 -0
  12. package/dist/cjs/dom/helpers.js +33 -0
  13. package/dist/cjs/dom/helpers.js.map +1 -0
  14. package/dist/cjs/dom/index.js +14 -0
  15. package/dist/cjs/dom/index.js.map +1 -0
  16. package/dist/cjs/dom/tag.js +207 -0
  17. package/dist/cjs/dom/tag.js.map +1 -0
  18. package/dist/cjs/element.js +265 -0
  19. package/dist/cjs/element.js.map +1 -0
  20. package/dist/cjs/events.js +204 -0
  21. package/dist/cjs/events.js.map +1 -0
  22. package/dist/cjs/ext.js +20 -0
  23. package/dist/cjs/ext.js.map +1 -0
  24. package/dist/cjs/index.js +40 -1455
  25. package/dist/cjs/index.js.map +1 -1
  26. package/dist/cjs/reactive/computed.js +36 -0
  27. package/dist/cjs/reactive/computed.js.map +1 -0
  28. package/dist/cjs/reactive/effect.js +197 -0
  29. package/dist/cjs/reactive/effect.js.map +1 -0
  30. package/dist/cjs/reactive/reactive.js +106 -0
  31. package/dist/cjs/reactive/reactive.js.map +1 -0
  32. package/dist/mjs/constants.js +10 -0
  33. package/dist/mjs/constants.js.map +1 -0
  34. package/dist/mjs/dom/bind-each.js +86 -0
  35. package/dist/mjs/dom/bind-each.js.map +1 -0
  36. package/dist/mjs/dom/bind.js +26 -0
  37. package/dist/mjs/dom/bind.js.map +1 -0
  38. package/dist/mjs/dom/binding-cleanup.js +156 -0
  39. package/dist/mjs/dom/binding-cleanup.js.map +1 -0
  40. package/dist/mjs/dom/dom.js +169 -0
  41. package/dist/mjs/dom/dom.js.map +1 -0
  42. package/dist/mjs/dom/helpers.js +29 -0
  43. package/dist/mjs/dom/helpers.js.map +1 -0
  44. package/dist/mjs/dom/index.js +12 -0
  45. package/dist/mjs/dom/index.js.map +1 -0
  46. package/dist/mjs/dom/tag.js +203 -0
  47. package/dist/mjs/dom/tag.js.map +1 -0
  48. package/dist/mjs/element.js +260 -0
  49. package/dist/mjs/element.js.map +1 -0
  50. package/dist/mjs/events.js +202 -0
  51. package/dist/mjs/events.js.map +1 -0
  52. package/dist/mjs/ext.js +18 -0
  53. package/dist/mjs/ext.js.map +1 -0
  54. package/dist/mjs/index.js +11 -1431
  55. package/dist/mjs/index.js.map +1 -1
  56. package/dist/mjs/reactive/computed.js +33 -0
  57. package/dist/mjs/reactive/computed.js.map +1 -0
  58. package/dist/mjs/reactive/effect.js +187 -0
  59. package/dist/mjs/reactive/effect.js.map +1 -0
  60. package/dist/mjs/reactive/reactive.js +102 -0
  61. package/dist/mjs/reactive/reactive.js.map +1 -0
  62. package/dist/types.d.ts +18 -2
  63. package/package.json +9 -1
package/dist/mjs/index.js CHANGED
@@ -1,1432 +1,12 @@
1
- let ListenCounter = 1;
2
- /**
3
- * Minimal event emitter with support for one-off listeners and cross-emitter listening.
4
- *
5
- * The optional `TEvents` type parameter is an event map (`{ eventName: (args) => void }`)
6
- * that gives subclasses strongly-typed event names, callback signatures and `trigger` arguments.
7
- * When omitted it defaults to a loose map, so untyped usage keeps working.
8
- */
9
- class EventEmitter {
10
- _events;
11
- _listenId;
12
- _listeningTo;
13
- on(eventName, callback, context) {
14
- const events = this._getOrCreateEvents(eventName);
15
- events.push({ callback, context: context || undefined, ctx: context || this });
16
- return this;
17
- }
18
- once(eventName, callback, context) {
19
- const wrapper = (...args) => {
20
- this.off(eventName, wrapper, context);
21
- callback.apply(context || this, args);
22
- };
23
- return this.on(eventName, wrapper, context);
24
- }
25
- off(eventName, callback, context) {
26
- if (!eventName && !callback && !context)
27
- throw new Error("Require off arguments.");
28
- const events = this._events;
29
- if (!events)
30
- return this;
31
- callback = callback || undefined;
32
- context = context || undefined;
33
- const eventNames = eventName ? [eventName.toLowerCase()] : Object.keys(events);
34
- for (let i = 0; i < eventNames.length; i++) {
35
- const name = eventNames[i];
36
- const currentCallbacks = events[name];
37
- if (!currentCallbacks)
38
- continue;
39
- // reset callback list
40
- const newCallbacks = events[name] = [];
41
- if (callback || context) {
42
- currentCallbacks.forEach(c => {
43
- const callbackMatch = !callback || c.callback === callback;
44
- const contextMatch = !context || c.context === context;
45
- if (callbackMatch && contextMatch)
46
- return;
47
- newCallbacks.push(c);
48
- });
49
- }
50
- if (!newCallbacks.length)
51
- delete events[name];
52
- }
53
- return this;
54
- }
55
- /**
56
- * Listen to an event on another emitter, tracking the subscription so it can be released via `stopListening`.
57
- * @param source Emitter to subscribe to.
58
- * @param eventName Event name to listen for.
59
- * @param callback Handler invoked when the event is triggered.
60
- */
61
- listenTo(source, eventName, callback) {
62
- this._addListeningTo(source, eventName, callback, callback);
63
- source.on(eventName, callback, this);
64
- return this;
65
- }
66
- /**
67
- * Listen once to an event on another emitter; the subscription is tracked and auto-removed after it fires.
68
- * @param source Emitter to subscribe to.
69
- * @param eventName Event name to listen for.
70
- * @param callback Handler invoked once when the event is triggered.
71
- */
72
- listenToOnce(source, eventName, callback) {
73
- // Use our own one-shot wrapper instead of source.once so that, when it fires,
74
- // we can drop this exact subscription from BOTH sides — the source and our
75
- // own _listeningTo registry — keeping the two in sync (no dangling tracking).
76
- const wrapper = (...args) => {
77
- source.off(eventName, wrapper, this);
78
- this._removeListeningSubscription(source, eventName, wrapper);
79
- callback.apply(this, args);
80
- };
81
- this._addListeningTo(source, eventName, wrapper, callback);
82
- source.on(eventName, wrapper, this);
83
- return this;
84
- }
85
- /**
86
- * Release subscriptions previously established with `listenTo`/`listenToOnce`.
87
- * Omitted arguments broaden the match (e.g. no `source` releases all tracked emitters).
88
- * @param source Limit to a specific emitter, or omit for all.
89
- * @param eventName Limit to a specific event, or omit for all.
90
- * @param callback Limit to a specific handler, or omit for all.
91
- */
92
- stopListening(source, eventName, callback) {
93
- if (!this._listeningTo)
94
- return this;
95
- // A source we never listened to has no tracked subscriptions — nothing to do.
96
- const listenings = source
97
- ? (source._listenId && this._listeningTo[source._listenId] ? [this._listeningTo[source._listenId]] : [])
98
- : Object.values(this._listeningTo);
99
- const targetEvent = eventName ? eventName.toLowerCase() : undefined;
100
- listenings.forEach(listening => {
101
- // snapshot: the loop splices listening.subscriptions
102
- listening.subscriptions.slice().forEach(sub => {
103
- if (targetEvent && sub.eventName !== targetEvent)
104
- return;
105
- // match the user-supplied callback against both the registered
106
- // callback and the original (so a listenToOnce can be stopped by
107
- // the callback the caller passed, not the internal wrapper)
108
- if (callback && sub.callback !== callback && sub.origin !== callback)
109
- return;
110
- listening.emitter.off(sub.eventName, sub.callback, this);
111
- const index = listening.subscriptions.indexOf(sub);
112
- if (index >= 0)
113
- listening.subscriptions.splice(index, 1);
114
- });
115
- if (!listening.subscriptions.length && listening.emitter._listenId)
116
- delete this._listeningTo[listening.emitter._listenId];
117
- });
118
- if (!this._listeningTo || Object.keys(this._listeningTo).length === 0)
119
- delete this._listeningTo;
120
- return this;
121
- }
122
- _addListeningTo(source, eventName, callback, origin) {
123
- const listeningTo = this._listeningTo || (this._listeningTo = {});
124
- const listenId = source._listenId || (source._listenId = `l${ListenCounter++}`);
125
- const listenTo = listeningTo[listenId] || (listeningTo[listenId] = { emitter: source, subscriptions: [] });
126
- // one entry per subscription (not per event name) so each can be removed
127
- // independently — e.g. a listenToOnce dropping itself without disturbing
128
- // a co-registered persistent listenTo on the same source+event.
129
- listenTo.subscriptions.push({ eventName: eventName.toLowerCase(), callback, origin });
130
- }
131
- /** Remove a single tracked subscription (matched by event + registered callback). @internal */
132
- _removeListeningSubscription(source, eventName, callback) {
133
- const listenId = source._listenId;
134
- if (!this._listeningTo || !listenId)
135
- return;
136
- const listening = this._listeningTo[listenId];
137
- if (!listening)
138
- return;
139
- eventName = eventName.toLowerCase();
140
- const index = listening.subscriptions.findIndex(s => s.eventName === eventName && s.callback === callback);
141
- if (index >= 0)
142
- listening.subscriptions.splice(index, 1);
143
- if (!listening.subscriptions.length)
144
- delete this._listeningTo[listenId];
145
- if (Object.keys(this._listeningTo).length === 0)
146
- delete this._listeningTo;
147
- }
148
- stopAllListeners() {
149
- if (!this._events)
150
- return;
151
- Object.values(this._events).forEach(callbacks => {
152
- callbacks.forEach(callback => {
153
- if (callback.context instanceof EventEmitter)
154
- callback.context.stopListening(this);
155
- });
156
- });
157
- delete this._events;
158
- }
159
- trigger(eventName, ...args) {
160
- eventName = eventName.toLowerCase();
161
- if (eventName === "all")
162
- throw new Error('Not allow trigger all event.');
163
- if (!this._events)
164
- return this;
165
- const events = this._getEvents(eventName);
166
- const allEvents = this._getEvents("all");
167
- this._triggerEvent(events, ...args);
168
- this._triggerEvent(allEvents, ...args);
169
- return this;
170
- }
171
- _triggerEvent(events, ...args) {
172
- if (!events || !events.length)
173
- return;
174
- // snapshot so callbacks added/removed during dispatch don't affect this trigger
175
- const snapshot = events.slice();
176
- for (let i = 0; i < snapshot.length; i++) {
177
- const event = snapshot[i];
178
- event.callback.apply(event.ctx, args);
179
- }
180
- }
181
- _getOrCreateEvents(eventName) {
182
- eventName = eventName.toLowerCase();
183
- const events = this._events || (this._events = {});
184
- return events[eventName] || (events[eventName] = []);
185
- }
186
- _getEvents(eventName) {
187
- if (!eventName)
188
- return;
189
- const events = this._events;
190
- if (!events)
191
- return;
192
- return events[eventName];
193
- }
194
- /** Release every subscription: both events this emitter listens to and listeners registered on it. */
195
- stopEvents() {
196
- this.stopListening();
197
- this.stopAllListeners();
198
- }
199
- }
200
-
201
- /** Key used to track iteration/length dependencies (object key enumeration, array growth). */
202
- const ITERATE_KEY = Symbol("iterate");
203
- /**
204
- * A reactive effect: a function whose reactive reads are tracked, so it re-runs
205
- * whenever any of those reactive dependencies change.
206
- */
207
- class ReactiveEffect {
208
- __fn;
209
- /** Dependency sets this effect is currently subscribed to. @internal */
210
- deps = [];
211
- __active = true;
212
- /** Called instead of `run` when a dependency changes (used by `computed`). */
213
- scheduler;
214
- /** Invoked once when the effect is stopped. */
215
- onStop;
216
- constructor(__fn, scheduler) {
217
- this.__fn = __fn;
218
- this.scheduler = scheduler;
219
- activeScope?.add(this);
220
- }
221
- /** Whether the effect is still active (not stopped). */
222
- get active() { return this.__active; }
223
- /** Run the effect, (re)collecting its dependencies. */
224
- run() {
225
- if (!this.__active)
226
- return this.__fn();
227
- cleanupEffect(this);
228
- const prevEffect = activeEffect;
229
- activeEffect = this;
230
- try {
231
- return this.__fn();
232
- }
233
- finally {
234
- activeEffect = prevEffect;
235
- }
236
- }
237
- /** Stop the effect: remove its subscriptions and run `onStop`. */
238
- stop() {
239
- if (!this.__active)
240
- return;
241
- cleanupEffect(this);
242
- this.__active = false;
243
- this.onStop?.();
244
- }
245
- }
246
- let activeEffect;
247
- let activeScope;
248
- const targetMap = new WeakMap();
249
- /** Record the active effect (if any) as depending on `target[key]`. @internal */
250
- function track(target, key) {
251
- if (!activeEffect)
252
- return;
253
- let depsMap = targetMap.get(target);
254
- if (!depsMap)
255
- targetMap.set(target, depsMap = new Map());
256
- let dep = depsMap.get(key);
257
- if (!dep)
258
- depsMap.set(key, dep = new Set());
259
- if (!dep.has(activeEffect)) {
260
- dep.add(activeEffect);
261
- activeEffect.deps.push(dep);
262
- }
263
- }
264
- /** Re-run (or schedule) every effect that depends on `target[key]`. @internal */
265
- function trigger(target, key) {
266
- const depsMap = targetMap.get(target);
267
- if (!depsMap)
268
- return;
269
- const dep = depsMap.get(key);
270
- if (!dep)
271
- return;
272
- // copy first: running an effect re-collects deps and would mutate the live set
273
- const effects = new Set();
274
- dep.forEach(e => { if (e !== activeEffect)
275
- effects.add(e); });
276
- effects.forEach(e => {
277
- if (e.scheduler)
278
- e.scheduler();
279
- else
280
- queueEffect(e);
281
- });
282
- }
283
- const jobQueue = new Set();
284
- const resolvedPromise = Promise.resolve();
285
- let isFlushPending = false;
286
- let currentFlush = null;
287
- /** Queue an effect to run on the next microtask, deduplicated so multiple sync changes batch into one run. */
288
- function queueEffect(effect) {
289
- jobQueue.add(effect);
290
- if (!isFlushPending) {
291
- isFlushPending = true;
292
- currentFlush = resolvedPromise.then(flushJobs);
293
- }
294
- }
295
- function flushJobs() {
296
- isFlushPending = false;
297
- let guard = 0;
298
- while (jobQueue.size) {
299
- if (++guard > 10000) {
300
- jobQueue.clear();
301
- throw new Error("Reactive effect flush exceeded the iteration limit (cyclic update?).");
302
- }
303
- const jobs = Array.from(jobQueue);
304
- jobQueue.clear();
305
- for (const effect of jobs) {
306
- if (effect.active)
307
- effect.run();
308
- }
309
- }
310
- currentFlush = null;
311
- }
312
- /**
313
- * Resolves after the currently pending batch of reactive effects has flushed.
314
- * Effect re-runs (and the DOM updates they drive) are batched on the microtask queue,
315
- * so await `nextTick()` to observe their results.
316
- */
317
- function nextTick(fn) {
318
- const p = currentFlush || resolvedPromise;
319
- return fn ? p.then(fn).then(() => { }) : p.then(() => { });
320
- }
321
- /**
322
- * Execute `fn` without tracking any reactive reads.
323
- * Use inside a reactive context when you want to read state without creating a dependency.
324
- */
325
- function untrack(fn) {
326
- const prev = activeEffect;
327
- activeEffect = undefined;
328
- try {
329
- return fn();
330
- }
331
- finally {
332
- activeEffect = prev;
333
- }
334
- }
335
- function cleanupEffect(effect) {
336
- const { deps } = effect;
337
- deps.forEach(dep => dep.delete(effect));
338
- deps.length = 0;
339
- }
340
- /** Create and immediately run a reactive effect. Returns the {@link ReactiveEffect}. */
341
- function effect(fn, scheduler) {
342
- const e = new ReactiveEffect(fn, scheduler);
343
- e.run();
344
- return e;
345
- }
346
- /** A disposable container that collects effects (and nested scopes) so they can be stopped together. */
347
- class EffectScope {
348
- __effects = [];
349
- __scopes = [];
350
- __active = true;
351
- /** Whether the scope is still active (not stopped). */
352
- get active() { return this.__active; }
353
- /** @internal */
354
- add(effect) { this.__effects.push(effect); }
355
- /** @internal */
356
- addScope(scope) { this.__scopes.push(scope); }
357
- /** Run `fn` with this scope active; effects created during the call register to it. */
358
- run(fn) {
359
- const prev = activeScope;
360
- activeScope = this;
361
- try {
362
- return fn();
363
- }
364
- finally {
365
- activeScope = prev;
366
- }
367
- }
368
- /** Stop all effects and nested scopes collected by this scope. */
369
- stop() {
370
- if (!this.__active)
371
- return;
372
- this.__active = false;
373
- this.__effects.forEach(e => e.stop());
374
- this.__effects.length = 0;
375
- this.__scopes.forEach(s => s.stop());
376
- this.__scopes.length = 0;
377
- }
378
- }
379
- /** Create a new {@link EffectScope}, nested in the current scope if one is active. */
380
- function effectScope() {
381
- const scope = new EffectScope();
382
- activeScope?.addScope(scope);
383
- return scope;
384
- }
385
-
386
- const RAW = Symbol("raw");
387
- const reactiveMap = new WeakMap();
388
- function isObject(value) {
389
- return value !== null && typeof value === "object";
390
- }
391
- function isIntegerKey(key) {
392
- return typeof key === "string" && /^\d+$/.test(key);
393
- }
394
- /** Whether a value is a reactive proxy created by {@link reactive}. */
395
- function isReactive(value) {
396
- return isObject(value) && !!value[RAW];
397
- }
398
- /** Return the underlying raw (non-reactive) object behind a reactive proxy, or the value itself. */
399
- function toRaw(value) {
400
- const raw = isObject(value) && value[RAW];
401
- return raw ? raw : value;
402
- }
403
- const handlers = {
404
- get(target, key, receiver) {
405
- if (key === RAW)
406
- return target;
407
- const result = Reflect.get(target, key, receiver);
408
- // don't track symbol keys (well-known symbols, internal lookups)
409
- if (typeof key === "symbol")
410
- return result;
411
- track(target, key);
412
- // deep: wrap nested objects/arrays lazily on read
413
- return isObject(result) ? reactive(result) : result;
414
- },
415
- set(target, key, value, receiver) {
416
- const isArray = Array.isArray(target);
417
- const isIndex = isArray && isIntegerKey(key);
418
- const oldLength = isArray ? target.length : 0;
419
- const hadKey = isIndex
420
- ? Number(key) < oldLength
421
- : Object.prototype.hasOwnProperty.call(target, key);
422
- const oldValue = target[key];
423
- const result = Reflect.set(target, key, toRaw(value), receiver);
424
- if (!result)
425
- return result;
426
- if (!hadKey) {
427
- trigger(target, key);
428
- trigger(target, ITERATE_KEY);
429
- }
430
- else if (!Object.is(oldValue, toRaw(value))) {
431
- trigger(target, key);
432
- }
433
- if (isArray && key === "length") {
434
- // shrinking the array drops indices [newLength, oldLength) — notify
435
- // effects that read them, plus iteration; growing only changes iteration
436
- const newLength = Number(value);
437
- for (let i = newLength; i < oldLength; i++)
438
- trigger(target, String(i));
439
- if (newLength !== oldLength)
440
- trigger(target, ITERATE_KEY);
441
- }
442
- else if (isIndex && Number(key) >= oldLength) {
443
- // a new index extended the array, so its length changed
444
- trigger(target, "length");
445
- }
446
- return result;
447
- },
448
- deleteProperty(target, key) {
449
- const hadKey = Object.prototype.hasOwnProperty.call(target, key);
450
- const result = Reflect.deleteProperty(target, key);
451
- if (hadKey && result) {
452
- trigger(target, key);
453
- trigger(target, ITERATE_KEY);
454
- }
455
- return result;
456
- },
457
- has(target, key) {
458
- if (typeof key !== "symbol")
459
- track(target, key);
460
- return Reflect.has(target, key);
461
- },
462
- ownKeys(target) {
463
- track(target, ITERATE_KEY);
464
- return Reflect.ownKeys(target);
465
- }
466
- };
467
- /**
468
- * Wrap an object or array in a deep reactive proxy. Reads are tracked and writes
469
- * notify effects. Returns the same proxy for the same target, and non-objects unchanged.
470
- */
471
- function reactive(target) {
472
- if (!isObject(target))
473
- return target;
474
- if (target[RAW])
475
- return target; // already reactive
476
- const existing = reactiveMap.get(target);
477
- if (existing)
478
- return existing;
479
- const proxy = new Proxy(target, handlers);
480
- reactiveMap.set(target, proxy);
481
- return proxy;
482
- }
483
-
484
- /** A lazily-evaluated, cached reactive value derived from other reactive state. */
485
- class ComputedRef {
486
- __value;
487
- __dirty = true;
488
- __effect;
489
- constructor(getter) {
490
- this.__effect = new ReactiveEffect(getter, () => {
491
- // a dependency changed: mark dirty and notify effects that read this computed
492
- if (!this.__dirty) {
493
- this.__dirty = true;
494
- trigger(this, "value");
495
- }
496
- });
497
- }
498
- /** The current value; recomputed on read only when its dependencies have changed. */
499
- get value() {
500
- if (this.__dirty) {
501
- this.__value = this.__effect.run();
502
- this.__dirty = false;
503
- }
504
- track(this, "value");
505
- return this.__value;
506
- }
507
- }
508
- /** Create a {@link ComputedRef} from a getter. */
509
- function computed(getter) {
510
- return new ComputedRef(getter);
511
- }
512
-
513
- // UIElements are located via the data attribute `UIElement.setElement` already sets
514
- // (`elem.dataset.uiElement` → `data-ui-element`), so no extra marker is needed for them.
515
- const UIELEM_SELECTOR = "[data-ui-element]";
516
- // Marker placed on every element a reactive binding renders into, so a removed
517
- // subtree can be queried for affected bindings instead of scanning all of them.
518
- const BINDING_ATTR = "data-bui-binding";
519
- const BINDING_SELECTOR = "[data-bui-binding]";
520
- // node → UIElement auto-destroy entry
521
- const trackedElements = new Map();
522
- // container element → bindings rendered into it (one marker per container)
523
- const bindingsByContainer = new Map();
524
- let observer;
525
- function ensureObserver() {
526
- if (typeof MutationObserver !== "undefined" && !observer) {
527
- observer = new MutationObserver(onMutations);
528
- observer.observe(document, { childList: true, subtree: true });
529
- }
530
- }
531
- function disconnectIfEmpty() {
532
- if (trackedElements.size === 0 && bindingsByContainer.size === 0 && observer) {
533
- observer.disconnect();
534
- observer = undefined;
535
- }
536
- }
537
- /**
538
- * React only to *removed* nodes (insertions never disconnect anything), and within each
539
- * removed subtree dispose only the tracked UIElements/bindings that ended up disconnected.
540
- * Work is proportional to the removed subtree, not to the total number tracked.
541
- */
542
- function onMutations(mutations) {
543
- for (const mutation of mutations) {
544
- mutation.removedNodes.forEach(node => {
545
- // Bindings/UIElements are tracked by their (element) container, so a removed
546
- // text/comment node can neither be one nor contain one — only elements matter.
547
- if (node instanceof HTMLElement)
548
- disposeDisconnectedWithin(node);
549
- });
550
- }
551
- disconnectIfEmpty();
552
- }
553
- /** Apply `fn` to `root` itself (when it matches) and every descendant matching `selector`. */
554
- function forEachSelfAndMatches(root, selector, fn) {
555
- if (root.matches(selector))
556
- fn(root);
557
- root.querySelectorAll(selector).forEach(el => fn(el));
558
- }
559
- /** Destroy/stop tracked entries inside a removed subtree that are no longer in the document. */
560
- function disposeDisconnectedWithin(removed) {
561
- // UIElements first: destroying one cascades to its nested UIElements and bindings.
562
- forEachSelfAndMatches(removed, UIELEM_SELECTOR, el => {
563
- // `isConnected` guards against moves (removed from one place, re-inserted in another).
564
- if (!el.isConnected) {
565
- const entry = trackedElements.get(el);
566
- if (entry)
567
- entry.destroy(); // destroy() untracks itself, cascades, and disposes its bindings
568
- }
569
- });
570
- // Bindings not already cleaned by a UIElement destroy above (e.g. standalone containers).
571
- forEachSelfAndMatches(removed, BINDING_SELECTOR, container => {
572
- if (!container.isConnected)
573
- stopContainerBindings(container);
574
- });
575
- }
576
- function stopContainerBindings(container) {
577
- const set = bindingsByContainer.get(container);
578
- if (!set)
579
- return;
580
- bindingsByContainer.delete(container);
581
- container.removeAttribute(BINDING_ATTR);
582
- set.forEach(binding => binding.effect.stop());
583
- }
584
- /**
585
- * Track a binding so its reactive effect is stopped automatically once the element it renders
586
- * into has been mounted and then removed from the document (via a shared `MutationObserver`),
587
- * and so {@link disposeBindingsWithin} can stop it when its owning UIElement is destroyed.
588
- *
589
- * Containers that are never mounted are not auto-disposed; the `MutationObserver` part is a
590
- * no-op where it is unavailable, but `disposeBindingsWithin` still works.
591
- *
592
- * @param container Element the binding renders its node(s) into (its connectivity drives disposal).
593
- * @param effect The reactive effect to stop on disposal.
594
- */
595
- function autoDisposeBinding(container, effect) {
596
- let set = bindingsByContainer.get(container);
597
- if (!set) {
598
- bindingsByContainer.set(container, set = new Set());
599
- container.setAttribute(BINDING_ATTR, "");
600
- }
601
- set.add({ container, effect });
602
- ensureObserver();
603
- }
604
- /**
605
- * Register a `UIElement`'s DOM node for auto-destroy: once the node has been mounted
606
- * into the document and then removed, `destroy` is called automatically.
607
- * @internal — called by `UIElement.setElement`.
608
- */
609
- function trackAutoDestroy(node, destroy) {
610
- trackedElements.set(node, { node, destroy });
611
- ensureObserver();
612
- }
613
- /**
614
- * Remove a node from auto-destroy tracking (called when `UIElement.destroy` is
615
- * invoked explicitly so the entry does not linger).
616
- * @internal — called by `UIElement.destroy`.
617
- */
618
- function untrackAutoDestroy(node) {
619
- trackedElements.delete(node);
620
- disconnectIfEmpty();
621
- }
622
- /**
623
- * Destroy every tracked `UIElement` nested within `root` (excluding `root` itself),
624
- * deepest first. Located via the `data-ui-element` marker, so the cost is proportional
625
- * to the subtree rather than to the total number of tracked elements.
626
- * @internal — called by `UIElement.destroy`.
627
- */
628
- function destroyUIElementsWithin(root) {
629
- const victims = [];
630
- root.querySelectorAll(UIELEM_SELECTOR).forEach(el => {
631
- if (trackedElements.has(el))
632
- victims.push(el);
633
- });
634
- // querySelectorAll yields document order (ancestors before descendants);
635
- // iterate in reverse so deeper / nested elements are destroyed first.
636
- for (let i = victims.length - 1; i >= 0; i--)
637
- trackedElements.get(victims[i])?.destroy();
638
- }
639
- /**
640
- * Stop and forget every tracked binding rendered within `root` (used when a UIElement is
641
- * destroyed). Unlike the observer path this is unconditional — it does not check connectivity,
642
- * because the owner is being torn down.
643
- */
644
- function disposeBindingsWithin(root) {
645
- if (root instanceof HTMLElement)
646
- forEachSelfAndMatches(root, BINDING_SELECTOR, stopContainerBindings);
647
- disconnectIfEmpty();
648
- }
649
-
650
- /** Default constant values used across the library. */
651
- const constants = {
652
- ElemAttributeName: "uiElement",
653
- ElemPropertyName: "uielement",
654
- CommandAttributeName: "command",
655
- CommandExecutingCssClassName: "executing"
656
- };
657
-
658
- var constants$1 = /*#__PURE__*/Object.freeze({
659
- __proto__: null,
660
- default: constants
661
- });
662
-
663
- /**
664
- * Wraps an `HTMLElement` and binds business logic, commands and events to it.
665
- *
666
- * The optional `TEvents` event map is merged with {@link UIElementEvents}, so subclasses
667
- * can declare their own typed events in addition to the built-in `command`/`destroy`.
668
- */
669
- class UIElement extends EventEmitter {
670
- __element;
671
- __events;
672
- __commands;
673
- __destroyed;
674
- // Element members
675
- /** The bound DOM element, or `undefined` until `setElement` is called. */
676
- get element() { return this.__element; }
677
- /**
678
- * Bind a DOM element to this instance and run render logic. Can be called only once.
679
- * @param elem Element to bind; throws if already bound or owned by another `UIElement`.
680
- */
681
- setElement(elem) {
682
- if (!elem)
683
- throw new Error("Not set value elem.");
684
- if (this.__element || UIElement.hasElement(elem))
685
- throw new Error("UIElement already defined");
686
- this.__element = elem;
687
- elem[constants.ElemPropertyName] = this;
688
- elem.dataset[constants.ElemAttributeName] = this.typeName;
689
- trackAutoDestroy(elem, () => this.destroy());
690
- this._onRenderElement(elem);
691
- this.__raise("rendered", this);
692
- }
693
- // static members
694
- /**
695
- * Whether the given element is already bound to a `UIElement`.
696
- * @param elem Element to test.
697
- */
698
- static hasElement(elem) {
699
- return !!elem.dataset[constants.ElemAttributeName];
700
- }
701
- // Command members
702
- /**
703
- * Register a handler for a command declared in markup via the `data-command` attribute.
704
- * @param name Command name (case-insensitive); throws if already registered.
705
- * @param execute Handler run when the command fires; may return a `Promise` for async commands.
706
- * @param canExecute Optional predicate gating whether the command may run.
707
- * @returns This instance for chaining.
708
- */
709
- registerCommand(name, execute, canExecute) {
710
- if (this.__destroyed)
711
- return this;
712
- const commands = this.__commands || (this.__commands = {});
713
- const normalizedName = name.toLowerCase();
714
- if (normalizedName in commands)
715
- throw new Error(`Command "${name}" already registered.`);
716
- commands[normalizedName] = {
717
- name: name,
718
- execute,
719
- canExecute
720
- };
721
- return this;
722
- }
723
- /**
724
- * Whether a command with the given name is registered.
725
- * @param name Command name (case-insensitive).
726
- */
727
- hasCommand(name) {
728
- return !!this.__commands && name.toLowerCase() in this.__commands;
729
- }
730
- /**
731
- * Execute a registered command against a target element.
732
- * @internal
733
- */
734
- __execCommand(name, target) {
735
- if (this.__destroyed || !this.__element)
736
- throw new Error("UIElement is destroyed or has no element.");
737
- const key = name.toLowerCase();
738
- const command = this.__commands?.[key];
739
- if (!command)
740
- throw new Error(`Command "${name}" is not registered.`);
741
- const context = {
742
- target,
743
- uiElem: this
744
- };
745
- if (command.isExecuting)
746
- return { status: "already", context };
747
- command.isExecuting = true;
748
- // keep isExecuting cleanup inside finally so a throw in the
749
- // guards/trigger/execute can never leave the command stuck.
750
- let isAsync = false;
751
- try {
752
- if (!this._onCanExecCommand(name, target))
753
- return { status: "disallow", context };
754
- if (command.canExecute && !command.canExecute(context))
755
- return { status: "disallow", context };
756
- this.__raise("command", { element: this, name: command.name });
757
- const commandResult = command.execute(context);
758
- if (commandResult instanceof Promise) {
759
- isAsync = true;
760
- target.classList.add(constants.CommandExecutingCssClassName);
761
- commandResult
762
- // command owns its errors; log so failures aren't silent, and avoid unhandled rejection
763
- .catch((reason) => console.error(`Command "${command.name}" failed.`, reason))
764
- .finally(() => {
765
- target.classList.remove(constants.CommandExecutingCssClassName);
766
- delete command.isExecuting;
767
- });
768
- }
769
- return { status: "success", context };
770
- }
771
- finally {
772
- if (!isAsync)
773
- delete command.isExecuting;
774
- }
775
- }
776
- /**
777
- * Hook invoked when an element is bound via `setElement`. Override to render or wire up the element.
778
- * @param _elem The newly bound element.
779
- */
780
- /** Trigger a built-in event. Typed by {@link UIElementEvents}; bypasses the generic trigger overload internally. */
781
- __raise(name, ...args) {
782
- this.trigger(name, ...args);
783
- }
784
- _onRenderElement(_elem) { }
785
- /**
786
- * Hook deciding whether a command may execute. Override to add element-wide gating.
787
- * @param _name Command name being executed.
788
- * @param _elem Target element of the command.
789
- * @returns `true` to allow execution (default), `false` to disallow.
790
- */
791
- _onCanExecCommand(_name, _elem) {
792
- return true;
793
- }
794
- /**
795
- * Create an {@link EffectScope} whose reactive effects are stopped automatically when
796
- * this element is destroyed. Use it to scope `bind`/`effect` to the element's lifetime.
797
- */
798
- effectScope() {
799
- const scope = effectScope();
800
- this.on("destroy", () => scope.stop());
801
- return scope;
802
- }
803
- /** Returns the `typeName` of this element. */
804
- toString() { return this.typeName; }
805
- /** Destroy the element: trigger the `destroy` event, release events/commands and detach from the DOM element. */
806
- destroy() {
807
- if (this.__destroyed)
808
- return;
809
- this.__destroyed = true;
810
- this.__raise("destroy", this);
811
- super.stopEvents();
812
- const elem = this.__element;
813
- if (elem) {
814
- destroyUIElementsWithin(elem); // cascade to nested UIElements, deepest first
815
- untrackAutoDestroy(elem);
816
- disposeBindingsWithin(elem);
817
- delete elem.dataset[constants.ElemAttributeName];
818
- delete elem[constants.ElemPropertyName];
819
- }
820
- delete this.__element;
821
- delete this.__events;
822
- delete this.__commands;
823
- }
824
- }
825
- /**
826
- * A {@link UIElement} whose DOM element is bound in the constructor, so its `element`
827
- * is always defined (typed `HTMLElement`, never `undefined`). Subclasses pass their
828
- * `typeName` and the element to `super`.
829
- *
830
- * Use {@link UIElement} directly when the element is bound later (e.g. an application
831
- * that binds its element on run).
832
- */
833
- class UIElementBound extends UIElement {
834
- __typeName;
835
- /** @param typeName Unique type name of this element. @param elem Element to bind. */
836
- constructor(typeName, elem) {
837
- super();
838
- this.__typeName = typeName; // set before setElement (no field-initializer race)
839
- this.setElement(elem);
840
- }
841
- get typeName() { return this.__typeName; }
842
- /** The bound DOM element; always defined since it is set in the constructor. */
843
- get element() { return super.element; }
844
- }
845
- const findUiElementByCommand = (elem, commandName) => {
846
- let current = elem;
847
- while (current) {
848
- if (current.dataset[constants.ElemAttributeName]) {
849
- const uiElem = current[constants.ElemPropertyName];
850
- if (uiElem.hasCommand(commandName))
851
- return uiElem;
852
- }
853
- current = current.parentElement;
854
- }
855
- return null;
856
- };
857
- const commandClickHandler = (e) => {
858
- // walk up from the clicked element to the nearest ancestor declaring a command
859
- let commandElem = e.target;
860
- while (commandElem && !commandElem.dataset[constants.CommandAttributeName])
861
- commandElem = commandElem.parentElement;
862
- if (!commandElem)
863
- return;
864
- const commandName = commandElem.dataset[constants.CommandAttributeName];
865
- if (!commandName)
866
- throw new Error("Command data attribute does not have a value.");
867
- const uiElem = findUiElementByCommand(commandElem, commandName);
868
- // A click on a data-command element is owned by the library: prevent the element's
869
- // default action (e.g. <a> navigation) and stop the click chain — UNLESS the command
870
- // completed successfully and asked to stay transparent. This runs in `finally`, so a
871
- // throwing command can never skip preventDefault and let the page navigate/reload.
872
- let transparent = false;
873
- try {
874
- if (uiElem) {
875
- const result = uiElem.__execCommand(commandName, commandElem);
876
- transparent = result.status === "success" && !!result.context.transparent;
877
- }
878
- else
879
- console.warn(`Not find handler for command "${commandName}".`);
880
- }
881
- catch (reason) {
882
- console.error(`Command "${commandName}" failed.`, reason);
883
- }
884
- finally {
885
- if (!transparent) {
886
- e.preventDefault();
887
- e.stopPropagation();
888
- e.stopImmediatePropagation();
889
- }
890
- }
891
- };
892
- // Guarded so importing the module does not throw in a non-DOM environment (SSR/Node).
893
- if (typeof window !== "undefined")
894
- window.addEventListener("click", commandClickHandler);
895
- /** Remove the global click handler registered by brandup-ui. Call on app teardown or HMR disposal. */
896
- function destroyUI() {
897
- if (typeof window !== "undefined")
898
- window.removeEventListener("click", commandClickHandler);
899
- }
900
-
901
- // Guarded so importing the module does not throw in a non-DOM environment (SSR/Node).
902
- if (typeof HTMLElement !== "undefined") {
903
- HTMLElement.prototype.ui = function (factory) {
904
- factory(this);
905
- return this;
906
- };
907
- }
908
-
909
- /**
910
- * A reactive binding for use as a {@link tag} child. Its `compute` function is
911
- * re-evaluated and re-rendered whenever the reactive state it reads changes.
912
- */
913
- class Binding {
914
- compute;
915
- constructor(compute) {
916
- this.compute = compute;
917
- }
918
- }
919
- /**
920
- * Create a reactive {@link Binding} that can be passed as a `tag` child. The
921
- * compute function is tracked: it re-runs (updating the DOM in place) whenever
922
- * any reactive value it reads changes.
923
- *
924
- * @example
925
- * const state = reactive({ name: "Alice" });
926
- * DOM.tag("div", null, "Hi, ", bind(() => state.name));
927
- * state.name = "Bob"; // the text updates in place
928
- */
929
- function bind(compute) {
930
- return new Binding(compute);
931
- }
932
-
933
- /** A keyed-list binding produced by {@link bindEach}; handled by `appendChild` in `tag.ts`. */
934
- class BindingEach {
935
- getItems;
936
- getKey;
937
- render;
938
- constructor(getItems, getKey, render) {
939
- this.getItems = getItems;
940
- this.getKey = getKey;
941
- this.render = render;
942
- }
943
- }
944
- /**
945
- * Create a reactive keyed-list binding for use as a {@link tag} child.
946
- *
947
- * The `getItems` function is tracked; the list is reconciled whenever the array
948
- * changes (push, splice, reassignment, etc.). Items are matched by `getKey` so
949
- * unchanged nodes stay in place — only new, removed, or reordered nodes are touched.
950
- *
951
- * `render` is called **once per key** and runs **untracked**. Use `bind()` inside
952
- * the render function for item properties that should update independently:
953
- *
954
- * ⚠️ The item object passed to `render` is captured at first render for that key.
955
- * Mutate items in place (`item.name = "..."`) so `bind()` reactions fire. Replacing
956
- * the array with **new objects that reuse the same keys** keeps the cached node bound
957
- * to the *old* object, so per-item `bind()`s won't update — change the key, or mutate
958
- * the existing item, when its identity should change.
959
- *
960
- * @example
961
- * DOM.tag("ul", null,
962
- * bindEach(() => state.users, u => u.id, u =>
963
- * DOM.tag("li", null, bind(() => u.name))
964
- * )
965
- * );
966
- */
967
- function bindEach(getItems, getKey, render) {
968
- return new BindingEach(getItems, getKey, render);
969
- }
970
- /**
971
- * Mount a {@link BindingEach} into `container` and start tracking.
972
- * Called by `appendChild` in `tag.ts`; not intended for direct use.
973
- * @internal
974
- */
975
- function appendBindingEach(container, binding) {
976
- const nodes = new Map();
977
- // Anchor comment marks the start of the managed region inside the container.
978
- // Using an anchor (rather than container.firstChild) lets other children coexist.
979
- const anchor = document.createComment("");
980
- container.append(anchor);
981
- const eff = effect(() => {
982
- const items = binding.getItems();
983
- const nextKeys = new Set();
984
- for (let i = 0; i < items.length; i++)
985
- nextKeys.add(binding.getKey(items[i], i));
986
- // Remove nodes whose keys are no longer present
987
- for (const [key, node] of nodes) {
988
- if (!nextKeys.has(key)) {
989
- node.remove();
990
- nodes.delete(key);
991
- }
992
- }
993
- // Insert new nodes and restore order in a single pass starting after the anchor.
994
- // If the node is already at the expected position we advance; otherwise insertBefore moves it.
995
- let cursor = anchor.nextSibling;
996
- for (let i = 0; i < items.length; i++) {
997
- const key = binding.getKey(items[i], i);
998
- let node = nodes.get(key);
999
- if (!node) {
1000
- // Render untracked so item-property reads don't create dependencies on
1001
- // this list effect — use bind() inside render for fine-grained updates.
1002
- node = untrack(() => binding.render(items[i]));
1003
- nodes.set(key, node);
1004
- }
1005
- if (cursor !== node)
1006
- container.insertBefore(node, cursor);
1007
- else
1008
- cursor = cursor.nextSibling;
1009
- }
1010
- });
1011
- autoDisposeBinding(container, eff);
1012
- }
1013
-
1014
- /**
1015
- * @internal
1016
- * Adds one or more CSS classes to a single element. A string is split on spaces. No-op when `cssClass` is falsy.
1017
- */
1018
- const addCssClass = (elem, cssClass) => {
1019
- if (!cssClass)
1020
- return;
1021
- const tokens = (Array.isArray(cssClass) ? cssClass : cssClass.split(' ')).filter(Boolean);
1022
- if (tokens.length)
1023
- elem.classList.add(...tokens);
1024
- };
1025
- /**
1026
- * @internal
1027
- * Removes one or more CSS classes from a single element. A string is split on spaces. No-op when `cssClass` is falsy.
1028
- */
1029
- const removeCssClass = (elem, cssClass) => {
1030
- if (!cssClass)
1031
- return;
1032
- const tokens = (Array.isArray(cssClass) ? cssClass : cssClass.split(' ')).filter(Boolean);
1033
- if (tokens.length)
1034
- elem.classList.remove(...tokens);
1035
- };
1036
- var helpers = {
1037
- addCssClass,
1038
- removeCssClass
1039
- };
1040
-
1041
- /**
1042
- * Finds an element by its `id` within the whole document.
1043
- * @param id The element id to look up.
1044
- * @returns The matching element, or `null` if none exists.
1045
- */
1046
- function getById(id) {
1047
- return document.getElementById(id);
1048
- }
1049
- /**
1050
- * Returns the first descendant of `container` that has the given class.
1051
- * @param container Element to search within.
1052
- * @param className Single class name to match.
1053
- * @returns The first matching element, or `null` if none found.
1054
- */
1055
- function getByClass(container, className) {
1056
- const elements = container.getElementsByClassName(className);
1057
- if (elements.length === 0)
1058
- return null;
1059
- return elements.item(0);
1060
- }
1061
- /**
1062
- * Returns the first element in the document with the given `name` attribute.
1063
- * @param name The `name` attribute value to look up.
1064
- * @returns The first matching element, or `null` if none found.
1065
- */
1066
- function getByName(name) {
1067
- const elements = document.getElementsByName(name);
1068
- if (elements.length === 0)
1069
- return null;
1070
- return elements.item(0);
1071
- }
1072
- /**
1073
- * Returns the first descendant of `container` with the given tag name.
1074
- * @param container Element to search within.
1075
- * @param tagName Tag name to match (e.g. `"input"`).
1076
- * @returns The first matching element, or `null` if none found.
1077
- */
1078
- function getElementByTagName(container, tagName) {
1079
- const elements = container.getElementsByTagName(tagName);
1080
- if (elements.length === 0)
1081
- return null;
1082
- return elements.item(0);
1083
- }
1084
- /**
1085
- * Returns all descendants of `container` with the given tag name as a live collection.
1086
- * @param container Element to search within.
1087
- * @param tagName Tag name to match (e.g. `"li"`).
1088
- * @returns A live `HTMLCollection` of matching elements.
1089
- */
1090
- function getElementsByTagName(container, tagName) {
1091
- return container.getElementsByTagName(tagName);
1092
- }
1093
- /**
1094
- * Returns the first descendant of `container` matching the CSS selector.
1095
- * @param container Element to search within.
1096
- * @param query CSS selector.
1097
- * @returns The first matching element, or `null` if none found.
1098
- */
1099
- function queryElement(container, query) {
1100
- return container.querySelector(query);
1101
- }
1102
- /**
1103
- * Returns all descendants of `container` matching the CSS selector.
1104
- * @param container Element to search within.
1105
- * @param query CSS selector.
1106
- * @returns A static `NodeList` of matching elements.
1107
- */
1108
- function queryElements(container, query) {
1109
- return container.querySelectorAll(query);
1110
- }
1111
- /**
1112
- * Walks forward through the following siblings of `current` and returns the first one that has the given class.
1113
- * @param current Element to start from.
1114
- * @param className Class name to match.
1115
- * @returns The first matching following sibling, or `null` if none found.
1116
- */
1117
- function nextElementByClass(current, className) {
1118
- let elem = current.nextSibling;
1119
- while (elem) {
1120
- if (elem.nodeType === Node.ELEMENT_NODE && elem instanceof HTMLElement && elem.classList.contains(className))
1121
- return elem;
1122
- elem = elem.nextSibling;
1123
- }
1124
- return null;
1125
- }
1126
- /**
1127
- * Walks backward through the preceding siblings of `current` and returns the first one that has the given class.
1128
- * @param current Element to start from.
1129
- * @param className Class name to match.
1130
- * @returns The first matching preceding sibling, or `null` if none found.
1131
- */
1132
- function prevElementByClass(current, className) {
1133
- let elem = current.previousSibling;
1134
- while (elem) {
1135
- if (elem.nodeType === Node.ELEMENT_NODE && elem instanceof HTMLElement && elem.classList.contains(className))
1136
- return elem;
1137
- elem = elem.previousSibling;
1138
- }
1139
- return null;
1140
- }
1141
- /**
1142
- * Returns the nearest preceding sibling element of `current`, skipping non-element nodes (e.g. text nodes).
1143
- * @param current Element to start from.
1144
- * @returns The previous sibling element, or `null` if none found.
1145
- */
1146
- function prevElement(current) {
1147
- let elem = current.previousSibling;
1148
- while (elem) {
1149
- if (elem.nodeType === Node.ELEMENT_NODE && elem instanceof HTMLElement)
1150
- return elem;
1151
- elem = elem.previousSibling;
1152
- }
1153
- return null;
1154
- }
1155
- /**
1156
- * Returns the nearest following sibling element of `current`, skipping non-element nodes (e.g. text nodes).
1157
- * @param current Element to start from.
1158
- * @returns The next sibling element, or `null` if none found.
1159
- */
1160
- function nextElement(current) {
1161
- let elem = current.nextSibling;
1162
- while (elem) {
1163
- if (elem.nodeType === Node.ELEMENT_NODE && elem instanceof HTMLElement)
1164
- return elem;
1165
- elem = elem.nextSibling;
1166
- }
1167
- return null;
1168
- }
1169
- /**
1170
- * Adds the given CSS class(es) to every descendant of `container` matching the selector. No-op when `container` or `cssClass` is falsy.
1171
- * @param container Element to search within (ignored when null/undefined).
1172
- * @param selectors CSS selector for the elements to modify.
1173
- * @param cssClass Class name(s) to add.
1174
- */
1175
- function addClass(container, selectors, cssClass) {
1176
- if (!container || !cssClass)
1177
- return;
1178
- const nodes = container.querySelectorAll(selectors);
1179
- nodes.forEach(node => helpers.addCssClass(node, cssClass));
1180
- }
1181
- /**
1182
- * Removes the given CSS class(es) from every descendant of `container` matching the selector. No-op when `container` or `cssClass` is falsy.
1183
- * @param container Element to search within (ignored when null/undefined).
1184
- * @param selectors CSS selector for the elements to modify.
1185
- * @param cssClass Class name(s) to remove.
1186
- */
1187
- function removeClass(container, selectors, cssClass) {
1188
- if (!container || !cssClass)
1189
- return;
1190
- const nodes = container.querySelectorAll(selectors);
1191
- nodes.forEach(elem => helpers.removeCssClass(elem, cssClass));
1192
- }
1193
- /**
1194
- * Removes all child nodes from `container`, leaving it empty. No-op when `container` is null/undefined.
1195
- * @param container Element to clear.
1196
- */
1197
- function empty(container) {
1198
- if (!container)
1199
- return;
1200
- while (container.hasChildNodes()) {
1201
- if (container.firstChild)
1202
- container.removeChild(container.firstChild);
1203
- }
1204
- }
1205
-
1206
- var DomHelpers = /*#__PURE__*/Object.freeze({
1207
- __proto__: null,
1208
- addClass: addClass,
1209
- empty: empty,
1210
- getByClass: getByClass,
1211
- getById: getById,
1212
- getByName: getByName,
1213
- getElementByTagName: getElementByTagName,
1214
- getElementsByTagName: getElementsByTagName,
1215
- nextElement: nextElement,
1216
- nextElementByClass: nextElementByClass,
1217
- prevElement: prevElement,
1218
- prevElementByClass: prevElementByClass,
1219
- queryElement: queryElement,
1220
- queryElements: queryElements,
1221
- removeClass: removeClass
1222
- });
1223
-
1224
- /** Returns `true` when `value` should be treated as options (or absent options), `false` when it is the first child. */
1225
- const isOptionsArg = (value) => {
1226
- if (value === null || value === undefined)
1227
- return true;
1228
- if (typeof value !== "object")
1229
- return false; // string, number, boolean, function → child
1230
- if (Array.isArray(value))
1231
- return false; // array → children
1232
- if (value instanceof Element || value instanceof UIElement || value instanceof Binding || value instanceof BindingEach || value instanceof Promise)
1233
- return false;
1234
- return true; // plain object → ElementOptions
1235
- };
1236
- /**
1237
- * Creates an HTML element, optionally applying options and appending children.
1238
- *
1239
- * The second argument is **options** when it is `null` or a plain {@link ElementOptions} object.
1240
- * It is treated as the **first child** for any {@link TagFirstChild} value — strings, numbers,
1241
- * elements, bindings, arrays, etc. — so `tag("div", "hello")` appends "hello" as HTML text,
1242
- * and `tag("ul", bindEach(...))` works without a leading `null`.
1243
- * To apply a CSS class use `{ class: "name" }` in options.
1244
- */
1245
- function tag(tagName, optionsOrChild, ...rest) {
1246
- const elem = document.createElement(tagName);
1247
- if (isOptionsArg(optionsOrChild)) {
1248
- applyOptions(elem, optionsOrChild);
1249
- appendChild(elem, rest);
1250
- }
1251
- else {
1252
- appendChild(elem, optionsOrChild !== undefined ? [optionsOrChild, ...rest] : rest);
1253
- }
1254
- return elem;
1255
- }
1256
- /**
1257
- * Applies element options to an existing element. A string or array is treated as a {@link CssClass}; otherwise each {@link ElementOptions} key is applied (`id`, `styles`, `class`, `command`, `dataset`, `events`, or a plain attribute). `undefined` values are skipped.
1258
- * @param elem Target element to mutate.
1259
- * @param options Options object, {@link CssClass} shorthand, or `null`/`undefined` for none.
1260
- */
1261
- const applyOptions = (elem, options) => {
1262
- if (!options)
1263
- return;
1264
- if (typeof options === "string" || Array.isArray(options))
1265
- helpers.addCssClass(elem, options);
1266
- else {
1267
- for (const key in options) {
1268
- const value = options[key];
1269
- if (value === undefined)
1270
- continue;
1271
- switch (key) {
1272
- case "id":
1273
- elem.id = value;
1274
- break;
1275
- case "styles": {
1276
- if (value) {
1277
- for (const sKey in value)
1278
- elem.style[sKey] = value[sKey];
1279
- }
1280
- break;
1281
- }
1282
- case "class": {
1283
- helpers.addCssClass(elem, value);
1284
- break;
1285
- }
1286
- case "command": {
1287
- elem.dataset["command"] = value;
1288
- break;
1289
- }
1290
- case "dataset": {
1291
- if (value) {
1292
- for (const dataName in value)
1293
- elem.dataset[dataName] = value[dataName];
1294
- }
1295
- break;
1296
- }
1297
- case "events": {
1298
- if (value) {
1299
- for (const eventName in value)
1300
- elem.addEventListener(eventName, value[eventName]);
1301
- }
1302
- break;
1303
- }
1304
- default: {
1305
- if (value === null)
1306
- elem.setAttribute(key, "");
1307
- else if (typeof value === "object")
1308
- elem.setAttribute(key, JSON.stringify(value));
1309
- else
1310
- elem.setAttribute(key, String(value));
1311
- break;
1312
- }
1313
- }
1314
- }
1315
- }
1316
- };
1317
- /**
1318
- * Appends one or more children to a container, recursively resolving arrays, promises and factory functions. Elements are appended as-is; a {@link UIElement} appends its bound element (or defers until `setElement` binds one); a reactive {@link Binding} renders and live-updates in place; strings/numbers/booleans are inserted as HTML; `null`/`undefined` are ignored.
1319
- * @param container Element to append the children to.
1320
- * @param children Child or children to append. See {@link TagChildrenLike}.
1321
- * @throws Error When a child resolves to an unsupported type.
1322
- */
1323
- const appendChild = (container, children) => {
1324
- if (children === null || children === undefined)
1325
- return;
1326
- if (children instanceof Array)
1327
- children.forEach(child => appendChild(container, child));
1328
- else if (children instanceof Element)
1329
- container.append(children);
1330
- else if (children instanceof UIElement) {
1331
- if (children.element)
1332
- container.append(children.element);
1333
- else {
1334
- // reserve the position now; replace the placeholder once setElement
1335
- // binds the element (after _onRenderElement), keeping child order
1336
- const placeholder = document.createComment("");
1337
- container.append(placeholder);
1338
- children.once("rendered", () => {
1339
- if (children.element)
1340
- placeholder.replaceWith(children.element);
1341
- });
1342
- }
1343
- }
1344
- else if (children instanceof Binding)
1345
- appendBinding(container, children);
1346
- else if (children instanceof BindingEach)
1347
- appendBindingEach(container, children);
1348
- else if (children instanceof Promise)
1349
- children.then((child) => appendChild(container, child));
1350
- else {
1351
- const typeName = typeof children;
1352
- let html;
1353
- switch (typeName) {
1354
- case "string":
1355
- html = children;
1356
- break;
1357
- case "number":
1358
- case "boolean":
1359
- html = children.toString();
1360
- break;
1361
- case "function":
1362
- const child = children(container);
1363
- appendChild(container, child);
1364
- return;
1365
- default:
1366
- throw new Error(`Not support child type of ${typeName}.`);
1367
- }
1368
- container.insertAdjacentHTML("beforeend", html);
1369
- }
1370
- };
1371
- /**
1372
- * Renders a reactive {@link Binding} child and keeps it up to date: a reactive
1373
- * effect re-evaluates the binding and updates the DOM in place — reusing a text
1374
- * node for text values and swapping the node when an element/UIElement is returned.
1375
- */
1376
- const appendBinding = (container, binding) => {
1377
- let current = document.createTextNode("");
1378
- let textNode = current;
1379
- container.append(current);
1380
- const eff = effect(() => {
1381
- const value = binding.compute();
1382
- if (value instanceof Element || value instanceof UIElement) {
1383
- const next = (value instanceof UIElement ? value.element : value) ?? document.createComment("");
1384
- current.replaceWith(next);
1385
- current = next;
1386
- textNode = null;
1387
- // deferred UIElement: its element is bound later — swap the placeholder
1388
- // for the real element once setElement raises "rendered"
1389
- if (value instanceof UIElement && !value.element) {
1390
- const placeholder = next;
1391
- value.once("rendered", () => {
1392
- // skip if the binding has since re-rendered to a different node
1393
- if (value.element && current === placeholder) {
1394
- placeholder.replaceWith(value.element);
1395
- current = value.element;
1396
- }
1397
- });
1398
- }
1399
- }
1400
- else {
1401
- // null/undefined/false render as empty text
1402
- const text = (value === null || value === undefined || value === false) ? "" : String(value);
1403
- if (textNode && textNode === current) {
1404
- textNode.textContent = text;
1405
- }
1406
- else {
1407
- const next = document.createTextNode(text);
1408
- current.replaceWith(next);
1409
- current = next;
1410
- textNode = next;
1411
- }
1412
- }
1413
- });
1414
- // stop the effect when the container is removed from the document
1415
- autoDisposeBinding(container, eff);
1416
- };
1417
-
1418
- var TagHelpers = /*#__PURE__*/Object.freeze({
1419
- __proto__: null,
1420
- appendChild: appendChild,
1421
- applyOptions: applyOptions,
1422
- tag: tag
1423
- });
1424
-
1425
- /** Collection of DOM helper functions: element queries/traversal ({@link getById}, {@link queryElement}, {@link nextElement}, ...), class manipulation ({@link addClass}, {@link removeClass}), {@link empty}, and element creation via {@link tag}. */
1426
- const DOM = {
1427
- ...DomHelpers,
1428
- ...TagHelpers
1429
- };
1430
-
1431
- export { Binding, BindingEach, ComputedRef, DOM, EffectScope, EventEmitter, ITERATE_KEY, ReactiveEffect, constants$1 as UICONSTANTS, UIElement, UIElementBound, appendBindingEach, bind, bindEach, computed, destroyUI, effect, effectScope, isReactive, nextTick, reactive, toRaw, track, trigger, untrack };
1
+ export { EventEmitter } from './events.js';
2
+ export { UIElement, UIElementBound, destroyUI, initUICommands } from './element.js';
3
+ export { enableElementExtensions } from './ext.js';
4
+ export { EffectScope, ITERATE_KEY, ReactiveEffect, effect, effectScope, nextTick, track, trigger, untrack } from './reactive/effect.js';
5
+ export { isReactive, reactive, toRaw } from './reactive/reactive.js';
6
+ export { ComputedRef, computed } from './reactive/computed.js';
7
+ export { DOM } from './dom/index.js';
8
+ import * as constants from './constants.js';
9
+ export { constants as UICONSTANTS };
10
+ export { Binding, bind } from './dom/bind.js';
11
+ export { BindingEach, appendBindingEach, bindEach } from './dom/bind-each.js';
1432
12
  //# sourceMappingURL=index.js.map