| 1 | /// <reference lib="dom" /> |
| 2 | |
| 3 | // DOM geometry can already reflect a resize while Chromium is still routing |
| 4 | // input using the preceding compositor frame. Observe two animation frames |
| 5 | // before dispatch, and reject rather than silently retarget a moving point. |
| 6 | export function pageInputReady(input: { x: number; y: number }): Promise<boolean> { |
| 7 | const target = document.elementFromPoint(input.x, input.y); |
| 8 | const sample = () => { |
| 9 | const rect = target?.getBoundingClientRect(); |
| 10 | return JSON.stringify([innerWidth, innerHeight, scrollX, scrollY, rect?.x, rect?.y, rect?.width, rect?.height]); |
| 11 | }; |
| 12 | const initial = sample(); |
| 13 | return new Promise(resolve => { |
| 14 | let frame = 0, remaining = 2, finished = false; |
| 15 | const finish = (ready: boolean) => { |
| 16 | if (finished) return; |
| 17 | finished = true; |
| 18 | clearTimeout(timer); |
| 19 | cancelAnimationFrame(frame); |
| 20 | resolve(ready); |
| 21 | }; |
| 22 | const timer = setTimeout(() => finish(false), 500); |
| 23 | const observe = () => { |
| 24 | if (sample() !== initial || document.elementFromPoint(input.x, input.y) !== target || target && !target.isConnected) { finish(false); return; } |
| 25 | if (--remaining === 0) { finish(true); return; } |
| 26 | frame = requestAnimationFrame(observe); |
| 27 | }; |
| 28 | frame = requestAnimationFrame(observe); |
| 29 | }); |
| 30 | } |
| 31 | |
| 32 | export function pageInputFrame(input: { x: number; y: number }) { |
| 33 | let hit = document.elementFromPoint(input.x, input.y); |
| 34 | while (hit?.shadowRoot) { |
| 35 | const nested = hit.shadowRoot.elementFromPoint(input.x, input.y); |
| 36 | if (!nested || nested === hit) break; |
| 37 | hit = nested; |
| 38 | } |
| 39 | if (!(hit instanceof HTMLIFrameElement || hit instanceof HTMLFrameElement)) return null; |
| 40 | const transform = getComputedStyle(hit).transform; |
| 41 | if (transform && transform !== "none") { |
| 42 | const matrix = new DOMMatrixReadOnly(transform); |
| 43 | if (matrix.b !== 0 || matrix.c !== 0 || matrix.a <= 0 || matrix.d <= 0) return { unsupported: true }; |
| 44 | } |
| 45 | const rect = hit.getBoundingClientRect(); |
| 46 | const index = Array.from({ length: window.length }, (_, i) => i).find(i => window.frames[i] === hit.contentWindow); |
| 47 | if (index === undefined || !rect.width || !rect.height) return null; |
| 48 | const x = (input.x - rect.x) * hit.offsetWidth / rect.width - hit.clientLeft; |
| 49 | const y = (input.y - rect.y) * hit.offsetHeight / rect.height - hit.clientTop; |
| 50 | if (x < 0 || y < 0 || x >= hit.clientWidth || y >= hit.clientHeight) return null; |
| 51 | return { index, x, y }; |
| 52 | } |
| 53 |