webview-preview 1.1.2 → 1.2.1

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.
@@ -0,0 +1,787 @@
1
+ export function annotatePanel({ key, name, markup, styles, icons: ICONS, keepOpen = false }, { keys, selectors, react, vue, dom }) {
2
+ const { DEFAULT_KEYS, KEY_ACTIONS, bindings, showKey, comboOf, actionFor } = keys;
3
+ const { textOf, describe, shortName, buildSelector, lookOf, isPrivate, privateElements } = selectors;
4
+ const componentOf = element => react.componentOf(element) ?? vue.componentOf(element);
5
+ if (window[key]?.status === "open") return "open";
6
+ const draft = window[`${key}Draft`] ?? null;
7
+ delete window[`${key}Draft`];
8
+ const state = { status: "open", result: null, keys: null, version: 0, captures: [] }
9
+ window[key] = state;
10
+ if (draft?.keys) {
11
+ Object.assign(bindings, draft.keys);
12
+ state.keys = { ...bindings }
13
+ }
14
+
15
+ const host = document.createElement("webview-preview-annotate");
16
+ host.style.cssText = "all: initial; position: fixed; inset: 0; z-index: 2147483647; pointer-events: none";
17
+ const root = host.attachShadow({ mode: "closed" });
18
+ dom.adoptStyles(root, styles);
19
+ root.append(dom.fragment(markup));
20
+ document.documentElement.append(host);
21
+ const noSelect = document.createElement("style");
22
+ noSelect.textContent = "* { user-select: none !important; -webkit-user-select: none !important }";
23
+
24
+ const $ = selector => root.querySelector(selector);
25
+ const dock = $(".dock");
26
+ const toolbar = $(".toolbar");
27
+ const outline = $(".outline");
28
+ const outlineLabel = $(".outline-label");
29
+ const picks = $(".picks");
30
+ const masksLayer = $(".masks");
31
+ const privacy = $(".privacy");
32
+ const notesList = $(".notes");
33
+ const general = $(".general");
34
+ const pickButton = $(".pick");
35
+ const notesButton = $(".notes-button");
36
+ const notesBadge = $(".notes-button .badge");
37
+ const freezeButton = $(".freeze");
38
+ const freezeBadge = $(".freeze-badge");
39
+ const toggleButton = $(".toggle");
40
+ const keysButton = $(".keys");
41
+ const notesSheet = $(".notes-sheet");
42
+ const keysSheet = $(".keys-sheet");
43
+ const keyRows = $(".rows");
44
+ const tip = $(".tip");
45
+ const hint = $(".hint");
46
+ $(".run-name").textContent = name;
47
+
48
+ let listeningFor = null;
49
+ let isCollapsed = false;
50
+ const notes = [];
51
+ const records = new Map();
52
+ const markers = new Map();
53
+ let nextElement = 1;
54
+ let masked = [];
55
+ const pending = new Set();
56
+ let nextNote = 1;
57
+ let isPicking = true;
58
+ let isFrozen = false;
59
+ let freezeTimer = 0;
60
+ let pausedAnimations = [];
61
+ const FREEZE_SECONDS = 3;
62
+ const HELD_EVENTS = ["mouseleave", "mouseout", "pointerleave", "pointerout", "mousemove", "pointermove", "blur", "focusout"];
63
+ let hovered = null;
64
+ let hoverTrail = [];
65
+ let activeNote = null;
66
+ let frame = 0;
67
+
68
+ const noteNumber = note => notes.indexOf(note) + 1;
69
+ const noteMark = note => String(noteNumber(note));
70
+ let openSheet = null;
71
+ const isOurs = event => event.composedPath().includes(host);
72
+ const changed = () => {
73
+ state.version += 1;
74
+ }
75
+
76
+ const infoOf = element => {
77
+ const rect = element.getBoundingClientRect();
78
+ return {
79
+ ...buildSelector(element),
80
+ tag: element.tagName.toLowerCase(),
81
+ role: element.getAttribute("role"),
82
+ name: isPrivate(element) ? null : element.getAttribute("aria-label"),
83
+ text: textOf(element).slice(0, 120) || null,
84
+ label: describe(element),
85
+ component: componentOf(element),
86
+ ...lookOf(element),
87
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
88
+ url: location.href,
89
+ isPrivate: isPrivate(element)
90
+ }
91
+ }
92
+
93
+ const syncMasks = () => {
94
+ const elements = privateElements();
95
+ const isSame = elements.length === masked.length && elements.every((element, index) => masked[index].element === element);
96
+ if (!isSame) {
97
+ masksLayer.replaceChildren();
98
+ masked = elements.map(element => {
99
+ const group = document.createElement("div");
100
+ masksLayer.append(group);
101
+ return { element, group }
102
+ });
103
+ }
104
+ privacy.textContent = `Notes, page text and pictures go to whoever started this run. Hidden: ${masked.length} private element(s).`;
105
+ }
106
+
107
+ const drawMasks = () => {
108
+ for (const { element, group } of masked) {
109
+ const rects = element.isConnected ? [...element.getClientRects()].filter(rect => rect.width > 0 && rect.height > 0) : [];
110
+ while (group.children.length < rects.length) {
111
+ const box = document.createElement("div");
112
+ box.className = "mask";
113
+ group.append(box);
114
+ }
115
+ while (group.children.length > rects.length) group.lastElementChild.remove();
116
+ rects.forEach((rect, index) => {
117
+ Object.assign(group.children[index].style, { left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, height: `${rect.height}px` });
118
+ });
119
+ }
120
+ }
121
+
122
+ const liveElement = id => {
123
+ const record = records.get(id);
124
+ return record?.element?.isConnected && record.info.url === location.href ? record.element : null;
125
+ }
126
+
127
+ const findAgain = info => {
128
+ if (info.url !== location.href) return null;
129
+ try {
130
+ return document.querySelector(info.selector);
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ const elementId = element => {
137
+ for (const [id, record] of records) {
138
+ if (record.element === element && record.info.url === location.href) return id;
139
+ }
140
+ const id = `e${nextElement++}`;
141
+ const info = infoOf(element);
142
+ records.set(id, { element, info });
143
+ pending.add(id);
144
+ state.captures.push({ id, rect: info.rect });
145
+ changed();
146
+ return id;
147
+ }
148
+
149
+ const labelOf = id => records.get(id)?.info.label ?? id;
150
+
151
+ const pickAt = (x, y) => document.elementsFromPoint(x, y).find(element => element !== host && element !== document.documentElement) ?? null;
152
+
153
+ const showOutline = element => {
154
+ if (!element || !isPicking) {
155
+ outline.style.display = "none";
156
+ return;
157
+ }
158
+ const rect = element.getBoundingClientRect();
159
+ Object.assign(outline.style, { display: "block", left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, height: `${rect.height}px` });
160
+ const parent = element.parentElement && element.parentElement !== document.body && element.parentElement !== document.documentElement ? `${shortName(element.parentElement)} › ` : "";
161
+ outlineLabel.textContent = `${parent}${describe(element)} · ${Math.round(rect.width)} × ${Math.round(rect.height)}`;
162
+ outlineLabel.classList.toggle("below", rect.top < 28);
163
+ }
164
+
165
+ const noteSummary = note => note.text.trim() || `${note.ids.length === 1 ? labelOf(note.ids[0]) : `${note.ids.length} elements`} — no text yet`;
166
+
167
+ const noteEditor = note => {
168
+ const body = document.createElement("div");
169
+ body.className = "note-body";
170
+ const elements = document.createElement("ul");
171
+ elements.className = "elements";
172
+ elements.append(...note.ids.map((id, index) => {
173
+ const item = document.createElement("li");
174
+ const label = document.createElement("span");
175
+ const isElsewhere = !liveElement(id) && records.get(id)?.info.url !== location.href;
176
+ label.textContent = `${note.ids.length > 1 ? String.fromCharCode(97 + index) + " · " : ""}${labelOf(id)}${isElsewhere ? " · another page" : ""}`;
177
+ item.append(label);
178
+ if (note.ids.length > 1) {
179
+ const remove = document.createElement("button");
180
+ remove.type = "button";
181
+ remove.className = "tool";
182
+ remove.setAttribute("aria-label", "Remove from the note");
183
+ remove.replaceChildren(dom.fragment(ICONS.cancel));
184
+ remove.addEventListener("click", () => {
185
+ note.ids.splice(index, 1);
186
+ changed();
187
+ update();
188
+ });
189
+ item.append(remove);
190
+ }
191
+ return item;
192
+ }));
193
+ const text = document.createElement("textarea");
194
+ text.className = "text";
195
+ text.rows = 3;
196
+ text.placeholder = "What to change";
197
+ text.value = note.text;
198
+ text.addEventListener("input", () => {
199
+ note.text = text.value;
200
+ changed();
201
+ const summary = body.previousElementSibling?.querySelector("span");
202
+ if (summary) {
203
+ summary.textContent = noteSummary(note);
204
+ summary.classList.toggle("empty", !note.text.trim());
205
+ }
206
+ });
207
+ const actions = document.createElement("div");
208
+ actions.className = "actions";
209
+ const remove = document.createElement("button");
210
+ remove.type = "button";
211
+ remove.className = "button danger";
212
+ remove.textContent = "Delete note";
213
+ remove.addEventListener("click", () => {
214
+ notes.splice(notes.indexOf(note), 1);
215
+ activeNote = null;
216
+ changed();
217
+ update();
218
+ });
219
+ actions.append(remove);
220
+ body.append(elements, text, actions);
221
+ return body;
222
+ }
223
+
224
+ const renderNotes = () => {
225
+ notesBadge.textContent = notes.length ? String(notes.length) : "";
226
+ notesList.replaceChildren(...notes.map(note => {
227
+ const item = document.createElement("li");
228
+ const row = document.createElement("div");
229
+ row.className = `note${note === activeNote ? " active" : ""}`;
230
+ const mark = document.createElement("b");
231
+ mark.textContent = noteMark(note);
232
+ const summary = document.createElement("span");
233
+ summary.textContent = noteSummary(note);
234
+ if (!note.text.trim()) summary.className = "empty";
235
+ row.append(mark, summary);
236
+ row.addEventListener("click", () => openNote(note === activeNote ? null : note, true));
237
+ item.append(row);
238
+ if (note === activeNote) item.append(noteEditor(note));
239
+ return item;
240
+ }));
241
+ }
242
+
243
+ const update = () => {
244
+ renderNotes();
245
+ drawPicks();
246
+ }
247
+
248
+ const openNote = (note, shouldScroll = false) => {
249
+ activeNote = note;
250
+ if (note) showSheet("notes");
251
+ if (shouldScroll && note) liveElement(note.ids[0])?.scrollIntoView({ block: "nearest", inline: "nearest" });
252
+ update();
253
+ if (note) notesList.querySelector(".text")?.focus();
254
+ }
255
+
256
+ const drawPicks = () => {
257
+ const seen = new Set();
258
+ for (const note of notes) {
259
+ note.ids.forEach((id, index) => {
260
+ const element = liveElement(id);
261
+ if (!element || pending.has(id)) return;
262
+ const key = `${note.uid}:${id}`;
263
+ seen.add(key);
264
+ let entry = markers.get(key);
265
+ if (!entry) {
266
+ const box = document.createElement("div");
267
+ box.className = "picked";
268
+ const marker = document.createElement("div");
269
+ marker.addEventListener("click", event => {
270
+ event.stopPropagation();
271
+ openNote(note);
272
+ });
273
+ picks.append(box, marker);
274
+ entry = { box, marker }
275
+ markers.set(key, entry);
276
+ }
277
+ const rect = element.getBoundingClientRect();
278
+ Object.assign(entry.box.style, { left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, height: `${rect.height}px` });
279
+ entry.marker.className = `marker${note === activeNote ? " active" : ""}`;
280
+ entry.marker.textContent = `${noteMark(note)}${note.ids.length > 1 ? String.fromCharCode(97 + index) : ""}`;
281
+ Object.assign(entry.marker.style, {
282
+ left: `${Math.min(Math.max(2, rect.left - 10), innerWidth - 30)}px`,
283
+ top: `${Math.min(Math.max(2, rect.top - 10), innerHeight - 22)}px`
284
+ });
285
+ });
286
+ }
287
+ for (const [key, entry] of markers) {
288
+ if (seen.has(key)) continue;
289
+ entry.box.remove();
290
+ entry.marker.remove();
291
+ markers.delete(key);
292
+ }
293
+ }
294
+
295
+ const relink = () => {
296
+ for (const record of records.values()) {
297
+ if (record.element?.isConnected) continue;
298
+ record.element = findAgain(record.info);
299
+ }
300
+ }
301
+
302
+ const pick = (element, isAdding) => {
303
+ if (state.status !== "open") return;
304
+ hovered = null;
305
+ showOutline(null);
306
+ const id = elementId(element);
307
+ if (isAdding && activeNote) {
308
+ if (!activeNote.ids.includes(id)) {
309
+ activeNote.ids.push(id);
310
+ changed();
311
+ }
312
+ openNote(activeNote);
313
+ return;
314
+ }
315
+ const existing = notes.find(note => note.ids.includes(id));
316
+ if (existing) {
317
+ openNote(existing);
318
+ return;
319
+ }
320
+ const note = { uid: nextNote++, text: "", ids: [id], url: location.href, isFrozen }
321
+ notes.push(note);
322
+ changed();
323
+ openNote(note);
324
+ }
325
+
326
+ const setPicking = value => {
327
+ isPicking = value;
328
+ if (value) {
329
+ document.head.append(noSelect);
330
+ getSelection()?.removeAllRanges();
331
+ } else noSelect.remove();
332
+ pickButton.setAttribute("aria-pressed", String(value));
333
+ if (!value) showOutline(null);
334
+ }
335
+
336
+ const tipText = button => {
337
+ const action = button.dataset.action;
338
+ if (action === "toggle") return isCollapsed ? "Show tools" : "Hide tools";
339
+ if (action === "pick") return isPicking ? "Pick elements — on" : "Pick elements — off";
340
+ if (button.classList.contains("done") && keepOpen) return "Finish — sends what is left and closes the window";
341
+ if (action === "freeze") return isFrozen ? "Unfreeze" : freezeTimer ? "Freezing… press again to stop" : `Freeze in ${FREEZE_SECONDS} s — hover what has to stay open`;
342
+ return button.dataset.tip;
343
+ }
344
+
345
+ const showTip = button => {
346
+ const action = button.dataset.action;
347
+ tip.replaceChildren();
348
+ tip.append(tipText(button));
349
+ if (action && bindings[action] && !(keepOpen && button.classList.contains("done"))) {
350
+ const shortcut = document.createElement("kbd");
351
+ shortcut.textContent = showKey(bindings[action]);
352
+ tip.append(shortcut);
353
+ }
354
+ const rect = button.getBoundingClientRect();
355
+ tip.style.display = "block";
356
+ const width = tip.offsetWidth;
357
+ const height = tip.offsetHeight;
358
+ const hasRoomRight = rect.right + 8 + width <= innerWidth - 8;
359
+ const left = hasRoomRight ? rect.right + 8 : Math.max(8, rect.left - 8 - width);
360
+ const top = Math.min(Math.max(8, rect.top + rect.height / 2 - height / 2), innerHeight - height - 8);
361
+ Object.assign(tip.style, { left: `${left}px`, top: `${top}px` });
362
+ }
363
+
364
+ const placeDock = () => {
365
+ const rect = dock.getBoundingClientRect();
366
+ const left = Math.min(Math.max(0, rect.left), innerWidth - rect.width);
367
+ const top = Math.min(Math.max(0, rect.top), innerHeight - rect.height);
368
+ if (left !== rect.left || top !== rect.top) Object.assign(dock.style, { left: `${left}px`, top: `${top}px` });
369
+ const sheet = openSheet === "notes" ? notesSheet : openSheet === "keys" ? keysSheet : null;
370
+ const sheetWidth = sheet ? sheet.offsetWidth : Math.min(300, innerWidth - 72);
371
+ const sheetHeight = sheet ? sheet.offsetHeight : 0;
372
+ const hasRoomRight = left + rect.width + 6 + sheetWidth <= innerWidth - 8;
373
+ const hasRoomLeft = left - 6 - sheetWidth >= 8;
374
+ dock.classList.toggle("flip", !hasRoomRight && hasRoomLeft);
375
+ dock.classList.toggle("raise", top + sheetHeight > innerHeight - 8 && top + rect.height - sheetHeight >= 8);
376
+ }
377
+
378
+ const renderHint = () => {
379
+ hint.replaceChildren();
380
+ const parts = [
381
+ ["Click", "new note"],
382
+ ["Shift+click", "add to the open note"],
383
+ [`Alt+wheel, ${showKey(bindings.parent)} ${showKey(bindings.child)}`, "parent / child"],
384
+ [showKey(bindings.pick), "pick on / off"],
385
+ [showKey(bindings.done), keepOpen ? "send" : "done"]
386
+ ];
387
+ parts.forEach(([keys, text], index) => {
388
+ const shortcut = document.createElement("kbd");
389
+ shortcut.textContent = keys;
390
+ hint.append(shortcut, ` ${text}${index < parts.length - 1 ? " · " : ""}`);
391
+ });
392
+ }
393
+
394
+ const renderKeys = () => {
395
+ keyRows.replaceChildren(...KEY_ACTIONS.map(([action, label]) => {
396
+ const row = document.createElement("div");
397
+ row.className = "row";
398
+ const text = document.createElement("span");
399
+ text.textContent = label;
400
+ const button = document.createElement("button");
401
+ button.type = "button";
402
+ button.className = `kbd${listeningFor === action ? " listening" : ""}`;
403
+ button.textContent = listeningFor === action ? "Press a key…" : showKey(bindings[action]);
404
+ button.addEventListener("click", () => {
405
+ listeningFor = listeningFor === action ? null : action;
406
+ renderKeys();
407
+ });
408
+ row.append(text, button);
409
+ return row;
410
+ }));
411
+ renderHint();
412
+ }
413
+
414
+ const setBinding = (action, combo) => {
415
+ const taken = actionFor(combo);
416
+ if (taken && taken !== action) bindings[taken] = bindings[action];
417
+ bindings[action] = combo;
418
+ state.keys = { ...bindings }
419
+ changed();
420
+ listeningFor = null;
421
+ renderKeys();
422
+ }
423
+
424
+ const showSheet = sheet => {
425
+ openSheet = sheet;
426
+ notesSheet.hidden = sheet !== "notes";
427
+ keysSheet.hidden = sheet !== "keys";
428
+ notesButton.setAttribute("aria-pressed", String(sheet === "notes"));
429
+ keysButton.setAttribute("aria-pressed", String(sheet === "keys"));
430
+ listeningFor = null;
431
+ renderKeys();
432
+ }
433
+
434
+ const setCollapsed = value => {
435
+ isCollapsed = value;
436
+ dock.classList.toggle("collapsed", value);
437
+ toggleButton.replaceChildren(dom.fragment(value ? ICONS.open : ICONS.close));
438
+ toggleButton.setAttribute("aria-label", value ? "Show tools" : "Hide tools");
439
+ toggleButton.setAttribute("aria-expanded", String(!value));
440
+ if (value) listeningFor = null;
441
+ }
442
+
443
+ const collect = () => {
444
+ const elements = {}
445
+ for (const id of new Set(notes.flatMap(note => note.ids))) {
446
+ const element = liveElement(id);
447
+ const record = records.get(id);
448
+ if (element) {
449
+ try {
450
+ record.info = infoOf(element);
451
+ } catch {}
452
+ }
453
+ elements[id] = { ...record.info, isLive: !!element }
454
+ }
455
+ return {
456
+ notes: notes.map(note => ({ id: noteNumber(note), text: note.text.trim(), elements: [...note.ids], url: note.url, isFrozen: !!note.isFrozen })),
457
+ generalNote: general.value.trim() || null,
458
+ url: location.href,
459
+ viewport: { width: innerWidth, height: innerHeight },
460
+ maskedCount: masked.length,
461
+ elements
462
+ }
463
+ }
464
+
465
+ state.draft = () => ({
466
+ notes: notes.map(note => ({ uid: note.uid, text: note.text, ids: [...note.ids], url: note.url, isFrozen: !!note.isFrozen })),
467
+ generalNote: general.value,
468
+ elements: Object.fromEntries([...records].map(([id, record]) => [id, record.info])),
469
+ nextElement,
470
+ nextNote,
471
+ keys: state.keys
472
+ });
473
+
474
+ const hidden = new Set();
475
+ state.setHidden = value => {
476
+ if (!value) {
477
+ for (const part of hidden) part.style.visibility = "";
478
+ hidden.clear();
479
+ pending.clear();
480
+ state.capturing = [];
481
+ return;
482
+ }
483
+ const rects = (state.capturing ?? []).map(capture => capture.rect);
484
+ const covers = part => {
485
+ const rect = part.getBoundingClientRect();
486
+ return rect.width > 0 && rects.some(target => rect.left < target.x + target.width + 12 && rect.right > target.x - 12
487
+ && rect.top < target.y + target.height + 12 && rect.bottom > target.y - 12);
488
+ }
489
+ const parts = [outline, tip, toolbar, notesSheet, keysSheet, ...[...markers.values()].flatMap(entry => [entry.box, entry.marker])];
490
+ for (const part of parts) {
491
+ if (!covers(part)) continue;
492
+ part.style.visibility = "hidden";
493
+ hidden.add(part);
494
+ }
495
+ }
496
+
497
+ const blocked = ["pointerdown", "pointerup", "mousedown", "mouseup", "click", "dblclick", "contextmenu"];
498
+ const onBlocked = event => {
499
+ if (!isPicking || isOurs(event) || event.button > 0) return;
500
+ if (event.type === "click" && event.detail === 0) return;
501
+ event.preventDefault();
502
+ event.stopImmediatePropagation();
503
+ if (event.type !== "click") return;
504
+ const element = pickAt(event.clientX, event.clientY);
505
+ if (element) pick(element, event.shiftKey);
506
+ }
507
+
508
+ const onMove = event => {
509
+ if (!isPicking) return;
510
+ if (isOurs(event)) {
511
+ showOutline(null);
512
+ return;
513
+ }
514
+ hovered = pickAt(event.clientX, event.clientY);
515
+ hoverTrail = [];
516
+ showOutline(hovered);
517
+ }
518
+
519
+ const isEditable = target => target instanceof Element && (target.isContentEditable || /^(input|textarea|select)$/i.test(target.tagName));
520
+
521
+ const holdEvent = event => {
522
+ if (!isOurs(event)) event.stopImmediatePropagation();
523
+ }
524
+
525
+ const stopCountdown = () => {
526
+ clearInterval(freezeTimer);
527
+ freezeTimer = 0;
528
+ freezeBadge.textContent = "";
529
+ }
530
+
531
+ const setFrozen = value => {
532
+ stopCountdown();
533
+ if (value === isFrozen) return;
534
+ isFrozen = value;
535
+ if (value) {
536
+ pausedAnimations = document.getAnimations().filter(animation => animation.playState === "running");
537
+ for (const animation of pausedAnimations) animation.pause();
538
+ for (const type of HELD_EVENTS) window.addEventListener(type, holdEvent, true);
539
+ } else {
540
+ for (const animation of pausedAnimations) {
541
+ try {
542
+ animation.play();
543
+ } catch {}
544
+ }
545
+ pausedAnimations = [];
546
+ for (const type of HELD_EVENTS) window.removeEventListener(type, holdEvent, true);
547
+ }
548
+ freezeButton.setAttribute("aria-pressed", String(value));
549
+ }
550
+
551
+ const toggleFreeze = () => {
552
+ if (isFrozen) return setFrozen(false);
553
+ if (freezeTimer) return stopCountdown();
554
+ let left = FREEZE_SECONDS;
555
+ freezeBadge.textContent = String(left);
556
+ freezeTimer = setInterval(() => {
557
+ left -= 1;
558
+ if (left <= 0) setFrozen(true);
559
+ else freezeBadge.textContent = String(left);
560
+ }, 1000);
561
+ }
562
+
563
+ const run = action => {
564
+ if (action === "done") keepOpen ? send() : finish("done");
565
+ else if (action === "freeze") toggleFreeze();
566
+ else if (action === "pick") setPicking(!isPicking);
567
+ else if (action === "toggle") setCollapsed(!isCollapsed);
568
+ else if (action === "notes") showSheet(openSheet === "notes" ? null : "notes");
569
+ else if (action === "parent" && hovered?.parentElement && hovered.parentElement !== document.documentElement) {
570
+ hoverTrail.push(hovered);
571
+ hovered = hovered.parentElement;
572
+ showOutline(hovered);
573
+ } else if (action === "child" && hoverTrail.length) {
574
+ hovered = hoverTrail.pop();
575
+ showOutline(hovered);
576
+ } else return false;
577
+ return true;
578
+ }
579
+
580
+ const onKey = event => {
581
+ if (state.status !== "open") return;
582
+ const fromUs = isOurs(event);
583
+ const combo = comboOf(event);
584
+ if (listeningFor) {
585
+ if (!combo) return;
586
+ event.preventDefault();
587
+ event.stopImmediatePropagation();
588
+ if (combo === "Escape") {
589
+ listeningFor = null;
590
+ renderKeys();
591
+ } else setBinding(listeningFor, combo);
592
+ return;
593
+ }
594
+ const action = combo && actionFor(combo);
595
+ const isTyping = isEditable(fromUs ? root.activeElement : event.target);
596
+ if (action === "done") {
597
+ event.preventDefault();
598
+ event.stopImmediatePropagation();
599
+ run("done");
600
+ return;
601
+ }
602
+ if (combo === "Escape" && openSheet) {
603
+ event.preventDefault();
604
+ event.stopImmediatePropagation();
605
+ activeNote = null;
606
+ showSheet(null);
607
+ update();
608
+ return;
609
+ }
610
+ if (fromUs && isTyping) {
611
+ event.stopImmediatePropagation();
612
+ return;
613
+ }
614
+ if (!fromUs && isTyping) return;
615
+ if (action && (action !== "parent" && action !== "child" || (isPicking && hovered)) && run(action)) {
616
+ event.preventDefault();
617
+ event.stopImmediatePropagation();
618
+ } else if (fromUs) event.stopImmediatePropagation();
619
+ }
620
+
621
+ const onWheel = event => {
622
+ if (!isPicking || !event.altKey || isOurs(event) || !hovered) return;
623
+ event.preventDefault();
624
+ event.stopImmediatePropagation();
625
+ if (event.deltaY < 0) run("parent");
626
+ else if (event.deltaY > 0) run("child");
627
+ }
628
+
629
+ const onSelectStart = event => {
630
+ if (isPicking && !isOurs(event)) event.preventDefault();
631
+ }
632
+
633
+ const stopInside = event => event.stopPropagation();
634
+ for (const type of ["keydown", "keyup", "keypress", "input", "beforeinput", "wheel", "focusin", "focusout"]) host.addEventListener(type, stopInside);
635
+
636
+ let relinkCountdown = 0;
637
+ const tick = () => {
638
+ relinkCountdown -= 1;
639
+ if (relinkCountdown <= 0) {
640
+ relinkCountdown = 30;
641
+ relink();
642
+ syncMasks();
643
+ }
644
+ drawPicks();
645
+ drawMasks();
646
+ placeDock();
647
+ if (hovered && isPicking) showOutline(hovered);
648
+ frame = requestAnimationFrame(tick);
649
+ }
650
+
651
+ const send = () => {
652
+ if (state.status !== "open" || (notes.length === 0 && !general.value.trim())) return;
653
+ setFrozen(false);
654
+ try {
655
+ state.result = collect();
656
+ } catch (error) {
657
+ state.result = { notes: [], generalNote: general.value.trim() || null, url: location.href, viewport: { width: innerWidth, height: innerHeight }, elements: {}, error: String(error) }
658
+ }
659
+ state.status = "sent";
660
+ activeNote = null;
661
+ syncMasks();
662
+ drawMasks();
663
+ drawPicks();
664
+ for (const part of [dock, tip, outline]) part.style.display = "none";
665
+ state.cleanup = () => {
666
+ picks.style.display = "none";
667
+ }
668
+ state.resume = () => {
669
+ notes.length = 0;
670
+ records.clear();
671
+ for (const entry of markers.values()) {
672
+ entry.box.remove();
673
+ entry.marker.remove();
674
+ }
675
+ markers.clear();
676
+ pending.clear();
677
+ nextElement = 1;
678
+ nextNote = 1;
679
+ general.value = "";
680
+ picks.style.display = "";
681
+ for (const part of [dock, tip, outline]) part.style.display = "";
682
+ state.result = null;
683
+ state.status = "open";
684
+ changed();
685
+ showSheet(null);
686
+ update();
687
+ }
688
+ }
689
+
690
+ const finish = status => {
691
+ if (state.status !== "open") return;
692
+ setFrozen(false);
693
+ try {
694
+ state.result = status === "done" ? collect() : null;
695
+ } catch (error) {
696
+ state.result = { notes: [], generalNote: general.value.trim() || null, url: location.href, viewport: { width: innerWidth, height: innerHeight }, elements: {}, error: String(error) }
697
+ }
698
+ state.status = status;
699
+ cancelAnimationFrame(frame);
700
+ for (const type of blocked) window.removeEventListener(type, onBlocked, true);
701
+ window.removeEventListener("mousemove", onMove, true);
702
+ window.removeEventListener("keydown", onKey, true);
703
+ window.removeEventListener("wheel", onWheel, { capture: true });
704
+ window.removeEventListener("selectstart", onSelectStart, true);
705
+ noSelect.remove();
706
+ state.cleanup = () => {
707
+ picks.style.display = "none";
708
+ }
709
+ state.dispose = () => host.remove();
710
+ if (status !== "done") {
711
+ host.remove();
712
+ return;
713
+ }
714
+ activeNote = null;
715
+ syncMasks();
716
+ drawMasks();
717
+ drawPicks();
718
+ for (const part of [dock, tip, outline]) part.style.display = "none";
719
+ }
720
+
721
+ pickButton.addEventListener("click", () => run("pick"));
722
+ notesButton.addEventListener("click", () => run("notes"));
723
+ freezeButton.addEventListener("click", () => run("freeze"));
724
+ toggleButton.addEventListener("click", () => run("toggle"));
725
+ keysButton.addEventListener("click", () => showSheet(openSheet === "keys" ? null : "keys"));
726
+ $(".done").addEventListener("click", () => finish("done"));
727
+ $(".send").addEventListener("click", () => send());
728
+ $(".send").hidden = !keepOpen;
729
+ $(".cancel").addEventListener("click", () => finish("cancelled"));
730
+ $(".reset-keys").addEventListener("click", () => {
731
+ Object.assign(bindings, DEFAULT_KEYS);
732
+ state.keys = { ...bindings }
733
+ listeningFor = null;
734
+ renderKeys();
735
+ });
736
+ for (const button of root.querySelectorAll(".tool[data-tip], .toggle")) {
737
+ button.addEventListener("pointerenter", () => showTip(button));
738
+ button.addEventListener("pointerleave", () => {
739
+ tip.style.display = "none";
740
+ });
741
+ button.addEventListener("click", () => {
742
+ if (tip.style.display === "block") showTip(button);
743
+ });
744
+ }
745
+
746
+ const grip = $(".grip");
747
+ grip.addEventListener("pointerdown", event => {
748
+ const rect = dock.getBoundingClientRect();
749
+ const offsetX = event.clientX - rect.left;
750
+ const offsetY = event.clientY - rect.top;
751
+ grip.setPointerCapture(event.pointerId);
752
+ const move = moveEvent => {
753
+ dock.style.left = `${Math.min(Math.max(0, moveEvent.clientX - offsetX), innerWidth - rect.width)}px`;
754
+ dock.style.top = `${Math.min(Math.max(0, moveEvent.clientY - offsetY), innerHeight - rect.height)}px`;
755
+ placeDock();
756
+ }
757
+ const up = () => {
758
+ grip.removeEventListener("pointermove", move);
759
+ grip.removeEventListener("pointerup", up);
760
+ }
761
+ grip.addEventListener("pointermove", move);
762
+ grip.addEventListener("pointerup", up);
763
+ });
764
+
765
+ for (const type of blocked) window.addEventListener(type, onBlocked, true);
766
+ window.addEventListener("mousemove", onMove, true);
767
+ window.addEventListener("keydown", onKey, true);
768
+ window.addEventListener("wheel", onWheel, { capture: true, passive: false });
769
+ window.addEventListener("selectstart", onSelectStart, true);
770
+ if (draft) {
771
+ for (const [id, info] of Object.entries(draft.elements ?? {})) records.set(id, { element: findAgain(info), info });
772
+ for (const note of draft.notes ?? []) notes.push({ ...note });
773
+ general.value = draft.generalNote ?? "";
774
+ nextElement = draft.nextElement ?? records.size + 1;
775
+ nextNote = draft.nextNote ?? notes.length + 1;
776
+ }
777
+ general.addEventListener("input", changed);
778
+ syncMasks();
779
+ drawMasks();
780
+ setPicking(true);
781
+ setCollapsed(false);
782
+ showSheet(null);
783
+ update();
784
+ state.finish = finish;
785
+ frame = requestAnimationFrame(tick);
786
+ return "open";
787
+ }