dsh-workspace-explorer 1.0.1 → 1.1.1
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/CHANGELOG.md +16 -0
- package/README.md +111 -107
- package/lib/client.js +12272 -11307
- package/lib/index.js +1 -1
- package/package.json +53 -49
- package/scripts/build-client.mjs +1 -0
- package/src/client/conversation-drop.js +142 -0
- package/src/client/main.js +451 -224
- package/src/client/sidebar-motion.js +69 -0
- package/src/document-types.js +2 -0
- package/src/host/ExplorerShell.cs +183 -183
- package/src/host/files.js +61 -16
- package/src/host/pdf-assets.js +30 -0
- package/src/host/shell-files.js +81 -81
- package/src/thumbnail-types.js +18 -18
- package/src/ui/composer-drop.js +119 -119
- package/src/ui/explorer.js +52 -46
- package/src/ui/favorites.js +176 -124
- package/src/ui/file-menu.js +125 -125
- package/src/ui/horizontal-wheel.js +11 -11
- package/src/ui/navigation.js +9 -0
- package/src/ui/pdf-preview.js +135 -0
- package/src/ui/preview-unavailable.js +6 -6
- package/src/ui/preview-zoom.js +47 -0
- package/src/ui/preview.js +150 -20
- package/src/ui/styles.js +122 -20
- package/src/ui/thumbnail.js +105 -104
- package/src/ui/workspace.js +3 -1
- package/src/vendor/ui/extra-views.js +1 -1
- package/src/vendor/ui/views.js +3 -2
package/src/ui/explorer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { joinPath, move, parentOf, renamePath, resourceAddress, underPath, visit, windowsPath } from './navigation.js';
|
|
2
|
-
import { Preview, PreviewBoundary } from './preview.js';
|
|
2
|
+
import { formatSize, Preview, PreviewBoundary } from './preview.js';
|
|
3
3
|
import { disks } from './workspace.js';
|
|
4
4
|
import { useComposerDrop } from './composer-drop.js';
|
|
5
5
|
import { Thumbnail } from './thumbnail.js';
|
|
@@ -80,14 +80,6 @@ function selectNames(current, name, entries, shift, ctrl) {
|
|
|
80
80
|
return { names, anchor: name };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function formatSize(bytes) {
|
|
84
|
-
if (typeof bytes !== 'number') return '—';
|
|
85
|
-
if (bytes < 1024) return bytes + ' B';
|
|
86
|
-
if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + ' KB';
|
|
87
|
-
if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + ' MB';
|
|
88
|
-
return (bytes / 1024 ** 3).toFixed(1) + ' GB';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
83
|
function entryType(entry) {
|
|
92
84
|
if (entry.type === 'drive') return '磁盘';
|
|
93
85
|
if (entry.type === 'directory') return '文件夹';
|
|
@@ -138,8 +130,10 @@ const clampSplit = ratio => Math.min(splitMax, Math.max(splitMin, ratio));
|
|
|
138
130
|
|
|
139
131
|
function PreviewDivider({ root, ratio, setRatio }) {
|
|
140
132
|
const grabOffset = useRef(0);
|
|
133
|
+
const [dragging, setDragging] = useState(false);
|
|
141
134
|
return h('div', {
|
|
142
135
|
className: 'dwe-divider', role: 'separator', tabIndex: 0,
|
|
136
|
+
'data-dragging': dragging || undefined,
|
|
143
137
|
'aria-label': '调整文件预览分栏高度', 'aria-orientation': 'horizontal',
|
|
144
138
|
'aria-valuemin': splitMin * 100, 'aria-valuemax': splitMax * 100,
|
|
145
139
|
'aria-valuenow': Math.round(ratio * 100),
|
|
@@ -147,16 +141,18 @@ function PreviewDivider({ root, ratio, setRatio }) {
|
|
|
147
141
|
if (event.button !== 0) return;
|
|
148
142
|
event.preventDefault();
|
|
149
143
|
const divider = event.currentTarget;
|
|
150
|
-
|
|
144
|
+
const bounds = divider.getBoundingClientRect();
|
|
145
|
+
grabOffset.current = event.clientY - (bounds.top + bounds.height / 2);
|
|
151
146
|
divider.focus();
|
|
152
147
|
divider.setPointerCapture(event.pointerId);
|
|
153
148
|
},
|
|
149
|
+
onGotPointerCapture: () => setDragging(true),
|
|
150
|
+
onLostPointerCapture: () => setDragging(false),
|
|
154
151
|
onPointerMove: event => {
|
|
155
152
|
const divider = event.currentTarget;
|
|
156
153
|
if (!divider.hasPointerCapture(event.pointerId)) return;
|
|
157
154
|
const bounds = root.current.getBoundingClientRect();
|
|
158
|
-
|
|
159
|
-
if (available > 0) setRatio(clampSplit((event.clientY - bounds.top - grabOffset.current) / available));
|
|
155
|
+
if (bounds.height > 0) setRatio(clampSplit((event.clientY - bounds.top - grabOffset.current) / bounds.height));
|
|
160
156
|
},
|
|
161
157
|
onKeyDown: event => {
|
|
162
158
|
if (event.key === 'ArrowUp') setRatio(value => clampSplit(value - 0.05));
|
|
@@ -171,21 +167,19 @@ function PreviewDivider({ root, ratio, setRatio }) {
|
|
|
171
167
|
}
|
|
172
168
|
|
|
173
169
|
export function Explorer({
|
|
174
|
-
cwd, sessionId, initialPath, bindFileActions, getTarget, onPath, openLocal, openFolderTab,
|
|
170
|
+
cwd, sessionId, initialPath, bindFileActions, getTarget, onPath, openLocal, openDocument, openFolderTab,
|
|
175
171
|
refreshDirectories, updateRenamedPath,
|
|
176
172
|
}) {
|
|
177
173
|
const workspace = windowsPath(cwd);
|
|
178
174
|
const [localHistory, setLocalHistory] = useState(() => firstPath(initialPath ?? workspace));
|
|
179
175
|
const [scoped, setScoped] = useState(null);
|
|
180
176
|
const workspaceMode = scoped !== null;
|
|
181
|
-
const root =
|
|
182
|
-
const history =
|
|
177
|
+
const root = scoped?.root ?? null;
|
|
178
|
+
const history = scoped?.history ?? localHistory;
|
|
183
179
|
const changeHistory = useCallback(change => {
|
|
184
|
-
if (workspaceMode) setScoped(current => current && {
|
|
185
|
-
root, history: change(current.root === root ? current.history : firstPath(root)),
|
|
186
|
-
});
|
|
180
|
+
if (workspaceMode) setScoped(current => current && { ...current, history: change(current.history) });
|
|
187
181
|
else setLocalHistory(change);
|
|
188
|
-
}, [workspaceMode
|
|
182
|
+
}, [workspaceMode]);
|
|
189
183
|
const [revision, setRevision] = useState(0);
|
|
190
184
|
const [listing, setListing] = useState(null);
|
|
191
185
|
const [selection, setSelection] = useState(emptySelection);
|
|
@@ -240,7 +234,7 @@ export function Explorer({
|
|
|
240
234
|
if (!current) return current;
|
|
241
235
|
const absolute = renamePath(current.absolute, from, to);
|
|
242
236
|
return absolute === current.absolute ? current
|
|
243
|
-
: { absolute, name: absolute.split('\\').at(-1), address: resourceAddress(absolute) };
|
|
237
|
+
: { ...current, absolute, name: absolute.split('\\').at(-1), address: resourceAddress(absolute) };
|
|
244
238
|
});
|
|
245
239
|
}, []);
|
|
246
240
|
const closeMenu = useCallback((restore = false) => {
|
|
@@ -291,12 +285,6 @@ export function Explorer({
|
|
|
291
285
|
setScoped(workspaceMode ? null : { root: workspace, history: firstPath(workspace) });
|
|
292
286
|
}
|
|
293
287
|
|
|
294
|
-
useLayoutEffect(() => {
|
|
295
|
-
if (!workspaceMode || !root || root === scoped.root) return;
|
|
296
|
-
resetNavigation();
|
|
297
|
-
setScoped({ root, history: firstPath(root) });
|
|
298
|
-
}, [workspaceMode, root, scoped?.root, resetNavigation]);
|
|
299
|
-
|
|
300
288
|
useLayoutEffect(() => { onPath(path); }, [onPath, path]);
|
|
301
289
|
|
|
302
290
|
useEffect(() => {
|
|
@@ -327,7 +315,6 @@ export function Explorer({
|
|
|
327
315
|
}, [root, term, revision, sessionId]);
|
|
328
316
|
|
|
329
317
|
useEffect(() => () => pathRequest.current?.abort(), []);
|
|
330
|
-
useLayoutEffect(() => bindFileActions({ refresh, rename }), [bindFileActions, refresh, rename]);
|
|
331
318
|
useLayoutEffect(() => { scroll.current.scrollTop = 0; }, [path, term]);
|
|
332
319
|
useLayoutEffect(() => {
|
|
333
320
|
const result = resultToReveal.current;
|
|
@@ -352,20 +339,33 @@ export function Explorer({
|
|
|
352
339
|
changeHistory(value => move(value, delta));
|
|
353
340
|
}
|
|
354
341
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
342
|
+
const openPreview = useCallback((absolute, name, size, line) => {
|
|
343
|
+
const targetLine = Number.isSafeInteger(line) && line > 0 ? line : undefined;
|
|
344
|
+
setPreview(current => current?.absolute === absolute && !targetLine ? current : {
|
|
345
|
+
name, absolute, size, line: targetLine, address: resourceAddress(absolute),
|
|
358
346
|
});
|
|
359
|
-
}
|
|
347
|
+
}, []);
|
|
360
348
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
349
|
+
const revealFile = useCallback((absolute, name, size, line) => {
|
|
350
|
+
const folder = parentOf(absolute);
|
|
351
|
+
if (workspaceMode && !underPath(folder, root)) {
|
|
352
|
+
resetNavigation();
|
|
353
|
+
setScoped(null);
|
|
354
|
+
setLocalHistory(history => visit(history, folder));
|
|
355
|
+
} else {
|
|
356
|
+
navigate(folder);
|
|
357
|
+
if (folder === path && ready && !ready.entries.some(entry => entry.name === name)) refresh();
|
|
358
|
+
}
|
|
359
|
+
resultToReveal.current = { folder, name };
|
|
360
|
+
setSelection(singleSelection(name));
|
|
361
|
+
openPreview(absolute, name, size, line);
|
|
368
362
|
setPreviewOpen(true);
|
|
363
|
+
}, [workspaceMode, root, path, ready, resetNavigation, navigate, refresh, openPreview]);
|
|
364
|
+
|
|
365
|
+
useLayoutEffect(() => bindFileActions({ refresh, rename, reveal: revealFile }), [bindFileActions, refresh, rename, revealFile]);
|
|
366
|
+
|
|
367
|
+
function openSearchHit(match) {
|
|
368
|
+
if (underPath(match.path, root)) revealFile(match.path, match.name);
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
function select(event, entry) {
|
|
@@ -374,7 +374,7 @@ export function Explorer({
|
|
|
374
374
|
setSelection(current => selectNames(current, entry.name, entries, shiftKey, ctrlKey));
|
|
375
375
|
setActionError('');
|
|
376
376
|
setActionNotice('');
|
|
377
|
-
if (!shiftKey && !ctrlKey && entry.type === 'file') openPreview(joinPath(path, entry.name), entry.name);
|
|
377
|
+
if (!shiftKey && !ctrlKey && entry.type === 'file') openPreview(joinPath(path, entry.name), entry.name, entry.size);
|
|
378
378
|
}
|
|
379
379
|
|
|
380
380
|
async function openWithWindows(path) {
|
|
@@ -507,7 +507,7 @@ export function Explorer({
|
|
|
507
507
|
if (target.type === 'directory') navigate(windowsPath(target.path));
|
|
508
508
|
else {
|
|
509
509
|
closeEditor();
|
|
510
|
-
openPreview(target.path, target.name);
|
|
510
|
+
openPreview(target.path, target.name, target.size);
|
|
511
511
|
}
|
|
512
512
|
scroll.current.focus();
|
|
513
513
|
} catch (error) {
|
|
@@ -525,7 +525,9 @@ export function Explorer({
|
|
|
525
525
|
if (!target || !shownPreview) return;
|
|
526
526
|
try {
|
|
527
527
|
if (root) await disks.locate(shownPreview.absolute, undefined, root);
|
|
528
|
-
if (getTarget() === target)
|
|
528
|
+
if (getTarget() === target) openDocument(resourceAddress(shownPreview.absolute, target.sessionId), {
|
|
529
|
+
paneId: target.paneId, params: shownPreview.line ? { line: shownPreview.line } : undefined,
|
|
530
|
+
});
|
|
529
531
|
} catch (error) {
|
|
530
532
|
setActionError(error?.message || String(error));
|
|
531
533
|
}
|
|
@@ -582,6 +584,7 @@ export function Explorer({
|
|
|
582
584
|
tool('home', workspaceMode ? '返回工作区根目录' : '返回此电脑', () => navigate(root), path === root),
|
|
583
585
|
tool('workspace', '跳转至工作区', () => navigate(workspace), !workspace || path === workspace || workspaceMode)),
|
|
584
586
|
h('div', { className: 'dwe-nav' },
|
|
587
|
+
h('span', { className: 'dwe-v-divider', 'aria-hidden': true }),
|
|
585
588
|
tool('copy', '复制', () => runAction('copy', selectedEntries), !modifySelected),
|
|
586
589
|
tool('cut', '剪切', () => runAction('cut', selectedEntries), !modifySelected),
|
|
587
590
|
tool('paste', '粘贴', () => runAction('paste'), !path || working),
|
|
@@ -647,20 +650,23 @@ export function Explorer({
|
|
|
647
650
|
!grid && h('span', { className: 'dwe-meta' }, entryType(entry)),
|
|
648
651
|
!grid && h('span', { className: 'dwe-meta' }, formatSize(entry.size))))))),
|
|
649
652
|
h('footer', { className: 'dwe-status-bar' },
|
|
653
|
+
h('div', { className: 'dwe-nav', role: 'group', 'aria-label': '文件视图' },
|
|
654
|
+
tool('list', '列表', () => changeView('list'), false, !grid),
|
|
655
|
+
tool('grid', '大图标', () => changeView('grid'), false, grid)),
|
|
656
|
+
h('span', { className: 'dwe-v-divider', 'aria-hidden': true }),
|
|
650
657
|
h('span', { className: 'dwe-status', role: 'status', title: actionNotice },
|
|
651
658
|
term ? found?.matches ? found.matches.length + ' 个结果' + (found.truncated ? ' · 请缩小关键词' : '') : ' '
|
|
652
659
|
: actionNotice || (selectedEntries.length > 1 ? '已选择 ' + selectedEntries.length + ' 项'
|
|
653
660
|
: ready?.entries ? ready.entries.length + ' 个项目' + (ready.truncated ? ' · 列表已截断' : '') : ' ')),
|
|
654
|
-
|
|
655
|
-
tool('list', '列表', () => changeView('list'), false, !grid),
|
|
656
|
-
tool('grid', '大图标', () => changeView('grid'), false, grid),
|
|
657
|
-
tool('preview', '预览窗格', () => setPreviewOpen(open => !open), false, previewOpen)))),
|
|
661
|
+
tool('preview', '预览窗格', () => setPreviewOpen(open => !open), false, previewOpen))),
|
|
658
662
|
previewOpen && h(PreviewDivider, { root: layout, ratio: splitRatio, setRatio: setSplitRatio }),
|
|
659
663
|
previewOpen && h('section', { className: 'dwe-preview', 'aria-label': '文件预览' },
|
|
660
664
|
h('header', { className: 'dwe-preview-bar' },
|
|
661
665
|
h('span', { className: 'dwe-preview-label' }, '预览'),
|
|
662
666
|
shownPreview && h('span', { className: 'dwe-preview-name', title: shownPreview.absolute },
|
|
663
|
-
fileIcon(shownPreview.name, 'file'),
|
|
667
|
+
fileIcon(shownPreview.name, 'file'),
|
|
668
|
+
h('span', null, shownPreview.name),
|
|
669
|
+
tool('close', '关闭当前文件预览', () => setPreview(null))),
|
|
664
670
|
shownPreview && tool('external', '在 DSH 文档预览中打开', openNative)),
|
|
665
671
|
h('div', { className: 'dwe-preview-body' },
|
|
666
672
|
shownPreview ? h(PreviewBoundary, { key: JSON.stringify([shownPreview.absolute, root]) }, h(Preview, { file: shownPreview, root }))
|
package/src/ui/favorites.js
CHANGED
|
@@ -1,125 +1,177 @@
|
|
|
1
|
-
import { renamePath, underPath, windowsPath } from './navigation.js';
|
|
2
|
-
|
|
3
|
-
const key = 'dsh-workspace-explorer.favorites';
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
seen.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
folders
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
listeners.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
1
|
+
import { renamePath, underPath, windowsPath } from './navigation.js';
|
|
2
|
+
|
|
3
|
+
const key = 'dsh-workspace-explorer.favorites';
|
|
4
|
+
const dragType = 'application/x-dwe-favorite';
|
|
5
|
+
const { createElement: h, useLayoutEffect, useRef, useSyncExternalStore } = react;
|
|
6
|
+
const listeners = new Set();
|
|
7
|
+
|
|
8
|
+
function unique(paths) {
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
return paths.filter(path => {
|
|
11
|
+
const id = path.toLowerCase();
|
|
12
|
+
if (seen.has(id)) return false;
|
|
13
|
+
seen.add(id);
|
|
14
|
+
return true;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function read() {
|
|
19
|
+
try {
|
|
20
|
+
const paths = JSON.parse(localStorage.getItem(key));
|
|
21
|
+
return Array.isArray(paths) ? unique(paths.map(windowsPath).filter(Boolean)) : [];
|
|
22
|
+
} catch { return []; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let folders = read();
|
|
26
|
+
|
|
27
|
+
function update(next, persist = true) {
|
|
28
|
+
if (next.length === folders.length && next.every((path, index) => path === folders[index])) return;
|
|
29
|
+
folders = next;
|
|
30
|
+
if (persist) {
|
|
31
|
+
try { localStorage.setItem(key, JSON.stringify(folders)); } catch { /* Keep the current window's favorites. */ }
|
|
32
|
+
}
|
|
33
|
+
for (const listener of listeners) listener();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function storageChanged(event) {
|
|
37
|
+
if (event.key === key || event.key === null) update(read(), false);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function subscribe(listener) {
|
|
41
|
+
if (!listeners.size) window.addEventListener('storage', storageChanged);
|
|
42
|
+
listeners.add(listener);
|
|
43
|
+
return () => {
|
|
44
|
+
listeners.delete(listener);
|
|
45
|
+
if (!listeners.size) window.removeEventListener('storage', storageChanged);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function useFavorites() {
|
|
50
|
+
return useSyncExternalStore(subscribe, () => folders);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function removeFavorite(path) {
|
|
54
|
+
update(folders.filter(folder => folder.toLowerCase() !== path.toLowerCase()));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function toggleFavorite(path) {
|
|
58
|
+
if (folders.some(folder => folder.toLowerCase() === path.toLowerCase())) removeFavorite(path);
|
|
59
|
+
else update([...folders, path]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function clearFavorites() {
|
|
63
|
+
update([]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function renameFavorites(from, to) {
|
|
67
|
+
update(unique(folders.map(path => renamePath(path, from, to))));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function moveFavorite(from, to, after) {
|
|
71
|
+
if (from === to || !folders.includes(from) || !folders.includes(to)) return;
|
|
72
|
+
const next = folders.filter(path => path !== from);
|
|
73
|
+
next.splice(next.indexOf(to) + Number(after), 0, from);
|
|
74
|
+
update(next);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function Favorites({ anchor, close, folders, navigate, currentPath, removeIcon, workspaceMode, root }) {
|
|
78
|
+
const panel = useRef(null);
|
|
79
|
+
const dragged = useRef(null);
|
|
80
|
+
const dropRow = useRef(null);
|
|
81
|
+
|
|
82
|
+
function clearDropRow() {
|
|
83
|
+
if (dropRow.current) delete dropRow.current.dataset.dweFavoriteDrop;
|
|
84
|
+
dropRow.current = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resetDrag() {
|
|
88
|
+
dragged.current = null;
|
|
89
|
+
clearDropRow();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isAfter(event) {
|
|
93
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
94
|
+
return event.clientY >= bounds.top + bounds.height / 2;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
useLayoutEffect(() => {
|
|
98
|
+
const popup = panel.current;
|
|
99
|
+
const button = anchor.current;
|
|
100
|
+
const parent = popup.offsetParent;
|
|
101
|
+
if (!button || !parent) return;
|
|
102
|
+
|
|
103
|
+
function position() {
|
|
104
|
+
const bounds = parent.getBoundingClientRect();
|
|
105
|
+
const trigger = button.getBoundingClientRect();
|
|
106
|
+
popup.style.left = Math.max(4, Math.min(trigger.right - bounds.left - popup.offsetWidth,
|
|
107
|
+
bounds.width - popup.offsetWidth - 4)) + 'px';
|
|
108
|
+
popup.style.top = trigger.bottom - bounds.top + 4 + 'px';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const observer = new ResizeObserver(position);
|
|
112
|
+
observer.observe(parent);
|
|
113
|
+
observer.observe(popup);
|
|
114
|
+
position();
|
|
115
|
+
const lifetime = new AbortController();
|
|
116
|
+
const options = { capture: true, signal: lifetime.signal };
|
|
117
|
+
function outside(event) {
|
|
118
|
+
if (popup.contains(event.target) || button.contains(event.target)) return;
|
|
119
|
+
close();
|
|
120
|
+
}
|
|
121
|
+
for (const event of ['pointerdown', 'click', 'wheel', 'focusin']) {
|
|
122
|
+
document.addEventListener(event, outside, options);
|
|
123
|
+
}
|
|
124
|
+
window.addEventListener('blur', close, { signal: lifetime.signal });
|
|
125
|
+
document.addEventListener('keydown', event => {
|
|
126
|
+
if (event.key === 'Escape') {
|
|
127
|
+
if (popup.contains(document.activeElement)) button.focus({ preventScroll: true });
|
|
128
|
+
close();
|
|
129
|
+
} else outside(event);
|
|
130
|
+
}, options);
|
|
131
|
+
return () => { observer.disconnect(); lifetime.abort(); };
|
|
132
|
+
}, [anchor, close]);
|
|
133
|
+
|
|
134
|
+
return h('section', { ref: panel, className: 'dwe-favorites', 'aria-label': '收藏夹' },
|
|
135
|
+
h('header', null, h('strong', null, '收藏夹'),
|
|
136
|
+
h('button', { type: 'button', className: 'dwe-clear-favorites', disabled: !folders.length, onClick: clearFavorites }, '清空收藏')),
|
|
137
|
+
folders.length ? h('ul', { onDragLeave: event => {
|
|
138
|
+
if (!event.currentTarget.contains(event.relatedTarget)) clearDropRow();
|
|
139
|
+
} }, ...folders.map(path => {
|
|
140
|
+
const name = path.length === 3 ? path.slice(0, 2) : path.split('\\').at(-1);
|
|
141
|
+
return h('li', {
|
|
142
|
+
key: path, draggable: true,
|
|
143
|
+
onDragStart: event => {
|
|
144
|
+
dragged.current = path;
|
|
145
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
146
|
+
event.dataTransfer.setData(dragType, path);
|
|
147
|
+
},
|
|
148
|
+
onDragOver: event => {
|
|
149
|
+
if (!dragged.current || !event.dataTransfer.types.includes(dragType)) return;
|
|
150
|
+
if (dropRow.current !== event.currentTarget) clearDropRow();
|
|
151
|
+
if (dragged.current === path) return;
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
event.dataTransfer.dropEffect = 'move';
|
|
154
|
+
dropRow.current = event.currentTarget;
|
|
155
|
+
dropRow.current.dataset.dweFavoriteDrop = isAfter(event) ? 'after' : 'before';
|
|
156
|
+
},
|
|
157
|
+
onDrop: event => {
|
|
158
|
+
const from = dragged.current;
|
|
159
|
+
if (!from || event.dataTransfer.getData(dragType) !== from) return;
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
resetDrag();
|
|
162
|
+
moveFavorite(from, path, isAfter(event));
|
|
163
|
+
},
|
|
164
|
+
onDragEnd: resetDrag,
|
|
165
|
+
},
|
|
166
|
+
h('button', {
|
|
167
|
+
type: 'button', className: 'dwe-favorite-location', title: path, onClick: () => navigate(path),
|
|
168
|
+
disabled: workspaceMode && !underPath(path, root),
|
|
169
|
+
'aria-current': currentPath?.toLowerCase() === path.toLowerCase() ? 'location' : undefined,
|
|
170
|
+
}, h(primitives.FileTypeIcon, { kind: 'folder', size: 16 }),
|
|
171
|
+
h('span', { className: 'dwe-favorite-text' }, name)),
|
|
172
|
+
h('button', {
|
|
173
|
+
type: 'button', className: 'dwe-tool', title: '移除收藏', 'aria-label': '移除收藏 ' + name,
|
|
174
|
+
onClick: () => removeFavorite(path),
|
|
175
|
+
}, removeIcon));
|
|
176
|
+
})) : h('p', null, '点击地址栏右侧的星标收藏当前文件夹。'));
|
|
125
177
|
}
|