@xcanwin/manyoyo 5.9.2 → 5.9.11

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.
@@ -31583,8 +31583,10 @@
31583
31583
  const initialDoc = String(options.doc || "");
31584
31584
  const initialLanguage = String(options.language || "text").trim();
31585
31585
  const initialReadOnly = options.readOnly !== false;
31586
+ const onChange = typeof options.onChange === "function" ? options.onChange : null;
31586
31587
  const languageCompartment = new Compartment();
31587
31588
  const readOnlyCompartment = new Compartment();
31589
+ let suppressChange = false;
31588
31590
  const view = new EditorView({
31589
31591
  parent: target,
31590
31592
  state: EditorState.create({
@@ -31592,6 +31594,12 @@
31592
31594
  extensions: [
31593
31595
  basicSetup,
31594
31596
  EditorView.lineWrapping,
31597
+ EditorView.updateListener.of(function(update) {
31598
+ if (!update.docChanged || suppressChange || !onChange) {
31599
+ return;
31600
+ }
31601
+ onChange(update.state.doc.toString());
31602
+ }),
31595
31603
  readOnlyCompartment.of([
31596
31604
  EditorState.readOnly.of(initialReadOnly),
31597
31605
  EditorView.editable.of(!initialReadOnly)
@@ -31612,6 +31620,7 @@
31612
31620
  return {
31613
31621
  setValue(nextValue) {
31614
31622
  const text = String(nextValue == null ? "" : nextValue);
31623
+ suppressChange = true;
31615
31624
  view.dispatch({
31616
31625
  changes: {
31617
31626
  from: 0,
@@ -31619,6 +31628,10 @@
31619
31628
  insert: text
31620
31629
  }
31621
31630
  });
31631
+ suppressChange = false;
31632
+ },
31633
+ getValue() {
31634
+ return view.state.doc.toString();
31622
31635
  },
31623
31636
  setLanguage(nextLanguage) {
31624
31637
  view.dispatch({
@@ -1,4 +1,5 @@
1
1
  (function () {
2
+ const FILE_EDIT_MAX_BYTES = 2 * 1024 * 1024;
2
3
  function escapeHtml(value) {
3
4
  return String(value == null ? '' : value)
4
5
  .replace(/&/g, '&')
@@ -83,10 +84,14 @@
83
84
  root.innerHTML = `
84
85
  <section class="files-browser">
85
86
  <header class="files-toolbar">
86
- <button type="button" class="secondary" data-action="up">上一级</button>
87
- <button type="button" class="secondary" data-action="refresh">刷新</button>
88
- <div class="files-toolbar-path" data-role="path">/</div>
89
- <div class="files-toolbar-status" data-role="status">未加载</div>
87
+ <div class="files-toolbar-path-group">
88
+ <input type="text" class="files-toolbar-path-input" data-role="path" value="/" spellcheck="false" />
89
+ <button type="button" class="secondary" data-action="visit">访问</button>
90
+ </div>
91
+ <div class="files-toolbar-meta">
92
+ <div class="files-toolbar-status" data-role="status">未加载</div>
93
+ <button type="button" class="secondary" data-action="mkdir">新建目录</button>
94
+ </div>
90
95
  </header>
91
96
  <div class="files-layout">
92
97
  <aside class="files-sidebar">
@@ -94,8 +99,13 @@
94
99
  </aside>
95
100
  <section class="files-preview">
96
101
  <header class="files-preview-head">
97
- <div class="files-preview-title" data-role="preview-title">未选择文件</div>
98
- <div class="files-preview-meta" data-role="preview-meta">请选择左侧文件或目录</div>
102
+ <div class="files-preview-head-main">
103
+ <div class="files-preview-title" data-role="preview-title">未选择文件</div>
104
+ <div class="files-preview-meta" data-role="preview-meta">请选择左侧文件或目录</div>
105
+ </div>
106
+ <div class="files-preview-actions">
107
+ <button type="button" class="secondary" data-action="save" disabled>保存</button>
108
+ </div>
99
109
  </header>
100
110
  <div class="files-preview-body" data-role="preview-body"></div>
101
111
  </section>
@@ -109,8 +119,9 @@
109
119
  const previewTitleNode = root.querySelector('[data-role="preview-title"]');
110
120
  const previewMetaNode = root.querySelector('[data-role="preview-meta"]');
111
121
  const previewBodyNode = root.querySelector('[data-role="preview-body"]');
112
- const upBtn = root.querySelector('[data-action="up"]');
113
- const refreshBtn = root.querySelector('[data-action="refresh"]');
122
+ const visitBtn = root.querySelector('[data-action="visit"]');
123
+ const mkdirBtn = root.querySelector('[data-action="mkdir"]');
124
+ const saveBtn = root.querySelector('[data-action="save"]');
114
125
 
115
126
  const state = {
116
127
  visible: false,
@@ -119,16 +130,21 @@
119
130
  containerPath: '',
120
131
  historyOnly: false,
121
132
  currentPath: '',
133
+ pathDraft: '',
122
134
  parentPath: '',
123
135
  entries: [],
124
136
  selectedPath: '',
125
137
  selectedFile: null,
138
+ selectedEntry: null,
126
139
  loadingList: false,
127
140
  loadingFile: false,
141
+ savingFile: false,
128
142
  listRequestId: 0,
129
143
  readRequestId: 0,
130
144
  editor: null,
131
- editorHost: null
145
+ editorHost: null,
146
+ previewReadOnly: true,
147
+ previewDirty: false
132
148
  };
133
149
 
134
150
  function setStatus(text) {
@@ -145,7 +161,28 @@
145
161
  state.editorHost = null;
146
162
  }
147
163
 
164
+ function isEditablePreview() {
165
+ return Boolean(
166
+ state.selectedFile
167
+ && state.selectedFile.kind === 'text'
168
+ && state.previewReadOnly === false
169
+ && state.historyOnly !== true
170
+ && state.savingFile !== true
171
+ );
172
+ }
173
+
174
+ function syncSaveButton() {
175
+ if (!saveBtn) {
176
+ return;
177
+ }
178
+ saveBtn.disabled = !isEditablePreview();
179
+ saveBtn.textContent = state.savingFile ? '保存中...' : '保存';
180
+ }
181
+
148
182
  function renderPreviewEmpty(title, description) {
183
+ state.selectedFile = null;
184
+ state.previewReadOnly = true;
185
+ state.previewDirty = false;
149
186
  if (previewTitleNode) {
150
187
  previewTitleNode.textContent = title;
151
188
  }
@@ -156,6 +193,7 @@
156
193
  previewBodyNode.innerHTML = `<div class="files-empty">${escapeHtml(description)}</div>`;
157
194
  }
158
195
  destroyEditor();
196
+ syncSaveButton();
159
197
  }
160
198
 
161
199
  function ensureEditorHost() {
@@ -172,6 +210,8 @@
172
210
 
173
211
  function renderPreviewPayload(payload) {
174
212
  state.selectedFile = payload || null;
213
+ state.previewReadOnly = !(payload && payload.editable === true);
214
+ state.previewDirty = false;
175
215
  if (!payload) {
176
216
  renderPreviewEmpty('未选择文件', '请选择左侧文件进行预览。');
177
217
  return;
@@ -180,9 +220,13 @@
180
220
  previewTitleNode.textContent = payload.path || '未命名文件';
181
221
  }
182
222
  if (previewMetaNode) {
183
- previewMetaNode.textContent = `${payload.kind === 'text' ? '文本文件' : '文件'} · ${formatBytes(payload.size)}${payload.truncated ? ' · 已截断预览' : ''}`;
223
+ const modeLabel = payload.kind === 'text'
224
+ ? (payload.editable === true ? '可编辑' : '只读预览')
225
+ : '只读预览';
226
+ previewMetaNode.textContent = `${payload.kind === 'text' ? '文本文件' : '文件'} · ${formatBytes(payload.size)} · ${modeLabel}${payload.truncated ? ' · 已截断预览' : ''}`;
184
227
  }
185
228
  if (!previewBodyNode) {
229
+ syncSaveButton();
186
230
  return;
187
231
  }
188
232
 
@@ -196,35 +240,42 @@
196
240
  state.editor = window.ManyoyoCodeEditor.create(host, {
197
241
  doc: String(payload.content || ''),
198
242
  language,
199
- readOnly: true
243
+ readOnly: state.previewReadOnly,
244
+ onChange: function () {
245
+ state.previewDirty = true;
246
+ syncSaveButton();
247
+ }
200
248
  });
201
249
  }
202
250
  } else {
203
251
  state.editor.setValue(String(payload.content || ''));
204
252
  state.editor.setLanguage(language);
205
- state.editor.setReadOnly(true);
253
+ state.editor.setReadOnly(state.previewReadOnly);
206
254
  }
255
+ syncSaveButton();
207
256
  return;
208
257
  }
209
258
 
210
259
  destroyEditor();
211
260
  previewBodyNode.innerHTML = `<pre class="files-pre">${escapeHtml(String(payload.content || ''))}</pre>`;
261
+ syncSaveButton();
212
262
  return;
213
263
  }
214
264
 
215
265
  destroyEditor();
216
266
  previewBodyNode.innerHTML = `<div class="files-note">当前文件暂不支持在线预览。文件类型:${escapeHtml(payload.kind || 'unknown')}</div>`;
267
+ syncSaveButton();
217
268
  }
218
269
 
219
270
  function renderList() {
220
271
  if (pathNode) {
221
- pathNode.textContent = state.currentPath || state.containerPath || '/';
272
+ pathNode.value = state.pathDraft || state.currentPath || state.containerPath || '/';
222
273
  }
223
- if (upBtn) {
224
- upBtn.disabled = state.loadingList || !state.parentPath;
274
+ if (visitBtn) {
275
+ visitBtn.disabled = state.loadingList || state.loadingFile || !(state.pathDraft || '').trim();
225
276
  }
226
- if (refreshBtn) {
227
- refreshBtn.disabled = state.loadingList || state.loadingFile;
277
+ if (mkdirBtn) {
278
+ mkdirBtn.disabled = state.loadingList || state.loadingFile || !state.sessionName || state.historyOnly === true;
228
279
  }
229
280
  if (!listNode) {
230
281
  return;
@@ -248,8 +299,25 @@
248
299
  setStatus('读取目录中');
249
300
  return;
250
301
  }
302
+
303
+ if (state.parentPath) {
304
+ const parentButton = document.createElement('button');
305
+ parentButton.type = 'button';
306
+ parentButton.className = 'files-entry files-entry-parent';
307
+ parentButton.title = state.parentPath;
308
+ parentButton.addEventListener('click', function () {
309
+ loadDirectory(state.parentPath);
310
+ });
311
+ parentButton.innerHTML = `
312
+ <span class="files-entry-name">
313
+ <span class="files-entry-title">..</span>
314
+ </span>
315
+ <span class="files-entry-meta">上一级</span>
316
+ `;
317
+ listNode.appendChild(parentButton);
318
+ }
319
+
251
320
  if (!state.entries.length) {
252
- listNode.innerHTML = '<div class="files-empty">当前目录为空。</div>';
253
321
  setStatus('目录为空');
254
322
  return;
255
323
  }
@@ -264,7 +332,7 @@
264
332
  loadDirectory(entry.path);
265
333
  return;
266
334
  }
267
- loadFile(entry.path);
335
+ loadFile(entry.path, entry);
268
336
  });
269
337
  button.innerHTML = `
270
338
  <span class="files-entry-name">
@@ -282,7 +350,9 @@
282
350
  const requestId = state.listRequestId + 1;
283
351
  state.listRequestId = requestId;
284
352
  state.loadingList = true;
353
+ state.pathDraft = pathText;
285
354
  state.selectedPath = '';
355
+ state.selectedEntry = null;
286
356
  renderList();
287
357
  try {
288
358
  const payload = await api('/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/list?path=' + encodeURIComponent(pathText));
@@ -290,9 +360,10 @@
290
360
  return;
291
361
  }
292
362
  state.currentPath = payload && payload.path ? payload.path : pathText;
363
+ state.pathDraft = state.currentPath;
293
364
  state.parentPath = payload && payload.parentPath ? payload.parentPath : '';
294
365
  state.entries = Array.isArray(payload && payload.entries) ? payload.entries : [];
295
- renderPreviewEmpty(state.currentPath, '请选择左侧文件进行预览。');
366
+ renderPreviewEmpty('未选择文件', '请选择左侧文件进行预览。');
296
367
  } catch (e) {
297
368
  if (requestId !== state.listRequestId) {
298
369
  return;
@@ -307,22 +378,39 @@
307
378
  }
308
379
  }
309
380
 
310
- async function loadFile(targetPath) {
381
+ async function loadFile(targetPath, entry) {
311
382
  const pathText = String(targetPath || '').trim();
312
383
  if (!pathText) {
313
384
  return;
314
385
  }
386
+ const selectedEntry = entry && typeof entry === 'object' ? entry : null;
387
+ const fileSize = Number(selectedEntry && selectedEntry.size);
388
+ const requiresReadonlyConfirm = Number.isFinite(fileSize) && fileSize >= FILE_EDIT_MAX_BYTES;
389
+ if (requiresReadonlyConfirm) {
390
+ const yes = window.confirm(`文件较大(${formatBytes(fileSize)}),继续后将以只读方式全量预览,无法保存。是否继续?`);
391
+ if (!yes) {
392
+ return;
393
+ }
394
+ }
315
395
  const requestId = state.readRequestId + 1;
316
396
  state.readRequestId = requestId;
317
397
  state.loadingFile = true;
318
398
  state.selectedPath = pathText;
399
+ state.selectedEntry = selectedEntry;
319
400
  renderList();
320
401
  renderPreviewEmpty(pathText, '正在读取文件内容...');
321
402
  try {
322
- const payload = await api('/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/read?path=' + encodeURIComponent(pathText));
403
+ const payload = await api(
404
+ '/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/read?path='
405
+ + encodeURIComponent(pathText)
406
+ + '&full=1'
407
+ );
323
408
  if (requestId !== state.readRequestId) {
324
409
  return;
325
410
  }
411
+ if (requiresReadonlyConfirm && payload && payload.kind === 'text') {
412
+ payload.editable = false;
413
+ }
326
414
  renderPreviewPayload(payload);
327
415
  } catch (e) {
328
416
  if (requestId !== state.readRequestId) {
@@ -339,6 +427,83 @@
339
427
  }
340
428
  }
341
429
 
430
+ async function saveCurrentFile() {
431
+ if (!isEditablePreview() || !state.editor || typeof state.editor.getValue !== 'function' || !state.selectedFile || !state.selectedFile.path) {
432
+ return;
433
+ }
434
+ state.savingFile = true;
435
+ syncSaveButton();
436
+ setStatus('保存中');
437
+ try {
438
+ const nextContent = state.editor.getValue();
439
+ const payload = await api('/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/write', {
440
+ method: 'PUT',
441
+ body: JSON.stringify({
442
+ path: state.selectedFile.path,
443
+ content: nextContent
444
+ })
445
+ });
446
+ state.previewDirty = false;
447
+ if (state.selectedFile) {
448
+ state.selectedFile.content = nextContent;
449
+ state.selectedFile.size = payload && typeof payload.size === 'number'
450
+ ? payload.size
451
+ : new TextEncoder().encode(nextContent).length;
452
+ }
453
+ const matchedEntry = state.entries.find(function (item) {
454
+ return item && state.selectedFile && item.path === state.selectedFile.path;
455
+ });
456
+ if (matchedEntry && state.selectedFile) {
457
+ matchedEntry.size = state.selectedFile.size;
458
+ }
459
+ renderPreviewPayload(state.selectedFile);
460
+ renderList();
461
+ setStatus('已保存');
462
+ } catch (e) {
463
+ setStatus('保存失败');
464
+ onError(e && e.message ? e.message : '保存文件失败');
465
+ } finally {
466
+ state.savingFile = false;
467
+ syncSaveButton();
468
+ }
469
+ }
470
+
471
+ function joinDirectoryPath(basePath, childName) {
472
+ const base = String(basePath || '/').trim() || '/';
473
+ const child = String(childName || '').trim();
474
+ if (!child) {
475
+ return base;
476
+ }
477
+ if (base === '/') {
478
+ return '/' + child.replace(/^\/+/, '');
479
+ }
480
+ return base.replace(/\/+$/, '') + '/' + child.replace(/^\/+/, '');
481
+ }
482
+
483
+ async function createDirectory() {
484
+ if (!state.sessionName || state.historyOnly === true || state.loadingList || state.loadingFile) {
485
+ return;
486
+ }
487
+ const input = window.prompt('请输入新目录名称');
488
+ const name = String(input || '').trim();
489
+ if (!name) {
490
+ return;
491
+ }
492
+ const targetPath = joinDirectoryPath(state.currentPath || state.containerPath || '/', name);
493
+ setStatus('创建目录中');
494
+ try {
495
+ await api('/api/sessions/' + encodeURIComponent(state.sessionName) + '/fs/mkdir', {
496
+ method: 'POST',
497
+ body: JSON.stringify({ path: targetPath })
498
+ });
499
+ await loadDirectory(state.currentPath || state.containerPath || '/');
500
+ setStatus('已创建目录');
501
+ } catch (e) {
502
+ setStatus('创建目录失败');
503
+ onError(e && e.message ? e.message : '创建目录失败');
504
+ }
505
+ }
506
+
342
507
  function sync(context) {
343
508
  const session = context && context.session;
344
509
  const detail = context && context.detail;
@@ -362,12 +527,17 @@
362
527
  state.containerName = nextContainerName;
363
528
  state.containerPath = nextContainerPath;
364
529
  state.currentPath = '';
530
+ state.pathDraft = nextContainerPath;
365
531
  state.parentPath = '';
366
532
  state.entries = [];
367
533
  state.selectedPath = '';
534
+ state.selectedEntry = null;
368
535
  renderPreviewEmpty('未选择文件', '请选择左侧文件进行预览。');
369
536
  } else if (containerPathChanged) {
370
537
  state.containerPath = nextContainerPath;
538
+ if (!state.currentPath) {
539
+ state.pathDraft = nextContainerPath;
540
+ }
371
541
  }
372
542
 
373
543
  renderList();
@@ -379,17 +549,38 @@
379
549
  }
380
550
  }
381
551
 
382
- if (upBtn) {
383
- upBtn.addEventListener('click', function () {
384
- if (state.parentPath) {
385
- loadDirectory(state.parentPath);
552
+ if (pathNode) {
553
+ pathNode.addEventListener('input', function () {
554
+ state.pathDraft = pathNode.value;
555
+ renderList();
556
+ });
557
+ pathNode.addEventListener('keydown', function (event) {
558
+ if (event.key === 'Enter') {
559
+ event.preventDefault();
560
+ loadDirectory(pathNode.value);
386
561
  }
387
562
  });
388
563
  }
389
564
 
390
- if (refreshBtn) {
391
- refreshBtn.addEventListener('click', function () {
392
- loadDirectory(state.currentPath || state.containerPath || '/');
565
+ if (visitBtn) {
566
+ visitBtn.addEventListener('click', function () {
567
+ loadDirectory(state.pathDraft || state.currentPath || state.containerPath || '/');
568
+ });
569
+ }
570
+
571
+ if (mkdirBtn) {
572
+ mkdirBtn.addEventListener('click', function () {
573
+ createDirectory().catch(function (e) {
574
+ onError(e && e.message ? e.message : '创建目录失败');
575
+ });
576
+ });
577
+ }
578
+
579
+ if (saveBtn) {
580
+ saveBtn.addEventListener('click', function () {
581
+ saveCurrentFile().catch(function (e) {
582
+ onError(e && e.message ? e.message : '保存文件失败');
583
+ });
393
584
  });
394
585
  }
395
586