@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/dist/index.mjs CHANGED
@@ -1,43 +1,57 @@
1
- function c(n, t, e) {
2
- return n + (t - n) * e;
1
+ const c = "2.4.0";
2
+ function p(o, t, e) {
3
+ return o + (t - o) * e;
3
4
  }
4
- function d(n, t, e, i) {
5
- return c(n, t, 1 - Math.exp(-e * i));
5
+ function d(o, t, e, s) {
6
+ return p(o, t, 1 - Math.exp(-e * s));
6
7
  }
7
- function p(n, t) {
8
- return Math.atan2(t, n) * (180 / Math.PI);
9
- }
10
- class f {
11
- constructor(t, e, i, o) {
12
- this.state = t, this.options = e, this.getHoverSelector = i, this.onEnableChange = o, this.checkDeviceCapability(), this.checkMotionPreference(), this.bindEvents();
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
- * Master switch for input processing.
23
- * Toggled by `Supermouse.enable()`/`disable()` or automatically by device capability checks.
24
- */
25
- isEnabled = !0;
34
+ dataPrefix;
35
+ normalizedDataPrefix;
36
+ ignoreAttribute;
26
37
  abortController = new AbortController();
27
- /**
28
- * Automatically disables the custom cursor on devices without fine pointer control.
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.mediaQueryHandler = (t) => {
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.options.resolveInteraction) {
55
- this.state.interaction = this.options.resolveInteraction(t);
56
- return;
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
- if (this.options.rules)
60
- for (const [o, s] of Object.entries(this.options.rules))
61
- t.matches(o) && Object.assign(e, s);
62
- const i = t.dataset;
63
- for (const o in i)
64
- if (o.startsWith("supermouse")) {
65
- const s = o.slice(10);
66
- if (s) {
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
- handleMove(t) {
74
- if (!this.isEnabled || this.options.autoDisableOnMobile && t.pointerType === "touch") return;
75
- let e = t.clientX, i = t.clientY;
76
- if (this.options.container && this.options.container !== document.body) {
77
- const o = this.options.container.getBoundingClientRect();
78
- e -= o.left, i -= o.top;
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
- handleDown() {
83
- this.isEnabled && (this.state.isDown = !0);
130
+ isOutsideContainer(t) {
131
+ const { container: e } = this.options;
132
+ return !!e && e !== document.body && !e.contains(t);
84
133
  }
85
- handleUp() {
86
- this.isEnabled && (this.state.isDown = !1);
134
+ resolveComputedCursor(t) {
135
+ return window.getComputedStyle(t).cursor;
87
136
  }
88
- handleMouseOver(t) {
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 (e.closest("[data-supermouse-ignore]")) {
92
- this.state.isNative = !0;
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
- const i = this.getHoverSelector(), o = e.closest(i);
96
- o && (this.state.isHover = !0, this.state.hoverTarget = o, this.parseDOMInteraction(this.state.hoverTarget));
97
- const s = this.options.ignoreOnNative;
98
- if (s) {
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
- handleMouseOut(t) {
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)) && (!t.relatedTarget || !this.state.hoverTarget?.contains(t.relatedTarget)) && (this.state.isHover = !1, this.state.hoverTarget = null, this.state.interaction = {}), this.state.isNative && (this.state.isNative = !1);
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.bind(this), { passive: !0, signal: t }), window.addEventListener("pointerdown", this.handleDown.bind(this), { passive: !0, signal: t }), window.addEventListener("pointerup", this.handleUp.bind(this), { signal: t }), document.addEventListener("mouseover", this.handleMouseOver.bind(this), { signal: t }), document.addEventListener("mouseout", this.handleMouseOut.bind(this), { signal: t }), document.addEventListener("mouseleave", this.handleWindowLeave.bind(this), { signal: t });
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 g = 0;
132
- class v {
133
- constructor(t = document.body, e) {
134
- if (this.container = t, this.hideNativeCursor = e, !t || !(t instanceof HTMLElement))
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
- const i = g++;
137
- this.id = `supermouse-style-${i}`, this.scopeClass = `supermouse-scope-${i}`;
138
- const o = t === document.body;
139
- this.element = document.createElement("div"), Object.assign(this.element.style, {
140
- position: o ? "fixed" : "absolute",
141
- top: "0",
142
- left: "0",
143
- width: "100%",
144
- height: "100%",
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: "9999",
208
+ zIndex: String(this.zIndex),
147
209
  opacity: "1",
148
210
  transition: "opacity 0.15s ease"
149
- }), o || window.getComputedStyle(t).position === "static" && (t.style.position = "relative"), 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.hideNativeCursor && this.setNativeCursor("none");
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
- hideNativeCursor;
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
- * Adds a new CSS selector to the `selectors` set.
170
- * Called by `Supermouse` and subsequently plugins during install to ensure
171
- * the native cursor is hidden on their specific interactive targets.
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.selectors.add(t), this.hideNativeCursor && this.updateCursorCSS();
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
- !this.hideNativeCursor && t === "none" || t !== this.currentCursorState && (this.currentCursorState = t, t === "none" ? (this.container.style.cursor = "none", this.updateCursorCSS()) : (this.container.style.cursor = "", this.styleTag.innerText = ""));
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 = t.map((i) => `.${this.scopeClass} ${i}`).join(", ");
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
- ${e} {
195
- cursor: none !important;
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 = "", this.container.classList.remove(this.scopeClass);
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
- const u = { x: -100, y: -100 }, m = [
204
- "a",
205
- "button",
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
- stage;
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
- * Creates a new Supermouse instance.
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
- ignoreOnNative: "auto",
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.options.container || (this.options.container = document.body), this.state = {
244
- pointer: { x: -100, y: -100 },
245
- target: { x: -100, y: -100 },
246
- smooth: { x: -100, y: -100 },
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
- forcedCursor: null,
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.options.hoverSelectors ? this.hoverSelectors = new Set(this.options.hoverSelectors) : this.hoverSelectors = new Set(m), this.stage = new v(this.options.container, !!this.options.hideCursor), this.hoverSelectors.forEach((e) => this.stage.addSelector(e)), this.input = new f(
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
- () => Array.from(this.hoverSelectors).join(", "),
328
+ () => this.hoverSelectorString,
262
329
  (e) => {
263
- e || this.resetPosition();
330
+ e || this.reset(!0);
264
331
  }
265
- ), this.options.plugins && this.options.plugins.forEach((e) => this.use(e)), this.init();
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 && (e.isEnabled = !1, e.onDisable?.(this));
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.stage.addSelector(t));
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.stage.element;
371
+ return this.options.container;
310
372
  }
311
- /**
312
- * Manually override the native cursor visibility.
313
- *
314
- * @param type 'auto' (Show Native), 'none' (Hide Native), or null (Resume Auto-detection)
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.forcedCursor = t;
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.stage.setNativeCursor("none");
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.stage.setNativeCursor("auto"), this.resetPosition();
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
- return this.plugins.find((e) => e.name === t.name) ? (console.warn(`[Supermouse] Plugin "${t.name}" already installed.`), this) : (t.isEnabled === void 0 && (t.isEnabled = !0), this.plugins.push(t), this.plugins.sort((e, i) => (e.priority || 0) - (i.priority || 0)), t.install?.(this), this);
335
- }
336
- resetCoords() {
337
- this.state.pointer = { ...u }, this.state.target = { ...u }, this.state.smooth = { ...u }, this.state.velocity = { x: 0, y: 0 }, this.state.angle = 0;
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
- resetPosition() {
340
- this.resetCoords(), this.state.hasReceivedInput = !1, this.state.shape = null, this.state.interaction = {};
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.tick(this.lastTime));
422
+ this.isRunning || (this.isRunning = !0, !document.hidden && (this.lastTime = performance.now(), this.rafId = requestAnimationFrame(this.tick)));
344
423
  }
345
- /**
346
- * Manually steps the animation loop.
347
- *
348
- * @param time Current timestamp in milliseconds.
349
- */
424
+ /** Start the animation loop. */
425
+ start() {
426
+ this.startLoop();
427
+ }
428
+ /** Manually step the animation loop. */
350
429
  step(t) {
351
- this.tick(t);
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 (i) {
358
- console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`, i), t.isEnabled = !1;
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 (o) {
362
- console.error(`[Supermouse] Failed to execute onDisable for plugin '${t.name}'.`, o);
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
- this.input.isEnabled ? (this.state.target.x = this.state.pointer.x, this.state.target.y = this.state.pointer.y) : this.resetCoords();
378
- for (let s = 0; s < this.plugins.length; s++)
379
- this.runPluginSafe(this.plugins[s], e);
380
- if (this.input.isEnabled) {
381
- const s = this.state.reducedMotion ? 1e3 : 1 / this.options.smoothness * 2;
382
- this.state.smooth.x = d(this.state.smooth.x, this.state.target.x, s, i), this.state.smooth.y = d(this.state.smooth.y, this.state.target.y, s, i);
383
- const r = this.state.target.x - this.state.smooth.x, a = this.state.target.y - this.state.smooth.y;
384
- this.state.velocity.x = r, this.state.velocity.y = a, (Math.abs(r) > 0.1 || Math.abs(a) > 0.1) && (this.state.angle = p(r, a));
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
- this.options.autoStart && this.isRunning && (this.rafId = requestAnimationFrame(this.tick));
477
+ }
478
+ tick = (t) => {
479
+ this.update(t), this.isRunning && (this.rafId = requestAnimationFrame(this.tick));
387
480
  };
388
- /**
389
- * Destroys the instance.
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.stage.destroy(), this.plugins.forEach((t) => t.destroy?.(this)), this.plugins = [];
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
- f as Input,
398
- v as Stage,
399
- y as Supermouse
498
+ v as Input,
499
+ y as Stage,
500
+ C as Supermouse
400
501
  };