foldkit 0.147.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/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/domReflection.d.ts +73 -0
- package/dist/domReflection.d.ts.map +1 -0
- package/dist/domReflection.js +557 -0
- package/dist/experimental/server/host.d.ts +102 -8
- package/dist/experimental/server/host.d.ts.map +1 -1
- package/dist/experimental/server/host.js +203 -13
- package/dist/experimental/server/public.d.ts +2 -2
- package/dist/experimental/server/public.d.ts.map +1 -1
- package/dist/experimental/server/public.js +1 -1
- package/dist/experimental/server/serialize.d.ts +15 -5
- package/dist/experimental/server/serialize.d.ts.map +1 -1
- package/dist/experimental/server/serialize.js +367 -144
- package/dist/experimental/server/server.d.ts +55 -14
- package/dist/experimental/server/server.d.ts.map +1 -1
- package/dist/experimental/server/server.js +544 -21
- package/dist/experimental/server/template.d.ts +8 -0
- package/dist/experimental/server/template.d.ts.map +1 -1
- package/dist/experimental/server/template.js +437 -2
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +436 -29
- package/dist/hydrate.d.ts +1 -1
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +449 -121
- 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 +1 -1
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/runtime.d.ts +30 -6
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +230 -27
- package/dist/snabbdom/attributes.d.ts.map +1 -1
- package/dist/snabbdom/attributes.js +65 -37
- package/dist/snabbdom/style.d.ts.map +1 -1
- package/dist/snabbdom/style.js +53 -34
- 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/package.json +2 -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';
|
|
@@ -110,11 +114,45 @@ const setModuleData = (ctx, key, value, dataMask) => {
|
|
|
110
114
|
// NOTE: single-key fast paths. The bulk of attribute handlers set exactly
|
|
111
115
|
// one prop, attr, or event handler; writing it directly into the vnode data
|
|
112
116
|
// avoids allocating a `{ key: value }` literal and merging it per attribute.
|
|
113
|
-
const
|
|
117
|
+
const writeDataProp = (ctx, key, value) => {
|
|
114
118
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
115
119
|
const props = (ctx.data.props ??= {});
|
|
116
|
-
props
|
|
120
|
+
Object.defineProperty(props, key, {
|
|
121
|
+
configurable: true,
|
|
122
|
+
enumerable: true,
|
|
123
|
+
value,
|
|
124
|
+
writable: true,
|
|
125
|
+
});
|
|
117
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);
|
|
118
156
|
};
|
|
119
157
|
const setDataAttr = (ctx, key, value) => {
|
|
120
158
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
@@ -176,6 +214,62 @@ const updateDataOn = (ctx, on) => {
|
|
|
176
214
|
}
|
|
177
215
|
}
|
|
178
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
|
+
};
|
|
179
273
|
const updatePropsWithPostpatch = (ctx, propName, value) => {
|
|
180
274
|
setDataProp(ctx, propName, value);
|
|
181
275
|
ctx.getPostpatchProps().push({ propName, value });
|
|
@@ -238,7 +332,7 @@ const attributeHandlers = {
|
|
|
238
332
|
Title: ({ value }, ctx) => setDataProp(ctx, 'title', value),
|
|
239
333
|
Lang: ({ value }, ctx) => setDataProp(ctx, 'lang', value),
|
|
240
334
|
Dir: ({ value }, ctx) => setDataProp(ctx, 'dir', value),
|
|
241
|
-
Tabindex: ({ value }, ctx) =>
|
|
335
|
+
Tabindex: ({ value }, ctx) => setNumericDataProp(ctx, 'tabIndex', value),
|
|
242
336
|
Hidden: ({ value }, ctx) => setDataProp(ctx, 'hidden', value),
|
|
243
337
|
Contenteditable: ({ value }, ctx) => setDataAttr(ctx, 'contenteditable', value),
|
|
244
338
|
Draggable: ({ value }, ctx) => setDataProp(ctx, 'draggable', value),
|
|
@@ -536,11 +630,11 @@ const attributeHandlers = {
|
|
|
536
630
|
Accept: ({ value }, ctx) => setDataProp(ctx, 'accept', value),
|
|
537
631
|
Autocomplete: ({ value }, ctx) => setDataProp(ctx, 'autocomplete', value),
|
|
538
632
|
Pattern: ({ value }, ctx) => setDataProp(ctx, 'pattern', value),
|
|
539
|
-
Maxlength: ({ value }, ctx) =>
|
|
540
|
-
Minlength: ({ value }, ctx) =>
|
|
541
|
-
Size: ({ value }, ctx) =>
|
|
542
|
-
Cols: ({ value }, ctx) =>
|
|
543
|
-
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),
|
|
544
638
|
Max: ({ value }, ctx) => setDataProp(ctx, 'max', value),
|
|
545
639
|
Min: ({ value }, ctx) => setDataProp(ctx, 'min', value),
|
|
546
640
|
Step: ({ value }, ctx) => setDataProp(ctx, 'step', value),
|
|
@@ -560,12 +654,12 @@ const attributeHandlers = {
|
|
|
560
654
|
Formnovalidate: ({ value }, ctx) => setDataProp(ctx, 'formNoValidate', value),
|
|
561
655
|
Formtarget: ({ value }, ctx) => setDataProp(ctx, 'formTarget', value),
|
|
562
656
|
Formenctype: ({ value }, ctx) => setDataProp(ctx, 'formEnctype', value),
|
|
563
|
-
Colspan: ({ value }, ctx) =>
|
|
564
|
-
Rowspan: ({ value }, ctx) =>
|
|
657
|
+
Colspan: ({ value }, ctx) => setNumericDataProp(ctx, 'colSpan', value),
|
|
658
|
+
Rowspan: ({ value }, ctx) => setNumericDataProp(ctx, 'rowSpan', value),
|
|
565
659
|
Scope: ({ value }, ctx) => setDataAttr(ctx, 'scope', value),
|
|
566
660
|
Headers: ({ value }, ctx) => setDataAttr(ctx, 'headers', value),
|
|
567
|
-
Span: ({ value }, ctx) =>
|
|
568
|
-
Start: ({ value }, ctx) =>
|
|
661
|
+
Span: ({ value }, ctx) => setNumericDataProp(ctx, 'span', value),
|
|
662
|
+
Start: ({ value }, ctx) => setNumericDataProp(ctx, 'start', value),
|
|
569
663
|
Reversed: ({ value }, ctx) => setDataProp(ctx, 'reversed', value),
|
|
570
664
|
CiteAttr: ({ value }, ctx) => setDataProp(ctx, 'cite', value),
|
|
571
665
|
Datetime: ({ value }, ctx) => setDataProp(ctx, 'dateTime', value),
|
|
@@ -596,9 +690,9 @@ const attributeHandlers = {
|
|
|
596
690
|
Poster: ({ value }, ctx) => setDataProp(ctx, 'poster', value),
|
|
597
691
|
Preload: ({ value }, ctx) => setDataProp(ctx, 'preload', value),
|
|
598
692
|
Playsinline: ({ value }, ctx) => setDataProp(ctx, 'playsInline', value),
|
|
599
|
-
High: ({ value }, ctx) =>
|
|
600
|
-
Low: ({ value }, ctx) =>
|
|
601
|
-
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),
|
|
602
696
|
Usemap: ({ value }, ctx) => setDataAttr(ctx, 'usemap', value),
|
|
603
697
|
Ismap: ({ value }, ctx) => setDataProp(ctx, 'isMap', value),
|
|
604
698
|
Role: ({ value }, ctx) => setDataAttr(ctx, 'role', value),
|
|
@@ -650,8 +744,8 @@ const attributeHandlers = {
|
|
|
650
744
|
AriaValuetext: ({ value }, ctx) => setDataAttr(ctx, 'aria-valuetext', value),
|
|
651
745
|
Attribute: ({ key, value }, ctx) => setDataAttr(ctx, key, value),
|
|
652
746
|
DataAttribute: ({ key, value }, ctx) => setDataAttr(ctx, `data-${key}`, value),
|
|
653
|
-
Style: ({ value }, ctx) => setModuleData(ctx, 'style', value, VNodeDataMask.Style),
|
|
654
|
-
InnerHTML: ({ value }, ctx) =>
|
|
747
|
+
Style: ({ value }, ctx) => setModuleData(ctx, 'style', normalizedStyleProperties(value), VNodeDataMask.Style),
|
|
748
|
+
InnerHTML: ({ value }, ctx) => setTrustedInnerHtml(ctx, value),
|
|
655
749
|
ViewBox: ({ value }, ctx) => setDataAttr(ctx, 'viewBox', value),
|
|
656
750
|
Xmlns: ({ value }, ctx) => setDataAttr(ctx, 'xmlns', value),
|
|
657
751
|
Fill: ({ value }, ctx) => setDataAttr(ctx, 'fill', value),
|
|
@@ -742,7 +836,7 @@ const attributeHandlers = {
|
|
|
742
836
|
RefY: ({ value }, ctx) => setDataAttr(ctx, 'refY', value),
|
|
743
837
|
Orient: ({ value }, ctx) => setDataAttr(ctx, 'orient', value),
|
|
744
838
|
PreserveAspectRatio: ({ value }, ctx) => setDataAttr(ctx, 'preserveAspectRatio', value),
|
|
745
|
-
Prop: ({ key, value }, ctx) =>
|
|
839
|
+
Prop: ({ key, value }, ctx) => setClientOnlyDataProp(ctx, key, value),
|
|
746
840
|
OnCustomEvent: ({ name, f: toMessage }, ctx) => updateDataOn(ctx, {
|
|
747
841
|
[name]: (event) => {
|
|
748
842
|
if (event instanceof CustomEvent) {
|
|
@@ -868,19 +962,58 @@ const capturedContextOrEmpty = () => {
|
|
|
868
962
|
return Context.empty();
|
|
869
963
|
}
|
|
870
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
|
+
};
|
|
871
991
|
const attachPostpatchHook = (data, postpatchProps) => {
|
|
992
|
+
const existingInsert = data.hook?.insert;
|
|
872
993
|
data.hook = {
|
|
873
994
|
...data.hook,
|
|
995
|
+
insert: vnode => {
|
|
996
|
+
applyControlledProps(vnode, postpatchProps);
|
|
997
|
+
existingInsert?.(vnode);
|
|
998
|
+
},
|
|
874
999
|
postpatch: (_oldVnode, vnode) => {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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);
|
|
884
1017
|
}
|
|
885
1018
|
},
|
|
886
1019
|
};
|
|
@@ -956,14 +1089,288 @@ const copyChildrenDroppingEmpty = (children) => {
|
|
|
956
1089
|
}
|
|
957
1090
|
return next;
|
|
958
1091
|
};
|
|
959
|
-
|
|
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);
|
|
960
1367
|
const element = () => (tagName) => (attributes = [], children = []) => createElement(tagName, attributes, children);
|
|
961
1368
|
export const customElement = () => (tagName) => (attributes = [], children = []) => createElement(tagName, attributes, children);
|
|
962
1369
|
const voidElement = () => (tagName) => (attributes = []) => createElement(tagName, attributes, []);
|
|
963
1370
|
const keyed = () => (tagName) => (key, attributes = [], children = []) => {
|
|
964
1371
|
const data = buildVNodeData(attributes);
|
|
965
1372
|
data.key = key;
|
|
966
|
-
return
|
|
1373
|
+
return buildElement(tagName, data, children);
|
|
967
1374
|
};
|
|
968
1375
|
const htmlElements = () => {
|
|
969
1376
|
const el = element();
|
package/dist/hydrate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { VNode } from './snabbdom/index.js';
|
|
2
2
|
export declare const __elementSignature: (element: Element, vnode: VNode) => string;
|
|
3
|
-
export declare const __hydrateVNode: (hydrationRoot: Element, nextVNode: VNode | null, seen
|
|
3
|
+
export declare const __hydrateVNode: (hydrationRoot: Element, nextVNode: VNode | null, seen: Set<object> | undefined, buildId: string) => VNode;
|
|
4
4
|
//# sourceMappingURL=hydrate.d.ts.map
|
package/dist/hydrate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"
|
|
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"}
|