foldkit 0.146.0 → 0.148.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/buildToken.d.ts +3 -0
- package/dist/buildToken.d.ts.map +1 -0
- package/dist/buildToken.js +21 -0
- package/dist/controlledDomState.d.ts +25 -0
- package/dist/controlledDomState.d.ts.map +1 -0
- package/dist/controlledDomState.js +240 -0
- package/dist/cssStyleProperties.d.ts +6 -0
- package/dist/cssStyleProperties.d.ts.map +1 -0
- package/dist/cssStyleProperties.js +91 -0
- package/dist/customElement/index.d.ts.map +1 -1
- package/dist/customElement/index.js +23 -0
- package/dist/domReflection.d.ts +73 -0
- package/dist/domReflection.d.ts.map +1 -0
- package/dist/domReflection.js +557 -0
- package/dist/experimental/index.d.ts +1 -0
- package/dist/experimental/index.d.ts.map +1 -1
- package/dist/experimental/index.js +1 -0
- package/dist/experimental/machine/machine.d.ts +22 -3
- package/dist/experimental/machine/machine.d.ts.map +1 -1
- package/dist/experimental/machine/machine.js +8 -0
- package/dist/experimental/server/entry.d.ts +73 -0
- package/dist/experimental/server/entry.d.ts.map +1 -0
- package/dist/experimental/server/entry.js +41 -0
- package/dist/experimental/server/host.d.ts +136 -0
- package/dist/experimental/server/host.d.ts.map +1 -0
- package/dist/experimental/server/host.js +359 -0
- package/dist/experimental/server/index.d.ts +5 -0
- package/dist/experimental/server/index.d.ts.map +1 -0
- package/dist/experimental/server/index.js +4 -0
- package/dist/experimental/server/public.d.ts +3 -0
- package/dist/experimental/server/public.d.ts.map +1 -0
- package/dist/experimental/server/public.js +1 -0
- package/dist/experimental/server/serialize.d.ts +43 -0
- package/dist/experimental/server/serialize.d.ts.map +1 -0
- package/dist/experimental/server/serialize.js +786 -0
- package/dist/experimental/server/server.d.ts +242 -0
- package/dist/experimental/server/server.d.ts.map +1 -0
- package/dist/experimental/server/server.js +946 -0
- package/dist/experimental/server/template.d.ts +53 -0
- package/dist/experimental/server/template.d.ts.map +1 -0
- package/dist/experimental/server/template.js +617 -0
- package/dist/html/index.d.ts +5 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +474 -33
- package/dist/hydrate.d.ts +4 -0
- package/dist/hydrate.d.ts.map +1 -0
- package/dist/hydrate.js +850 -0
- package/dist/hydrationMarker.d.ts +10 -0
- package/dist/hydrationMarker.d.ts.map +1 -0
- package/dist/hydrationMarker.js +9 -0
- package/dist/hydrationMarkers.d.ts +15 -0
- package/dist/hydrationMarkers.d.ts.map +1 -0
- package/dist/hydrationMarkers.js +70 -0
- package/dist/nativeInnerHtml.d.ts +13 -0
- package/dist/nativeInnerHtml.d.ts.map +1 -0
- package/dist/nativeInnerHtml.js +30 -0
- package/dist/propertyProvenance.d.ts +33 -0
- package/dist/propertyProvenance.d.ts.map +1 -0
- package/dist/propertyProvenance.js +78 -0
- package/dist/propsModule.d.ts.map +1 -1
- package/dist/propsModule.js +149 -15
- package/dist/runtime/public.d.ts +2 -2
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/public.js +1 -1
- package/dist/runtime/runtime.d.ts +99 -36
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +412 -71
- package/dist/snabbdom/attributes.d.ts.map +1 -1
- package/dist/snabbdom/attributes.js +65 -37
- package/dist/snabbdom/h.d.ts +1 -0
- package/dist/snabbdom/h.d.ts.map +1 -1
- package/dist/snabbdom/h.js +85 -4
- package/dist/snabbdom/style.d.ts.map +1 -1
- package/dist/snabbdom/style.js +53 -34
- package/dist/snabbdom/tovnode.d.ts.map +1 -1
- package/dist/snabbdom/tovnode.js +5 -1
- package/dist/tagName.d.ts +6 -0
- package/dist/tagName.d.ts.map +1 -0
- package/dist/tagName.js +11 -0
- package/dist/test/apps/attributes.d.ts +1 -0
- package/dist/test/apps/attributes.d.ts.map +1 -1
- package/dist/test/apps/attributes.js +8 -1
- package/dist/test/apps/login.js +1 -1
- package/dist/test/matchers.d.ts.map +1 -1
- package/dist/test/matchers.js +2 -1
- package/dist/test/scene.d.ts.map +1 -1
- package/dist/test/scene.js +2 -1
- package/dist/vdom.d.ts.map +1 -1
- package/dist/vdom.js +25 -1
- package/package.json +9 -2
package/dist/html/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Array, Context, Data, Effect, Fiber, Function, Option, Schema as S, Stream, } from 'effect';
|
|
2
|
+
import { restoreUncontrolledContent, synchronizeControlledDefault, } from '../controlledDomState.js';
|
|
3
|
+
import { FOREIGN_REPRESENTABLE_PROPERTIES, assertStyleIsRepresentable, htmlAttributeValue, isHtmlPropertyRepresentable, normalizedStyleProperties, reflectedAttributeName, } from '../domReflection.js';
|
|
2
4
|
import { MountTracker } from '../mount/index.js';
|
|
5
|
+
import { hasTrustedInnerHtml, isClientOnlyProperty, markClientOnlyProperty, markTrustedInnerHtml, unmarkClientOnlyProperty, } from '../propertyProvenance.js';
|
|
3
6
|
import { VNodeDataMask, h, vnodeDataMaskKey, } from '../snabbdom/index.js';
|
|
7
|
+
import { tagNameFromSelector } from '../tagName.js';
|
|
4
8
|
import { isChildAttribute } from './childAttribute.js';
|
|
5
9
|
import { checkScheduledLeave, clearDragZoneAfterDrop, getDragZoneState, processDragEnter, processDragLeave, } from './dragZoneTracking.js';
|
|
6
10
|
import { clearRuntime, requireBoundaryMappers, requireDispatch, requireRuntimeContext, requireUnmountResolver, setRuntime, } from './runtimeSingleton.js';
|
|
@@ -58,6 +62,16 @@ const keyboardModifiers = (event) => ({
|
|
|
58
62
|
/** Text direction for the document root, applied to `dir` on the `<html>`
|
|
59
63
|
* element. `Auto` defers to the browser's first-strong-character heuristic. */
|
|
60
64
|
export const TextDirection = S.Literals(['Ltr', 'Rtl', 'Auto']);
|
|
65
|
+
const textDirectionAttributes = {
|
|
66
|
+
Ltr: 'ltr',
|
|
67
|
+
Rtl: 'rtl',
|
|
68
|
+
Auto: 'auto',
|
|
69
|
+
};
|
|
70
|
+
/** Maps a {@link TextDirection} to the lowercase value written to the `dir`
|
|
71
|
+
* attribute on the `<html>` element. Shared by the client runtime, which sets
|
|
72
|
+
* it after each render, and server rendering, which stamps it into the served
|
|
73
|
+
* shell so the direction is correct on first paint. */
|
|
74
|
+
export const textDirectionToAttribute = (direction) => textDirectionAttributes[direction];
|
|
61
75
|
const onMountStates = new WeakMap();
|
|
62
76
|
// NOTE: snabbdom `destroy` hooks fire during the patch that removes an element,
|
|
63
77
|
// and the hook on the OLD (being-removed) VNode was built in a prior live
|
|
@@ -100,11 +114,45 @@ const setModuleData = (ctx, key, value, dataMask) => {
|
|
|
100
114
|
// NOTE: single-key fast paths. The bulk of attribute handlers set exactly
|
|
101
115
|
// one prop, attr, or event handler; writing it directly into the vnode data
|
|
102
116
|
// avoids allocating a `{ key: value }` literal and merging it per attribute.
|
|
103
|
-
const
|
|
117
|
+
const writeDataProp = (ctx, key, value) => {
|
|
104
118
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
105
119
|
const props = (ctx.data.props ??= {});
|
|
106
|
-
props
|
|
120
|
+
Object.defineProperty(props, key, {
|
|
121
|
+
configurable: true,
|
|
122
|
+
enumerable: true,
|
|
123
|
+
value,
|
|
124
|
+
writable: true,
|
|
125
|
+
});
|
|
107
126
|
addVNodeDataMask(ctx, VNodeDataMask.Props);
|
|
127
|
+
return props;
|
|
128
|
+
};
|
|
129
|
+
// A typed attribute builder writes the DOM property that reflects the HTML
|
|
130
|
+
// attribute it is named after, so the server serializer can emit it as that
|
|
131
|
+
// attribute even on a custom element, where a same-named component property
|
|
132
|
+
// would mean something else entirely. Overwriting a property a generic write
|
|
133
|
+
// left behind takes the name back, so the mark always describes the value that
|
|
134
|
+
// is actually in the bag. The unmark is a no-op unless a generic write happened
|
|
135
|
+
// on this element, which keeps the common path free of any bookkeeping.
|
|
136
|
+
const setDataProp = (ctx, key, value) => {
|
|
137
|
+
const props = writeDataProp(ctx, key, value);
|
|
138
|
+
unmarkClientOnlyProperty(props, key);
|
|
139
|
+
};
|
|
140
|
+
// `Prop` writes a DOM property and claims nothing about what it means: the
|
|
141
|
+
// value may be a client-only component property, and the name may collide with
|
|
142
|
+
// one an attribute builder owns. `CustomElement.define` property factories
|
|
143
|
+
// produce `Prop`, so a declared component property lands here too.
|
|
144
|
+
const setClientOnlyDataProp = (ctx, key, value) => {
|
|
145
|
+
const props = writeDataProp(ctx, key, value);
|
|
146
|
+
markClientOnlyProperty(props, key);
|
|
147
|
+
};
|
|
148
|
+
// NOTE: `h.InnerHTML` marks the value it wrote as markup the view author intends
|
|
149
|
+
// to be parsed as HTML, which is what lets the server serializer write it to the
|
|
150
|
+
// raw sink. A custom-element property or raw `Prop` named `innerHTML` marks the
|
|
151
|
+
// same key client-only instead. The marks are mutually exclusive, so the last
|
|
152
|
+
// builder owns the key even when both builders wrote exactly the same string.
|
|
153
|
+
const setTrustedInnerHtml = (ctx, value) => {
|
|
154
|
+
const props = writeDataProp(ctx, 'innerHTML', value);
|
|
155
|
+
markTrustedInnerHtml(props, value);
|
|
108
156
|
};
|
|
109
157
|
const setDataAttr = (ctx, key, value) => {
|
|
110
158
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
@@ -166,6 +214,62 @@ const updateDataOn = (ctx, on) => {
|
|
|
166
214
|
}
|
|
167
215
|
}
|
|
168
216
|
};
|
|
217
|
+
// The values a numeric builder accepts, per property.
|
|
218
|
+
//
|
|
219
|
+
// The server writes these as attribute text and a browser parses them; the
|
|
220
|
+
// client assigns them through the DOM property. The two do not agree
|
|
221
|
+
// everywhere. Assigning a negative `maxLength` throws IndexSizeError while the
|
|
222
|
+
// attribute parses to -1; `size = 0` throws while the attribute falls back to
|
|
223
|
+
// 20; `Infinity` and `NaN` become 0 through the property and the attribute's own
|
|
224
|
+
// default through the parser; and past 2^31 the property conversions wrap while
|
|
225
|
+
// the attribute clamps.
|
|
226
|
+
//
|
|
227
|
+
// The bounds below were measured in Chromium: within them, a parsed attribute
|
|
228
|
+
// and a direct assignment produce the same property for every builder here.
|
|
229
|
+
// Outside them the two disagree or the assignment throws, so the value is
|
|
230
|
+
// refused where it is written rather than diverging once served.
|
|
231
|
+
const SIGNED_LONG_MAXIMUM = 2147483647;
|
|
232
|
+
const SIGNED_LONG_MINIMUM = -SIGNED_LONG_MAXIMUM - 1;
|
|
233
|
+
const NUMERIC_PROPERTY_RANGES = {
|
|
234
|
+
cols: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
|
|
235
|
+
colSpan: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
|
|
236
|
+
maxLength: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
|
|
237
|
+
minLength: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
|
|
238
|
+
rows: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
|
|
239
|
+
rowSpan: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
|
|
240
|
+
size: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
|
|
241
|
+
span: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
|
|
242
|
+
// An ordered list may start anywhere, and negatives agree on both sides.
|
|
243
|
+
start: { minimum: SIGNED_LONG_MINIMUM, maximum: SIGNED_LONG_MAXIMUM },
|
|
244
|
+
// Any element can carry one, and the two sides agree across the whole signed
|
|
245
|
+
// long range. Outside it they part company in a way no value hints at: an
|
|
246
|
+
// out-of-range assignment wraps (2147483648 becomes -2147483648) while the
|
|
247
|
+
// attribute falls back to the element's default, which is 0 on a focusable
|
|
248
|
+
// element and -1 on any other, so the same view yields a focusable served
|
|
249
|
+
// element and an unreachable fresh one.
|
|
250
|
+
tabIndex: { minimum: SIGNED_LONG_MINIMUM, maximum: SIGNED_LONG_MAXIMUM },
|
|
251
|
+
};
|
|
252
|
+
const numericPropertyRefusal = (propName, value) => `[foldkit] ${propName} was given ${String(value)}, which a browser reads ` +
|
|
253
|
+
'differently depending on whether it arrives as parsed markup or as a ' +
|
|
254
|
+
'property assignment, and which can throw outright when assigned. ';
|
|
255
|
+
const setNumericDataProp = (ctx, propName, value) => {
|
|
256
|
+
const range = NUMERIC_PROPERTY_RANGES[propName];
|
|
257
|
+
if (range !== undefined &&
|
|
258
|
+
!(Number.isInteger(value) &&
|
|
259
|
+
value >= range.minimum &&
|
|
260
|
+
value <= range.maximum)) {
|
|
261
|
+
throw new Error(numericPropertyRefusal(propName, value) +
|
|
262
|
+
`Use an integer from ${String(range.minimum)} to ` +
|
|
263
|
+
`${String(range.maximum)}.`);
|
|
264
|
+
}
|
|
265
|
+
setDataProp(ctx, propName, value);
|
|
266
|
+
};
|
|
267
|
+
const setFiniteNumericDataProp = (ctx, propName, value) => {
|
|
268
|
+
if (!Number.isFinite(value)) {
|
|
269
|
+
throw new Error(numericPropertyRefusal(propName, value) + 'Use a finite number.');
|
|
270
|
+
}
|
|
271
|
+
setDataProp(ctx, propName, value);
|
|
272
|
+
};
|
|
169
273
|
const updatePropsWithPostpatch = (ctx, propName, value) => {
|
|
170
274
|
setDataProp(ctx, propName, value);
|
|
171
275
|
ctx.getPostpatchProps().push({ propName, value });
|
|
@@ -197,6 +301,30 @@ const classObjectFor = (value) => {
|
|
|
197
301
|
classObjectCache.set(value, classObject);
|
|
198
302
|
return classObject;
|
|
199
303
|
};
|
|
304
|
+
// NOTE: navigation and resource URL attributes (href, src, action,
|
|
305
|
+
// formaction) execute script when their scheme is `javascript:` or
|
|
306
|
+
// `vbscript:`, so an untrusted value bound to them is an XSS sink. Browsers
|
|
307
|
+
// ignore ASCII control characters embedded in a scheme (`java\tscript:`
|
|
308
|
+
// still runs), so those are stripped before the scheme is read. A dangerous
|
|
309
|
+
// scheme neutralizes to an empty value; every other URL, including relative
|
|
310
|
+
// paths, http(s), mailto, tel, and data URLs, passes through unchanged.
|
|
311
|
+
const DANGEROUS_URL_SCHEMES = new Set([
|
|
312
|
+
'javascript',
|
|
313
|
+
'vbscript',
|
|
314
|
+
]);
|
|
315
|
+
const URL_CONTROL_CHARACTERS = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
316
|
+
const URL_SCHEME_PATTERN = /^\s*([a-zA-Z][a-zA-Z0-9+.-]*)\s*:/;
|
|
317
|
+
const sanitizeUrl = (value) => {
|
|
318
|
+
const match = URL_SCHEME_PATTERN.exec(value.replace(URL_CONTROL_CHARACTERS, ''));
|
|
319
|
+
if (match !== null) {
|
|
320
|
+
const scheme = match[1];
|
|
321
|
+
if (scheme !== undefined &&
|
|
322
|
+
DANGEROUS_URL_SCHEMES.has(scheme.toLowerCase())) {
|
|
323
|
+
return '';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return value;
|
|
327
|
+
};
|
|
200
328
|
const attributeHandlers = {
|
|
201
329
|
Key: ({ value }, ctx) => setData(ctx, 'key', value),
|
|
202
330
|
Class: ({ value }, ctx) => setModuleData(ctx, 'class', classObjectFor(value), VNodeDataMask.Class),
|
|
@@ -204,7 +332,7 @@ const attributeHandlers = {
|
|
|
204
332
|
Title: ({ value }, ctx) => setDataProp(ctx, 'title', value),
|
|
205
333
|
Lang: ({ value }, ctx) => setDataProp(ctx, 'lang', value),
|
|
206
334
|
Dir: ({ value }, ctx) => setDataProp(ctx, 'dir', value),
|
|
207
|
-
Tabindex: ({ value }, ctx) =>
|
|
335
|
+
Tabindex: ({ value }, ctx) => setNumericDataProp(ctx, 'tabIndex', value),
|
|
208
336
|
Hidden: ({ value }, ctx) => setDataProp(ctx, 'hidden', value),
|
|
209
337
|
Contenteditable: ({ value }, ctx) => setDataAttr(ctx, 'contenteditable', value),
|
|
210
338
|
Draggable: ({ value }, ctx) => setDataProp(ctx, 'draggable', value),
|
|
@@ -502,36 +630,36 @@ const attributeHandlers = {
|
|
|
502
630
|
Accept: ({ value }, ctx) => setDataProp(ctx, 'accept', value),
|
|
503
631
|
Autocomplete: ({ value }, ctx) => setDataProp(ctx, 'autocomplete', value),
|
|
504
632
|
Pattern: ({ value }, ctx) => setDataProp(ctx, 'pattern', value),
|
|
505
|
-
Maxlength: ({ value }, ctx) =>
|
|
506
|
-
Minlength: ({ value }, ctx) =>
|
|
507
|
-
Size: ({ value }, ctx) =>
|
|
508
|
-
Cols: ({ value }, ctx) =>
|
|
509
|
-
Rows: ({ value }, ctx) =>
|
|
633
|
+
Maxlength: ({ value }, ctx) => setNumericDataProp(ctx, 'maxLength', value),
|
|
634
|
+
Minlength: ({ value }, ctx) => setNumericDataProp(ctx, 'minLength', value),
|
|
635
|
+
Size: ({ value }, ctx) => setNumericDataProp(ctx, 'size', value),
|
|
636
|
+
Cols: ({ value }, ctx) => setNumericDataProp(ctx, 'cols', value),
|
|
637
|
+
Rows: ({ value }, ctx) => setNumericDataProp(ctx, 'rows', value),
|
|
510
638
|
Max: ({ value }, ctx) => setDataProp(ctx, 'max', value),
|
|
511
639
|
Min: ({ value }, ctx) => setDataProp(ctx, 'min', value),
|
|
512
640
|
Step: ({ value }, ctx) => setDataProp(ctx, 'step', value),
|
|
513
641
|
For: ({ value }, ctx) => setDataProp(ctx, 'htmlFor', value),
|
|
514
|
-
Href: ({ value }, ctx) => setDataProp(ctx, 'href', value),
|
|
515
|
-
Src: ({ value }, ctx) => setDataProp(ctx, 'src', value),
|
|
642
|
+
Href: ({ value }, ctx) => setDataProp(ctx, 'href', sanitizeUrl(value)),
|
|
643
|
+
Src: ({ value }, ctx) => setDataProp(ctx, 'src', sanitizeUrl(value)),
|
|
516
644
|
Alt: ({ value }, ctx) => setDataProp(ctx, 'alt', value),
|
|
517
645
|
Target: ({ value }, ctx) => setDataProp(ctx, 'target', value),
|
|
518
646
|
Rel: ({ value }, ctx) => setDataProp(ctx, 'rel', value),
|
|
519
647
|
Download: ({ value }, ctx) => setDataProp(ctx, 'download', value),
|
|
520
|
-
Action: ({ value }, ctx) => setDataProp(ctx, 'action', value),
|
|
648
|
+
Action: ({ value }, ctx) => setDataProp(ctx, 'action', sanitizeUrl(value)),
|
|
521
649
|
Method: ({ value }, ctx) => setDataProp(ctx, 'method', value),
|
|
522
650
|
Enctype: ({ value }, ctx) => setDataProp(ctx, 'enctype', value),
|
|
523
651
|
Novalidate: ({ value }, ctx) => setDataProp(ctx, 'noValidate', value),
|
|
524
|
-
Formaction: ({ value }, ctx) => setDataProp(ctx, 'formAction', value),
|
|
652
|
+
Formaction: ({ value }, ctx) => setDataProp(ctx, 'formAction', sanitizeUrl(value)),
|
|
525
653
|
Formmethod: ({ value }, ctx) => setDataProp(ctx, 'formMethod', value),
|
|
526
654
|
Formnovalidate: ({ value }, ctx) => setDataProp(ctx, 'formNoValidate', value),
|
|
527
655
|
Formtarget: ({ value }, ctx) => setDataProp(ctx, 'formTarget', value),
|
|
528
656
|
Formenctype: ({ value }, ctx) => setDataProp(ctx, 'formEnctype', value),
|
|
529
|
-
Colspan: ({ value }, ctx) =>
|
|
530
|
-
Rowspan: ({ value }, ctx) =>
|
|
657
|
+
Colspan: ({ value }, ctx) => setNumericDataProp(ctx, 'colSpan', value),
|
|
658
|
+
Rowspan: ({ value }, ctx) => setNumericDataProp(ctx, 'rowSpan', value),
|
|
531
659
|
Scope: ({ value }, ctx) => setDataAttr(ctx, 'scope', value),
|
|
532
660
|
Headers: ({ value }, ctx) => setDataAttr(ctx, 'headers', value),
|
|
533
|
-
Span: ({ value }, ctx) =>
|
|
534
|
-
Start: ({ value }, ctx) =>
|
|
661
|
+
Span: ({ value }, ctx) => setNumericDataProp(ctx, 'span', value),
|
|
662
|
+
Start: ({ value }, ctx) => setNumericDataProp(ctx, 'start', value),
|
|
535
663
|
Reversed: ({ value }, ctx) => setDataProp(ctx, 'reversed', value),
|
|
536
664
|
CiteAttr: ({ value }, ctx) => setDataProp(ctx, 'cite', value),
|
|
537
665
|
Datetime: ({ value }, ctx) => setDataProp(ctx, 'dateTime', value),
|
|
@@ -562,9 +690,9 @@ const attributeHandlers = {
|
|
|
562
690
|
Poster: ({ value }, ctx) => setDataProp(ctx, 'poster', value),
|
|
563
691
|
Preload: ({ value }, ctx) => setDataProp(ctx, 'preload', value),
|
|
564
692
|
Playsinline: ({ value }, ctx) => setDataProp(ctx, 'playsInline', value),
|
|
565
|
-
High: ({ value }, ctx) =>
|
|
566
|
-
Low: ({ value }, ctx) =>
|
|
567
|
-
Optimum: ({ value }, ctx) =>
|
|
693
|
+
High: ({ value }, ctx) => setFiniteNumericDataProp(ctx, 'high', value),
|
|
694
|
+
Low: ({ value }, ctx) => setFiniteNumericDataProp(ctx, 'low', value),
|
|
695
|
+
Optimum: ({ value }, ctx) => setFiniteNumericDataProp(ctx, 'optimum', value),
|
|
568
696
|
Usemap: ({ value }, ctx) => setDataAttr(ctx, 'usemap', value),
|
|
569
697
|
Ismap: ({ value }, ctx) => setDataProp(ctx, 'isMap', value),
|
|
570
698
|
Role: ({ value }, ctx) => setDataAttr(ctx, 'role', value),
|
|
@@ -616,8 +744,8 @@ const attributeHandlers = {
|
|
|
616
744
|
AriaValuetext: ({ value }, ctx) => setDataAttr(ctx, 'aria-valuetext', value),
|
|
617
745
|
Attribute: ({ key, value }, ctx) => setDataAttr(ctx, key, value),
|
|
618
746
|
DataAttribute: ({ key, value }, ctx) => setDataAttr(ctx, `data-${key}`, value),
|
|
619
|
-
Style: ({ value }, ctx) => setModuleData(ctx, 'style', value, VNodeDataMask.Style),
|
|
620
|
-
InnerHTML: ({ value }, ctx) =>
|
|
747
|
+
Style: ({ value }, ctx) => setModuleData(ctx, 'style', normalizedStyleProperties(value), VNodeDataMask.Style),
|
|
748
|
+
InnerHTML: ({ value }, ctx) => setTrustedInnerHtml(ctx, value),
|
|
621
749
|
ViewBox: ({ value }, ctx) => setDataAttr(ctx, 'viewBox', value),
|
|
622
750
|
Xmlns: ({ value }, ctx) => setDataAttr(ctx, 'xmlns', value),
|
|
623
751
|
Fill: ({ value }, ctx) => setDataAttr(ctx, 'fill', value),
|
|
@@ -708,7 +836,7 @@ const attributeHandlers = {
|
|
|
708
836
|
RefY: ({ value }, ctx) => setDataAttr(ctx, 'refY', value),
|
|
709
837
|
Orient: ({ value }, ctx) => setDataAttr(ctx, 'orient', value),
|
|
710
838
|
PreserveAspectRatio: ({ value }, ctx) => setDataAttr(ctx, 'preserveAspectRatio', value),
|
|
711
|
-
Prop: ({ key, value }, ctx) =>
|
|
839
|
+
Prop: ({ key, value }, ctx) => setClientOnlyDataProp(ctx, key, value),
|
|
712
840
|
OnCustomEvent: ({ name, f: toMessage }, ctx) => updateDataOn(ctx, {
|
|
713
841
|
[name]: (event) => {
|
|
714
842
|
if (event instanceof CustomEvent) {
|
|
@@ -834,19 +962,58 @@ const capturedContextOrEmpty = () => {
|
|
|
834
962
|
return Context.empty();
|
|
835
963
|
}
|
|
836
964
|
};
|
|
965
|
+
// NOTE: reasserted on `insert` as well as on `postpatch`. The props module sets
|
|
966
|
+
// a controlled property while the element is being created, before its children
|
|
967
|
+
// exist, and a `<select>`'s `value` setter has nothing to match against until
|
|
968
|
+
// its `<option>`s are there, so a fresh render left the select on the browser's
|
|
969
|
+
// own default until some later patch corrected it. The server instead marks the
|
|
970
|
+
// matching option, so the served page was right and the fresh one was not.
|
|
971
|
+
// `insert` fires once the subtree is built, which is where the two agree.
|
|
972
|
+
const applyControlledProps = (vnode, controlledProps) => {
|
|
973
|
+
if (!vnode.elm) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
Array.forEach(controlledProps, ({ propName, value }) => {
|
|
977
|
+
const isOutputValue = vnode.elm instanceof Element &&
|
|
978
|
+
vnode.elm.localName === 'output' &&
|
|
979
|
+
propName === 'value';
|
|
980
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
981
|
+
if (isOutputValue || vnode.elm[propName] !== value) {
|
|
982
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
983
|
+
;
|
|
984
|
+
vnode.elm[propName] = value;
|
|
985
|
+
}
|
|
986
|
+
if (vnode.elm instanceof Element) {
|
|
987
|
+
synchronizeControlledDefault(vnode.elm, propName, value);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
};
|
|
837
991
|
const attachPostpatchHook = (data, postpatchProps) => {
|
|
992
|
+
const existingInsert = data.hook?.insert;
|
|
838
993
|
data.hook = {
|
|
839
994
|
...data.hook,
|
|
995
|
+
insert: vnode => {
|
|
996
|
+
applyControlledProps(vnode, postpatchProps);
|
|
997
|
+
existingInsert?.(vnode);
|
|
998
|
+
},
|
|
840
999
|
postpatch: (_oldVnode, vnode) => {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1000
|
+
applyControlledProps(vnode, postpatchProps);
|
|
1001
|
+
},
|
|
1002
|
+
};
|
|
1003
|
+
};
|
|
1004
|
+
const attachControlledContentOwnershipHook = (data) => {
|
|
1005
|
+
const existingPostpatch = data.hook?.postpatch;
|
|
1006
|
+
data.hook = {
|
|
1007
|
+
...data.hook,
|
|
1008
|
+
postpatch: (oldVnode, vnode) => {
|
|
1009
|
+
existingPostpatch?.(oldVnode, vnode);
|
|
1010
|
+
const oldProperties = oldVnode.data?.props;
|
|
1011
|
+
const properties = vnode.data?.props;
|
|
1012
|
+
if (oldProperties !== undefined &&
|
|
1013
|
+
Object.hasOwn(oldProperties, 'value') &&
|
|
1014
|
+
(properties === undefined || !Object.hasOwn(properties, 'value')) &&
|
|
1015
|
+
vnode.elm instanceof Element) {
|
|
1016
|
+
restoreUncontrolledContent(vnode.elm, vnode);
|
|
850
1017
|
}
|
|
851
1018
|
},
|
|
852
1019
|
};
|
|
@@ -922,14 +1089,288 @@ const copyChildrenDroppingEmpty = (children) => {
|
|
|
922
1089
|
}
|
|
923
1090
|
return next;
|
|
924
1091
|
};
|
|
925
|
-
|
|
1092
|
+
// Elements whose content a controlled `value` owns. Giving one both a value and
|
|
1093
|
+
// trusted raw HTML asks two mechanisms to own the same text.
|
|
1094
|
+
const CONTROLLED_CONTENT_ELEMENTS = new Set([
|
|
1095
|
+
'output',
|
|
1096
|
+
'select',
|
|
1097
|
+
'textarea',
|
|
1098
|
+
]);
|
|
1099
|
+
// Elements that carry no content at all, so raw HTML written into one cannot be
|
|
1100
|
+
// represented in served markup. Assigning the property in a browser can still
|
|
1101
|
+
// build child nodes, which is a difference no server render can reproduce.
|
|
1102
|
+
const VOID_CONTENT_ELEMENTS = new Set([
|
|
1103
|
+
'area',
|
|
1104
|
+
'base',
|
|
1105
|
+
'br',
|
|
1106
|
+
'col',
|
|
1107
|
+
'embed',
|
|
1108
|
+
'hr',
|
|
1109
|
+
'img',
|
|
1110
|
+
'input',
|
|
1111
|
+
'link',
|
|
1112
|
+
'meta',
|
|
1113
|
+
'param',
|
|
1114
|
+
'source',
|
|
1115
|
+
'track',
|
|
1116
|
+
'wbr',
|
|
1117
|
+
]);
|
|
1118
|
+
// One owner per element's content. Both `h.InnerHTML` and a client-only
|
|
1119
|
+
// `innerHTML` property hand the element an opaque subtree. A view that also
|
|
1120
|
+
// declares children or a controlled value gives the differ child vnodes for
|
|
1121
|
+
// nodes the property replaces, leaving those vnodes detached and making a later
|
|
1122
|
+
// patch unable to restore the declared children. Trusted `h.InnerHTML` also
|
|
1123
|
+
// disagrees with the server serializer in these combinations. Rejecting at the
|
|
1124
|
+
// builder makes the server render, a fresh client render, and hydration refuse
|
|
1125
|
+
// the same self-contradictory view.
|
|
1126
|
+
const assertSingleContentOwner = (tagName, data, children) => {
|
|
1127
|
+
const lowerTagName = tagName.toLowerCase();
|
|
1128
|
+
if ((lowerTagName === 'textarea' || lowerTagName === 'output') &&
|
|
1129
|
+
data.props?.['value'] !== undefined &&
|
|
1130
|
+
children.length > 0) {
|
|
1131
|
+
throw new Error(`[foldkit] <${lowerTagName}> was given both a controlled value and ` +
|
|
1132
|
+
'children. Both own the element content, so a property assignment ' +
|
|
1133
|
+
'replaces the child nodes the differ expects to patch. Keep one owner.');
|
|
1134
|
+
}
|
|
1135
|
+
const properties = data.props;
|
|
1136
|
+
if (properties === undefined || !Object.hasOwn(properties, 'innerHTML')) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const innerHtmlOwner = hasTrustedInnerHtml(properties)
|
|
1140
|
+
? 'h.InnerHTML'
|
|
1141
|
+
: 'a client-only innerHTML property';
|
|
1142
|
+
if (children.length > 0) {
|
|
1143
|
+
throw new Error(`[foldkit] <${lowerTagName}> was given both ${innerHtmlOwner} and children. ` +
|
|
1144
|
+
'innerHTML owns the whole of an element\u2019s content, so the two ' +
|
|
1145
|
+
'cannot both describe it: server rendering emits the raw HTML alone ' +
|
|
1146
|
+
'for h.InnerHTML, while a browser property assignment replaces the ' +
|
|
1147
|
+
'nodes the differ expects to patch. Keep one content owner.');
|
|
1148
|
+
}
|
|
1149
|
+
if (CONTROLLED_CONTENT_ELEMENTS.has(lowerTagName) &&
|
|
1150
|
+
data.props?.['value'] !== undefined) {
|
|
1151
|
+
throw new Error(`[foldkit] <${lowerTagName}> was given both ${innerHtmlOwner} and a ` +
|
|
1152
|
+
'controlled value. Both own this element\u2019s content, and they ' +
|
|
1153
|
+
'disagree once the client reasserts the value. Keep one of them.');
|
|
1154
|
+
}
|
|
1155
|
+
if (VOID_CONTENT_ELEMENTS.has(lowerTagName)) {
|
|
1156
|
+
throw new Error(`[foldkit] <${lowerTagName}> cannot hold content, so ${innerHtmlOwner} ` +
|
|
1157
|
+
'on it ' +
|
|
1158
|
+
'has no representation in served HTML. A browser can still build ' +
|
|
1159
|
+
'child nodes from the property, which no server render reproduces. ' +
|
|
1160
|
+
'Remove the innerHTML value.');
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
const assertSingleStyleOwner = (data) => {
|
|
1164
|
+
if (data.style === undefined ||
|
|
1165
|
+
htmlAttributeValue(data.attrs, 'style') === undefined) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
throw new Error('[foldkit] An element was given both h.Style and a raw ' +
|
|
1169
|
+
"h.Attribute('style', ...). They are two owners of one declaration " +
|
|
1170
|
+
'block, and CSS parsing can make one swallow or override the other. ' +
|
|
1171
|
+
'Keep all inline declarations in one of them.');
|
|
1172
|
+
};
|
|
1173
|
+
// The state an element would be in once it exists, read from the tag, the raw
|
|
1174
|
+
// attributes, and the typed properties together. A builder on its own cannot
|
|
1175
|
+
// decide any of this: `h.Value` is a string on an input and a number on a
|
|
1176
|
+
// meter, `type="file"` refuses the value an input takes everywhere else, and a
|
|
1177
|
+
// raw attribute means nothing until something says which property claims the
|
|
1178
|
+
// same name.
|
|
1179
|
+
//
|
|
1180
|
+
// Each rule below marks a view whose server render and fresh client render
|
|
1181
|
+
// cannot agree. Refusing it where it is written is what makes the two, plus a
|
|
1182
|
+
// hydration of the first by the second, fail the same way rather than diverge
|
|
1183
|
+
// quietly.
|
|
1184
|
+
// A number both a parser and an assignment read identically: ASCII digits, one
|
|
1185
|
+
// optional leading minus, an optional fraction, an optional exponent. Anything
|
|
1186
|
+
// else parts the two: `0x10` is 16 to `Number` and invalid to the parser, a
|
|
1187
|
+
// leading `+` or surrounding whitespace is accepted by one and not the other,
|
|
1188
|
+
// and `Infinity` throws on assignment while the attribute falls back to the
|
|
1189
|
+
// element's default.
|
|
1190
|
+
const DECIMAL_NUMBER = /^-?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/;
|
|
1191
|
+
const DECIMAL_INTEGER = /^-?[0-9]+$/;
|
|
1192
|
+
// Properties the DOM defines as numbers on these elements and as strings
|
|
1193
|
+
// elsewhere, so the same builder means different things depending on where it
|
|
1194
|
+
// is written.
|
|
1195
|
+
const DOUBLE_IDL_PROPERTIES = {
|
|
1196
|
+
meter: new Set(['high', 'low', 'max', 'min', 'optimum', 'value']),
|
|
1197
|
+
progress: new Set(['max', 'value']),
|
|
1198
|
+
};
|
|
1199
|
+
const LONG_IDL_PROPERTIES = {
|
|
1200
|
+
li: new Set(['value']),
|
|
1201
|
+
};
|
|
1202
|
+
const assertDoubleIdlValue = (tagName, propName, value) => {
|
|
1203
|
+
const isRepresentable = typeof value === 'number'
|
|
1204
|
+
? Number.isFinite(value)
|
|
1205
|
+
: typeof value === 'string' &&
|
|
1206
|
+
DECIMAL_NUMBER.test(value) &&
|
|
1207
|
+
Number.isFinite(Number(value));
|
|
1208
|
+
if (!isRepresentable) {
|
|
1209
|
+
throw new Error(`[foldkit] <${tagName}> reads ${propName} as a number, and ` +
|
|
1210
|
+
`${JSON.stringify(value)} is not one a browser reads the same way from ` +
|
|
1211
|
+
'markup and from a property assignment. Use a finite decimal number.');
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
const assertLongIdlValue = (tagName, propName, value) => {
|
|
1215
|
+
const parsed = typeof value === 'number'
|
|
1216
|
+
? value
|
|
1217
|
+
: typeof value === 'string' && DECIMAL_INTEGER.test(value)
|
|
1218
|
+
? Number(value)
|
|
1219
|
+
: Number.NaN;
|
|
1220
|
+
if (!Number.isInteger(parsed) ||
|
|
1221
|
+
parsed < SIGNED_LONG_MINIMUM ||
|
|
1222
|
+
parsed > SIGNED_LONG_MAXIMUM) {
|
|
1223
|
+
throw new Error(`[foldkit] <${tagName}> reads ${propName} as an integer, and ` +
|
|
1224
|
+
`${JSON.stringify(value)} is not one a browser reads the same way from ` +
|
|
1225
|
+
'markup and from a property assignment. Use an integer from ' +
|
|
1226
|
+
`${String(SIGNED_LONG_MINIMUM)} to ${String(SIGNED_LONG_MAXIMUM)}.`);
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
const effectiveInputType = (data) => {
|
|
1230
|
+
const typed = data.props?.['type'];
|
|
1231
|
+
if (typeof typed === 'string') {
|
|
1232
|
+
return typed.toLowerCase();
|
|
1233
|
+
}
|
|
1234
|
+
const raw = htmlAttributeValue(data.attrs, 'type');
|
|
1235
|
+
return typeof raw === 'string' ? raw.toLowerCase() : undefined;
|
|
1236
|
+
};
|
|
1237
|
+
const assertElementStateIsRepresentable = (tagName, data) => {
|
|
1238
|
+
const props = data.props;
|
|
1239
|
+
if (props === undefined) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const lowerTagName = tagName.toLowerCase();
|
|
1243
|
+
const doubleIdl = DOUBLE_IDL_PROPERTIES[lowerTagName];
|
|
1244
|
+
const longIdl = LONG_IDL_PROPERTIES[lowerTagName];
|
|
1245
|
+
for (const propName of Object.keys(props)) {
|
|
1246
|
+
const value = props[propName];
|
|
1247
|
+
if (value === undefined || propName === 'innerHTML') {
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (doubleIdl?.has(propName) === true) {
|
|
1251
|
+
assertDoubleIdlValue(lowerTagName, propName, value);
|
|
1252
|
+
}
|
|
1253
|
+
if (longIdl?.has(propName) === true) {
|
|
1254
|
+
assertLongIdlValue(lowerTagName, propName, value);
|
|
1255
|
+
}
|
|
1256
|
+
if (lowerTagName === 'input' && propName === 'size' && value === 0) {
|
|
1257
|
+
throw new Error(numericPropertyRefusal(propName, value) +
|
|
1258
|
+
`Use an integer from 1 to ${String(SIGNED_LONG_MAXIMUM)} on an input.`);
|
|
1259
|
+
}
|
|
1260
|
+
// A raw attribute and a typed builder that name the same attribute are two
|
|
1261
|
+
// owners of one piece of state, and their served form has no source
|
|
1262
|
+
// spelling. `h.Attribute('checked', '')` with `h.Checked(false)` serves no
|
|
1263
|
+
// attribute at all, so the served element has `defaultChecked` false while
|
|
1264
|
+
// a fresh render parses the attribute and has it true: `form.reset()`,
|
|
1265
|
+
// `:default`, and an attribute selector then read the two pages
|
|
1266
|
+
// differently. A client-only property carries no such claim, so a
|
|
1267
|
+
// `CustomElement.define` property never conflicts with an attribute.
|
|
1268
|
+
const attributeName = reflectedAttributeName(propName);
|
|
1269
|
+
if (attributeName !== undefined &&
|
|
1270
|
+
!isClientOnlyProperty(props, propName) &&
|
|
1271
|
+
htmlAttributeValue(data.attrs, attributeName) !== undefined) {
|
|
1272
|
+
throw new Error(`[foldkit] <${lowerTagName}> was given both a typed ${propName} and a ` +
|
|
1273
|
+
`raw h.Attribute('${attributeName}', ...). They are two owners of ` +
|
|
1274
|
+
'one attribute, and the state they describe together has no spelling ' +
|
|
1275
|
+
'in served HTML: the property decides what is served while a fresh ' +
|
|
1276
|
+
'render parses the attribute as the default too. Keep one of them.');
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (lowerTagName === 'input' && effectiveInputType(data) === 'file') {
|
|
1280
|
+
const value = props['value'];
|
|
1281
|
+
if (typeof value === 'string' && value !== '') {
|
|
1282
|
+
throw new Error('[foldkit] <input type="file"> was given a value. A file input takes ' +
|
|
1283
|
+
'no value from markup or from the Model: the served attribute is ' +
|
|
1284
|
+
'ignored and assigning the property throws InvalidStateError, so the ' +
|
|
1285
|
+
'view crashes on a fresh render and on hydration. Drop the value.');
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
// NOTE: A typed property on an HTML element whose native interface does not own it
|
|
1290
|
+
// has always been a client-side expando. For example, RadioGroup deliberately
|
|
1291
|
+
// includes `h.Type('button')` in an attribute bundle that consumers may spread
|
|
1292
|
+
// onto a button, div, or span. Mark the wrong-element case as client-only so
|
|
1293
|
+
// server rendering omits it and hydration applies the same expando a fresh
|
|
1294
|
+
// render does, without turning a pre-existing client pattern into live markup.
|
|
1295
|
+
const markUnreflectedHtmlPropertiesClientOnly = (tagName, data) => {
|
|
1296
|
+
const props = data.props;
|
|
1297
|
+
if (props === undefined) {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
for (const propName of Object.keys(props)) {
|
|
1301
|
+
if (reflectedAttributeName(propName) !== undefined &&
|
|
1302
|
+
!isHtmlPropertyRepresentable(tagName, propName)) {
|
|
1303
|
+
markClientOnlyProperty(props, propName);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
// The same question in SVG and MathML, where the answer is decided by the
|
|
1308
|
+
// namespace rather than by the tag. A foreign element has none of the HTML
|
|
1309
|
+
// interface members a typed builder writes, apart from the three measured to
|
|
1310
|
+
// reflect there. Assigning `href` on an SVG `<a>` throws, since bundled code is
|
|
1311
|
+
// strict and `SVGAElement.href` is readonly; assigning `title` sets an expando
|
|
1312
|
+
// no attribute ever sees. The serializer would have written an attribute for
|
|
1313
|
+
// both. Raw `h.Attribute` is the mechanism SVG and MathML use, and it behaves
|
|
1314
|
+
// identically on both sides.
|
|
1315
|
+
//
|
|
1316
|
+
// The namespace is only known once the `svg` or `math` element that introduces
|
|
1317
|
+
// it is built, because that is when it propagates down. The walk stops where
|
|
1318
|
+
// the namespace stops, so an HTML integration point such as `<foreignObject>`
|
|
1319
|
+
// keeps ordinary HTML rules for its content.
|
|
1320
|
+
const assertForeignPropertiesAreRepresentable = (node) => {
|
|
1321
|
+
const data = node.data;
|
|
1322
|
+
if (data === undefined || data.ns === undefined) {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
const props = data.props;
|
|
1326
|
+
if (props !== undefined) {
|
|
1327
|
+
for (const propName of Object.keys(props)) {
|
|
1328
|
+
if (props[propName] === undefined ||
|
|
1329
|
+
propName === 'innerHTML' ||
|
|
1330
|
+
FOREIGN_REPRESENTABLE_PROPERTIES.has(propName) ||
|
|
1331
|
+
reflectedAttributeName(propName) === undefined) {
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
throw new Error(`[foldkit] <${tagNameFromSelector(node.sel ?? '')}> is in the SVG or ` +
|
|
1335
|
+
`MathML namespace, where ${propName} is not a property the element ` +
|
|
1336
|
+
'has. Assigning it on the client sets a value no attribute reflects, ' +
|
|
1337
|
+
'or throws outright, while server rendering writes the attribute, so ' +
|
|
1338
|
+
`the two disagree. Write it as h.Attribute('${propName}', ...), ` +
|
|
1339
|
+
'which foreign content reads the same way on both sides.');
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
for (const child of node.children ?? []) {
|
|
1343
|
+
if (typeof child !== 'string') {
|
|
1344
|
+
assertForeignPropertiesAreRepresentable(child);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
const buildElement = (tagName, data, children) => {
|
|
1349
|
+
const copiedChildren = copyChildrenDroppingEmpty(children);
|
|
1350
|
+
markUnreflectedHtmlPropertiesClientOnly(tagName, data);
|
|
1351
|
+
assertSingleContentOwner(tagName, data, copiedChildren);
|
|
1352
|
+
assertSingleStyleOwner(data);
|
|
1353
|
+
if (data.style !== undefined) {
|
|
1354
|
+
assertStyleIsRepresentable(data.style);
|
|
1355
|
+
}
|
|
1356
|
+
assertElementStateIsRepresentable(tagName, data);
|
|
1357
|
+
if (tagName.toLowerCase() === 'select' ||
|
|
1358
|
+
tagName.toLowerCase() === 'textarea' ||
|
|
1359
|
+
tagName.toLowerCase() === 'output') {
|
|
1360
|
+
attachControlledContentOwnershipHook(data);
|
|
1361
|
+
}
|
|
1362
|
+
const built = h(tagName, data, copiedChildren);
|
|
1363
|
+
assertForeignPropertiesAreRepresentable(built);
|
|
1364
|
+
return built;
|
|
1365
|
+
};
|
|
1366
|
+
const createElement = (tagName, attributes = [], children = []) => buildElement(tagName, buildVNodeData(attributes), children);
|
|
926
1367
|
const element = () => (tagName) => (attributes = [], children = []) => createElement(tagName, attributes, children);
|
|
927
1368
|
export const customElement = () => (tagName) => (attributes = [], children = []) => createElement(tagName, attributes, children);
|
|
928
1369
|
const voidElement = () => (tagName) => (attributes = []) => createElement(tagName, attributes, []);
|
|
929
1370
|
const keyed = () => (tagName) => (key, attributes = [], children = []) => {
|
|
930
1371
|
const data = buildVNodeData(attributes);
|
|
931
1372
|
data.key = key;
|
|
932
|
-
return
|
|
1373
|
+
return buildElement(tagName, data, children);
|
|
933
1374
|
};
|
|
934
1375
|
const htmlElements = () => {
|
|
935
1376
|
const el = element();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { VNode } from './snabbdom/index.js';
|
|
2
|
+
export declare const __elementSignature: (element: Element, vnode: VNode) => string;
|
|
3
|
+
export declare const __hydrateVNode: (hydrationRoot: Element, nextVNode: VNode | null, seen: Set<object> | undefined, buildId: string) => VNode;
|
|
4
|
+
//# sourceMappingURL=hydrate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAwPhD,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,EAAE,OAAO,KAAK,KAAG,MA4BnE,CAAA;AAmsBD,eAAO,MAAM,cAAc,GACzB,eAAe,OAAO,EACtB,WAAW,KAAK,GAAG,IAAI,EACvB,MAAM,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,EAC7B,SAAS,MAAM,KACd,KAwDF,CAAA"}
|