@uinstinct/svelte-wheel-picker 0.1.13 → 0.1.15
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.
|
@@ -1,61 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
var ControllableState = /** @class */ (function () {
|
|
14
|
-
function ControllableState(opts) {
|
|
15
|
-
_ControllableState_internal.set(this, $state(undefined));
|
|
16
|
-
_ControllableState_onChange.set(this, void 0);
|
|
17
|
-
_ControllableState_isControlled.set(this, void 0);
|
|
18
|
-
// $state so that reactive consumers (selectedIndex $derived) re-evaluate when prop changes
|
|
19
|
-
_ControllableState_controlledValue.set(this, $state(undefined));
|
|
20
|
-
__classPrivateFieldSet(this, _ControllableState_isControlled, typeof opts.onChange === 'function', "f");
|
|
21
|
-
__classPrivateFieldSet(this, _ControllableState_onChange, opts.onChange, "f");
|
|
22
|
-
__classPrivateFieldSet(this, _ControllableState_controlledValue, opts.value, "f");
|
|
23
|
-
if (__classPrivateFieldGet(this, _ControllableState_isControlled, "f")) {
|
|
24
|
-
__classPrivateFieldSet(this, _ControllableState_internal, opts.value, "f");
|
|
1
|
+
class ControllableState {
|
|
2
|
+
#internal = $state(undefined);
|
|
3
|
+
#onChange;
|
|
4
|
+
#isControlled;
|
|
5
|
+
// $state so that reactive consumers (selectedIndex $derived) re-evaluate when prop changes
|
|
6
|
+
#controlledValue = $state(undefined);
|
|
7
|
+
constructor(opts) {
|
|
8
|
+
this.#isControlled = typeof opts.onChange === 'function';
|
|
9
|
+
this.#onChange = opts.onChange;
|
|
10
|
+
this.#controlledValue = opts.value;
|
|
11
|
+
if (this.#isControlled) {
|
|
12
|
+
this.#internal = opts.value;
|
|
25
13
|
}
|
|
26
14
|
else {
|
|
27
|
-
|
|
15
|
+
this.#internal = opts.defaultValue;
|
|
28
16
|
}
|
|
29
17
|
}
|
|
30
18
|
/**
|
|
31
19
|
* Update the tracked controlled value when the external `value` prop changes.
|
|
32
20
|
* Must be called from a $effect in the consuming component whenever `value` changes.
|
|
33
21
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
53
|
-
enumerable: false,
|
|
54
|
-
configurable: true
|
|
55
|
-
});
|
|
56
|
-
return ControllableState;
|
|
57
|
-
}());
|
|
58
|
-
_ControllableState_internal = new WeakMap(), _ControllableState_onChange = new WeakMap(), _ControllableState_isControlled = new WeakMap(), _ControllableState_controlledValue = new WeakMap();
|
|
22
|
+
updateControlledValue(value) {
|
|
23
|
+
this.#controlledValue = value;
|
|
24
|
+
}
|
|
25
|
+
get current() {
|
|
26
|
+
if (this.#isControlled) {
|
|
27
|
+
return this.#controlledValue;
|
|
28
|
+
}
|
|
29
|
+
return this.#internal;
|
|
30
|
+
}
|
|
31
|
+
set current(next) {
|
|
32
|
+
if (this.#isControlled) {
|
|
33
|
+
this.#onChange?.(next);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.#internal = next;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
59
40
|
export function useControllableState(opts) {
|
|
60
41
|
return new ControllableState(opts);
|
|
61
42
|
}
|
|
@@ -1,77 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11
|
-
};
|
|
12
|
-
var _TypeaheadSearch_instances, _TypeaheadSearch_buffer, _TypeaheadSearch_lastKey, _TypeaheadSearch_lastTime, _TypeaheadSearch_timer, _TypeaheadSearch_getSearchText, _TypeaheadSearch_findFirst, _TypeaheadSearch_cycleMatch;
|
|
13
|
-
var TypeaheadSearch = /** @class */ (function () {
|
|
14
|
-
function TypeaheadSearch() {
|
|
15
|
-
_TypeaheadSearch_instances.add(this);
|
|
16
|
-
_TypeaheadSearch_buffer.set(this, $state(''));
|
|
17
|
-
_TypeaheadSearch_lastKey.set(this, $state(''));
|
|
18
|
-
_TypeaheadSearch_lastTime.set(this, 0);
|
|
19
|
-
_TypeaheadSearch_timer.set(this, null);
|
|
20
|
-
}
|
|
21
|
-
TypeaheadSearch.prototype.search = function (key, options, currentIndex) {
|
|
22
|
-
var _this = this;
|
|
23
|
-
var now = Date.now();
|
|
24
|
-
var withinWindow = now - __classPrivateFieldGet(this, _TypeaheadSearch_lastTime, "f") < 500;
|
|
25
|
-
var singleChar = key.length === 1;
|
|
1
|
+
class TypeaheadSearch {
|
|
2
|
+
#buffer = $state('');
|
|
3
|
+
#lastKey = $state('');
|
|
4
|
+
#lastTime = 0;
|
|
5
|
+
#timer = null;
|
|
6
|
+
search(key, options, currentIndex) {
|
|
7
|
+
const now = Date.now();
|
|
8
|
+
const withinWindow = now - this.#lastTime < 500;
|
|
9
|
+
const singleChar = key.length === 1;
|
|
26
10
|
if (!singleChar)
|
|
27
11
|
return -1;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
12
|
+
const lowerKey = key.toLowerCase();
|
|
13
|
+
const isSameKey = withinWindow && lowerKey === this.#lastKey;
|
|
14
|
+
this.#lastKey = lowerKey;
|
|
15
|
+
this.#lastTime = now;
|
|
32
16
|
// Reset timer
|
|
33
|
-
if (
|
|
34
|
-
clearTimeout(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}, 500)
|
|
17
|
+
if (this.#timer)
|
|
18
|
+
clearTimeout(this.#timer);
|
|
19
|
+
this.#timer = setTimeout(() => {
|
|
20
|
+
this.#buffer = '';
|
|
21
|
+
this.#lastKey = '';
|
|
22
|
+
}, 500);
|
|
39
23
|
if (isSameKey) {
|
|
40
24
|
// Same key cycling (D-02): find next match after currentIndex
|
|
41
|
-
return
|
|
25
|
+
return this.#cycleMatch(lowerKey, options, currentIndex);
|
|
42
26
|
}
|
|
43
27
|
else {
|
|
44
28
|
// Accumulate or start fresh
|
|
45
|
-
|
|
46
|
-
return
|
|
29
|
+
this.#buffer = withinWindow ? this.#buffer + lowerKey : lowerKey;
|
|
30
|
+
return this.#findFirst(this.#buffer, options);
|
|
47
31
|
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
32
|
+
}
|
|
33
|
+
#getSearchText(option) {
|
|
34
|
+
return (option.textValue ?? option.label).toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
#findFirst(prefix, options) {
|
|
37
|
+
return options.findIndex((o) => !o.disabled && this.#getSearchText(o).startsWith(prefix));
|
|
38
|
+
}
|
|
39
|
+
#cycleMatch(key, options, fromIndex) {
|
|
40
|
+
const prefix = key.toLowerCase();
|
|
41
|
+
const matches = options
|
|
42
|
+
.map((o, i) => ({ i, matches: !o.disabled && this.#getSearchText(o).startsWith(prefix) }))
|
|
43
|
+
.filter((x) => x.matches)
|
|
44
|
+
.map((x) => x.i);
|
|
45
|
+
if (matches.length === 0)
|
|
46
|
+
return -1;
|
|
47
|
+
const afterCurrent = matches.find((i) => i > fromIndex);
|
|
48
|
+
return afterCurrent ?? matches[0]; // wrap around
|
|
49
|
+
}
|
|
50
|
+
destroy() {
|
|
51
|
+
if (this.#timer) {
|
|
52
|
+
clearTimeout(this.#timer);
|
|
53
|
+
this.#timer = null;
|
|
53
54
|
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
}());
|
|
57
|
-
_TypeaheadSearch_buffer = new WeakMap(), _TypeaheadSearch_lastKey = new WeakMap(), _TypeaheadSearch_lastTime = new WeakMap(), _TypeaheadSearch_timer = new WeakMap(), _TypeaheadSearch_instances = new WeakSet(), _TypeaheadSearch_getSearchText = function _TypeaheadSearch_getSearchText(option) {
|
|
58
|
-
var _a;
|
|
59
|
-
return ((_a = option.textValue) !== null && _a !== void 0 ? _a : option.label).toLowerCase();
|
|
60
|
-
}, _TypeaheadSearch_findFirst = function _TypeaheadSearch_findFirst(prefix, options) {
|
|
61
|
-
var _this = this;
|
|
62
|
-
return options.findIndex(function (o) { return !o.disabled && __classPrivateFieldGet(_this, _TypeaheadSearch_instances, "m", _TypeaheadSearch_getSearchText).call(_this, o).startsWith(prefix); });
|
|
63
|
-
}, _TypeaheadSearch_cycleMatch = function _TypeaheadSearch_cycleMatch(key, options, fromIndex) {
|
|
64
|
-
var _this = this;
|
|
65
|
-
var prefix = key.toLowerCase();
|
|
66
|
-
var matches = options
|
|
67
|
-
.map(function (o, i) { return ({ i: i, matches: !o.disabled && __classPrivateFieldGet(_this, _TypeaheadSearch_instances, "m", _TypeaheadSearch_getSearchText).call(_this, o).startsWith(prefix) }); })
|
|
68
|
-
.filter(function (x) { return x.matches; })
|
|
69
|
-
.map(function (x) { return x.i; });
|
|
70
|
-
if (matches.length === 0)
|
|
71
|
-
return -1;
|
|
72
|
-
var afterCurrent = matches.find(function (i) { return i > fromIndex; });
|
|
73
|
-
return afterCurrent !== null && afterCurrent !== void 0 ? afterCurrent : matches[0]; // wrap around
|
|
74
|
-
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
75
57
|
export function useTypeaheadSearch() {
|
|
76
58
|
return new TypeaheadSearch();
|
|
77
59
|
}
|
|
@@ -10,67 +10,53 @@
|
|
|
10
10
|
* - Boundary resistance uses RESISTANCE constant from React v1.2.2 source.
|
|
11
11
|
* - Disabled options are always skipped when computing snap targets.
|
|
12
12
|
*/
|
|
13
|
-
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
14
|
-
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
15
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
16
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
17
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
18
|
-
};
|
|
19
|
-
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
20
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
21
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
22
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
23
|
-
};
|
|
24
|
-
var _WheelPhysics_instances, _WheelPhysics_itemHeight, _WheelPhysics_visibleCount, _WheelPhysics_dragSensitivity, _WheelPhysics_scrollSensitivity, _WheelPhysics_options, _WheelPhysics_onSnap, _WheelPhysics_infinite, _WheelPhysics_rafId, _WheelPhysics_isDragging, _WheelPhysics_dragStartOffset, _WheelPhysics_dragStartY, _WheelPhysics_yList, _WheelPhysics_lastWheelTime, _WheelPhysics_animating, _WheelPhysics_cancelRaf, _WheelPhysics_indexToOffset, _WheelPhysics_offsetToIndex;
|
|
25
13
|
import { easeOutCubic, indexToOffset, offsetToIndex, clampIndex, wrapIndex, snapToNearestEnabled, calculateVelocity, computeSnapTarget, computeAnimationDuration, RESISTANCE, DEFAULT_DRAG_SENSITIVITY, DEFAULT_SCROLL_SENSITIVITY, DEFAULT_ITEM_HEIGHT, DEFAULT_VISIBLE_COUNT, SNAP_BACK_DECELERATION, } from './wheel-physics-utils.js';
|
|
26
14
|
// Re-export for convenience so consumers only need one import
|
|
27
15
|
export { DEFAULT_DRAG_SENSITIVITY, DEFAULT_SCROLL_SENSITIVITY, DEFAULT_ITEM_HEIGHT, DEFAULT_VISIBLE_COUNT, };
|
|
28
|
-
|
|
16
|
+
export class WheelPhysics {
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Public reactive state ($state) — the ONLY field bound to the DOM transform
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
offset = $state(0);
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Private configuration (set in constructor, may be updated by update())
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
#itemHeight;
|
|
25
|
+
#visibleCount;
|
|
26
|
+
#dragSensitivity;
|
|
27
|
+
#scrollSensitivity;
|
|
28
|
+
#options;
|
|
29
|
+
#onSnap;
|
|
30
|
+
#infinite;
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Private non-reactive animation/drag state (NOT $state — Pitfall 2)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/** Current RAF handle — null when no animation running */
|
|
35
|
+
#rafId = null;
|
|
36
|
+
/** True between pointerdown and pointerup */
|
|
37
|
+
#isDragging = false;
|
|
38
|
+
/** The offset value when the current drag began */
|
|
39
|
+
#dragStartOffset = 0;
|
|
40
|
+
/** clientY at the start of the current drag (used for direct delta) */
|
|
41
|
+
#dragStartY = 0;
|
|
42
|
+
/** Recent pointer positions for velocity calculation: [clientY, timestamp][] */
|
|
43
|
+
#yList = [];
|
|
44
|
+
/** Timestamp of the last wheel event (100ms debounce guard) */
|
|
45
|
+
#lastWheelTime = -Infinity;
|
|
46
|
+
/** True while a snap or inertia animation is running */
|
|
47
|
+
#animating = false;
|
|
29
48
|
// ---------------------------------------------------------------------------
|
|
30
49
|
// Constructor
|
|
31
50
|
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
_WheelPhysics_itemHeight.set(this, void 0);
|
|
43
|
-
_WheelPhysics_visibleCount.set(this, void 0);
|
|
44
|
-
_WheelPhysics_dragSensitivity.set(this, void 0);
|
|
45
|
-
_WheelPhysics_scrollSensitivity.set(this, void 0);
|
|
46
|
-
_WheelPhysics_options.set(this, void 0);
|
|
47
|
-
_WheelPhysics_onSnap.set(this, void 0);
|
|
48
|
-
_WheelPhysics_infinite.set(this, void 0);
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// Private non-reactive animation/drag state (NOT $state — Pitfall 2)
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
/** Current RAF handle — null when no animation running */
|
|
53
|
-
_WheelPhysics_rafId.set(this, null);
|
|
54
|
-
/** True between pointerdown and pointerup */
|
|
55
|
-
_WheelPhysics_isDragging.set(this, false);
|
|
56
|
-
/** The offset value when the current drag began */
|
|
57
|
-
_WheelPhysics_dragStartOffset.set(this, 0);
|
|
58
|
-
/** clientY at the start of the current drag (used for direct delta) */
|
|
59
|
-
_WheelPhysics_dragStartY.set(this, 0);
|
|
60
|
-
/** Recent pointer positions for velocity calculation: [clientY, timestamp][] */
|
|
61
|
-
_WheelPhysics_yList.set(this, []);
|
|
62
|
-
/** Timestamp of the last wheel event (100ms debounce guard) */
|
|
63
|
-
_WheelPhysics_lastWheelTime.set(this, -Infinity);
|
|
64
|
-
/** True while a snap or inertia animation is running */
|
|
65
|
-
_WheelPhysics_animating.set(this, false);
|
|
66
|
-
__classPrivateFieldSet(this, _WheelPhysics_itemHeight, (_a = opts.itemHeight) !== null && _a !== void 0 ? _a : DEFAULT_ITEM_HEIGHT, "f");
|
|
67
|
-
__classPrivateFieldSet(this, _WheelPhysics_visibleCount, (_b = opts.visibleCount) !== null && _b !== void 0 ? _b : DEFAULT_VISIBLE_COUNT, "f");
|
|
68
|
-
__classPrivateFieldSet(this, _WheelPhysics_dragSensitivity, (_c = opts.dragSensitivity) !== null && _c !== void 0 ? _c : DEFAULT_DRAG_SENSITIVITY, "f");
|
|
69
|
-
__classPrivateFieldSet(this, _WheelPhysics_scrollSensitivity, (_d = opts.scrollSensitivity) !== null && _d !== void 0 ? _d : DEFAULT_SCROLL_SENSITIVITY, "f");
|
|
70
|
-
__classPrivateFieldSet(this, _WheelPhysics_infinite, (_e = opts.infinite) !== null && _e !== void 0 ? _e : false, "f");
|
|
71
|
-
__classPrivateFieldSet(this, _WheelPhysics_options, opts.options, "f");
|
|
72
|
-
__classPrivateFieldSet(this, _WheelPhysics_onSnap, opts.onSnap, "f");
|
|
73
|
-
this.offset = __classPrivateFieldGet(this, _WheelPhysics_instances, "m", _WheelPhysics_indexToOffset).call(this, opts.initialIndex);
|
|
51
|
+
constructor(opts) {
|
|
52
|
+
this.#itemHeight = opts.itemHeight ?? DEFAULT_ITEM_HEIGHT;
|
|
53
|
+
this.#visibleCount = opts.visibleCount ?? DEFAULT_VISIBLE_COUNT;
|
|
54
|
+
this.#dragSensitivity = opts.dragSensitivity ?? DEFAULT_DRAG_SENSITIVITY;
|
|
55
|
+
this.#scrollSensitivity = opts.scrollSensitivity ?? DEFAULT_SCROLL_SENSITIVITY;
|
|
56
|
+
this.#infinite = opts.infinite ?? false;
|
|
57
|
+
this.#options = opts.options;
|
|
58
|
+
this.#onSnap = opts.onSnap;
|
|
59
|
+
this.offset = this.#indexToOffset(opts.initialIndex);
|
|
74
60
|
}
|
|
75
61
|
// ---------------------------------------------------------------------------
|
|
76
62
|
// Configuration update (called when props change in parent component)
|
|
@@ -79,47 +65,47 @@ var WheelPhysics = /** @class */ (function () {
|
|
|
79
65
|
* Update configuration when parent props change.
|
|
80
66
|
* This does NOT re-trigger animation — it takes effect on the next interaction.
|
|
81
67
|
*/
|
|
82
|
-
|
|
68
|
+
update(opts) {
|
|
83
69
|
if (opts.itemHeight !== undefined)
|
|
84
|
-
|
|
70
|
+
this.#itemHeight = opts.itemHeight;
|
|
85
71
|
if (opts.visibleCount !== undefined)
|
|
86
|
-
|
|
72
|
+
this.#visibleCount = opts.visibleCount;
|
|
87
73
|
if (opts.dragSensitivity !== undefined)
|
|
88
|
-
|
|
74
|
+
this.#dragSensitivity = opts.dragSensitivity;
|
|
89
75
|
if (opts.scrollSensitivity !== undefined)
|
|
90
|
-
|
|
76
|
+
this.#scrollSensitivity = opts.scrollSensitivity;
|
|
91
77
|
if (opts.infinite !== undefined)
|
|
92
|
-
|
|
78
|
+
this.#infinite = opts.infinite;
|
|
93
79
|
if (opts.options !== undefined)
|
|
94
|
-
|
|
80
|
+
this.#options = opts.options;
|
|
95
81
|
if (opts.onSnap !== undefined)
|
|
96
|
-
|
|
97
|
-
}
|
|
82
|
+
this.#onSnap = opts.onSnap;
|
|
83
|
+
}
|
|
98
84
|
// ---------------------------------------------------------------------------
|
|
99
85
|
// Drag handlers (pointer events)
|
|
100
86
|
// ---------------------------------------------------------------------------
|
|
101
87
|
/**
|
|
102
88
|
* Called on pointerdown. Cancels any running animation and begins tracking.
|
|
103
89
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
90
|
+
startDrag(clientY) {
|
|
91
|
+
this.#cancelRaf();
|
|
92
|
+
this.#isDragging = true;
|
|
93
|
+
this.#animating = false;
|
|
94
|
+
this.#dragStartOffset = this.offset;
|
|
95
|
+
this.#dragStartY = clientY;
|
|
96
|
+
this.#yList = [[clientY, performance.now()]];
|
|
97
|
+
}
|
|
112
98
|
/**
|
|
113
99
|
* Called on pointermove. Updates offset applying boundary resistance at ends.
|
|
114
100
|
*/
|
|
115
|
-
|
|
116
|
-
if (!
|
|
101
|
+
moveDrag(clientY) {
|
|
102
|
+
if (!this.#isDragging)
|
|
117
103
|
return;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (
|
|
104
|
+
const delta = clientY - this.#dragStartY;
|
|
105
|
+
const maxOffset = this.#indexToOffset(0);
|
|
106
|
+
const minOffset = this.#indexToOffset(this.#options.length - 1);
|
|
107
|
+
let newOffset = this.#dragStartOffset + delta;
|
|
108
|
+
if (this.#infinite) {
|
|
123
109
|
// Infinite mode: normalize offset when the drag exceeds the ghost item bounds.
|
|
124
110
|
// The DOM has 3×N items (before-ghosts + real + after-ghosts), covering rawIndex
|
|
125
111
|
// -N..2N-1. When the pointer is captured outside the container, the user can drag
|
|
@@ -128,18 +114,18 @@ var WheelPhysics = /** @class */ (function () {
|
|
|
128
114
|
//
|
|
129
115
|
// Applying the same shift to #dragStartOffset keeps future delta computations
|
|
130
116
|
// consistent so the drag feels continuous across the normalization boundary.
|
|
131
|
-
|
|
117
|
+
const loopDistance = this.#options.length * this.#itemHeight;
|
|
132
118
|
// After-ghost overflow: newOffset went past the last after-ghost (rawIndex >= 2N)
|
|
133
|
-
|
|
119
|
+
const afterGhostEnd = this.#indexToOffset(2 * this.#options.length);
|
|
134
120
|
// Before-ghost overflow: newOffset went past the first before-ghost (rawIndex < -N)
|
|
135
|
-
|
|
121
|
+
const beforeGhostEnd = this.#indexToOffset(-this.#options.length - 1);
|
|
136
122
|
while (newOffset < afterGhostEnd) {
|
|
137
123
|
newOffset += loopDistance;
|
|
138
|
-
|
|
124
|
+
this.#dragStartOffset += loopDistance;
|
|
139
125
|
}
|
|
140
126
|
while (newOffset > beforeGhostEnd) {
|
|
141
127
|
newOffset -= loopDistance;
|
|
142
|
-
|
|
128
|
+
this.#dragStartOffset -= loopDistance;
|
|
143
129
|
}
|
|
144
130
|
}
|
|
145
131
|
else {
|
|
@@ -153,31 +139,31 @@ var WheelPhysics = /** @class */ (function () {
|
|
|
153
139
|
}
|
|
154
140
|
this.offset = newOffset;
|
|
155
141
|
// Track last 5 pointer positions for velocity calculation
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
142
|
+
this.#yList.push([clientY, performance.now()]);
|
|
143
|
+
if (this.#yList.length > 5) {
|
|
144
|
+
this.#yList.shift();
|
|
159
145
|
}
|
|
160
|
-
}
|
|
146
|
+
}
|
|
161
147
|
/**
|
|
162
148
|
* Called on pointerup. Computes velocity and kicks off inertia or direct snap.
|
|
163
149
|
*/
|
|
164
|
-
|
|
165
|
-
console.log('[endDrag] called, isDragging=',
|
|
166
|
-
if (!
|
|
150
|
+
endDrag() {
|
|
151
|
+
console.log('[endDrag] called, isDragging=', this.#isDragging, 'offset=', this.offset);
|
|
152
|
+
if (!this.#isDragging)
|
|
167
153
|
return;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
154
|
+
this.#isDragging = false;
|
|
155
|
+
const velocity = calculateVelocity(this.#yList, this.#itemHeight);
|
|
156
|
+
const rawIndex = this.#offsetToIndex(this.offset);
|
|
157
|
+
const N = this.#options.length;
|
|
158
|
+
const currentIndex = this.#infinite
|
|
173
159
|
? wrapIndex(rawIndex, N)
|
|
174
160
|
: clampIndex(rawIndex, N);
|
|
175
|
-
console.log('[endDrag] velocity=', velocity, 'rawIndex=', rawIndex, 'currentIndex=', currentIndex, 'infinite=',
|
|
161
|
+
console.log('[endDrag] velocity=', velocity, 'rawIndex=', rawIndex, 'currentIndex=', currentIndex, 'infinite=', this.#infinite);
|
|
176
162
|
if (Math.abs(velocity) < 0.5) {
|
|
177
163
|
// Slow release — snap directly to nearest enabled option
|
|
178
|
-
|
|
164
|
+
const snapIndex = snapToNearestEnabled(currentIndex, this.#options);
|
|
179
165
|
console.log('[endDrag] slow path, snapIndex=', snapIndex);
|
|
180
|
-
if (
|
|
166
|
+
if (this.#infinite) {
|
|
181
167
|
// Preserve ghost-section context so the snap animation continues in the
|
|
182
168
|
// same direction as the drag rather than jumping backward to real-section.
|
|
183
169
|
// rawIndex is in [-N, 2N-1] (guaranteed by moveDrag normalization).
|
|
@@ -185,7 +171,7 @@ var WheelPhysics = /** @class */ (function () {
|
|
|
185
171
|
// before-ghost (rawIndex < 0): animate to snapIndex - N
|
|
186
172
|
// after-ghost (rawIndex >= N): animate to snapIndex + N
|
|
187
173
|
// real section (rawIndex in [0, N-1]): animate to snapIndex directly
|
|
188
|
-
|
|
174
|
+
const loopOffset = rawIndex < 0 ? -N : rawIndex >= N ? N : 0;
|
|
189
175
|
this.animateTo(snapIndex + loopOffset);
|
|
190
176
|
}
|
|
191
177
|
else {
|
|
@@ -196,25 +182,25 @@ var WheelPhysics = /** @class */ (function () {
|
|
|
196
182
|
// Inertia — compute overshoot target
|
|
197
183
|
// Use rawIndex (not currentIndex) so the overshoot accounts for the current
|
|
198
184
|
// ghost-loop position, giving the correct item index after inertia deceleration.
|
|
199
|
-
|
|
200
|
-
if (
|
|
185
|
+
const rawTarget = computeSnapTarget(rawIndex, velocity, this.#dragSensitivity);
|
|
186
|
+
if (this.#infinite) {
|
|
201
187
|
// snapIndex is the nearest enabled item to the overshoot target (in [0, N-1])
|
|
202
|
-
|
|
203
|
-
|
|
188
|
+
const wrapped = wrapIndex(rawTarget, N);
|
|
189
|
+
const snapIndex = snapToNearestEnabled(wrapped, this.#options);
|
|
204
190
|
// Animate to the ghost-section position of snapIndex matching the current
|
|
205
191
|
// loop, so the animation moves in the same direction as the drag.
|
|
206
192
|
// snapIndex is in [0,N-1]; loopOffset is -N, 0, or +N based on current section.
|
|
207
|
-
|
|
193
|
+
const loopOffset = rawIndex < 0 ? -N : rawIndex >= N ? N : 0;
|
|
208
194
|
console.log('[endDrag] inertia path, rawTarget=', rawTarget, 'wrapped=', wrapped, 'snapIndex=', snapIndex, 'loopOffset=', loopOffset);
|
|
209
195
|
this.animateTo(snapIndex + loopOffset);
|
|
210
196
|
}
|
|
211
197
|
else {
|
|
212
|
-
|
|
213
|
-
|
|
198
|
+
const clamped = clampIndex(rawTarget, N);
|
|
199
|
+
const snapIndex = snapToNearestEnabled(clamped, this.#options);
|
|
214
200
|
this.animateTo(snapIndex);
|
|
215
201
|
}
|
|
216
202
|
}
|
|
217
|
-
}
|
|
203
|
+
}
|
|
218
204
|
// ---------------------------------------------------------------------------
|
|
219
205
|
// Wheel event handler
|
|
220
206
|
// ---------------------------------------------------------------------------
|
|
@@ -224,29 +210,29 @@ var WheelPhysics = /** @class */ (function () {
|
|
|
224
210
|
* deltaY > 0: scroll down → move to next item (higher index)
|
|
225
211
|
* deltaY < 0: scroll up → move to previous item (lower index)
|
|
226
212
|
*/
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (now -
|
|
213
|
+
handleWheel(deltaY) {
|
|
214
|
+
const now = performance.now();
|
|
215
|
+
if (now - this.#lastWheelTime < 100)
|
|
230
216
|
return;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
? wrapIndex(rawIndex,
|
|
235
|
-
: clampIndex(rawIndex,
|
|
217
|
+
this.#lastWheelTime = now;
|
|
218
|
+
const rawIndex = this.#offsetToIndex(this.offset);
|
|
219
|
+
const currentIndex = this.#infinite
|
|
220
|
+
? wrapIndex(rawIndex, this.#options.length)
|
|
221
|
+
: clampIndex(rawIndex, this.#options.length);
|
|
236
222
|
// deltaY > 0 = scroll down = move to next item (increment index)
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
223
|
+
const direction = deltaY > 0 ? 1 : -1;
|
|
224
|
+
if (this.#infinite) {
|
|
225
|
+
const next = currentIndex + direction;
|
|
226
|
+
const wrapped = wrapIndex(next, this.#options.length);
|
|
227
|
+
const snapIndex = snapToNearestEnabled(wrapped, this.#options);
|
|
242
228
|
this.animateTo(snapIndex);
|
|
243
229
|
}
|
|
244
230
|
else {
|
|
245
|
-
|
|
246
|
-
|
|
231
|
+
const targetIndex = clampIndex(currentIndex + direction, this.#options.length);
|
|
232
|
+
const snapIndex = snapToNearestEnabled(targetIndex, this.#options);
|
|
247
233
|
this.animateTo(snapIndex);
|
|
248
234
|
}
|
|
249
|
-
}
|
|
235
|
+
}
|
|
250
236
|
// ---------------------------------------------------------------------------
|
|
251
237
|
// Animation
|
|
252
238
|
// ---------------------------------------------------------------------------
|
|
@@ -256,86 +242,96 @@ var WheelPhysics = /** @class */ (function () {
|
|
|
256
242
|
*
|
|
257
243
|
* Cancels any currently running animation before starting.
|
|
258
244
|
*/
|
|
259
|
-
|
|
260
|
-
var _this = this;
|
|
245
|
+
animateTo(targetIndex) {
|
|
261
246
|
console.log('[animateTo] targetIndex=', targetIndex, 'from offset=', this.offset);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (!
|
|
247
|
+
this.#cancelRaf();
|
|
248
|
+
this.#animating = true;
|
|
249
|
+
const startOffset = this.offset;
|
|
250
|
+
const targetOffset = this.#indexToOffset(targetIndex);
|
|
251
|
+
const distance = Math.abs(targetIndex - this.#offsetToIndex(startOffset));
|
|
252
|
+
const durationSec = computeAnimationDuration(distance, this.#scrollSensitivity);
|
|
253
|
+
const durationMs = durationSec * 1000;
|
|
254
|
+
const startTime = performance.now();
|
|
255
|
+
const tick = (now) => {
|
|
256
|
+
if (!this.#animating)
|
|
272
257
|
return;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
258
|
+
const elapsed = now - startTime;
|
|
259
|
+
const progress = Math.min(elapsed / durationMs, 1);
|
|
260
|
+
const eased = easeOutCubic(progress);
|
|
261
|
+
this.offset = startOffset + (targetOffset - startOffset) * eased;
|
|
277
262
|
if (progress < 1) {
|
|
278
|
-
|
|
263
|
+
this.#rafId = requestAnimationFrame(tick);
|
|
279
264
|
}
|
|
280
265
|
else {
|
|
281
266
|
// Snap to exact target to avoid float accumulation
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
267
|
+
this.offset = targetOffset;
|
|
268
|
+
this.#rafId = null;
|
|
269
|
+
this.#animating = false;
|
|
270
|
+
this.#onSnap(targetIndex);
|
|
286
271
|
}
|
|
287
272
|
};
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
},
|
|
301
|
-
enumerable: false,
|
|
302
|
-
configurable: true
|
|
303
|
-
});
|
|
273
|
+
this.#rafId = requestAnimationFrame(tick);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Returns the option index that the current offset visually corresponds to.
|
|
277
|
+
* Used to guard against redundant animations when the wheel is already positioned correctly.
|
|
278
|
+
*/
|
|
279
|
+
get currentIndex() {
|
|
280
|
+
const raw = this.#offsetToIndex(this.offset);
|
|
281
|
+
return this.#infinite
|
|
282
|
+
? wrapIndex(raw, this.#options.length)
|
|
283
|
+
: clampIndex(raw, this.#options.length);
|
|
284
|
+
}
|
|
304
285
|
/**
|
|
305
286
|
* Immediately sets the offset to the position for the given index, no animation.
|
|
306
287
|
* Used for initial render and controlled value updates.
|
|
307
288
|
*/
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
this.offset =
|
|
311
|
-
}
|
|
289
|
+
jumpTo(index) {
|
|
290
|
+
this.#cancelRaf();
|
|
291
|
+
this.offset = this.#indexToOffset(index);
|
|
292
|
+
}
|
|
312
293
|
/**
|
|
313
294
|
* Cancels any in-progress animation.
|
|
314
295
|
*/
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
296
|
+
cancelAnimation() {
|
|
297
|
+
this.#cancelRaf();
|
|
298
|
+
}
|
|
318
299
|
/**
|
|
319
300
|
* Cleans up RAF on component destroy.
|
|
320
301
|
*/
|
|
321
|
-
|
|
302
|
+
destroy() {
|
|
322
303
|
this.cancelAnimation();
|
|
323
|
-
};
|
|
324
|
-
return WheelPhysics;
|
|
325
|
-
}());
|
|
326
|
-
export { WheelPhysics };
|
|
327
|
-
_WheelPhysics_itemHeight = new WeakMap(), _WheelPhysics_visibleCount = new WeakMap(), _WheelPhysics_dragSensitivity = new WeakMap(), _WheelPhysics_scrollSensitivity = new WeakMap(), _WheelPhysics_options = new WeakMap(), _WheelPhysics_onSnap = new WeakMap(), _WheelPhysics_infinite = new WeakMap(), _WheelPhysics_rafId = new WeakMap(), _WheelPhysics_isDragging = new WeakMap(), _WheelPhysics_dragStartOffset = new WeakMap(), _WheelPhysics_dragStartY = new WeakMap(), _WheelPhysics_yList = new WeakMap(), _WheelPhysics_lastWheelTime = new WeakMap(), _WheelPhysics_animating = new WeakMap(), _WheelPhysics_instances = new WeakSet(), _WheelPhysics_cancelRaf = function _WheelPhysics_cancelRaf() {
|
|
328
|
-
if (__classPrivateFieldGet(this, _WheelPhysics_rafId, "f") !== null) {
|
|
329
|
-
cancelAnimationFrame(__classPrivateFieldGet(this, _WheelPhysics_rafId, "f"));
|
|
330
|
-
__classPrivateFieldSet(this, _WheelPhysics_rafId, null, "f");
|
|
331
304
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Private helpers
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
#cancelRaf() {
|
|
309
|
+
if (this.#rafId !== null) {
|
|
310
|
+
cancelAnimationFrame(this.#rafId);
|
|
311
|
+
this.#rafId = null;
|
|
312
|
+
}
|
|
313
|
+
this.#animating = false;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Converts an option index to a translateY offset, accounting for before-ghost
|
|
317
|
+
* rows prepended to the DOM container in infinite mode.
|
|
318
|
+
*
|
|
319
|
+
* In infinite mode the container begins with N = options.length ghost rows, so
|
|
320
|
+
* real item[i] sits at DOM position (N + i) * itemHeight. The offset must be
|
|
321
|
+
* shifted by -N * itemHeight relative to the non-infinite formula.
|
|
322
|
+
*/
|
|
323
|
+
#indexToOffset(index) {
|
|
324
|
+
const ghostCount = this.#infinite ? this.#options.length : 0;
|
|
325
|
+
return indexToOffset(index + ghostCount, this.#itemHeight, this.#visibleCount);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Converts a translateY offset back to an option index, accounting for the
|
|
329
|
+
* before-ghost prefix in infinite mode (inverse of #indexToOffset).
|
|
330
|
+
*/
|
|
331
|
+
#offsetToIndex(offset) {
|
|
332
|
+
const ghostCount = this.#infinite ? this.#options.length : 0;
|
|
333
|
+
return offsetToIndex(offset, this.#itemHeight, this.#visibleCount) - ghostCount;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
340
336
|
// Unused export to prevent unused import warnings
|
|
341
337
|
void SNAP_BACK_DECELERATION;
|
|
@@ -11,21 +11,21 @@
|
|
|
11
11
|
// Physics constants (from React source v1.2.2)
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
/** Boundary resistance factor — how much drag is applied when pulling past the ends. */
|
|
14
|
-
export
|
|
14
|
+
export const RESISTANCE = 0.3;
|
|
15
15
|
/** Maximum scroll velocity in items/second. */
|
|
16
|
-
export
|
|
16
|
+
export const MAX_VELOCITY = 30;
|
|
17
17
|
/** Default drag sensitivity (pointer drag delta multiplier for inertia). */
|
|
18
|
-
export
|
|
18
|
+
export const DEFAULT_DRAG_SENSITIVITY = 3;
|
|
19
19
|
/** Default scroll sensitivity (wheel event multiplier for snap animation duration). */
|
|
20
|
-
export
|
|
20
|
+
export const DEFAULT_SCROLL_SENSITIVITY = 5;
|
|
21
21
|
/** Default height in pixels of each option row. */
|
|
22
|
-
export
|
|
22
|
+
export const DEFAULT_ITEM_HEIGHT = 30;
|
|
23
23
|
/** Default number of visible option rows. */
|
|
24
|
-
export
|
|
24
|
+
export const DEFAULT_VISIBLE_COUNT = 5;
|
|
25
25
|
/** Deceleration constant used in snap-back calculations. */
|
|
26
|
-
export
|
|
26
|
+
export const SNAP_BACK_DECELERATION = 10;
|
|
27
27
|
/** Minimum scaleY for cylindrical mode to prevent items collapsing to zero height. */
|
|
28
|
-
export
|
|
28
|
+
export const MIN_CYLINDRICAL_SCALE = 0.1;
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
// Pure physics functions
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
@@ -99,18 +99,17 @@ export function wrapIndex(index, optionsLength) {
|
|
|
99
99
|
* @returns The nearest enabled index
|
|
100
100
|
*/
|
|
101
101
|
export function snapToNearestEnabled(targetIndex, options) {
|
|
102
|
-
|
|
103
|
-
if (!((_a = options[targetIndex]) === null || _a === void 0 ? void 0 : _a.disabled)) {
|
|
102
|
+
if (!options[targetIndex]?.disabled) {
|
|
104
103
|
return targetIndex;
|
|
105
104
|
}
|
|
106
105
|
// Walk outward from targetIndex to find the nearest enabled option
|
|
107
|
-
for (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (lower >= 0 && !
|
|
106
|
+
for (let delta = 1; delta < options.length; delta++) {
|
|
107
|
+
const lower = targetIndex - delta;
|
|
108
|
+
const upper = targetIndex + delta;
|
|
109
|
+
if (lower >= 0 && !options[lower]?.disabled) {
|
|
111
110
|
return lower;
|
|
112
111
|
}
|
|
113
|
-
if (upper < options.length && !
|
|
112
|
+
if (upper < options.length && !options[upper]?.disabled) {
|
|
114
113
|
return upper;
|
|
115
114
|
}
|
|
116
115
|
}
|
|
@@ -131,13 +130,13 @@ export function calculateVelocity(yList, itemHeight) {
|
|
|
131
130
|
if (yList.length < 2) {
|
|
132
131
|
return 0;
|
|
133
132
|
}
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
const [y1, t1] = yList[yList.length - 2];
|
|
134
|
+
const [y2, t2] = yList[yList.length - 1];
|
|
136
135
|
if (t2 === t1) {
|
|
137
136
|
return 0;
|
|
138
137
|
}
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
const velocity = ((y2 - y1) / itemHeight) * (1000 / (t2 - t1));
|
|
139
|
+
const clamped = Math.max(-MAX_VELOCITY, Math.min(MAX_VELOCITY, velocity));
|
|
141
140
|
return clamped;
|
|
142
141
|
}
|
|
143
142
|
/**
|
|
@@ -153,11 +152,11 @@ export function calculateVelocity(yList, itemHeight) {
|
|
|
153
152
|
* @returns The rounded target index after inertia overshoot
|
|
154
153
|
*/
|
|
155
154
|
export function computeSnapTarget(currentIndexFromOffset, velocity, dragSensitivity) {
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
const baseDeceleration = dragSensitivity * 10;
|
|
156
|
+
const overshoot = (0.5 * velocity * velocity) / baseDeceleration;
|
|
158
157
|
// Velocity sign is inverted relative to index direction:
|
|
159
158
|
// drag down (positive velocity) increases offset → decreases index → overshoot toward lower index.
|
|
160
|
-
|
|
159
|
+
const rawTarget = currentIndexFromOffset - Math.sign(velocity) * overshoot;
|
|
161
160
|
return Math.round(rawTarget);
|
|
162
161
|
}
|
|
163
162
|
/**
|
|
@@ -171,7 +170,7 @@ export function computeSnapTarget(currentIndexFromOffset, velocity, dragSensitiv
|
|
|
171
170
|
* @returns Animation duration in seconds
|
|
172
171
|
*/
|
|
173
172
|
export function computeAnimationDuration(distance, scrollSensitivity) {
|
|
174
|
-
|
|
173
|
+
const raw = Math.sqrt(Math.abs(distance) / scrollSensitivity);
|
|
175
174
|
return Math.max(0.1, Math.min(0.6, raw));
|
|
176
175
|
}
|
|
177
176
|
/**
|
|
@@ -193,7 +192,7 @@ export function computeAnimationDuration(distance, scrollSensitivity) {
|
|
|
193
192
|
* @returns scaleY in range [MIN_CYLINDRICAL_SCALE, 1.0]
|
|
194
193
|
*/
|
|
195
194
|
export function cylindricalScaleY(slotIndex, offset, itemHeight, visibleCount) {
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
const dist = slotIndex + offset / itemHeight - Math.floor(visibleCount / 2);
|
|
196
|
+
const angle = (dist * Math.PI) / visibleCount;
|
|
198
197
|
return Math.max(MIN_CYLINDRICAL_SCALE, Math.cos(angle));
|
|
199
198
|
}
|
package/package.json
CHANGED