opendevbrowser 0.0.12 → 0.0.15
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.
- package/LICENSE +21 -0
- package/README.md +216 -28
- package/dist/chunk-JVBMT2O5.js +7173 -0
- package/dist/chunk-JVBMT2O5.js.map +1 -0
- package/dist/cli/index.js +2486 -589
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1057 -194
- package/dist/index.js.map +1 -1
- package/dist/opendevbrowser.js +1057 -194
- package/dist/opendevbrowser.js.map +1 -1
- package/extension/dist/annotate-content.css +237 -0
- package/extension/dist/annotate-content.js +934 -0
- package/extension/dist/background.js +1194 -32
- package/extension/dist/logging.js +50 -0
- package/extension/dist/ops/dom-bridge.js +355 -0
- package/extension/dist/ops/ops-runtime.js +1249 -0
- package/extension/dist/ops/ops-session-store.js +189 -0
- package/extension/dist/ops/redaction.js +52 -0
- package/extension/dist/ops/snapshot-builder.js +4 -0
- package/extension/dist/ops/snapshot-shared.js +220 -0
- package/extension/dist/popup.js +370 -25
- package/extension/dist/relay-settings.js +1 -0
- package/extension/dist/services/CDPRouter.js +501 -103
- package/extension/dist/services/ConnectionManager.js +464 -57
- package/extension/dist/services/NativePortManager.js +182 -0
- package/extension/dist/services/RelayClient.js +227 -26
- package/extension/dist/services/TabManager.js +81 -0
- package/extension/dist/services/TargetSessionMap.js +146 -0
- package/extension/dist/services/cdp-router-commands.js +203 -0
- package/extension/dist/services/url-restrictions.js +41 -0
- package/extension/dist/types.js +3 -1
- package/extension/manifest.json +17 -3
- package/extension/popup.html +144 -0
- package/package.json +2 -2
- package/skills/AGENTS.md +34 -62
- package/skills/data-extraction/SKILL.md +95 -103
- package/skills/form-testing/SKILL.md +75 -82
- package/skills/login-automation/SKILL.md +76 -66
- package/skills/opendevbrowser-best-practices/SKILL.md +90 -49
- package/skills/opendevbrowser-continuity-ledger/SKILL.md +57 -23
- package/dist/chunk-WTFSMBVH.js +0 -2815
- package/dist/chunk-WTFSMBVH.js.map +0 -1
- package/extension/dist/popup.jsx +0 -150
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const logError = (context, error, options) => {
|
|
3
|
+
const detail = error instanceof Error
|
|
4
|
+
? { message: error.message, name: error.name, stack: error.stack }
|
|
5
|
+
: typeof error === "string"
|
|
6
|
+
? { message: error }
|
|
7
|
+
: (() => {
|
|
8
|
+
try {
|
|
9
|
+
return { message: JSON.stringify(error) };
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return { message: "Unknown error" };
|
|
13
|
+
}
|
|
14
|
+
})();
|
|
15
|
+
const payload = {
|
|
16
|
+
context,
|
|
17
|
+
code: options?.code ?? "unknown",
|
|
18
|
+
...detail,
|
|
19
|
+
...(options?.extra ?? {})
|
|
20
|
+
};
|
|
21
|
+
console.error("[opendevbrowser]", payload);
|
|
22
|
+
};
|
|
23
|
+
const ROOT_ID = "odb-annotate-root";
|
|
24
|
+
const ATTR_UI = "data-odb-annotate";
|
|
25
|
+
const DEFAULT_OPTIONS = {
|
|
26
|
+
screenshotMode: "visible",
|
|
27
|
+
includeScreenshots: false,
|
|
28
|
+
debug: true
|
|
29
|
+
};
|
|
30
|
+
const state = {
|
|
31
|
+
session: { requestId: null, options: DEFAULT_OPTIONS, active: false, completed: false },
|
|
32
|
+
selections: new Map(),
|
|
33
|
+
hoverEl: null,
|
|
34
|
+
hoverChain: [],
|
|
35
|
+
hoverIndex: 0,
|
|
36
|
+
root: null,
|
|
37
|
+
highlight: null,
|
|
38
|
+
tooltip: null,
|
|
39
|
+
panel: null,
|
|
40
|
+
connectorLayer: null,
|
|
41
|
+
globalNote: null,
|
|
42
|
+
debugToggle: null,
|
|
43
|
+
screenshotsToggle: null,
|
|
44
|
+
countLabel: null,
|
|
45
|
+
copyButton: null,
|
|
46
|
+
copyTimeout: null,
|
|
47
|
+
panelPosition: null
|
|
48
|
+
};
|
|
49
|
+
const ensureRoot = () => {
|
|
50
|
+
if (state.root)
|
|
51
|
+
return;
|
|
52
|
+
const root = document.createElement("div");
|
|
53
|
+
root.id = ROOT_ID;
|
|
54
|
+
root.setAttribute(ATTR_UI, "true");
|
|
55
|
+
const highlight = document.createElement("div");
|
|
56
|
+
highlight.className = "odb-highlight";
|
|
57
|
+
highlight.setAttribute(ATTR_UI, "true");
|
|
58
|
+
const tooltip = document.createElement("div");
|
|
59
|
+
tooltip.className = "odb-tooltip";
|
|
60
|
+
tooltip.setAttribute(ATTR_UI, "true");
|
|
61
|
+
const connectors = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
62
|
+
connectors.classList.add("odb-connectors");
|
|
63
|
+
connectors.setAttribute(ATTR_UI, "true");
|
|
64
|
+
const panel = document.createElement("div");
|
|
65
|
+
panel.className = "odb-panel";
|
|
66
|
+
panel.setAttribute(ATTR_UI, "true");
|
|
67
|
+
panel.innerHTML = `
|
|
68
|
+
<div class="odb-panel-header">
|
|
69
|
+
<div class="odb-title">Annotate</div>
|
|
70
|
+
<div class="odb-actions">
|
|
71
|
+
<button class="odb-btn odb-btn-ghost" data-action="copy">Copy</button>
|
|
72
|
+
<button class="odb-btn odb-btn-ghost" data-action="cancel">Cancel</button>
|
|
73
|
+
<button class="odb-btn odb-btn-primary" data-action="submit">Submit</button>
|
|
74
|
+
<button class="odb-btn odb-btn-icon" data-action="close" aria-label="Close">×</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="odb-panel-body">
|
|
78
|
+
<div class="odb-row">
|
|
79
|
+
<span class="odb-label">Selected</span>
|
|
80
|
+
<span class="odb-count" data-role="count">0</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="odb-row">
|
|
83
|
+
<span class="odb-label">Debug mode</span>
|
|
84
|
+
<label class="odb-switch">
|
|
85
|
+
<input type="checkbox" data-role="debug" />
|
|
86
|
+
<span></span>
|
|
87
|
+
</label>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="odb-row">
|
|
90
|
+
<span class="odb-label">Screenshots (Base64)</span>
|
|
91
|
+
<label class="odb-switch">
|
|
92
|
+
<input type="checkbox" data-role="screenshots" />
|
|
93
|
+
<span></span>
|
|
94
|
+
</label>
|
|
95
|
+
</div>
|
|
96
|
+
<label class="odb-label" style="margin-top:10px;">Context</label>
|
|
97
|
+
<textarea class="odb-textarea" data-role="context" rows="2" placeholder="Add overall context..."></textarea>
|
|
98
|
+
</div>
|
|
99
|
+
`;
|
|
100
|
+
root.appendChild(connectors);
|
|
101
|
+
root.appendChild(highlight);
|
|
102
|
+
root.appendChild(tooltip);
|
|
103
|
+
root.appendChild(panel);
|
|
104
|
+
document.documentElement.appendChild(root);
|
|
105
|
+
state.root = root;
|
|
106
|
+
state.highlight = highlight;
|
|
107
|
+
state.tooltip = tooltip;
|
|
108
|
+
state.panel = panel;
|
|
109
|
+
state.connectorLayer = connectors;
|
|
110
|
+
state.globalNote = panel.querySelector("textarea[data-role='context']");
|
|
111
|
+
state.debugToggle = panel.querySelector("input[data-role='debug']");
|
|
112
|
+
state.screenshotsToggle = panel.querySelector("input[data-role='screenshots']");
|
|
113
|
+
state.countLabel = panel.querySelector("[data-role='count']");
|
|
114
|
+
state.copyButton = panel.querySelector("button[data-action='copy']");
|
|
115
|
+
const panelRect = panel.getBoundingClientRect();
|
|
116
|
+
const panelPosition = {
|
|
117
|
+
x: panelRect.left,
|
|
118
|
+
y: panelRect.top
|
|
119
|
+
};
|
|
120
|
+
state.panelPosition = panelPosition;
|
|
121
|
+
positionPanel(panel, panelPosition);
|
|
122
|
+
const header = panel.querySelector(".odb-panel-header");
|
|
123
|
+
header?.addEventListener("mousedown", (event) => startPanelDrag(event));
|
|
124
|
+
panel.addEventListener("click", (event) => {
|
|
125
|
+
const target = event.target;
|
|
126
|
+
if (!target)
|
|
127
|
+
return;
|
|
128
|
+
const action = target.getAttribute("data-action");
|
|
129
|
+
if (action === "copy") {
|
|
130
|
+
copyPayload().catch((error) => {
|
|
131
|
+
logError("annotation.copy_payload", error, { code: "annotation_copy_failed" });
|
|
132
|
+
setCopyFeedback("Copy failed");
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (action === "cancel") {
|
|
136
|
+
cancelSession();
|
|
137
|
+
}
|
|
138
|
+
if (action === "close") {
|
|
139
|
+
cancelSession();
|
|
140
|
+
}
|
|
141
|
+
if (action === "submit") {
|
|
142
|
+
submitSession().catch((error) => {
|
|
143
|
+
logError("annotation.submit", error, { code: "annotation_submit_failed" });
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
state.debugToggle?.addEventListener("change", () => {
|
|
148
|
+
state.session.options.debug = Boolean(state.debugToggle?.checked);
|
|
149
|
+
});
|
|
150
|
+
state.screenshotsToggle?.addEventListener("change", () => {
|
|
151
|
+
state.session.options.includeScreenshots = Boolean(state.screenshotsToggle?.checked);
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const teardown = () => {
|
|
155
|
+
removeListeners();
|
|
156
|
+
state.selections.clear();
|
|
157
|
+
state.hoverEl = null;
|
|
158
|
+
state.hoverChain = [];
|
|
159
|
+
state.hoverIndex = 0;
|
|
160
|
+
state.session.active = false;
|
|
161
|
+
state.session.completed = false;
|
|
162
|
+
if (state.root) {
|
|
163
|
+
state.root.remove();
|
|
164
|
+
}
|
|
165
|
+
state.root = null;
|
|
166
|
+
state.highlight = null;
|
|
167
|
+
state.tooltip = null;
|
|
168
|
+
state.panel = null;
|
|
169
|
+
state.connectorLayer = null;
|
|
170
|
+
state.globalNote = null;
|
|
171
|
+
state.debugToggle = null;
|
|
172
|
+
state.screenshotsToggle = null;
|
|
173
|
+
state.countLabel = null;
|
|
174
|
+
state.copyButton = null;
|
|
175
|
+
if (state.copyTimeout !== null) {
|
|
176
|
+
window.clearTimeout(state.copyTimeout);
|
|
177
|
+
}
|
|
178
|
+
state.copyTimeout = null;
|
|
179
|
+
state.panelPosition = null;
|
|
180
|
+
};
|
|
181
|
+
const addListeners = () => {
|
|
182
|
+
document.addEventListener("mousemove", handleHover, true);
|
|
183
|
+
document.addEventListener("click", handleClick, true);
|
|
184
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
185
|
+
document.addEventListener("wheel", handleWheel, { capture: true, passive: false });
|
|
186
|
+
window.addEventListener("scroll", scheduleConnectorUpdate, true);
|
|
187
|
+
window.addEventListener("resize", scheduleConnectorUpdate, true);
|
|
188
|
+
};
|
|
189
|
+
const removeListeners = () => {
|
|
190
|
+
document.removeEventListener("mousemove", handleHover, true);
|
|
191
|
+
document.removeEventListener("click", handleClick, true);
|
|
192
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
193
|
+
document.removeEventListener("wheel", handleWheel, true);
|
|
194
|
+
window.removeEventListener("scroll", scheduleConnectorUpdate, true);
|
|
195
|
+
window.removeEventListener("resize", scheduleConnectorUpdate, true);
|
|
196
|
+
};
|
|
197
|
+
const isUiElement = (element) => {
|
|
198
|
+
if (!element)
|
|
199
|
+
return false;
|
|
200
|
+
return Boolean(element.closest(`[${ATTR_UI}]`));
|
|
201
|
+
};
|
|
202
|
+
const startSession = (requestId, options) => {
|
|
203
|
+
ensureRoot();
|
|
204
|
+
state.session.requestId = requestId;
|
|
205
|
+
state.session.options = mergeOptions(options);
|
|
206
|
+
state.session.completed = false;
|
|
207
|
+
if (state.debugToggle) {
|
|
208
|
+
state.debugToggle.checked = state.session.options.debug;
|
|
209
|
+
}
|
|
210
|
+
if (state.screenshotsToggle) {
|
|
211
|
+
state.screenshotsToggle.checked = state.session.options.includeScreenshots;
|
|
212
|
+
}
|
|
213
|
+
if (state.globalNote && state.session.options.context) {
|
|
214
|
+
state.globalNote.value = state.session.options.context;
|
|
215
|
+
}
|
|
216
|
+
state.session.active = true;
|
|
217
|
+
addListeners();
|
|
218
|
+
scheduleConnectorUpdate();
|
|
219
|
+
};
|
|
220
|
+
const cancelSession = () => {
|
|
221
|
+
const requestId = state.session.requestId;
|
|
222
|
+
if (requestId && !state.session.completed) {
|
|
223
|
+
chrome.runtime.sendMessage({ type: "annotation:cancelled", requestId });
|
|
224
|
+
}
|
|
225
|
+
teardown();
|
|
226
|
+
};
|
|
227
|
+
const submitSession = async () => {
|
|
228
|
+
if (!state.session.active || state.session.completed)
|
|
229
|
+
return;
|
|
230
|
+
if (!state.session.requestId) {
|
|
231
|
+
finalizeSubmission();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const requestId = state.session.requestId;
|
|
235
|
+
try {
|
|
236
|
+
const payload = await buildPayload();
|
|
237
|
+
chrome.runtime.sendMessage({ type: "annotation:complete", requestId, payload });
|
|
238
|
+
finalizeSubmission();
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
const message = error instanceof Error ? error.message : "Annotation failed.";
|
|
242
|
+
const code = shouldReportCaptureFailure(message) ? "capture_failed" : "unknown";
|
|
243
|
+
chrome.runtime.sendMessage({ type: "annotation:error", requestId, error: { code, message } });
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const finalizeSubmission = () => {
|
|
247
|
+
state.session.completed = true;
|
|
248
|
+
state.session.active = false;
|
|
249
|
+
removeListeners();
|
|
250
|
+
if (state.highlight) {
|
|
251
|
+
state.highlight.style.opacity = "0";
|
|
252
|
+
}
|
|
253
|
+
if (state.tooltip) {
|
|
254
|
+
state.tooltip.style.opacity = "0";
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const handleHover = (event) => {
|
|
258
|
+
if (!state.session.active)
|
|
259
|
+
return;
|
|
260
|
+
const elements = document.elementsFromPoint(event.clientX, event.clientY);
|
|
261
|
+
const target = elements.find((el) => !isUiElement(el)) ?? null;
|
|
262
|
+
if (!target || target === document.documentElement || target === document.body)
|
|
263
|
+
return;
|
|
264
|
+
if (state.hoverEl === target)
|
|
265
|
+
return;
|
|
266
|
+
state.hoverEl = target;
|
|
267
|
+
state.hoverChain = buildAncestorChain(target);
|
|
268
|
+
state.hoverIndex = 0;
|
|
269
|
+
updateHighlight(target, event.clientX, event.clientY);
|
|
270
|
+
};
|
|
271
|
+
const handleClick = (event) => {
|
|
272
|
+
if (!state.session.active)
|
|
273
|
+
return;
|
|
274
|
+
if (isUiElement(event.target))
|
|
275
|
+
return;
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
event.stopPropagation();
|
|
278
|
+
const target = state.hoverEl;
|
|
279
|
+
if (!target)
|
|
280
|
+
return;
|
|
281
|
+
if (event.shiftKey) {
|
|
282
|
+
toggleSelection(target);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
clearSelections();
|
|
286
|
+
addSelection(target);
|
|
287
|
+
}
|
|
288
|
+
updateCount();
|
|
289
|
+
scheduleConnectorUpdate();
|
|
290
|
+
};
|
|
291
|
+
const handleKeyDown = (event) => {
|
|
292
|
+
if (!state.session.active)
|
|
293
|
+
return;
|
|
294
|
+
if (event.key === "Escape") {
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
cancelSession();
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const handleWheel = (event) => {
|
|
300
|
+
if (!state.session.active || !state.hoverEl)
|
|
301
|
+
return;
|
|
302
|
+
if (isUiElement(event.target))
|
|
303
|
+
return;
|
|
304
|
+
if (!state.hoverChain.length)
|
|
305
|
+
return;
|
|
306
|
+
event.preventDefault();
|
|
307
|
+
const direction = event.deltaY > 0 ? 1 : -1;
|
|
308
|
+
const nextIndex = clamp(state.hoverIndex + direction, 0, state.hoverChain.length - 1);
|
|
309
|
+
if (nextIndex === state.hoverIndex)
|
|
310
|
+
return;
|
|
311
|
+
state.hoverIndex = nextIndex;
|
|
312
|
+
const next = state.hoverChain[nextIndex];
|
|
313
|
+
if (!next)
|
|
314
|
+
return;
|
|
315
|
+
state.hoverEl = next;
|
|
316
|
+
updateHighlight(next, event.clientX, event.clientY);
|
|
317
|
+
};
|
|
318
|
+
const updateHighlight = (element, x, y) => {
|
|
319
|
+
if (!state.highlight || !state.tooltip)
|
|
320
|
+
return;
|
|
321
|
+
const rect = element.getBoundingClientRect();
|
|
322
|
+
state.highlight.style.opacity = "1";
|
|
323
|
+
state.highlight.style.transform = `translate(${rect.left}px, ${rect.top}px)`;
|
|
324
|
+
state.highlight.style.width = `${rect.width}px`;
|
|
325
|
+
state.highlight.style.height = `${rect.height}px`;
|
|
326
|
+
const label = describeElement(element);
|
|
327
|
+
state.tooltip.textContent = label;
|
|
328
|
+
state.tooltip.style.opacity = "1";
|
|
329
|
+
const tooltipX = clamp(x + 12, 8, window.innerWidth - 240);
|
|
330
|
+
const tooltipY = clamp(y + 12, 8, window.innerHeight - 40);
|
|
331
|
+
state.tooltip.style.transform = `translate(${tooltipX}px, ${tooltipY}px)`;
|
|
332
|
+
};
|
|
333
|
+
const addSelection = (element) => {
|
|
334
|
+
const id = generateId();
|
|
335
|
+
const noteEl = createNote(element, id);
|
|
336
|
+
const selection = {
|
|
337
|
+
id,
|
|
338
|
+
element,
|
|
339
|
+
note: "",
|
|
340
|
+
noteEl,
|
|
341
|
+
noteInput: noteEl.querySelector("textarea"),
|
|
342
|
+
position: { x: window.innerWidth - 340, y: 140 + state.selections.size * 120 }
|
|
343
|
+
};
|
|
344
|
+
state.selections.set(id, selection);
|
|
345
|
+
positionNote(selection);
|
|
346
|
+
};
|
|
347
|
+
const toggleSelection = (element) => {
|
|
348
|
+
const existing = findSelectionByElement(element);
|
|
349
|
+
if (existing) {
|
|
350
|
+
existing.noteEl.remove();
|
|
351
|
+
state.selections.delete(existing.id);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
addSelection(element);
|
|
355
|
+
};
|
|
356
|
+
const clearSelections = () => {
|
|
357
|
+
for (const selection of state.selections.values()) {
|
|
358
|
+
selection.noteEl.remove();
|
|
359
|
+
}
|
|
360
|
+
state.selections.clear();
|
|
361
|
+
};
|
|
362
|
+
const updateCount = () => {
|
|
363
|
+
if (state.countLabel) {
|
|
364
|
+
state.countLabel.textContent = String(state.selections.size);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const createNote = (element, id) => {
|
|
368
|
+
const note = document.createElement("div");
|
|
369
|
+
note.className = "odb-note";
|
|
370
|
+
note.setAttribute(ATTR_UI, "true");
|
|
371
|
+
note.dataset.noteId = id;
|
|
372
|
+
note.innerHTML = `
|
|
373
|
+
<div class="odb-note-header">
|
|
374
|
+
<span>${describeElement(element)}</span>
|
|
375
|
+
<button class="odb-note-close" aria-label="Remove">x</button>
|
|
376
|
+
</div>
|
|
377
|
+
<textarea class="odb-note-input" rows="3" placeholder="Add annotation..."></textarea>
|
|
378
|
+
`;
|
|
379
|
+
const close = note.querySelector("button");
|
|
380
|
+
close.addEventListener("click", () => {
|
|
381
|
+
note.remove();
|
|
382
|
+
state.selections.delete(id);
|
|
383
|
+
updateCount();
|
|
384
|
+
scheduleConnectorUpdate();
|
|
385
|
+
});
|
|
386
|
+
const textarea = note.querySelector("textarea");
|
|
387
|
+
textarea.addEventListener("input", () => {
|
|
388
|
+
const selection = state.selections.get(id);
|
|
389
|
+
if (!selection)
|
|
390
|
+
return;
|
|
391
|
+
selection.note = textarea.value;
|
|
392
|
+
});
|
|
393
|
+
const header = note.querySelector(".odb-note-header");
|
|
394
|
+
header.addEventListener("mousedown", (event) => startDrag(event, id));
|
|
395
|
+
document.documentElement.appendChild(note);
|
|
396
|
+
bringToFront(note);
|
|
397
|
+
return note;
|
|
398
|
+
};
|
|
399
|
+
const startDrag = (event, id) => {
|
|
400
|
+
event.preventDefault();
|
|
401
|
+
const selection = state.selections.get(id);
|
|
402
|
+
if (!selection)
|
|
403
|
+
return;
|
|
404
|
+
bringToFront(selection.noteEl);
|
|
405
|
+
const start = { x: event.clientX, y: event.clientY };
|
|
406
|
+
const origin = { ...selection.position };
|
|
407
|
+
const onMove = (moveEvent) => {
|
|
408
|
+
const next = {
|
|
409
|
+
x: origin.x + (moveEvent.clientX - start.x),
|
|
410
|
+
y: origin.y + (moveEvent.clientY - start.y)
|
|
411
|
+
};
|
|
412
|
+
selection.position = clampToViewport(next, selection.noteEl);
|
|
413
|
+
positionNote(selection);
|
|
414
|
+
scheduleConnectorUpdate();
|
|
415
|
+
};
|
|
416
|
+
const onUp = () => {
|
|
417
|
+
document.removeEventListener("mousemove", onMove, true);
|
|
418
|
+
document.removeEventListener("mouseup", onUp, true);
|
|
419
|
+
};
|
|
420
|
+
document.addEventListener("mousemove", onMove, true);
|
|
421
|
+
document.addEventListener("mouseup", onUp, true);
|
|
422
|
+
};
|
|
423
|
+
const positionNote = (selection) => {
|
|
424
|
+
selection.noteEl.style.transform = `translate(${selection.position.x}px, ${selection.position.y}px)`;
|
|
425
|
+
};
|
|
426
|
+
let zIndexCounter = 10;
|
|
427
|
+
const bringToFront = (element) => {
|
|
428
|
+
zIndexCounter += 1;
|
|
429
|
+
element.style.zIndex = String(zIndexCounter);
|
|
430
|
+
};
|
|
431
|
+
const clampToViewport = (position, element) => {
|
|
432
|
+
const maxX = Math.max(0, window.innerWidth - element.offsetWidth);
|
|
433
|
+
const maxY = Math.max(0, window.innerHeight - element.offsetHeight);
|
|
434
|
+
return {
|
|
435
|
+
x: clamp(position.x, 0, maxX),
|
|
436
|
+
y: clamp(position.y, 0, maxY)
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
const positionPanel = (panel, position) => {
|
|
440
|
+
panel.style.left = `${position.x}px`;
|
|
441
|
+
panel.style.top = `${position.y}px`;
|
|
442
|
+
panel.style.right = "auto";
|
|
443
|
+
};
|
|
444
|
+
const startPanelDrag = (event) => {
|
|
445
|
+
if (!state.panel)
|
|
446
|
+
return;
|
|
447
|
+
const target = event.target;
|
|
448
|
+
if (target?.closest(".odb-actions"))
|
|
449
|
+
return;
|
|
450
|
+
event.preventDefault();
|
|
451
|
+
bringToFront(state.panel);
|
|
452
|
+
const panel = state.panel;
|
|
453
|
+
const rect = panel.getBoundingClientRect();
|
|
454
|
+
const start = { x: event.clientX, y: event.clientY };
|
|
455
|
+
const origin = { x: rect.left, y: rect.top };
|
|
456
|
+
const onMove = (moveEvent) => {
|
|
457
|
+
const next = {
|
|
458
|
+
x: origin.x + (moveEvent.clientX - start.x),
|
|
459
|
+
y: origin.y + (moveEvent.clientY - start.y)
|
|
460
|
+
};
|
|
461
|
+
const clamped = clampToViewport(next, panel);
|
|
462
|
+
state.panelPosition = clamped;
|
|
463
|
+
positionPanel(panel, clamped);
|
|
464
|
+
};
|
|
465
|
+
const onUp = () => {
|
|
466
|
+
document.removeEventListener("mousemove", onMove, true);
|
|
467
|
+
document.removeEventListener("mouseup", onUp, true);
|
|
468
|
+
};
|
|
469
|
+
document.addEventListener("mousemove", onMove, true);
|
|
470
|
+
document.addEventListener("mouseup", onUp, true);
|
|
471
|
+
};
|
|
472
|
+
let connectorFrame = 0;
|
|
473
|
+
const scheduleConnectorUpdate = () => {
|
|
474
|
+
if (connectorFrame)
|
|
475
|
+
return;
|
|
476
|
+
connectorFrame = requestAnimationFrame(() => {
|
|
477
|
+
connectorFrame = 0;
|
|
478
|
+
updateConnectors();
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
const updateConnectors = () => {
|
|
482
|
+
if (!state.connectorLayer)
|
|
483
|
+
return;
|
|
484
|
+
state.connectorLayer.innerHTML = "";
|
|
485
|
+
for (const selection of state.selections.values()) {
|
|
486
|
+
const rect = selection.element.getBoundingClientRect();
|
|
487
|
+
const noteRect = selection.noteEl.getBoundingClientRect();
|
|
488
|
+
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
489
|
+
line.setAttribute("x1", String(rect.left + rect.width / 2));
|
|
490
|
+
line.setAttribute("y1", String(rect.top + rect.height / 2));
|
|
491
|
+
line.setAttribute("x2", String(noteRect.left + noteRect.width / 2));
|
|
492
|
+
line.setAttribute("y2", String(noteRect.top + 18));
|
|
493
|
+
line.setAttribute("stroke", "currentColor");
|
|
494
|
+
line.setAttribute("stroke-width", "1.5");
|
|
495
|
+
state.connectorLayer.appendChild(line);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
const buildPayload = async () => {
|
|
499
|
+
const url = window.location.href;
|
|
500
|
+
const title = document.title;
|
|
501
|
+
const timestamp = new Date().toISOString();
|
|
502
|
+
const context = state.globalNote?.value?.trim() || state.session.options.context;
|
|
503
|
+
const screenshotMode = state.session.options.screenshotMode;
|
|
504
|
+
const includeScreenshots = state.session.options.includeScreenshots;
|
|
505
|
+
const effectiveScreenshotMode = includeScreenshots ? screenshotMode : "none";
|
|
506
|
+
const annotations = [];
|
|
507
|
+
for (const selection of state.selections.values()) {
|
|
508
|
+
annotations.push(buildAnnotationItem(selection));
|
|
509
|
+
}
|
|
510
|
+
const screenshots = await captureScreenshots(effectiveScreenshotMode, annotations);
|
|
511
|
+
return {
|
|
512
|
+
url,
|
|
513
|
+
title,
|
|
514
|
+
timestamp,
|
|
515
|
+
context,
|
|
516
|
+
screenshotMode: effectiveScreenshotMode,
|
|
517
|
+
screenshots,
|
|
518
|
+
annotations
|
|
519
|
+
};
|
|
520
|
+
};
|
|
521
|
+
const extractBase64 = (dataUrl) => {
|
|
522
|
+
if (!dataUrl.includes(","))
|
|
523
|
+
return dataUrl;
|
|
524
|
+
return dataUrl.split(",")[1] ?? "";
|
|
525
|
+
};
|
|
526
|
+
const captureScreenshots = async (mode, annotations) => {
|
|
527
|
+
if (mode === "none")
|
|
528
|
+
return [];
|
|
529
|
+
const screenshots = [];
|
|
530
|
+
await setUiVisibility(false);
|
|
531
|
+
try {
|
|
532
|
+
if (mode === "visible") {
|
|
533
|
+
const dataUrl = await requestCapture("visible");
|
|
534
|
+
const image = await loadImage(dataUrl);
|
|
535
|
+
const scaleX = image.naturalWidth / window.innerWidth;
|
|
536
|
+
const scaleY = image.naturalHeight / window.innerHeight;
|
|
537
|
+
for (const annotation of annotations) {
|
|
538
|
+
const rect = annotation.rect;
|
|
539
|
+
const padded = padRect(rect, 12, window.innerWidth, window.innerHeight);
|
|
540
|
+
const crop = cropImage(image, padded, scaleX, scaleY);
|
|
541
|
+
const id = generateId();
|
|
542
|
+
screenshots.push({ id, label: "element", base64: crop, mime: "image/png", width: rect.width, height: rect.height });
|
|
543
|
+
annotation.screenshotId = id;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (mode === "full") {
|
|
547
|
+
const dataUrl = await captureFullPage();
|
|
548
|
+
const image = await loadImage(dataUrl);
|
|
549
|
+
const id = generateId();
|
|
550
|
+
screenshots.push({
|
|
551
|
+
id,
|
|
552
|
+
label: "full-page",
|
|
553
|
+
base64: extractBase64(dataUrl),
|
|
554
|
+
mime: "image/png",
|
|
555
|
+
width: image.naturalWidth,
|
|
556
|
+
height: image.naturalHeight
|
|
557
|
+
});
|
|
558
|
+
annotations.forEach((annotation) => {
|
|
559
|
+
annotation.screenshotId = id;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
finally {
|
|
564
|
+
await setUiVisibility(true);
|
|
565
|
+
}
|
|
566
|
+
return screenshots;
|
|
567
|
+
};
|
|
568
|
+
const requestCapture = async (mode) => {
|
|
569
|
+
return await new Promise((resolve, reject) => {
|
|
570
|
+
chrome.runtime.sendMessage({ type: "annotation:capture", requestId: state.session.requestId ?? "local", mode }, (response) => {
|
|
571
|
+
const lastError = chrome.runtime.lastError;
|
|
572
|
+
if (lastError) {
|
|
573
|
+
reject(new Error(lastError.message));
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (!response || response.ok !== true || !response.dataUrl) {
|
|
577
|
+
reject(new Error(response?.error ?? "Capture failed"));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
resolve(response.dataUrl);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
};
|
|
584
|
+
const captureFullPage = async () => {
|
|
585
|
+
const original = { x: window.scrollX, y: window.scrollY };
|
|
586
|
+
const viewportHeight = window.innerHeight;
|
|
587
|
+
const totalHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
|
|
588
|
+
const totalWidth = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, window.innerWidth);
|
|
589
|
+
const slices = [];
|
|
590
|
+
const maxSlices = Math.ceil(totalHeight / viewportHeight);
|
|
591
|
+
if (maxSlices > 12) {
|
|
592
|
+
throw new Error("Page too tall for full-page capture.");
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
for (let index = 0; index < maxSlices; index += 1) {
|
|
596
|
+
const y = index * viewportHeight;
|
|
597
|
+
window.scrollTo({ top: y, behavior: "auto" });
|
|
598
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
599
|
+
const dataUrl = await requestCapture("visible");
|
|
600
|
+
slices.push(await loadImage(dataUrl));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
finally {
|
|
604
|
+
window.scrollTo({ top: original.y, left: original.x, behavior: "auto" });
|
|
605
|
+
}
|
|
606
|
+
const canvas = document.createElement("canvas");
|
|
607
|
+
canvas.width = totalWidth;
|
|
608
|
+
canvas.height = totalHeight;
|
|
609
|
+
const ctx = canvas.getContext("2d");
|
|
610
|
+
if (!ctx) {
|
|
611
|
+
throw new Error("Canvas unavailable");
|
|
612
|
+
}
|
|
613
|
+
slices.forEach((slice, index) => {
|
|
614
|
+
ctx.drawImage(slice, 0, index * viewportHeight, slice.naturalWidth, slice.naturalHeight);
|
|
615
|
+
});
|
|
616
|
+
return canvas.toDataURL("image/png");
|
|
617
|
+
};
|
|
618
|
+
const loadImage = (dataUrl) => {
|
|
619
|
+
return new Promise((resolve, reject) => {
|
|
620
|
+
const img = new Image();
|
|
621
|
+
img.onload = () => resolve(img);
|
|
622
|
+
img.onerror = () => reject(new Error("Image decode failed"));
|
|
623
|
+
img.src = dataUrl;
|
|
624
|
+
});
|
|
625
|
+
};
|
|
626
|
+
const cropImage = (image, rect, scaleX, scaleY) => {
|
|
627
|
+
const canvas = document.createElement("canvas");
|
|
628
|
+
canvas.width = Math.max(1, Math.round(rect.width * scaleX));
|
|
629
|
+
canvas.height = Math.max(1, Math.round(rect.height * scaleY));
|
|
630
|
+
const ctx = canvas.getContext("2d");
|
|
631
|
+
if (!ctx) {
|
|
632
|
+
return "";
|
|
633
|
+
}
|
|
634
|
+
ctx.drawImage(image, rect.x * scaleX, rect.y * scaleY, rect.width * scaleX, rect.height * scaleY, 0, 0, rect.width * scaleX, rect.height * scaleY);
|
|
635
|
+
return extractBase64(canvas.toDataURL("image/png"));
|
|
636
|
+
};
|
|
637
|
+
const padRect = (rect, padding, maxWidth, maxHeight) => {
|
|
638
|
+
const x = clamp(rect.x - padding, 0, maxWidth);
|
|
639
|
+
const y = clamp(rect.y - padding, 0, maxHeight);
|
|
640
|
+
const width = clamp(rect.width + padding * 2, 1, maxWidth - x);
|
|
641
|
+
const height = clamp(rect.height + padding * 2, 1, maxHeight - y);
|
|
642
|
+
return { x, y, width, height };
|
|
643
|
+
};
|
|
644
|
+
const setUiVisibility = async (visible) => {
|
|
645
|
+
if (!state.root)
|
|
646
|
+
return;
|
|
647
|
+
state.root.style.opacity = visible ? "1" : "0";
|
|
648
|
+
state.root.style.pointerEvents = visible ? "auto" : "none";
|
|
649
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
650
|
+
};
|
|
651
|
+
const buildAnnotationItem = (selection) => {
|
|
652
|
+
const element = selection.element;
|
|
653
|
+
const rect = element.getBoundingClientRect();
|
|
654
|
+
const computed = window.getComputedStyle(element);
|
|
655
|
+
const a11y = {
|
|
656
|
+
role: element.getAttribute("role") ?? undefined,
|
|
657
|
+
label: element.getAttribute("aria-label") ?? undefined,
|
|
658
|
+
labelledBy: element.getAttribute("aria-labelledby") ?? undefined,
|
|
659
|
+
describedBy: element.getAttribute("aria-describedby") ?? undefined,
|
|
660
|
+
hidden: element.getAttribute("aria-hidden") === "true"
|
|
661
|
+
};
|
|
662
|
+
const attributes = captureAttributes(element);
|
|
663
|
+
const styles = {
|
|
664
|
+
color: computed.color,
|
|
665
|
+
backgroundColor: computed.backgroundColor,
|
|
666
|
+
fontSize: computed.fontSize,
|
|
667
|
+
fontFamily: computed.fontFamily,
|
|
668
|
+
fontWeight: computed.fontWeight,
|
|
669
|
+
lineHeight: computed.lineHeight,
|
|
670
|
+
display: computed.display,
|
|
671
|
+
position: computed.position
|
|
672
|
+
};
|
|
673
|
+
const debug = state.session.options.debug
|
|
674
|
+
? {
|
|
675
|
+
computedStyles: captureComputedStyles(computed),
|
|
676
|
+
cssVariables: captureCssVariables(computed),
|
|
677
|
+
parentChain: buildParentChain(element)
|
|
678
|
+
}
|
|
679
|
+
: undefined;
|
|
680
|
+
return {
|
|
681
|
+
id: selection.id,
|
|
682
|
+
selector: getSelector(element),
|
|
683
|
+
tag: element.tagName.toLowerCase(),
|
|
684
|
+
idAttr: element.id || undefined,
|
|
685
|
+
classes: Array.from(element.classList ?? []),
|
|
686
|
+
text: getTextContent(element),
|
|
687
|
+
rect: {
|
|
688
|
+
x: rect.left,
|
|
689
|
+
y: rect.top,
|
|
690
|
+
width: rect.width,
|
|
691
|
+
height: rect.height
|
|
692
|
+
},
|
|
693
|
+
attributes,
|
|
694
|
+
a11y,
|
|
695
|
+
styles,
|
|
696
|
+
note: selection.note.trim() || undefined,
|
|
697
|
+
screenshotId: selection.screenshotId,
|
|
698
|
+
debug
|
|
699
|
+
};
|
|
700
|
+
};
|
|
701
|
+
const captureAttributes = (element) => {
|
|
702
|
+
const allowed = new Set(["href", "src", "alt", "title", "role", "aria-label", "aria-labelledby", "aria-describedby", "type", "name"]);
|
|
703
|
+
const attrs = {};
|
|
704
|
+
for (const attr of Array.from(element.attributes)) {
|
|
705
|
+
const name = attr.name.toLowerCase();
|
|
706
|
+
if (name === "value")
|
|
707
|
+
continue;
|
|
708
|
+
if (!allowed.has(name) && !name.startsWith("data-")) {
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (looksSensitive(attr.value)) {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
attrs[attr.name] = attr.value;
|
|
715
|
+
}
|
|
716
|
+
return attrs;
|
|
717
|
+
};
|
|
718
|
+
const captureComputedStyles = (computed) => {
|
|
719
|
+
const values = {};
|
|
720
|
+
for (const prop of ["display", "position", "margin", "padding", "color", "background-color", "font-size", "font-family", "font-weight", "line-height"]) {
|
|
721
|
+
values[prop] = computed.getPropertyValue(prop);
|
|
722
|
+
}
|
|
723
|
+
return values;
|
|
724
|
+
};
|
|
725
|
+
const captureCssVariables = (computed) => {
|
|
726
|
+
const vars = {};
|
|
727
|
+
for (let i = 0; i < computed.length; i += 1) {
|
|
728
|
+
const name = computed.item(i);
|
|
729
|
+
if (name.startsWith("--")) {
|
|
730
|
+
const value = computed.getPropertyValue(name).trim();
|
|
731
|
+
if (value) {
|
|
732
|
+
vars[name] = value;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return vars;
|
|
737
|
+
};
|
|
738
|
+
const buildParentChain = (element) => {
|
|
739
|
+
const chain = [];
|
|
740
|
+
let current = element.parentElement;
|
|
741
|
+
let depth = 0;
|
|
742
|
+
while (current && depth < 3) {
|
|
743
|
+
chain.push({
|
|
744
|
+
tag: current.tagName.toLowerCase(),
|
|
745
|
+
id: current.id || undefined,
|
|
746
|
+
classes: Array.from(current.classList ?? []),
|
|
747
|
+
role: current.getAttribute("role") ?? undefined
|
|
748
|
+
});
|
|
749
|
+
current = current.parentElement;
|
|
750
|
+
depth += 1;
|
|
751
|
+
}
|
|
752
|
+
return chain;
|
|
753
|
+
};
|
|
754
|
+
const buildAncestorChain = (element) => {
|
|
755
|
+
const chain = [];
|
|
756
|
+
let current = element;
|
|
757
|
+
while (current && current !== document.body && current !== document.documentElement) {
|
|
758
|
+
chain.push(current);
|
|
759
|
+
current = current.parentElement;
|
|
760
|
+
}
|
|
761
|
+
return chain;
|
|
762
|
+
};
|
|
763
|
+
const getSelector = (element) => {
|
|
764
|
+
if (element.id) {
|
|
765
|
+
return `#${cssEscape(element.id)}`;
|
|
766
|
+
}
|
|
767
|
+
const parts = [];
|
|
768
|
+
let current = element;
|
|
769
|
+
while (current && parts.length < 5) {
|
|
770
|
+
let part = current.tagName.toLowerCase();
|
|
771
|
+
const classes = Array.from(current.classList).filter(Boolean).slice(0, 2);
|
|
772
|
+
if (classes.length) {
|
|
773
|
+
part += "." + classes.map((cls) => cssEscape(cls)).join(".");
|
|
774
|
+
}
|
|
775
|
+
if (current.parentElement) {
|
|
776
|
+
const siblings = Array.from(current.parentElement.children).filter((el) => el.tagName === current?.tagName);
|
|
777
|
+
if (siblings.length > 1) {
|
|
778
|
+
const index = siblings.indexOf(current) + 1;
|
|
779
|
+
part += `:nth-of-type(${index})`;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
parts.unshift(part);
|
|
783
|
+
current = current.parentElement;
|
|
784
|
+
}
|
|
785
|
+
return parts.join(" > ");
|
|
786
|
+
};
|
|
787
|
+
const cssEscape = (value) => {
|
|
788
|
+
if (typeof CSS !== "undefined" && "escape" in CSS) {
|
|
789
|
+
return CSS.escape(value);
|
|
790
|
+
}
|
|
791
|
+
return value.replace(/[^a-z0-9_-]/gi, (match) => `\\${match}`);
|
|
792
|
+
};
|
|
793
|
+
const getTextContent = (element) => {
|
|
794
|
+
const text = element.textContent?.trim() ?? "";
|
|
795
|
+
if (!text)
|
|
796
|
+
return undefined;
|
|
797
|
+
return looksSensitive(text) ? "[redacted]" : text.slice(0, 240);
|
|
798
|
+
};
|
|
799
|
+
const describeElement = (element) => {
|
|
800
|
+
const id = element.id ? `#${element.id}` : "";
|
|
801
|
+
const classes = element.classList.length ? `.${Array.from(element.classList).slice(0, 2).join(".")}` : "";
|
|
802
|
+
const rect = element.getBoundingClientRect();
|
|
803
|
+
return `${element.tagName.toLowerCase()}${id}${classes} (${Math.round(rect.width)}x${Math.round(rect.height)})`;
|
|
804
|
+
};
|
|
805
|
+
const findSelectionByElement = (element) => {
|
|
806
|
+
for (const selection of state.selections.values()) {
|
|
807
|
+
if (selection.element === element) {
|
|
808
|
+
return selection;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return null;
|
|
812
|
+
};
|
|
813
|
+
const clamp = (value, min, max) => {
|
|
814
|
+
return Math.min(Math.max(value, min), max);
|
|
815
|
+
};
|
|
816
|
+
const generateId = () => {
|
|
817
|
+
return Math.random().toString(36).slice(2, 10);
|
|
818
|
+
};
|
|
819
|
+
const looksSensitive = (value) => {
|
|
820
|
+
const trimmed = value.trim();
|
|
821
|
+
if (trimmed.length < 12)
|
|
822
|
+
return false;
|
|
823
|
+
if (/token|secret|password|apikey/i.test(trimmed))
|
|
824
|
+
return true;
|
|
825
|
+
if (/^[A-Za-z0-9+/_-]{24,}={0,2}$/.test(trimmed))
|
|
826
|
+
return true;
|
|
827
|
+
return false;
|
|
828
|
+
};
|
|
829
|
+
const shouldReportCaptureFailure = (message) => {
|
|
830
|
+
const lowered = message.toLowerCase();
|
|
831
|
+
return (lowered.includes("capture")
|
|
832
|
+
|| lowered.includes("image")
|
|
833
|
+
|| lowered.includes("canvas")
|
|
834
|
+
|| lowered.includes("page too tall"));
|
|
835
|
+
};
|
|
836
|
+
const mergeOptions = (options) => {
|
|
837
|
+
return {
|
|
838
|
+
screenshotMode: options?.screenshotMode ?? DEFAULT_OPTIONS.screenshotMode,
|
|
839
|
+
includeScreenshots: options?.includeScreenshots ?? DEFAULT_OPTIONS.includeScreenshots,
|
|
840
|
+
debug: options?.debug ?? DEFAULT_OPTIONS.debug,
|
|
841
|
+
context: options?.context ?? DEFAULT_OPTIONS.context
|
|
842
|
+
};
|
|
843
|
+
};
|
|
844
|
+
const copyPayload = async () => {
|
|
845
|
+
const payload = await buildPayload();
|
|
846
|
+
const text = JSON.stringify(payload);
|
|
847
|
+
await writeClipboard(text);
|
|
848
|
+
setCopyFeedback("Copied");
|
|
849
|
+
};
|
|
850
|
+
const writeClipboard = async (value) => {
|
|
851
|
+
if (navigator.clipboard?.writeText) {
|
|
852
|
+
try {
|
|
853
|
+
await navigator.clipboard.writeText(value);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
// Fall back to execCommand below.
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const textarea = document.createElement("textarea");
|
|
861
|
+
textarea.value = value;
|
|
862
|
+
textarea.setAttribute("readonly", "true");
|
|
863
|
+
textarea.style.position = "fixed";
|
|
864
|
+
textarea.style.left = "-9999px";
|
|
865
|
+
document.body.appendChild(textarea);
|
|
866
|
+
textarea.select();
|
|
867
|
+
const ok = document.execCommand("copy");
|
|
868
|
+
textarea.remove();
|
|
869
|
+
if (!ok) {
|
|
870
|
+
throw new Error("Copy failed");
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
const setCopyFeedback = (label) => {
|
|
874
|
+
const button = state.copyButton;
|
|
875
|
+
if (!button)
|
|
876
|
+
return;
|
|
877
|
+
const original = button.dataset.originalLabel ?? button.textContent ?? "Copy";
|
|
878
|
+
if (!button.dataset.originalLabel) {
|
|
879
|
+
button.dataset.originalLabel = original;
|
|
880
|
+
}
|
|
881
|
+
button.textContent = label;
|
|
882
|
+
if (state.copyTimeout !== null) {
|
|
883
|
+
window.clearTimeout(state.copyTimeout);
|
|
884
|
+
}
|
|
885
|
+
state.copyTimeout = window.setTimeout(() => {
|
|
886
|
+
button.textContent = original;
|
|
887
|
+
}, 1500);
|
|
888
|
+
};
|
|
889
|
+
const bootstrap = () => {
|
|
890
|
+
if (window.__odbAnnotate) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
window.__odbAnnotate = {
|
|
894
|
+
active: false,
|
|
895
|
+
toggle: () => {
|
|
896
|
+
if (state.session.active) {
|
|
897
|
+
cancelSession();
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
startSession(null);
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
start: (requestId, options) => {
|
|
904
|
+
if (state.session.active) {
|
|
905
|
+
cancelSession();
|
|
906
|
+
}
|
|
907
|
+
startSession(requestId, options);
|
|
908
|
+
},
|
|
909
|
+
cancel: () => cancelSession()
|
|
910
|
+
};
|
|
911
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
912
|
+
if (message.type === "annotation:ping") {
|
|
913
|
+
sendResponse({ ok: true });
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
if (message.type === "annotation:toggle") {
|
|
917
|
+
window.__odbAnnotate?.toggle();
|
|
918
|
+
sendResponse({ ok: true });
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
if (message.type === "annotation:start") {
|
|
922
|
+
window.__odbAnnotate?.start(message.requestId, message.options);
|
|
923
|
+
sendResponse({ ok: true });
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
if (message.type === "annotation:cancel") {
|
|
927
|
+
window.__odbAnnotate?.cancel(message.requestId);
|
|
928
|
+
sendResponse({ ok: true });
|
|
929
|
+
return true;
|
|
930
|
+
}
|
|
931
|
+
return false;
|
|
932
|
+
});
|
|
933
|
+
};
|
|
934
|
+
bootstrap();
|