@anyknown/axis 0.1.0 → 0.2.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/README.md +1 -0
- package/dist/axis/constants.d.ts +12 -0
- package/dist/axis/constants.js +27 -0
- package/dist/axis/create-axis.d.ts +10 -0
- package/dist/axis/create-axis.js +212 -0
- package/dist/axis/create-drag-client.d.ts +6 -0
- package/dist/axis/create-drag-client.js +95 -0
- package/dist/axis/create-geometry.d.ts +2 -0
- package/dist/axis/create-geometry.js +11 -0
- package/dist/axis/create-hold.d.ts +2 -0
- package/dist/axis/create-hold.js +38 -0
- package/dist/axis/create-scroll-driver.d.ts +2 -0
- package/dist/axis/create-scroll-driver.js +139 -0
- package/dist/axis/create-snap-suspension.d.ts +2 -0
- package/dist/axis/create-snap-suspension.js +23 -0
- package/dist/axis/create-style-overrides.d.ts +2 -0
- package/dist/axis/create-style-overrides.js +35 -0
- package/dist/axis/create-wheel.d.ts +5 -0
- package/dist/axis/create-wheel.js +57 -0
- package/dist/axis/drags-along.d.ts +2 -0
- package/dist/axis/drags-along.js +1 -0
- package/dist/axis/index.d.ts +2 -0
- package/dist/axis/index.js +1 -0
- package/dist/axis/merge-options.d.ts +2 -0
- package/dist/axis/merge-options.js +9 -0
- package/dist/axis/next-axis-state.d.ts +2 -0
- package/dist/axis/next-axis-state.js +14 -0
- package/dist/axis/resolve-axis.d.ts +2 -0
- package/dist/axis/resolve-axis.js +8 -0
- package/dist/{axis.d.ts → axis/types.d.ts} +57 -9
- package/dist/axis/types.js +1 -0
- package/dist/coordinator/constants.d.ts +5 -0
- package/dist/coordinator/constants.js +5 -0
- package/dist/coordinator/create-coordinator.d.ts +2 -0
- package/dist/coordinator/create-coordinator.js +191 -0
- package/dist/coordinator/index.d.ts +2 -0
- package/dist/coordinator/index.js +1 -0
- package/dist/coordinator/pick-owner.d.ts +3 -0
- package/dist/coordinator/pick-owner.js +7 -0
- package/dist/coordinator/register.d.ts +2 -0
- package/dist/coordinator/register.js +11 -0
- package/dist/coordinator/types.d.ts +24 -0
- package/dist/coordinator/types.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/react/constants.d.ts +3 -0
- package/dist/react/constants.js +8 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +1 -0
- package/dist/react/types.d.ts +12 -0
- package/dist/react/types.js +1 -0
- package/dist/react/use-applied-options.d.ts +3 -0
- package/dist/react/use-applied-options.js +11 -0
- package/dist/react/use-axis-instance.d.ts +3 -0
- package/dist/react/use-axis-instance.js +19 -0
- package/dist/react/use-axis.d.ts +11 -0
- package/dist/react/use-axis.js +14 -0
- package/dist/react/use-isomorphic-layout-effect.d.ts +2 -0
- package/dist/react/use-isomorphic-layout-effect.js +2 -0
- package/dist/react/use-selected-state.d.ts +4 -0
- package/dist/react/use-selected-state.js +14 -0
- package/dist/react/use-stable-api.d.ts +4 -0
- package/dist/react/use-stable-api.js +7 -0
- package/dist/utils/adjacent-snap.d.ts +1 -0
- package/dist/utils/adjacent-snap.js +8 -0
- package/dist/utils/cap-velocity.d.ts +2 -0
- package/dist/utils/cap-velocity.js +6 -0
- package/dist/utils/clamp.d.ts +1 -0
- package/dist/utils/clamp.js +1 -0
- package/dist/utils/composed-path-matches.d.ts +1 -0
- package/dist/utils/composed-path-matches.js +9 -0
- package/dist/utils/constants.d.ts +9 -0
- package/dist/utils/constants.js +19 -0
- package/dist/utils/css-length-to-px.d.ts +1 -0
- package/dist/utils/css-length-to-px.js +6 -0
- package/dist/utils/dominant-axis.d.ts +2 -0
- package/dist/utils/dominant-axis.js +3 -0
- package/dist/utils/ease-out-cubic.d.ts +1 -0
- package/dist/utils/ease-out-cubic.js +1 -0
- package/dist/utils/glide-duration.d.ts +1 -0
- package/dist/utils/glide-duration.js +9 -0
- package/dist/utils/index.d.ts +28 -0
- package/dist/utils/index.js +27 -0
- package/dist/utils/is-inside-editable.d.ts +1 -0
- package/dist/utils/is-inside-editable.js +2 -0
- package/dist/utils/is-on-scrollbar.d.ts +1 -0
- package/dist/utils/is-on-scrollbar.js +6 -0
- package/dist/utils/is-rtl.d.ts +1 -0
- package/dist/utils/is-rtl.js +1 -0
- package/dist/utils/is-scroll-key.d.ts +1 -0
- package/dist/utils/is-scroll-key.js +7 -0
- package/dist/utils/max-scroll.d.ts +2 -0
- package/dist/utils/max-scroll.js +4 -0
- package/dist/utils/measure-snaps.d.ts +1 -0
- package/dist/utils/measure-snaps.js +31 -0
- package/dist/utils/momentum-distance.d.ts +1 -0
- package/dist/utils/momentum-distance.js +4 -0
- package/dist/utils/momentum-step.d.ts +4 -0
- package/dist/utils/momentum-step.js +4 -0
- package/dist/utils/nearest-snap.d.ts +1 -0
- package/dist/utils/nearest-snap.js +12 -0
- package/dist/utils/parse-snap-align.d.ts +5 -0
- package/dist/utils/parse-snap-align.js +5 -0
- package/dist/utils/parse-snap-type.d.ts +4 -0
- package/dist/utils/parse-snap-type.js +7 -0
- package/dist/utils/pointer-sample.d.ts +2 -0
- package/dist/utils/pointer-sample.js +5 -0
- package/dist/utils/prefers-reduced-motion.d.ts +1 -0
- package/dist/utils/prefers-reduced-motion.js +1 -0
- package/dist/utils/read-scroll.d.ts +2 -0
- package/dist/utils/read-scroll.js +6 -0
- package/dist/utils/snap-items.d.ts +2 -0
- package/dist/utils/snap-items.js +18 -0
- package/dist/utils/snap-positions.d.ts +2 -0
- package/dist/utils/snap-positions.js +28 -0
- package/dist/utils/supported-user-select.d.ts +1 -0
- package/dist/utils/supported-user-select.js +1 -0
- package/dist/utils/to-logical.d.ts +1 -0
- package/dist/utils/to-logical.js +3 -0
- package/dist/utils/to-physical.d.ts +1 -0
- package/dist/utils/to-physical.js +3 -0
- package/dist/utils/try-capture-pointer.d.ts +1 -0
- package/dist/utils/try-capture-pointer.js +6 -0
- package/dist/utils/try-release-pointer.d.ts +1 -0
- package/dist/utils/try-release-pointer.js +7 -0
- package/dist/utils/types.d.ts +22 -0
- package/dist/utils/types.js +1 -0
- package/dist/utils/velocity-from-samples.d.ts +2 -0
- package/dist/utils/velocity-from-samples.js +21 -0
- package/dist/utils/wheel-delta-y-px.d.ts +1 -0
- package/dist/utils/wheel-delta-y-px.js +5 -0
- package/dist/utils/write-scroll.d.ts +2 -0
- package/dist/utils/write-scroll.js +7 -0
- package/package.json +6 -5
- package/dist/axis.js +0 -655
- package/dist/coordinator.d.ts +0 -36
- package/dist/coordinator.js +0 -247
- package/dist/math.d.ts +0 -90
- package/dist/math.js +0 -139
- package/dist/react.d.ts +0 -22
- package/dist/react.js +0 -49
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { adjacentSnap, clamp, nearestSnap, wheelDeltaYPx } from "../utils/index.js";
|
|
2
|
+
import { EDGE, ZERO } from "./constants.js";
|
|
3
|
+
export function createWheel(core, snap, driver) {
|
|
4
|
+
const { el, rtl, scrollRange, position, writePosition, options, motion, setMotion, isDestroyed } = core;
|
|
5
|
+
let direction = 1;
|
|
6
|
+
let attached = false;
|
|
7
|
+
const settleOnNextSnapInWheelDirection = () => {
|
|
8
|
+
if (isDestroyed())
|
|
9
|
+
return;
|
|
10
|
+
if (!snap.snapAxes().x) {
|
|
11
|
+
driver.settle();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const current = position().x;
|
|
15
|
+
const positions = core.snapPoints().x;
|
|
16
|
+
const target = adjacentSnap(positions, current - 2 * direction, direction) ?? nearestSnap(positions, current);
|
|
17
|
+
driver.glideTo({ x: target }, ZERO, "glide");
|
|
18
|
+
};
|
|
19
|
+
const onWheel = (event) => {
|
|
20
|
+
if (options().axis !== "x" || event.ctrlKey || event.shiftKey)
|
|
21
|
+
return;
|
|
22
|
+
if (Math.abs(event.deltaY) <= Math.abs(event.deltaX))
|
|
23
|
+
return;
|
|
24
|
+
if (motion() === "pressed" || motion() === "dragging")
|
|
25
|
+
return;
|
|
26
|
+
const delta = wheelDeltaYPx(event, el.clientWidth);
|
|
27
|
+
const isRtlNow = rtl();
|
|
28
|
+
const max = scrollRange();
|
|
29
|
+
const current = position(isRtlNow, max).x;
|
|
30
|
+
const leavesPageScrollingAtEdge = delta > 0 ? current >= max.x - EDGE : current <= EDGE;
|
|
31
|
+
if (leavesPageScrollingAtEdge)
|
|
32
|
+
return;
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
driver.stop();
|
|
35
|
+
snap.suspend();
|
|
36
|
+
direction = delta > 0 ? 1 : -1;
|
|
37
|
+
writePosition({ x: clamp(current + delta, 0, max.x) }, isRtlNow);
|
|
38
|
+
if (motion() !== "wheel")
|
|
39
|
+
setMotion("wheel");
|
|
40
|
+
if (isDestroyed())
|
|
41
|
+
return;
|
|
42
|
+
driver.afterWheelPause(settleOnNextSnapInWheelDirection);
|
|
43
|
+
};
|
|
44
|
+
const sync = () => {
|
|
45
|
+
const { wheel, axis } = options();
|
|
46
|
+
const wanted = wheel && axis === "x";
|
|
47
|
+
if (wanted === attached)
|
|
48
|
+
return;
|
|
49
|
+
attached = wanted;
|
|
50
|
+
if (wanted)
|
|
51
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
52
|
+
else
|
|
53
|
+
el.removeEventListener("wheel", onWheel);
|
|
54
|
+
};
|
|
55
|
+
const remove = () => el.removeEventListener("wheel", onWheel);
|
|
56
|
+
return { sync, remove };
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const dragsAlong = (mode, axis) => mode === "both" || mode === axis;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createAxis } from "./create-axis.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EDGE } from "./constants.js";
|
|
2
|
+
export function nextAxisState(scroll, max, previous) {
|
|
3
|
+
const next = {
|
|
4
|
+
scroll,
|
|
5
|
+
maxScroll: max,
|
|
6
|
+
progress: max > 0 ? scroll / max : 0,
|
|
7
|
+
canScrollPrev: scroll > EDGE,
|
|
8
|
+
canScrollNext: scroll < max - EDGE,
|
|
9
|
+
};
|
|
10
|
+
for (const key of Object.keys(next))
|
|
11
|
+
if (next[key] !== previous[key])
|
|
12
|
+
return next;
|
|
13
|
+
return previous;
|
|
14
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Vector } from "../utils/index.js";
|
|
1
2
|
/** The direction(s) a mouse drag scrolls: horizontal, vertical, or free 2D panning. */
|
|
2
3
|
export type AxisMode = "x" | "y" | "both";
|
|
3
4
|
/** A single scroll axis. */
|
|
@@ -15,6 +16,12 @@ export interface AxisOptions {
|
|
|
15
16
|
* @default true
|
|
16
17
|
*/
|
|
17
18
|
momentum?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The farthest a momentum glide may travel after one release, in px. A faster fling is slowed down to land
|
|
21
|
+
* within it, so one gesture never moves more than this.
|
|
22
|
+
* @default Infinity
|
|
23
|
+
*/
|
|
24
|
+
maxGlide?: number;
|
|
18
25
|
/**
|
|
19
26
|
* Translate vertical mouse-wheel scrolling into horizontal scrolling. Only used with `axis: "x"`; at either
|
|
20
27
|
* end the wheel is left alone, so the page keeps scrolling.
|
|
@@ -87,12 +94,53 @@ export interface AxisInstance {
|
|
|
87
94
|
/** Removes every listener and observer and restores every inline style that axis changed. */
|
|
88
95
|
destroy(): void;
|
|
89
96
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
export
|
|
97
|
+
export type Motion = "idle" | "pressed" | "dragging" | "momentum" | "glide" | "wheel";
|
|
98
|
+
export type Glide = "momentum" | "glide";
|
|
99
|
+
export interface Geometry {
|
|
100
|
+
rtl(): boolean;
|
|
101
|
+
scrollRange(): Vector;
|
|
102
|
+
position(rtl?: boolean, range?: Vector): Vector;
|
|
103
|
+
writePosition(position: AxisPosition, rtl?: boolean): void;
|
|
104
|
+
}
|
|
105
|
+
export interface AxisCore extends Geometry {
|
|
106
|
+
el: HTMLElement;
|
|
107
|
+
win: Window;
|
|
108
|
+
options(): Required<AxisOptions>;
|
|
109
|
+
motion(): Motion;
|
|
110
|
+
setMotion(next: Motion): void;
|
|
111
|
+
isDestroyed(): boolean;
|
|
112
|
+
snapPoints(): Record<AxisName, number[]>;
|
|
113
|
+
}
|
|
114
|
+
export interface StyleOverrides {
|
|
115
|
+
override(prop: string, value: string, important?: boolean): void;
|
|
116
|
+
restore(prop: string): void;
|
|
117
|
+
restoreAll(): void;
|
|
118
|
+
}
|
|
119
|
+
export interface SnapSuspension {
|
|
120
|
+
suspend(): void;
|
|
121
|
+
resume(): void;
|
|
122
|
+
snapAxes(): {
|
|
123
|
+
x: boolean;
|
|
124
|
+
y: boolean;
|
|
125
|
+
};
|
|
126
|
+
isSuspended(): boolean;
|
|
127
|
+
}
|
|
128
|
+
export interface Hold {
|
|
129
|
+
holdCurrent(): void;
|
|
130
|
+
release(): void;
|
|
131
|
+
reset(): void;
|
|
132
|
+
yieldDrag(glideHoldsAtItsEnd: boolean): void;
|
|
133
|
+
holdIfYielded(): void;
|
|
134
|
+
follow(axis: AxisName): void;
|
|
135
|
+
undoAutoscroll(): void;
|
|
136
|
+
}
|
|
137
|
+
export interface ScrollDriver {
|
|
138
|
+
stop(): void;
|
|
139
|
+
settle(): void;
|
|
140
|
+
interrupt(): void;
|
|
141
|
+
jump(target: AxisPosition): void;
|
|
142
|
+
glideTo(target: AxisPosition, velocity: Vector, kind: Glide): void;
|
|
143
|
+
fling(scrollVelocity: Vector): void;
|
|
144
|
+
afterWheelPause(settle: () => void): void;
|
|
145
|
+
glideTarget(): Vector;
|
|
146
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Coordinator } from "./types.js";
|
|
2
|
+
export declare const DRAG_THRESHOLD = 5;
|
|
3
|
+
export declare const CLICK_SUPPRESS_MS = 100;
|
|
4
|
+
export declare const NO_DRAG = "input, textarea, select, [contenteditable]:not([contenteditable='false']), [data-axis-no-drag]";
|
|
5
|
+
export declare const coordinators: WeakMap<Window, Coordinator>;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { composedPathMatches, isScrollKey, pointerSample, VELOCITY_WINDOW, velocityFromSamples, } from "../utils/index.js";
|
|
2
|
+
import { CLICK_SUPPRESS_MS, coordinators, DRAG_THRESHOLD, NO_DRAG } from "./constants.js";
|
|
3
|
+
import { pickOwner } from "./pick-owner.js";
|
|
4
|
+
export function createCoordinator(win) {
|
|
5
|
+
const clients = new Map();
|
|
6
|
+
let session = null;
|
|
7
|
+
let suppressClick = false;
|
|
8
|
+
let suppressTimer = 0;
|
|
9
|
+
const capture = { capture: true };
|
|
10
|
+
const passiveCapture = { capture: true, passive: true };
|
|
11
|
+
const stillRegistered = (client) => clients.get(client.el) === client;
|
|
12
|
+
const forgetSuppressedClick = () => {
|
|
13
|
+
suppressClick = false;
|
|
14
|
+
win.clearTimeout(suppressTimer);
|
|
15
|
+
};
|
|
16
|
+
const detach = () => {
|
|
17
|
+
win.removeEventListener("pointermove", onPointerMove);
|
|
18
|
+
win.removeEventListener("pointerup", onPointerUp);
|
|
19
|
+
win.removeEventListener("pointercancel", onPointerCancel);
|
|
20
|
+
win.removeEventListener("wheel", releaseHoldsOnUserScroll, passiveCapture);
|
|
21
|
+
win.removeEventListener("keydown", releaseHoldsOnUserScroll, capture);
|
|
22
|
+
win.removeEventListener("contextmenu", cancelPress, capture);
|
|
23
|
+
win.removeEventListener("blur", cancelPress);
|
|
24
|
+
win.document.removeEventListener("visibilitychange", cancelPressWhenHidden);
|
|
25
|
+
session = null;
|
|
26
|
+
};
|
|
27
|
+
const endSession = (ended, pointerVelocity) => {
|
|
28
|
+
detach();
|
|
29
|
+
if (ended.owner && stillRegistered(ended.owner))
|
|
30
|
+
ended.owner.end(pointerVelocity);
|
|
31
|
+
for (const client of ended.chain) {
|
|
32
|
+
if (client !== ended.owner && stillRegistered(client))
|
|
33
|
+
client.pass();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const cancelPress = () => {
|
|
37
|
+
if (session)
|
|
38
|
+
endSession(session, null);
|
|
39
|
+
};
|
|
40
|
+
const cancelPressWhenHidden = () => {
|
|
41
|
+
if (win.document.visibilityState === "hidden")
|
|
42
|
+
cancelPress();
|
|
43
|
+
};
|
|
44
|
+
const releaseHoldsOnUserScroll = (event) => {
|
|
45
|
+
if (event.type === "keydown" && !isScrollKey(event))
|
|
46
|
+
return;
|
|
47
|
+
if (session)
|
|
48
|
+
for (const client of session.chain)
|
|
49
|
+
client.interrupt();
|
|
50
|
+
};
|
|
51
|
+
const attach = () => {
|
|
52
|
+
win.addEventListener("pointermove", onPointerMove);
|
|
53
|
+
win.addEventListener("pointerup", onPointerUp);
|
|
54
|
+
win.addEventListener("pointercancel", onPointerCancel);
|
|
55
|
+
win.addEventListener("wheel", releaseHoldsOnUserScroll, passiveCapture);
|
|
56
|
+
win.addEventListener("keydown", releaseHoldsOnUserScroll, capture);
|
|
57
|
+
win.addEventListener("contextmenu", cancelPress, capture);
|
|
58
|
+
win.addEventListener("blur", cancelPress);
|
|
59
|
+
win.document.addEventListener("visibilitychange", cancelPressWhenHidden);
|
|
60
|
+
};
|
|
61
|
+
const onPointerDown = (event) => {
|
|
62
|
+
forgetSuppressedClick();
|
|
63
|
+
if (session)
|
|
64
|
+
return;
|
|
65
|
+
const path = event.composedPath();
|
|
66
|
+
const chain = [];
|
|
67
|
+
for (const node of path) {
|
|
68
|
+
const client = clients.get(node);
|
|
69
|
+
if (client)
|
|
70
|
+
chain.push(client);
|
|
71
|
+
}
|
|
72
|
+
if (chain.length === 0)
|
|
73
|
+
return;
|
|
74
|
+
const draggable = event.pointerType === "mouse" &&
|
|
75
|
+
event.button === 0 &&
|
|
76
|
+
!composedPathMatches(path, NO_DRAG, chain.at(-1).el) &&
|
|
77
|
+
!chain.some((client) => client.onScrollbar(event));
|
|
78
|
+
if (!draggable) {
|
|
79
|
+
for (const client of chain)
|
|
80
|
+
client.interrupt();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const client of chain)
|
|
84
|
+
client.press();
|
|
85
|
+
const pressed = chain.filter(stillRegistered);
|
|
86
|
+
if (pressed.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
session = {
|
|
89
|
+
pointerId: event.pointerId,
|
|
90
|
+
start: { x: event.clientX, y: event.clientY },
|
|
91
|
+
chain: pressed,
|
|
92
|
+
owner: null,
|
|
93
|
+
samples: [pointerSample(event)],
|
|
94
|
+
};
|
|
95
|
+
attach();
|
|
96
|
+
};
|
|
97
|
+
const handOver = (current, delta) => {
|
|
98
|
+
const owner = pickOwner(current.chain, delta);
|
|
99
|
+
if (!owner) {
|
|
100
|
+
endSession(current, null);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
current.owner = owner;
|
|
104
|
+
for (const client of current.chain)
|
|
105
|
+
if (client !== owner)
|
|
106
|
+
client.yield();
|
|
107
|
+
owner.start(current.pointerId);
|
|
108
|
+
};
|
|
109
|
+
const onPointerMove = (event) => {
|
|
110
|
+
const current = session;
|
|
111
|
+
if (!current || event.pointerId !== current.pointerId)
|
|
112
|
+
return;
|
|
113
|
+
const releasedOutsideWindow = !(event.buttons & 1);
|
|
114
|
+
if (releasedOutsideWindow) {
|
|
115
|
+
cancelPress();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const { samples } = current;
|
|
119
|
+
samples.push(pointerSample(event));
|
|
120
|
+
while (samples.length > 2 && event.timeStamp - samples[0].t > VELOCITY_WINDOW * 2)
|
|
121
|
+
samples.shift();
|
|
122
|
+
const delta = { x: event.clientX - current.start.x, y: event.clientY - current.start.y };
|
|
123
|
+
if (!current.owner) {
|
|
124
|
+
if (Math.hypot(delta.x, delta.y) <= DRAG_THRESHOLD)
|
|
125
|
+
return;
|
|
126
|
+
handOver(current, delta);
|
|
127
|
+
}
|
|
128
|
+
const endedByListener = session !== current;
|
|
129
|
+
if (endedByListener || !current.owner)
|
|
130
|
+
return;
|
|
131
|
+
current.owner.move(delta);
|
|
132
|
+
};
|
|
133
|
+
const onPointerUp = (event) => {
|
|
134
|
+
const current = session;
|
|
135
|
+
if (!current || event.pointerId !== current.pointerId)
|
|
136
|
+
return;
|
|
137
|
+
current.samples.push(pointerSample(event));
|
|
138
|
+
if (current.owner) {
|
|
139
|
+
suppressClick = true;
|
|
140
|
+
win.clearTimeout(suppressTimer);
|
|
141
|
+
suppressTimer = win.setTimeout(() => (suppressClick = false), CLICK_SUPPRESS_MS);
|
|
142
|
+
}
|
|
143
|
+
endSession(current, current.owner ? velocityFromSamples(current.samples) : null);
|
|
144
|
+
};
|
|
145
|
+
const onPointerCancel = (event) => {
|
|
146
|
+
if (session && event.pointerId === session.pointerId)
|
|
147
|
+
cancelPress();
|
|
148
|
+
};
|
|
149
|
+
const swallowClickAfterDrag = (event) => {
|
|
150
|
+
if (!suppressClick)
|
|
151
|
+
return;
|
|
152
|
+
forgetSuppressedClick();
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
event.stopPropagation();
|
|
155
|
+
};
|
|
156
|
+
const preventNativeDragAndDrop = (event) => {
|
|
157
|
+
if (session)
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
};
|
|
160
|
+
const remove = (client) => {
|
|
161
|
+
if (clients.get(client.el) !== client)
|
|
162
|
+
return;
|
|
163
|
+
clients.delete(client.el);
|
|
164
|
+
if (session) {
|
|
165
|
+
const current = session;
|
|
166
|
+
current.chain = current.chain.filter((other) => other !== client);
|
|
167
|
+
if (current.owner === client || current.chain.length === 0) {
|
|
168
|
+
current.owner = null;
|
|
169
|
+
endSession(current, null);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (clients.size === 0) {
|
|
173
|
+
win.removeEventListener("pointerdown", onPointerDown, capture);
|
|
174
|
+
win.removeEventListener("click", swallowClickAfterDrag, capture);
|
|
175
|
+
win.removeEventListener("dragstart", preventNativeDragAndDrop, capture);
|
|
176
|
+
forgetSuppressedClick();
|
|
177
|
+
coordinators.delete(win);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
add(client) {
|
|
182
|
+
if (clients.size === 0) {
|
|
183
|
+
win.addEventListener("pointerdown", onPointerDown, capture);
|
|
184
|
+
win.addEventListener("click", swallowClickAfterDrag, capture);
|
|
185
|
+
win.addEventListener("dragstart", preventNativeDragAndDrop, capture);
|
|
186
|
+
}
|
|
187
|
+
clients.set(client.el, client);
|
|
188
|
+
return () => remove(client);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { register } from "./register.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { dominantAxis } from "../utils/index.js";
|
|
2
|
+
export function pickOwner(innermostFirst, delta) {
|
|
3
|
+
const axis = dominantAxis(delta);
|
|
4
|
+
const dragsAlongAxis = innermostFirst.filter((client) => client.accepts(axis));
|
|
5
|
+
const canStillScroll = dragsAlongAxis.find((client) => client.canScroll(delta));
|
|
6
|
+
return canStillScroll ?? dragsAlongAxis[0] ?? null;
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { coordinators } from "./constants.js";
|
|
2
|
+
import { createCoordinator } from "./create-coordinator.js";
|
|
3
|
+
export function register(client) {
|
|
4
|
+
const win = client.el.ownerDocument.defaultView ?? window;
|
|
5
|
+
let coordinator = coordinators.get(win);
|
|
6
|
+
if (!coordinator) {
|
|
7
|
+
coordinator = createCoordinator(win);
|
|
8
|
+
coordinators.set(win, coordinator);
|
|
9
|
+
}
|
|
10
|
+
return coordinator.add(client);
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Sample, Vector } from "../utils/index.js";
|
|
2
|
+
export interface DragClient {
|
|
3
|
+
readonly el: HTMLElement;
|
|
4
|
+
onScrollbar(event: PointerEvent): boolean;
|
|
5
|
+
accepts(axis: "x" | "y"): boolean;
|
|
6
|
+
canScroll(delta: Vector): boolean;
|
|
7
|
+
press(): void;
|
|
8
|
+
yield(): void;
|
|
9
|
+
start(pointerId: number): void;
|
|
10
|
+
move(delta: Vector): void;
|
|
11
|
+
pass(): void;
|
|
12
|
+
end(pointerVelocity: Vector | null): void;
|
|
13
|
+
interrupt(): void;
|
|
14
|
+
}
|
|
15
|
+
export interface Session {
|
|
16
|
+
pointerId: number;
|
|
17
|
+
start: Vector;
|
|
18
|
+
chain: DragClient[];
|
|
19
|
+
owner: DragClient | null;
|
|
20
|
+
samples: Sample[];
|
|
21
|
+
}
|
|
22
|
+
export interface Coordinator {
|
|
23
|
+
add(client: DragClient): () => void;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createAxis } from "./axis.js";
|
|
2
|
-
export type { AxisInstance, AxisMode, AxisName, AxisOptions, AxisPosition, AxisScrollState, AxisScrollToOptions, AxisState, } from "./axis.js";
|
|
1
|
+
export { createAxis } from "./axis/index.js";
|
|
2
|
+
export type { AxisInstance, AxisMode, AxisName, AxisOptions, AxisPosition, AxisScrollState, AxisScrollToOptions, AxisState, } from "./axis/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createAxis } from "./axis.js";
|
|
1
|
+
export { createAxis } from "./axis/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useAxis } from "./use-axis.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AxisInstance, AxisScrollState } from "../axis/index.js";
|
|
2
|
+
/** Imperative scroll controls returned by {@link useAxis}. Safe to call before the element mounts (no-op). */
|
|
3
|
+
export type AxisApi = Pick<AxisInstance, "scrollPrev" | "scrollNext" | "scrollTo" | "refresh">;
|
|
4
|
+
/** What {@link useAxis} returns. */
|
|
5
|
+
export interface UseAxisResult<T extends HTMLElement = HTMLElement, S = AxisScrollState> {
|
|
6
|
+
/** Callback ref for the scroll container. Stable. */
|
|
7
|
+
ref: (element: T | null) => void;
|
|
8
|
+
/** The scroll state of both axes, or what the selector picked from it. Re-renders only when it changes. */
|
|
9
|
+
state: S;
|
|
10
|
+
/** Stable scroll controls. */
|
|
11
|
+
api: AxisApi;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
|
|
3
|
+
export function useAppliedOptions(options, instanceRef) {
|
|
4
|
+
const optionsRef = useRef(options);
|
|
5
|
+
const { axis, momentum, wheel, cursor } = options;
|
|
6
|
+
useIsomorphicLayoutEffect(() => {
|
|
7
|
+
optionsRef.current = { axis, momentum, wheel, cursor };
|
|
8
|
+
instanceRef.current?.setOptions(optionsRef.current);
|
|
9
|
+
}, [axis, momentum, wheel, cursor]);
|
|
10
|
+
return optionsRef;
|
|
11
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
import { type AxisInstance, type AxisOptions } from "../axis/index.js";
|
|
3
|
+
export declare function useAxisInstance(element: HTMLElement | null, optionsRef: RefObject<AxisOptions>, instanceRef: RefObject<AxisInstance | null>): AxisInstance | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { createAxis } from "../axis/index.js";
|
|
3
|
+
import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
|
|
4
|
+
export function useAxisInstance(element, optionsRef, instanceRef) {
|
|
5
|
+
const [instance, setInstance] = useState(null);
|
|
6
|
+
useIsomorphicLayoutEffect(() => {
|
|
7
|
+
if (!element)
|
|
8
|
+
return;
|
|
9
|
+
const created = createAxis(element, optionsRef.current);
|
|
10
|
+
instanceRef.current = created;
|
|
11
|
+
setInstance(created);
|
|
12
|
+
return () => {
|
|
13
|
+
created.destroy();
|
|
14
|
+
instanceRef.current = null;
|
|
15
|
+
setInstance(null);
|
|
16
|
+
};
|
|
17
|
+
}, [element]);
|
|
18
|
+
return instance;
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AxisOptions, AxisScrollState } from "../axis/index.js";
|
|
2
|
+
import type { UseAxisResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Binds axis to an element through a callback ref. The instance is created when the element mounts and
|
|
5
|
+
* destroyed when it unmounts or changes. Option changes are applied without re-creating the instance.
|
|
6
|
+
*
|
|
7
|
+
* With a `selector`, `state` is `selector(state)` and the component re-renders only when that result changes
|
|
8
|
+
* (compared with `Object.is`).
|
|
9
|
+
*/
|
|
10
|
+
export declare function useAxis<T extends HTMLElement = HTMLElement>(options?: AxisOptions): UseAxisResult<T>;
|
|
11
|
+
export declare function useAxis<S>(options: AxisOptions | undefined, selector: (state: AxisScrollState) => S): UseAxisResult<HTMLElement, S>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { useAppliedOptions } from "./use-applied-options.js";
|
|
3
|
+
import { useAxisInstance } from "./use-axis-instance.js";
|
|
4
|
+
import { useSelectedState } from "./use-selected-state.js";
|
|
5
|
+
import { useStableApi } from "./use-stable-api.js";
|
|
6
|
+
export function useAxis(options = {}, selector) {
|
|
7
|
+
const [element, setElement] = useState(null);
|
|
8
|
+
const instanceRef = useRef(null);
|
|
9
|
+
const optionsRef = useAppliedOptions(options, instanceRef);
|
|
10
|
+
const instance = useAxisInstance(element, optionsRef, instanceRef);
|
|
11
|
+
const state = useSelectedState(instance, selector);
|
|
12
|
+
const api = useStableApi(instanceRef);
|
|
13
|
+
return { ref: setElement, state, api };
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { EMPTY_STATE, NOOP } from "./constants.js";
|
|
3
|
+
export function useSelectedState(instance, selector) {
|
|
4
|
+
const subscribe = useCallback((listener) => instance?.subscribe(listener) ?? NOOP, [instance]);
|
|
5
|
+
const snapshotMemo = useRef(null);
|
|
6
|
+
const getSnapshot = useCallback(() => {
|
|
7
|
+
const current = instance?.getState() ?? EMPTY_STATE;
|
|
8
|
+
if (snapshotMemo.current?.state !== current || snapshotMemo.current.selector !== selector) {
|
|
9
|
+
snapshotMemo.current = { state: current, selector, selected: selector ? selector(current) : current };
|
|
10
|
+
}
|
|
11
|
+
return snapshotMemo.current.selected;
|
|
12
|
+
}, [instance, selector]);
|
|
13
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
14
|
+
}
|