@stsgs1980/fab-inspector 3.4.1 → 3.5.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/README.md +27 -0
- package/dist/api-source-route.d.ts +14 -0
- package/dist/api-source-route.d.ts.map +1 -0
- package/dist/api-source-route.js +54 -0
- package/dist/api-source-route.js.map +1 -0
- package/dist/box-model-section.d.ts +5 -0
- package/dist/box-model-section.d.ts.map +1 -0
- package/dist/box-model-section.js +15 -0
- package/dist/box-model-section.js.map +1 -0
- package/dist/highlight-overlay.d.ts +4 -0
- package/dist/highlight-overlay.d.ts.map +1 -0
- package/dist/highlight-overlay.js +14 -0
- package/dist/highlight-overlay.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/inspector-fab.d.ts +6 -0
- package/dist/inspector-fab.d.ts.map +1 -0
- package/dist/inspector-fab.js +14 -0
- package/dist/inspector-fab.js.map +1 -0
- package/dist/inspector-panel.d.ts +14 -0
- package/dist/inspector-panel.d.ts.map +1 -0
- package/dist/inspector-panel.js +36 -0
- package/dist/inspector-panel.js.map +1 -0
- package/dist/panel-sections.d.ts +25 -0
- package/dist/panel-sections.d.ts.map +1 -0
- package/dist/panel-sections.js +48 -0
- package/dist/panel-sections.js.map +1 -0
- package/dist/plugins/data-src-plugin.d.ts +38 -0
- package/dist/plugins/data-src-plugin.d.ts.map +1 -0
- package/dist/plugins/data-src-plugin.js +90 -0
- package/dist/plugins/data-src-plugin.js.map +1 -0
- package/dist/select-element-fab.d.ts +2 -0
- package/dist/select-element-fab.d.ts.map +1 -0
- package/{select-element-fab.tsx → dist/select-element-fab.js} +8 -45
- package/dist/select-element-fab.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/use-element-inspector.d.ts +19 -0
- package/dist/use-element-inspector.d.ts.map +1 -0
- package/dist/use-element-inspector.js +239 -0
- package/dist/use-element-inspector.js.map +1 -0
- package/dist/use-panel-drag.d.ts +11 -0
- package/dist/use-panel-drag.d.ts.map +1 -0
- package/dist/use-panel-drag.js +35 -0
- package/dist/use-panel-drag.js.map +1 -0
- package/package.json +25 -9
- package/api-source-route.ts +0 -60
- package/box-model-section.tsx +0 -97
- package/highlight-overlay.tsx +0 -23
- package/index.ts +0 -10
- package/inspector-fab.tsx +0 -63
- package/inspector-panel.tsx +0 -190
- package/panel-sections.tsx +0 -207
- package/plugins/data-src-plugin.ts +0 -110
- package/types.ts +0 -45
- package/use-element-inspector.ts +0 -264
- package/use-panel-drag.ts +0 -47
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* data-src Injector Plugin for Turbopack (Next.js 15.3+)
|
|
3
|
-
*
|
|
4
|
-
* Автоматически добавляет data-src="file:line" атрибут на JSX-элементы.
|
|
5
|
-
*
|
|
6
|
-
* Подключение в next.config.ts:
|
|
7
|
-
* import { dataSrcPlugin } from './src/components/inspector/plugins/data-src-plugin';
|
|
8
|
-
*
|
|
9
|
-
* const nextConfig = {
|
|
10
|
-
* experimental: {
|
|
11
|
-
* turbo: {
|
|
12
|
-
* plugins: [dataSrcPlugin()],
|
|
13
|
-
* },
|
|
14
|
-
* },
|
|
15
|
-
* };
|
|
16
|
-
*
|
|
17
|
-
* Пропускает: node_modules, inspector/, строки с комментариями и декларациями.
|
|
18
|
-
* Только dev-режим (Next.js не вызывает turbo plugins в production build).
|
|
19
|
-
*
|
|
20
|
-
* ⚠️ API экспериментальное и может измениться в следующих версиях Next.js.
|
|
21
|
-
* При проблемах — добавляйте data-src вручную.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
type TransformResult = { code: string };
|
|
25
|
-
|
|
26
|
-
export const dataSrcPlugin = () => ({
|
|
27
|
-
name: 'data-src-injector',
|
|
28
|
-
|
|
29
|
-
setup(api: {
|
|
30
|
-
transform: (
|
|
31
|
-
opts: { filter: RegExp },
|
|
32
|
-
handler: (args: { code: string; resource: string }) => TransformResult,
|
|
33
|
-
) => void;
|
|
34
|
-
}) {
|
|
35
|
-
api.transform({ filter: /\.(tsx|jsx)$/ }, ({ code, resource }) => {
|
|
36
|
-
if (
|
|
37
|
-
resource.includes('node_modules') ||
|
|
38
|
-
resource.includes('/inspector/')
|
|
39
|
-
) {
|
|
40
|
-
return { code };
|
|
41
|
-
}
|
|
42
|
-
return injectDataSrc(code, resource);
|
|
43
|
-
});
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
function injectDataSrc(code: string, resource: string): TransformResult {
|
|
48
|
-
const lines = code.split('\n');
|
|
49
|
-
const relativePath = toRelativePath(resource);
|
|
50
|
-
const result: string[] = [];
|
|
51
|
-
|
|
52
|
-
for (let i = 0; i < lines.length; i++) {
|
|
53
|
-
const line = lines[i];
|
|
54
|
-
const lineNum = i + 1;
|
|
55
|
-
|
|
56
|
-
if (isJsxOpeningTag(line) && !hasDataSrc(line)) {
|
|
57
|
-
result.push(injectOnLine(line, relativePath, lineNum));
|
|
58
|
-
} else {
|
|
59
|
-
result.push(line);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { code: result.join('\n') };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function injectOnLine(line: string, path: string, lineNum: number): string {
|
|
67
|
-
const attr = ` data-src="${path}:${lineNum}"`;
|
|
68
|
-
return line.replace(
|
|
69
|
-
/(<[A-Za-z][A-Za-z0-9-]*)(\s|>)/,
|
|
70
|
-
(_match, tag: string, after: string) => {
|
|
71
|
-
if (after === '>') return `${tag}${attr}>`;
|
|
72
|
-
return `${tag}${attr} `;
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function isJsxOpeningTag(line: string): boolean {
|
|
78
|
-
const t = line.trimStart();
|
|
79
|
-
if (
|
|
80
|
-
t.startsWith('//') ||
|
|
81
|
-
t.startsWith('*') ||
|
|
82
|
-
t.startsWith('/*') ||
|
|
83
|
-
t.startsWith('import ') ||
|
|
84
|
-
t.startsWith('export ') ||
|
|
85
|
-
t.startsWith('type ') ||
|
|
86
|
-
t.startsWith('interface ') ||
|
|
87
|
-
t.startsWith('function ') ||
|
|
88
|
-
t.startsWith('const ') ||
|
|
89
|
-
t.startsWith('let ') ||
|
|
90
|
-
t.startsWith('var ') ||
|
|
91
|
-
t.startsWith('{') ||
|
|
92
|
-
t.startsWith('}') ||
|
|
93
|
-
t.startsWith('return ') ||
|
|
94
|
-
t.startsWith('if ') ||
|
|
95
|
-
t.startsWith('else')
|
|
96
|
-
) {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
return /<[A-Za-z][A-Za-z0-9-]*[\s>]/.test(t);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function hasDataSrc(line: string): boolean {
|
|
103
|
-
return /data-src\s*=/.test(line);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function toRelativePath(resource: string): string {
|
|
107
|
-
const idx = resource.indexOf('/src/');
|
|
108
|
-
if (idx !== -1) return resource.slice(idx + 1);
|
|
109
|
-
return resource.split('/').pop() || resource;
|
|
110
|
-
}
|
package/types.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export interface SourceInfo {
|
|
2
|
-
file: string;
|
|
3
|
-
line: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface BoxModel {
|
|
7
|
-
marginTop: string;
|
|
8
|
-
marginRight: string;
|
|
9
|
-
marginBottom: string;
|
|
10
|
-
marginLeft: string;
|
|
11
|
-
paddingTop: string;
|
|
12
|
-
paddingRight: string;
|
|
13
|
-
paddingBottom: string;
|
|
14
|
-
paddingLeft: string;
|
|
15
|
-
borderTop: string;
|
|
16
|
-
borderRight: string;
|
|
17
|
-
borderBottom: string;
|
|
18
|
-
borderLeft: string;
|
|
19
|
-
width: string;
|
|
20
|
-
height: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ElementInfo {
|
|
24
|
-
tag: string;
|
|
25
|
-
id: string;
|
|
26
|
-
classes: string;
|
|
27
|
-
rect: DOMRect;
|
|
28
|
-
text: string;
|
|
29
|
-
outerHTML: string;
|
|
30
|
-
cssPath: string;
|
|
31
|
-
computedStyles: Record<string, string>;
|
|
32
|
-
boxModel: BoxModel | null;
|
|
33
|
-
source: SourceInfo | null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface SnippetData {
|
|
37
|
-
file: string;
|
|
38
|
-
line: number;
|
|
39
|
-
totalLines: number;
|
|
40
|
-
snippet: {
|
|
41
|
-
startLine: number;
|
|
42
|
-
lines: string[];
|
|
43
|
-
highlightLine: number;
|
|
44
|
-
};
|
|
45
|
-
}
|
package/use-element-inspector.ts
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
-
import type { ElementInfo, SnippetData, SourceInfo, BoxModel } from './types';
|
|
3
|
-
|
|
4
|
-
function findSource(el: HTMLElement): SourceInfo | null {
|
|
5
|
-
let current: HTMLElement | null = el;
|
|
6
|
-
while (current) {
|
|
7
|
-
const src = current.getAttribute('data-src');
|
|
8
|
-
if (src) {
|
|
9
|
-
const lastColon = src.lastIndexOf(':');
|
|
10
|
-
if (lastColon > 0) {
|
|
11
|
-
return {
|
|
12
|
-
file: src.slice(0, lastColon),
|
|
13
|
-
line: parseInt(src.slice(lastColon + 1), 10) || 1,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
return { file: src, line: 1 };
|
|
17
|
-
}
|
|
18
|
-
current = current.parentElement;
|
|
19
|
-
}
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getCssPath(el: HTMLElement): string {
|
|
24
|
-
const parts: string[] = [];
|
|
25
|
-
let current: HTMLElement | null = el;
|
|
26
|
-
while (current && current !== document.body && current !== document.documentElement) {
|
|
27
|
-
let selector = current.tagName.toLowerCase();
|
|
28
|
-
if (current.id) {
|
|
29
|
-
selector += '#' + current.id;
|
|
30
|
-
parts.unshift(selector);
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
33
|
-
const parent = current.parentElement;
|
|
34
|
-
if (parent) {
|
|
35
|
-
const siblings = Array.from(parent.children).filter(
|
|
36
|
-
(c) => (c as Element).tagName === current!.tagName,
|
|
37
|
-
);
|
|
38
|
-
if (siblings.length > 1) {
|
|
39
|
-
const index = siblings.indexOf(current) + 1;
|
|
40
|
-
selector += `:nth-of-type(${index})`;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
parts.unshift(selector);
|
|
44
|
-
current = parent;
|
|
45
|
-
}
|
|
46
|
-
return parts.join(' > ');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function getTextForInspector(el: HTMLElement): string {
|
|
50
|
-
const TEXT_TAGS = new Set([
|
|
51
|
-
'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
|
52
|
-
'BUTTON', 'A', 'LABEL', 'P', 'SPAN', 'LI', 'TD', 'TH', 'DT', 'DD',
|
|
53
|
-
]);
|
|
54
|
-
if (!TEXT_TAGS.has(el.tagName)) return '';
|
|
55
|
-
|
|
56
|
-
// Только прямые текстовые узлы — без содержимого дочерних элементов.
|
|
57
|
-
// Для <span><Icon /> Wiki Codex <em>v2</em></span> вернёт "Wiki Codex",
|
|
58
|
-
// а не "Wiki Codex v2" (как textContent).
|
|
59
|
-
let direct = '';
|
|
60
|
-
for (const node of Array.from(el.childNodes)) {
|
|
61
|
-
if (node.nodeType === Node.TEXT_NODE && node.textContent) {
|
|
62
|
-
direct += node.textContent;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const directTrim = direct.trim();
|
|
66
|
-
|
|
67
|
-
// Если прямого текста нет (всё во вложенных) — fallback на textContent,
|
|
68
|
-
// чтобы не потерять <button><Icon />Save</button> (там текст прямой,
|
|
69
|
-
// но иногда иконка съедает весь текст).
|
|
70
|
-
const fallback = (el.textContent || '').trim();
|
|
71
|
-
const result = directTrim || fallback;
|
|
72
|
-
return result.slice(0, 120);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function getElementInfo(el: HTMLElement): ElementInfo | null {
|
|
76
|
-
const rect = el.getBoundingClientRect();
|
|
77
|
-
const cs = window.getComputedStyle(el);
|
|
78
|
-
if (
|
|
79
|
-
el.closest('[data-se-fab]') ||
|
|
80
|
-
el.closest('[data-se-panel]') ||
|
|
81
|
-
el.closest('[data-se-highlight]')
|
|
82
|
-
) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
let outerHTML = el.outerHTML;
|
|
86
|
-
if (outerHTML.length > 2000) {
|
|
87
|
-
outerHTML = outerHTML.slice(0, 2000) + '\n ...';
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
tag: el.tagName.toLowerCase(),
|
|
91
|
-
id: el.id || '',
|
|
92
|
-
classes: typeof el.className === 'string' ? el.className : '',
|
|
93
|
-
rect,
|
|
94
|
-
text: getTextForInspector(el),
|
|
95
|
-
outerHTML,
|
|
96
|
-
cssPath: getCssPath(el),
|
|
97
|
-
computedStyles: {
|
|
98
|
-
fontSize: cs.fontSize,
|
|
99
|
-
fontWeight: cs.fontWeight,
|
|
100
|
-
color: cs.color,
|
|
101
|
-
lineHeight: cs.lineHeight,
|
|
102
|
-
width: `${Math.round(rect.width)}px`,
|
|
103
|
-
height: `${Math.round(rect.height)}px`,
|
|
104
|
-
},
|
|
105
|
-
boxModel: {
|
|
106
|
-
marginTop: cs.marginTop,
|
|
107
|
-
marginRight: cs.marginRight,
|
|
108
|
-
marginBottom: cs.marginBottom,
|
|
109
|
-
marginLeft: cs.marginLeft,
|
|
110
|
-
paddingTop: cs.paddingTop,
|
|
111
|
-
paddingRight: cs.paddingRight,
|
|
112
|
-
paddingBottom: cs.paddingBottom,
|
|
113
|
-
paddingLeft: cs.paddingLeft,
|
|
114
|
-
borderTop: cs.borderTopWidth,
|
|
115
|
-
borderRight: cs.borderRightWidth,
|
|
116
|
-
borderBottom: cs.borderBottomWidth,
|
|
117
|
-
borderLeft: cs.borderLeftWidth,
|
|
118
|
-
width: cs.width,
|
|
119
|
-
height: cs.height,
|
|
120
|
-
},
|
|
121
|
-
source: findSource(el),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function computePanelPos(rect: DOMRect): { x: number; y: number } {
|
|
126
|
-
const panelW = 380;
|
|
127
|
-
const panelH = 400;
|
|
128
|
-
let x = rect.right + 8;
|
|
129
|
-
let y = rect.top;
|
|
130
|
-
if (x + panelW > window.innerWidth - 16) {
|
|
131
|
-
x = rect.left - panelW - 8;
|
|
132
|
-
}
|
|
133
|
-
if (x < 8) x = 8;
|
|
134
|
-
if (y + panelH > window.innerHeight - 16) {
|
|
135
|
-
y = window.innerHeight - panelH - 16;
|
|
136
|
-
}
|
|
137
|
-
if (y < 8) y = 8;
|
|
138
|
-
return { x, y };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function useElementInspector() {
|
|
142
|
-
const [active, setActive] = useState(false);
|
|
143
|
-
const [elementInfo, setElementInfo] = useState<ElementInfo | null>(null);
|
|
144
|
-
const [panelPos, setPanelPos] = useState({ x: 0, y: 0 });
|
|
145
|
-
const [highlightBox, setHighlightBox] = useState<DOMRect | null>(null);
|
|
146
|
-
const [snippet, setSnippet] = useState<SnippetData | null>(null);
|
|
147
|
-
const [snippetLoading, setSnippetLoading] = useState(false);
|
|
148
|
-
|
|
149
|
-
const fetchSnippet = useCallback(async (file: string, line: number) => {
|
|
150
|
-
setSnippetLoading(true);
|
|
151
|
-
setSnippet(null);
|
|
152
|
-
try {
|
|
153
|
-
const res = await fetch(
|
|
154
|
-
`/api/source?file=${encodeURIComponent(file)}&line=${line}&ctx=10`,
|
|
155
|
-
);
|
|
156
|
-
if (res.ok) {
|
|
157
|
-
const data = await res.json();
|
|
158
|
-
setSnippet(data);
|
|
159
|
-
}
|
|
160
|
-
} catch {
|
|
161
|
-
// ignore
|
|
162
|
-
}
|
|
163
|
-
setSnippetLoading(false);
|
|
164
|
-
}, []);
|
|
165
|
-
|
|
166
|
-
const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
167
|
-
const target = e.target as HTMLElement;
|
|
168
|
-
if (
|
|
169
|
-
target.closest('[data-se-fab]') ||
|
|
170
|
-
target.closest('[data-se-panel]') ||
|
|
171
|
-
target.closest('[data-se-highlight]')
|
|
172
|
-
) {
|
|
173
|
-
setHighlightBox(null);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
setHighlightBox(target.getBoundingClientRect());
|
|
177
|
-
}, []);
|
|
178
|
-
|
|
179
|
-
const handleClick = useCallback(
|
|
180
|
-
(e: MouseEvent) => {
|
|
181
|
-
const target = e.target as HTMLElement;
|
|
182
|
-
if (
|
|
183
|
-
target.closest('[data-se-fab]') ||
|
|
184
|
-
target.closest('[data-se-panel]') ||
|
|
185
|
-
target.closest('[data-se-highlight]')
|
|
186
|
-
) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
e.preventDefault();
|
|
190
|
-
e.stopPropagation();
|
|
191
|
-
|
|
192
|
-
const info = getElementInfo(target);
|
|
193
|
-
if (info) {
|
|
194
|
-
setElementInfo(info);
|
|
195
|
-
setSnippet(null);
|
|
196
|
-
setPanelPos(computePanelPos(info.rect));
|
|
197
|
-
if (info.source) {
|
|
198
|
-
fetchSnippet(info.source.file, info.source.line);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
[fetchSnippet],
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
const handleScroll = useCallback(() => {
|
|
206
|
-
setHighlightBox(null);
|
|
207
|
-
}, []);
|
|
208
|
-
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
if (!active) return;
|
|
211
|
-
document.addEventListener('mousemove', handleMouseMove, true);
|
|
212
|
-
document.addEventListener('click', handleClick, true);
|
|
213
|
-
document.addEventListener('scroll', handleScroll, true);
|
|
214
|
-
document.body.style.cursor = 'crosshair';
|
|
215
|
-
return () => {
|
|
216
|
-
document.removeEventListener('mousemove', handleMouseMove, true);
|
|
217
|
-
document.removeEventListener('click', handleClick, true);
|
|
218
|
-
document.removeEventListener('scroll', handleScroll, true);
|
|
219
|
-
document.body.style.cursor = '';
|
|
220
|
-
};
|
|
221
|
-
}, [active, handleMouseMove, handleClick, handleScroll]);
|
|
222
|
-
|
|
223
|
-
useEffect(() => {
|
|
224
|
-
if (!active) return;
|
|
225
|
-
const handler = (e: KeyboardEvent) => {
|
|
226
|
-
if (e.key === 'Escape') {
|
|
227
|
-
setActive(false);
|
|
228
|
-
setElementInfo(null);
|
|
229
|
-
setHighlightBox(null);
|
|
230
|
-
setSnippet(null);
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
window.addEventListener('keydown', handler);
|
|
234
|
-
return () => window.removeEventListener('keydown', handler);
|
|
235
|
-
}, [active]);
|
|
236
|
-
|
|
237
|
-
const toggleActive = useCallback(() => {
|
|
238
|
-
setActive((v) => {
|
|
239
|
-
const next = !v;
|
|
240
|
-
if (!next) {
|
|
241
|
-
setElementInfo(null);
|
|
242
|
-
setHighlightBox(null);
|
|
243
|
-
setSnippet(null);
|
|
244
|
-
}
|
|
245
|
-
return next;
|
|
246
|
-
});
|
|
247
|
-
}, []);
|
|
248
|
-
|
|
249
|
-
const closePanel = useCallback(() => {
|
|
250
|
-
setElementInfo(null);
|
|
251
|
-
}, []);
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
active,
|
|
255
|
-
elementInfo,
|
|
256
|
-
panelPos,
|
|
257
|
-
setPanelPos,
|
|
258
|
-
highlightBox,
|
|
259
|
-
snippet,
|
|
260
|
-
snippetLoading,
|
|
261
|
-
toggleActive,
|
|
262
|
-
closePanel,
|
|
263
|
-
};
|
|
264
|
-
}
|
package/use-panel-drag.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback, useRef } from 'react';
|
|
2
|
-
|
|
3
|
-
export function usePanelDrag(
|
|
4
|
-
panelPos: { x: number; y: number },
|
|
5
|
-
setPanelPos: (pos: { x: number; y: number }) => void,
|
|
6
|
-
) {
|
|
7
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
8
|
-
const dragRef = useRef<{
|
|
9
|
-
startX: number;
|
|
10
|
-
startY: number;
|
|
11
|
-
startLeft: number;
|
|
12
|
-
startTop: number;
|
|
13
|
-
} | null>(null);
|
|
14
|
-
|
|
15
|
-
const handleDragStart = useCallback(
|
|
16
|
-
(e: React.MouseEvent) => {
|
|
17
|
-
e.preventDefault();
|
|
18
|
-
setIsDragging(true);
|
|
19
|
-
dragRef.current = {
|
|
20
|
-
startX: e.clientX,
|
|
21
|
-
startY: e.clientY,
|
|
22
|
-
startLeft: panelPos.x,
|
|
23
|
-
startTop: panelPos.y,
|
|
24
|
-
};
|
|
25
|
-
const onMove = (ev: MouseEvent) => {
|
|
26
|
-
if (!dragRef.current) return;
|
|
27
|
-
const dx = ev.clientX - dragRef.current.startX;
|
|
28
|
-
const dy = ev.clientY - dragRef.current.startY;
|
|
29
|
-
setPanelPos({
|
|
30
|
-
x: Math.max(0, Math.min(window.innerWidth - 396, dragRef.current.startLeft + dx)),
|
|
31
|
-
y: Math.max(0, Math.min(window.innerHeight - 48, dragRef.current.startTop + dy)),
|
|
32
|
-
});
|
|
33
|
-
};
|
|
34
|
-
const onUp = () => {
|
|
35
|
-
setIsDragging(false);
|
|
36
|
-
dragRef.current = null;
|
|
37
|
-
document.removeEventListener('mousemove', onMove);
|
|
38
|
-
document.removeEventListener('mouseup', onUp);
|
|
39
|
-
};
|
|
40
|
-
document.addEventListener('mousemove', onMove);
|
|
41
|
-
document.addEventListener('mouseup', onUp);
|
|
42
|
-
},
|
|
43
|
-
[panelPos, setPanelPos],
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
return { isDragging, handleDragStart };
|
|
47
|
-
}
|