balises 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -18
- package/dist/balises.esm.js +295 -192
- package/dist/balises.esm.js.map +1 -1
- package/dist/balises.iife.js +295 -192
- package/dist/balises.iife.js.map +1 -1
- package/dist/balises.iife.min.js +1 -1
- package/dist/balises.iife.min.js.map +1 -1
- package/dist/esm/async.d.ts.map +1 -1
- package/dist/esm/async.js +6 -2
- package/dist/esm/async.js.map +1 -1
- package/dist/esm/each.js +29 -10
- package/dist/esm/each.js.map +1 -1
- package/dist/esm/parser.d.ts.map +1 -1
- package/dist/esm/parser.js +7 -13
- package/dist/esm/parser.js.map +1 -1
- package/dist/esm/signals/computed.d.ts +16 -3
- package/dist/esm/signals/computed.d.ts.map +1 -1
- package/dist/esm/signals/computed.js +47 -22
- package/dist/esm/signals/computed.js.map +1 -1
- package/dist/esm/signals/context.d.ts +9 -2
- package/dist/esm/signals/context.d.ts.map +1 -1
- package/dist/esm/signals/context.js.map +1 -1
- package/dist/esm/signals/signal.d.ts +33 -1
- package/dist/esm/signals/signal.d.ts.map +1 -1
- package/dist/esm/signals/signal.js +81 -5
- package/dist/esm/signals/signal.js.map +1 -1
- package/dist/esm/template.d.ts +7 -18
- package/dist/esm/template.d.ts.map +1 -1
- package/dist/esm/template.js +220 -182
- package/dist/esm/template.js.map +1 -1
- package/package.json +2 -2
package/dist/balises.esm.js
CHANGED
|
@@ -97,6 +97,29 @@ function removeFromArray(array, item) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
|
+
* A tracking slot for .is() comparisons.
|
|
101
|
+
* Self-removes from parent map when all targets are gone.
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
var IsSlot = class {
|
|
105
|
+
targets = [];
|
|
106
|
+
#map;
|
|
107
|
+
#key;
|
|
108
|
+
constructor(map, key) {
|
|
109
|
+
this.#map = map;
|
|
110
|
+
this.#key = key;
|
|
111
|
+
}
|
|
112
|
+
deleteTarget(target) {
|
|
113
|
+
removeFromArray(this.targets, target);
|
|
114
|
+
if (!this.targets.length) this.#map.delete(this.#key);
|
|
115
|
+
}
|
|
116
|
+
/** Notify all targets that they may need to recompute */
|
|
117
|
+
notify() {
|
|
118
|
+
const copy = this.targets.slice();
|
|
119
|
+
for (let i = 0; i < copy.length; i++) copy[i].markDirty();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
100
123
|
* A reactive value container. When the value changes, all dependent
|
|
101
124
|
* computeds are marked dirty and subscribers are notified.
|
|
102
125
|
*
|
|
@@ -106,6 +129,7 @@ var Signal = class {
|
|
|
106
129
|
#value;
|
|
107
130
|
#subs = [];
|
|
108
131
|
#targets = [];
|
|
132
|
+
#isSlots;
|
|
109
133
|
constructor(value) {
|
|
110
134
|
this.#value = value;
|
|
111
135
|
}
|
|
@@ -116,12 +140,19 @@ var Signal = class {
|
|
|
116
140
|
}
|
|
117
141
|
set value(v) {
|
|
118
142
|
if (Object.is(this.#value, v)) return;
|
|
143
|
+
const prev = this.#value;
|
|
119
144
|
this.#value = v;
|
|
120
145
|
const targets = this.#targets;
|
|
121
|
-
|
|
146
|
+
const isSlots = this.#isSlots;
|
|
147
|
+
if (!isSlots) for (let i = 0; i < targets.length; i++) targets[i].markDirty();
|
|
148
|
+
else batch(() => {
|
|
149
|
+
isSlots.get(prev)?.notify();
|
|
150
|
+
isSlots.get(v)?.notify();
|
|
151
|
+
for (let i = 0; i < targets.length; i++) targets[i].markDirty();
|
|
152
|
+
});
|
|
122
153
|
if (this.#subs.length) if (isBatching()) enqueueBatchAll(this.#subs);
|
|
123
154
|
else {
|
|
124
|
-
const subs =
|
|
155
|
+
const subs = this.#subs.slice();
|
|
125
156
|
for (let i = 0; i < subs.length; i++) subs[i]();
|
|
126
157
|
}
|
|
127
158
|
}
|
|
@@ -154,6 +185,28 @@ var Signal = class {
|
|
|
154
185
|
peek() {
|
|
155
186
|
return this.#value;
|
|
156
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Check if the signal's value equals the given value.
|
|
190
|
+
* Enables O(1) selection updates - only the old and new matching values
|
|
191
|
+
* trigger recomputes, not all dependents.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* const selected = signal<number | null>(null);
|
|
196
|
+
*
|
|
197
|
+
* // In each row - only 2 rows recompute when selection changes
|
|
198
|
+
* html`<tr class=${() => selected.is(row.id) ? 'danger' : ''}>...`
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
is(value) {
|
|
202
|
+
if (context) {
|
|
203
|
+
const slots = this.#isSlots ?? (this.#isSlots = /* @__PURE__ */ new Map());
|
|
204
|
+
let slot = slots.get(value);
|
|
205
|
+
if (!slot) slots.set(value, slot = new IsSlot(slots, value));
|
|
206
|
+
context.trackSource(slot);
|
|
207
|
+
}
|
|
208
|
+
return Object.is(this.#value, value);
|
|
209
|
+
}
|
|
157
210
|
/** @internal */
|
|
158
211
|
get targets() {
|
|
159
212
|
return this.#targets;
|
|
@@ -190,6 +243,13 @@ var ReadonlySignal = class {
|
|
|
190
243
|
subscribe(fn) {
|
|
191
244
|
return this.#signal.subscribe(fn);
|
|
192
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Check if the signal's value equals the given value.
|
|
248
|
+
* Enables O(1) selection updates.
|
|
249
|
+
*/
|
|
250
|
+
is(value) {
|
|
251
|
+
return this.#signal.is(value);
|
|
252
|
+
}
|
|
193
253
|
};
|
|
194
254
|
|
|
195
255
|
//#endregion
|
|
@@ -212,6 +272,7 @@ var Computed = class {
|
|
|
212
272
|
#targets = [];
|
|
213
273
|
#sources = [];
|
|
214
274
|
#sourceIndex = 0;
|
|
275
|
+
#isSlots;
|
|
215
276
|
constructor(fn) {
|
|
216
277
|
this.#fn = fn;
|
|
217
278
|
this.#recompute();
|
|
@@ -230,12 +291,33 @@ var Computed = class {
|
|
|
230
291
|
dispose() {
|
|
231
292
|
this.#fn = void 0;
|
|
232
293
|
const sources = this.#sources;
|
|
233
|
-
for (let i = 0; i < sources.length; i++)
|
|
234
|
-
const source = sources[i];
|
|
235
|
-
if (source) source.deleteTarget(this);
|
|
236
|
-
}
|
|
294
|
+
for (let i = 0; i < sources.length; i++) sources[i]?.deleteTarget(this);
|
|
237
295
|
this.#sources = [];
|
|
238
296
|
this.#subs.length = 0;
|
|
297
|
+
this.#isSlots?.clear();
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Check if the computed's value equals the given value.
|
|
301
|
+
* Enables O(1) selection updates - only the old and new matching values
|
|
302
|
+
* trigger recomputes, not all dependents.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* ```ts
|
|
306
|
+
* const selected = computed(() => items.find(i => i.active)?.id ?? null);
|
|
307
|
+
*
|
|
308
|
+
* // In each row - only 2 rows recompute when selection changes
|
|
309
|
+
* html`<tr class=${() => selected.is(row.id) ? 'danger' : ''}>...`
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
is(value) {
|
|
313
|
+
if (this.#dirty) this.#recompute();
|
|
314
|
+
if (context) {
|
|
315
|
+
const slots = this.#isSlots ?? (this.#isSlots = /* @__PURE__ */ new Map());
|
|
316
|
+
let slot = slots.get(value);
|
|
317
|
+
if (!slot) slots.set(value, slot = new IsSlot(slots, value));
|
|
318
|
+
context.trackSource(slot);
|
|
319
|
+
}
|
|
320
|
+
return Object.is(this.#value, value);
|
|
239
321
|
}
|
|
240
322
|
/**
|
|
241
323
|
* Called by sources when accessed during recompute.
|
|
@@ -276,20 +358,20 @@ var Computed = class {
|
|
|
276
358
|
const t = targets[j];
|
|
277
359
|
if (!t.#dirty) queue.push(t);
|
|
278
360
|
}
|
|
279
|
-
if (c.#subs.length && c.#fn) toNotify.push(
|
|
280
|
-
c,
|
|
281
|
-
old: c.#value
|
|
282
|
-
});
|
|
361
|
+
if ((c.#subs.length || c.#isSlots?.size) && c.#fn) toNotify.push([c, c.#value]);
|
|
283
362
|
}
|
|
284
363
|
for (let i = 0; i < toNotify.length; i++) {
|
|
285
|
-
const
|
|
364
|
+
const [c, old] = toNotify[i];
|
|
286
365
|
const notify = () => {
|
|
287
|
-
if (c.#fn)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
366
|
+
if (!c.#fn) return;
|
|
367
|
+
c.#recompute();
|
|
368
|
+
if (!Object.is(c.#value, old)) {
|
|
369
|
+
if (c.#isSlots) {
|
|
370
|
+
c.#isSlots.get(old)?.notify();
|
|
371
|
+
c.#isSlots.get(c.#value)?.notify();
|
|
292
372
|
}
|
|
373
|
+
const subs = c.#subs.slice();
|
|
374
|
+
for (let j = 0; j < subs.length; j++) subs[j]();
|
|
293
375
|
}
|
|
294
376
|
};
|
|
295
377
|
isBatching() ? enqueueBatchOne(notify) : notify();
|
|
@@ -317,10 +399,7 @@ var Computed = class {
|
|
|
317
399
|
const newLen = this.#sourceIndex;
|
|
318
400
|
if (newLen < prevLen) {
|
|
319
401
|
const sources = this.#sources;
|
|
320
|
-
for (let i = newLen; i < prevLen; i++)
|
|
321
|
-
const source = sources[i];
|
|
322
|
-
if (source) source.deleteTarget(this);
|
|
323
|
-
}
|
|
402
|
+
for (let i = newLen; i < prevLen; i++) sources[i]?.deleteTarget(this);
|
|
324
403
|
sources.length = newLen;
|
|
325
404
|
}
|
|
326
405
|
this.#dirty = false;
|
|
@@ -588,15 +667,9 @@ var HTMLParser = class {
|
|
|
588
667
|
this.statics = [];
|
|
589
668
|
this.indexes = [];
|
|
590
669
|
}
|
|
591
|
-
isA(c)
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
isT(c) {
|
|
595
|
-
return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "-" || c === ":";
|
|
596
|
-
}
|
|
597
|
-
isW(c) {
|
|
598
|
-
return c <= " " && c !== "";
|
|
599
|
-
}
|
|
670
|
+
isA = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z";
|
|
671
|
+
isT = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9" || c === "-" || c === ":";
|
|
672
|
+
isW = (c) => c <= " " && c !== "";
|
|
600
673
|
};
|
|
601
674
|
|
|
602
675
|
//#endregion
|
|
@@ -613,207 +686,237 @@ var HTMLParser = class {
|
|
|
613
686
|
* - Arrays: ${items.map(i => html`<li>${i}</li>`)}
|
|
614
687
|
*
|
|
615
688
|
* Extend with plugins via html.with(...plugins) for additional interpolation types.
|
|
689
|
+
*
|
|
690
|
+
* Templates are cached by their static string parts - the DOM structure is built
|
|
691
|
+
* once and cloned for subsequent renders, significantly improving performance.
|
|
616
692
|
*/
|
|
617
693
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
694
|
+
/** Template cache - keyed by static string parts identity */
|
|
695
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
618
696
|
/**
|
|
619
|
-
*
|
|
697
|
+
* Wrap a function in a scoped computed.
|
|
620
698
|
* Nested computeds/effects are automatically disposed on re-run.
|
|
621
|
-
*
|
|
622
|
-
* and any nested reactives from the last run.
|
|
699
|
+
* Registers disposal of both the computed and nested reactives.
|
|
623
700
|
*/
|
|
624
|
-
function
|
|
625
|
-
let
|
|
701
|
+
function wrapFn(fn, d) {
|
|
702
|
+
let cleanup;
|
|
626
703
|
const c = computed(() => {
|
|
627
|
-
|
|
628
|
-
const [
|
|
629
|
-
|
|
630
|
-
return
|
|
704
|
+
cleanup?.();
|
|
705
|
+
const [r, dispose] = scope(fn);
|
|
706
|
+
cleanup = dispose;
|
|
707
|
+
return r;
|
|
631
708
|
});
|
|
632
|
-
|
|
709
|
+
d.push(() => (c.dispose(), cleanup?.()));
|
|
710
|
+
return c;
|
|
633
711
|
}
|
|
634
712
|
/**
|
|
635
713
|
* Bind a value to an update function.
|
|
636
|
-
* If reactive, subscribes and returns unsubscribe. Otherwise returns null.
|
|
637
714
|
* Functions are wrapped in computed() for automatic reactivity.
|
|
638
715
|
* Nested computeds/effects created inside functions are automatically
|
|
639
716
|
* disposed when the function re-runs or the binding is disposed.
|
|
640
717
|
*/
|
|
641
|
-
function bind(
|
|
642
|
-
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
update(
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
718
|
+
function bind(v, update, d) {
|
|
719
|
+
if (typeof v === "function") v = wrapFn(v, d);
|
|
720
|
+
if (isSignal(v)) {
|
|
721
|
+
update(v.value);
|
|
722
|
+
d.push(v.subscribe(() => update(v.value)));
|
|
723
|
+
} else update(v);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Collect nodes for all bindings using a single TreeWalker pass.
|
|
727
|
+
* TreeWalker with filter 129 (SHOW_ELEMENT | SHOW_COMMENT) visits nodes
|
|
728
|
+
* in the same order they were created, matching our nodeIndex counter.
|
|
729
|
+
* Bindings are in document order but may share nodes (multiple attrs).
|
|
730
|
+
*/
|
|
731
|
+
function collectBindingNodes(frag, bindings) {
|
|
732
|
+
if (!bindings.length) return [];
|
|
733
|
+
const result = new Array(bindings.length);
|
|
734
|
+
const walker = document.createTreeWalker(frag, 129);
|
|
735
|
+
let nodeIndex = -1;
|
|
736
|
+
let node = null;
|
|
737
|
+
for (let i = 0; i < bindings.length; i++) {
|
|
738
|
+
const targetIndex = bindings[i][1];
|
|
739
|
+
while (nodeIndex < targetIndex) {
|
|
740
|
+
node = walker.nextNode();
|
|
741
|
+
nodeIndex++;
|
|
742
|
+
}
|
|
743
|
+
result[i] = node;
|
|
744
|
+
}
|
|
745
|
+
return result;
|
|
650
746
|
}
|
|
651
747
|
/** A parsed HTML template. Call render() to create live DOM. */
|
|
652
|
-
var Template = class {
|
|
748
|
+
var Template = class Template {
|
|
749
|
+
#strings;
|
|
750
|
+
#values;
|
|
751
|
+
#plugins;
|
|
653
752
|
constructor(strings, values, plugins = []) {
|
|
654
|
-
this
|
|
655
|
-
this
|
|
656
|
-
this
|
|
753
|
+
this.#strings = strings;
|
|
754
|
+
this.#values = values;
|
|
755
|
+
this.#plugins = plugins;
|
|
657
756
|
}
|
|
658
757
|
/**
|
|
659
758
|
* Parse template and create live DOM.
|
|
660
759
|
* Returns the fragment and a dispose function to clean up subscriptions.
|
|
760
|
+
*
|
|
761
|
+
* Templates are cached by their static string parts - subsequent renders
|
|
762
|
+
* clone the cached DOM structure instead of rebuilding it.
|
|
661
763
|
*/
|
|
662
764
|
render() {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
if (name[0] === ".") {
|
|
680
|
-
if (idx0 != null) {
|
|
681
|
-
const unsub = bind(values[idx0], (v) => {
|
|
682
|
-
el[name.slice(1)] = v;
|
|
683
|
-
});
|
|
684
|
-
if (unsub) disposers.push(unsub);
|
|
685
|
-
}
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
if (!indexes.length) {
|
|
689
|
-
const value = statics[0] ?? "";
|
|
690
|
-
el.setAttribute(name, value);
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
const reactives = [];
|
|
694
|
-
for (const idx of indexes) {
|
|
695
|
-
const v = values[idx];
|
|
696
|
-
if (typeof v === "function") {
|
|
697
|
-
const [c, dispose] = scopedComputed(v);
|
|
698
|
-
values[idx] = c;
|
|
699
|
-
reactives.push(c);
|
|
700
|
-
disposers.push(dispose);
|
|
701
|
-
} else if (isSignal(v)) reactives.push(v);
|
|
702
|
-
}
|
|
703
|
-
const update = () => {
|
|
704
|
-
let result = statics[0];
|
|
705
|
-
for (let i = 0; i < indexes.length; i++) {
|
|
706
|
-
const v = values[indexes[i]];
|
|
707
|
-
const val = isSignal(v) ? v.value : v;
|
|
708
|
-
if (indexes.length === 1 && (val == null || val === false)) {
|
|
709
|
-
el.removeAttribute(name);
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
result += (val === true ? "" : val ?? "") + statics[i + 1];
|
|
713
|
-
}
|
|
714
|
-
el.setAttribute(name, result);
|
|
715
|
-
};
|
|
716
|
-
update();
|
|
717
|
-
for (const s of reactives) disposers.push(s.subscribe(update));
|
|
718
|
-
};
|
|
719
|
-
/**
|
|
720
|
-
* Bind dynamic content at a marker position.
|
|
721
|
-
* Tries plugins first (first match wins), then falls back to default handling.
|
|
722
|
-
*/
|
|
723
|
-
const bindContent = (marker, value) => {
|
|
724
|
-
for (const plugin of plugins) {
|
|
725
|
-
const binder = plugin(value);
|
|
726
|
-
if (binder) {
|
|
727
|
-
const pluginDisposers = [];
|
|
728
|
-
binder(marker, pluginDisposers);
|
|
729
|
-
return () => pluginDisposers.forEach((d) => d());
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
let nodes = [];
|
|
733
|
-
let childDisposers = [];
|
|
734
|
-
const clear = () => {
|
|
735
|
-
childDisposers.forEach((d) => d());
|
|
736
|
-
childDisposers = [];
|
|
737
|
-
nodes.forEach((n) => n.remove());
|
|
738
|
-
nodes = [];
|
|
739
|
-
};
|
|
740
|
-
const update = (v) => {
|
|
741
|
-
clear();
|
|
742
|
-
renderContent(marker, v, nodes, childDisposers);
|
|
743
|
-
};
|
|
744
|
-
const unsub = bind(value, update);
|
|
745
|
-
return () => {
|
|
746
|
-
unsub?.();
|
|
747
|
-
clear();
|
|
748
|
-
};
|
|
749
|
-
};
|
|
750
|
-
parser.parseTemplate(this.strings, {
|
|
751
|
-
onText: (text) => {
|
|
752
|
-
stack.at(-1).append(text);
|
|
753
|
-
},
|
|
754
|
-
onOpenTag: (tag, attrs, selfClosing) => {
|
|
755
|
-
const parent = stack.at(-1);
|
|
765
|
+
let cached = cache.get(this.#strings);
|
|
766
|
+
if (!cached) cache.set(this.#strings, cached = this.#buildPrototype());
|
|
767
|
+
return this.#instantiate(cached);
|
|
768
|
+
}
|
|
769
|
+
/** Build the prototype fragment and collect binding descriptors */
|
|
770
|
+
#buildPrototype() {
|
|
771
|
+
const frag = document.createDocumentFragment();
|
|
772
|
+
const bindings = [];
|
|
773
|
+
const stack = [frag];
|
|
774
|
+
let nodeIndex = 0;
|
|
775
|
+
new HTMLParser().parseTemplate(this.#strings, {
|
|
776
|
+
onText: (t) => stack[stack.length - 1].append(t),
|
|
777
|
+
onOpenTag: (tag, attrs, selfClose) => {
|
|
778
|
+
const parent = stack[stack.length - 1];
|
|
756
779
|
const el = tag === "svg" || tag === "SVG" || parent instanceof Element && parent.namespaceURI === SVG_NS ? document.createElementNS(SVG_NS, tag) : document.createElement(tag);
|
|
757
|
-
|
|
780
|
+
const elIndex = nodeIndex++;
|
|
781
|
+
for (const [name, statics, slots] of attrs) if (!slots.length) el.setAttribute(name, statics[0] ?? "");
|
|
782
|
+
else {
|
|
783
|
+
const c = name[0];
|
|
784
|
+
if (c === "@") bindings.push([
|
|
785
|
+
3,
|
|
786
|
+
elIndex,
|
|
787
|
+
name.slice(1),
|
|
788
|
+
slots[0]
|
|
789
|
+
]);
|
|
790
|
+
else if (c === ".") bindings.push([
|
|
791
|
+
2,
|
|
792
|
+
elIndex,
|
|
793
|
+
name.slice(1),
|
|
794
|
+
slots[0]
|
|
795
|
+
]);
|
|
796
|
+
else bindings.push([
|
|
797
|
+
1,
|
|
798
|
+
elIndex,
|
|
799
|
+
name,
|
|
800
|
+
statics,
|
|
801
|
+
slots
|
|
802
|
+
]);
|
|
803
|
+
}
|
|
758
804
|
parent.appendChild(el);
|
|
759
|
-
if (!
|
|
805
|
+
if (!selfClose) stack.push(el);
|
|
760
806
|
},
|
|
761
807
|
onClose: () => {
|
|
762
808
|
if (stack.length > 1) stack.pop();
|
|
763
809
|
},
|
|
764
|
-
onSlot: (
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
810
|
+
onSlot: (i) => {
|
|
811
|
+
stack[stack.length - 1].appendChild(document.createComment(""));
|
|
812
|
+
bindings.push([
|
|
813
|
+
0,
|
|
814
|
+
nodeIndex++,
|
|
815
|
+
i
|
|
816
|
+
]);
|
|
768
817
|
}
|
|
769
818
|
});
|
|
819
|
+
return [frag, bindings];
|
|
820
|
+
}
|
|
821
|
+
/** Clone the prototype and apply bindings with current values */
|
|
822
|
+
#instantiate([proto, bindings]) {
|
|
823
|
+
const frag = proto.cloneNode(true);
|
|
824
|
+
const disposers = [];
|
|
825
|
+
const values = this.#values;
|
|
826
|
+
const nodes = collectBindingNodes(frag, bindings);
|
|
827
|
+
for (let i = 0; i < bindings.length; i++) {
|
|
828
|
+
const b = bindings[i];
|
|
829
|
+
const node = nodes[i];
|
|
830
|
+
if (b[0] === 0) {
|
|
831
|
+
const value = values[b[2]];
|
|
832
|
+
const t = typeof value;
|
|
833
|
+
if (t === "string" || t === "number" || t === "bigint") {
|
|
834
|
+
const n = document.createTextNode(String(value));
|
|
835
|
+
node.parentNode.insertBefore(n, node);
|
|
836
|
+
} else if (value == null || t === "boolean") {} else this.#bindContent(node, value, disposers);
|
|
837
|
+
} else if (b[0] === 1) {
|
|
838
|
+
const [, , name, statics, slots] = b;
|
|
839
|
+
const resolved = slots.map((s) => {
|
|
840
|
+
const v = values[s];
|
|
841
|
+
return typeof v === "function" ? wrapFn(v, disposers) : v;
|
|
842
|
+
});
|
|
843
|
+
let prev;
|
|
844
|
+
const update = () => {
|
|
845
|
+
let result = statics[0], allNull = true;
|
|
846
|
+
for (let j = 0; j < resolved.length; j++) {
|
|
847
|
+
const val = isSignal(resolved[j]) ? resolved[j].value : resolved[j];
|
|
848
|
+
if (val != null && val !== false) allNull = false;
|
|
849
|
+
result += (val === true ? "" : val ?? "") + statics[j + 1];
|
|
850
|
+
}
|
|
851
|
+
const next = slots.length === 1 && allNull ? null : result;
|
|
852
|
+
if (next !== prev) {
|
|
853
|
+
prev = next;
|
|
854
|
+
if (next === null) node.removeAttribute(name);
|
|
855
|
+
else node.setAttribute(name, next);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
update();
|
|
859
|
+
for (const r of resolved) if (isSignal(r)) disposers.push(r.subscribe(update));
|
|
860
|
+
} else if (b[0] === 2) {
|
|
861
|
+
const [, , name, slot] = b;
|
|
862
|
+
bind(values[slot], (v) => node[name] = v, disposers);
|
|
863
|
+
} else {
|
|
864
|
+
const [, , name, slot] = b;
|
|
865
|
+
const handler = values[slot];
|
|
866
|
+
node.addEventListener(name, handler);
|
|
867
|
+
disposers.push(() => node.removeEventListener(name, handler));
|
|
868
|
+
}
|
|
869
|
+
}
|
|
770
870
|
return {
|
|
771
|
-
fragment,
|
|
772
|
-
dispose: () => disposers.forEach((
|
|
871
|
+
fragment: frag,
|
|
872
|
+
dispose: () => disposers.forEach((f) => f())
|
|
773
873
|
};
|
|
774
874
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
childDisposers
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
875
|
+
/** Bind content slot - handles plugins, templates, arrays, and reactive values */
|
|
876
|
+
#bindContent(marker, value, disposers) {
|
|
877
|
+
for (const plugin of this.#plugins) {
|
|
878
|
+
const binder = plugin(value);
|
|
879
|
+
if (binder) {
|
|
880
|
+
binder(marker, disposers);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
let currentNodes = [], childDisposers = [];
|
|
885
|
+
const clear = () => {
|
|
886
|
+
childDisposers.forEach((f) => f());
|
|
887
|
+
childDisposers = [];
|
|
888
|
+
currentNodes.forEach((n) => n.remove());
|
|
889
|
+
currentNodes = [];
|
|
890
|
+
};
|
|
891
|
+
const update = (v) => {
|
|
892
|
+
if (v != null && typeof v !== "boolean" && typeof v !== "object" && currentNodes.length === 1 && !childDisposers.length && currentNodes[0] instanceof Text) {
|
|
893
|
+
currentNodes[0].textContent = String(v);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
clear();
|
|
897
|
+
const parent = marker.parentNode;
|
|
898
|
+
const items = Array.isArray(v) ? v.flat() : [v];
|
|
899
|
+
for (const item of items) if (item instanceof Template) {
|
|
900
|
+
const { fragment, dispose } = item.render();
|
|
901
|
+
childDisposers.push(dispose);
|
|
902
|
+
currentNodes.push(...fragment.childNodes);
|
|
903
|
+
parent.insertBefore(fragment, marker);
|
|
904
|
+
} else if (item != null && typeof item !== "boolean") {
|
|
905
|
+
const n = document.createTextNode(String(item));
|
|
906
|
+
currentNodes.push(n);
|
|
907
|
+
parent.insertBefore(n, marker);
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
bind(value, update, disposers);
|
|
911
|
+
disposers.push(clear);
|
|
791
912
|
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
* Create an html tag function with the given plugins.
|
|
795
|
-
*/
|
|
796
|
-
function createHtmlWithPlugins(plugins) {
|
|
913
|
+
};
|
|
914
|
+
function createHtml(plugins) {
|
|
797
915
|
const tag = ((strings, ...values) => new Template(strings, values, plugins));
|
|
798
|
-
tag.with = (...
|
|
916
|
+
tag.with = (...more) => createHtml([...plugins, ...more]);
|
|
799
917
|
return tag;
|
|
800
918
|
}
|
|
801
|
-
|
|
802
|
-
* Tagged template literal for creating reactive HTML templates.
|
|
803
|
-
* Use .with(...plugins) to add interpolation handlers like each() or async generators.
|
|
804
|
-
*
|
|
805
|
-
* @example
|
|
806
|
-
* ```ts
|
|
807
|
-
* import { html } from "balises";
|
|
808
|
-
* import eachPlugin, { each } from "balises/each";
|
|
809
|
-
* import asyncPlugin from "balises/async";
|
|
810
|
-
*
|
|
811
|
-
* const html = baseHtml.with(eachPlugin, asyncPlugin);
|
|
812
|
-
*
|
|
813
|
-
* html`<div>${async function* () { ... }}</div>`.render();
|
|
814
|
-
* ```
|
|
815
|
-
*/
|
|
816
|
-
const html = createHtmlWithPlugins([]);
|
|
919
|
+
const html = createHtml([]);
|
|
817
920
|
|
|
818
921
|
//#endregion
|
|
819
922
|
export { batch, computed, effect, html, scope, signal, store };
|