dsh-folder-path 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,323 @@
1
+ /**
2
+ * `dsh-folder-path` browser half.
3
+ *
4
+ * Shipped as the package's `./client` bundle: the DSH client module system
5
+ * loads it as a `window.__ModuleLoader__.load({ id, factory })` script, calls
6
+ * `factory(require)`, and mounts the exported plugin (`apply`) into the browser
7
+ * Cordis tree.
8
+ *
9
+ * What it does — one thing, in the capture phase, before the conversation
10
+ * plugin's Lexical paste command can turn the folder into a doomed upload:
11
+ *
12
+ * * a paste is inspected for entries that are *directories*
13
+ * (`webkitGetAsEntry().isDirectory`; the paste payload is a `File` of size 0
14
+ * with no bytes to upload, which is why 上传失败 appears today);
15
+ * * the pasted folder names go to the host half, which reads the host
16
+ * machine's clipboard (Windows file-drop list, macOS NSPasteboard file
17
+ * URLs) and answers with the real full paths;
18
+ * * those paths are inserted into the composer as plain text by re-dispatching
19
+ * a synthetic text/plain paste, so the insertion goes through DSH's own
20
+ * composer path (command menu, draft state, undo history) instead of
21
+ * poking at the DOM.
22
+ *
23
+ * Ordinary pastes — text, images, real files — are never touched: the handler
24
+ * returns before `preventDefault()` unless a directory is present.
25
+ */
26
+
27
+ /** The host route; hand-mirrored from `src/index.ts` (ROUTE_PATH). */
28
+ var ROUTE_PATH = '/dsh-folder-path/api/v1/clipboard-paths'
29
+
30
+ /** Marks the synthetic events this plugin dispatches, so they are not re-read. */
31
+ var SYNTHETIC = '__dshFolderPathSynthetic'
32
+
33
+ /** The composer's editable root, as Lexical renders it. */
34
+ var EDITABLE_SELECTOR = '[contenteditable=""],[contenteditable="true"]'
35
+
36
+ /** How long a failure notice stays on screen. */
37
+ var TOAST_MS = 5000
38
+
39
+ /** Directories and files a paste carried, split apart by `classifyPayload`. */
40
+ interface ClassifiedPayload {
41
+ directories: string[]
42
+ files: File[]
43
+ }
44
+
45
+ /** The host half's answer to a clipboard probe. */
46
+ interface HostAnswer {
47
+ paths: string[]
48
+ reason: string | null
49
+ }
50
+
51
+ /** The browser Cordis context surface this plugin uses. */
52
+ interface BrowserContext {
53
+ effect(factory: () => () => void, label: string): void
54
+ }
55
+
56
+ /**
57
+ * Split a paste payload into directories and files.
58
+ *
59
+ * `webkitGetAsEntry()` is the only reliable directory signal. The
60
+ * `size === 0 && type === ''` arm is a fallback for engines without the entry
61
+ * API; it is deliberately not applied when an entry exists, so a genuinely
62
+ * empty file is never mistaken for a folder.
63
+ *
64
+ * @param dataTransfer - the event's clipboard or drag data.
65
+ * @returns directory basenames and the non-directory files.
66
+ */
67
+ function classifyPayload(dataTransfer: DataTransfer): ClassifiedPayload {
68
+ var directories: string[] = []
69
+ var files: File[] = []
70
+ var items: DataTransferItem[] = dataTransfer.items ? Array.prototype.slice.call(dataTransfer.items) : []
71
+
72
+ for (var index = 0; index < items.length; index += 1) {
73
+ var item = items[index]
74
+ if (item.kind !== 'file') continue
75
+
76
+ var entry: FileSystemEntry | null = null
77
+ try {
78
+ entry = typeof item.webkitGetAsEntry === 'function' ? item.webkitGetAsEntry() : null
79
+ } catch (error) {
80
+ entry = null
81
+ }
82
+
83
+ if (entry !== null) {
84
+ // The entry API answers both questions. Reading the file of a directory is
85
+ // deliberately skipped: the browser hands over a zero-byte `File` whose
86
+ // bytes cannot be read, which is the upload failure this plugin exists to
87
+ // avoid, so there is nothing to forward for it.
88
+ if (entry.isDirectory === true) {
89
+ if (entry.name) directories.push(entry.name)
90
+ continue
91
+ }
92
+ var entryFile: File | null = null
93
+ try {
94
+ entryFile = item.getAsFile()
95
+ } catch (error) {
96
+ entryFile = null
97
+ }
98
+ if (entryFile !== null) files.push(entryFile)
99
+ continue
100
+ }
101
+
102
+ var file: File | null = null
103
+ try {
104
+ file = item.getAsFile()
105
+ } catch (error) {
106
+ file = null
107
+ }
108
+ if (file === null) continue
109
+ if (file.size === 0 && file.type === '') {
110
+ // No entry API (a non-Chromium engine): a zero-byte, type-less item is
111
+ // the only directory-shaped signal left to go on.
112
+ if (file.name) directories.push(file.name)
113
+ continue
114
+ }
115
+ files.push(file)
116
+ }
117
+
118
+ return { directories: directories, files: files }
119
+ }
120
+
121
+ /**
122
+ * Find the composer root a paste belongs to.
123
+ *
124
+ * A paste into anything that is not an editable surface is left alone: the
125
+ * plugin swallows events it handles, and swallowing a paste it cannot insert
126
+ * into would silently eat user data.
127
+ *
128
+ * @param target - the event target.
129
+ * @returns the editable root element, or null.
130
+ */
131
+ function editableRootOf(target: EventTarget | null): HTMLElement | null {
132
+ var candidates: Array<Element | null> = [target as Element | null, document.activeElement]
133
+ for (var index = 0; index < candidates.length; index += 1) {
134
+ var candidate = candidates[index]
135
+ if (candidate && typeof candidate.closest === 'function') {
136
+ var root = candidate.closest(EDITABLE_SELECTOR)
137
+ if (root !== null) return root as HTMLElement
138
+ }
139
+ }
140
+ return null
141
+ }
142
+
143
+ /**
144
+ * Ask the host half for the full paths behind the pasted folder names.
145
+ * @param names - directory basenames the browser saw.
146
+ * @returns the resolved paths (empty when the host could not answer).
147
+ */
148
+ async function requestPaths(names: string[]): Promise<HostAnswer> {
149
+ var response = await fetch(ROUTE_PATH, {
150
+ method: 'POST',
151
+ headers: { 'content-type': 'application/json' },
152
+ body: JSON.stringify({ names: names }),
153
+ })
154
+ if (!response.ok) return { paths: [], reason: 'http-' + String(response.status) }
155
+ var payload = await response.json()
156
+ if (payload && payload.ok === true && Array.isArray(payload.paths)) {
157
+ return { paths: payload.paths, reason: null }
158
+ }
159
+ return { paths: [], reason: (payload && payload.reason) || 'unknown' }
160
+ }
161
+
162
+ /**
163
+ * Insert plain text through DSH's own paste path.
164
+ *
165
+ * A synthetic `paste` carrying only `text/plain` reaches the same Lexical
166
+ * command a real text paste does, so the draft, command menu and undo history
167
+ * stay consistent. `execCommand` is the fallback for engines that refuse to
168
+ * construct a `ClipboardEvent` with clipboard data.
169
+ *
170
+ * @param root - the composer's editable root.
171
+ * @param text - the text to insert.
172
+ */
173
+ function insertText(root: HTMLElement, text: string): void {
174
+ root.focus({ preventScroll: true })
175
+ try {
176
+ var dataTransfer = new DataTransfer()
177
+ dataTransfer.setData('text/plain', text)
178
+ var event = new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true })
179
+ ;(event as ClipboardEvent & Record<string, unknown>)[SYNTHETIC] = true
180
+ root.dispatchEvent(event)
181
+ if (event.defaultPrevented) return
182
+ } catch (error) {
183
+ /* fall through to the DOM insertion below */
184
+ }
185
+ document.execCommand('insertText', false, text)
186
+ }
187
+
188
+ /**
189
+ * Hand non-directory files back to the composer.
190
+ *
191
+ * Copying a folder and files together is legal; the files still belong to the
192
+ * attachment flow, so they are forwarded as one synthetic file paste after the
193
+ * paths are in.
194
+ *
195
+ * @param root - the composer's editable root.
196
+ * @param files - the files taken out of the original paste.
197
+ */
198
+ function forwardFiles(root: HTMLElement, files: File[]): void {
199
+ if (files.length === 0) return
200
+ try {
201
+ var dataTransfer = new DataTransfer()
202
+ for (var index = 0; index < files.length; index += 1) dataTransfer.items.add(files[index])
203
+ var event = new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true })
204
+ ;(event as ClipboardEvent & Record<string, unknown>)[SYNTHETIC] = true
205
+ root.dispatchEvent(event)
206
+ } catch (error) {
207
+ /* the paths are the point; a failed forward must not undo them */
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Failure copy for a host answer.
213
+ * @param reason - the host's reason code.
214
+ * @returns the message to show.
215
+ */
216
+ function failureText(reason: string | null): string {
217
+ if (reason === 'unsupported-platform') return '当前宿主系统不支持读取剪贴板里的文件夹路径(仅支持 Windows / macOS)'
218
+ if (reason === 'remote-not-supported') return '当前页面不在运行 dsh web 的这台机器上,读不到这台机器的剪贴板'
219
+ if (reason === 'clipboard-error') return '读取剪贴板失败,没能解析文件夹的完整路径'
220
+ if (reason === 'no-match') return '剪贴板里的内容与粘贴的文件夹不一致,没能解析完整路径'
221
+ return '没能解析文件夹的完整路径(宿主未响应)'
222
+ }
223
+
224
+ /**
225
+ * Show a short-lived notice above the composer.
226
+ * @param message - the text to show.
227
+ */
228
+ function toast(message: string): void {
229
+ try {
230
+ var element = document.createElement('div')
231
+ element.textContent = message
232
+ element.setAttribute('data-dsh-folder-path-toast', '')
233
+ element.style.cssText =
234
+ 'position:fixed;left:50%;bottom:96px;transform:translateX(-50%);z-index:2147483000;' +
235
+ 'max-width:70vw;padding:8px 14px;border-radius:10px;background:rgba(28,28,30,0.92);color:#fff;' +
236
+ 'font-size:13px;line-height:1.45;box-shadow:0 6px 24px rgba(0,0,0,0.28);pointer-events:none;'
237
+ document.body.appendChild(element)
238
+ setTimeout(function () {
239
+ element.remove()
240
+ }, TOAST_MS)
241
+ } catch (error) {
242
+ /* a missing notice must never break the paste */
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Build the browser-half plugin from the bundle's `require`.
248
+ * @param require - the module system's synchronous require.
249
+ * @returns the plugin's `apply`.
250
+ */
251
+ function createPlugin(require: (id: string) => unknown): { apply: (ctx: BrowserContext) => void } {
252
+ /**
253
+ * Resolve the pasted folders and put their paths in the composer.
254
+ * @param root - the composer's editable root.
255
+ * @param directories - the pasted directory basenames.
256
+ * @param files - the non-directory files from the same paste.
257
+ */
258
+ async function resolveAndInsert(root: HTMLElement, directories: string[], files: File[]): Promise<void> {
259
+ var result: HostAnswer
260
+ try {
261
+ result = await requestPaths(directories)
262
+ } catch (error) {
263
+ result = { paths: [], reason: 'host-unreachable' }
264
+ }
265
+ if (result.paths.length === 0) {
266
+ toast(failureText(result.reason))
267
+ forwardFiles(root, files)
268
+ return
269
+ }
270
+ if (!root.isConnected) {
271
+ toast('输入框已经关闭,路径没有插入:' + result.paths.join('、'))
272
+ return
273
+ }
274
+ insertText(root, result.paths.join('\n'))
275
+ forwardFiles(root, files)
276
+ }
277
+
278
+ /**
279
+ * Capture-phase paste handler: intercepts directory pastes only.
280
+ * @param event - the paste event.
281
+ */
282
+ function onPaste(event: ClipboardEvent): void {
283
+ if ((event as ClipboardEvent & Record<string, unknown>)[SYNTHETIC] === true) return
284
+ if (!event.clipboardData) return
285
+
286
+ var payload = classifyPayload(event.clipboardData)
287
+ if (payload.directories.length === 0) return
288
+
289
+ var root = editableRootOf(event.target)
290
+ if (root === null) return
291
+
292
+ event.preventDefault()
293
+ event.stopPropagation()
294
+ void resolveAndInsert(root, payload.directories, payload.files)
295
+ }
296
+
297
+ /**
298
+ * Mount the listener; the returned plugin is applied by the client runner.
299
+ * @param ctx - browser Cordis context.
300
+ */
301
+ function apply(ctx: BrowserContext): void {
302
+ document.addEventListener('paste', onPaste, true)
303
+ ctx.effect(
304
+ function () {
305
+ return function () {
306
+ document.removeEventListener('paste', onPaste, true)
307
+ }
308
+ },
309
+ 'dsh-folder-path: paste listener',
310
+ )
311
+
312
+ // Troubleshooting hook: the same resolution the paste path uses, callable
313
+ // from the devtools console (`await __dshFolderPath.probe(['name'])`).
314
+ window.__dshFolderPath = {
315
+ route: ROUTE_PATH,
316
+ probe: requestPaths,
317
+ insert: insertText,
318
+ classify: classifyPayload,
319
+ }
320
+ }
321
+
322
+ return { apply: apply }
323
+ }
@@ -0,0 +1,21 @@
1
+ # The dsh-folder-path bundle patch: one row over the profile root.
2
+ #
3
+ # The row name resolves from the profile's own node_modules, where
4
+ # `dsh plugin --profile web add <path>` installed this package. Both halves of
5
+ # the plugin ride this one row: the host half registers the clipboard route, and
6
+ # the browser half (`dsh.client.platform: web` in package.json) is served as a
7
+ # client bundle the module system loads at boot.
8
+ #
9
+ # Every knob is optional. `enabled: false` mounts the host half inert; the
10
+ # browser half then keeps its hands off folder pastes and DSH behaves as before.
11
+ - insert:
12
+ - id: folder-path
13
+ name: dsh-folder-path
14
+ # config:
15
+ # enabled: true
16
+ # timeoutMs: 5000
17
+ # # Executable used for the clipboard probe, per platform:
18
+ # # Windows — PowerShell (default: powershell.exe); it needs Windows Forms.
19
+ # # macOS — osascript (default: /usr/bin/osascript), run with -l JavaScript.
20
+ # powershell: powershell.exe
21
+ # osascript: /usr/bin/osascript
package/docs/DETAIL.md ADDED
@@ -0,0 +1,10 @@
1
+ # Paste folder path
2
+
3
+ Pasting a folder into the composer normally tries to upload it and fails. This plugin reads the folder path from the system clipboard and inserts it instead.
4
+
5
+ ## Behavior
6
+ - Windows and macOS only; other platforms report unsupported-platform.
7
+ - The client half intercepts paste; the host half reads the system clipboard (CF_HDROP on Windows, NSPasteboard on macOS).
8
+
9
+ ## Notes
10
+ - Falls back to default behavior when the clipboard is unavailable.
@@ -0,0 +1,10 @@
1
+ # 粘贴文件夹路径
2
+
3
+ 浏览器里往输入框粘贴文件夹时,默认行为是尝试上传(文件夹无法上传,只会失败)。这个插件改成读取系统剪贴板里的文件夹路径并插入输入框。
4
+
5
+ ## 行为
6
+ - 仅 Windows / macOS 生效;其它平台返回 unsupported-platform。
7
+ - 浏览器半边拦截粘贴事件,宿主半边读系统剪贴板拿路径(Windows 走 CF_HDROP,macOS 走 NSPasteboard)。
8
+
9
+ ## 注意
10
+ - 需要宿主能读系统剪贴板;远程或无剪贴板权限时退回默认行为。
package/icon.svg ADDED
@@ -0,0 +1 @@
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 11a2 2 0 012-2h6l2.5 3H28a2 2 0 012 2v11a2 2 0 01-2 2H8a2 2 0 01-2-2V11z" fill="#22C55E"/><path d="M18 16v8m0 0l-3-3m3 3l3-3" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
package/lib/client.js ADDED
@@ -0,0 +1,318 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-folder-path",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ // ---- .client-build/index.js ----
8
+ "use strict";
9
+ /**
10
+ * `dsh-folder-path` browser half.
11
+ *
12
+ * Shipped as the package's `./client` bundle: the DSH client module system
13
+ * loads it as a `window.__ModuleLoader__.load({ id, factory })` script, calls
14
+ * `factory(require)`, and mounts the exported plugin (`apply`) into the browser
15
+ * Cordis tree.
16
+ *
17
+ * What it does — one thing, in the capture phase, before the conversation
18
+ * plugin's Lexical paste command can turn the folder into a doomed upload:
19
+ *
20
+ * * a paste is inspected for entries that are *directories*
21
+ * (`webkitGetAsEntry().isDirectory`; the paste payload is a `File` of size 0
22
+ * with no bytes to upload, which is why 上传失败 appears today);
23
+ * * the pasted folder names go to the host half, which reads the host
24
+ * machine's clipboard (Windows file-drop list, macOS NSPasteboard file
25
+ * URLs) and answers with the real full paths;
26
+ * * those paths are inserted into the composer as plain text by re-dispatching
27
+ * a synthetic text/plain paste, so the insertion goes through DSH's own
28
+ * composer path (command menu, draft state, undo history) instead of
29
+ * poking at the DOM.
30
+ *
31
+ * Ordinary pastes — text, images, real files — are never touched: the handler
32
+ * returns before `preventDefault()` unless a directory is present.
33
+ */
34
+ /** The host route; hand-mirrored from `src/index.ts` (ROUTE_PATH). */
35
+ var ROUTE_PATH = '/dsh-folder-path/api/v1/clipboard-paths';
36
+ /** Marks the synthetic events this plugin dispatches, so they are not re-read. */
37
+ var SYNTHETIC = '__dshFolderPathSynthetic';
38
+ /** The composer's editable root, as Lexical renders it. */
39
+ var EDITABLE_SELECTOR = '[contenteditable=""],[contenteditable="true"]';
40
+ /** How long a failure notice stays on screen. */
41
+ var TOAST_MS = 5000;
42
+ /**
43
+ * Split a paste payload into directories and files.
44
+ *
45
+ * `webkitGetAsEntry()` is the only reliable directory signal. The
46
+ * `size === 0 && type === ''` arm is a fallback for engines without the entry
47
+ * API; it is deliberately not applied when an entry exists, so a genuinely
48
+ * empty file is never mistaken for a folder.
49
+ *
50
+ * @param dataTransfer - the event's clipboard or drag data.
51
+ * @returns directory basenames and the non-directory files.
52
+ */
53
+ function classifyPayload(dataTransfer) {
54
+ var directories = [];
55
+ var files = [];
56
+ var items = dataTransfer.items ? Array.prototype.slice.call(dataTransfer.items) : [];
57
+ for (var index = 0; index < items.length; index += 1) {
58
+ var item = items[index];
59
+ if (item.kind !== 'file')
60
+ continue;
61
+ var entry = null;
62
+ try {
63
+ entry = typeof item.webkitGetAsEntry === 'function' ? item.webkitGetAsEntry() : null;
64
+ }
65
+ catch (error) {
66
+ entry = null;
67
+ }
68
+ if (entry !== null) {
69
+ // The entry API answers both questions. Reading the file of a directory is
70
+ // deliberately skipped: the browser hands over a zero-byte `File` whose
71
+ // bytes cannot be read, which is the upload failure this plugin exists to
72
+ // avoid, so there is nothing to forward for it.
73
+ if (entry.isDirectory === true) {
74
+ if (entry.name)
75
+ directories.push(entry.name);
76
+ continue;
77
+ }
78
+ var entryFile = null;
79
+ try {
80
+ entryFile = item.getAsFile();
81
+ }
82
+ catch (error) {
83
+ entryFile = null;
84
+ }
85
+ if (entryFile !== null)
86
+ files.push(entryFile);
87
+ continue;
88
+ }
89
+ var file = null;
90
+ try {
91
+ file = item.getAsFile();
92
+ }
93
+ catch (error) {
94
+ file = null;
95
+ }
96
+ if (file === null)
97
+ continue;
98
+ if (file.size === 0 && file.type === '') {
99
+ // No entry API (a non-Chromium engine): a zero-byte, type-less item is
100
+ // the only directory-shaped signal left to go on.
101
+ if (file.name)
102
+ directories.push(file.name);
103
+ continue;
104
+ }
105
+ files.push(file);
106
+ }
107
+ return { directories: directories, files: files };
108
+ }
109
+ /**
110
+ * Find the composer root a paste belongs to.
111
+ *
112
+ * A paste into anything that is not an editable surface is left alone: the
113
+ * plugin swallows events it handles, and swallowing a paste it cannot insert
114
+ * into would silently eat user data.
115
+ *
116
+ * @param target - the event target.
117
+ * @returns the editable root element, or null.
118
+ */
119
+ function editableRootOf(target) {
120
+ var candidates = [target, document.activeElement];
121
+ for (var index = 0; index < candidates.length; index += 1) {
122
+ var candidate = candidates[index];
123
+ if (candidate && typeof candidate.closest === 'function') {
124
+ var root = candidate.closest(EDITABLE_SELECTOR);
125
+ if (root !== null)
126
+ return root;
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+ /**
132
+ * Ask the host half for the full paths behind the pasted folder names.
133
+ * @param names - directory basenames the browser saw.
134
+ * @returns the resolved paths (empty when the host could not answer).
135
+ */
136
+ async function requestPaths(names) {
137
+ var response = await fetch(ROUTE_PATH, {
138
+ method: 'POST',
139
+ headers: { 'content-type': 'application/json' },
140
+ body: JSON.stringify({ names: names }),
141
+ });
142
+ if (!response.ok)
143
+ return { paths: [], reason: 'http-' + String(response.status) };
144
+ var payload = await response.json();
145
+ if (payload && payload.ok === true && Array.isArray(payload.paths)) {
146
+ return { paths: payload.paths, reason: null };
147
+ }
148
+ return { paths: [], reason: (payload && payload.reason) || 'unknown' };
149
+ }
150
+ /**
151
+ * Insert plain text through DSH's own paste path.
152
+ *
153
+ * A synthetic `paste` carrying only `text/plain` reaches the same Lexical
154
+ * command a real text paste does, so the draft, command menu and undo history
155
+ * stay consistent. `execCommand` is the fallback for engines that refuse to
156
+ * construct a `ClipboardEvent` with clipboard data.
157
+ *
158
+ * @param root - the composer's editable root.
159
+ * @param text - the text to insert.
160
+ */
161
+ function insertText(root, text) {
162
+ root.focus({ preventScroll: true });
163
+ try {
164
+ var dataTransfer = new DataTransfer();
165
+ dataTransfer.setData('text/plain', text);
166
+ var event = new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true });
167
+ event[SYNTHETIC] = true;
168
+ root.dispatchEvent(event);
169
+ if (event.defaultPrevented)
170
+ return;
171
+ }
172
+ catch (error) {
173
+ /* fall through to the DOM insertion below */
174
+ }
175
+ document.execCommand('insertText', false, text);
176
+ }
177
+ /**
178
+ * Hand non-directory files back to the composer.
179
+ *
180
+ * Copying a folder and files together is legal; the files still belong to the
181
+ * attachment flow, so they are forwarded as one synthetic file paste after the
182
+ * paths are in.
183
+ *
184
+ * @param root - the composer's editable root.
185
+ * @param files - the files taken out of the original paste.
186
+ */
187
+ function forwardFiles(root, files) {
188
+ if (files.length === 0)
189
+ return;
190
+ try {
191
+ var dataTransfer = new DataTransfer();
192
+ for (var index = 0; index < files.length; index += 1)
193
+ dataTransfer.items.add(files[index]);
194
+ var event = new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true });
195
+ event[SYNTHETIC] = true;
196
+ root.dispatchEvent(event);
197
+ }
198
+ catch (error) {
199
+ /* the paths are the point; a failed forward must not undo them */
200
+ }
201
+ }
202
+ /**
203
+ * Failure copy for a host answer.
204
+ * @param reason - the host's reason code.
205
+ * @returns the message to show.
206
+ */
207
+ function failureText(reason) {
208
+ if (reason === 'unsupported-platform')
209
+ return '当前宿主系统不支持读取剪贴板里的文件夹路径(仅支持 Windows / macOS)';
210
+ if (reason === 'remote-not-supported')
211
+ return '当前页面不在运行 dsh web 的这台机器上,读不到这台机器的剪贴板';
212
+ if (reason === 'clipboard-error')
213
+ return '读取剪贴板失败,没能解析文件夹的完整路径';
214
+ if (reason === 'no-match')
215
+ return '剪贴板里的内容与粘贴的文件夹不一致,没能解析完整路径';
216
+ return '没能解析文件夹的完整路径(宿主未响应)';
217
+ }
218
+ /**
219
+ * Show a short-lived notice above the composer.
220
+ * @param message - the text to show.
221
+ */
222
+ function toast(message) {
223
+ try {
224
+ var element = document.createElement('div');
225
+ element.textContent = message;
226
+ element.setAttribute('data-dsh-folder-path-toast', '');
227
+ element.style.cssText =
228
+ 'position:fixed;left:50%;bottom:96px;transform:translateX(-50%);z-index:2147483000;' +
229
+ 'max-width:70vw;padding:8px 14px;border-radius:10px;background:rgba(28,28,30,0.92);color:#fff;' +
230
+ 'font-size:13px;line-height:1.45;box-shadow:0 6px 24px rgba(0,0,0,0.28);pointer-events:none;';
231
+ document.body.appendChild(element);
232
+ setTimeout(function () {
233
+ element.remove();
234
+ }, TOAST_MS);
235
+ }
236
+ catch (error) {
237
+ /* a missing notice must never break the paste */
238
+ }
239
+ }
240
+ /**
241
+ * Build the browser-half plugin from the bundle's `require`.
242
+ * @param require - the module system's synchronous require.
243
+ * @returns the plugin's `apply`.
244
+ */
245
+ function createPlugin(require) {
246
+ /**
247
+ * Resolve the pasted folders and put their paths in the composer.
248
+ * @param root - the composer's editable root.
249
+ * @param directories - the pasted directory basenames.
250
+ * @param files - the non-directory files from the same paste.
251
+ */
252
+ async function resolveAndInsert(root, directories, files) {
253
+ var result;
254
+ try {
255
+ result = await requestPaths(directories);
256
+ }
257
+ catch (error) {
258
+ result = { paths: [], reason: 'host-unreachable' };
259
+ }
260
+ if (result.paths.length === 0) {
261
+ toast(failureText(result.reason));
262
+ forwardFiles(root, files);
263
+ return;
264
+ }
265
+ if (!root.isConnected) {
266
+ toast('输入框已经关闭,路径没有插入:' + result.paths.join('、'));
267
+ return;
268
+ }
269
+ insertText(root, result.paths.join('\n'));
270
+ forwardFiles(root, files);
271
+ }
272
+ /**
273
+ * Capture-phase paste handler: intercepts directory pastes only.
274
+ * @param event - the paste event.
275
+ */
276
+ function onPaste(event) {
277
+ if (event[SYNTHETIC] === true)
278
+ return;
279
+ if (!event.clipboardData)
280
+ return;
281
+ var payload = classifyPayload(event.clipboardData);
282
+ if (payload.directories.length === 0)
283
+ return;
284
+ var root = editableRootOf(event.target);
285
+ if (root === null)
286
+ return;
287
+ event.preventDefault();
288
+ event.stopPropagation();
289
+ void resolveAndInsert(root, payload.directories, payload.files);
290
+ }
291
+ /**
292
+ * Mount the listener; the returned plugin is applied by the client runner.
293
+ * @param ctx - browser Cordis context.
294
+ */
295
+ function apply(ctx) {
296
+ document.addEventListener('paste', onPaste, true);
297
+ ctx.effect(function () {
298
+ return function () {
299
+ document.removeEventListener('paste', onPaste, true);
300
+ };
301
+ }, 'dsh-folder-path: paste listener');
302
+ // Troubleshooting hook: the same resolution the paste path uses, callable
303
+ // from the devtools console (`await __dshFolderPath.probe(['name'])`).
304
+ window.__dshFolderPath = {
305
+ route: ROUTE_PATH,
306
+ probe: requestPaths,
307
+ insert: insertText,
308
+ classify: classifyPayload,
309
+ };
310
+ }
311
+ return { apply: apply };
312
+ }
313
+
314
+ const plugin = createPlugin(require);
315
+ exports.apply = plugin.apply;
316
+ return module.exports;
317
+ }
318
+ });