@praxisjs/runtime 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/dist/children.d.ts.map +1 -1
- package/dist/children.js +2 -1
- package/dist/children.js.map +1 -1
- package/dist/component.d.ts +2 -3
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +133 -60
- package/dist/component.js.map +1 -1
- package/dist/context.d.ts +13 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +24 -2
- package/dist/context.js.map +1 -1
- package/dist/dom/constants.d.ts +1 -1
- package/dist/dom/constants.d.ts.map +1 -1
- package/dist/dom/constants.js +24 -83
- package/dist/dom/constants.js.map +1 -1
- package/dist/dom/create.d.ts +1 -1
- package/dist/dom/create.d.ts.map +1 -1
- package/dist/dom/create.js +3 -3
- package/dist/dom/create.js.map +1 -1
- package/dist/dom/events.d.ts +29 -1
- package/dist/dom/events.d.ts.map +1 -1
- package/dist/dom/events.js +167 -17
- package/dist/dom/events.js.map +1 -1
- package/dist/dom/props.d.ts +4 -0
- package/dist/dom/props.d.ts.map +1 -1
- package/dist/dom/props.js +57 -8
- package/dist/dom/props.js.map +1 -1
- package/dist/dom/range.d.ts +4 -0
- package/dist/dom/range.d.ts.map +1 -0
- package/dist/dom/range.js +43 -0
- package/dist/dom/range.js.map +1 -0
- package/dist/dom/remove.d.ts.map +1 -1
- package/dist/dom/remove.js +2 -8
- package/dist/dom/remove.js.map +1 -1
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +7 -5
- package/dist/element.js.map +1 -1
- package/dist/hmr.d.ts +35 -0
- package/dist/hmr.d.ts.map +1 -0
- package/dist/hmr.js +85 -0
- package/dist/hmr.js.map +1 -0
- package/dist/hydration-context.d.ts +9 -1
- package/dist/hydration-context.d.ts.map +1 -1
- package/dist/hydration-context.js +34 -13
- package/dist/hydration-context.js.map +1 -1
- package/dist/hydration.d.ts.map +1 -1
- package/dist/hydration.js +4 -11
- package/dist/hydration.js.map +1 -1
- package/dist/index.d.ts +16 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -7
- package/dist/index.js.map +1 -1
- package/dist/list.d.ts +72 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +786 -0
- package/dist/list.js.map +1 -0
- package/dist/portal.d.ts.map +1 -1
- package/dist/portal.js +3 -0
- package/dist/portal.js.map +1 -1
- package/dist/reactive.d.ts +2 -2
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +109 -34
- package/dist/reactive.js.map +1 -1
- package/dist/scope.d.ts +14 -4
- package/dist/scope.d.ts.map +1 -1
- package/dist/scope.js +55 -15
- package/dist/scope.js.map +1 -1
- package/dist/template.d.ts +48 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +270 -0
- package/dist/template.js.map +1 -0
- package/package.json +15 -6
- package/src/children.ts +2 -1
- package/src/component.ts +135 -63
- package/src/context.ts +35 -4
- package/src/dom/constants.ts +25 -82
- package/src/dom/create.ts +3 -3
- package/src/dom/events.ts +188 -17
- package/src/dom/props.ts +66 -9
- package/src/dom/range.ts +45 -0
- package/src/dom/remove.ts +3 -7
- package/src/element.ts +8 -6
- package/src/hmr.ts +110 -0
- package/src/hydration-context.ts +34 -13
- package/src/hydration.ts +4 -9
- package/src/index.ts +54 -7
- package/src/list.ts +886 -0
- package/src/portal.ts +3 -0
- package/src/reactive.ts +147 -40
- package/src/scope.ts +63 -16
- package/src/template.ts +271 -0
- package/dist/__tests__/children.test.d.ts +0 -2
- package/dist/__tests__/children.test.d.ts.map +0 -1
- package/dist/__tests__/children.test.js +0 -241
- package/dist/__tests__/children.test.js.map +0 -1
- package/dist/__tests__/component.test.d.ts +0 -2
- package/dist/__tests__/component.test.d.ts.map +0 -1
- package/dist/__tests__/component.test.js +0 -331
- package/dist/__tests__/component.test.js.map +0 -1
- package/dist/__tests__/context.test.d.ts +0 -2
- package/dist/__tests__/context.test.d.ts.map +0 -1
- package/dist/__tests__/context.test.js +0 -111
- package/dist/__tests__/context.test.js.map +0 -1
- package/dist/__tests__/dom.test.d.ts +0 -2
- package/dist/__tests__/dom.test.d.ts.map +0 -1
- package/dist/__tests__/dom.test.js +0 -385
- package/dist/__tests__/dom.test.js.map +0 -1
- package/dist/__tests__/hydration.test.d.ts +0 -2
- package/dist/__tests__/hydration.test.d.ts.map +0 -1
- package/dist/__tests__/hydration.test.js +0 -299
- package/dist/__tests__/hydration.test.js.map +0 -1
- package/dist/__tests__/portal.test.d.ts +0 -2
- package/dist/__tests__/portal.test.d.ts.map +0 -1
- package/dist/__tests__/portal.test.js +0 -157
- package/dist/__tests__/portal.test.js.map +0 -1
- package/dist/__tests__/remove.test.d.ts +0 -2
- package/dist/__tests__/remove.test.d.ts.map +0 -1
- package/dist/__tests__/remove.test.js +0 -45
- package/dist/__tests__/remove.test.js.map +0 -1
- package/dist/__tests__/render.test.d.ts +0 -2
- package/dist/__tests__/render.test.d.ts.map +0 -1
- package/dist/__tests__/render.test.js +0 -97
- package/dist/__tests__/render.test.js.map +0 -1
- package/dist/__tests__/scope.test.d.ts +0 -2
- package/dist/__tests__/scope.test.d.ts.map +0 -1
- package/dist/__tests__/scope.test.js +0 -110
- package/dist/__tests__/scope.test.js.map +0 -1
- package/src/__tests__/children.test.ts +0 -276
- package/src/__tests__/component.test.ts +0 -363
- package/src/__tests__/context.test.ts +0 -128
- package/src/__tests__/dom.test.ts +0 -434
- package/src/__tests__/hydration.test.ts +0 -360
- package/src/__tests__/portal.test.ts +0 -177
- package/src/__tests__/remove.test.ts +0 -51
- package/src/__tests__/render.test.ts +0 -119
- package/src/__tests__/scope.test.ts +0 -120
- package/tsconfig.json +0 -8
package/src/list.ts
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RootComponent,
|
|
3
|
+
batch,
|
|
4
|
+
getComponentProps,
|
|
5
|
+
setComponentAnchor,
|
|
6
|
+
setComponentStart,
|
|
7
|
+
signal,
|
|
8
|
+
untrack,
|
|
9
|
+
} from "@praxisjs/core/internal";
|
|
10
|
+
import type { Children, Signal } from "@praxisjs/shared";
|
|
11
|
+
|
|
12
|
+
import { mountChildren } from "./children";
|
|
13
|
+
import { getCurrentScope, swapScope } from "./context";
|
|
14
|
+
import { moveRange, removeRange } from "./dom/range";
|
|
15
|
+
import { recording, onHydrationReplace } from "./hydration-context";
|
|
16
|
+
import { Scope } from "./scope";
|
|
17
|
+
|
|
18
|
+
declare const process: { env: { NODE_ENV?: string } };
|
|
19
|
+
|
|
20
|
+
/** Anything with Promise-based `enter`/`leave` hooks, such as `createTransition()` from `@praxisjs/motion`. */
|
|
21
|
+
export interface ListTransition {
|
|
22
|
+
enter?(el: HTMLElement): Promise<void>;
|
|
23
|
+
leave?(el: HTMLElement): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ListSource<T> =
|
|
27
|
+
| readonly T[]
|
|
28
|
+
| Iterable<T>
|
|
29
|
+
| null
|
|
30
|
+
| undefined
|
|
31
|
+
| (() => readonly T[] | Iterable<T> | null | undefined);
|
|
32
|
+
|
|
33
|
+
export interface ForProps<T> {
|
|
34
|
+
/** The items, or a function returning them. Only this function is tracked by the list. */
|
|
35
|
+
each: ListSource<T>;
|
|
36
|
+
/**
|
|
37
|
+
* Identifies an item across updates: a function of the item (and its index), or the name of a
|
|
38
|
+
* property holding the key. Defaults to the item itself.
|
|
39
|
+
*/
|
|
40
|
+
by?: ((item: T, index: number) => unknown) | keyof T;
|
|
41
|
+
/** Rendered while the list is empty. A function is mounted (and disposed) each time it shows. */
|
|
42
|
+
fallback?: Children;
|
|
43
|
+
/** Runs `enter` on the elements of added rows and keeps removed rows until `leave` resolves. */
|
|
44
|
+
transition?: ListTransition;
|
|
45
|
+
/** Also run `transition.enter` for the rows of the first render. */
|
|
46
|
+
appear?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Renders one row, once per key. `item()` returns the current item for that key and is reactive
|
|
49
|
+
* when read inside a binding; `index()` returns the row's current position.
|
|
50
|
+
*/
|
|
51
|
+
children: (item: () => T, index: () => number) => Children;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface IndexProps<T> {
|
|
55
|
+
each: ListSource<T>;
|
|
56
|
+
fallback?: Children;
|
|
57
|
+
transition?: ListTransition;
|
|
58
|
+
appear?: boolean;
|
|
59
|
+
/** Renders the row at one position. `item()` is reactive and follows whatever sits at `index`. */
|
|
60
|
+
children: (item: () => T, index: number) => Children;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type RowRender<T> = (item: () => T, index: (() => number) | number) => unknown;
|
|
64
|
+
|
|
65
|
+
// A row is its own scope: one allocation per row instead of two, which shows up when thousands of
|
|
66
|
+
// rows are created at once. `first` and `last` are assigned once the row is built, always in that
|
|
67
|
+
// order: the other order would put rows on a second hidden class and make the code reading them
|
|
68
|
+
// polymorphic.
|
|
69
|
+
class Row<T> extends Scope {
|
|
70
|
+
declare first: Node;
|
|
71
|
+
declare last: Node;
|
|
72
|
+
declare itemSignal: Signal<T> | null;
|
|
73
|
+
declare indexSignal: Signal<number> | null;
|
|
74
|
+
declare key: unknown;
|
|
75
|
+
declare item: T;
|
|
76
|
+
declare index: number;
|
|
77
|
+
|
|
78
|
+
constructor(key: unknown, item: T, index: number) {
|
|
79
|
+
super();
|
|
80
|
+
this.key = key;
|
|
81
|
+
this.item = item;
|
|
82
|
+
this.index = index;
|
|
83
|
+
this.itemSignal = null;
|
|
84
|
+
this.indexSignal = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Keeps the row shape (fields included) alive across full GCs; see the keeper in scope.ts.
|
|
89
|
+
const rowKeeper = new Row<unknown>(null, undefined, 0);
|
|
90
|
+
rowKeeper.first = null as unknown as Node;
|
|
91
|
+
rowKeeper.last = null as unknown as Node;
|
|
92
|
+
Object.defineProperty(Row, "shapeKeeper", { value: rowKeeper });
|
|
93
|
+
|
|
94
|
+
const NO_ROWS: never[] = [];
|
|
95
|
+
|
|
96
|
+
const DETACHED_FILL_MIN = 64;
|
|
97
|
+
|
|
98
|
+
// Plain containers only: removing a dialog, a popover, media, a form control or a custom element from
|
|
99
|
+
// the document, even for a moment, has visible side effects (closing, stopping, lifecycle callbacks).
|
|
100
|
+
const DETACHABLE = new Set([
|
|
101
|
+
"tbody", "thead", "tfoot", "table", "tr", "ul", "ol", "dl", "div", "section", "article", "main",
|
|
102
|
+
"nav", "aside", "header", "footer", "span", "p", "colgroup", "fieldset", "figure", "menu",
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
function canDetach(parent: Node, start: Node, end: Node): boolean {
|
|
106
|
+
if (parent.firstChild !== start || start.nextSibling !== end || parent.lastChild !== end) return false;
|
|
107
|
+
const el = parent as Element;
|
|
108
|
+
if (el.nodeType !== 1 || !el.isConnected || !DETACHABLE.has(el.localName)) return false;
|
|
109
|
+
if (el.hasAttribute("popover") || document.fullscreenElement === el) return false;
|
|
110
|
+
if (el.contains(document.activeElement)) return false;
|
|
111
|
+
// Re-inserting an element restarts its CSS animations. Not every DOM implementation has the API.
|
|
112
|
+
const { getAnimations } = el as Partial<Pick<Element, "getAnimations">>;
|
|
113
|
+
return getAnimations === undefined || el.getAnimations().length === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
function toArray<T>(value: readonly T[] | Iterable<T> | null | undefined): readonly T[] {
|
|
118
|
+
if (value === null || value === undefined) return NO_ROWS;
|
|
119
|
+
if (Array.isArray(value)) return value as readonly T[];
|
|
120
|
+
return Array.from(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// A row needs a start marker only when its first node can change: a top-level reactive child
|
|
124
|
+
// inserts its nodes before its own end comment, so whatever comes first may later be replaced.
|
|
125
|
+
// null means the value renders nothing.
|
|
126
|
+
function leadsWithReactive(value: unknown): boolean | null {
|
|
127
|
+
if (typeof value === "function") return true;
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
for (const v of value) {
|
|
130
|
+
const leads = leadsWithReactive(v);
|
|
131
|
+
if (leads !== null) return leads;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (typeof value === "string" || typeof value === "number" || value instanceof Node) return false;
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function describeKey(key: unknown): string {
|
|
140
|
+
if (typeof key === "string") return JSON.stringify(key);
|
|
141
|
+
if (typeof key === "object" && key !== null) return Object.prototype.toString.call(key);
|
|
142
|
+
return String(key as number | boolean | symbol | null | undefined);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function warnDuplicates(keys: readonly unknown[], owner: string): void {
|
|
146
|
+
const seen = new Set<unknown>();
|
|
147
|
+
const duplicates = new Set<unknown>();
|
|
148
|
+
for (const key of keys) {
|
|
149
|
+
if (seen.has(key)) duplicates.add(key);
|
|
150
|
+
else seen.add(key);
|
|
151
|
+
}
|
|
152
|
+
for (const key of duplicates) {
|
|
153
|
+
console.warn(
|
|
154
|
+
`[PraxisJS] <${owner}> received the key ${describeKey(key)} more than once. Keys must be unique: ` +
|
|
155
|
+
"the first occurrence keeps its row, later ones get a new row on every update.",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Longest increasing subsequence of `sources`, ignoring zeros (rows without a previous position).
|
|
162
|
+
* Returns the positions in `sources` that belong to it, in increasing order. O(n log n).
|
|
163
|
+
*/
|
|
164
|
+
export function longestIncreasingSubsequence(sources: Int32Array): Int32Array {
|
|
165
|
+
const n = sources.length;
|
|
166
|
+
const predecessors = new Int32Array(n);
|
|
167
|
+
const tails = new Int32Array(n);
|
|
168
|
+
let length = 0;
|
|
169
|
+
for (let i = 0; i < n; i++) {
|
|
170
|
+
const value = sources[i];
|
|
171
|
+
if (value === 0) continue;
|
|
172
|
+
if (length > 0 && sources[tails[length - 1]] < value) {
|
|
173
|
+
predecessors[i] = tails[length - 1];
|
|
174
|
+
tails[length++] = i;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
let lo = 0;
|
|
178
|
+
let hi = length;
|
|
179
|
+
while (lo < hi) {
|
|
180
|
+
const mid = (lo + hi) >> 1;
|
|
181
|
+
if (sources[tails[mid]] < value) lo = mid + 1;
|
|
182
|
+
else hi = mid;
|
|
183
|
+
}
|
|
184
|
+
if (lo > 0) predecessors[i] = tails[lo - 1];
|
|
185
|
+
tails[lo] = i;
|
|
186
|
+
if (lo === length) length++;
|
|
187
|
+
}
|
|
188
|
+
const result = new Int32Array(length);
|
|
189
|
+
let k = length > 0 ? tails[length - 1] : 0;
|
|
190
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
191
|
+
result[i] = k;
|
|
192
|
+
k = predecessors[k];
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sameKeys(rows: ReadonlyArray<{ key: unknown }>, from: number, keys: readonly unknown[], to: number, count: number): boolean {
|
|
198
|
+
for (let t = 0; t < count; t++) {
|
|
199
|
+
if (rows[from + t].key !== keys[to + t]) return false;
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function elementsOf(row: { first: Node; last: Node }): HTMLElement[] {
|
|
205
|
+
const elements: HTMLElement[] = [];
|
|
206
|
+
let node: Node | null = row.first;
|
|
207
|
+
while (node !== null) {
|
|
208
|
+
if (node.nodeType === 1) elements.push(node as HTMLElement);
|
|
209
|
+
if (node === row.last) break;
|
|
210
|
+
node = node.nextSibling;
|
|
211
|
+
}
|
|
212
|
+
return elements;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The hooks are always called as methods of the transition object, so class-based transitions keep
|
|
216
|
+
// their `this`.
|
|
217
|
+
function canEnter(transition: ListTransition | undefined): transition is Required<Pick<ListTransition, "enter">> {
|
|
218
|
+
return transition?.enter !== undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function canLeave(transition: ListTransition | undefined): transition is Required<Pick<ListTransition, "leave">> {
|
|
222
|
+
return transition?.leave !== undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function reportLater(error: unknown): void {
|
|
226
|
+
queueMicrotask(() => { throw error; });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// A module function rather than a method: bundles without hydration drop it along with the rest.
|
|
230
|
+
function trackHydration(row: { first: Node; last: Node }, fresh: Node): void {
|
|
231
|
+
onHydrationReplace(fresh, (real) => {
|
|
232
|
+
if (row.first === fresh) row.first = real;
|
|
233
|
+
if (row.last === fresh) row.last = real;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface ListOptions<T> {
|
|
238
|
+
owner: string;
|
|
239
|
+
render: RowRender<T>;
|
|
240
|
+
key: ((item: T, index: number) => unknown) | null;
|
|
241
|
+
/** Index's update, passed in rather than called from ListView so that bundles without Index leave it out. */
|
|
242
|
+
positional: ((view: ListView<T>, parent: Node, items: readonly T[]) => void) | null;
|
|
243
|
+
fallback: unknown;
|
|
244
|
+
transition: ListTransition | undefined;
|
|
245
|
+
appear: boolean;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Members used only inside the class are #private: minifiers shorten their names, which they cannot
|
|
249
|
+
// do for public ones, and this class is most of the list code a bundle ships.
|
|
250
|
+
class ListView<T> {
|
|
251
|
+
rows: Array<Row<T>>;
|
|
252
|
+
readonly end: Comment;
|
|
253
|
+
readonly #start: Comment;
|
|
254
|
+
readonly #options: ListOptions<T>;
|
|
255
|
+
// Created by the first leave transition.
|
|
256
|
+
#leaving: Set<Row<T>> | null;
|
|
257
|
+
#fallbackRow: Row<undefined> | null;
|
|
258
|
+
#errors: unknown[];
|
|
259
|
+
#entering: Array<Row<T>> | null;
|
|
260
|
+
#initial: boolean;
|
|
261
|
+
readonly #scratch: DocumentFragment;
|
|
262
|
+
readonly #passIndex: boolean;
|
|
263
|
+
readonly #itemChanges: boolean;
|
|
264
|
+
|
|
265
|
+
constructor(start: Comment, end: Comment, options: ListOptions<T>) {
|
|
266
|
+
this.rows = NO_ROWS;
|
|
267
|
+
this.end = end;
|
|
268
|
+
this.#start = start;
|
|
269
|
+
this.#options = options;
|
|
270
|
+
this.#leaving = null;
|
|
271
|
+
this.#fallbackRow = null;
|
|
272
|
+
this.#errors = [];
|
|
273
|
+
this.#entering = null;
|
|
274
|
+
this.#initial = true;
|
|
275
|
+
this.#scratch = document.createDocumentFragment();
|
|
276
|
+
this.#passIndex = options.render.length !== 1;
|
|
277
|
+
this.#itemChanges = options.key !== null || options.positional !== null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
update(items: readonly T[]): void {
|
|
281
|
+
const parent = this.end.parentNode;
|
|
282
|
+
if (parent === null) return;
|
|
283
|
+
const { positional } = this.#options;
|
|
284
|
+
if (positional !== null) positional(this, parent, items);
|
|
285
|
+
else this.#updateKeyed(parent, items);
|
|
286
|
+
this.#finish();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── keyed ──────────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
#updateKeyed(parent: Node, items: readonly T[]): void {
|
|
292
|
+
const keyOf = this.#options.key;
|
|
293
|
+
const oldRows = this.rows;
|
|
294
|
+
const oldLen = oldRows.length;
|
|
295
|
+
const newLen = items.length;
|
|
296
|
+
|
|
297
|
+
let keys: readonly unknown[] = items;
|
|
298
|
+
if (keyOf !== null) {
|
|
299
|
+
const computed = new Array<unknown>(newLen);
|
|
300
|
+
for (let i = 0; i < newLen; i++) computed[i] = keyOf(items[i], i);
|
|
301
|
+
keys = computed;
|
|
302
|
+
}
|
|
303
|
+
// Written inline so bundlers fold `process.env.NODE_ENV` and drop the check from production
|
|
304
|
+
// builds. Without a bundler `process` is undefined and the try keeps that from throwing.
|
|
305
|
+
try {
|
|
306
|
+
if (process.env.NODE_ENV !== "production") warnDuplicates(keys, this.#options.owner);
|
|
307
|
+
} catch {
|
|
308
|
+
// no `process`: no dev warnings
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (newLen === 0) {
|
|
312
|
+
if (oldLen > 0) this.removeAll(parent);
|
|
313
|
+
this.rows = NO_ROWS;
|
|
314
|
+
this.showFallback(parent);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const newRows = new Array<Row<T>>(newLen);
|
|
319
|
+
|
|
320
|
+
if (oldLen === 0) {
|
|
321
|
+
this.hideFallback(parent);
|
|
322
|
+
this.insertNew(parent, items, keys, newRows, 0, newLen - 1, this.end);
|
|
323
|
+
this.rows = newRows;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let aStart = 0;
|
|
328
|
+
let bStart = 0;
|
|
329
|
+
let aEnd = oldLen - 1;
|
|
330
|
+
let bEnd = newLen - 1;
|
|
331
|
+
|
|
332
|
+
while (aStart <= aEnd && bStart <= bEnd) {
|
|
333
|
+
const row = oldRows[aStart];
|
|
334
|
+
if (row.key !== keys[bStart]) break;
|
|
335
|
+
this.updateRow(row, items[bStart], bStart);
|
|
336
|
+
newRows[bStart++] = row;
|
|
337
|
+
aStart++;
|
|
338
|
+
}
|
|
339
|
+
while (aStart <= aEnd && bStart <= bEnd) {
|
|
340
|
+
const row = oldRows[aEnd];
|
|
341
|
+
if (row.key !== keys[bEnd]) break;
|
|
342
|
+
this.updateRow(row, items[bEnd], bEnd);
|
|
343
|
+
newRows[bEnd--] = row;
|
|
344
|
+
aEnd--;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (aStart > aEnd) {
|
|
348
|
+
if (bStart <= bEnd) {
|
|
349
|
+
const ref = bEnd + 1 < newLen ? newRows[bEnd + 1].first : this.end;
|
|
350
|
+
this.insertNew(parent, items, keys, newRows, bStart, bEnd, ref);
|
|
351
|
+
}
|
|
352
|
+
} else if (bStart > bEnd) {
|
|
353
|
+
this.removeSpan(parent, oldRows, aStart, aEnd);
|
|
354
|
+
} else if (!this.#moveOneOrSwap(parent, oldRows, items, keys, newRows, aStart, aEnd, bStart, bEnd)) {
|
|
355
|
+
this.#reconcileMiddle(parent, oldRows, items, keys, newRows, aStart, aEnd, bStart, bEnd);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this.rows = newRows;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// After trimming, the most common reorderings (one row moved anywhere, two rows swapped) leave
|
|
362
|
+
// ranges that differ only at their ends. Recognising them needs key comparisons only: no Map,
|
|
363
|
+
// no LIS, and the result is already the minimum number of moves.
|
|
364
|
+
#moveOneOrSwap(
|
|
365
|
+
parent: Node,
|
|
366
|
+
oldRows: Array<Row<T>>,
|
|
367
|
+
items: readonly T[],
|
|
368
|
+
keys: readonly unknown[],
|
|
369
|
+
newRows: Array<Row<T>>,
|
|
370
|
+
aStart: number,
|
|
371
|
+
aEnd: number,
|
|
372
|
+
bStart: number,
|
|
373
|
+
bEnd: number,
|
|
374
|
+
): boolean {
|
|
375
|
+
const span = aEnd - aStart;
|
|
376
|
+
if (span !== bEnd - bStart || span < 1) return false;
|
|
377
|
+
const head = oldRows[aStart];
|
|
378
|
+
const tail = oldRows[aEnd];
|
|
379
|
+
const ref = bEnd + 1 < newRows.length ? newRows[bEnd + 1].first : this.end;
|
|
380
|
+
|
|
381
|
+
if (head.key === keys[bEnd] && sameKeys(oldRows, aStart + 1, keys, bStart, span)) {
|
|
382
|
+
for (let t = 0; t < span; t++) this.#keep(oldRows[aStart + 1 + t], items, newRows, bStart + t);
|
|
383
|
+
this.#keep(head, items, newRows, bEnd);
|
|
384
|
+
moveRange(parent, head.first, head.last, ref);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (tail.key === keys[bStart] && sameKeys(oldRows, aStart, keys, bStart + 1, span)) {
|
|
388
|
+
for (let t = 0; t < span; t++) this.#keep(oldRows[aStart + t], items, newRows, bStart + 1 + t);
|
|
389
|
+
this.#keep(tail, items, newRows, bStart);
|
|
390
|
+
moveRange(parent, tail.first, tail.last, head.first);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
if (
|
|
394
|
+
span > 1 &&
|
|
395
|
+
head.key === keys[bEnd] &&
|
|
396
|
+
tail.key === keys[bStart] &&
|
|
397
|
+
sameKeys(oldRows, aStart + 1, keys, bStart + 1, span - 1)
|
|
398
|
+
) {
|
|
399
|
+
for (let t = 1; t < span; t++) this.#keep(oldRows[aStart + t], items, newRows, bStart + t);
|
|
400
|
+
this.#keep(head, items, newRows, bEnd);
|
|
401
|
+
this.#keep(tail, items, newRows, bStart);
|
|
402
|
+
moveRange(parent, tail.first, tail.last, head.first);
|
|
403
|
+
moveRange(parent, head.first, head.last, ref);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#keep(row: Row<T>, items: readonly T[], newRows: Array<Row<T>>, index: number): void {
|
|
410
|
+
this.updateRow(row, items[index], index);
|
|
411
|
+
newRows[index] = row;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Vue 3 / ivi / Inferno strategy: key → new position map, each surviving row records its old
|
|
415
|
+
// position, and the rows on the longest increasing subsequence of those positions stay put.
|
|
416
|
+
// Everything else moves once, which is the minimum for arbitrary reorders.
|
|
417
|
+
#reconcileMiddle(
|
|
418
|
+
parent: Node,
|
|
419
|
+
oldRows: Array<Row<T>>,
|
|
420
|
+
items: readonly T[],
|
|
421
|
+
keys: readonly unknown[],
|
|
422
|
+
newRows: Array<Row<T>>,
|
|
423
|
+
aStart: number,
|
|
424
|
+
aEnd: number,
|
|
425
|
+
bStart: number,
|
|
426
|
+
bEnd: number,
|
|
427
|
+
): void {
|
|
428
|
+
const newLen = newRows.length;
|
|
429
|
+
const count = bEnd - bStart + 1;
|
|
430
|
+
const keyToIndex = new Map<unknown, number>();
|
|
431
|
+
// Walking backwards leaves the first occurrence of a duplicate key in the map.
|
|
432
|
+
for (let j = bEnd; j >= bStart; j--) keyToIndex.set(keys[j], j);
|
|
433
|
+
|
|
434
|
+
const sources = new Int32Array(count);
|
|
435
|
+
const kept = new Uint8Array(aEnd - aStart + 1);
|
|
436
|
+
let moved = false;
|
|
437
|
+
let maxSoFar = 0;
|
|
438
|
+
let reused = 0;
|
|
439
|
+
for (let i = aStart; i <= aEnd; i++) {
|
|
440
|
+
const row = oldRows[i];
|
|
441
|
+
const j = keyToIndex.get(row.key);
|
|
442
|
+
if (j === undefined || sources[j - bStart] !== 0) continue;
|
|
443
|
+
sources[j - bStart] = i + 1;
|
|
444
|
+
kept[i - aStart] = 1;
|
|
445
|
+
this.#keep(row, items, newRows, j);
|
|
446
|
+
reused++;
|
|
447
|
+
if (j < maxSoFar) moved = true;
|
|
448
|
+
else maxSoFar = j;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const ref = bEnd + 1 < newLen ? newRows[bEnd + 1].first : this.end;
|
|
452
|
+
|
|
453
|
+
if (reused === 0) {
|
|
454
|
+
if (aStart === 0 && aEnd === oldRows.length - 1) this.removeAll(parent);
|
|
455
|
+
else this.removeSpan(parent, oldRows, aStart, aEnd);
|
|
456
|
+
this.insertNew(parent, items, keys, newRows, bStart, bEnd, ref);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
for (let i = aStart; i <= aEnd; i++) {
|
|
461
|
+
if (kept[i - aStart] === 0) this.#removeRow(parent, oldRows[i]);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const stable = moved ? longestIncreasingSubsequence(sources) : null;
|
|
465
|
+
let s = stable === null ? -1 : stable.length - 1;
|
|
466
|
+
for (let k = count - 1; k >= 0; k--) {
|
|
467
|
+
const j = bStart + k;
|
|
468
|
+
const before = j + 1 < newLen ? newRows[j + 1].first : ref;
|
|
469
|
+
if (sources[k] === 0) {
|
|
470
|
+
newRows[j] = this.#insertRow(parent, items[j], keys[j], j, before);
|
|
471
|
+
} else if (stable !== null) {
|
|
472
|
+
if (s >= 0 && stable[s] === k) {
|
|
473
|
+
s--;
|
|
474
|
+
} else {
|
|
475
|
+
const row = newRows[j];
|
|
476
|
+
moveRange(parent, row.first, row.last, before);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ── rows ───────────────────────────────────────────────────────────────────
|
|
483
|
+
|
|
484
|
+
updateRow(row: Row<T>, item: T, index: number): void {
|
|
485
|
+
if (row.item !== item) {
|
|
486
|
+
row.item = item;
|
|
487
|
+
row.itemSignal?.set(item);
|
|
488
|
+
}
|
|
489
|
+
if (row.index !== index) {
|
|
490
|
+
row.index = index;
|
|
491
|
+
row.indexSignal?.set(index);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
#createRow(item: T, key: unknown, index: number, container: Node): Row<T> {
|
|
496
|
+
const row = new Row(key, item, index);
|
|
497
|
+
const itemAccessor = this.#itemChanges
|
|
498
|
+
? () => (row.itemSignal ??= signal(row.item))()
|
|
499
|
+
: () => row.item;
|
|
500
|
+
let indexArg: (() => number) | number = index;
|
|
501
|
+
if (this.#options.positional === null && this.#passIndex) {
|
|
502
|
+
indexArg = () => (row.indexSignal ??= signal(row.index))();
|
|
503
|
+
}
|
|
504
|
+
this.#mount(row, container, this.#options.render, itemAccessor, indexArg);
|
|
505
|
+
if (this.#entering !== null) this.#entering.push(row);
|
|
506
|
+
return row;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
#mount<R, A, B>(row: Row<R>, container: Node, build: (a: A, b: B) => unknown, a: A, b: B): void {
|
|
510
|
+
// The container's last node before this row's, read once the row is built (building it never
|
|
511
|
+
// touches the container) and only when the row is more than one node. Undefined until then.
|
|
512
|
+
let before: ChildNode | null | undefined = undefined;
|
|
513
|
+
const prevScope = swapScope(row);
|
|
514
|
+
try {
|
|
515
|
+
const value = build(a, b);
|
|
516
|
+
if (value instanceof Node && value.nodeType !== 11) {
|
|
517
|
+
container.appendChild(value);
|
|
518
|
+
row.first = value;
|
|
519
|
+
row.last = value;
|
|
520
|
+
if (recording && value.nodeType === 1) trackHydration(row, value);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
before = container.lastChild;
|
|
524
|
+
if (leadsWithReactive(value) === true) container.appendChild(document.createComment(""));
|
|
525
|
+
mountChildren(container, value, row);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
this.#errors.push(error);
|
|
528
|
+
this.#disposeRow(row);
|
|
529
|
+
if (before !== undefined) {
|
|
530
|
+
let node = before === null ? container.firstChild : before.nextSibling;
|
|
531
|
+
while (node !== null) {
|
|
532
|
+
const next = node.nextSibling;
|
|
533
|
+
container.removeChild(node);
|
|
534
|
+
node = next;
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
before = container.lastChild;
|
|
538
|
+
}
|
|
539
|
+
// A unique key never matches again, so the next update renders this item from scratch.
|
|
540
|
+
row.key = {};
|
|
541
|
+
} finally {
|
|
542
|
+
swapScope(prevScope);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const first = before === null ? container.firstChild : before.nextSibling;
|
|
546
|
+
if (first === null) {
|
|
547
|
+
const placeholder = document.createComment("");
|
|
548
|
+
container.appendChild(placeholder);
|
|
549
|
+
row.first = placeholder;
|
|
550
|
+
row.last = placeholder;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
row.first = first;
|
|
554
|
+
row.last = container.lastChild as Node;
|
|
555
|
+
|
|
556
|
+
if (recording) {
|
|
557
|
+
if (row.first.nodeType === 1) trackHydration(row, row.first);
|
|
558
|
+
if (row.last !== row.first && row.last.nodeType === 1) trackHydration(row, row.last);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
#insertRow(parent: Node, item: T, key: unknown, index: number, ref: Node): Row<T> {
|
|
563
|
+
const scratch = this.#scratch;
|
|
564
|
+
const row = this.#createRow(item, key, index, scratch);
|
|
565
|
+
parent.insertBefore(row.first === row.last ? row.first : scratch, ref);
|
|
566
|
+
return row;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
insertNew(
|
|
570
|
+
parent: Node,
|
|
571
|
+
items: readonly T[],
|
|
572
|
+
keys: readonly unknown[],
|
|
573
|
+
newRows: Array<Row<T>>,
|
|
574
|
+
from: number,
|
|
575
|
+
to: number,
|
|
576
|
+
ref: Node,
|
|
577
|
+
): void {
|
|
578
|
+
if (from === to) {
|
|
579
|
+
newRows[from] = this.#insertRow(parent, items[from], keys[from], from, ref);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// Building rows straight into a parent that is attached to the document makes every insertion
|
|
583
|
+
// pay for the live tree, so they go into a detached fragment first. When the list was empty and
|
|
584
|
+
// owns all of its parent, filling the parent while it is briefly detached is cheaper still: no
|
|
585
|
+
// node has to move out of a fragment afterwards.
|
|
586
|
+
const owner = parent.parentNode;
|
|
587
|
+
if (to - from + 1 >= DETACHED_FILL_MIN && ref === this.end && owner !== null && canDetach(parent, this.#start, this.end)) {
|
|
588
|
+
const next = parent.nextSibling;
|
|
589
|
+
owner.removeChild(parent);
|
|
590
|
+
parent.removeChild(this.end);
|
|
591
|
+
try {
|
|
592
|
+
for (let j = from; j <= to; j++) newRows[j] = this.#createRow(items[j], keys[j], j, parent);
|
|
593
|
+
} finally {
|
|
594
|
+
parent.appendChild(this.end);
|
|
595
|
+
owner.insertBefore(parent, next);
|
|
596
|
+
}
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const fragment = document.createDocumentFragment();
|
|
600
|
+
for (let j = from; j <= to; j++) newRows[j] = this.#createRow(items[j], keys[j], j, fragment);
|
|
601
|
+
parent.insertBefore(fragment, ref);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
#disposeRow(row: Scope): void {
|
|
605
|
+
try {
|
|
606
|
+
row.dispose();
|
|
607
|
+
} catch (error) {
|
|
608
|
+
this.#errors.push(error);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
#removeRow(parent: Node, row: Row<T>): void {
|
|
613
|
+
const { transition } = this.#options;
|
|
614
|
+
if (canLeave(transition)) {
|
|
615
|
+
this.#leave(row, transition);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
this.#disposeRow(row);
|
|
619
|
+
removeRange(parent, row.first, row.last);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Rows in [from, to] are contiguous in the DOM unless some are still leaving, so without a leave
|
|
623
|
+
// transition one walk from the first boundary to the last removes them all.
|
|
624
|
+
removeSpan(parent: Node, rows: Array<Row<T>>, from: number, to: number): void {
|
|
625
|
+
if (canLeave(this.#options.transition)) {
|
|
626
|
+
for (let i = from; i <= to; i++) this.#removeRow(parent, rows[i]);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
for (let i = from; i <= to; i++) this.#disposeRow(rows[i]);
|
|
630
|
+
removeRange(parent, rows[from].first, rows[to].last);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
removeAll(parent: Node): void {
|
|
634
|
+
const rows = this.rows;
|
|
635
|
+
if (canLeave(this.#options.transition) || parent.firstChild !== this.#start || parent.lastChild !== this.end) {
|
|
636
|
+
this.removeSpan(parent, rows, 0, rows.length - 1);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
// The list owns every child of its parent: clearing it in one call is far cheaper than removing
|
|
640
|
+
// rows one by one, then the two boundary comments go back in.
|
|
641
|
+
for (const row of rows) this.#disposeRow(row);
|
|
642
|
+
parent.textContent = "";
|
|
643
|
+
parent.appendChild(this.#start);
|
|
644
|
+
parent.appendChild(this.end);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
#leave(row: Row<T>, transition: Required<Pick<ListTransition, "leave">>): void {
|
|
648
|
+
const leaving = (this.#leaving ??= new Set());
|
|
649
|
+
leaving.add(row);
|
|
650
|
+
const done = () => {
|
|
651
|
+
if (!leaving.delete(row)) return;
|
|
652
|
+
this.#disposeRow(row);
|
|
653
|
+
const parent = row.first.parentNode;
|
|
654
|
+
if (parent !== null) removeRange(parent, row.first, row.last);
|
|
655
|
+
this.flushErrors(reportLater);
|
|
656
|
+
};
|
|
657
|
+
let pending: Promise<unknown>;
|
|
658
|
+
try {
|
|
659
|
+
pending = Promise.all(elementsOf(row).map((el) => transition.leave(el)));
|
|
660
|
+
} catch (error) {
|
|
661
|
+
pending = Promise.reject(error as Error);
|
|
662
|
+
}
|
|
663
|
+
pending.then(done, (error: unknown) => {
|
|
664
|
+
done();
|
|
665
|
+
reportLater(error);
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ── fallback ───────────────────────────────────────────────────────────────
|
|
670
|
+
|
|
671
|
+
showFallback(parent: Node): void {
|
|
672
|
+
const { fallback } = this.#options;
|
|
673
|
+
if (fallback === undefined || fallback === null || this.#fallbackRow !== null) return;
|
|
674
|
+
const row = new Row(null, undefined, 0);
|
|
675
|
+
this.#mount(row, this.#scratch, () => fallback, null, null);
|
|
676
|
+
parent.insertBefore(row.first === row.last ? row.first : this.#scratch, this.end);
|
|
677
|
+
this.#fallbackRow = row;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
hideFallback(parent: Node): void {
|
|
681
|
+
const row = this.#fallbackRow;
|
|
682
|
+
if (row === null) return;
|
|
683
|
+
this.#fallbackRow = null;
|
|
684
|
+
this.#disposeRow(row);
|
|
685
|
+
removeRange(parent, row.first, row.last);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
beginPass(): void {
|
|
691
|
+
const { transition, appear } = this.#options;
|
|
692
|
+
if (canEnter(transition) && (!this.#initial || appear)) this.#entering = [];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
#finish(): void {
|
|
696
|
+
this.#initial = false;
|
|
697
|
+
const entering = this.#entering;
|
|
698
|
+
const { transition } = this.#options;
|
|
699
|
+
this.#entering = null;
|
|
700
|
+
if (entering !== null && canEnter(transition)) {
|
|
701
|
+
for (const row of entering) {
|
|
702
|
+
for (const el of elementsOf(row)) {
|
|
703
|
+
try {
|
|
704
|
+
transition.enter(el).catch(reportLater);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
reportLater(error);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
flushErrors(report: (error: unknown) => void): void {
|
|
714
|
+
const errors = this.#errors;
|
|
715
|
+
if (errors.length === 0) return;
|
|
716
|
+
this.#errors = [];
|
|
717
|
+
report(errors.length === 1 ? errors[0] : new AggregateError(errors, `[PraxisJS] Multiple errors while updating <${this.#options.owner}>`));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
dispose(): void {
|
|
721
|
+
for (const row of this.rows) this.#disposeRow(row);
|
|
722
|
+
const leaving = this.#leaving;
|
|
723
|
+
if (leaving !== null) {
|
|
724
|
+
for (const row of leaving) this.#disposeRow(row);
|
|
725
|
+
leaving.clear();
|
|
726
|
+
}
|
|
727
|
+
this.rows = NO_ROWS;
|
|
728
|
+
if (this.#fallbackRow !== null) this.#disposeRow(this.#fallbackRow);
|
|
729
|
+
this.#fallbackRow = null;
|
|
730
|
+
this.flushErrors((error) => { throw error; });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ── positional (Index) ─────────────────────────────────────────────────────
|
|
735
|
+
|
|
736
|
+
function updatePositional<T>(view: ListView<T>, parent: Node, items: readonly T[]): void {
|
|
737
|
+
const oldRows = view.rows;
|
|
738
|
+
const oldLen = oldRows.length;
|
|
739
|
+
const newLen = items.length;
|
|
740
|
+
|
|
741
|
+
if (newLen === 0) {
|
|
742
|
+
if (oldLen > 0) view.removeAll(parent);
|
|
743
|
+
view.rows = NO_ROWS;
|
|
744
|
+
view.showFallback(parent);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (oldLen === 0) view.hideFallback(parent);
|
|
748
|
+
|
|
749
|
+
const common = Math.min(oldLen, newLen);
|
|
750
|
+
for (let i = 0; i < common; i++) view.updateRow(oldRows[i], items[i], i);
|
|
751
|
+
|
|
752
|
+
if (newLen === oldLen) return;
|
|
753
|
+
const newRows = oldRows.slice(0, common);
|
|
754
|
+
if (newLen > oldLen) {
|
|
755
|
+
newRows.length = newLen;
|
|
756
|
+
view.insertNew(parent, items, items, newRows, oldLen, newLen - 1, view.end);
|
|
757
|
+
} else {
|
|
758
|
+
view.removeSpan(parent, oldRows, newLen, oldLen - 1);
|
|
759
|
+
}
|
|
760
|
+
view.rows = newRows;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function resolveRender<T>(children: unknown, owner: string): RowRender<T> | null {
|
|
764
|
+
const candidate = Array.isArray(children) ? (children as unknown[]).find((c) => typeof c === "function") : children;
|
|
765
|
+
if (typeof candidate === "function") return candidate as RowRender<T>;
|
|
766
|
+
try {
|
|
767
|
+
if (process.env.NODE_ENV !== "production") console.error(`[PraxisJS] <${owner}> expects a function child that renders one row.`);
|
|
768
|
+
} catch {
|
|
769
|
+
// no `process`: no dev warnings
|
|
770
|
+
}
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function resolveKey<T>(by: ForProps<T>["by"]): ((item: T, index: number) => unknown) | null {
|
|
775
|
+
if (by === undefined) return null;
|
|
776
|
+
if (typeof by === "function") return by;
|
|
777
|
+
return (item: T) => (item as Record<PropertyKey, unknown>)[by];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function mountList<T>(
|
|
781
|
+
instance: object,
|
|
782
|
+
each: ListSource<T>,
|
|
783
|
+
options: ListOptions<T>,
|
|
784
|
+
): Node {
|
|
785
|
+
const scope = getCurrentScope();
|
|
786
|
+
const start = document.createComment("");
|
|
787
|
+
const end = document.createComment("");
|
|
788
|
+
// The list's boundaries are its component's too, so mountComponent adds no comments of its own.
|
|
789
|
+
setComponentStart(instance, start);
|
|
790
|
+
setComponentAnchor(instance, end);
|
|
791
|
+
|
|
792
|
+
// The first update runs before mountComponent attaches anything, so the rows are built in front
|
|
793
|
+
// of `end` inside this fragment, which is what render() returns.
|
|
794
|
+
const fragment = document.createDocumentFragment();
|
|
795
|
+
fragment.appendChild(start);
|
|
796
|
+
fragment.appendChild(end);
|
|
797
|
+
|
|
798
|
+
const view = new ListView(start, end, options);
|
|
799
|
+
const run = () => {
|
|
800
|
+
const items = toArray(typeof each === "function" ? each() : each);
|
|
801
|
+
untrack(() => {
|
|
802
|
+
view.beginPass();
|
|
803
|
+
batch(() => { view.update(items); });
|
|
804
|
+
});
|
|
805
|
+
};
|
|
806
|
+
let initial = true;
|
|
807
|
+
scope.effect(() => {
|
|
808
|
+
if (!initial) {
|
|
809
|
+
run();
|
|
810
|
+
view.flushErrors((error) => { throw error; });
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
initial = false;
|
|
814
|
+
// A throw here would escape scope.effect() before the effect is registered for disposal, and
|
|
815
|
+
// mountComponent swallows render() errors unless the component defines onError, so the first
|
|
816
|
+
// pass reports asynchronously instead.
|
|
817
|
+
try {
|
|
818
|
+
run();
|
|
819
|
+
} catch (error) {
|
|
820
|
+
reportLater(error);
|
|
821
|
+
}
|
|
822
|
+
view.flushErrors(reportLater);
|
|
823
|
+
});
|
|
824
|
+
scope.add(() => { view.dispose(); });
|
|
825
|
+
return fragment;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Keyed list. Rows are created once per key and kept (with their DOM, state and effects) while the
|
|
830
|
+
* key stays in the list; reorders move the existing nodes with the minimum number of moves.
|
|
831
|
+
*
|
|
832
|
+
* @example
|
|
833
|
+
* <For each={() => this.todos} by={(todo) => todo.id} fallback={<p>Nothing to do</p>}>
|
|
834
|
+
* {(todo, index) => <li>{() => `${index() + 1}. ${todo().title}`}</li>}
|
|
835
|
+
* </For>
|
|
836
|
+
*/
|
|
837
|
+
export class For<T> extends RootComponent<ForProps<T>> {
|
|
838
|
+
static readonly __isComponent = true as const;
|
|
839
|
+
static readonly __isStateless = true;
|
|
840
|
+
static readonly __jsxRawProps = true as const;
|
|
841
|
+
|
|
842
|
+
render(): Node | null {
|
|
843
|
+
const props = getComponentProps(this) as ForProps<T>;
|
|
844
|
+
const render = resolveRender<T>(props.children, "For");
|
|
845
|
+
if (render === null) return null;
|
|
846
|
+
return mountList(this, props.each, {
|
|
847
|
+
owner: "For",
|
|
848
|
+
render,
|
|
849
|
+
key: resolveKey(props.by),
|
|
850
|
+
positional: null,
|
|
851
|
+
fallback: props.fallback,
|
|
852
|
+
transition: props.transition,
|
|
853
|
+
appear: props.appear === true,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Positional list, for primitives and lists without stable identity. Row `i` is created once and
|
|
860
|
+
* kept; when the value at `i` changes only the bindings reading `item()` update.
|
|
861
|
+
*
|
|
862
|
+
* @example
|
|
863
|
+
* <Index each={() => this.tags}>
|
|
864
|
+
* {(tag, i) => <input value={() => tag()} onInput={(e) => { this.rename(i, e) }} />}
|
|
865
|
+
* </Index>
|
|
866
|
+
*/
|
|
867
|
+
export class Index<T> extends RootComponent<IndexProps<T>> {
|
|
868
|
+
static readonly __isComponent = true as const;
|
|
869
|
+
static readonly __isStateless = true;
|
|
870
|
+
static readonly __jsxRawProps = true as const;
|
|
871
|
+
|
|
872
|
+
render(): Node | null {
|
|
873
|
+
const props = getComponentProps(this) as IndexProps<T>;
|
|
874
|
+
const render = resolveRender<T>(props.children, "Index");
|
|
875
|
+
if (render === null) return null;
|
|
876
|
+
return mountList(this, props.each, {
|
|
877
|
+
owner: "Index",
|
|
878
|
+
render,
|
|
879
|
+
key: null,
|
|
880
|
+
positional: updatePositional,
|
|
881
|
+
fallback: props.fallback,
|
|
882
|
+
transition: props.transition,
|
|
883
|
+
appear: props.appear === true,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|