@symbiote-native/svelte 3.0.4 → 3.0.5
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/build/dom-shim/animation.d.ts +10 -0
- package/build/dom-shim/animation.js +80 -0
- package/build/dom-shim/animations-gate.d.ts +2 -0
- package/build/dom-shim/animations-gate.js +10 -0
- package/build/dom-shim/computed-style.d.ts +3 -0
- package/build/dom-shim/computed-style.js +34 -0
- package/build/dom-shim/document.d.ts +2 -1
- package/build/dom-shim/document.js +8 -11
- package/build/dom-shim/element.d.ts +15 -1
- package/build/dom-shim/element.js +45 -1
- package/build/dom-shim/patch-globals.js +34 -0
- package/package.json +7 -7
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ShimElement } from './element';
|
|
2
|
+
export type IKeyframe = Readonly<Record<string, string>>;
|
|
3
|
+
export type IShimAnimation = {
|
|
4
|
+
onfinish: (() => void) | null;
|
|
5
|
+
effect: unknown;
|
|
6
|
+
readonly playState: 'idle' | 'running' | 'finished';
|
|
7
|
+
readonly currentTime: number;
|
|
8
|
+
cancel(): void;
|
|
9
|
+
};
|
|
10
|
+
export declare function animateShimElement(element: ShimElement, keyframes: readonly IKeyframe[], duration: number): IShimAnimation;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// `Element.animate()`, the subset `svelte/transition`/`svelte/animate` call: Svelte's own tick
|
|
2
|
+
// loop already bakes each frame's `css(t, u)` into the keyframe array, so this only replays them
|
|
3
|
+
// over real time (`.currentTime`/`.playState`/`.onfinish`/`.effect`/`.cancel()` contract).
|
|
4
|
+
import { isSymbioteNode, requestCommitFor, routeProp, } from '@symbiote-native/engine';
|
|
5
|
+
import { areShimAnimationsEnabled } from './animations-gate.js';
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null;
|
|
8
|
+
}
|
|
9
|
+
// `opacity` parses to a number, matching a real CSSStyleDeclaration read back; every other key
|
|
10
|
+
// (transform, filter, ...) passes through as the authored string.
|
|
11
|
+
function applyKeyframe(node, style, frame) {
|
|
12
|
+
const next = isRecord(style) ? { ...style } : {};
|
|
13
|
+
for (const [key, value] of Object.entries(frame)) {
|
|
14
|
+
next[key] = key === 'opacity' ? Number(value) : value;
|
|
15
|
+
}
|
|
16
|
+
routeProp(node, 'style', next);
|
|
17
|
+
// Our own rAF loop, not a framework render - nothing else publishes this write per frame.
|
|
18
|
+
requestCommitFor(node);
|
|
19
|
+
}
|
|
20
|
+
class ShimAnimation {
|
|
21
|
+
node;
|
|
22
|
+
currentStyle;
|
|
23
|
+
keyframes;
|
|
24
|
+
duration;
|
|
25
|
+
onfinish = null;
|
|
26
|
+
effect = null;
|
|
27
|
+
playState = 'idle';
|
|
28
|
+
startTime = undefined;
|
|
29
|
+
rafId = undefined;
|
|
30
|
+
cancelled = false;
|
|
31
|
+
constructor(node, currentStyle, keyframes, duration) {
|
|
32
|
+
this.node = node;
|
|
33
|
+
this.currentStyle = currentStyle;
|
|
34
|
+
this.keyframes = keyframes;
|
|
35
|
+
this.duration = duration;
|
|
36
|
+
if (node === undefined || keyframes.length === 0 || duration <= 0) {
|
|
37
|
+
this.playState = 'finished';
|
|
38
|
+
// Guarded like `tick`: a same-tick `cancel()` (Svelte replacing this animation before the
|
|
39
|
+
// microtask runs) must not fire a handler for a transition that never got to play.
|
|
40
|
+
void Promise.resolve().then(() => {
|
|
41
|
+
if (!this.cancelled)
|
|
42
|
+
this.onfinish?.();
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.playState = 'running';
|
|
47
|
+
this.rafId = requestAnimationFrame(this.tick);
|
|
48
|
+
}
|
|
49
|
+
get currentTime() {
|
|
50
|
+
return this.startTime === undefined ? 0 : Date.now() - this.startTime;
|
|
51
|
+
}
|
|
52
|
+
tick = (now) => {
|
|
53
|
+
if (this.cancelled || this.node === undefined)
|
|
54
|
+
return;
|
|
55
|
+
this.startTime ??= now;
|
|
56
|
+
const progress = Math.min((now - this.startTime) / this.duration, 1);
|
|
57
|
+
const index = Math.round(progress * (this.keyframes.length - 1));
|
|
58
|
+
const frame = this.keyframes[index];
|
|
59
|
+
if (frame !== undefined)
|
|
60
|
+
applyKeyframe(this.node, this.currentStyle, frame);
|
|
61
|
+
if (progress >= 1) {
|
|
62
|
+
this.playState = 'finished';
|
|
63
|
+
this.onfinish?.();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.rafId = requestAnimationFrame(this.tick);
|
|
67
|
+
};
|
|
68
|
+
cancel() {
|
|
69
|
+
this.cancelled = true;
|
|
70
|
+
this.playState = 'idle';
|
|
71
|
+
if (this.rafId !== undefined)
|
|
72
|
+
cancelAnimationFrame(this.rafId);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// A node not yet live plays no animation - the same shape Svelte's own dummy zero-duration
|
|
76
|
+
// animation already takes, so a caller sees a normal, already-finished Animation.
|
|
77
|
+
export function animateShimElement(element, keyframes, duration) {
|
|
78
|
+
const node = element.engineNode;
|
|
79
|
+
return new ShimAnimation(areShimAnimationsEnabled() && isSymbioteNode(node) ? node : undefined, element.p.style, keyframes, duration);
|
|
80
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// TODO: включить, когда API стабилизируется: `animate:flip` работает, shuffle-реордер нет
|
|
2
|
+
// Экран `TransitionAnimateDemo` снят с Canary, вернуть вместе с включением
|
|
3
|
+
// Пока выключено для потребителя, тесты включают через `setShimAnimationsEnabled`
|
|
4
|
+
let enabled = false;
|
|
5
|
+
export function setShimAnimationsEnabled(value) {
|
|
6
|
+
enabled = value;
|
|
7
|
+
}
|
|
8
|
+
export function areShimAnimationsEnabled() {
|
|
9
|
+
return enabled;
|
|
10
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The bare global `getComputedStyle`, which svelte/transition's fade/fly/scale/slide/blur call
|
|
2
|
+
// directly, not `document.getComputedStyle`. A Proxy, since a caller reads an arbitrary key built
|
|
3
|
+
// at runtime (slide derives `paddingTop`/`marginBottom` from its `axis` option).
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null;
|
|
6
|
+
}
|
|
7
|
+
const DEFAULTS = {
|
|
8
|
+
opacity: '1',
|
|
9
|
+
transform: 'none',
|
|
10
|
+
filter: 'none',
|
|
11
|
+
display: 'flex',
|
|
12
|
+
strokeLinecap: 'butt',
|
|
13
|
+
strokeWidth: '0',
|
|
14
|
+
};
|
|
15
|
+
function styleValueOf(element, key) {
|
|
16
|
+
const style = element.p.style;
|
|
17
|
+
const raw = isRecord(style) ? style[key] : undefined;
|
|
18
|
+
if (raw !== undefined)
|
|
19
|
+
return typeof raw === 'number' ? `${raw}px` : String(raw);
|
|
20
|
+
// Two tokens, not `DEFAULTS`' single `'0px'` - `flip()` destructures this into [ox, oy] and
|
|
21
|
+
// divides by clientWidth/Height, so a lone token leaves oy as NaN (real default is `50% 50%`).
|
|
22
|
+
if (key === 'transformOrigin') {
|
|
23
|
+
const { width, height } = element.getBoundingClientRect();
|
|
24
|
+
return `${width / 2}px ${height / 2}px`;
|
|
25
|
+
}
|
|
26
|
+
return DEFAULTS[key] ?? '0px';
|
|
27
|
+
}
|
|
28
|
+
export function computedStyleOf(element) {
|
|
29
|
+
return new Proxy({}, {
|
|
30
|
+
get(_target, key) {
|
|
31
|
+
return typeof key === 'string' ? styleValueOf(element, key) : undefined;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -2,6 +2,7 @@ import { ShimElement } from './element';
|
|
|
2
2
|
import { ShimText } from './text';
|
|
3
3
|
import { ShimComment } from './comment';
|
|
4
4
|
import { ShimDocumentFragment } from './document-fragment';
|
|
5
|
+
import { type IComputedStyle } from './computed-style';
|
|
5
6
|
export declare class ShimDocument {
|
|
6
7
|
readonly body: ShimElement;
|
|
7
8
|
createElement(tag: string, options?: {
|
|
@@ -17,7 +18,7 @@ export declare class ShimDocument {
|
|
|
17
18
|
importNode(node: ShimText, deep: boolean): ShimText;
|
|
18
19
|
importNode(node: ShimComment, deep: boolean): ShimComment;
|
|
19
20
|
importNode(node: ShimDocumentFragment, deep: boolean): ShimDocumentFragment;
|
|
20
|
-
getComputedStyle():
|
|
21
|
+
getComputedStyle(element: ShimElement): IComputedStyle;
|
|
21
22
|
}
|
|
22
23
|
export declare function getShimDocument(): ShimDocument;
|
|
23
24
|
export declare function resetShimDocumentForTests(): void;
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
// The shim's `document` singleton
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// harmless no-op instead of a hard crash. No `<svelte:head|window|body|document>` support
|
|
5
|
-
// beyond that stub — those tags are rejected at build time by the preprocessor (§7), not
|
|
6
|
-
// handled here.
|
|
1
|
+
// The shim's `document` singleton - the six factories `init_operations()`/`from_tree` require,
|
|
2
|
+
// plus `document.body` (a minimal unattached node) and `getComputedStyle` (computed-style.ts).
|
|
3
|
+
// No `<svelte:head|window|body|document>` - rejected at build time by the preprocessor.
|
|
7
4
|
import { ShimElement } from './element.js';
|
|
8
5
|
import { ShimText } from './text.js';
|
|
9
6
|
import { ShimComment } from './comment.js';
|
|
10
7
|
import { ShimDocumentFragment } from './document-fragment.js';
|
|
11
8
|
import { registerShimDocumentFactory } from './shim-node.js';
|
|
9
|
+
import { computedStyleOf } from './computed-style.js';
|
|
12
10
|
export class ShimDocument {
|
|
13
11
|
// The delegation root real Svelte events bubble toward (`dom/elements/events.js:122`).
|
|
14
12
|
// Our own event names never delegate (§5c: no camelCase name matches Svelte's 23-name
|
|
@@ -39,11 +37,10 @@ export class ShimDocument {
|
|
|
39
37
|
importNode(node, deep) {
|
|
40
38
|
return node.cloneNode(deep);
|
|
41
39
|
}
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return {};
|
|
40
|
+
// Kept in step with the bare global patch-globals.ts installs (computed-style.ts is the one
|
|
41
|
+
// implementation both call).
|
|
42
|
+
getComputedStyle(element) {
|
|
43
|
+
return computedStyleOf(element);
|
|
47
44
|
}
|
|
48
45
|
}
|
|
49
46
|
let singleton;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
1
|
+
import { type IDomRect, type ISymbioteNode } from '@symbiote-native/engine';
|
|
2
|
+
import { type IKeyframe, type IShimAnimation } from './animation';
|
|
2
3
|
import { ShimNode } from './shim-node';
|
|
3
4
|
export type IShimPropBag = Record<string, unknown>;
|
|
4
5
|
export declare abstract class ShimElementBase extends ShimNode {
|
|
@@ -30,6 +31,19 @@ export declare class ShimElement extends ShimElementBase {
|
|
|
30
31
|
addEventListener(name: string, handler: (event: unknown) => void): void;
|
|
31
32
|
removeEventListener(name: string): void;
|
|
32
33
|
cloneNode(deep?: boolean): ShimElement;
|
|
34
|
+
get parentElement(): ShimElement | null;
|
|
35
|
+
get clientWidth(): number;
|
|
36
|
+
get clientHeight(): number;
|
|
37
|
+
getBoundingClientRect(): IDomRect & {
|
|
38
|
+
top: number;
|
|
39
|
+
left: number;
|
|
40
|
+
right: number;
|
|
41
|
+
bottom: number;
|
|
42
|
+
};
|
|
43
|
+
dispatchEvent(): boolean;
|
|
44
|
+
animate(keyframes: readonly IKeyframe[], options: {
|
|
45
|
+
duration: number;
|
|
46
|
+
}): IShimAnimation;
|
|
33
47
|
createEngineNode(): ISymbioteNode;
|
|
34
48
|
onMadeLive(): void;
|
|
35
49
|
}
|
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
// A per-key diff in the setter is MANDATORY (§3g(c)), not an optimization: Svelte's
|
|
23
23
|
// `set_custom_element_data` has no early-out guard and runs on every effect re-fire, so
|
|
24
24
|
// without the diff every prop gets rewritten whenever any one of them changes.
|
|
25
|
-
import { createElement, dlog, routeProp, toPublicInstance, } from '@symbiote-native/engine';
|
|
25
|
+
import { createElement, dlog, getBoundingClientRect, isSymbioteNode, routeProp, toPublicInstance, } from '@symbiote-native/engine';
|
|
26
26
|
import { descriptorFor } from '@symbiote-native/components';
|
|
27
27
|
import { normalizeSvelteClass } from '../class-value.js';
|
|
28
|
+
import { animateShimElement, } from './animation.js';
|
|
29
|
+
import { areShimAnimationsEnabled } from './animations-gate.js';
|
|
28
30
|
import { BOOLEAN_PROP_NAMES, CANONICAL_BY_LOWER, CANONICAL_PROP_NAMES, } from './canonical-prop-names.js';
|
|
29
31
|
import { ShimNode } from './shim-node.js';
|
|
30
32
|
import { discoverStyleCacheKey } from './style-cache.js';
|
|
@@ -251,6 +253,48 @@ export class ShimElement extends ShimElementBase {
|
|
|
251
253
|
}
|
|
252
254
|
return clone;
|
|
253
255
|
}
|
|
256
|
+
// `flip`'s `get_zoom()` walks this until `null`; without it the walk hit `undefined` and threw
|
|
257
|
+
// `Cannot read properties of undefined ('parentElement')`, aborting the whole reorder.
|
|
258
|
+
get parentElement() {
|
|
259
|
+
const result = this.parent instanceof ShimElement ? this.parent : null;
|
|
260
|
+
dlog(`parentElement tag=${this.tagName} -> ${result === null ? 'null' : result.tagName}`);
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
// `flip` divides by these for its scale factor; unset they read as `NaN`, not zero.
|
|
264
|
+
get clientWidth() {
|
|
265
|
+
return this.getBoundingClientRect().width;
|
|
266
|
+
}
|
|
267
|
+
get clientHeight() {
|
|
268
|
+
return this.getBoundingClientRect().height;
|
|
269
|
+
}
|
|
270
|
+
// Real DOM shape (top/left/right/bottom derived), for svelte/animate's `flip` and
|
|
271
|
+
// svelte/transition's `crossfade`, both of which measure before and after a reorder.
|
|
272
|
+
getBoundingClientRect() {
|
|
273
|
+
const node = this.engineNode;
|
|
274
|
+
const rect = areShimAnimationsEnabled() && isSymbioteNode(node)
|
|
275
|
+
? getBoundingClientRect(node, false)
|
|
276
|
+
: undefined;
|
|
277
|
+
dlog(`getBoundingClientRect tag=${this.tagName} hasNode=${isSymbioteNode(node)} rect=${JSON.stringify(rect)}`);
|
|
278
|
+
const { x, y, width, height } = rect ?? { x: 0, y: 0, width: 0, height: 0 };
|
|
279
|
+
return {
|
|
280
|
+
x,
|
|
281
|
+
y,
|
|
282
|
+
width,
|
|
283
|
+
height,
|
|
284
|
+
top: y,
|
|
285
|
+
left: x,
|
|
286
|
+
right: x + width,
|
|
287
|
+
bottom: y + height,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// `transitions.js`'s `dispatch_event` fires introstart/introend/outrostart/outroend at this;
|
|
291
|
+
// nothing in this adapter listens, so a no-op is the correct answer, not a missing method.
|
|
292
|
+
dispatchEvent() {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
animate(keyframes, options) {
|
|
296
|
+
return animateShimElement(this, keyframes, options.duration);
|
|
297
|
+
}
|
|
254
298
|
// measure/measureInWindow/measureLayout/setNativeProps/focus/blur — the imperative API a
|
|
255
299
|
// `bind:this` host ref hands back — ride on the engine node's prototype, so toPublicInstance is
|
|
256
300
|
// the identity and this reads the same as Vue's renderer (renderer/index.ts) and React's
|
|
@@ -29,6 +29,7 @@ import { ShimComment } from './comment.js';
|
|
|
29
29
|
import { ShimDocumentFragment } from './document-fragment.js';
|
|
30
30
|
import { ShimNode } from './shim-node.js';
|
|
31
31
|
import { getShimDocument } from './document.js';
|
|
32
|
+
import { computedStyleOf } from './computed-style.js';
|
|
32
33
|
const PATCHED_KEYS = [
|
|
33
34
|
'Node',
|
|
34
35
|
'Element',
|
|
@@ -40,7 +41,36 @@ const PATCHED_KEYS = [
|
|
|
40
41
|
'document',
|
|
41
42
|
'customElements',
|
|
42
43
|
'HTMLMediaElement',
|
|
44
|
+
'getComputedStyle',
|
|
45
|
+
'CustomEvent',
|
|
43
46
|
];
|
|
47
|
+
// `svelte/transition`'s fade/fly/scale/slide/blur call this BARE global, not
|
|
48
|
+
// `document.getComputedStyle`, so it must be a real assigned property too.
|
|
49
|
+
function getComputedStyleGlobal(element) {
|
|
50
|
+
return element instanceof ShimElement
|
|
51
|
+
? computedStyleOf(element)
|
|
52
|
+
: computedStyleOf(new ShimElement('view'));
|
|
53
|
+
}
|
|
54
|
+
// `createEventDispatcher`'s own event object; `svelte/events`'s `on()` also constructs one.
|
|
55
|
+
// RN installs no CustomEvent of its own, so this must be a real assigned property, matching the
|
|
56
|
+
// same class of trap as `customElements`/`HTMLMediaElement` below.
|
|
57
|
+
class FakeCustomEvent {
|
|
58
|
+
type;
|
|
59
|
+
detail;
|
|
60
|
+
bubbles;
|
|
61
|
+
cancelable;
|
|
62
|
+
defaultPrevented = false;
|
|
63
|
+
constructor(type, options) {
|
|
64
|
+
this.type = type;
|
|
65
|
+
this.detail = options?.detail;
|
|
66
|
+
this.bubbles = options?.bubbles ?? false;
|
|
67
|
+
this.cancelable = options?.cancelable ?? false;
|
|
68
|
+
}
|
|
69
|
+
preventDefault() {
|
|
70
|
+
if (this.cancelable)
|
|
71
|
+
this.defaultPrevented = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
44
74
|
// Real DOM's `CustomElementRegistry`. `set_custom_element_data` (svelte's
|
|
45
75
|
// dom/elements/attributes.js) reads the BARE global `customElements` unconditionally, with no
|
|
46
76
|
// `typeof` guard — an undeclared global throws ReferenceError, not just `undefined`, so this
|
|
@@ -81,6 +111,8 @@ export function patchGlobals() {
|
|
|
81
111
|
document: g.document,
|
|
82
112
|
customElements: g.customElements,
|
|
83
113
|
HTMLMediaElement: g.HTMLMediaElement,
|
|
114
|
+
getComputedStyle: g.getComputedStyle,
|
|
115
|
+
CustomEvent: g.CustomEvent,
|
|
84
116
|
};
|
|
85
117
|
g.Node = ShimNode;
|
|
86
118
|
// ShimElementBase, NOT ShimElement: `get_setters` stops AT `Element.prototype`, so pointing this
|
|
@@ -97,6 +129,8 @@ export function patchGlobals() {
|
|
|
97
129
|
g.document = getShimDocument();
|
|
98
130
|
g.customElements = FAKE_CUSTOM_ELEMENT_REGISTRY;
|
|
99
131
|
g.HTMLMediaElement = FakeHTMLMediaElement;
|
|
132
|
+
g.getComputedStyle = getComputedStyleGlobal;
|
|
133
|
+
g.CustomEvent = FakeCustomEvent;
|
|
100
134
|
dlog('svelte dom-shim: patchGlobals installed');
|
|
101
135
|
}
|
|
102
136
|
export function restoreGlobals() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/svelte",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
4
|
"description": "SymbioteNative's Svelte adapter — a DOM shim over stock compiled Svelte output driving real native iOS/Android views through the same engine as every other adapter.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -73,22 +73,22 @@
|
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"typescript": "~6.0.0",
|
|
76
|
-
"@symbiote-native/components": "3.1.
|
|
76
|
+
"@symbiote-native/components": "3.1.3",
|
|
77
77
|
"@symbiote-native/css-parser": "0.5.2"
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
80
|
"svelte": ">=5.56.0",
|
|
81
81
|
"react-native": ">=0.86",
|
|
82
|
-
"@symbiote-native/engine": "^1.
|
|
82
|
+
"@symbiote-native/engine": "^1.4.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@vue/runtime-core": "^3.5.13",
|
|
86
86
|
"react": "19.2.3",
|
|
87
87
|
"svelte": "^5.56.0",
|
|
88
|
-
"@symbiote-native/engine": "1.
|
|
89
|
-
"@symbiote-native/
|
|
90
|
-
"@symbiote-native/
|
|
91
|
-
"@symbiote-native/vue": "3.0
|
|
88
|
+
"@symbiote-native/engine": "1.4.0",
|
|
89
|
+
"@symbiote-native/test-utils": "0.4.5",
|
|
90
|
+
"@symbiote-native/react": "3.1.0",
|
|
91
|
+
"@symbiote-native/vue": "3.1.0"
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
|
94
94
|
"typecheck": "tsc --build",
|