tracky-mouse 2.6.0 → 2.8.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.
Files changed (76) hide show
  1. package/README.md +2 -1
  2. package/audio/click-press.wav +0 -0
  3. package/audio/click-release.wav +0 -0
  4. package/audio/middle-click-press.wav +0 -0
  5. package/audio/middle-click-release.wav +0 -0
  6. package/audio/pause.wav +0 -0
  7. package/audio/unpause.wav +0 -0
  8. package/lib/face-landmarks-detection.min.js +1 -1
  9. package/lib/face_mesh/face_mesh.js +1 -1
  10. package/locales/ar/translation.json +8 -2
  11. package/locales/ar-EG/translation.json +8 -2
  12. package/locales/bg/translation.json +8 -2
  13. package/locales/bn/translation.json +8 -2
  14. package/locales/ca/translation.json +8 -2
  15. package/locales/ce/translation.json +8 -2
  16. package/locales/ceb/translation.json +9 -3
  17. package/locales/cs/translation.json +8 -2
  18. package/locales/da/translation.json +8 -2
  19. package/locales/de/translation.json +8 -2
  20. package/locales/el/translation.json +8 -2
  21. package/locales/emoji/emoji-translation-notes.md +1 -0
  22. package/locales/emoji/translation.json +12 -6
  23. package/locales/en/translation.json +8 -2
  24. package/locales/eo/translation.json +8 -2
  25. package/locales/es/translation.json +8 -2
  26. package/locales/eu/translation.json +8 -2
  27. package/locales/fa/translation.json +8 -2
  28. package/locales/fi/translation.json +8 -2
  29. package/locales/fr/translation.json +8 -2
  30. package/locales/gu/translation.json +8 -2
  31. package/locales/ha/translation.json +8 -2
  32. package/locales/he/translation.json +8 -2
  33. package/locales/hi/translation.json +8 -2
  34. package/locales/hr/translation.json +8 -2
  35. package/locales/hu/translation.json +8 -2
  36. package/locales/hy/translation.json +8 -2
  37. package/locales/id/translation.json +8 -2
  38. package/locales/it/translation.json +8 -2
  39. package/locales/ja/translation.json +8 -2
  40. package/locales/jv/translation.json +9 -3
  41. package/locales/ko/translation.json +8 -2
  42. package/locales/mr/translation.json +8 -2
  43. package/locales/ms/translation.json +8 -2
  44. package/locales/nan/translation.json +8 -2
  45. package/locales/nb/translation.json +8 -2
  46. package/locales/nl/translation.json +8 -2
  47. package/locales/pa/translation.json +8 -2
  48. package/locales/pl/translation.json +8 -2
  49. package/locales/pt/translation.json +8 -2
  50. package/locales/pt-BR/translation.json +8 -2
  51. package/locales/ro/translation.json +8 -2
  52. package/locales/ru/translation.json +8 -2
  53. package/locales/sk/translation.json +8 -2
  54. package/locales/sl/translation.json +8 -2
  55. package/locales/sr/translation.json +8 -2
  56. package/locales/sv/translation.json +8 -2
  57. package/locales/sw/translation.json +8 -2
  58. package/locales/ta/translation.json +8 -2
  59. package/locales/te/translation.json +8 -2
  60. package/locales/th/translation.json +8 -2
  61. package/locales/tl/translation.json +9 -3
  62. package/locales/tr/translation.json +8 -2
  63. package/locales/tt/translation.json +8 -2
  64. package/locales/uk/translation.json +8 -2
  65. package/locales/ur/translation.json +8 -2
  66. package/locales/uz/translation.json +8 -2
  67. package/locales/vi/translation.json +8 -2
  68. package/locales/war/translation.json +9 -3
  69. package/locales/zh/translation.json +8 -2
  70. package/locales/zh-simplified/translation.json +8 -2
  71. package/package.json +3 -1
  72. package/src/audio.js +145 -0
  73. package/src/autoscroll.js +189 -0
  74. package/src/input-simulator.js +518 -0
  75. package/tracky-mouse.css +32 -1
  76. package/tracky-mouse.js +457 -185
@@ -0,0 +1,518 @@
1
+ import { autoscroll } from "./autoscroll.js";
2
+
3
+ // Pointer event simulation logic should be built into tracky-mouse in the future.
4
+ // These simulated events connect the Tracky Mouse head tracker to the Tracky Mouse dwell clicker,
5
+ // as well as any other pointermove/pointerenter/pointerleave/click handlers on the page.
6
+
7
+ /** a special value so other code can detect these simulated events */
8
+ export const TM_POINTER_ID = 1234567890;
9
+
10
+ export class InputSimulator {
11
+ pointerId = TM_POINTER_ID;
12
+ buttonStates = {
13
+ 0: false,
14
+ 1: false,
15
+ 2: false,
16
+ };
17
+ lastElOver = null;
18
+ simulatedMousePosition = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
19
+ getEventOptions({ x, y }) {
20
+ return {
21
+ view: window, // needed so the browser can calculate offsetX/Y from the clientX/Y
22
+ clientX: x,
23
+ clientY: y,
24
+ pointerId: this.pointerId,
25
+ pointerType: "mouse",
26
+ isPrimary: true,
27
+ };
28
+ }
29
+ getCurrentRotation(el) {
30
+ // Source: https://stackoverflow.com/a/54492696/2624876
31
+ const st = window.getComputedStyle(el, null);
32
+ const tm = st.getPropertyValue("-webkit-transform") ||
33
+ st.getPropertyValue("-moz-transform") ||
34
+ st.getPropertyValue("-ms-transform") ||
35
+ st.getPropertyValue("-o-transform") ||
36
+ st.getPropertyValue("transform") ||
37
+ "none";
38
+ if (tm !== "none") {
39
+ const [a, b] = tm.split('(')[1].split(')')[0].split(',');
40
+ return Math.round(Math.atan2(a, b) * (180 / Math.PI));
41
+ }
42
+ return 0;
43
+ }
44
+ pointerMove(x, y) {
45
+ this.simulatedMousePosition = { x, y };
46
+ // TODO: handle persistent button state
47
+ const target = this.targetFromPoint(x, y);
48
+ if (target !== this.lastElOver) {
49
+ if (this.lastElOver) {
50
+ const event = new PointerEvent("pointerleave", Object.assign(this.getEventOptions({ x, y }), {
51
+ button: 0,
52
+ buttons: 1,
53
+ bubbles: false,
54
+ cancelable: false,
55
+ }));
56
+ this.lastElOver.dispatchEvent(event);
57
+ }
58
+ const event = new PointerEvent("pointerenter", Object.assign(this.getEventOptions({ x, y }), {
59
+ button: 0,
60
+ buttons: 1,
61
+ bubbles: false,
62
+ cancelable: false,
63
+ }));
64
+ target.dispatchEvent(event);
65
+ this.lastElOver = target;
66
+ }
67
+ const event = new PointerEvent("pointermove", Object.assign(this.getEventOptions({ x, y }), {
68
+ button: 0,
69
+ buttons: 1,
70
+ bubbles: true,
71
+ cancelable: true,
72
+ }));
73
+ target.dispatchEvent(event);
74
+
75
+ // TODO: support double click, triple click selection behaviors, dragging selection
76
+ // TODO: avoid starting selection in links or other draggable/interactive elements
77
+ if (this.textSelectionStart && this.buttonStates[0]) {
78
+ const textSelectionEnd = this.caretPositionFromPoint(x, y);
79
+ const selection = window.getSelection();
80
+ if (textSelectionEnd && selection) {
81
+ selection.setBaseAndExtent(
82
+ this.textSelectionStart.offsetNode,
83
+ this.textSelectionStart.offset,
84
+ textSelectionEnd.offsetNode,
85
+ textSelectionEnd.offset
86
+ );
87
+ }
88
+ }
89
+
90
+ autoscroll.pointerMove(target, x, y, this.pointerId);
91
+ }
92
+ pointerDown(target, x, y, buttonIndex = 0) {
93
+ // TODO: handle nuance to moving across elements (nested elements, pointer capture)
94
+ this.buttonStates[buttonIndex] = true;
95
+ const event = new PointerEvent("pointerdown", Object.assign(this.getEventOptions({ x, y }), {
96
+ button: buttonIndex,
97
+ buttons: this.buttonStates[0] * 1 + this.buttonStates[1] * 2 + this.buttonStates[2] * 4,
98
+ bubbles: true,
99
+ cancelable: true,
100
+ }));
101
+ const result = target.dispatchEvent(event);
102
+ this.pointerDownElement = target;
103
+
104
+ // TODO: don't deselect when starting autoscroll
105
+ // TODO: also dispatch mouse* events and let mousedown cancel selection too
106
+ if (result) {
107
+ window.getSelection()?.removeAllRanges();
108
+ this.textSelectionStart = this.caretPositionFromPoint(x, y);
109
+ } else {
110
+ this.textSelectionStart = null;
111
+ }
112
+ // TODO: allow preventing MMB scroll? but make sure not to break
113
+ // autoscroll ending behavior
114
+ // FIXME: using gamepad, it fails to stop autoscroll with MMB because it starts immediately again
115
+ autoscroll.pointerDown(target, x, y, buttonIndex, this.pointerId);
116
+ }
117
+ pointerUp(target, x, y, buttonIndex = 0) {
118
+ // TODO: handle nuance to moving across elements (nested elements, pointer capture), event cancellation?
119
+ this.buttonStates[buttonIndex] = false;
120
+ const event = new PointerEvent("pointerup", Object.assign(this.getEventOptions({ x, y }), {
121
+ button: buttonIndex,
122
+ buttons: this.buttonStates[0] * 1 + this.buttonStates[1] * 2 + this.buttonStates[2] * 4,
123
+ bubbles: true,
124
+ cancelable: true,
125
+ }));
126
+ target.dispatchEvent(event);
127
+ if (buttonIndex === 0) {
128
+ if (this.pointerDownElement === target) {
129
+ this.click(target, x, y);
130
+ }
131
+ } else if (buttonIndex === 2) {
132
+ const contextMenuEvent = new MouseEvent("contextmenu", Object.assign(this.getEventOptions({ x, y }), {
133
+ button: buttonIndex,
134
+ bubbles: true,
135
+ cancelable: true,
136
+ }));
137
+ const contextMenuEventResult = target.dispatchEvent(contextMenuEvent);
138
+ if (contextMenuEventResult) {
139
+ this.showContextMenu(target, x, y);
140
+ }
141
+ }
142
+ this.pointerDownElement = null;
143
+
144
+ // TODO: support also MMB to open links in a new tab
145
+ autoscroll.pointerUp(target, x, y, buttonIndex, this.pointerId);
146
+ }
147
+ setMouseButtonState(buttonIndex, pressed) {
148
+ if (this.buttonStates[buttonIndex] !== pressed) {
149
+ const { x, y } = this.simulatedMousePosition;
150
+ const target = this.targetFromPoint(x, y);
151
+ if (pressed) {
152
+ this.pointerDown(target, x, y, buttonIndex);
153
+ } else {
154
+ this.pointerUp(target, x, y, buttonIndex);
155
+ }
156
+ return true;
157
+ }
158
+ return false;
159
+ }
160
+ dropdownToCloseFunction = new WeakMap();
161
+ openDropdown(dropdown, { focus = true } = {}) {
162
+ if (this.dropdownToCloseFunction.has(dropdown)) {
163
+ return; // avoid double opening
164
+ }
165
+
166
+ const flyout = document.createElement("ul");
167
+
168
+ // fake button displayed on top just in case you use arrow keys, because the value shouldn't change in the original select
169
+ // (an alternative hack might be to override `value` with a getter)
170
+ const dropdownDisplayButton = dropdown.cloneNode(true);
171
+ dropdownDisplayButton.value = dropdown.value;
172
+ dropdownDisplayButton.style.pointerEvents = "none";
173
+
174
+ const dropdownValueWhenOpened = dropdown.value;
175
+ let dropdownValueToBeWhenClosed = dropdown.value;
176
+
177
+ let highlightIndex = dropdown.selectedIndex;
178
+ const buttons = [];
179
+ function updateHighlightStyles() {
180
+ for (let optionIndex = 0; optionIndex < buttons.length; optionIndex++) {
181
+ buttons[optionIndex].style.backgroundColor = highlightIndex === optionIndex ? "Highlight" : "transparent";
182
+ buttons[optionIndex].style.color = highlightIndex === optionIndex ? "HighlightText" : "";
183
+ }
184
+ }
185
+
186
+ for (let optionIndex = 0; optionIndex < dropdown.options.length; optionIndex++) {
187
+ const option = dropdown.options[optionIndex];
188
+ const li = document.createElement("li");
189
+ flyout.append(li);
190
+ const button = document.createElement("button");
191
+ button.textContent = option.textContent;
192
+ button.dataset.value = option.value;
193
+ button.disabled = option.disabled;
194
+ li.append(button);
195
+ button.style.padding = "5px";
196
+ button.style.border = "none";
197
+ button.style.width = "100%";
198
+ button.style.textAlign = "left";
199
+ button.style.display = "block";
200
+ button.style.cssText += option.style.cssText;
201
+
202
+ // Hover effect
203
+ button.addEventListener("pointerenter", () => {
204
+ if (button.disabled) return;
205
+ highlightIndex = buttons.indexOf(button);
206
+ updateHighlightStyles();
207
+ });
208
+ button.addEventListener("click", () => {
209
+ if (button.disabled) return;
210
+ dropdownValueToBeWhenClosed = button.dataset.value;
211
+ dropdownDisplayButton.value = button.dataset.value;
212
+ this.closeDropdown(dropdown);
213
+ });
214
+
215
+ buttons.push(button);
216
+ }
217
+ updateHighlightStyles();
218
+
219
+ document.body.append(flyout, dropdownDisplayButton);
220
+
221
+ flyout.style.zIndex = "100";
222
+ flyout.style.overflow = "auto";
223
+ flyout.style.background = "white";
224
+ flyout.style.color = "black";
225
+ flyout.style.border = "1px solid gray";
226
+ flyout.style.outline = "0";
227
+ flyout.style.padding = "0";
228
+ flyout.style.margin = "0";
229
+ flyout.style.listStyle = "none";
230
+ flyout.style.boxSizing = "border-box";
231
+ flyout.style.userSelect = "none";
232
+
233
+ // Handle opening downwards, upwards or both directions as needed, limited to the full page height
234
+ let animationFrameId = null;
235
+ const positionElements = () => {
236
+ const dropdownRect = dropdown.getBoundingClientRect();
237
+ dropdownDisplayButton.style.position = "fixed";
238
+ dropdownDisplayButton.style.top = `${dropdownRect.top}px`;
239
+ dropdownDisplayButton.style.left = `${dropdownRect.left}px`;
240
+ dropdownDisplayButton.style.width = `${dropdownRect.width}px`;
241
+ flyout.style.position = "fixed";
242
+ flyout.style.top = `${dropdownRect.bottom}px`;
243
+ flyout.style.left = `${dropdownRect.left}px`;
244
+ flyout.style.width = `${dropdownRect.width}px`;
245
+ if (flyout.getBoundingClientRect().bottom > window.innerHeight) {
246
+ flyout.style.top = `${dropdownRect.top - flyout.getBoundingClientRect().height}px`;
247
+ }
248
+ if (flyout.getBoundingClientRect().top < 0) {
249
+ flyout.style.top = "0px";
250
+ }
251
+ flyout.style.maxHeight = "100vh";
252
+ animationFrameId = requestAnimationFrame(positionElements);
253
+ };
254
+ positionElements();
255
+
256
+ flyout.tabIndex = 0;
257
+ if (focus) {
258
+ flyout.focus();
259
+ flyout.addEventListener("blur", () => {
260
+ this.closeDropdown(dropdown);
261
+ }, { once: true });
262
+ }
263
+ flyout.addEventListener("keydown", (event) => {
264
+ // TODO: should Esc/Enter be global? (maybe even arrow keys?)
265
+ if (event.key === "Escape" || event.key === "Enter") {
266
+ this.closeDropdown(dropdown);
267
+ }
268
+ const dx = (event.key === "ArrowRight") - (event.key === "ArrowLeft");
269
+ const dy = (event.key === "ArrowDown") - (event.key === "ArrowUp");
270
+ if (dy !== 0 || dx !== 0) {
271
+ const newIndex = highlightIndex === -1 ? 0 : ((highlightIndex + dy + buttons.length) % buttons.length);
272
+ highlightIndex = newIndex;
273
+ updateHighlightStyles();
274
+ buttons[newIndex].scrollIntoView({ block: "nearest", container: "nearest" });
275
+ dropdownValueToBeWhenClosed = buttons[newIndex].dataset.value;
276
+ dropdownDisplayButton.value = buttons[newIndex].dataset.value;
277
+ event.preventDefault();
278
+ }
279
+ });
280
+ let flyoutPointerDownOutsideHandler;
281
+ addEventListener("pointerdown", flyoutPointerDownOutsideHandler = (event) => {
282
+ if (!event.target?.closest || (event.target.closest("ul") !== flyout && event.target.closest("select") !== dropdown)) {
283
+ this.closeDropdown(dropdown);
284
+ }
285
+ });
286
+
287
+ flyout.addEventListener("pointerdown", (event) => {
288
+ event.preventDefault(); // prevent starting text selection
289
+ });
290
+ flyout.addEventListener("contextmenu", (event) => {
291
+ event.preventDefault();
292
+ });
293
+
294
+ const closeFunction = () => {
295
+ cancelAnimationFrame(animationFrameId);
296
+ removeEventListener("pointerdown", flyoutPointerDownOutsideHandler);
297
+ if (!flyout || this._closingDropdown) {
298
+ return;
299
+ }
300
+ this._closingDropdown = true; // TODO: should this flag be scoped to each dropdown or stay global?
301
+ if (dropdownValueWhenOpened !== dropdownValueToBeWhenClosed) {
302
+ dropdown.value = dropdownValueToBeWhenClosed;
303
+ dropdown.dispatchEvent(new Event("input", { bubbles: true }));
304
+ dropdown.dispatchEvent(new Event("change", { bubbles: true }));
305
+ }
306
+ flyout.remove(); // Can trigger blur event in Chromium-based browsers
307
+ dropdownDisplayButton.remove();
308
+ this._closingDropdown = false;
309
+ };
310
+ this.dropdownToCloseFunction.set(dropdown, closeFunction);
311
+ }
312
+ closeDropdown(dropdown) {
313
+ this.dropdownToCloseFunction.get(dropdown)?.();
314
+ this.dropdownToCloseFunction.delete(dropdown);
315
+ }
316
+ click(target, x, y) {
317
+ if (target.matches("input[type='range']")) {
318
+ // Special handling for sliders
319
+ // TODO: support continuous dragging
320
+ const rect = target.getBoundingClientRect();
321
+ const vertical = target.getAttribute("orient") === "vertical" ||
322
+ (this.getCurrentRotation(target) !== 0) ||
323
+ rect.height > rect.width;
324
+ const min = Number(target.min);
325
+ const max = Number(target.max);
326
+ const style = window.getComputedStyle(target);
327
+ const isRTL = style.direction === "rtl";
328
+ const fraction = vertical
329
+ ? (y - rect.top) / rect.height
330
+ : (isRTL ? (rect.right - x) / rect.width : (x - rect.left) / rect.width);
331
+ target.value = fraction * (max - min) + min;
332
+ target.dispatchEvent(new Event("input", { bubbles: true }));
333
+ target.dispatchEvent(new Event("change", { bubbles: true }));
334
+ } else if (target.matches("option")) {
335
+ const select = target.closest("select");
336
+ if (select) {
337
+ select.value = target.value;
338
+ select.dispatchEvent(new Event("input", { bubbles: true }));
339
+ select.dispatchEvent(new Event("change", { bubbles: true }));
340
+ }
341
+ } else if (target.matches("select")) {
342
+ // Special handling for dropdowns
343
+ if (target.getAttribute("size")) {
344
+ // Fallback logic assuming all options are the same height
345
+ // Do any browsers actually not give you <option> elements with document.getElementFromPoint?
346
+ // I assumed they wouldn't when I wrote this, but it's great that they do, or Firefox does at least
347
+ const rect = target.getBoundingClientRect();
348
+ const fraction = (y - rect.top) / rect.height;
349
+ const newValue = target.options[Math.floor(fraction * target.options.length)].value;
350
+ if (newValue !== target.value) {
351
+ target.value = newValue;
352
+ target.dispatchEvent(new Event("input", { bubbles: true }));
353
+ target.dispatchEvent(new Event("change", { bubbles: true }));
354
+ }
355
+ } else if (this.dropdownToCloseFunction.has(target)) {
356
+ this.closeDropdown(target);
357
+ } else {
358
+ this.openDropdown(target);
359
+ }
360
+ } else {
361
+ // Normal click
362
+ // HTMLElement has click() but SVGElement does not
363
+ if (target.click) {
364
+ target.click();
365
+ } else {
366
+ const event = new MouseEvent("click", Object.assign(this.getEventOptions({ x, y }), {
367
+ button: 0,
368
+ bubbles: true,
369
+ cancelable: true,
370
+ }));
371
+ target.dispatchEvent(event);
372
+ }
373
+ if (target.matches("input, textarea")) {
374
+ target.focus();
375
+ }
376
+ }
377
+ }
378
+ showContextMenu(target, x, y) {
379
+ const commands = ["copy", "cut", "paste", "delete", "selectAll", "undo", "redo"];
380
+ const supportedCommands = commands.filter(cmd => document.queryCommandSupported(cmd));
381
+ const enabledCommands = supportedCommands.filter(cmd => document.queryCommandEnabled(cmd));
382
+ console.log({ supportedCommands, enabledCommands });
383
+
384
+ const execCommandItem = (label, command) => {
385
+ return {
386
+ label,
387
+ visible: supportedCommands.includes(command),
388
+ enabled: enabledCommands.includes(command),
389
+ action: () => document.execCommand(command),
390
+ };
391
+ };
392
+
393
+ const menuItems = [
394
+ // TODO: avoid executing first item when dismissing context menu
395
+ // For now, include a no-op first item
396
+ { label: '' }, // not enabled: false so that another item isn't highlighted with a gray background
397
+
398
+ {
399
+ label: 'Open Link in New Tab',
400
+ visible: target.matches("a[href]"),
401
+ action: () => {
402
+ const a = target?.closest('a');
403
+ if (a?.href) {
404
+ return !!window.open(a.href, '_blank');
405
+ }
406
+ }
407
+ },
408
+ { separator: true, visible: target.matches("a[href]") },
409
+
410
+ execCommandItem('Copy', 'copy'),
411
+ execCommandItem('Cut', 'cut'),
412
+ execCommandItem('Paste', 'paste'),
413
+ execCommandItem('Delete', 'delete'),
414
+ execCommandItem('Select All', 'selectAll'),
415
+ ];
416
+
417
+ // Create an invisible select element for context menu positioning, in order to reuse the dropdown code
418
+ const select = document.createElement('select');
419
+ select.style.position = 'absolute';
420
+ select.style.left = `${x + window.scrollX}px`;
421
+ const height = 16; // arbitrary (but maybe not zero? and should be accounted for if it's not zero)
422
+ select.style.height = `${height}px`;
423
+ select.style.top = `${y - height + window.scrollY}px`;
424
+ select.style.opacity = '0';
425
+ select.style.pointerEvents = 'none';
426
+ select.tabIndex = -1;
427
+
428
+ const optionToMenuItem = new WeakMap();
429
+ for (const item of menuItems) {
430
+ if (item.visible === false) {
431
+ continue;
432
+ }
433
+ const option = document.createElement('option');
434
+ option.textContent = item.label;
435
+ option.disabled = item.enabled === false || item.separator === true;
436
+ if (!item.label) {
437
+ option.style.fontSize = '0';
438
+ option.style.padding = '0';
439
+ }
440
+ if (item.separator) {
441
+ option.style.borderTop = '1px solid #ccc';
442
+ option.style.fontSize = '4px';
443
+ option.style.marginTop = '4px';
444
+ }
445
+ select.appendChild(option);
446
+ optionToMenuItem.set(option, item);
447
+ }
448
+
449
+ document.body.appendChild(select);
450
+ // technically the dropdown should have focus, but
451
+ // 1. this is an interface to allow for (virtual-)mouse-only access, so keyboard is not so important
452
+ // 2. I think it's more important to keep showing the selection you're about to copy/cut/delete
453
+ target.focus();
454
+ this.openDropdown(select, { focus: false });
455
+
456
+ select.addEventListener("change", () => {
457
+ const item = optionToMenuItem.get(select.options[select.selectedIndex]);
458
+ select.remove();
459
+ target.focus();
460
+ if (item.action && item.enabled !== false) {
461
+ const result = item.action();
462
+ this.showToast(item.label + (result === false ? " not allowed" : ""));
463
+ }
464
+ }, { once: true });
465
+ }
466
+ showToast(message, position = this.simulatedMousePosition) {
467
+ const { x, y } = position;
468
+ const toast = document.createElement("div");
469
+ toast.textContent = message;
470
+ toast.style.position = "fixed";
471
+ toast.style.left = `${x}px`;
472
+ toast.style.top = `${y}px`;
473
+ toast.style.background = "rgba(0, 0, 0, 0.7)";
474
+ toast.style.color = "white";
475
+ toast.style.padding = "2px 5px";
476
+ toast.style.borderRadius = "3px";
477
+ toast.style.pointerEvents = "none";
478
+ toast.style.animation = "tracky-mouse-fade-out 2s ease-in-out forwards 2s";
479
+ document.body.appendChild(toast);
480
+ setTimeout(() => {
481
+ toast.remove();
482
+ }, 4000);
483
+ }
484
+ caretPositionFromPoint(x, y) {
485
+ // Firefox (standard)
486
+ if (document.caretPositionFromPoint) {
487
+ return document.caretPositionFromPoint(x, y);
488
+ }
489
+ // Chrome/Edge/Safari (non-standard)
490
+ if (document.caretRangeFromPoint) {
491
+ const range = document.caretRangeFromPoint(x, y);
492
+ if (range) {
493
+ return { offsetNode: range.startContainer, offset: range.startOffset };
494
+ }
495
+ return null;
496
+ }
497
+ throw new Error('Neither caretPositionFromPoint nor caretRangeFromPoint is supported.');
498
+ }
499
+ targetFromPoint(x, y) {
500
+ const skip = ".tracky-mouse-click-through, .tracky-mouse-click-through *";
501
+ const fallback = document.body; // would documentElement make more sense?
502
+
503
+ let target = document.elementFromPoint(x, y);
504
+ if (!target) {
505
+ return fallback;
506
+ }
507
+
508
+ if (target.matches(skip)) {
509
+ const elements = document.elementsFromPoint(x, y);
510
+ target = elements.find(el => !el.matches(skip));
511
+ if (!target) {
512
+ return fallback;
513
+ }
514
+ }
515
+
516
+ return target || fallback;
517
+ }
518
+ }
package/tracky-mouse.css CHANGED
@@ -161,7 +161,7 @@
161
161
  gap: 10px;
162
162
  }
163
163
 
164
- .tracky-mouse-canvas-container-container {
164
+ .tracky-mouse-camera-area {
165
165
  flex: 1;
166
166
  flex-basis: 0;
167
167
  min-width: 0;
@@ -214,6 +214,14 @@
214
214
  color: white;
215
215
  padding: 5px;
216
216
  animation: tracky-mouse-jello-vertical 1s ease 0s 1 normal none;
217
+ white-space: pre-wrap;
218
+ }
219
+
220
+ .tracky-mouse-error-message pre {
221
+ white-space: pre-wrap;
222
+ font-size: small;
223
+ background: rgba(0, 0, 0, 0.5);
224
+ color: white;
217
225
  }
218
226
 
219
227
  @keyframes tracky-mouse-jello-vertical {
@@ -246,6 +254,21 @@
246
254
  }
247
255
  }
248
256
 
257
+ .tracky-mouse-toast {
258
+ position: fixed;
259
+ top: 25px + env(titlebar-area-height, 0px);
260
+ top: calc(25px + env(titlebar-area-height, 0px));
261
+ right: 25px;
262
+ z-index: 100;
263
+ background-color: rgba(255, 233, 38, 0.8);
264
+ color: black;
265
+ padding: 10px 20px;
266
+ border-radius: 5px;
267
+ animation: tracky-mouse-fade-out 3s 3s ease-out forwards;
268
+ white-space: pre-wrap;
269
+ font-family: sans-serif;
270
+ }
271
+
249
272
  /* Screen overlay in electron app */
250
273
 
251
274
  .tracky-mouse-hide-near-cursor {
@@ -258,6 +281,14 @@
258
281
  user-select: none;
259
282
  }
260
283
 
284
+ #tracky-mouse-screen-overlay-work-area {
285
+ position: fixed;
286
+ top: 0;
287
+ left: 0;
288
+ width: 100%;
289
+ height: 100%;
290
+ }
291
+
261
292
  #tracky-mouse-screen-overlay-message {
262
293
  color: yellow;
263
294
  position: absolute;