@craft-ts/component 0.7.0-beta.13
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/LICENSE +21 -0
- package/package.json +36 -0
- package/src/index.d.ts +40 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +38 -0
- package/src/index.js.map +1 -0
- package/src/lib/a11y-control.d.ts +58 -0
- package/src/lib/a11y-control.d.ts.map +1 -0
- package/src/lib/a11y-control.js +74 -0
- package/src/lib/a11y-control.js.map +1 -0
- package/src/lib/a11y.d.ts +87 -0
- package/src/lib/a11y.d.ts.map +1 -0
- package/src/lib/a11y.js +96 -0
- package/src/lib/a11y.js.map +1 -0
- package/src/lib/ai/ai-context-menu.d.ts +140 -0
- package/src/lib/ai/ai-context-menu.d.ts.map +1 -0
- package/src/lib/ai/ai-context-menu.js +66 -0
- package/src/lib/ai/ai-context-menu.js.map +1 -0
- package/src/lib/ai/ai-send-dialog.d.ts +1263 -0
- package/src/lib/ai/ai-send-dialog.d.ts.map +1 -0
- package/src/lib/ai/ai-send-dialog.js +288 -0
- package/src/lib/ai/ai-send-dialog.js.map +1 -0
- package/src/lib/ai/send-context-to-ai.d.ts +29 -0
- package/src/lib/ai/send-context-to-ai.d.ts.map +1 -0
- package/src/lib/ai/send-context-to-ai.js +127 -0
- package/src/lib/ai/send-context-to-ai.js.map +1 -0
- package/src/lib/assert-defined-input.d.ts +26 -0
- package/src/lib/assert-defined-input.d.ts.map +1 -0
- package/src/lib/assert-defined-input.js +47 -0
- package/src/lib/assert-defined-input.js.map +1 -0
- package/src/lib/block.d.ts +36 -0
- package/src/lib/block.d.ts.map +1 -0
- package/src/lib/block.js +55 -0
- package/src/lib/block.js.map +1 -0
- package/src/lib/bootstrap.d.ts +32 -0
- package/src/lib/bootstrap.d.ts.map +1 -0
- package/src/lib/bootstrap.js +60 -0
- package/src/lib/bootstrap.js.map +1 -0
- package/src/lib/bridge.d.ts +56 -0
- package/src/lib/bridge.d.ts.map +1 -0
- package/src/lib/bridge.js +81 -0
- package/src/lib/bridge.js.map +1 -0
- package/src/lib/component.d.ts +46 -0
- package/src/lib/component.d.ts.map +1 -0
- package/src/lib/component.js +118 -0
- package/src/lib/component.js.map +1 -0
- package/src/lib/composition.d.ts +17 -0
- package/src/lib/composition.d.ts.map +1 -0
- package/src/lib/composition.js +28 -0
- package/src/lib/composition.js.map +1 -0
- package/src/lib/craft-defaults.d.ts +2 -0
- package/src/lib/craft-defaults.d.ts.map +1 -0
- package/src/lib/craft-defaults.js +41 -0
- package/src/lib/craft-defaults.js.map +1 -0
- package/src/lib/craft-host-tokens.d.ts +6 -0
- package/src/lib/craft-host-tokens.d.ts.map +1 -0
- package/src/lib/craft-host-tokens.js +7 -0
- package/src/lib/craft-host-tokens.js.map +1 -0
- package/src/lib/craft-router-outlet.d.ts +15 -0
- package/src/lib/craft-router-outlet.d.ts.map +1 -0
- package/src/lib/craft-router-outlet.js +10 -0
- package/src/lib/craft-router-outlet.js.map +1 -0
- package/src/lib/css-vars.d.ts +31 -0
- package/src/lib/css-vars.d.ts.map +1 -0
- package/src/lib/css-vars.js +43 -0
- package/src/lib/css-vars.js.map +1 -0
- package/src/lib/css-vars.type.d.ts +151 -0
- package/src/lib/css-vars.type.d.ts.map +1 -0
- package/src/lib/css-vars.type.js +1 -0
- package/src/lib/css-vars.type.js.map +1 -0
- package/src/lib/defer.d.ts +31 -0
- package/src/lib/defer.d.ts.map +1 -0
- package/src/lib/defer.js +19 -0
- package/src/lib/defer.js.map +1 -0
- package/src/lib/directive.d.ts +9 -0
- package/src/lib/directive.d.ts.map +1 -0
- package/src/lib/directive.js +31 -0
- package/src/lib/directive.js.map +1 -0
- package/src/lib/each-scheduling.d.ts +59 -0
- package/src/lib/each-scheduling.d.ts.map +1 -0
- package/src/lib/each-scheduling.js +120 -0
- package/src/lib/each-scheduling.js.map +1 -0
- package/src/lib/each.d.ts +29 -0
- package/src/lib/each.d.ts.map +1 -0
- package/src/lib/each.js +23 -0
- package/src/lib/each.js.map +1 -0
- package/src/lib/factory-runtime.d.ts +5 -0
- package/src/lib/factory-runtime.d.ts.map +1 -0
- package/src/lib/factory-runtime.js +26 -0
- package/src/lib/factory-runtime.js.map +1 -0
- package/src/lib/field-exception-block.d.ts +96 -0
- package/src/lib/field-exception-block.d.ts.map +1 -0
- package/src/lib/field-exception-block.js +44 -0
- package/src/lib/field-exception-block.js.map +1 -0
- package/src/lib/host-runtime.d.ts +60 -0
- package/src/lib/host-runtime.d.ts.map +1 -0
- package/src/lib/host-runtime.js +16 -0
- package/src/lib/host-runtime.js.map +1 -0
- package/src/lib/hydrate.d.ts +15 -0
- package/src/lib/hydrate.d.ts.map +1 -0
- package/src/lib/hydrate.js +79 -0
- package/src/lib/hydrate.js.map +1 -0
- package/src/lib/hyperscript.d.ts +148 -0
- package/src/lib/hyperscript.d.ts.map +1 -0
- package/src/lib/hyperscript.js +96 -0
- package/src/lib/hyperscript.js.map +1 -0
- package/src/lib/if-block.d.ts +8 -0
- package/src/lib/if-block.d.ts.map +1 -0
- package/src/lib/if-block.js +16 -0
- package/src/lib/if-block.js.map +1 -0
- package/src/lib/locator.d.ts +170 -0
- package/src/lib/locator.d.ts.map +1 -0
- package/src/lib/locator.js +151 -0
- package/src/lib/locator.js.map +1 -0
- package/src/lib/match-block.d.ts +38 -0
- package/src/lib/match-block.d.ts.map +1 -0
- package/src/lib/match-block.js +29 -0
- package/src/lib/match-block.js.map +1 -0
- package/src/lib/pending-block.d.ts +125 -0
- package/src/lib/pending-block.d.ts.map +1 -0
- package/src/lib/pending-block.js +48 -0
- package/src/lib/pending-block.js.map +1 -0
- package/src/lib/project.d.ts +19 -0
- package/src/lib/project.d.ts.map +1 -0
- package/src/lib/project.js +56 -0
- package/src/lib/project.js.map +1 -0
- package/src/lib/render/hydration-metadata.d.ts +3 -0
- package/src/lib/render/hydration-metadata.d.ts.map +1 -0
- package/src/lib/render/hydration-metadata.js +8 -0
- package/src/lib/render/hydration-metadata.js.map +1 -0
- package/src/lib/render/hydration.d.ts +24 -0
- package/src/lib/render/hydration.d.ts.map +1 -0
- package/src/lib/render/hydration.js +181 -0
- package/src/lib/render/hydration.js.map +1 -0
- package/src/lib/render/interpreter.d.ts +32 -0
- package/src/lib/render/interpreter.d.ts.map +1 -0
- package/src/lib/render/interpreter.js +3588 -0
- package/src/lib/render/interpreter.js.map +1 -0
- package/src/lib/render/server-style-collector.d.ts +6 -0
- package/src/lib/render/server-style-collector.d.ts.map +1 -0
- package/src/lib/render/server-style-collector.js +23 -0
- package/src/lib/render/server-style-collector.js.map +1 -0
- package/src/lib/render/ssr-coordinator.d.ts +20 -0
- package/src/lib/render/ssr-coordinator.d.ts.map +1 -0
- package/src/lib/render/ssr-coordinator.js +99 -0
- package/src/lib/render/ssr-coordinator.js.map +1 -0
- package/src/lib/render/string-dom.d.ts +9 -0
- package/src/lib/render/string-dom.d.ts.map +1 -0
- package/src/lib/render/string-dom.js +377 -0
- package/src/lib/render/string-dom.js.map +1 -0
- package/src/lib/render/style-registry.d.ts +14 -0
- package/src/lib/render/style-registry.d.ts.map +1 -0
- package/src/lib/render/style-registry.js +79 -0
- package/src/lib/render/style-registry.js.map +1 -0
- package/src/lib/render/style-scope.d.ts +30 -0
- package/src/lib/render/style-scope.d.ts.map +1 -0
- package/src/lib/render/style-scope.js +261 -0
- package/src/lib/render/style-scope.js.map +1 -0
- package/src/lib/render/vnode.d.ts +330 -0
- package/src/lib/render/vnode.d.ts.map +1 -0
- package/src/lib/render/vnode.js +293 -0
- package/src/lib/render/vnode.js.map +1 -0
- package/src/lib/server-render.d.ts +34 -0
- package/src/lib/server-render.d.ts.map +1 -0
- package/src/lib/server-render.js +84 -0
- package/src/lib/server-render.js.map +1 -0
- package/src/lib/start.d.ts +13 -0
- package/src/lib/start.d.ts.map +1 -0
- package/src/lib/start.js +20 -0
- package/src/lib/start.js.map +1 -0
- package/src/lib/template-contract.d.ts +347 -0
- package/src/lib/template-contract.d.ts.map +1 -0
- package/src/lib/template-contract.js +1 -0
- package/src/lib/template-contract.js.map +1 -0
- package/src/lib/template.d.ts +11 -0
- package/src/lib/template.d.ts.map +1 -0
- package/src/lib/template.js +21 -0
- package/src/lib/template.js.map +1 -0
- package/src/lib/testing.d.ts +157 -0
- package/src/lib/testing.d.ts.map +1 -0
- package/src/lib/testing.js +416 -0
- package/src/lib/testing.js.map +1 -0
- package/src/lib/types.d.ts +442 -0
- package/src/lib/types.d.ts.map +1 -0
- package/src/lib/types.js +22 -0
- package/src/lib/types.js.map +1 -0
- package/testing/public-api.d.ts +3 -0
- package/testing/public-api.d.ts.map +1 -0
- package/testing/public-api.js +3 -0
- package/testing/public-api.js.map +1 -0
|
@@ -0,0 +1,3588 @@
|
|
|
1
|
+
import { CRAFT_SERVICE_PROVIDER_BRAND, CRAFT_DOM_EVENT_HOOK, CRAFT_NODE_DIRECTIVE, CRAFT_NODE_EFFECT_FACTORY, CRAFT_FIELD_EXCEPTION_BOUNDARY, CRAFT_FIELD_EXCEPTION_SOURCE, CRAFT_TEMPORAL_RUNTIME, COMPONENT_REGISTER, craftEffect, craftLazy, createBrowserDomAdapter, registerCraftSyncTemplateFlush, executeYieldable, HOST_TAG_LIST, executeTemplateTrace, isCraftException, isCraftGenShortCircuit, isCraftNotSettled, ɵwithSettledReadObserver, isGeneratorFunction, isCraftField, isGenerator, markYieldableValue, markYieldableMethod, isYieldableValue, isYieldableMethod, isYieldableReactiveValue, DEEP_YIELDABLE, provideHostName, rawReactiveFacade, rawReactiveValue, toYieldable, yieldableInvocation, ɵfallbackComponentRegister, ɵregisterCraftTarget, ɵcraftInjectorFromHost, fieldExceptionVisibilityMatches, YIELDABLE_VALUE, RealCraftTemporalRuntime, CRAFT_HYDRATION_ID, childCraftRenderIdentity, createCraftRenderIdentity, CRAFT_SSR_POLICY, CRAFT_MATCH, } from '@craft-ts/core';
|
|
2
|
+
import { executeCraftComponentFactory } from '../factory-runtime';
|
|
3
|
+
import { computed, createEnvironmentInjector, ElementRef, EnvironmentInjector, Injector, runInInjectionContext, signal, untracked, } from '../host-runtime';
|
|
4
|
+
import { CRAFT_COMPONENT, CRAFT_DIRECTIVE, CONTENT_DECLARATION_CONTEXT, } from '../types';
|
|
5
|
+
import { cssVarStyles } from '../css-vars';
|
|
6
|
+
import { normalizeChildren, mergeHostProps, withCraftRenderContext, CRAFT_NODE_DIRECTIVES, } from './vnode';
|
|
7
|
+
import { CraftUnhandledPendingError, resolvePendingBlockHandler, } from '../pending-block';
|
|
8
|
+
import { CraftUnhandledExceptionError, resolveCatchBlockHandler, } from '../block';
|
|
9
|
+
import { EACH_SCHEDULER, createEachScheduler, } from '../each-scheduling';
|
|
10
|
+
import { executeCraftComponentFactoryAsync } from '../factory-runtime';
|
|
11
|
+
import { CRAFT_STYLE_REGISTRY, ɵfallbackCraftStyleRegistry, } from './style-registry';
|
|
12
|
+
import { scopeCss, scopeIdFor, validateStyleScope } from './style-scope';
|
|
13
|
+
import { CRAFT_LOCATOR_CONTENT_NAMES, directCraftContentNames, findCraftTemplateLocator, } from '../locator';
|
|
14
|
+
import { HydrationMismatchError } from './hydration';
|
|
15
|
+
import { setNodeHydrationKey } from './hydration-metadata';
|
|
16
|
+
let templateGeneratorDepth = 0;
|
|
17
|
+
function childContext(context, overrides = {}) {
|
|
18
|
+
return { ...context, ...overrides };
|
|
19
|
+
}
|
|
20
|
+
function childNodeContext(context, node, index) {
|
|
21
|
+
const segments = node.kind === 'component'
|
|
22
|
+
? [node.component[CRAFT_COMPONENT].name, index]
|
|
23
|
+
: node.kind === 'each'
|
|
24
|
+
? [`each:${node.sourceName ?? index}`]
|
|
25
|
+
: node.kind === 'if'
|
|
26
|
+
? [`if:${node.conditionName}`]
|
|
27
|
+
: [index];
|
|
28
|
+
return childContext(context, {
|
|
29
|
+
identity: childCraftRenderIdentity(context.identity, ...segments),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function lexicalContext(context) {
|
|
33
|
+
return context.declarationContext ?? context;
|
|
34
|
+
}
|
|
35
|
+
function preserveContentDeclarationContext(value, context) {
|
|
36
|
+
if (typeof value !== 'function')
|
|
37
|
+
return value;
|
|
38
|
+
if (value[CONTENT_DECLARATION_CONTEXT] !== undefined) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
const wrapped = (...args) => value(...args);
|
|
42
|
+
Object.defineProperty(wrapped, CONTENT_DECLARATION_CONTEXT, {
|
|
43
|
+
value: context,
|
|
44
|
+
enumerable: false,
|
|
45
|
+
configurable: false,
|
|
46
|
+
writable: false,
|
|
47
|
+
});
|
|
48
|
+
return wrapped;
|
|
49
|
+
}
|
|
50
|
+
function renderChildrenCallback(context, callback, args = [], kind = 'callback', name) {
|
|
51
|
+
const phase = tracePhase(context);
|
|
52
|
+
return executeTemplateTrace(context.injector, traceContext(context, kind, phase, name ?? callback.name), () => invokeChildrenCallback(context, callback, args));
|
|
53
|
+
}
|
|
54
|
+
function invokeChildrenCallback(context, callback, args) {
|
|
55
|
+
return runInInjectionContext(context.injector, () => withCraftRenderContext(context, () => callback(...args)));
|
|
56
|
+
}
|
|
57
|
+
function renderBlockChildren(context, name, callback, args = []) {
|
|
58
|
+
return executeTemplateTrace(context.injector, traceContext(context, 'block', tracePhase(context), name), () => invokeChildrenCallback(context, callback, args));
|
|
59
|
+
}
|
|
60
|
+
function renderDeferChildren(context, name, callback, args = []) {
|
|
61
|
+
return executeTemplateTrace(context.injector, traceContext(context, 'defer', tracePhase(context), name), () => invokeChildrenCallback(context, callback, args));
|
|
62
|
+
}
|
|
63
|
+
function tracePhase(context) {
|
|
64
|
+
return (context.traceState?.renderCount ?? 0) <= 1
|
|
65
|
+
? 'initialRender'
|
|
66
|
+
: 'update';
|
|
67
|
+
}
|
|
68
|
+
function traceContext(context, kind, phase, name) {
|
|
69
|
+
return {
|
|
70
|
+
kind,
|
|
71
|
+
phase,
|
|
72
|
+
...(context.componentName ? { componentName: context.componentName } : {}),
|
|
73
|
+
...(name ? { name } : {}),
|
|
74
|
+
...(context.directiveNames && context.directiveNames.length > 0
|
|
75
|
+
? { directiveNames: context.directiveNames }
|
|
76
|
+
: {}),
|
|
77
|
+
renderCount: context.traceState?.renderCount ?? 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function traceComponentLifecycle(injector, phase, componentName, renderCount) {
|
|
81
|
+
executeTemplateTrace(injector, {
|
|
82
|
+
kind: 'component',
|
|
83
|
+
phase,
|
|
84
|
+
componentName,
|
|
85
|
+
renderCount,
|
|
86
|
+
}, () => []);
|
|
87
|
+
}
|
|
88
|
+
function eagerlyResolveBrandedProviders(providers, injector, branded = false) {
|
|
89
|
+
const overrides = [];
|
|
90
|
+
const trackers = [];
|
|
91
|
+
for (const provider of providers) {
|
|
92
|
+
if (Array.isArray(provider)) {
|
|
93
|
+
const result = eagerlyResolveBrandedProviders(provider, injector, branded || CRAFT_SERVICE_PROVIDER_BRAND in provider);
|
|
94
|
+
overrides.push(...result.overrides);
|
|
95
|
+
trackers.push(...result.trackers);
|
|
96
|
+
if (result.exception) {
|
|
97
|
+
return { exception: result.exception, overrides, trackers };
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!branded || typeof provider !== 'object' || provider === null) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const token = Reflect.get(provider, 'provide');
|
|
105
|
+
const useFactory = Reflect.get(provider, 'useFactory');
|
|
106
|
+
if (token !== undefined && branded && typeof useFactory === 'function') {
|
|
107
|
+
// Resolve Craft providers through a computed so signals read by their
|
|
108
|
+
// factories remain dependencies of the component render effect.
|
|
109
|
+
const resolved = computed(() => runInInjectionContext(injector, useFactory));
|
|
110
|
+
trackers.push(resolved);
|
|
111
|
+
const value = resolved();
|
|
112
|
+
if (isCraftException(value)) {
|
|
113
|
+
return { exception: value, overrides, trackers };
|
|
114
|
+
}
|
|
115
|
+
overrides.push({ provide: token, useValue: value });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { overrides, trackers };
|
|
119
|
+
}
|
|
120
|
+
function findResourceException(value, seen = new Set()) {
|
|
121
|
+
if (typeof value !== 'object' || value === null || seen.has(value)) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
seen.add(value);
|
|
125
|
+
const candidate = value;
|
|
126
|
+
if (typeof candidate.exceptions === 'function') {
|
|
127
|
+
const exceptions = (isYieldableReactiveValue(candidate.exceptions)
|
|
128
|
+
? rawReactiveValue(candidate.exceptions)()
|
|
129
|
+
: candidate.exceptions());
|
|
130
|
+
const exception = exceptions.list?.find(isCraftException);
|
|
131
|
+
if (exception)
|
|
132
|
+
return exception;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
for (const child of value) {
|
|
136
|
+
const exception = findResourceException(child, seen);
|
|
137
|
+
if (exception)
|
|
138
|
+
return exception;
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
for (const child of Object.values(value)) {
|
|
143
|
+
const exception = findResourceException(child, seen);
|
|
144
|
+
if (exception)
|
|
145
|
+
return exception;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
function scopeTokens(parent, own) {
|
|
150
|
+
return [parent, own].filter(Boolean).join(' ');
|
|
151
|
+
}
|
|
152
|
+
function styleValues(styles) {
|
|
153
|
+
return typeof styles === 'string' ? [styles] : (styles ?? []);
|
|
154
|
+
}
|
|
155
|
+
function acquireStyles(context, owners, ownerScope) {
|
|
156
|
+
if (context.serverStyles) {
|
|
157
|
+
return owners.flatMap((owner, index) => {
|
|
158
|
+
const css = styleValues(owner.styles);
|
|
159
|
+
if (!css.length)
|
|
160
|
+
return [];
|
|
161
|
+
const scope = scopeIdFor(owner.definition ?? {}, owner.name);
|
|
162
|
+
const dev = typeof ngDevMode === 'undefined' || ngDevMode;
|
|
163
|
+
if (dev)
|
|
164
|
+
validateStyleScope(scope, css.join('\n'));
|
|
165
|
+
return [
|
|
166
|
+
context.serverStyles.acquire(`craft:${ownerScope}:${scope}`, scopeCss(ownerScope, css.join('\n')), index),
|
|
167
|
+
];
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!context.styleRoot || !context.styles)
|
|
171
|
+
return [];
|
|
172
|
+
const registry = context.styles;
|
|
173
|
+
const releases = [];
|
|
174
|
+
owners.forEach((owner, index) => {
|
|
175
|
+
const css = styleValues(owner.styles);
|
|
176
|
+
if (!css.length)
|
|
177
|
+
return;
|
|
178
|
+
const scope = scopeIdFor(owner.definition ?? {}, owner.name);
|
|
179
|
+
const dev = typeof ngDevMode === 'undefined' || ngDevMode;
|
|
180
|
+
if (dev)
|
|
181
|
+
validateStyleScope(scope, css.join('\n'));
|
|
182
|
+
releases.push(registry.acquire(context.styleRoot, `craft:${ownerScope}:${scope}`, scopeCss(ownerScope, css.join('\n')), index));
|
|
183
|
+
});
|
|
184
|
+
return releases;
|
|
185
|
+
}
|
|
186
|
+
function contentScopeId(ownerScope, slotName) {
|
|
187
|
+
return `${ownerScope}::content::${slotName}`;
|
|
188
|
+
}
|
|
189
|
+
function acquireContentStyle(context, slotName, stylePolicy) {
|
|
190
|
+
if (stylePolicy !== 'allow-container-styles' ||
|
|
191
|
+
!slotName ||
|
|
192
|
+
!context.ownerScope ||
|
|
193
|
+
!context.contentStyles?.[slotName] ||
|
|
194
|
+
(!context.serverStyles && (!context.styleRoot || !context.styles))) {
|
|
195
|
+
return { release: () => undefined };
|
|
196
|
+
}
|
|
197
|
+
const scope = contentScopeId(context.ownerScope, slotName);
|
|
198
|
+
const css = scopeCss(scope, context.contentStyles[slotName], {
|
|
199
|
+
rootAttribute: 'data-craft-content',
|
|
200
|
+
limitSelector: '[data-craft-root]',
|
|
201
|
+
});
|
|
202
|
+
const release = context.serverStyles
|
|
203
|
+
? context.serverStyles.acquire(`craft:content:${scope}`, css, 1000)
|
|
204
|
+
: context.styles.acquire(context.styleRoot, `craft:content:${scope}`, css, 1000);
|
|
205
|
+
return { scope, release };
|
|
206
|
+
}
|
|
207
|
+
function activeSsrRoute(context) {
|
|
208
|
+
const match = context.injector.get(CRAFT_MATCH, null);
|
|
209
|
+
try {
|
|
210
|
+
return match?.()?.route?.path;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function createRenderEffect(context, name, effectFn) {
|
|
217
|
+
// One token per binding: a binding that suspends holds it until it next runs
|
|
218
|
+
// to completion. The effect stays subscribed across the suspension — the
|
|
219
|
+
// settled read registered its source's `status` before throwing — so the
|
|
220
|
+
// boundary is released without any polling.
|
|
221
|
+
const token = {};
|
|
222
|
+
// A binding that short-circuited must tell its boundary when it recovers —
|
|
223
|
+
// but only that binding, so a sibling still in exception keeps its fallback.
|
|
224
|
+
let shortCircuited = false;
|
|
225
|
+
const ref = createEffectInInjector(context.injector, name, () => {
|
|
226
|
+
try {
|
|
227
|
+
// A settled read reports its source while it evaluates — that is how the
|
|
228
|
+
// boundary learns about a source that is refetching rather than
|
|
229
|
+
// suspending, since a refetch throws nothing.
|
|
230
|
+
ɵwithSettledReadObserver(context.settledReadObserver, effectFn);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (isCraftNotSettled(error)) {
|
|
234
|
+
if (!context.pendingBoundary) {
|
|
235
|
+
if (context.ssr) {
|
|
236
|
+
const routePolicy = context.injector.get(CRAFT_SSR_POLICY, null);
|
|
237
|
+
if (routePolicy) {
|
|
238
|
+
context.ssr.suspend(token, error.source, routePolicy.mode, {
|
|
239
|
+
route: activeSsrRoute(context),
|
|
240
|
+
timeoutMs: routePolicy.timeoutMs,
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
context.ssr.unhandled(error.source, activeSsrRoute(context));
|
|
245
|
+
}
|
|
246
|
+
throw new CraftUnhandledPendingError(error.source);
|
|
247
|
+
}
|
|
248
|
+
context.pendingBoundary.suspend(token, error.source);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!isCraftGenShortCircuit(error))
|
|
252
|
+
throw error;
|
|
253
|
+
// A settled read whose source carries a business exception: route it to
|
|
254
|
+
// the nearest catchBlock, exactly like a component initialization one.
|
|
255
|
+
context.pendingBoundary?.resume(token);
|
|
256
|
+
if (!context.exceptionBoundary?.(error.exception, token)) {
|
|
257
|
+
throw new CraftUnhandledExceptionError(error.exception);
|
|
258
|
+
}
|
|
259
|
+
shortCircuited = true;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
context.ssr?.resume(token);
|
|
263
|
+
context.pendingBoundary?.resume(token);
|
|
264
|
+
if (shortCircuited) {
|
|
265
|
+
shortCircuited = false;
|
|
266
|
+
context.exceptionBoundaryResolved?.(token);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
destroy: () => {
|
|
271
|
+
context.ssr?.resume(token);
|
|
272
|
+
context.pendingBoundary?.resume(token);
|
|
273
|
+
if (shortCircuited)
|
|
274
|
+
context.exceptionBoundaryResolved?.(token);
|
|
275
|
+
ref.destroy();
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function createEffectInInjector(injector, name, effectFn) {
|
|
280
|
+
return untracked(() => runInInjectionContext(injector, () => craftEffect(name, effectFn, { manualCleanup: true })));
|
|
281
|
+
}
|
|
282
|
+
function executeTemplateCallback(callback, args, context) {
|
|
283
|
+
const isGeneratorCallback = isGeneratorFunction(callback);
|
|
284
|
+
if (isGeneratorCallback)
|
|
285
|
+
templateGeneratorDepth++;
|
|
286
|
+
try {
|
|
287
|
+
return executeYieldable(callback, args, context.injector);
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
if (isGeneratorCallback)
|
|
291
|
+
templateGeneratorDepth--;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function resolveTemplateValue(value, context) {
|
|
295
|
+
return typeof value === 'function'
|
|
296
|
+
? executeTemplateCallback(value, [], context)
|
|
297
|
+
: value;
|
|
298
|
+
}
|
|
299
|
+
function resolveTemplateContext(context, renderContext) {
|
|
300
|
+
if (typeof context === 'function') {
|
|
301
|
+
return resolveTemplateValue(context, renderContext);
|
|
302
|
+
}
|
|
303
|
+
if (context === null ||
|
|
304
|
+
typeof context !== 'object' ||
|
|
305
|
+
Array.isArray(context)) {
|
|
306
|
+
return context;
|
|
307
|
+
}
|
|
308
|
+
return Object.fromEntries(Object.entries(context).map(([key, value]) => [
|
|
309
|
+
key,
|
|
310
|
+
typeof value === 'function'
|
|
311
|
+
? resolveTemplateValue(value, renderContext)
|
|
312
|
+
: value,
|
|
313
|
+
]));
|
|
314
|
+
}
|
|
315
|
+
function resolveProjectionValue(value, renderContext) {
|
|
316
|
+
let current = value;
|
|
317
|
+
for (let depth = 0; depth < 3 && typeof current === 'function'; depth++) {
|
|
318
|
+
current = resolveTemplateValue(current, renderContext);
|
|
319
|
+
}
|
|
320
|
+
return current;
|
|
321
|
+
}
|
|
322
|
+
function projectYieldableTemplateContext(value, seen = new WeakMap()) {
|
|
323
|
+
// Field trees are dynamic proxies: copying their enumerable members would
|
|
324
|
+
// discard lazily-created child fields such as `form.email`.
|
|
325
|
+
if (isCraftField(value))
|
|
326
|
+
return value;
|
|
327
|
+
if (isYieldableValue(value)) {
|
|
328
|
+
const projected = (...args) => {
|
|
329
|
+
const result = Reflect.apply(value, undefined, args);
|
|
330
|
+
if (templateGeneratorDepth === 0)
|
|
331
|
+
return result;
|
|
332
|
+
return toYieldable(() => result)();
|
|
333
|
+
};
|
|
334
|
+
const namedProjected = markYieldableValue(projected, value[YIELDABLE_VALUE]);
|
|
335
|
+
if (typeof value === 'function' &&
|
|
336
|
+
(Object.keys(value).length > 0 || DEEP_YIELDABLE in value)) {
|
|
337
|
+
return new Proxy(namedProjected, {
|
|
338
|
+
get(_target, property) {
|
|
339
|
+
return projectYieldableTemplateContext(Reflect.get(value, property), seen);
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return namedProjected;
|
|
344
|
+
}
|
|
345
|
+
if (typeof value === 'function') {
|
|
346
|
+
if (isYieldableMethod(value)) {
|
|
347
|
+
return (...args) => {
|
|
348
|
+
const result = Reflect.apply(value, undefined, args);
|
|
349
|
+
if (templateGeneratorDepth === 0)
|
|
350
|
+
return result;
|
|
351
|
+
return toYieldable(() => result)();
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
// State and resource refs are callable objects with exposed insertions
|
|
355
|
+
// (for example `counter.disabled`). Keep the callable ref intact while
|
|
356
|
+
// projecting its nested yieldable members into the template context.
|
|
357
|
+
if (Object.keys(value).length === 0)
|
|
358
|
+
return value;
|
|
359
|
+
if (seen.has(value))
|
|
360
|
+
return seen.get(value);
|
|
361
|
+
const projected = new Proxy(value, {
|
|
362
|
+
get(target, property, receiver) {
|
|
363
|
+
return projectYieldableTemplateContext(Reflect.get(target, property, receiver), seen);
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
seen.set(value, projected);
|
|
367
|
+
return projected;
|
|
368
|
+
}
|
|
369
|
+
if (typeof value !== 'object' || value === null)
|
|
370
|
+
return value;
|
|
371
|
+
// Craft nodes carry runtime objects such as RenderContext and the DOM adapter
|
|
372
|
+
// through their declaration context. Copying them as plain objects would
|
|
373
|
+
// strip prototype methods from those runtime services before projection.
|
|
374
|
+
if ('kind' in value &&
|
|
375
|
+
typeof value.kind === 'string') {
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
if (seen.has(value))
|
|
379
|
+
return seen.get(value);
|
|
380
|
+
if (Array.isArray(value)) {
|
|
381
|
+
const result = [];
|
|
382
|
+
seen.set(value, result);
|
|
383
|
+
for (const item of value) {
|
|
384
|
+
result.push(projectYieldableTemplateContext(item, seen));
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
const result = {};
|
|
389
|
+
seen.set(value, result);
|
|
390
|
+
for (const [key, child] of Object.entries(value)) {
|
|
391
|
+
result[key] = projectYieldableTemplateContext(child, seen);
|
|
392
|
+
}
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
class CraftNodeDirectiveMount {
|
|
396
|
+
inputs;
|
|
397
|
+
environmentInjector;
|
|
398
|
+
cleanup;
|
|
399
|
+
registrationRelease;
|
|
400
|
+
constructor(descriptor, element, context) {
|
|
401
|
+
this.inputs = signal(descriptor.inputs);
|
|
402
|
+
const definition = descriptor.directive[CRAFT_NODE_DIRECTIVE];
|
|
403
|
+
this.environmentInjector = createEnvironmentInjector([
|
|
404
|
+
{ provide: ElementRef, useValue: new ElementRef(element) },
|
|
405
|
+
{
|
|
406
|
+
provide: CRAFT_NODE_EFFECT_FACTORY,
|
|
407
|
+
deps: [Injector],
|
|
408
|
+
useFactory: (injector) => (name, effectFn) => createEffectInInjector(injector, `node-directive-${name}`, effectFn),
|
|
409
|
+
},
|
|
410
|
+
...(context.fieldExceptionBoundary
|
|
411
|
+
? [
|
|
412
|
+
{
|
|
413
|
+
provide: CRAFT_FIELD_EXCEPTION_BOUNDARY,
|
|
414
|
+
useValue: context.fieldExceptionBoundary,
|
|
415
|
+
},
|
|
416
|
+
]
|
|
417
|
+
: []),
|
|
418
|
+
], context.injector.get(EnvironmentInjector), `CraftNodeDirective(${definition.name})`);
|
|
419
|
+
const inputs = this.inputs;
|
|
420
|
+
const nodeContext = {
|
|
421
|
+
element,
|
|
422
|
+
injector: this.environmentInjector,
|
|
423
|
+
renderer: context.renderer,
|
|
424
|
+
get props() {
|
|
425
|
+
return inputs();
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
this.registrationRelease = ɵregisterCraftTarget(this.environmentInjector, descriptor.directive, nodeContext, allocateCraftHostName(this.environmentInjector, 'directive', definition.name), false);
|
|
429
|
+
const cleanup = untracked(() => runInInjectionContext(this.environmentInjector, () => definition.mount(nodeContext)));
|
|
430
|
+
this.cleanup = typeof cleanup === 'function' ? cleanup : undefined;
|
|
431
|
+
}
|
|
432
|
+
update(descriptor) {
|
|
433
|
+
this.inputs.set(descriptor.inputs);
|
|
434
|
+
}
|
|
435
|
+
destroy() {
|
|
436
|
+
this.registrationRelease();
|
|
437
|
+
this.cleanup?.();
|
|
438
|
+
this.environmentInjector.destroy();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function sameCraftDirectives(left, right) {
|
|
442
|
+
return (left.length === right.length &&
|
|
443
|
+
left.every((directive, index) => directive === right[index]));
|
|
444
|
+
}
|
|
445
|
+
function sameCraftNodeDirectives(left, right) {
|
|
446
|
+
return (left.length === right.length &&
|
|
447
|
+
left.every((descriptor, index) => descriptor.directive === right[index]?.directive));
|
|
448
|
+
}
|
|
449
|
+
function insertBefore(renderer, parent, node, before) {
|
|
450
|
+
renderer.insertBefore(parent, node, before);
|
|
451
|
+
}
|
|
452
|
+
function removeNode(renderer, node) {
|
|
453
|
+
const parent = node.parentNode;
|
|
454
|
+
if (parent) {
|
|
455
|
+
renderer.removeChild(parent, node);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function isElementNode(node) {
|
|
459
|
+
return (typeof node === 'object' &&
|
|
460
|
+
node !== null &&
|
|
461
|
+
node.nodeType === 1);
|
|
462
|
+
}
|
|
463
|
+
function mountComment(parent, value, before, context) {
|
|
464
|
+
if (context.mode === 'hydrate' && context.hydration) {
|
|
465
|
+
try {
|
|
466
|
+
return context.hydration.claimBoundary(context.identity.hydrationKey, value);
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
if (!(error instanceof HydrationMismatchError))
|
|
470
|
+
throw error;
|
|
471
|
+
context.hydration.recordMismatch(error);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const comment = context.renderer.createComment(context.emitHydrationKeys
|
|
475
|
+
? `${value}|hk:${context.identity.hydrationKey}`
|
|
476
|
+
: value);
|
|
477
|
+
insertBefore(context.renderer, parent, comment, before);
|
|
478
|
+
return comment;
|
|
479
|
+
}
|
|
480
|
+
function createElementForRender(tag, parent, before, context) {
|
|
481
|
+
if (context.mode === 'hydrate' && context.hydration) {
|
|
482
|
+
try {
|
|
483
|
+
return {
|
|
484
|
+
element: context.hydration.claimElement(context.identity.hydrationKey, tag),
|
|
485
|
+
context,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
if (!(error instanceof HydrationMismatchError))
|
|
490
|
+
throw error;
|
|
491
|
+
context.hydration.recordMismatch(error);
|
|
492
|
+
const actual = error.actualNode;
|
|
493
|
+
const replacementBefore = actual?.parentNode === parent ? actual.nextSibling : before;
|
|
494
|
+
if (actual?.parentNode)
|
|
495
|
+
removeNode(context.renderer, actual);
|
|
496
|
+
const createContext = childContext(context, {
|
|
497
|
+
mode: 'create',
|
|
498
|
+
hydration: undefined,
|
|
499
|
+
});
|
|
500
|
+
const element = createContext.renderer.createElement(tag);
|
|
501
|
+
if (createContext.emitHydrationKeys) {
|
|
502
|
+
createContext.renderer.setAttribute(element, 'data-craft-hk', createContext.identity.hydrationKey);
|
|
503
|
+
}
|
|
504
|
+
insertBefore(createContext.renderer, parent, element, replacementBefore);
|
|
505
|
+
return { element, context: createContext };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const element = context.renderer.createElement(tag);
|
|
509
|
+
if (context.emitHydrationKeys) {
|
|
510
|
+
context.renderer.setAttribute(element, 'data-craft-hk', context.identity.hydrationKey);
|
|
511
|
+
}
|
|
512
|
+
insertBefore(context.renderer, parent, element, before);
|
|
513
|
+
return { element, context };
|
|
514
|
+
}
|
|
515
|
+
function createTextForRender(value, parent, before, context, compare) {
|
|
516
|
+
if (context.mode === 'hydrate' && context.hydration) {
|
|
517
|
+
try {
|
|
518
|
+
return {
|
|
519
|
+
text: context.hydration.claimText(context.identity.hydrationKey, compare ? value : undefined),
|
|
520
|
+
context,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
if (!(error instanceof HydrationMismatchError))
|
|
525
|
+
throw error;
|
|
526
|
+
context.hydration.recordMismatch(error);
|
|
527
|
+
const actual = error.actualNode;
|
|
528
|
+
const replacementBefore = actual?.parentNode === parent ? actual.nextSibling : before;
|
|
529
|
+
if (actual?.parentNode)
|
|
530
|
+
removeNode(context.renderer, actual);
|
|
531
|
+
const createContext = childContext(context, {
|
|
532
|
+
mode: 'create',
|
|
533
|
+
hydration: undefined,
|
|
534
|
+
});
|
|
535
|
+
const text = createContext.renderer.createText(value);
|
|
536
|
+
if (createContext.emitHydrationKeys) {
|
|
537
|
+
setNodeHydrationKey(text, createContext.identity.hydrationKey);
|
|
538
|
+
}
|
|
539
|
+
insertBefore(createContext.renderer, parent, text, replacementBefore);
|
|
540
|
+
return { text, context: createContext };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const text = context.renderer.createText(value);
|
|
544
|
+
if (context.emitHydrationKeys) {
|
|
545
|
+
setNodeHydrationKey(text, context.identity.hydrationKey);
|
|
546
|
+
}
|
|
547
|
+
insertBefore(context.renderer, parent, text, before);
|
|
548
|
+
return { text, context };
|
|
549
|
+
}
|
|
550
|
+
function patchRenderedChildren(parent, rendered, children, before, context) {
|
|
551
|
+
const nextNodes = normalizeChildren(children);
|
|
552
|
+
const sharedLength = Math.min(rendered.length, nextNodes.length);
|
|
553
|
+
for (let index = 0; index < sharedLength; index += 1) {
|
|
554
|
+
const current = rendered[index];
|
|
555
|
+
const next = nextNodes[index];
|
|
556
|
+
if (!current.patch(next)) {
|
|
557
|
+
const replacement = mountNode(next, parent, current.firstNode(), childNodeContext(context, next, index));
|
|
558
|
+
current.destroy();
|
|
559
|
+
rendered[index] = replacement;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
while (rendered.length > nextNodes.length) {
|
|
563
|
+
rendered.pop()?.destroy();
|
|
564
|
+
}
|
|
565
|
+
for (let index = rendered.length; index < nextNodes.length; index += 1) {
|
|
566
|
+
rendered.push(mountNode(nextNodes[index], parent, before, childNodeContext(context, nextNodes[index], index)));
|
|
567
|
+
}
|
|
568
|
+
return rendered;
|
|
569
|
+
}
|
|
570
|
+
class TextRenderedNode {
|
|
571
|
+
kind = 'text';
|
|
572
|
+
node;
|
|
573
|
+
value;
|
|
574
|
+
renderer;
|
|
575
|
+
constructor(node, value, renderer) {
|
|
576
|
+
this.node = node;
|
|
577
|
+
this.value = value;
|
|
578
|
+
this.renderer = renderer;
|
|
579
|
+
}
|
|
580
|
+
firstNode() {
|
|
581
|
+
return this.node;
|
|
582
|
+
}
|
|
583
|
+
lastNode() {
|
|
584
|
+
return this.node;
|
|
585
|
+
}
|
|
586
|
+
patch(node) {
|
|
587
|
+
if (node.kind !== 'text') {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
if (node.value !== this.value) {
|
|
591
|
+
this.renderer.setValue(this.node, node.value);
|
|
592
|
+
this.value = node.value;
|
|
593
|
+
}
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
destroy() {
|
|
597
|
+
removeNode(this.renderer, this.node);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
class ReactiveTextRenderedNode {
|
|
601
|
+
kind = 'reactive-text';
|
|
602
|
+
binding;
|
|
603
|
+
effectRef;
|
|
604
|
+
value = '';
|
|
605
|
+
node;
|
|
606
|
+
context;
|
|
607
|
+
constructor(node, binding, context) {
|
|
608
|
+
this.node = node;
|
|
609
|
+
this.context = context;
|
|
610
|
+
this.binding = binding;
|
|
611
|
+
this.effectRef = this.createEffect();
|
|
612
|
+
}
|
|
613
|
+
firstNode() {
|
|
614
|
+
return this.node;
|
|
615
|
+
}
|
|
616
|
+
lastNode() {
|
|
617
|
+
return this.node;
|
|
618
|
+
}
|
|
619
|
+
patch(node) {
|
|
620
|
+
if (node.kind !== 'reactive-text') {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
if (node.binding !== this.binding) {
|
|
624
|
+
this.effectRef.destroy();
|
|
625
|
+
this.binding = node.binding;
|
|
626
|
+
this.effectRef = this.createEffect();
|
|
627
|
+
}
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
destroy() {
|
|
631
|
+
this.effectRef.destroy();
|
|
632
|
+
removeNode(this.context.renderer, this.node);
|
|
633
|
+
}
|
|
634
|
+
createEffect() {
|
|
635
|
+
return createRenderEffect(this.context, 'text-binding', () => {
|
|
636
|
+
const resolved = executeTemplateCallback(this.binding, [], this.context);
|
|
637
|
+
const next = resolved === null || resolved === undefined || resolved === false
|
|
638
|
+
? ''
|
|
639
|
+
: String(resolved);
|
|
640
|
+
if (next !== this.value) {
|
|
641
|
+
this.context.renderer.setValue(this.node, next);
|
|
642
|
+
this.value = next;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function renderCraftDirectiveNode(node, context) {
|
|
648
|
+
let template = (_componentContext) => [node.node];
|
|
649
|
+
for (const directive of node.directives) {
|
|
650
|
+
template = directive[CRAFT_DIRECTIVE].template(template);
|
|
651
|
+
}
|
|
652
|
+
return renderChildrenCallback(context, template, [context.componentContext], 'callback', 'directive');
|
|
653
|
+
}
|
|
654
|
+
class CraftDirectiveRenderedNode {
|
|
655
|
+
kind = 'directive';
|
|
656
|
+
view;
|
|
657
|
+
descriptor;
|
|
658
|
+
effectRef;
|
|
659
|
+
styleReleases;
|
|
660
|
+
registrationReleases;
|
|
661
|
+
node;
|
|
662
|
+
context;
|
|
663
|
+
constructor(node, parent, before, context) {
|
|
664
|
+
this.node = node;
|
|
665
|
+
this.context = context;
|
|
666
|
+
this.descriptor = signal(node);
|
|
667
|
+
const owners = node.directives.map((directive) => ({
|
|
668
|
+
name: directive[CRAFT_DIRECTIVE].name,
|
|
669
|
+
styles: [
|
|
670
|
+
...styleValues(directive[CRAFT_DIRECTIVE].meta.styles),
|
|
671
|
+
...styleValues(directive[CRAFT_DIRECTIVE].meta.stylesUrl),
|
|
672
|
+
],
|
|
673
|
+
definition: directive[CRAFT_DIRECTIVE],
|
|
674
|
+
}));
|
|
675
|
+
this.styleReleases = acquireStyles(context, owners, context.ownerScope ?? '');
|
|
676
|
+
this.registrationReleases = node.directives.map((directive) => ɵregisterCraftTarget(context.injector, directive, context.componentContext, allocateCraftHostName(context.injector, 'directive', directive[CRAFT_DIRECTIVE].name), false));
|
|
677
|
+
this.view = createFragment(parent, before, context, [], 'craft-directive');
|
|
678
|
+
this.effectRef = createRenderEffect(context, 'craft-directive', () => {
|
|
679
|
+
this.node = this.descriptor();
|
|
680
|
+
this.view.patchChildren(renderCraftDirectiveNode(this.node, context));
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
firstNode() {
|
|
684
|
+
return this.view.firstNode();
|
|
685
|
+
}
|
|
686
|
+
lastNode() {
|
|
687
|
+
return this.view.lastNode();
|
|
688
|
+
}
|
|
689
|
+
patch(node) {
|
|
690
|
+
if (node.kind !== 'directive' ||
|
|
691
|
+
!sameCraftDirectives(this.node.directives, node.directives)) {
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
this.descriptor.set(node);
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
destroy() {
|
|
698
|
+
this.registrationReleases.forEach((release) => release());
|
|
699
|
+
this.effectRef.destroy();
|
|
700
|
+
this.view.destroy();
|
|
701
|
+
this.styleReleases.forEach((release) => release());
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const EVENT_NAMES = new Set([
|
|
705
|
+
'abort',
|
|
706
|
+
'blur',
|
|
707
|
+
'change',
|
|
708
|
+
'click',
|
|
709
|
+
'contextmenu',
|
|
710
|
+
'dblclick',
|
|
711
|
+
'error',
|
|
712
|
+
'focus',
|
|
713
|
+
'input',
|
|
714
|
+
'keydown',
|
|
715
|
+
'keypress',
|
|
716
|
+
'keyup',
|
|
717
|
+
'load',
|
|
718
|
+
'mousedown',
|
|
719
|
+
'mouseenter',
|
|
720
|
+
'mouseleave',
|
|
721
|
+
'mousemove',
|
|
722
|
+
'mouseout',
|
|
723
|
+
'mouseover',
|
|
724
|
+
'mouseup',
|
|
725
|
+
'pointerdown',
|
|
726
|
+
'pointerenter',
|
|
727
|
+
'pointerleave',
|
|
728
|
+
'pointermove',
|
|
729
|
+
'pointerup',
|
|
730
|
+
'reset',
|
|
731
|
+
'scroll',
|
|
732
|
+
'submit',
|
|
733
|
+
'touchend',
|
|
734
|
+
'touchmove',
|
|
735
|
+
'touchstart',
|
|
736
|
+
]);
|
|
737
|
+
const PROPERTY_NAMES = new Set([
|
|
738
|
+
'checked',
|
|
739
|
+
'disabled',
|
|
740
|
+
'multiple',
|
|
741
|
+
'selected',
|
|
742
|
+
'value',
|
|
743
|
+
]);
|
|
744
|
+
function eventNameFor(key, value) {
|
|
745
|
+
if (typeof value !== 'function') {
|
|
746
|
+
return undefined;
|
|
747
|
+
}
|
|
748
|
+
if (EVENT_NAMES.has(key)) {
|
|
749
|
+
return key;
|
|
750
|
+
}
|
|
751
|
+
if (/^on[A-Z]/.test(key)) {
|
|
752
|
+
return `${key[2].toLowerCase()}${key.slice(3)}`;
|
|
753
|
+
}
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
756
|
+
function interactionName(eventName, tag, localName, componentName) {
|
|
757
|
+
return [componentName, tag, localName, eventName]
|
|
758
|
+
.filter((part) => Boolean(part))
|
|
759
|
+
.join(':');
|
|
760
|
+
}
|
|
761
|
+
function executeDomEventHooks(hooks, interaction, action) {
|
|
762
|
+
let current = action;
|
|
763
|
+
for (let index = hooks.length - 1; index >= 0; index -= 1) {
|
|
764
|
+
const hook = hooks[index];
|
|
765
|
+
const next = current;
|
|
766
|
+
current = () => hook(interaction, next);
|
|
767
|
+
}
|
|
768
|
+
return current();
|
|
769
|
+
}
|
|
770
|
+
const HOST_PROPERTY_NAMES = new Set([
|
|
771
|
+
'accessKey',
|
|
772
|
+
'autocapitalize',
|
|
773
|
+
'autofocus',
|
|
774
|
+
'class',
|
|
775
|
+
'contentEditable',
|
|
776
|
+
'dir',
|
|
777
|
+
'draggable',
|
|
778
|
+
'hidden',
|
|
779
|
+
'id',
|
|
780
|
+
'inert',
|
|
781
|
+
'lang',
|
|
782
|
+
'nonce',
|
|
783
|
+
'slot',
|
|
784
|
+
'spellcheck',
|
|
785
|
+
'style',
|
|
786
|
+
'tabIndex',
|
|
787
|
+
'title',
|
|
788
|
+
'translate',
|
|
789
|
+
'attrs',
|
|
790
|
+
'directives',
|
|
791
|
+
]);
|
|
792
|
+
function isHostProperty(key) {
|
|
793
|
+
return (key === 'cssVars' ||
|
|
794
|
+
HOST_PROPERTY_NAMES.has(key) ||
|
|
795
|
+
key.startsWith('data-') ||
|
|
796
|
+
key.startsWith('aria-') ||
|
|
797
|
+
EVENT_NAMES.has(key) ||
|
|
798
|
+
(/^on[A-Z]/.test(key) &&
|
|
799
|
+
EVENT_NAMES.has(`${key[2].toLowerCase()}${key.slice(3)}`)));
|
|
800
|
+
}
|
|
801
|
+
function hostPropsFromComponentProps(props) {
|
|
802
|
+
const hostProps = Object.fromEntries(Object.entries(props).filter(([key]) => key !== 'cssVars' && isHostProperty(key)));
|
|
803
|
+
const vars = cssVarStyles(props['cssVars']);
|
|
804
|
+
return Object.keys(vars).length
|
|
805
|
+
? mergeHostProps({ style: vars }, hostProps)
|
|
806
|
+
: hostProps;
|
|
807
|
+
}
|
|
808
|
+
function className(value, context) {
|
|
809
|
+
if (typeof value === 'function') {
|
|
810
|
+
return className(resolveTemplateValue(value, context), context);
|
|
811
|
+
}
|
|
812
|
+
if (Array.isArray(value)) {
|
|
813
|
+
return value
|
|
814
|
+
.flatMap((item) => className(item, context))
|
|
815
|
+
.filter(Boolean)
|
|
816
|
+
.join(' ');
|
|
817
|
+
}
|
|
818
|
+
if (typeof value === 'object' && value !== null) {
|
|
819
|
+
return Object.entries(value)
|
|
820
|
+
.filter(([, enabled]) => enabled)
|
|
821
|
+
.map(([name]) => name)
|
|
822
|
+
.join(' ');
|
|
823
|
+
}
|
|
824
|
+
return value == null ? '' : String(value);
|
|
825
|
+
}
|
|
826
|
+
function containsRenderBinding(value, seen = new Set()) {
|
|
827
|
+
if (typeof value === 'function')
|
|
828
|
+
return true;
|
|
829
|
+
if (typeof value !== 'object' || value === null || seen.has(value)) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
seen.add(value);
|
|
833
|
+
return Object.values(value).some((child) => containsRenderBinding(child, seen));
|
|
834
|
+
}
|
|
835
|
+
function resolveStyleBindingValue(value, context) {
|
|
836
|
+
const resolved = resolveTemplateValue(value, context);
|
|
837
|
+
if (typeof resolved !== 'object' || resolved === null)
|
|
838
|
+
return resolved;
|
|
839
|
+
return Object.fromEntries(Object.entries(resolved).map(([key, child]) => [
|
|
840
|
+
key,
|
|
841
|
+
resolveTemplateValue(child, context),
|
|
842
|
+
]));
|
|
843
|
+
}
|
|
844
|
+
function sameStyleValue(left, right) {
|
|
845
|
+
if (Object.is(left, right))
|
|
846
|
+
return true;
|
|
847
|
+
if (typeof left !== 'object' ||
|
|
848
|
+
left === null ||
|
|
849
|
+
typeof right !== 'object' ||
|
|
850
|
+
right === null) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
const leftEntries = Object.entries(left);
|
|
854
|
+
const rightEntries = Object.entries(right);
|
|
855
|
+
const rightRecord = right;
|
|
856
|
+
return (leftEntries.length === rightEntries.length &&
|
|
857
|
+
leftEntries.every(([key, value]) => Object.is(value, rightRecord[key])));
|
|
858
|
+
}
|
|
859
|
+
function setStyleValue(_renderer, element, key, value, context) {
|
|
860
|
+
value = resolveTemplateValue(value, context);
|
|
861
|
+
const style = element
|
|
862
|
+
.style;
|
|
863
|
+
if (value === null || value === undefined || value === false) {
|
|
864
|
+
if (key.startsWith('--')) {
|
|
865
|
+
style.removeProperty(key);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
style[key] = '';
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
else if (key.startsWith('--')) {
|
|
872
|
+
style.setProperty(key, String(value));
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
style[key] = String(value);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function applyStyles(renderer, element, previous, next, context) {
|
|
879
|
+
previous = resolveTemplateValue(previous, context);
|
|
880
|
+
next = resolveTemplateValue(next, context);
|
|
881
|
+
if (typeof previous === 'object' && previous !== null) {
|
|
882
|
+
for (const key of Object.keys(previous)) {
|
|
883
|
+
if (typeof next !== 'object' || next === null || !(key in next)) {
|
|
884
|
+
setStyleValue(renderer, element, key, null, context);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
else if (typeof previous === 'string' && previous !== next) {
|
|
889
|
+
renderer.removeAttribute(element, 'style');
|
|
890
|
+
}
|
|
891
|
+
if (typeof next === 'string') {
|
|
892
|
+
renderer.setAttribute(element, 'style', next);
|
|
893
|
+
}
|
|
894
|
+
else if (typeof next === 'object' && next !== null) {
|
|
895
|
+
for (const [key, value] of Object.entries(next)) {
|
|
896
|
+
setStyleValue(renderer, element, key, value, context);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
renderer.removeAttribute(element, 'style');
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function flattenAttributes(props) {
|
|
904
|
+
const attributes = new Map();
|
|
905
|
+
for (const [key, value] of Object.entries(props)) {
|
|
906
|
+
if (key === 'attrs' && typeof value === 'object' && value !== null) {
|
|
907
|
+
for (const [attribute, attributeValue] of Object.entries(value)) {
|
|
908
|
+
attributes.set(attribute, attributeValue);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
else if (key !== 'class' &&
|
|
912
|
+
key !== 'style' &&
|
|
913
|
+
key !== 'directives' &&
|
|
914
|
+
key !== 'labelledBy' &&
|
|
915
|
+
key !== 'onClose' &&
|
|
916
|
+
!eventNameFor(key, value)) {
|
|
917
|
+
attributes.set(key, value);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return attributes;
|
|
921
|
+
}
|
|
922
|
+
function applyAttribute(renderer, element, key, value, context) {
|
|
923
|
+
value = resolveTemplateValue(value, context);
|
|
924
|
+
if (key.startsWith('data-craft-')) {
|
|
925
|
+
const dev = typeof ngDevMode === 'undefined' || ngDevMode;
|
|
926
|
+
if (dev) {
|
|
927
|
+
throw new Error('data-craft-* attributes are reserved for the Craft renderer.');
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (key === 'htmlFor') {
|
|
931
|
+
renderer.setProperty(element, key, value ?? '');
|
|
932
|
+
if (value === null || value === undefined || value === false) {
|
|
933
|
+
renderer.removeAttribute(element, 'for');
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (PROPERTY_NAMES.has(key)) {
|
|
938
|
+
renderer.setProperty(element, key, value ?? false);
|
|
939
|
+
if (value === null || value === undefined || value === false) {
|
|
940
|
+
renderer.removeAttribute(element, key);
|
|
941
|
+
}
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (value === null || value === undefined) {
|
|
945
|
+
renderer.removeAttribute(element, key);
|
|
946
|
+
}
|
|
947
|
+
else if (key.startsWith('aria-') && typeof value === 'boolean') {
|
|
948
|
+
renderer.setAttribute(element, key, value ? 'true' : 'false');
|
|
949
|
+
}
|
|
950
|
+
else if (value === false) {
|
|
951
|
+
renderer.removeAttribute(element, key);
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
renderer.setAttribute(element, key, value === true ? '' : String(value));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
class ElementRenderedNode {
|
|
958
|
+
kind = 'element';
|
|
959
|
+
children = [];
|
|
960
|
+
props = {};
|
|
961
|
+
listeners = new Map();
|
|
962
|
+
bindings = new Map();
|
|
963
|
+
craftNodeDirectiveTypes = [];
|
|
964
|
+
craftNodeDirectiveMounts = [];
|
|
965
|
+
localName;
|
|
966
|
+
dialogCleanup;
|
|
967
|
+
node;
|
|
968
|
+
tag;
|
|
969
|
+
context;
|
|
970
|
+
constructor(node, tag, context, initial) {
|
|
971
|
+
this.node = node;
|
|
972
|
+
this.tag = tag;
|
|
973
|
+
this.context = context;
|
|
974
|
+
this.patchProperties(initial);
|
|
975
|
+
this.installDialog(initial);
|
|
976
|
+
const nested = this.tag === 'dialog'
|
|
977
|
+
? childContext(context, {
|
|
978
|
+
rootScope: undefined,
|
|
979
|
+
contentScope: undefined,
|
|
980
|
+
headingLevel: 1,
|
|
981
|
+
})
|
|
982
|
+
: childContext(context, {
|
|
983
|
+
rootScope: undefined,
|
|
984
|
+
contentScope: undefined,
|
|
985
|
+
});
|
|
986
|
+
this.children = patchRenderedChildren(this.node, this.children, initial.children, null, nested);
|
|
987
|
+
this.reapplySelectValue(initial);
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* A `<select>` only accepts a `value` that matches one of its `<option>`s,
|
|
991
|
+
* and props are applied before children are mounted — so the initial
|
|
992
|
+
* assignment lands on an empty element, is dropped, and the browser falls
|
|
993
|
+
* back to the first option. The control then disagrees with the state it is
|
|
994
|
+
* bound to, silently.
|
|
995
|
+
*
|
|
996
|
+
* Re-assert it now that the options exist. Later updates arrive through the
|
|
997
|
+
* binding, by which point there is nothing to correct.
|
|
998
|
+
*/
|
|
999
|
+
reapplySelectValue(node) {
|
|
1000
|
+
if (this.tag !== 'select') {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const value = flattenAttributes(node.props).get('value');
|
|
1004
|
+
if (value === null || value === undefined) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const resolved = typeof value === 'function'
|
|
1008
|
+
? untracked(() => resolveTemplateValue(value, this.context))
|
|
1009
|
+
: value;
|
|
1010
|
+
applyAttribute(this.context.renderer, this.node, 'value', resolved, this.context);
|
|
1011
|
+
}
|
|
1012
|
+
firstNode() {
|
|
1013
|
+
return this.node;
|
|
1014
|
+
}
|
|
1015
|
+
lastNode() {
|
|
1016
|
+
return this.node;
|
|
1017
|
+
}
|
|
1018
|
+
patch(node) {
|
|
1019
|
+
if (node.kind !== 'element' ||
|
|
1020
|
+
node.tag !== this.tag ||
|
|
1021
|
+
node.localName !== this.localName) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
this.patchProperties(node);
|
|
1025
|
+
this.installDialog(node);
|
|
1026
|
+
this.children = patchRenderedChildren(this.node, this.children, node.children, null, childContext(this.context, {
|
|
1027
|
+
rootScope: undefined,
|
|
1028
|
+
contentScope: undefined,
|
|
1029
|
+
}));
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
patchProperties(nextNode) {
|
|
1033
|
+
const next = nextNode.props;
|
|
1034
|
+
const renderer = this.context.renderer;
|
|
1035
|
+
const previousAttributes = flattenAttributes(this.props);
|
|
1036
|
+
const nextAttributes = flattenAttributes(next);
|
|
1037
|
+
for (const key of previousAttributes.keys()) {
|
|
1038
|
+
if (!nextAttributes.has(key)) {
|
|
1039
|
+
this.destroyBinding(`attribute:${key}`);
|
|
1040
|
+
applyAttribute(renderer, this.node, key, null, this.context);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
for (const [key, value] of nextAttributes) {
|
|
1044
|
+
if (typeof value === 'function') {
|
|
1045
|
+
this.updateBinding(`attribute:${key}`, value, () => resolveTemplateValue(value, this.context), (resolved) => applyAttribute(renderer, this.node, key, resolved, this.context));
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
this.destroyBinding(`attribute:${key}`);
|
|
1049
|
+
}
|
|
1050
|
+
if (typeof value !== 'function' &&
|
|
1051
|
+
(!Object.is(previousAttributes.get(key), value) ||
|
|
1052
|
+
typeof previousAttributes.get(key) === 'function')) {
|
|
1053
|
+
applyAttribute(renderer, this.node, key, value, this.context);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (containsRenderBinding(next['class'])) {
|
|
1057
|
+
this.updateBinding('class', next['class'], () => className(next['class'], this.context), (value) => {
|
|
1058
|
+
if (value) {
|
|
1059
|
+
renderer.setAttribute(this.node, 'class', String(value));
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
renderer.removeAttribute(this.node, 'class');
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
this.destroyBinding('class');
|
|
1068
|
+
}
|
|
1069
|
+
if (!containsRenderBinding(next['class']) &&
|
|
1070
|
+
(!Object.is(this.props['class'], next['class']) ||
|
|
1071
|
+
containsRenderBinding(this.props['class']))) {
|
|
1072
|
+
const value = className(next['class'], this.context);
|
|
1073
|
+
if (value) {
|
|
1074
|
+
renderer.setAttribute(this.node, 'class', value);
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
renderer.removeAttribute(this.node, 'class');
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (containsRenderBinding(next['style'])) {
|
|
1081
|
+
this.updateBinding('style', next['style'], () => resolveStyleBindingValue(next['style'], this.context), (value) => {
|
|
1082
|
+
renderer.removeAttribute(this.node, 'style');
|
|
1083
|
+
applyStyles(renderer, this.node, undefined, value, this.context);
|
|
1084
|
+
}, sameStyleValue);
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
this.destroyBinding('style');
|
|
1088
|
+
}
|
|
1089
|
+
if (!containsRenderBinding(next['style']) &&
|
|
1090
|
+
(!Object.is(this.props['style'], next['style']) ||
|
|
1091
|
+
containsRenderBinding(this.props['style']))) {
|
|
1092
|
+
if (containsRenderBinding(this.props['style'])) {
|
|
1093
|
+
renderer.removeAttribute(this.node, 'style');
|
|
1094
|
+
}
|
|
1095
|
+
applyStyles(renderer, this.node, containsRenderBinding(this.props['style'])
|
|
1096
|
+
? undefined
|
|
1097
|
+
: this.props['style'], next['style'], this.context);
|
|
1098
|
+
}
|
|
1099
|
+
const nextEvents = new Map();
|
|
1100
|
+
for (const [key, value] of Object.entries(next)) {
|
|
1101
|
+
const eventName = eventNameFor(key, value);
|
|
1102
|
+
if (eventName) {
|
|
1103
|
+
nextEvents.set(eventName, value);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
for (const [eventName, dispose] of this.listeners) {
|
|
1107
|
+
if (!nextEvents.has(eventName)) {
|
|
1108
|
+
dispose();
|
|
1109
|
+
this.listeners.delete(eventName);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
for (const [eventName] of nextEvents) {
|
|
1113
|
+
if (!this.listeners.has(eventName)) {
|
|
1114
|
+
this.listeners.set(eventName, renderer.listen(this.node, eventName, (event) => {
|
|
1115
|
+
const listener = Object.entries(this.props).find(([key, value]) => eventNameFor(key, value) === eventName)?.[1];
|
|
1116
|
+
if (!listener) {
|
|
1117
|
+
return undefined;
|
|
1118
|
+
}
|
|
1119
|
+
const interaction = {
|
|
1120
|
+
event,
|
|
1121
|
+
eventName,
|
|
1122
|
+
element: this.node,
|
|
1123
|
+
elementTag: this.tag,
|
|
1124
|
+
elementName: this.localName,
|
|
1125
|
+
componentName: this.context.componentName,
|
|
1126
|
+
interactionName: interactionName(eventName, this.tag, this.localName, this.context.componentName),
|
|
1127
|
+
};
|
|
1128
|
+
const hooks = this.context.injector.get(CRAFT_DOM_EVENT_HOOK);
|
|
1129
|
+
runInInjectionContext(this.context.injector, () => executeDomEventHooks(hooks, interaction, () => executeTemplateCallback(listener, [event], this.context)));
|
|
1130
|
+
return undefined;
|
|
1131
|
+
}));
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
const craftNodeDirectives = nextNode[CRAFT_NODE_DIRECTIVES] ?? [];
|
|
1135
|
+
if (!sameCraftNodeDirectives(this.craftNodeDirectiveTypes, craftNodeDirectives)) {
|
|
1136
|
+
this.craftNodeDirectiveMounts.forEach((mount) => mount.destroy());
|
|
1137
|
+
this.craftNodeDirectiveMounts = craftNodeDirectives.map((descriptor) => new CraftNodeDirectiveMount(descriptor, this.node, this.context));
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
this.craftNodeDirectiveMounts.forEach((mount, index) => mount.update(craftNodeDirectives[index]));
|
|
1141
|
+
}
|
|
1142
|
+
this.craftNodeDirectiveTypes = craftNodeDirectives;
|
|
1143
|
+
if (this.localName !== nextNode.localName) {
|
|
1144
|
+
if (nextNode.localName === undefined) {
|
|
1145
|
+
renderer.removeAttribute(this.node, 'data-craft-name');
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
renderer.setAttribute(this.node, 'data-craft-name', nextNode.localName);
|
|
1149
|
+
}
|
|
1150
|
+
this.localName = nextNode.localName;
|
|
1151
|
+
}
|
|
1152
|
+
const contentNames = directCraftContentNames(nextNode.children);
|
|
1153
|
+
if (contentNames.length > 0) {
|
|
1154
|
+
this.node[CRAFT_LOCATOR_CONTENT_NAMES] = contentNames;
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
delete this.node[CRAFT_LOCATOR_CONTENT_NAMES];
|
|
1158
|
+
}
|
|
1159
|
+
this.props = next;
|
|
1160
|
+
}
|
|
1161
|
+
updateBinding(key, source, evaluate, apply, equals = Object.is) {
|
|
1162
|
+
const current = this.bindings.get(key);
|
|
1163
|
+
if (current?.source === source)
|
|
1164
|
+
return;
|
|
1165
|
+
current?.effectRef.destroy();
|
|
1166
|
+
let initialized = false;
|
|
1167
|
+
let previous;
|
|
1168
|
+
const effectRef = createRenderEffect(this.context, `element-${key}-binding`, () => {
|
|
1169
|
+
const value = evaluate();
|
|
1170
|
+
if (!initialized || !equals(previous, value)) {
|
|
1171
|
+
apply(value);
|
|
1172
|
+
previous = value;
|
|
1173
|
+
initialized = true;
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
this.bindings.set(key, { source, effectRef });
|
|
1177
|
+
}
|
|
1178
|
+
destroyBinding(key) {
|
|
1179
|
+
this.bindings.get(key)?.effectRef.destroy();
|
|
1180
|
+
this.bindings.delete(key);
|
|
1181
|
+
}
|
|
1182
|
+
installDialog(nextNode) {
|
|
1183
|
+
if (this.tag !== 'dialog')
|
|
1184
|
+
return;
|
|
1185
|
+
const dialog = this.node;
|
|
1186
|
+
const renderer = this.context.renderer;
|
|
1187
|
+
if (!dialog.getAttribute('role')) {
|
|
1188
|
+
renderer.setAttribute(dialog, 'role', 'dialog');
|
|
1189
|
+
}
|
|
1190
|
+
if (!dialog.getAttribute('aria-modal')) {
|
|
1191
|
+
renderer.setAttribute(dialog, 'aria-modal', 'true');
|
|
1192
|
+
}
|
|
1193
|
+
const labelledBy = nextNode.props['labelledBy'];
|
|
1194
|
+
const label = nextNode.props['label'] ?? nextNode.props['aria-label'];
|
|
1195
|
+
if (typeof labelledBy === 'string' && labelledBy) {
|
|
1196
|
+
renderer.setAttribute(dialog, 'aria-labelledby', labelledBy);
|
|
1197
|
+
}
|
|
1198
|
+
else if (typeof label === 'string' && label) {
|
|
1199
|
+
renderer.setAttribute(dialog, 'aria-label', label);
|
|
1200
|
+
}
|
|
1201
|
+
const onClose = nextNode.props['onClose'];
|
|
1202
|
+
this.dialogCleanup?.();
|
|
1203
|
+
const cancel = renderer.listen(dialog, 'cancel', (event) => {
|
|
1204
|
+
event.preventDefault();
|
|
1205
|
+
if (typeof onClose === 'function')
|
|
1206
|
+
onClose(event);
|
|
1207
|
+
if (typeof dialog.close === 'function')
|
|
1208
|
+
dialog.close();
|
|
1209
|
+
});
|
|
1210
|
+
const close = renderer.listen(dialog, 'close', (event) => {
|
|
1211
|
+
if (typeof onClose === 'function')
|
|
1212
|
+
onClose(event);
|
|
1213
|
+
});
|
|
1214
|
+
this.dialogCleanup = () => {
|
|
1215
|
+
cancel();
|
|
1216
|
+
close();
|
|
1217
|
+
};
|
|
1218
|
+
const open = nextNode.props['open'];
|
|
1219
|
+
if (open && typeof dialog.showModal === 'function' && !dialog.open) {
|
|
1220
|
+
dialog.showModal();
|
|
1221
|
+
}
|
|
1222
|
+
else if (open === false &&
|
|
1223
|
+
typeof dialog.close === 'function' &&
|
|
1224
|
+
dialog.open) {
|
|
1225
|
+
dialog.close();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
destroy() {
|
|
1229
|
+
this.dialogCleanup?.();
|
|
1230
|
+
this.bindings.forEach(({ effectRef }) => effectRef.destroy());
|
|
1231
|
+
this.bindings.clear();
|
|
1232
|
+
this.listeners.forEach((dispose) => dispose());
|
|
1233
|
+
this.listeners.clear();
|
|
1234
|
+
this.craftNodeDirectiveMounts.forEach((mount) => mount.destroy());
|
|
1235
|
+
this.craftNodeDirectiveMounts = [];
|
|
1236
|
+
this.children.forEach((child) => child.destroy());
|
|
1237
|
+
removeNode(this.context.renderer, this.node);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
class FragmentRenderedNode {
|
|
1241
|
+
kind = 'fragment';
|
|
1242
|
+
children = [];
|
|
1243
|
+
/**
|
|
1244
|
+
* Where this fragment currently lives. A boundary can detach a range into a
|
|
1245
|
+
* holder fragment to hide it without destroying it, and the range keeps being
|
|
1246
|
+
* reconciled while detached — so every insertion has to follow the markers
|
|
1247
|
+
* rather than the parent captured at construction time.
|
|
1248
|
+
*/
|
|
1249
|
+
get liveParent() {
|
|
1250
|
+
return this.start.parentNode ?? this.parent;
|
|
1251
|
+
}
|
|
1252
|
+
parent;
|
|
1253
|
+
start;
|
|
1254
|
+
end;
|
|
1255
|
+
context;
|
|
1256
|
+
constructor(parent, start, end, context, initialChildren) {
|
|
1257
|
+
this.parent = parent;
|
|
1258
|
+
this.start = start;
|
|
1259
|
+
this.end = end;
|
|
1260
|
+
this.context = context;
|
|
1261
|
+
this.children = patchRenderedChildren(parent, this.children, initialChildren, end, context);
|
|
1262
|
+
}
|
|
1263
|
+
firstNode() {
|
|
1264
|
+
return this.start;
|
|
1265
|
+
}
|
|
1266
|
+
lastNode() {
|
|
1267
|
+
return this.end;
|
|
1268
|
+
}
|
|
1269
|
+
patch(_node) {
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
patchChildren(children) {
|
|
1273
|
+
this.children = patchRenderedChildren(this.liveParent, this.children, children, this.end, this.context);
|
|
1274
|
+
}
|
|
1275
|
+
hasChildren() {
|
|
1276
|
+
return this.children.length > 0;
|
|
1277
|
+
}
|
|
1278
|
+
appendChildren(children) {
|
|
1279
|
+
const nextNodes = normalizeChildren(children);
|
|
1280
|
+
nextNodes.forEach((child) => {
|
|
1281
|
+
this.children.push(mountNode(child, this.liveParent, this.end, this.context));
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
prependChildren(children) {
|
|
1285
|
+
const nextNodes = normalizeChildren(children);
|
|
1286
|
+
const before = this.children[0]?.firstNode() ?? this.end;
|
|
1287
|
+
const mounted = [];
|
|
1288
|
+
for (let index = nextNodes.length - 1; index >= 0; index -= 1) {
|
|
1289
|
+
mounted.unshift(mountNode(nextNodes[index], this.liveParent, before, this.context));
|
|
1290
|
+
}
|
|
1291
|
+
this.children.unshift(...mounted);
|
|
1292
|
+
}
|
|
1293
|
+
updateContext(context) {
|
|
1294
|
+
this.context = context;
|
|
1295
|
+
}
|
|
1296
|
+
moveBefore(before) {
|
|
1297
|
+
let current = this.start;
|
|
1298
|
+
const nodes = [];
|
|
1299
|
+
while (current) {
|
|
1300
|
+
nodes.push(current);
|
|
1301
|
+
if (current === this.end) {
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
current = current.nextSibling;
|
|
1305
|
+
}
|
|
1306
|
+
nodes.forEach((node) => insertBefore(this.context.renderer, this.liveParent, node, before));
|
|
1307
|
+
}
|
|
1308
|
+
destroy() {
|
|
1309
|
+
this.children.forEach((child) => child.destroy());
|
|
1310
|
+
removeNode(this.context.renderer, this.start);
|
|
1311
|
+
removeNode(this.context.renderer, this.end);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
class ProjectionRenderedNode {
|
|
1315
|
+
kind = 'projection';
|
|
1316
|
+
view;
|
|
1317
|
+
styleRelease;
|
|
1318
|
+
node;
|
|
1319
|
+
declarationContext;
|
|
1320
|
+
projectionContext;
|
|
1321
|
+
descriptor;
|
|
1322
|
+
effectRef;
|
|
1323
|
+
constructor(node, parent, before, context) {
|
|
1324
|
+
this.node = node;
|
|
1325
|
+
this.descriptor = signal(node);
|
|
1326
|
+
this.declarationContext =
|
|
1327
|
+
node.declarationContext ??
|
|
1328
|
+
lexicalContext(context);
|
|
1329
|
+
const contentStyle = acquireContentStyle(context, node.slotName, node.stylePolicy);
|
|
1330
|
+
this.styleRelease = contentStyle.release;
|
|
1331
|
+
this.projectionContext = childContext(this.declarationContext, {
|
|
1332
|
+
contentScope: contentStyle.scope,
|
|
1333
|
+
});
|
|
1334
|
+
this.view = createFragment(parent, before, this.projectionContext, [], 'craft-projection');
|
|
1335
|
+
this.effectRef = createRenderEffect(this.projectionContext, 'projection', () => {
|
|
1336
|
+
this.node = this.descriptor();
|
|
1337
|
+
this.view.patchChildren(this.children());
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
firstNode() {
|
|
1341
|
+
return this.view.firstNode();
|
|
1342
|
+
}
|
|
1343
|
+
lastNode() {
|
|
1344
|
+
return this.view.lastNode();
|
|
1345
|
+
}
|
|
1346
|
+
patch(node) {
|
|
1347
|
+
if (node.kind !== 'projection' ||
|
|
1348
|
+
node.declarationContext !== this.node.declarationContext) {
|
|
1349
|
+
return false;
|
|
1350
|
+
}
|
|
1351
|
+
this.descriptor.set(node);
|
|
1352
|
+
return true;
|
|
1353
|
+
}
|
|
1354
|
+
children() {
|
|
1355
|
+
return executeTemplateTrace(this.projectionContext.injector, traceContext(this.projectionContext, 'projection', tracePhase(this.projectionContext), this.node.slotName), () => runInInjectionContext(this.declarationContext.injector, () => withCraftRenderContext(this.declarationContext, () => resolveProjectionValue(this.node.render(), this.projectionContext))));
|
|
1356
|
+
}
|
|
1357
|
+
destroy() {
|
|
1358
|
+
this.effectRef.destroy();
|
|
1359
|
+
this.view.destroy();
|
|
1360
|
+
this.styleRelease();
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
class TemplateRenderedNode {
|
|
1364
|
+
kind = 'template';
|
|
1365
|
+
view;
|
|
1366
|
+
node;
|
|
1367
|
+
declarationContext;
|
|
1368
|
+
descriptor;
|
|
1369
|
+
effectRef;
|
|
1370
|
+
constructor(node, parent, before, context) {
|
|
1371
|
+
this.node = node;
|
|
1372
|
+
this.descriptor = signal(node);
|
|
1373
|
+
this.declarationContext =
|
|
1374
|
+
node.declarationContext ??
|
|
1375
|
+
lexicalContext(context);
|
|
1376
|
+
this.view = createFragment(parent, before, this.declarationContext, [], 'craft-template');
|
|
1377
|
+
this.effectRef = createRenderEffect(this.declarationContext, 'template', () => {
|
|
1378
|
+
this.node = this.descriptor();
|
|
1379
|
+
this.view.patchChildren(this.children());
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
firstNode() {
|
|
1383
|
+
return this.view.firstNode();
|
|
1384
|
+
}
|
|
1385
|
+
lastNode() {
|
|
1386
|
+
return this.view.lastNode();
|
|
1387
|
+
}
|
|
1388
|
+
patch(node) {
|
|
1389
|
+
if (node.kind !== 'template' || node.template !== this.node.template) {
|
|
1390
|
+
return false;
|
|
1391
|
+
}
|
|
1392
|
+
this.descriptor.set(node);
|
|
1393
|
+
return true;
|
|
1394
|
+
}
|
|
1395
|
+
children() {
|
|
1396
|
+
return renderChildrenCallback(this.declarationContext, this.node.template, [
|
|
1397
|
+
resolveTemplateContext(this.node.context, this.declarationContext),
|
|
1398
|
+
]);
|
|
1399
|
+
}
|
|
1400
|
+
destroy() {
|
|
1401
|
+
this.effectRef.destroy();
|
|
1402
|
+
this.view.destroy();
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function createFragment(parent, before, context, children, label) {
|
|
1406
|
+
const start = mountComment(parent, `${label}:start`, before, context);
|
|
1407
|
+
const end = mountComment(parent, `${label}:end`, before, context);
|
|
1408
|
+
return new FragmentRenderedNode(parent, start, end, context, children);
|
|
1409
|
+
}
|
|
1410
|
+
class EachRenderedNode {
|
|
1411
|
+
kind = 'each';
|
|
1412
|
+
entries = new Map();
|
|
1413
|
+
ordered = [];
|
|
1414
|
+
emptyView;
|
|
1415
|
+
descriptor;
|
|
1416
|
+
effectRef;
|
|
1417
|
+
node;
|
|
1418
|
+
parent;
|
|
1419
|
+
start;
|
|
1420
|
+
end;
|
|
1421
|
+
context;
|
|
1422
|
+
scheduler;
|
|
1423
|
+
ownsScheduler = false;
|
|
1424
|
+
schedulePolicy;
|
|
1425
|
+
schedulerConfigured = false;
|
|
1426
|
+
pendingTasks = new Set();
|
|
1427
|
+
reconcileVersion = 0;
|
|
1428
|
+
constructor(node, parent, start, end, context) {
|
|
1429
|
+
this.node = node;
|
|
1430
|
+
this.parent = parent;
|
|
1431
|
+
this.start = start;
|
|
1432
|
+
this.end = end;
|
|
1433
|
+
this.context = context;
|
|
1434
|
+
this.scheduler = createEachScheduler({
|
|
1435
|
+
enabled: false,
|
|
1436
|
+
strategy: 'sync',
|
|
1437
|
+
frameBudgetMs: 4,
|
|
1438
|
+
});
|
|
1439
|
+
this.configureScheduler(node.schedule);
|
|
1440
|
+
this.descriptor = signal(node);
|
|
1441
|
+
this.effectRef = createRenderEffect(context, 'each-block', () => {
|
|
1442
|
+
const previousItemTemplate = this.node.itemTemplate;
|
|
1443
|
+
this.node = this.descriptor();
|
|
1444
|
+
this.configureScheduler(this.node.schedule);
|
|
1445
|
+
this.reconcile(this.node.itemTemplate !== previousItemTemplate);
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
firstNode() {
|
|
1449
|
+
return this.start;
|
|
1450
|
+
}
|
|
1451
|
+
lastNode() {
|
|
1452
|
+
return this.end;
|
|
1453
|
+
}
|
|
1454
|
+
patch(node) {
|
|
1455
|
+
if (node.kind !== 'each') {
|
|
1456
|
+
return false;
|
|
1457
|
+
}
|
|
1458
|
+
this.descriptor.set(node);
|
|
1459
|
+
return true;
|
|
1460
|
+
}
|
|
1461
|
+
reconcile(itemTemplateChanged = false) {
|
|
1462
|
+
const items = resolveTemplateValue(this.node.source, this.context);
|
|
1463
|
+
const collection = items ?? [];
|
|
1464
|
+
const keys = collection.map((item, index) => this.node.track(item, index));
|
|
1465
|
+
const seenKeys = new Set();
|
|
1466
|
+
keys.forEach((key) => {
|
|
1467
|
+
if (seenKeys.has(key)) {
|
|
1468
|
+
throw new Error(`each() received the duplicate key "${String(key)}".`);
|
|
1469
|
+
}
|
|
1470
|
+
seenKeys.add(key);
|
|
1471
|
+
});
|
|
1472
|
+
this.reconcileVersion += 1;
|
|
1473
|
+
this.cancelPendingTasks();
|
|
1474
|
+
if (collection.length === 0) {
|
|
1475
|
+
this.entries.forEach((entry) => {
|
|
1476
|
+
entry.renderEffect?.destroy();
|
|
1477
|
+
entry.view.destroy();
|
|
1478
|
+
});
|
|
1479
|
+
this.entries.clear();
|
|
1480
|
+
this.ordered = [];
|
|
1481
|
+
if (this.node.empty) {
|
|
1482
|
+
const emptyContext = childContext(this.context, {
|
|
1483
|
+
identity: childCraftRenderIdentity(this.context.identity, 'empty'),
|
|
1484
|
+
});
|
|
1485
|
+
if (this.emptyView) {
|
|
1486
|
+
this.emptyView.patchChildren(renderBlockChildren(emptyContext, 'empty', this.node.empty));
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
this.emptyView = createFragment(this.parent, this.end, emptyContext, renderBlockChildren(emptyContext, 'empty', this.node.empty), 'craft-empty');
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
else {
|
|
1493
|
+
this.emptyView?.destroy();
|
|
1494
|
+
this.emptyView = undefined;
|
|
1495
|
+
}
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
this.emptyView?.destroy();
|
|
1499
|
+
this.emptyView = undefined;
|
|
1500
|
+
const previous = this.entries;
|
|
1501
|
+
const next = new Map();
|
|
1502
|
+
const nextOrdered = [];
|
|
1503
|
+
const entriesToRender = [];
|
|
1504
|
+
collection.forEach((item, index) => {
|
|
1505
|
+
const key = keys[index];
|
|
1506
|
+
let entry = previous.get(key);
|
|
1507
|
+
if (entry) {
|
|
1508
|
+
if (itemTemplateChanged ||
|
|
1509
|
+
!Object.is(entry.item, item) ||
|
|
1510
|
+
entry.index !== index ||
|
|
1511
|
+
!entry.rendered) {
|
|
1512
|
+
entry.item = item;
|
|
1513
|
+
entry.index = index;
|
|
1514
|
+
entriesToRender.push(entry);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
else {
|
|
1518
|
+
const traceState = { renderCount: 0 };
|
|
1519
|
+
const entryContext = childContext(this.context, {
|
|
1520
|
+
identity: childCraftRenderIdentity(this.context.identity, `item:${String(key)}`),
|
|
1521
|
+
});
|
|
1522
|
+
entry = {
|
|
1523
|
+
key,
|
|
1524
|
+
item,
|
|
1525
|
+
index,
|
|
1526
|
+
traceState,
|
|
1527
|
+
rendered: false,
|
|
1528
|
+
view: createFragment(this.parent, this.end, entryContext, [], `craft-each:${String(key)}`),
|
|
1529
|
+
};
|
|
1530
|
+
entriesToRender.push(entry);
|
|
1531
|
+
}
|
|
1532
|
+
next.set(key, entry);
|
|
1533
|
+
nextOrdered.push(entry.view);
|
|
1534
|
+
});
|
|
1535
|
+
previous.forEach((entry, key) => {
|
|
1536
|
+
if (!next.has(key)) {
|
|
1537
|
+
entry.renderEffect?.destroy();
|
|
1538
|
+
entry.view.destroy();
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
let before = this.end;
|
|
1542
|
+
for (let index = nextOrdered.length - 1; index >= 0; index -= 1) {
|
|
1543
|
+
const entry = nextOrdered[index];
|
|
1544
|
+
if (entry.lastNode().nextSibling !== before) {
|
|
1545
|
+
entry.moveBefore(before);
|
|
1546
|
+
}
|
|
1547
|
+
before = entry.firstNode();
|
|
1548
|
+
}
|
|
1549
|
+
this.entries = next;
|
|
1550
|
+
this.ordered = nextOrdered;
|
|
1551
|
+
entriesToRender.forEach((entry) => this.scheduleEntryRender(entry));
|
|
1552
|
+
}
|
|
1553
|
+
configureScheduler(policy) {
|
|
1554
|
+
if (this.schedulerConfigured && this.schedulePolicy === policy)
|
|
1555
|
+
return;
|
|
1556
|
+
if (this.ownsScheduler)
|
|
1557
|
+
this.scheduler.destroy?.();
|
|
1558
|
+
this.schedulePolicy = policy;
|
|
1559
|
+
this.schedulerConfigured = true;
|
|
1560
|
+
this.cancelPendingTasks();
|
|
1561
|
+
const injected = this.context.injector.get(EACH_SCHEDULER, null);
|
|
1562
|
+
if (injected) {
|
|
1563
|
+
this.scheduler = injected;
|
|
1564
|
+
this.ownsScheduler = false;
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
this.scheduler = createEachScheduler(policy ?? { enabled: false, strategy: 'sync', frameBudgetMs: 4 });
|
|
1568
|
+
this.ownsScheduler = true;
|
|
1569
|
+
}
|
|
1570
|
+
scheduleEntryRender(entry) {
|
|
1571
|
+
const version = this.reconcileVersion;
|
|
1572
|
+
const scheduled = {
|
|
1573
|
+
completed: false,
|
|
1574
|
+
handle: undefined,
|
|
1575
|
+
};
|
|
1576
|
+
const handle = this.scheduler.schedule(() => {
|
|
1577
|
+
scheduled.completed = true;
|
|
1578
|
+
if (scheduled.handle)
|
|
1579
|
+
this.pendingTasks.delete(scheduled.handle);
|
|
1580
|
+
if (version !== this.reconcileVersion || !this.entries.has(entry.key)) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
entry.renderEffect?.destroy();
|
|
1584
|
+
entry.renderEffect = createRenderEffect(this.context, 'each-item', () => entry.view.patchChildren(this.renderItem(entry.traceState, entry.item, entry.index, entry.key)));
|
|
1585
|
+
entry.rendered = true;
|
|
1586
|
+
});
|
|
1587
|
+
scheduled.handle = handle;
|
|
1588
|
+
if (!scheduled.completed)
|
|
1589
|
+
this.pendingTasks.add(handle);
|
|
1590
|
+
}
|
|
1591
|
+
cancelPendingTasks() {
|
|
1592
|
+
this.pendingTasks.forEach((handle) => handle.cancel());
|
|
1593
|
+
this.pendingTasks.clear();
|
|
1594
|
+
}
|
|
1595
|
+
renderItem(traceState, item, index, key) {
|
|
1596
|
+
traceState.renderCount += 1;
|
|
1597
|
+
return renderBlockChildren(childContext(this.context, {
|
|
1598
|
+
traceState,
|
|
1599
|
+
identity: childCraftRenderIdentity(this.context.identity, `item:${String(key)}`),
|
|
1600
|
+
}), 'each', this.node.itemTemplate, [
|
|
1601
|
+
function* () {
|
|
1602
|
+
return item;
|
|
1603
|
+
},
|
|
1604
|
+
index,
|
|
1605
|
+
]);
|
|
1606
|
+
}
|
|
1607
|
+
destroy() {
|
|
1608
|
+
this.effectRef.destroy();
|
|
1609
|
+
this.reconcileVersion += 1;
|
|
1610
|
+
this.cancelPendingTasks();
|
|
1611
|
+
if (this.ownsScheduler)
|
|
1612
|
+
this.scheduler.destroy?.();
|
|
1613
|
+
this.entries.forEach((entry) => {
|
|
1614
|
+
entry.renderEffect?.destroy();
|
|
1615
|
+
entry.view.destroy();
|
|
1616
|
+
});
|
|
1617
|
+
this.emptyView?.destroy();
|
|
1618
|
+
removeNode(this.context.renderer, this.start);
|
|
1619
|
+
removeNode(this.context.renderer, this.end);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
class IfBlockRenderedNode {
|
|
1623
|
+
kind = 'if';
|
|
1624
|
+
view;
|
|
1625
|
+
active;
|
|
1626
|
+
descriptor;
|
|
1627
|
+
effectRef;
|
|
1628
|
+
node;
|
|
1629
|
+
context;
|
|
1630
|
+
constructor(node, parent, before, context) {
|
|
1631
|
+
this.node = node;
|
|
1632
|
+
this.context = context;
|
|
1633
|
+
this.descriptor = signal(node);
|
|
1634
|
+
this.view = createFragment(parent, before, context, [], `craft-if:${node.conditionName}`);
|
|
1635
|
+
this.effectRef = createRenderEffect(context, 'if-block', () => {
|
|
1636
|
+
this.node = this.descriptor();
|
|
1637
|
+
this.active = this.isTrue();
|
|
1638
|
+
this.view.patchChildren(this.children());
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
firstNode() {
|
|
1642
|
+
return this.view.firstNode();
|
|
1643
|
+
}
|
|
1644
|
+
lastNode() {
|
|
1645
|
+
return this.view.lastNode();
|
|
1646
|
+
}
|
|
1647
|
+
patch(node) {
|
|
1648
|
+
if (node.kind !== 'if' || node.conditionName !== this.node.conditionName) {
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
this.descriptor.set(node);
|
|
1652
|
+
return true;
|
|
1653
|
+
}
|
|
1654
|
+
isTrue() {
|
|
1655
|
+
return Boolean(resolveTemplateValue(this.node.condition, this.context));
|
|
1656
|
+
}
|
|
1657
|
+
children() {
|
|
1658
|
+
return this.active
|
|
1659
|
+
? renderBlockChildren(this.context, this.node.conditionName, this.node.whenTrue)
|
|
1660
|
+
: this.node.whenFalse
|
|
1661
|
+
? renderBlockChildren(this.context, this.node.conditionName, this.node.whenFalse)
|
|
1662
|
+
: [];
|
|
1663
|
+
}
|
|
1664
|
+
destroy() {
|
|
1665
|
+
this.effectRef.destroy();
|
|
1666
|
+
this.view.destroy();
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
class MatchBlockRenderedNode {
|
|
1670
|
+
kind = 'match-block';
|
|
1671
|
+
view;
|
|
1672
|
+
handledExceptionCode;
|
|
1673
|
+
descriptor;
|
|
1674
|
+
effectRef;
|
|
1675
|
+
node;
|
|
1676
|
+
context;
|
|
1677
|
+
constructor(node, parent, before, context) {
|
|
1678
|
+
this.node = node;
|
|
1679
|
+
this.context = context;
|
|
1680
|
+
this.descriptor = signal(node);
|
|
1681
|
+
this.view = createFragment(parent, before, context, [], 'craft-match-block');
|
|
1682
|
+
this.effectRef = createRenderEffect(context, 'match-block', () => {
|
|
1683
|
+
this.node = this.descriptor();
|
|
1684
|
+
this.view.patchChildren(this.children());
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
firstNode() {
|
|
1688
|
+
return this.view.firstNode();
|
|
1689
|
+
}
|
|
1690
|
+
lastNode() {
|
|
1691
|
+
return this.view.lastNode();
|
|
1692
|
+
}
|
|
1693
|
+
patch(node) {
|
|
1694
|
+
if (node.kind !== 'match-block' || node.key !== this.node.key) {
|
|
1695
|
+
return false;
|
|
1696
|
+
}
|
|
1697
|
+
this.descriptor.set(node);
|
|
1698
|
+
return true;
|
|
1699
|
+
}
|
|
1700
|
+
children() {
|
|
1701
|
+
if (this.handledExceptionCode) {
|
|
1702
|
+
this.context.handledResourceExceptionCodes?.delete(this.handledExceptionCode);
|
|
1703
|
+
this.handledExceptionCode = undefined;
|
|
1704
|
+
}
|
|
1705
|
+
const exception = resolveTemplateValue(this.node.source, this.context);
|
|
1706
|
+
if (!exception)
|
|
1707
|
+
return [];
|
|
1708
|
+
const code = exception[this.node.key];
|
|
1709
|
+
// Resource exception buckets use an empty object when no exception is
|
|
1710
|
+
// present. Do not turn that implementation detail into an exception with
|
|
1711
|
+
// an `undefined` discriminator during a later component rerender.
|
|
1712
|
+
if (code === undefined || code === null)
|
|
1713
|
+
return [];
|
|
1714
|
+
const handler = this.node.handlers[String(code)];
|
|
1715
|
+
if (!handler) {
|
|
1716
|
+
throw new CraftUnhandledExceptionError(exception);
|
|
1717
|
+
}
|
|
1718
|
+
this.handledExceptionCode = String(code);
|
|
1719
|
+
this.context.handledResourceExceptionCodes?.add(this.handledExceptionCode);
|
|
1720
|
+
return executeTemplateTrace(this.context.injector, traceContext(this.context, 'block', tracePhase(this.context), String(this.node.key)), () => handler(exception));
|
|
1721
|
+
}
|
|
1722
|
+
destroy() {
|
|
1723
|
+
this.effectRef.destroy();
|
|
1724
|
+
if (this.handledExceptionCode) {
|
|
1725
|
+
this.context.handledResourceExceptionCodes?.delete(this.handledExceptionCode);
|
|
1726
|
+
}
|
|
1727
|
+
this.view.destroy();
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Detaches a mounted DOM range into a holder fragment, keeping every node (and
|
|
1732
|
+
* therefore every binding) alive. Both boundaries hide their subtree this way:
|
|
1733
|
+
* destroying it would take with it the bindings that report recovery.
|
|
1734
|
+
*
|
|
1735
|
+
* The nodes go into a real `DocumentFragment` rather than a plain array so that
|
|
1736
|
+
* a NESTED boundary can still insert relative to its own markers while an outer
|
|
1737
|
+
* boundary holds them detached — the markers keep a parent to insert into.
|
|
1738
|
+
*/
|
|
1739
|
+
function detachRange(renderer, first, last) {
|
|
1740
|
+
const holder = renderer.createFragment();
|
|
1741
|
+
let current = first;
|
|
1742
|
+
const nodes = [];
|
|
1743
|
+
while (current) {
|
|
1744
|
+
nodes.push(current);
|
|
1745
|
+
if (current === last)
|
|
1746
|
+
break;
|
|
1747
|
+
current = current.nextSibling;
|
|
1748
|
+
}
|
|
1749
|
+
nodes.forEach((node) => {
|
|
1750
|
+
removeNode(renderer, node);
|
|
1751
|
+
renderer.appendChild(holder, node);
|
|
1752
|
+
});
|
|
1753
|
+
return holder;
|
|
1754
|
+
}
|
|
1755
|
+
/** Re-inserts a detached range before `anchor`, in whatever parent it now has. */
|
|
1756
|
+
function reattachRange(renderer, holder, anchor, fallbackParent) {
|
|
1757
|
+
const parent = anchor.parentNode ?? fallbackParent;
|
|
1758
|
+
while (holder.firstChild) {
|
|
1759
|
+
const node = holder.firstChild;
|
|
1760
|
+
removeNode(renderer, node);
|
|
1761
|
+
insertBefore(renderer, parent, node, anchor);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
/** Stands in for reporters that do not identify themselves (component renders). */
|
|
1765
|
+
const ANONYMOUS_EXCEPTION_REPORTER = {};
|
|
1766
|
+
class CatchBlockRenderedNode {
|
|
1767
|
+
kind = 'catch-block';
|
|
1768
|
+
start;
|
|
1769
|
+
end;
|
|
1770
|
+
sourceView;
|
|
1771
|
+
fallbackView;
|
|
1772
|
+
handling = false;
|
|
1773
|
+
fallbackVisible = false;
|
|
1774
|
+
fallbackPosition;
|
|
1775
|
+
detached;
|
|
1776
|
+
/** Reporters still failing. The fallback stays up until this empties. */
|
|
1777
|
+
failing = new Set();
|
|
1778
|
+
node;
|
|
1779
|
+
parent;
|
|
1780
|
+
context;
|
|
1781
|
+
constructor(node, parent, before, context) {
|
|
1782
|
+
this.node = node;
|
|
1783
|
+
this.parent = parent;
|
|
1784
|
+
this.context = context;
|
|
1785
|
+
this.fallbackPosition = node.position;
|
|
1786
|
+
this.start = mountComment(parent, 'craft-catch-block:start', before, context);
|
|
1787
|
+
this.end = mountComment(parent, 'craft-catch-block:end', before, context);
|
|
1788
|
+
// The fallback renders in the OUTER context: an exception thrown by a
|
|
1789
|
+
// fallback belongs to the next boundary up, never to this one.
|
|
1790
|
+
const createFallback = () => createFragment(parent, this.end, this.context, [], 'craft-catch-fallback');
|
|
1791
|
+
const createSource = () => createFragment(parent, this.end, this.boundaryContext(), [], 'craft-catch-source');
|
|
1792
|
+
if (this.fallbackPosition === 'before') {
|
|
1793
|
+
this.fallbackView = createFallback();
|
|
1794
|
+
this.sourceView = createSource();
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
this.sourceView = createSource();
|
|
1798
|
+
this.fallbackView = createFallback();
|
|
1799
|
+
}
|
|
1800
|
+
try {
|
|
1801
|
+
this.sourceView.patchChildren([this.node.source]);
|
|
1802
|
+
}
|
|
1803
|
+
catch (error) {
|
|
1804
|
+
if (!isCraftGenShortCircuit(error))
|
|
1805
|
+
throw error;
|
|
1806
|
+
this.handle(error.exception);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
firstNode() {
|
|
1810
|
+
return this.start;
|
|
1811
|
+
}
|
|
1812
|
+
lastNode() {
|
|
1813
|
+
return this.end;
|
|
1814
|
+
}
|
|
1815
|
+
patch(node) {
|
|
1816
|
+
if (node.kind !== 'catch-block' || node.position !== this.node.position) {
|
|
1817
|
+
return false;
|
|
1818
|
+
}
|
|
1819
|
+
this.node = node;
|
|
1820
|
+
try {
|
|
1821
|
+
this.sourceView.patchChildren([node.source]);
|
|
1822
|
+
if (this.detached) {
|
|
1823
|
+
// A re-render re-materialises the source range: hide it again.
|
|
1824
|
+
this.detached = undefined;
|
|
1825
|
+
this.hideSource();
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
catch (error) {
|
|
1829
|
+
if (!isCraftGenShortCircuit(error))
|
|
1830
|
+
throw error;
|
|
1831
|
+
this.handle(error.exception);
|
|
1832
|
+
}
|
|
1833
|
+
return true;
|
|
1834
|
+
}
|
|
1835
|
+
boundaryContext() {
|
|
1836
|
+
return childContext(this.context, {
|
|
1837
|
+
exceptionBoundary: (exception, token) => this.handle(exception, token),
|
|
1838
|
+
exceptionBoundaryResolved: (token) => this.resolved(token),
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
handle(exception, token) {
|
|
1842
|
+
if (this.handling) {
|
|
1843
|
+
return (this.context.exceptionBoundary?.(exception, token) ??
|
|
1844
|
+
this.unhandled(exception));
|
|
1845
|
+
}
|
|
1846
|
+
const handler = this.node.handlers[exception._tag];
|
|
1847
|
+
if (!handler) {
|
|
1848
|
+
return (this.context.exceptionBoundary?.(exception, token) ??
|
|
1849
|
+
this.unhandled(exception));
|
|
1850
|
+
}
|
|
1851
|
+
this.failing.add(token ?? ANONYMOUS_EXCEPTION_REPORTER);
|
|
1852
|
+
this.handling = true;
|
|
1853
|
+
this.fallbackVisible = true;
|
|
1854
|
+
try {
|
|
1855
|
+
const resolved = resolveCatchBlockHandler(handler, exception, true, this.node.position);
|
|
1856
|
+
this.applyPosition(resolved.position);
|
|
1857
|
+
this.fallbackView.patchChildren(hasLiveRegion(resolved.children)
|
|
1858
|
+
? resolved.children
|
|
1859
|
+
: a11yElement('div', { role: 'alert' }, resolved.children));
|
|
1860
|
+
if (resolved.showSource) {
|
|
1861
|
+
this.restoreSource();
|
|
1862
|
+
}
|
|
1863
|
+
else {
|
|
1864
|
+
this.hideSource();
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
finally {
|
|
1868
|
+
this.handling = false;
|
|
1869
|
+
}
|
|
1870
|
+
return true;
|
|
1871
|
+
}
|
|
1872
|
+
resolved(token) {
|
|
1873
|
+
this.failing.delete(token ?? ANONYMOUS_EXCEPTION_REPORTER);
|
|
1874
|
+
if (this.handling || !this.fallbackVisible || this.failing.size > 0)
|
|
1875
|
+
return;
|
|
1876
|
+
this.fallbackVisible = false;
|
|
1877
|
+
this.applyPosition(this.node.position);
|
|
1878
|
+
this.fallbackView.patchChildren([]);
|
|
1879
|
+
this.restoreSource();
|
|
1880
|
+
}
|
|
1881
|
+
applyPosition(position) {
|
|
1882
|
+
if (position === this.fallbackPosition)
|
|
1883
|
+
return;
|
|
1884
|
+
this.fallbackPosition = position;
|
|
1885
|
+
this.fallbackView.moveBefore(position === 'before' ? this.sourceFirstNode() : this.end);
|
|
1886
|
+
}
|
|
1887
|
+
sourceFirstNode() {
|
|
1888
|
+
return this.detached ? this.end : this.sourceView.firstNode();
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* A hidden source is detached from the DOM but stays mounted. Destroying it
|
|
1892
|
+
* would take its bindings with it — and those bindings are the only channel
|
|
1893
|
+
* that reports the exception is gone, so the fallback could never be removed.
|
|
1894
|
+
*/
|
|
1895
|
+
hideSource() {
|
|
1896
|
+
if (this.detached)
|
|
1897
|
+
return;
|
|
1898
|
+
this.detached = detachRange(this.context.renderer, this.sourceView.firstNode(), this.sourceView.lastNode());
|
|
1899
|
+
}
|
|
1900
|
+
restoreSource() {
|
|
1901
|
+
if (!this.detached)
|
|
1902
|
+
return;
|
|
1903
|
+
const anchor = this.fallbackPosition === 'before'
|
|
1904
|
+
? this.end
|
|
1905
|
+
: this.fallbackView.firstNode();
|
|
1906
|
+
reattachRange(this.context.renderer, this.detached, anchor, this.parent);
|
|
1907
|
+
this.detached = undefined;
|
|
1908
|
+
}
|
|
1909
|
+
unhandled(exception) {
|
|
1910
|
+
throw new CraftUnhandledExceptionError(exception);
|
|
1911
|
+
}
|
|
1912
|
+
destroy() {
|
|
1913
|
+
this.restoreSource();
|
|
1914
|
+
this.fallbackView.destroy();
|
|
1915
|
+
this.sourceView.destroy();
|
|
1916
|
+
removeNode(this.context.renderer, this.start);
|
|
1917
|
+
removeNode(this.context.renderer, this.end);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
const fieldExceptionAccessibility = new WeakMap();
|
|
1921
|
+
let nextFieldExceptionBoundaryId = 0;
|
|
1922
|
+
function componentFieldExceptionSources(value, seen = new WeakSet(), depth = 0) {
|
|
1923
|
+
if ((typeof value !== 'object' || value === null) &&
|
|
1924
|
+
typeof value !== 'function') {
|
|
1925
|
+
return [];
|
|
1926
|
+
}
|
|
1927
|
+
const objectValue = value;
|
|
1928
|
+
if (seen.has(objectValue))
|
|
1929
|
+
return [];
|
|
1930
|
+
seen.add(objectValue);
|
|
1931
|
+
const source = value[CRAFT_FIELD_EXCEPTION_SOURCE];
|
|
1932
|
+
if (source) {
|
|
1933
|
+
return [rawReactiveFacade(source)];
|
|
1934
|
+
}
|
|
1935
|
+
const sources = [];
|
|
1936
|
+
if (depth >= 4)
|
|
1937
|
+
return sources;
|
|
1938
|
+
for (const key of Object.keys(value)) {
|
|
1939
|
+
sources.push(...componentFieldExceptionSources(value[key], seen, depth + 1));
|
|
1940
|
+
}
|
|
1941
|
+
return sources;
|
|
1942
|
+
}
|
|
1943
|
+
function fieldExceptionHandler(handlers, path, code) {
|
|
1944
|
+
const grouped = handlers[path];
|
|
1945
|
+
if (typeof grouped === 'object' && grouped !== null) {
|
|
1946
|
+
return grouped[code];
|
|
1947
|
+
}
|
|
1948
|
+
const local = handlers[code];
|
|
1949
|
+
return typeof local === 'function' ? local : undefined;
|
|
1950
|
+
}
|
|
1951
|
+
function fieldExceptionValidatorName(source, exception) {
|
|
1952
|
+
const branded = exception.__brand;
|
|
1953
|
+
if (typeof branded === 'string')
|
|
1954
|
+
return branded;
|
|
1955
|
+
const byValidator = source.exceptions().byValidator;
|
|
1956
|
+
return (Object.entries(byValidator).find(([, value]) => value === exception)?.[0] ??
|
|
1957
|
+
'unknown');
|
|
1958
|
+
}
|
|
1959
|
+
function fieldExceptionMessageNode(id, children) {
|
|
1960
|
+
return {
|
|
1961
|
+
kind: 'element',
|
|
1962
|
+
tag: 'div',
|
|
1963
|
+
props: {
|
|
1964
|
+
id,
|
|
1965
|
+
role: 'alert',
|
|
1966
|
+
'data-field-exception': '',
|
|
1967
|
+
},
|
|
1968
|
+
children,
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
function a11yElement(tag, props, children = []) {
|
|
1972
|
+
return { kind: 'element', tag, props, children };
|
|
1973
|
+
}
|
|
1974
|
+
function firstElementChild(value) {
|
|
1975
|
+
const list = Array.isArray(value) ? value : [value];
|
|
1976
|
+
for (const child of list) {
|
|
1977
|
+
if (child &&
|
|
1978
|
+
typeof child === 'object' &&
|
|
1979
|
+
'kind' in child &&
|
|
1980
|
+
child.kind === 'element') {
|
|
1981
|
+
return child;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
return undefined;
|
|
1985
|
+
}
|
|
1986
|
+
function hasLiveRegion(children) {
|
|
1987
|
+
const element = firstElementChild(children);
|
|
1988
|
+
if (!element)
|
|
1989
|
+
return false;
|
|
1990
|
+
const role = element.props['role'];
|
|
1991
|
+
const live = element.props['aria-live'];
|
|
1992
|
+
return (role === 'alert' ||
|
|
1993
|
+
role === 'status' ||
|
|
1994
|
+
live === 'polite' ||
|
|
1995
|
+
live === 'assertive');
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* The `pendingBlock` boundary.
|
|
1999
|
+
*
|
|
2000
|
+
* Layout is fixed at construction — a fallback fragment and a source fragment,
|
|
2001
|
+
* ordered by `position` — so showing or hiding the fallback never reconciles
|
|
2002
|
+
* the source subtree.
|
|
2003
|
+
*
|
|
2004
|
+
* While at least one descendant binding holds a pending token, the fallback is
|
|
2005
|
+
* rendered and the source's DOM range is detached (its nodes are kept, not
|
|
2006
|
+
* destroyed). Keeping the subtree alive is what makes resumption work at all:
|
|
2007
|
+
* the suspended bindings stay subscribed to their source's `status`, so they
|
|
2008
|
+
* re-run — and release their token — the moment the data arrives.
|
|
2009
|
+
*/
|
|
2010
|
+
class PendingBlockRenderedNode {
|
|
2011
|
+
kind = 'pending-block';
|
|
2012
|
+
start;
|
|
2013
|
+
end;
|
|
2014
|
+
sourceView;
|
|
2015
|
+
fallbackView;
|
|
2016
|
+
position;
|
|
2017
|
+
/** Held tokens, by the binding that suspended, in suspension order. */
|
|
2018
|
+
held = new Map();
|
|
2019
|
+
/** Sources seen below this boundary, by name — the reload watch reads them. */
|
|
2020
|
+
watched = new Map();
|
|
2021
|
+
reloadWatch;
|
|
2022
|
+
reloadingSource;
|
|
2023
|
+
detached;
|
|
2024
|
+
/** Depth of an in-flight render into the source range. */
|
|
2025
|
+
rendering = 0;
|
|
2026
|
+
/** A fallback render asked for while the source range was mid-render. */
|
|
2027
|
+
renderDeferred = false;
|
|
2028
|
+
node;
|
|
2029
|
+
parent;
|
|
2030
|
+
context;
|
|
2031
|
+
constructor(node, parent, before, context) {
|
|
2032
|
+
this.node = node;
|
|
2033
|
+
this.parent = parent;
|
|
2034
|
+
this.context = context;
|
|
2035
|
+
this.position = node.position;
|
|
2036
|
+
if (context.ssr &&
|
|
2037
|
+
node.ssr === 'client' &&
|
|
2038
|
+
!node.handlers &&
|
|
2039
|
+
!node.fallback) {
|
|
2040
|
+
throw new Error('pendingBlock({ ssr: "client" }) requires an explicit fallback or shell.');
|
|
2041
|
+
}
|
|
2042
|
+
this.start = mountComment(parent, 'craft-pending:start', before, context);
|
|
2043
|
+
this.end = mountComment(parent, 'craft-pending:end', before, context);
|
|
2044
|
+
// The fallback renders in the OUTER context: a fallback that suspends in
|
|
2045
|
+
// turn belongs to the next boundary up, never to this one.
|
|
2046
|
+
const createFallback = () => createFragment(parent, this.end, this.context, [], 'craft-pending-fallback');
|
|
2047
|
+
const createSource = () => createFragment(parent, this.end, this.boundaryContext(), [], 'craft-pending-source');
|
|
2048
|
+
// Both ranges are staked out EMPTY first. A binding below this boundary
|
|
2049
|
+
// suspends on its very first run, synchronously, from inside the source
|
|
2050
|
+
// range's own construction — so both views have to exist (and the range has
|
|
2051
|
+
// to be complete) before that first run happens.
|
|
2052
|
+
if (this.position === 'before') {
|
|
2053
|
+
this.fallbackView = createFallback();
|
|
2054
|
+
this.sourceView = createSource();
|
|
2055
|
+
}
|
|
2056
|
+
else {
|
|
2057
|
+
this.sourceView = createSource();
|
|
2058
|
+
this.fallbackView = createFallback();
|
|
2059
|
+
}
|
|
2060
|
+
this.renderSource([node.source]);
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Fills the source range, deferring any fallback render that a synchronous
|
|
2064
|
+
* suspension asks for until the range is whole again. Rendering the fallback
|
|
2065
|
+
* mid-pass would detach the range out from under the insertion that is still
|
|
2066
|
+
* walking it.
|
|
2067
|
+
*/
|
|
2068
|
+
renderSource(children) {
|
|
2069
|
+
this.rendering += 1;
|
|
2070
|
+
try {
|
|
2071
|
+
this.sourceView.patchChildren(children);
|
|
2072
|
+
}
|
|
2073
|
+
finally {
|
|
2074
|
+
this.rendering -= 1;
|
|
2075
|
+
}
|
|
2076
|
+
if (this.rendering === 0 && this.renderDeferred) {
|
|
2077
|
+
this.renderDeferred = false;
|
|
2078
|
+
this.renderFallback();
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
firstNode() {
|
|
2082
|
+
return this.start;
|
|
2083
|
+
}
|
|
2084
|
+
lastNode() {
|
|
2085
|
+
return this.end;
|
|
2086
|
+
}
|
|
2087
|
+
patch(node) {
|
|
2088
|
+
if (node.kind !== 'pending-block') {
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
const next = node;
|
|
2092
|
+
if (next.position !== this.position) {
|
|
2093
|
+
return false;
|
|
2094
|
+
}
|
|
2095
|
+
this.node = next;
|
|
2096
|
+
// A re-render re-materialises the source range: hide it again.
|
|
2097
|
+
if (this.held.size > 0)
|
|
2098
|
+
this.detached = undefined;
|
|
2099
|
+
this.renderSource([next.source]);
|
|
2100
|
+
if (this.held.size > 0)
|
|
2101
|
+
this.showFallback();
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
destroy() {
|
|
2105
|
+
this.reloadWatch?.destroy();
|
|
2106
|
+
this.restoreSource();
|
|
2107
|
+
this.fallbackView.destroy();
|
|
2108
|
+
this.sourceView.destroy();
|
|
2109
|
+
removeNode(this.context.renderer, this.start);
|
|
2110
|
+
removeNode(this.context.renderer, this.end);
|
|
2111
|
+
}
|
|
2112
|
+
boundaryContext() {
|
|
2113
|
+
return childContext(this.context, {
|
|
2114
|
+
pendingBoundary: {
|
|
2115
|
+
suspend: (token, source) => this.suspend(token, source),
|
|
2116
|
+
resume: (token) => this.resume(token),
|
|
2117
|
+
},
|
|
2118
|
+
settledReadObserver: (notice) => this.watch(notice),
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
/**
|
|
2122
|
+
* Records a source read below this boundary. A refetch throws nothing, so the
|
|
2123
|
+
* boundary cannot learn about it from the suspension channel: it watches the
|
|
2124
|
+
* source's own status instead, on its own effect, which keeps working even
|
|
2125
|
+
* when the binding that first reported it never re-runs.
|
|
2126
|
+
*/
|
|
2127
|
+
watch(notice) {
|
|
2128
|
+
if (!this.handles(notice.source)) {
|
|
2129
|
+
this.context.settledReadObserver?.(notice);
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
if (this.watched.has(notice.source))
|
|
2133
|
+
return;
|
|
2134
|
+
this.watched.set(notice.source, notice);
|
|
2135
|
+
this.reloadWatch?.destroy();
|
|
2136
|
+
this.reloadWatch = createEffectInInjector(this.context.injector, 'pending-reload-watch', () => this.onReloadStateChange());
|
|
2137
|
+
}
|
|
2138
|
+
onReloadStateChange() {
|
|
2139
|
+
let reloading;
|
|
2140
|
+
for (const [source, notice] of this.watched) {
|
|
2141
|
+
const status = notice.status();
|
|
2142
|
+
// Refetching with something already on screen — the settled read returns
|
|
2143
|
+
// the stale value, so the subtree stays visible.
|
|
2144
|
+
if (notice.value() !== undefined &&
|
|
2145
|
+
(status === 'loading' || status === 'reloading')) {
|
|
2146
|
+
reloading = source;
|
|
2147
|
+
break;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
if (reloading === this.reloadingSource)
|
|
2151
|
+
return;
|
|
2152
|
+
this.reloadingSource = reloading;
|
|
2153
|
+
// A suspension outranks a reload: it already owns the fallback slot.
|
|
2154
|
+
if (this.held.size === 0)
|
|
2155
|
+
this.requestFallbackRender();
|
|
2156
|
+
}
|
|
2157
|
+
suspend(token, source) {
|
|
2158
|
+
if (this.handles(source)) {
|
|
2159
|
+
const routePolicy = this.context.injector.get(CRAFT_SSR_POLICY, null);
|
|
2160
|
+
this.context.ssr?.suspend(token, source, this.node.ssr ?? routePolicy?.mode, {
|
|
2161
|
+
route: activeSsrRoute(this.context),
|
|
2162
|
+
timeoutMs: routePolicy?.timeoutMs,
|
|
2163
|
+
});
|
|
2164
|
+
const wasIdle = this.held.size === 0;
|
|
2165
|
+
this.held.set(token, source);
|
|
2166
|
+
if (wasIdle)
|
|
2167
|
+
this.showFallback();
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
// Not ours to handle (an `.exhaustive` boundary that does not list this
|
|
2171
|
+
// source): delegate outwards.
|
|
2172
|
+
if (!this.context.pendingBoundary) {
|
|
2173
|
+
if (this.context.ssr) {
|
|
2174
|
+
this.context.ssr.unhandled(source, activeSsrRoute(this.context));
|
|
2175
|
+
}
|
|
2176
|
+
throw new CraftUnhandledPendingError(source);
|
|
2177
|
+
}
|
|
2178
|
+
this.context.pendingBoundary.suspend(token, source);
|
|
2179
|
+
}
|
|
2180
|
+
resume(token) {
|
|
2181
|
+
if (!this.held.delete(token)) {
|
|
2182
|
+
this.context.pendingBoundary?.resume(token);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
this.context.ssr?.resume(token);
|
|
2186
|
+
if (this.held.size === 0)
|
|
2187
|
+
this.hideFallback();
|
|
2188
|
+
}
|
|
2189
|
+
/** The catch-all form handles every source; `.exhaustive` only what it lists. */
|
|
2190
|
+
handles(source) {
|
|
2191
|
+
return !this.node.handlers || source in this.node.handlers;
|
|
2192
|
+
}
|
|
2193
|
+
fallbackChildren() {
|
|
2194
|
+
const handlers = this.node.handlers;
|
|
2195
|
+
const suspended = this.held.size > 0;
|
|
2196
|
+
// Several listed sources may be pending at once — the first to suspend wins.
|
|
2197
|
+
const [firstSuspended] = this.held.values();
|
|
2198
|
+
const source = suspended ? firstSuspended : this.reloadingSource;
|
|
2199
|
+
if (!source)
|
|
2200
|
+
return [];
|
|
2201
|
+
if (!handlers) {
|
|
2202
|
+
return this.announceFallback(suspended
|
|
2203
|
+
? (this.node.fallback?.() ?? [])
|
|
2204
|
+
: (this.node.reloading?.() ?? []), true);
|
|
2205
|
+
}
|
|
2206
|
+
const handler = handlers[source];
|
|
2207
|
+
const inner = handler
|
|
2208
|
+
? resolvePendingBlockHandler(handler, suspended ? 'pending' : 'reloading')
|
|
2209
|
+
: [];
|
|
2210
|
+
return this.announceFallback(inner, true);
|
|
2211
|
+
}
|
|
2212
|
+
announceFallback(children, busy) {
|
|
2213
|
+
if (!children || (Array.isArray(children) && children.length === 0)) {
|
|
2214
|
+
return [];
|
|
2215
|
+
}
|
|
2216
|
+
return a11yElement('span', {
|
|
2217
|
+
'aria-live': 'polite',
|
|
2218
|
+
'aria-atomic': 'true',
|
|
2219
|
+
'aria-busy': busy ? 'true' : 'false',
|
|
2220
|
+
}, children);
|
|
2221
|
+
}
|
|
2222
|
+
/** Renders whatever the current state calls for: a suspension, a reload, or nothing. */
|
|
2223
|
+
renderFallback() {
|
|
2224
|
+
if (this.held.size > 0) {
|
|
2225
|
+
this.detachSource();
|
|
2226
|
+
}
|
|
2227
|
+
else {
|
|
2228
|
+
// A reload keeps the stale subtree on screen next to the indicator.
|
|
2229
|
+
this.restoreSource();
|
|
2230
|
+
}
|
|
2231
|
+
this.fallbackView.patchChildren(this.fallbackChildren());
|
|
2232
|
+
}
|
|
2233
|
+
showFallback() {
|
|
2234
|
+
this.requestFallbackRender();
|
|
2235
|
+
}
|
|
2236
|
+
hideFallback() {
|
|
2237
|
+
this.requestFallbackRender();
|
|
2238
|
+
}
|
|
2239
|
+
requestFallbackRender() {
|
|
2240
|
+
if (this.rendering > 0) {
|
|
2241
|
+
this.renderDeferred = true;
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
this.renderFallback();
|
|
2245
|
+
}
|
|
2246
|
+
focused;
|
|
2247
|
+
detachSource() {
|
|
2248
|
+
if (this.detached)
|
|
2249
|
+
return;
|
|
2250
|
+
const documentRef = this.parent.ownerDocument;
|
|
2251
|
+
const active = documentRef?.activeElement ?? null;
|
|
2252
|
+
if (active && this.sourceContains(active)) {
|
|
2253
|
+
this.focused = active;
|
|
2254
|
+
}
|
|
2255
|
+
this.detached = detachRange(this.context.renderer, this.sourceView.firstNode(), this.sourceView.lastNode());
|
|
2256
|
+
}
|
|
2257
|
+
sourceContains(node) {
|
|
2258
|
+
let current = this.sourceView.firstNode();
|
|
2259
|
+
const end = this.sourceView.lastNode();
|
|
2260
|
+
while (current) {
|
|
2261
|
+
if (current === node)
|
|
2262
|
+
return true;
|
|
2263
|
+
if (isElementNode(current) && current.contains(node))
|
|
2264
|
+
return true;
|
|
2265
|
+
if (current === end)
|
|
2266
|
+
break;
|
|
2267
|
+
current = current.nextSibling;
|
|
2268
|
+
}
|
|
2269
|
+
return false;
|
|
2270
|
+
}
|
|
2271
|
+
restoreSource() {
|
|
2272
|
+
if (!this.detached)
|
|
2273
|
+
return;
|
|
2274
|
+
// Back before the marker that follows the source range, so the original
|
|
2275
|
+
// order (fallback / source) is preserved.
|
|
2276
|
+
const anchor = this.position === 'before' ? this.end : this.fallbackView.firstNode();
|
|
2277
|
+
reattachRange(this.context.renderer, this.detached, anchor, this.parent);
|
|
2278
|
+
this.detached = undefined;
|
|
2279
|
+
if (this.focused &&
|
|
2280
|
+
'focus' in this.focused &&
|
|
2281
|
+
typeof this.focused.focus === 'function') {
|
|
2282
|
+
this.focused.focus();
|
|
2283
|
+
}
|
|
2284
|
+
this.focused = undefined;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
class FieldExceptionBlockRenderedNode {
|
|
2288
|
+
kind = 'field-exception-block';
|
|
2289
|
+
boundaryId = nextFieldExceptionBoundaryId++;
|
|
2290
|
+
descriptor;
|
|
2291
|
+
sourcesVersion = signal(0);
|
|
2292
|
+
sources = new Set();
|
|
2293
|
+
componentSourceCleanups;
|
|
2294
|
+
view;
|
|
2295
|
+
effectRef;
|
|
2296
|
+
node;
|
|
2297
|
+
context;
|
|
2298
|
+
constructor(node, parent, before, context) {
|
|
2299
|
+
this.node = node;
|
|
2300
|
+
this.context = context;
|
|
2301
|
+
this.descriptor = signal(node);
|
|
2302
|
+
this.view = createFragment(parent, before, this.boundaryContext(), [node.source], 'craft-field-exception-block');
|
|
2303
|
+
this.componentSourceCleanups = componentFieldExceptionSources(context.componentContext).map((source) => this.register(source));
|
|
2304
|
+
this.effectRef = createRenderEffect(context, 'field-exception-block', () => {
|
|
2305
|
+
this.node = this.descriptor();
|
|
2306
|
+
this.sourcesVersion();
|
|
2307
|
+
const fallbacks = this.renderFallbacks();
|
|
2308
|
+
this.view.patchChildren(this.node.options.position === 'before'
|
|
2309
|
+
? [fallbacks, this.node.source]
|
|
2310
|
+
: [this.node.source, fallbacks]);
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
firstNode() {
|
|
2314
|
+
return this.view.firstNode();
|
|
2315
|
+
}
|
|
2316
|
+
lastNode() {
|
|
2317
|
+
return this.view.lastNode();
|
|
2318
|
+
}
|
|
2319
|
+
patch(node) {
|
|
2320
|
+
if (node.kind !== 'field-exception-block')
|
|
2321
|
+
return false;
|
|
2322
|
+
this.descriptor.set(node);
|
|
2323
|
+
return true;
|
|
2324
|
+
}
|
|
2325
|
+
boundaryContext() {
|
|
2326
|
+
return childContext(this.context, {
|
|
2327
|
+
fieldExceptionBoundary: {
|
|
2328
|
+
register: (source, element) => this.register(source, element),
|
|
2329
|
+
},
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
register(source, element) {
|
|
2333
|
+
const rawSource = rawReactiveFacade(source);
|
|
2334
|
+
const unregisterParent = this.context.fieldExceptionBoundary?.register(rawSource, element);
|
|
2335
|
+
const registered = {
|
|
2336
|
+
source: rawSource,
|
|
2337
|
+
element,
|
|
2338
|
+
messageIds: new Map(),
|
|
2339
|
+
};
|
|
2340
|
+
if (element) {
|
|
2341
|
+
this.accessibilityState(element).registrations.add(this.boundaryId);
|
|
2342
|
+
}
|
|
2343
|
+
this.sources.add(registered);
|
|
2344
|
+
untracked(() => this.sourcesVersion.update((value) => value + 1));
|
|
2345
|
+
return () => {
|
|
2346
|
+
this.releaseAccessibility(registered);
|
|
2347
|
+
this.sources.delete(registered);
|
|
2348
|
+
untracked(() => this.sourcesVersion.update((value) => value + 1));
|
|
2349
|
+
unregisterParent?.();
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
orderedSources() {
|
|
2353
|
+
const ordered = [...this.sources].sort((left, right) => {
|
|
2354
|
+
if (!left.element)
|
|
2355
|
+
return 1;
|
|
2356
|
+
if (!right.element)
|
|
2357
|
+
return -1;
|
|
2358
|
+
if (left.element === right.element)
|
|
2359
|
+
return 0;
|
|
2360
|
+
const relation = left.element.compareDocumentPosition(right.element);
|
|
2361
|
+
return relation & 4 /* Node.DOCUMENT_POSITION_FOLLOWING */ ? -1 : 1;
|
|
2362
|
+
});
|
|
2363
|
+
const seen = new Set();
|
|
2364
|
+
return ordered.filter(({ source }) => {
|
|
2365
|
+
if (seen.has(source))
|
|
2366
|
+
return false;
|
|
2367
|
+
seen.add(source);
|
|
2368
|
+
return true;
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
activeExceptions(source) {
|
|
2372
|
+
const visibility = this.node.options.visibility ?? 'visibleExceptions';
|
|
2373
|
+
if (visibility === 'visibleExceptions') {
|
|
2374
|
+
if (this.node.options.mode === 'first') {
|
|
2375
|
+
const first = source.visibleFirstLeftFailedValidation();
|
|
2376
|
+
if (first && this.handles(source, first))
|
|
2377
|
+
return [first];
|
|
2378
|
+
return source
|
|
2379
|
+
.visibleExceptions()
|
|
2380
|
+
.list.filter((exception) => this.handles(source, exception))
|
|
2381
|
+
.slice(0, 1);
|
|
2382
|
+
}
|
|
2383
|
+
return source.visibleExceptions().list;
|
|
2384
|
+
}
|
|
2385
|
+
if (!fieldExceptionVisibilityMatches(visibility, {
|
|
2386
|
+
field: source.field,
|
|
2387
|
+
hasAttemptedSubmit: source.hasAttemptedSubmit,
|
|
2388
|
+
})) {
|
|
2389
|
+
return [];
|
|
2390
|
+
}
|
|
2391
|
+
if (this.node.options.mode === 'first') {
|
|
2392
|
+
const first = source.firstLeftFailedValidation();
|
|
2393
|
+
if (first && this.handles(source, first))
|
|
2394
|
+
return [first];
|
|
2395
|
+
return source
|
|
2396
|
+
.exceptions()
|
|
2397
|
+
.list.filter((exception) => this.handles(source, exception))
|
|
2398
|
+
.slice(0, 1);
|
|
2399
|
+
}
|
|
2400
|
+
return source.exceptions().list;
|
|
2401
|
+
}
|
|
2402
|
+
handles(source, exception) {
|
|
2403
|
+
return Boolean(fieldExceptionHandler(this.node.handlers, source.path, exception._tag));
|
|
2404
|
+
}
|
|
2405
|
+
renderFallbacks() {
|
|
2406
|
+
const children = [];
|
|
2407
|
+
for (const registered of this.orderedSources()) {
|
|
2408
|
+
const { source } = registered;
|
|
2409
|
+
const activeMessageIds = [];
|
|
2410
|
+
const exceptions = this.activeExceptions(source);
|
|
2411
|
+
exceptions.forEach((exception, index) => {
|
|
2412
|
+
const handler = fieldExceptionHandler(this.node.handlers, source.path, exception._tag);
|
|
2413
|
+
if (!handler)
|
|
2414
|
+
return;
|
|
2415
|
+
const validatorName = fieldExceptionValidatorName(source, exception);
|
|
2416
|
+
const messageKey = `${validatorName}:${exception._tag}:${index}`;
|
|
2417
|
+
let id = registered.messageIds.get(messageKey);
|
|
2418
|
+
if (!id) {
|
|
2419
|
+
id = `craft-field-exception-${this.boundaryId}-${registered.messageIds.size}`;
|
|
2420
|
+
registered.messageIds.set(messageKey, id);
|
|
2421
|
+
}
|
|
2422
|
+
activeMessageIds.push(id);
|
|
2423
|
+
children.push(fieldExceptionMessageNode(id, handler({
|
|
2424
|
+
field: source.field,
|
|
2425
|
+
path: source.path,
|
|
2426
|
+
runtimePath: source.runtimePath,
|
|
2427
|
+
validatorName,
|
|
2428
|
+
exception,
|
|
2429
|
+
})));
|
|
2430
|
+
});
|
|
2431
|
+
this.applyAccessibility(registered, activeMessageIds);
|
|
2432
|
+
}
|
|
2433
|
+
return children;
|
|
2434
|
+
}
|
|
2435
|
+
applyAccessibility(registered, messageIds) {
|
|
2436
|
+
if (!registered.element)
|
|
2437
|
+
return;
|
|
2438
|
+
if (messageIds.length) {
|
|
2439
|
+
this.accessibilityState(registered.element).messagesByBoundary.set(this.boundaryId, messageIds);
|
|
2440
|
+
}
|
|
2441
|
+
else {
|
|
2442
|
+
this.accessibilityState(registered.element).messagesByBoundary.delete(this.boundaryId);
|
|
2443
|
+
}
|
|
2444
|
+
this.reconcileAccessibility(registered.element);
|
|
2445
|
+
}
|
|
2446
|
+
accessibilityState(element) {
|
|
2447
|
+
const existing = fieldExceptionAccessibility.get(element);
|
|
2448
|
+
if (existing)
|
|
2449
|
+
return existing;
|
|
2450
|
+
const created = {
|
|
2451
|
+
originalAriaInvalid: element.getAttribute('aria-invalid'),
|
|
2452
|
+
originalAriaDescribedBy: element.getAttribute('aria-describedby'),
|
|
2453
|
+
registrations: new Set(),
|
|
2454
|
+
messagesByBoundary: new Map(),
|
|
2455
|
+
};
|
|
2456
|
+
fieldExceptionAccessibility.set(element, created);
|
|
2457
|
+
return created;
|
|
2458
|
+
}
|
|
2459
|
+
reconcileAccessibility(element) {
|
|
2460
|
+
const state = this.accessibilityState(element);
|
|
2461
|
+
const renderer = this.context.renderer;
|
|
2462
|
+
const messageIds = [...state.messagesByBoundary.values()].flat();
|
|
2463
|
+
if (messageIds.length) {
|
|
2464
|
+
renderer.setAttribute(element, 'aria-invalid', 'true');
|
|
2465
|
+
}
|
|
2466
|
+
else {
|
|
2467
|
+
if (state.originalAriaInvalid === null) {
|
|
2468
|
+
renderer.removeAttribute(element, 'aria-invalid');
|
|
2469
|
+
}
|
|
2470
|
+
else {
|
|
2471
|
+
renderer.setAttribute(element, 'aria-invalid', state.originalAriaInvalid);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
const describedBy = [
|
|
2475
|
+
...(state.originalAriaDescribedBy?.split(/\s+/).filter(Boolean) ?? []),
|
|
2476
|
+
...messageIds,
|
|
2477
|
+
];
|
|
2478
|
+
if (describedBy.length) {
|
|
2479
|
+
renderer.setAttribute(element, 'aria-describedby', [...new Set(describedBy)].join(' '));
|
|
2480
|
+
}
|
|
2481
|
+
else {
|
|
2482
|
+
renderer.removeAttribute(element, 'aria-describedby');
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
releaseAccessibility(registered) {
|
|
2486
|
+
if (!registered.element)
|
|
2487
|
+
return;
|
|
2488
|
+
const state = this.accessibilityState(registered.element);
|
|
2489
|
+
state.messagesByBoundary.delete(this.boundaryId);
|
|
2490
|
+
state.registrations.delete(this.boundaryId);
|
|
2491
|
+
this.reconcileAccessibility(registered.element);
|
|
2492
|
+
if (!state.registrations.size) {
|
|
2493
|
+
fieldExceptionAccessibility.delete(registered.element);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
destroy() {
|
|
2497
|
+
this.effectRef.destroy();
|
|
2498
|
+
this.componentSourceCleanups.forEach((cleanup) => cleanup());
|
|
2499
|
+
this.view.destroy();
|
|
2500
|
+
this.sources.forEach((source) => this.releaseAccessibility(source));
|
|
2501
|
+
this.sources.clear();
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
function componentHostName(injector, name) {
|
|
2505
|
+
const tags = injector.get(HOST_TAG_LIST, []);
|
|
2506
|
+
const current = tags[tags.length - 1];
|
|
2507
|
+
if (current?.startsWith('component:')) {
|
|
2508
|
+
return current;
|
|
2509
|
+
}
|
|
2510
|
+
return allocateCraftHostName(injector, 'component', name);
|
|
2511
|
+
}
|
|
2512
|
+
function allocateCraftHostName(injector, kind, name) {
|
|
2513
|
+
const register = injector.get(COMPONENT_REGISTER, null) ?? ɵfallbackComponentRegister;
|
|
2514
|
+
return `${kind}:${name}#${register.next()}`;
|
|
2515
|
+
}
|
|
2516
|
+
class ComponentRenderedNode {
|
|
2517
|
+
kind = 'component';
|
|
2518
|
+
environmentInjector;
|
|
2519
|
+
propKeys;
|
|
2520
|
+
propSources;
|
|
2521
|
+
hostPropsSource;
|
|
2522
|
+
view;
|
|
2523
|
+
effectRef;
|
|
2524
|
+
composedTemplateEffect;
|
|
2525
|
+
componentExceptionEffect;
|
|
2526
|
+
syncTemplateFlushRelease;
|
|
2527
|
+
componentFallbackVisible = false;
|
|
2528
|
+
componentFallbackException;
|
|
2529
|
+
styleReleases;
|
|
2530
|
+
providerTrackers = [];
|
|
2531
|
+
templateOnly;
|
|
2532
|
+
traceState;
|
|
2533
|
+
componentRenderContext;
|
|
2534
|
+
traceCreated = false;
|
|
2535
|
+
factoryContext;
|
|
2536
|
+
latestTemplate = [];
|
|
2537
|
+
registrationReleases = [];
|
|
2538
|
+
hostBindings;
|
|
2539
|
+
component;
|
|
2540
|
+
context;
|
|
2541
|
+
constructor(component, props, parent, before, context, hostTarget, templateContext, additionalProviders = [], declarationContext) {
|
|
2542
|
+
this.component = component;
|
|
2543
|
+
this.context = context;
|
|
2544
|
+
this.templateOnly = templateContext !== undefined;
|
|
2545
|
+
const definition = component[CRAFT_COMPONENT];
|
|
2546
|
+
const composition = definition.composition;
|
|
2547
|
+
const ownScope = scopeIdFor(definition.scopeDefinition, definition.name);
|
|
2548
|
+
this.traceState = { renderCount: 0 };
|
|
2549
|
+
this.styleReleases = acquireStyles(context, definition.styleOwners, ownScope);
|
|
2550
|
+
const componentRenderContext = childContext(context, {
|
|
2551
|
+
ownerScope: ownScope,
|
|
2552
|
+
rootScope: scopeTokens(context.rootScope, ownScope),
|
|
2553
|
+
componentName: definition.name,
|
|
2554
|
+
contentStyles: definition.meta.contentStyles,
|
|
2555
|
+
contentScope: undefined,
|
|
2556
|
+
declarationContext: declarationContext ?? context,
|
|
2557
|
+
directiveNames: definition.styleOwners
|
|
2558
|
+
.slice(1)
|
|
2559
|
+
.map((owner) => owner.name),
|
|
2560
|
+
traceState: this.traceState,
|
|
2561
|
+
});
|
|
2562
|
+
this.componentRenderContext = componentRenderContext;
|
|
2563
|
+
this.hostBindings = hostTarget
|
|
2564
|
+
? new HostPropertyBindings(hostTarget, context)
|
|
2565
|
+
: undefined;
|
|
2566
|
+
const componentElement = hostTarget ?? (isElementNode(parent) ? parent : parent.parentElement);
|
|
2567
|
+
// Angular's runtime accepts any Injector as the R3Injector parent even
|
|
2568
|
+
// though the public helper narrows the type to EnvironmentInjector. Keep
|
|
2569
|
+
// the immediate parent here: unwrapping it through
|
|
2570
|
+
// `get(EnvironmentInjector)` would skip route-scoped providers such as
|
|
2571
|
+
// ActivatedRoute and ChildrenOutletContexts.
|
|
2572
|
+
const parentInjector = context.injector;
|
|
2573
|
+
if (!composition) {
|
|
2574
|
+
this.environmentInjector = createEnvironmentInjector([
|
|
2575
|
+
...provideHostName(`component:${definition.name}`),
|
|
2576
|
+
{
|
|
2577
|
+
provide: CRAFT_HYDRATION_ID,
|
|
2578
|
+
useValue: componentRenderContext.identity,
|
|
2579
|
+
},
|
|
2580
|
+
...(definition.meta.providers ?? []),
|
|
2581
|
+
{
|
|
2582
|
+
provide: ElementRef,
|
|
2583
|
+
useValue: new ElementRef(componentElement),
|
|
2584
|
+
},
|
|
2585
|
+
...additionalProviders,
|
|
2586
|
+
], parentInjector, 'CraftComponent');
|
|
2587
|
+
this.traceCreated = true;
|
|
2588
|
+
traceComponentLifecycle(this.environmentInjector, 'create', definition.name, this.traceState.renderCount);
|
|
2589
|
+
}
|
|
2590
|
+
this.propKeys = this.templateOnly
|
|
2591
|
+
? []
|
|
2592
|
+
: Object.keys(props).filter((key) => !isHostProperty(key));
|
|
2593
|
+
this.propSources = this.propKeys.map((key) => signal(props[key]));
|
|
2594
|
+
this.hostPropsSource = signal(hostPropsFromComponentProps(props));
|
|
2595
|
+
const inputShells = this.propSources.map((source) => {
|
|
2596
|
+
const input = markYieldableMethod((...callbackArgs) => {
|
|
2597
|
+
const current = source();
|
|
2598
|
+
if (callbackArgs.length > 0 && typeof current === 'function') {
|
|
2599
|
+
return current(...callbackArgs);
|
|
2600
|
+
}
|
|
2601
|
+
const value = typeof current === 'function'
|
|
2602
|
+
? current()
|
|
2603
|
+
: current;
|
|
2604
|
+
return isGenerator(value)
|
|
2605
|
+
? value
|
|
2606
|
+
: yieldableInvocation(value);
|
|
2607
|
+
});
|
|
2608
|
+
// Content inputs are object-shaped at the call site but component
|
|
2609
|
+
// factories receive callable input shells. Expose object properties on
|
|
2610
|
+
// the shell so the factory can use `content.header` without eagerly
|
|
2611
|
+
// evaluating a slot.
|
|
2612
|
+
return new Proxy(input, {
|
|
2613
|
+
get(target, property, receiver) {
|
|
2614
|
+
const current = source();
|
|
2615
|
+
if (typeof current === 'object' &&
|
|
2616
|
+
current !== null &&
|
|
2617
|
+
property in current) {
|
|
2618
|
+
return Reflect.get(current, property);
|
|
2619
|
+
}
|
|
2620
|
+
return Reflect.get(target, property, receiver);
|
|
2621
|
+
},
|
|
2622
|
+
has(target, property) {
|
|
2623
|
+
const current = source();
|
|
2624
|
+
return ((typeof current === 'object' &&
|
|
2625
|
+
current !== null &&
|
|
2626
|
+
property in current) ||
|
|
2627
|
+
property in target);
|
|
2628
|
+
},
|
|
2629
|
+
ownKeys(target) {
|
|
2630
|
+
const current = source();
|
|
2631
|
+
return [
|
|
2632
|
+
...new Set([
|
|
2633
|
+
...Reflect.ownKeys(target),
|
|
2634
|
+
...(typeof current === 'object' && current !== null
|
|
2635
|
+
? Reflect.ownKeys(current)
|
|
2636
|
+
: []),
|
|
2637
|
+
]),
|
|
2638
|
+
];
|
|
2639
|
+
},
|
|
2640
|
+
getOwnPropertyDescriptor(target, property) {
|
|
2641
|
+
const current = source();
|
|
2642
|
+
if (typeof current === 'object' &&
|
|
2643
|
+
current !== null &&
|
|
2644
|
+
property in current) {
|
|
2645
|
+
return {
|
|
2646
|
+
configurable: true,
|
|
2647
|
+
enumerable: true,
|
|
2648
|
+
value: Reflect.get(current, property),
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
return Reflect.getOwnPropertyDescriptor(target, property);
|
|
2652
|
+
},
|
|
2653
|
+
});
|
|
2654
|
+
});
|
|
2655
|
+
// A one-argument logic factory may model its complete input as one typed
|
|
2656
|
+
// object. The callable shell keeps the historical Input<T> invocation
|
|
2657
|
+
// while exposing the object's properties for the contract-based API.
|
|
2658
|
+
const args = !this.templateOnly && definition.factory.length === 1
|
|
2659
|
+
? [
|
|
2660
|
+
new Proxy(((...callbackArgs) => inputShells[0](...callbackArgs)), {
|
|
2661
|
+
get: (target, property, receiver) => {
|
|
2662
|
+
const index = this.propKeys.indexOf(String(property));
|
|
2663
|
+
if (index !== -1) {
|
|
2664
|
+
return preserveContentDeclarationContext(this.propSources[index](), declarationContext ?? context);
|
|
2665
|
+
}
|
|
2666
|
+
return Reflect.get(target, property, receiver);
|
|
2667
|
+
},
|
|
2668
|
+
has: (target, property) => this.propKeys.includes(String(property)) ||
|
|
2669
|
+
Reflect.has(target, property),
|
|
2670
|
+
ownKeys: (target) => [
|
|
2671
|
+
...new Set([...Reflect.ownKeys(target), ...this.propKeys]),
|
|
2672
|
+
],
|
|
2673
|
+
getOwnPropertyDescriptor: (target, property) => {
|
|
2674
|
+
const index = this.propKeys.indexOf(String(property));
|
|
2675
|
+
return index === -1
|
|
2676
|
+
? Reflect.getOwnPropertyDescriptor(target, property)
|
|
2677
|
+
: {
|
|
2678
|
+
configurable: true,
|
|
2679
|
+
enumerable: true,
|
|
2680
|
+
value: preserveContentDeclarationContext(this.propSources[index](), declarationContext ?? context),
|
|
2681
|
+
};
|
|
2682
|
+
},
|
|
2683
|
+
}),
|
|
2684
|
+
]
|
|
2685
|
+
: inputShells;
|
|
2686
|
+
const factoryContext = composition
|
|
2687
|
+
? undefined
|
|
2688
|
+
: this.templateOnly
|
|
2689
|
+
? templateContext?.value
|
|
2690
|
+
: untracked(() => executeCraftComponentFactory(definition.factory, args, this.environmentInjector));
|
|
2691
|
+
if (!composition &&
|
|
2692
|
+
!this.templateOnly &&
|
|
2693
|
+
typeof factoryContext === 'object' &&
|
|
2694
|
+
factoryContext !== null &&
|
|
2695
|
+
'then' in factoryContext) {
|
|
2696
|
+
throw new Error('Async component factories are not renderable directly. Move asynchronous work behind defer().');
|
|
2697
|
+
}
|
|
2698
|
+
this.factoryContext = factoryContext;
|
|
2699
|
+
if (!composition) {
|
|
2700
|
+
this.registerRuntimeTargets(definition, factoryContext, this.environmentInjector);
|
|
2701
|
+
}
|
|
2702
|
+
this.view = createFragment(parent, before, childContext(componentRenderContext, {
|
|
2703
|
+
injector: composition ? parentInjector : this.environmentInjector,
|
|
2704
|
+
componentContext: factoryContext,
|
|
2705
|
+
}), composition ? [] : [], 'craft-component');
|
|
2706
|
+
this.effectRef = untracked(() => runInInjectionContext(composition ? parentInjector : this.environmentInjector, () => craftEffect('component-render', composition
|
|
2707
|
+
? (onCleanup) => {
|
|
2708
|
+
// Composition providers and public inputs are the only
|
|
2709
|
+
// dependencies that should recreate the component scope.
|
|
2710
|
+
// Template signals (for example a query status) are tracked
|
|
2711
|
+
// by the dedicated template effect created by
|
|
2712
|
+
// refreshComposedComponent().
|
|
2713
|
+
this.propSources.forEach((source) => source());
|
|
2714
|
+
this.hostPropsSource();
|
|
2715
|
+
untracked(() => this.refreshComposedComponent(definition, args, componentElement, parentInjector, additionalProviders, hostTarget, context, componentRenderContext, onCleanup));
|
|
2716
|
+
this.providerTrackers.forEach((tracker) => tracker());
|
|
2717
|
+
}
|
|
2718
|
+
: () => {
|
|
2719
|
+
try {
|
|
2720
|
+
const callSiteHostProps = this.hostPropsSource();
|
|
2721
|
+
const hostProps = mergeHostProps(definition.meta.host ?? {}, callSiteHostProps);
|
|
2722
|
+
const renderContext = childContext(componentRenderContext, {
|
|
2723
|
+
injector: this.environmentInjector,
|
|
2724
|
+
componentContext: this.factoryContext,
|
|
2725
|
+
});
|
|
2726
|
+
this.traceState.renderCount += 1;
|
|
2727
|
+
this.latestTemplate = executeTemplateTrace(renderContext.injector, traceContext(renderContext, 'component', this.traceState.renderCount === 1
|
|
2728
|
+
? 'initialRender'
|
|
2729
|
+
: 'update', definition.name), () => withCraftRenderContext(renderContext, () => definition.template(projectYieldableTemplateContext(this.factoryContext), callSiteHostProps)));
|
|
2730
|
+
this.view.patchChildren(this.latestTemplate);
|
|
2731
|
+
this.hostBindings?.patch(hostProps);
|
|
2732
|
+
context.exceptionBoundaryResolved?.();
|
|
2733
|
+
}
|
|
2734
|
+
catch (error) {
|
|
2735
|
+
if (isCraftGenShortCircuit(error) &&
|
|
2736
|
+
context.exceptionBoundary?.(error.exception)) {
|
|
2737
|
+
this.view.patchChildren([]);
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
if (isCraftGenShortCircuit(error)) {
|
|
2741
|
+
throw new CraftUnhandledExceptionError(error.exception);
|
|
2742
|
+
}
|
|
2743
|
+
throw error;
|
|
2744
|
+
}
|
|
2745
|
+
})));
|
|
2746
|
+
}
|
|
2747
|
+
firstNode() {
|
|
2748
|
+
return this.view.firstNode();
|
|
2749
|
+
}
|
|
2750
|
+
refreshComposedComponent(definition, args, componentElement, parentInjector, additionalProviders, hostTarget, context, componentRenderContext, onCleanup) {
|
|
2751
|
+
const composition = definition.composition;
|
|
2752
|
+
if (!composition) {
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
this.view.patchChildren([]);
|
|
2756
|
+
this.environmentInjector = createEnvironmentInjector([
|
|
2757
|
+
...provideHostName(`component:${definition.name}`),
|
|
2758
|
+
{
|
|
2759
|
+
provide: CRAFT_HYDRATION_ID,
|
|
2760
|
+
useValue: componentRenderContext.identity,
|
|
2761
|
+
},
|
|
2762
|
+
...(definition.meta.providers ?? []),
|
|
2763
|
+
...(composition.providers ?? []),
|
|
2764
|
+
{
|
|
2765
|
+
provide: ElementRef,
|
|
2766
|
+
useValue: new ElementRef(componentElement),
|
|
2767
|
+
},
|
|
2768
|
+
...additionalProviders,
|
|
2769
|
+
], parentInjector, 'CraftComponent');
|
|
2770
|
+
if (!this.traceCreated) {
|
|
2771
|
+
this.traceCreated = true;
|
|
2772
|
+
traceComponentLifecycle(this.environmentInjector, 'create', definition.name, this.traceState.renderCount);
|
|
2773
|
+
}
|
|
2774
|
+
const environmentInjector = this.environmentInjector;
|
|
2775
|
+
let renderInjector = environmentInjector;
|
|
2776
|
+
onCleanup(() => {
|
|
2777
|
+
this.registrationReleases.forEach((release) => release());
|
|
2778
|
+
this.registrationReleases = [];
|
|
2779
|
+
this.composedTemplateEffect?.destroy();
|
|
2780
|
+
this.composedTemplateEffect = undefined;
|
|
2781
|
+
this.syncTemplateFlushRelease?.();
|
|
2782
|
+
this.syncTemplateFlushRelease = undefined;
|
|
2783
|
+
this.componentExceptionEffect?.destroy();
|
|
2784
|
+
this.componentExceptionEffect = undefined;
|
|
2785
|
+
this.componentFallbackVisible = false;
|
|
2786
|
+
this.componentFallbackException = undefined;
|
|
2787
|
+
this.view.patchChildren([]);
|
|
2788
|
+
renderInjector.destroy();
|
|
2789
|
+
if (renderInjector !== environmentInjector) {
|
|
2790
|
+
environmentInjector.destroy();
|
|
2791
|
+
}
|
|
2792
|
+
this.providerTrackers = [];
|
|
2793
|
+
if (this.environmentInjector === renderInjector) {
|
|
2794
|
+
this.environmentInjector = undefined;
|
|
2795
|
+
this.factoryContext = undefined;
|
|
2796
|
+
}
|
|
2797
|
+
});
|
|
2798
|
+
let factoryContext;
|
|
2799
|
+
let renderContext;
|
|
2800
|
+
const handledResourceExceptionCodes = new Set();
|
|
2801
|
+
const componentBoundary = composition.catchBlockPosition
|
|
2802
|
+
? (exception) => {
|
|
2803
|
+
if (!composition.catchHandlers?.[exception._tag]) {
|
|
2804
|
+
return context.exceptionBoundary?.(exception) ?? false;
|
|
2805
|
+
}
|
|
2806
|
+
this.renderComposedException(definition, exception, renderInjector, renderContext, context, hostTarget);
|
|
2807
|
+
return true;
|
|
2808
|
+
}
|
|
2809
|
+
: undefined;
|
|
2810
|
+
renderContext = childContext(componentRenderContext, {
|
|
2811
|
+
injector: environmentInjector,
|
|
2812
|
+
handledResourceExceptionCodes,
|
|
2813
|
+
...(componentBoundary
|
|
2814
|
+
? {
|
|
2815
|
+
exceptionBoundary: componentBoundary,
|
|
2816
|
+
}
|
|
2817
|
+
: {}),
|
|
2818
|
+
});
|
|
2819
|
+
try {
|
|
2820
|
+
const providerResolution = eagerlyResolveBrandedProviders(composition.providers ?? [], environmentInjector);
|
|
2821
|
+
this.providerTrackers = providerResolution.trackers;
|
|
2822
|
+
if (providerResolution.exception) {
|
|
2823
|
+
renderContext = childContext(componentRenderContext, {
|
|
2824
|
+
injector: environmentInjector,
|
|
2825
|
+
});
|
|
2826
|
+
this.view.updateContext(renderContext);
|
|
2827
|
+
this.renderComposedException(definition, providerResolution.exception, environmentInjector, renderContext, context, hostTarget);
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
if (providerResolution.overrides.length > 0) {
|
|
2831
|
+
renderInjector = createEnvironmentInjector([...providerResolution.overrides], environmentInjector, 'CraftComponentProviders');
|
|
2832
|
+
this.environmentInjector = renderInjector;
|
|
2833
|
+
}
|
|
2834
|
+
renderContext = childContext(componentRenderContext, {
|
|
2835
|
+
injector: renderInjector,
|
|
2836
|
+
handledResourceExceptionCodes,
|
|
2837
|
+
});
|
|
2838
|
+
this.view.updateContext(renderContext);
|
|
2839
|
+
factoryContext = untracked(() => executeCraftComponentFactory(definition.factory, args, renderInjector));
|
|
2840
|
+
if (typeof factoryContext === 'object' &&
|
|
2841
|
+
factoryContext !== null &&
|
|
2842
|
+
'then' in factoryContext) {
|
|
2843
|
+
throw new Error('Async component factories are not renderable directly. Move asynchronous work behind defer().');
|
|
2844
|
+
}
|
|
2845
|
+
this.registerRuntimeTargets(definition, factoryContext, renderInjector);
|
|
2846
|
+
}
|
|
2847
|
+
catch (error) {
|
|
2848
|
+
if (!isCraftGenShortCircuit(error)) {
|
|
2849
|
+
throw error;
|
|
2850
|
+
}
|
|
2851
|
+
if (this.context.exceptionBoundary?.(error.exception)) {
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
this.renderComposedException(definition, error.exception, renderInjector, renderContext, context, hostTarget);
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
if (isCraftException(factoryContext)) {
|
|
2858
|
+
this.renderComposedException(definition, factoryContext, renderInjector, renderContext, context, hostTarget);
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
this.composedTemplateEffect = untracked(() => runInInjectionContext(renderInjector, () => craftEffect('component-template', () => {
|
|
2862
|
+
this.renderComposedTemplate(definition, factoryContext, renderInjector, renderContext, context, hostTarget);
|
|
2863
|
+
if (composition.catchBlockPosition &&
|
|
2864
|
+
!this.componentExceptionEffect) {
|
|
2865
|
+
this.installComponentExceptionEffect(definition, factoryContext, renderInjector, renderContext, context, hostTarget);
|
|
2866
|
+
}
|
|
2867
|
+
})));
|
|
2868
|
+
this.syncTemplateFlushRelease?.();
|
|
2869
|
+
this.syncTemplateFlushRelease = registerCraftSyncTemplateFlush(() => {
|
|
2870
|
+
this.renderComposedTemplate(definition, factoryContext, renderInjector, renderContext, context, hostTarget);
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
installComponentExceptionEffect(definition, factoryContext, renderInjector, renderContext, context, hostTarget) {
|
|
2874
|
+
this.componentExceptionEffect = untracked(() => runInInjectionContext(renderInjector, () => craftEffect('component-catch-block-resource-exceptions', () => {
|
|
2875
|
+
const exception = findResourceException(factoryContext);
|
|
2876
|
+
if (exception) {
|
|
2877
|
+
if (renderContext.handledResourceExceptionCodes?.has(exception._tag)) {
|
|
2878
|
+
if (this.componentFallbackVisible) {
|
|
2879
|
+
this.componentFallbackVisible = false;
|
|
2880
|
+
this.componentFallbackException = undefined;
|
|
2881
|
+
untracked(() => this.renderComposedTemplate(definition, factoryContext, renderInjector, renderContext, context, hostTarget));
|
|
2882
|
+
}
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
if (this.componentFallbackVisible &&
|
|
2886
|
+
this.componentFallbackException === exception) {
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
this.componentFallbackVisible = true;
|
|
2890
|
+
this.componentFallbackException = exception;
|
|
2891
|
+
untracked(() => this.renderComposedException(definition, exception, renderInjector, renderContext, context, hostTarget, true));
|
|
2892
|
+
}
|
|
2893
|
+
else if (this.componentFallbackVisible) {
|
|
2894
|
+
this.componentFallbackVisible = false;
|
|
2895
|
+
this.componentFallbackException = undefined;
|
|
2896
|
+
untracked(() => this.renderComposedTemplate(definition, factoryContext, renderInjector, renderContext, context, hostTarget));
|
|
2897
|
+
}
|
|
2898
|
+
}, { manualCleanup: true })));
|
|
2899
|
+
}
|
|
2900
|
+
registerRuntimeTargets(definition, factoryContext, injector) {
|
|
2901
|
+
this.registrationReleases.forEach((release) => release());
|
|
2902
|
+
this.registrationReleases = [
|
|
2903
|
+
ɵregisterCraftTarget(injector, this.component, factoryContext, componentHostName(injector, definition.name)),
|
|
2904
|
+
...definition.styleOwners.flatMap((owner) => owner.registrationTarget === undefined
|
|
2905
|
+
? []
|
|
2906
|
+
: [
|
|
2907
|
+
ɵregisterCraftTarget(injector, owner.registrationTarget, factoryContext, allocateCraftHostName(injector, 'directive', owner.name)),
|
|
2908
|
+
]),
|
|
2909
|
+
];
|
|
2910
|
+
}
|
|
2911
|
+
renderComposedTemplate(definition, factoryContext, renderInjector, renderContext, context, hostTarget) {
|
|
2912
|
+
if (this.componentFallbackVisible)
|
|
2913
|
+
return;
|
|
2914
|
+
try {
|
|
2915
|
+
this.traceState.renderCount += 1;
|
|
2916
|
+
const rendered = this.composedTemplateChildren(definition, factoryContext, renderContext);
|
|
2917
|
+
const children = this.withComponentFieldExceptionBlock(definition, rendered.children);
|
|
2918
|
+
this.latestTemplate = children;
|
|
2919
|
+
this.view.patchChildren(children);
|
|
2920
|
+
this.hostBindings?.patch(rendered.hostProps);
|
|
2921
|
+
}
|
|
2922
|
+
catch (error) {
|
|
2923
|
+
if (!isCraftGenShortCircuit(error))
|
|
2924
|
+
throw error;
|
|
2925
|
+
if (renderContext.exceptionBoundary?.(error.exception))
|
|
2926
|
+
return;
|
|
2927
|
+
this.renderComposedException(definition, error.exception, renderInjector, renderContext, context, hostTarget);
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
renderContext.exceptionBoundaryResolved?.();
|
|
2931
|
+
}
|
|
2932
|
+
composedTemplateChildren(definition, factoryContext, renderContext) {
|
|
2933
|
+
this.factoryContext = factoryContext;
|
|
2934
|
+
this.view.updateContext(childContext(renderContext, { componentContext: factoryContext }));
|
|
2935
|
+
const callSiteHostProps = this.hostPropsSource();
|
|
2936
|
+
const hostProps = mergeHostProps(definition.meta.host ?? {}, callSiteHostProps);
|
|
2937
|
+
return {
|
|
2938
|
+
children: executeTemplateTrace(renderContext.injector, traceContext(renderContext, 'component', this.traceState.renderCount === 1 ? 'initialRender' : 'update', definition.name), () => withCraftRenderContext(renderContext, () => definition.template(projectYieldableTemplateContext(factoryContext), callSiteHostProps))),
|
|
2939
|
+
hostProps,
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
withComponentFieldExceptionBlock(definition, children) {
|
|
2943
|
+
const handlers = definition.composition?.fieldExceptionHandlers;
|
|
2944
|
+
const options = definition.composition?.fieldExceptionOptions;
|
|
2945
|
+
if (!handlers || !options)
|
|
2946
|
+
return children;
|
|
2947
|
+
return {
|
|
2948
|
+
kind: 'field-exception-block',
|
|
2949
|
+
source: children,
|
|
2950
|
+
handlers,
|
|
2951
|
+
options,
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
renderComposedException(definition, exception, environmentInjector, renderContext, context, hostTarget, preserveFactoryContext = false) {
|
|
2955
|
+
const blockHandler = definition.composition?.catchHandlers?.[exception._tag];
|
|
2956
|
+
const tagHandler = definition.composition?.catchTagHandlers?.[exception._tag];
|
|
2957
|
+
if (!blockHandler && !tagHandler) {
|
|
2958
|
+
if (this.context.exceptionBoundary?.(exception)) {
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
throw new CraftUnhandledExceptionError(exception);
|
|
2962
|
+
}
|
|
2963
|
+
if (!preserveFactoryContext) {
|
|
2964
|
+
this.factoryContext = undefined;
|
|
2965
|
+
}
|
|
2966
|
+
this.view.updateContext(renderContext);
|
|
2967
|
+
const hostProps = mergeHostProps(definition.meta.host ?? {}, this.hostPropsSource());
|
|
2968
|
+
if (tagHandler) {
|
|
2969
|
+
try {
|
|
2970
|
+
executeCraftComponentFactory(function* () {
|
|
2971
|
+
return yield* tagHandler(exception);
|
|
2972
|
+
}, [], environmentInjector);
|
|
2973
|
+
}
|
|
2974
|
+
catch (error) {
|
|
2975
|
+
if (!isCraftGenShortCircuit(error))
|
|
2976
|
+
throw error;
|
|
2977
|
+
if (context.exceptionBoundary?.(error.exception))
|
|
2978
|
+
return;
|
|
2979
|
+
throw new CraftUnhandledExceptionError(error.exception);
|
|
2980
|
+
}
|
|
2981
|
+
this.view.patchChildren([]);
|
|
2982
|
+
}
|
|
2983
|
+
else {
|
|
2984
|
+
const resolved = runInInjectionContext(environmentInjector, () => resolveCatchBlockHandler(blockHandler, exception, false, definition.composition?.catchBlockPosition ?? 'after'));
|
|
2985
|
+
if (preserveFactoryContext && resolved.showSource) {
|
|
2986
|
+
try {
|
|
2987
|
+
const source = untracked(() => this.composedTemplateChildren(definition, this.factoryContext, renderContext));
|
|
2988
|
+
const children = resolved.position === 'before'
|
|
2989
|
+
? [
|
|
2990
|
+
resolved.children,
|
|
2991
|
+
this.withComponentFieldExceptionBlock(definition, source.children),
|
|
2992
|
+
]
|
|
2993
|
+
: [
|
|
2994
|
+
this.withComponentFieldExceptionBlock(definition, source.children),
|
|
2995
|
+
resolved.children,
|
|
2996
|
+
];
|
|
2997
|
+
this.view.patchChildren(children);
|
|
2998
|
+
this.hostBindings?.patch(source.hostProps);
|
|
2999
|
+
}
|
|
3000
|
+
catch (error) {
|
|
3001
|
+
if (!isCraftGenShortCircuit(error))
|
|
3002
|
+
throw error;
|
|
3003
|
+
this.view.patchChildren(resolved.children);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
else {
|
|
3007
|
+
this.view.patchChildren(resolved.children);
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
this.hostBindings?.patch(hostProps);
|
|
3011
|
+
}
|
|
3012
|
+
lastNode() {
|
|
3013
|
+
return this.view.lastNode();
|
|
3014
|
+
}
|
|
3015
|
+
patch(node) {
|
|
3016
|
+
if (node.kind !== 'component' ||
|
|
3017
|
+
node.component !== this.component ||
|
|
3018
|
+
(node.injector !== undefined && node.injector !== this.context.injector)) {
|
|
3019
|
+
return false;
|
|
3020
|
+
}
|
|
3021
|
+
const props = node.props;
|
|
3022
|
+
this.propKeys.forEach((key, index) => {
|
|
3023
|
+
this.propSources[index].set(props[key]);
|
|
3024
|
+
});
|
|
3025
|
+
this.hostPropsSource.set(hostPropsFromComponentProps(props));
|
|
3026
|
+
return true;
|
|
3027
|
+
}
|
|
3028
|
+
updateProps(props) {
|
|
3029
|
+
if (this.templateOnly) {
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
this.patch({
|
|
3033
|
+
kind: 'component',
|
|
3034
|
+
component: this.component,
|
|
3035
|
+
props,
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
updateContext(context) {
|
|
3039
|
+
if (!this.templateOnly) {
|
|
3040
|
+
throw new Error('Cannot update the context of a logic-backed component.');
|
|
3041
|
+
}
|
|
3042
|
+
this.factoryContext = context;
|
|
3043
|
+
const definition = this.component[CRAFT_COMPONENT];
|
|
3044
|
+
const callSiteHostProps = this.hostPropsSource();
|
|
3045
|
+
const hostProps = mergeHostProps(definition.meta.host ?? {}, callSiteHostProps);
|
|
3046
|
+
this.traceState.renderCount += 1;
|
|
3047
|
+
this.latestTemplate = executeTemplateTrace(this.environmentInjector ?? this.context.injector, traceContext(this.componentRenderContext, 'component', this.traceState.renderCount === 1 ? 'initialRender' : 'update', definition.name), () => definition.template(projectYieldableTemplateContext(context), callSiteHostProps));
|
|
3048
|
+
this.view.patchChildren(this.latestTemplate);
|
|
3049
|
+
this.hostBindings?.patch(hostProps);
|
|
3050
|
+
}
|
|
3051
|
+
locator(host, tag, criteria) {
|
|
3052
|
+
return findCraftTemplateLocator(host, this.latestTemplate, tag, criteria);
|
|
3053
|
+
}
|
|
3054
|
+
destroy() {
|
|
3055
|
+
if (this.traceCreated) {
|
|
3056
|
+
traceComponentLifecycle(this.environmentInjector ?? this.context.injector, 'destroy', this.componentRenderContext.componentName ?? 'unknown', this.traceState.renderCount);
|
|
3057
|
+
this.traceCreated = false;
|
|
3058
|
+
}
|
|
3059
|
+
this.effectRef.destroy();
|
|
3060
|
+
this.hostBindings?.destroy();
|
|
3061
|
+
this.view.destroy();
|
|
3062
|
+
this.styleReleases.forEach((release) => release());
|
|
3063
|
+
this.environmentInjector?.destroy();
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
class DeferRenderedNode {
|
|
3067
|
+
kind = 'defer';
|
|
3068
|
+
view;
|
|
3069
|
+
state = 'placeholder';
|
|
3070
|
+
destroyed = false;
|
|
3071
|
+
triggerCleanup;
|
|
3072
|
+
triggerTimer;
|
|
3073
|
+
loadedValue;
|
|
3074
|
+
loadError;
|
|
3075
|
+
node;
|
|
3076
|
+
parent;
|
|
3077
|
+
context;
|
|
3078
|
+
constructor(node, parent, before, context) {
|
|
3079
|
+
this.node = node;
|
|
3080
|
+
this.parent = parent;
|
|
3081
|
+
this.context = context;
|
|
3082
|
+
this.view = createFragment(parent, before, context, node.placeholder
|
|
3083
|
+
? renderDeferChildren(context, 'placeholder', node.placeholder)
|
|
3084
|
+
: [], 'craft-defer');
|
|
3085
|
+
this.installTrigger();
|
|
3086
|
+
}
|
|
3087
|
+
firstNode() {
|
|
3088
|
+
return this.view.firstNode();
|
|
3089
|
+
}
|
|
3090
|
+
lastNode() {
|
|
3091
|
+
return this.view.lastNode();
|
|
3092
|
+
}
|
|
3093
|
+
patch(node) {
|
|
3094
|
+
if (node.kind !== 'defer' || node.loader !== this.node.loader) {
|
|
3095
|
+
return false;
|
|
3096
|
+
}
|
|
3097
|
+
this.node = node;
|
|
3098
|
+
if (this.state === 'loaded') {
|
|
3099
|
+
this.view.patchChildren(renderDeferChildren(this.context, 'resolve', this.node.resolve, [
|
|
3100
|
+
this.loadedValue,
|
|
3101
|
+
]));
|
|
3102
|
+
}
|
|
3103
|
+
else if (this.state === 'error') {
|
|
3104
|
+
this.view.patchChildren(this.node.error
|
|
3105
|
+
? renderDeferChildren(this.context, 'error', this.node.error, [
|
|
3106
|
+
this.loadError,
|
|
3107
|
+
])
|
|
3108
|
+
: []);
|
|
3109
|
+
}
|
|
3110
|
+
else if (this.state === 'loading') {
|
|
3111
|
+
this.view.patchChildren(this.node.loading
|
|
3112
|
+
? renderDeferChildren(this.context, 'loading', this.node.loading)
|
|
3113
|
+
: this.node.placeholder
|
|
3114
|
+
? renderDeferChildren(this.context, 'placeholder', this.node.placeholder)
|
|
3115
|
+
: []);
|
|
3116
|
+
}
|
|
3117
|
+
else {
|
|
3118
|
+
this.view.patchChildren(this.node.placeholder
|
|
3119
|
+
? renderDeferChildren(this.context, 'placeholder', this.node.placeholder)
|
|
3120
|
+
: []);
|
|
3121
|
+
}
|
|
3122
|
+
return true;
|
|
3123
|
+
}
|
|
3124
|
+
installTrigger() {
|
|
3125
|
+
if (this.node.trigger === 'immediate') {
|
|
3126
|
+
this.startLoad();
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
if (this.node.trigger === 'idle') {
|
|
3130
|
+
const windowRef = this.parent.ownerDocument?.defaultView;
|
|
3131
|
+
const idleWindow = windowRef;
|
|
3132
|
+
if (idleWindow?.requestIdleCallback) {
|
|
3133
|
+
const handle = idleWindow.requestIdleCallback(() => this.startLoad());
|
|
3134
|
+
this.triggerCleanup = () => idleWindow.cancelIdleCallback?.(handle);
|
|
3135
|
+
}
|
|
3136
|
+
else {
|
|
3137
|
+
this.triggerTimer = this.context.injector
|
|
3138
|
+
.get(CRAFT_TEMPORAL_RUNTIME, new RealCraftTemporalRuntime())
|
|
3139
|
+
.schedule(() => this.startLoad(), 0, {
|
|
3140
|
+
kind: 'defer-trigger',
|
|
3141
|
+
owner: this.context.componentName,
|
|
3142
|
+
});
|
|
3143
|
+
this.triggerCleanup = () => this.triggerTimer?.cancel();
|
|
3144
|
+
}
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
if (this.node.trigger === 'viewport' &&
|
|
3148
|
+
typeof IntersectionObserver !== 'undefined') {
|
|
3149
|
+
const target = this.firstElementInView();
|
|
3150
|
+
if (target) {
|
|
3151
|
+
const observer = new IntersectionObserver((entries) => {
|
|
3152
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
3153
|
+
observer.disconnect();
|
|
3154
|
+
this.startLoad();
|
|
3155
|
+
}
|
|
3156
|
+
});
|
|
3157
|
+
observer.observe(target);
|
|
3158
|
+
this.triggerCleanup = () => observer.disconnect();
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
if (this.node.trigger === 'interaction') {
|
|
3163
|
+
const target = this.firstElementInView() ?? this.parent;
|
|
3164
|
+
const start = (event) => {
|
|
3165
|
+
if (event && event.type === 'keydown') {
|
|
3166
|
+
const key = event.key;
|
|
3167
|
+
if (key !== 'Enter' && key !== ' ')
|
|
3168
|
+
return;
|
|
3169
|
+
event.preventDefault();
|
|
3170
|
+
}
|
|
3171
|
+
this.startLoad();
|
|
3172
|
+
};
|
|
3173
|
+
if (isElementNode(target)) {
|
|
3174
|
+
const tag = target.tagName.toLowerCase();
|
|
3175
|
+
const interactive = tag === 'button' ||
|
|
3176
|
+
tag === 'a' ||
|
|
3177
|
+
tag === 'input' ||
|
|
3178
|
+
tag === 'select' ||
|
|
3179
|
+
tag === 'textarea';
|
|
3180
|
+
if (!interactive) {
|
|
3181
|
+
if (!target.hasAttribute('tabindex')) {
|
|
3182
|
+
this.context.renderer.setAttribute(target, 'tabindex', '0');
|
|
3183
|
+
}
|
|
3184
|
+
if (!target.getAttribute('role')) {
|
|
3185
|
+
this.context.renderer.setAttribute(target, 'role', 'button');
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
const clickCleanup = this.context.renderer.listen(target, 'click', start);
|
|
3190
|
+
const keyCleanup = this.context.renderer.listen(target, 'keydown', start);
|
|
3191
|
+
this.triggerCleanup = () => {
|
|
3192
|
+
clickCleanup();
|
|
3193
|
+
keyCleanup();
|
|
3194
|
+
};
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
this.triggerTimer = this.context.injector
|
|
3198
|
+
.get(CRAFT_TEMPORAL_RUNTIME, new RealCraftTemporalRuntime())
|
|
3199
|
+
.schedule(() => this.startLoad(), 0, {
|
|
3200
|
+
kind: 'defer-trigger',
|
|
3201
|
+
owner: this.context.componentName,
|
|
3202
|
+
});
|
|
3203
|
+
this.triggerCleanup = () => this.triggerTimer?.cancel();
|
|
3204
|
+
}
|
|
3205
|
+
firstElementInView() {
|
|
3206
|
+
let current = this.view.firstNode().nextSibling;
|
|
3207
|
+
while (current && current !== this.view.lastNode()) {
|
|
3208
|
+
if (current.nodeType === 1) {
|
|
3209
|
+
return current;
|
|
3210
|
+
}
|
|
3211
|
+
current = current.nextSibling;
|
|
3212
|
+
}
|
|
3213
|
+
return undefined;
|
|
3214
|
+
}
|
|
3215
|
+
setBusy(busy) {
|
|
3216
|
+
const target = this.firstElementInView();
|
|
3217
|
+
if (!target)
|
|
3218
|
+
return;
|
|
3219
|
+
if (busy) {
|
|
3220
|
+
this.context.renderer.setAttribute(target, 'aria-busy', 'true');
|
|
3221
|
+
}
|
|
3222
|
+
else {
|
|
3223
|
+
this.context.renderer.removeAttribute(target, 'aria-busy');
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
startLoad() {
|
|
3227
|
+
if (this.state !== 'placeholder' || this.destroyed) {
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
this.state = 'loading';
|
|
3231
|
+
this.triggerCleanup?.();
|
|
3232
|
+
this.triggerCleanup = undefined;
|
|
3233
|
+
this.setBusy(true);
|
|
3234
|
+
this.view.patchChildren(this.node.loading
|
|
3235
|
+
? renderDeferChildren(this.context, 'loading', this.node.loading)
|
|
3236
|
+
: this.node.placeholder
|
|
3237
|
+
? renderDeferChildren(this.context, 'placeholder', this.node.placeholder)
|
|
3238
|
+
: []);
|
|
3239
|
+
const loader = this.node.loader;
|
|
3240
|
+
executeCraftComponentFactoryAsync(function* () {
|
|
3241
|
+
return yield* craftLazy(loader);
|
|
3242
|
+
}, [], this.context.injector)
|
|
3243
|
+
.then((settled) => {
|
|
3244
|
+
if (this.destroyed) {
|
|
3245
|
+
return;
|
|
3246
|
+
}
|
|
3247
|
+
if (settled.kind === 'shortCircuit') {
|
|
3248
|
+
this.state = 'error';
|
|
3249
|
+
this.loadError = settled.exception;
|
|
3250
|
+
this.view.patchChildren(this.node.error
|
|
3251
|
+
? renderDeferChildren(this.context, 'error', this.node.error, [
|
|
3252
|
+
settled.exception,
|
|
3253
|
+
])
|
|
3254
|
+
: []);
|
|
3255
|
+
this.setBusy(false);
|
|
3256
|
+
return;
|
|
3257
|
+
}
|
|
3258
|
+
this.state = 'loaded';
|
|
3259
|
+
this.loadedValue = settled.value;
|
|
3260
|
+
this.view.patchChildren(renderDeferChildren(this.context, 'resolve', this.node.resolve, [
|
|
3261
|
+
settled.value,
|
|
3262
|
+
]));
|
|
3263
|
+
this.setBusy(false);
|
|
3264
|
+
})
|
|
3265
|
+
.catch((error) => {
|
|
3266
|
+
if (!this.destroyed) {
|
|
3267
|
+
this.state = 'error';
|
|
3268
|
+
this.loadError = error;
|
|
3269
|
+
this.view.patchChildren(this.node.error
|
|
3270
|
+
? renderDeferChildren(this.context, 'error', this.node.error, [
|
|
3271
|
+
error,
|
|
3272
|
+
])
|
|
3273
|
+
: []);
|
|
3274
|
+
this.setBusy(false);
|
|
3275
|
+
}
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
destroy() {
|
|
3279
|
+
this.destroyed = true;
|
|
3280
|
+
this.triggerCleanup?.();
|
|
3281
|
+
this.view.destroy();
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
class HeadingSectionRenderedNode {
|
|
3285
|
+
kind = 'heading-section';
|
|
3286
|
+
view;
|
|
3287
|
+
constructor(node, parent, before, context) {
|
|
3288
|
+
const level = node.reset ? 1 : Math.min(6, (context.headingLevel ?? 1) + 1);
|
|
3289
|
+
this.view = createFragment(parent, before, childContext(context, { headingLevel: level }), node.children, 'craft-heading-section');
|
|
3290
|
+
}
|
|
3291
|
+
firstNode() {
|
|
3292
|
+
return this.view.firstNode();
|
|
3293
|
+
}
|
|
3294
|
+
lastNode() {
|
|
3295
|
+
return this.view.lastNode();
|
|
3296
|
+
}
|
|
3297
|
+
patch(node) {
|
|
3298
|
+
if (node.kind !== 'heading-section')
|
|
3299
|
+
return false;
|
|
3300
|
+
this.view.patchChildren(node.children);
|
|
3301
|
+
return true;
|
|
3302
|
+
}
|
|
3303
|
+
destroy() {
|
|
3304
|
+
this.view.destroy();
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
class HeadingRenderedNode {
|
|
3308
|
+
kind = 'heading';
|
|
3309
|
+
elementView;
|
|
3310
|
+
tag;
|
|
3311
|
+
constructor(node, parent, before, context) {
|
|
3312
|
+
const level = Math.min(6, Math.max(1, context.headingLevel ?? 1));
|
|
3313
|
+
this.tag = `h${level}`;
|
|
3314
|
+
const created = createElementForRender(this.tag, parent, before, context);
|
|
3315
|
+
this.elementView = new ElementRenderedNode(created.element, this.tag, created.context, {
|
|
3316
|
+
kind: 'element',
|
|
3317
|
+
tag: this.tag,
|
|
3318
|
+
props: node.props,
|
|
3319
|
+
children: node.children,
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
firstNode() {
|
|
3323
|
+
return this.elementView.firstNode();
|
|
3324
|
+
}
|
|
3325
|
+
lastNode() {
|
|
3326
|
+
return this.elementView.lastNode();
|
|
3327
|
+
}
|
|
3328
|
+
patch(node) {
|
|
3329
|
+
if (node.kind !== 'heading')
|
|
3330
|
+
return false;
|
|
3331
|
+
return this.elementView.patch({
|
|
3332
|
+
kind: 'element',
|
|
3333
|
+
tag: this.tag,
|
|
3334
|
+
props: node.props,
|
|
3335
|
+
children: node.children,
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
destroy() {
|
|
3339
|
+
this.elementView.destroy();
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
function mountNode(node, parent, before, context) {
|
|
3343
|
+
switch (node.kind) {
|
|
3344
|
+
case 'text': {
|
|
3345
|
+
const created = createTextForRender(node.value, parent, before, context, true);
|
|
3346
|
+
return new TextRenderedNode(created.text, node.value, created.context.renderer);
|
|
3347
|
+
}
|
|
3348
|
+
case 'reactive-text': {
|
|
3349
|
+
const created = createTextForRender('', parent, before, context, false);
|
|
3350
|
+
return new ReactiveTextRenderedNode(created.text, node.binding, created.context);
|
|
3351
|
+
}
|
|
3352
|
+
case 'element': {
|
|
3353
|
+
const created = createElementForRender(node.tag, parent, before, context);
|
|
3354
|
+
if (created.context.rootScope) {
|
|
3355
|
+
created.context.renderer.setAttribute(created.element, 'data-craft-root', created.context.rootScope);
|
|
3356
|
+
}
|
|
3357
|
+
if (created.context.contentScope) {
|
|
3358
|
+
created.context.renderer.setAttribute(created.element, 'data-craft-content', created.context.contentScope);
|
|
3359
|
+
}
|
|
3360
|
+
return new ElementRenderedNode(created.element, node.tag, created.context, node);
|
|
3361
|
+
}
|
|
3362
|
+
case 'component':
|
|
3363
|
+
return new ComponentRenderedNode(node.component, node.props, parent, before, node.injector
|
|
3364
|
+
? childContext(context, { injector: node.injector })
|
|
3365
|
+
: context, undefined, undefined, [], node.declarationContext);
|
|
3366
|
+
case 'directive':
|
|
3367
|
+
return new CraftDirectiveRenderedNode(node, parent, before, context);
|
|
3368
|
+
case 'each': {
|
|
3369
|
+
const start = mountComment(parent, 'craft-each:start', before, context);
|
|
3370
|
+
const end = mountComment(parent, 'craft-each:end', before, context);
|
|
3371
|
+
return new EachRenderedNode(node, parent, start, end, context);
|
|
3372
|
+
}
|
|
3373
|
+
case 'if': {
|
|
3374
|
+
return new IfBlockRenderedNode(node, parent, before, context);
|
|
3375
|
+
}
|
|
3376
|
+
case 'heading': {
|
|
3377
|
+
return new HeadingRenderedNode(node, parent, before, context);
|
|
3378
|
+
}
|
|
3379
|
+
case 'heading-section': {
|
|
3380
|
+
return new HeadingSectionRenderedNode(node, parent, before, context);
|
|
3381
|
+
}
|
|
3382
|
+
case 'pending-block': {
|
|
3383
|
+
return new PendingBlockRenderedNode(node, parent, before, context);
|
|
3384
|
+
}
|
|
3385
|
+
case 'catch-block': {
|
|
3386
|
+
return new CatchBlockRenderedNode(node, parent, before, context);
|
|
3387
|
+
}
|
|
3388
|
+
case 'field-exception-block': {
|
|
3389
|
+
return new FieldExceptionBlockRenderedNode(node, parent, before, context);
|
|
3390
|
+
}
|
|
3391
|
+
case 'match-block': {
|
|
3392
|
+
return new MatchBlockRenderedNode(node, parent, before, context);
|
|
3393
|
+
}
|
|
3394
|
+
case 'defer':
|
|
3395
|
+
return new DeferRenderedNode(node, parent, before, context);
|
|
3396
|
+
case 'projection':
|
|
3397
|
+
return new ProjectionRenderedNode(node, parent, before, context);
|
|
3398
|
+
case 'template':
|
|
3399
|
+
return new TemplateRenderedNode(node, parent, before, context);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
class HostPropertyBindings {
|
|
3403
|
+
props = {};
|
|
3404
|
+
bindings = new Map();
|
|
3405
|
+
host;
|
|
3406
|
+
context;
|
|
3407
|
+
constructor(host, context) {
|
|
3408
|
+
this.host = host;
|
|
3409
|
+
this.context = context;
|
|
3410
|
+
}
|
|
3411
|
+
patch(next) {
|
|
3412
|
+
const renderer = this.context.renderer;
|
|
3413
|
+
const previousAttributes = flattenAttributes(this.props);
|
|
3414
|
+
const nextAttributes = flattenAttributes(next);
|
|
3415
|
+
for (const key of previousAttributes.keys()) {
|
|
3416
|
+
if (!nextAttributes.has(key)) {
|
|
3417
|
+
this.destroyBinding(`attribute:${key}`);
|
|
3418
|
+
applyAttribute(renderer, this.host, key, null, this.context);
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
for (const [key, value] of nextAttributes) {
|
|
3422
|
+
if (typeof value === 'function') {
|
|
3423
|
+
this.updateBinding(`attribute:${key}`, value, () => resolveTemplateValue(value, this.context), (resolved) => applyAttribute(renderer, this.host, key, resolved, this.context));
|
|
3424
|
+
}
|
|
3425
|
+
else {
|
|
3426
|
+
this.destroyBinding(`attribute:${key}`);
|
|
3427
|
+
if (!Object.is(previousAttributes.get(key), value) ||
|
|
3428
|
+
typeof previousAttributes.get(key) === 'function') {
|
|
3429
|
+
applyAttribute(renderer, this.host, key, value, this.context);
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
if (containsRenderBinding(next['class'])) {
|
|
3434
|
+
this.updateBinding('class', next['class'], () => className(next['class'], this.context), (value) => {
|
|
3435
|
+
if (value) {
|
|
3436
|
+
renderer.setAttribute(this.host, 'class', String(value));
|
|
3437
|
+
}
|
|
3438
|
+
else {
|
|
3439
|
+
renderer.removeAttribute(this.host, 'class');
|
|
3440
|
+
}
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
else {
|
|
3444
|
+
this.destroyBinding('class');
|
|
3445
|
+
if (!Object.is(this.props['class'], next['class']) ||
|
|
3446
|
+
containsRenderBinding(this.props['class'])) {
|
|
3447
|
+
const value = className(next['class'], this.context);
|
|
3448
|
+
if (value) {
|
|
3449
|
+
renderer.setAttribute(this.host, 'class', value);
|
|
3450
|
+
}
|
|
3451
|
+
else {
|
|
3452
|
+
renderer.removeAttribute(this.host, 'class');
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
if (containsRenderBinding(next['style'])) {
|
|
3457
|
+
this.updateBinding('style', next['style'], () => resolveStyleBindingValue(next['style'], this.context), (value) => {
|
|
3458
|
+
renderer.removeAttribute(this.host, 'style');
|
|
3459
|
+
applyStyles(renderer, this.host, undefined, value, this.context);
|
|
3460
|
+
}, sameStyleValue);
|
|
3461
|
+
}
|
|
3462
|
+
else {
|
|
3463
|
+
this.destroyBinding('style');
|
|
3464
|
+
if (!Object.is(this.props['style'], next['style']) ||
|
|
3465
|
+
containsRenderBinding(this.props['style'])) {
|
|
3466
|
+
if (containsRenderBinding(this.props['style'])) {
|
|
3467
|
+
renderer.removeAttribute(this.host, 'style');
|
|
3468
|
+
}
|
|
3469
|
+
applyStyles(renderer, this.host, containsRenderBinding(this.props['style'])
|
|
3470
|
+
? undefined
|
|
3471
|
+
: this.props['style'], next['style'], this.context);
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
this.props = next;
|
|
3475
|
+
}
|
|
3476
|
+
destroy() {
|
|
3477
|
+
this.bindings.forEach(({ effectRef }) => effectRef.destroy());
|
|
3478
|
+
this.bindings.clear();
|
|
3479
|
+
}
|
|
3480
|
+
updateBinding(key, source, evaluate, apply, equals = Object.is) {
|
|
3481
|
+
const current = this.bindings.get(key);
|
|
3482
|
+
if (current?.source === source)
|
|
3483
|
+
return;
|
|
3484
|
+
current?.effectRef.destroy();
|
|
3485
|
+
let initialized = false;
|
|
3486
|
+
let previous;
|
|
3487
|
+
const effectRef = createRenderEffect(this.context, `host-${key}-binding`, () => {
|
|
3488
|
+
const value = evaluate();
|
|
3489
|
+
if (!initialized || !equals(previous, value)) {
|
|
3490
|
+
apply(value);
|
|
3491
|
+
previous = value;
|
|
3492
|
+
initialized = true;
|
|
3493
|
+
}
|
|
3494
|
+
});
|
|
3495
|
+
this.bindings.set(key, { source, effectRef });
|
|
3496
|
+
}
|
|
3497
|
+
destroyBinding(key) {
|
|
3498
|
+
this.bindings.get(key)?.effectRef.destroy();
|
|
3499
|
+
this.bindings.delete(key);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
export function mountInterpretedComponent(component, host, injector, props) {
|
|
3503
|
+
const craftInjector = ɵcraftInjectorFromHost(injector);
|
|
3504
|
+
const renderer = createBrowserDomAdapter(host.ownerDocument);
|
|
3505
|
+
const rootNode = host.getRootNode();
|
|
3506
|
+
const styleRoot = (typeof Document !== 'undefined' && rootNode instanceof Document) ||
|
|
3507
|
+
(typeof ShadowRoot !== 'undefined' && rootNode instanceof ShadowRoot)
|
|
3508
|
+
? rootNode
|
|
3509
|
+
: (host.ownerDocument ?? document);
|
|
3510
|
+
const styles = craftInjector.get(CRAFT_STYLE_REGISTRY, ɵfallbackCraftStyleRegistry);
|
|
3511
|
+
return mountInterpretedComponentWithOptions(component, host, craftInjector, props, { renderer, styleRoot, styles });
|
|
3512
|
+
}
|
|
3513
|
+
export function mountInterpretedComponentWithOptions(component, host, injector, props, options) {
|
|
3514
|
+
const craftInjector = ɵcraftInjectorFromHost(injector);
|
|
3515
|
+
const identity = options.identity ??
|
|
3516
|
+
createCraftRenderIdentity([component[CRAFT_COMPONENT].name, 0]);
|
|
3517
|
+
let instance;
|
|
3518
|
+
try {
|
|
3519
|
+
instance = new ComponentRenderedNode(component, props, host, null, {
|
|
3520
|
+
renderer: options.renderer,
|
|
3521
|
+
injector: craftInjector,
|
|
3522
|
+
mode: options.mode ?? 'create',
|
|
3523
|
+
identity,
|
|
3524
|
+
hydration: options.hydration,
|
|
3525
|
+
emitHydrationKeys: options.emitHydrationKeys,
|
|
3526
|
+
styleRoot: options.styleRoot,
|
|
3527
|
+
styles: options.styles,
|
|
3528
|
+
ssr: options.ssr,
|
|
3529
|
+
serverStyles: options.serverStyles,
|
|
3530
|
+
}, host);
|
|
3531
|
+
}
|
|
3532
|
+
catch (error) {
|
|
3533
|
+
if (isCraftGenShortCircuit(error)) {
|
|
3534
|
+
throw new CraftUnhandledExceptionError(error.exception);
|
|
3535
|
+
}
|
|
3536
|
+
throw error;
|
|
3537
|
+
}
|
|
3538
|
+
return {
|
|
3539
|
+
updateProps(nextProps) {
|
|
3540
|
+
instance.updateProps(nextProps);
|
|
3541
|
+
},
|
|
3542
|
+
destroy() {
|
|
3543
|
+
instance.destroy();
|
|
3544
|
+
},
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
export function mountInterpretedComponentTemplate(component, host, injector, context, additionalProviders = []) {
|
|
3548
|
+
const craftInjector = ɵcraftInjectorFromHost(injector);
|
|
3549
|
+
const renderer = createBrowserDomAdapter(host.ownerDocument);
|
|
3550
|
+
const rootNode = host.getRootNode();
|
|
3551
|
+
const styleRoot = (typeof Document !== 'undefined' && rootNode instanceof Document) ||
|
|
3552
|
+
(typeof ShadowRoot !== 'undefined' && rootNode instanceof ShadowRoot)
|
|
3553
|
+
? rootNode
|
|
3554
|
+
: (host.ownerDocument ?? document);
|
|
3555
|
+
const styles = craftInjector.get(CRAFT_STYLE_REGISTRY, ɵfallbackCraftStyleRegistry);
|
|
3556
|
+
let instance;
|
|
3557
|
+
try {
|
|
3558
|
+
instance = new ComponentRenderedNode(component, {}, host, null, {
|
|
3559
|
+
renderer,
|
|
3560
|
+
injector: craftInjector,
|
|
3561
|
+
styleRoot,
|
|
3562
|
+
styles,
|
|
3563
|
+
mode: 'create',
|
|
3564
|
+
identity: createCraftRenderIdentity([
|
|
3565
|
+
component[CRAFT_COMPONENT].name,
|
|
3566
|
+
0,
|
|
3567
|
+
]),
|
|
3568
|
+
}, host, { value: context }, additionalProviders);
|
|
3569
|
+
}
|
|
3570
|
+
catch (error) {
|
|
3571
|
+
if (isCraftGenShortCircuit(error)) {
|
|
3572
|
+
throw new CraftUnhandledExceptionError(error.exception);
|
|
3573
|
+
}
|
|
3574
|
+
throw error;
|
|
3575
|
+
}
|
|
3576
|
+
return {
|
|
3577
|
+
updateContext(nextContext) {
|
|
3578
|
+
instance.updateContext(nextContext);
|
|
3579
|
+
},
|
|
3580
|
+
locator(tag, criteria) {
|
|
3581
|
+
return instance.locator(host, tag, criteria);
|
|
3582
|
+
},
|
|
3583
|
+
destroy() {
|
|
3584
|
+
instance.destroy();
|
|
3585
|
+
},
|
|
3586
|
+
};
|
|
3587
|
+
}
|
|
3588
|
+
//# sourceMappingURL=interpreter.js.map
|