dsh-workspace-explorer 1.1.0 → 1.1.2

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,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
  }
@@ -1,126 +1,126 @@
1
- const { createElement: h, useEffect, useLayoutEffect, useRef, useState } = react;
2
-
3
- export function FileMenu({ menu, items, close, invoke }) {
4
- const root = useRef(null);
5
- useLayoutEffect(() => {
6
- const node = root.current;
7
- const bounds = node.getBoundingClientRect();
8
- node.style.left = Math.max(4, Math.min(menu.x, window.innerWidth - bounds.width - 4)) + 'px';
9
- node.style.top = Math.max(4, Math.min(menu.y, window.innerHeight - bounds.height - 4)) + 'px';
10
- node.querySelector('button:not(:disabled)')?.focus({ preventScroll: true });
11
- }, [menu]);
12
-
13
- useEffect(() => {
14
- const controller = new AbortController();
15
- const options = { capture: true, signal: controller.signal };
16
- function outside(event) {
17
- if (!root.current?.contains(event.target)) close();
18
- }
19
- document.addEventListener('pointerdown', outside, options);
20
- document.addEventListener('wheel', outside, options);
21
- window.addEventListener('resize', close, options);
22
- return () => controller.abort();
23
- }, [close]);
24
-
25
- return reactDom.createPortal(h('div', {
26
- ref: root, className: 'dwe-file-menu', role: 'menu', 'aria-label': '文件操作',
27
- onContextMenu: event => event.preventDefault(),
28
- onKeyDown: event => {
29
- if (event.key === 'Escape') {
30
- event.preventDefault();
31
- event.stopPropagation();
32
- close(true);
33
- return;
34
- }
35
- const keys = ['ArrowDown', 'ArrowUp', 'Home', 'End'];
36
- if (!keys.includes(event.key)) return;
37
- const buttons = [...root.current.querySelectorAll('button:not(:disabled)')];
38
- if (!buttons.length) return;
39
- const current = buttons.indexOf(document.activeElement);
40
- const next = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1
41
- : (current + (event.key === 'ArrowDown' ? 1 : -1) + buttons.length) % buttons.length;
42
- buttons[next].focus({ preventScroll: true });
43
- event.preventDefault();
44
- event.stopPropagation();
45
- },
46
- }, ...items.map(item => h('button', {
47
- type: 'button', role: 'menuitem', key: item.id, disabled: item.disabled, 'data-dwe-action': item.id,
48
- onClick: () => invoke(item.id),
49
- }, item.icon, item.label))), document.body);
50
- }
51
-
52
- export function DeleteDialog({ entries, close, remove }) {
53
- const root = useRef(null);
54
- const [working, setWorking] = useState(false);
55
- const [error, setError] = useState('');
56
- useLayoutEffect(() => {
57
- const dialog = root.current;
58
- dialog.showModal();
59
- return () => dialog.close();
60
- }, []);
61
-
62
- return h('dialog', {
63
- ref: root, className: 'dwe-file-dialog', 'aria-label': '删除项目',
64
- onKeyDown: event => event.stopPropagation(),
65
- onCancel: event => { event.preventDefault(); if (!working) close(); },
66
- }, h('form', {
67
- onSubmit: async event => {
68
- event.preventDefault();
69
- if (working) return;
70
- setWorking(true);
71
- setError('');
72
- try { await remove(); }
73
- catch (error) { setError(error.message); setWorking(false); }
74
- },
75
- },
76
- h('h2', null, entries.length === 1 ? '移入回收站?' : '将这 ' + entries.length + ' 项移入回收站?'),
77
- entries.length === 1
78
- ? h('div', { className: 'dwe-delete-path', title: entries[0].absolute }, entries[0].absolute)
79
- : h('ul', { className: 'dwe-delete-path dwe-delete-paths' },
80
- ...entries.map(entry => h('li', { key: entry.absolute, title: entry.absolute }, entry.absolute))),
81
- error && h('p', { role: 'alert' }, error),
82
- h('div', { className: 'dwe-dialog-actions' },
83
- h('button', { type: 'button', autoFocus: true, onClick: close, disabled: working }, '取消'),
84
- h('button', {
85
- type: 'submit', 'data-dwe-action': 'delete', disabled: working,
86
- }, working ? '正在移入…' : '移入回收站'))));
87
- }
88
-
89
- export function RenameDialog({ entry, close, rename }) {
90
- const root = useRef(null);
91
- const input = useRef(null);
92
- const [name, setName] = useState(entry.name);
93
- const [saving, setSaving] = useState(false);
94
- const [error, setError] = useState('');
95
- useLayoutEffect(() => {
96
- const dialog = root.current;
97
- dialog.showModal();
98
- const dot = entry.type === 'file' ? entry.name.lastIndexOf('.') : -1;
99
- input.current.setSelectionRange(0, dot > 0 ? dot : entry.name.length);
100
- return () => dialog.close();
101
- }, [entry]);
102
-
103
- return h('dialog', {
104
- ref: root, className: 'dwe-file-dialog', 'aria-label': '重命名',
105
- onKeyDown: event => event.stopPropagation(),
106
- onCancel: event => { event.preventDefault(); if (!saving) close(); },
107
- }, h('form', {
108
- onSubmit: async event => {
109
- event.preventDefault();
110
- if (saving || !name.trim()) return;
111
- setSaving(true);
112
- setError('');
113
- try { await rename(name); }
114
- catch (error) { setError(error.message); setSaving(false); }
115
- },
116
- },
117
- h('h2', null, '重命名'),
118
- h('label', null, '新名称', h('input', {
119
- ref: input, value: name, autoFocus: true, spellCheck: false, readOnly: saving,
120
- onChange: event => { setName(event.target.value); setError(''); },
121
- })),
122
- error && h('p', { role: 'alert' }, error),
123
- h('div', { className: 'dwe-dialog-actions' },
124
- h('button', { type: 'button', onClick: close, disabled: saving }, '取消'),
125
- h('button', { type: 'submit', disabled: saving || !name.trim() }, saving ? '正在重命名…' : '确定'))));
1
+ const { createElement: h, useEffect, useLayoutEffect, useRef, useState } = react;
2
+
3
+ export function FileMenu({ menu, items, close, invoke }) {
4
+ const root = useRef(null);
5
+ useLayoutEffect(() => {
6
+ const node = root.current;
7
+ const bounds = node.getBoundingClientRect();
8
+ node.style.left = Math.max(4, Math.min(menu.x, window.innerWidth - bounds.width - 4)) + 'px';
9
+ node.style.top = Math.max(4, Math.min(menu.y, window.innerHeight - bounds.height - 4)) + 'px';
10
+ node.querySelector('button:not(:disabled)')?.focus({ preventScroll: true });
11
+ }, [menu]);
12
+
13
+ useEffect(() => {
14
+ const controller = new AbortController();
15
+ const options = { capture: true, signal: controller.signal };
16
+ function outside(event) {
17
+ if (!root.current?.contains(event.target)) close();
18
+ }
19
+ document.addEventListener('pointerdown', outside, options);
20
+ document.addEventListener('wheel', outside, options);
21
+ window.addEventListener('resize', close, options);
22
+ return () => controller.abort();
23
+ }, [close]);
24
+
25
+ return reactDom.createPortal(h('div', {
26
+ ref: root, className: 'dwe-file-menu', role: 'menu', 'aria-label': '文件操作',
27
+ onContextMenu: event => event.preventDefault(),
28
+ onKeyDown: event => {
29
+ if (event.key === 'Escape') {
30
+ event.preventDefault();
31
+ event.stopPropagation();
32
+ close(true);
33
+ return;
34
+ }
35
+ const keys = ['ArrowDown', 'ArrowUp', 'Home', 'End'];
36
+ if (!keys.includes(event.key)) return;
37
+ const buttons = [...root.current.querySelectorAll('button:not(:disabled)')];
38
+ if (!buttons.length) return;
39
+ const current = buttons.indexOf(document.activeElement);
40
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1
41
+ : (current + (event.key === 'ArrowDown' ? 1 : -1) + buttons.length) % buttons.length;
42
+ buttons[next].focus({ preventScroll: true });
43
+ event.preventDefault();
44
+ event.stopPropagation();
45
+ },
46
+ }, ...items.map(item => h('button', {
47
+ type: 'button', role: 'menuitem', key: item.id, disabled: item.disabled, 'data-dwe-action': item.id,
48
+ onClick: () => invoke(item.id),
49
+ }, item.icon, item.label))), document.body);
50
+ }
51
+
52
+ export function DeleteDialog({ entries, close, remove }) {
53
+ const root = useRef(null);
54
+ const [working, setWorking] = useState(false);
55
+ const [error, setError] = useState('');
56
+ useLayoutEffect(() => {
57
+ const dialog = root.current;
58
+ dialog.showModal();
59
+ return () => dialog.close();
60
+ }, []);
61
+
62
+ return h('dialog', {
63
+ ref: root, className: 'dwe-file-dialog', 'aria-label': '删除项目',
64
+ onKeyDown: event => event.stopPropagation(),
65
+ onCancel: event => { event.preventDefault(); if (!working) close(); },
66
+ }, h('form', {
67
+ onSubmit: async event => {
68
+ event.preventDefault();
69
+ if (working) return;
70
+ setWorking(true);
71
+ setError('');
72
+ try { await remove(); }
73
+ catch (error) { setError(error.message); setWorking(false); }
74
+ },
75
+ },
76
+ h('h2', null, entries.length === 1 ? '移入回收站?' : '将这 ' + entries.length + ' 项移入回收站?'),
77
+ entries.length === 1
78
+ ? h('div', { className: 'dwe-delete-path', title: entries[0].absolute }, entries[0].absolute)
79
+ : h('ul', { className: 'dwe-delete-path dwe-delete-paths' },
80
+ ...entries.map(entry => h('li', { key: entry.absolute, title: entry.absolute }, entry.absolute))),
81
+ error && h('p', { role: 'alert' }, error),
82
+ h('div', { className: 'dwe-dialog-actions' },
83
+ h('button', { type: 'button', autoFocus: true, onClick: close, disabled: working }, '取消'),
84
+ h('button', {
85
+ type: 'submit', 'data-dwe-action': 'delete', disabled: working,
86
+ }, working ? '正在移入…' : '移入回收站'))));
87
+ }
88
+
89
+ export function RenameDialog({ entry, close, rename }) {
90
+ const root = useRef(null);
91
+ const input = useRef(null);
92
+ const [name, setName] = useState(entry.name);
93
+ const [saving, setSaving] = useState(false);
94
+ const [error, setError] = useState('');
95
+ useLayoutEffect(() => {
96
+ const dialog = root.current;
97
+ dialog.showModal();
98
+ const dot = entry.type === 'file' ? entry.name.lastIndexOf('.') : -1;
99
+ input.current.setSelectionRange(0, dot > 0 ? dot : entry.name.length);
100
+ return () => dialog.close();
101
+ }, [entry]);
102
+
103
+ return h('dialog', {
104
+ ref: root, className: 'dwe-file-dialog', 'aria-label': '重命名',
105
+ onKeyDown: event => event.stopPropagation(),
106
+ onCancel: event => { event.preventDefault(); if (!saving) close(); },
107
+ }, h('form', {
108
+ onSubmit: async event => {
109
+ event.preventDefault();
110
+ if (saving || !name.trim()) return;
111
+ setSaving(true);
112
+ setError('');
113
+ try { await rename(name); }
114
+ catch (error) { setError(error.message); setSaving(false); }
115
+ },
116
+ },
117
+ h('h2', null, '重命名'),
118
+ h('label', null, '新名称', h('input', {
119
+ ref: input, value: name, autoFocus: true, spellCheck: false, readOnly: saving,
120
+ onChange: event => { setName(event.target.value); setError(''); },
121
+ })),
122
+ error && h('p', { role: 'alert' }, error),
123
+ h('div', { className: 'dwe-dialog-actions' },
124
+ h('button', { type: 'button', onClick: close, disabled: saving }, '取消'),
125
+ h('button', { type: 'submit', disabled: saving || !name.trim() }, saving ? '正在重命名…' : '确定'))));
126
126
  }
@@ -1,12 +1,12 @@
1
- export function scrollHorizontally(event, element) {
2
- if (event.ctrlKey || !event.deltaY || Math.abs(event.deltaX) >= Math.abs(event.deltaY)) return;
3
- const max = element.scrollWidth - element.clientWidth;
4
- if (max <= 1) return;
5
- const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? element.clientWidth : 1;
6
- const distance = Math.sign(event.deltaY) * Math.min(36, Math.abs(event.deltaY) * unit * 0.55);
7
- const position = element.scrollLeft;
8
- if (distance < 0 ? position <= 0 : position >= max - 1) return;
9
- event.preventDefault();
10
- event.stopPropagation();
11
- element.scrollTo({ left: position + distance, behavior: 'instant' });
1
+ export function scrollHorizontally(event, element) {
2
+ if (event.ctrlKey || !event.deltaY || Math.abs(event.deltaX) >= Math.abs(event.deltaY)) return;
3
+ const max = element.scrollWidth - element.clientWidth;
4
+ if (max <= 1) return;
5
+ const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? element.clientWidth : 1;
6
+ const distance = Math.sign(event.deltaY) * Math.min(36, Math.abs(event.deltaY) * unit * 0.55);
7
+ const position = element.scrollLeft;
8
+ if (distance < 0 ? position <= 0 : position >= max - 1) return;
9
+ event.preventDefault();
10
+ event.stopPropagation();
11
+ element.scrollTo({ left: position + distance, behavior: 'instant' });
12
12
  }
@@ -39,3 +39,12 @@ export function resourceAddress(path, sessionId) {
39
39
  const encoded = path.split('\\').map(segment => encodeURIComponent(segment).replace(/%3A/gi, ':')).join('/');
40
40
  return 'dsh-resource://file/' + (sessionId ? 'session/' + encodeURIComponent(sessionId) : 'absolute') + '/' + encoded;
41
41
  }
42
+
43
+ export function sessionFilePath(address, sessionId) {
44
+ const match = /^dsh-resource:\/\/file\/session\/([^/?#]+)\/([^?#]+)(?:[?#].*)?$/.exec(address);
45
+ if (!match || !sessionId) return null;
46
+ try {
47
+ if (decodeURIComponent(match[1]) !== sessionId) return null;
48
+ return match[2].split('/').map(decodeURIComponent).join('/');
49
+ } catch { return null; }
50
+ }