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