@humanspeak/svelte-motion 0.5.1 → 0.5.3
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/projection.context.d.ts +22 -0
- package/dist/components/projection.context.js +56 -0
- package/dist/html/_MotionContainer.svelte +328 -24
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +78 -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/pan.d.ts +135 -0
- package/dist/utils/pan.js +426 -0
- 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 +5 -5
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { cancelFrame, frame, frameData, isPrimaryPointer } from 'motion-dom';
|
|
2
|
+
/**
|
|
3
|
+
* Brand we stamp on already-wrapped handlers so passing them back through
|
|
4
|
+
* `wrapHandlers` (e.g. by a future middleware layer) doesn't double-defer
|
|
5
|
+
* — that would compound frame latency invisibly per wrap depth. Symbol
|
|
6
|
+
* scoping keeps it private to this module.
|
|
7
|
+
*/
|
|
8
|
+
const WRAPPED_BRAND = Symbol('svelte-motion:pan:wrapped');
|
|
9
|
+
const wrapUpdate = (handler, isAlive) => {
|
|
10
|
+
if (!handler)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (handler[WRAPPED_BRAND])
|
|
13
|
+
return handler;
|
|
14
|
+
const wrapped = (event, info) => {
|
|
15
|
+
frame.update(() => {
|
|
16
|
+
// `isAlive` flips false on teardown; any frame.update closure
|
|
17
|
+
// queued before teardown but not yet flushed will see this and
|
|
18
|
+
// short-circuit — that's our cancellation path for the
|
|
19
|
+
// otherwise-uncancellable anonymous closures `frame.update`
|
|
20
|
+
// accepts.
|
|
21
|
+
if (!isAlive())
|
|
22
|
+
return;
|
|
23
|
+
handler(event, info);
|
|
24
|
+
}, false, true);
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(wrapped, WRAPPED_BRAND, { value: true });
|
|
27
|
+
return wrapped;
|
|
28
|
+
};
|
|
29
|
+
const wrapPostRender = (handler, isAlive) => {
|
|
30
|
+
if (!handler)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (handler[WRAPPED_BRAND])
|
|
33
|
+
return handler;
|
|
34
|
+
const wrapped = (event, info) => {
|
|
35
|
+
frame.postRender(() => {
|
|
36
|
+
if (!isAlive())
|
|
37
|
+
return;
|
|
38
|
+
handler(event, info);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(wrapped, WRAPPED_BRAND, { value: true });
|
|
42
|
+
return wrapped;
|
|
43
|
+
};
|
|
44
|
+
const wrapHandlers = (handlers, isAlive) => ({
|
|
45
|
+
onSessionStart: wrapUpdate(handlers.onSessionStart, isAlive),
|
|
46
|
+
onStart: wrapUpdate(handlers.onStart, isAlive),
|
|
47
|
+
onMove: wrapUpdate(handlers.onMove, isAlive),
|
|
48
|
+
onEnd: wrapPostRender(handlers.onEnd, isAlive),
|
|
49
|
+
onSessionEnd: wrapPostRender(handlers.onSessionEnd, isAlive)
|
|
50
|
+
});
|
|
51
|
+
const overflowStyles = new Set(['auto', 'scroll']);
|
|
52
|
+
const millisecondsToSeconds = (ms) => ms / 1000;
|
|
53
|
+
const secondsToMilliseconds = (s) => s * 1000;
|
|
54
|
+
const subtractPoint = (a, b) => ({
|
|
55
|
+
x: a.x - b.x,
|
|
56
|
+
y: a.y - b.y
|
|
57
|
+
});
|
|
58
|
+
const distance2D = (a, b) => Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
|
|
59
|
+
/**
|
|
60
|
+
* Compute velocity (px/s) from the history of timestamped points,
|
|
61
|
+
* looking back `timeDelta` seconds for stability. Matches upstream's
|
|
62
|
+
* `getVelocity` including the hold-then-flick safeguard (skip
|
|
63
|
+
* history[0] if it's > 2× timeDelta old AND there are alternatives).
|
|
64
|
+
*/
|
|
65
|
+
const getVelocity = (history, timeDelta) => {
|
|
66
|
+
if (history.length < 2)
|
|
67
|
+
return { x: 0, y: 0 };
|
|
68
|
+
let i = history.length - 1;
|
|
69
|
+
let timestampedPoint = null;
|
|
70
|
+
const lastPoint = history[history.length - 1];
|
|
71
|
+
while (i >= 0) {
|
|
72
|
+
timestampedPoint = history[i];
|
|
73
|
+
if (lastPoint.timestamp - timestampedPoint.timestamp > secondsToMilliseconds(timeDelta)) {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
i--;
|
|
77
|
+
}
|
|
78
|
+
if (!timestampedPoint)
|
|
79
|
+
return { x: 0, y: 0 };
|
|
80
|
+
if (timestampedPoint === history[0] &&
|
|
81
|
+
history.length > 2 &&
|
|
82
|
+
lastPoint.timestamp - timestampedPoint.timestamp > secondsToMilliseconds(timeDelta) * 2) {
|
|
83
|
+
timestampedPoint = history[1];
|
|
84
|
+
}
|
|
85
|
+
const time = millisecondsToSeconds(lastPoint.timestamp - timestampedPoint.timestamp);
|
|
86
|
+
if (time === 0)
|
|
87
|
+
return { x: 0, y: 0 };
|
|
88
|
+
const v = {
|
|
89
|
+
x: (lastPoint.x - timestampedPoint.x) / time,
|
|
90
|
+
y: (lastPoint.y - timestampedPoint.y) / time
|
|
91
|
+
};
|
|
92
|
+
if (v.x === Infinity)
|
|
93
|
+
v.x = 0;
|
|
94
|
+
if (v.y === Infinity)
|
|
95
|
+
v.y = 0;
|
|
96
|
+
return v;
|
|
97
|
+
};
|
|
98
|
+
const getPanInfo = (point, history) => ({
|
|
99
|
+
point,
|
|
100
|
+
delta: subtractPoint(point, history[history.length - 1]),
|
|
101
|
+
offset: subtractPoint(point, history[0]),
|
|
102
|
+
velocity: getVelocity(history, 0.1)
|
|
103
|
+
});
|
|
104
|
+
const extractEventPoint = (event) => ({
|
|
105
|
+
x: event.pageX,
|
|
106
|
+
y: event.pageY
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* Attach a pan gesture session to `el`. Returns a cleanup function that
|
|
110
|
+
* tears down the pointerdown listener and ends any in-flight session,
|
|
111
|
+
* with a `.update(next)` method for hot-swapping handlers mid-gesture.
|
|
112
|
+
*
|
|
113
|
+
* Internally a fresh `PanSession` spawns on each pointerdown — the
|
|
114
|
+
* outer attachment just keeps the pointerdown listener alive across the
|
|
115
|
+
* element's lifetime.
|
|
116
|
+
*
|
|
117
|
+
* SSR-safe: returns a no-op cleanup if `window` is undefined. The Svelte
|
|
118
|
+
* `$effect` consumer never fires on the server anyway, but defending the
|
|
119
|
+
* boundary lets the module load cleanly in node-only test runners.
|
|
120
|
+
*
|
|
121
|
+
* Lifecycle guarantee: when the returned cleanup runs mid-gesture, the
|
|
122
|
+
* session synthesizes `onEnd` + `onSessionEnd` against the raw handlers
|
|
123
|
+
* BEFORE removing listeners (see `PanSession.dispatchTerminal`). Hosts
|
|
124
|
+
* (e.g. `_MotionContainer`'s pan `$effect`) can put their `whilePan`
|
|
125
|
+
* revert logic inside the user-supplied `onEnd` and rely on it firing
|
|
126
|
+
* exactly once per gesture — whether the user released or the host
|
|
127
|
+
* forced teardown.
|
|
128
|
+
*
|
|
129
|
+
* @param el Target element to bind `pointerdown` on. Move/up/cancel
|
|
130
|
+
* events are listened for on the element's owning window so a fast
|
|
131
|
+
* swipe past the element's bounds keeps the gesture alive.
|
|
132
|
+
* @param handlers Pan lifecycle handlers. Any subset of
|
|
133
|
+
* `onSessionStart` (fires on pointerdown), `onStart` (fires the first
|
|
134
|
+
* time the cumulative offset crosses `distanceThreshold`), `onMove`
|
|
135
|
+
* (per-frame-throttled on every pointermove past threshold), `onEnd`
|
|
136
|
+
* (fires on pointerup/cancel if `onStart` ever fired), `onSessionEnd`
|
|
137
|
+
* (fires on every pointerup/cancel where a pointermove occurred).
|
|
138
|
+
* @param options Per-session config. `distanceThreshold` (default 3px)
|
|
139
|
+
* gates the start callback; `contextWindow` overrides the owning
|
|
140
|
+
* window (use for shadow-root / iframe scenarios).
|
|
141
|
+
* @returns A cleanup function with an attached `.update(next)` method.
|
|
142
|
+
* Calling the cleanup ends the session + removes the pointerdown
|
|
143
|
+
* listener. Calling `.update(next)` swaps handlers in place on the
|
|
144
|
+
* live session without rebuilding it — the canonical Svelte pattern
|
|
145
|
+
* for inline arrow handlers that change identity each render.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const cleanup = attachPan(node, {
|
|
150
|
+
* onStart: (_event, info) => console.log('start', info.offset),
|
|
151
|
+
* onMove: (_event, info) => x.set(info.offset.x),
|
|
152
|
+
* onEnd: (_event, info) => {
|
|
153
|
+
* if (Math.abs(info.velocity.x) > 600) commit()
|
|
154
|
+
* else animate(x, 0, { type: 'spring' })
|
|
155
|
+
* }
|
|
156
|
+
* })
|
|
157
|
+
*
|
|
158
|
+
* // Later, swap handlers without ending the live gesture:
|
|
159
|
+
* cleanup.update({ onMove: (_e, info) => x.set(info.offset.x * 2) })
|
|
160
|
+
*
|
|
161
|
+
* // On unmount:
|
|
162
|
+
* cleanup()
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export const attachPan = (el, handlers, options = {}) => {
|
|
166
|
+
if (typeof window === 'undefined') {
|
|
167
|
+
const noop = () => { };
|
|
168
|
+
return Object.assign(noop, { update: () => { } });
|
|
169
|
+
}
|
|
170
|
+
const contextWindow = options.contextWindow ?? el.ownerDocument?.defaultView ?? window;
|
|
171
|
+
const distanceThreshold = options.distanceThreshold ?? 3;
|
|
172
|
+
let session = null;
|
|
173
|
+
let rawHandlers = handlers;
|
|
174
|
+
// Liveness flag the wrapped handler closures consult before invoking
|
|
175
|
+
// the user callback. Flips false at teardown so any frame.update /
|
|
176
|
+
// frame.postRender callbacks queued before teardown — but not yet
|
|
177
|
+
// flushed — see the flag and skip dispatch. This is our only way to
|
|
178
|
+
// cancel the anonymous closures the wrappers schedule (frame.update
|
|
179
|
+
// doesn't return a handle we can store per call).
|
|
180
|
+
let isAlive = true;
|
|
181
|
+
const aliveGuard = () => isAlive;
|
|
182
|
+
// Frame-scheduled mirror of the live handlers — onSessionStart / onStart /
|
|
183
|
+
// onMove are queued onto motion-dom's `update` step, onEnd / onSessionEnd
|
|
184
|
+
// onto `postRender`. This is the wrap upstream applies via `asyncHandler`
|
|
185
|
+
// + `frame.postRender` in PanGesture.createPanHandlers; see the
|
|
186
|
+
// wrapUpdate / wrapPostRender helpers at the top of this file for the
|
|
187
|
+
// rationale. PanSession itself stays scheduler-unaware (and synchronous,
|
|
188
|
+
// for testability) — the scheduling lives at the `attachPan` boundary.
|
|
189
|
+
let liveHandlers = wrapHandlers(handlers, aliveGuard);
|
|
190
|
+
const onPointerDown = (event) => {
|
|
191
|
+
// Match upstream: ignore non-primary pointers (multi-touch, right-click).
|
|
192
|
+
if (!isPrimaryPointer(event))
|
|
193
|
+
return;
|
|
194
|
+
// Defensively end any prior session before overwriting the reference.
|
|
195
|
+
// Without this, a second primary pointerdown that arrives before the
|
|
196
|
+
// first pointerup orphans the prior session's contextWindow listeners.
|
|
197
|
+
session?.end();
|
|
198
|
+
session = new PanSession(event, liveHandlers, {
|
|
199
|
+
distanceThreshold,
|
|
200
|
+
contextWindow,
|
|
201
|
+
element: el
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
el.addEventListener('pointerdown', onPointerDown);
|
|
205
|
+
const update = (next) => {
|
|
206
|
+
rawHandlers = next;
|
|
207
|
+
liveHandlers = wrapHandlers(next, aliveGuard);
|
|
208
|
+
session?.updateHandlers(liveHandlers);
|
|
209
|
+
};
|
|
210
|
+
const teardown = () => {
|
|
211
|
+
// Synthesize the gesture's terminal lifecycle BEFORE flipping
|
|
212
|
+
// `isAlive`, so a host that tears us down mid-pan (effect re-run,
|
|
213
|
+
// component unmount) still sees a balanced onPanEnd / onPanSessionEnd
|
|
214
|
+
// pair. Dispatched against the *raw* (unwrapped) handlers so the
|
|
215
|
+
// delivery is synchronous — the wrapped lane would otherwise queue
|
|
216
|
+
// the callbacks onto frame.postRender just for them to be cancelled
|
|
217
|
+
// by the `isAlive = false` line immediately below.
|
|
218
|
+
if (session) {
|
|
219
|
+
session.dispatchTerminal(rawHandlers);
|
|
220
|
+
session.end();
|
|
221
|
+
session = null;
|
|
222
|
+
}
|
|
223
|
+
isAlive = false;
|
|
224
|
+
el.removeEventListener('pointerdown', onPointerDown);
|
|
225
|
+
};
|
|
226
|
+
return Object.assign(teardown, { update });
|
|
227
|
+
};
|
|
228
|
+
class PanSession {
|
|
229
|
+
history = [];
|
|
230
|
+
startEvent = null;
|
|
231
|
+
lastMoveEvent = null;
|
|
232
|
+
lastMovePoint = null;
|
|
233
|
+
handlers = {};
|
|
234
|
+
contextWindow = window;
|
|
235
|
+
distanceThreshold = 3;
|
|
236
|
+
element = null;
|
|
237
|
+
scrollPositions = new Map();
|
|
238
|
+
/**
|
|
239
|
+
* Idempotency flag — set the first time the gesture's terminal
|
|
240
|
+
* lifecycle pair (`onEnd` + `onSessionEnd`) fires. Both
|
|
241
|
+
* `handlePointerUp` (the natural release path) and
|
|
242
|
+
* `dispatchTerminal` (the forced-teardown path called by
|
|
243
|
+
* `attachPan.teardown`) check this and bail if already dispatched.
|
|
244
|
+
* Without it, a normal pointerup followed by a host-side teardown
|
|
245
|
+
* (e.g. `$effect` cleanup, component unmount) would replay
|
|
246
|
+
* `onEnd`/`onSessionEnd` against handlers that already saw them.
|
|
247
|
+
*/
|
|
248
|
+
terminalDispatched = false;
|
|
249
|
+
removeScrollListeners = null;
|
|
250
|
+
removeListeners = null;
|
|
251
|
+
constructor(event, handlers, opts) {
|
|
252
|
+
// Bail on non-primary pointers. Properties keep their declared
|
|
253
|
+
// defaults so TypeScript sees them initialized regardless of which
|
|
254
|
+
// constructor branch ran.
|
|
255
|
+
if (!isPrimaryPointer(event))
|
|
256
|
+
return;
|
|
257
|
+
this.handlers = handlers;
|
|
258
|
+
this.contextWindow = opts.contextWindow;
|
|
259
|
+
this.distanceThreshold = opts.distanceThreshold;
|
|
260
|
+
this.element = opts.element;
|
|
261
|
+
const point = extractEventPoint(event);
|
|
262
|
+
this.history = [{ ...point, timestamp: frameData.timestamp }];
|
|
263
|
+
this.handlers.onSessionStart?.(event, getPanInfo(point, this.history));
|
|
264
|
+
const moveHandler = (e) => this.handlePointerMove(e);
|
|
265
|
+
const upHandler = (e) => this.handlePointerUp(e);
|
|
266
|
+
this.contextWindow.addEventListener('pointermove', moveHandler);
|
|
267
|
+
this.contextWindow.addEventListener('pointerup', upHandler);
|
|
268
|
+
this.contextWindow.addEventListener('pointercancel', upHandler);
|
|
269
|
+
this.removeListeners = () => {
|
|
270
|
+
this.contextWindow.removeEventListener('pointermove', moveHandler);
|
|
271
|
+
this.contextWindow.removeEventListener('pointerup', upHandler);
|
|
272
|
+
this.contextWindow.removeEventListener('pointercancel', upHandler);
|
|
273
|
+
};
|
|
274
|
+
if (this.element)
|
|
275
|
+
this.startScrollTracking(this.element);
|
|
276
|
+
}
|
|
277
|
+
updateHandlers(handlers) {
|
|
278
|
+
this.handlers = handlers;
|
|
279
|
+
}
|
|
280
|
+
end() {
|
|
281
|
+
this.removeListeners?.();
|
|
282
|
+
this.removeListeners = null;
|
|
283
|
+
this.removeScrollListeners?.();
|
|
284
|
+
this.removeScrollListeners = null;
|
|
285
|
+
this.scrollPositions.clear();
|
|
286
|
+
cancelFrame(this.updatePoint);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Synthesize the gesture's terminal lifecycle pair (`onEnd` then
|
|
290
|
+
* `onSessionEnd`) against the supplied *raw* (unwrapped) handlers,
|
|
291
|
+
* using the last observed event + point as the synthetic terminal
|
|
292
|
+
* sample. Called by `attachPan.teardown` when a host kills the
|
|
293
|
+
* session mid-gesture — without this, an `$effect` re-run that
|
|
294
|
+
* tears down the attachment silently strands the consumer's state
|
|
295
|
+
* machine in an "in-progress" state (whilePan keyframes never
|
|
296
|
+
* revert, threshold-based commit decisions never run).
|
|
297
|
+
*
|
|
298
|
+
* Bypasses the frame-loop wrappers deliberately: the wrapped
|
|
299
|
+
* handlers would queue to `frame.postRender` only for the
|
|
300
|
+
* about-to-flip `isAlive` flag in attachPan to cancel them. Raw
|
|
301
|
+
* dispatch keeps the lifecycle synchronous with teardown.
|
|
302
|
+
*
|
|
303
|
+
* No-op when no pointermove ever fired — matches the
|
|
304
|
+
* `handlePointerUp` no-movement contract upstream uses.
|
|
305
|
+
*/
|
|
306
|
+
dispatchTerminal(rawHandlers) {
|
|
307
|
+
if (this.terminalDispatched)
|
|
308
|
+
return;
|
|
309
|
+
if (!(this.lastMoveEvent && this.lastMovePoint))
|
|
310
|
+
return;
|
|
311
|
+
const info = getPanInfo(this.lastMovePoint, this.history);
|
|
312
|
+
if (this.startEvent)
|
|
313
|
+
rawHandlers.onEnd?.(this.lastMoveEvent, info);
|
|
314
|
+
rawHandlers.onSessionEnd?.(this.lastMoveEvent, info);
|
|
315
|
+
this.terminalDispatched = true;
|
|
316
|
+
}
|
|
317
|
+
handlePointerMove = (event) => {
|
|
318
|
+
this.lastMoveEvent = event;
|
|
319
|
+
this.lastMovePoint = extractEventPoint(event);
|
|
320
|
+
// Per-frame throttle so a 1000hz mouse doesn't drown handlers.
|
|
321
|
+
frame.update(this.updatePoint, true);
|
|
322
|
+
};
|
|
323
|
+
handlePointerUp = (event) => {
|
|
324
|
+
this.end();
|
|
325
|
+
if (this.terminalDispatched)
|
|
326
|
+
return;
|
|
327
|
+
if (!(this.lastMoveEvent && this.lastMovePoint)) {
|
|
328
|
+
// No pointermove ever fired — match upstream framer-motion
|
|
329
|
+
// (`packages/framer-motion/src/gestures/pan/PanSession.ts`
|
|
330
|
+
// ~line 320) and return WITHOUT firing onEnd / onSessionEnd.
|
|
331
|
+
// Consumers that want a "tap" signal should use the press /
|
|
332
|
+
// tap gesture instead. This prevents a spurious
|
|
333
|
+
// onPanSessionStart → onPanSessionEnd pair on every plain
|
|
334
|
+
// click of a pan-enabled element.
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const finalPoint = event.type === 'pointercancel' ? this.lastMovePoint : extractEventPoint(event);
|
|
338
|
+
const info = getPanInfo(finalPoint, this.history);
|
|
339
|
+
if (this.startEvent)
|
|
340
|
+
this.handlers.onEnd?.(event, info);
|
|
341
|
+
this.handlers.onSessionEnd?.(event, info);
|
|
342
|
+
// Mark idempotent so a later forced teardown via
|
|
343
|
+
// `dispatchTerminal` doesn't replay this pair.
|
|
344
|
+
this.terminalDispatched = true;
|
|
345
|
+
};
|
|
346
|
+
updatePoint = () => {
|
|
347
|
+
if (!(this.lastMoveEvent && this.lastMovePoint))
|
|
348
|
+
return;
|
|
349
|
+
const info = getPanInfo(this.lastMovePoint, this.history);
|
|
350
|
+
const panAlreadyStarted = this.startEvent !== null;
|
|
351
|
+
const pastThreshold = distance2D(info.offset, { x: 0, y: 0 }) >= this.distanceThreshold;
|
|
352
|
+
if (!panAlreadyStarted && !pastThreshold)
|
|
353
|
+
return;
|
|
354
|
+
this.history.push({ ...this.lastMovePoint, timestamp: frameData.timestamp });
|
|
355
|
+
if (!panAlreadyStarted) {
|
|
356
|
+
this.handlers.onStart?.(this.lastMoveEvent, info);
|
|
357
|
+
this.startEvent = this.lastMoveEvent;
|
|
358
|
+
}
|
|
359
|
+
this.handlers.onMove?.(this.lastMoveEvent, info);
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* Track scrollable ancestors so we can compensate for scroll deltas
|
|
363
|
+
* during the gesture — mirrors upstream's `startScrollTracking`.
|
|
364
|
+
* For element scrolls: adjust `history[0]` so offset stays sane
|
|
365
|
+
* (pageX/pageY unaffected by element scroll). For window scrolls:
|
|
366
|
+
* adjust `lastMovePoint` (pageX/pageY shift with window scroll).
|
|
367
|
+
*/
|
|
368
|
+
startScrollTracking(element) {
|
|
369
|
+
let current = element.parentElement;
|
|
370
|
+
while (current) {
|
|
371
|
+
const style = getComputedStyle(current);
|
|
372
|
+
if (overflowStyles.has(style.overflowX) || overflowStyles.has(style.overflowY)) {
|
|
373
|
+
this.scrollPositions.set(current, {
|
|
374
|
+
x: current.scrollLeft,
|
|
375
|
+
y: current.scrollTop
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
current = current.parentElement;
|
|
379
|
+
}
|
|
380
|
+
this.scrollPositions.set(this.contextWindow, {
|
|
381
|
+
x: this.contextWindow.scrollX,
|
|
382
|
+
y: this.contextWindow.scrollY
|
|
383
|
+
});
|
|
384
|
+
const onElementScroll = (event) => {
|
|
385
|
+
this.handleScroll(event.target);
|
|
386
|
+
};
|
|
387
|
+
const onWindowScroll = () => {
|
|
388
|
+
this.handleScroll(this.contextWindow);
|
|
389
|
+
};
|
|
390
|
+
this.contextWindow.addEventListener('scroll', onElementScroll, { capture: true });
|
|
391
|
+
this.contextWindow.addEventListener('scroll', onWindowScroll);
|
|
392
|
+
this.removeScrollListeners = () => {
|
|
393
|
+
this.contextWindow.removeEventListener('scroll', onElementScroll, {
|
|
394
|
+
capture: true
|
|
395
|
+
});
|
|
396
|
+
this.contextWindow.removeEventListener('scroll', onWindowScroll);
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
handleScroll(target) {
|
|
400
|
+
const initial = this.scrollPositions.get(target);
|
|
401
|
+
if (!initial)
|
|
402
|
+
return;
|
|
403
|
+
const isWindow = target === this.contextWindow;
|
|
404
|
+
const current = isWindow
|
|
405
|
+
? { x: this.contextWindow.scrollX, y: this.contextWindow.scrollY }
|
|
406
|
+
: {
|
|
407
|
+
x: target.scrollLeft,
|
|
408
|
+
y: target.scrollTop
|
|
409
|
+
};
|
|
410
|
+
const delta = { x: current.x - initial.x, y: current.y - initial.y };
|
|
411
|
+
if (delta.x === 0 && delta.y === 0)
|
|
412
|
+
return;
|
|
413
|
+
if (isWindow) {
|
|
414
|
+
if (this.lastMovePoint) {
|
|
415
|
+
this.lastMovePoint.x += delta.x;
|
|
416
|
+
this.lastMovePoint.y += delta.y;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else if (this.history.length > 0) {
|
|
420
|
+
this.history[0].x -= delta.x;
|
|
421
|
+
this.history[0].y -= delta.y;
|
|
422
|
+
}
|
|
423
|
+
this.scrollPositions.set(target, current);
|
|
424
|
+
frame.update(this.updatePoint, true);
|
|
425
|
+
}
|
|
426
|
+
}
|