mithril-lynx 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRACT.md +151 -0
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/background.d.ts +54 -0
- package/background.js +169 -0
- package/element.d.ts +34 -0
- package/element.js +79 -0
- package/gesture.d.ts +40 -0
- package/gesture.js +117 -0
- package/internal/constants.js +26 -0
- package/internal/virtual-node.js +388 -0
- package/list.d.ts +31 -0
- package/list.js +185 -0
- package/main-thread.d.ts +43 -0
- package/main-thread.js +165 -0
- package/package.json +100 -0
- package/plugin.d.ts +17 -0
- package/plugin.js +185 -0
- package/renderer/background.d.ts +21 -0
- package/renderer/background.js +84 -0
- package/renderer/main-thread.d.ts +12 -0
- package/renderer/main-thread.js +175 -0
- package/src/lynx-mithril-shim.d.ts +16 -0
- package/src/lynx-mithril-shim.js +1477 -0
- package/src/worklet-runtime.js +82 -0
- package/testing.d.ts +10 -0
- package/testing.js +91 -0
package/element.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Ambient declaration for the ESM element.js (the file itself is not
|
|
2
|
+
// type-checked; this describes its runtime export shape for TS consumers).
|
|
3
|
+
|
|
4
|
+
export interface SelectorParams {
|
|
5
|
+
onlyCurrentComponent?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface AnimationTimingOptions {
|
|
9
|
+
name?: string;
|
|
10
|
+
duration?: number | string;
|
|
11
|
+
delay?: number | string;
|
|
12
|
+
iterationCount?: number | string;
|
|
13
|
+
fillMode?: string;
|
|
14
|
+
timingFunction?: string;
|
|
15
|
+
direction?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type Keyframe = Record<string, string | number>;
|
|
19
|
+
|
|
20
|
+
export interface MainThreadElement {
|
|
21
|
+
setStyleProperty(name: string, value: string | number): void;
|
|
22
|
+
setStyleProperties(styles: Record<string, string | number>): void;
|
|
23
|
+
setAttribute(name: string, value: unknown): void;
|
|
24
|
+
querySelector(selector: string, params?: SelectorParams): MainThreadElement | null;
|
|
25
|
+
querySelectorAll(selector: string, params?: SelectorParams): MainThreadElement[];
|
|
26
|
+
animate(keyframes: Keyframe[], options?: AnimationTimingOptions): void;
|
|
27
|
+
playAnimation(name: string): void;
|
|
28
|
+
pauseAnimation(name: string): void;
|
|
29
|
+
cancelAnimation(name: string): void;
|
|
30
|
+
invoke(method: string, params?: Record<string, unknown>): Promise<{ code: number; data: unknown }>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Wraps a node (anything with a `_handle`, i.e. a real LynxNodeWrapper) with imperative PAPI methods. */
|
|
34
|
+
export function wrapElement(node: { _handle: unknown }): MainThreadElement;
|
package/element.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// element.js
|
|
2
|
+
//
|
|
3
|
+
// Ergonomic imperative escape hatch for a main-thread node — whatever
|
|
4
|
+
// Mithril's oncreate(vnode)/onupdate(vnode) hooks hand you as `vnode.dom`
|
|
5
|
+
// (works for any real LynxNodeWrapper: main-thread-owned mode, or
|
|
6
|
+
// data-channel mode's main-thread half). Not part of render.js's own DOM
|
|
7
|
+
// contract (see ../CONTRACT.md) — these are Element PAPI capabilities apps
|
|
8
|
+
// reach for directly (focusing a native input, animating, calling a native
|
|
9
|
+
// custom element's method), so they live in their own small module instead
|
|
10
|
+
// of the shim.
|
|
11
|
+
//
|
|
12
|
+
// Project plan, Phase 5. Background-thread refs are the selector-query
|
|
13
|
+
// equivalent — see background.js's createRef().
|
|
14
|
+
|
|
15
|
+
const ANIMATION_OPERATION = { START: 0, PLAY: 1, PAUSE: 2, CANCEL: 3 };
|
|
16
|
+
|
|
17
|
+
function wrapRef(handle) {
|
|
18
|
+
return handle == null ? null : wrapElement({ _handle: handle });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wraps a node (anything with a `_handle`, i.e. a real LynxNodeWrapper OR a
|
|
23
|
+
* raw ElementRef from querySelector) with imperative PAPI methods render.js
|
|
24
|
+
* itself never needs. setAttribute mirrors the real LynxNodeWrapper's own
|
|
25
|
+
* class/id/data-prefixed/generic-attribute special-casing (see
|
|
26
|
+
* ../CONTRACT.md) rather than delegating to node.setAttribute() directly —
|
|
27
|
+
* a raw querySelector result has no such method, only a `_handle`.
|
|
28
|
+
*/
|
|
29
|
+
export function wrapElement(node) {
|
|
30
|
+
const handle = node._handle;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
setStyleProperty(name, value) {
|
|
34
|
+
__SetInlineStyles(handle, { [name]: value });
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
setStyleProperties(styles) {
|
|
38
|
+
__SetInlineStyles(handle, styles);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
setAttribute(name, value) {
|
|
42
|
+
if (name === "class") __SetClasses(handle, value == null ? undefined : String(value));
|
|
43
|
+
else if (name === "id") __SetID(handle, value == null ? null : String(value));
|
|
44
|
+
else if (name.slice(0, 5) === "data-") __AddDataset(handle, name.slice(5), value);
|
|
45
|
+
else __SetAttribute(handle, name, value == null ? null : value);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
querySelector(selector, params) {
|
|
49
|
+
return wrapRef(__QuerySelector(handle, selector, params || {}));
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
querySelectorAll(selector, params) {
|
|
53
|
+
return __QuerySelectorAll(handle, selector, params || {}).map((ref) => wrapRef(ref));
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
animate(keyframes, options) {
|
|
57
|
+
__ElementAnimate(handle, [ANIMATION_OPERATION.START, (options && options.name) || "", keyframes, options]);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
playAnimation(name) {
|
|
61
|
+
__ElementAnimate(handle, [ANIMATION_OPERATION.PLAY, name]);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
pauseAnimation(name) {
|
|
65
|
+
__ElementAnimate(handle, [ANIMATION_OPERATION.PAUSE, name]);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
cancelAnimation(name) {
|
|
69
|
+
__ElementAnimate(handle, [ANIMATION_OPERATION.CANCEL, name]);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/** Always resolves with { code, data } — check `code` yourself; PAPI's success/failure convention isn't assumed here. */
|
|
73
|
+
invoke(method, params) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
__InvokeUIMethod(handle, method, params || {}, (res) => resolve(res));
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
package/gesture.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Ambient declaration for the ESM gesture.js (the file itself is not
|
|
2
|
+
// type-checked; this describes its runtime export shape for TS consumers).
|
|
3
|
+
|
|
4
|
+
export const GestureType: {
|
|
5
|
+
readonly COMPOSED: -1;
|
|
6
|
+
readonly PAN: 0;
|
|
7
|
+
readonly FLING: 1;
|
|
8
|
+
readonly DEFAULT: 2;
|
|
9
|
+
readonly TAP: 3;
|
|
10
|
+
readonly LONGPRESS: 4;
|
|
11
|
+
readonly ROTATION: 5;
|
|
12
|
+
readonly PINCH: 6;
|
|
13
|
+
readonly NATIVE: 7;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type GestureTypeName = keyof typeof GestureType;
|
|
17
|
+
|
|
18
|
+
export interface Gesture {
|
|
19
|
+
id: number;
|
|
20
|
+
remove(): void;
|
|
21
|
+
setState(state: number): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GestureController {
|
|
25
|
+
__SetGestureState(state: number): void;
|
|
26
|
+
__ConsumeGesture(options: Record<string, boolean>): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CreateGestureOptions {
|
|
30
|
+
type: number | GestureTypeName;
|
|
31
|
+
/** Each callback is invoked as `(event, controller) => {}`; `controller` can usually be ignored. */
|
|
32
|
+
callbacks?: Record<string, (event: unknown, controller: GestureController) => unknown>;
|
|
33
|
+
waitFor?: Gesture[];
|
|
34
|
+
simultaneousWith?: Gesture[];
|
|
35
|
+
continueWith?: Gesture[];
|
|
36
|
+
config?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Registers a gesture detector on a node (anything with a `_handle`). See the project plan, Phase 7. */
|
|
40
|
+
export function createGesture(node: { _handle: unknown }, options: CreateGestureOptions): Gesture;
|
package/gesture.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// gesture.js
|
|
2
|
+
//
|
|
3
|
+
// Gesture composition API (project plan, Phase 7), main-thread only. Built
|
|
4
|
+
// on __SetGestureDetector — a plain, same-thread PAPI call (no cross-thread
|
|
5
|
+
// serialization boundary), so this needs nothing beyond a real node handle
|
|
6
|
+
// PLUS the small worklet-registration step below. A callback that needs to
|
|
7
|
+
// notify background-owned state can call main-thread.js's
|
|
8
|
+
// runOnBackground() itself, inside the callback body — an explicit, opt-in
|
|
9
|
+
// cross-thread hop only where the app actually needs one.
|
|
10
|
+
//
|
|
11
|
+
// RESOLVED (2026-09-09), via the SAME technique used to fix list.js's Tier
|
|
12
|
+
// 2 (read the real, shipped @lynx-js/react source more deeply — see
|
|
13
|
+
// DEVICE_VERIFICATION.md), plus one config-level bug found the hard way.
|
|
14
|
+
// THREE issues stacked here, found and fixed in this order:
|
|
15
|
+
//
|
|
16
|
+
// 1. The config/relationMap shape below matches @lynx-js/react's own
|
|
17
|
+
// runtime/lib/snapshot/gesture/processGesture.js verbatim (real, shipped
|
|
18
|
+
// code — __SetGestureDetector for both create and update, has-react-
|
|
19
|
+
// gesture/flatten set first). But that file's own callback slot type is
|
|
20
|
+
// loose (`callback: unknown`) — reading one file deeper,
|
|
21
|
+
// runtime/lib/worklet-runtime/workletRuntime.js, revealed native invokes
|
|
22
|
+
// gesture callbacks through a global `runWorklet(ctx, params)`, whose
|
|
23
|
+
// `validateWorklet(ctx)` requires `typeof ctx === 'object'`. A plain JS
|
|
24
|
+
// function fails that and is dropped with NO error — exactly this
|
|
25
|
+
// project's first real-device symptom (zero console output on a swipe).
|
|
26
|
+
// Fixed by `./src/worklet-runtime.js`'s `wrapWorkletCallback()`.
|
|
27
|
+
// 2. Even with (1) fixed, still zero output — because `enableNewGesture`
|
|
28
|
+
// (a Lynx SDK compiler/runtime option, @lynx-js/type-config's
|
|
29
|
+
// config.d.ts, `@defaultValue false`) was never turned on for this app.
|
|
30
|
+
// Without it, __SetGestureDetector registrations are accepted but the
|
|
31
|
+
// runtime keeps using "the legacy touch-only gesture path" and never
|
|
32
|
+
// acts on them at all — see the consuming app's lynx.config.ts.
|
|
33
|
+
// 3. With both fixed, native finally called into JS — and immediately threw
|
|
34
|
+
// `TypeError: not a object`. Bisected step-by-step on-device: native
|
|
35
|
+
// calls a gesture callback with TWO arguments, `(event, controller)`,
|
|
36
|
+
// where `controller` is a native host object (`{__SetGestureState,
|
|
37
|
+
// __ConsumeGesture}`). That object throws when it crosses
|
|
38
|
+
// `Function.prototype.apply()`'s argument-list marshalling, but is fine
|
|
39
|
+
// passed positionally — matching @lynx-js/react's own runWorkletImpl,
|
|
40
|
+
// which never uses apply()/call() either (`worklet(...params_)`, a
|
|
41
|
+
// plain spread call). Fixed in `./src/worklet-runtime.js`.
|
|
42
|
+
//
|
|
43
|
+
// Confirmed end-to-end on the real device: a full pan (start → update →
|
|
44
|
+
// end) updates UI state with zero errors. See `./src/worklet-runtime.js`
|
|
45
|
+
// for exactly what's reimplemented vs. scoped out of the real worklet
|
|
46
|
+
// runtime, and `callbacks`' new second-argument note below.
|
|
47
|
+
|
|
48
|
+
import { wrapWorkletCallback } from "./src/worklet-runtime.js";
|
|
49
|
+
|
|
50
|
+
export const GestureType = {
|
|
51
|
+
COMPOSED: -1,
|
|
52
|
+
PAN: 0,
|
|
53
|
+
FLING: 1,
|
|
54
|
+
DEFAULT: 2,
|
|
55
|
+
TAP: 3,
|
|
56
|
+
LONGPRESS: 4,
|
|
57
|
+
ROTATION: 5,
|
|
58
|
+
PINCH: 6,
|
|
59
|
+
NATIVE: 7,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let nextGestureId = 1;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Registers a gesture detector on `node` (anything with a `_handle`, i.e. a
|
|
66
|
+
* real LynxNodeWrapper). `type` is a GestureType value or its string key
|
|
67
|
+
* ("pan", "tap", ...). `callbacks` keys are event names (e.g. "onStart",
|
|
68
|
+
* "onUpdate", "onEnd") mapped to plain functions, called directly, same
|
|
69
|
+
* thread. Each is invoked as `(event, controller) => {}` — `controller`
|
|
70
|
+
* (`{__SetGestureState, __ConsumeGesture}`) is a native gesture-arena
|
|
71
|
+
* handle; most callbacks can ignore it entirely.
|
|
72
|
+
* `waitFor`/`simultaneousWith`/`continueWith` are arrays of OTHER
|
|
73
|
+
* createGesture() return values, for gesture-arena composition.
|
|
74
|
+
*/
|
|
75
|
+
export function createGesture(node, options) {
|
|
76
|
+
const {
|
|
77
|
+
type,
|
|
78
|
+
callbacks = {},
|
|
79
|
+
waitFor = [],
|
|
80
|
+
simultaneousWith = [],
|
|
81
|
+
continueWith = [],
|
|
82
|
+
config,
|
|
83
|
+
} = options;
|
|
84
|
+
const handle = node._handle;
|
|
85
|
+
const gestureType = typeof type === "string" ? GestureType[type.toUpperCase()] : type;
|
|
86
|
+
const id = nextGestureId++;
|
|
87
|
+
|
|
88
|
+
// Marker attributes native needs to recognize a gesture-enabled element —
|
|
89
|
+
// kept verbatim from processGesture.js's own names/values, since it's
|
|
90
|
+
// unclear whether native checks these exact strings independent of
|
|
91
|
+
// framework, or whether they're purely a ReactLynx-side bookkeeping detail.
|
|
92
|
+
__SetAttribute(handle, "has-react-gesture", true);
|
|
93
|
+
__SetAttribute(handle, "flatten", false);
|
|
94
|
+
|
|
95
|
+
const detectorConfig = {
|
|
96
|
+
callbacks: Object.keys(callbacks).map((name) => ({ name, callback: wrapWorkletCallback(callbacks[name]) })),
|
|
97
|
+
};
|
|
98
|
+
if (config != null) detectorConfig.config = config;
|
|
99
|
+
|
|
100
|
+
const relationMap = {
|
|
101
|
+
waitFor: waitFor.map((g) => g.id),
|
|
102
|
+
simultaneous: simultaneousWith.map((g) => g.id),
|
|
103
|
+
continueWith: continueWith.map((g) => g.id),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
__SetGestureDetector(handle, id, gestureType, detectorConfig, relationMap);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
remove() {
|
|
111
|
+
if (typeof __RemoveGestureDetector === "function") __RemoveGestureDetector(handle, id);
|
|
112
|
+
},
|
|
113
|
+
setState(state) {
|
|
114
|
+
__SetGestureState(handle, id, state);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Shared event-name constants for the main-thread <-> background-thread
|
|
2
|
+
// channel, ported from lynx-examples/examples/vanilla/src/common/constant.ts.
|
|
3
|
+
// Native engine lifecycle events keep their exact native names; custom
|
|
4
|
+
// app-level events are namespaced to avoid colliding with other libraries
|
|
5
|
+
// dispatching on the same shared event bus.
|
|
6
|
+
|
|
7
|
+
export const renderPageEventName = "__RenderPage";
|
|
8
|
+
export const updatePageEventName = "__UpdatePage";
|
|
9
|
+
export const destroyLifetimeEventName = "__DestroyLifetime";
|
|
10
|
+
|
|
11
|
+
export const updateDataFromMainThreadEventName = "MithrilLynx:UpdateDataFromMainThread";
|
|
12
|
+
export const updateDataFromBackgroundEventName = "MithrilLynx:UpdateDataFromBackground";
|
|
13
|
+
export const dispatchEventToBackgroundEventName = "MithrilLynx:DispatchEventToBackground";
|
|
14
|
+
|
|
15
|
+
// Renderer mode (Phase 4): background thread ships op-log patches to main
|
|
16
|
+
// thread; main thread forwards real PAPI events back to background by vid.
|
|
17
|
+
export const rendererPatchEventName = "MithrilLynx:RendererPatch";
|
|
18
|
+
export const rendererEventEventName = "MithrilLynx:RendererEvent";
|
|
19
|
+
|
|
20
|
+
// Cross-thread function registry (Phase 6, worklet substitute): call/return
|
|
21
|
+
// correlation for genuinely cross-thread calls. "ToMainThread"/"ToBackground"
|
|
22
|
+
// name which side the CALL travels to — each has its own result event.
|
|
23
|
+
export const callMainThreadEventName = "MithrilLynx:CallMainThread";
|
|
24
|
+
export const callMainThreadResultEventName = "MithrilLynx:CallMainThreadResult";
|
|
25
|
+
export const callBackgroundEventName = "MithrilLynx:CallBackground";
|
|
26
|
+
export const callBackgroundResultEventName = "MithrilLynx:CallBackgroundResult";
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// internal/virtual-node.js
|
|
2
|
+
//
|
|
3
|
+
// Background-thread-only "virtual" mirror of LynxNodeWrapper/LynxStyleProxy
|
|
4
|
+
// (src/lynx-mithril-shim.js). Implements the EXACT SAME duck-typed DOM
|
|
5
|
+
// surface Mithril's render.js needs (per ../CONTRACT.md) — createElement,
|
|
6
|
+
// appendChild, style, addEventListener, etc — but backed by a plain-JS
|
|
7
|
+
// shadow tree instead of real Element PAPI calls, since the background
|
|
8
|
+
// thread has no PAPI access at all.
|
|
9
|
+
//
|
|
10
|
+
// Every node gets a unique `vid` (vid 0 is reserved for the page root, by
|
|
11
|
+
// convention shared with renderer/main-thread.js — no handshake needed).
|
|
12
|
+
// Every WRITE emits a serializable op addressed by vid; ops describe the
|
|
13
|
+
// SAME method calls a real LynxNodeWrapper would receive (createElement,
|
|
14
|
+
// appendChild, setProp, setAttribute, setStyleProps, ...), so
|
|
15
|
+
// renderer/main-thread.js's applyPatch() can replay them by calling those
|
|
16
|
+
// exact methods on real LynxNodeWrapper instances — no PAPI-mapping logic
|
|
17
|
+
// is duplicated here; the real wrapper's own (already-tested) setters do
|
|
18
|
+
// that mapping on replay.
|
|
19
|
+
//
|
|
20
|
+
// Because the render algorithm (factory() in the shim) is UNMODIFIED and
|
|
21
|
+
// only ever talks to `dom.*` methods generically, this file is a complete,
|
|
22
|
+
// independent implementation of the wrapper contract — it does not import
|
|
23
|
+
// or extend the real LynxNodeWrapper.
|
|
24
|
+
|
|
25
|
+
"use strict"
|
|
26
|
+
|
|
27
|
+
var DIRECT_PROPS = ["value", "checked", "selectedIndex", "className", "id", "type"]
|
|
28
|
+
|
|
29
|
+
function camelize(str) {
|
|
30
|
+
return str.replace(/-([a-z])/g, function (m, c) { return c.toUpperCase() })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Mirrors the real LynxStyleProxy's registry exactly: render.js sometimes
|
|
34
|
+
// writes a style property via plain camelCase assignment
|
|
35
|
+
// (`element.style.fontSize = "20px"`) with no prior setProperty() call, so
|
|
36
|
+
// no accessor exists yet to trigger a flush on write. Flushing every
|
|
37
|
+
// registered proxy at the end of a render/redraw pass (mirroring the real
|
|
38
|
+
// shim's flushTree(), see createVirtualDocument's returned flushStyleProxies)
|
|
39
|
+
// is the safety net that catches those. The registry is scoped to one
|
|
40
|
+
// createVirtualDocument() call (passed in as `register`), not module-level —
|
|
41
|
+
// a real app only ever creates one document, but this keeps independent
|
|
42
|
+
// virtual trees (as in tests) from leaking proxies into each other.
|
|
43
|
+
function VirtualStyleProxy(wrapper, register) {
|
|
44
|
+
this._wrapper = wrapper
|
|
45
|
+
this._props = {}
|
|
46
|
+
register(this)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
VirtualStyleProxy.prototype._flush = function () {
|
|
50
|
+
var styles = {}
|
|
51
|
+
var keys = Object.keys(this)
|
|
52
|
+
for (var i = 0; i < keys.length; i++) {
|
|
53
|
+
var key = keys[i]
|
|
54
|
+
if (key === "_wrapper" || key === "_props") continue
|
|
55
|
+
var value = this[key]
|
|
56
|
+
if (value == null) continue
|
|
57
|
+
styles[camelize(key)] = String(value)
|
|
58
|
+
}
|
|
59
|
+
this._wrapper._emit({ op: "setStyleProps", vid: this._wrapper._vid, styles: styles })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
VirtualStyleProxy.prototype.setProperty = function (name, value) {
|
|
63
|
+
var key = camelize(name)
|
|
64
|
+
if (value == null || value === "") {
|
|
65
|
+
this.removeProperty(name)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
var self = this
|
|
69
|
+
if (!Object.prototype.hasOwnProperty.call(this, key)) {
|
|
70
|
+
Object.defineProperty(this, key, {
|
|
71
|
+
configurable: true,
|
|
72
|
+
enumerable: true,
|
|
73
|
+
get: function () { return self._props[key] },
|
|
74
|
+
set: function (v) {
|
|
75
|
+
if (v == null || v === "") delete self._props[key]
|
|
76
|
+
else self._props[key] = String(v)
|
|
77
|
+
self._flush()
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
this._props[key] = String(value)
|
|
82
|
+
this._flush()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
VirtualStyleProxy.prototype.removeProperty = function (name) {
|
|
86
|
+
var key = camelize(name)
|
|
87
|
+
delete this._props[key]
|
|
88
|
+
if (Object.prototype.hasOwnProperty.call(this, key)) delete this[key]
|
|
89
|
+
this._flush()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Object.defineProperty(VirtualStyleProxy.prototype, "cssText", {
|
|
93
|
+
get: function () {
|
|
94
|
+
var out = []
|
|
95
|
+
for (var key in this._props) out.push(key + ":" + this._props[key])
|
|
96
|
+
return out.join(";")
|
|
97
|
+
},
|
|
98
|
+
set: function (value) {
|
|
99
|
+
var self = this
|
|
100
|
+
Object.keys(this).forEach(function (key) {
|
|
101
|
+
if (key !== "_wrapper" && key !== "_props") delete self[key]
|
|
102
|
+
})
|
|
103
|
+
this._props = {}
|
|
104
|
+
if (value) {
|
|
105
|
+
// Only used for the shim's first-render `dom.style = ""` clear path
|
|
106
|
+
// in practice — a non-empty string assignment isn't exercised by
|
|
107
|
+
// render.js itself (see CONTRACT.md §f), so no parsing is needed here.
|
|
108
|
+
this._flush()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
function VirtualNodeWrapper(vid, emit, registerStyleProxy) {
|
|
114
|
+
this._vid = vid
|
|
115
|
+
this._emit = emit
|
|
116
|
+
this._style = new VirtualStyleProxy(this, registerStyleProxy)
|
|
117
|
+
this._directProps = {}
|
|
118
|
+
this._listeners = Object.create(null)
|
|
119
|
+
this._isRawText = false
|
|
120
|
+
this._text = null
|
|
121
|
+
this._document = null
|
|
122
|
+
this._tag = null
|
|
123
|
+
this._parent = null
|
|
124
|
+
this._children = []
|
|
125
|
+
this.vnodes = null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Object.defineProperty(VirtualNodeWrapper.prototype, "nodeType", {
|
|
129
|
+
get: function () { return this._isRawText ? 3 : 1 }
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
Object.defineProperty(VirtualNodeWrapper.prototype, "ownerDocument", {
|
|
133
|
+
get: function () { return this._document }
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
Object.defineProperty(VirtualNodeWrapper.prototype, "namespaceURI", {
|
|
137
|
+
get: function () { return undefined }
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
Object.defineProperty(VirtualNodeWrapper.prototype, "parentNode", {
|
|
141
|
+
get: function () { return this._parent }
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
Object.defineProperty(VirtualNodeWrapper.prototype, "firstChild", {
|
|
145
|
+
get: function () { return this._children.length > 0 ? this._children[0] : null }
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
Object.defineProperty(VirtualNodeWrapper.prototype, "nextSibling", {
|
|
149
|
+
get: function () {
|
|
150
|
+
if (this._parent == null) return null
|
|
151
|
+
var siblings = this._parent._children
|
|
152
|
+
var index = siblings.indexOf(this)
|
|
153
|
+
return index >= 0 && index + 1 < siblings.length ? siblings[index + 1] : null
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
Object.defineProperty(VirtualNodeWrapper.prototype, "textContent", {
|
|
158
|
+
get: function () {
|
|
159
|
+
if (this._isRawText) return this._text
|
|
160
|
+
var out = ""
|
|
161
|
+
for (var i = 0; i < this._children.length; i++) out += this._children[i].textContent
|
|
162
|
+
return out
|
|
163
|
+
},
|
|
164
|
+
set: function (value) {
|
|
165
|
+
if (this._isRawText) {
|
|
166
|
+
this.nodeValue = value
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
// Mirrors the real wrapper: only ever called with "" (first-render
|
|
170
|
+
// clear). A genuinely non-empty assignment isn't exercised by
|
|
171
|
+
// render.js (see CONTRACT.md) so it's intentionally not implemented.
|
|
172
|
+
if (this._children.length > 0) {
|
|
173
|
+
var removed = this._children.slice()
|
|
174
|
+
for (var i = 0; i < removed.length; i++) this.removeChild(removed[i])
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
Object.defineProperty(VirtualNodeWrapper.prototype, "nodeValue", {
|
|
180
|
+
get: function () { return this._isRawText ? this._text : null },
|
|
181
|
+
set: function (value) {
|
|
182
|
+
if (this._isRawText) {
|
|
183
|
+
this._text = String(value)
|
|
184
|
+
this._emit({ op: "setText", vid: this._vid, value: this._text })
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
Object.defineProperty(VirtualNodeWrapper.prototype, "innerHTML", {
|
|
190
|
+
get: function () { return this.textContent },
|
|
191
|
+
set: function () {
|
|
192
|
+
throw new Error("m.trust / innerHTML is not supported by the Lynx shim (renderer mode).")
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
Object.defineProperty(VirtualNodeWrapper.prototype, "style", {
|
|
197
|
+
get: function () { return this._style },
|
|
198
|
+
set: function (value) {
|
|
199
|
+
if (value == null) this._style.cssText = ""
|
|
200
|
+
else if (typeof value === "string") this._style.cssText = value
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
DIRECT_PROPS.forEach(function (p) {
|
|
205
|
+
Object.defineProperty(VirtualNodeWrapper.prototype, p, {
|
|
206
|
+
configurable: true,
|
|
207
|
+
enumerable: true,
|
|
208
|
+
get: function () { return this._directProps[p] },
|
|
209
|
+
set: function (v) {
|
|
210
|
+
this._directProps[p] = v
|
|
211
|
+
this._emit({ op: "setProp", vid: this._vid, key: p, value: v })
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
VirtualNodeWrapper.prototype.setAttribute = function (key, value) {
|
|
217
|
+
this._emit({ op: "setAttribute", vid: this._vid, key: key, value: value == null ? null : String(value) })
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
VirtualNodeWrapper.prototype.removeAttribute = function (key) {
|
|
221
|
+
this._emit({ op: "removeAttribute", vid: this._vid, key: key })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
VirtualNodeWrapper.prototype.setAttributeNS = function (ns, key, value) {
|
|
225
|
+
this.setAttribute(key, value)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
VirtualNodeWrapper.prototype.appendChild = function (child) {
|
|
229
|
+
if (child == null) return child
|
|
230
|
+
if (child.nodeType === 11) return child // inert fragment
|
|
231
|
+
if (child._parent != null) {
|
|
232
|
+
var oldSiblings = child._parent._children
|
|
233
|
+
var oldIndex = oldSiblings.indexOf(child)
|
|
234
|
+
if (oldIndex >= 0) oldSiblings.splice(oldIndex, 1)
|
|
235
|
+
}
|
|
236
|
+
this._children.push(child)
|
|
237
|
+
child._parent = this
|
|
238
|
+
this._emit({ op: "appendChild", parentVid: this._vid, childVid: child._vid })
|
|
239
|
+
return child
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
VirtualNodeWrapper.prototype.insertBefore = function (child, ref) {
|
|
243
|
+
if (child == null) return child
|
|
244
|
+
if (child.nodeType === 11) return child
|
|
245
|
+
if (ref == null) return this.appendChild(child)
|
|
246
|
+
if (child._parent != null) {
|
|
247
|
+
var oldSiblings = child._parent._children
|
|
248
|
+
var oldIndex = oldSiblings.indexOf(child)
|
|
249
|
+
if (oldIndex >= 0) oldSiblings.splice(oldIndex, 1)
|
|
250
|
+
}
|
|
251
|
+
var refIndex = this._children.indexOf(ref)
|
|
252
|
+
this._children.splice(refIndex >= 0 ? refIndex : this._children.length, 0, child)
|
|
253
|
+
child._parent = this
|
|
254
|
+
this._emit({ op: "insertBefore", parentVid: this._vid, childVid: child._vid, refVid: ref._vid })
|
|
255
|
+
return child
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
VirtualNodeWrapper.prototype.removeChild = function (child) {
|
|
259
|
+
if (child == null) return child
|
|
260
|
+
if (child.nodeType === 11) return child
|
|
261
|
+
var index = this._children.indexOf(child)
|
|
262
|
+
if (index >= 0) this._children.splice(index, 1)
|
|
263
|
+
child._parent = null
|
|
264
|
+
this._emit({ op: "removeChild", parentVid: this._vid, childVid: child._vid })
|
|
265
|
+
return child
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
VirtualNodeWrapper.prototype.contains = function (other) {
|
|
269
|
+
if (other == null) return false
|
|
270
|
+
var node = other
|
|
271
|
+
while (node != null) {
|
|
272
|
+
if (node === this) return true
|
|
273
|
+
node = node._parent
|
|
274
|
+
}
|
|
275
|
+
return false
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
VirtualNodeWrapper.prototype.focus = function () {}
|
|
279
|
+
|
|
280
|
+
// Events: unlike the real wrapper (which wraps + registers a real PAPI
|
|
281
|
+
// listener per type), this only needs to know WHETHER a type transitions
|
|
282
|
+
// between "has a handler" and "has none" — that's what the main thread
|
|
283
|
+
// needs to know to attach/detach its forwarding listener. The actual
|
|
284
|
+
// listener function/object is resolved by dispatchEvent() at CALL time, not
|
|
285
|
+
// registration time, so Mithril swapping vnode.events' handlers across
|
|
286
|
+
// redraws (a very common pattern) never needs a new op.
|
|
287
|
+
VirtualNodeWrapper.prototype.addEventListener = function (type, listener) {
|
|
288
|
+
var hadListener = this._listeners[type] != null
|
|
289
|
+
this._listeners[type] = listener
|
|
290
|
+
if (!hadListener) this._emit({ op: "addEvent", vid: this._vid, type: type })
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
VirtualNodeWrapper.prototype.removeEventListener = function (type, listener) {
|
|
294
|
+
if (this._listeners[type] !== listener) return
|
|
295
|
+
delete this._listeners[type]
|
|
296
|
+
this._emit({ op: "removeEvent", vid: this._vid, type: type })
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Invoked when renderer/background.js receives a forwarded real-PAPI event.
|
|
300
|
+
VirtualNodeWrapper.prototype.dispatchEvent = function (ev) {
|
|
301
|
+
var listener = this._listeners[ev.type]
|
|
302
|
+
if (listener == null) return
|
|
303
|
+
if (typeof listener === "function") listener.call(ev.currentTarget, ev)
|
|
304
|
+
else if (typeof listener.handleEvent === "function") listener.handleEvent(ev)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function createRawTextNode(vid, emit, registerStyleProxy, value) {
|
|
308
|
+
var wrapper = new VirtualNodeWrapper(vid, emit, registerStyleProxy)
|
|
309
|
+
wrapper._isRawText = true
|
|
310
|
+
wrapper._text = String(value)
|
|
311
|
+
emit({ op: "createText", vid: vid, value: wrapper._text })
|
|
312
|
+
return wrapper
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function createElementWrapper(vid, emit, registerStyleProxy, tag) {
|
|
316
|
+
var wrapper = new VirtualNodeWrapper(vid, emit, registerStyleProxy)
|
|
317
|
+
wrapper._tag = tag
|
|
318
|
+
emit({ op: "createElement", vid: vid, tag: tag })
|
|
319
|
+
return wrapper
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Inert fragment — same "never attached, children go straight to the real
|
|
323
|
+
// parent" contract as the real shim's createFragment().
|
|
324
|
+
function createFragment(document) {
|
|
325
|
+
return {
|
|
326
|
+
nodeType: 11,
|
|
327
|
+
ownerDocument: document,
|
|
328
|
+
appendChild: function () {},
|
|
329
|
+
insertBefore: function () {},
|
|
330
|
+
removeChild: function () {}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// createVirtualDocument(emit, onCreateNode) — emit(op) is called
|
|
335
|
+
// synchronously for every mutation; the caller (renderer/background.js) is
|
|
336
|
+
// responsible for batching and flushing the accumulated ops across the
|
|
337
|
+
// thread boundary. onCreateNode(wrapper) is called for every node created
|
|
338
|
+
// (root included) so the caller can index wrappers by vid. Each call gets
|
|
339
|
+
// its own independent style-proxy registry and vid counter — see
|
|
340
|
+
// VirtualStyleProxy's comment for why this isn't module-level.
|
|
341
|
+
function createVirtualDocument(emit, onCreateNode) {
|
|
342
|
+
var nextVid = 1 // vid 0 is reserved for the page root.
|
|
343
|
+
var notify = onCreateNode || function () {}
|
|
344
|
+
var styleProxies = []
|
|
345
|
+
var registerStyleProxy = function (proxy) { styleProxies.push(proxy) }
|
|
346
|
+
|
|
347
|
+
var document = {
|
|
348
|
+
createElement: function (tag) {
|
|
349
|
+
var wrapper = createElementWrapper(nextVid++, emit, registerStyleProxy, tag)
|
|
350
|
+
wrapper._document = document
|
|
351
|
+
notify(wrapper)
|
|
352
|
+
return wrapper
|
|
353
|
+
},
|
|
354
|
+
createElementNS: function (ns, tag) {
|
|
355
|
+
return document.createElement(tag)
|
|
356
|
+
},
|
|
357
|
+
createTextNode: function (value) {
|
|
358
|
+
var wrapper = createRawTextNode(nextVid++, emit, registerStyleProxy, value)
|
|
359
|
+
wrapper._document = document
|
|
360
|
+
notify(wrapper)
|
|
361
|
+
return wrapper
|
|
362
|
+
},
|
|
363
|
+
createDocumentFragment: function () {
|
|
364
|
+
return createFragment(document)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
Object.defineProperty(document, "activeElement", {
|
|
368
|
+
get: function () { return null }
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
function createRootWrapper() {
|
|
372
|
+
var root = new VirtualNodeWrapper(0, emit, registerStyleProxy)
|
|
373
|
+
root._document = document
|
|
374
|
+
root._tag = "page"
|
|
375
|
+
notify(root)
|
|
376
|
+
return root
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function flushStyleProxies() {
|
|
380
|
+
for (var i = 0; i < styleProxies.length; i++) {
|
|
381
|
+
try { styleProxies[i]._flush() } catch (e) { /* ignore */ }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { document: document, createRootWrapper: createRootWrapper, flushStyleProxies: flushStyleProxies }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export { createVirtualDocument, VirtualNodeWrapper, VirtualStyleProxy }
|