mithril-lynx 2.0.2 → 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 +17 -1
- package/src/apply-patch.js +268 -16
- package/src/backends/virtual-backend.js +28 -0
- package/src/fake-dom.js +99 -7
- package/src/list-cell.d.ts +20 -0
- package/src/list-cell.js +73 -0
- package/src/list-support.d.ts +13 -0
- package/src/list-support.js +171 -0
- package/src/mount-redraw.d.ts +13 -0
- package/src/mount-redraw.js +7 -0
- package/src/patch-protocol.js +62 -0
- package/src/testing.d.ts +16 -0
- package/src/testing.js +48 -0
- package/test/gesture.test.ts +175 -0
- package/test/list.test.ts +133 -0
- package/test/setup.ts +5 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mithril-lynx",
|
|
3
|
-
"version": "2.0
|
|
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",
|
|
@@ -24,6 +24,22 @@
|
|
|
24
24
|
"./request": {
|
|
25
25
|
"types": "./src/request.d.ts",
|
|
26
26
|
"default": "./src/request.js"
|
|
27
|
+
},
|
|
28
|
+
"./mount-redraw": {
|
|
29
|
+
"types": "./src/mount-redraw.d.ts",
|
|
30
|
+
"default": "./src/mount-redraw.js"
|
|
31
|
+
},
|
|
32
|
+
"./testing": {
|
|
33
|
+
"types": "./src/testing.d.ts",
|
|
34
|
+
"default": "./src/testing.js"
|
|
35
|
+
},
|
|
36
|
+
"./list-support": {
|
|
37
|
+
"types": "./src/list-support.d.ts",
|
|
38
|
+
"default": "./src/list-support.js"
|
|
39
|
+
},
|
|
40
|
+
"./list-cell": {
|
|
41
|
+
"types": "./src/list-cell.d.ts",
|
|
42
|
+
"default": "./src/list-cell.js"
|
|
27
43
|
}
|
|
28
44
|
},
|
|
29
45
|
"scripts": {
|
package/src/apply-patch.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
// src/apply-patch.js
|
|
2
2
|
//
|
|
3
|
-
// The main-thread half of the patch protocol. Deliberately NOT a DOM
|
|
4
|
-
// never runs Mithril's render.js (only
|
|
5
|
-
// background.js) — it is a direct,
|
|
6
|
-
//
|
|
7
|
-
// own `snapshotPatchApply.js`
|
|
8
|
-
// §4.3): a switch over op
|
|
3
|
+
// The main-thread half of the patch protocol. Deliberately NOT a DOM for
|
|
4
|
+
// the app's OWN tree — it never runs Mithril's render.js for that (only
|
|
5
|
+
// the background thread does, see background.js) — it is a direct,
|
|
6
|
+
// low-level interpreter of the flat op array straight onto the real
|
|
7
|
+
// Element PAPI, in the spirit of ReactLynx's own `snapshotPatchApply.js`
|
|
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. 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.
|
|
9
14
|
//
|
|
10
15
|
// The exact `__Create*`/pageId contract below (one `pageId` shared by every
|
|
11
16
|
// element on a page, `__CreateView`/`__CreateText`/generic `__CreateElement`
|
|
@@ -18,21 +23,209 @@
|
|
|
18
23
|
// commit/reload layer (commit.js, reload/*.js), never in this mapping.
|
|
19
24
|
|
|
20
25
|
import { Op } from "./patch-protocol.js";
|
|
26
|
+
import { createNativeList } from "./list-support.js";
|
|
27
|
+
|
|
28
|
+
// --- Native gesture support (Op.SetGestureDetector) ------------------------
|
|
29
|
+
//
|
|
30
|
+
// See patch-protocol.js's own note and mithril-lynx-ui's
|
|
31
|
+
// docs/native-papi/papi-05-native-gestures.md for the full design writeup.
|
|
32
|
+
// Everything below runs on the MAIN thread, synchronously, inside a native
|
|
33
|
+
// gesture callback — never on the background thread, and never waits on a
|
|
34
|
+
// round trip to it. The arena-claim decision (arenaPolicy) is evaluated
|
|
35
|
+
// here using only the event's own coordinates; the resulting touches-down/
|
|
36
|
+
// move/up events are then forwarded to the background thread as ordinary
|
|
37
|
+
// events (via `onEvent`, the exact same callback Op.AddEvent already uses),
|
|
38
|
+
// so app code sees them as plain "gesturedown"/"gesturemove"/"gestureup"
|
|
39
|
+
// listeners with no gesture-specific machinery of its own.
|
|
40
|
+
|
|
41
|
+
const GESTURE_TYPE_CODES = { composed: -1, pan: 0, fling: 1, default: 2, tap: 3, longpress: 4, rotation: 5, pinch: 6, native: 7 };
|
|
42
|
+
const GestureState = { active: 1, fail: 2, end: 3 };
|
|
43
|
+
|
|
44
|
+
// Native does not call a gesture callback function directly — it calls a
|
|
45
|
+
// global `runWorklet(ctx, params)`, looking up the real function by
|
|
46
|
+
// `ctx._wkltId`. This is a real native requirement (confirmed on device by
|
|
47
|
+
// this project's own predecessor, mithril-lynx v1's gesture.js), not
|
|
48
|
+
// specific to any one package — every gesture callback has to be wrapped
|
|
49
|
+
// through this registry before being handed to __SetGestureDetector.
|
|
50
|
+
function ensureWorkletRuntime() {
|
|
51
|
+
if (globalThis.lynxWorkletImpl !== undefined) return;
|
|
52
|
+
globalThis.lynxWorkletImpl = { _workletMap: {} };
|
|
53
|
+
globalThis.registerWorklet = function (_type, id, fn) {
|
|
54
|
+
globalThis.lynxWorkletImpl._workletMap[id] = fn;
|
|
55
|
+
};
|
|
56
|
+
globalThis.runWorklet = function (ctx, params) {
|
|
57
|
+
if (typeof ctx !== "object" || ctx === null || !("_wkltId" in ctx)) return;
|
|
58
|
+
const fn = globalThis.lynxWorkletImpl._workletMap[ctx._wkltId];
|
|
59
|
+
if (typeof fn !== "function") return;
|
|
60
|
+
const args = Array.isArray(params) ? params : params != null ? [params] : [];
|
|
61
|
+
// A plain call, deliberately not .apply()/.call() — native's own
|
|
62
|
+
// `controller` argument throws if marshalled through either (same
|
|
63
|
+
// finding mithril-lynx v1's gesture.js already made).
|
|
64
|
+
return fn.bind(ctx)(...args);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let nextWorkletId = 1;
|
|
69
|
+
function wrapWorkletCallback(fn) {
|
|
70
|
+
ensureWorkletRuntime();
|
|
71
|
+
const id = "mithril-lynx-gesture-" + nextWorkletId++;
|
|
72
|
+
globalThis.registerWorklet("main-thread", id, fn);
|
|
73
|
+
return { _wkltId: id };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A small, generic claim/release policy — covers the two real shapes this
|
|
78
|
+
* project's consumers need, not an arbitrary one:
|
|
79
|
+
* - `{ mode: "claim" }` — claim on touches-down, never reconsider (a
|
|
80
|
+
* single-axis drag with nothing else competing for the gesture).
|
|
81
|
+
* - `{ mode: "axis-lock", axis: "horizontal" | "vertical", referenceMoves }`
|
|
82
|
+
* — claim eagerly on touches-down, then on the move `referenceMoves + 1`
|
|
83
|
+
* (0: decide using the down position as reference, right on the first
|
|
84
|
+
* move; 1: use the first move's own position as reference and decide on
|
|
85
|
+
* the second), release and fail the gesture if the dominant axis of the
|
|
86
|
+
* resulting delta doesn't match `axis`.
|
|
87
|
+
* Unverified on a real device (no device access this session) — the claim
|
|
88
|
+
* timing (down vs. first/second move) mirrors what mithril-lynx v1's own
|
|
89
|
+
* device-verified sheet.js/swipe-action.js/swiper.js already did; the NEW
|
|
90
|
+
* part, evaluating it here instead of in app code, has not been confirmed
|
|
91
|
+
* to feel the same on-device.
|
|
92
|
+
*/
|
|
93
|
+
function createArenaTracker(policy) {
|
|
94
|
+
const mode = (policy && policy.mode) || "claim";
|
|
95
|
+
let refX = null;
|
|
96
|
+
let refY = null;
|
|
97
|
+
let movesSeen = 0;
|
|
98
|
+
let decided = false;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
onDown(x, y, consume) {
|
|
102
|
+
consume(true);
|
|
103
|
+
if (mode === "axis-lock" && (policy.referenceMoves || 0) === 0) {
|
|
104
|
+
refX = x;
|
|
105
|
+
refY = y;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
onMove(x, y, consume, fail) {
|
|
109
|
+
if (mode !== "axis-lock" || decided) return;
|
|
110
|
+
if ((policy.referenceMoves || 0) === 1 && movesSeen === 0) {
|
|
111
|
+
refX = x;
|
|
112
|
+
refY = y;
|
|
113
|
+
movesSeen++;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
movesSeen++;
|
|
117
|
+
if (refX == null) return;
|
|
118
|
+
const dx = x - refX;
|
|
119
|
+
const dy = y - refY;
|
|
120
|
+
if (dx === 0 && dy === 0) return; // not enough signal yet
|
|
121
|
+
decided = true;
|
|
122
|
+
const isHorizontal = Math.abs(dx) >= Math.abs(dy);
|
|
123
|
+
const wins = policy.axis === "horizontal" ? isHorizontal : !isHorizontal;
|
|
124
|
+
if (wins) {
|
|
125
|
+
consume(true);
|
|
126
|
+
} else {
|
|
127
|
+
// Release the claim touches-down made eagerly, THEN fail —
|
|
128
|
+
// both, not just the latter: a bare fail() with the arena
|
|
129
|
+
// still marked "claimed" would keep blocking an ancestor
|
|
130
|
+
// (e.g. a <scroll-view>) from ever seeing this touch.
|
|
131
|
+
consume(false);
|
|
132
|
+
fail();
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function registerGestureDetector(handle, id, gestureId, gestureType, arenaPolicy, onEvent) {
|
|
139
|
+
const tracker = createArenaTracker(arenaPolicy);
|
|
140
|
+
const gestureTypeCode = typeof gestureType === "string" ? GESTURE_TYPE_CODES[gestureType] : gestureType;
|
|
141
|
+
|
|
142
|
+
function consume(controller, shouldClaim) {
|
|
143
|
+
if (controller != null && typeof controller.__ConsumeGesture === "function") {
|
|
144
|
+
controller.__ConsumeGesture(handle, gestureId, { consume: shouldClaim, inner: false });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function fail(controller) {
|
|
148
|
+
if (controller != null && typeof controller.__SetGestureState === "function") {
|
|
149
|
+
controller.__SetGestureState(handle, gestureId, GestureState.fail);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// timestamp: a real field native's own touch/gesture params already
|
|
153
|
+
// carry (mithril-lynx v1's gesture consumers already read
|
|
154
|
+
// event.params.timestamp for velocity calculations) — forwarded as-is
|
|
155
|
+
// rather than having a consumer approximate it from receipt time on
|
|
156
|
+
// the background thread, which would fold cross-thread forwarding
|
|
157
|
+
// latency into a velocity computation.
|
|
158
|
+
function coordsOf(event) {
|
|
159
|
+
const p = (event && event.params) || {};
|
|
160
|
+
return { clientX: p.clientX, clientY: p.clientY, timestamp: p.timestamp };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const callbacks = {
|
|
164
|
+
onTouchesDown: (event, controller) => {
|
|
165
|
+
const coords = coordsOf(event);
|
|
166
|
+
tracker.onDown(coords.clientX, coords.clientY, (claim) => consume(controller, claim));
|
|
167
|
+
onEvent?.(id, "gesturedown", coords);
|
|
168
|
+
},
|
|
169
|
+
onTouchesMove: (event, controller) => {
|
|
170
|
+
const coords = coordsOf(event);
|
|
171
|
+
tracker.onMove(coords.clientX, coords.clientY, (claim) => consume(controller, claim), () => fail(controller));
|
|
172
|
+
onEvent?.(id, "gesturemove", coords);
|
|
173
|
+
},
|
|
174
|
+
onTouchesUp: (event) => {
|
|
175
|
+
onEvent?.(id, "gestureup", coordsOf(event));
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
__SetAttribute(handle, "has-react-gesture", true);
|
|
180
|
+
__SetAttribute(handle, "flatten", false);
|
|
181
|
+
__SetGestureDetector(
|
|
182
|
+
handle,
|
|
183
|
+
gestureId,
|
|
184
|
+
gestureTypeCode,
|
|
185
|
+
{ callbacks: Object.keys(callbacks).map((name) => ({ name, callback: wrapWorkletCallback(callbacks[name]) })) },
|
|
186
|
+
{},
|
|
187
|
+
);
|
|
188
|
+
}
|
|
21
189
|
|
|
22
190
|
/**
|
|
23
191
|
* @param {number} pageId - `__GetElementUniqueID(pageElement)` of the real
|
|
24
192
|
* page this applier is attached to. Every element this applier creates
|
|
25
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.
|
|
26
206
|
*/
|
|
27
|
-
export function createPatchApplier(pageId, { onEvent } = {}) {
|
|
207
|
+
export function createPatchApplier(pageId, { onEvent, flush = true } = {}) {
|
|
28
208
|
// id (as allocated by the background's virtual backend) -> real PAPI
|
|
29
209
|
// element handle. id 0 is reserved for "the page itself" (see
|
|
30
210
|
// fake-dom.js's LynxDocument) — pre-seeded here so the very first
|
|
31
211
|
// InsertBefore/AppendChild targeting id 0 has somewhere real to land.
|
|
32
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
|
+
}
|
|
33
226
|
|
|
34
227
|
function registerPageRoot(pageElementHandle) {
|
|
35
|
-
|
|
228
|
+
registerRoot(0, pageElementHandle);
|
|
36
229
|
}
|
|
37
230
|
|
|
38
231
|
function createElementHandle(tag) {
|
|
@@ -42,12 +235,15 @@ export function createPatchApplier(pageId, { onEvent } = {}) {
|
|
|
42
235
|
}
|
|
43
236
|
|
|
44
237
|
/**
|
|
45
|
-
* Applies one commit's worth of ops, then
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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.
|
|
51
247
|
*/
|
|
52
248
|
function applyPatch(ops) {
|
|
53
249
|
for (let i = 0; i < ops.length; ) {
|
|
@@ -101,7 +297,15 @@ export function createPatchApplier(pageId, { onEvent } = {}) {
|
|
|
101
297
|
const name = ops[i++];
|
|
102
298
|
const value = ops[i++];
|
|
103
299
|
const handle = handles.get(id);
|
|
300
|
+
// "class" and "id" each have their own dedicated PAPI call
|
|
301
|
+
// (__SetClasses/__SetID) — __SetAttribute itself rejects
|
|
302
|
+
// both ("Cannot use __SetAttribute for \"class\"/\"id\"").
|
|
303
|
+
// Found porting a component that assigns a native `id` for
|
|
304
|
+
// an imperative selector-query ref (see mithril-lynx-ui's
|
|
305
|
+
// docs/native-papi/papi-01-imperative-refs.md) — nothing
|
|
306
|
+
// in this rewrite's own test suite had set `id` before.
|
|
104
307
|
if (name === "class") __SetClasses(handle, value == null ? "" : value);
|
|
308
|
+
else if (name === "id") __SetID(handle, value == null ? null : value);
|
|
105
309
|
else __SetAttribute(handle, name, value);
|
|
106
310
|
break;
|
|
107
311
|
}
|
|
@@ -110,6 +314,7 @@ export function createPatchApplier(pageId, { onEvent } = {}) {
|
|
|
110
314
|
const name = ops[i++];
|
|
111
315
|
const handle = handles.get(id);
|
|
112
316
|
if (name === "class") __SetClasses(handle, "");
|
|
317
|
+
else if (name === "id") __SetID(handle, null);
|
|
113
318
|
else __SetAttribute(handle, name, null);
|
|
114
319
|
break;
|
|
115
320
|
}
|
|
@@ -168,12 +373,59 @@ export function createPatchApplier(pageId, { onEvent } = {}) {
|
|
|
168
373
|
i += 2;
|
|
169
374
|
break;
|
|
170
375
|
}
|
|
376
|
+
case Op.SetGestureDetector: {
|
|
377
|
+
const id = ops[i++];
|
|
378
|
+
const gestureId = ops[i++];
|
|
379
|
+
const gestureType = ops[i++];
|
|
380
|
+
const arenaPolicy = ops[i++];
|
|
381
|
+
const handle = handles.get(id);
|
|
382
|
+
registerGestureDetector(handle, id, gestureId, gestureType, arenaPolicy, onEvent);
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
case Op.RemoveGestureDetector: {
|
|
386
|
+
const id = ops[i++];
|
|
387
|
+
const gestureId = ops[i++];
|
|
388
|
+
const handle = handles.get(id);
|
|
389
|
+
if (typeof __RemoveGestureDetector === "function") __RemoveGestureDetector(handle, gestureId);
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case Op.CreateList: {
|
|
393
|
+
const id = ops[i++];
|
|
394
|
+
const scrollOrientation = ops[i++];
|
|
395
|
+
const listType = ops[i++];
|
|
396
|
+
const spanCount = ops[i++];
|
|
397
|
+
const { handle, setCells } = createNativeList(pageId, scrollOrientation, listType, spanCount, createPatchApplier, onEvent);
|
|
398
|
+
handles.set(id, handle);
|
|
399
|
+
listSetters.set(id, setCells);
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case Op.SetListItems: {
|
|
403
|
+
const id = ops[i++];
|
|
404
|
+
const cellsJSON = ops[i++];
|
|
405
|
+
listSetters.get(id)(JSON.parse(cellsJSON));
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
171
408
|
default:
|
|
172
409
|
throw new Error(`[mithril-lynx] Unknown patch opcode: ${opcode}`);
|
|
173
410
|
}
|
|
174
411
|
}
|
|
175
|
-
__FlushElementTree();
|
|
412
|
+
if (flush) __FlushElementTree();
|
|
176
413
|
}
|
|
177
414
|
|
|
178
|
-
return {
|
|
415
|
+
return {
|
|
416
|
+
registerPageRoot,
|
|
417
|
+
registerRoot,
|
|
418
|
+
applyPatch,
|
|
419
|
+
/** The real PAPI element handle for a given background-side id, or
|
|
420
|
+
* `undefined` if nothing was ever created for it. Exists for tests
|
|
421
|
+
* (see mithril-lynx/testing) that need to correlate a fake-dom node's
|
|
422
|
+
* `_id` with the real element the testing environment's PAPI
|
|
423
|
+
* recording (mithril-lynx-v1's own installTestingPolyfills wraps
|
|
424
|
+
* every `__`-prefixed call regardless of which package called it, so
|
|
425
|
+
* this is how a v2 test finds "which of those calls targeted THIS
|
|
426
|
+
* element") — never needed by application code. */
|
|
427
|
+
getHandle(id) {
|
|
428
|
+
return handles.get(id);
|
|
429
|
+
},
|
|
430
|
+
};
|
|
179
431
|
}
|
|
@@ -69,6 +69,20 @@ export function createVirtualBackend() {
|
|
|
69
69
|
removeEvent(id, type) {
|
|
70
70
|
pushOp(ops, Op.RemoveEvent, id, type);
|
|
71
71
|
},
|
|
72
|
+
setGestureDetector(id, gestureId, gestureType, arenaPolicy) {
|
|
73
|
+
pushOp(ops, Op.SetGestureDetector, id, gestureId, gestureType, arenaPolicy);
|
|
74
|
+
},
|
|
75
|
+
removeGestureDetector(id, gestureId) {
|
|
76
|
+
pushOp(ops, Op.RemoveGestureDetector, id, gestureId);
|
|
77
|
+
},
|
|
78
|
+
createList(scrollOrientation, listType, spanCount) {
|
|
79
|
+
const id = nextId++;
|
|
80
|
+
pushOp(ops, Op.CreateList, id, scrollOrientation, listType, spanCount);
|
|
81
|
+
return id;
|
|
82
|
+
},
|
|
83
|
+
setListItems(id, cells) {
|
|
84
|
+
pushOp(ops, Op.SetListItems, id, JSON.stringify(cells));
|
|
85
|
+
},
|
|
72
86
|
/** Drains and returns the accumulated ops. Called once per commit. */
|
|
73
87
|
takeOps() {
|
|
74
88
|
if (ops.length === 0) return null;
|
|
@@ -76,5 +90,19 @@ export function createVirtualBackend() {
|
|
|
76
90
|
ops = [];
|
|
77
91
|
return out;
|
|
78
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
|
+
},
|
|
79
107
|
};
|
|
80
108
|
}
|
package/src/fake-dom.js
CHANGED
|
@@ -65,6 +65,15 @@ class LynxContainerNode extends LynxNode {
|
|
|
65
65
|
return this._children[0] ?? null;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Real Mithril's render.js reads `fragment.childNodes.length` right
|
|
69
|
+
// after `insertDOM`'ing a multi-node children list into a
|
|
70
|
+
// `createDocumentFragment()` (createNodes' fragment-batching path) — a
|
|
71
|
+
// real DOM's `childNodes` is a live NodeList, but render.js only ever
|
|
72
|
+
// reads `.length` off it here, so the plain backing array is enough.
|
|
73
|
+
get childNodes() {
|
|
74
|
+
return this._children;
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
contains(other) {
|
|
69
78
|
let node = other;
|
|
70
79
|
while (node) {
|
|
@@ -121,6 +130,7 @@ class LynxContainerNode extends LynxNode {
|
|
|
121
130
|
function createStyleProxy(element) {
|
|
122
131
|
const methods = {
|
|
123
132
|
setProperty(name, value) {
|
|
133
|
+
element._styleEverSet = true;
|
|
124
134
|
element._backend.setStyleProperty(element._id, name, String(value));
|
|
125
135
|
},
|
|
126
136
|
removeProperty(name) {
|
|
@@ -140,6 +150,7 @@ function createStyleProxy(element) {
|
|
|
140
150
|
if (value === "" || value == null) {
|
|
141
151
|
element._backend.removeStyleProperty(element._id, name);
|
|
142
152
|
} else {
|
|
153
|
+
element._styleEverSet = true;
|
|
143
154
|
element._backend.setStyleProperty(element._id, name, String(value));
|
|
144
155
|
}
|
|
145
156
|
return true;
|
|
@@ -147,15 +158,33 @@ function createStyleProxy(element) {
|
|
|
147
158
|
});
|
|
148
159
|
}
|
|
149
160
|
|
|
161
|
+
// Module-level, not per-document: mirrors patch-protocol.js's own id spaces
|
|
162
|
+
// (element ids are per-backend, but a gesture id only needs to be unique
|
|
163
|
+
// within whatever set apply-patch.js's real __SetGestureDetector call sees
|
|
164
|
+
// on the main thread — a single incrementing counter is simplest).
|
|
165
|
+
let nextGestureId = 1;
|
|
166
|
+
|
|
150
167
|
export class LynxElement extends LynxContainerNode {
|
|
151
|
-
|
|
168
|
+
/**
|
|
169
|
+
* `listConfig`, when given, makes this a native virtualized list
|
|
170
|
+
* element instead of a plain one — see patch-protocol.js's
|
|
171
|
+
* Op.CreateList and docs/native-papi/papi-06-virtualized-lists.md
|
|
172
|
+
* (mithril-lynx-ui) for the full design. Not constructed directly;
|
|
173
|
+
* use LynxDocument#createNativeList().
|
|
174
|
+
*/
|
|
175
|
+
constructor(ownerDocument, backend, tag, ns, listConfig) {
|
|
152
176
|
super(ownerDocument);
|
|
153
177
|
this._backend = backend;
|
|
154
178
|
this.tag = tag;
|
|
155
179
|
this.namespaceURI = ns;
|
|
156
|
-
this._id =
|
|
180
|
+
this._id = listConfig
|
|
181
|
+
? backend.createList(listConfig.scrollOrientation, listConfig.listType, listConfig.spanCount)
|
|
182
|
+
: ns
|
|
183
|
+
? backend.createElementNS(ns, tag)
|
|
184
|
+
: backend.createElement(tag);
|
|
157
185
|
ownerDocument._nodesById.set(this._id, this);
|
|
158
186
|
this._style = null;
|
|
187
|
+
this._styleEverSet = false;
|
|
159
188
|
this._listeners = Object.create(null);
|
|
160
189
|
// `hasPropertyKey` (CONTRACT.md §e) requires `"value" in vnode.dom` etc.
|
|
161
190
|
// to be true for the property-write fast path to apply to form
|
|
@@ -173,11 +202,20 @@ export class LynxElement extends LynxContainerNode {
|
|
|
173
202
|
set style(value) {
|
|
174
203
|
if (value == null || value === "") {
|
|
175
204
|
// `element.style = ""` (CONTRACT.md §f, lines 750-752): clear.
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
this
|
|
205
|
+
// render.js's own updateStyle() calls this UNCONDITIONALLY right
|
|
206
|
+
// before applying an object style, even on an element that never
|
|
207
|
+
// had any style at all (its "old is missing or a string, style is
|
|
208
|
+
// an object" branch — see mithril-runtime/render/render.js) — so
|
|
209
|
+
// without this guard, EVERY component with a plain object style
|
|
210
|
+
// prop (an extremely common pattern, not an edge case) would hit
|
|
211
|
+
// apply-patch.js's "bulk-clear not implemented" throw on its very
|
|
212
|
+
// first render. `_styleEverSet` (set by createStyleProxy whenever
|
|
213
|
+
// a real property is written) is exactly "was there anything to
|
|
214
|
+
// clear" — skip emitting the op at all when there wasn't; the
|
|
215
|
+
// throw still fires for the genuine case (an update actually
|
|
216
|
+
// replacing a previously-set style), where a real bulk-clear PAPI
|
|
217
|
+
// call would actually be needed and hasn't been validated yet.
|
|
218
|
+
if (this._styleEverSet) this._backend.removeStyleProperty(this._id, "*");
|
|
181
219
|
return;
|
|
182
220
|
}
|
|
183
221
|
if (typeof value !== "object") {
|
|
@@ -233,6 +271,31 @@ export class LynxElement extends LynxContainerNode {
|
|
|
233
271
|
this._backend.setAttributeNS(this._id, ns, name, value == null ? null : String(value));
|
|
234
272
|
}
|
|
235
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Registers a real native gesture detector on this element — see
|
|
276
|
+
* patch-protocol.js's Op.SetGestureDetector for the design. `type` is
|
|
277
|
+
* one of "pan"/"native"/... matching the native GestureType names.
|
|
278
|
+
* `arenaPolicy` decides claim/release timing on the main thread; the
|
|
279
|
+
* resulting touches-down/move/up events arrive back here as ordinary
|
|
280
|
+
* "gesturedown"/"gesturemove"/"gestureup" events — add plain listeners
|
|
281
|
+
* for those the same way as any other event. Returns an id to pass to
|
|
282
|
+
* removeGestureDetector().
|
|
283
|
+
*/
|
|
284
|
+
setGestureDetector(type, arenaPolicy) {
|
|
285
|
+
const gestureId = nextGestureId++;
|
|
286
|
+
this._backend.setGestureDetector(this._id, gestureId, type, arenaPolicy);
|
|
287
|
+
return gestureId;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
removeGestureDetector(gestureId) {
|
|
291
|
+
this._backend.removeGestureDetector(this._id, gestureId);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Only meaningful on an element created via LynxDocument#createNativeList(). */
|
|
295
|
+
setListItems(items) {
|
|
296
|
+
this._backend.setListItems(this._id, items);
|
|
297
|
+
}
|
|
298
|
+
|
|
236
299
|
addEventListener(type, listener) {
|
|
237
300
|
const isNew = !(type in this._listeners);
|
|
238
301
|
this._listeners[type] = listener;
|
|
@@ -298,6 +361,13 @@ export class LynxText extends LynxNode {
|
|
|
298
361
|
constructor(ownerDocument, backend, text) {
|
|
299
362
|
super(ownerDocument);
|
|
300
363
|
this._backend = backend;
|
|
364
|
+
// Mithril's render.js never reads `.nodeValue` back itself (it only
|
|
365
|
+
// ever WRITES it, on an update pass — see render.js's own updateText),
|
|
366
|
+
// so this had no effect on real rendering; it only broke anything
|
|
367
|
+
// ELSE reading a freshly-created text node's value before its first
|
|
368
|
+
// update (found writing a real device-verification test for
|
|
369
|
+
// mithril-lynx-ui — see that repo's test/v2-harness.ts).
|
|
370
|
+
this._text = text;
|
|
301
371
|
this._id = backend.createText(text);
|
|
302
372
|
}
|
|
303
373
|
|
|
@@ -367,6 +437,28 @@ export class LynxDocument extends LynxContainerNode {
|
|
|
367
437
|
createDocumentFragment() {
|
|
368
438
|
return new LynxFragment(this);
|
|
369
439
|
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* A native virtualized list — see patch-protocol.js's Op.CreateList.
|
|
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.
|
|
446
|
+
*/
|
|
447
|
+
createNativeList(options = {}) {
|
|
448
|
+
return new LynxElement(this, this._backend, "list", undefined, {
|
|
449
|
+
scrollOrientation: options.scrollOrientation ?? "vertical",
|
|
450
|
+
listType: options.listType ?? "single",
|
|
451
|
+
spanCount: options.spanCount ?? 1,
|
|
452
|
+
});
|
|
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
|
+
}
|
|
370
462
|
}
|
|
371
463
|
|
|
372
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;
|