@supermousejs/core 2.3.0 → 2.4.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/CHANGELOG.md +40 -0
- package/README.md +314 -10
- package/dist/index.d.ts +106 -139
- package/dist/index.mjs +316 -227
- package/dist/index.umd.js +10 -5
- package/package.json +4 -3
- package/src/Supermouse.ts +524 -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 +58 -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.1";
|
|
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,174 @@ 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
|
-
|
|
119
|
-
|
|
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
|
+
/**
|
|
174
|
+
* Handles the case where the pointer leaves the entire document.
|
|
175
|
+
* This is more reliable than the `mouseleave` event on `document`.
|
|
176
|
+
*/
|
|
177
|
+
handleDocumentMouseOut = (t) => {
|
|
178
|
+
this.isEnabled && t.relatedTarget === null && this.options.hideOnLeave && this.handleWindowLeave();
|
|
179
|
+
};
|
|
180
|
+
handleWindowLeave = () => {
|
|
181
|
+
this.options.hideOnLeave && (this.state.hasReceivedInput = !1, this.state.pointer = { ...h });
|
|
182
|
+
};
|
|
121
183
|
clearHover() {
|
|
122
|
-
this.state.isHover = !1, this.state.hoverTarget = null, this.state.isNative = !1, this.state.interaction = {};
|
|
184
|
+
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 = [];
|
|
185
|
+
}
|
|
186
|
+
/** Returns the raw element currently under the pointer. */
|
|
187
|
+
getCurrentTarget() {
|
|
188
|
+
return this.currentTarget;
|
|
123
189
|
}
|
|
124
190
|
bindEvents() {
|
|
125
191
|
const { signal: t } = this.abortController;
|
|
126
|
-
window.addEventListener("pointermove", this.handleMove
|
|
192
|
+
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 });
|
|
193
|
+
const s = !this.options.container || this.options.container === document.body ? document : this.options.container;
|
|
194
|
+
s.addEventListener("mouseover", this.handleMouseOver, { signal: t }), s.addEventListener("mouseout", this.handleMouseOut, { signal: t }), document.addEventListener("mouseout", this.handleDocumentMouseOut, { signal: t });
|
|
127
195
|
}
|
|
128
196
|
destroy() {
|
|
129
|
-
this.abortController.abort();
|
|
197
|
+
this.abortController.abort(), this.resizeObserver?.disconnect();
|
|
130
198
|
}
|
|
131
199
|
}
|
|
132
|
-
let
|
|
133
|
-
class
|
|
134
|
-
constructor(t = document.body, e) {
|
|
135
|
-
if (this.container = t, this.
|
|
200
|
+
let b = 0;
|
|
201
|
+
class y {
|
|
202
|
+
constructor(t = document.body, e = 9999) {
|
|
203
|
+
if (this.container = t, this.zIndex = e, !t || !(t instanceof HTMLElement))
|
|
136
204
|
throw new Error(`[Supermouse] Invalid container: ${t}. Must be an HTMLElement.`);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
205
|
+
t.isConnected || console.warn(
|
|
206
|
+
"[Supermouse] container is not attached to the document — stage sizing/positioning will be wrong until it is."
|
|
207
|
+
);
|
|
208
|
+
const s = b++;
|
|
209
|
+
this.id = `supermouse-style-${s}`, this.scopeClass = `supermouse-scope-${s}`, this.hideClass = `supermouse-hide-${s}`;
|
|
210
|
+
const i = t === document.body;
|
|
140
211
|
if (this.element = document.createElement("div"), Object.assign(this.element.style, {
|
|
141
|
-
position:
|
|
212
|
+
position: i ? "fixed" : "absolute",
|
|
142
213
|
inset: "0px",
|
|
143
214
|
pointerEvents: "none",
|
|
144
|
-
zIndex:
|
|
215
|
+
zIndex: String(this.zIndex),
|
|
145
216
|
opacity: "1",
|
|
146
217
|
transition: "opacity 0.15s ease"
|
|
147
|
-
}), !
|
|
148
|
-
const
|
|
149
|
-
this.originalContainerPosition =
|
|
218
|
+
}), !i) {
|
|
219
|
+
const n = window.getComputedStyle(t);
|
|
220
|
+
this.originalContainerPosition = n.position, n.position === "static" && (t.style.position = "relative");
|
|
150
221
|
}
|
|
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.
|
|
222
|
+
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
223
|
}
|
|
153
224
|
container;
|
|
154
|
-
|
|
155
|
-
/** The container element appended to the document. */
|
|
225
|
+
zIndex;
|
|
156
226
|
element;
|
|
157
227
|
styleTag;
|
|
158
228
|
id;
|
|
159
229
|
scopeClass;
|
|
230
|
+
hideClass;
|
|
160
231
|
currentCursorState = null;
|
|
161
232
|
originalContainerPosition = "";
|
|
162
233
|
originalContainerCursor = "";
|
|
234
|
+
/** Selectors that need explicit `cursor: none !important` to override UA styles. */
|
|
163
235
|
selectors = /* @__PURE__ */ new Set([
|
|
164
236
|
"a",
|
|
165
237
|
"button",
|
|
@@ -169,23 +241,26 @@ class v {
|
|
|
169
241
|
'[role="button"]',
|
|
170
242
|
"[tabindex]"
|
|
171
243
|
]);
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
244
|
+
/** Batch add selectors. Comma‑separated groups are split and scoped individually. */
|
|
245
|
+
addSelectors(t) {
|
|
246
|
+
let e = !1;
|
|
247
|
+
for (const s of t)
|
|
248
|
+
s.split(",").forEach((i) => {
|
|
249
|
+
const n = i.trim();
|
|
250
|
+
n && !this.selectors.has(n) && (this.selectors.add(n), e = !0);
|
|
251
|
+
});
|
|
252
|
+
e && this.updateCursorCSS();
|
|
253
|
+
}
|
|
254
|
+
/** Add a single selector (or comma‑separated group). */
|
|
177
255
|
addSelector(t) {
|
|
178
|
-
this.
|
|
256
|
+
this.addSelectors([t]);
|
|
179
257
|
}
|
|
180
258
|
setVisibility(t) {
|
|
181
259
|
this.element.style.opacity = t ? "1" : "0";
|
|
182
260
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Toggles the visibility of the native cursor via CSS injection.
|
|
185
|
-
* @param type 'none' to hide, 'auto' to show.
|
|
186
|
-
*/
|
|
261
|
+
/** Toggle native cursor visibility. */
|
|
187
262
|
setNativeCursor(t) {
|
|
188
|
-
|
|
263
|
+
t !== this.currentCursorState && (this.currentCursorState = t, this.container.classList.toggle(this.hideClass, t === "none"), this.container.style.cursor = t === "none" ? "none" : this.originalContainerCursor);
|
|
189
264
|
}
|
|
190
265
|
updateCursorCSS() {
|
|
191
266
|
const t = Array.from(this.selectors);
|
|
@@ -193,227 +268,241 @@ class v {
|
|
|
193
268
|
this.styleTag.innerText = "";
|
|
194
269
|
return;
|
|
195
270
|
}
|
|
196
|
-
const e =
|
|
271
|
+
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(`
|
|
272
|
+
`), n = `.${this.scopeClass}.${this.hideClass} *${e} { cursor: none !important; }`, r = `.${this.scopeClass}.${this.hideClass} { cursor: none !important; }`;
|
|
197
273
|
this.styleTag.innerText = `
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
274
|
+
${r}
|
|
275
|
+
${n}
|
|
276
|
+
${i}
|
|
277
|
+
${s("label")}
|
|
278
|
+
${s("select")}
|
|
279
|
+
${s('input[type="range"]::-webkit-slider-thumb')}
|
|
280
|
+
${s('input[type="range"]::-moz-range-thumb')}
|
|
281
|
+
`;
|
|
202
282
|
}
|
|
203
283
|
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 = "");
|
|
284
|
+
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
285
|
}
|
|
206
286
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"input",
|
|
211
|
-
"textarea",
|
|
212
|
-
"[data-hover]",
|
|
213
|
-
"[data-cursor]"
|
|
214
|
-
];
|
|
215
|
-
class y {
|
|
216
|
-
static version = d;
|
|
217
|
-
version = d;
|
|
287
|
+
class C {
|
|
288
|
+
static version = c;
|
|
289
|
+
version = c;
|
|
218
290
|
state;
|
|
219
|
-
/**
|
|
220
|
-
* Configuration options.
|
|
221
|
-
*/
|
|
291
|
+
/** Configuration options, fully resolved with defaults applied. */
|
|
222
292
|
options;
|
|
223
293
|
plugins = [];
|
|
224
|
-
|
|
294
|
+
_stage;
|
|
225
295
|
input;
|
|
226
296
|
rafId = 0;
|
|
227
297
|
lastTime = 0;
|
|
228
298
|
isRunning = !1;
|
|
299
|
+
isSuspended = !1;
|
|
300
|
+
visibilityAbortController = new AbortController();
|
|
229
301
|
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
|
-
*/
|
|
302
|
+
hoverSelectorString;
|
|
303
|
+
crashedPlugins = [];
|
|
236
304
|
constructor(t = {}) {
|
|
237
305
|
this.options = {
|
|
238
306
|
smoothness: 0.15,
|
|
239
307
|
enableTouch: !1,
|
|
240
308
|
autoDisableOnMobile: !0,
|
|
241
|
-
|
|
242
|
-
hideCursor: !0,
|
|
309
|
+
cursor: "auto",
|
|
243
310
|
hideOnLeave: !0,
|
|
244
311
|
autoStart: !0,
|
|
245
312
|
container: document.body,
|
|
246
313
|
dataPrefix: "supermouse",
|
|
314
|
+
zIndex: 9999,
|
|
247
315
|
...t
|
|
248
316
|
}, this.state = {
|
|
249
|
-
pointer: {
|
|
250
|
-
target: {
|
|
251
|
-
smooth: {
|
|
317
|
+
pointer: { ...h },
|
|
318
|
+
target: { ...h },
|
|
319
|
+
smooth: { ...h },
|
|
252
320
|
velocity: { x: 0, y: 0 },
|
|
321
|
+
displacement: { x: 0, y: 0 },
|
|
253
322
|
angle: 0,
|
|
254
323
|
isDown: !1,
|
|
255
324
|
isHover: !1,
|
|
256
325
|
isNative: !1,
|
|
257
|
-
|
|
326
|
+
cursorMode: this.options.cursor,
|
|
258
327
|
hoverTarget: null,
|
|
259
328
|
reducedMotion: !1,
|
|
260
329
|
hasReceivedInput: !1,
|
|
261
330
|
shape: null,
|
|
262
331
|
interaction: {}
|
|
263
|
-
}, this.
|
|
332
|
+
}, 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
333
|
this.state,
|
|
265
334
|
this.options,
|
|
266
|
-
() =>
|
|
335
|
+
() => this.hoverSelectorString,
|
|
267
336
|
(e) => {
|
|
268
337
|
e || this.reset(!0);
|
|
269
338
|
}
|
|
270
|
-
), this.options.plugins
|
|
339
|
+
), this.options.plugins?.forEach((e) => this.use(e)), this.bindVisibilityHandling(), this.init();
|
|
271
340
|
}
|
|
272
|
-
/**
|
|
273
|
-
* Retrieves a registered plugin instance by its unique name.
|
|
274
|
-
*/
|
|
341
|
+
/** Look up a registered plugin by name. */
|
|
275
342
|
getPlugin(t) {
|
|
276
343
|
return this.plugins.find((e) => e.name === t);
|
|
277
344
|
}
|
|
278
|
-
/**
|
|
279
|
-
* Returns whether the cursor system is currently enabled (processing input).
|
|
280
|
-
*/
|
|
345
|
+
/** Whether the instance is not disabled/suspended and is processing input. */
|
|
281
346
|
get isEnabled() {
|
|
282
347
|
return this.input.isEnabled;
|
|
283
348
|
}
|
|
284
|
-
/**
|
|
285
|
-
* Enables a specific plugin by name.
|
|
286
|
-
* Triggers the `onEnable` lifecycle hook of the plugin.
|
|
287
|
-
*/
|
|
349
|
+
/** Enable a plugin by name. */
|
|
288
350
|
enablePlugin(t) {
|
|
289
351
|
const e = this.getPlugin(t);
|
|
290
|
-
e && e.isEnabled === !1 && (e.isEnabled = !0, e.onEnable?.(this));
|
|
352
|
+
e && e.isEnabled === !1 && (e.isEnabled = !0, e.element && (e.element.style.display = ""), e.onEnable?.(this));
|
|
291
353
|
}
|
|
292
|
-
/**
|
|
293
|
-
* Disables a specific plugin by name.
|
|
294
|
-
* Triggers the `onDisable` lifecycle hook.
|
|
295
|
-
*/
|
|
354
|
+
/** Disable a plugin by name and hide its element. */
|
|
296
355
|
disablePlugin(t) {
|
|
297
356
|
const e = this.getPlugin(t);
|
|
298
|
-
e && e.isEnabled !== !1
|
|
357
|
+
if (e && e.isEnabled !== !1) {
|
|
358
|
+
e.isEnabled = !1;
|
|
359
|
+
const s = () => {
|
|
360
|
+
e.element && (e.element.style.display = "none"), e.onDisable?.(this);
|
|
361
|
+
}, i = e.onBeforeDisable?.(this);
|
|
362
|
+
i && typeof i.then == "function" ? Promise.resolve(i).then(s).catch((n) => {
|
|
363
|
+
console.error(`[Supermouse] Plugin '${e.name}' onBeforeDisable threw:`, n), s();
|
|
364
|
+
}) : s();
|
|
365
|
+
}
|
|
299
366
|
}
|
|
300
|
-
/**
|
|
301
|
-
* Toggles the enabled state of a plugin.
|
|
302
|
-
*/
|
|
367
|
+
/** Toggle a plugin's enabled state by name. */
|
|
303
368
|
togglePlugin(t) {
|
|
304
369
|
const e = this.getPlugin(t);
|
|
305
370
|
e && (e.isEnabled === !1 ? this.enablePlugin(t) : this.disablePlugin(t));
|
|
306
371
|
}
|
|
372
|
+
/** Add a selector to hover detection and cursor suppression. */
|
|
307
373
|
registerHoverTarget(t) {
|
|
308
|
-
this.hoverSelectors.has(t) || (this.hoverSelectors.add(t), this.
|
|
374
|
+
this.hoverSelectors.has(t) || (this.hoverSelectors.add(t), this.hoverSelectorString = Array.from(this.hoverSelectors).join(", "), this._stage.addSelector(t));
|
|
309
375
|
}
|
|
310
|
-
/**
|
|
311
|
-
* The fixed container element where plugins should append their DOM nodes.
|
|
312
|
-
*/
|
|
376
|
+
/** The DOM element the instance is scoped to. */
|
|
313
377
|
get container() {
|
|
314
|
-
return this.
|
|
378
|
+
return this.options.container;
|
|
315
379
|
}
|
|
316
|
-
/**
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
this.state.
|
|
380
|
+
/** The stage element that plugins append their visuals into. */
|
|
381
|
+
get stage() {
|
|
382
|
+
return this._stage.element;
|
|
383
|
+
}
|
|
384
|
+
/** Set the current cursor mode. */
|
|
385
|
+
setCursor(t) {
|
|
386
|
+
this.state.cursorMode = t;
|
|
323
387
|
}
|
|
324
388
|
init() {
|
|
325
389
|
this.options.autoStart && this.startLoop();
|
|
326
390
|
}
|
|
391
|
+
/** Re‑enable input processing and re‑apply cursor state. */
|
|
327
392
|
enable() {
|
|
328
|
-
this.input.isEnabled = !0, this.
|
|
393
|
+
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
394
|
}
|
|
395
|
+
/** Disable input processing and restore native cursor. */
|
|
330
396
|
disable() {
|
|
331
|
-
this.input.isEnabled = !1, this.
|
|
397
|
+
this.input.isEnabled = !1, this._stage.setNativeCursor("auto"), this._stage.setVisibility(!1), this.reset(!0);
|
|
398
|
+
}
|
|
399
|
+
/** Temporarily yield to a scoped instance. */
|
|
400
|
+
suspend() {
|
|
401
|
+
this.input.isEnabled && (this.isSuspended = !0, this.input.isEnabled = !1, this.input.clearHover(), this._stage.setVisibility(!1));
|
|
402
|
+
}
|
|
403
|
+
/** Resume from `suspend()`. */
|
|
404
|
+
resume() {
|
|
405
|
+
if (this.isSuspended) {
|
|
406
|
+
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());
|
|
407
|
+
for (let t = this.plugins.length - 1; t >= 0; t--)
|
|
408
|
+
this.runPluginSafe(this.plugins[t], 0);
|
|
409
|
+
this._stage.setVisibility(!0);
|
|
410
|
+
}
|
|
332
411
|
}
|
|
333
|
-
/**
|
|
334
|
-
* Registers a new plugin.
|
|
335
|
-
*
|
|
336
|
-
* @param plugin - The plugin object to install.
|
|
337
|
-
*/
|
|
412
|
+
/** Register a new plugin. */
|
|
338
413
|
use(t) {
|
|
339
|
-
if (this.plugins.some((
|
|
414
|
+
if (this.plugins.some((e) => e.name === t.name))
|
|
340
415
|
return console.warn(`[Supermouse] Plugin "${t.name}" already installed.`), this;
|
|
341
|
-
t.isEnabled
|
|
416
|
+
t.isEnabled ??= !0;
|
|
342
417
|
try {
|
|
343
418
|
t.install?.(this);
|
|
344
|
-
} catch (
|
|
345
|
-
return console.error(`[Supermouse] Failed to install plugin '${t.name}'.`,
|
|
419
|
+
} catch (e) {
|
|
420
|
+
return console.error(`[Supermouse] Failed to install plugin '${t.name}'.`, e), this;
|
|
346
421
|
}
|
|
347
|
-
return this.plugins.push(t), this.plugins.sort((
|
|
422
|
+
return this.plugins.push(t), this.plugins.sort((e, s) => (e.priority ?? 0) - (s.priority ?? 0)), this;
|
|
348
423
|
}
|
|
424
|
+
/** Reset physics; optionally clear all input state. */
|
|
349
425
|
reset(t = !1) {
|
|
350
|
-
this.state.
|
|
426
|
+
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
427
|
}
|
|
352
428
|
startLoop() {
|
|
353
|
-
this.isRunning || (this.isRunning = !0, this.lastTime = performance.now(), this.
|
|
429
|
+
this.isRunning || (this.isRunning = !0, !document.hidden && (this.lastTime = performance.now(), this.rafId = requestAnimationFrame(this.tick)));
|
|
354
430
|
}
|
|
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
|
-
*/
|
|
431
|
+
/** Start the animation loop. */
|
|
359
432
|
start() {
|
|
360
433
|
this.startLoop();
|
|
361
434
|
}
|
|
362
|
-
/**
|
|
363
|
-
* Manually steps the animation loop.
|
|
364
|
-
*
|
|
365
|
-
* @param time Current timestamp in milliseconds.
|
|
366
|
-
*/
|
|
435
|
+
/** Manually step the animation loop. */
|
|
367
436
|
step(t) {
|
|
368
|
-
this.
|
|
437
|
+
this.update(t);
|
|
369
438
|
}
|
|
370
439
|
runPluginSafe(t, e) {
|
|
371
440
|
if (t.isEnabled !== !1)
|
|
372
441
|
try {
|
|
373
442
|
t.update?.(this, e);
|
|
374
|
-
} catch (
|
|
375
|
-
console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`,
|
|
376
|
-
|
|
377
|
-
|
|
443
|
+
} catch (s) {
|
|
444
|
+
console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`, s), t.isEnabled = !1, this.crashedPlugins.push(t);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
cleanupCrashedPlugins() {
|
|
448
|
+
if (this.crashedPlugins.length !== 0) {
|
|
449
|
+
for (const t of this.crashedPlugins) {
|
|
450
|
+
const e = this.plugins.indexOf(t);
|
|
451
|
+
e > -1 && this.plugins.splice(e, 1);
|
|
378
452
|
try {
|
|
379
|
-
t.
|
|
453
|
+
t.onDisable?.(this), t.destroy?.(this);
|
|
380
454
|
} catch (s) {
|
|
381
455
|
console.error(`[Supermouse] Failed to cleanup crashed plugin '${t.name}'.`, s);
|
|
382
456
|
}
|
|
457
|
+
t.element?.remove();
|
|
383
458
|
}
|
|
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);
|
|
459
|
+
this.crashedPlugins = [];
|
|
395
460
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
461
|
+
}
|
|
462
|
+
resolveStageVisibility() {
|
|
463
|
+
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;
|
|
464
|
+
}
|
|
465
|
+
resolveCursorState() {
|
|
466
|
+
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";
|
|
467
|
+
}
|
|
468
|
+
resetMotion() {
|
|
469
|
+
this.state.velocity = { x: 0, y: 0 }, this.state.displacement = { x: 0, y: 0 };
|
|
470
|
+
}
|
|
471
|
+
update(t) {
|
|
472
|
+
const e = t - this.lastTime, s = Math.min(e / 1e3, 0.1);
|
|
473
|
+
this.lastTime = t;
|
|
474
|
+
const i = this.input.getCurrentTarget();
|
|
475
|
+
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);
|
|
476
|
+
for (let n = 0; n < this.plugins.length; n++)
|
|
477
|
+
this.runPluginSafe(this.plugins[n], e);
|
|
478
|
+
if (this.cleanupCrashedPlugins(), this.input.isEnabled) {
|
|
479
|
+
const n = this.state.reducedMotion ? 1e3 : 1 / this.options.smoothness * 2, r = this.state.smooth.x, a = this.state.smooth.y;
|
|
480
|
+
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);
|
|
481
|
+
const { x: l, y: u } = this.state.velocity;
|
|
482
|
+
(Math.abs(l) > 0.1 || Math.abs(u) > 0.1) && (this.state.angle = Math.atan2(u, l) * (180 / Math.PI));
|
|
404
483
|
}
|
|
405
|
-
|
|
484
|
+
}
|
|
485
|
+
tick = (t) => {
|
|
486
|
+
this.update(t), this.isRunning && (this.rafId = requestAnimationFrame(this.tick));
|
|
406
487
|
};
|
|
407
|
-
/**
|
|
408
|
-
|
|
409
|
-
|
|
488
|
+
/** Pause rAF loop when tab hidden; resume on visible. */
|
|
489
|
+
bindVisibilityHandling() {
|
|
490
|
+
document.addEventListener(
|
|
491
|
+
"visibilitychange",
|
|
492
|
+
() => {
|
|
493
|
+
this.isRunning && (document.hidden ? cancelAnimationFrame(this.rafId) : (this.lastTime = performance.now(), this.rafId = requestAnimationFrame(this.tick)));
|
|
494
|
+
},
|
|
495
|
+
{ signal: this.visibilityAbortController.signal }
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
/** Destroy the instance, freeing all resources. */
|
|
410
499
|
destroy() {
|
|
411
|
-
this.isRunning = !1, cancelAnimationFrame(this.rafId), this.input.destroy(), this.
|
|
500
|
+
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
501
|
}
|
|
413
502
|
}
|
|
414
503
|
export {
|
|
415
504
|
m as DEFAULT_HOVER_SELECTORS,
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
505
|
+
v as Input,
|
|
506
|
+
y as Stage,
|
|
507
|
+
C as Supermouse
|
|
419
508
|
};
|