@supermousejs/core 2.3.0 → 2.4.0
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/CHANGELOG.md +33 -0
- package/dist/index.d.ts +106 -139
- package/dist/index.mjs +309 -227
- package/dist/index.umd.js +10 -5
- package/package.json +4 -3
- package/src/Supermouse.ts +517 -295
- package/src/__tests__/core.spec.ts +291 -0
- package/src/__tests__/cursor-mode.spec.ts +109 -0
- package/src/__tests__/input.spec.ts +261 -0
- package/src/__tests__/integration.spec.ts +121 -0
- package/src/__tests__/lifecycle.spec.ts +199 -0
- package/src/__tests__/nested-instance.spec.ts +51 -0
- package/src/__tests__/plugin.spec.ts +130 -0
- package/src/__tests__/setup.ts +14 -6
- package/src/__tests__/stage.spec.ts +235 -0
- package/src/__tests__/suspend-resume.spec.ts +50 -0
- package/src/types.ts +165 -183
- package/vitest.config.ts +1 -1
- package/src/__tests__/supermouse.test.ts +0 -19
package/dist/index.mjs
CHANGED
|
@@ -1,44 +1,57 @@
|
|
|
1
|
-
const
|
|
2
|
-
function p(
|
|
3
|
-
return
|
|
1
|
+
const c = "2.4.0";
|
|
2
|
+
function p(o, t, e) {
|
|
3
|
+
return o + (t - o) * e;
|
|
4
4
|
}
|
|
5
|
-
function
|
|
6
|
-
return p(
|
|
5
|
+
function d(o, t, e, s) {
|
|
6
|
+
return p(o, t, 1 - Math.exp(-e * s));
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const h = { x: -100, y: -100 }, g = /* @__PURE__ */ new Set(["input", "textarea", "select"]), f = /* @__PURE__ */ new Set([
|
|
9
|
+
"default",
|
|
10
|
+
"auto",
|
|
11
|
+
"pointer",
|
|
12
|
+
"none",
|
|
13
|
+
"inherit",
|
|
14
|
+
"grab",
|
|
15
|
+
"grabbing"
|
|
16
|
+
]), m = [
|
|
17
|
+
"a",
|
|
18
|
+
"button",
|
|
19
|
+
"input",
|
|
20
|
+
"textarea",
|
|
21
|
+
"[data-hover]",
|
|
22
|
+
"[data-cursor]"
|
|
23
|
+
];
|
|
24
|
+
class v {
|
|
25
|
+
constructor(t, e, s, i) {
|
|
26
|
+
this.state = t, this.options = e, this.getHoverSelector = s, this.onEnableChange = i, this.dataPrefix = this.options.dataPrefix ?? "supermouse", this.normalizedDataPrefix = this.dataPrefix.toLowerCase(), this.ignoreAttribute = `data-${this.dataPrefix}-ignore`, this.ruleEntries = this.options.rules ? Object.entries(this.options.rules) : [], this.checkDeviceCapability(), this.checkMotionPreference(), this.setupContainerRectTracking(), this.bindEvents();
|
|
11
27
|
}
|
|
12
28
|
state;
|
|
13
29
|
options;
|
|
14
30
|
getHoverSelector;
|
|
15
31
|
onEnableChange;
|
|
16
32
|
mediaQueryList;
|
|
17
|
-
mediaQueryHandler;
|
|
18
33
|
motionQuery;
|
|
19
34
|
dataPrefix;
|
|
20
35
|
normalizedDataPrefix;
|
|
21
36
|
ignoreAttribute;
|
|
22
|
-
/**
|
|
23
|
-
* Master switch for input processing.
|
|
24
|
-
* Toggled by `Supermouse.enable()`/`disable()` or automatically by device capability checks.
|
|
25
|
-
*/
|
|
26
|
-
isEnabled = !0;
|
|
27
37
|
abortController = new AbortController();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
nativeTarget = null;
|
|
39
|
+
hasSeenPointer = !1;
|
|
40
|
+
isEnabled = !0;
|
|
41
|
+
containerRect = null;
|
|
42
|
+
resizeObserver;
|
|
43
|
+
/** Cached matched rules for the current interaction element. */
|
|
44
|
+
matchedRules = [];
|
|
45
|
+
lastParsedTarget = null;
|
|
46
|
+
/** Precomputed list of rule entries for faster iteration. */
|
|
47
|
+
ruleEntries;
|
|
48
|
+
/** The actual element currently under the pointer (regardless of hover selectors). */
|
|
49
|
+
currentTarget = null;
|
|
31
50
|
checkDeviceCapability() {
|
|
32
|
-
this.options.autoDisableOnMobile && (this.mediaQueryList = window.matchMedia("(pointer: fine)"), this.updateEnabledState(this.mediaQueryList.matches), this.
|
|
33
|
-
this.updateEnabledState(t.matches);
|
|
34
|
-
}, this.mediaQueryList.addEventListener("change", this.mediaQueryHandler, {
|
|
51
|
+
this.options.autoDisableOnMobile && (this.mediaQueryList = window.matchMedia("(pointer: fine)"), this.updateEnabledState(this.mediaQueryList.matches), this.mediaQueryList.addEventListener("change", (t) => this.updateEnabledState(t.matches), {
|
|
35
52
|
signal: this.abortController.signal
|
|
36
53
|
}));
|
|
37
54
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Checks for `prefers-reduced-motion`.
|
|
40
|
-
* If true, the core physics engine will switch to instant snapping (high damping) to avoid motion sickness.
|
|
41
|
-
*/
|
|
42
55
|
checkMotionPreference() {
|
|
43
56
|
this.motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"), this.state.reducedMotion = this.motionQuery.matches, this.motionQuery.addEventListener(
|
|
44
57
|
"change",
|
|
@@ -51,115 +64,167 @@ class f {
|
|
|
51
64
|
updateEnabledState(t) {
|
|
52
65
|
this.isEnabled = t, this.onEnableChange(t);
|
|
53
66
|
}
|
|
67
|
+
/** Caches container rect; updates on resize/scroll/ResizeObserver. */
|
|
68
|
+
setupContainerRectTracking() {
|
|
69
|
+
const t = this.options.container;
|
|
70
|
+
if (!t || t === document.body) return;
|
|
71
|
+
const e = () => {
|
|
72
|
+
this.containerRect = t.getBoundingClientRect();
|
|
73
|
+
};
|
|
74
|
+
e(), window.addEventListener("resize", e, { signal: this.abortController.signal }), window.addEventListener("scroll", e, {
|
|
75
|
+
passive: !0,
|
|
76
|
+
signal: this.abortController.signal
|
|
77
|
+
}), typeof ResizeObserver < "u" && (this.resizeObserver = new ResizeObserver(e), this.resizeObserver.observe(t));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Evaluates rules against the given element.
|
|
81
|
+
* Selector matching is cached per element; only function values re-evaluated each frame.
|
|
82
|
+
*/
|
|
54
83
|
parseDOMInteraction(t) {
|
|
55
|
-
if (this.
|
|
56
|
-
this.
|
|
57
|
-
|
|
84
|
+
if (t !== this.lastParsedTarget) {
|
|
85
|
+
this.lastParsedTarget = t, this.matchedRules = [];
|
|
86
|
+
for (const [i, n] of this.ruleEntries)
|
|
87
|
+
this.matchesSelector(t, i) && this.matchedRules.push({ selector: i, rules: n });
|
|
58
88
|
}
|
|
59
89
|
const e = {};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
t
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
90
|
+
for (const { rules: i } of this.matchedRules)
|
|
91
|
+
try {
|
|
92
|
+
const n = typeof i == "function" ? i(t) : i;
|
|
93
|
+
if (!n || typeof n != "object") continue;
|
|
94
|
+
for (const [r, a] of Object.entries(n))
|
|
95
|
+
e[r] = typeof a == "function" ? a(t) : a;
|
|
96
|
+
} catch (n) {
|
|
97
|
+
console.error("[Supermouse] Rule threw during evaluation:", n);
|
|
98
|
+
}
|
|
99
|
+
const s = this.normalizedDataPrefix;
|
|
100
|
+
for (const i in t.dataset) {
|
|
101
|
+
if (!i.toLowerCase().startsWith(s)) continue;
|
|
102
|
+
const n = i.slice(s.length);
|
|
103
|
+
if (!n) continue;
|
|
104
|
+
const r = t.dataset[i];
|
|
105
|
+
e[n[0].toLowerCase() + n.slice(1)] = r === "" ? !0 : r;
|
|
70
106
|
}
|
|
71
107
|
this.state.interaction = e;
|
|
72
108
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Optimized selector matching:
|
|
111
|
+
* - For simple selectors (no spaces), just call `element.matches`.
|
|
112
|
+
* - For complex selectors with descendant combinators, fallback to splitting.
|
|
113
|
+
*/
|
|
114
|
+
matchesSelector(t, e) {
|
|
115
|
+
try {
|
|
116
|
+
if (!/\s/.test(e.trim()))
|
|
117
|
+
return t.matches(e);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
const s = e.trim().split(/\s+/);
|
|
121
|
+
if (s.length < 2) return !1;
|
|
122
|
+
const i = s.pop(), n = s.join(" ");
|
|
123
|
+
if (!i || !n) return !1;
|
|
124
|
+
try {
|
|
125
|
+
return t.matches(i) && !!t.closest(n);
|
|
126
|
+
} catch {
|
|
127
|
+
return !1;
|
|
80
128
|
}
|
|
81
|
-
this.state.pointer.x = e, this.state.pointer.y = i, this.state.hasReceivedInput || (this.state.hasReceivedInput = !0, this.state.target.x = this.state.smooth.x = e, this.state.target.y = this.state.smooth.y = i);
|
|
82
129
|
}
|
|
83
|
-
|
|
84
|
-
|
|
130
|
+
isOutsideContainer(t) {
|
|
131
|
+
const { container: e } = this.options;
|
|
132
|
+
return !!e && e !== document.body && !e.contains(t);
|
|
85
133
|
}
|
|
86
|
-
|
|
87
|
-
|
|
134
|
+
resolveComputedCursor(t) {
|
|
135
|
+
return window.getComputedStyle(t).cursor;
|
|
88
136
|
}
|
|
89
|
-
|
|
137
|
+
handleMove = (t) => {
|
|
138
|
+
if (this.options.autoDisableOnMobile && t.pointerType === "touch" && !this.options.enableTouch)
|
|
139
|
+
return;
|
|
140
|
+
let e = t.clientX, s = t.clientY;
|
|
141
|
+
const i = this.options.container;
|
|
142
|
+
i && this.containerRect && i !== document.body && (e -= this.containerRect.left, s -= this.containerRect.top), this.state.pointer.x = e, this.state.pointer.y = s, this.hasSeenPointer = !0, this.isEnabled && (this.state.hasReceivedInput || (this.state.hasReceivedInput = !0, this.state.target.x = this.state.smooth.x = e, this.state.target.y = this.state.smooth.y = s));
|
|
143
|
+
};
|
|
144
|
+
handleDown = () => {
|
|
145
|
+
this.isEnabled && (this.state.isDown = !0);
|
|
146
|
+
};
|
|
147
|
+
handleUp = () => {
|
|
148
|
+
this.isEnabled && (this.state.isDown = !1);
|
|
149
|
+
};
|
|
150
|
+
handleMouseOver = (t) => {
|
|
90
151
|
if (!this.isEnabled) return;
|
|
91
152
|
const e = t.target;
|
|
92
|
-
if (
|
|
93
|
-
|
|
153
|
+
if (this.isOutsideContainer(e)) return;
|
|
154
|
+
if (this.state.cursorMode === "auto" && e.closest(`[${this.ignoreAttribute}]`)) {
|
|
155
|
+
this.state.isHover = !1, this.state.hoverTarget = null, this.state.interaction = {}, this.currentTarget = null, this.lastParsedTarget = null, this.matchedRules = [], this.state.isNative = !0, this.nativeTarget = e;
|
|
94
156
|
return;
|
|
95
157
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const o = s === "auto" || s === "tag", r = s === "auto" || s === "css";
|
|
101
|
-
let u = !1;
|
|
102
|
-
if (o) {
|
|
103
|
-
const h = e.localName;
|
|
104
|
-
(h === "input" || h === "textarea" || h === "select" || e.isContentEditable) && (u = !0);
|
|
105
|
-
}
|
|
106
|
-
if (!u && r) {
|
|
107
|
-
const h = window.getComputedStyle(e).cursor;
|
|
108
|
-
["default", "auto", "pointer", "none", "inherit"].includes(h) || (u = !0);
|
|
109
|
-
}
|
|
110
|
-
u && (this.state.isNative = !0);
|
|
158
|
+
if (this.state.isNative = !1, this.nativeTarget = null, this.currentTarget = e, this.parseDOMInteraction(e), this.state.cursorMode !== "auto") {
|
|
159
|
+
const r = e.closest(this.getHoverSelector());
|
|
160
|
+
r && (this.state.isHover = !0, this.state.hoverTarget = r);
|
|
161
|
+
return;
|
|
111
162
|
}
|
|
112
|
-
|
|
113
|
-
|
|
163
|
+
const s = e.closest(this.getHoverSelector());
|
|
164
|
+
s && (this.state.isHover = !0, this.state.hoverTarget = s);
|
|
165
|
+
const i = g.has(e.localName) || e.isContentEditable, n = !f.has(this.resolveComputedCursor(e));
|
|
166
|
+
(i || n) && (this.state.isNative = !0, this.nativeTarget = e);
|
|
167
|
+
};
|
|
168
|
+
handleMouseOut = (t) => {
|
|
114
169
|
if (!this.isEnabled) return;
|
|
115
|
-
const e = t.target;
|
|
116
|
-
(e === this.state.hoverTarget || e.contains(this.state.hoverTarget)) && (!
|
|
117
|
-
}
|
|
118
|
-
handleWindowLeave() {
|
|
119
|
-
this.options.hideOnLeave && (this.state.hasReceivedInput = !1, this.state.pointer = { ...
|
|
120
|
-
}
|
|
170
|
+
const e = t.target, s = t.relatedTarget;
|
|
171
|
+
this.isOutsideContainer(e) || ((e === this.state.hoverTarget || e.contains(this.state.hoverTarget)) && (!s || !this.state.hoverTarget?.contains(s)) && (this.state.isHover = !1, this.state.hoverTarget = null), this.nativeTarget && (e === this.nativeTarget || e.contains(this.nativeTarget)) && (!s || !this.nativeTarget.contains(s)) && (this.state.isNative = !1, this.nativeTarget = null), e === this.currentTarget && (this.currentTarget = null, this.lastParsedTarget = null, this.matchedRules = []));
|
|
172
|
+
};
|
|
173
|
+
handleWindowLeave = () => {
|
|
174
|
+
this.options.hideOnLeave && (this.state.hasReceivedInput = !1, this.state.pointer = { ...h });
|
|
175
|
+
};
|
|
121
176
|
clearHover() {
|
|
122
|
-
this.state.isHover = !1, this.state.hoverTarget = null, this.state.isNative = !1, this.state.interaction = {};
|
|
177
|
+
this.state.isHover = !1, this.state.hoverTarget = null, this.state.isNative = !1, this.nativeTarget = null, this.state.interaction = {}, this.currentTarget = null, this.lastParsedTarget = null, this.matchedRules = [];
|
|
178
|
+
}
|
|
179
|
+
/** Returns the raw element currently under the pointer. */
|
|
180
|
+
getCurrentTarget() {
|
|
181
|
+
return this.currentTarget;
|
|
123
182
|
}
|
|
124
183
|
bindEvents() {
|
|
125
184
|
const { signal: t } = this.abortController;
|
|
126
|
-
window.addEventListener("pointermove", this.handleMove
|
|
185
|
+
window.addEventListener("pointermove", this.handleMove, { passive: !0, signal: t }), window.addEventListener("pointerdown", this.handleDown, { passive: !0, signal: t }), window.addEventListener("pointerup", this.handleUp, { signal: t });
|
|
186
|
+
const s = !this.options.container || this.options.container === document.body ? document : this.options.container;
|
|
187
|
+
s.addEventListener("mouseover", this.handleMouseOver, { signal: t }), s.addEventListener("mouseout", this.handleMouseOut, { signal: t }), document.addEventListener("mouseleave", this.handleWindowLeave, { signal: t });
|
|
127
188
|
}
|
|
128
189
|
destroy() {
|
|
129
|
-
this.abortController.abort();
|
|
190
|
+
this.abortController.abort(), this.resizeObserver?.disconnect();
|
|
130
191
|
}
|
|
131
192
|
}
|
|
132
|
-
let
|
|
133
|
-
class
|
|
134
|
-
constructor(t = document.body, e) {
|
|
135
|
-
if (this.container = t, this.
|
|
193
|
+
let b = 0;
|
|
194
|
+
class y {
|
|
195
|
+
constructor(t = document.body, e = 9999) {
|
|
196
|
+
if (this.container = t, this.zIndex = e, !t || !(t instanceof HTMLElement))
|
|
136
197
|
throw new Error(`[Supermouse] Invalid container: ${t}. Must be an HTMLElement.`);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
198
|
+
t.isConnected || console.warn(
|
|
199
|
+
"[Supermouse] container is not attached to the document — stage sizing/positioning will be wrong until it is."
|
|
200
|
+
);
|
|
201
|
+
const s = b++;
|
|
202
|
+
this.id = `supermouse-style-${s}`, this.scopeClass = `supermouse-scope-${s}`, this.hideClass = `supermouse-hide-${s}`;
|
|
203
|
+
const i = t === document.body;
|
|
140
204
|
if (this.element = document.createElement("div"), Object.assign(this.element.style, {
|
|
141
|
-
position:
|
|
205
|
+
position: i ? "fixed" : "absolute",
|
|
142
206
|
inset: "0px",
|
|
143
207
|
pointerEvents: "none",
|
|
144
|
-
zIndex:
|
|
208
|
+
zIndex: String(this.zIndex),
|
|
145
209
|
opacity: "1",
|
|
146
210
|
transition: "opacity 0.15s ease"
|
|
147
|
-
}), !
|
|
148
|
-
const
|
|
149
|
-
this.originalContainerPosition =
|
|
211
|
+
}), !i) {
|
|
212
|
+
const n = window.getComputedStyle(t);
|
|
213
|
+
this.originalContainerPosition = n.position, n.position === "static" && (t.style.position = "relative");
|
|
150
214
|
}
|
|
151
|
-
this.originalContainerCursor = t.style.cursor, t.appendChild(this.element), this.styleTag = document.createElement("style"), this.styleTag.id = this.id, document.head.appendChild(this.styleTag), this.container.classList.add(this.scopeClass), this.
|
|
215
|
+
this.originalContainerCursor = t.style.cursor, t.appendChild(this.element), this.styleTag = document.createElement("style"), this.styleTag.id = this.id, document.head.appendChild(this.styleTag), this.container.classList.add("supermouse-scope", this.scopeClass), this.updateCursorCSS();
|
|
152
216
|
}
|
|
153
217
|
container;
|
|
154
|
-
|
|
155
|
-
/** The container element appended to the document. */
|
|
218
|
+
zIndex;
|
|
156
219
|
element;
|
|
157
220
|
styleTag;
|
|
158
221
|
id;
|
|
159
222
|
scopeClass;
|
|
223
|
+
hideClass;
|
|
160
224
|
currentCursorState = null;
|
|
161
225
|
originalContainerPosition = "";
|
|
162
226
|
originalContainerCursor = "";
|
|
227
|
+
/** Selectors that need explicit `cursor: none !important` to override UA styles. */
|
|
163
228
|
selectors = /* @__PURE__ */ new Set([
|
|
164
229
|
"a",
|
|
165
230
|
"button",
|
|
@@ -169,23 +234,26 @@ class v {
|
|
|
169
234
|
'[role="button"]',
|
|
170
235
|
"[tabindex]"
|
|
171
236
|
]);
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
237
|
+
/** Batch add selectors. Comma‑separated groups are split and scoped individually. */
|
|
238
|
+
addSelectors(t) {
|
|
239
|
+
let e = !1;
|
|
240
|
+
for (const s of t)
|
|
241
|
+
s.split(",").forEach((i) => {
|
|
242
|
+
const n = i.trim();
|
|
243
|
+
n && !this.selectors.has(n) && (this.selectors.add(n), e = !0);
|
|
244
|
+
});
|
|
245
|
+
e && this.updateCursorCSS();
|
|
246
|
+
}
|
|
247
|
+
/** Add a single selector (or comma‑separated group). */
|
|
177
248
|
addSelector(t) {
|
|
178
|
-
this.
|
|
249
|
+
this.addSelectors([t]);
|
|
179
250
|
}
|
|
180
251
|
setVisibility(t) {
|
|
181
252
|
this.element.style.opacity = t ? "1" : "0";
|
|
182
253
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Toggles the visibility of the native cursor via CSS injection.
|
|
185
|
-
* @param type 'none' to hide, 'auto' to show.
|
|
186
|
-
*/
|
|
254
|
+
/** Toggle native cursor visibility. */
|
|
187
255
|
setNativeCursor(t) {
|
|
188
|
-
|
|
256
|
+
t !== this.currentCursorState && (this.currentCursorState = t, this.container.classList.toggle(this.hideClass, t === "none"), this.container.style.cursor = t === "none" ? "none" : this.originalContainerCursor);
|
|
189
257
|
}
|
|
190
258
|
updateCursorCSS() {
|
|
191
259
|
const t = Array.from(this.selectors);
|
|
@@ -193,227 +261,241 @@ class v {
|
|
|
193
261
|
this.styleTag.innerText = "";
|
|
194
262
|
return;
|
|
195
263
|
}
|
|
196
|
-
const e =
|
|
264
|
+
const e = `:not(.${this.scopeClass} .supermouse-scope):not(.${this.scopeClass} .supermouse-scope *)`, s = (a) => `.${this.scopeClass}.${this.hideClass} ${a}${e} { cursor: none !important; }`, i = t.map(s).join(`
|
|
265
|
+
`), n = `.${this.scopeClass}.${this.hideClass} *${e} { cursor: none !important; }`, r = `.${this.scopeClass}.${this.hideClass} { cursor: none !important; }`;
|
|
197
266
|
this.styleTag.innerText = `
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
267
|
+
${r}
|
|
268
|
+
${n}
|
|
269
|
+
${i}
|
|
270
|
+
${s("label")}
|
|
271
|
+
${s("select")}
|
|
272
|
+
${s('input[type="range"]::-webkit-slider-thumb')}
|
|
273
|
+
${s('input[type="range"]::-moz-range-thumb')}
|
|
274
|
+
`;
|
|
202
275
|
}
|
|
203
276
|
destroy() {
|
|
204
|
-
this.element.remove(), this.styleTag.remove(), this.container.style.cursor = this.originalContainerCursor, this.container.classList.remove(this.scopeClass), this.container !== document.body && this.originalContainerPosition === "static" && (this.container.style.position = "");
|
|
277
|
+
this.element.remove(), this.styleTag.remove(), this.container.style.cursor = this.originalContainerCursor, this.container.classList.remove("supermouse-scope", this.scopeClass, this.hideClass), this.container !== document.body && this.originalContainerPosition === "static" && (this.container.style.position = "");
|
|
205
278
|
}
|
|
206
279
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"input",
|
|
211
|
-
"textarea",
|
|
212
|
-
"[data-hover]",
|
|
213
|
-
"[data-cursor]"
|
|
214
|
-
];
|
|
215
|
-
class y {
|
|
216
|
-
static version = d;
|
|
217
|
-
version = d;
|
|
280
|
+
class C {
|
|
281
|
+
static version = c;
|
|
282
|
+
version = c;
|
|
218
283
|
state;
|
|
219
|
-
/**
|
|
220
|
-
* Configuration options.
|
|
221
|
-
*/
|
|
284
|
+
/** Configuration options, fully resolved with defaults applied. */
|
|
222
285
|
options;
|
|
223
286
|
plugins = [];
|
|
224
|
-
|
|
287
|
+
_stage;
|
|
225
288
|
input;
|
|
226
289
|
rafId = 0;
|
|
227
290
|
lastTime = 0;
|
|
228
291
|
isRunning = !1;
|
|
292
|
+
isSuspended = !1;
|
|
293
|
+
visibilityAbortController = new AbortController();
|
|
229
294
|
hoverSelectors;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
*
|
|
233
|
-
* @param options - Global configuration options.
|
|
234
|
-
* @throws Will throw if running in a non-browser environment (window/document undefined).
|
|
235
|
-
*/
|
|
295
|
+
hoverSelectorString;
|
|
296
|
+
crashedPlugins = [];
|
|
236
297
|
constructor(t = {}) {
|
|
237
298
|
this.options = {
|
|
238
299
|
smoothness: 0.15,
|
|
239
300
|
enableTouch: !1,
|
|
240
301
|
autoDisableOnMobile: !0,
|
|
241
|
-
|
|
242
|
-
hideCursor: !0,
|
|
302
|
+
cursor: "auto",
|
|
243
303
|
hideOnLeave: !0,
|
|
244
304
|
autoStart: !0,
|
|
245
305
|
container: document.body,
|
|
246
306
|
dataPrefix: "supermouse",
|
|
307
|
+
zIndex: 9999,
|
|
247
308
|
...t
|
|
248
309
|
}, this.state = {
|
|
249
|
-
pointer: {
|
|
250
|
-
target: {
|
|
251
|
-
smooth: {
|
|
310
|
+
pointer: { ...h },
|
|
311
|
+
target: { ...h },
|
|
312
|
+
smooth: { ...h },
|
|
252
313
|
velocity: { x: 0, y: 0 },
|
|
314
|
+
displacement: { x: 0, y: 0 },
|
|
253
315
|
angle: 0,
|
|
254
316
|
isDown: !1,
|
|
255
317
|
isHover: !1,
|
|
256
318
|
isNative: !1,
|
|
257
|
-
|
|
319
|
+
cursorMode: this.options.cursor,
|
|
258
320
|
hoverTarget: null,
|
|
259
321
|
reducedMotion: !1,
|
|
260
322
|
hasReceivedInput: !1,
|
|
261
323
|
shape: null,
|
|
262
324
|
interaction: {}
|
|
263
|
-
}, this.
|
|
325
|
+
}, this.hoverSelectors = new Set(this.options.hoverSelectors ?? m), this.hoverSelectorString = Array.from(this.hoverSelectors).join(", "), this._stage = new y(this.options.container, this.options.zIndex), this._stage.addSelectors(this.hoverSelectors), this.input = new v(
|
|
264
326
|
this.state,
|
|
265
327
|
this.options,
|
|
266
|
-
() =>
|
|
328
|
+
() => this.hoverSelectorString,
|
|
267
329
|
(e) => {
|
|
268
330
|
e || this.reset(!0);
|
|
269
331
|
}
|
|
270
|
-
), this.options.plugins
|
|
332
|
+
), this.options.plugins?.forEach((e) => this.use(e)), this.bindVisibilityHandling(), this.init();
|
|
271
333
|
}
|
|
272
|
-
/**
|
|
273
|
-
* Retrieves a registered plugin instance by its unique name.
|
|
274
|
-
*/
|
|
334
|
+
/** Look up a registered plugin by name. */
|
|
275
335
|
getPlugin(t) {
|
|
276
336
|
return this.plugins.find((e) => e.name === t);
|
|
277
337
|
}
|
|
278
|
-
/**
|
|
279
|
-
* Returns whether the cursor system is currently enabled (processing input).
|
|
280
|
-
*/
|
|
338
|
+
/** Whether the instance is not disabled/suspended and is processing input. */
|
|
281
339
|
get isEnabled() {
|
|
282
340
|
return this.input.isEnabled;
|
|
283
341
|
}
|
|
284
|
-
/**
|
|
285
|
-
* Enables a specific plugin by name.
|
|
286
|
-
* Triggers the `onEnable` lifecycle hook of the plugin.
|
|
287
|
-
*/
|
|
342
|
+
/** Enable a plugin by name. */
|
|
288
343
|
enablePlugin(t) {
|
|
289
344
|
const e = this.getPlugin(t);
|
|
290
|
-
e && e.isEnabled === !1 && (e.isEnabled = !0, e.onEnable?.(this));
|
|
345
|
+
e && e.isEnabled === !1 && (e.isEnabled = !0, e.element && (e.element.style.display = ""), e.onEnable?.(this));
|
|
291
346
|
}
|
|
292
|
-
/**
|
|
293
|
-
* Disables a specific plugin by name.
|
|
294
|
-
* Triggers the `onDisable` lifecycle hook.
|
|
295
|
-
*/
|
|
347
|
+
/** Disable a plugin by name and hide its element. */
|
|
296
348
|
disablePlugin(t) {
|
|
297
349
|
const e = this.getPlugin(t);
|
|
298
|
-
e && e.isEnabled !== !1
|
|
350
|
+
if (e && e.isEnabled !== !1) {
|
|
351
|
+
e.isEnabled = !1;
|
|
352
|
+
const s = () => {
|
|
353
|
+
e.element && (e.element.style.display = "none"), e.onDisable?.(this);
|
|
354
|
+
}, i = e.onBeforeDisable?.(this);
|
|
355
|
+
i && typeof i.then == "function" ? Promise.resolve(i).then(s).catch((n) => {
|
|
356
|
+
console.error(`[Supermouse] Plugin '${e.name}' onBeforeDisable threw:`, n), s();
|
|
357
|
+
}) : s();
|
|
358
|
+
}
|
|
299
359
|
}
|
|
300
|
-
/**
|
|
301
|
-
* Toggles the enabled state of a plugin.
|
|
302
|
-
*/
|
|
360
|
+
/** Toggle a plugin's enabled state by name. */
|
|
303
361
|
togglePlugin(t) {
|
|
304
362
|
const e = this.getPlugin(t);
|
|
305
363
|
e && (e.isEnabled === !1 ? this.enablePlugin(t) : this.disablePlugin(t));
|
|
306
364
|
}
|
|
365
|
+
/** Add a selector to hover detection and cursor suppression. */
|
|
307
366
|
registerHoverTarget(t) {
|
|
308
|
-
this.hoverSelectors.has(t) || (this.hoverSelectors.add(t), this.
|
|
367
|
+
this.hoverSelectors.has(t) || (this.hoverSelectors.add(t), this.hoverSelectorString = Array.from(this.hoverSelectors).join(", "), this._stage.addSelector(t));
|
|
309
368
|
}
|
|
310
|
-
/**
|
|
311
|
-
* The fixed container element where plugins should append their DOM nodes.
|
|
312
|
-
*/
|
|
369
|
+
/** The DOM element the instance is scoped to. */
|
|
313
370
|
get container() {
|
|
314
|
-
return this.
|
|
371
|
+
return this.options.container;
|
|
315
372
|
}
|
|
316
|
-
/**
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
this.state.
|
|
373
|
+
/** The stage element that plugins append their visuals into. */
|
|
374
|
+
get stage() {
|
|
375
|
+
return this._stage.element;
|
|
376
|
+
}
|
|
377
|
+
/** Set the current cursor mode. */
|
|
378
|
+
setCursor(t) {
|
|
379
|
+
this.state.cursorMode = t;
|
|
323
380
|
}
|
|
324
381
|
init() {
|
|
325
382
|
this.options.autoStart && this.startLoop();
|
|
326
383
|
}
|
|
384
|
+
/** Re‑enable input processing and re‑apply cursor state. */
|
|
327
385
|
enable() {
|
|
328
|
-
this.input.isEnabled = !0, this.
|
|
386
|
+
this.input.isEnabled = !0, this.input.hasSeenPointer && (this.state.target.x = this.state.smooth.x = this.state.pointer.x, this.state.target.y = this.state.smooth.y = this.state.pointer.y, this.resetMotion(), this.state.hasReceivedInput = !0), this._stage.setNativeCursor(this.resolveCursorState());
|
|
329
387
|
}
|
|
388
|
+
/** Disable input processing and restore native cursor. */
|
|
330
389
|
disable() {
|
|
331
|
-
this.input.isEnabled = !1, this.
|
|
390
|
+
this.input.isEnabled = !1, this._stage.setNativeCursor("auto"), this._stage.setVisibility(!1), this.reset(!0);
|
|
391
|
+
}
|
|
392
|
+
/** Temporarily yield to a scoped instance. */
|
|
393
|
+
suspend() {
|
|
394
|
+
this.input.isEnabled && (this.isSuspended = !0, this.input.isEnabled = !1, this.input.clearHover(), this._stage.setVisibility(!1));
|
|
395
|
+
}
|
|
396
|
+
/** Resume from `suspend()`. */
|
|
397
|
+
resume() {
|
|
398
|
+
if (this.isSuspended) {
|
|
399
|
+
this.isSuspended = !1, this.input.isEnabled = !0, this.state.hasReceivedInput && (this.state.target.x = this.state.smooth.x = this.state.pointer.x, this.state.target.y = this.state.smooth.y = this.state.pointer.y, this.resetMotion());
|
|
400
|
+
for (let t = this.plugins.length - 1; t >= 0; t--)
|
|
401
|
+
this.runPluginSafe(this.plugins[t], 0);
|
|
402
|
+
this._stage.setVisibility(!0);
|
|
403
|
+
}
|
|
332
404
|
}
|
|
333
|
-
/**
|
|
334
|
-
* Registers a new plugin.
|
|
335
|
-
*
|
|
336
|
-
* @param plugin - The plugin object to install.
|
|
337
|
-
*/
|
|
405
|
+
/** Register a new plugin. */
|
|
338
406
|
use(t) {
|
|
339
|
-
if (this.plugins.some((
|
|
407
|
+
if (this.plugins.some((e) => e.name === t.name))
|
|
340
408
|
return console.warn(`[Supermouse] Plugin "${t.name}" already installed.`), this;
|
|
341
|
-
t.isEnabled
|
|
409
|
+
t.isEnabled ??= !0;
|
|
342
410
|
try {
|
|
343
411
|
t.install?.(this);
|
|
344
|
-
} catch (
|
|
345
|
-
return console.error(`[Supermouse] Failed to install plugin '${t.name}'.`,
|
|
412
|
+
} catch (e) {
|
|
413
|
+
return console.error(`[Supermouse] Failed to install plugin '${t.name}'.`, e), this;
|
|
346
414
|
}
|
|
347
|
-
return this.plugins.push(t), this.plugins.sort((
|
|
415
|
+
return this.plugins.push(t), this.plugins.sort((e, s) => (e.priority ?? 0) - (s.priority ?? 0)), this;
|
|
348
416
|
}
|
|
417
|
+
/** Reset physics; optionally clear all input state. */
|
|
349
418
|
reset(t = !1) {
|
|
350
|
-
this.state.
|
|
419
|
+
this.state.target = { ...h }, this.state.smooth = { ...h }, this.resetMotion(), this.state.angle = 0, t && (this.state.hasReceivedInput = !1, this.state.shape = null, this.state.interaction = {});
|
|
351
420
|
}
|
|
352
421
|
startLoop() {
|
|
353
|
-
this.isRunning || (this.isRunning = !0, this.lastTime = performance.now(), this.
|
|
422
|
+
this.isRunning || (this.isRunning = !0, !document.hidden && (this.lastTime = performance.now(), this.rafId = requestAnimationFrame(this.tick)));
|
|
354
423
|
}
|
|
355
|
-
/**
|
|
356
|
-
* Starts the animation loop. This is automatically called if `autoStart` is true.
|
|
357
|
-
* Plugins can call this method to resume the loop if it has been stopped.
|
|
358
|
-
*/
|
|
424
|
+
/** Start the animation loop. */
|
|
359
425
|
start() {
|
|
360
426
|
this.startLoop();
|
|
361
427
|
}
|
|
362
|
-
/**
|
|
363
|
-
* Manually steps the animation loop.
|
|
364
|
-
*
|
|
365
|
-
* @param time Current timestamp in milliseconds.
|
|
366
|
-
*/
|
|
428
|
+
/** Manually step the animation loop. */
|
|
367
429
|
step(t) {
|
|
368
|
-
this.
|
|
430
|
+
this.update(t);
|
|
369
431
|
}
|
|
370
432
|
runPluginSafe(t, e) {
|
|
371
433
|
if (t.isEnabled !== !1)
|
|
372
434
|
try {
|
|
373
435
|
t.update?.(this, e);
|
|
374
|
-
} catch (
|
|
375
|
-
console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`,
|
|
376
|
-
|
|
377
|
-
|
|
436
|
+
} catch (s) {
|
|
437
|
+
console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`, s), t.isEnabled = !1, this.crashedPlugins.push(t);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
cleanupCrashedPlugins() {
|
|
441
|
+
if (this.crashedPlugins.length !== 0) {
|
|
442
|
+
for (const t of this.crashedPlugins) {
|
|
443
|
+
const e = this.plugins.indexOf(t);
|
|
444
|
+
e > -1 && this.plugins.splice(e, 1);
|
|
378
445
|
try {
|
|
379
|
-
t.
|
|
446
|
+
t.onDisable?.(this), t.destroy?.(this);
|
|
380
447
|
} catch (s) {
|
|
381
448
|
console.error(`[Supermouse] Failed to cleanup crashed plugin '${t.name}'.`, s);
|
|
382
449
|
}
|
|
450
|
+
t.element?.remove();
|
|
383
451
|
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Runs on every animation frame.
|
|
387
|
-
*/
|
|
388
|
-
tick = (t) => {
|
|
389
|
-
const e = t - this.lastTime, i = Math.min(e / 1e3, 0.1);
|
|
390
|
-
this.lastTime = t, this.state.hoverTarget && !this.state.hoverTarget.isConnected && this.input.clearHover();
|
|
391
|
-
const a = this.input.isEnabled && !this.state.isNative && this.state.hasReceivedInput;
|
|
392
|
-
if (this.stage.setVisibility(a), this.input.isEnabled && this.options.hideCursor) {
|
|
393
|
-
let s = "auto";
|
|
394
|
-
this.state.forcedCursor !== null ? s = this.state.forcedCursor : s = this.state.isNative || !this.state.hasReceivedInput ? "auto" : "none", this.stage.setNativeCursor(s);
|
|
452
|
+
this.crashedPlugins = [];
|
|
395
453
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
454
|
+
}
|
|
455
|
+
resolveStageVisibility() {
|
|
456
|
+
return this.state.cursorMode === "native" ? !1 : this.state.cursorMode === "both" ? this.input.isEnabled && this.state.hasReceivedInput : this.state.cursorMode === "custom" ? this.input.isEnabled && this.state.hasReceivedInput : this.input.isEnabled && !this.state.isNative && this.state.hasReceivedInput;
|
|
457
|
+
}
|
|
458
|
+
resolveCursorState() {
|
|
459
|
+
return !this.input.isEnabled || this.state.cursorMode === "both" || this.state.cursorMode === "native" ? "auto" : this.state.cursorMode === "custom" ? "none" : this.state.isNative || !this.state.hasReceivedInput ? "auto" : "none";
|
|
460
|
+
}
|
|
461
|
+
resetMotion() {
|
|
462
|
+
this.state.velocity = { x: 0, y: 0 }, this.state.displacement = { x: 0, y: 0 };
|
|
463
|
+
}
|
|
464
|
+
update(t) {
|
|
465
|
+
const e = t - this.lastTime, s = Math.min(e / 1e3, 0.1);
|
|
466
|
+
this.lastTime = t;
|
|
467
|
+
const i = this.input.getCurrentTarget();
|
|
468
|
+
i && !i.isConnected ? this.input.clearHover() : i && this.input.parseDOMInteraction(i), this._stage.setVisibility(this.resolveStageVisibility()), this.input.isEnabled && this._stage.setNativeCursor(this.resolveCursorState()), this.input.isEnabled && this.state.hasReceivedInput && (this.state.target.x = this.state.pointer.x, this.state.target.y = this.state.pointer.y);
|
|
469
|
+
for (let n = 0; n < this.plugins.length; n++)
|
|
470
|
+
this.runPluginSafe(this.plugins[n], e);
|
|
471
|
+
if (this.cleanupCrashedPlugins(), this.input.isEnabled) {
|
|
472
|
+
const n = this.state.reducedMotion ? 1e3 : 1 / this.options.smoothness * 2, r = this.state.smooth.x, a = this.state.smooth.y;
|
|
473
|
+
this.state.smooth.x = d(this.state.smooth.x, this.state.target.x, n, s), this.state.smooth.y = d(this.state.smooth.y, this.state.target.y, n, s), this.state.displacement.x = this.state.target.x - this.state.smooth.x, this.state.displacement.y = this.state.target.y - this.state.smooth.y, s > 0 ? (this.state.velocity.x = (this.state.smooth.x - r) / s, this.state.velocity.y = (this.state.smooth.y - a) / s) : (this.state.velocity.x = 0, this.state.velocity.y = 0);
|
|
474
|
+
const { x: l, y: u } = this.state.velocity;
|
|
475
|
+
(Math.abs(l) > 0.1 || Math.abs(u) > 0.1) && (this.state.angle = Math.atan2(u, l) * (180 / Math.PI));
|
|
404
476
|
}
|
|
405
|
-
|
|
477
|
+
}
|
|
478
|
+
tick = (t) => {
|
|
479
|
+
this.update(t), this.isRunning && (this.rafId = requestAnimationFrame(this.tick));
|
|
406
480
|
};
|
|
407
|
-
/**
|
|
408
|
-
|
|
409
|
-
|
|
481
|
+
/** Pause rAF loop when tab hidden; resume on visible. */
|
|
482
|
+
bindVisibilityHandling() {
|
|
483
|
+
document.addEventListener(
|
|
484
|
+
"visibilitychange",
|
|
485
|
+
() => {
|
|
486
|
+
this.isRunning && (document.hidden ? cancelAnimationFrame(this.rafId) : (this.lastTime = performance.now(), this.rafId = requestAnimationFrame(this.tick)));
|
|
487
|
+
},
|
|
488
|
+
{ signal: this.visibilityAbortController.signal }
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
/** Destroy the instance, freeing all resources. */
|
|
410
492
|
destroy() {
|
|
411
|
-
this.isRunning = !1, cancelAnimationFrame(this.rafId), this.input.destroy(), this.
|
|
493
|
+
this.isRunning = !1, cancelAnimationFrame(this.rafId), this.visibilityAbortController.abort(), this.input.destroy(), this._stage.destroy(), this.plugins.forEach((t) => t.destroy?.(this)), this.plugins = [];
|
|
412
494
|
}
|
|
413
495
|
}
|
|
414
496
|
export {
|
|
415
497
|
m as DEFAULT_HOVER_SELECTORS,
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
498
|
+
v as Input,
|
|
499
|
+
y as Stage,
|
|
500
|
+
C as Supermouse
|
|
419
501
|
};
|