@praxisjs/runtime 0.7.1 → 0.8.1

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