@supermousejs/core 2.1.0 → 2.2.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 +57 -51
- package/dist/index.d.ts +10 -3
- package/dist/index.mjs +133 -125
- package/dist/index.umd.js +2 -2
- package/package.json +1 -1
- package/src/Supermouse.ts +412 -43
- package/tsconfig.json +9 -9
- package/src/systems/Input.ts +0 -231
- package/src/systems/Stage.ts +0 -126
- package/src/systems/index.ts +0 -2
- package/src/utils/math.ts +0 -11
package/src/systems/Input.ts
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import type { MouseState, SupermouseOptions } from "../types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* This class listens to browser events and mutates the shared `MouseState` object.
|
|
5
|
-
*
|
|
6
|
-
* @internal This is an internal system class instantiated by `Supermouse`.
|
|
7
|
-
*/
|
|
8
|
-
export class Input {
|
|
9
|
-
private mediaQueryList?: MediaQueryList;
|
|
10
|
-
private mediaQueryHandler?: (e: MediaQueryListEvent) => void;
|
|
11
|
-
private motionQuery?: MediaQueryList;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Master switch for input processing.
|
|
15
|
-
* Toggled by `Supermouse.enable()`/`disable()` or automatically by device capability checks.
|
|
16
|
-
*/
|
|
17
|
-
public isEnabled: boolean = true;
|
|
18
|
-
|
|
19
|
-
constructor(
|
|
20
|
-
private state: MouseState,
|
|
21
|
-
private options: SupermouseOptions,
|
|
22
|
-
private getHoverSelector: () => string,
|
|
23
|
-
private onEnableChange: (enabled: boolean) => void
|
|
24
|
-
) {
|
|
25
|
-
this.checkDeviceCapability();
|
|
26
|
-
this.checkMotionPreference();
|
|
27
|
-
this.bindEvents();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Automatically disables the custom cursor on devices without fine pointer control.
|
|
32
|
-
* Relies on `matchMedia('(pointer: fine)')`.
|
|
33
|
-
*/
|
|
34
|
-
private checkDeviceCapability() {
|
|
35
|
-
if (!this.options.autoDisableOnMobile) return;
|
|
36
|
-
|
|
37
|
-
this.mediaQueryList = window.matchMedia("(pointer: fine)");
|
|
38
|
-
this.updateEnabledState(this.mediaQueryList.matches);
|
|
39
|
-
|
|
40
|
-
this.mediaQueryHandler = (e: MediaQueryListEvent) => {
|
|
41
|
-
this.updateEnabledState(e.matches);
|
|
42
|
-
};
|
|
43
|
-
this.mediaQueryList.addEventListener("change", this.mediaQueryHandler);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Checks for `prefers-reduced-motion`.
|
|
48
|
-
* If true, the core physics engine will switch to instant snapping (high damping) to avoid motion sickness.
|
|
49
|
-
*/
|
|
50
|
-
private checkMotionPreference() {
|
|
51
|
-
this.motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
52
|
-
this.state.reducedMotion = this.motionQuery.matches;
|
|
53
|
-
|
|
54
|
-
this.motionQuery.addEventListener("change", (e) => {
|
|
55
|
-
this.state.reducedMotion = e.matches;
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private updateEnabledState(enabled: boolean) {
|
|
60
|
-
this.isEnabled = enabled;
|
|
61
|
-
this.onEnableChange(enabled);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private parseDOMInteraction(element: HTMLElement) {
|
|
65
|
-
if (this.options.resolveInteraction) {
|
|
66
|
-
this.state.interaction = this.options.resolveInteraction(element);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const data: Record<string, string | boolean> = {};
|
|
71
|
-
|
|
72
|
-
if (this.options.rules) {
|
|
73
|
-
for (const [selector, rules] of Object.entries(this.options.rules)) {
|
|
74
|
-
if (element.matches(selector)) {
|
|
75
|
-
Object.assign(data, rules);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const dataset = element.dataset;
|
|
81
|
-
for (const key in dataset) {
|
|
82
|
-
if (key.startsWith("supermouse")) {
|
|
83
|
-
const prop = key.slice(10);
|
|
84
|
-
if (prop) {
|
|
85
|
-
const cleanKey = prop.charAt(0).toLowerCase() + prop.slice(1);
|
|
86
|
-
const val = dataset[key];
|
|
87
|
-
if (val !== undefined) {
|
|
88
|
-
data[cleanKey] = val === "" ? true : val;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
this.state.interaction = data;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private handleMove(e: PointerEvent) {
|
|
98
|
-
if (!this.isEnabled) return;
|
|
99
|
-
|
|
100
|
-
if (this.options.autoDisableOnMobile && e.pointerType === "touch") return;
|
|
101
|
-
|
|
102
|
-
let x = e.clientX;
|
|
103
|
-
let y = e.clientY;
|
|
104
|
-
|
|
105
|
-
if (this.options.container && this.options.container !== document.body) {
|
|
106
|
-
const rect = this.options.container.getBoundingClientRect();
|
|
107
|
-
x -= rect.left;
|
|
108
|
-
y -= rect.top;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
this.state.pointer.x = x;
|
|
112
|
-
this.state.pointer.y = y;
|
|
113
|
-
|
|
114
|
-
if (!this.state.hasReceivedInput) {
|
|
115
|
-
this.state.hasReceivedInput = true;
|
|
116
|
-
this.state.target.x = this.state.smooth.x = x;
|
|
117
|
-
this.state.target.y = this.state.smooth.y = y;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private handleDown(): void {
|
|
122
|
-
if (this.isEnabled) this.state.isDown = true;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private handleUp(): void {
|
|
126
|
-
if (this.isEnabled) this.state.isDown = false;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private handleMouseOver(e: MouseEvent) {
|
|
130
|
-
if (!this.isEnabled) return;
|
|
131
|
-
const target = e.target as HTMLElement;
|
|
132
|
-
|
|
133
|
-
if (target.closest("[data-supermouse-ignore]")) {
|
|
134
|
-
this.state.isNative = true;
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const selector = this.getHoverSelector();
|
|
139
|
-
const hoverable = target.closest(selector);
|
|
140
|
-
|
|
141
|
-
if (hoverable) {
|
|
142
|
-
this.state.isHover = true;
|
|
143
|
-
this.state.hoverTarget = hoverable as HTMLElement;
|
|
144
|
-
this.parseDOMInteraction(this.state.hoverTarget);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const strategy = this.options.ignoreOnNative;
|
|
148
|
-
|
|
149
|
-
if (strategy) {
|
|
150
|
-
const checkTags = strategy === true || strategy === "auto" || strategy === "tag";
|
|
151
|
-
const checkCSS = strategy === true || strategy === "auto" || strategy === "css";
|
|
152
|
-
let isNative = false;
|
|
153
|
-
|
|
154
|
-
if (checkTags) {
|
|
155
|
-
const tag = target.localName;
|
|
156
|
-
if (tag === "input" || tag === "textarea" || tag === "select" || target.isContentEditable) {
|
|
157
|
-
isNative = true;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (!isNative && checkCSS) {
|
|
162
|
-
const style = window.getComputedStyle(target).cursor;
|
|
163
|
-
const supermouseAllowed = ["default", "auto", "pointer", "none", "inherit"];
|
|
164
|
-
if (!supermouseAllowed.includes(style)) {
|
|
165
|
-
isNative = true;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (isNative) {
|
|
170
|
-
this.state.isNative = true;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private handleMouseOut(e: MouseEvent) {
|
|
176
|
-
if (!this.isEnabled) return;
|
|
177
|
-
const target = e.target as HTMLElement;
|
|
178
|
-
|
|
179
|
-
if (target === this.state.hoverTarget || target.contains(this.state.hoverTarget)) {
|
|
180
|
-
if (!e.relatedTarget || !this.state.hoverTarget?.contains(e.relatedTarget as Node)) {
|
|
181
|
-
this.state.isHover = false;
|
|
182
|
-
this.state.hoverTarget = null;
|
|
183
|
-
this.state.interaction = {};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (this.state.isNative) {
|
|
188
|
-
this.state.isNative = false;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private handleWindowLeave(): void {
|
|
193
|
-
if (this.options.hideOnLeave) {
|
|
194
|
-
this.state.hasReceivedInput = false;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
public clearHover() {
|
|
199
|
-
this.state.isHover = false;
|
|
200
|
-
this.state.hoverTarget = null;
|
|
201
|
-
this.state.isNative = false;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private bindEvents() {
|
|
205
|
-
window.addEventListener("pointermove", this.handleMove.bind(this), { passive: true });
|
|
206
|
-
window.addEventListener("pointerdown", this.handleDown.bind(this), { passive: true });
|
|
207
|
-
window.addEventListener("pointerup", this.handleUp.bind(this));
|
|
208
|
-
|
|
209
|
-
document.addEventListener("mouseover", this.handleMouseOver.bind(this));
|
|
210
|
-
document.addEventListener("mouseout", this.handleMouseOut.bind(this));
|
|
211
|
-
document.addEventListener("mouseleave", this.handleWindowLeave.bind(this));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
public destroy() {
|
|
215
|
-
if (this.mediaQueryList && this.mediaQueryHandler) {
|
|
216
|
-
this.mediaQueryList.removeEventListener("change", this.mediaQueryHandler);
|
|
217
|
-
}
|
|
218
|
-
if (this.motionQuery) {
|
|
219
|
-
// Modern browsers support removeEventListener on MediaQueryList
|
|
220
|
-
this.motionQuery.onchange = null;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
window.removeEventListener("pointermove", this.handleMove);
|
|
224
|
-
window.removeEventListener("pointerdown", this.handleDown);
|
|
225
|
-
window.removeEventListener("pointerup", this.handleUp);
|
|
226
|
-
|
|
227
|
-
document.removeEventListener("mouseover", this.handleMouseOver);
|
|
228
|
-
document.removeEventListener("mouseout", this.handleMouseOut);
|
|
229
|
-
document.removeEventListener("mouseleave", this.handleWindowLeave);
|
|
230
|
-
}
|
|
231
|
-
}
|
package/src/systems/Stage.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
let stageCount = 0;
|
|
2
|
-
|
|
3
|
-
export class Stage {
|
|
4
|
-
/** The container element appended to the document. */
|
|
5
|
-
public readonly element: HTMLDivElement;
|
|
6
|
-
private styleTag: HTMLStyleElement;
|
|
7
|
-
private id: string;
|
|
8
|
-
private scopeClass: string;
|
|
9
|
-
|
|
10
|
-
private currentCursorState: "none" | "auto" | "" | null = null;
|
|
11
|
-
|
|
12
|
-
private selectors: Set<string> = new Set([
|
|
13
|
-
"a",
|
|
14
|
-
"button",
|
|
15
|
-
"input",
|
|
16
|
-
"textarea",
|
|
17
|
-
"select",
|
|
18
|
-
'[role="button"]',
|
|
19
|
-
"[tabindex]"
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
constructor(
|
|
23
|
-
private container: HTMLElement = document.body,
|
|
24
|
-
private hideNativeCursor: boolean
|
|
25
|
-
) {
|
|
26
|
-
if (!container || !(container instanceof HTMLElement)) {
|
|
27
|
-
throw new Error(`[Supermouse] Invalid container: ${container}. Must be an HTMLElement.`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const instanceId = stageCount++;
|
|
31
|
-
this.id = `supermouse-style-${instanceId}`;
|
|
32
|
-
this.scopeClass = `supermouse-scope-${instanceId}`;
|
|
33
|
-
|
|
34
|
-
const isBody = container === document.body;
|
|
35
|
-
|
|
36
|
-
this.element = document.createElement("div");
|
|
37
|
-
Object.assign(this.element.style, {
|
|
38
|
-
position: isBody ? "fixed" : "absolute",
|
|
39
|
-
top: "0",
|
|
40
|
-
left: "0",
|
|
41
|
-
width: "100%",
|
|
42
|
-
height: "100%",
|
|
43
|
-
pointerEvents: "none",
|
|
44
|
-
zIndex: "9999",
|
|
45
|
-
opacity: "1",
|
|
46
|
-
transition: "opacity 0.15s ease"
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (!isBody) {
|
|
50
|
-
const computed = window.getComputedStyle(container);
|
|
51
|
-
if (computed.position === "static") {
|
|
52
|
-
container.style.position = "relative";
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
container.appendChild(this.element);
|
|
57
|
-
|
|
58
|
-
this.styleTag = document.createElement("style");
|
|
59
|
-
this.styleTag.id = this.id;
|
|
60
|
-
document.head.appendChild(this.styleTag);
|
|
61
|
-
|
|
62
|
-
this.container.classList.add(this.scopeClass);
|
|
63
|
-
|
|
64
|
-
if (this.hideNativeCursor) {
|
|
65
|
-
this.setNativeCursor("none");
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Adds a new CSS selector to the Hide Native Cursor list.
|
|
71
|
-
* Called by `Supermouse` (and subsequently plugins) during install to ensure
|
|
72
|
-
* the native cursor is hidden on their specific interactive targets.
|
|
73
|
-
*/
|
|
74
|
-
public addSelector(selector: string) {
|
|
75
|
-
this.selectors.add(selector);
|
|
76
|
-
if (this.hideNativeCursor) {
|
|
77
|
-
this.updateCursorCSS();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
public setVisibility(visible: boolean) {
|
|
82
|
-
this.element.style.opacity = visible ? "1" : "0";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Toggles the visibility of the native cursor via CSS injection.
|
|
87
|
-
* @param type 'none' to hide, 'auto' to show.
|
|
88
|
-
*/
|
|
89
|
-
public setNativeCursor(type: "none" | "auto" | "") {
|
|
90
|
-
if (!this.hideNativeCursor && type === "none") return;
|
|
91
|
-
|
|
92
|
-
if (type === this.currentCursorState) return;
|
|
93
|
-
this.currentCursorState = type;
|
|
94
|
-
|
|
95
|
-
if (type === "none") {
|
|
96
|
-
this.container.style.cursor = "none";
|
|
97
|
-
this.updateCursorCSS();
|
|
98
|
-
} else {
|
|
99
|
-
this.container.style.cursor = "";
|
|
100
|
-
this.styleTag.innerText = "";
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private updateCursorCSS() {
|
|
105
|
-
const rawSelectors = Array.from(this.selectors);
|
|
106
|
-
if (rawSelectors.length === 0) {
|
|
107
|
-
this.styleTag.innerText = "";
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const scopedSelectors = rawSelectors.map((s) => `.${this.scopeClass} ${s}`).join(", ");
|
|
112
|
-
|
|
113
|
-
this.styleTag.innerText = `
|
|
114
|
-
${scopedSelectors} {
|
|
115
|
-
cursor: none !important;
|
|
116
|
-
}
|
|
117
|
-
`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
public destroy() {
|
|
121
|
-
this.element.remove();
|
|
122
|
-
this.styleTag.remove();
|
|
123
|
-
this.container.style.cursor = "";
|
|
124
|
-
this.container.classList.remove(this.scopeClass);
|
|
125
|
-
}
|
|
126
|
-
}
|
package/src/systems/index.ts
DELETED
package/src/utils/math.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export function lerp(start: number, end: number, factor: number): number {
|
|
2
|
-
return start + (end - start) * factor;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function damp(a: number, b: number, lambda: number, dt: number): number {
|
|
6
|
-
return lerp(a, b, 1 - Math.exp(-lambda * dt));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function angle(x: number, y: number): number {
|
|
10
|
-
return Math.atan2(y, x) * (180 / Math.PI);
|
|
11
|
-
}
|