@humanspeak/svelte-motion 0.5.2 → 0.5.4
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/dist/components/LazyMotion.svelte +70 -0
- package/dist/components/LazyMotion.svelte.d.ts +16 -0
- package/dist/components/lazyMotion.context.d.ts +25 -0
- package/dist/components/lazyMotion.context.js +19 -0
- package/dist/components/projection.context.d.ts +22 -0
- package/dist/components/projection.context.js +56 -0
- package/dist/features/domAnimation.d.ts +6 -0
- package/dist/features/domAnimation.js +8 -0
- package/dist/features/domMax.d.ts +5 -0
- package/dist/features/domMax.js +9 -0
- package/dist/features/domMin.d.ts +5 -0
- package/dist/features/domMin.js +6 -0
- package/dist/features/index.d.ts +39 -0
- package/dist/features/index.js +18 -0
- package/dist/html/_MotionContainer.svelte +202 -31
- package/dist/index.d.ts +8 -2
- package/dist/index.js +6 -1
- package/dist/m.d.ts +9 -0
- package/dist/m.js +9 -0
- package/dist/types.d.ts +59 -0
- package/dist/utils/drag.d.ts +42 -11
- package/dist/utils/drag.js +103 -12
- package/dist/utils/layout.d.ts +26 -2
- package/dist/utils/layout.js +13 -2
- package/dist/utils/projection.d.ts +287 -0
- package/dist/utils/projection.js +392 -0
- package/dist/utils/style.d.ts +23 -0
- package/dist/utils/style.js +27 -0
- package/dist/utils/variants.d.ts +26 -0
- package/dist/utils/variants.js +42 -0
- package/package.json +2 -2
package/dist/utils/drag.js
CHANGED
|
@@ -137,23 +137,36 @@ const computeReleaseVelocity = (history, nowMs) => {
|
|
|
137
137
|
/**
|
|
138
138
|
* Attach a drag gesture to an element.
|
|
139
139
|
*
|
|
140
|
-
* Captures the pointer
|
|
141
|
-
* applies elastic overflow against constraints, emits
|
|
142
|
-
* and runs a momentum animation on
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Attach a drag gesture to an HTMLElement.
|
|
140
|
+
* Captures the pointer and updates x/y transforms with axis and optional
|
|
141
|
+
* direction lock, applies elastic overflow against constraints, emits
|
|
142
|
+
* lifecycle callbacks with `DragInfo`, and runs a momentum animation on
|
|
143
|
+
* release when enabled.
|
|
146
144
|
*
|
|
147
145
|
* Lifecycle:
|
|
148
146
|
* - pointerdown → capture pointer, snapshot origin, start velocity history, enter whileDrag
|
|
149
147
|
* - pointermove → compute deltas, direction lock, apply constraints + elastic, write x/y
|
|
150
148
|
* - pointerup/cancel → either run momentum decay to a target or settle/clamp instantly
|
|
151
149
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
150
|
+
* Invariant: `applied` tracks the currently applied x/y transform — it
|
|
151
|
+
* must stay in sync when writing transforms or finishing animations, or a
|
|
152
|
+
* second drag "jumps" from a stale origin (commonly a missed update after
|
|
153
|
+
* a non-zero-duration settle animation).
|
|
154
|
+
*
|
|
155
|
+
* @param el The element to make draggable.
|
|
156
|
+
* @param opts Drag options — `axis`, `constraints`, `elastic`,
|
|
157
|
+
* `momentum`, `whileDrag`, and the `onDrag*` lifecycle callbacks.
|
|
158
|
+
* @returns A callable cleanup handle ({@link AttachDragCleanup}): call it
|
|
159
|
+
* to detach the gesture's listeners (in-flight momentum is not
|
|
160
|
+
* cancelled — see the type docs), or call its `adjustOrigin(dx, dy)` to
|
|
161
|
+
* reposition the live gesture mid-drag.
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* const cleanup = attachDrag(el, { axis: 'x', momentum: true })
|
|
165
|
+
* // …when a layout swap shifts the slot under the cursor mid-drag:
|
|
166
|
+
* cleanup.adjustOrigin(10, -5)
|
|
167
|
+
* // on teardown:
|
|
168
|
+
* cleanup()
|
|
169
|
+
* ```
|
|
157
170
|
*/
|
|
158
171
|
export const attachDrag = (el, opts) => {
|
|
159
172
|
const EL_ID = el.getAttribute('data-testid') || el.id || el.tagName;
|
|
@@ -249,6 +262,83 @@ export const attachDrag = (el, opts) => {
|
|
|
249
262
|
}
|
|
250
263
|
}
|
|
251
264
|
};
|
|
265
|
+
/**
|
|
266
|
+
* Write absolute element-space translation by mutating
|
|
267
|
+
* `el.style.transform` DIRECTLY — no `animate()`, no epsilon skip,
|
|
268
|
+
* no Playwright retry. The translate channel is rewritten while any
|
|
269
|
+
* non-translate transform the element already carries (e.g. a
|
|
270
|
+
* `whileDrag` scale) is preserved as a suffix.
|
|
271
|
+
*
|
|
272
|
+
* Why this exists separately from `setXY`: routing through
|
|
273
|
+
* `animate(el, _, { duration: 0 })` defers the write to Motion's
|
|
274
|
+
* scheduler, costing ~1 frame before the new position paints. For
|
|
275
|
+
* the projection-driven origin compensation (where a layout swap
|
|
276
|
+
* must keep the dragged element under the cursor in the SAME frame
|
|
277
|
+
* the swap commits), that frame of lag manifests as a visible
|
|
278
|
+
* wobble. This path lands synchronously. See #379 / the
|
|
279
|
+
* `adjustOrigin` hook below.
|
|
280
|
+
*/
|
|
281
|
+
const setXYImmediate = (x, y) => {
|
|
282
|
+
const parts = [];
|
|
283
|
+
if (axis === true || axis === 'x')
|
|
284
|
+
parts.push(`translateX(${x}px)`);
|
|
285
|
+
if (axis === true || axis === 'y')
|
|
286
|
+
parts.push(`translateY(${y}px)`);
|
|
287
|
+
// Strip existing translate channels, keep the rest (scale/rotate/etc.).
|
|
288
|
+
const nonTranslate = el.style.transform.replace(/translate[XYZ3d]*\([^)]*\)/g, '').trim();
|
|
289
|
+
el.style.transform = [...parts, nonTranslate].filter(Boolean).join(' ');
|
|
290
|
+
if (axis === true || axis === 'x')
|
|
291
|
+
applied.x = x;
|
|
292
|
+
if (axis === true || axis === 'y')
|
|
293
|
+
applied.y = y;
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* Adjust the drag origin + visual offset by a layout-shift delta,
|
|
297
|
+
* mid-gesture, keeping the dragged element pinned under the cursor
|
|
298
|
+
* while its underlying layout slot moves.
|
|
299
|
+
*
|
|
300
|
+
* Direct port of framer-motion's projection `didUpdate` handler in
|
|
301
|
+
* `VisualElementDragControls.ts:742-758`:
|
|
302
|
+
*
|
|
303
|
+
* ```ts
|
|
304
|
+
* this.originPoint[axis] += delta[axis].translate
|
|
305
|
+
* motionValue.set(motionValue.get() + delta[axis].translate)
|
|
306
|
+
* ```
|
|
307
|
+
*
|
|
308
|
+
* We do the same two-write dance: shift `origin` (the gesture's
|
|
309
|
+
* reference zero) AND the applied visual transform by the same
|
|
310
|
+
* delta, so `lastPoint - startPoint + origin` continues to resolve
|
|
311
|
+
* to the correct on-screen position after the layout slot moved.
|
|
312
|
+
* Uses `setXYImmediate` so the compensation is visible the same
|
|
313
|
+
* frame as the layout change.
|
|
314
|
+
*
|
|
315
|
+
* Not wired to any projection node in this PR — exposed on the
|
|
316
|
+
* `attachDrag` return handle for the Reorder PR (#310) to call from
|
|
317
|
+
* its `ProjectionNode.didUpdate` listener.
|
|
318
|
+
*
|
|
319
|
+
* @param dx Layout delta on the x axis (px).
|
|
320
|
+
* @param dy Layout delta on the y axis (px).
|
|
321
|
+
*/
|
|
322
|
+
const adjustOrigin = (dx, dy) => {
|
|
323
|
+
if (!dragging)
|
|
324
|
+
return;
|
|
325
|
+
// Compensate the origin on BOTH axes unconditionally — upstream's
|
|
326
|
+
// didUpdate handler applies the delta per-axis via `eachAxis`
|
|
327
|
+
// regardless of the drag axis or direction lock, because a layout
|
|
328
|
+
// slot can shift on either axis.
|
|
329
|
+
origin.x += dx;
|
|
330
|
+
origin.y += dy;
|
|
331
|
+
// The VISUAL write is `setXYImmediate`, which only writes the axis
|
|
332
|
+
// this drag manages (`opts.axis`). For the dragged axis that pins
|
|
333
|
+
// the element same-frame; the cross-axis case (e.g. drag="x" + a
|
|
334
|
+
// y-shift) only updates `origin`, not the transform. Fully
|
|
335
|
+
// rendering cross-axis compensation needs to route through the
|
|
336
|
+
// Motion value the move path uses (a direct write here would be
|
|
337
|
+
// wiped by the next `setXY`), so it's finalized when this hook is
|
|
338
|
+
// wired in #310. The common Reorder case (drag axis === the shift
|
|
339
|
+
// axis) is fully compensated today.
|
|
340
|
+
setXYImmediate(applied.x + dx, applied.y + dy);
|
|
341
|
+
};
|
|
252
342
|
const startWhileDrag = () => {
|
|
253
343
|
if (!opts.whileDrag)
|
|
254
344
|
return;
|
|
@@ -758,7 +848,7 @@ export const attachDrag = (el, opts) => {
|
|
|
758
848
|
}
|
|
759
849
|
el.addEventListener('pointerdown', onPointerDown);
|
|
760
850
|
pwLog('[drag] pointerdown listener attached', { el: EL_ID });
|
|
761
|
-
|
|
851
|
+
const teardown = () => {
|
|
762
852
|
pwLog('[drag] detach', { el: EL_ID });
|
|
763
853
|
el.removeEventListener('pointerdown', onPointerDown);
|
|
764
854
|
el.removeEventListener('pointermove', onPointerMove);
|
|
@@ -768,4 +858,5 @@ export const attachDrag = (el, opts) => {
|
|
|
768
858
|
window.removeEventListener('pointerup', onPointerUp);
|
|
769
859
|
window.removeEventListener('pointercancel', onPointerCancel);
|
|
770
860
|
};
|
|
861
|
+
return Object.assign(teardown, { adjustOrigin });
|
|
771
862
|
};
|
package/dist/utils/layout.d.ts
CHANGED
|
@@ -14,8 +14,19 @@ import { type AnimationOptions } from 'motion';
|
|
|
14
14
|
*
|
|
15
15
|
* Pass an empty array (or omit) for viewport-relative behaviour.
|
|
16
16
|
*
|
|
17
|
+
* `baseTransform` is the value the element's `transform` is set to while
|
|
18
|
+
* measuring (default `'none'`, i.e. all transforms removed). The
|
|
19
|
+
* projection system passes the element's mount-time transform here so
|
|
20
|
+
* that a user-authored static `transform` is preserved in the
|
|
21
|
+
* measurement while only the motion-applied portion (written after
|
|
22
|
+
* mount) is removed — mirroring framer-motion's `removeBoxTransforms`,
|
|
23
|
+
* which only subtracts motion-tracked `latestValues` and leaves
|
|
24
|
+
* user-authored transforms intact. Existing FLIP callers omit it and
|
|
25
|
+
* get the original strip-everything behaviour.
|
|
26
|
+
*
|
|
17
27
|
* @param el Element to measure.
|
|
18
28
|
* @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
|
|
29
|
+
* @param baseTransform Transform string applied during measurement. Defaults to `'none'`.
|
|
19
30
|
* @returns DOMRect snapshot of the element.
|
|
20
31
|
*
|
|
21
32
|
* @example
|
|
@@ -30,7 +41,20 @@ import { type AnimationOptions } from 'motion';
|
|
|
30
41
|
* const rect = measureRect(node, [innerScroll, outerScroll])
|
|
31
42
|
* ```
|
|
32
43
|
*/
|
|
33
|
-
export declare const measureRect: (el: HTMLElement, scrollContainers?: HTMLElement[]) => DOMRect;
|
|
44
|
+
export declare const measureRect: (el: HTMLElement, scrollContainers?: HTMLElement[], baseTransform?: string) => DOMRect;
|
|
45
|
+
/**
|
|
46
|
+
* Minimal rectangle shape `computeFlipTransforms` reads. A `DOMRect`
|
|
47
|
+
* satisfies it structurally, and so does a projection `Box` converted to
|
|
48
|
+
* `{ left, top, width, height }`. Declared here (rather than importing
|
|
49
|
+
* the projection `Box`) so `layout.ts` stays free of a circular
|
|
50
|
+
* dependency on `projection.ts`, which imports `measureRect` from here.
|
|
51
|
+
*/
|
|
52
|
+
export interface RectLike {
|
|
53
|
+
left: number;
|
|
54
|
+
top: number;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
}
|
|
34
58
|
/**
|
|
35
59
|
* Compute FLIP transform deltas between two rects.
|
|
36
60
|
*
|
|
@@ -39,7 +63,7 @@ export declare const measureRect: (el: HTMLElement, scrollContainers?: HTMLEleme
|
|
|
39
63
|
* @param mode `true` for translate+scale, `'position'` for translate only.
|
|
40
64
|
* @return Deltas and flags indicating which transforms to apply.
|
|
41
65
|
*/
|
|
42
|
-
export declare const computeFlipTransforms: (prev:
|
|
66
|
+
export declare const computeFlipTransforms: (prev: RectLike, next: RectLike, mode: boolean | "position") => {
|
|
43
67
|
dx: number;
|
|
44
68
|
dy: number;
|
|
45
69
|
sx: number;
|
package/dist/utils/layout.js
CHANGED
|
@@ -14,8 +14,19 @@ import { animate } from 'motion';
|
|
|
14
14
|
*
|
|
15
15
|
* Pass an empty array (or omit) for viewport-relative behaviour.
|
|
16
16
|
*
|
|
17
|
+
* `baseTransform` is the value the element's `transform` is set to while
|
|
18
|
+
* measuring (default `'none'`, i.e. all transforms removed). The
|
|
19
|
+
* projection system passes the element's mount-time transform here so
|
|
20
|
+
* that a user-authored static `transform` is preserved in the
|
|
21
|
+
* measurement while only the motion-applied portion (written after
|
|
22
|
+
* mount) is removed — mirroring framer-motion's `removeBoxTransforms`,
|
|
23
|
+
* which only subtracts motion-tracked `latestValues` and leaves
|
|
24
|
+
* user-authored transforms intact. Existing FLIP callers omit it and
|
|
25
|
+
* get the original strip-everything behaviour.
|
|
26
|
+
*
|
|
17
27
|
* @param el Element to measure.
|
|
18
28
|
* @param scrollContainers Optional ancestor chain with `layoutScroll` enabled.
|
|
29
|
+
* @param baseTransform Transform string applied during measurement. Defaults to `'none'`.
|
|
19
30
|
* @returns DOMRect snapshot of the element.
|
|
20
31
|
*
|
|
21
32
|
* @example
|
|
@@ -30,10 +41,10 @@ import { animate } from 'motion';
|
|
|
30
41
|
* const rect = measureRect(node, [innerScroll, outerScroll])
|
|
31
42
|
* ```
|
|
32
43
|
*/
|
|
33
|
-
export const measureRect = (el, scrollContainers) => {
|
|
44
|
+
export const measureRect = (el, scrollContainers, baseTransform = 'none') => {
|
|
34
45
|
const prev = el.style.transform;
|
|
35
46
|
try {
|
|
36
|
-
el.style.transform =
|
|
47
|
+
el.style.transform = baseTransform;
|
|
37
48
|
const rect = el.getBoundingClientRect();
|
|
38
49
|
if (!scrollContainers || scrollContainers.length === 0)
|
|
39
50
|
return rect;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local mirrors of `motion-utils`'s `Axis` / `Box` / `AxisDelta` /
|
|
3
|
+
* `Delta` types. Inlined because `motion-utils` is a runtime-only
|
|
4
|
+
* transitive dep through `motion-dom` — the runtime helpers
|
|
5
|
+
* (`calcBoxDelta`, `createDelta`, `isDeltaZero`) are re-exported, but
|
|
6
|
+
* the type aliases are not. Same approach we use in
|
|
7
|
+
* `src/lib/components/Reorder/context.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Values match upstream byte-for-byte so handoff between our types and
|
|
10
|
+
* the runtime helpers from motion-dom is implicit.
|
|
11
|
+
*/
|
|
12
|
+
export interface Axis {
|
|
13
|
+
min: number;
|
|
14
|
+
max: number;
|
|
15
|
+
}
|
|
16
|
+
export interface Box {
|
|
17
|
+
x: Axis;
|
|
18
|
+
y: Axis;
|
|
19
|
+
}
|
|
20
|
+
export interface AxisDelta {
|
|
21
|
+
translate: number;
|
|
22
|
+
scale: number;
|
|
23
|
+
origin: number;
|
|
24
|
+
originPoint: number;
|
|
25
|
+
}
|
|
26
|
+
export interface Delta {
|
|
27
|
+
x: AxisDelta;
|
|
28
|
+
y: AxisDelta;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Event names the projection node fans out. Mirrors framer-motion's
|
|
32
|
+
* `LayoutEvents` subset that's actually consumed externally.
|
|
33
|
+
*
|
|
34
|
+
* - `willUpdate` — fires inside `willUpdate()`, AFTER the pre-mutation
|
|
35
|
+
* snapshot has been captured. Receives the snapshot Box.
|
|
36
|
+
* - `didUpdate` — fires inside `didUpdate()`, AFTER the post-mutation
|
|
37
|
+
* re-measure. Receives the full `LayoutUpdateData` payload (layout,
|
|
38
|
+
* snapshot, delta, hasLayoutChanged).
|
|
39
|
+
* - `measure` — fires every time `measure()` returns a non-null Box.
|
|
40
|
+
* Useful for debug overlays and follow-up event consumers.
|
|
41
|
+
*/
|
|
42
|
+
export type ProjectionEventName = 'willUpdate' | 'didUpdate' | 'measure';
|
|
43
|
+
/**
|
|
44
|
+
* Payload delivered to `didUpdate` listeners.
|
|
45
|
+
*
|
|
46
|
+
* `layout` is the post-mutation measurement; `snapshot` is the
|
|
47
|
+
* pre-mutation one. `delta` is `calcBoxDelta(snapshot, layout)` and is
|
|
48
|
+
* the value drag-listeners apply via `originPoint += delta.translate`
|
|
49
|
+
* + `motionValue.set(motionValue.get() + delta.translate)` to keep a
|
|
50
|
+
* dragged element under the cursor while its slot moves.
|
|
51
|
+
*
|
|
52
|
+
* `hasLayoutChanged` is `!isDeltaZero(delta)`. `isDeltaZero` (from
|
|
53
|
+
* motion-dom) treats an axis as unchanged only when its translate is
|
|
54
|
+
* within ±0.01px and its scale within ±0.0001 of identity — a tight
|
|
55
|
+
* floating-point epsilon, NOT a 1px rounding threshold. A genuine
|
|
56
|
+
* sub-pixel layout shift (say 0.4px) is therefore reported as a change.
|
|
57
|
+
*/
|
|
58
|
+
export interface ProjectionDidUpdateData {
|
|
59
|
+
layout: Box;
|
|
60
|
+
snapshot: Box;
|
|
61
|
+
delta: Delta;
|
|
62
|
+
hasLayoutChanged: boolean;
|
|
63
|
+
}
|
|
64
|
+
type Listener<E extends ProjectionEventName> = E extends 'didUpdate' ? (data: ProjectionDidUpdateData) => void : E extends 'willUpdate' ? (snapshot: Box) => void : (layout: Box) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Options passed at `ProjectionNode` construction time. All optional —
|
|
67
|
+
* a node with no options still works as a leaf measurement target.
|
|
68
|
+
*/
|
|
69
|
+
export interface ProjectionNodeOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Parent node in the projection tree. Wire-up is callsite-driven
|
|
72
|
+
* (the consumer reads `getProjectionParent()` from the Svelte
|
|
73
|
+
* context system and passes the result here); the node stores it
|
|
74
|
+
* as `this.parent` and registers self in `parent.children` on
|
|
75
|
+
* `mount()`.
|
|
76
|
+
*/
|
|
77
|
+
parent?: ProjectionNode | null;
|
|
78
|
+
/**
|
|
79
|
+
* Thunk returning the `layoutScroll` ancestor chain at measure
|
|
80
|
+
* time. Used as the second argument to `measureRect`, which
|
|
81
|
+
* shifts the returned rect by the sum of ancestor
|
|
82
|
+
* `scrollLeft`/`scrollTop` so FLIP deltas stay correct when
|
|
83
|
+
* scrollable ancestors scroll between two measurements.
|
|
84
|
+
*
|
|
85
|
+
* Returning `[]` (or omitting the option entirely) gives
|
|
86
|
+
* viewport-relative measurements — fine for the common case.
|
|
87
|
+
*/
|
|
88
|
+
getScrollContainers?: () => HTMLElement[];
|
|
89
|
+
/**
|
|
90
|
+
* Thunk returning the element's USER-authored `transform` — the
|
|
91
|
+
* value `measure()` resets to while reading, so the motion-applied
|
|
92
|
+
* portion (`initial`/`animate`/FLIP/drag) is stripped but the user's
|
|
93
|
+
* own transform is preserved.
|
|
94
|
+
*
|
|
95
|
+
* Preferred over the mount-time `baseTransform` capture, because at
|
|
96
|
+
* mount the inline `style.transform` already carries any
|
|
97
|
+
* transform-type `initial` keyframe (it is serialized inline before
|
|
98
|
+
* effects run). The consumer therefore sources this from the `style`
|
|
99
|
+
* prop instead (see `extractTransform`). When omitted, the node
|
|
100
|
+
* falls back to the mount-captured `baseTransform`.
|
|
101
|
+
*/
|
|
102
|
+
getBaseTransform?: () => string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Per-element node in the projection tree. Created at component setup
|
|
106
|
+
* time in `_MotionContainer.svelte`, mounted when the element ref
|
|
107
|
+
* binds, unmounted on cleanup.
|
|
108
|
+
*
|
|
109
|
+
* Lifecycle:
|
|
110
|
+
* 1. `new ProjectionNode({ parent, getScrollContainers })` at setup.
|
|
111
|
+
* 2. `node.mount(element)` once the element ref binds.
|
|
112
|
+
* 3. `node.willUpdate()` before any layout-mutating state change (e.g.
|
|
113
|
+
* a `values` reassign that reorders DOM children).
|
|
114
|
+
* 4. State mutates → Svelte commits the DOM update.
|
|
115
|
+
* 5. `node.didUpdate()` after the DOM update is flushed — fires
|
|
116
|
+
* `didUpdate` listeners with the snapshot→current delta.
|
|
117
|
+
* 6. `node.unmount()` on cleanup.
|
|
118
|
+
*/
|
|
119
|
+
export declare class ProjectionNode {
|
|
120
|
+
/** The mounted element. `null` until `mount()` runs. */
|
|
121
|
+
element: HTMLElement | null;
|
|
122
|
+
/**
|
|
123
|
+
* Parent node in the projection tree. Captured at construction
|
|
124
|
+
* from the Svelte context. Set to `null` for root-level motion
|
|
125
|
+
* elements that have no motion ancestor.
|
|
126
|
+
*/
|
|
127
|
+
parent: ProjectionNode | null;
|
|
128
|
+
/**
|
|
129
|
+
* Descendant nodes registered via `mount()`. Iterated when we
|
|
130
|
+
* need to broadcast to the subtree (none in this PR; reserved
|
|
131
|
+
* for follow-up work).
|
|
132
|
+
*/
|
|
133
|
+
readonly children: Set<ProjectionNode>;
|
|
134
|
+
/** Most-recent post-mutation measurement, or `null` before first measure. */
|
|
135
|
+
latestLayout: Box | null;
|
|
136
|
+
/**
|
|
137
|
+
* Pre-mutation snapshot captured by `willUpdate`. Cleared by
|
|
138
|
+
* `didUpdate` after the delta has been computed. Idempotent for
|
|
139
|
+
* repeat `willUpdate` calls in the same frame — only the first
|
|
140
|
+
* snapshots; subsequent calls no-op so a parent broadcasting
|
|
141
|
+
* `willUpdate` to its children doesn't clobber a child's own
|
|
142
|
+
* earlier snapshot.
|
|
143
|
+
*/
|
|
144
|
+
snapshot: Box | null;
|
|
145
|
+
/** Whether `mount()` has been called and `unmount()` has not. */
|
|
146
|
+
isMounted: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Fallback user-authored base transform, captured from
|
|
149
|
+
* `element.style.transform` at `mount()` time. Used only when no
|
|
150
|
+
* `getBaseTransform` thunk was provided (e.g. unit tests that
|
|
151
|
+
* construct a bare node and set the transform before mounting).
|
|
152
|
+
*
|
|
153
|
+
* For real `motion.*` elements the `getBaseTransform` thunk is
|
|
154
|
+
* preferred: capturing at mount is unsafe because a transform-type
|
|
155
|
+
* `initial` keyframe is serialized into the inline `style.transform`
|
|
156
|
+
* BEFORE effects run, so the mount-time value can be a motion
|
|
157
|
+
* transform rather than the user's. `resolveBaseTransform()` picks
|
|
158
|
+
* the thunk first.
|
|
159
|
+
*
|
|
160
|
+
* `measure()` resets ancestors (and self, via `measureRect`) to this
|
|
161
|
+
* base rather than to `'none'`, removing the motion-applied portion
|
|
162
|
+
* while leaving the user-authored part intact — the same distinction
|
|
163
|
+
* framer-motion draws by only subtracting motion-tracked
|
|
164
|
+
* `latestValues` in `removeBoxTransforms`.
|
|
165
|
+
*/
|
|
166
|
+
baseTransform: string;
|
|
167
|
+
private readonly listeners;
|
|
168
|
+
private readonly getScrollContainers;
|
|
169
|
+
private readonly getBaseTransform;
|
|
170
|
+
constructor(options?: ProjectionNodeOptions);
|
|
171
|
+
/**
|
|
172
|
+
* The transform `measure()` resets this node's element to while
|
|
173
|
+
* reading. Prefers the `getBaseTransform` thunk (the user's authored
|
|
174
|
+
* `style` transform, motion-independent); falls back to the
|
|
175
|
+
* mount-captured `baseTransform`. See `getBaseTransform` /
|
|
176
|
+
* `baseTransform` for why the thunk is the safe source.
|
|
177
|
+
*/
|
|
178
|
+
private resolveBaseTransform;
|
|
179
|
+
/**
|
|
180
|
+
* Register this node with its parent + bind to a DOM element.
|
|
181
|
+
* Idempotent — calling `mount()` twice on the same element is a
|
|
182
|
+
* no-op for the registration steps.
|
|
183
|
+
*
|
|
184
|
+
* Re-mounting onto a DIFFERENT element swaps the element in place
|
|
185
|
+
* WITHOUT a full `unmount()`. A full unmount would `children.clear()`,
|
|
186
|
+
* orphaning still-mounted descendants that registered themselves in
|
|
187
|
+
* this node's `children` (they keep their `parent` pointer but the
|
|
188
|
+
* set would never be repopulated). Listeners and children are
|
|
189
|
+
* therefore preserved across an element swap.
|
|
190
|
+
*
|
|
191
|
+
* @param element The DOM element this node represents.
|
|
192
|
+
*/
|
|
193
|
+
mount(element: HTMLElement): void;
|
|
194
|
+
/**
|
|
195
|
+
* Tear down. Detaches from parent, clears children references,
|
|
196
|
+
* drops all listeners. Safe to call on a never-mounted node and
|
|
197
|
+
* safe to call twice.
|
|
198
|
+
*/
|
|
199
|
+
unmount(): void;
|
|
200
|
+
/**
|
|
201
|
+
* Read the element's layout box with every ancestor's
|
|
202
|
+
* motion-applied transform temporarily removed, while preserving
|
|
203
|
+
* each ancestor's user-authored base transform.
|
|
204
|
+
*
|
|
205
|
+
* Mechanism:
|
|
206
|
+
* 1. Walk `this.parent` chain bottom-up, collecting every
|
|
207
|
+
* mounted ancestor node (excludes self — `measureRect`
|
|
208
|
+
* handles self's transform internally).
|
|
209
|
+
* 2. Snapshot each ancestor's current `el.style.transform`.
|
|
210
|
+
* 3. Set each to its node's resolved base transform (the
|
|
211
|
+
* user-authored value, via `resolveBaseTransform`). This strips
|
|
212
|
+
* any FLIP/drag/initial transform while keeping the user's static
|
|
213
|
+
* one — see `getBaseTransform` / `baseTransform`.
|
|
214
|
+
* 4. Delegate to `measureRect(self.element, scrollContainers,
|
|
215
|
+
* self base)`, which applies self's base transform inside its own
|
|
216
|
+
* try/finally and returns the scroll-compensated DOMRect.
|
|
217
|
+
* 5. Restore ancestor transforms in reverse order inside a
|
|
218
|
+
* `finally` block — guarantees restoration even if measure
|
|
219
|
+
* throws.
|
|
220
|
+
* 6. Convert DOMRect → Box and cache as `latestLayout`.
|
|
221
|
+
*
|
|
222
|
+
* Returns `null` when `element` is not mounted.
|
|
223
|
+
*/
|
|
224
|
+
measure(): Box | null;
|
|
225
|
+
/**
|
|
226
|
+
* Snapshot the current layout box for use by the next
|
|
227
|
+
* `didUpdate()`. Caller's contract: invoke this BEFORE the
|
|
228
|
+
* layout-mutating state change so the snapshot reflects the
|
|
229
|
+
* pre-mutation position.
|
|
230
|
+
*
|
|
231
|
+
* Idempotent within a frame — once a snapshot exists, subsequent
|
|
232
|
+
* `willUpdate()` calls are no-ops until `didUpdate()` consumes it.
|
|
233
|
+
* This means a parent that broadcasts `willUpdate` to its
|
|
234
|
+
* children before its own snapshot is fine: children snapshot
|
|
235
|
+
* themselves first via their own willUpdate, parent's broadcast
|
|
236
|
+
* is a no-op.
|
|
237
|
+
*/
|
|
238
|
+
willUpdate(): void;
|
|
239
|
+
/**
|
|
240
|
+
* Re-measure post-mutation, compute the delta against the
|
|
241
|
+
* snapshot, fire `didUpdate` listeners. No-op when there's no
|
|
242
|
+
* snapshot (matches upstream — `willUpdate` MUST precede
|
|
243
|
+
* `didUpdate` for the cycle to fire).
|
|
244
|
+
*
|
|
245
|
+
* Always clears the snapshot at the end so the next gesture's
|
|
246
|
+
* `willUpdate`/`didUpdate` cycle starts fresh.
|
|
247
|
+
*/
|
|
248
|
+
didUpdate(): void;
|
|
249
|
+
/**
|
|
250
|
+
* Observer-driven layout-change commit. Unlike the explicit
|
|
251
|
+
* `willUpdate()` → mutate → `didUpdate()` cycle (used when a
|
|
252
|
+
* consumer controls the exact mutation moment, e.g. Reorder.Group
|
|
253
|
+
* before a `values` reassign), this is for the reactive path where
|
|
254
|
+
* a layout change has ALREADY happened and we only learn about it
|
|
255
|
+
* after the fact (the existing `observeLayoutChanges` FLIP loop in
|
|
256
|
+
* `_MotionContainer`).
|
|
257
|
+
*
|
|
258
|
+
* Uses the cached `latestLayout` (the pre-change position from
|
|
259
|
+
* mount or the previous commit) as the snapshot, re-measures the
|
|
260
|
+
* post-change position, and fires `didUpdate` with the delta.
|
|
261
|
+
*
|
|
262
|
+
* First call after mount just seeds `latestLayout` (no prior
|
|
263
|
+
* position to diff against) and fires nothing.
|
|
264
|
+
*
|
|
265
|
+
* Returns the freshly-measured `Box` (or `null` when unmounted) so
|
|
266
|
+
* the caller can reuse it — the FLIP loop in `_MotionContainer` uses
|
|
267
|
+
* this as its `next` rect instead of measuring a second time per
|
|
268
|
+
* frame.
|
|
269
|
+
*
|
|
270
|
+
* The `didUpdate` fan-out is gated on a non-zero delta. The FLIP
|
|
271
|
+
* animation this commit runs alongside writes its own inverse
|
|
272
|
+
* `transform` to the element every frame, and those writes re-trigger
|
|
273
|
+
* the same `observeLayoutChanges` signal that drives this method.
|
|
274
|
+
* Those re-fires carry no real layout change (the ancestor-stripped
|
|
275
|
+
* measure is identical to the previous one), so without the
|
|
276
|
+
* `isDeltaZero` gate every animation frame would fan out a `delta: 0`
|
|
277
|
+
* event and clobber the genuine delta from the originating change.
|
|
278
|
+
*/
|
|
279
|
+
commitLayoutChange(): Box | null;
|
|
280
|
+
/**
|
|
281
|
+
* Subscribe to a projection event. Returns an unsubscribe
|
|
282
|
+
* function. Safe to call after `unmount()` (becomes a no-op).
|
|
283
|
+
*/
|
|
284
|
+
addEventListener<E extends ProjectionEventName>(name: E, cb: Listener<E>): () => void;
|
|
285
|
+
private notify;
|
|
286
|
+
}
|
|
287
|
+
export {};
|