@farm.js/react 0.1.0-beta.1 → 0.1.0-beta.3
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 +101 -20
- package/dist/compiler-runtime.cjs +411 -1
- package/dist/compiler-runtime.cjs.map +1 -1
- package/dist/compiler-runtime.d.mts +36 -1
- package/dist/compiler-runtime.d.ts +36 -1
- package/dist/compiler-runtime.mjs +411 -1
- package/dist/compiler-runtime.mjs.map +1 -1
- package/dist/vite.cjs +489 -20
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.mjs +489 -20
- package/dist/vite.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -62,7 +62,8 @@ The current compiler handles components that it can prove have:
|
|
|
62
62
|
- optional synchronous `const` or function-declaration handlers used by JSX events;
|
|
63
63
|
- state-driven text, attributes, per-property inline styles, and controlled form properties;
|
|
64
64
|
- host-rooted `condition && <element>` and `condition ? <element> : <element>` child blocks at
|
|
65
|
-
statically known locations, including
|
|
65
|
+
statically known locations, including direct ranges among static siblings and at the component
|
|
66
|
+
root;
|
|
66
67
|
- item-keyed `collection.map(...)` children and explicit `List` boundaries at statically known
|
|
67
68
|
container locations, including supported non-mutating collection pipelines;
|
|
68
69
|
- inline synchronous events inside otherwise eligible host-only keyed rows;
|
|
@@ -73,8 +74,8 @@ The current compiler handles components that it can prove have:
|
|
|
73
74
|
|
|
74
75
|
The generated component preserves React ownership of initial placement, props, events, SSR, and
|
|
75
76
|
hydration. Local state cells batch updates into a microtask and patch only compiler-known DOM
|
|
76
|
-
targets. Proven
|
|
77
|
-
|
|
77
|
+
targets. Proven host containers can transfer child ownership after mount for host-only conditional
|
|
78
|
+
branches and ranges, plus non-interactive host-only keyed rows. An interactive row or a row with
|
|
78
79
|
local conditional slots uses a hybrid boundary instead: React retains its events, conditional
|
|
79
80
|
Fibers, and structural reconciliation, while Farm patches same-key bindings and refreshes only
|
|
80
81
|
changed row-local branches. React still creates or hydrates all initial DOM. Anything outside those
|
|
@@ -96,6 +97,26 @@ or hydration, a same-branch update patches those bindings without replacing the
|
|
|
96
97
|
change creates, removes, or replaces only the selected branch. No marker node is added to SSR output.
|
|
97
98
|
More complex conditional shapes keep the existing React-owned conditional boundary.
|
|
98
99
|
|
|
100
|
+
Eligible direct conditionals can also share a host container with static host siblings:
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
<article data-update={updates}>
|
|
104
|
+
<header>Status</header>
|
|
105
|
+
{loading && <p>Loading {updates}</p>}
|
|
106
|
+
<section>Stable content</section>
|
|
107
|
+
{enabled ? <strong>Enabled</strong> : <span>Disabled</span>}
|
|
108
|
+
<footer>Actions</footer>
|
|
109
|
+
</article>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
That `article` may be the component's exact returned root. Farm adopts the original container and
|
|
113
|
+
counts its direct static elements as anchors for the conditional ranges. Same-branch bindings patch
|
|
114
|
+
in place; branch changes insert, remove, or replace only their range. Multiple changed ranges use
|
|
115
|
+
one state snapshot and reconcile from right to left, including when adjacent ranges are empty. The
|
|
116
|
+
root and every unchanged static sibling keep their DOM identity, and no wrapper or marker is added.
|
|
117
|
+
Branch events, components, fragments, refs, SVG, spreads, nested dynamic blocks, or another
|
|
118
|
+
unsupported direct child keep the container on the React-owned path.
|
|
119
|
+
|
|
99
120
|
For a common keyed map, no new API is required:
|
|
100
121
|
|
|
101
122
|
```tsx
|
|
@@ -188,11 +209,68 @@ and unproven calls use the original React fallback.
|
|
|
188
209
|
`toSorted` and `toReversed` are emitted as standard runtime calls rather than polyfilled. Configure
|
|
189
210
|
the TypeScript `lib` with ES2023 and target a runtime that supports them when using those methods.
|
|
190
211
|
|
|
191
|
-
React remains the fallback and compatibility boundary.
|
|
192
|
-
non-
|
|
193
|
-
|
|
194
|
-
existing React-owned keyed boundary. The outer
|
|
195
|
-
reconciles that list's rows and owns their
|
|
212
|
+
React remains the fallback and compatibility boundary. An interactive map beside static children,
|
|
213
|
+
a non-host sibling between ranges, a row with a non-inline or async handler, a file input or dynamic
|
|
214
|
+
form control shape, a fragment, a ref, a custom component, or a conditional mixed directly beside
|
|
215
|
+
other children in the same host slot uses the existing React-owned keyed boundary. The outer
|
|
216
|
+
compiled component can still be skipped, but React reconciles that list's rows and owns their
|
|
217
|
+
events, lifecycle, and state.
|
|
218
|
+
|
|
219
|
+
Non-interactive host maps can share a nested or component-root container with static host siblings:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
<ul>
|
|
223
|
+
<li>Primary</li>
|
|
224
|
+
{primary.map((item) => (
|
|
225
|
+
<li key={item.id}>{item.label}</li>
|
|
226
|
+
))}
|
|
227
|
+
<li>Secondary</li>
|
|
228
|
+
{secondary.map((item) => (
|
|
229
|
+
<li key={item.id}>{item.label}</li>
|
|
230
|
+
))}
|
|
231
|
+
<li>{primary.length + secondary.length} total</li>
|
|
232
|
+
</ul>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The compiler emits one range block for that host container. React renders and hydrates the original
|
|
236
|
+
markup. After mount, Farm records the static element segments and reconciles each keyed range with
|
|
237
|
+
its own row table and LIS pass. It does not add wrappers or hydration markers. When the container is
|
|
238
|
+
the component root, the block forwards that exact host element to the outer binding runtime, so
|
|
239
|
+
root attributes and styles keep updating normally. A root with one eligible map or `List` also
|
|
240
|
+
works without static siblings. Direct children must be host elements or eligible ranges; events,
|
|
241
|
+
controlled rows, row conditionals, components, fragments, and nested dynamic structures keep the
|
|
242
|
+
React boundary. Duplicate keys, an adoption shape mismatch, or a parent-driven static markup change
|
|
243
|
+
remounts the complete container through React.
|
|
244
|
+
|
|
245
|
+
Controlled host fields can use the hybrid keyed-row path:
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
<ul>
|
|
249
|
+
{items.map((item) => (
|
|
250
|
+
<li key={item.id}>
|
|
251
|
+
<input
|
|
252
|
+
value={item.label}
|
|
253
|
+
onChange={(event) =>
|
|
254
|
+
setItems((current) =>
|
|
255
|
+
current.map((row) =>
|
|
256
|
+
row.id === item.id ? { ...row, label: event.currentTarget.value } : row,
|
|
257
|
+
),
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
/>
|
|
261
|
+
<input type="checkbox" checked={item.done} onChange={/* keyed update */} />
|
|
262
|
+
</li>
|
|
263
|
+
))}
|
|
264
|
+
</ul>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
React owns the controls, events, hydration, and every structural commit. Farm patches `value`,
|
|
268
|
+
`checked`, selected options, and dependent row bindings by stable key. It preserves focused text
|
|
269
|
+
selection and leaves composition events on React. When `event.currentTarget` is referenced inside
|
|
270
|
+
a queued functional compiler-state updater, generated code snapshots the referenced property while
|
|
271
|
+
the handler is active. Static input types and static option attributes are required. File inputs,
|
|
272
|
+
dynamic types or options, `contentEditable`, refs, custom controls, textarea children beside
|
|
273
|
+
`value`, and async or non-inline handlers keep the React fallback.
|
|
196
274
|
|
|
197
275
|
For custom rows or an explicit key selector, use the public component:
|
|
198
276
|
|
|
@@ -233,14 +311,16 @@ Generated callback refs give directly patched host elements stable private targe
|
|
|
233
311
|
React-owned component may therefore return `null`, a fragment, or multiple host nodes without
|
|
234
312
|
shifting an unrelated compiler binding. These refs do not add attributes to SSR output.
|
|
235
313
|
|
|
236
|
-
Conditional blocks have
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
314
|
+
Conditional blocks have three safe ownership levels. One or more proven host-only direct branches
|
|
315
|
+
may become compiler-owned ranges among static host siblings, including at the exact component root.
|
|
316
|
+
A dedicated container with exactly one conditional child uses the smaller compiler-owned branch
|
|
317
|
+
boundary. The more general React-owned conditional path accepts supported events, nested host
|
|
318
|
+
conditionals, keyed lists, and component islands. Keys, custom components, fragments, refs, SVG,
|
|
319
|
+
attribute spreads, `dangerouslySetInnerHTML`, nested dynamic blocks, and branch events keep React
|
|
320
|
+
ownership. An empty ternary branch may be `null` or `false`. The inactive branch is described at
|
|
321
|
+
build time but is never pre-mounted or cached. If a logical `&&` evaluates to a number such as `0`,
|
|
322
|
+
or adopted DOM no longer matches its descriptor, the affected container remounts through React so
|
|
323
|
+
JavaScript, hydration, and React rendering semantics remain exact.
|
|
244
324
|
|
|
245
325
|
All component-level boundary types share one block graph and one ID sequence. A nested
|
|
246
326
|
binding records its nearest conditional parent. If one state flush affects both an outer
|
|
@@ -276,8 +356,9 @@ assertion; it is not presented as a cross-machine timing benchmark.
|
|
|
276
356
|
Application and prototype calls, dynamic style objects, handlers outside JSX events, nested,
|
|
277
357
|
computed, and rest props patterns, async handlers, unkeyed or index-keyed lists, chained maps,
|
|
278
358
|
unsupported conditional roots, effects, and more advanced hook support intentionally stay on React
|
|
279
|
-
in this release. Compiler-owned keyed rows
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
359
|
+
in this release. Compiler-owned keyed rows support either one dedicated host-only map/`List` or
|
|
360
|
+
one or more non-interactive ranges in a nested or component-root host container. Inline synchronous
|
|
361
|
+
events and dedicated row-local host conditional slots can use the single-list hybrid path, but
|
|
362
|
+
branch events, interactive ranges beside siblings, nested dynamic blocks, conditionals mixed with
|
|
363
|
+
siblings in one slot, non-inline or async row handlers, unsupported form shapes, custom components,
|
|
283
364
|
fragments, refs, SVG, and duplicate runtime keys keep or switch to React ownership.
|
|
@@ -510,6 +510,181 @@ function createHostConditionalBlockComponent(owner) {
|
|
|
510
510
|
FarmHostConditionalBlock.displayName = "FarmCompiledHostConditional";
|
|
511
511
|
return FarmHostConditionalBlock;
|
|
512
512
|
}
|
|
513
|
+
function createConditionalRangesBlockComponent(owner) {
|
|
514
|
+
class FarmConditionalRangesBlock extends React__default.default.Component {
|
|
515
|
+
constructor() {
|
|
516
|
+
super(...arguments);
|
|
517
|
+
this.state = { fallback: false };
|
|
518
|
+
this.root = null;
|
|
519
|
+
this.mounted = false;
|
|
520
|
+
this.fallbackRequested = false;
|
|
521
|
+
this.fallbackVersion = 0;
|
|
522
|
+
this.propFallbackQueued = false;
|
|
523
|
+
this.currentProps = this.props;
|
|
524
|
+
this.rangeInstances = [];
|
|
525
|
+
this.staticSegments = [];
|
|
526
|
+
this.captureRoot = (root) => {
|
|
527
|
+
this.root = root;
|
|
528
|
+
this.currentProps.rootRef?.(root);
|
|
529
|
+
};
|
|
530
|
+
this.refresh = (afterCommit) => {
|
|
531
|
+
this.reconcile(afterCommit);
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
readSelections(props) {
|
|
535
|
+
const selections = [];
|
|
536
|
+
for (const range of props.ranges) {
|
|
537
|
+
const selection = hostConditionalSelection(range);
|
|
538
|
+
if (selection.kind === "fallback") return null;
|
|
539
|
+
selections.push(selection);
|
|
540
|
+
}
|
|
541
|
+
return selections;
|
|
542
|
+
}
|
|
543
|
+
adopt(props = this.currentProps) {
|
|
544
|
+
if (!this.root || !Number.isSafeInteger(props.trailing) || props.trailing < 0) return false;
|
|
545
|
+
const selections = this.readSelections(props);
|
|
546
|
+
if (!selections) return false;
|
|
547
|
+
const elements = [...this.root.children];
|
|
548
|
+
const staticSegments = [];
|
|
549
|
+
const instances = [];
|
|
550
|
+
let cursor = 0;
|
|
551
|
+
for (let rangeIndex = 0; rangeIndex < props.ranges.length; rangeIndex += 1) {
|
|
552
|
+
const range = props.ranges[rangeIndex];
|
|
553
|
+
if (!Number.isSafeInteger(range.before) || range.before < 0) return false;
|
|
554
|
+
const staticEnd = cursor + range.before;
|
|
555
|
+
if (staticEnd > elements.length) return false;
|
|
556
|
+
staticSegments.push(elements.slice(cursor, staticEnd));
|
|
557
|
+
cursor = staticEnd;
|
|
558
|
+
const selection = selections[rangeIndex];
|
|
559
|
+
if (selection.kind === "empty") {
|
|
560
|
+
instances.push(null);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const element = elements[cursor];
|
|
564
|
+
if (!element) return false;
|
|
565
|
+
const descriptor = selection.branch.create();
|
|
566
|
+
if (!matchesCompilerHostElement(element, descriptor)) return false;
|
|
567
|
+
instances.push({
|
|
568
|
+
key: selection.key,
|
|
569
|
+
host: {
|
|
570
|
+
element,
|
|
571
|
+
values: readHostConditionalBindingValues(selection.branch)
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
cursor += 1;
|
|
575
|
+
}
|
|
576
|
+
if (cursor + props.trailing !== elements.length) return false;
|
|
577
|
+
staticSegments.push(elements.slice(cursor));
|
|
578
|
+
this.staticSegments = staticSegments;
|
|
579
|
+
this.rangeInstances = instances;
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
anchorAfter(rangeIndex) {
|
|
583
|
+
for (let next = rangeIndex + 1; next < this.rangeInstances.length; next += 1) {
|
|
584
|
+
const staticAnchor = this.staticSegments[next]?.[0];
|
|
585
|
+
if (staticAnchor) return staticAnchor;
|
|
586
|
+
const rangeAnchor = this.rangeInstances[next]?.host.element;
|
|
587
|
+
if (rangeAnchor) return rangeAnchor;
|
|
588
|
+
}
|
|
589
|
+
return this.staticSegments[this.rangeInstances.length]?.[0] || null;
|
|
590
|
+
}
|
|
591
|
+
reconcileRange(rangeIndex, selection) {
|
|
592
|
+
if (!this.root) return;
|
|
593
|
+
const previous = this.rangeInstances[rangeIndex];
|
|
594
|
+
if (selection.kind === "empty") {
|
|
595
|
+
previous?.host.element.remove();
|
|
596
|
+
this.rangeInstances[rangeIndex] = null;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (previous?.key === selection.key) {
|
|
600
|
+
applyHostConditionalBindings(selection.branch, previous.host);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const element = createCompilerHostElement(this.root.ownerDocument, selection.branch.create());
|
|
604
|
+
if (previous) previous.host.element.replaceWith(element);
|
|
605
|
+
else this.root.insertBefore(element, this.anchorAfter(rangeIndex));
|
|
606
|
+
this.rangeInstances[rangeIndex] = {
|
|
607
|
+
key: selection.key,
|
|
608
|
+
host: {
|
|
609
|
+
element,
|
|
610
|
+
values: readHostConditionalBindingValues(selection.branch)
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
reconcile(afterCommit) {
|
|
615
|
+
if (!this.mounted || !this.root) {
|
|
616
|
+
afterCommit?.();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (this.state.fallback) {
|
|
620
|
+
this.fallbackVersion += 1;
|
|
621
|
+
this.forceUpdate(afterCommit);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (this.currentProps.ranges.length !== this.rangeInstances.length) {
|
|
625
|
+
this.activateFallback(afterCommit);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const selections = this.readSelections(this.currentProps);
|
|
629
|
+
if (!selections) {
|
|
630
|
+
this.activateFallback(afterCommit);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
for (let rangeIndex = this.rangeInstances.length - 1; rangeIndex >= 0; rangeIndex -= 1) {
|
|
634
|
+
this.reconcileRange(rangeIndex, selections[rangeIndex]);
|
|
635
|
+
}
|
|
636
|
+
afterCommit?.();
|
|
637
|
+
}
|
|
638
|
+
activateFallback(afterCommit) {
|
|
639
|
+
if (!this.mounted || this.state.fallback || this.fallbackRequested) {
|
|
640
|
+
afterCommit?.();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
this.fallbackRequested = true;
|
|
644
|
+
this.setState({ fallback: true }, afterCommit);
|
|
645
|
+
}
|
|
646
|
+
schedulePropFallback() {
|
|
647
|
+
if (this.propFallbackQueued) return;
|
|
648
|
+
this.propFallbackQueued = true;
|
|
649
|
+
queueMicrotask(() => {
|
|
650
|
+
this.propFallbackQueued = false;
|
|
651
|
+
if (this.mounted && !this.state.fallback) this.activateFallback();
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
shouldComponentUpdate(nextProps, nextState) {
|
|
655
|
+
this.currentProps = nextProps;
|
|
656
|
+
if (nextState.fallback || this.state.fallback) return true;
|
|
657
|
+
this.schedulePropFallback();
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
componentDidMount() {
|
|
661
|
+
this.mounted = true;
|
|
662
|
+
this.unsubscribe = owner.subscribe(this.props.id, this.refresh);
|
|
663
|
+
if (!this.adopt()) this.activateFallback();
|
|
664
|
+
}
|
|
665
|
+
componentWillUnmount() {
|
|
666
|
+
this.mounted = false;
|
|
667
|
+
this.unsubscribe?.();
|
|
668
|
+
this.root = null;
|
|
669
|
+
this.rangeInstances = [];
|
|
670
|
+
this.staticSegments = [];
|
|
671
|
+
}
|
|
672
|
+
render() {
|
|
673
|
+
const container = this.currentProps.render();
|
|
674
|
+
if (!React__default.default.isValidElement(container) || typeof container.type !== "string") {
|
|
675
|
+
throw new TypeError(
|
|
676
|
+
`Compiled conditional ranges ${this.currentProps.id} must own one host container.`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return React__default.default.cloneElement(container, {
|
|
680
|
+
key: this.state.fallback ? `react-${this.fallbackVersion}` : "compiled",
|
|
681
|
+
ref: this.captureRoot
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
FarmConditionalRangesBlock.displayName = "FarmCompiledConditionalRanges";
|
|
686
|
+
return FarmConditionalRangesBlock;
|
|
687
|
+
}
|
|
513
688
|
function longestIncreasingSubsequencePositions(sequence) {
|
|
514
689
|
const tails = [];
|
|
515
690
|
const previous = Array.from({ length: sequence.length }, () => -1);
|
|
@@ -906,6 +1081,219 @@ function createKeyedRowsBlockComponent(owner) {
|
|
|
906
1081
|
FarmKeyedRowsBlock.displayName = "FarmCompiledKeyedRows";
|
|
907
1082
|
return FarmKeyedRowsBlock;
|
|
908
1083
|
}
|
|
1084
|
+
function createKeyedRangesBlockComponent(owner) {
|
|
1085
|
+
class FarmKeyedRangesBlock extends React__default.default.Component {
|
|
1086
|
+
constructor() {
|
|
1087
|
+
super(...arguments);
|
|
1088
|
+
this.state = { fallback: false };
|
|
1089
|
+
this.root = null;
|
|
1090
|
+
this.mounted = false;
|
|
1091
|
+
this.fallbackRequested = false;
|
|
1092
|
+
this.fallbackVersion = 0;
|
|
1093
|
+
this.propFallbackQueued = false;
|
|
1094
|
+
this.currentProps = this.props;
|
|
1095
|
+
this.rangeInstances = [];
|
|
1096
|
+
this.staticSegments = [];
|
|
1097
|
+
this.captureRoot = (root) => {
|
|
1098
|
+
this.root = root;
|
|
1099
|
+
this.currentProps.rootRef?.(root);
|
|
1100
|
+
};
|
|
1101
|
+
this.refresh = (afterCommit) => {
|
|
1102
|
+
this.reconcile(afterCommit);
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
readRange(range) {
|
|
1106
|
+
const source = range.items();
|
|
1107
|
+
const items = source ? Array.from(source) : [];
|
|
1108
|
+
const keys = items.map((item, index) => keyedRowIdentity(range.rowKey(item, index)));
|
|
1109
|
+
if (new Set(keys).size !== keys.length) return null;
|
|
1110
|
+
return { items, keys };
|
|
1111
|
+
}
|
|
1112
|
+
readRanges(props) {
|
|
1113
|
+
const ranges = [];
|
|
1114
|
+
for (const range of props.ranges) {
|
|
1115
|
+
const rows = this.readRange(range);
|
|
1116
|
+
if (!rows) return null;
|
|
1117
|
+
ranges.push(rows);
|
|
1118
|
+
}
|
|
1119
|
+
return ranges;
|
|
1120
|
+
}
|
|
1121
|
+
adopt(props = this.currentProps) {
|
|
1122
|
+
if (!this.root || !Number.isSafeInteger(props.trailing) || props.trailing < 0) return false;
|
|
1123
|
+
const rowsByRange = this.readRanges(props);
|
|
1124
|
+
if (!rowsByRange) return false;
|
|
1125
|
+
const elements = [...this.root.children];
|
|
1126
|
+
const staticSegments = [];
|
|
1127
|
+
const instancesByRange = [];
|
|
1128
|
+
let cursor = 0;
|
|
1129
|
+
for (let rangeIndex = 0; rangeIndex < props.ranges.length; rangeIndex += 1) {
|
|
1130
|
+
const range = props.ranges[rangeIndex];
|
|
1131
|
+
const rows = rowsByRange[rangeIndex];
|
|
1132
|
+
if (!Number.isSafeInteger(range.before) || range.before < 0) return false;
|
|
1133
|
+
const staticEnd = cursor + range.before;
|
|
1134
|
+
if (staticEnd > elements.length) return false;
|
|
1135
|
+
staticSegments.push(elements.slice(cursor, staticEnd));
|
|
1136
|
+
cursor = staticEnd;
|
|
1137
|
+
const rowEnd = cursor + rows.items.length;
|
|
1138
|
+
if (rowEnd > elements.length) return false;
|
|
1139
|
+
const instances = /* @__PURE__ */ new Map();
|
|
1140
|
+
for (let index = 0; index < rows.items.length; index += 1) {
|
|
1141
|
+
const element = elements[cursor + index];
|
|
1142
|
+
if (!matchesCompilerHostElement(element, range.create(rows.items[index], index))) {
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
instances.set(rows.keys[index], {
|
|
1146
|
+
key: rows.keys[index],
|
|
1147
|
+
element,
|
|
1148
|
+
values: readKeyedRowBindingValues(range, rows.items[index], index),
|
|
1149
|
+
item: rows.items[index],
|
|
1150
|
+
index,
|
|
1151
|
+
conditionalValues: /* @__PURE__ */ new Map()
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
instancesByRange.push(instances);
|
|
1155
|
+
cursor = rowEnd;
|
|
1156
|
+
}
|
|
1157
|
+
if (cursor + props.trailing !== elements.length) return false;
|
|
1158
|
+
staticSegments.push(elements.slice(cursor));
|
|
1159
|
+
this.staticSegments = staticSegments;
|
|
1160
|
+
this.rangeInstances = instancesByRange;
|
|
1161
|
+
return true;
|
|
1162
|
+
}
|
|
1163
|
+
anchorAfter(rangeIndex) {
|
|
1164
|
+
for (let next = rangeIndex + 1; next < this.rangeInstances.length; next += 1) {
|
|
1165
|
+
const staticAnchor = this.staticSegments[next]?.[0];
|
|
1166
|
+
if (staticAnchor) return staticAnchor;
|
|
1167
|
+
const rowAnchor = this.rangeInstances[next].values().next().value?.element;
|
|
1168
|
+
if (rowAnchor) return rowAnchor;
|
|
1169
|
+
}
|
|
1170
|
+
return this.staticSegments[this.rangeInstances.length]?.[0] || null;
|
|
1171
|
+
}
|
|
1172
|
+
reconcileRange(rangeIndex, rows) {
|
|
1173
|
+
if (!this.root) return;
|
|
1174
|
+
const range = this.currentProps.ranges[rangeIndex];
|
|
1175
|
+
const previous = this.rangeInstances[rangeIndex];
|
|
1176
|
+
const oldIndices = /* @__PURE__ */ new Map();
|
|
1177
|
+
[...previous.keys()].forEach((key, index) => oldIndices.set(key, index));
|
|
1178
|
+
const nextKeys = new Set(rows.keys);
|
|
1179
|
+
for (const [key, instance] of previous) {
|
|
1180
|
+
if (!nextKeys.has(key)) instance.element.remove();
|
|
1181
|
+
}
|
|
1182
|
+
const sequence = rows.keys.map((key) => oldIndices.get(key) ?? -1);
|
|
1183
|
+
const stablePositions = longestIncreasingSubsequencePositions(sequence);
|
|
1184
|
+
const nextInstances = [];
|
|
1185
|
+
for (let index = 0; index < rows.items.length; index += 1) {
|
|
1186
|
+
const key = rows.keys[index];
|
|
1187
|
+
const existing = previous.get(key);
|
|
1188
|
+
if (existing) {
|
|
1189
|
+
applyKeyedRowBindings(range, existing, rows.items[index], index);
|
|
1190
|
+
existing.item = rows.items[index];
|
|
1191
|
+
existing.index = index;
|
|
1192
|
+
nextInstances.push(existing);
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const element = createCompilerHostElement(
|
|
1196
|
+
this.root.ownerDocument,
|
|
1197
|
+
range.create(rows.items[index], index)
|
|
1198
|
+
);
|
|
1199
|
+
nextInstances.push({
|
|
1200
|
+
key,
|
|
1201
|
+
element,
|
|
1202
|
+
values: readKeyedRowBindingValues(range, rows.items[index], index),
|
|
1203
|
+
item: rows.items[index],
|
|
1204
|
+
index,
|
|
1205
|
+
conditionalValues: /* @__PURE__ */ new Map()
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
let anchor = this.anchorAfter(rangeIndex);
|
|
1209
|
+
for (let index = nextInstances.length - 1; index >= 0; index -= 1) {
|
|
1210
|
+
const instance = nextInstances[index];
|
|
1211
|
+
if (sequence[index] < 0) {
|
|
1212
|
+
this.root.insertBefore(instance.element, anchor);
|
|
1213
|
+
} else if (!stablePositions.has(index) && instance.element.nextSibling !== anchor) {
|
|
1214
|
+
this.root.insertBefore(instance.element, anchor);
|
|
1215
|
+
}
|
|
1216
|
+
anchor = instance.element;
|
|
1217
|
+
}
|
|
1218
|
+
this.rangeInstances[rangeIndex] = new Map(
|
|
1219
|
+
nextInstances.map((instance) => [instance.key, instance])
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
reconcile(afterCommit) {
|
|
1223
|
+
if (!this.mounted || !this.root) {
|
|
1224
|
+
afterCommit?.();
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (this.state.fallback) {
|
|
1228
|
+
this.fallbackVersion += 1;
|
|
1229
|
+
this.forceUpdate(afterCommit);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const rowsByRange = this.readRanges(this.currentProps);
|
|
1233
|
+
if (!rowsByRange || rowsByRange.length !== this.rangeInstances.length) {
|
|
1234
|
+
this.activateFallback(afterCommit);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const activeElement = this.root.ownerDocument.activeElement;
|
|
1238
|
+
const restoreFocus = Boolean(activeElement && this.root.contains(activeElement));
|
|
1239
|
+
for (let rangeIndex = rowsByRange.length - 1; rangeIndex >= 0; rangeIndex -= 1) {
|
|
1240
|
+
this.reconcileRange(rangeIndex, rowsByRange[rangeIndex]);
|
|
1241
|
+
}
|
|
1242
|
+
if (restoreFocus && activeElement?.isConnected && activeElement.ownerDocument.activeElement !== activeElement && "focus" in activeElement) {
|
|
1243
|
+
activeElement.focus({ preventScroll: true });
|
|
1244
|
+
}
|
|
1245
|
+
afterCommit?.();
|
|
1246
|
+
}
|
|
1247
|
+
activateFallback(afterCommit) {
|
|
1248
|
+
if (!this.mounted || this.state.fallback || this.fallbackRequested) {
|
|
1249
|
+
afterCommit?.();
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
this.fallbackRequested = true;
|
|
1253
|
+
this.setState({ fallback: true }, afterCommit);
|
|
1254
|
+
}
|
|
1255
|
+
schedulePropFallback() {
|
|
1256
|
+
if (this.propFallbackQueued) return;
|
|
1257
|
+
this.propFallbackQueued = true;
|
|
1258
|
+
queueMicrotask(() => {
|
|
1259
|
+
this.propFallbackQueued = false;
|
|
1260
|
+
if (this.mounted && !this.state.fallback) this.activateFallback();
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
shouldComponentUpdate(nextProps, nextState) {
|
|
1264
|
+
this.currentProps = nextProps;
|
|
1265
|
+
if (nextState.fallback || this.state.fallback) return true;
|
|
1266
|
+
this.schedulePropFallback();
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
componentDidMount() {
|
|
1270
|
+
this.mounted = true;
|
|
1271
|
+
this.unsubscribe = owner.subscribe(this.props.id, this.refresh);
|
|
1272
|
+
if (!this.adopt()) this.activateFallback();
|
|
1273
|
+
}
|
|
1274
|
+
componentWillUnmount() {
|
|
1275
|
+
this.mounted = false;
|
|
1276
|
+
this.unsubscribe?.();
|
|
1277
|
+
this.root = null;
|
|
1278
|
+
this.rangeInstances = [];
|
|
1279
|
+
this.staticSegments = [];
|
|
1280
|
+
}
|
|
1281
|
+
render() {
|
|
1282
|
+
const container = this.currentProps.render();
|
|
1283
|
+
if (!React__default.default.isValidElement(container) || typeof container.type !== "string") {
|
|
1284
|
+
throw new TypeError(
|
|
1285
|
+
`Compiled keyed ranges ${this.currentProps.id} must own one host container.`
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
return React__default.default.cloneElement(container, {
|
|
1289
|
+
key: this.state.fallback ? `react-${this.fallbackVersion}` : "compiled",
|
|
1290
|
+
ref: this.captureRoot
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
FarmKeyedRangesBlock.displayName = "FarmCompiledKeyedRanges";
|
|
1295
|
+
return FarmKeyedRangesBlock;
|
|
1296
|
+
}
|
|
909
1297
|
function createComponentBlockComponent(owner) {
|
|
910
1298
|
class FarmComponentBlock extends React__default.default.Component {
|
|
911
1299
|
constructor() {
|
|
@@ -996,12 +1384,18 @@ function createCompiledComponent(definition) {
|
|
|
996
1384
|
HostConditional: createHostConditionalBlockComponent({
|
|
997
1385
|
subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
|
|
998
1386
|
}),
|
|
1387
|
+
ConditionalRanges: createConditionalRangesBlockComponent({
|
|
1388
|
+
subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
|
|
1389
|
+
}),
|
|
999
1390
|
KeyedList: createKeyedListBlockComponent({
|
|
1000
1391
|
subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
|
|
1001
1392
|
}),
|
|
1002
1393
|
KeyedRows: createKeyedRowsBlockComponent({
|
|
1003
1394
|
subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
|
|
1004
1395
|
}),
|
|
1396
|
+
KeyedRanges: createKeyedRangesBlockComponent({
|
|
1397
|
+
subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
|
|
1398
|
+
}),
|
|
1005
1399
|
Component: createComponentBlockComponent({
|
|
1006
1400
|
subscribe: (id, refresh) => this.subscribeToBlock(id, refresh)
|
|
1007
1401
|
}),
|
|
@@ -1159,7 +1553,23 @@ function createCompiledComponent(definition) {
|
|
|
1159
1553
|
if (this.hasBindingError) throw this.bindingError;
|
|
1160
1554
|
const currentDefinition = definitionReference.current;
|
|
1161
1555
|
const element = currentDefinition.render(this.props, this.cells, this.blockRuntime);
|
|
1162
|
-
if (!React__default.default.isValidElement(element)
|
|
1556
|
+
if (!React__default.default.isValidElement(element)) {
|
|
1557
|
+
throw new TypeError(
|
|
1558
|
+
`Compiled component ${currentDefinition.displayName} must return one host element.`
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
if (element.type === this.blockRuntime.KeyedRanges) {
|
|
1562
|
+
return React__default.default.cloneElement(element, {
|
|
1563
|
+
rootRef: this.captureRoot
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
if (element.type === this.blockRuntime.ConditionalRanges) {
|
|
1567
|
+
return React__default.default.cloneElement(
|
|
1568
|
+
element,
|
|
1569
|
+
{ rootRef: this.captureRoot }
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
if (typeof element.type !== "string") {
|
|
1163
1573
|
throw new TypeError(
|
|
1164
1574
|
`Compiled component ${currentDefinition.displayName} must return one host element.`
|
|
1165
1575
|
);
|