@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
package/src/core/api.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { extractMetadata, identifyComponent } from './identifier';
|
|
2
|
-
import { createOverlayController } from './overlay';
|
|
3
|
-
import type { OverlayOptions } from './overlay';
|
|
4
|
-
import { createToggleWidget } from './widget';
|
|
5
|
-
|
|
6
|
-
export interface ComponentInfo {
|
|
7
|
-
name: string;
|
|
8
|
-
file: string;
|
|
9
|
-
props: Record<string, any>;
|
|
10
|
-
data: Record<string, any>;
|
|
11
|
-
element: HTMLElement;
|
|
12
|
-
line?: number;
|
|
13
|
-
column?: number;
|
|
14
|
-
vnode?: any;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export type VueGrabOptions = OverlayOptions;
|
|
18
|
-
|
|
19
|
-
export interface VueGrabAPI {
|
|
20
|
-
activate(): void;
|
|
21
|
-
deactivate(): void;
|
|
22
|
-
readonly isActive: boolean;
|
|
23
|
-
grabAt(x: number, y: number): ComponentInfo | null;
|
|
24
|
-
grabFromSelector(selector: string): ComponentInfo | null;
|
|
25
|
-
grabFromElement(element: Element): ComponentInfo | null;
|
|
26
|
-
highlight(selector: string): void;
|
|
27
|
-
enable(): void;
|
|
28
|
-
disable(): void;
|
|
29
|
-
getComponentDetails(selectorOrElement: string | Element): ComponentInfo | null;
|
|
30
|
-
setOverlayStyle(style: Record<string, string>): void;
|
|
31
|
-
setDomFileResolver(
|
|
32
|
-
resolver: VueGrabOptions['domFileResolver']
|
|
33
|
-
): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function getComponentInfo(
|
|
37
|
-
el: HTMLElement | null,
|
|
38
|
-
resolver?: VueGrabOptions['domFileResolver']
|
|
39
|
-
): ComponentInfo | null {
|
|
40
|
-
if (!el) return null;
|
|
41
|
-
const instance = identifyComponent(el);
|
|
42
|
-
const fallback = !instance && resolver ? resolver(el) : null;
|
|
43
|
-
const metadata = extractMetadata(instance, el);
|
|
44
|
-
if (fallback?.file) metadata.file = fallback.file;
|
|
45
|
-
if (typeof fallback?.line === 'number') metadata.line = fallback.line;
|
|
46
|
-
if (typeof fallback?.column === 'number') metadata.column = fallback.column;
|
|
47
|
-
if (!metadata) return null;
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
...metadata,
|
|
51
|
-
element: el
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function createVueGrabAPI(
|
|
56
|
-
targetWindow: Window,
|
|
57
|
-
options: VueGrabOptions = {}
|
|
58
|
-
): VueGrabAPI {
|
|
59
|
-
let active = false;
|
|
60
|
-
let domFileResolver = options.domFileResolver ?? null;
|
|
61
|
-
const overlay = createOverlayController(targetWindow, {
|
|
62
|
-
...options,
|
|
63
|
-
onAfterCopy: () => {
|
|
64
|
-
api.deactivate();
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
const widget = createToggleWidget(targetWindow, {
|
|
68
|
-
onToggle(nextActive) {
|
|
69
|
-
if (nextActive) {
|
|
70
|
-
api.activate();
|
|
71
|
-
} else {
|
|
72
|
-
api.deactivate();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const api: VueGrabAPI = {
|
|
78
|
-
activate() {
|
|
79
|
-
active = true;
|
|
80
|
-
overlay.start();
|
|
81
|
-
widget.setActive(true);
|
|
82
|
-
},
|
|
83
|
-
deactivate() {
|
|
84
|
-
active = false;
|
|
85
|
-
overlay.stop();
|
|
86
|
-
widget.setActive(false);
|
|
87
|
-
},
|
|
88
|
-
get isActive() {
|
|
89
|
-
return active;
|
|
90
|
-
},
|
|
91
|
-
grabAt(x: number, y: number) {
|
|
92
|
-
if (typeof targetWindow.document.elementFromPoint !== 'function') return null;
|
|
93
|
-
const el = targetWindow.document.elementFromPoint(x, y) as HTMLElement | null;
|
|
94
|
-
return getComponentInfo(el, domFileResolver);
|
|
95
|
-
},
|
|
96
|
-
grabFromSelector(selector: string) {
|
|
97
|
-
const el = targetWindow.document.querySelector(selector) as HTMLElement | null;
|
|
98
|
-
return getComponentInfo(el, domFileResolver);
|
|
99
|
-
},
|
|
100
|
-
grabFromElement(element: Element) {
|
|
101
|
-
return getComponentInfo(element as HTMLElement, domFileResolver);
|
|
102
|
-
},
|
|
103
|
-
highlight(selector: string) {
|
|
104
|
-
const el = targetWindow.document.querySelector(selector) as HTMLElement | null;
|
|
105
|
-
overlay.highlight(el);
|
|
106
|
-
},
|
|
107
|
-
enable() {
|
|
108
|
-
this.activate();
|
|
109
|
-
},
|
|
110
|
-
disable() {
|
|
111
|
-
this.deactivate();
|
|
112
|
-
},
|
|
113
|
-
getComponentDetails(selectorOrElement: string | Element) {
|
|
114
|
-
if (typeof selectorOrElement === 'string') {
|
|
115
|
-
const el = targetWindow.document.querySelector(selectorOrElement) as
|
|
116
|
-
| HTMLElement
|
|
117
|
-
| null;
|
|
118
|
-
return getComponentInfo(el, domFileResolver);
|
|
119
|
-
}
|
|
120
|
-
return getComponentInfo(selectorOrElement as HTMLElement, domFileResolver);
|
|
121
|
-
},
|
|
122
|
-
setOverlayStyle(style: Record<string, string>) {
|
|
123
|
-
overlay.setStyle(style);
|
|
124
|
-
},
|
|
125
|
-
setDomFileResolver(resolver: VueGrabOptions['domFileResolver']) {
|
|
126
|
-
domFileResolver = resolver ?? null;
|
|
127
|
-
overlay.setDomFileResolver(domFileResolver);
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
widget.mount();
|
|
132
|
-
widget.setActive(active);
|
|
133
|
-
|
|
134
|
-
return api;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function installVueGrab(targetWindow: Window, options: VueGrabOptions = {}) {
|
|
138
|
-
const existing = (targetWindow as any).__VUE_GRAB__ as VueGrabAPI | undefined;
|
|
139
|
-
if (existing) return existing;
|
|
140
|
-
|
|
141
|
-
const api = createVueGrabAPI(targetWindow, options);
|
|
142
|
-
(targetWindow as any).__VUE_GRAB__ = api;
|
|
143
|
-
return api;
|
|
144
|
-
}
|
package/src/core/identifier.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
export function identifyComponent(el: HTMLElement | null): any {
|
|
2
|
-
let curr = el;
|
|
3
|
-
while (curr) {
|
|
4
|
-
// Vue 3 stores the internal component instance on the DOM element
|
|
5
|
-
// under specific keys depending on the version/environment.
|
|
6
|
-
const currAny = curr as any;
|
|
7
|
-
const instance =
|
|
8
|
-
currAny.__vueParentComponent ||
|
|
9
|
-
currAny.__vnode?.component ||
|
|
10
|
-
currAny.__vnode?.ctx ||
|
|
11
|
-
currAny.__vue__;
|
|
12
|
-
if (instance) return instance;
|
|
13
|
-
curr = curr.parentElement;
|
|
14
|
-
}
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function extractMetadata(instance: any, el?: HTMLElement | null) {
|
|
19
|
-
if (!instance && !el) return null;
|
|
20
|
-
|
|
21
|
-
const resolveType = (start: any) => {
|
|
22
|
-
let curr = start;
|
|
23
|
-
while (curr) {
|
|
24
|
-
const t = curr.type || curr.$options || curr.type?.__vccOpts || {};
|
|
25
|
-
const file = t.__file || t.__vccOpts?.__file;
|
|
26
|
-
if (file) {
|
|
27
|
-
return { type: t, instance: curr };
|
|
28
|
-
}
|
|
29
|
-
curr = curr.parent;
|
|
30
|
-
}
|
|
31
|
-
return { type: start.type || start.$options || start.type?.__vccOpts || {}, instance: start };
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const resolved = instance ? resolveType(instance) : { type: {}, instance: null };
|
|
35
|
-
const type = resolved.type || {};
|
|
36
|
-
const props = instance?.props || instance?.$props || {};
|
|
37
|
-
const data =
|
|
38
|
-
(instance?.data && Object.keys(instance.data).length > 0
|
|
39
|
-
? instance.data
|
|
40
|
-
: instance?.setupState) ||
|
|
41
|
-
instance?.$data ||
|
|
42
|
-
{};
|
|
43
|
-
const vnode = instance?.vnode || instance?.$vnode;
|
|
44
|
-
const loc =
|
|
45
|
-
vnode?.loc?.start ||
|
|
46
|
-
resolved.instance?.vnode?.loc?.start ||
|
|
47
|
-
resolved.instance?.parent?.vnode?.loc?.start;
|
|
48
|
-
|
|
49
|
-
const metadata: Record<string, any> = instance
|
|
50
|
-
? {
|
|
51
|
-
name: type.name || type.__name || type.__vccOpts?.name || 'AnonymousComponent',
|
|
52
|
-
file: type.__file || type.__vccOpts?.__file || 'unknown',
|
|
53
|
-
props,
|
|
54
|
-
data
|
|
55
|
-
}
|
|
56
|
-
: {
|
|
57
|
-
name: el?.tagName ? `<${el.tagName.toLowerCase()}>` : 'unknown',
|
|
58
|
-
file: 'unknown',
|
|
59
|
-
props: {},
|
|
60
|
-
data: {},
|
|
61
|
-
element: el || null
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
if (typeof loc?.line === 'number') {
|
|
65
|
-
metadata.line = loc.line;
|
|
66
|
-
} else if (typeof type.__line === 'number') {
|
|
67
|
-
metadata.line = type.__line;
|
|
68
|
-
} else if (typeof type.line === 'number') {
|
|
69
|
-
metadata.line = type.line;
|
|
70
|
-
} else if (typeof type.__vccOpts?.__line === 'number') {
|
|
71
|
-
metadata.line = type.__vccOpts.__line;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (typeof loc?.column === 'number') {
|
|
75
|
-
metadata.column = loc.column;
|
|
76
|
-
} else if (typeof type.__column === 'number') {
|
|
77
|
-
metadata.column = type.__column;
|
|
78
|
-
} else if (typeof type.column === 'number') {
|
|
79
|
-
metadata.column = type.column;
|
|
80
|
-
} else if (typeof type.__vccOpts?.__column === 'number') {
|
|
81
|
-
metadata.column = type.__vccOpts.__column;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (vnode) {
|
|
85
|
-
metadata.vnode = vnode;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return metadata;
|
|
89
|
-
}
|
package/src/core/overlay.ts
DELETED
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
import { extractMetadata, identifyComponent } from './identifier';
|
|
2
|
-
|
|
3
|
-
type OverlayController = {
|
|
4
|
-
start: () => void;
|
|
5
|
-
stop: () => void;
|
|
6
|
-
isActive: () => boolean;
|
|
7
|
-
highlight: (el: HTMLElement | null) => void;
|
|
8
|
-
clear: () => void;
|
|
9
|
-
setStyle: (style: OverlayStyle) => void;
|
|
10
|
-
setDomFileResolver: (resolver: OverlayOptions['domFileResolver']) => void;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export type OverlayStyle = Record<string, string>;
|
|
14
|
-
|
|
15
|
-
export type OverlayOptions = {
|
|
16
|
-
overlayStyle?: OverlayStyle;
|
|
17
|
-
onCopy?: (payload: string) => void;
|
|
18
|
-
onAfterCopy?: () => void;
|
|
19
|
-
copyOnClick?: boolean;
|
|
20
|
-
rootDir?: string;
|
|
21
|
-
domFileResolver?: (el: HTMLElement) => {
|
|
22
|
-
file?: string;
|
|
23
|
-
line?: number;
|
|
24
|
-
column?: number;
|
|
25
|
-
} | null;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
function createOverlayElement(targetWindow: Window, options?: OverlayOptions) {
|
|
29
|
-
const el = targetWindow.document.createElement('div');
|
|
30
|
-
el.setAttribute('data-vue-grab-overlay', 'true');
|
|
31
|
-
el.style.position = 'fixed';
|
|
32
|
-
el.style.pointerEvents = 'none';
|
|
33
|
-
el.style.zIndex = '2147483647';
|
|
34
|
-
el.style.border = '2px solid #e67e22';
|
|
35
|
-
el.style.background = 'rgba(230, 126, 34, 0.08)';
|
|
36
|
-
el.style.top = '0';
|
|
37
|
-
el.style.left = '0';
|
|
38
|
-
el.style.width = '0';
|
|
39
|
-
el.style.height = '0';
|
|
40
|
-
if (options?.overlayStyle) {
|
|
41
|
-
for (const [key, value] of Object.entries(options.overlayStyle)) {
|
|
42
|
-
(el.style as any)[key] = value;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return el;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function updateOverlayPosition(overlay: HTMLDivElement, rect: DOMRect) {
|
|
49
|
-
overlay.style.top = `${rect.top}px`;
|
|
50
|
-
overlay.style.left = `${rect.left}px`;
|
|
51
|
-
overlay.style.width = `${rect.width}px`;
|
|
52
|
-
overlay.style.height = `${rect.height}px`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function createTooltipElement(targetWindow: Window) {
|
|
56
|
-
const el = targetWindow.document.createElement('div');
|
|
57
|
-
el.setAttribute('data-vue-grab-tooltip', 'true');
|
|
58
|
-
el.style.position = 'fixed';
|
|
59
|
-
el.style.pointerEvents = 'none';
|
|
60
|
-
el.style.zIndex = '2147483647';
|
|
61
|
-
el.style.padding = '4px 8px';
|
|
62
|
-
el.style.borderRadius = '6px';
|
|
63
|
-
el.style.background = '#111';
|
|
64
|
-
el.style.color = '#fff';
|
|
65
|
-
el.style.fontSize = '11px';
|
|
66
|
-
el.style.fontFamily = 'system-ui, -apple-system, sans-serif';
|
|
67
|
-
el.style.whiteSpace = 'nowrap';
|
|
68
|
-
el.style.opacity = '0';
|
|
69
|
-
el.style.transition = 'opacity 0.12s ease';
|
|
70
|
-
return el;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function updateTooltipPosition(
|
|
74
|
-
tooltip: HTMLDivElement,
|
|
75
|
-
rect: DOMRect,
|
|
76
|
-
targetWindow: Window
|
|
77
|
-
) {
|
|
78
|
-
const offset = 6;
|
|
79
|
-
const top = rect.top - 24 - offset;
|
|
80
|
-
const nextTop = top >= 0 ? top : rect.bottom + offset;
|
|
81
|
-
const maxLeft = targetWindow.innerWidth - tooltip.offsetWidth - offset;
|
|
82
|
-
const maxTop = targetWindow.innerHeight - tooltip.offsetHeight - offset;
|
|
83
|
-
const left = Math.max(0, Math.min(rect.left, maxLeft));
|
|
84
|
-
const finalTop = Math.max(0, Math.min(nextTop, maxTop));
|
|
85
|
-
tooltip.style.left = `${left}px`;
|
|
86
|
-
tooltip.style.top = `${finalTop}px`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function formatLocation(metadata: ReturnType<typeof extractMetadata>, rootDir?: string) {
|
|
90
|
-
if (!metadata?.file) return '';
|
|
91
|
-
if (metadata.file === 'unknown') return metadata.name || '';
|
|
92
|
-
const file = (() => {
|
|
93
|
-
if (rootDir && metadata.file.startsWith(rootDir)) {
|
|
94
|
-
const relative = metadata.file.slice(rootDir.length).replace(/^\/+/, '');
|
|
95
|
-
return relative || metadata.file;
|
|
96
|
-
}
|
|
97
|
-
if (metadata.file.includes('/src/')) return metadata.file.split('/src/')[1];
|
|
98
|
-
return metadata.file;
|
|
99
|
-
})();
|
|
100
|
-
const line = typeof metadata.line === 'number' ? metadata.line : null;
|
|
101
|
-
const column = typeof metadata.column === 'number' ? metadata.column : null;
|
|
102
|
-
if (line !== null && column !== null) {
|
|
103
|
-
return `${file}:${line}:${column}`;
|
|
104
|
-
}
|
|
105
|
-
return file;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function isVueComponentProxy(value: unknown) {
|
|
109
|
-
if (!value || typeof value !== 'object') return false;
|
|
110
|
-
return (
|
|
111
|
-
'$el' in value ||
|
|
112
|
-
'$props' in value ||
|
|
113
|
-
'$data' in value ||
|
|
114
|
-
'__v_isVNode' in value ||
|
|
115
|
-
'__isVue' in value
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function cloneVnode(value: Record<string, unknown>, seen: WeakSet<object>, depth: number) {
|
|
120
|
-
return {
|
|
121
|
-
type: safeClone(value.type, seen, depth - 1),
|
|
122
|
-
key: safeClone(value.key, seen, depth - 1),
|
|
123
|
-
props: safeClone(value.props, seen, depth - 1),
|
|
124
|
-
children: safeClone(value.children, seen, depth - 1),
|
|
125
|
-
el: safeClone(value.el, seen, depth - 1),
|
|
126
|
-
shapeFlag: safeClone(value.shapeFlag, seen, depth - 1),
|
|
127
|
-
patchFlag: safeClone(value.patchFlag, seen, depth - 1)
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function safeClone(value: unknown, seen: WeakSet<object>, depth: number): unknown {
|
|
132
|
-
if (depth <= 0) return '[DepthLimit]';
|
|
133
|
-
if (value === null || typeof value !== 'object') return value;
|
|
134
|
-
if (typeof value === 'function') {
|
|
135
|
-
const name = value.name ? ` ${value.name}` : '';
|
|
136
|
-
return `[Function${name}]`;
|
|
137
|
-
}
|
|
138
|
-
if (seen.has(value)) return '[Circular]';
|
|
139
|
-
seen.add(value);
|
|
140
|
-
|
|
141
|
-
let tag = '[object Object]';
|
|
142
|
-
try {
|
|
143
|
-
tag = Object.prototype.toString.call(value);
|
|
144
|
-
} catch {
|
|
145
|
-
return '[Unserializable]';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (tag === '[object Window]') return '[Window]';
|
|
149
|
-
if (tag === '[object Document]') return '[Document]';
|
|
150
|
-
if (tag === '[object HTMLCollection]') return '[HTMLCollection]';
|
|
151
|
-
if (tag === '[object NodeList]') return '[NodeList]';
|
|
152
|
-
if (tag.startsWith('[object HTML')) return '[HTMLElement]';
|
|
153
|
-
if ((value as Record<string, unknown>).__v_isVNode) {
|
|
154
|
-
return cloneVnode(value as Record<string, unknown>, seen, depth);
|
|
155
|
-
}
|
|
156
|
-
if (isVueComponentProxy(value)) return '[VueComponent]';
|
|
157
|
-
|
|
158
|
-
if (Array.isArray(value)) {
|
|
159
|
-
return value.slice(0, 20).map((item) => safeClone(item, seen, depth - 1));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const result: Record<string, unknown> = {};
|
|
163
|
-
let keys: string[] = [];
|
|
164
|
-
try {
|
|
165
|
-
keys = Object.keys(value as object).slice(0, 50);
|
|
166
|
-
} catch {
|
|
167
|
-
return '[Unserializable]';
|
|
168
|
-
}
|
|
169
|
-
for (const key of keys) {
|
|
170
|
-
try {
|
|
171
|
-
const item = (value as Record<string, unknown>)[key];
|
|
172
|
-
result[key] = safeClone(item, seen, depth - 1);
|
|
173
|
-
} catch {
|
|
174
|
-
result[key] = '[Unserializable]';
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return result;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function safeStringify(value: unknown) {
|
|
181
|
-
const cloned = safeClone(value ?? {}, new WeakSet(), 5);
|
|
182
|
-
return JSON.stringify(cloned, null, 2);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function serializeMetadata(metadata: ReturnType<typeof extractMetadata>) {
|
|
186
|
-
return safeStringify(metadata);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function copyToClipboard(targetWindow: Window, text: string) {
|
|
190
|
-
const clipboard = targetWindow.navigator.clipboard;
|
|
191
|
-
if (clipboard?.writeText) {
|
|
192
|
-
await clipboard.writeText(text);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const textarea = targetWindow.document.createElement('textarea');
|
|
197
|
-
textarea.value = text;
|
|
198
|
-
textarea.style.position = 'fixed';
|
|
199
|
-
textarea.style.left = '-9999px';
|
|
200
|
-
targetWindow.document.body.appendChild(textarea);
|
|
201
|
-
textarea.select();
|
|
202
|
-
if (typeof targetWindow.document.execCommand === 'function') {
|
|
203
|
-
targetWindow.document.execCommand('copy');
|
|
204
|
-
}
|
|
205
|
-
textarea.remove();
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function createOverlayController(
|
|
209
|
-
targetWindow: Window,
|
|
210
|
-
options?: OverlayOptions
|
|
211
|
-
): OverlayController {
|
|
212
|
-
let overlay: HTMLDivElement | null = null;
|
|
213
|
-
let tooltip: HTMLDivElement | null = null;
|
|
214
|
-
let active = false;
|
|
215
|
-
let overlayStyle: OverlayStyle = options?.overlayStyle ?? {};
|
|
216
|
-
let domFileResolver = options?.domFileResolver ?? null;
|
|
217
|
-
|
|
218
|
-
const ensureOverlay = () => {
|
|
219
|
-
if (!overlay) {
|
|
220
|
-
overlay = createOverlayElement(targetWindow, {
|
|
221
|
-
overlayStyle
|
|
222
|
-
});
|
|
223
|
-
targetWindow.document.body.appendChild(overlay);
|
|
224
|
-
}
|
|
225
|
-
return overlay;
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const ensureTooltip = () => {
|
|
229
|
-
if (!tooltip) {
|
|
230
|
-
tooltip = createTooltipElement(targetWindow);
|
|
231
|
-
targetWindow.document.body.appendChild(tooltip);
|
|
232
|
-
}
|
|
233
|
-
return tooltip;
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const handleMove = (event: MouseEvent) => {
|
|
237
|
-
const activeOverlay = ensureOverlay();
|
|
238
|
-
const activeTooltip = ensureTooltip();
|
|
239
|
-
const el = targetWindow.document.elementFromPoint(event.clientX, event.clientY) as
|
|
240
|
-
| HTMLElement
|
|
241
|
-
| null;
|
|
242
|
-
if (!el) {
|
|
243
|
-
activeOverlay.style.width = '0';
|
|
244
|
-
activeOverlay.style.height = '0';
|
|
245
|
-
activeTooltip.style.opacity = '0';
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const rect = el.getBoundingClientRect();
|
|
249
|
-
updateOverlayPosition(activeOverlay, rect);
|
|
250
|
-
|
|
251
|
-
const instance = identifyComponent(el);
|
|
252
|
-
const fallback = !instance && domFileResolver ? domFileResolver(el) : null;
|
|
253
|
-
const metadata = extractMetadata(instance, el);
|
|
254
|
-
if (fallback?.file) metadata.file = fallback.file;
|
|
255
|
-
if (typeof fallback?.line === 'number') metadata.line = fallback.line;
|
|
256
|
-
if (typeof fallback?.column === 'number') metadata.column = fallback.column;
|
|
257
|
-
const label = formatLocation(metadata, options?.rootDir);
|
|
258
|
-
if (label) {
|
|
259
|
-
activeTooltip.textContent = label;
|
|
260
|
-
updateTooltipPosition(activeTooltip, rect, targetWindow);
|
|
261
|
-
activeTooltip.style.opacity = '1';
|
|
262
|
-
} else {
|
|
263
|
-
activeTooltip.style.opacity = '0';
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const handleClick = (event: MouseEvent) => {
|
|
268
|
-
if (options?.copyOnClick === false) return;
|
|
269
|
-
event.preventDefault();
|
|
270
|
-
event.stopPropagation();
|
|
271
|
-
const el = event.target as HTMLElement | null;
|
|
272
|
-
const instance = identifyComponent(el);
|
|
273
|
-
const fallback = !instance && domFileResolver ? domFileResolver(el) : null;
|
|
274
|
-
const metadata = extractMetadata(instance, el);
|
|
275
|
-
if (fallback?.file) metadata.file = fallback.file;
|
|
276
|
-
if (typeof fallback?.line === 'number') metadata.line = fallback.line;
|
|
277
|
-
if (typeof fallback?.column === 'number') metadata.column = fallback.column;
|
|
278
|
-
const payload = serializeMetadata(metadata);
|
|
279
|
-
|
|
280
|
-
const activeTooltip = ensureTooltip();
|
|
281
|
-
const originalText = activeTooltip.textContent;
|
|
282
|
-
activeTooltip.textContent = 'Copied!';
|
|
283
|
-
activeTooltip.style.background = '#27ae60';
|
|
284
|
-
|
|
285
|
-
const finish = () => {
|
|
286
|
-
if (options?.onAfterCopy) {
|
|
287
|
-
options.onAfterCopy();
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
if (options?.onCopy) {
|
|
292
|
-
options.onCopy(payload);
|
|
293
|
-
setTimeout(finish, 600);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
void copyToClipboard(targetWindow, payload).then(() => {
|
|
298
|
-
setTimeout(finish, 600);
|
|
299
|
-
});
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
start() {
|
|
304
|
-
if (active) return;
|
|
305
|
-
active = true;
|
|
306
|
-
ensureOverlay();
|
|
307
|
-
targetWindow.document.addEventListener('mousemove', handleMove);
|
|
308
|
-
targetWindow.document.addEventListener('click', handleClick, true);
|
|
309
|
-
},
|
|
310
|
-
stop() {
|
|
311
|
-
if (!active) return;
|
|
312
|
-
active = false;
|
|
313
|
-
targetWindow.document.removeEventListener('mousemove', handleMove);
|
|
314
|
-
targetWindow.document.removeEventListener('click', handleClick, true);
|
|
315
|
-
overlay?.remove();
|
|
316
|
-
overlay = null;
|
|
317
|
-
tooltip?.remove();
|
|
318
|
-
tooltip = null;
|
|
319
|
-
},
|
|
320
|
-
isActive() {
|
|
321
|
-
return active;
|
|
322
|
-
},
|
|
323
|
-
highlight(el: HTMLElement | null) {
|
|
324
|
-
if (!el) {
|
|
325
|
-
this.clear();
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const activeOverlay = ensureOverlay();
|
|
329
|
-
const rect = el.getBoundingClientRect();
|
|
330
|
-
updateOverlayPosition(activeOverlay, rect);
|
|
331
|
-
},
|
|
332
|
-
clear() {
|
|
333
|
-
if (!overlay) return;
|
|
334
|
-
overlay.style.width = '0';
|
|
335
|
-
overlay.style.height = '0';
|
|
336
|
-
},
|
|
337
|
-
setStyle(style: OverlayStyle) {
|
|
338
|
-
overlayStyle = style;
|
|
339
|
-
if (!overlay) return;
|
|
340
|
-
for (const [key, value] of Object.entries(style)) {
|
|
341
|
-
(overlay.style as any)[key] = value;
|
|
342
|
-
}
|
|
343
|
-
},
|
|
344
|
-
setDomFileResolver(resolver: OverlayOptions['domFileResolver']) {
|
|
345
|
-
domFileResolver = resolver ?? null;
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
}
|