@supermousejs/core 2.2.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 +44 -0
- package/dist/index.d.ts +113 -127
- package/dist/index.mjs +329 -228
- package/dist/index.umd.js +10 -5
- package/package.json +4 -3
- package/src/Supermouse.ts +953 -708
- 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 +21 -0
- package/src/__tests__/stage.spec.ts +235 -0
- package/src/__tests__/suspend-resume.spec.ts +50 -0
- package/src/types.ts +165 -168
- package/vitest.config.ts +10 -0
package/dist/index.mjs
CHANGED
|
@@ -1,43 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const c = "2.4.0";
|
|
2
|
+
function p(o, t, e) {
|
|
3
|
+
return o + (t - o) * e;
|
|
3
4
|
}
|
|
4
|
-
function d(
|
|
5
|
-
return
|
|
5
|
+
function d(o, t, e, s) {
|
|
6
|
+
return p(o, t, 1 - Math.exp(-e * s));
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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();
|
|
13
27
|
}
|
|
14
28
|
state;
|
|
15
29
|
options;
|
|
16
30
|
getHoverSelector;
|
|
17
31
|
onEnableChange;
|
|
18
32
|
mediaQueryList;
|
|
19
|
-
mediaQueryHandler;
|
|
20
33
|
motionQuery;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*/
|
|
25
|
-
isEnabled = !0;
|
|
34
|
+
dataPrefix;
|
|
35
|
+
normalizedDataPrefix;
|
|
36
|
+
ignoreAttribute;
|
|
26
37
|
abortController = new AbortController();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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;
|
|
30
50
|
checkDeviceCapability() {
|
|
31
|
-
this.options.autoDisableOnMobile && (this.mediaQueryList = window.matchMedia("(pointer: fine)"), this.updateEnabledState(this.mediaQueryList.matches), this.
|
|
32
|
-
this.updateEnabledState(t.matches);
|
|
33
|
-
}, 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), {
|
|
34
52
|
signal: this.abortController.signal
|
|
35
53
|
}));
|
|
36
54
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Checks for `prefers-reduced-motion`.
|
|
39
|
-
* If true, the core physics engine will switch to instant snapping (high damping) to avoid motion sickness.
|
|
40
|
-
*/
|
|
41
55
|
checkMotionPreference() {
|
|
42
56
|
this.motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"), this.state.reducedMotion = this.motionQuery.matches, this.motionQuery.addEventListener(
|
|
43
57
|
"change",
|
|
@@ -50,112 +64,167 @@ class f {
|
|
|
50
64
|
updateEnabledState(t) {
|
|
51
65
|
this.isEnabled = t, this.onEnableChange(t);
|
|
52
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
|
+
*/
|
|
53
83
|
parseDOMInteraction(t) {
|
|
54
|
-
if (this.
|
|
55
|
-
this.
|
|
56
|
-
|
|
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 });
|
|
57
88
|
}
|
|
58
89
|
const e = {};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
t
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const r = s.charAt(0).toLowerCase() + s.slice(1), a = i[o];
|
|
68
|
-
a !== void 0 && (e[r] = a === "" ? !0 : a);
|
|
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);
|
|
70
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;
|
|
106
|
+
}
|
|
71
107
|
this.state.interaction = e;
|
|
72
108
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
79
128
|
}
|
|
80
|
-
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);
|
|
81
129
|
}
|
|
82
|
-
|
|
83
|
-
|
|
130
|
+
isOutsideContainer(t) {
|
|
131
|
+
const { container: e } = this.options;
|
|
132
|
+
return !!e && e !== document.body && !e.contains(t);
|
|
84
133
|
}
|
|
85
|
-
|
|
86
|
-
|
|
134
|
+
resolveComputedCursor(t) {
|
|
135
|
+
return window.getComputedStyle(t).cursor;
|
|
87
136
|
}
|
|
88
|
-
|
|
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) => {
|
|
89
151
|
if (!this.isEnabled) return;
|
|
90
152
|
const e = t.target;
|
|
91
|
-
if (
|
|
92
|
-
|
|
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;
|
|
93
156
|
return;
|
|
94
157
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const r = s === !0 || s === "auto" || s === "tag", a = s === !0 || s === "auto" || s === "css";
|
|
100
|
-
let l = !1;
|
|
101
|
-
if (r) {
|
|
102
|
-
const h = e.localName;
|
|
103
|
-
(h === "input" || h === "textarea" || h === "select" || e.isContentEditable) && (l = !0);
|
|
104
|
-
}
|
|
105
|
-
if (!l && a) {
|
|
106
|
-
const h = window.getComputedStyle(e).cursor;
|
|
107
|
-
["default", "auto", "pointer", "none", "inherit"].includes(h) || (l = !0);
|
|
108
|
-
}
|
|
109
|
-
l && (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;
|
|
110
162
|
}
|
|
111
|
-
|
|
112
|
-
|
|
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) => {
|
|
113
169
|
if (!this.isEnabled) return;
|
|
114
|
-
const e = t.target;
|
|
115
|
-
(e === this.state.hoverTarget || e.contains(this.state.hoverTarget)) && (!
|
|
116
|
-
}
|
|
117
|
-
handleWindowLeave() {
|
|
118
|
-
this.options.hideOnLeave && (this.state.hasReceivedInput = !1);
|
|
119
|
-
}
|
|
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
|
+
};
|
|
120
176
|
clearHover() {
|
|
121
|
-
this.state.isHover = !1, this.state.hoverTarget = null, this.state.isNative = !1;
|
|
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;
|
|
122
182
|
}
|
|
123
183
|
bindEvents() {
|
|
124
184
|
const { signal: t } = this.abortController;
|
|
125
|
-
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 });
|
|
126
188
|
}
|
|
127
189
|
destroy() {
|
|
128
|
-
this.abortController.abort();
|
|
190
|
+
this.abortController.abort(), this.resizeObserver?.disconnect();
|
|
129
191
|
}
|
|
130
192
|
}
|
|
131
|
-
let
|
|
132
|
-
class
|
|
133
|
-
constructor(t = document.body, e) {
|
|
134
|
-
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))
|
|
135
197
|
throw new Error(`[Supermouse] Invalid container: ${t}. Must be an HTMLElement.`);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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;
|
|
204
|
+
if (this.element = document.createElement("div"), Object.assign(this.element.style, {
|
|
205
|
+
position: i ? "fixed" : "absolute",
|
|
206
|
+
inset: "0px",
|
|
145
207
|
pointerEvents: "none",
|
|
146
|
-
zIndex:
|
|
208
|
+
zIndex: String(this.zIndex),
|
|
147
209
|
opacity: "1",
|
|
148
210
|
transition: "opacity 0.15s ease"
|
|
149
|
-
}),
|
|
211
|
+
}), !i) {
|
|
212
|
+
const n = window.getComputedStyle(t);
|
|
213
|
+
this.originalContainerPosition = n.position, n.position === "static" && (t.style.position = "relative");
|
|
214
|
+
}
|
|
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();
|
|
150
216
|
}
|
|
151
217
|
container;
|
|
152
|
-
|
|
153
|
-
/** The container element appended to the document. */
|
|
218
|
+
zIndex;
|
|
154
219
|
element;
|
|
155
220
|
styleTag;
|
|
156
221
|
id;
|
|
157
222
|
scopeClass;
|
|
223
|
+
hideClass;
|
|
158
224
|
currentCursorState = null;
|
|
225
|
+
originalContainerPosition = "";
|
|
226
|
+
originalContainerCursor = "";
|
|
227
|
+
/** Selectors that need explicit `cursor: none !important` to override UA styles. */
|
|
159
228
|
selectors = /* @__PURE__ */ new Set([
|
|
160
229
|
"a",
|
|
161
230
|
"button",
|
|
@@ -165,23 +234,26 @@ class v {
|
|
|
165
234
|
'[role="button"]',
|
|
166
235
|
"[tabindex]"
|
|
167
236
|
]);
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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). */
|
|
173
248
|
addSelector(t) {
|
|
174
|
-
this.
|
|
249
|
+
this.addSelectors([t]);
|
|
175
250
|
}
|
|
176
251
|
setVisibility(t) {
|
|
177
252
|
this.element.style.opacity = t ? "1" : "0";
|
|
178
253
|
}
|
|
179
|
-
/**
|
|
180
|
-
* Toggles the visibility of the native cursor via CSS injection.
|
|
181
|
-
* @param type 'none' to hide, 'auto' to show.
|
|
182
|
-
*/
|
|
254
|
+
/** Toggle native cursor visibility. */
|
|
183
255
|
setNativeCursor(t) {
|
|
184
|
-
|
|
256
|
+
t !== this.currentCursorState && (this.currentCursorState = t, this.container.classList.toggle(this.hideClass, t === "none"), this.container.style.cursor = t === "none" ? "none" : this.originalContainerCursor);
|
|
185
257
|
}
|
|
186
258
|
updateCursorCSS() {
|
|
187
259
|
const t = Array.from(this.selectors);
|
|
@@ -189,212 +261,241 @@ class v {
|
|
|
189
261
|
this.styleTag.innerText = "";
|
|
190
262
|
return;
|
|
191
263
|
}
|
|
192
|
-
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; }`;
|
|
193
266
|
this.styleTag.innerText = `
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
`;
|
|
198
275
|
}
|
|
199
276
|
destroy() {
|
|
200
|
-
this.element.remove(), this.styleTag.remove(), this.container.style.cursor =
|
|
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 = "");
|
|
201
278
|
}
|
|
202
279
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"input",
|
|
207
|
-
"textarea",
|
|
208
|
-
"[data-hover]",
|
|
209
|
-
"[data-cursor]"
|
|
210
|
-
];
|
|
211
|
-
class y {
|
|
212
|
-
static version = "2.2.0";
|
|
213
|
-
version = "2.2.0";
|
|
280
|
+
class C {
|
|
281
|
+
static version = c;
|
|
282
|
+
version = c;
|
|
214
283
|
state;
|
|
215
|
-
/**
|
|
216
|
-
* Configuration options.
|
|
217
|
-
*/
|
|
284
|
+
/** Configuration options, fully resolved with defaults applied. */
|
|
218
285
|
options;
|
|
219
286
|
plugins = [];
|
|
220
|
-
|
|
287
|
+
_stage;
|
|
221
288
|
input;
|
|
222
289
|
rafId = 0;
|
|
223
290
|
lastTime = 0;
|
|
224
291
|
isRunning = !1;
|
|
292
|
+
isSuspended = !1;
|
|
293
|
+
visibilityAbortController = new AbortController();
|
|
225
294
|
hoverSelectors;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
*
|
|
229
|
-
* @param options - Global configuration options.
|
|
230
|
-
* @throws Will throw if running in a non-browser environment (window/document undefined).
|
|
231
|
-
*/
|
|
295
|
+
hoverSelectorString;
|
|
296
|
+
crashedPlugins = [];
|
|
232
297
|
constructor(t = {}) {
|
|
233
298
|
this.options = {
|
|
234
299
|
smoothness: 0.15,
|
|
235
300
|
enableTouch: !1,
|
|
236
301
|
autoDisableOnMobile: !0,
|
|
237
|
-
|
|
238
|
-
hideCursor: !0,
|
|
302
|
+
cursor: "auto",
|
|
239
303
|
hideOnLeave: !0,
|
|
240
304
|
autoStart: !0,
|
|
241
305
|
container: document.body,
|
|
306
|
+
dataPrefix: "supermouse",
|
|
307
|
+
zIndex: 9999,
|
|
242
308
|
...t
|
|
243
|
-
}, this.
|
|
244
|
-
pointer: {
|
|
245
|
-
target: {
|
|
246
|
-
smooth: {
|
|
309
|
+
}, this.state = {
|
|
310
|
+
pointer: { ...h },
|
|
311
|
+
target: { ...h },
|
|
312
|
+
smooth: { ...h },
|
|
247
313
|
velocity: { x: 0, y: 0 },
|
|
314
|
+
displacement: { x: 0, y: 0 },
|
|
248
315
|
angle: 0,
|
|
249
316
|
isDown: !1,
|
|
250
317
|
isHover: !1,
|
|
251
318
|
isNative: !1,
|
|
252
|
-
|
|
319
|
+
cursorMode: this.options.cursor,
|
|
253
320
|
hoverTarget: null,
|
|
254
321
|
reducedMotion: !1,
|
|
255
322
|
hasReceivedInput: !1,
|
|
256
323
|
shape: null,
|
|
257
324
|
interaction: {}
|
|
258
|
-
}, 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(
|
|
259
326
|
this.state,
|
|
260
327
|
this.options,
|
|
261
|
-
() =>
|
|
328
|
+
() => this.hoverSelectorString,
|
|
262
329
|
(e) => {
|
|
263
|
-
e || this.
|
|
330
|
+
e || this.reset(!0);
|
|
264
331
|
}
|
|
265
|
-
), this.options.plugins
|
|
332
|
+
), this.options.plugins?.forEach((e) => this.use(e)), this.bindVisibilityHandling(), this.init();
|
|
266
333
|
}
|
|
267
|
-
/**
|
|
268
|
-
* Retrieves a registered plugin instance by its unique name.
|
|
269
|
-
*/
|
|
334
|
+
/** Look up a registered plugin by name. */
|
|
270
335
|
getPlugin(t) {
|
|
271
336
|
return this.plugins.find((e) => e.name === t);
|
|
272
337
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Returns whether the cursor system is currently enabled (processing input).
|
|
275
|
-
*/
|
|
338
|
+
/** Whether the instance is not disabled/suspended and is processing input. */
|
|
276
339
|
get isEnabled() {
|
|
277
340
|
return this.input.isEnabled;
|
|
278
341
|
}
|
|
279
|
-
/**
|
|
280
|
-
* Enables a specific plugin by name.
|
|
281
|
-
* Triggers the `onEnable` lifecycle hook of the plugin.
|
|
282
|
-
*/
|
|
342
|
+
/** Enable a plugin by name. */
|
|
283
343
|
enablePlugin(t) {
|
|
284
344
|
const e = this.getPlugin(t);
|
|
285
|
-
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));
|
|
286
346
|
}
|
|
287
|
-
/**
|
|
288
|
-
* Disables a specific plugin by name.
|
|
289
|
-
* Triggers the `onDisable` lifecycle hook.
|
|
290
|
-
*/
|
|
347
|
+
/** Disable a plugin by name and hide its element. */
|
|
291
348
|
disablePlugin(t) {
|
|
292
349
|
const e = this.getPlugin(t);
|
|
293
|
-
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
|
+
}
|
|
294
359
|
}
|
|
295
|
-
/**
|
|
296
|
-
* Toggles the enabled state of a plugin.
|
|
297
|
-
*/
|
|
360
|
+
/** Toggle a plugin's enabled state by name. */
|
|
298
361
|
togglePlugin(t) {
|
|
299
362
|
const e = this.getPlugin(t);
|
|
300
363
|
e && (e.isEnabled === !1 ? this.enablePlugin(t) : this.disablePlugin(t));
|
|
301
364
|
}
|
|
365
|
+
/** Add a selector to hover detection and cursor suppression. */
|
|
302
366
|
registerHoverTarget(t) {
|
|
303
|
-
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));
|
|
304
368
|
}
|
|
305
|
-
/**
|
|
306
|
-
* The fixed container element where plugins should append their DOM nodes.
|
|
307
|
-
*/
|
|
369
|
+
/** The DOM element the instance is scoped to. */
|
|
308
370
|
get container() {
|
|
309
|
-
return this.
|
|
371
|
+
return this.options.container;
|
|
310
372
|
}
|
|
311
|
-
/**
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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. */
|
|
316
378
|
setCursor(t) {
|
|
317
|
-
this.state.
|
|
379
|
+
this.state.cursorMode = t;
|
|
318
380
|
}
|
|
319
381
|
init() {
|
|
320
382
|
this.options.autoStart && this.startLoop();
|
|
321
383
|
}
|
|
384
|
+
/** Re‑enable input processing and re‑apply cursor state. */
|
|
322
385
|
enable() {
|
|
323
|
-
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());
|
|
324
387
|
}
|
|
388
|
+
/** Disable input processing and restore native cursor. */
|
|
325
389
|
disable() {
|
|
326
|
-
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
|
+
}
|
|
327
404
|
}
|
|
328
|
-
/**
|
|
329
|
-
* Registers a new plugin.
|
|
330
|
-
*
|
|
331
|
-
* @param plugin - The plugin object to install.
|
|
332
|
-
*/
|
|
405
|
+
/** Register a new plugin. */
|
|
333
406
|
use(t) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
407
|
+
if (this.plugins.some((e) => e.name === t.name))
|
|
408
|
+
return console.warn(`[Supermouse] Plugin "${t.name}" already installed.`), this;
|
|
409
|
+
t.isEnabled ??= !0;
|
|
410
|
+
try {
|
|
411
|
+
t.install?.(this);
|
|
412
|
+
} catch (e) {
|
|
413
|
+
return console.error(`[Supermouse] Failed to install plugin '${t.name}'.`, e), this;
|
|
414
|
+
}
|
|
415
|
+
return this.plugins.push(t), this.plugins.sort((e, s) => (e.priority ?? 0) - (s.priority ?? 0)), this;
|
|
338
416
|
}
|
|
339
|
-
|
|
340
|
-
|
|
417
|
+
/** Reset physics; optionally clear all input state. */
|
|
418
|
+
reset(t = !1) {
|
|
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 = {});
|
|
341
420
|
}
|
|
342
421
|
startLoop() {
|
|
343
|
-
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)));
|
|
344
423
|
}
|
|
345
|
-
/**
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
424
|
+
/** Start the animation loop. */
|
|
425
|
+
start() {
|
|
426
|
+
this.startLoop();
|
|
427
|
+
}
|
|
428
|
+
/** Manually step the animation loop. */
|
|
350
429
|
step(t) {
|
|
351
|
-
this.
|
|
430
|
+
this.update(t);
|
|
352
431
|
}
|
|
353
432
|
runPluginSafe(t, e) {
|
|
354
433
|
if (t.isEnabled !== !1)
|
|
355
434
|
try {
|
|
356
435
|
t.update?.(this, e);
|
|
357
|
-
} catch (
|
|
358
|
-
console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`,
|
|
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);
|
|
359
445
|
try {
|
|
360
|
-
t.onDisable?.(this);
|
|
361
|
-
} catch (
|
|
362
|
-
console.error(`[Supermouse] Failed to
|
|
446
|
+
t.onDisable?.(this), t.destroy?.(this);
|
|
447
|
+
} catch (s) {
|
|
448
|
+
console.error(`[Supermouse] Failed to cleanup crashed plugin '${t.name}'.`, s);
|
|
363
449
|
}
|
|
450
|
+
t.element?.remove();
|
|
364
451
|
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Runs on every animation frame.
|
|
368
|
-
*/
|
|
369
|
-
tick = (t) => {
|
|
370
|
-
const e = t - this.lastTime, i = Math.min(e / 1e3, 0.1);
|
|
371
|
-
this.lastTime = t, this.state.hoverTarget && !this.state.hoverTarget.isConnected && this.input.clearHover();
|
|
372
|
-
const o = this.input.isEnabled && !this.state.isNative && this.state.hasReceivedInput;
|
|
373
|
-
if (this.stage.setVisibility(o), this.input.isEnabled && this.options.hideCursor) {
|
|
374
|
-
let s = "auto";
|
|
375
|
-
this.state.forcedCursor !== null ? s = this.state.forcedCursor : s = this.state.isNative || !this.state.hasReceivedInput ? "auto" : "none", this.stage.setNativeCursor(s);
|
|
452
|
+
this.crashedPlugins = [];
|
|
376
453
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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));
|
|
385
476
|
}
|
|
386
|
-
|
|
477
|
+
}
|
|
478
|
+
tick = (t) => {
|
|
479
|
+
this.update(t), this.isRunning && (this.rafId = requestAnimationFrame(this.tick));
|
|
387
480
|
};
|
|
388
|
-
/**
|
|
389
|
-
|
|
390
|
-
|
|
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. */
|
|
391
492
|
destroy() {
|
|
392
|
-
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 = [];
|
|
393
494
|
}
|
|
394
495
|
}
|
|
395
496
|
export {
|
|
396
497
|
m as DEFAULT_HOVER_SELECTORS,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
498
|
+
v as Input,
|
|
499
|
+
y as Stage,
|
|
500
|
+
C as Supermouse
|
|
400
501
|
};
|