effectweb 0.2.3 → 0.3.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 +93 -1
- package/dist/AsyncContent.d.ts +2 -2
- package/dist/AsyncContent.js +6 -3
- package/dist/actions.d.ts +4 -3
- package/dist/actions.js +1 -0
- package/dist/cache-internals.d.ts +14 -0
- package/dist/cache-internals.js +11 -0
- package/dist/cache.d.ts +17 -14
- package/dist/cache.js +98 -26
- package/dist/collection.d.ts +10 -9
- package/dist/collection.js +32 -6
- package/dist/component.d.ts +8 -7
- package/dist/diagnostics.d.ts +25 -0
- package/dist/diagnostics.js +167 -1
- package/dist/dom-attributes.json +71 -0
- package/dist/dom.d.ts +33 -12
- package/dist/dom.js +466 -80
- package/dist/form.d.ts +61 -0
- package/dist/form.js +98 -0
- package/dist/html-attributes.d.ts +602 -0
- package/dist/html-attributes.js +1 -0
- package/dist/http.d.ts +5 -0
- package/dist/http.js +7 -0
- package/dist/index.d.ts +12 -10
- package/dist/index.js +7 -5
- package/dist/jsx.d.ts +165 -33
- package/dist/jsx.js +2 -1
- package/dist/lazy.d.ts +23 -0
- package/dist/lazy.js +58 -0
- package/dist/load.d.ts +3 -3
- package/dist/mount.d.ts +8 -11
- package/dist/mount.js +1 -3
- package/dist/owner.d.ts +11 -9
- package/dist/owner.js +27 -36
- package/dist/pages.d.ts +23 -22
- package/dist/pages.js +15 -9
- package/dist/program.d.ts +19 -9
- package/dist/program.js +145 -49
- package/dist/query-internals.d.ts +11 -0
- package/dist/query-internals.js +10 -0
- package/dist/query.d.ts +24 -19
- package/dist/query.js +60 -7
- package/dist/resource.d.ts +4 -10
- package/dist/resource.js +14 -5
- package/dist/runtime.d.ts +1 -2
- package/dist/runtime.js +3 -3
- package/dist/session.d.ts +12 -15
- package/dist/session.js +15 -12
- package/dist/share.d.ts +5 -4
- package/dist/share.js +5 -44
- package/dist/sharing.d.ts +6 -0
- package/dist/sharing.js +93 -0
- package/dist/snapshot.d.ts +16 -5
- package/dist/snapshot.js +6 -7
- package/dist/svg-attributes.d.ts +12 -0
- package/dist/svg-attributes.js +1 -0
- package/dist/task.d.ts +9 -8
- package/dist/task.js +75 -47
- package/dist/tasks.d.ts +20 -15
- package/dist/tasks.js +39 -25
- package/dist/testing.d.ts +10 -14
- package/package.json +5 -1
package/dist/dom.js
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
|
+
import attributeData from './dom-attributes.json' with { type: 'json' };
|
|
2
|
+
import { validateIdentities } from './collection.js';
|
|
1
3
|
import { runAll, reportError, reportSafely } from './errors.js';
|
|
2
4
|
import { eventEffects } from './effectEvent.js';
|
|
3
5
|
import { traceBinding } from './diagnostics.js';
|
|
4
|
-
import {
|
|
6
|
+
import { shareData } from './sharing.js';
|
|
7
|
+
import { jsxComponent } from './jsx.js';
|
|
5
8
|
import { startMount } from './mount.js';
|
|
6
9
|
const equal = (a, b) => a.length === b.length && a.every((value, index) => Object.is(value, b[index]));
|
|
10
|
+
// A model publication reconciles DOM first and controlled dependent properties second.
|
|
11
|
+
// Nested child scopes join the same synchronous commit; there is no reactive graph.
|
|
12
|
+
let commitDepth = 0;
|
|
13
|
+
const controlCommits = new Set();
|
|
14
|
+
function commitDom(work) {
|
|
15
|
+
commitDepth++;
|
|
16
|
+
try {
|
|
17
|
+
return work();
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
if (--commitDepth === 0) {
|
|
21
|
+
for (const apply of controlCommits) {
|
|
22
|
+
controlCommits.delete(apply);
|
|
23
|
+
apply();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function afterDom(apply) {
|
|
29
|
+
if (commitDepth)
|
|
30
|
+
controlCommits.add(apply);
|
|
31
|
+
else
|
|
32
|
+
apply();
|
|
33
|
+
}
|
|
7
34
|
/** Compiler implementation. No implicit tracking, proxies, or per-binding subscriptions. */
|
|
8
35
|
export class Scope {
|
|
9
36
|
value;
|
|
@@ -27,7 +54,7 @@ export class Scope {
|
|
|
27
54
|
return value;
|
|
28
55
|
const next = dependencies();
|
|
29
56
|
if (!previous || !equal(previous, next)) {
|
|
30
|
-
value = previous ?
|
|
57
|
+
value = previous ? shareData(value, compute()) : compute();
|
|
31
58
|
traceBinding(source, previous, next, 'derive');
|
|
32
59
|
previous = next;
|
|
33
60
|
}
|
|
@@ -54,18 +81,20 @@ export class Scope {
|
|
|
54
81
|
set(value) {
|
|
55
82
|
if (this.disposed)
|
|
56
83
|
return;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
commitDom(() => {
|
|
85
|
+
this.value = value;
|
|
86
|
+
this.revision++;
|
|
87
|
+
for (const job of this.jobs) {
|
|
88
|
+
if (this.disposed)
|
|
89
|
+
break;
|
|
90
|
+
try {
|
|
91
|
+
job();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
reportSafely(this.report, error);
|
|
95
|
+
}
|
|
67
96
|
}
|
|
68
|
-
}
|
|
97
|
+
});
|
|
69
98
|
}
|
|
70
99
|
dispose() {
|
|
71
100
|
if (this.disposed)
|
|
@@ -75,6 +104,14 @@ export class Scope {
|
|
|
75
104
|
runAll(this.cleanups.splice(0).reverse(), this.report);
|
|
76
105
|
}
|
|
77
106
|
}
|
|
107
|
+
/** Compiler marker for mounting a view with an explicit model and message dispatcher. */
|
|
108
|
+
export const ViewBinding = /* @__PURE__ */ Object.assign(function ViewBinding(_props) {
|
|
109
|
+
throw new Error('ViewBinding reached runtime without the EffectWeb JSX compiler');
|
|
110
|
+
}, { [jsxComponent]: true });
|
|
111
|
+
/** Untyped callers get an actionable failure instead of silently losing child messages. */
|
|
112
|
+
export function unboundSend(_message) {
|
|
113
|
+
throw new Error('This view needs a dispatcher. Mount it through ViewBinding with model and send.');
|
|
114
|
+
}
|
|
78
115
|
const contentBrand = Symbol('compiled content');
|
|
79
116
|
/** Compiler marker: declare inside view(), or directly in a compiled component prop. */
|
|
80
117
|
export function slot(_render) {
|
|
@@ -148,8 +185,14 @@ export function view(_render) {
|
|
|
148
185
|
export function compiled(build) {
|
|
149
186
|
return Object.assign(() => {
|
|
150
187
|
throw new Error('Mount compiled views with mountView or inside another compiled view');
|
|
151
|
-
}, { build });
|
|
188
|
+
}, { build, [jsxComponent]: true });
|
|
152
189
|
}
|
|
190
|
+
/** Owned content in an element target, defaulting to the document body. */
|
|
191
|
+
export const Portal = /* @__PURE__ */ compiled((scope) => {
|
|
192
|
+
portal(scope, (child, parent, before) => {
|
|
193
|
+
text(child, parent, before, () => [child.value.children], () => child.value.children);
|
|
194
|
+
}, () => scope.value.mount);
|
|
195
|
+
});
|
|
153
196
|
function markers(parent, before) {
|
|
154
197
|
const start = document.createComment('');
|
|
155
198
|
const end = document.createComment('');
|
|
@@ -210,25 +253,27 @@ function buildFragment(parent) {
|
|
|
210
253
|
return fragment;
|
|
211
254
|
}
|
|
212
255
|
export function mountView(parent, definition, source, options = {}) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
256
|
+
return commitDom(() => {
|
|
257
|
+
const { start, end } = markers(parent, null);
|
|
258
|
+
const scope = new Scope(source.model(), source.send, options.onError);
|
|
259
|
+
let unsubscribe = () => { };
|
|
260
|
+
try {
|
|
261
|
+
const fragment = buildFragment(parent);
|
|
262
|
+
definition.build(scope, fragment, null);
|
|
263
|
+
parent.insertBefore(fragment, end);
|
|
264
|
+
unsubscribe = source.subscribe((model) => scope.set(model));
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
scope.dispose();
|
|
268
|
+
remove(start, end);
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
return () => {
|
|
272
|
+
if (scope.disposed)
|
|
273
|
+
return;
|
|
274
|
+
runAll([unsubscribe, () => scope.dispose(), () => remove(start, end)], scope.report);
|
|
275
|
+
};
|
|
276
|
+
});
|
|
232
277
|
}
|
|
233
278
|
export function element(parent, before, tag) {
|
|
234
279
|
const doc = parent.ownerDocument ?? document;
|
|
@@ -239,6 +284,7 @@ export function element(parent, before, tag) {
|
|
|
239
284
|
return node;
|
|
240
285
|
}
|
|
241
286
|
const classTokens = new WeakMap();
|
|
287
|
+
let booleanAttributes;
|
|
242
288
|
const styleProperties = new WeakMap();
|
|
243
289
|
function scalar(value) {
|
|
244
290
|
if (value == null)
|
|
@@ -251,7 +297,11 @@ function scalar(value) {
|
|
|
251
297
|
throw new Error('DOM values must be scalar. Render objects as compiled child views or domain collections.');
|
|
252
298
|
}
|
|
253
299
|
export function attribute(element, name, value) {
|
|
254
|
-
const
|
|
300
|
+
const aliases = attributeData.aliases;
|
|
301
|
+
const key = aliases[name] ?? name;
|
|
302
|
+
const booleanValues = attributeData.booleanValues;
|
|
303
|
+
if (typeof value === 'boolean')
|
|
304
|
+
value = booleanValues[key]?.[String(value)] ?? value;
|
|
255
305
|
if (name === 'classList') {
|
|
256
306
|
const tokens = (value ?? {});
|
|
257
307
|
const next = new Set();
|
|
@@ -287,31 +337,19 @@ export function attribute(element, name, value) {
|
|
|
287
337
|
if (element.value !== next)
|
|
288
338
|
element.value = next;
|
|
289
339
|
}
|
|
290
|
-
else if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
'
|
|
296
|
-
|
|
297
|
-
'controls',
|
|
298
|
-
'autoplay',
|
|
299
|
-
'loop',
|
|
300
|
-
'muted',
|
|
301
|
-
'playsinline',
|
|
302
|
-
'readonly',
|
|
303
|
-
'required',
|
|
304
|
-
'open',
|
|
305
|
-
'inert',
|
|
306
|
-
'download',
|
|
307
|
-
].includes(name) &&
|
|
308
|
-
typeof value === 'boolean') {
|
|
309
|
-
element.toggleAttribute(name, Boolean(value));
|
|
310
|
-
if (name in element)
|
|
311
|
-
Reflect.set(element, name, Boolean(value));
|
|
340
|
+
else if ((typeof value === 'boolean' || value == null) &&
|
|
341
|
+
(booleanAttributes ??= new Set(attributeData.boolean)).has(key)) {
|
|
342
|
+
element.toggleAttribute(key, Boolean(value));
|
|
343
|
+
const properties = attributeData.properties;
|
|
344
|
+
const property = properties[key] ?? key;
|
|
345
|
+
if (typeof Reflect.get(element, property) === 'boolean')
|
|
346
|
+
Reflect.set(element, property, Boolean(value));
|
|
312
347
|
}
|
|
313
348
|
else if (value == null ||
|
|
314
|
-
(value === false &&
|
|
349
|
+
(value === false &&
|
|
350
|
+
!attributeData.booleanish.includes(key) &&
|
|
351
|
+
!name.startsWith('aria-') &&
|
|
352
|
+
!name.startsWith('data-'))) {
|
|
315
353
|
element.removeAttribute(key);
|
|
316
354
|
}
|
|
317
355
|
else {
|
|
@@ -322,8 +360,12 @@ export function attribute(element, name, value) {
|
|
|
322
360
|
if (key === 'class')
|
|
323
361
|
for (const token of classTokens.get(element) ?? [])
|
|
324
362
|
element.classList.add(token);
|
|
325
|
-
if (name === 'style' && (value == null || typeof value !== 'object'))
|
|
326
|
-
|
|
363
|
+
if (name === 'style' && (value == null || typeof value !== 'object')) {
|
|
364
|
+
if (value == null)
|
|
365
|
+
styleProperties.delete(element);
|
|
366
|
+
else if ('style' in element)
|
|
367
|
+
styleProperties.set(element, new Set(Array.from(element.style)));
|
|
368
|
+
}
|
|
327
369
|
}
|
|
328
370
|
/** Compiler output for attributes whose unchanged value needs no DOM refresh. */
|
|
329
371
|
export function bindAttribute(scope, element, name, dependencies, read, source) {
|
|
@@ -348,13 +390,124 @@ export function bindAttribute(scope, element, name, dependencies, read, source)
|
|
|
348
390
|
previous = next;
|
|
349
391
|
});
|
|
350
392
|
}
|
|
393
|
+
const controlRestorations = new WeakMap();
|
|
394
|
+
/** Handled edits can change a control even when dispatch publishes no new model. */
|
|
395
|
+
export function bindControl(scope, element, name, dependencies, read, source) {
|
|
396
|
+
let composing = false;
|
|
397
|
+
let deferred = false;
|
|
398
|
+
let pending;
|
|
399
|
+
const select = element.tagName === 'SELECT' && name === 'value';
|
|
400
|
+
const write = () => {
|
|
401
|
+
if (scope.disposed)
|
|
402
|
+
return;
|
|
403
|
+
try {
|
|
404
|
+
if (composing)
|
|
405
|
+
deferred = true;
|
|
406
|
+
else
|
|
407
|
+
attribute(element, name, read());
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
reportSafely(scope.report, error);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const apply = () => {
|
|
414
|
+
if (select)
|
|
415
|
+
afterDom(write);
|
|
416
|
+
else
|
|
417
|
+
write();
|
|
418
|
+
};
|
|
419
|
+
let initialized = false;
|
|
420
|
+
let published;
|
|
421
|
+
scope.watch(dependencies, () => {
|
|
422
|
+
const next = read();
|
|
423
|
+
// A coarse dependency can change while this field's published value stays equal.
|
|
424
|
+
// Preserve unfinished blur drafts; handled edits still restore through apply below.
|
|
425
|
+
if (!initialized || !Object.is(published, next)) {
|
|
426
|
+
initialized = true;
|
|
427
|
+
published = next;
|
|
428
|
+
apply();
|
|
429
|
+
}
|
|
430
|
+
}, source);
|
|
431
|
+
if (select) {
|
|
432
|
+
// The selected value may stay equal while its options change on this publication.
|
|
433
|
+
scope.jobs.push(apply);
|
|
434
|
+
// Options may also belong to a separately updating component or a DOM host.
|
|
435
|
+
// Observe only option-affecting mutations; setting selectedness does not mutate attributes.
|
|
436
|
+
const observer = new MutationObserver(apply);
|
|
437
|
+
observer.observe(element, {
|
|
438
|
+
childList: true,
|
|
439
|
+
subtree: true,
|
|
440
|
+
characterData: true,
|
|
441
|
+
attributes: true,
|
|
442
|
+
attributeFilter: ['value', 'selected', 'disabled'],
|
|
443
|
+
});
|
|
444
|
+
scope.cleanups.push(() => {
|
|
445
|
+
observer.disconnect();
|
|
446
|
+
controlCommits.delete(write);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
const restore = () => {
|
|
450
|
+
if (composing) {
|
|
451
|
+
deferred = true;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (pending !== undefined || scope.disposed)
|
|
455
|
+
return;
|
|
456
|
+
// Native dispatch may run microtasks between listeners. A task waits for all
|
|
457
|
+
// handlers (including ancestors) before restoring the latest model value.
|
|
458
|
+
pending = setTimeout(() => {
|
|
459
|
+
pending = undefined;
|
|
460
|
+
if (!scope.disposed) {
|
|
461
|
+
try {
|
|
462
|
+
apply();
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
reportSafely(scope.report, error);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}, 0);
|
|
469
|
+
};
|
|
470
|
+
const start = () => {
|
|
471
|
+
composing = true;
|
|
472
|
+
};
|
|
473
|
+
const end = () => {
|
|
474
|
+
composing = false;
|
|
475
|
+
if (deferred) {
|
|
476
|
+
deferred = false;
|
|
477
|
+
restore();
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
const handled = (type) => {
|
|
481
|
+
// Clicking a text control must not commit its unfinished blur draft.
|
|
482
|
+
if (type !== 'click' || name === 'checked')
|
|
483
|
+
restore();
|
|
484
|
+
};
|
|
485
|
+
let restorations = controlRestorations.get(element);
|
|
486
|
+
if (!restorations)
|
|
487
|
+
controlRestorations.set(element, (restorations = new Set()));
|
|
488
|
+
restorations.add(handled);
|
|
489
|
+
element.addEventListener('compositionstart', start);
|
|
490
|
+
element.addEventListener('compositionend', end);
|
|
491
|
+
scope.cleanups.push(() => {
|
|
492
|
+
clearTimeout(pending);
|
|
493
|
+
restorations.delete(handled);
|
|
494
|
+
if (!restorations.size)
|
|
495
|
+
controlRestorations.delete(element);
|
|
496
|
+
element.removeEventListener('compositionstart', start);
|
|
497
|
+
element.removeEventListener('compositionend', end);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
351
500
|
export function text(scope, parent, before, dependencies, read, source) {
|
|
352
501
|
const node = document.createTextNode('');
|
|
353
502
|
parent.insertBefore(node, before);
|
|
354
503
|
let start;
|
|
355
504
|
let content;
|
|
356
505
|
scope.watch(dependencies, () => {
|
|
357
|
-
|
|
506
|
+
let value = read();
|
|
507
|
+
if (Array.isArray(value)) {
|
|
508
|
+
validateContent(value);
|
|
509
|
+
value = { [contentBrand]: true, definition: arrayContent, value };
|
|
510
|
+
}
|
|
358
511
|
if (isContent(value)) {
|
|
359
512
|
node.data = '';
|
|
360
513
|
if (content?.definition === value.definition) {
|
|
@@ -372,26 +525,93 @@ export function text(scope, parent, before, dependencies, read, source) {
|
|
|
372
525
|
}
|
|
373
526
|
content = {
|
|
374
527
|
definition: value.definition,
|
|
375
|
-
mounted: value.definition.mount(node.parentNode, node, value.value),
|
|
528
|
+
mounted: value.definition.mount(node.parentNode, node, value.value, scope.report),
|
|
376
529
|
};
|
|
377
530
|
return;
|
|
378
531
|
}
|
|
532
|
+
if (value != null && typeof value === 'object')
|
|
533
|
+
throw new Error('Unsupported JSX content. Use compiled views or slots for markup, collections for entities, and an owned DOM host for native nodes.');
|
|
534
|
+
const next = value == null || typeof value === 'boolean' ? '' : scalar(value);
|
|
379
535
|
if (content) {
|
|
380
536
|
content.mounted.dispose();
|
|
381
537
|
content = undefined;
|
|
382
538
|
clear(start, node);
|
|
383
539
|
}
|
|
384
|
-
if (value != null && typeof value === 'object')
|
|
385
|
-
throw new Error('Object rendered as text. Use collection(...).from(items).map(...) for entity lists.');
|
|
386
|
-
const next = value == null || typeof value === 'boolean' ? '' : scalar(value);
|
|
387
540
|
if (node.data !== next)
|
|
388
541
|
node.data = next;
|
|
389
542
|
}, source);
|
|
390
543
|
}
|
|
544
|
+
function validateContent(value, active = new Set()) {
|
|
545
|
+
if (Array.isArray(value)) {
|
|
546
|
+
if (active.has(value))
|
|
547
|
+
throw new TypeError('JSX content arrays cannot contain cycles.');
|
|
548
|
+
active.add(value);
|
|
549
|
+
for (const child of value)
|
|
550
|
+
validateContent(child, active);
|
|
551
|
+
active.delete(value);
|
|
552
|
+
}
|
|
553
|
+
else if (!isContent(value) &&
|
|
554
|
+
value != null &&
|
|
555
|
+
!['string', 'number', 'boolean'].includes(typeof value)) {
|
|
556
|
+
throw new TypeError('Unsupported JSX content. Use an owned DOM host for native nodes and compiled views for objects.');
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const arrayContent = {
|
|
560
|
+
mount: (_parent, before, value, report) => mountArray(report, before, value),
|
|
561
|
+
};
|
|
562
|
+
/** Runtime content arrays are positional; domain lists retain the explicit collection contract. */
|
|
563
|
+
function mountArray(report, before, initial) {
|
|
564
|
+
const cells = [];
|
|
565
|
+
const disposeCell = (cell) => {
|
|
566
|
+
cell.scope.dispose();
|
|
567
|
+
remove(cell.start, cell.end);
|
|
568
|
+
};
|
|
569
|
+
const set = (input) => {
|
|
570
|
+
const values = input;
|
|
571
|
+
validateContent(values);
|
|
572
|
+
while (cells.length > values.length)
|
|
573
|
+
disposeCell(cells.pop());
|
|
574
|
+
for (let index = 0; index < values.length; index++) {
|
|
575
|
+
const cell = cells[index];
|
|
576
|
+
if (cell) {
|
|
577
|
+
if (!Object.is(cell.scope.value, values[index]))
|
|
578
|
+
cell.scope.set(values[index]);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const scope = new Scope(values[index], unboundSend, report);
|
|
582
|
+
const fragment = buildFragment(before.parentNode);
|
|
583
|
+
const range = markers(fragment, null);
|
|
584
|
+
try {
|
|
585
|
+
text(scope, fragment, range.end, () => [scope.value], () => scope.value);
|
|
586
|
+
before.parentNode.insertBefore(fragment, before);
|
|
587
|
+
cells.push({ scope, ...range });
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
scope.dispose();
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
const dispose = () => {
|
|
596
|
+
for (const cell of cells.splice(0))
|
|
597
|
+
disposeCell(cell);
|
|
598
|
+
};
|
|
599
|
+
try {
|
|
600
|
+
set(initial);
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
dispose();
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
return { set, dispose };
|
|
607
|
+
}
|
|
391
608
|
export function event(scope, element, name, handler) {
|
|
392
609
|
let effects;
|
|
393
|
-
const capture = name.endsWith('Capture');
|
|
394
|
-
const
|
|
610
|
+
const capture = name.endsWith('Capture') && name !== 'onGotPointerCapture' && name !== 'onLostPointerCapture';
|
|
611
|
+
const nativeName = name.slice(2, capture ? -7 : undefined);
|
|
612
|
+
const type = nativeName === 'Begin' || nativeName === 'End' || nativeName === 'Repeat'
|
|
613
|
+
? `${nativeName.toLowerCase()}Event`
|
|
614
|
+
: nativeName.toLowerCase();
|
|
395
615
|
const listener = (event) => {
|
|
396
616
|
if (!scope.disposed) {
|
|
397
617
|
try {
|
|
@@ -404,6 +624,13 @@ export function event(scope, element, name, handler) {
|
|
|
404
624
|
catch (error) {
|
|
405
625
|
reportSafely(scope.report, error);
|
|
406
626
|
}
|
|
627
|
+
finally {
|
|
628
|
+
// Restore only after an application handler commits or rejects an edit.
|
|
629
|
+
// The target also covers handlers delegated to an ancestor.
|
|
630
|
+
if (event.target && ['input', 'change', 'blur', 'focusout', 'click'].includes(type))
|
|
631
|
+
for (const restore of controlRestorations.get(event.target) ?? [])
|
|
632
|
+
restore(type);
|
|
633
|
+
}
|
|
407
634
|
}
|
|
408
635
|
};
|
|
409
636
|
element.addEventListener(type, listener, capture);
|
|
@@ -412,6 +639,84 @@ export function event(scope, element, name, handler) {
|
|
|
412
639
|
effects?.dispose();
|
|
413
640
|
});
|
|
414
641
|
}
|
|
642
|
+
/** One handler identity owns its requests, regardless of the JSX attribute spelling. */
|
|
643
|
+
export function bindEvent(scope, element, name, dependencies, read) {
|
|
644
|
+
let current;
|
|
645
|
+
let owned;
|
|
646
|
+
scope.cleanups.push(() => owned?.dispose());
|
|
647
|
+
scope.watch(dependencies, () => {
|
|
648
|
+
const next = read();
|
|
649
|
+
if (next != null && typeof next !== 'function')
|
|
650
|
+
throw new TypeError(`Event ${name} must be a synchronous handler.`);
|
|
651
|
+
if (Object.is(current, next))
|
|
652
|
+
return;
|
|
653
|
+
owned?.dispose();
|
|
654
|
+
owned = undefined;
|
|
655
|
+
current = next;
|
|
656
|
+
if (typeof next === 'function') {
|
|
657
|
+
const handler = next;
|
|
658
|
+
owned = new Scope(next, scope.send, scope.report);
|
|
659
|
+
event(owned, element, name, handler);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
/** Reconcile merged JSX attributes. Each host, event and control owns its cleanup. */
|
|
664
|
+
export function bindAttributes(scope, element, dependencies, read) {
|
|
665
|
+
const bindings = new Map();
|
|
666
|
+
let previous = {};
|
|
667
|
+
const isEvent = (name) => /^on[A-Z]/u.test(name);
|
|
668
|
+
const isControl = (name) => (name === 'value' && ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName)) ||
|
|
669
|
+
(name === 'checked' && element.tagName === 'INPUT');
|
|
670
|
+
scope.cleanups.push(() => {
|
|
671
|
+
runAll([...bindings.values()].reverse().map((binding) => () => binding.dispose()), scope.report);
|
|
672
|
+
bindings.clear();
|
|
673
|
+
});
|
|
674
|
+
scope.watch(dependencies, () => {
|
|
675
|
+
const next = read();
|
|
676
|
+
for (const name of Object.keys(next)) {
|
|
677
|
+
if (['key', 'ref', 'innerHTML', 'children'].includes(name))
|
|
678
|
+
throw new Error(`Spread attribute ${name} is unsupported. Use collections, DOM hosts, or JSX children.`);
|
|
679
|
+
if (isEvent(name) && next[name] != null && typeof next[name] !== 'function')
|
|
680
|
+
throw new Error(`Spread event ${name} must be a synchronous handler.`);
|
|
681
|
+
}
|
|
682
|
+
for (const name of Object.keys(previous)) {
|
|
683
|
+
if (Object.hasOwn(next, name))
|
|
684
|
+
continue;
|
|
685
|
+
bindings.get(name)?.dispose();
|
|
686
|
+
bindings.delete(name);
|
|
687
|
+
if (name !== 'use' && !isEvent(name))
|
|
688
|
+
attribute(element, name, undefined);
|
|
689
|
+
}
|
|
690
|
+
for (const [name, value] of Object.entries(next)) {
|
|
691
|
+
if (name !== 'use' && !isEvent(name) && !isControl(name)) {
|
|
692
|
+
attribute(element, name, value);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
let binding = bindings.get(name);
|
|
696
|
+
if (binding && Object.is(binding.value, value))
|
|
697
|
+
continue;
|
|
698
|
+
if (binding) {
|
|
699
|
+
binding.set(value);
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const owned = new Scope(value, scope.send, scope.report);
|
|
703
|
+
bindings.set(name, owned);
|
|
704
|
+
if (name === 'use')
|
|
705
|
+
attach(owned, element, () => [owned.value], () => owned.value);
|
|
706
|
+
else if (isControl(name))
|
|
707
|
+
bindControl(owned, element, name, () => [owned.value], () => owned.value);
|
|
708
|
+
else
|
|
709
|
+
bindEvent(owned, element, name, () => [owned.value], () => owned.value);
|
|
710
|
+
}
|
|
711
|
+
previous = next;
|
|
712
|
+
});
|
|
713
|
+
if (element.tagName === 'SELECT')
|
|
714
|
+
scope.jobs.push(() => {
|
|
715
|
+
const binding = bindings.get('value');
|
|
716
|
+
if (binding)
|
|
717
|
+
binding.set(binding.value);
|
|
718
|
+
});
|
|
719
|
+
}
|
|
415
720
|
export function branch(scope, parent, before, choose, yes, no) {
|
|
416
721
|
const { start, end } = markers(parent, before);
|
|
417
722
|
let child;
|
|
@@ -478,15 +783,13 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
478
783
|
previousIdentities = [];
|
|
479
784
|
return;
|
|
480
785
|
}
|
|
481
|
-
const identities = items
|
|
786
|
+
const identities = validateIdentities(items, (item, index) => {
|
|
482
787
|
const key = collection ? collection.identity(item, index) : item;
|
|
483
788
|
if (typeof key !== 'string' && typeof key !== 'number')
|
|
484
789
|
throw new Error('Object lists need domain identity. Declare collection(identity) once; do not add JSX keys.');
|
|
485
790
|
return key;
|
|
486
791
|
});
|
|
487
792
|
const keep = new Set(identities);
|
|
488
|
-
if (keep.size !== identities.length)
|
|
489
|
-
throw new Error('Duplicate collection identity. Identity must be unique within the rendered collection.');
|
|
490
793
|
for (const [key, row] of rows) {
|
|
491
794
|
if (keep.has(key))
|
|
492
795
|
continue;
|
|
@@ -497,7 +800,7 @@ export function each(scope, parent, before, read, outer, indexUsed, build) {
|
|
|
497
800
|
for (let index = 0; index < items.length; index++) {
|
|
498
801
|
const key = identities[index];
|
|
499
802
|
const row = rows.get(key);
|
|
500
|
-
const item = row ?
|
|
803
|
+
const item = row ? shareData(row.scope.value[0], items[index]) : items[index];
|
|
501
804
|
if (!row) {
|
|
502
805
|
const fragment = buildFragment(target);
|
|
503
806
|
const range = markers(fragment, null);
|
|
@@ -596,6 +899,53 @@ export function child(scope, parent, before, definition, dependencies, model, se
|
|
|
596
899
|
child.set(next);
|
|
597
900
|
});
|
|
598
901
|
}
|
|
902
|
+
/** A late-bound compiled definition owns one replaceable DOM region. */
|
|
903
|
+
export function viewRegion(scope, parent, before) {
|
|
904
|
+
const { start, end } = markers(parent, before);
|
|
905
|
+
let active;
|
|
906
|
+
let definition;
|
|
907
|
+
scope.jobs.push(() => active?.set(scope.value));
|
|
908
|
+
scope.cleanups.push(() => {
|
|
909
|
+
active?.dispose();
|
|
910
|
+
remove(start, end);
|
|
911
|
+
});
|
|
912
|
+
return (next) => commitDom(() => {
|
|
913
|
+
if (scope.disposed)
|
|
914
|
+
return;
|
|
915
|
+
if (definition === next) {
|
|
916
|
+
active?.set(scope.value);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const previous = active;
|
|
920
|
+
active = undefined;
|
|
921
|
+
definition = undefined;
|
|
922
|
+
previous?.dispose();
|
|
923
|
+
if (scope.disposed)
|
|
924
|
+
return;
|
|
925
|
+
clear(start, end);
|
|
926
|
+
if (!next)
|
|
927
|
+
return;
|
|
928
|
+
const child = new Scope(scope.value, scope.send, scope.report);
|
|
929
|
+
const fragment = buildFragment(end.parentNode);
|
|
930
|
+
active = child;
|
|
931
|
+
definition = next;
|
|
932
|
+
try {
|
|
933
|
+
next.build(child, fragment, null);
|
|
934
|
+
if (scope.disposed || active !== child)
|
|
935
|
+
child.dispose();
|
|
936
|
+
else
|
|
937
|
+
end.parentNode.insertBefore(fragment, end);
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
child.dispose();
|
|
941
|
+
if (active === child) {
|
|
942
|
+
active = undefined;
|
|
943
|
+
definition = undefined;
|
|
944
|
+
}
|
|
945
|
+
throw error;
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
}
|
|
599
949
|
export function attach(scope, element, dependencies, read) {
|
|
600
950
|
let generation = 0;
|
|
601
951
|
let active;
|
|
@@ -616,7 +966,11 @@ export function attach(scope, element, dependencies, read) {
|
|
|
616
966
|
queueMicrotask(() => {
|
|
617
967
|
if (!scope.disposed && token === generation) {
|
|
618
968
|
try {
|
|
619
|
-
|
|
969
|
+
const acquired = startMount(element, mount, scope.report);
|
|
970
|
+
if (scope.disposed || token !== generation)
|
|
971
|
+
acquired.dispose();
|
|
972
|
+
else
|
|
973
|
+
active = acquired;
|
|
620
974
|
}
|
|
621
975
|
catch (error) {
|
|
622
976
|
reportSafely(scope.report, error);
|
|
@@ -625,17 +979,49 @@ export function attach(scope, element, dependencies, read) {
|
|
|
625
979
|
});
|
|
626
980
|
});
|
|
627
981
|
}
|
|
628
|
-
export function portal(scope, build) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
document.body.appendChild(host);
|
|
632
|
-
const child = new Scope(scope.value, scope.send, scope.report);
|
|
982
|
+
export function portal(scope, build, mount = () => undefined) {
|
|
983
|
+
let host;
|
|
984
|
+
let child;
|
|
633
985
|
scope.cleanups.push(() => {
|
|
634
|
-
child
|
|
635
|
-
host
|
|
986
|
+
child?.dispose();
|
|
987
|
+
host?.remove();
|
|
636
988
|
});
|
|
637
|
-
|
|
638
|
-
|
|
989
|
+
const update = () => {
|
|
990
|
+
const target = mount() ?? document.body;
|
|
991
|
+
const svg = svgChildren(target);
|
|
992
|
+
if (!host || (host.namespaceURI === svgNamespace) !== svg) {
|
|
993
|
+
child?.dispose();
|
|
994
|
+
host?.remove();
|
|
995
|
+
if (scope.disposed)
|
|
996
|
+
return;
|
|
997
|
+
host = svg
|
|
998
|
+
? target.ownerDocument.createElementNS(svgNamespace, 'g')
|
|
999
|
+
: target.ownerDocument.createElement('div');
|
|
1000
|
+
host.style.display = 'contents';
|
|
1001
|
+
child = new Scope(scope.value, scope.send, scope.report);
|
|
1002
|
+
target.appendChild(host);
|
|
1003
|
+
build(child, host, null);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (host.parentNode !== target) {
|
|
1007
|
+
const focused = host.ownerDocument.activeElement;
|
|
1008
|
+
const move = Reflect.get(target, 'moveBefore');
|
|
1009
|
+
if (typeof move === 'function' &&
|
|
1010
|
+
target.isConnected &&
|
|
1011
|
+
host.isConnected &&
|
|
1012
|
+
target.ownerDocument === host.ownerDocument)
|
|
1013
|
+
move.call(target, host, null);
|
|
1014
|
+
else
|
|
1015
|
+
target.appendChild(host);
|
|
1016
|
+
if (focused instanceof HTMLElement &&
|
|
1017
|
+
focused.isConnected &&
|
|
1018
|
+
focused.ownerDocument.activeElement !== focused)
|
|
1019
|
+
focused.focus({ preventScroll: true });
|
|
1020
|
+
}
|
|
1021
|
+
child.set(scope.value);
|
|
1022
|
+
};
|
|
1023
|
+
scope.jobs.push(update);
|
|
1024
|
+
update();
|
|
639
1025
|
}
|
|
640
1026
|
// HTML parsing supplies the initial nodes. Unusual placements, such as mounting
|
|
641
1027
|
// an HTML-named view inside SVG, still follow the same namespace rules as element().
|