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.
@@ -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
- grabOffset.current = event.clientY - divider.getBoundingClientRect().top;
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
- const available = bounds.height - divider.offsetHeight;
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 = workspaceMode ? (sessionId ? workspace : scoped.root) : null;
182
- const history = workspaceMode ? (root === scoped.root ? scoped.history : firstPath(root)) : localHistory;
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, root]);
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
- function openPreview(absolute, name) {
356
- setPreview(current => current?.absolute === absolute ? current : {
357
- name, absolute, address: resourceAddress(absolute),
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
- function openSearchHit(match) {
362
- if (!underPath(match.path, root)) return;
363
- const folder = parentOf(match.path);
364
- navigate(folder);
365
- resultToReveal.current = { folder, name: match.name };
366
- setSelection(singleSelection(match.name));
367
- openPreview(match.path, match.name);
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) target.actions.openResource(resourceAddress(shownPreview.absolute, target.sessionId));
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
- h('div', { className: 'dwe-nav', role: 'group', 'aria-label': '视图与预览' },
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'), h('span', null, shownPreview.name)),
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 }))
@@ -1,125 +1,177 @@
1
- import { renamePath, underPath, windowsPath } from './navigation.js';
2
-
3
- const key = 'dsh-workspace-explorer.favorites';
4
- const { createElement: h, useLayoutEffect, useRef, useSyncExternalStore } = react;
5
- const listeners = new Set();
6
-
7
- function unique(paths) {
8
- const seen = new Set();
9
- return paths.filter(path => {
10
- const id = path.toLowerCase();
11
- if (seen.has(id)) return false;
12
- seen.add(id);
13
- return true;
14
- });
15
- }
16
-
17
- function read() {
18
- try {
19
- const paths = JSON.parse(localStorage.getItem(key));
20
- return Array.isArray(paths) ? unique(paths.map(windowsPath).filter(Boolean)) : [];
21
- } catch { return []; }
22
- }
23
-
24
- let folders = read();
25
-
26
- function update(next, persist = true) {
27
- if (next.length === folders.length && next.every((path, index) => path === folders[index])) return;
28
- folders = next;
29
- if (persist) {
30
- try { localStorage.setItem(key, JSON.stringify(folders)); } catch { /* Keep the current window's favorites. */ }
31
- }
32
- for (const listener of listeners) listener();
33
- }
34
-
35
- function storageChanged(event) {
36
- if (event.key === key || event.key === null) update(read(), false);
37
- }
38
-
39
- function subscribe(listener) {
40
- if (!listeners.size) window.addEventListener('storage', storageChanged);
41
- listeners.add(listener);
42
- return () => {
43
- listeners.delete(listener);
44
- if (!listeners.size) window.removeEventListener('storage', storageChanged);
45
- };
46
- }
47
-
48
- export function useFavorites() {
49
- return useSyncExternalStore(subscribe, () => folders);
50
- }
51
-
52
- export function removeFavorite(path) {
53
- update(folders.filter(folder => folder.toLowerCase() !== path.toLowerCase()));
54
- }
55
-
56
- export function toggleFavorite(path) {
57
- if (folders.some(folder => folder.toLowerCase() === path.toLowerCase())) removeFavorite(path);
58
- else update([...folders, path]);
59
- }
60
-
61
- export function clearFavorites() {
62
- update([]);
63
- }
64
-
65
- export function renameFavorites(from, to) {
66
- update(unique(folders.map(path => renamePath(path, from, to))));
67
- }
68
-
69
- export function Favorites({ anchor, close, folders, navigate, currentPath, removeIcon, workspaceMode, root }) {
70
- const panel = useRef(null);
71
- useLayoutEffect(() => {
72
- const popup = panel.current;
73
- const button = anchor.current;
74
- const parent = popup.offsetParent;
75
- if (!button || !parent) return;
76
-
77
- function position() {
78
- const bounds = parent.getBoundingClientRect();
79
- const trigger = button.getBoundingClientRect();
80
- popup.style.left = Math.max(4, Math.min(trigger.right - bounds.left - popup.offsetWidth,
81
- bounds.width - popup.offsetWidth - 4)) + 'px';
82
- popup.style.top = trigger.bottom - bounds.top + 4 + 'px';
83
- }
84
-
85
- const observer = new ResizeObserver(position);
86
- observer.observe(parent);
87
- observer.observe(popup);
88
- position();
89
- const lifetime = new AbortController();
90
- const options = { capture: true, signal: lifetime.signal };
91
- function outside(event) {
92
- if (popup.contains(event.target) || button.contains(event.target)) return;
93
- close();
94
- }
95
- for (const event of ['pointerdown', 'click', 'wheel', 'focusin']) {
96
- document.addEventListener(event, outside, options);
97
- }
98
- window.addEventListener('blur', close, { signal: lifetime.signal });
99
- document.addEventListener('keydown', event => {
100
- if (event.key === 'Escape') {
101
- if (popup.contains(document.activeElement)) button.focus({ preventScroll: true });
102
- close();
103
- } else outside(event);
104
- }, options);
105
- return () => { observer.disconnect(); lifetime.abort(); };
106
- }, [anchor, close]);
107
-
108
- return h('section', { ref: panel, className: 'dwe-favorites', 'aria-label': '收藏夹' },
109
- h('header', null, h('strong', null, '收藏夹'),
110
- h('button', { type: 'button', className: 'dwe-clear-favorites', disabled: !folders.length, onClick: clearFavorites }, '清空收藏')),
111
- folders.length ? h('ul', null, ...folders.map(path => {
112
- const name = path.length === 3 ? path.slice(0, 2) : path.split('\\').at(-1);
113
- return h('li', { key: path },
114
- h('button', {
115
- type: 'button', className: 'dwe-favorite-location', title: path, onClick: () => navigate(path),
116
- disabled: workspaceMode && !underPath(path, root),
117
- 'aria-current': currentPath?.toLowerCase() === path.toLowerCase() ? 'location' : undefined,
118
- }, h(primitives.FileTypeIcon, { kind: 'folder', size: 16 }),
119
- h('span', { className: 'dwe-favorite-text' }, name)),
120
- h('button', {
121
- type: 'button', className: 'dwe-tool', title: '移除收藏', 'aria-label': '移除收藏 ' + name,
122
- onClick: () => removeFavorite(path),
123
- }, removeIcon));
124
- })) : h('p', null, '点击地址栏右侧的星标收藏当前文件夹。'));
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
  }