|
19 | 19 | return { left, top, width: rect.width, height: rect.height }; |
20 | 20 | }; |
21 | 21 |
|
22 | | - // Where to click an element, after scrolling it into the middle of the |
23 | | - // screen, and what else is on top of that point, if anything. |
| 22 | + // Whether something else is on top of a point of an element: true when |
| 23 | + // the point shows the element, a part of it or what it sits in. |
| 24 | + const shows = (el, x, y) => { |
| 25 | + const hit = el.ownerDocument.elementFromPoint(x, y); |
| 26 | + return !!hit && (hit === el || el.contains(hit) || hit.contains(el)); |
| 27 | + }; |
| 28 | + |
| 29 | + // A point inside the element that is not covered, the middle first, then |
| 30 | + // the corners and edges of its middle part, or null. Returns the fraction |
| 31 | + // of the box it is at. |
| 32 | + const freePoint = (el, local) => { |
| 33 | + const view = el.ownerDocument.defaultView; |
| 34 | + for (const fy of [0.5, 0.2, 0.8]) { |
| 35 | + for (const fx of [0.5, 0.2, 0.8]) { |
| 36 | + const x = local.left + local.width * fx; |
| 37 | + const y = local.top + local.height * fy; |
| 38 | + if (x < 0 || y < 0 || x >= view.innerWidth || y >= view.innerHeight) continue; |
| 39 | + if (shows(el, x, y)) return { fx, fy }; |
| 40 | + } |
| 41 | + } |
| 42 | + return null; |
| 43 | + }; |
| 44 | + |
| 45 | + // Where to click an element. It scrolls into the middle of the screen; |
| 46 | + // when a fixed header, a banner or a chat widget covers it there, it tries |
| 47 | + // the other alignments and the other points of the element, and when |
| 48 | + // nothing is free it says so (`script`), so the click can go through the |
| 49 | + // page instead of the mouse. Also what is still on top, if anything. |
24 | 50 | const point = (ref) => { |
25 | 51 | const el = get(ref); |
26 | 52 | if (!el) return { missing: true, next: next() }; |
27 | | - el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" }); |
| 53 | + let pick = null; |
| 54 | + for (const block of ["center", "end", "start", "nearest"]) { |
| 55 | + el.scrollIntoView({ block, inline: "center", behavior: "instant" }); |
| 56 | + const local = el.getBoundingClientRect(); |
| 57 | + if (local.width === 0 || local.height === 0) break; |
| 58 | + pick = freePoint(el, local); |
| 59 | + if (pick) break; |
| 60 | + } |
| 61 | + if (!pick) el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" }); |
28 | 62 | const box = topRect(el); |
29 | 63 | const role = roleOf(el, styleOf(el), "auto") ?? el.localName; |
30 | 64 | const label = clean(nameOf(el), 60); |
31 | 65 | if (box.width === 0 || box.height === 0) return { hidden: true, role, label, next: next() }; |
32 | | - const x = Math.min(Math.max(box.left + box.width / 2, 0), innerWidth - 1); |
33 | | - const y = Math.min(Math.max(box.top + box.height / 2, 0), innerHeight - 1); |
| 66 | + const [fx, fy] = pick ? [pick.fx, pick.fy] : [0.5, 0.5]; |
| 67 | + const x = Math.min(Math.max(box.left + box.width * fx, 0), innerWidth - 1); |
| 68 | + const y = Math.min(Math.max(box.top + box.height * fy, 0), innerHeight - 1); |
34 | 69 | const local = el.getBoundingClientRect(); |
35 | 70 | const hit = el.ownerDocument.elementFromPoint( |
36 | | - local.left + local.width / 2, |
37 | | - local.top + local.height / 2, |
| 71 | + local.left + local.width * fx, |
| 72 | + local.top + local.height * fy, |
38 | 73 | ); |
39 | 74 | let cover = null; |
40 | | - if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) { |
| 75 | + if (!pick && hit && hit !== el && !el.contains(hit) && !hit.contains(el)) { |
41 | 76 | let shown = hit; |
42 | 77 | while (shown && !roleOf(shown, styleOf(shown), "auto")) shown = shown.parentElement; |
43 | 78 | cover = shown ? token(shown, roleOf(shown, styleOf(shown), "auto")) : `<${hit.localName}>`; |
44 | 79 | } |
45 | | - return { x, y, role, label, text: isTextField(el), cover, next: next() }; |
| 80 | + return { x, y, role, label, text: isTextField(el), cover, script: !pick && cover !== null, next: next() }; |
| 81 | + }; |
| 82 | + |
| 83 | + // The page's own click on an element, for when the mouse cannot reach it. |
| 84 | + const clickScript = (ref) => { |
| 85 | + const el = get(ref); |
| 86 | + if (!el) return { missing: true }; |
| 87 | + el.click(); |
| 88 | + return { ok: true }; |
46 | 89 | }; |
47 | 90 |
|
48 | 91 | // Focuses a text field and selects what it holds, so typing replaces it. |
|
112 | 155 | return el && el !== document.body && isTextField(el) ? meaning(el) : { none: true }; |
113 | 156 | }; |
114 | 157 |
|
115 | | - return { point, prepareType, select, scrollEnd, at, focused }; |
| 158 | + return { point, clickScript, prepareType, select, scrollEnd, at, focused }; |
116 | 159 | } |
0 commit comments