dsh-folder-path 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/README_EN.md +2 -2
- package/client/globals.d.ts +30 -0
- package/client/{index.js → index.ts} +41 -24
- package/lib/client.js +196 -197
- package/lib/index.js +234 -258
- package/lib/types/index.d.ts +110 -0
- package/package.json +11 -4
- package/scripts/{build-client.mjs → build-client.mts} +9 -8
package/README.md
CHANGED
|
@@ -128,10 +128,13 @@ dsh plugin --profile <profile> remove dsh-folder-path
|
|
|
128
128
|
## 开发
|
|
129
129
|
|
|
130
130
|
```sh
|
|
131
|
-
npm run build # client/index.
|
|
132
|
-
npm test #
|
|
131
|
+
npm run build # tsc: src/index.ts -> lib/index.js,client/index.ts -> .client-build/index.js;再包成 lib/client.js
|
|
132
|
+
npm test # 先 build,再跑宿主路由 / 浏览器半契约 / 清单契约测试(node --test,无需 DSH 服务器)
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
宿主半边源码在 `src/index.ts`(TypeScript,`lib/index.js` 与 `lib/types/` 是 tsc 产物,随仓库提交);
|
|
136
|
+
浏览器半边是 `client/index.ts`(`.client-build/index.js` 是 tsc 产物),由构建脚本包成 `lib/client.js`。
|
|
137
|
+
|
|
135
138
|
浏览器侧调试钩子(devtools 控制台):
|
|
136
139
|
|
|
137
140
|
```js
|
package/README_EN.md
CHANGED
|
@@ -145,8 +145,8 @@ or this package's own `cordis.patch.yml`, which `dsh plugin add` wires in):
|
|
|
145
145
|
## Development
|
|
146
146
|
|
|
147
147
|
```sh
|
|
148
|
-
npm run build # client/index.js -> lib/client.js (wraps the browser half as a __ModuleLoader__ bundle)
|
|
149
|
-
npm test #
|
|
148
|
+
npm run build # client/index.ts -> .client-build/index.js -> lib/client.js (wraps the browser half as a __ModuleLoader__ bundle)
|
|
149
|
+
npm test # builds both halves, then host route / browser-half contract / manifest contract tests
|
|
150
150
|
# (node --test, no DSH server needed)
|
|
151
151
|
```
|
|
152
152
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient declarations for the browser half.
|
|
3
|
+
*
|
|
4
|
+
* `client/index.ts` is a classic script (it has no imports or exports), so the
|
|
5
|
+
* bundle wrapper in `scripts/build-client.mts` can embed it inside a
|
|
6
|
+
* `window.__ModuleLoader__.load({ id, factory })` factory unchanged. The DOM
|
|
7
|
+
* types come from the `DOM` lib in `tsconfig.client.json`; only the two
|
|
8
|
+
* page globals this plugin defines or consumes are declared here.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The troubleshooting hook the browser half installs on `window`. */
|
|
12
|
+
interface FolderPathDebugHook {
|
|
13
|
+
/** The host route the plugin calls. */
|
|
14
|
+
route: string
|
|
15
|
+
/** Ask the host for the full paths behind these folder names. */
|
|
16
|
+
probe(names: string[]): Promise<{ paths: string[]; reason: string | null }>
|
|
17
|
+
/** Insert text through DSH's own paste path. */
|
|
18
|
+
insert(root: HTMLElement, text: string): void
|
|
19
|
+
/** Split a paste payload into directories and files. */
|
|
20
|
+
classify(dataTransfer: DataTransfer): { directories: string[]; files: File[] }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Window {
|
|
24
|
+
/** The DSH client module system; the bundle wrapper registers through it. */
|
|
25
|
+
__ModuleLoader__: {
|
|
26
|
+
load(registration: { id: string; factory: (require: (id: string) => unknown) => unknown }): void
|
|
27
|
+
}
|
|
28
|
+
/** Installed by `apply` so the devtools console can drive the plugin. */
|
|
29
|
+
__dshFolderPath?: FolderPathDebugHook
|
|
30
|
+
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* returns before `preventDefault()` unless a directory is present.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
/** The host route; hand-mirrored from `
|
|
27
|
+
/** The host route; hand-mirrored from `src/index.ts` (ROUTE_PATH). */
|
|
28
28
|
var ROUTE_PATH = '/dsh-folder-path/api/v1/clipboard-paths'
|
|
29
29
|
|
|
30
30
|
/** Marks the synthetic events this plugin dispatches, so they are not re-read. */
|
|
@@ -36,6 +36,23 @@ var EDITABLE_SELECTOR = '[contenteditable=""],[contenteditable="true"]'
|
|
|
36
36
|
/** How long a failure notice stays on screen. */
|
|
37
37
|
var TOAST_MS = 5000
|
|
38
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
|
+
|
|
39
56
|
/**
|
|
40
57
|
* Split a paste payload into directories and files.
|
|
41
58
|
*
|
|
@@ -47,16 +64,16 @@ var TOAST_MS = 5000
|
|
|
47
64
|
* @param dataTransfer - the event's clipboard or drag data.
|
|
48
65
|
* @returns directory basenames and the non-directory files.
|
|
49
66
|
*/
|
|
50
|
-
function classifyPayload(dataTransfer) {
|
|
51
|
-
var directories = []
|
|
52
|
-
var files = []
|
|
53
|
-
var items = dataTransfer.items ? Array.prototype.slice.call(dataTransfer.items) : []
|
|
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) : []
|
|
54
71
|
|
|
55
72
|
for (var index = 0; index < items.length; index += 1) {
|
|
56
73
|
var item = items[index]
|
|
57
74
|
if (item.kind !== 'file') continue
|
|
58
75
|
|
|
59
|
-
var entry = null
|
|
76
|
+
var entry: FileSystemEntry | null = null
|
|
60
77
|
try {
|
|
61
78
|
entry = typeof item.webkitGetAsEntry === 'function' ? item.webkitGetAsEntry() : null
|
|
62
79
|
} catch (error) {
|
|
@@ -72,7 +89,7 @@ function classifyPayload(dataTransfer) {
|
|
|
72
89
|
if (entry.name) directories.push(entry.name)
|
|
73
90
|
continue
|
|
74
91
|
}
|
|
75
|
-
var entryFile = null
|
|
92
|
+
var entryFile: File | null = null
|
|
76
93
|
try {
|
|
77
94
|
entryFile = item.getAsFile()
|
|
78
95
|
} catch (error) {
|
|
@@ -82,7 +99,7 @@ function classifyPayload(dataTransfer) {
|
|
|
82
99
|
continue
|
|
83
100
|
}
|
|
84
101
|
|
|
85
|
-
var file = null
|
|
102
|
+
var file: File | null = null
|
|
86
103
|
try {
|
|
87
104
|
file = item.getAsFile()
|
|
88
105
|
} catch (error) {
|
|
@@ -111,13 +128,13 @@ function classifyPayload(dataTransfer) {
|
|
|
111
128
|
* @param target - the event target.
|
|
112
129
|
* @returns the editable root element, or null.
|
|
113
130
|
*/
|
|
114
|
-
function editableRootOf(target) {
|
|
115
|
-
var candidates = [target, document.activeElement]
|
|
131
|
+
function editableRootOf(target: EventTarget | null): HTMLElement | null {
|
|
132
|
+
var candidates: Array<Element | null> = [target as Element | null, document.activeElement]
|
|
116
133
|
for (var index = 0; index < candidates.length; index += 1) {
|
|
117
134
|
var candidate = candidates[index]
|
|
118
135
|
if (candidate && typeof candidate.closest === 'function') {
|
|
119
136
|
var root = candidate.closest(EDITABLE_SELECTOR)
|
|
120
|
-
if (root !== null) return root
|
|
137
|
+
if (root !== null) return root as HTMLElement
|
|
121
138
|
}
|
|
122
139
|
}
|
|
123
140
|
return null
|
|
@@ -128,7 +145,7 @@ function editableRootOf(target) {
|
|
|
128
145
|
* @param names - directory basenames the browser saw.
|
|
129
146
|
* @returns the resolved paths (empty when the host could not answer).
|
|
130
147
|
*/
|
|
131
|
-
async function requestPaths(names) {
|
|
148
|
+
async function requestPaths(names: string[]): Promise<HostAnswer> {
|
|
132
149
|
var response = await fetch(ROUTE_PATH, {
|
|
133
150
|
method: 'POST',
|
|
134
151
|
headers: { 'content-type': 'application/json' },
|
|
@@ -153,13 +170,13 @@ async function requestPaths(names) {
|
|
|
153
170
|
* @param root - the composer's editable root.
|
|
154
171
|
* @param text - the text to insert.
|
|
155
172
|
*/
|
|
156
|
-
function insertText(root, text) {
|
|
173
|
+
function insertText(root: HTMLElement, text: string): void {
|
|
157
174
|
root.focus({ preventScroll: true })
|
|
158
175
|
try {
|
|
159
176
|
var dataTransfer = new DataTransfer()
|
|
160
177
|
dataTransfer.setData('text/plain', text)
|
|
161
178
|
var event = new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true })
|
|
162
|
-
event[SYNTHETIC] = true
|
|
179
|
+
;(event as ClipboardEvent & Record<string, unknown>)[SYNTHETIC] = true
|
|
163
180
|
root.dispatchEvent(event)
|
|
164
181
|
if (event.defaultPrevented) return
|
|
165
182
|
} catch (error) {
|
|
@@ -178,13 +195,13 @@ function insertText(root, text) {
|
|
|
178
195
|
* @param root - the composer's editable root.
|
|
179
196
|
* @param files - the files taken out of the original paste.
|
|
180
197
|
*/
|
|
181
|
-
function forwardFiles(root, files) {
|
|
198
|
+
function forwardFiles(root: HTMLElement, files: File[]): void {
|
|
182
199
|
if (files.length === 0) return
|
|
183
200
|
try {
|
|
184
201
|
var dataTransfer = new DataTransfer()
|
|
185
202
|
for (var index = 0; index < files.length; index += 1) dataTransfer.items.add(files[index])
|
|
186
203
|
var event = new ClipboardEvent('paste', { clipboardData: dataTransfer, bubbles: true, cancelable: true })
|
|
187
|
-
event[SYNTHETIC] = true
|
|
204
|
+
;(event as ClipboardEvent & Record<string, unknown>)[SYNTHETIC] = true
|
|
188
205
|
root.dispatchEvent(event)
|
|
189
206
|
} catch (error) {
|
|
190
207
|
/* the paths are the point; a failed forward must not undo them */
|
|
@@ -196,7 +213,7 @@ function forwardFiles(root, files) {
|
|
|
196
213
|
* @param reason - the host's reason code.
|
|
197
214
|
* @returns the message to show.
|
|
198
215
|
*/
|
|
199
|
-
function failureText(reason) {
|
|
216
|
+
function failureText(reason: string | null): string {
|
|
200
217
|
if (reason === 'unsupported-platform') return '当前宿主系统不支持读取剪贴板里的文件夹路径(仅支持 Windows / macOS)'
|
|
201
218
|
if (reason === 'remote-not-supported') return '当前页面不在运行 dsh web 的这台机器上,读不到这台机器的剪贴板'
|
|
202
219
|
if (reason === 'clipboard-error') return '读取剪贴板失败,没能解析文件夹的完整路径'
|
|
@@ -208,7 +225,7 @@ function failureText(reason) {
|
|
|
208
225
|
* Show a short-lived notice above the composer.
|
|
209
226
|
* @param message - the text to show.
|
|
210
227
|
*/
|
|
211
|
-
function toast(message) {
|
|
228
|
+
function toast(message: string): void {
|
|
212
229
|
try {
|
|
213
230
|
var element = document.createElement('div')
|
|
214
231
|
element.textContent = message
|
|
@@ -231,15 +248,15 @@ function toast(message) {
|
|
|
231
248
|
* @param require - the module system's synchronous require.
|
|
232
249
|
* @returns the plugin's `apply`.
|
|
233
250
|
*/
|
|
234
|
-
function createPlugin(require) {
|
|
251
|
+
function createPlugin(require: (id: string) => unknown): { apply: (ctx: BrowserContext) => void } {
|
|
235
252
|
/**
|
|
236
253
|
* Resolve the pasted folders and put their paths in the composer.
|
|
237
254
|
* @param root - the composer's editable root.
|
|
238
255
|
* @param directories - the pasted directory basenames.
|
|
239
256
|
* @param files - the non-directory files from the same paste.
|
|
240
257
|
*/
|
|
241
|
-
async function resolveAndInsert(root, directories, files) {
|
|
242
|
-
var result
|
|
258
|
+
async function resolveAndInsert(root: HTMLElement, directories: string[], files: File[]): Promise<void> {
|
|
259
|
+
var result: HostAnswer
|
|
243
260
|
try {
|
|
244
261
|
result = await requestPaths(directories)
|
|
245
262
|
} catch (error) {
|
|
@@ -262,8 +279,8 @@ function createPlugin(require) {
|
|
|
262
279
|
* Capture-phase paste handler: intercepts directory pastes only.
|
|
263
280
|
* @param event - the paste event.
|
|
264
281
|
*/
|
|
265
|
-
function onPaste(event) {
|
|
266
|
-
if (event[SYNTHETIC] === true) return
|
|
282
|
+
function onPaste(event: ClipboardEvent): void {
|
|
283
|
+
if ((event as ClipboardEvent & Record<string, unknown>)[SYNTHETIC] === true) return
|
|
267
284
|
if (!event.clipboardData) return
|
|
268
285
|
|
|
269
286
|
var payload = classifyPayload(event.clipboardData)
|
|
@@ -281,7 +298,7 @@ function createPlugin(require) {
|
|
|
281
298
|
* Mount the listener; the returned plugin is applied by the client runner.
|
|
282
299
|
* @param ctx - browser Cordis context.
|
|
283
300
|
*/
|
|
284
|
-
function apply(ctx) {
|
|
301
|
+
function apply(ctx: BrowserContext): void {
|
|
285
302
|
document.addEventListener('paste', onPaste, true)
|
|
286
303
|
ctx.effect(
|
|
287
304
|
function () {
|