|
4 | 4 | // eslint-disable-next-line node/no-missing-import
|
5 | 5 | import options from 'virtual:svelte-inspector-options';
|
6 | 6 | const toggle_combo = options.toggleKeyCombo?.toLowerCase().split('-');
|
7 |
| -
|
| 7 | + const nav_keys = Object.values(options.navKeys).map((k) => k.toLowerCase()); |
8 | 8 | let enabled = false;
|
9 | 9 |
|
10 | 10 | const icon = `data:image/svg+xml;base64,${btoa(
|
|
37 | 37 | y = event.y;
|
38 | 38 | }
|
39 | 39 |
|
40 |
| - function find_parent_with_meta(el) { |
41 |
| - while (el) { |
42 |
| - if (has_meta(el)) { |
| 40 | + function find_selectable_parent(el) { |
| 41 | + do { |
| 42 | + el = el.parentNode; |
| 43 | + if (is_selectable(el)) { |
43 | 44 | return el;
|
44 | 45 | }
|
45 |
| - el = el.parentNode; |
46 |
| - } |
| 46 | + } while (el); |
| 47 | + } |
| 48 | +
|
| 49 | + function find_selectable_child(el) { |
| 50 | + return [...el.querySelectorAll('*')].find(is_selectable); |
| 51 | + } |
| 52 | +
|
| 53 | + function find_selectable_sibling(el, prev = false) { |
| 54 | + do { |
| 55 | + el = prev ? el.previousElementSibling : el.nextElementSibling; |
| 56 | + if (is_selectable(el)) { |
| 57 | + return el; |
| 58 | + } |
| 59 | + } while (el); |
47 | 60 | }
|
48 | 61 |
|
49 |
| - function find_child_with_meta(el) { |
50 |
| - return [...el.querySelectorAll('*')].find(has_meta); |
| 62 | + function find_selectable_for_nav(key) { |
| 63 | + const el = active_el; |
| 64 | + if (!el) { |
| 65 | + return find_selectable_child(document?.body); |
| 66 | + } |
| 67 | + switch (key) { |
| 68 | + case options.navKeys.parent: |
| 69 | + return find_selectable_parent(el); |
| 70 | + case options.navKeys.child: |
| 71 | + return find_selectable_child(el); |
| 72 | + case options.navKeys.next: |
| 73 | + return find_selectable_sibling(el) || find_selectable_parent(el); |
| 74 | + case options.navKeys.prev: |
| 75 | + return find_selectable_sibling(el, true) || find_selectable_parent(el); |
| 76 | + default: |
| 77 | + return; |
| 78 | + } |
51 | 79 | }
|
52 | 80 |
|
53 |
| - function has_meta(el) { |
54 |
| - const file = el.__svelte_meta?.loc?.file; |
55 |
| - return el !== toggle_el && file && !file.includes('node_modules/'); |
| 81 | + function is_selectable(el) { |
| 82 | + if (el === toggle_el) { |
| 83 | + return false; // toggle is our own |
| 84 | + } |
| 85 | + const file = el?.__svelte_meta?.loc?.file; |
| 86 | + if (!file || file.includes('node_modules/')) { |
| 87 | + return false; // no file or 3rd party |
| 88 | + } |
| 89 | + if (['svelte-announcer', 'svelte-inspector-announcer'].includes(el.getAttribute('id'))) { |
| 90 | + return false; // ignore some elements by id that would be selectable from keyboard nav otherwise |
| 91 | + } |
| 92 | + return true; |
56 | 93 | }
|
57 | 94 |
|
58 | 95 | function mouseover(event) {
|
59 |
| - const el = find_parent_with_meta(event.target); |
60 |
| - activate(el); |
| 96 | + const el = find_selectable_parent(event.target); |
| 97 | + activate(el, false); |
61 | 98 | }
|
62 | 99 |
|
63 |
| - function activate(el) { |
| 100 | + function activate(el, set_bubble_pos = true) { |
64 | 101 | if (options.customStyles && el !== active_el) {
|
65 | 102 | if (active_el) {
|
66 | 103 | active_el.classList.remove('svelte-inspector-active-target');
|
|
76 | 113 | file_loc = null;
|
77 | 114 | }
|
78 | 115 | active_el = el;
|
| 116 | + if (set_bubble_pos) { |
| 117 | + const pos = el.getBoundingClientRect(); |
| 118 | + x = Math.ceil(pos.left); |
| 119 | + y = Math.ceil(pos.bottom - 20); |
| 120 | + } |
79 | 121 | }
|
80 | 122 |
|
81 |
| - function click(event) { |
| 123 | + function open_editor(event) { |
82 | 124 | if (file_loc) {
|
83 | 125 | stop(event);
|
84 | 126 | fetch(`/__open-in-editor?file=${encodeURIComponent(file_loc)}`);
|
|
104 | 146 | return toggle_combo?.every((key) => is_key_active(key, event));
|
105 | 147 | }
|
106 | 148 |
|
| 149 | + function is_nav(event) { |
| 150 | + return nav_keys?.some((key) => is_key_active(key, event)); |
| 151 | + } |
| 152 | +
|
| 153 | + function is_open(event) { |
| 154 | + return options.openKey && options.openKey.toLowerCase() === event.key.toLowerCase(); |
| 155 | + } |
| 156 | +
|
107 | 157 | function is_holding() {
|
108 | 158 | return enabled_ts && Date.now() - enabled_ts > 250;
|
109 | 159 | }
|
|
124 | 174 | if (options.holdMode && enabled) {
|
125 | 175 | enabled_ts = Date.now();
|
126 | 176 | }
|
127 |
| - } else if (event.key === options.drillKeys.up && active_el) { |
128 |
| - const el = find_parent_with_meta(active_el.parentNode); |
129 |
| - if (el) { |
130 |
| - activate(el); |
131 |
| - stop(event); |
132 |
| - } |
133 |
| - } else if (event.key === options.drillKeys.down && active_el) { |
134 |
| - const el = find_child_with_meta(active_el); |
| 177 | + } else if (is_nav(event)) { |
| 178 | + const el = find_selectable_for_nav(event.key); |
135 | 179 | if (el) {
|
136 | 180 | activate(el);
|
137 | 181 | stop(event);
|
138 | 182 | }
|
| 183 | + } else if (is_open(event)) { |
| 184 | + open_editor(event); |
139 | 185 | }
|
140 | 186 | }
|
141 | 187 |
|
|
159 | 205 | const l = enabled ? body.addEventListener : body.removeEventListener;
|
160 | 206 | l('mousemove', mousemove);
|
161 | 207 | l('mouseover', mouseover);
|
162 |
| - l('click', click, true); |
| 208 | + l('click', open_editor, true); |
163 | 209 | }
|
164 | 210 |
|
165 | 211 | function enable() {
|
|
169 | 215 | b.classList.add('svelte-inspector-enabled');
|
170 | 216 | }
|
171 | 217 | listeners(b, enabled);
|
| 218 | + activate_initial_el(); |
| 219 | + } |
| 220 | +
|
| 221 | + function activate_initial_el() { |
| 222 | + const hov = innermost_hover_el(); |
| 223 | + let el = is_selectable(hov) ? hov : find_selectable_parent(hov); |
| 224 | + if (!el) { |
| 225 | + const act = document.activeElement; |
| 226 | + el = is_selectable(act) ? act : find_selectable_parent(act); |
| 227 | + } |
| 228 | + if (!el) { |
| 229 | + el = find_selectable_child(document.body); |
| 230 | + } |
| 231 | + if (el) { |
| 232 | + activate(el); |
| 233 | + } |
| 234 | + } |
| 235 | +
|
| 236 | + function innermost_hover_el() { |
| 237 | + let e = document.body.querySelector(':hover'); |
| 238 | + let result; |
| 239 | + while (e) { |
| 240 | + result = e; |
| 241 | + e = e.querySelector(':hover'); |
| 242 | + } |
| 243 | + return result; |
172 | 244 | }
|
173 | 245 |
|
174 | 246 | function disable() {
|
|
213 | 285 | </script>
|
214 | 286 |
|
215 | 287 | {#if show_toggle}
|
216 |
| - <div |
| 288 | + <button |
217 | 289 | class="svelte-inspector-toggle"
|
218 | 290 | class:enabled
|
219 | 291 | style={`background-image: var(--svelte-inspector-icon);${options.toggleButtonPos
|
|
222 | 294 | .join('')}`}
|
223 | 295 | on:click={() => toggle()}
|
224 | 296 | bind:this={toggle_el}
|
| 297 | + aria-label={`${enabled ? 'disable' : 'enable'} svelte-inspector`} |
225 | 298 | />
|
226 | 299 | {/if}
|
227 |
| -{#if enabled && file_loc} |
| 300 | +{#if enabled && active_el && file_loc} |
| 301 | + {@const loc = active_el.__svelte_meta.loc} |
228 | 302 | <div
|
229 | 303 | class="svelte-inspector-overlay"
|
230 |
| - style:left="{Math.min(x + 3, document.body.clientWidth - w - 10)}px" |
231 |
| - style:top="{y + 30}px" |
| 304 | + style:left="{Math.min(x + 3, document.documentElement.clientWidth - w - 10)}px" |
| 305 | + style:top="{document.documentElement.clientHeight < y + 50 ? y - 30 : y + 30}px" |
232 | 306 | bind:offsetWidth={w}
|
233 | 307 | >
|
234 | 308 | <{active_el.tagName.toLowerCase()}> {file_loc}
|
235 | 309 | </div>
|
| 310 | + <div id="svelte-inspector-announcer" aria-live="assertive" aria-atomic="true"> |
| 311 | + {active_el.tagName.toLowerCase()} in file {loc.file} on line {loc.line} column {loc.column} |
| 312 | + </div> |
236 | 313 | {/if}
|
237 | 314 |
|
238 | 315 | <style>
|
|
253 | 330 | }
|
254 | 331 |
|
255 | 332 | .svelte-inspector-toggle {
|
| 333 | + all: unset; |
256 | 334 | border: 1px solid #ff3e00;
|
257 | 335 | border-radius: 8px;
|
258 | 336 | position: fixed;
|
|
264 | 342 | cursor: pointer;
|
265 | 343 | }
|
266 | 344 |
|
| 345 | + #svelte-inspector-announcer { |
| 346 | + position: absolute; |
| 347 | + left: 0px; |
| 348 | + top: 0px; |
| 349 | + clip: rect(0px, 0px, 0px, 0px); |
| 350 | + clip-path: inset(50%); |
| 351 | + overflow: hidden; |
| 352 | + white-space: nowrap; |
| 353 | + width: 1px; |
| 354 | + height: 1px; |
| 355 | + } |
| 356 | +
|
267 | 357 | .svelte-inspector-toggle:not(.enabled) {
|
268 | 358 | filter: grayscale(1);
|
269 | 359 | }
|
|
0 commit comments