@webjsdev/core 0.7.64 → 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/dist/webjs-core-browser.js +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +21 -22
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +6 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +6 -2
- package/index.d.ts +7 -3
- package/index.js +6 -2
- package/package.json +1 -1
- package/src/attribute-directives.d.ts +6 -0
- package/src/attribute-directives.js +49 -0
- package/src/attribute-reader.js +37 -3
- package/src/binding-prefixes.js +10 -41
- package/src/component/lifecycle.js +261 -101
- package/src/component/reactive.js +5 -4
- package/src/component.d.ts +11 -5
- package/src/directives.d.ts +2 -0
- package/src/directives.js +2 -0
- package/src/escape.js +12 -2
- package/src/form-action.js +7 -7
- package/src/html.d.ts +4 -0
- package/src/html.js +22 -1
- package/src/optimistic.js +3 -3
- package/src/registry.d.ts +5 -0
- package/src/registry.js +35 -4
- package/src/render-client/adopt-slots.js +245 -0
- package/src/render-client/parts.js +53 -19
- package/src/render-client/reconciler.js +28 -12
- package/src/render-client/template-compiler.js +19 -5
- package/src/render-client.d.ts +2 -1
- package/src/render-server/attrs.js +5 -4
- package/src/render-server/compile.js +433 -0
- package/src/render-server/dsd.js +11 -435
- package/src/render-server/errors.js +84 -0
- package/src/render-server/expand.js +346 -0
- package/src/render-server/html-scan.js +19 -358
- package/src/render-server/served-attrs.js +79 -0
- package/src/render-server/stream.js +11 -13
- package/src/render-server/template-renderer.js +13 -936
- package/src/render-server/walk.js +798 -0
- package/src/router-client/anchors.js +3 -1
- package/src/router-client/boundaries.js +15 -1
- package/src/router-client/constants.js +30 -0
- package/src/router-client/fetch-apply.js +21 -0
- package/src/router-client/frames.js +6 -3
- package/src/router-client/head-merge.js +58 -10
- package/src/router-client/navigator.js +4 -0
- package/src/router-client/owned-styles.js +372 -0
- package/src/router-client/prefetch.js +3 -0
- package/src/router-client/snapshot-cache.js +2 -12
- package/src/router-client/stylesheets.js +191 -0
- package/src/router-client/swap.js +49 -9
- package/src/sentinels.d.ts +2 -0
- package/src/sentinels.js +4 -0
- package/src/slot.js +1 -1
- package/src/webjs-config.d.ts +1 -1
- package/src/render-server/slots.js +0 -213
package/src/directives.d.ts
CHANGED
|
@@ -96,3 +96,5 @@ export function asyncReplace<T>(
|
|
|
96
96
|
export function isAsyncReplace(x: unknown): x is AsyncReplaceDirective;
|
|
97
97
|
export function watch<T>(sig: { get(): T; __isSignal: true }): WatchDirective<T>;
|
|
98
98
|
export function isWatch(x: unknown): x is WatchDirective;
|
|
99
|
+
|
|
100
|
+
export { classMap, styleMap, ifDefined } from './attribute-directives.js';
|
package/src/directives.js
CHANGED
package/src/escape.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
// Both escapers skip the three replaces when the string carries none of the
|
|
2
|
+
// characters they rewrite, which is the common case for a template value and
|
|
3
|
+
// was measurable on the SSR hot path (#1646).
|
|
4
|
+
const TEXT_SPECIAL = /[&<>]/;
|
|
5
|
+
const ATTR_SPECIAL = /[&"<]/;
|
|
6
|
+
|
|
1
7
|
/** @param {string} s */
|
|
2
8
|
export function escapeText(s) {
|
|
3
|
-
|
|
9
|
+
const str = String(s);
|
|
10
|
+
if (!TEXT_SPECIAL.test(str)) return str;
|
|
11
|
+
return str
|
|
4
12
|
.replace(/&/g, '&')
|
|
5
13
|
.replace(/</g, '<')
|
|
6
14
|
.replace(/>/g, '>');
|
|
@@ -8,7 +16,9 @@ export function escapeText(s) {
|
|
|
8
16
|
|
|
9
17
|
/** @param {string} s */
|
|
10
18
|
export function escapeAttr(s) {
|
|
11
|
-
|
|
19
|
+
const str = String(s);
|
|
20
|
+
if (!ATTR_SPECIAL.test(str)) return str;
|
|
21
|
+
return str
|
|
12
22
|
.replace(/&/g, '&')
|
|
13
23
|
.replace(/"/g, '"')
|
|
14
24
|
.replace(/</g, '<');
|
package/src/form-action.js
CHANGED
|
@@ -87,10 +87,10 @@ import { escapeAttr } from './escape.js';
|
|
|
87
87
|
* it looks like from any one file and are easy to change one at a time. That
|
|
88
88
|
* is why the predicates live here rather than at each site.
|
|
89
89
|
*
|
|
90
|
-
*
|
|
91
|
-
* `renderToStream(v, { ssr: false })`,
|
|
92
|
-
*
|
|
93
|
-
*
|
|
90
|
+
* Since #1646 there is ONE SSR interpreter (`render-server/walk.js`), shared
|
|
91
|
+
* by the buffered page render and `renderToStream(v, { ssr: false })`, so the
|
|
92
|
+
* two can no longer drift; the predicates still live here because the CLIENT
|
|
93
|
+
* renderer applies the same rules.
|
|
94
94
|
*/
|
|
95
95
|
|
|
96
96
|
/**
|
|
@@ -967,9 +967,9 @@ function ensureIdentityField(form, id) {
|
|
|
967
967
|
/**
|
|
968
968
|
* SSR: judge one submitter's emitted START TAG, at its `>`.
|
|
969
969
|
*
|
|
970
|
-
* Called from
|
|
971
|
-
*
|
|
972
|
-
* and not
|
|
970
|
+
* Called from the one SSR interpreter (`render-server/walk.js`, #1646); there
|
|
971
|
+
* used to be two hand-synchronised state machines, which is the failure #1154
|
|
972
|
+
* had once (a guard shipped in one and not the other). Judged at the `>` rather than at the hole
|
|
973
973
|
* because every input is an attribute the author may have written AFTER the
|
|
974
974
|
* binding.
|
|
975
975
|
*
|
package/src/html.d.ts
CHANGED
|
@@ -2,8 +2,12 @@ export interface TemplateResult {
|
|
|
2
2
|
_$webjs: 'template';
|
|
3
3
|
strings: TemplateStringsArray | string[];
|
|
4
4
|
values: unknown[];
|
|
5
|
+
/** Set by the `svg` tag: the client parses the strings inside an `<svg>` wrapper. */
|
|
6
|
+
svg?: boolean;
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
export function html(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult;
|
|
10
|
+
/** lit's `svg` tag: a fragment parsed as SVG on the client, rendered like `html` on the server. */
|
|
11
|
+
export function svg(strings: TemplateStringsArray | string[], ...values: unknown[]): TemplateResult;
|
|
8
12
|
export function isTemplate(x: unknown): x is TemplateResult;
|
|
9
13
|
export const MARKER: 'wjm-';
|
package/src/html.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A TemplateResult is an isomorphic description of HTML: the static string
|
|
5
5
|
* pieces plus the dynamic values. Server and client renderers both consume it.
|
|
6
6
|
*
|
|
7
|
-
* @typedef {{ _$webjs: 'template', strings: TemplateStringsArray | string[], values: unknown[] }} TemplateResult
|
|
7
|
+
* @typedef {{ _$webjs: 'template', strings: TemplateStringsArray | string[], values: unknown[], svg?: boolean }} TemplateResult
|
|
8
8
|
*
|
|
9
9
|
* @param {TemplateStringsArray | string[]} strings
|
|
10
10
|
* @param {...unknown} values
|
|
@@ -14,6 +14,27 @@ export function html(strings, ...values) {
|
|
|
14
14
|
return { _$webjs: 'template', strings, values };
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Tagged template literal for an SVG FRAGMENT, lit's `svg` tag.
|
|
19
|
+
*
|
|
20
|
+
* Use it for markup that sits INSIDE an `<svg>` element but is built in its
|
|
21
|
+
* own template (a `<rect>` per data point, a `<path>` a helper returns).
|
|
22
|
+
* An `html` template is parsed as HTML on the client, so a `<rect>` there
|
|
23
|
+
* becomes an HTML element and never paints. An `svg` template is parsed
|
|
24
|
+
* inside an `<svg>` wrapper, so its elements are SVG elements. A whole
|
|
25
|
+
* `<svg>` element still belongs in `html`.
|
|
26
|
+
*
|
|
27
|
+
* The server renders it exactly like `html` (markup is text there), so the
|
|
28
|
+
* two differ only in how the client parses the static strings.
|
|
29
|
+
*
|
|
30
|
+
* @param {TemplateStringsArray | string[]} strings
|
|
31
|
+
* @param {...unknown} values
|
|
32
|
+
* @returns {TemplateResult}
|
|
33
|
+
*/
|
|
34
|
+
export function svg(strings, ...values) {
|
|
35
|
+
return { _$webjs: 'template', strings, values, svg: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
/**
|
|
18
39
|
* Identity check for TemplateResult.
|
|
19
40
|
* @param {unknown} x
|
package/src/optimistic.js
CHANGED
|
@@ -44,7 +44,7 @@ class OptimisticState {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
async function
|
|
47
|
+
async function runImperativeOptimistic(signal, value, action) {
|
|
48
48
|
const prev = signal.get();
|
|
49
49
|
signal.set(value);
|
|
50
50
|
let result;
|
|
@@ -105,7 +105,7 @@ async function runLegacyOptimistic(signal, value, action) {
|
|
|
105
105
|
* 4. The returned `release()` fn removes the update by ID and re-renders.
|
|
106
106
|
* 5. Concurrent updates stack; each release removes only its own entry.
|
|
107
107
|
*
|
|
108
|
-
*
|
|
108
|
+
* Imperative form (signal-based rollback), for a one-shot toggle:
|
|
109
109
|
* await optimistic(signal, value, () => likePost(postId));
|
|
110
110
|
*
|
|
111
111
|
* Client-only: it calls `host.requestUpdate()` (client work), so a component
|
|
@@ -119,7 +119,7 @@ async function runLegacyOptimistic(signal, value, action) {
|
|
|
119
119
|
*/
|
|
120
120
|
export function optimistic(first, second, third) {
|
|
121
121
|
if (first && typeof first.get === 'function' && typeof first.set === 'function') {
|
|
122
|
-
return
|
|
122
|
+
return runImperativeOptimistic(first, second, third);
|
|
123
123
|
}
|
|
124
124
|
return new OptimisticState(first, second);
|
|
125
125
|
}
|
package/src/registry.d.ts
CHANGED
|
@@ -15,3 +15,8 @@ export function lookupModuleUrl(tag: string): string | undefined;
|
|
|
15
15
|
export function isLazy(tag: string): boolean;
|
|
16
16
|
export function tagOf(cls: WebComponentInstanceConstructor): string | undefined;
|
|
17
17
|
export function allTags(): string[];
|
|
18
|
+
/**
|
|
19
|
+
* The registry's tag-set version: bumped on a first-time registration, so a
|
|
20
|
+
* memo keyed on it (the SSR scanner's tag regex) is valid while it is unchanged.
|
|
21
|
+
*/
|
|
22
|
+
export function registryVersion(): number;
|
package/src/registry.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* user-app: ThemeToggle.register('theme-toggle') → instance-A registry
|
|
32
32
|
* server: lookup('theme-toggle') → instance-B registry (empty)
|
|
33
33
|
*
|
|
34
|
-
* SSR's `
|
|
34
|
+
* The SSR walk's `lookup(tag)` would then find no class and emit bare
|
|
35
35
|
* `<theme-toggle></theme-toggle>` even though the user registered it
|
|
36
36
|
* correctly. Symptom: components render with no children server-side
|
|
37
37
|
* and only "appear" after the browser hydrates.
|
|
@@ -50,6 +50,21 @@ const registry = _g[REGISTRY_KEY] || (_g[REGISTRY_KEY] = new Map());
|
|
|
50
50
|
/** @type {WeakMap<Function, string>} */
|
|
51
51
|
const classToTag = _g[CLASS_TO_TAG_KEY] || (_g[CLASS_TO_TAG_KEY] = new WeakMap());
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The registry's tag-set version (#1644): bumped whenever a NEW key enters
|
|
55
|
+
* the registry, by a class registration OR by `primeModuleUrl` (the prod
|
|
56
|
+
* component scan creates the entry before the class arrives), so the SSR
|
|
57
|
+
* scanner can memoize the regex it builds from every registered tag and
|
|
58
|
+
* rebuild it only when the set changed. Shared on
|
|
59
|
+
* `globalThis` like the registry itself, because under `bun --hot` the app
|
|
60
|
+
* imports a fresh copy of this module while the first run's server keeps
|
|
61
|
+
* rendering through the old one; a copy-local counter would leave that server
|
|
62
|
+
* blind to a tag the new copy registered.
|
|
63
|
+
* @type {{ v: number }}
|
|
64
|
+
*/
|
|
65
|
+
const REGISTRY_VERSION_KEY = Symbol.for('webjs:registryVersion');
|
|
66
|
+
const version = _g[REGISTRY_VERSION_KEY] || (_g[REGISTRY_VERSION_KEY] = { v: 0 });
|
|
67
|
+
|
|
53
68
|
const isBrowser =
|
|
54
69
|
typeof window !== 'undefined' && typeof customElements !== 'undefined';
|
|
55
70
|
|
|
@@ -76,6 +91,8 @@ function registerInternal(tag, cls) {
|
|
|
76
91
|
entry.lazy = lazy;
|
|
77
92
|
} else {
|
|
78
93
|
registry.set(tag, { cls, moduleUrl: null, lazy, tag });
|
|
94
|
+
// A new tag changes the set the SSR scanner matches on (#1644).
|
|
95
|
+
version.v += 1;
|
|
79
96
|
}
|
|
80
97
|
classToTag.set(cls, tag);
|
|
81
98
|
}
|
|
@@ -126,9 +143,10 @@ if (!isBrowser) {
|
|
|
126
143
|
* ------------------------------------------------------------------ */
|
|
127
144
|
|
|
128
145
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
146
|
+
* Register a component class under a tag. The primitive behind
|
|
147
|
+
* `Class.register('tag')` (the app-facing idiom): `customElements.define` in
|
|
148
|
+
* the browser, the SSR registry on the server. Reachable as
|
|
149
|
+
* `@webjsdev/core/registry` for framework tooling; apps use the static method.
|
|
132
150
|
*
|
|
133
151
|
* @param {string} tag
|
|
134
152
|
* @param {typeof import('./component.js').WebComponent} cls
|
|
@@ -163,6 +181,10 @@ export function primeModuleUrl(tag, moduleUrl) {
|
|
|
163
181
|
lazy: false,
|
|
164
182
|
tag,
|
|
165
183
|
});
|
|
184
|
+
// A primed tag is a new KEY, and `allTags()` lists keys: the SSR scanner's
|
|
185
|
+
// pattern must include it before its class arrives, or a pattern memoized
|
|
186
|
+
// while the registry was empty would stay empty for the process (#1644).
|
|
187
|
+
version.v += 1;
|
|
166
188
|
}
|
|
167
189
|
|
|
168
190
|
/** @param {string} tag */
|
|
@@ -188,3 +210,12 @@ export function tagOf(cls) {
|
|
|
188
210
|
export function allTags() {
|
|
189
211
|
return [...registry.keys()];
|
|
190
212
|
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The current tag-set version: moves only when a tag is registered for the
|
|
216
|
+
* first time. A memo keyed on it is valid for as long as it is unchanged.
|
|
217
|
+
* @returns {number}
|
|
218
|
+
*/
|
|
219
|
+
export function registryVersion() {
|
|
220
|
+
return version.v;
|
|
221
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hydration slot adoption: keep a light-DOM host's SSR-projected children in
|
|
3
|
+
* the document while its first client render replaces the SSR markup (#1666).
|
|
4
|
+
*
|
|
5
|
+
* Hydration renders the template from a fresh clone and swaps it in for the
|
|
6
|
+
* SSR markup. The projected children sit inside the SSR `<slot>`, so swapping
|
|
7
|
+
* the SSR tree out took them out of the document, and the first slot apply put
|
|
8
|
+
* them back a microtask later. A removal is not free: the HTML spec navigates
|
|
9
|
+
* a re-inserted iframe again (and `loading="lazy"` defers only the FIRST
|
|
10
|
+
* insertion, so a hidden lazy frame loads), and every nested custom element
|
|
11
|
+
* disconnects and reconnects.
|
|
12
|
+
*
|
|
13
|
+
* So for each of the template's own slots whose SSR counterpart holds exactly
|
|
14
|
+
* the nodes the host's record adopted, the SSR slot and its ancestor chain up
|
|
15
|
+
* to the host are KEPT: the parts that target a fresh element on that chain
|
|
16
|
+
* are pointed at its live twin, and the fresh siblings around the chain are
|
|
17
|
+
* inserted into the live elements instead of the live tree being replaced. No
|
|
18
|
+
* kept node moves, so the projected children never leave the document. The
|
|
19
|
+
* fresh slot's cloned fallback goes to the slot's holding fragment, where the
|
|
20
|
+
* slot part would have put it.
|
|
21
|
+
*
|
|
22
|
+
* Conservative by construction: a slot is adopted only when every link of
|
|
23
|
+
* both chains matches by tag, no link is a custom element (whose SSR markup is
|
|
24
|
+
* its own rendered template) or a `<form>` (form-action reconciliation inserts
|
|
25
|
+
* children into it), the slot's name is static, and the kept nodes sit in the
|
|
26
|
+
* same relative order on both sides. Anything else, a slot inside a
|
|
27
|
+
* conditional template among them, falls back to the plain swap.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
import {
|
|
32
|
+
PROJECTION_ACTUAL, PROJECTION_ATTR, SLOT_FALLBACK_FRAG, SLOT_OWNER_ATTR,
|
|
33
|
+
ensureSlotState, hasSlotState, keyOfName,
|
|
34
|
+
} from '../slot.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Live SSR slots a hydration render adopted. The slot part's finalize reads
|
|
38
|
+
* it to leave the slot's children (the projected nodes) in place instead of
|
|
39
|
+
* harvesting them as fallback, since the holding fragment is already set.
|
|
40
|
+
* @type {WeakSet<Element>}
|
|
41
|
+
*/
|
|
42
|
+
export const adoptedSlots = new WeakSet();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {{ map: Map<Node, Element>, slots: [Element, Element][], chains: [Element, Element][] }} AdoptionPlan
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Plan the adoption for a hydration render, after the parts are bound and
|
|
50
|
+
* before any is applied. Returns null when no slot qualifies.
|
|
51
|
+
*
|
|
52
|
+
* @param {Element | DocumentFragment | ShadowRoot} container
|
|
53
|
+
* @param {DocumentFragment} frag the fresh clone
|
|
54
|
+
* @param {any[]} bound the template's bound parts
|
|
55
|
+
* @returns {AdoptionPlan | null}
|
|
56
|
+
*/
|
|
57
|
+
export function planSlotAdoption(container, frag, bound) {
|
|
58
|
+
if (/** @type {any} */ (container).nodeType !== 1 || !hasSlotState(/** @type {Element} */ (container))) return null;
|
|
59
|
+
const state = ensureSlotState(/** @type {Element} */ (container));
|
|
60
|
+
/** @type {Set<Element>} */
|
|
61
|
+
const dynamicName = new Set();
|
|
62
|
+
for (const p of bound) {
|
|
63
|
+
if ((p.kind === 'attr' || p.kind === 'attr-mixed' || p.kind === 'bool') && p.name === 'name') dynamicName.add(p.el);
|
|
64
|
+
}
|
|
65
|
+
/** @type {Map<Node, Element>} fresh element to its live twin */
|
|
66
|
+
const map = new Map();
|
|
67
|
+
/** @type {Set<Element>} */
|
|
68
|
+
const usedLive = new Set();
|
|
69
|
+
/** @type {[Element, Element][]} */
|
|
70
|
+
const slots = [];
|
|
71
|
+
/** @type {[Element, Element][]} */
|
|
72
|
+
const chains = [];
|
|
73
|
+
for (const p of bound) {
|
|
74
|
+
if (p.kind !== 'slot') continue;
|
|
75
|
+
const slotF = /** @type {Element} */ (p.slotEl);
|
|
76
|
+
if (dynamicName.has(slotF)) continue;
|
|
77
|
+
const assigned = state.assignedByName.get(keyOfName(slotF.getAttribute('name')));
|
|
78
|
+
if (!assigned || assigned.length === 0) continue;
|
|
79
|
+
const slotL = /** @type {Element | null} */ (assigned[0].parentNode);
|
|
80
|
+
if (!slotL || slotL.nodeType !== 1 || slotL.localName !== 'slot' || usedLive.has(slotL)) continue;
|
|
81
|
+
if (slotL.getAttribute(PROJECTION_ATTR) !== PROJECTION_ACTUAL) continue;
|
|
82
|
+
if (!sameNodes(slotL.childNodes, assigned)) continue;
|
|
83
|
+
const fChain = ancestorsUpTo(slotF, frag);
|
|
84
|
+
const lChain = ancestorsUpTo(slotL, container);
|
|
85
|
+
if (!fChain || !lChain || fChain.length !== lChain.length) continue;
|
|
86
|
+
let ok = true;
|
|
87
|
+
for (let i = 0; i < fChain.length && ok; i++) {
|
|
88
|
+
const f = fChain[i];
|
|
89
|
+
const l = lChain[i];
|
|
90
|
+
const name = f.localName;
|
|
91
|
+
if (name !== l.localName || name.includes('-') || name === 'form') ok = false;
|
|
92
|
+
else {
|
|
93
|
+
const prior = map.get(f);
|
|
94
|
+
if (prior ? prior !== l : usedLive.has(l)) ok = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!ok) continue;
|
|
98
|
+
map.set(slotF, slotL);
|
|
99
|
+
usedLive.add(slotL);
|
|
100
|
+
slots.push([slotF, slotL]);
|
|
101
|
+
for (let i = 0; i < fChain.length; i++) {
|
|
102
|
+
if (map.has(fChain[i])) continue;
|
|
103
|
+
map.set(fChain[i], lChain[i]);
|
|
104
|
+
usedLive.add(lChain[i]);
|
|
105
|
+
chains.push([fChain[i], lChain[i]]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (slots.length === 0) return null;
|
|
109
|
+
// No kept node may have to move: under each kept parent the kept children
|
|
110
|
+
// must already sit in the order the template puts them in.
|
|
111
|
+
for (const fParent of [frag, ...chains.map(([f]) => f)]) {
|
|
112
|
+
let last = null;
|
|
113
|
+
for (const child of fParent.childNodes) {
|
|
114
|
+
const live = map.get(child);
|
|
115
|
+
if (!live) continue;
|
|
116
|
+
if (last && !(last.compareDocumentPosition(live) & Node.DOCUMENT_POSITION_FOLLOWING)) return null;
|
|
117
|
+
last = live;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { map, slots, chains };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Point every bound part that targets a kept fresh element at its live twin,
|
|
125
|
+
* and give each kept element the fresh one's static attributes. Runs before
|
|
126
|
+
* the parts apply, so every dynamic value lands on the live element directly.
|
|
127
|
+
*
|
|
128
|
+
* @param {AdoptionPlan} plan
|
|
129
|
+
* @param {any[]} bound
|
|
130
|
+
*/
|
|
131
|
+
export function retargetParts(plan, bound) {
|
|
132
|
+
/** @type {Map<Element, Set<string>>} attributes a part owns, per live element */
|
|
133
|
+
const owned = new Map();
|
|
134
|
+
for (const p of bound) {
|
|
135
|
+
if (p.kind === 'slot') {
|
|
136
|
+
const live = plan.map.get(p.slotEl);
|
|
137
|
+
if (live) p.slotEl = live;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!p.el) continue;
|
|
141
|
+
const live = plan.map.get(p.el);
|
|
142
|
+
if (!live) continue;
|
|
143
|
+
if (p.kind === 'event') {
|
|
144
|
+
p.el.removeEventListener(p.name, p.dispatcher);
|
|
145
|
+
live.addEventListener(p.name, p.dispatcher);
|
|
146
|
+
}
|
|
147
|
+
p.el = live;
|
|
148
|
+
if (p.kind === 'attr' || p.kind === 'attr-mixed' || p.kind === 'bool') {
|
|
149
|
+
let names = owned.get(live);
|
|
150
|
+
if (!names) owned.set(live, names = new Set());
|
|
151
|
+
names.add(p.name);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const [fresh, live] of [...plan.chains, ...plan.slots]) {
|
|
155
|
+
const isSlot = live.localName === 'slot';
|
|
156
|
+
const names = owned.get(live);
|
|
157
|
+
for (const a of Array.from(live.attributes)) {
|
|
158
|
+
if (fresh.hasAttribute(a.name) || names?.has(a.name)) continue;
|
|
159
|
+
// The SSR projection stamps stay: the slot IS projected, and the owner
|
|
160
|
+
// carrier is what isOwnSlot reads until the owner symbol is stamped.
|
|
161
|
+
if (isSlot && (a.name === PROJECTION_ATTR || a.name === SLOT_OWNER_ATTR)) continue;
|
|
162
|
+
live.removeAttribute(a.name);
|
|
163
|
+
}
|
|
164
|
+
for (const a of Array.from(fresh.attributes)) {
|
|
165
|
+
if (live.getAttribute(a.name) !== a.value) live.setAttribute(a.name, a.value);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Commit the fresh template into the container around the kept live nodes:
|
|
172
|
+
* every live child that is not kept is removed, every fresh node is inserted
|
|
173
|
+
* in template order, and no kept node moves. The fresh slots' cloned fallback
|
|
174
|
+
* goes to each kept slot's holding fragment.
|
|
175
|
+
*
|
|
176
|
+
* @param {AdoptionPlan} plan
|
|
177
|
+
* @param {Element} container
|
|
178
|
+
* @param {Node[]} top the container's new children, bookends included
|
|
179
|
+
*/
|
|
180
|
+
export function commitAdoption(plan, container, top) {
|
|
181
|
+
const mapped = (/** @type {Node} */ n) => plan.map.get(n) || n;
|
|
182
|
+
const state = ensureSlotState(container);
|
|
183
|
+
for (const [fresh, live] of plan.chains) mergeChildren(live, Array.from(fresh.childNodes, mapped));
|
|
184
|
+
mergeChildren(container, top.map(mapped));
|
|
185
|
+
for (const [fresh, live] of plan.slots) {
|
|
186
|
+
const fallback = document.createDocumentFragment();
|
|
187
|
+
while (fresh.firstChild) fallback.appendChild(fresh.firstChild);
|
|
188
|
+
/** @type {any} */ (live)[SLOT_FALLBACK_FRAG] = fallback;
|
|
189
|
+
adoptedSlots.add(live);
|
|
190
|
+
// Forget the adopted snapshot so the first apply reports the initial
|
|
191
|
+
// projection (one slotchange), exactly as the plain swap did.
|
|
192
|
+
state.lastSnapshot.delete(live);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Make `parent`'s children exactly `want`, removing what is not wanted and
|
|
198
|
+
* inserting the rest, without moving a node already in place in order.
|
|
199
|
+
*
|
|
200
|
+
* @param {Node} parent
|
|
201
|
+
* @param {Node[]} want
|
|
202
|
+
*/
|
|
203
|
+
function mergeChildren(parent, want) {
|
|
204
|
+
const keep = new Set(want);
|
|
205
|
+
for (const c of Array.from(parent.childNodes)) {
|
|
206
|
+
if (!keep.has(c)) parent.removeChild(c);
|
|
207
|
+
}
|
|
208
|
+
let cursor = parent.firstChild;
|
|
209
|
+
for (const node of want) {
|
|
210
|
+
if (node === cursor) {
|
|
211
|
+
cursor = cursor.nextSibling;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
parent.insertBefore(node, cursor);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The element ancestors of `node` below `root`, nearest first, or null when
|
|
220
|
+
* `root` is not an ancestor.
|
|
221
|
+
*
|
|
222
|
+
* @param {Node} node
|
|
223
|
+
* @param {Node} root
|
|
224
|
+
* @returns {Element[] | null}
|
|
225
|
+
*/
|
|
226
|
+
function ancestorsUpTo(node, root) {
|
|
227
|
+
const out = [];
|
|
228
|
+
let p = node.parentNode;
|
|
229
|
+
while (p && p !== root) {
|
|
230
|
+
if (p.nodeType !== 1) return null;
|
|
231
|
+
out.push(/** @type {Element} */ (p));
|
|
232
|
+
p = p.parentNode;
|
|
233
|
+
}
|
|
234
|
+
return p === root ? out : null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @param {NodeListOf<ChildNode>} list
|
|
239
|
+
* @param {Node[]} nodes
|
|
240
|
+
*/
|
|
241
|
+
function sameNodes(list, nodes) {
|
|
242
|
+
if (list.length !== nodes.length) return false;
|
|
243
|
+
for (let i = 0; i < nodes.length; i++) if (list[i] !== nodes[i]) return false;
|
|
244
|
+
return true;
|
|
245
|
+
}
|