dsh-folder-path 0.1.0 → 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.
- 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/lib/client.js
CHANGED
|
@@ -4,7 +4,8 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
// ---- client/index.js ----
|
|
7
|
+
// ---- .client-build/index.js ----
|
|
8
|
+
"use strict";
|
|
8
9
|
/**
|
|
9
10
|
* `dsh-folder-path` browser half.
|
|
10
11
|
*
|
|
@@ -30,19 +31,14 @@ window.__ModuleLoader__.load({
|
|
|
30
31
|
* Ordinary pastes — text, images, real files — are never touched: the handler
|
|
31
32
|
* returns before `preventDefault()` unless a directory is present.
|
|
32
33
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
var ROUTE_PATH = '/dsh-folder-path/api/v1/clipboard-paths'
|
|
36
|
-
|
|
34
|
+
/** The host route; hand-mirrored from `src/index.ts` (ROUTE_PATH). */
|
|
35
|
+
var ROUTE_PATH = '/dsh-folder-path/api/v1/clipboard-paths';
|
|
37
36
|
/** Marks the synthetic events this plugin dispatches, so they are not re-read. */
|
|
38
|
-
var SYNTHETIC = '__dshFolderPathSynthetic'
|
|
39
|
-
|
|
37
|
+
var SYNTHETIC = '__dshFolderPathSynthetic';
|
|
40
38
|
/** The composer's editable root, as Lexical renders it. */
|
|
41
|
-
var EDITABLE_SELECTOR = '[contenteditable=""],[contenteditable="true"]'
|
|
42
|
-
|
|
39
|
+
var EDITABLE_SELECTOR = '[contenteditable=""],[contenteditable="true"]';
|
|
43
40
|
/** How long a failure notice stays on screen. */
|
|
44
|
-
var TOAST_MS = 5000
|
|
45
|
-
|
|
41
|
+
var TOAST_MS = 5000;
|
|
46
42
|
/**
|
|
47
43
|
* Split a paste payload into directories and files.
|
|
48
44
|
*
|
|
@@ -55,59 +51,61 @@ window.__ModuleLoader__.load({
|
|
|
55
51
|
* @returns directory basenames and the non-directory files.
|
|
56
52
|
*/
|
|
57
53
|
function classifyPayload(dataTransfer) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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);
|
|
104
106
|
}
|
|
105
|
-
files
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return { directories: directories, files: files }
|
|
107
|
+
return { directories: directories, files: files };
|
|
109
108
|
}
|
|
110
|
-
|
|
111
109
|
/**
|
|
112
110
|
* Find the composer root a paste belongs to.
|
|
113
111
|
*
|
|
@@ -119,36 +117,36 @@ window.__ModuleLoader__.load({
|
|
|
119
117
|
* @returns the editable root element, or null.
|
|
120
118
|
*/
|
|
121
119
|
function editableRootOf(target) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
128
|
}
|
|
129
|
-
|
|
130
|
-
return null
|
|
129
|
+
return null;
|
|
131
130
|
}
|
|
132
|
-
|
|
133
131
|
/**
|
|
134
132
|
* Ask the host half for the full paths behind the pasted folder names.
|
|
135
133
|
* @param names - directory basenames the browser saw.
|
|
136
134
|
* @returns the resolved paths (empty when the host could not answer).
|
|
137
135
|
*/
|
|
138
136
|
async function requestPaths(names) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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' };
|
|
150
149
|
}
|
|
151
|
-
|
|
152
150
|
/**
|
|
153
151
|
* Insert plain text through DSH's own paste path.
|
|
154
152
|
*
|
|
@@ -161,20 +159,21 @@ window.__ModuleLoader__.load({
|
|
|
161
159
|
* @param text - the text to insert.
|
|
162
160
|
*/
|
|
163
161
|
function insertText(root, text) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
176
|
}
|
|
177
|
-
|
|
178
177
|
/**
|
|
179
178
|
* Hand non-directory files back to the composer.
|
|
180
179
|
*
|
|
@@ -186,130 +185,130 @@ window.__ModuleLoader__.load({
|
|
|
186
185
|
* @param files - the files taken out of the original paste.
|
|
187
186
|
*/
|
|
188
187
|
function forwardFiles(root, files) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|
|
199
201
|
}
|
|
200
|
-
|
|
201
202
|
/**
|
|
202
203
|
* Failure copy for a host answer.
|
|
203
204
|
* @param reason - the host's reason code.
|
|
204
205
|
* @returns the message to show.
|
|
205
206
|
*/
|
|
206
207
|
function failureText(reason) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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 '没能解析文件夹的完整路径(宿主未响应)';
|
|
212
217
|
}
|
|
213
|
-
|
|
214
218
|
/**
|
|
215
219
|
* Show a short-lived notice above the composer.
|
|
216
220
|
* @param message - the text to show.
|
|
217
221
|
*/
|
|
218
222
|
function toast(message) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
+
}
|
|
234
239
|
}
|
|
235
|
-
|
|
236
240
|
/**
|
|
237
241
|
* Build the browser-half plugin from the bundle's `require`.
|
|
238
242
|
* @param require - the module system's synchronous require.
|
|
239
243
|
* @returns the plugin's `apply`.
|
|
240
244
|
*/
|
|
241
245
|
function createPlugin(require) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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);
|
|
254
271
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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);
|
|
259
290
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
};
|
|
263
310
|
}
|
|
264
|
-
|
|
265
|
-
forwardFiles(root, files)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Capture-phase paste handler: intercepts directory pastes only.
|
|
270
|
-
* @param event - the paste event.
|
|
271
|
-
*/
|
|
272
|
-
function onPaste(event) {
|
|
273
|
-
if (event[SYNTHETIC] === true) return
|
|
274
|
-
if (!event.clipboardData) return
|
|
275
|
-
|
|
276
|
-
var payload = classifyPayload(event.clipboardData)
|
|
277
|
-
if (payload.directories.length === 0) return
|
|
278
|
-
|
|
279
|
-
var root = editableRootOf(event.target)
|
|
280
|
-
if (root === null) return
|
|
281
|
-
|
|
282
|
-
event.preventDefault()
|
|
283
|
-
event.stopPropagation()
|
|
284
|
-
void resolveAndInsert(root, payload.directories, payload.files)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Mount the listener; the returned plugin is applied by the client runner.
|
|
289
|
-
* @param ctx - browser Cordis context.
|
|
290
|
-
*/
|
|
291
|
-
function apply(ctx) {
|
|
292
|
-
document.addEventListener('paste', onPaste, true)
|
|
293
|
-
ctx.effect(
|
|
294
|
-
function () {
|
|
295
|
-
return function () {
|
|
296
|
-
document.removeEventListener('paste', onPaste, true)
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
'dsh-folder-path: paste listener',
|
|
300
|
-
)
|
|
301
|
-
|
|
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
|
-
|
|
312
|
-
return { apply: apply }
|
|
311
|
+
return { apply: apply };
|
|
313
312
|
}
|
|
314
313
|
|
|
315
314
|
const plugin = createPlugin(require);
|