@immense/vue-pom-generator 1.0.66 → 1.0.67
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/AGENTS.md +21 -0
- package/README.md +1 -0
- package/RELEASE_NOTES.md +78 -11
- package/class-generation/index.ts +16 -5
- package/dist/class-generation/index.d.ts.map +1 -1
- package/dist/compiler-metadata-utils.d.ts.map +1 -1
- package/dist/index.cjs +257 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +257 -78
- package/dist/index.mjs.map +1 -1
- package/dist/manifest-generator.d.ts +2 -0
- package/dist/manifest-generator.d.ts.map +1 -1
- package/dist/metadata-collector.d.ts +2 -0
- package/dist/metadata-collector.d.ts.map +1 -1
- package/dist/plugin/create-vue-pom-generator-plugins.d.ts.map +1 -1
- package/dist/plugin/internal/build-plugin.d.ts.map +1 -0
- package/dist/plugin/internal/dev-plugin.d.ts.map +1 -0
- package/dist/plugin/internal/virtual-modules.d.ts.map +1 -0
- package/dist/plugin/{support-plugins.d.ts → internal-plugins.d.ts} +14 -3
- package/dist/plugin/internal-plugins.d.ts.map +1 -0
- package/dist/plugin/runtime/annotator/client.d.ts +67 -0
- package/dist/plugin/runtime/annotator/client.d.ts.map +1 -0
- package/dist/plugin/runtime/annotator/format.d.ts +13 -0
- package/dist/plugin/runtime/annotator/format.d.ts.map +1 -0
- package/dist/plugin/runtime/annotator/plugin.d.ts +12 -0
- package/dist/plugin/runtime/annotator/plugin.d.ts.map +1 -0
- package/dist/plugin/runtime/annotator/styles.d.ts +3 -0
- package/dist/plugin/runtime/annotator/styles.d.ts.map +1 -0
- package/dist/plugin/runtime/annotator/vue-detector.d.ts +12 -0
- package/dist/plugin/runtime/annotator/vue-detector.d.ts.map +1 -0
- package/dist/plugin/types.d.ts +58 -3
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/vue-plugin.d.ts +4 -0
- package/dist/plugin/vue-plugin.d.ts.map +1 -1
- package/dist/transform.d.ts +4 -0
- package/dist/transform.d.ts.map +1 -1
- package/dist/utils.d.ts +19 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +3 -1
- package/plugin/runtime/annotator/client.ts +1005 -0
- package/plugin/runtime/annotator/format.ts +76 -0
- package/plugin/runtime/annotator/plugin.ts +109 -0
- package/plugin/runtime/annotator/styles.ts +379 -0
- package/plugin/runtime/annotator/vue-detector.ts +216 -0
- package/dist/plugin/support/build-plugin.d.ts.map +0 -1
- package/dist/plugin/support/dev-plugin.d.ts.map +0 -1
- package/dist/plugin/support/virtual-modules.d.ts.map +0 -1
- package/dist/plugin/support-plugins.d.ts.map +0 -1
- /package/dist/plugin/{support → internal}/build-plugin.d.ts +0 -0
- /package/dist/plugin/{support → internal}/dev-plugin.d.ts +0 -0
- /package/dist/plugin/{support → internal}/virtual-modules.d.ts +0 -0
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
import { arrow, autoPlacement, autoUpdate, computePosition, offset, shift, type Placement } from "@floating-ui/dom";
|
|
2
|
+
|
|
3
|
+
import { formatAnnotations, formatSingleAnnotationPreview, type FormattedAnnotation, type OutputDetail } from "./format";
|
|
4
|
+
import { ANNOTATOR_ROOT_ATTR, ANNOTATOR_STYLES } from "./styles";
|
|
5
|
+
import { resolveVueComponentInfo, type VueDetectorOptions } from "./vue-detector";
|
|
6
|
+
|
|
7
|
+
interface AnnotatorClientOptions extends VueDetectorOptions {
|
|
8
|
+
outputDetail: OutputDetail;
|
|
9
|
+
copyToClipboard: boolean;
|
|
10
|
+
showComponentTree: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AnnotatorSettings {
|
|
14
|
+
outputDetail: OutputDetail;
|
|
15
|
+
copyToClipboard: boolean;
|
|
16
|
+
showComponentTree: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface AnnotationRecord extends FormattedAnnotation {
|
|
20
|
+
id: string;
|
|
21
|
+
pageX: number;
|
|
22
|
+
pageY: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SETTINGS_STORAGE_KEY = "vpg-annotator-settings";
|
|
26
|
+
const ANNOTATIONS_STORAGE_KEY = "vpg-annotator-annotations";
|
|
27
|
+
const TOOLBAR_POSITION_STORAGE_KEY = "vpg-annotator-toolbar-position";
|
|
28
|
+
const RUNTIME_GUARD = "__VUE_POM_GENERATOR_ANNOTATOR_RUNTIME__";
|
|
29
|
+
|
|
30
|
+
type RuntimeWindow = Window & {
|
|
31
|
+
[RUNTIME_GUARD]?: AnnotatorRuntime;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface ToolbarPosition {
|
|
35
|
+
left: number;
|
|
36
|
+
top: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ButtonOptions {
|
|
40
|
+
primary?: boolean;
|
|
41
|
+
pressed?: boolean;
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
shortcut?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type ToolbarIconName = "drag" | "inspect" | "preview" | "copy" | "clear" | "settings";
|
|
47
|
+
|
|
48
|
+
const TOOLBAR_ICON_MARKUP: Record<ToolbarIconName, string> = {
|
|
49
|
+
drag: '<svg viewBox="0 0 16 16"><path d="M5 3h1v1H5zM10 3h1v1h-1zM5 7h1v1H5zM10 7h1v1h-1zM5 11h1v1H5zM10 11h1v1h-1z" fill="currentColor" stroke="none" /></svg>',
|
|
50
|
+
inspect: '<svg viewBox="0 0 16 16"><path d="M8 2v3M8 11v3M2 8h3M11 8h3M4 4l2 2M10 10l2 2M12 4l-2 2M6 10l-2 2"/><circle cx="8" cy="8" r="2.5"/></svg>',
|
|
51
|
+
preview: '<svg viewBox="0 0 16 16"><path d="M1.5 8s2.4-4 6.5-4 6.5 4 6.5 4-2.4 4-6.5 4-6.5-4-6.5-4Z"/><circle cx="8" cy="8" r="1.8"/></svg>',
|
|
52
|
+
copy: '<svg viewBox="0 0 16 16"><path d="M6 2.5h6.5v9H6z"/><path d="M3.5 5.5H5v6.5h5.5v1.5h-7z"/></svg>',
|
|
53
|
+
clear: '<svg viewBox="0 0 16 16"><path d="M2.5 4.5h11"/><path d="M6 2.5h4"/><path d="M5 4.5v8"/><path d="M8 4.5v8"/><path d="M11 4.5v8"/><path d="M4 4.5h8l-.6 9H4.6z"/></svg>',
|
|
54
|
+
settings: '<svg viewBox="0 0 16 16"><path d="M8 2.2l1 .6 1.2-.2.8 1 .9.7-.3 1.2.5 1-.5 1 .3 1.2-.9.7-.8 1-1.2-.2-1 .6-1-.6-1.2.2-.8-1-.9-.7.3-1.2-.5-1 .5-1-.3-1.2.9-.7.8-1 1.2.2z"/><circle cx="8" cy="8" r="2.2"/></svg>',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const SHORTCUT_LABELS = {
|
|
58
|
+
select: "S",
|
|
59
|
+
preview: "P",
|
|
60
|
+
copy: "C",
|
|
61
|
+
clear: "X",
|
|
62
|
+
settings: ",",
|
|
63
|
+
cancel: "Esc",
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
function normalizeText(value: string | undefined): string | undefined {
|
|
67
|
+
const normalized = value?.replace(/\s+/g, " ").trim();
|
|
68
|
+
return normalized || undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatShortcutTitle(label: string, shortcut: string | undefined): string {
|
|
72
|
+
return shortcut ? `${label} (${shortcut})` : label;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isInsideAnnotatorTree(node: EventTarget | null): boolean {
|
|
76
|
+
return node instanceof Element && !!node.closest(`[${ANNOTATOR_ROOT_ATTR}]`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isEditableTarget(node: EventTarget | null): boolean {
|
|
80
|
+
if (!(node instanceof Element)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return node.closest('[contenteditable=""], [contenteditable="true"]') !== null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getElementSummary(element: Element): string {
|
|
92
|
+
const testId = element.getAttribute("data-testid");
|
|
93
|
+
if (testId) {
|
|
94
|
+
return `[data-testid="${testId}"]`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const id = element.getAttribute("id");
|
|
98
|
+
if (id) {
|
|
99
|
+
return `${element.tagName.toLowerCase()}#${id}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const classes = Array.from(element.classList).slice(0, 2).join(".");
|
|
103
|
+
return classes ? `${element.tagName.toLowerCase()}.${classes}` : element.tagName.toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getElementPath(element: Element): string {
|
|
107
|
+
const segments: string[] = [];
|
|
108
|
+
let current: Element | null = element;
|
|
109
|
+
let depth = 0;
|
|
110
|
+
|
|
111
|
+
while (current && current !== document.body && depth < 8) {
|
|
112
|
+
const summary = getElementSummary(current);
|
|
113
|
+
segments.unshift(summary);
|
|
114
|
+
current = current.parentElement;
|
|
115
|
+
depth += 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return segments.join(" > ");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getNearbyText(element: Element): string | undefined {
|
|
122
|
+
const ownText = normalizeText(element.textContent || undefined);
|
|
123
|
+
if (ownText && ownText.length >= 2) {
|
|
124
|
+
return ownText.length > 160 ? `${ownText.slice(0, 160)}...` : ownText;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const parentText = normalizeText(element.parentElement?.textContent || undefined);
|
|
128
|
+
if (parentText) {
|
|
129
|
+
return parentText.length > 160 ? `${parentText.slice(0, 160)}...` : parentText;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getNearbyLocator(element: Element): string | undefined {
|
|
136
|
+
const testId = element.getAttribute("data-testid") || element.getAttribute("data-v-pom-testid");
|
|
137
|
+
if (testId) {
|
|
138
|
+
return `near \`[data-testid="${testId}"]\``;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const siblings = Array.from(element.parentElement?.children || [])
|
|
142
|
+
.filter(sibling => sibling !== element)
|
|
143
|
+
.slice(0, 3)
|
|
144
|
+
.map(getElementSummary);
|
|
145
|
+
|
|
146
|
+
return siblings.length ? `near ${siblings.map(value => `\`${value}\``).join(", ")}` : undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function createFloatingArrow(): HTMLDivElement {
|
|
150
|
+
const arrowEl = document.createElement("div");
|
|
151
|
+
arrowEl.className = "vpg-annotator-arrow";
|
|
152
|
+
arrowEl.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
153
|
+
return arrowEl;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function toCssPixels(value: number): string {
|
|
157
|
+
return `${Math.round(value)}px`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function clamp(value: number, min: number, max: number): number {
|
|
161
|
+
return Math.min(Math.max(value, min), max);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createButtonBase(label: string, onClick: () => void, options: ButtonOptions = {}): HTMLButtonElement {
|
|
165
|
+
const button = document.createElement("button");
|
|
166
|
+
button.type = "button";
|
|
167
|
+
button.className = `vpg-annotator-btn${options.primary ? " vpg-annotator-btn--primary" : ""}`;
|
|
168
|
+
button.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
169
|
+
button.setAttribute("aria-label", label);
|
|
170
|
+
button.title = formatShortcutTitle(label, options.shortcut);
|
|
171
|
+
if (options.shortcut) {
|
|
172
|
+
button.setAttribute("aria-keyshortcuts", options.shortcut);
|
|
173
|
+
}
|
|
174
|
+
if (options.pressed) {
|
|
175
|
+
button.setAttribute("aria-pressed", "true");
|
|
176
|
+
}
|
|
177
|
+
if (options.disabled) {
|
|
178
|
+
button.disabled = true;
|
|
179
|
+
}
|
|
180
|
+
button.addEventListener("click", (event) => {
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
event.stopPropagation();
|
|
183
|
+
onClick();
|
|
184
|
+
});
|
|
185
|
+
return button;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function createIcon(iconName: ToolbarIconName): HTMLSpanElement {
|
|
189
|
+
const icon = document.createElement("span");
|
|
190
|
+
icon.className = "vpg-annotator-icon";
|
|
191
|
+
icon.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
192
|
+
icon.setAttribute("aria-hidden", "true");
|
|
193
|
+
icon.innerHTML = TOOLBAR_ICON_MARKUP[iconName];
|
|
194
|
+
return icon;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function createButton(label: string, onClick: () => void, options: ButtonOptions = {}): HTMLButtonElement {
|
|
198
|
+
const button = createButtonBase(label, onClick, options);
|
|
199
|
+
button.textContent = label;
|
|
200
|
+
return button;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function createIconButton(
|
|
204
|
+
label: string,
|
|
205
|
+
iconName: ToolbarIconName,
|
|
206
|
+
onClick: () => void,
|
|
207
|
+
options: ButtonOptions = {},
|
|
208
|
+
): HTMLButtonElement {
|
|
209
|
+
const button = createButtonBase(label, onClick, options);
|
|
210
|
+
button.classList.add("vpg-annotator-btn--icon");
|
|
211
|
+
button.appendChild(createIcon(iconName));
|
|
212
|
+
return button;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function createToolbarHandle(): HTMLDivElement {
|
|
216
|
+
const handle = document.createElement("div");
|
|
217
|
+
handle.className = "vpg-annotator-toolbar-handle";
|
|
218
|
+
handle.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
219
|
+
handle.setAttribute("title", "Drag annotator toolbar");
|
|
220
|
+
handle.setAttribute("aria-hidden", "true");
|
|
221
|
+
handle.appendChild(createIcon("drag"));
|
|
222
|
+
return handle;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function copyTextToClipboard(text: string) {
|
|
226
|
+
if (navigator.clipboard?.writeText) {
|
|
227
|
+
await navigator.clipboard.writeText(text);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const textarea = document.createElement("textarea");
|
|
232
|
+
textarea.value = text;
|
|
233
|
+
textarea.setAttribute("readonly", "true");
|
|
234
|
+
textarea.style.position = "fixed";
|
|
235
|
+
textarea.style.top = "0";
|
|
236
|
+
textarea.style.left = "0";
|
|
237
|
+
textarea.style.opacity = "0";
|
|
238
|
+
document.body.appendChild(textarea);
|
|
239
|
+
textarea.select();
|
|
240
|
+
|
|
241
|
+
const copied = document.execCommand("copy");
|
|
242
|
+
textarea.remove();
|
|
243
|
+
|
|
244
|
+
if (!copied) {
|
|
245
|
+
throw new Error("Clipboard copy failed.");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
class AnnotatorRuntime {
|
|
250
|
+
private readonly options: AnnotatorClientOptions;
|
|
251
|
+
private readonly settings: AnnotatorSettings;
|
|
252
|
+
private annotations: AnnotationRecord[] = [];
|
|
253
|
+
private hoveredElement: Element | null = null;
|
|
254
|
+
private inspectMode = false;
|
|
255
|
+
private pendingAnnotation: AnnotationRecord | null = null;
|
|
256
|
+
private editingAnnotationId: string | null = null;
|
|
257
|
+
private toolbarEl!: HTMLDivElement;
|
|
258
|
+
private markerLayerEl!: HTMLDivElement;
|
|
259
|
+
private panelLayerEl!: HTMLDivElement;
|
|
260
|
+
private highlightEl!: HTMLDivElement;
|
|
261
|
+
private highlightLabelEl!: HTMLDivElement;
|
|
262
|
+
private shieldEl!: HTMLDivElement;
|
|
263
|
+
private toastEl: HTMLDivElement | null = null;
|
|
264
|
+
private previewButton: HTMLButtonElement | null = null;
|
|
265
|
+
private settingsButton: HTMLButtonElement | null = null;
|
|
266
|
+
private currentPanelCleanup: (() => void) | null = null;
|
|
267
|
+
private currentPanelEl: HTMLElement | null = null;
|
|
268
|
+
private toastTimer: number | null = null;
|
|
269
|
+
private toolbarPosition: ToolbarPosition | null = null;
|
|
270
|
+
|
|
271
|
+
constructor(options: AnnotatorClientOptions) {
|
|
272
|
+
this.options = options;
|
|
273
|
+
this.settings = this.loadSettings();
|
|
274
|
+
this.annotations = this.loadAnnotations();
|
|
275
|
+
this.toolbarPosition = this.loadToolbarPosition();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
mount() {
|
|
279
|
+
this.ensureStyles();
|
|
280
|
+
this.createChrome();
|
|
281
|
+
this.renderToolbar();
|
|
282
|
+
this.renderMarkers();
|
|
283
|
+
this.attachGlobalListeners();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private ensureStyles() {
|
|
287
|
+
if (document.getElementById("vpg-annotator-styles")) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const style = document.createElement("style");
|
|
291
|
+
style.id = "vpg-annotator-styles";
|
|
292
|
+
style.textContent = ANNOTATOR_STYLES;
|
|
293
|
+
document.head.appendChild(style);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private createChrome() {
|
|
297
|
+
this.toolbarEl = document.createElement("div");
|
|
298
|
+
this.toolbarEl.className = "vpg-annotator-toolbar";
|
|
299
|
+
this.toolbarEl.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
300
|
+
|
|
301
|
+
this.markerLayerEl = document.createElement("div");
|
|
302
|
+
this.markerLayerEl.className = "vpg-annotator-layer vpg-annotator-layer--markers";
|
|
303
|
+
this.markerLayerEl.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
304
|
+
|
|
305
|
+
this.panelLayerEl = document.createElement("div");
|
|
306
|
+
this.panelLayerEl.className = "vpg-annotator-layer vpg-annotator-layer--panels";
|
|
307
|
+
this.panelLayerEl.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
308
|
+
|
|
309
|
+
this.highlightEl = document.createElement("div");
|
|
310
|
+
this.highlightEl.className = "vpg-annotator-highlight";
|
|
311
|
+
this.highlightEl.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
312
|
+
this.highlightEl.hidden = true;
|
|
313
|
+
|
|
314
|
+
this.highlightLabelEl = document.createElement("div");
|
|
315
|
+
this.highlightLabelEl.className = "vpg-annotator-highlight-label";
|
|
316
|
+
this.highlightLabelEl.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
317
|
+
this.highlightEl.appendChild(this.highlightLabelEl);
|
|
318
|
+
this.panelLayerEl.appendChild(this.highlightEl);
|
|
319
|
+
|
|
320
|
+
this.shieldEl = document.createElement("div");
|
|
321
|
+
this.shieldEl.className = "vpg-annotator-shield";
|
|
322
|
+
this.shieldEl.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
323
|
+
this.shieldEl.hidden = true;
|
|
324
|
+
|
|
325
|
+
document.body.append(this.markerLayerEl, this.panelLayerEl, this.shieldEl, this.toolbarEl);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private renderToolbar() {
|
|
329
|
+
this.toolbarEl.replaceChildren();
|
|
330
|
+
|
|
331
|
+
const handle = createToolbarHandle();
|
|
332
|
+
this.attachToolbarDrag(handle);
|
|
333
|
+
|
|
334
|
+
const count = document.createElement("span");
|
|
335
|
+
count.className = "vpg-annotator-count vpg-annotator-subtle";
|
|
336
|
+
count.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
337
|
+
count.textContent = `${this.annotations.length} annotation${this.annotations.length === 1 ? "" : "s"}`;
|
|
338
|
+
|
|
339
|
+
const selectButton = createIconButton(this.inspectMode ? "Stop selecting" : "Select element", "inspect", () => {
|
|
340
|
+
this.setInspectMode(!this.inspectMode);
|
|
341
|
+
}, { primary: this.inspectMode, pressed: this.inspectMode, shortcut: SHORTCUT_LABELS.select });
|
|
342
|
+
|
|
343
|
+
const previewButton = createIconButton("Preview annotations", "preview", () => this.openPreview(), {
|
|
344
|
+
disabled: this.annotations.length === 0,
|
|
345
|
+
shortcut: SHORTCUT_LABELS.preview,
|
|
346
|
+
});
|
|
347
|
+
this.previewButton = previewButton;
|
|
348
|
+
|
|
349
|
+
const copyButton = createIconButton("Copy annotations", "copy", () => this.copyAnnotations(), {
|
|
350
|
+
disabled: this.annotations.length === 0,
|
|
351
|
+
shortcut: SHORTCUT_LABELS.copy,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const clearButton = createIconButton("Clear annotations", "clear", () => this.clearAnnotations(), {
|
|
355
|
+
disabled: this.annotations.length === 0,
|
|
356
|
+
shortcut: SHORTCUT_LABELS.clear,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const settingsButton = createIconButton("Annotator settings", "settings", () => this.openSettings(), {
|
|
360
|
+
shortcut: SHORTCUT_LABELS.settings,
|
|
361
|
+
});
|
|
362
|
+
this.settingsButton = settingsButton;
|
|
363
|
+
|
|
364
|
+
this.toolbarEl.append(handle, selectButton, previewButton, copyButton, clearButton, settingsButton, count);
|
|
365
|
+
this.applyToolbarPosition();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private renderMarkers() {
|
|
369
|
+
this.markerLayerEl.replaceChildren();
|
|
370
|
+
for (const [index, annotation] of this.annotations.entries()) {
|
|
371
|
+
const marker = document.createElement("button");
|
|
372
|
+
marker.type = "button";
|
|
373
|
+
marker.className = "vpg-annotator-marker";
|
|
374
|
+
marker.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
375
|
+
marker.textContent = String(index + 1);
|
|
376
|
+
marker.style.left = toCssPixels(annotation.pageX);
|
|
377
|
+
marker.style.top = toCssPixels(annotation.pageY - window.scrollY);
|
|
378
|
+
marker.addEventListener("click", (event) => {
|
|
379
|
+
event.preventDefault();
|
|
380
|
+
event.stopPropagation();
|
|
381
|
+
this.openInputForExistingAnnotation(annotation, marker);
|
|
382
|
+
});
|
|
383
|
+
this.markerLayerEl.appendChild(marker);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private attachGlobalListeners() {
|
|
388
|
+
this.shieldEl.addEventListener("mousemove", (event) => this.onShieldMouseMove(event));
|
|
389
|
+
this.shieldEl.addEventListener("click", (event) => this.onShieldClick(event));
|
|
390
|
+
window.addEventListener("scroll", () => this.renderMarkers(), true);
|
|
391
|
+
window.addEventListener("resize", () => {
|
|
392
|
+
this.renderMarkers();
|
|
393
|
+
this.applyToolbarPosition();
|
|
394
|
+
});
|
|
395
|
+
window.addEventListener("keydown", (event) => this.onWindowKeyDown(event), true);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private loadSettings(): AnnotatorSettings {
|
|
399
|
+
try {
|
|
400
|
+
const raw = localStorage.getItem(SETTINGS_STORAGE_KEY);
|
|
401
|
+
const parsed = raw ? JSON.parse(raw) as Partial<AnnotatorSettings> : {};
|
|
402
|
+
return {
|
|
403
|
+
outputDetail: parsed.outputDetail ?? this.options.outputDetail,
|
|
404
|
+
copyToClipboard: parsed.copyToClipboard ?? this.options.copyToClipboard,
|
|
405
|
+
showComponentTree: parsed.showComponentTree ?? this.options.showComponentTree,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return {
|
|
410
|
+
outputDetail: this.options.outputDetail,
|
|
411
|
+
copyToClipboard: this.options.copyToClipboard,
|
|
412
|
+
showComponentTree: this.options.showComponentTree,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private saveSettings() {
|
|
418
|
+
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(this.settings));
|
|
419
|
+
this.renderToolbar();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private loadAnnotations(): AnnotationRecord[] {
|
|
423
|
+
try {
|
|
424
|
+
const raw = sessionStorage.getItem(ANNOTATIONS_STORAGE_KEY);
|
|
425
|
+
if (!raw) {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
const parsed = JSON.parse(raw) as Record<string, AnnotationRecord[]> | AnnotationRecord[];
|
|
429
|
+
if (Array.isArray(parsed)) {
|
|
430
|
+
return parsed;
|
|
431
|
+
}
|
|
432
|
+
return Array.isArray(parsed[window.location.href]) ? parsed[window.location.href]! : [];
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private saveAnnotations() {
|
|
440
|
+
const store = { [window.location.href]: this.annotations };
|
|
441
|
+
sessionStorage.setItem(ANNOTATIONS_STORAGE_KEY, JSON.stringify(store));
|
|
442
|
+
this.renderToolbar();
|
|
443
|
+
this.renderMarkers();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private loadToolbarPosition(): ToolbarPosition | null {
|
|
447
|
+
try {
|
|
448
|
+
const raw = localStorage.getItem(TOOLBAR_POSITION_STORAGE_KEY);
|
|
449
|
+
if (!raw) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
const parsed = JSON.parse(raw) as Partial<ToolbarPosition>;
|
|
453
|
+
if (typeof parsed.left !== "number" || typeof parsed.top !== "number") {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
return { left: parsed.left, top: parsed.top };
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private saveToolbarPosition() {
|
|
464
|
+
try {
|
|
465
|
+
if (!this.toolbarPosition) {
|
|
466
|
+
localStorage.removeItem(TOOLBAR_POSITION_STORAGE_KEY);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
localStorage.setItem(TOOLBAR_POSITION_STORAGE_KEY, JSON.stringify(this.toolbarPosition));
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
// Ignore storage failures and keep the current in-memory position.
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private clampToolbarPosition(position: ToolbarPosition): ToolbarPosition {
|
|
477
|
+
const margin = 12;
|
|
478
|
+
const width = this.toolbarEl.offsetWidth || 0;
|
|
479
|
+
const height = this.toolbarEl.offsetHeight || 0;
|
|
480
|
+
const maxLeft = Math.max(margin, window.innerWidth - width - margin);
|
|
481
|
+
const maxTop = Math.max(margin, window.innerHeight - height - margin);
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
left: clamp(position.left, margin, maxLeft),
|
|
485
|
+
top: clamp(position.top, margin, maxTop),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private applyToolbarPosition() {
|
|
490
|
+
if (!this.toolbarPosition) {
|
|
491
|
+
this.toolbarEl.style.left = "";
|
|
492
|
+
this.toolbarEl.style.top = "";
|
|
493
|
+
this.toolbarEl.style.right = "";
|
|
494
|
+
this.toolbarEl.style.bottom = "";
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const position = this.clampToolbarPosition(this.toolbarPosition);
|
|
499
|
+
this.toolbarPosition = position;
|
|
500
|
+
this.toolbarEl.style.left = toCssPixels(position.left);
|
|
501
|
+
this.toolbarEl.style.top = toCssPixels(position.top);
|
|
502
|
+
this.toolbarEl.style.right = "auto";
|
|
503
|
+
this.toolbarEl.style.bottom = "auto";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private attachToolbarDrag(handle: HTMLElement) {
|
|
507
|
+
handle.addEventListener("pointerdown", (event) => {
|
|
508
|
+
if (event.button !== 0) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
event.preventDefault();
|
|
513
|
+
event.stopPropagation();
|
|
514
|
+
|
|
515
|
+
const rect = this.toolbarEl.getBoundingClientRect();
|
|
516
|
+
const startX = event.clientX;
|
|
517
|
+
const startY = event.clientY;
|
|
518
|
+
const startLeft = rect.left;
|
|
519
|
+
const startTop = rect.top;
|
|
520
|
+
this.toolbarEl.classList.add("vpg-annotator-toolbar--dragging");
|
|
521
|
+
|
|
522
|
+
const onPointerMove = (moveEvent: PointerEvent) => {
|
|
523
|
+
this.toolbarPosition = {
|
|
524
|
+
left: startLeft + moveEvent.clientX - startX,
|
|
525
|
+
top: startTop + moveEvent.clientY - startY,
|
|
526
|
+
};
|
|
527
|
+
this.applyToolbarPosition();
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const onPointerUp = () => {
|
|
531
|
+
this.toolbarEl.classList.remove("vpg-annotator-toolbar--dragging");
|
|
532
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
533
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
534
|
+
window.removeEventListener("pointercancel", onPointerUp);
|
|
535
|
+
this.saveToolbarPosition();
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
539
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
540
|
+
window.addEventListener("pointercancel", onPointerUp);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private onWindowKeyDown(event: KeyboardEvent) {
|
|
545
|
+
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (event.key === "Escape") {
|
|
550
|
+
if (!this.inspectMode && !this.currentPanelEl) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
event.preventDefault();
|
|
554
|
+
if (this.inspectMode) {
|
|
555
|
+
this.setInspectMode(false);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
this.closePanel();
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (isEditableTarget(event.target)) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const key = event.key.toLowerCase();
|
|
568
|
+
if (key === SHORTCUT_LABELS.select.toLowerCase()) {
|
|
569
|
+
event.preventDefault();
|
|
570
|
+
this.setInspectMode(!this.inspectMode);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (key === SHORTCUT_LABELS.preview.toLowerCase()) {
|
|
575
|
+
if (!this.annotations.length) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
event.preventDefault();
|
|
579
|
+
this.openPreview();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (key === SHORTCUT_LABELS.copy.toLowerCase()) {
|
|
584
|
+
if (!this.annotations.length) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
event.preventDefault();
|
|
588
|
+
void this.copyAnnotations();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (key === SHORTCUT_LABELS.clear.toLowerCase()) {
|
|
593
|
+
if (!this.annotations.length) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
event.preventDefault();
|
|
597
|
+
this.clearAnnotations();
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (event.key === SHORTCUT_LABELS.settings) {
|
|
602
|
+
event.preventDefault();
|
|
603
|
+
this.openSettings();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private setInspectMode(next: boolean) {
|
|
608
|
+
this.inspectMode = next;
|
|
609
|
+
this.shieldEl.hidden = !next;
|
|
610
|
+
this.highlightEl.hidden = !next;
|
|
611
|
+
if (!next) {
|
|
612
|
+
this.hoveredElement = null;
|
|
613
|
+
this.highlightEl.hidden = true;
|
|
614
|
+
}
|
|
615
|
+
this.closePanel();
|
|
616
|
+
this.renderToolbar();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private stopInspectModePreservingPending() {
|
|
620
|
+
this.inspectMode = false;
|
|
621
|
+
this.shieldEl.hidden = true;
|
|
622
|
+
this.highlightEl.hidden = true;
|
|
623
|
+
this.hoveredElement = null;
|
|
624
|
+
this.renderToolbar();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private elementFromPoint(clientX: number, clientY: number): Element | null {
|
|
628
|
+
const previousPointerEvents = this.shieldEl.style.pointerEvents;
|
|
629
|
+
this.shieldEl.style.pointerEvents = "none";
|
|
630
|
+
const elements = document.elementsFromPoint(clientX, clientY);
|
|
631
|
+
this.shieldEl.style.pointerEvents = previousPointerEvents;
|
|
632
|
+
return elements.find(element => !isInsideAnnotatorTree(element)) ?? null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private onShieldMouseMove(event: MouseEvent) {
|
|
636
|
+
if (!this.inspectMode) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const element = this.elementFromPoint(event.clientX, event.clientY);
|
|
641
|
+
if (!element) {
|
|
642
|
+
this.hoveredElement = null;
|
|
643
|
+
this.highlightEl.hidden = true;
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
this.hoveredElement = element;
|
|
648
|
+
const rect = element.getBoundingClientRect();
|
|
649
|
+
const info = resolveVueComponentInfo(element, this.options);
|
|
650
|
+
Object.assign(this.highlightEl.style, {
|
|
651
|
+
left: toCssPixels(rect.left),
|
|
652
|
+
top: toCssPixels(rect.top),
|
|
653
|
+
width: toCssPixels(rect.width),
|
|
654
|
+
height: toCssPixels(rect.height),
|
|
655
|
+
});
|
|
656
|
+
this.highlightLabelEl.textContent = info?.formatted || getElementSummary(element);
|
|
657
|
+
this.highlightEl.hidden = false;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private onShieldClick(event: MouseEvent) {
|
|
661
|
+
if (!this.inspectMode) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
event.preventDefault();
|
|
666
|
+
event.stopPropagation();
|
|
667
|
+
const element = this.elementFromPoint(event.clientX, event.clientY);
|
|
668
|
+
if (!element) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
this.hoveredElement = element;
|
|
673
|
+
this.pendingAnnotation = this.buildAnnotationDraft(element, "");
|
|
674
|
+
this.editingAnnotationId = null;
|
|
675
|
+
this.stopInspectModePreservingPending();
|
|
676
|
+
this.openInputForDraft(element);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private buildAnnotationDraft(element: Element, comment: string): AnnotationRecord {
|
|
680
|
+
const rect = element.getBoundingClientRect();
|
|
681
|
+
const componentInfo = resolveVueComponentInfo(element, this.options);
|
|
682
|
+
return {
|
|
683
|
+
id: `draft-${Date.now()}`,
|
|
684
|
+
comment,
|
|
685
|
+
component: this.settings.showComponentTree ? componentInfo?.component : undefined,
|
|
686
|
+
source: componentInfo?.source,
|
|
687
|
+
targetLabel: componentInfo?.component || getElementSummary(element),
|
|
688
|
+
uiText: getNearbyText(element),
|
|
689
|
+
locator: getNearbyLocator(element),
|
|
690
|
+
domHint: getElementPath(element),
|
|
691
|
+
pageX: rect.left + rect.width / 2,
|
|
692
|
+
pageY: rect.top + window.scrollY,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private createPanelBase(className: string) {
|
|
697
|
+
const panel = document.createElement("div");
|
|
698
|
+
panel.className = className;
|
|
699
|
+
panel.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
700
|
+
const arrowEl = createFloatingArrow();
|
|
701
|
+
const body = document.createElement("div");
|
|
702
|
+
body.className = className === "vpg-annotator-settings" ? "vpg-annotator-settings-body" : className === "vpg-annotator-input" ? "vpg-annotator-input-body" : "vpg-annotator-panel-body";
|
|
703
|
+
body.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
704
|
+
panel.append(arrowEl, body);
|
|
705
|
+
document.body.appendChild(panel);
|
|
706
|
+
return { panel, arrowEl, body };
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private attachFloating(reference: Element, panel: HTMLElement, arrowEl: HTMLElement, placement: Placement, allowedPlacements: readonly Placement[]) {
|
|
710
|
+
const cleanup = autoUpdate(reference, panel, async () => {
|
|
711
|
+
const result = await computePosition(reference, panel, {
|
|
712
|
+
strategy: "fixed",
|
|
713
|
+
placement,
|
|
714
|
+
middleware: [
|
|
715
|
+
offset(14),
|
|
716
|
+
autoPlacement({ allowedPlacements: [...allowedPlacements], padding: 12 }),
|
|
717
|
+
shift({ padding: 12 }),
|
|
718
|
+
arrow({ element: arrowEl, padding: 10 }),
|
|
719
|
+
],
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
panel.setAttribute("data-placement", result.placement);
|
|
723
|
+
panel.style.left = toCssPixels(result.x);
|
|
724
|
+
panel.style.top = toCssPixels(result.y);
|
|
725
|
+
panel.style.visibility = "visible";
|
|
726
|
+
|
|
727
|
+
const arrowData = result.middlewareData.arrow;
|
|
728
|
+
const side = result.placement.split("-")[0];
|
|
729
|
+
const staticSide = side === "top" ? "bottom" : side === "bottom" ? "top" : side === "left" ? "right" : "left";
|
|
730
|
+
arrowEl.style.removeProperty("top");
|
|
731
|
+
arrowEl.style.removeProperty("right");
|
|
732
|
+
arrowEl.style.removeProperty("bottom");
|
|
733
|
+
arrowEl.style.removeProperty("left");
|
|
734
|
+
if (typeof arrowData?.x === "number") {
|
|
735
|
+
arrowEl.style.left = toCssPixels(arrowData.x);
|
|
736
|
+
}
|
|
737
|
+
if (typeof arrowData?.y === "number") {
|
|
738
|
+
arrowEl.style.top = toCssPixels(arrowData.y);
|
|
739
|
+
}
|
|
740
|
+
arrowEl.style.setProperty(staticSide, "-7px");
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
return cleanup;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
private openInputForDraft(reference: Element) {
|
|
747
|
+
if (!this.pendingAnnotation) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const { panel, body, arrowEl } = this.createPanelBase("vpg-annotator-input");
|
|
752
|
+
const title = document.createElement("div");
|
|
753
|
+
title.className = "vpg-annotator-heading";
|
|
754
|
+
title.textContent = this.pendingAnnotation.component || this.pendingAnnotation.targetLabel;
|
|
755
|
+
|
|
756
|
+
const subtitle = document.createElement("p");
|
|
757
|
+
subtitle.className = "vpg-annotator-subtle";
|
|
758
|
+
subtitle.textContent = this.pendingAnnotation.source || "Unable to find component file path.";
|
|
759
|
+
|
|
760
|
+
const metadata = document.createElement("textarea");
|
|
761
|
+
metadata.className = "vpg-annotator-textarea vpg-annotator-input-meta";
|
|
762
|
+
metadata.readOnly = true;
|
|
763
|
+
metadata.value = formatSingleAnnotationPreview(this.pendingAnnotation, this.settings.outputDetail, window.location.href);
|
|
764
|
+
|
|
765
|
+
const commentLabel = document.createElement("label");
|
|
766
|
+
commentLabel.className = "vpg-annotator-label";
|
|
767
|
+
commentLabel.textContent = "Comment";
|
|
768
|
+
|
|
769
|
+
const comment = document.createElement("textarea");
|
|
770
|
+
comment.className = "vpg-annotator-comment";
|
|
771
|
+
comment.value = this.pendingAnnotation.comment;
|
|
772
|
+
comment.placeholder = "Add a comment...";
|
|
773
|
+
|
|
774
|
+
const actions = document.createElement("div");
|
|
775
|
+
actions.className = "vpg-annotator-actions";
|
|
776
|
+
const cancelButton = createButton("Cancel", () => this.closePanel());
|
|
777
|
+
const saveButton = createButton(this.editingAnnotationId ? "Save" : "Add", () => {
|
|
778
|
+
if (!this.pendingAnnotation) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
this.pendingAnnotation.comment = comment.value.trim();
|
|
782
|
+
if (this.editingAnnotationId) {
|
|
783
|
+
const index = this.annotations.findIndex(annotation => annotation.id === this.editingAnnotationId);
|
|
784
|
+
if (index >= 0) {
|
|
785
|
+
this.annotations[index] = { ...this.pendingAnnotation, id: this.editingAnnotationId };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
this.annotations.push({ ...this.pendingAnnotation, id: `annotation-${Date.now()}` });
|
|
790
|
+
}
|
|
791
|
+
this.saveAnnotations();
|
|
792
|
+
this.closePanel();
|
|
793
|
+
}, { primary: true });
|
|
794
|
+
|
|
795
|
+
actions.append(cancelButton);
|
|
796
|
+
if (this.editingAnnotationId) {
|
|
797
|
+
const deleteButton = createButton("Delete", () => {
|
|
798
|
+
this.annotations = this.annotations.filter(annotation => annotation.id !== this.editingAnnotationId);
|
|
799
|
+
this.saveAnnotations();
|
|
800
|
+
this.closePanel();
|
|
801
|
+
});
|
|
802
|
+
actions.append(deleteButton);
|
|
803
|
+
}
|
|
804
|
+
actions.append(saveButton);
|
|
805
|
+
|
|
806
|
+
body.append(title, subtitle, metadata, commentLabel, comment, actions);
|
|
807
|
+
|
|
808
|
+
this.showPanel(panel, () => this.attachFloating(reference, panel, arrowEl, "right-start", ["right-start", "left-start", "bottom-start", "top-start"]));
|
|
809
|
+
comment.focus();
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
private openInputForExistingAnnotation(annotation: AnnotationRecord, reference: Element) {
|
|
813
|
+
this.pendingAnnotation = { ...annotation };
|
|
814
|
+
this.editingAnnotationId = annotation.id;
|
|
815
|
+
this.openInputForDraft(reference);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
private openPreview() {
|
|
819
|
+
if (!this.previewButton || this.annotations.length === 0) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const { panel, body, arrowEl } = this.createPanelBase("vpg-annotator-panel");
|
|
824
|
+
const title = document.createElement("div");
|
|
825
|
+
title.className = "vpg-annotator-heading";
|
|
826
|
+
title.textContent = "Annotation preview";
|
|
827
|
+
const subtitle = document.createElement("p");
|
|
828
|
+
subtitle.className = "vpg-annotator-subtle";
|
|
829
|
+
subtitle.textContent = "Exact text that Copy will copy.";
|
|
830
|
+
const textarea = document.createElement("textarea");
|
|
831
|
+
textarea.className = "vpg-annotator-textarea";
|
|
832
|
+
textarea.readOnly = true;
|
|
833
|
+
textarea.value = formatAnnotations(this.annotations, this.settings.outputDetail, window.location.href);
|
|
834
|
+
body.append(title, subtitle, textarea);
|
|
835
|
+
|
|
836
|
+
this.showPanel(panel, () => this.attachFloating(this.previewButton!, panel, arrowEl, "top-start", ["top-start", "left-start", "top-end", "left-end"]));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private openSettings() {
|
|
840
|
+
if (!this.settingsButton) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const { panel, body, arrowEl } = this.createPanelBase("vpg-annotator-settings");
|
|
845
|
+
const title = document.createElement("div");
|
|
846
|
+
title.className = "vpg-annotator-heading";
|
|
847
|
+
title.textContent = "Annotator settings";
|
|
848
|
+
|
|
849
|
+
const detailField = document.createElement("div");
|
|
850
|
+
detailField.className = "vpg-annotator-field";
|
|
851
|
+
const detailLabel = document.createElement("label");
|
|
852
|
+
detailLabel.className = "vpg-annotator-label";
|
|
853
|
+
detailLabel.textContent = "Output detail";
|
|
854
|
+
const detailSelect = document.createElement("select");
|
|
855
|
+
detailSelect.className = "vpg-annotator-select";
|
|
856
|
+
detailSelect.innerHTML = '<option value="standard">Standard</option><option value="forensic">Forensic</option>';
|
|
857
|
+
detailSelect.value = this.settings.outputDetail;
|
|
858
|
+
detailSelect.addEventListener("change", () => {
|
|
859
|
+
this.settings.outputDetail = detailSelect.value === "forensic" ? "forensic" : "standard";
|
|
860
|
+
this.saveSettings();
|
|
861
|
+
});
|
|
862
|
+
detailField.append(detailLabel, detailSelect);
|
|
863
|
+
|
|
864
|
+
const showComponentField = document.createElement("label");
|
|
865
|
+
showComponentField.className = "vpg-annotator-row";
|
|
866
|
+
const showComponentCopy = document.createElement("span");
|
|
867
|
+
showComponentCopy.className = "vpg-annotator-label";
|
|
868
|
+
showComponentCopy.textContent = "Show component labels";
|
|
869
|
+
const showComponentCheckbox = document.createElement("input");
|
|
870
|
+
showComponentCheckbox.type = "checkbox";
|
|
871
|
+
showComponentCheckbox.className = "vpg-annotator-checkbox";
|
|
872
|
+
showComponentCheckbox.checked = this.settings.showComponentTree;
|
|
873
|
+
showComponentCheckbox.addEventListener("change", () => {
|
|
874
|
+
this.settings.showComponentTree = showComponentCheckbox.checked;
|
|
875
|
+
this.saveSettings();
|
|
876
|
+
});
|
|
877
|
+
showComponentField.append(showComponentCopy, showComponentCheckbox);
|
|
878
|
+
|
|
879
|
+
const shortcutsField = document.createElement("div");
|
|
880
|
+
shortcutsField.className = "vpg-annotator-field";
|
|
881
|
+
const shortcutsLabel = document.createElement("div");
|
|
882
|
+
shortcutsLabel.className = "vpg-annotator-label";
|
|
883
|
+
shortcutsLabel.textContent = "Keyboard shortcuts";
|
|
884
|
+
const shortcutsList = document.createElement("div");
|
|
885
|
+
shortcutsList.className = "vpg-annotator-shortcuts";
|
|
886
|
+
|
|
887
|
+
for (const [shortcut, description] of [
|
|
888
|
+
[SHORTCUT_LABELS.select, "Toggle selection mode"],
|
|
889
|
+
[SHORTCUT_LABELS.preview, "Open preview"],
|
|
890
|
+
[SHORTCUT_LABELS.copy, "Copy annotations"],
|
|
891
|
+
[SHORTCUT_LABELS.clear, "Clear annotations"],
|
|
892
|
+
[SHORTCUT_LABELS.settings, "Open settings"],
|
|
893
|
+
[SHORTCUT_LABELS.cancel, "Close panel / cancel selection"],
|
|
894
|
+
] as const) {
|
|
895
|
+
const row = document.createElement("div");
|
|
896
|
+
row.className = "vpg-annotator-shortcut-row";
|
|
897
|
+
const text = document.createElement("span");
|
|
898
|
+
text.className = "vpg-annotator-subtle";
|
|
899
|
+
text.textContent = description;
|
|
900
|
+
const kbd = document.createElement("kbd");
|
|
901
|
+
kbd.className = "vpg-annotator-kbd";
|
|
902
|
+
kbd.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
903
|
+
kbd.textContent = shortcut;
|
|
904
|
+
row.append(text, kbd);
|
|
905
|
+
shortcutsList.appendChild(row);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
shortcutsField.append(shortcutsLabel, shortcutsList);
|
|
909
|
+
|
|
910
|
+
body.append(title, detailField, showComponentField, shortcutsField);
|
|
911
|
+
this.showPanel(panel, () => this.attachFloating(this.settingsButton!, panel, arrowEl, "top-start", ["top-start", "left-start", "top-end", "left-end"]));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private disposeCurrentPanel() {
|
|
915
|
+
this.currentPanelCleanup?.();
|
|
916
|
+
this.currentPanelCleanup = null;
|
|
917
|
+
this.currentPanelEl?.remove();
|
|
918
|
+
this.currentPanelEl = null;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
private showPanel(panel: HTMLElement, attachFloatingFn: () => () => void) {
|
|
922
|
+
this.disposeCurrentPanel();
|
|
923
|
+
this.currentPanelEl = panel;
|
|
924
|
+
this.currentPanelCleanup = attachFloatingFn();
|
|
925
|
+
|
|
926
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
927
|
+
const target = event.target;
|
|
928
|
+
if (isInsideAnnotatorTree(target)) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
this.closePanel();
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
935
|
+
const originalCleanup = this.currentPanelCleanup;
|
|
936
|
+
this.currentPanelCleanup = () => {
|
|
937
|
+
originalCleanup?.();
|
|
938
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
private closePanel() {
|
|
943
|
+
this.disposeCurrentPanel();
|
|
944
|
+
this.pendingAnnotation = null;
|
|
945
|
+
this.editingAnnotationId = null;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private clearAnnotations() {
|
|
949
|
+
if (!this.annotations.length) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
this.annotations = [];
|
|
953
|
+
this.saveAnnotations();
|
|
954
|
+
this.showToast("Annotations cleared");
|
|
955
|
+
this.closePanel();
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private async copyAnnotations() {
|
|
959
|
+
if (!this.annotations.length) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const text = formatAnnotations(this.annotations, this.settings.outputDetail, window.location.href);
|
|
964
|
+
|
|
965
|
+
try {
|
|
966
|
+
await copyTextToClipboard(text);
|
|
967
|
+
this.showToast("Copied");
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
this.showToast("Copy failed");
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
private showToast(message: string) {
|
|
975
|
+
this.toastEl?.remove();
|
|
976
|
+
if (this.toastTimer !== null) {
|
|
977
|
+
window.clearTimeout(this.toastTimer);
|
|
978
|
+
}
|
|
979
|
+
const toast = document.createElement("div");
|
|
980
|
+
toast.className = "vpg-annotator-toast";
|
|
981
|
+
toast.setAttribute(ANNOTATOR_ROOT_ATTR, "");
|
|
982
|
+
toast.textContent = message;
|
|
983
|
+
document.body.appendChild(toast);
|
|
984
|
+
this.toastEl = toast;
|
|
985
|
+
this.toastTimer = window.setTimeout(() => {
|
|
986
|
+
toast.remove();
|
|
987
|
+
if (this.toastEl === toast) {
|
|
988
|
+
this.toastEl = null;
|
|
989
|
+
}
|
|
990
|
+
this.toastTimer = null;
|
|
991
|
+
}, 1600);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
export function mountAnnotatorClient(options: AnnotatorClientOptions) {
|
|
996
|
+
const runtimeWindow = window as RuntimeWindow;
|
|
997
|
+
if (runtimeWindow[RUNTIME_GUARD]) {
|
|
998
|
+
return runtimeWindow[RUNTIME_GUARD]!;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const runtime = new AnnotatorRuntime(options);
|
|
1002
|
+
runtime.mount();
|
|
1003
|
+
runtimeWindow[RUNTIME_GUARD] = runtime;
|
|
1004
|
+
return runtime;
|
|
1005
|
+
}
|