@lijian-ui/dsh-file-manager 0.2.2 → 0.2.4
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/LICENSE +38 -38
- package/README.i18n.yaml +6 -6
- package/README.md +66 -66
- package/README.zh.md +66 -66
- package/cordis.patch.yml +13 -13
- package/lib/client.js +27354 -355
- package/lib/client.js.map +1 -1
- package/lib/index.js +474 -42
- package/lib/tsconfig.client.tsbuildinfo +1 -1
- package/lib/tsconfig.host.tsbuildinfo +1 -1
- package/lib/types/client/file-source.d.ts +10 -0
- package/lib/types/client/file-source.d.ts.map +1 -1
- package/lib/types/client/fileType.d.ts +19 -4
- package/lib/types/client/fileType.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +0 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +3 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/mount.d.ts +1 -1
- package/lib/types/client/mount.d.ts.map +1 -1
- package/lib/types/client/picker/FilePickerModal.d.ts +8 -6
- package/lib/types/client/picker/FilePickerModal.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
- package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
- package/lib/types/client/preview/content.d.ts +3 -1
- package/lib/types/client/preview/content.d.ts.map +1 -1
- package/lib/types/client/preview/office.d.ts +23 -0
- package/lib/types/client/preview/office.d.ts.map +1 -0
- package/lib/types/client/reference.d.ts +23 -10
- package/lib/types/client/reference.d.ts.map +1 -1
- package/lib/types/client/store.d.ts.map +1 -1
- package/lib/types/host/fs-service.d.ts.map +1 -1
- package/lib/types/host/office-preview.d.ts +13 -0
- package/lib/types/host/office-preview.d.ts.map +1 -0
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +19 -4
- package/src/client/DockItem.tsx +1 -1
- package/src/client/FileManagerSettingsCard.tsx +117 -117
- package/src/client/PluginSettingsCard.tsx +337 -337
- package/src/client/chat/file-ref.ts +144 -144
- package/src/client/chat/mermaid-chat.tsx +102 -102
- package/src/client/chat/placeholder-hint.tsx +46 -46
- package/src/client/components/ExplorerPanel.tsx +534 -534
- package/src/client/components/FileIcon.tsx +46 -46
- package/src/client/components/ScmPanel.tsx +480 -480
- package/src/client/components/a11y.ts +20 -20
- package/src/client/components/icons.tsx +274 -274
- package/src/client/components/overlay.tsx +225 -225
- package/src/client/drag/DragFileInlay.tsx +89 -89
- package/src/client/drag/file-drag.ts +67 -67
- package/src/client/drag.ts +154 -154
- package/src/client/file-source.ts +34 -0
- package/src/client/fileType.ts +32 -5
- package/src/client/hooks/useResizableSplit.ts +91 -91
- package/src/client/hooks/useStore.ts +15 -15
- package/src/client/index.ts +45 -17
- package/src/client/layout.ts +523 -523
- package/src/client/locales.ts +6 -0
- package/src/client/maximize.ts +34 -34
- package/src/client/mount.tsx +2 -1
- package/src/client/persist.ts +194 -194
- package/src/client/picker/FilePickerModal.tsx +526 -507
- package/src/client/picker/file-picker.ts +86 -86
- package/src/client/preview/PreviewPanel.tsx +8 -1
- package/src/client/preview/PreviewTabs.tsx +159 -159
- package/src/client/preview/PreviewToolbar.tsx +3 -2
- package/src/client/preview/content.tsx +7 -2
- package/src/client/preview/markdown.ts +351 -351
- package/src/client/preview/mermaid.ts +274 -274
- package/src/client/preview/office.tsx +321 -0
- package/src/client/reference.ts +62 -8
- package/src/client/settings-card.module.css +316 -316
- package/src/client/settings-form.ts +451 -451
- package/src/client/store.ts +17 -12
- package/src/client/styles/drag.module.css +30 -30
- package/src/client/styles/explorer.module.css +371 -371
- package/src/client/styles/picker.module.css +344 -344
- package/src/client/styles/preview.module.css +18 -11
- package/src/client/styles/scm.module.css +380 -380
- package/src/client/styles/tokens.module.css +376 -367
- package/src/core/types.ts +162 -162
- package/src/host/fs-service.ts +7 -1
- package/src/host/gate.ts +73 -73
- package/src/host/git-runner.ts +116 -116
- package/src/host/git-service.ts +395 -395
- package/src/host/loopback.ts +63 -63
- package/src/host/office-preview.ts +399 -0
- package/src/host/poll-guard.ts +109 -109
- package/src/host/routes.ts +72 -1
- package/src/index.ts +3 -1
- package/src/mount-once.ts +48 -48
- package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
- package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
- package/lib/types/client/floating.d.ts +0 -27
- package/lib/types/client/floating.d.ts.map +0 -1
- package/lib/types/client/mention.d.ts +0 -44
- package/lib/types/client/mention.d.ts.map +0 -1
- package/src/client/styles/chip.module.css +0 -38
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure drag-to-composer helpers shared by the explorer rows (the drag
|
|
3
|
-
* source) and the composer dock inlay (the drop target): the custom MIME
|
|
4
|
-
* type, the drag-state detector, and the draft-splicing rule. Deliberately
|
|
5
|
-
* framework-free so the splicing math is unit-testable in isolation.
|
|
6
|
-
*
|
|
7
|
-
* The composer host only accepts OS image drops (its document-level drop
|
|
8
|
-
* handler checks `dataTransfer.types` for `Files` and routes through the
|
|
9
|
-
* image pipeline), so a workspace file needs its own MIME. A plain relative
|
|
10
|
-
* path is inserted into the draft — the agent reads the file through its
|
|
11
|
-
* existing tools without any prefix grammar.
|
|
12
|
-
* @module dsh-filemgr/client/drag/file-drag
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/** Custom MIME carrying a workspace-relative file path. */
|
|
16
|
-
export const FILE_DRAG_MIME = 'application/x-dsh-file'
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Whether a drag event carries our file payload.
|
|
20
|
-
* @param types - the live `dataTransfer.types` list (read-only during drag).
|
|
21
|
-
* @returns true when our MIME is present.
|
|
22
|
-
*/
|
|
23
|
-
export function hasFileDrag(types: readonly string[] | undefined): boolean {
|
|
24
|
-
return types !== undefined && types.includes(FILE_DRAG_MIME)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Whether a drop payload is a plausible workspace-relative path. The custom
|
|
29
|
-
* MIME only proves the drag carries *some* string — a foreign page can set it
|
|
30
|
-
* too, so the payload itself is validated before it reaches the draft:
|
|
31
|
-
* relative POSIX shape only; no absolute paths, no '..' segments, no
|
|
32
|
-
* backslashes, no control characters, and a sane length.
|
|
33
|
-
* @param path - the raw payload from dataTransfer.
|
|
34
|
-
* @returns true when the payload is safe to splice into the draft.
|
|
35
|
-
*/
|
|
36
|
-
export function isValidFileDragPayload(path: string): boolean {
|
|
37
|
-
if (path === '' || path.length > 512) return false
|
|
38
|
-
if (path.startsWith('/') || path.includes('\\')) return false
|
|
39
|
-
// eslint-disable-next-line no-control-regex
|
|
40
|
-
if (/[\x00-\x1f\x7f]/.test(path)) return false
|
|
41
|
-
if (path.split('/').some(segment => segment === '..')) return false
|
|
42
|
-
return true
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Splice a workspace-relative path into a composer draft at the caret.
|
|
47
|
-
*
|
|
48
|
-
* Separator rule: one space is added before the path unless the caret sits
|
|
49
|
-
* at the start of the draft or right after whitespace; one space is added
|
|
50
|
-
* after the path unless the caret sits at the end of the draft or right
|
|
51
|
-
* before whitespace. Empty path or an out-of-range caret are no-ops.
|
|
52
|
-
*
|
|
53
|
-
* @param draft - the current draft text.
|
|
54
|
-
* @param path - the relative path to insert.
|
|
55
|
-
* @param caret - insertion offset (default: the end of the draft).
|
|
56
|
-
* @returns the next draft; the caller owns writing it through the input
|
|
57
|
-
* facade.
|
|
58
|
-
*/
|
|
59
|
-
export function insertPathIntoDraft(draft: string, path: string, caret?: number): string {
|
|
60
|
-
if (path === '') return draft
|
|
61
|
-
const at = caret === undefined ? draft.length : Math.min(Math.max(caret, 0), draft.length)
|
|
62
|
-
const before = draft.slice(0, at)
|
|
63
|
-
const after = draft.slice(at)
|
|
64
|
-
const needBefore = before !== '' && !/\s$/.test(before)
|
|
65
|
-
const needAfter = after !== '' && !/^\s/.test(after)
|
|
66
|
-
return before + (needBefore ? ' ' : '') + path + (needAfter ? ' ' : '') + after
|
|
67
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pure drag-to-composer helpers shared by the explorer rows (the drag
|
|
3
|
+
* source) and the composer dock inlay (the drop target): the custom MIME
|
|
4
|
+
* type, the drag-state detector, and the draft-splicing rule. Deliberately
|
|
5
|
+
* framework-free so the splicing math is unit-testable in isolation.
|
|
6
|
+
*
|
|
7
|
+
* The composer host only accepts OS image drops (its document-level drop
|
|
8
|
+
* handler checks `dataTransfer.types` for `Files` and routes through the
|
|
9
|
+
* image pipeline), so a workspace file needs its own MIME. A plain relative
|
|
10
|
+
* path is inserted into the draft — the agent reads the file through its
|
|
11
|
+
* existing tools without any prefix grammar.
|
|
12
|
+
* @module dsh-filemgr/client/drag/file-drag
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Custom MIME carrying a workspace-relative file path. */
|
|
16
|
+
export const FILE_DRAG_MIME = 'application/x-dsh-file'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether a drag event carries our file payload.
|
|
20
|
+
* @param types - the live `dataTransfer.types` list (read-only during drag).
|
|
21
|
+
* @returns true when our MIME is present.
|
|
22
|
+
*/
|
|
23
|
+
export function hasFileDrag(types: readonly string[] | undefined): boolean {
|
|
24
|
+
return types !== undefined && types.includes(FILE_DRAG_MIME)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether a drop payload is a plausible workspace-relative path. The custom
|
|
29
|
+
* MIME only proves the drag carries *some* string — a foreign page can set it
|
|
30
|
+
* too, so the payload itself is validated before it reaches the draft:
|
|
31
|
+
* relative POSIX shape only; no absolute paths, no '..' segments, no
|
|
32
|
+
* backslashes, no control characters, and a sane length.
|
|
33
|
+
* @param path - the raw payload from dataTransfer.
|
|
34
|
+
* @returns true when the payload is safe to splice into the draft.
|
|
35
|
+
*/
|
|
36
|
+
export function isValidFileDragPayload(path: string): boolean {
|
|
37
|
+
if (path === '' || path.length > 512) return false
|
|
38
|
+
if (path.startsWith('/') || path.includes('\\')) return false
|
|
39
|
+
// eslint-disable-next-line no-control-regex
|
|
40
|
+
if (/[\x00-\x1f\x7f]/.test(path)) return false
|
|
41
|
+
if (path.split('/').some(segment => segment === '..')) return false
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Splice a workspace-relative path into a composer draft at the caret.
|
|
47
|
+
*
|
|
48
|
+
* Separator rule: one space is added before the path unless the caret sits
|
|
49
|
+
* at the start of the draft or right after whitespace; one space is added
|
|
50
|
+
* after the path unless the caret sits at the end of the draft or right
|
|
51
|
+
* before whitespace. Empty path or an out-of-range caret are no-ops.
|
|
52
|
+
*
|
|
53
|
+
* @param draft - the current draft text.
|
|
54
|
+
* @param path - the relative path to insert.
|
|
55
|
+
* @param caret - insertion offset (default: the end of the draft).
|
|
56
|
+
* @returns the next draft; the caller owns writing it through the input
|
|
57
|
+
* facade.
|
|
58
|
+
*/
|
|
59
|
+
export function insertPathIntoDraft(draft: string, path: string, caret?: number): string {
|
|
60
|
+
if (path === '') return draft
|
|
61
|
+
const at = caret === undefined ? draft.length : Math.min(Math.max(caret, 0), draft.length)
|
|
62
|
+
const before = draft.slice(0, at)
|
|
63
|
+
const after = draft.slice(at)
|
|
64
|
+
const needBefore = before !== '' && !/\s$/.test(before)
|
|
65
|
+
const needAfter = after !== '' && !/^\s/.test(after)
|
|
66
|
+
return before + (needBefore ? ' ' : '') + path + (needAfter ? ' ' : '') + after
|
|
67
|
+
}
|
package/src/client/drag.ts
CHANGED
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Framework-free drag machinery shared by the React hook (useResizableSplit)
|
|
3
|
-
* and the DOM layout controller: one pointer-down handler that owns capture,
|
|
4
|
-
* the rAF-flush loop, the e.buttons === 0 fallback, every end path, and the
|
|
5
|
-
* drag styles. Pure DOM — no React, so both callers run the exact same
|
|
6
|
-
* smoothness core (FileManager's useResizableSplit architecture, re-implemented).
|
|
7
|
-
* @module dsh-filemgr/client/drag
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface DragStartOptions {
|
|
11
|
-
/** Drag left = grow (handle on the left edge of its container). */
|
|
12
|
-
reverse: boolean
|
|
13
|
-
/** Width at drag start (read at pointer-down time). */
|
|
14
|
-
getStartWidth: () => number
|
|
15
|
-
/** Clamped width from the raw delta (px mode adds deltaX directly). */
|
|
16
|
-
compute: (startWidth: number, deltaX: number) => number
|
|
17
|
-
/** Per-frame flush (rAF-merged; called at most once per frame). */
|
|
18
|
-
onFrame: (width: number) => void
|
|
19
|
-
/** Final commit (also fired on every end path). */
|
|
20
|
-
onEnd: (width: number) => void
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Whether a pointer event is the primary (left) button or touch. */
|
|
24
|
-
function isPrimaryPointer(event: PointerEvent): boolean {
|
|
25
|
-
return event.pointerType === 'touch' || event.button === 0
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Handle one pointer-down: wire capture + window listeners, run the rAF
|
|
30
|
-
* loop, and end on any of the five termination paths. Call from a
|
|
31
|
-
* onPointerDown handler (React or plain DOM).
|
|
32
|
-
* @param event - the raw pointerdown event.
|
|
33
|
-
* @param el - the handle element (capture target + reverse marker source).
|
|
34
|
-
* @param opts - drag behavior.
|
|
35
|
-
* @returns a disposer (idempotent; also called internally on end).
|
|
36
|
-
*/
|
|
37
|
-
export function handlePointerDragStart(event: PointerEvent, el: HTMLElement, opts: DragStartOptions): () => void {
|
|
38
|
-
if (!isPrimaryPointer(event)) return () => {}
|
|
39
|
-
event.preventDefault()
|
|
40
|
-
|
|
41
|
-
const startX = event.clientX
|
|
42
|
-
const startWidth = opts.getStartWidth()
|
|
43
|
-
const pointerId = event.pointerId
|
|
44
|
-
const reverse = opts.reverse
|
|
45
|
-
|
|
46
|
-
let rafId: number | null = null
|
|
47
|
-
let pendingWidth: number | null = null
|
|
48
|
-
let latestWidth = startWidth
|
|
49
|
-
let isDragging = true
|
|
50
|
-
let cleanup: (() => void) | null = null
|
|
51
|
-
|
|
52
|
-
const flushPending = (): void => {
|
|
53
|
-
if (pendingWidth === null) return
|
|
54
|
-
latestWidth = pendingWidth
|
|
55
|
-
opts.onFrame(pendingWidth)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const addWindowListener = <K extends keyof WindowEventMap>(
|
|
59
|
-
key: K,
|
|
60
|
-
handler: (e: WindowEventMap[K]) => void,
|
|
61
|
-
): (() => void) => {
|
|
62
|
-
window.addEventListener(key, handler)
|
|
63
|
-
return () => window.removeEventListener(key, handler)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const computeWidth = (clientX: number): number => {
|
|
67
|
-
const deltaX = reverse ? startX - clientX : clientX - startX
|
|
68
|
-
return opts.compute(startWidth, deltaX)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const finishDrag = (e?: PointerEvent | MouseEvent | FocusEvent): void => {
|
|
72
|
-
if (!isDragging) return
|
|
73
|
-
isDragging = false
|
|
74
|
-
if (rafId !== null) {
|
|
75
|
-
cancelAnimationFrame(rafId)
|
|
76
|
-
rafId = null
|
|
77
|
-
}
|
|
78
|
-
flushPending()
|
|
79
|
-
let finalWidth = latestWidth
|
|
80
|
-
if (e !== undefined && 'clientX' in e && typeof e.clientX === 'number') {
|
|
81
|
-
finalWidth = computeWidth(e.clientX)
|
|
82
|
-
}
|
|
83
|
-
opts.onEnd(finalWidth)
|
|
84
|
-
cleanup?.()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const handlePointerMove = (e: PointerEvent): void => {
|
|
88
|
-
if (!isDragging) return
|
|
89
|
-
if (e.buttons === 0) {
|
|
90
|
-
// Lost pointerup (capture steal / release outside): end now.
|
|
91
|
-
finishDrag(e)
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
pendingWidth = computeWidth(e.clientX)
|
|
95
|
-
if (rafId === null) {
|
|
96
|
-
rafId = requestAnimationFrame(() => {
|
|
97
|
-
rafId = null
|
|
98
|
-
flushPending()
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
const handlePointerUp = (e: PointerEvent): void => finishDrag(e)
|
|
103
|
-
const handlePointerCancel = (e: PointerEvent): void => finishDrag(e)
|
|
104
|
-
const handleMouseUp = (e: MouseEvent): void => finishDrag(e)
|
|
105
|
-
const handleLostCapture = (): void => finishDrag()
|
|
106
|
-
|
|
107
|
-
// Drag styles: no text selection, col-resize cursor, disable transitions.
|
|
108
|
-
const previousUserSelect = document.body.style.userSelect
|
|
109
|
-
const previousCursor = document.body.style.cursor
|
|
110
|
-
document.body.style.userSelect = 'none'
|
|
111
|
-
document.body.style.cursor = 'col-resize'
|
|
112
|
-
const frame = el.closest('[data-dsh-frame]')
|
|
113
|
-
frame?.setAttribute('data-filemgr-instant', '')
|
|
114
|
-
|
|
115
|
-
const restore = (): void => {
|
|
116
|
-
document.body.style.userSelect = previousUserSelect
|
|
117
|
-
document.body.style.cursor = previousCursor
|
|
118
|
-
frame?.removeAttribute('data-filemgr-instant')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (el.setPointerCapture) {
|
|
122
|
-
try {
|
|
123
|
-
el.setPointerCapture(pointerId)
|
|
124
|
-
el.addEventListener('lostpointercapture', handleLostCapture)
|
|
125
|
-
} catch {
|
|
126
|
-
// capture failed; the window listeners cover the drag
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const releaseCapture = (): void => {
|
|
131
|
-
try {
|
|
132
|
-
if (el.releasePointerCapture && el.hasPointerCapture?.(pointerId)) {
|
|
133
|
-
el.releasePointerCapture(pointerId)
|
|
134
|
-
}
|
|
135
|
-
} catch {
|
|
136
|
-
// already released
|
|
137
|
-
}
|
|
138
|
-
el.removeEventListener('lostpointercapture', handleLostCapture)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const listeners = [
|
|
142
|
-
addWindowListener('pointermove', handlePointerMove),
|
|
143
|
-
addWindowListener('pointerup', handlePointerUp),
|
|
144
|
-
addWindowListener('pointercancel', handlePointerCancel),
|
|
145
|
-
addWindowListener('mouseup', handleMouseUp),
|
|
146
|
-
addWindowListener('blur', () => finishDrag()),
|
|
147
|
-
]
|
|
148
|
-
cleanup = () => {
|
|
149
|
-
restore()
|
|
150
|
-
releaseCapture()
|
|
151
|
-
for (const dispose of listeners) dispose()
|
|
152
|
-
}
|
|
153
|
-
return cleanup
|
|
154
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Framework-free drag machinery shared by the React hook (useResizableSplit)
|
|
3
|
+
* and the DOM layout controller: one pointer-down handler that owns capture,
|
|
4
|
+
* the rAF-flush loop, the e.buttons === 0 fallback, every end path, and the
|
|
5
|
+
* drag styles. Pure DOM — no React, so both callers run the exact same
|
|
6
|
+
* smoothness core (FileManager's useResizableSplit architecture, re-implemented).
|
|
7
|
+
* @module dsh-filemgr/client/drag
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface DragStartOptions {
|
|
11
|
+
/** Drag left = grow (handle on the left edge of its container). */
|
|
12
|
+
reverse: boolean
|
|
13
|
+
/** Width at drag start (read at pointer-down time). */
|
|
14
|
+
getStartWidth: () => number
|
|
15
|
+
/** Clamped width from the raw delta (px mode adds deltaX directly). */
|
|
16
|
+
compute: (startWidth: number, deltaX: number) => number
|
|
17
|
+
/** Per-frame flush (rAF-merged; called at most once per frame). */
|
|
18
|
+
onFrame: (width: number) => void
|
|
19
|
+
/** Final commit (also fired on every end path). */
|
|
20
|
+
onEnd: (width: number) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Whether a pointer event is the primary (left) button or touch. */
|
|
24
|
+
function isPrimaryPointer(event: PointerEvent): boolean {
|
|
25
|
+
return event.pointerType === 'touch' || event.button === 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handle one pointer-down: wire capture + window listeners, run the rAF
|
|
30
|
+
* loop, and end on any of the five termination paths. Call from a
|
|
31
|
+
* onPointerDown handler (React or plain DOM).
|
|
32
|
+
* @param event - the raw pointerdown event.
|
|
33
|
+
* @param el - the handle element (capture target + reverse marker source).
|
|
34
|
+
* @param opts - drag behavior.
|
|
35
|
+
* @returns a disposer (idempotent; also called internally on end).
|
|
36
|
+
*/
|
|
37
|
+
export function handlePointerDragStart(event: PointerEvent, el: HTMLElement, opts: DragStartOptions): () => void {
|
|
38
|
+
if (!isPrimaryPointer(event)) return () => {}
|
|
39
|
+
event.preventDefault()
|
|
40
|
+
|
|
41
|
+
const startX = event.clientX
|
|
42
|
+
const startWidth = opts.getStartWidth()
|
|
43
|
+
const pointerId = event.pointerId
|
|
44
|
+
const reverse = opts.reverse
|
|
45
|
+
|
|
46
|
+
let rafId: number | null = null
|
|
47
|
+
let pendingWidth: number | null = null
|
|
48
|
+
let latestWidth = startWidth
|
|
49
|
+
let isDragging = true
|
|
50
|
+
let cleanup: (() => void) | null = null
|
|
51
|
+
|
|
52
|
+
const flushPending = (): void => {
|
|
53
|
+
if (pendingWidth === null) return
|
|
54
|
+
latestWidth = pendingWidth
|
|
55
|
+
opts.onFrame(pendingWidth)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const addWindowListener = <K extends keyof WindowEventMap>(
|
|
59
|
+
key: K,
|
|
60
|
+
handler: (e: WindowEventMap[K]) => void,
|
|
61
|
+
): (() => void) => {
|
|
62
|
+
window.addEventListener(key, handler)
|
|
63
|
+
return () => window.removeEventListener(key, handler)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const computeWidth = (clientX: number): number => {
|
|
67
|
+
const deltaX = reverse ? startX - clientX : clientX - startX
|
|
68
|
+
return opts.compute(startWidth, deltaX)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const finishDrag = (e?: PointerEvent | MouseEvent | FocusEvent): void => {
|
|
72
|
+
if (!isDragging) return
|
|
73
|
+
isDragging = false
|
|
74
|
+
if (rafId !== null) {
|
|
75
|
+
cancelAnimationFrame(rafId)
|
|
76
|
+
rafId = null
|
|
77
|
+
}
|
|
78
|
+
flushPending()
|
|
79
|
+
let finalWidth = latestWidth
|
|
80
|
+
if (e !== undefined && 'clientX' in e && typeof e.clientX === 'number') {
|
|
81
|
+
finalWidth = computeWidth(e.clientX)
|
|
82
|
+
}
|
|
83
|
+
opts.onEnd(finalWidth)
|
|
84
|
+
cleanup?.()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handlePointerMove = (e: PointerEvent): void => {
|
|
88
|
+
if (!isDragging) return
|
|
89
|
+
if (e.buttons === 0) {
|
|
90
|
+
// Lost pointerup (capture steal / release outside): end now.
|
|
91
|
+
finishDrag(e)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
pendingWidth = computeWidth(e.clientX)
|
|
95
|
+
if (rafId === null) {
|
|
96
|
+
rafId = requestAnimationFrame(() => {
|
|
97
|
+
rafId = null
|
|
98
|
+
flushPending()
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const handlePointerUp = (e: PointerEvent): void => finishDrag(e)
|
|
103
|
+
const handlePointerCancel = (e: PointerEvent): void => finishDrag(e)
|
|
104
|
+
const handleMouseUp = (e: MouseEvent): void => finishDrag(e)
|
|
105
|
+
const handleLostCapture = (): void => finishDrag()
|
|
106
|
+
|
|
107
|
+
// Drag styles: no text selection, col-resize cursor, disable transitions.
|
|
108
|
+
const previousUserSelect = document.body.style.userSelect
|
|
109
|
+
const previousCursor = document.body.style.cursor
|
|
110
|
+
document.body.style.userSelect = 'none'
|
|
111
|
+
document.body.style.cursor = 'col-resize'
|
|
112
|
+
const frame = el.closest('[data-dsh-frame]')
|
|
113
|
+
frame?.setAttribute('data-filemgr-instant', '')
|
|
114
|
+
|
|
115
|
+
const restore = (): void => {
|
|
116
|
+
document.body.style.userSelect = previousUserSelect
|
|
117
|
+
document.body.style.cursor = previousCursor
|
|
118
|
+
frame?.removeAttribute('data-filemgr-instant')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (el.setPointerCapture) {
|
|
122
|
+
try {
|
|
123
|
+
el.setPointerCapture(pointerId)
|
|
124
|
+
el.addEventListener('lostpointercapture', handleLostCapture)
|
|
125
|
+
} catch {
|
|
126
|
+
// capture failed; the window listeners cover the drag
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const releaseCapture = (): void => {
|
|
131
|
+
try {
|
|
132
|
+
if (el.releasePointerCapture && el.hasPointerCapture?.(pointerId)) {
|
|
133
|
+
el.releasePointerCapture(pointerId)
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// already released
|
|
137
|
+
}
|
|
138
|
+
el.removeEventListener('lostpointercapture', handleLostCapture)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const listeners = [
|
|
142
|
+
addWindowListener('pointermove', handlePointerMove),
|
|
143
|
+
addWindowListener('pointerup', handlePointerUp),
|
|
144
|
+
addWindowListener('pointercancel', handlePointerCancel),
|
|
145
|
+
addWindowListener('mouseup', handleMouseUp),
|
|
146
|
+
addWindowListener('blur', () => finishDrag()),
|
|
147
|
+
]
|
|
148
|
+
cleanup = () => {
|
|
149
|
+
restore()
|
|
150
|
+
releaseCapture()
|
|
151
|
+
for (const dispose of listeners) dispose()
|
|
152
|
+
}
|
|
153
|
+
return cleanup
|
|
154
|
+
}
|
|
@@ -56,3 +56,37 @@ export function registerFileReferenceSource(ctx: ClientContext): () => void {
|
|
|
56
56
|
return () => {}
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/** The roster name Excel-selection chips address (see reference.ts). */
|
|
61
|
+
export const SHEET_SOURCE_NAME = 'sheet'
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Register the inert `sheet` reference source (codec only). Mirrors the
|
|
65
|
+
* `file` source: it isn't a menu participant, its only job is to let the
|
|
66
|
+
* shell's submit pipeline serialize the `相对路径!Sheet名!Range` chip text.
|
|
67
|
+
* Inert — no pick, no menu candidates; Excel references are minted by
|
|
68
|
+
* reference.ts's appendSheetChip, purely for chip appearance.
|
|
69
|
+
*/
|
|
70
|
+
export function registerSheetReferenceSource(ctx: ClientContext): () => void {
|
|
71
|
+
const inputTriggers = (ctx.get as (name: string) => unknown)('inputTriggers') as
|
|
72
|
+
| { registerSource(src: InputTriggerSource): () => void }
|
|
73
|
+
| undefined
|
|
74
|
+
if (inputTriggers === undefined) return () => {}
|
|
75
|
+
const source: InputTriggerSource = {
|
|
76
|
+
trigger: '@',
|
|
77
|
+
name: SHEET_SOURCE_NAME,
|
|
78
|
+
candidates: async () => [],
|
|
79
|
+
onPick: () => undefined,
|
|
80
|
+
codec: {
|
|
81
|
+
clipboardText: (ref) => ref,
|
|
82
|
+
serialize: (ref, _signal) => Promise.resolve(ref),
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return inputTriggers.registerSource(source)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// Duplicate registration (HMR remount) is not a failure.
|
|
89
|
+
console.warn('[dsh-filemgr] sheet reference source already registered:', error)
|
|
90
|
+
return () => {}
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/client/fileType.ts
CHANGED
|
@@ -86,6 +86,16 @@ export function isEditableType(type: PreviewContentType): boolean {
|
|
|
86
86
|
return type === 'markdown' || type === 'html' || type === 'code' || type === 'csv' || type === 'text'
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** Whether a file path names an editable .xlsx (the only editable excel kind). */
|
|
90
|
+
export function isExcelEditablePath(path: string): boolean {
|
|
91
|
+
return path.toLowerCase().endsWith('.xlsx')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Whether a file path names an editable .pptx (the only editable ppt kind). */
|
|
95
|
+
export function isPptEditablePath(path: string): boolean {
|
|
96
|
+
return path.toLowerCase().endsWith('.pptx')
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
/** Whether the type reads its content as text (vs image data URL). */
|
|
90
100
|
export function isTextType(type: PreviewContentType): boolean {
|
|
91
101
|
return type !== 'image' && type !== 'pdf' && type !== 'word' && type !== 'excel'
|
|
@@ -117,13 +127,30 @@ export function parentRel(path: string): string {
|
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
/**
|
|
120
|
-
* The streaming URL a pdf tab renders: the host raw route serves the
|
|
121
|
-
* with mime
|
|
122
|
-
* base64 round-trip and no read-size cap. The nonce defeats
|
|
123
|
-
* when the tab is refreshed after the file changed on disk.
|
|
130
|
+
* The streaming URL a pdf/office tab renders: the host raw route serves the
|
|
131
|
+
* bytes with the derived mime, so the preview iframe / viewer loads them
|
|
132
|
+
* directly — no base64 round-trip and no read-size cap. The nonce defeats
|
|
133
|
+
* browser caching when the tab is refreshed after the file changed on disk.
|
|
134
|
+
*/
|
|
135
|
+
export function rawPreviewUrl(root: string, path: string, nonce: number): string {
|
|
136
|
+
return `/filemgr/raw?root=${encodeURIComponent(root)}&path=${encodeURIComponent(path)}&v=${nonce}`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Whether a content type streams its bytes through the raw route (never the
|
|
141
|
+
* /read text path): pdf and the office kinds. The tab's `content` is the
|
|
142
|
+
* raw-route URL; the viewer fetches it into bytes itself.
|
|
143
|
+
*/
|
|
144
|
+
export function isStreamedType(type: PreviewContentType): boolean {
|
|
145
|
+
return type === 'pdf' || type === 'word' || type === 'excel' || type === 'ppt'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The streaming URL a pdf tab renders (mime application/pdf; the preview
|
|
150
|
+
* iframe loads it directly). Kept as the pdf-specific alias of rawPreviewUrl.
|
|
124
151
|
*
|
|
125
152
|
* Contributed by EricWang1358 (#239).
|
|
126
153
|
*/
|
|
127
154
|
export function pdfPreviewUrl(root: string, path: string, nonce: number): string {
|
|
128
|
-
return
|
|
155
|
+
return rawPreviewUrl(root, path, nonce)
|
|
129
156
|
}
|