@akccakcctw/vue-grab 1.0.0 → 1.3.0
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/dist/core/api.d.ts +28 -0
- package/dist/core/api.js +105 -0
- package/dist/core/identifier.d.ts +2 -0
- package/dist/core/identifier.js +101 -0
- package/dist/core/overlay.d.ts +24 -0
- package/dist/core/overlay.js +509 -0
- package/dist/core/widget.d.ts +10 -0
- package/dist/core/widget.js +251 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/nuxt/module.d.ts +8 -0
- package/dist/nuxt/module.js +40 -0
- package/dist/nuxt/runtime/plugin.d.ts +2 -0
- package/dist/nuxt/runtime/plugin.js +14 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.js +47 -0
- package/dist/vite.d.ts +8 -0
- package/dist/vite.js +198 -0
- package/package.json +33 -8
- package/.github/release-please-config.json +0 -9
- package/.github/release-please-manifest.json +0 -3
- package/.github/workflows/release.yml +0 -37
- package/AGENTS.md +0 -75
- package/README.md +0 -116
- package/akccakcctw-vue-grab-1.0.0.tgz +0 -0
- package/docs/SDD.md +0 -188
- package/src/__tests__/plugin.spec.ts +0 -60
- package/src/core/__tests__/api.spec.ts +0 -178
- package/src/core/__tests__/identifier.spec.ts +0 -126
- package/src/core/__tests__/overlay.spec.ts +0 -431
- package/src/core/__tests__/widget.spec.ts +0 -57
- package/src/core/api.ts +0 -144
- package/src/core/identifier.ts +0 -89
- package/src/core/overlay.ts +0 -348
- package/src/core/widget.ts +0 -289
- package/src/index.ts +0 -8
- package/src/nuxt/module.ts +0 -102
- package/src/nuxt/runtime/plugin.ts +0 -13
- package/src/plugin.ts +0 -48
- package/tsconfig.json +0 -44
- package/vitest.config.ts +0 -9
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { extractMetadata, identifyComponent } from './identifier.js';
|
|
2
|
+
const IGNORE_EVENTS_ATTR = 'data-vue-grab-ignore-events';
|
|
3
|
+
const CONTEXT_MENU_ATTR = 'data-vue-grab-context';
|
|
4
|
+
const CONTEXT_MENU_ITEM_ATTR = 'data-vue-grab-context-item';
|
|
5
|
+
const CONTEXT_MENU_HINT = 'Right click for options';
|
|
6
|
+
function isIgnoredTarget(target) {
|
|
7
|
+
if (!(target instanceof Element))
|
|
8
|
+
return false;
|
|
9
|
+
return Boolean(target.closest(`[${IGNORE_EVENTS_ATTR}]`));
|
|
10
|
+
}
|
|
11
|
+
function createContextMenuElement(targetWindow) {
|
|
12
|
+
const doc = targetWindow.document;
|
|
13
|
+
const menu = doc.createElement('div');
|
|
14
|
+
menu.setAttribute(CONTEXT_MENU_ATTR, '');
|
|
15
|
+
menu.setAttribute(IGNORE_EVENTS_ATTR, '');
|
|
16
|
+
Object.assign(menu.style, {
|
|
17
|
+
position: 'fixed',
|
|
18
|
+
zIndex: '2147483647',
|
|
19
|
+
minWidth: '140px',
|
|
20
|
+
padding: '6px 0',
|
|
21
|
+
borderRadius: '8px',
|
|
22
|
+
border: '1px solid rgba(0, 0, 0, 0.08)',
|
|
23
|
+
background: '#fff',
|
|
24
|
+
boxShadow: '0 10px 28px rgba(0, 0, 0, 0.18)',
|
|
25
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
26
|
+
fontSize: '12px',
|
|
27
|
+
color: '#111',
|
|
28
|
+
display: 'none',
|
|
29
|
+
pointerEvents: 'auto'
|
|
30
|
+
});
|
|
31
|
+
const createItem = (label, value) => {
|
|
32
|
+
const item = doc.createElement('button');
|
|
33
|
+
item.type = 'button';
|
|
34
|
+
item.textContent = label;
|
|
35
|
+
item.setAttribute(CONTEXT_MENU_ITEM_ATTR, value);
|
|
36
|
+
Object.assign(item.style, {
|
|
37
|
+
display: 'block',
|
|
38
|
+
width: '100%',
|
|
39
|
+
textAlign: 'left',
|
|
40
|
+
border: 'none',
|
|
41
|
+
background: 'transparent',
|
|
42
|
+
padding: '6px 12px',
|
|
43
|
+
cursor: 'pointer',
|
|
44
|
+
color: 'inherit',
|
|
45
|
+
font: 'inherit'
|
|
46
|
+
});
|
|
47
|
+
item.addEventListener('mouseenter', () => {
|
|
48
|
+
item.style.background = 'rgba(0, 0, 0, 0.06)';
|
|
49
|
+
});
|
|
50
|
+
item.addEventListener('mouseleave', () => {
|
|
51
|
+
item.style.background = 'transparent';
|
|
52
|
+
});
|
|
53
|
+
menu.appendChild(item);
|
|
54
|
+
return item;
|
|
55
|
+
};
|
|
56
|
+
const copyItem = createItem('Copy', 'copy');
|
|
57
|
+
const copyHtmlItem = createItem('Copy HTML', 'copy-html');
|
|
58
|
+
return { menu, copyItem, copyHtmlItem };
|
|
59
|
+
}
|
|
60
|
+
function positionContextMenu(menu, x, y, targetWindow) {
|
|
61
|
+
const padding = 8;
|
|
62
|
+
const maxLeft = targetWindow.innerWidth - menu.offsetWidth - padding;
|
|
63
|
+
const maxTop = targetWindow.innerHeight - menu.offsetHeight - padding;
|
|
64
|
+
const left = Math.max(padding, Math.min(x, maxLeft));
|
|
65
|
+
const top = Math.max(padding, Math.min(y, maxTop));
|
|
66
|
+
menu.style.left = `${left}px`;
|
|
67
|
+
menu.style.top = `${top}px`;
|
|
68
|
+
}
|
|
69
|
+
function createOverlayElement(targetWindow, options) {
|
|
70
|
+
const el = targetWindow.document.createElement('div');
|
|
71
|
+
el.setAttribute('data-vue-grab-overlay', 'true');
|
|
72
|
+
el.style.position = 'fixed';
|
|
73
|
+
el.style.pointerEvents = 'none';
|
|
74
|
+
el.style.zIndex = '2147483647';
|
|
75
|
+
el.style.border = '2px solid #e67e22';
|
|
76
|
+
el.style.background = 'rgba(230, 126, 34, 0.08)';
|
|
77
|
+
el.style.top = '0';
|
|
78
|
+
el.style.left = '0';
|
|
79
|
+
el.style.width = '0';
|
|
80
|
+
el.style.height = '0';
|
|
81
|
+
if (options?.overlayStyle) {
|
|
82
|
+
for (const [key, value] of Object.entries(options.overlayStyle)) {
|
|
83
|
+
el.style[key] = value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return el;
|
|
87
|
+
}
|
|
88
|
+
function updateOverlayPosition(overlay, rect) {
|
|
89
|
+
overlay.style.top = `${rect.top}px`;
|
|
90
|
+
overlay.style.left = `${rect.left}px`;
|
|
91
|
+
overlay.style.width = `${rect.width}px`;
|
|
92
|
+
overlay.style.height = `${rect.height}px`;
|
|
93
|
+
}
|
|
94
|
+
function createTooltipElement(targetWindow) {
|
|
95
|
+
const el = targetWindow.document.createElement('div');
|
|
96
|
+
el.setAttribute('data-vue-grab-tooltip', 'true');
|
|
97
|
+
el.style.position = 'fixed';
|
|
98
|
+
el.style.pointerEvents = 'none';
|
|
99
|
+
el.style.zIndex = '2147483647';
|
|
100
|
+
el.style.padding = '4px 8px';
|
|
101
|
+
el.style.borderRadius = '6px';
|
|
102
|
+
el.style.background = '#111';
|
|
103
|
+
el.style.color = '#fff';
|
|
104
|
+
el.style.fontSize = '11px';
|
|
105
|
+
el.style.fontFamily = 'system-ui, -apple-system, sans-serif';
|
|
106
|
+
el.style.display = 'flex';
|
|
107
|
+
el.style.flexDirection = 'column';
|
|
108
|
+
el.style.gap = '2px';
|
|
109
|
+
el.style.whiteSpace = 'nowrap';
|
|
110
|
+
el.style.opacity = '0';
|
|
111
|
+
el.style.transition = 'opacity 0.12s ease';
|
|
112
|
+
const primary = targetWindow.document.createElement('div');
|
|
113
|
+
primary.setAttribute('data-vue-grab-tooltip-line', 'primary');
|
|
114
|
+
primary.style.whiteSpace = 'nowrap';
|
|
115
|
+
const secondary = targetWindow.document.createElement('div');
|
|
116
|
+
secondary.setAttribute('data-vue-grab-tooltip-line', 'secondary');
|
|
117
|
+
secondary.style.whiteSpace = 'nowrap';
|
|
118
|
+
secondary.style.color = 'rgba(255, 255, 255, 0.65)';
|
|
119
|
+
secondary.style.fontSize = '10px';
|
|
120
|
+
el.append(primary, secondary);
|
|
121
|
+
return { tooltip: el, primary, secondary };
|
|
122
|
+
}
|
|
123
|
+
function updateTooltipPosition(tooltip, rect, targetWindow) {
|
|
124
|
+
const offset = 6;
|
|
125
|
+
const top = rect.top - 24 - offset;
|
|
126
|
+
const nextTop = top >= 0 ? top : rect.bottom + offset;
|
|
127
|
+
const maxLeft = targetWindow.innerWidth - tooltip.offsetWidth - offset;
|
|
128
|
+
const maxTop = targetWindow.innerHeight - tooltip.offsetHeight - offset;
|
|
129
|
+
const left = Math.max(0, Math.min(rect.left, maxLeft));
|
|
130
|
+
const finalTop = Math.max(0, Math.min(nextTop, maxTop));
|
|
131
|
+
tooltip.style.left = `${left}px`;
|
|
132
|
+
tooltip.style.top = `${finalTop}px`;
|
|
133
|
+
}
|
|
134
|
+
function formatLocation(metadata, rootDir) {
|
|
135
|
+
if (!metadata?.file)
|
|
136
|
+
return '';
|
|
137
|
+
if (metadata.file === 'unknown')
|
|
138
|
+
return metadata.name || '';
|
|
139
|
+
const file = (() => {
|
|
140
|
+
if (rootDir && metadata.file.startsWith(rootDir)) {
|
|
141
|
+
const relative = metadata.file.slice(rootDir.length).replace(/^\/+/, '');
|
|
142
|
+
return relative || metadata.file;
|
|
143
|
+
}
|
|
144
|
+
if (metadata.file.includes('/src/'))
|
|
145
|
+
return metadata.file.split('/src/')[1];
|
|
146
|
+
return metadata.file;
|
|
147
|
+
})();
|
|
148
|
+
const line = typeof metadata.line === 'number' ? metadata.line : null;
|
|
149
|
+
const column = typeof metadata.column === 'number' ? metadata.column : null;
|
|
150
|
+
if (line !== null && column !== null) {
|
|
151
|
+
return `${file}:${line}:${column}`;
|
|
152
|
+
}
|
|
153
|
+
return file;
|
|
154
|
+
}
|
|
155
|
+
function isVueComponentProxy(value) {
|
|
156
|
+
if (!value || typeof value !== 'object')
|
|
157
|
+
return false;
|
|
158
|
+
return ('$el' in value ||
|
|
159
|
+
'$props' in value ||
|
|
160
|
+
'$data' in value ||
|
|
161
|
+
'__v_isVNode' in value ||
|
|
162
|
+
'__isVue' in value);
|
|
163
|
+
}
|
|
164
|
+
function cloneVnode(value, seen, depth) {
|
|
165
|
+
return {
|
|
166
|
+
type: safeClone(value.type, seen, depth - 1),
|
|
167
|
+
key: safeClone(value.key, seen, depth - 1),
|
|
168
|
+
props: safeClone(value.props, seen, depth - 1),
|
|
169
|
+
children: safeClone(value.children, seen, depth - 1),
|
|
170
|
+
el: safeClone(value.el, seen, depth - 1),
|
|
171
|
+
shapeFlag: safeClone(value.shapeFlag, seen, depth - 1),
|
|
172
|
+
patchFlag: safeClone(value.patchFlag, seen, depth - 1)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function safeClone(value, seen, depth) {
|
|
176
|
+
if (depth <= 0)
|
|
177
|
+
return '[DepthLimit]';
|
|
178
|
+
if (value === null || typeof value !== 'object')
|
|
179
|
+
return value;
|
|
180
|
+
if (typeof value === 'function') {
|
|
181
|
+
const name = value.name ? ` ${value.name}` : '';
|
|
182
|
+
return `[Function${name}]`;
|
|
183
|
+
}
|
|
184
|
+
if (seen.has(value))
|
|
185
|
+
return '[Circular]';
|
|
186
|
+
seen.add(value);
|
|
187
|
+
let tag = '[object Object]';
|
|
188
|
+
try {
|
|
189
|
+
tag = Object.prototype.toString.call(value);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return '[Unserializable]';
|
|
193
|
+
}
|
|
194
|
+
if (tag === '[object Window]')
|
|
195
|
+
return '[Window]';
|
|
196
|
+
if (tag === '[object Document]')
|
|
197
|
+
return '[Document]';
|
|
198
|
+
if (tag === '[object HTMLCollection]')
|
|
199
|
+
return '[HTMLCollection]';
|
|
200
|
+
if (tag === '[object NodeList]')
|
|
201
|
+
return '[NodeList]';
|
|
202
|
+
if (tag.startsWith('[object HTML'))
|
|
203
|
+
return '[HTMLElement]';
|
|
204
|
+
if (value.__v_isVNode) {
|
|
205
|
+
return cloneVnode(value, seen, depth);
|
|
206
|
+
}
|
|
207
|
+
if (isVueComponentProxy(value))
|
|
208
|
+
return '[VueComponent]';
|
|
209
|
+
if (Array.isArray(value)) {
|
|
210
|
+
return value.slice(0, 20).map((item) => safeClone(item, seen, depth - 1));
|
|
211
|
+
}
|
|
212
|
+
const result = {};
|
|
213
|
+
let keys = [];
|
|
214
|
+
try {
|
|
215
|
+
keys = Object.keys(value).slice(0, 50);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return '[Unserializable]';
|
|
219
|
+
}
|
|
220
|
+
for (const key of keys) {
|
|
221
|
+
try {
|
|
222
|
+
const item = value[key];
|
|
223
|
+
result[key] = safeClone(item, seen, depth - 1);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
result[key] = '[Unserializable]';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
function safeStringify(value) {
|
|
232
|
+
const cloned = safeClone(value ?? {}, new WeakSet(), 5);
|
|
233
|
+
return JSON.stringify(cloned, null, 2);
|
|
234
|
+
}
|
|
235
|
+
function serializeMetadata(metadata) {
|
|
236
|
+
return safeStringify(metadata);
|
|
237
|
+
}
|
|
238
|
+
async function copyToClipboard(targetWindow, text) {
|
|
239
|
+
const clipboard = targetWindow.navigator.clipboard;
|
|
240
|
+
if (clipboard?.writeText) {
|
|
241
|
+
await clipboard.writeText(text);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const textarea = targetWindow.document.createElement('textarea');
|
|
245
|
+
textarea.value = text;
|
|
246
|
+
textarea.style.position = 'fixed';
|
|
247
|
+
textarea.style.left = '-9999px';
|
|
248
|
+
targetWindow.document.body.appendChild(textarea);
|
|
249
|
+
textarea.select();
|
|
250
|
+
if (typeof targetWindow.document.execCommand === 'function') {
|
|
251
|
+
targetWindow.document.execCommand('copy');
|
|
252
|
+
}
|
|
253
|
+
textarea.remove();
|
|
254
|
+
}
|
|
255
|
+
export function createOverlayController(targetWindow, options) {
|
|
256
|
+
let overlay = null;
|
|
257
|
+
let tooltip = null;
|
|
258
|
+
let tooltipPrimary = null;
|
|
259
|
+
let tooltipSecondary = null;
|
|
260
|
+
let contextMenu = null;
|
|
261
|
+
let contextMenuTarget = null;
|
|
262
|
+
let active = false;
|
|
263
|
+
let overlayStyle = options?.overlayStyle ?? {};
|
|
264
|
+
let domFileResolver = options?.domFileResolver;
|
|
265
|
+
const ensureOverlay = () => {
|
|
266
|
+
if (!overlay) {
|
|
267
|
+
overlay = createOverlayElement(targetWindow, {
|
|
268
|
+
overlayStyle
|
|
269
|
+
});
|
|
270
|
+
targetWindow.document.body.appendChild(overlay);
|
|
271
|
+
}
|
|
272
|
+
return overlay;
|
|
273
|
+
};
|
|
274
|
+
const ensureTooltip = () => {
|
|
275
|
+
if (!tooltip) {
|
|
276
|
+
const created = createTooltipElement(targetWindow);
|
|
277
|
+
tooltip = created.tooltip;
|
|
278
|
+
tooltipPrimary = created.primary;
|
|
279
|
+
tooltipSecondary = created.secondary;
|
|
280
|
+
targetWindow.document.body.appendChild(tooltip);
|
|
281
|
+
}
|
|
282
|
+
return tooltip;
|
|
283
|
+
};
|
|
284
|
+
const setTooltipText = (primaryText, secondaryText) => {
|
|
285
|
+
ensureTooltip();
|
|
286
|
+
if (tooltipPrimary) {
|
|
287
|
+
tooltipPrimary.textContent = primaryText;
|
|
288
|
+
}
|
|
289
|
+
if (tooltipSecondary) {
|
|
290
|
+
if (secondaryText) {
|
|
291
|
+
tooltipSecondary.textContent = secondaryText;
|
|
292
|
+
tooltipSecondary.style.display = 'block';
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
tooltipSecondary.textContent = '';
|
|
296
|
+
tooltipSecondary.style.display = 'none';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const ensureContextMenu = () => {
|
|
301
|
+
if (!contextMenu) {
|
|
302
|
+
const created = createContextMenuElement(targetWindow);
|
|
303
|
+
contextMenu = created.menu;
|
|
304
|
+
created.copyItem.addEventListener('click', handleMenuCopy);
|
|
305
|
+
created.copyHtmlItem.addEventListener('click', handleMenuCopyHtml);
|
|
306
|
+
targetWindow.document.body.appendChild(contextMenu);
|
|
307
|
+
}
|
|
308
|
+
return contextMenu;
|
|
309
|
+
};
|
|
310
|
+
const hideContextMenu = () => {
|
|
311
|
+
if (!contextMenu)
|
|
312
|
+
return;
|
|
313
|
+
contextMenu.style.display = 'none';
|
|
314
|
+
contextMenuTarget = null;
|
|
315
|
+
};
|
|
316
|
+
const showContextMenu = (x, y, target) => {
|
|
317
|
+
const menu = ensureContextMenu();
|
|
318
|
+
contextMenuTarget = target;
|
|
319
|
+
menu.style.display = 'block';
|
|
320
|
+
positionContextMenu(menu, x, y, targetWindow);
|
|
321
|
+
};
|
|
322
|
+
const resolveMetadata = (el) => {
|
|
323
|
+
if (!el)
|
|
324
|
+
return null;
|
|
325
|
+
const instance = identifyComponent(el);
|
|
326
|
+
const fallback = !instance && domFileResolver ? domFileResolver(el) : null;
|
|
327
|
+
const metadata = extractMetadata(instance, el);
|
|
328
|
+
if (!metadata)
|
|
329
|
+
return null;
|
|
330
|
+
if (fallback?.file)
|
|
331
|
+
metadata.file = fallback.file;
|
|
332
|
+
if (typeof fallback?.line === 'number')
|
|
333
|
+
metadata.line = fallback.line;
|
|
334
|
+
if (typeof fallback?.column === 'number')
|
|
335
|
+
metadata.column = fallback.column;
|
|
336
|
+
return metadata;
|
|
337
|
+
};
|
|
338
|
+
const applyCopyFeedback = () => {
|
|
339
|
+
const activeTooltip = ensureTooltip();
|
|
340
|
+
setTooltipText('Copied!');
|
|
341
|
+
activeTooltip.style.background = '#27ae60';
|
|
342
|
+
};
|
|
343
|
+
const finishCopy = () => {
|
|
344
|
+
if (options?.onAfterCopy) {
|
|
345
|
+
options.onAfterCopy();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
const copyMetadataForElement = (el) => {
|
|
349
|
+
const metadata = resolveMetadata(el);
|
|
350
|
+
if (!metadata)
|
|
351
|
+
return;
|
|
352
|
+
const payload = serializeMetadata(metadata);
|
|
353
|
+
applyCopyFeedback();
|
|
354
|
+
if (options?.onCopy) {
|
|
355
|
+
options.onCopy(payload);
|
|
356
|
+
setTimeout(finishCopy, 600);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
void copyToClipboard(targetWindow, payload).then(() => {
|
|
360
|
+
setTimeout(finishCopy, 600);
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
const copyHtmlForElement = (el) => {
|
|
364
|
+
if (!el)
|
|
365
|
+
return;
|
|
366
|
+
applyCopyFeedback();
|
|
367
|
+
void copyToClipboard(targetWindow, el.outerHTML).then(() => {
|
|
368
|
+
setTimeout(finishCopy, 600);
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
const handleMove = (event) => {
|
|
372
|
+
if (isIgnoredTarget(event.target))
|
|
373
|
+
return;
|
|
374
|
+
const activeOverlay = ensureOverlay();
|
|
375
|
+
const activeTooltip = ensureTooltip();
|
|
376
|
+
const el = targetWindow.document.elementFromPoint(event.clientX, event.clientY);
|
|
377
|
+
if (!el) {
|
|
378
|
+
activeOverlay.style.width = '0';
|
|
379
|
+
activeOverlay.style.height = '0';
|
|
380
|
+
activeTooltip.style.opacity = '0';
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const rect = el.getBoundingClientRect();
|
|
384
|
+
updateOverlayPosition(activeOverlay, rect);
|
|
385
|
+
const metadata = resolveMetadata(el);
|
|
386
|
+
if (!metadata) {
|
|
387
|
+
activeTooltip.style.opacity = '0';
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const label = formatLocation(metadata, options?.rootDir);
|
|
391
|
+
if (label) {
|
|
392
|
+
setTooltipText(label, CONTEXT_MENU_HINT);
|
|
393
|
+
updateTooltipPosition(activeTooltip, rect, targetWindow);
|
|
394
|
+
activeTooltip.style.opacity = '1';
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
activeTooltip.style.opacity = '0';
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
const handleClick = (event) => {
|
|
401
|
+
if (contextMenu?.style.display === 'block') {
|
|
402
|
+
const target = event.target;
|
|
403
|
+
if (!target || !contextMenu.contains(target)) {
|
|
404
|
+
event.preventDefault();
|
|
405
|
+
event.stopPropagation();
|
|
406
|
+
hideContextMenu();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (options?.copyOnClick === false)
|
|
411
|
+
return;
|
|
412
|
+
if (isIgnoredTarget(event.target))
|
|
413
|
+
return;
|
|
414
|
+
event.preventDefault();
|
|
415
|
+
event.stopPropagation();
|
|
416
|
+
const el = event.target;
|
|
417
|
+
copyMetadataForElement(el);
|
|
418
|
+
};
|
|
419
|
+
const handleContextMenu = (event) => {
|
|
420
|
+
if (isIgnoredTarget(event.target))
|
|
421
|
+
return;
|
|
422
|
+
event.preventDefault();
|
|
423
|
+
event.stopPropagation();
|
|
424
|
+
const el = event.target;
|
|
425
|
+
if (!el)
|
|
426
|
+
return;
|
|
427
|
+
const rect = el.getBoundingClientRect();
|
|
428
|
+
updateOverlayPosition(ensureOverlay(), rect);
|
|
429
|
+
showContextMenu(event.clientX, event.clientY, el);
|
|
430
|
+
};
|
|
431
|
+
const handleMenuCopy = (event) => {
|
|
432
|
+
event.preventDefault();
|
|
433
|
+
event.stopPropagation();
|
|
434
|
+
const target = contextMenuTarget;
|
|
435
|
+
hideContextMenu();
|
|
436
|
+
copyMetadataForElement(target);
|
|
437
|
+
};
|
|
438
|
+
const handleMenuCopyHtml = (event) => {
|
|
439
|
+
event.preventDefault();
|
|
440
|
+
event.stopPropagation();
|
|
441
|
+
const target = contextMenuTarget;
|
|
442
|
+
hideContextMenu();
|
|
443
|
+
copyHtmlForElement(target);
|
|
444
|
+
};
|
|
445
|
+
const handleEscape = (event) => {
|
|
446
|
+
if (event.key !== 'Escape')
|
|
447
|
+
return;
|
|
448
|
+
hideContextMenu();
|
|
449
|
+
};
|
|
450
|
+
return {
|
|
451
|
+
start() {
|
|
452
|
+
if (active)
|
|
453
|
+
return;
|
|
454
|
+
active = true;
|
|
455
|
+
ensureOverlay();
|
|
456
|
+
targetWindow.document.addEventListener('mousemove', handleMove);
|
|
457
|
+
targetWindow.document.addEventListener('click', handleClick, true);
|
|
458
|
+
targetWindow.document.addEventListener('contextmenu', handleContextMenu, true);
|
|
459
|
+
targetWindow.document.addEventListener('keydown', handleEscape, true);
|
|
460
|
+
},
|
|
461
|
+
stop() {
|
|
462
|
+
if (!active)
|
|
463
|
+
return;
|
|
464
|
+
active = false;
|
|
465
|
+
targetWindow.document.removeEventListener('mousemove', handleMove);
|
|
466
|
+
targetWindow.document.removeEventListener('click', handleClick, true);
|
|
467
|
+
targetWindow.document.removeEventListener('contextmenu', handleContextMenu, true);
|
|
468
|
+
targetWindow.document.removeEventListener('keydown', handleEscape, true);
|
|
469
|
+
overlay?.remove();
|
|
470
|
+
overlay = null;
|
|
471
|
+
tooltip?.remove();
|
|
472
|
+
tooltip = null;
|
|
473
|
+
tooltipPrimary = null;
|
|
474
|
+
tooltipSecondary = null;
|
|
475
|
+
contextMenu?.remove();
|
|
476
|
+
contextMenu = null;
|
|
477
|
+
contextMenuTarget = null;
|
|
478
|
+
},
|
|
479
|
+
isActive() {
|
|
480
|
+
return active;
|
|
481
|
+
},
|
|
482
|
+
highlight(el) {
|
|
483
|
+
if (!el) {
|
|
484
|
+
this.clear();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const activeOverlay = ensureOverlay();
|
|
488
|
+
const rect = el.getBoundingClientRect();
|
|
489
|
+
updateOverlayPosition(activeOverlay, rect);
|
|
490
|
+
},
|
|
491
|
+
clear() {
|
|
492
|
+
if (!overlay)
|
|
493
|
+
return;
|
|
494
|
+
overlay.style.width = '0';
|
|
495
|
+
overlay.style.height = '0';
|
|
496
|
+
},
|
|
497
|
+
setStyle(style) {
|
|
498
|
+
overlayStyle = style;
|
|
499
|
+
if (!overlay)
|
|
500
|
+
return;
|
|
501
|
+
for (const [key, value] of Object.entries(style)) {
|
|
502
|
+
overlay.style[key] = value;
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
setDomFileResolver(resolver) {
|
|
506
|
+
domFileResolver = resolver;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type ToggleWidgetOptions = {
|
|
2
|
+
onToggle: (nextActive: boolean) => void;
|
|
3
|
+
};
|
|
4
|
+
type ToggleWidgetController = {
|
|
5
|
+
mount: () => void;
|
|
6
|
+
unmount: () => void;
|
|
7
|
+
setActive: (active: boolean) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function createToggleWidget(targetWindow: Window, options: ToggleWidgetOptions): ToggleWidgetController;
|
|
10
|
+
export {};
|