mithril-lynx 2.6.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/FETCH_INVESTIGATION.md +3 -2
- package/INPUT.md +115 -0
- package/README.md +47 -8
- package/ROUTE.md +111 -16
- package/ROUTE_CONTRACT_ANALYSIS.md +224 -0
- package/UPDATE_ANIMATION_GAP.md +133 -0
- package/package.json +1 -9
- package/plugin.js +33 -0
- package/src/apply-patch.js +369 -60
- package/src/backends/virtual-backend.js +120 -22
- package/src/background.d.ts +26 -0
- package/src/background.js +116 -7
- package/src/channel.js +22 -5
- package/src/commit.js +9 -0
- package/src/dev-reload-client.js +46 -0
- package/src/dev-transport-noop.js +3 -0
- package/src/fake-dom.js +559 -61
- package/src/list-attributes.js +69 -0
- package/src/list-runtime.js +425 -0
- package/src/main-thread.js +18 -1
- package/src/mount-redraw.js +14 -1
- package/src/patch-protocol.js +41 -16
- package/src/reload/version.js +10 -1
- package/src/request.js +38 -0
- package/src/route.d.ts +30 -2
- package/src/route.js +214 -56
- package/src/testing.d.ts +14 -1
- package/src/testing.js +56 -1
- package/test/event-render-coalescing.test.ts +117 -0
- package/test/gesture.test.ts +75 -0
- package/test/input.test.ts +388 -0
- package/test/list.test.ts +417 -113
- package/test/remove-subtree-cleanup.test.ts +75 -0
- package/test/route-hot-reload.test.ts +2 -1
- package/test/route.test.ts +201 -4
- package/test/style-clear.test.ts +97 -0
- package/.omo/plans/m-request-fetch-lynx.md +0 -306
- package/.omo/plans/m-route-en-memoria.md +0 -397
- package/.omo/plans/mithril-lynx-v2-desde-cero.md +0 -548
- package/.omo/run-continuation/ses_f2f02336cffeO0ssvuxqZ05iZq.json +0 -10
- package/.omo/run-continuation/ses_f2fd5cb5fffeN1tjdUyPqdKG6B.json +0 -10
- package/src/list-cell.d.ts +0 -20
- package/src/list-cell.js +0 -73
- package/src/list-support.d.ts +0 -13
- package/src/list-support.js +0 -197
package/src/fake-dom.js
CHANGED
|
@@ -25,22 +25,46 @@
|
|
|
25
25
|
// side needs to be "a DOM", the other side only needs to be "a PAPI patch
|
|
26
26
|
// applier".
|
|
27
27
|
|
|
28
|
+
import { TYPED_ATTRIBUTES_BY_TAG } from "./list-attributes.js";
|
|
29
|
+
import { FORM_FIELD_TAGS } from "./patch-protocol.js";
|
|
30
|
+
|
|
28
31
|
const DASH_CASE = /-/;
|
|
29
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Converts a camelCase style property name to its dash-case CSS form.
|
|
35
|
+
* @param {string} name - The camelCase property name (e.g. `backgroundColor`).
|
|
36
|
+
* @returns {string} The dash-case name (e.g. `background-color`).
|
|
37
|
+
* @example
|
|
38
|
+
* camelToDash("backgroundColor"); // "background-color"
|
|
39
|
+
*/
|
|
30
40
|
function camelToDash(name) {
|
|
31
41
|
return name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
|
|
32
42
|
}
|
|
33
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Base class for every fake-DOM node: tracks the owning document and the parent link.
|
|
46
|
+
*/
|
|
34
47
|
class LynxNode {
|
|
48
|
+
/**
|
|
49
|
+
* @param {LynxDocument|null} ownerDocument - The document that owns this node (`null` only for the document itself, which sets it afterwards).
|
|
50
|
+
*/
|
|
35
51
|
constructor(ownerDocument) {
|
|
36
52
|
this.ownerDocument = ownerDocument;
|
|
37
53
|
this._parent = null;
|
|
38
54
|
}
|
|
39
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The parent container, or `null` when detached.
|
|
58
|
+
* @returns {LynxContainerNode|null}
|
|
59
|
+
*/
|
|
40
60
|
get parentNode() {
|
|
41
61
|
return this._parent;
|
|
42
62
|
}
|
|
43
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The sibling immediately after this node in its parent's child list.
|
|
66
|
+
* @returns {LynxNode|null} The next sibling, or `null` when detached or last.
|
|
67
|
+
*/
|
|
44
68
|
get nextSibling() {
|
|
45
69
|
if (!this._parent) return null;
|
|
46
70
|
const siblings = this._parent._children;
|
|
@@ -55,12 +79,22 @@ class LynxNode {
|
|
|
55
79
|
// for fragments (CONTRACT.md §c, `createDocumentFragment`): inserting a
|
|
56
80
|
// fragment moves ITS children into the target and leaves the fragment
|
|
57
81
|
// empty, rather than inserting the fragment node itself.
|
|
82
|
+
/**
|
|
83
|
+
* Base class for nodes that can hold children (elements, fragments, the document).
|
|
84
|
+
*/
|
|
58
85
|
class LynxContainerNode extends LynxNode {
|
|
86
|
+
/**
|
|
87
|
+
* @param {LynxDocument|null} ownerDocument - The document that owns this node.
|
|
88
|
+
*/
|
|
59
89
|
constructor(ownerDocument) {
|
|
60
90
|
super(ownerDocument);
|
|
61
91
|
this._children = [];
|
|
62
92
|
}
|
|
63
93
|
|
|
94
|
+
/**
|
|
95
|
+
* The first child node.
|
|
96
|
+
* @returns {LynxNode|null} The first child, or `null` when empty.
|
|
97
|
+
*/
|
|
64
98
|
get firstChild() {
|
|
65
99
|
return this._children[0] ?? null;
|
|
66
100
|
}
|
|
@@ -70,10 +104,19 @@ class LynxContainerNode extends LynxNode {
|
|
|
70
104
|
// `createDocumentFragment()` (createNodes' fragment-batching path) — a
|
|
71
105
|
// real DOM's `childNodes` is a live NodeList, but render.js only ever
|
|
72
106
|
// reads `.length` off it here, so the plain backing array is enough.
|
|
107
|
+
/**
|
|
108
|
+
* The live backing array of child nodes (only `.length` is ever read by render.js).
|
|
109
|
+
* @returns {LynxNode[]}
|
|
110
|
+
*/
|
|
73
111
|
get childNodes() {
|
|
74
112
|
return this._children;
|
|
75
113
|
}
|
|
76
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Tests whether `other` is this node or one of its descendants.
|
|
117
|
+
* @param {LynxNode|null} other - The node to look for.
|
|
118
|
+
* @returns {boolean} `true` when `other` is this node or a descendant of it.
|
|
119
|
+
*/
|
|
77
120
|
contains(other) {
|
|
78
121
|
let node = other;
|
|
79
122
|
while (node) {
|
|
@@ -83,11 +126,23 @@ class LynxContainerNode extends LynxNode {
|
|
|
83
126
|
return false;
|
|
84
127
|
}
|
|
85
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Appends a child at the end of this container.
|
|
131
|
+
* @param {LynxNode} child - The node to append.
|
|
132
|
+
* @returns {LynxNode} The appended node.
|
|
133
|
+
*/
|
|
86
134
|
appendChild(child) {
|
|
87
135
|
this.insertBefore(child, null);
|
|
88
136
|
return child;
|
|
89
137
|
}
|
|
90
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Inserts a child before a reference node, moving it from any previous parent.
|
|
141
|
+
* A fragment is never attached itself: its children are moved in and it is left empty.
|
|
142
|
+
* @param {LynxNode} child - The node to insert.
|
|
143
|
+
* @param {LynxNode|null} refChild - The node to insert before, or `null` to append.
|
|
144
|
+
* @returns {LynxNode} The inserted node.
|
|
145
|
+
*/
|
|
91
146
|
insertBefore(child, refChild) {
|
|
92
147
|
if (child instanceof LynxFragment) {
|
|
93
148
|
// Real DOM semantics: the fragment itself is never attached —
|
|
@@ -112,14 +167,35 @@ class LynxContainerNode extends LynxNode {
|
|
|
112
167
|
return child;
|
|
113
168
|
}
|
|
114
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Removes a child from this container.
|
|
172
|
+
* @param {LynxNode} child - The node to remove.
|
|
173
|
+
* @returns {LynxNode} The removed node.
|
|
174
|
+
*/
|
|
115
175
|
removeChild(child) {
|
|
116
176
|
this._removeChildBookkeeping(child);
|
|
117
177
|
if (this._id != null && child._id != null) {
|
|
118
178
|
this._backend.removeChild(this._id, child._id);
|
|
179
|
+
// render.js only removes the ROOT of a removed subtree, so every
|
|
180
|
+
// descendant has to leave `_nodesById` here too — otherwise the
|
|
181
|
+
// whole detached subtree stays reachable forever. Moves never
|
|
182
|
+
// come through here (insertBefore uses _removeChildBookkeeping).
|
|
183
|
+
const nodesById = this.ownerDocument._nodesById;
|
|
184
|
+
const stack = [child];
|
|
185
|
+
while (stack.length > 0) {
|
|
186
|
+
const node = stack.pop();
|
|
187
|
+
nodesById.delete(node._id);
|
|
188
|
+
if (node._children) for (const c of node._children) stack.push(c);
|
|
189
|
+
}
|
|
119
190
|
}
|
|
120
191
|
return child;
|
|
121
192
|
}
|
|
122
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Detaches `child` from the local child list and clears its parent link, without touching the backend.
|
|
196
|
+
* @param {LynxNode} child - The child to detach.
|
|
197
|
+
* @returns {void}
|
|
198
|
+
*/
|
|
123
199
|
_removeChildBookkeeping(child) {
|
|
124
200
|
const index = this._children.indexOf(child);
|
|
125
201
|
if (index !== -1) this._children.splice(index, 1);
|
|
@@ -127,20 +203,49 @@ class LynxContainerNode extends LynxNode {
|
|
|
127
203
|
}
|
|
128
204
|
}
|
|
129
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Creates the `element.style` object: a proxy that forwards `setProperty`/`removeProperty`
|
|
208
|
+
* and camelCase or dash-case property assignments to the backend as dash-case ops.
|
|
209
|
+
* @param {LynxElement} element - The element the style object belongs to.
|
|
210
|
+
* @returns {CSSStyleDeclaration} A proxy that emulates the subset of `CSSStyleDeclaration` render.js uses.
|
|
211
|
+
*/
|
|
130
212
|
function createStyleProxy(element) {
|
|
131
213
|
const methods = {
|
|
214
|
+
/**
|
|
215
|
+
* @param {string} name - Dash-case style property name.
|
|
216
|
+
* @param {*} value - Value, coerced with `String()`.
|
|
217
|
+
* @returns {void}
|
|
218
|
+
*/
|
|
132
219
|
setProperty(name, value) {
|
|
133
|
-
element.
|
|
220
|
+
element._activeStylesSet.add(name);
|
|
134
221
|
element._backend.setStyleProperty(element._id, name, String(value));
|
|
135
222
|
},
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} name - Dash-case style property name.
|
|
225
|
+
* @returns {void}
|
|
226
|
+
*/
|
|
136
227
|
removeProperty(name) {
|
|
228
|
+
element._activeStylesSet.delete(name);
|
|
137
229
|
element._backend.removeStyleProperty(element._id, name);
|
|
138
230
|
},
|
|
139
231
|
};
|
|
140
232
|
return new Proxy(methods, {
|
|
233
|
+
/**
|
|
234
|
+
* Proxy trap: reads pass through to the `setProperty`/`removeProperty` methods.
|
|
235
|
+
* @param {Object} target - The methods object.
|
|
236
|
+
* @param {string|symbol} prop - The property being read.
|
|
237
|
+
* @returns {*} The property value.
|
|
238
|
+
*/
|
|
141
239
|
get(target, prop) {
|
|
142
240
|
return target[prop];
|
|
143
241
|
},
|
|
242
|
+
/**
|
|
243
|
+
* Proxy trap: assigning a property writes (or, for an empty value, removes) that style property.
|
|
244
|
+
* @param {Object} _target - Unused.
|
|
245
|
+
* @param {string|symbol} prop - The style property name (camelCase or dash-case); symbols are ignored.
|
|
246
|
+
* @param {*} value - The new value; `""`, `null` or `undefined` removes the property.
|
|
247
|
+
* @returns {boolean} Always `true`.
|
|
248
|
+
*/
|
|
144
249
|
set(_target, prop, value) {
|
|
145
250
|
if (typeof prop !== "string") return true;
|
|
146
251
|
// Direct camelCase assignment path (CONTRACT.md §f, line 764/781).
|
|
@@ -148,9 +253,10 @@ function createStyleProxy(element) {
|
|
|
148
253
|
// key shape regardless of which of Mithril's two style paths ran.
|
|
149
254
|
const name = DASH_CASE.test(prop) ? prop : camelToDash(prop);
|
|
150
255
|
if (value === "" || value == null) {
|
|
256
|
+
element._activeStylesSet.delete(name);
|
|
151
257
|
element._backend.removeStyleProperty(element._id, name);
|
|
152
258
|
} else {
|
|
153
|
-
element.
|
|
259
|
+
element._activeStylesSet.add(name);
|
|
154
260
|
element._backend.setStyleProperty(element._id, name, String(value));
|
|
155
261
|
}
|
|
156
262
|
return true;
|
|
@@ -164,41 +270,61 @@ function createStyleProxy(element) {
|
|
|
164
270
|
// on the main thread — a single incrementing counter is simplest).
|
|
165
271
|
let nextGestureId = 1;
|
|
166
272
|
|
|
273
|
+
/**
|
|
274
|
+
* A fake-DOM element that mirrors every mutation to the backend as a patch op.
|
|
275
|
+
*/
|
|
167
276
|
export class LynxElement extends LynxContainerNode {
|
|
168
277
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* use LynxDocument#createNativeList().
|
|
278
|
+
* @param {LynxDocument} ownerDocument - The owning document.
|
|
279
|
+
* @param {Object} backend - The patch backend that records ops.
|
|
280
|
+
* @param {string} tag - The tag name.
|
|
281
|
+
* @param {string} [ns] - The namespace URI, if any.
|
|
174
282
|
*/
|
|
175
|
-
constructor(ownerDocument, backend, tag, ns
|
|
283
|
+
constructor(ownerDocument, backend, tag, ns) {
|
|
176
284
|
super(ownerDocument);
|
|
177
285
|
this._backend = backend;
|
|
178
286
|
this.tag = tag;
|
|
179
287
|
this.namespaceURI = ns;
|
|
180
|
-
this._id =
|
|
181
|
-
? backend.createList(listConfig.scrollOrientation, listConfig.listType, listConfig.spanCount)
|
|
182
|
-
: ns
|
|
183
|
-
? backend.createElementNS(ns, tag)
|
|
184
|
-
: backend.createElement(tag);
|
|
288
|
+
this._id = ns ? backend.createElementNS(ns, tag) : backend.createElement(tag);
|
|
185
289
|
ownerDocument._nodesById.set(this._id, this);
|
|
186
290
|
this._style = null;
|
|
187
|
-
this._styleEverSet = false;
|
|
188
291
|
this._listeners = Object.create(null);
|
|
189
292
|
// `hasPropertyKey` (CONTRACT.md §e) requires `"value" in vnode.dom` etc.
|
|
190
293
|
// to be true for the property-write fast path to apply to form
|
|
191
|
-
// elements — plain own properties satisfy the `in` check.
|
|
192
|
-
|
|
294
|
+
// elements — plain own properties satisfy the `in` check. An
|
|
295
|
+
// <input>/<textarea> has a real `value` accessor on its prototype
|
|
296
|
+
// (LynxFormFieldElement), which an own property would shadow.
|
|
297
|
+
if (!("value" in this)) this.value = undefined;
|
|
193
298
|
this.checked = undefined;
|
|
194
299
|
this.selectedIndex = undefined;
|
|
195
300
|
}
|
|
196
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Lazily created style object for this element.
|
|
304
|
+
* @returns {CSSStyleDeclaration}
|
|
305
|
+
*/
|
|
197
306
|
get style() {
|
|
198
307
|
if (!this._style) this._style = createStyleProxy(this);
|
|
199
308
|
return this._style;
|
|
200
309
|
}
|
|
201
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Dash-case names of the style properties currently applied to this
|
|
313
|
+
* element, created lazily like `style` itself. Lets `style = ""` expand
|
|
314
|
+
* into one RemoveStyleProperty op per property actually set, since there
|
|
315
|
+
* is no device-validated bulk-clear PAPI call.
|
|
316
|
+
* @returns {Set<string>}
|
|
317
|
+
*/
|
|
318
|
+
get _activeStylesSet() {
|
|
319
|
+
if (!this._activeStyles) this._activeStyles = new Set();
|
|
320
|
+
return this._activeStyles;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Replaces the element's style. `""`/`null` clears it, an object assigns each key,
|
|
325
|
+
* and a CSS text string is unsupported (a warning is logged).
|
|
326
|
+
* @param {Object|string|null} value - The new style.
|
|
327
|
+
*/
|
|
202
328
|
set style(value) {
|
|
203
329
|
if (value == null || value === "") {
|
|
204
330
|
// `element.style = ""` (CONTRACT.md §f, lines 750-752): clear.
|
|
@@ -207,15 +333,17 @@ export class LynxElement extends LynxContainerNode {
|
|
|
207
333
|
// had any style at all (its "old is missing or a string, style is
|
|
208
334
|
// an object" branch — see mithril-runtime/render/render.js) — so
|
|
209
335
|
// without this guard, EVERY component with a plain object style
|
|
210
|
-
// prop (an extremely common pattern, not an edge case) would
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
if (
|
|
336
|
+
// prop (an extremely common pattern, not an edge case) would
|
|
337
|
+
// emit a clear op on its very first render. There is no
|
|
338
|
+
// device-validated bulk-clear PAPI call, so the clear is expanded
|
|
339
|
+
// here into one RemoveStyleProperty per property actually applied
|
|
340
|
+
// (tracked by createStyleProxy). The set is read through its
|
|
341
|
+
// backing field so a clear on an element that never had a style
|
|
342
|
+
// neither creates it nor emits anything.
|
|
343
|
+
const active = this._activeStyles;
|
|
344
|
+
if (active == null || active.size === 0) return;
|
|
345
|
+
for (const name of active) this._backend.removeStyleProperty(this._id, name);
|
|
346
|
+
active.clear();
|
|
219
347
|
return;
|
|
220
348
|
}
|
|
221
349
|
if (typeof value !== "object") {
|
|
@@ -238,10 +366,18 @@ export class LynxElement extends LynxContainerNode {
|
|
|
238
366
|
}
|
|
239
367
|
}
|
|
240
368
|
|
|
369
|
+
/**
|
|
370
|
+
* The element's class string.
|
|
371
|
+
* @returns {string} The class string, or `""` when unset.
|
|
372
|
+
*/
|
|
241
373
|
get className() {
|
|
242
374
|
return this._className ?? "";
|
|
243
375
|
}
|
|
244
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Sets the element's classes, emitting a `class` attribute op.
|
|
379
|
+
* @param {string|null|undefined} value - The class string; `null`/`undefined` clears it.
|
|
380
|
+
*/
|
|
245
381
|
set className(value) {
|
|
246
382
|
// Mithril's `setAttr`/`removeAttr` map `className` -> the `"class"`
|
|
247
383
|
// attribute (CONTRACT.md §e); routed here directly since `className`
|
|
@@ -251,6 +387,12 @@ export class LynxElement extends LynxContainerNode {
|
|
|
251
387
|
this._backend.setClasses(this._id, value == null ? "" : String(value));
|
|
252
388
|
}
|
|
253
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Sets an attribute; the `class` name is routed to `className`.
|
|
392
|
+
* @param {string} name - Attribute name.
|
|
393
|
+
* @param {*} value - Attribute value, coerced with `String()`; `null`/`undefined` is sent as `null`.
|
|
394
|
+
* @returns {void}
|
|
395
|
+
*/
|
|
254
396
|
setAttribute(name, value) {
|
|
255
397
|
if (name === "class") {
|
|
256
398
|
this.className = value;
|
|
@@ -259,6 +401,11 @@ export class LynxElement extends LynxContainerNode {
|
|
|
259
401
|
this._backend.setAttribute(this._id, name, value == null ? null : String(value));
|
|
260
402
|
}
|
|
261
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Removes an attribute; removing `class` clears the class string.
|
|
406
|
+
* @param {string} name - Attribute name.
|
|
407
|
+
* @returns {void}
|
|
408
|
+
*/
|
|
262
409
|
removeAttribute(name) {
|
|
263
410
|
if (name === "class") {
|
|
264
411
|
this.className = "";
|
|
@@ -267,6 +414,13 @@ export class LynxElement extends LynxContainerNode {
|
|
|
267
414
|
this._backend.removeAttribute(this._id, name);
|
|
268
415
|
}
|
|
269
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Sets a namespaced attribute.
|
|
419
|
+
* @param {string|null} ns - Attribute namespace.
|
|
420
|
+
* @param {string} name - Attribute name.
|
|
421
|
+
* @param {*} value - Attribute value, coerced with `String()`; `null`/`undefined` is sent as `null`.
|
|
422
|
+
* @returns {void}
|
|
423
|
+
*/
|
|
270
424
|
setAttributeNS(ns, name, value) {
|
|
271
425
|
this._backend.setAttributeNS(this._id, ns, name, value == null ? null : String(value));
|
|
272
426
|
}
|
|
@@ -287,21 +441,33 @@ export class LynxElement extends LynxContainerNode {
|
|
|
287
441
|
return gestureId;
|
|
288
442
|
}
|
|
289
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Removes a gesture detector previously registered with `setGestureDetector()`.
|
|
446
|
+
* @param {number} gestureId - The id returned by `setGestureDetector()`.
|
|
447
|
+
* @returns {void}
|
|
448
|
+
*/
|
|
290
449
|
removeGestureDetector(gestureId) {
|
|
291
450
|
this._backend.removeGestureDetector(this._id, gestureId);
|
|
292
451
|
}
|
|
293
452
|
|
|
294
|
-
/**
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Registers the single listener for an event type; the backend is told only the first time.
|
|
455
|
+
* Replaces any previous listener for the same type.
|
|
456
|
+
* @param {string} type - Event type (e.g. `tap`).
|
|
457
|
+
* @param {Function|{handleEvent: Function}} listener - A function or an EventListener object.
|
|
458
|
+
* @returns {void}
|
|
459
|
+
*/
|
|
299
460
|
addEventListener(type, listener) {
|
|
300
461
|
const isNew = !(type in this._listeners);
|
|
301
462
|
this._listeners[type] = listener;
|
|
302
463
|
if (isNew) this._backend.addEvent(this._id, type);
|
|
303
464
|
}
|
|
304
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Removes the listener for an event type, if any.
|
|
468
|
+
* @param {string} type - Event type.
|
|
469
|
+
* @returns {void}
|
|
470
|
+
*/
|
|
305
471
|
removeEventListener(type) {
|
|
306
472
|
if (!(type in this._listeners)) return;
|
|
307
473
|
delete this._listeners[type];
|
|
@@ -321,6 +487,10 @@ export class LynxElement extends LynxContainerNode {
|
|
|
321
487
|
else if (typeof listener.handleEvent === "function") listener.handleEvent(event);
|
|
322
488
|
}
|
|
323
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Only `""` is supported: removes every child. Any other value logs a warning and is ignored.
|
|
492
|
+
* @param {string} value - Must be the empty string.
|
|
493
|
+
*/
|
|
324
494
|
set textContent(value) {
|
|
325
495
|
// render.js only ever does `dom.textContent = ""` (first-render
|
|
326
496
|
// clear, CONTRACT.md §b line 898) — implemented as "remove every
|
|
@@ -335,6 +505,10 @@ export class LynxElement extends LynxContainerNode {
|
|
|
335
505
|
for (const child of this._children.slice()) this.removeChild(child);
|
|
336
506
|
}
|
|
337
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Unsupported on Lynx elements; logs a warning.
|
|
510
|
+
* @param {string} _value - Ignored.
|
|
511
|
+
*/
|
|
338
512
|
set innerHTML(_value) {
|
|
339
513
|
// `m.trust()`/contenteditable sync (CONTRACT.md §c) — Lynx elements
|
|
340
514
|
// have no HTML-string target to parse into. Documented as
|
|
@@ -347,17 +521,268 @@ export class LynxElement extends LynxContainerNode {
|
|
|
347
521
|
}
|
|
348
522
|
}
|
|
349
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Calls one of the native element's UI methods (`focus`, `getValue`,
|
|
526
|
+
* `scrollTo`, `autoScroll`, …) — the promise form of
|
|
527
|
+
* `lynx.createSelectorQuery().select(…).invoke({ method, params })`,
|
|
528
|
+
* without an `id` or a timer. The call travels with the next patch and
|
|
529
|
+
* runs on the main thread right after that patch is flushed, so it is
|
|
530
|
+
* safe from `oncreate`, when the element does not exist natively yet.
|
|
531
|
+
* Called outside a render (a timer, a promise), it is sent on its own
|
|
532
|
+
* within the same tick.
|
|
533
|
+
* @param {string} method - The UI method name.
|
|
534
|
+
* @param {Object} [params] - The method's parameters.
|
|
535
|
+
* @returns {Promise<unknown>} Resolves with the method's result; rejects
|
|
536
|
+
* with an `Error` carrying the native `code` and `data`, or when the
|
|
537
|
+
* element is removed before the call could run.
|
|
538
|
+
*/
|
|
539
|
+
invoke(method, params) {
|
|
540
|
+
return new Promise((resolve, reject) => {
|
|
541
|
+
const callbackId = this.ownerDocument._registerInvoke(method, resolve, reject);
|
|
542
|
+
this._backend.invokeUIMethod(this._id, method, params ?? {}, callbackId);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Fire-and-forget UI method call, for methods whose result nobody reads.
|
|
548
|
+
* @param {string} method - The UI method name.
|
|
549
|
+
* @returns {void}
|
|
550
|
+
*/
|
|
551
|
+
_invokeAndForget(method) {
|
|
552
|
+
this._backend.invokeUIMethod(this._id, method, {}, 0);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Focuses the element (the native `focus` UI method). Like `invoke()`,
|
|
557
|
+
* it runs once the current patch is applied, so it works from `oncreate`.
|
|
558
|
+
* Mithril's own post-render focus restoration never calls this: the
|
|
559
|
+
* fake document has no `activeElement` (see render.js's `render()`).
|
|
560
|
+
* @returns {void}
|
|
561
|
+
*/
|
|
350
562
|
focus() {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
563
|
+
this._invokeAndForget("focus");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Removes focus from the element (the native `blur` UI method).
|
|
568
|
+
* @returns {void}
|
|
569
|
+
*/
|
|
570
|
+
blur() {
|
|
571
|
+
this._invokeAndForget("blur");
|
|
357
572
|
}
|
|
358
573
|
}
|
|
359
574
|
|
|
575
|
+
/**
|
|
576
|
+
* An `<input>` or `<textarea>`. Native has no `value` attribute, only UI
|
|
577
|
+
* methods, so `value` here is a real property: assigning a different value
|
|
578
|
+
* sends a `setValue` call (Op.SetInputValue), and every forwarded event of
|
|
579
|
+
* the field syncs it back from `detail.value` — exactly what a browser's
|
|
580
|
+
* input does. That makes Mithril's own form handling work unchanged:
|
|
581
|
+
* render.js skips writing a `value` equal to `dom.value`, so the text the
|
|
582
|
+
* user just typed is never sent back, while a value the app changed (a
|
|
583
|
+
* clear, an uppercase transform) is sent once.
|
|
584
|
+
*/
|
|
585
|
+
class LynxFormFieldElement extends LynxElement {
|
|
586
|
+
constructor(ownerDocument, backend, tag, ns) {
|
|
587
|
+
super(ownerDocument, backend, tag, ns);
|
|
588
|
+
this._value = "";
|
|
589
|
+
// How many native `input` events the main thread had counted when the
|
|
590
|
+
// last synced event was sent — echoed in Op.SetInputValue so a value
|
|
591
|
+
// computed before a newer keystroke is dropped (see apply-patch.js).
|
|
592
|
+
this._inputSeq = 0;
|
|
593
|
+
this._selectionStart = -1;
|
|
594
|
+
this._selectionEnd = -1;
|
|
595
|
+
this._autofocused = false;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* The field's current text, as last seen from native or set here.
|
|
600
|
+
* @returns {string}
|
|
601
|
+
*/
|
|
602
|
+
get value() {
|
|
603
|
+
return this._value;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Sets the field's text; `null`/`undefined` clears it. Nothing is sent
|
|
608
|
+
* when it already holds that text.
|
|
609
|
+
* @param {*} value - The new text, coerced with `String()`.
|
|
610
|
+
*/
|
|
611
|
+
set value(value) {
|
|
612
|
+
const text = value == null ? "" : String(value);
|
|
613
|
+
if (text === this._value) return;
|
|
614
|
+
this._value = text;
|
|
615
|
+
this._backend.setInputValue(this._id, text, this._inputSeq);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Selection start from the last forwarded event, `-1` when unknown.
|
|
620
|
+
* @returns {number}
|
|
621
|
+
*/
|
|
622
|
+
get selectionStart() {
|
|
623
|
+
return this._selectionStart;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Read-only: ignored (a `selectionStart` attribute has no meaning here). */
|
|
627
|
+
set selectionStart(_value) {}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Selection end from the last forwarded event, `-1` when unknown.
|
|
631
|
+
* @returns {number}
|
|
632
|
+
*/
|
|
633
|
+
get selectionEnd() {
|
|
634
|
+
return this._selectionEnd;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Read-only: ignored (a `selectionEnd` attribute has no meaning here). */
|
|
638
|
+
set selectionEnd(_value) {}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Whether the field was focused on creation by `autofocus`.
|
|
642
|
+
* @returns {boolean}
|
|
643
|
+
*/
|
|
644
|
+
get autofocus() {
|
|
645
|
+
return this._autofocused;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* `autofocus: true` focuses the field once, when it is created — the
|
|
650
|
+
* HTML attribute's semantics (Lynx has no such attribute). Later
|
|
651
|
+
* redraws, or toggling it, never focus it again.
|
|
652
|
+
* @param {*} value - Truthy to focus.
|
|
653
|
+
*/
|
|
654
|
+
set autofocus(value) {
|
|
655
|
+
if (!value || this._autofocused) return;
|
|
656
|
+
this._autofocused = true;
|
|
657
|
+
this.focus();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Selects a range of the field's text (the native `setSelectionRange` UI method).
|
|
662
|
+
* @param {number} selectionStart - Start index.
|
|
663
|
+
* @param {number} selectionEnd - End index.
|
|
664
|
+
* @returns {Promise<unknown>}
|
|
665
|
+
*/
|
|
666
|
+
setSelectionRange(selectionStart, selectionEnd) {
|
|
667
|
+
return this.invoke("setSelectionRange", { selectionStart, selectionEnd });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Syncs `value`, the selection and the input sequence from a forwarded
|
|
672
|
+
* native event, before its handlers run. Silent: nothing is sent.
|
|
673
|
+
* @param {{detail?: {value?: unknown, selectionStart?: unknown, selectionEnd?: unknown}}} payload - The native event.
|
|
674
|
+
* @param {number} [seq] - The field's native `input` event count.
|
|
675
|
+
* @returns {void}
|
|
676
|
+
*/
|
|
677
|
+
_syncFromEvent(payload, seq) {
|
|
678
|
+
if (typeof seq === "number") this._inputSeq = seq;
|
|
679
|
+
const detail = payload && payload.detail;
|
|
680
|
+
if (detail == null) return;
|
|
681
|
+
if (typeof detail.value === "string") this._value = detail.value;
|
|
682
|
+
if (typeof detail.selectionStart === "number") this._selectionStart = detail.selectionStart;
|
|
683
|
+
if (typeof detail.selectionEnd === "number") this._selectionEnd = detail.selectionEnd;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* A fake-DOM text node backed by a native text element.
|
|
689
|
+
*/
|
|
690
|
+
/**
|
|
691
|
+
* An element whose native attributes are typed (`<list>`, `<list-item>`).
|
|
692
|
+
* Every catalogued attribute is a real property here (defined on the
|
|
693
|
+
* per-tag prototype by {@link typedElementClassFor}), so Mithril's property
|
|
694
|
+
* path assigns raw values — booleans, numbers, the `item-snap` object —
|
|
695
|
+
* instead of stringifying them or turning `false` into a removal. See
|
|
696
|
+
* list-attributes.js for why that matters.
|
|
697
|
+
*/
|
|
698
|
+
class LynxTypedElement extends LynxElement {
|
|
699
|
+
constructor(ownerDocument, backend, tag, ns) {
|
|
700
|
+
super(ownerDocument, backend, tag, ns);
|
|
701
|
+
this._typedValues = Object.create(null);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Sets or clears one typed attribute, keeping its raw value. Objects are
|
|
706
|
+
* compared by content: Mithril re-assigns object attributes on every
|
|
707
|
+
* redraw (render.js `setAttr` never skips `typeof value === "object"`),
|
|
708
|
+
* and re-sending an unchanged `item-snap` would be a wasted native write.
|
|
709
|
+
* @param {string} name - The attribute name.
|
|
710
|
+
* @param {*} value - The raw value; `null`/`undefined` removes the attribute.
|
|
711
|
+
* @returns {void}
|
|
712
|
+
*/
|
|
713
|
+
_setTypedAttribute(name, value) {
|
|
714
|
+
const had = name in this._typedValues;
|
|
715
|
+
if (value == null) {
|
|
716
|
+
if (!had) return;
|
|
717
|
+
delete this._typedValues[name];
|
|
718
|
+
this._backend.removeAttribute(this._id, name);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (had) {
|
|
722
|
+
const previous = this._typedValues[name];
|
|
723
|
+
if (previous === value) return;
|
|
724
|
+
if (typeof value === "object" && typeof previous === "object" && JSON.stringify(previous) === JSON.stringify(value)) return;
|
|
725
|
+
}
|
|
726
|
+
this._typedValues[name] = value;
|
|
727
|
+
this._backend.setAttribute(this._id, name, value);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Catalogued names keep their raw type; everything else behaves like
|
|
732
|
+
* {@link LynxElement#setAttribute}.
|
|
733
|
+
* @param {string} name - Attribute name.
|
|
734
|
+
* @param {*} value - Attribute value.
|
|
735
|
+
* @returns {void}
|
|
736
|
+
*/
|
|
737
|
+
setAttribute(name, value) {
|
|
738
|
+
if (TYPED_ATTRIBUTES_BY_TAG[this.tag].includes(name)) this._setTypedAttribute(name, value);
|
|
739
|
+
else super.setAttribute(name, value);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* @param {string} name - Attribute name.
|
|
744
|
+
* @returns {void}
|
|
745
|
+
*/
|
|
746
|
+
removeAttribute(name) {
|
|
747
|
+
if (TYPED_ATTRIBUTES_BY_TAG[this.tag].includes(name)) this._setTypedAttribute(name, null);
|
|
748
|
+
else super.removeAttribute(name);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const typedElementClasses = Object.create(null);
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Returns (and caches) the element class for a typed tag: a subclass of
|
|
756
|
+
* LynxTypedElement with one accessor per catalogued attribute on its
|
|
757
|
+
* prototype, so `name in element` holds for Mithril's `hasPropertyKey`.
|
|
758
|
+
* @param {string} tag - A key of TYPED_ATTRIBUTES_BY_TAG.
|
|
759
|
+
* @returns {typeof LynxTypedElement}
|
|
760
|
+
*/
|
|
761
|
+
function typedElementClassFor(tag) {
|
|
762
|
+
let cls = typedElementClasses[tag];
|
|
763
|
+
if (cls) return cls;
|
|
764
|
+
cls = class extends LynxTypedElement {};
|
|
765
|
+
for (const name of TYPED_ATTRIBUTES_BY_TAG[tag]) {
|
|
766
|
+
Object.defineProperty(cls.prototype, name, {
|
|
767
|
+
get() {
|
|
768
|
+
return this._typedValues[name];
|
|
769
|
+
},
|
|
770
|
+
set(value) {
|
|
771
|
+
this._setTypedAttribute(name, value);
|
|
772
|
+
},
|
|
773
|
+
configurable: true,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
typedElementClasses[tag] = cls;
|
|
777
|
+
return cls;
|
|
778
|
+
}
|
|
779
|
+
|
|
360
780
|
export class LynxText extends LynxNode {
|
|
781
|
+
/**
|
|
782
|
+
* @param {LynxDocument} ownerDocument - The owning document.
|
|
783
|
+
* @param {Object} backend - The patch backend used to create and update the text.
|
|
784
|
+
* @param {string} text - The initial text.
|
|
785
|
+
*/
|
|
361
786
|
constructor(ownerDocument, backend, text) {
|
|
362
787
|
super(ownerDocument);
|
|
363
788
|
this._backend = backend;
|
|
@@ -371,10 +796,18 @@ export class LynxText extends LynxNode {
|
|
|
371
796
|
this._id = backend.createText(text);
|
|
372
797
|
}
|
|
373
798
|
|
|
799
|
+
/**
|
|
800
|
+
* The node's text.
|
|
801
|
+
* @returns {string}
|
|
802
|
+
*/
|
|
374
803
|
get nodeValue() {
|
|
375
804
|
return this._text;
|
|
376
805
|
}
|
|
377
806
|
|
|
807
|
+
/**
|
|
808
|
+
* Updates the text and emits a set-text op.
|
|
809
|
+
* @param {string} value - The new text.
|
|
810
|
+
*/
|
|
378
811
|
set nodeValue(value) {
|
|
379
812
|
this._text = value;
|
|
380
813
|
this._backend.setText(this._id, value);
|
|
@@ -387,9 +820,18 @@ export class LynxText extends LynxNode {
|
|
|
387
820
|
// (this._id != null && child._id != null)` guards in insertBefore/
|
|
388
821
|
// removeChild are what keep a fragment-as-parent from ever trying to call
|
|
389
822
|
// the backend for itself.
|
|
823
|
+
/**
|
|
824
|
+
* A document fragment: never gets a backend id; its children are moved into the target on insertion.
|
|
825
|
+
*/
|
|
390
826
|
export class LynxFragment extends LynxContainerNode {}
|
|
391
827
|
|
|
828
|
+
/**
|
|
829
|
+
* The fake document handed to Mithril's render.js; id `0` is the real page container.
|
|
830
|
+
*/
|
|
392
831
|
export class LynxDocument extends LynxContainerNode {
|
|
832
|
+
/**
|
|
833
|
+
* @param {Object} backend - The patch backend that records ops (see `createVirtualBackend()`).
|
|
834
|
+
*/
|
|
393
835
|
constructor(backend) {
|
|
394
836
|
super(null);
|
|
395
837
|
this._backend = backend;
|
|
@@ -401,66 +843,122 @@ export class LynxDocument extends LynxContainerNode {
|
|
|
401
843
|
this._id = 0;
|
|
402
844
|
this.namespaceURI = undefined;
|
|
403
845
|
/** id -> node, for dispatching a forwarded native event (which only
|
|
404
|
-
* carries an id + type) to the right fake-dom element. Populated
|
|
405
|
-
*
|
|
406
|
-
* have no id
|
|
846
|
+
* carries an id + type) to the right fake-dom element. Populated only
|
|
847
|
+
* by the LynxElement constructor: text nodes have an id but are never
|
|
848
|
+
* event targets, fragments have no id at all, and the document itself
|
|
849
|
+
* (id 0) is not registered either. */
|
|
407
850
|
this._nodesById = new Map();
|
|
851
|
+
// callbackId -> pending `invoke()` promise, settled by resolveInvoke().
|
|
852
|
+
this._invokeCallbacks = new Map();
|
|
853
|
+
this._nextCallbackId = 1;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Registers a pending `invoke()` promise.
|
|
858
|
+
* @param {string} method - The UI method name, for the error message.
|
|
859
|
+
* @param {(data: unknown) => void} resolve - Settles it successfully.
|
|
860
|
+
* @param {(error: Error) => void} reject - Settles it with an error.
|
|
861
|
+
* @returns {number} The callback id to send with Op.InvokeUIMethod.
|
|
862
|
+
*/
|
|
863
|
+
_registerInvoke(method, resolve, reject) {
|
|
864
|
+
const callbackId = this._nextCallbackId++;
|
|
865
|
+
this._invokeCallbacks.set(callbackId, { method, resolve, reject });
|
|
866
|
+
return callbackId;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Settles a pending `invoke()` with the result forwarded by the main
|
|
871
|
+
* thread. Unknown ids are ignored.
|
|
872
|
+
* @param {{callbackId: number, code: number, data: unknown}} result - The result.
|
|
873
|
+
* @returns {void}
|
|
874
|
+
*/
|
|
875
|
+
resolveInvoke(result) {
|
|
876
|
+
const pending = result && this._invokeCallbacks.get(result.callbackId);
|
|
877
|
+
if (pending == null) return;
|
|
878
|
+
this._invokeCallbacks.delete(result.callbackId);
|
|
879
|
+
if (result.code === 0) {
|
|
880
|
+
pending.resolve(result.data);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const detail = typeof result.data === "string" ? result.data : JSON.stringify(result.data);
|
|
884
|
+
const error = new Error(`[mithril-lynx] UI method "${pending.method}" failed (code ${result.code}): ${detail}`);
|
|
885
|
+
error.code = result.code;
|
|
886
|
+
error.data = result.data;
|
|
887
|
+
pending.reject(error);
|
|
408
888
|
}
|
|
409
889
|
|
|
890
|
+
/**
|
|
891
|
+
* Looks up a node by backend id, used to route forwarded native events.
|
|
892
|
+
* @param {number} id - The backend id.
|
|
893
|
+
* @returns {LynxElement|null} The element, or `null` when unknown (text nodes and the document are never registered).
|
|
894
|
+
*/
|
|
410
895
|
getNodeById(id) {
|
|
411
896
|
return this._nodesById.get(id) ?? null;
|
|
412
897
|
}
|
|
413
898
|
|
|
899
|
+
/**
|
|
900
|
+
* No-op; present so render.js's focus-restoration check never throws.
|
|
901
|
+
* @returns {void}
|
|
902
|
+
*/
|
|
414
903
|
focus() {
|
|
415
904
|
// Never meaningfully called on the document root itself; present so
|
|
416
905
|
// render.js's post-render focus-restoration check (CONTRACT.md §b)
|
|
417
906
|
// never throws if `activeElement` happens to resolve to the root.
|
|
418
907
|
}
|
|
419
908
|
|
|
909
|
+
/**
|
|
910
|
+
* Only `""` is honored: removes every child of the document root.
|
|
911
|
+
* @param {string} value - Must be the empty string for any effect.
|
|
912
|
+
*/
|
|
420
913
|
set textContent(value) {
|
|
421
914
|
if (value !== "") return;
|
|
422
915
|
for (const child of this._children.slice()) this.removeChild(child);
|
|
423
916
|
}
|
|
424
917
|
|
|
918
|
+
/**
|
|
919
|
+
* Creates an element.
|
|
920
|
+
* @param {string} tag - The tag name.
|
|
921
|
+
* @returns {LynxElement}
|
|
922
|
+
*/
|
|
425
923
|
createElement(tag) {
|
|
924
|
+
if (tag in TYPED_ATTRIBUTES_BY_TAG) return new (typedElementClassFor(tag))(this, this._backend, tag, undefined);
|
|
925
|
+
if (FORM_FIELD_TAGS.includes(tag)) return new LynxFormFieldElement(this, this._backend, tag, undefined);
|
|
426
926
|
return new LynxElement(this, this._backend, tag, undefined);
|
|
427
927
|
}
|
|
428
928
|
|
|
929
|
+
/**
|
|
930
|
+
* Creates a namespaced element.
|
|
931
|
+
* @param {string} ns - The namespace URI.
|
|
932
|
+
* @param {string} tag - The tag name.
|
|
933
|
+
* @returns {LynxElement}
|
|
934
|
+
*/
|
|
429
935
|
createElementNS(ns, tag) {
|
|
430
936
|
return new LynxElement(this, this._backend, tag, ns);
|
|
431
937
|
}
|
|
432
938
|
|
|
939
|
+
/**
|
|
940
|
+
* Creates a text node.
|
|
941
|
+
* @param {string} text - The initial text.
|
|
942
|
+
* @returns {LynxText}
|
|
943
|
+
*/
|
|
433
944
|
createTextNode(text) {
|
|
434
945
|
return new LynxText(this, this._backend, text);
|
|
435
946
|
}
|
|
436
947
|
|
|
437
|
-
createDocumentFragment() {
|
|
438
|
-
return new LynxFragment(this);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
948
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* from an app's own `items`/`renderItem`) — see
|
|
445
|
-
* docs/native-papi/papi-06-virtualized-lists.md in mithril-lynx-ui.
|
|
949
|
+
* Creates an empty document fragment.
|
|
950
|
+
* @returns {LynxFragment}
|
|
446
951
|
*/
|
|
447
|
-
|
|
448
|
-
return new
|
|
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);
|
|
952
|
+
createDocumentFragment() {
|
|
953
|
+
return new LynxFragment(this);
|
|
461
954
|
}
|
|
462
955
|
}
|
|
463
956
|
|
|
957
|
+
/**
|
|
958
|
+
* Creates a fake document bound to a patch backend.
|
|
959
|
+
* @param {Object} backend - The patch backend (see `createVirtualBackend()`).
|
|
960
|
+
* @returns {LynxDocument}
|
|
961
|
+
*/
|
|
464
962
|
export function createLynxDocument(backend) {
|
|
465
963
|
return new LynxDocument(backend);
|
|
466
964
|
}
|