mithril-lynx 2.5.0 → 2.6.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/.omo/run-continuation/ses_f48265d07ffenKG0GAz9ZRFvY6.json +10 -0
- package/package.json +5 -1
- package/src/apply-patch.js +48 -19
- package/src/backends/virtual-backend.js +18 -4
- package/src/fake-dom.js +13 -10
- package/src/list-cell.d.ts +20 -0
- package/src/list-cell.js +73 -0
- package/src/list-support.d.ts +12 -9
- package/src/list-support.js +119 -115
- package/src/patch-protocol.js +47 -6
- package/test/list.test.ts +68 -40
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mithril-lynx",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Mithril.js on Lynx: real mithril/render/render.js driven through a Lynx-backed fake DOM, with an explicit single commit hook (no conditional global flush) and three reload modes (data-light, structural-light, full). A complete rewrite of the previous mithril-lynx (0.0.x).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"./list-support": {
|
|
37
37
|
"types": "./src/list-support.d.ts",
|
|
38
38
|
"default": "./src/list-support.js"
|
|
39
|
+
},
|
|
40
|
+
"./list-cell": {
|
|
41
|
+
"types": "./src/list-cell.d.ts",
|
|
42
|
+
"default": "./src/list-cell.js"
|
|
39
43
|
}
|
|
40
44
|
},
|
|
41
45
|
"scripts": {
|
package/src/apply-patch.js
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
// low-level interpreter of the flat op array straight onto the real
|
|
7
7
|
// Element PAPI, in the spirit of ReactLynx's own `snapshotPatchApply.js`
|
|
8
8
|
// (see rspeedy-react-analysis/LYNX_PAPI_SPEC.md §4.3): a switch over op
|
|
9
|
-
// codes, one real PAPI call per case, nothing else.
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// docs/native-papi/papi-06-virtualized-lists.md in mithril-lynx-ui
|
|
9
|
+
// codes, one real PAPI call per case, nothing else. Op.CreateList's own
|
|
10
|
+
// cell content (list-support.js) is no exception to that: componentAtIndex
|
|
11
|
+
// replays ops the background thread already computed (list-cell.js), the
|
|
12
|
+
// same way this function replays the app's own top-level tree — see
|
|
13
|
+
// docs/native-papi/papi-06-virtualized-lists.md in mithril-lynx-ui.
|
|
14
14
|
//
|
|
15
15
|
// The exact `__Create*`/pageId contract below (one `pageId` shared by every
|
|
16
16
|
// element on a page, `__CreateView`/`__CreateText`/generic `__CreateElement`
|
|
@@ -191,16 +191,41 @@ function registerGestureDetector(handle, id, gestureId, gestureType, arenaPolicy
|
|
|
191
191
|
* @param {number} pageId - `__GetElementUniqueID(pageElement)` of the real
|
|
192
192
|
* page this applier is attached to. Every element this applier creates
|
|
193
193
|
* belongs to that one page — see CONTRACT.md / lynx-mithril-shim.js.
|
|
194
|
+
* @param {object} [options]
|
|
195
|
+
* @param {Function} [options.onEvent]
|
|
196
|
+
* @param {boolean} [options.flush] - Whether `applyPatch` calls the bare,
|
|
197
|
+
* whole-page `__FlushElementTree()` after applying its ops. Defaults to
|
|
198
|
+
* `true` — the right default for the ONE real top-level applier per page
|
|
199
|
+
* (main-thread.js's own use). `false` for a per-cell applier
|
|
200
|
+
* (list-support.js): a list cell's real commit point is the list-specific
|
|
201
|
+
* `__FlushElementTree(wrapperHandle, {triggerLayout, operationID,
|
|
202
|
+
* elementID, listID})` call list-support.js already makes right after —
|
|
203
|
+
* calling the bare, whole-page flush too, from inside native's own
|
|
204
|
+
* synchronous componentAtIndex callback, is a second, unrelated flush this
|
|
205
|
+
* applier was never meant to trigger on that call site's behalf.
|
|
194
206
|
*/
|
|
195
|
-
export function createPatchApplier(pageId, { onEvent } = {}) {
|
|
207
|
+
export function createPatchApplier(pageId, { onEvent, flush = true } = {}) {
|
|
196
208
|
// id (as allocated by the background's virtual backend) -> real PAPI
|
|
197
209
|
// element handle. id 0 is reserved for "the page itself" (see
|
|
198
210
|
// fake-dom.js's LynxDocument) — pre-seeded here so the very first
|
|
199
211
|
// InsertBefore/AppendChild targeting id 0 has somewhere real to land.
|
|
200
212
|
const handles = new Map();
|
|
213
|
+
// list id -> its setCells(cells) function (list-support.js) — kept here,
|
|
214
|
+
// not as a property on the list's own handle: a real native list handle
|
|
215
|
+
// does not reliably hold a custom property across calls (confirmed on
|
|
216
|
+
// device), only `handles` (a plain Map) does.
|
|
217
|
+
const listSetters = new Map();
|
|
218
|
+
|
|
219
|
+
/** General form: seed the mapping for any id, not just the page root —
|
|
220
|
+
* list-support.js uses this to alias a list-cell.js `containerId` (an
|
|
221
|
+
* off-tree id from the background thread's OWN document) to the real
|
|
222
|
+
* native wrapper element it created for that cell. */
|
|
223
|
+
function registerRoot(id, handle) {
|
|
224
|
+
handles.set(id, handle);
|
|
225
|
+
}
|
|
201
226
|
|
|
202
227
|
function registerPageRoot(pageElementHandle) {
|
|
203
|
-
|
|
228
|
+
registerRoot(0, pageElementHandle);
|
|
204
229
|
}
|
|
205
230
|
|
|
206
231
|
function createElementHandle(tag) {
|
|
@@ -210,12 +235,15 @@ export function createPatchApplier(pageId, { onEvent } = {}) {
|
|
|
210
235
|
}
|
|
211
236
|
|
|
212
237
|
/**
|
|
213
|
-
* Applies one commit's worth of ops, then
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
238
|
+
* Applies one commit's worth of ops, then — unless this applier was
|
|
239
|
+
* created with `flush: false` (see this function's own constructor
|
|
240
|
+
* options above) — flushes exactly once with the bare, whole-page
|
|
241
|
+
* `__FlushElementTree()`; that call is the real commit for the ONE
|
|
242
|
+
* top-level applier per page, never looked up through a global. A
|
|
243
|
+
* per-cell applier (list-support.js) passes `flush: false` and issues
|
|
244
|
+
* its own list-specific `__FlushElementTree(wrapperHandle, {...})` call
|
|
245
|
+
* afterward instead — that one, not this one, is that cell's real
|
|
246
|
+
* commit point.
|
|
219
247
|
*/
|
|
220
248
|
function applyPatch(ops) {
|
|
221
249
|
for (let i = 0; i < ops.length; ) {
|
|
@@ -363,29 +391,30 @@ export function createPatchApplier(pageId, { onEvent } = {}) {
|
|
|
363
391
|
}
|
|
364
392
|
case Op.CreateList: {
|
|
365
393
|
const id = ops[i++];
|
|
366
|
-
const rendererKey = ops[i++];
|
|
367
394
|
const scrollOrientation = ops[i++];
|
|
368
395
|
const listType = ops[i++];
|
|
369
396
|
const spanCount = ops[i++];
|
|
370
|
-
|
|
397
|
+
const { handle, setCells } = createNativeList(pageId, scrollOrientation, listType, spanCount, createPatchApplier, onEvent);
|
|
398
|
+
handles.set(id, handle);
|
|
399
|
+
listSetters.set(id, setCells);
|
|
371
400
|
break;
|
|
372
401
|
}
|
|
373
402
|
case Op.SetListItems: {
|
|
374
403
|
const id = ops[i++];
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
listHandle.__setItems(JSON.parse(itemsJSON));
|
|
404
|
+
const cellsJSON = ops[i++];
|
|
405
|
+
listSetters.get(id)(JSON.parse(cellsJSON));
|
|
378
406
|
break;
|
|
379
407
|
}
|
|
380
408
|
default:
|
|
381
409
|
throw new Error(`[mithril-lynx] Unknown patch opcode: ${opcode}`);
|
|
382
410
|
}
|
|
383
411
|
}
|
|
384
|
-
__FlushElementTree();
|
|
412
|
+
if (flush) __FlushElementTree();
|
|
385
413
|
}
|
|
386
414
|
|
|
387
415
|
return {
|
|
388
416
|
registerPageRoot,
|
|
417
|
+
registerRoot,
|
|
389
418
|
applyPatch,
|
|
390
419
|
/** The real PAPI element handle for a given background-side id, or
|
|
391
420
|
* `undefined` if nothing was ever created for it. Exists for tests
|
|
@@ -75,13 +75,13 @@ export function createVirtualBackend() {
|
|
|
75
75
|
removeGestureDetector(id, gestureId) {
|
|
76
76
|
pushOp(ops, Op.RemoveGestureDetector, id, gestureId);
|
|
77
77
|
},
|
|
78
|
-
createList(
|
|
78
|
+
createList(scrollOrientation, listType, spanCount) {
|
|
79
79
|
const id = nextId++;
|
|
80
|
-
pushOp(ops, Op.CreateList, id,
|
|
80
|
+
pushOp(ops, Op.CreateList, id, scrollOrientation, listType, spanCount);
|
|
81
81
|
return id;
|
|
82
82
|
},
|
|
83
|
-
setListItems(id,
|
|
84
|
-
pushOp(ops, Op.SetListItems, id, JSON.stringify(
|
|
83
|
+
setListItems(id, cells) {
|
|
84
|
+
pushOp(ops, Op.SetListItems, id, JSON.stringify(cells));
|
|
85
85
|
},
|
|
86
86
|
/** Drains and returns the accumulated ops. Called once per commit. */
|
|
87
87
|
takeOps() {
|
|
@@ -90,5 +90,19 @@ export function createVirtualBackend() {
|
|
|
90
90
|
ops = [];
|
|
91
91
|
return out;
|
|
92
92
|
},
|
|
93
|
+
/**
|
|
94
|
+
* Runs `fn` (a DOM mutation against a node from THIS backend's own
|
|
95
|
+
* document — same id space as everything else, so event dispatch
|
|
96
|
+
* keeps working normally) and returns just the ops it produced,
|
|
97
|
+
* removing them from the shared buffer so they never also go out
|
|
98
|
+
* with the next `takeOps()`. Used by list-cell.js to render one list
|
|
99
|
+
* item off-tree and ship its construction ops separately, instead of
|
|
100
|
+
* as part of the app's own visible-tree patch.
|
|
101
|
+
*/
|
|
102
|
+
captureOps(fn) {
|
|
103
|
+
const start = ops.length;
|
|
104
|
+
fn();
|
|
105
|
+
return ops.splice(start, ops.length - start);
|
|
106
|
+
},
|
|
93
107
|
};
|
|
94
108
|
}
|
package/src/fake-dom.js
CHANGED
|
@@ -178,7 +178,7 @@ export class LynxElement extends LynxContainerNode {
|
|
|
178
178
|
this.tag = tag;
|
|
179
179
|
this.namespaceURI = ns;
|
|
180
180
|
this._id = listConfig
|
|
181
|
-
? backend.createList(listConfig.
|
|
181
|
+
? backend.createList(listConfig.scrollOrientation, listConfig.listType, listConfig.spanCount)
|
|
182
182
|
: ns
|
|
183
183
|
? backend.createElementNS(ns, tag)
|
|
184
184
|
: backend.createElement(tag);
|
|
@@ -440,22 +440,25 @@ export class LynxDocument extends LynxContainerNode {
|
|
|
440
440
|
|
|
441
441
|
/**
|
|
442
442
|
* A native virtualized list — see patch-protocol.js's Op.CreateList.
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
* docs/native-papi/papi-06-virtualized-lists.md in mithril-lynx-ui
|
|
446
|
-
* the renderer function itself can't cross the thread boundary, only
|
|
447
|
-
* this string key can. Call `.setListItems(items)` on the result to
|
|
448
|
-
* populate it — items must be JSON-serializable, since they DO cross
|
|
449
|
-
* the boundary, as data.
|
|
443
|
+
* Populate it via `.setListItems(cells)` (list-cell.js builds `cells`
|
|
444
|
+
* from an app's own `items`/`renderItem`) — see
|
|
445
|
+
* docs/native-papi/papi-06-virtualized-lists.md in mithril-lynx-ui.
|
|
450
446
|
*/
|
|
451
|
-
createNativeList(
|
|
447
|
+
createNativeList(options = {}) {
|
|
452
448
|
return new LynxElement(this, this._backend, "list", undefined, {
|
|
453
|
-
rendererKey,
|
|
454
449
|
scrollOrientation: options.scrollOrientation ?? "vertical",
|
|
455
450
|
listType: options.listType ?? "single",
|
|
456
451
|
spanCount: options.spanCount ?? 1,
|
|
457
452
|
});
|
|
458
453
|
}
|
|
454
|
+
|
|
455
|
+
/** Delegates to the backend — see virtual-backend.js's own captureOps
|
|
456
|
+
* for what this is for. Kept behind LynxDocument's public surface like
|
|
457
|
+
* every other backend interaction in this file, rather than exposing
|
|
458
|
+
* `_backend` itself to callers (list-cell.js). */
|
|
459
|
+
captureOps(fn) {
|
|
460
|
+
return this._backend.captureOps(fn);
|
|
461
|
+
}
|
|
459
462
|
}
|
|
460
463
|
|
|
461
464
|
export function createLynxDocument(backend) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Ambient declaration for the ESM src/list-cell.js — the background-thread
|
|
2
|
+
// half of native list support. mithril-lynx-ui's <List>/<FeedList> call
|
|
3
|
+
// this once per item, from the SAME thread/document as the rest of the
|
|
4
|
+
// app's own tree — see that file's own header for why.
|
|
5
|
+
|
|
6
|
+
export interface ListCell {
|
|
7
|
+
typeKey: string;
|
|
8
|
+
containerId: number;
|
|
9
|
+
ops: unknown[];
|
|
10
|
+
rootChildIds: number[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function renderListCell(
|
|
14
|
+
document: unknown,
|
|
15
|
+
render: (dom: unknown, vnodes: unknown[], redraw: () => void) => void,
|
|
16
|
+
redraw: () => void,
|
|
17
|
+
renderItem: (item: unknown, index: number) => unknown,
|
|
18
|
+
item: unknown,
|
|
19
|
+
index: number,
|
|
20
|
+
): ListCell;
|
package/src/list-cell.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/list-cell.js
|
|
2
|
+
//
|
|
3
|
+
// Runs ONLY on the background thread — the counterpart to list-support.js's
|
|
4
|
+
// main-thread half. Turns one `renderItem(item, index)` call into a
|
|
5
|
+
// self-contained set of construction ops, using the app's OWN document/
|
|
6
|
+
// backend/render (the same ones background.js already created for the rest
|
|
7
|
+
// of the tree) — never a separate, isolated render pipeline. That's the
|
|
8
|
+
// whole point: the item's real fake-dom nodes end up registered in the same
|
|
9
|
+
// `document._nodesById` map as everything else in the app, so a forwarded
|
|
10
|
+
// native event for one of them dispatches through `onEventFromMainThread`
|
|
11
|
+
// exactly like any other element's event already does. No extra reporting
|
|
12
|
+
// convention needed.
|
|
13
|
+
//
|
|
14
|
+
// The main thread never calls renderItem() itself — see list-support.js.
|
|
15
|
+
|
|
16
|
+
import { Op, forEachOp } from "./patch-protocol.js";
|
|
17
|
+
|
|
18
|
+
function typeKeyOf(vnode) {
|
|
19
|
+
return typeof vnode.tag === "string" ? vnode.tag : (vnode.tag && vnode.tag.name) || "default";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The real handle ids inserted directly under `containerId` — what a
|
|
23
|
+
* recycled cell on the main thread needs to remove before it can attach a
|
|
24
|
+
* different item's content into the same native wrapper (see
|
|
25
|
+
* list-support.js's clearWrapperChildren). Scanning the ops after the fact,
|
|
26
|
+
* instead of tracking during render, keeps this file from needing any
|
|
27
|
+
* backend-internal access beyond `captureOps` itself. */
|
|
28
|
+
function findTopLevelChildIds(ops, containerId) {
|
|
29
|
+
const ids = [];
|
|
30
|
+
forEachOp(ops, (opcode, args) => {
|
|
31
|
+
if (opcode === Op.InsertBefore && args[0] === containerId) ids.push(args[1]);
|
|
32
|
+
});
|
|
33
|
+
return ids;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {import("./fake-dom.js").LynxDocument} document - the app's own
|
|
38
|
+
* document (e.g. `vnode.dom.ownerDocument` from inside a component) — NOT
|
|
39
|
+
* a separate one. Sharing it is what keeps a list cell's ids in the same
|
|
40
|
+
* `_nodesById` space as the rest of the app.
|
|
41
|
+
* @param {(dom: object, vnodes: unknown[], redraw: () => void) => void} render
|
|
42
|
+
* - a `mithril-runtime/render/render.js` instance. Callers typically keep
|
|
43
|
+
* one shared instance per `<List>` (see mithril-lynx-ui's list.js), not
|
|
44
|
+
* one per cell — render() is designed to manage multiple independent
|
|
45
|
+
* containers safely.
|
|
46
|
+
* @param {() => void} redraw - forwarded as render()'s own redraw callback,
|
|
47
|
+
* so a `ontap` (etc.) handler inside the rendered cell triggers a REAL
|
|
48
|
+
* app-wide redraw afterward, exactly like any other event in the app —
|
|
49
|
+
* see mithril-lynx/mount-redraw's `redraw()`.
|
|
50
|
+
* @param {(item: unknown, index: number) => unknown} renderItem
|
|
51
|
+
* @param {unknown} item
|
|
52
|
+
* @param {number} index
|
|
53
|
+
* @returns {{ typeKey: string, containerId: number, ops: unknown[], rootChildIds: number[] }}
|
|
54
|
+
*/
|
|
55
|
+
export function renderListCell(document, render, redraw, renderItem, item, index) {
|
|
56
|
+
// An ordinary element in the SAME document, never inserted into the
|
|
57
|
+
// visible tree (no `appendChild`/`insertBefore` call targets it) — its
|
|
58
|
+
// own `Op.CreateElement` never gets captured below since it's created
|
|
59
|
+
// OUTSIDE the capture, before there's anything to record; only what
|
|
60
|
+
// render() does INSIDE it is captured. The main thread's list-support.js
|
|
61
|
+
// treats `containerId` as an alias for whatever real native wrapper it
|
|
62
|
+
// creates for this cell, the same way apply-patch.js's top-level applier
|
|
63
|
+
// treats id 0 as an alias for the real page element.
|
|
64
|
+
const container = document.createElement("list-cell-root");
|
|
65
|
+
const vnode = renderItem(item, index);
|
|
66
|
+
const typeKey = typeKeyOf(vnode);
|
|
67
|
+
|
|
68
|
+
const ops = document.captureOps(() => {
|
|
69
|
+
render(container, [vnode], redraw);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return { typeKey, containerId: container._id, ops, rootChildIds: findTopLevelChildIds(ops, container._id) };
|
|
73
|
+
}
|
package/src/list-support.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
// Ambient declaration for the ESM src/list-support.js
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// per list your app uses — see that file's own header, and
|
|
6
|
-
// docs/native-papi/papi-06-virtualized-lists.md in mithril-lynx-ui, for
|
|
7
|
-
// why the render function itself has to be registered here rather than
|
|
8
|
-
// passed in from background.ts directly.
|
|
1
|
+
// Ambient declaration for the ESM src/list-support.js — the main-thread
|
|
2
|
+
// half of native list support. Not meant to be imported directly by app
|
|
3
|
+
// code; apply-patch.js's own Op.CreateList case is the only caller. See
|
|
4
|
+
// list-cell.d.ts for the background-thread half an app actually uses.
|
|
9
5
|
|
|
10
|
-
export function
|
|
6
|
+
export function createNativeList(
|
|
7
|
+
pageId: number,
|
|
8
|
+
scrollOrientation: string,
|
|
9
|
+
listType: string,
|
|
10
|
+
spanCount: number,
|
|
11
|
+
createPatchApplier: (pageId: number, options?: { onEvent?: Function; flush?: boolean }) => object,
|
|
12
|
+
onEvent?: Function,
|
|
13
|
+
): { handle: unknown; setCells: (cells: unknown[]) => void };
|
package/src/list-support.js
CHANGED
|
@@ -1,153 +1,149 @@
|
|
|
1
1
|
// src/list-support.js
|
|
2
2
|
//
|
|
3
|
-
// Runs ONLY on the main thread —
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// are separate JS engine instances — no shared closures, only serializable
|
|
9
|
-
// messages), so an app registers it here by a string key instead, and
|
|
10
|
-
// mithril-lynx-ui's List component references that same key from
|
|
11
|
-
// background.ts.
|
|
3
|
+
// Runs ONLY on the main thread — the counterpart to list-cell.js's
|
|
4
|
+
// background-thread half. Wires up the native list contract
|
|
5
|
+
// (__CreateList/__UpdateListCallbacks, the synchronous componentAtIndex/
|
|
6
|
+
// enqueueComponent pair, recycling cells by a type key) — unchanged from
|
|
7
|
+
// mithril-lynx v1's own device-verified list.js.
|
|
12
8
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// trip could never satisfy that, so this is the only architecture that can.
|
|
9
|
+
// What's different from v1, and from this file's own first version: a
|
|
10
|
+
// cell's content is never rendered here. `componentAtIndex` only replays
|
|
11
|
+
// ops the background thread already computed (list-cell.js), the same way
|
|
12
|
+
// apply-patch.js's own top-level applyPatch replays the app's own tree —
|
|
13
|
+
// this file has no dependency on mithril-runtime, fake-dom.js, or a virtual
|
|
14
|
+
// backend at all anymore, because it never runs a render pass of its own.
|
|
15
|
+
// componentAtIndex(itemCount) cost is therefore the cost of creating/
|
|
16
|
+
// updating real elements from an already-known op list, same as any other
|
|
17
|
+
// patch — not a render.
|
|
23
18
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
function typeKeyOf(vnode) {
|
|
46
|
-
return typeof vnode.tag === "string" ? vnode.tag : (vnode.tag && vnode.tag.name) || "default";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function makeCell(pageId, wrapperHandle, createPatchApplier) {
|
|
50
|
-
const backend = createVirtualBackend();
|
|
51
|
-
const document = createLynxDocument(backend);
|
|
52
|
-
const render = renderFactory();
|
|
53
|
-
const applier = createPatchApplier(pageId);
|
|
54
|
-
applier.registerPageRoot(wrapperHandle);
|
|
55
|
-
return { wrapper: wrapperHandle, backend, document, render, applier, typeKey: null };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Re-renders `vnode` into `cell`'s own persistent fake-dom document —
|
|
59
|
-
* Mithril's own diff (real, not simulated) computes the minimal update
|
|
60
|
-
* against whatever this cell showed before, exactly like a normal redraw,
|
|
61
|
-
* so recycling a cell for a different item never needs a manual "clear
|
|
62
|
-
* old content first" step. */
|
|
63
|
-
function renderCellVnode(cell, vnode) {
|
|
64
|
-
cell.render(cell.document, [vnode], () => {});
|
|
65
|
-
const ops = cell.backend.takeOps();
|
|
66
|
-
if (ops) cell.applier.applyPatch(ops);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @param {number} pageId
|
|
71
|
-
* @param {string} rendererKey
|
|
72
|
-
* @param {(pageId: number, options?: {onEvent?: Function}) => object} createPatchApplier
|
|
73
|
-
* - passed in rather than imported, to avoid a circular import with
|
|
74
|
-
* apply-patch.js (the only caller).
|
|
75
|
-
* @param {Function} [onEvent] - forwarded into each cell's own patch
|
|
76
|
-
* applier, same as the top-level one — see this file's own "known gap"
|
|
77
|
-
* note above for what this does NOT yet solve.
|
|
78
|
-
*/
|
|
79
|
-
export function createNativeList(pageId, rendererKey, scrollOrientation, listType, spanCount, createPatchApplier, onEvent) {
|
|
80
|
-
const renderItem = renderers.get(rendererKey);
|
|
81
|
-
if (renderItem == null) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
`[mithril-lynx] no list renderer registered for "${rendererKey}" — call registerListRenderer("${rendererKey}", ...) from main-thread.ts.`,
|
|
84
|
-
);
|
|
19
|
+
// Recycling a cell for a different item clears its real children (removed
|
|
20
|
+
// by the handle ids list-cell.js recorded — see clearWrapperChildren below)
|
|
21
|
+
// and replays the new item's ops fresh; this is a real teardown-and-rebuild
|
|
22
|
+
// of that cell's content, not a diff against what was there before. A real
|
|
23
|
+
// Mithril diff on recycle would need a persistent per-slot fake-dom
|
|
24
|
+
// document kept in sync with which native cell native actually chose to
|
|
25
|
+
// reuse — information only native has, on the main thread, which the
|
|
26
|
+
// background thread (where the diff would have to run) can't see without a
|
|
27
|
+
// round trip. Not implemented.
|
|
28
|
+
|
|
29
|
+
export function createNativeList(pageId, scrollOrientation, listType, spanCount, createPatchApplier, onEvent) {
|
|
30
|
+
let cells = []; // current cells, indexed by cellIndex — see patch-protocol.js's Op.SetListItems
|
|
31
|
+
let count = 0;
|
|
32
|
+
const signMap = new Map(); // sign -> { wrapperHandle, applier, rootChildIds, cellIndex, typeKey }
|
|
33
|
+
const recycleMap = new Map(); // typeKey -> Map<sign, entry> (entries currently off-screen, available to reuse)
|
|
34
|
+
|
|
35
|
+
function replayCell(wrapperHandle, cell) {
|
|
36
|
+
const applier = createPatchApplier(pageId, { onEvent, flush: false });
|
|
37
|
+
applier.registerRoot(cell.containerId, wrapperHandle);
|
|
38
|
+
applier.applyPatch(cell.ops);
|
|
39
|
+
return applier;
|
|
85
40
|
}
|
|
86
41
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
42
|
+
function clearWrapperChildren(entry) {
|
|
43
|
+
for (const id of entry.rootChildIds) {
|
|
44
|
+
const handle = entry.applier.getHandle(id);
|
|
45
|
+
if (handle != null) __RemoveElement(entry.wrapperHandle, handle);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
91
48
|
|
|
92
|
-
function bindFreshItem(listHandle, listId, cellIndex, opId,
|
|
49
|
+
function bindFreshItem(listHandle, listId, cellIndex, opId, cell, flush) {
|
|
93
50
|
const wrapperHandle = __CreateElement("list-item", pageId, {});
|
|
94
51
|
__SetAttribute(wrapperHandle, "item-key", String(cellIndex));
|
|
95
52
|
__AppendElement(listHandle, wrapperHandle);
|
|
96
53
|
|
|
97
|
-
const
|
|
98
|
-
cell.typeKey = typeKey;
|
|
99
|
-
renderCellVnode(cell, vnode);
|
|
100
|
-
|
|
54
|
+
const applier = replayCell(wrapperHandle, cell);
|
|
101
55
|
const sign = __GetElementUniqueID(wrapperHandle);
|
|
102
|
-
signMap.set(sign, cell);
|
|
103
|
-
__FlushElementTree(wrapperHandle, { triggerLayout: true, operationID: opId, elementID: sign, listID: listId });
|
|
56
|
+
signMap.set(sign, { wrapperHandle, applier, rootChildIds: cell.rootChildIds, cellIndex, typeKey: cell.typeKey });
|
|
57
|
+
if (flush) __FlushElementTree(wrapperHandle, { triggerLayout: true, operationID: opId, elementID: sign, listID: listId });
|
|
104
58
|
return sign;
|
|
105
59
|
}
|
|
106
60
|
|
|
107
|
-
function bindRecycledItem(listId, cellIndex, opId,
|
|
108
|
-
const [sign,
|
|
61
|
+
function bindRecycledItem(listId, cellIndex, opId, cell, pool, flush) {
|
|
62
|
+
const [sign, entry] = pool.entries().next().value;
|
|
109
63
|
pool.delete(sign);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
64
|
+
clearWrapperChildren(entry);
|
|
65
|
+
__SetAttribute(entry.wrapperHandle, "item-key", String(cellIndex));
|
|
66
|
+
const applier = replayCell(entry.wrapperHandle, cell);
|
|
67
|
+
entry.applier = applier;
|
|
68
|
+
entry.rootChildIds = cell.rootChildIds;
|
|
69
|
+
entry.cellIndex = cellIndex;
|
|
70
|
+
signMap.set(sign, entry);
|
|
71
|
+
if (flush) __FlushElementTree(entry.wrapperHandle, { triggerLayout: true, operationID: opId, elementID: sign, listID: listId });
|
|
115
72
|
return sign;
|
|
116
73
|
}
|
|
117
74
|
|
|
118
|
-
function
|
|
75
|
+
function bindCell(listHandle, listId, cellIndex, opId, flush) {
|
|
119
76
|
if (cellIndex < 0 || cellIndex >= count) {
|
|
120
77
|
throw new Error(`[mithril-lynx] list: cellIndex ${cellIndex} out of range (itemCount=${count})`);
|
|
121
78
|
}
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
79
|
+
const cell = cells[cellIndex];
|
|
80
|
+
const pool = recycleMap.get(cell.typeKey);
|
|
81
|
+
if (pool && pool.size > 0) return bindRecycledItem(listId, cellIndex, opId, cell, pool, flush);
|
|
82
|
+
return bindFreshItem(listHandle, listId, cellIndex, opId, cell, flush);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function componentAtIndex(listHandle, listId, cellIndex, opId) {
|
|
86
|
+
return bindCell(listHandle, listId, cellIndex, opId, true);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The batched form of componentAtIndex — real native calls this (not a
|
|
91
|
+
* fallback: confirmed against `@lynx-js/react`'s own
|
|
92
|
+
* componentAtIndexFactory/attachListItemAtIndex, which always registers
|
|
93
|
+
* BOTH forms) for several cells at once, expecting exactly ONE
|
|
94
|
+
* `__FlushElementTree` covering all of them (`operationIDs`/`elementIDs`
|
|
95
|
+
* arrays), not one per cell — the real reason a native list's initial,
|
|
96
|
+
* simultaneously-visible cells need this at all.
|
|
97
|
+
*/
|
|
98
|
+
function componentAtIndexes(listHandle, listId, cellIndexes, operationIDs) {
|
|
99
|
+
const elementIDs = cellIndexes.map((cellIndex, i) => bindCell(listHandle, listId, cellIndex, operationIDs[i], false));
|
|
100
|
+
__FlushElementTree(listHandle, { triggerLayout: true, operationIDs, elementIDs, listID: listId });
|
|
127
101
|
}
|
|
128
102
|
|
|
129
103
|
function enqueueComponent(_listHandle, _listId, sign) {
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
104
|
+
const entry = signMap.get(sign);
|
|
105
|
+
if (entry == null) return;
|
|
132
106
|
signMap.delete(sign);
|
|
133
|
-
if (!recycleMap.has(
|
|
134
|
-
recycleMap.get(
|
|
107
|
+
if (!recycleMap.has(entry.typeKey)) recycleMap.set(entry.typeKey, new Map());
|
|
108
|
+
recycleMap.get(entry.typeKey).set(sign, entry);
|
|
135
109
|
}
|
|
136
110
|
|
|
137
111
|
function sendListInfo(listHandle, listId, insertAction, removeAction, updateAction) {
|
|
138
112
|
__SetAttribute(listHandle, "update-list-info", { insertAction, removeAction, updateAction });
|
|
139
|
-
__UpdateListCallbacks(listHandle, componentAtIndex, enqueueComponent);
|
|
113
|
+
__UpdateListCallbacks(listHandle, componentAtIndex, enqueueComponent, componentAtIndexes);
|
|
140
114
|
}
|
|
141
115
|
|
|
142
|
-
const listHandle = __CreateList(pageId, componentAtIndex, enqueueComponent, {});
|
|
116
|
+
const listHandle = __CreateList(pageId, componentAtIndex, enqueueComponent, {}, componentAtIndexes);
|
|
143
117
|
const listId = __GetElementUniqueID(listHandle);
|
|
144
118
|
__SetAttribute(listHandle, "scroll-orientation", scrollOrientation);
|
|
145
119
|
__SetAttribute(listHandle, "list-type", listType);
|
|
146
120
|
__SetAttribute(listHandle, "span-count", String(spanCount));
|
|
147
121
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
122
|
+
// Re-flushes every currently-attached (on-screen) cell with its newest
|
|
123
|
+
// ops — e.g. after a redraw triggered by a tap inside a cell, or any
|
|
124
|
+
// other app state change that reaches this list's items. Unconditional,
|
|
125
|
+
// same as List's own onupdate already unconditionally resends the whole
|
|
126
|
+
// `items` array on every relevant redraw (see mithril-lynx-ui's
|
|
127
|
+
// list/list.js) — a real "did this cell's content actually change"
|
|
128
|
+
// check would need to compare `ops` arrays, not implemented yet. Cells
|
|
129
|
+
// that aren't attached right now just pick up their new content next
|
|
130
|
+
// time componentAtIndex asks for that index.
|
|
131
|
+
function refreshAttachedCells(nextCells) {
|
|
132
|
+
for (const [sign, entry] of signMap) {
|
|
133
|
+
const nextCell = nextCells[entry.cellIndex];
|
|
134
|
+
clearWrapperChildren(entry);
|
|
135
|
+
entry.applier = replayCell(entry.wrapperHandle, nextCell);
|
|
136
|
+
entry.rootChildIds = nextCell.rootChildIds;
|
|
137
|
+
entry.typeKey = nextCell.typeKey;
|
|
138
|
+
signMap.set(sign, entry);
|
|
139
|
+
__FlushElementTree(entry.wrapperHandle, { triggerLayout: false });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function setCells(nextCells) {
|
|
144
|
+
const nextCount = nextCells.length;
|
|
145
|
+
refreshAttachedCells(nextCells);
|
|
146
|
+
cells = nextCells;
|
|
151
147
|
if (nextCount === count) return;
|
|
152
148
|
if (nextCount > count) {
|
|
153
149
|
sendListInfo(
|
|
@@ -161,7 +157,15 @@ export function createNativeList(pageId, rendererKey, scrollOrientation, listTyp
|
|
|
161
157
|
sendListInfo(listHandle, listId, [], Array.from({ length: count - nextCount }, (_, i) => nextCount + i), []);
|
|
162
158
|
}
|
|
163
159
|
count = nextCount;
|
|
164
|
-
}
|
|
160
|
+
}
|
|
165
161
|
|
|
166
|
-
|
|
162
|
+
// Returned separately, not as a property stuck onto `listHandle` itself:
|
|
163
|
+
// confirmed on real hardware (unlike @lynx-js/testing-environment's
|
|
164
|
+
// simulated PAPI) that a native list's own returned handle does not
|
|
165
|
+
// reliably hold a custom property across calls — assigning one there
|
|
166
|
+
// and reading it back later from apply-patch.js's Op.SetListItems case
|
|
167
|
+
// threw "TypeError: not a function" on the very first SetListItems, even
|
|
168
|
+
// with zero items. apply-patch.js keeps `setCells` in its own map
|
|
169
|
+
// instead, keyed the same way `handles` already is.
|
|
170
|
+
return { handle: listHandle, setCells };
|
|
167
171
|
}
|
package/src/patch-protocol.js
CHANGED
|
@@ -43,12 +43,13 @@ export const Op = Object.freeze({
|
|
|
43
43
|
SetGestureDetector: 14, // id, gestureId, gestureType, arenaPolicy
|
|
44
44
|
RemoveGestureDetector: 15, // id, gestureId
|
|
45
45
|
// A native virtualized list — see docs/native-papi/papi-06-virtualized-lists.md
|
|
46
|
-
// in mithril-lynx-ui for the full design.
|
|
47
|
-
//
|
|
48
|
-
// list-
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
// in mithril-lynx-ui for the full design. Each list item is rendered on
|
|
47
|
+
// the BACKGROUND thread, through the app's own real render pass
|
|
48
|
+
// (list-cell.js), same as everything else in the tree — the main thread
|
|
49
|
+
// never runs renderItem() itself, only replays the ops that rendering
|
|
50
|
+
// already produced (apply-patch.js's Op.CreateList case + list-support.js).
|
|
51
|
+
CreateList: 16, // id, scrollOrientation, listType, spanCount
|
|
52
|
+
SetListItems: 17, // id, cellsJSON — cellsJSON = JSON.stringify(cells), cells: Array<{ typeKey, containerId, ops, rootChildIds }>, one entry per current item, computed by list-cell.js's renderListCell(). `ops` is a flat op array in this SAME encoding, scoped to `containerId` as its root parent id.
|
|
52
53
|
});
|
|
53
54
|
|
|
54
55
|
/**
|
|
@@ -59,3 +60,43 @@ export const Op = Object.freeze({
|
|
|
59
60
|
export function pushOp(ops, opcode, ...args) {
|
|
60
61
|
ops.push(opcode, ...args);
|
|
61
62
|
}
|
|
63
|
+
|
|
64
|
+
// How many argument slots follow each opcode — the single source of truth
|
|
65
|
+
// for walking a flat ops array without re-interpreting it (apply-patch.js's
|
|
66
|
+
// own switch increments `i` inline instead of using this table, since it
|
|
67
|
+
// also needs to look at individual arg values as it goes; this exists for
|
|
68
|
+
// callers that only need to skip/scan, e.g. list-cell.js's
|
|
69
|
+
// findTopLevelChildIds(), which never applies the ops itself).
|
|
70
|
+
export const OP_ARITY = Object.freeze({
|
|
71
|
+
[Op.CreateElement]: 2, // tag, id
|
|
72
|
+
[Op.CreateElementNS]: 3, // ns, tag, id
|
|
73
|
+
[Op.CreateText]: 2, // value, id
|
|
74
|
+
[Op.CreateFragment]: 1, // id (never actually emitted — see fake-dom.js's LynxFragment)
|
|
75
|
+
[Op.InsertBefore]: 3, // parentId, childId, refId
|
|
76
|
+
[Op.RemoveChild]: 2, // parentId, childId
|
|
77
|
+
[Op.SetAttribute]: 3, // id, name, value
|
|
78
|
+
[Op.RemoveAttribute]: 2, // id, name
|
|
79
|
+
[Op.SetAttributeNS]: 4, // id, ns, name, value
|
|
80
|
+
[Op.SetStyleProperty]: 3, // id, name, value
|
|
81
|
+
[Op.RemoveStyleProperty]: 2, // id, name
|
|
82
|
+
[Op.SetText]: 2, // id, value
|
|
83
|
+
[Op.AddEvent]: 2, // id, type
|
|
84
|
+
[Op.RemoveEvent]: 2, // id, type
|
|
85
|
+
[Op.SetGestureDetector]: 4, // id, gestureId, gestureType, arenaPolicy
|
|
86
|
+
[Op.RemoveGestureDetector]: 2, // id, gestureId
|
|
87
|
+
[Op.CreateList]: 4, // id, scrollOrientation, listType, spanCount
|
|
88
|
+
[Op.SetListItems]: 2, // id, cellsJSON
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** Walks a flat ops array, calling `visit(opcode, args)` once per op — args
|
|
92
|
+
* is the plain slice of that op's own arguments (not including the opcode
|
|
93
|
+
* itself). Throws on an unknown opcode rather than silently desyncing. */
|
|
94
|
+
export function forEachOp(ops, visit) {
|
|
95
|
+
for (let i = 0; i < ops.length; ) {
|
|
96
|
+
const opcode = ops[i++];
|
|
97
|
+
const arity = OP_ARITY[opcode];
|
|
98
|
+
if (arity === undefined) throw new Error(`[mithril-lynx] Unknown patch opcode: ${opcode}`);
|
|
99
|
+
visit(opcode, ops.slice(i, i + arity));
|
|
100
|
+
i += arity;
|
|
101
|
+
}
|
|
102
|
+
}
|
package/test/list.test.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { describe, expect, it } from "@rstest/core";
|
|
2
2
|
import m from "mithril";
|
|
3
|
+
import renderFactory from "mithril-runtime/render/render.js";
|
|
3
4
|
import { createPatchApplier } from "../src/apply-patch.js";
|
|
4
|
-
import {
|
|
5
|
+
import { createVirtualBackend } from "../src/backends/virtual-backend.js";
|
|
6
|
+
import { createLynxDocument } from "../src/fake-dom.js";
|
|
7
|
+
import { renderListCell } from "../src/list-cell.js";
|
|
5
8
|
import { Op } from "../src/patch-protocol.js";
|
|
6
9
|
|
|
7
|
-
// Op.CreateList end-to-end
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// directly
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// already does against mithril-lynx-v1's list.js.
|
|
10
|
+
// Op.CreateList end-to-end. A cell's own content is computed on the
|
|
11
|
+
// BACKGROUND thread (list-cell.js's renderListCell(), through the app's own
|
|
12
|
+
// document/render — no separate render pipeline) and only replayed on the
|
|
13
|
+
// main thread (list-support.js) — see
|
|
14
|
+
// docs/native-papi/papi-06-virtualized-lists.md in mithril-lynx-ui. This
|
|
15
|
+
// test drives both halves directly: a background document renders each item
|
|
16
|
+
// into cells, then a main-thread applier replays Op.CreateList/
|
|
17
|
+
// Op.SetListItems with those cells, exactly like mithril-lynx-ui's own List
|
|
18
|
+
// component does end to end.
|
|
17
19
|
|
|
18
20
|
function requestCell(listHandle: any, index: number, opId = 1) {
|
|
19
21
|
const listId = __GetElementUniqueID(listHandle);
|
|
@@ -34,37 +36,37 @@ function textOf(node: any): string {
|
|
|
34
36
|
return out;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
/** Stands in for mithril-lynx-ui's <List>: one persistent background
|
|
40
|
+
* document + render instance, reused across calls — see list-cell.js's own
|
|
41
|
+
* header for why callers keep one of these per list, not one per cell. */
|
|
42
|
+
function makeCellSource(renderItem: (item: any, index: number) => unknown) {
|
|
43
|
+
const document = createLynxDocument(createVirtualBackend());
|
|
44
|
+
const render = renderFactory();
|
|
45
|
+
return {
|
|
46
|
+
document,
|
|
47
|
+
buildCells: (items: unknown[]) => items.map((item, index) => renderListCell(document, render, () => {}, renderItem, item, index)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function setupList() {
|
|
38
52
|
lynxTestingEnv.switchToMainThread();
|
|
39
53
|
const pageId = __GetElementUniqueID(__CreatePage());
|
|
40
54
|
const applier = createPatchApplier(pageId);
|
|
41
55
|
applier.registerPageRoot(__CreateView(pageId));
|
|
42
|
-
applier.applyPatch([Op.CreateList, 1,
|
|
56
|
+
applier.applyPatch([Op.CreateList, 1, "vertical", "single", 1]);
|
|
43
57
|
const listHandle = applier.getHandle(1) as any;
|
|
44
58
|
return { applier, listHandle };
|
|
45
59
|
}
|
|
46
60
|
|
|
47
|
-
function
|
|
48
|
-
applier.applyPatch([Op.SetListItems, 1, JSON.stringify(
|
|
61
|
+
function setCells(applier: ReturnType<typeof createPatchApplier>, cells: unknown[]) {
|
|
62
|
+
applier.applyPatch([Op.SetListItems, 1, JSON.stringify(cells)]);
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
describe("Op.CreateList (native virtualized list support)", () => {
|
|
52
|
-
it("
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
applier.registerPageRoot(__CreateView(pageId));
|
|
57
|
-
|
|
58
|
-
expect(() => applier.applyPatch([Op.CreateList, 1, "nonexistent-key", "vertical", "single", 1])).toThrow(
|
|
59
|
-
/no list renderer registered for "nonexistent-key"/,
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("renders real content per cell via the registered renderer, driven by componentAtIndex", () => {
|
|
64
|
-
registerListRenderer("basic", (item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
65
|
-
|
|
66
|
-
const { applier, listHandle } = setupList("basic");
|
|
67
|
-
setItems(applier, ["a", "b", "c"]);
|
|
66
|
+
it("renders real content per cell from ops the background thread already computed", () => {
|
|
67
|
+
const { buildCells } = makeCellSource((item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
68
|
+
const { applier, listHandle } = setupList();
|
|
69
|
+
setCells(applier, buildCells(["a", "b", "c"]));
|
|
68
70
|
|
|
69
71
|
requestCell(listHandle, 0);
|
|
70
72
|
requestCell(listHandle, 1);
|
|
@@ -73,10 +75,9 @@ describe("Op.CreateList (native virtualized list support)", () => {
|
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
it("recycles a cell for a different index, and its content updates to match", () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
setItems(applier, ["a", "b", "c", "d"]);
|
|
78
|
+
const { buildCells } = makeCellSource((item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
79
|
+
const { applier, listHandle } = setupList();
|
|
80
|
+
setCells(applier, buildCells(["a", "b", "c", "d"]));
|
|
80
81
|
|
|
81
82
|
const signA = requestCell(listHandle, 0);
|
|
82
83
|
const wrapperA = listHandle.children[0];
|
|
@@ -91,15 +92,42 @@ describe("Op.CreateList (native virtualized list support)", () => {
|
|
|
91
92
|
});
|
|
92
93
|
|
|
93
94
|
it("SetListItems with a larger array requests the newly available indices without error", () => {
|
|
94
|
-
|
|
95
|
+
const { buildCells } = makeCellSource((item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
96
|
+
const { applier, listHandle } = setupList();
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
setItems(applier, ["a", "b"]);
|
|
98
|
+
setCells(applier, buildCells(["a", "b"]));
|
|
99
99
|
expect(() => requestCell(listHandle, 1)).not.toThrow();
|
|
100
100
|
expect(() => requestCell(listHandle, 2)).toThrow(/cellIndex 2 out of range/);
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
setCells(applier, buildCells(["a", "b", "c"]));
|
|
103
103
|
expect(() => requestCell(listHandle, 2)).not.toThrow();
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
it("SetListItems re-flushes an already-attached cell's content in place", () => {
|
|
107
|
+
const { buildCells } = makeCellSource((item: string, index: number) => m("text", {}, `${index}:${item}`));
|
|
108
|
+
const { applier, listHandle } = setupList();
|
|
109
|
+
setCells(applier, buildCells(["a", "b"]));
|
|
110
|
+
|
|
111
|
+
requestCell(listHandle, 0);
|
|
112
|
+
const wrapper = listHandle.children[0];
|
|
113
|
+
expect(textOf(wrapper)).toBe("0:a");
|
|
114
|
+
|
|
115
|
+
setCells(applier, buildCells(["z", "b"])); // same count, index 0's content changed
|
|
116
|
+
expect(textOf(wrapper)).toBe("0:z");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("a tap inside a cell dispatches through the background thread's own fake-dom node", () => {
|
|
120
|
+
const { document, buildCells } = makeCellSource(() =>
|
|
121
|
+
m("text", { ontap: () => { taps += 1; } }, "tap me"),
|
|
122
|
+
);
|
|
123
|
+
let taps = 0;
|
|
124
|
+
const { applier, listHandle } = setupList();
|
|
125
|
+
const cells = buildCells([{}]);
|
|
126
|
+
setCells(applier, cells);
|
|
127
|
+
requestCell(listHandle, 0);
|
|
128
|
+
|
|
129
|
+
const node = document.getNodeById((cells[0] as any).rootChildIds[0]);
|
|
130
|
+
node!.dispatchEvent({ type: "tap", currentTarget: node, preventDefault() {}, stopPropagation() {} });
|
|
131
|
+
expect(taps).toBe(1);
|
|
132
|
+
});
|
|
105
133
|
});
|