collabmd 0.1.31 → 0.1.33
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/dist/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-BMTwP5mD.js} +1 -1
- package/dist/client/assets/drawioEditor-D6clB5OH.js +2 -0
- package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Dzy_85Wp.js} +2 -2
- package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
- package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CAP9F8CB.js} +3 -3
- package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DD4AvzYP.js} +2 -2
- package/dist/client/assets/exportDocument-BbkN85c4.css +1 -0
- package/dist/client/assets/index-BIt5EZXz.css +1 -0
- package/dist/client/assets/{index-kPgEYvEX.js → index-Czg_KL-i.js} +2 -2
- package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/main-DeowJOSp.js +1368 -0
- package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
- package/dist/client/assets/vault-api-client-p7h7cXKM.js +1 -0
- package/dist/client/drawio-editor.html +1 -1
- package/dist/client/excalidraw-editor.html +1 -1
- package/dist/client/export-document.html +2 -2
- package/dist/client/index.html +2 -2
- package/package.json +8 -5
- package/src/client/app/main-entry.js +2 -0
- package/src/client/application/app-shell/git-feature.js +28 -36
- package/src/client/application/app-shell/presence-feature.js +3 -1
- package/src/client/application/diagram-preview-hydrator.js +24 -15
- package/src/client/application/mermaid-preview-hydrator.js +121 -4
- package/src/client/application/plantuml-preview-hydrator.js +6 -16
- package/src/client/application/preview-renderer.js +11 -2
- package/src/client/bootstrap/collabmd-app-shell.js +182 -50
- package/src/client/domain/excalidraw-document-switch.js +3 -0
- package/src/client/excalidraw-editor.js +187 -84
- package/src/client/infrastructure/auth-client.js +4 -4
- package/src/client/infrastructure/backlinks-api-client.js +18 -0
- package/src/client/infrastructure/comment-thread-store.js +4 -0
- package/src/client/infrastructure/editor-session.js +6 -0
- package/src/client/infrastructure/editor-view-adapter.js +23 -0
- package/src/client/infrastructure/git-api-client.js +134 -0
- package/src/client/infrastructure/plantuml-api-client.js +21 -0
- package/src/client/infrastructure/vault-api-client.js +115 -0
- package/src/client/presentation/backlinks-panel.js +6 -9
- package/src/client/presentation/bases-preview-controller.js +1413 -34
- package/src/client/presentation/comments-panel.js +50 -5
- package/src/client/presentation/excalidraw-embed-controller.js +251 -15
- package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
- package/src/client/presentation/file-action-controller.js +97 -0
- package/src/client/presentation/file-explorer-controller.js +2 -0
- package/src/client/presentation/file-explorer-view.js +368 -0
- package/src/client/presentation/file-history-view-controller.js +41 -40
- package/src/client/presentation/git-diff-view-controller.js +70 -53
- package/src/client/presentation/git-panel-controller.js +12 -30
- package/src/client/styles/components/scrollbars.css +5 -0
- package/src/client/styles/export-document.css +45 -2
- package/src/client/styles/features/auth-gate.css +13 -24
- package/src/client/styles/features/comment-card.css +6 -0
- package/src/client/styles/features/comments-drawer.css +38 -18
- package/src/client/styles/features/diagram-preview.css +33 -0
- package/src/client/styles/features/embedded-preview.css +37 -11
- package/src/client/styles/features/preview-markdown.css +559 -39
- package/src/client/styles/foundation/themes.css +4 -0
- package/src/client/styles/foundation/tokens.css +4 -3
- package/src/client/styles/layout/sidebar.css +72 -0
- package/src/server/auth/auth-common.js +224 -0
- package/src/server/auth/auth-constants.js +30 -0
- package/src/server/auth/auth-strategies.js +320 -0
- package/src/server/auth/create-auth-service.js +24 -545
- package/src/server/domain/backlink-index.js +177 -5
- package/src/server/domain/bases/base-definition.js +382 -0
- package/src/server/domain/bases/base-expression-runtime.js +1196 -0
- package/src/server/domain/bases/base-index-snapshot-store.js +565 -0
- package/src/server/domain/bases/base-query-metadata.js +242 -0
- package/src/server/domain/bases/base-query-results.js +215 -0
- package/src/server/domain/bases/base-query-service.js +406 -1992
- package/src/server/domain/bases/base-transform.js +116 -0
- package/src/server/domain/collaboration/collaboration-room.js +36 -1
- package/src/server/domain/collaboration/room-client-state-store.js +1 -0
- package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +161 -1
- package/src/server/infrastructure/http/http-response.js +56 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
- package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
- package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
- package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
- package/dist/client/assets/index-D69HOZHr.css +0 -1
- package/dist/client/assets/main-DSxNTtD-.js +0 -1243
- package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
- package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
- /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Zfwq1XVx.js} +0 -0
|
@@ -38,23 +38,28 @@ function findNodeByPath(nodes = [], pathValue = '') {
|
|
|
38
38
|
|
|
39
39
|
const MOBILE_LONG_PRESS_DELAY_MS = 420;
|
|
40
40
|
const MOBILE_LONG_PRESS_MOVE_TOLERANCE_PX = 10;
|
|
41
|
+
const DRAG_AUTO_EXPAND_DELAY_MS = 700;
|
|
41
42
|
|
|
42
43
|
export class FileExplorerView {
|
|
43
44
|
constructor({
|
|
44
45
|
mobileBreakpointQuery = window.matchMedia('(max-width: 768px)'),
|
|
46
|
+
onEntryDrop,
|
|
45
47
|
onDirectorySelect,
|
|
46
48
|
onDirectoryToggle,
|
|
47
49
|
onFileContextMenu,
|
|
48
50
|
onFileSelect,
|
|
49
51
|
onSearchChange,
|
|
50
52
|
onTreeContextMenu,
|
|
53
|
+
onValidateDrop,
|
|
51
54
|
}) {
|
|
55
|
+
this.onEntryDrop = onEntryDrop;
|
|
52
56
|
this.onDirectorySelect = onDirectorySelect;
|
|
53
57
|
this.onDirectoryToggle = onDirectoryToggle;
|
|
54
58
|
this.onFileContextMenu = onFileContextMenu;
|
|
55
59
|
this.onFileSelect = onFileSelect;
|
|
56
60
|
this.onSearchChange = onSearchChange;
|
|
57
61
|
this.onTreeContextMenu = onTreeContextMenu;
|
|
62
|
+
this.onValidateDrop = onValidateDrop;
|
|
58
63
|
this.mobileBreakpointQuery = mobileBreakpointQuery;
|
|
59
64
|
this.treeContainer = document.getElementById('fileTree');
|
|
60
65
|
this.searchInput = document.getElementById('fileSearchInput');
|
|
@@ -66,6 +71,13 @@ export class FileExplorerView {
|
|
|
66
71
|
this.suppressedActivationTarget = null;
|
|
67
72
|
this.contextMenuCloseHandler = null;
|
|
68
73
|
this.actionSheetCloseHandler = null;
|
|
74
|
+
this.dragSource = null;
|
|
75
|
+
this.currentSearchQuery = '';
|
|
76
|
+
this.activeDropTarget = null;
|
|
77
|
+
this.invalidDropAttempt = null;
|
|
78
|
+
this.autoExpandTimer = 0;
|
|
79
|
+
this.autoExpandTargetPath = '';
|
|
80
|
+
this.rootDropZone = null;
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
initialize() {
|
|
@@ -108,6 +120,15 @@ export class FileExplorerView {
|
|
|
108
120
|
this.treeContainer?.addEventListener('scroll', () => {
|
|
109
121
|
this.cancelLongPress();
|
|
110
122
|
}, { passive: true });
|
|
123
|
+
this.treeContainer?.addEventListener('dragover', (event) => {
|
|
124
|
+
this.handleTreeDragOver(event);
|
|
125
|
+
});
|
|
126
|
+
this.treeContainer?.addEventListener('drop', (event) => {
|
|
127
|
+
this.handleTreeDrop(event);
|
|
128
|
+
});
|
|
129
|
+
this.treeContainer?.addEventListener('dragleave', (event) => {
|
|
130
|
+
this.handleTreeDragLeave(event);
|
|
131
|
+
});
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
render({ activeFilePath, changedPaths = null, expandedDirs, reset = false, searchMatches, searchQuery, tree }) {
|
|
@@ -115,6 +136,11 @@ export class FileExplorerView {
|
|
|
115
136
|
return;
|
|
116
137
|
}
|
|
117
138
|
|
|
139
|
+
this.currentSearchQuery = String(searchQuery ?? '');
|
|
140
|
+
if (this.currentSearchQuery) {
|
|
141
|
+
this.clearDragFeedback();
|
|
142
|
+
}
|
|
143
|
+
|
|
118
144
|
if (this.searchInput && this.searchInput.value !== searchQuery) {
|
|
119
145
|
this.searchInput.value = searchQuery;
|
|
120
146
|
}
|
|
@@ -179,6 +205,7 @@ export class FileExplorerView {
|
|
|
179
205
|
|
|
180
206
|
this.resetTreeIndexes();
|
|
181
207
|
this.treeContainer.innerHTML = '';
|
|
208
|
+
this.rootDropZone = null;
|
|
182
209
|
|
|
183
210
|
if (matches.length === 0) {
|
|
184
211
|
this.treeContainer.innerHTML = '<div class="file-tree-empty">No matches</div>';
|
|
@@ -206,6 +233,7 @@ export class FileExplorerView {
|
|
|
206
233
|
renderFullTree(tree, { activeFilePath, expandedDirs }) {
|
|
207
234
|
this.resetTreeIndexes();
|
|
208
235
|
this.treeContainer.innerHTML = '';
|
|
236
|
+
this.rootDropZone = null;
|
|
209
237
|
|
|
210
238
|
if (tree.length === 0) {
|
|
211
239
|
this.treeContainer.innerHTML = '<div class="file-tree-empty">No vault files found</div>';
|
|
@@ -213,6 +241,7 @@ export class FileExplorerView {
|
|
|
213
241
|
}
|
|
214
242
|
|
|
215
243
|
const fragment = document.createDocumentFragment();
|
|
244
|
+
fragment.appendChild(this.createRootDropZone());
|
|
216
245
|
this.renderNodes(tree, fragment, {
|
|
217
246
|
activeFilePath,
|
|
218
247
|
depth: 0,
|
|
@@ -258,11 +287,15 @@ export class FileExplorerView {
|
|
|
258
287
|
|
|
259
288
|
const isExpanded = expandedDirs.has(node.path);
|
|
260
289
|
button.setAttribute('aria-expanded', String(isExpanded));
|
|
290
|
+
button.dataset.path = node.path;
|
|
291
|
+
button.dataset.entryType = 'directory';
|
|
261
292
|
button.innerHTML = `
|
|
262
293
|
<svg class="file-tree-chevron${isExpanded ? ' expanded' : ''}" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
|
263
294
|
<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
264
295
|
<span class="file-tree-name">${escapeHtml(node.name)}</span>
|
|
265
296
|
`;
|
|
297
|
+
this.configureDragSource(button, { path: node.path, type: 'directory' });
|
|
298
|
+
this.bindDirectoryDropTarget(button, node.path);
|
|
266
299
|
|
|
267
300
|
button.addEventListener('click', (event) => {
|
|
268
301
|
if (this.consumeSuppressedActivation(button, event)) {
|
|
@@ -303,6 +336,8 @@ export class FileExplorerView {
|
|
|
303
336
|
button.className = 'file-tree-item file-tree-dir';
|
|
304
337
|
button.style.setProperty('--depth', 0);
|
|
305
338
|
button.dataset.depth = 0;
|
|
339
|
+
button.dataset.path = node.path;
|
|
340
|
+
button.dataset.entryType = 'directory';
|
|
306
341
|
button.innerHTML = `
|
|
307
342
|
<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
|
308
343
|
<span class="file-tree-name">${escapeHtml(node.name || getPathLeaf(node.path))}</span>
|
|
@@ -415,10 +450,12 @@ export class FileExplorerView {
|
|
|
415
450
|
button.style.setProperty('--depth', depth);
|
|
416
451
|
button.dataset.depth = depth;
|
|
417
452
|
button.dataset.path = filePath;
|
|
453
|
+
button.dataset.entryType = 'file';
|
|
418
454
|
button.innerHTML = `
|
|
419
455
|
${this.getFileIconSvg({ isBase, isDrawio, isExcalidraw, isImage, isMermaid, isPlantUml })}
|
|
420
456
|
<span class="file-tree-name">${escapeHtml(stripVaultFileExtension(name))}</span>
|
|
421
457
|
`;
|
|
458
|
+
this.configureDragSource(button, { path: filePath, type: 'file' });
|
|
422
459
|
|
|
423
460
|
button.addEventListener('click', (event) => {
|
|
424
461
|
if (this.consumeSuppressedActivation(button, event)) {
|
|
@@ -441,6 +478,336 @@ export class FileExplorerView {
|
|
|
441
478
|
return Boolean(this.mobileBreakpointQuery?.matches);
|
|
442
479
|
}
|
|
443
480
|
|
|
481
|
+
isDragAndDropEnabled() {
|
|
482
|
+
return !this.isMobileViewport() && !this.currentSearchQuery;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
configureDragSource(element, payload) {
|
|
486
|
+
if (!(element instanceof HTMLElement)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const isEnabled = this.isDragAndDropEnabled();
|
|
491
|
+
element.draggable = isEnabled;
|
|
492
|
+
if (!isEnabled) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
element.addEventListener('dragstart', (event) => {
|
|
497
|
+
this.dragSource = {
|
|
498
|
+
path: payload.path,
|
|
499
|
+
type: payload.type,
|
|
500
|
+
};
|
|
501
|
+
document.body?.classList.add('is-file-tree-dragging');
|
|
502
|
+
if (this.treeContainer) {
|
|
503
|
+
this.treeContainer.dataset.dragActive = 'true';
|
|
504
|
+
}
|
|
505
|
+
element.classList.add('is-dragging');
|
|
506
|
+
if (event.dataTransfer) {
|
|
507
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
508
|
+
event.dataTransfer.setData('text/plain', payload.path);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
element.addEventListener('dragend', () => {
|
|
512
|
+
element.classList.remove('is-dragging');
|
|
513
|
+
const dragSource = this.dragSource;
|
|
514
|
+
if (this.invalidDropAttempt && dragSource) {
|
|
515
|
+
void this.onEntryDrop?.(this.invalidDropAttempt);
|
|
516
|
+
}
|
|
517
|
+
this.dragSource = null;
|
|
518
|
+
this.invalidDropAttempt = null;
|
|
519
|
+
this.clearDragFeedback();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
if (payload.type === 'file') {
|
|
523
|
+
element.addEventListener('dragenter', (event) => {
|
|
524
|
+
this.handleNonDropTargetDragOver(event);
|
|
525
|
+
});
|
|
526
|
+
element.addEventListener('dragover', (event) => {
|
|
527
|
+
this.handleNonDropTargetDragOver(event);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
bindDirectoryDropTarget(element, directoryPath) {
|
|
533
|
+
if (!(element instanceof HTMLElement) || this.isMobileViewport()) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
element.addEventListener('dragenter', (event) => {
|
|
538
|
+
this.handleDirectoryDragEnter(event, element, directoryPath);
|
|
539
|
+
});
|
|
540
|
+
element.addEventListener('dragover', (event) => {
|
|
541
|
+
this.handleDirectoryDragOver(event, element, directoryPath);
|
|
542
|
+
});
|
|
543
|
+
element.addEventListener('drop', (event) => {
|
|
544
|
+
this.handleDirectoryDrop(event, directoryPath);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
createRootDropZone() {
|
|
549
|
+
const zone = document.createElement('div');
|
|
550
|
+
zone.className = 'file-tree-root-drop-zone';
|
|
551
|
+
zone.textContent = 'Drop here to move to vault root';
|
|
552
|
+
zone.addEventListener('dragenter', (event) => {
|
|
553
|
+
this.handleRootZoneDragEnter(event);
|
|
554
|
+
});
|
|
555
|
+
zone.addEventListener('dragover', (event) => {
|
|
556
|
+
this.handleRootZoneDragOver(event);
|
|
557
|
+
});
|
|
558
|
+
zone.addEventListener('drop', (event) => {
|
|
559
|
+
this.handleRootZoneDrop(event);
|
|
560
|
+
});
|
|
561
|
+
this.rootDropZone = zone;
|
|
562
|
+
return zone;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
validateDrop(destinationDirectory) {
|
|
566
|
+
if (!this.dragSource || this.currentSearchQuery) {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return this.onValidateDrop?.({
|
|
571
|
+
destinationDirectory,
|
|
572
|
+
sourcePath: this.dragSource.path,
|
|
573
|
+
sourceType: this.dragSource.type,
|
|
574
|
+
}) === true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
setDropTarget(target) {
|
|
578
|
+
if (this.activeDropTarget?.element && this.activeDropTarget.element !== target?.element) {
|
|
579
|
+
this.activeDropTarget.element.classList.remove('is-drop-target', 'is-drop-invalid');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (this.activeDropTarget?.root && !target?.root) {
|
|
583
|
+
this.treeContainer?.classList.remove('is-drop-target-root', 'is-drop-invalid');
|
|
584
|
+
}
|
|
585
|
+
if (this.activeDropTarget?.rootZone && !target?.rootZone) {
|
|
586
|
+
this.rootDropZone?.classList.remove('is-drop-target', 'is-drop-invalid');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
this.activeDropTarget = target;
|
|
590
|
+
if (!target || target.isValid) {
|
|
591
|
+
this.invalidDropAttempt = null;
|
|
592
|
+
} else if (this.dragSource) {
|
|
593
|
+
this.invalidDropAttempt = {
|
|
594
|
+
destinationDirectory: target.destinationDirectory || '',
|
|
595
|
+
sourcePath: this.dragSource.path,
|
|
596
|
+
sourceType: this.dragSource.type,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (!target) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (target.root) {
|
|
605
|
+
this.treeContainer?.classList.toggle('is-drop-target-root', target.isValid);
|
|
606
|
+
this.treeContainer?.classList.toggle('is-drop-invalid', !target.isValid);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (target.rootZone) {
|
|
611
|
+
this.rootDropZone?.classList.toggle('is-drop-target', target.isValid);
|
|
612
|
+
this.rootDropZone?.classList.toggle('is-drop-invalid', !target.isValid);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
target.element.classList.toggle('is-drop-target', target.isValid);
|
|
617
|
+
target.element.classList.toggle('is-drop-invalid', !target.isValid);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
scheduleAutoExpand(element, directoryPath) {
|
|
621
|
+
const isExpanded = element.getAttribute('aria-expanded') === 'true';
|
|
622
|
+
if (isExpanded || this.autoExpandTargetPath === directoryPath) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
this.cancelAutoExpand();
|
|
627
|
+
this.autoExpandTargetPath = directoryPath;
|
|
628
|
+
this.autoExpandTimer = window.setTimeout(() => {
|
|
629
|
+
this.autoExpandTimer = 0;
|
|
630
|
+
this.autoExpandTargetPath = '';
|
|
631
|
+
if (element.isConnected && element.getAttribute('aria-expanded') !== 'true') {
|
|
632
|
+
this.onDirectoryToggle?.(directoryPath);
|
|
633
|
+
}
|
|
634
|
+
}, DRAG_AUTO_EXPAND_DELAY_MS);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
cancelAutoExpand() {
|
|
638
|
+
if (this.autoExpandTimer) {
|
|
639
|
+
window.clearTimeout(this.autoExpandTimer);
|
|
640
|
+
this.autoExpandTimer = 0;
|
|
641
|
+
}
|
|
642
|
+
this.autoExpandTargetPath = '';
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
clearDragFeedback() {
|
|
646
|
+
this.cancelAutoExpand();
|
|
647
|
+
this.setDropTarget(null);
|
|
648
|
+
document.body?.classList.remove('is-file-tree-dragging');
|
|
649
|
+
if (this.treeContainer) {
|
|
650
|
+
delete this.treeContainer.dataset.dragActive;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
handleDirectoryDragEnter(event, element, directoryPath) {
|
|
655
|
+
if (!this.dragSource) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
event.preventDefault();
|
|
660
|
+
event.stopPropagation();
|
|
661
|
+
const isValid = this.validateDrop(directoryPath);
|
|
662
|
+
this.setDropTarget({ destinationDirectory: directoryPath, element, isValid, root: false });
|
|
663
|
+
if (isValid) {
|
|
664
|
+
this.scheduleAutoExpand(element, directoryPath);
|
|
665
|
+
} else {
|
|
666
|
+
this.cancelAutoExpand();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
handleDirectoryDragOver(event, element, directoryPath) {
|
|
671
|
+
if (!this.dragSource) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
event.preventDefault();
|
|
676
|
+
event.stopPropagation();
|
|
677
|
+
const isValid = this.validateDrop(directoryPath);
|
|
678
|
+
if (event.dataTransfer) {
|
|
679
|
+
event.dataTransfer.dropEffect = isValid ? 'move' : 'none';
|
|
680
|
+
}
|
|
681
|
+
this.setDropTarget({ destinationDirectory: directoryPath, element, isValid, root: false });
|
|
682
|
+
if (isValid) {
|
|
683
|
+
this.scheduleAutoExpand(element, directoryPath);
|
|
684
|
+
} else {
|
|
685
|
+
this.cancelAutoExpand();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
handleDirectoryDrop(event, directoryPath) {
|
|
690
|
+
if (!this.dragSource) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
event.preventDefault();
|
|
695
|
+
event.stopPropagation();
|
|
696
|
+
const payload = {
|
|
697
|
+
destinationDirectory: directoryPath,
|
|
698
|
+
sourcePath: this.dragSource.path,
|
|
699
|
+
sourceType: this.dragSource.type,
|
|
700
|
+
};
|
|
701
|
+
this.clearDragFeedback();
|
|
702
|
+
void this.onEntryDrop?.(payload);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
handleTreeDragOver(event) {
|
|
706
|
+
if (!this.dragSource || !this.treeContainer) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (event.target.closest('.file-tree-dir') || event.target.closest('.file-tree-item')) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
event.preventDefault();
|
|
715
|
+
this.cancelAutoExpand();
|
|
716
|
+
const isValid = this.validateDrop('');
|
|
717
|
+
if (event.dataTransfer) {
|
|
718
|
+
event.dataTransfer.dropEffect = isValid ? 'move' : 'none';
|
|
719
|
+
}
|
|
720
|
+
this.setDropTarget({ destinationDirectory: '', element: this.treeContainer, isValid, root: true });
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
handleTreeDrop(event) {
|
|
724
|
+
if (!this.dragSource || !this.treeContainer) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (event.target.closest('.file-tree-dir') || event.target.closest('.file-tree-item')) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
event.preventDefault();
|
|
733
|
+
const payload = {
|
|
734
|
+
destinationDirectory: '',
|
|
735
|
+
sourcePath: this.dragSource.path,
|
|
736
|
+
sourceType: this.dragSource.type,
|
|
737
|
+
};
|
|
738
|
+
this.clearDragFeedback();
|
|
739
|
+
void this.onEntryDrop?.(payload);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
handleTreeDragLeave(event) {
|
|
743
|
+
if (!this.dragSource || !this.treeContainer) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const nextTarget = event.relatedTarget;
|
|
748
|
+
if (nextTarget && this.treeContainer.contains(nextTarget)) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
this.clearDragFeedback();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
handleRootZoneDragEnter(event) {
|
|
756
|
+
if (!this.dragSource) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
event.preventDefault();
|
|
761
|
+
event.stopPropagation();
|
|
762
|
+
const isValid = this.validateDrop('');
|
|
763
|
+
this.cancelAutoExpand();
|
|
764
|
+
this.setDropTarget({ destinationDirectory: '', element: this.rootDropZone, isValid, rootZone: true });
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
handleRootZoneDragOver(event) {
|
|
768
|
+
if (!this.dragSource) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
event.preventDefault();
|
|
773
|
+
event.stopPropagation();
|
|
774
|
+
const isValid = this.validateDrop('');
|
|
775
|
+
if (event.dataTransfer) {
|
|
776
|
+
event.dataTransfer.dropEffect = isValid ? 'move' : 'none';
|
|
777
|
+
}
|
|
778
|
+
this.cancelAutoExpand();
|
|
779
|
+
this.setDropTarget({ destinationDirectory: '', element: this.rootDropZone, isValid, rootZone: true });
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
handleRootZoneDrop(event) {
|
|
783
|
+
if (!this.dragSource) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
event.preventDefault();
|
|
788
|
+
event.stopPropagation();
|
|
789
|
+
const payload = {
|
|
790
|
+
destinationDirectory: '',
|
|
791
|
+
sourcePath: this.dragSource.path,
|
|
792
|
+
sourceType: this.dragSource.type,
|
|
793
|
+
};
|
|
794
|
+
this.clearDragFeedback();
|
|
795
|
+
void this.onEntryDrop?.(payload);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
handleNonDropTargetDragOver(event) {
|
|
799
|
+
if (!this.dragSource) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
event.preventDefault();
|
|
804
|
+
event.stopPropagation();
|
|
805
|
+
if (event.dataTransfer) {
|
|
806
|
+
event.dataTransfer.dropEffect = 'none';
|
|
807
|
+
}
|
|
808
|
+
this.clearDragFeedback();
|
|
809
|
+
}
|
|
810
|
+
|
|
444
811
|
bindLongPress(element, callback) {
|
|
445
812
|
if (!(element instanceof HTMLElement) || typeof callback !== 'function') {
|
|
446
813
|
return;
|
|
@@ -659,6 +1026,7 @@ export class FileExplorerView {
|
|
|
659
1026
|
|
|
660
1027
|
removeContextMenu() {
|
|
661
1028
|
this.cancelLongPress();
|
|
1029
|
+
this.clearDragFeedback();
|
|
662
1030
|
document.querySelectorAll('.file-context-menu').forEach((menu) => menu.remove());
|
|
663
1031
|
if (this.contextMenuCloseHandler) {
|
|
664
1032
|
document.removeEventListener('click', this.contextMenuCloseHandler);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { escapeHtml } from '../domain/vault-utils.js';
|
|
2
|
-
import { resolveApiUrl } from '../domain/runtime-paths.js';
|
|
3
2
|
import { buttonClassNames } from './components/ui/button.js';
|
|
4
3
|
|
|
5
4
|
const HISTORY_PAGE_SIZE = 30;
|
|
@@ -62,6 +61,7 @@ function createEmptyHistoryState() {
|
|
|
62
61
|
export class FileHistoryViewController {
|
|
63
62
|
constructor({
|
|
64
63
|
diffRenderer = null,
|
|
64
|
+
gitApiClient = null,
|
|
65
65
|
onOpenCommitDiff = () => {},
|
|
66
66
|
onOpenFile = () => {},
|
|
67
67
|
onOpenPreview = () => {},
|
|
@@ -69,6 +69,7 @@ export class FileHistoryViewController {
|
|
|
69
69
|
toastController = null,
|
|
70
70
|
} = {}) {
|
|
71
71
|
this.diffRenderer = diffRenderer;
|
|
72
|
+
this.gitApiClient = gitApiClient;
|
|
72
73
|
this.onOpenCommitDiff = onOpenCommitDiff;
|
|
73
74
|
this.onOpenFile = onOpenFile;
|
|
74
75
|
this.onOpenPreview = onOpenPreview;
|
|
@@ -89,6 +90,7 @@ export class FileHistoryViewController {
|
|
|
89
90
|
this.nextButton = document.getElementById('diffNextBtn');
|
|
90
91
|
this.layoutToggle = document.getElementById('diffLayoutToggle');
|
|
91
92
|
this.modeButtons = Array.from(document.querySelectorAll('[data-diff-mode]'));
|
|
93
|
+
this.isActive = false;
|
|
92
94
|
this.currentFilePath = null;
|
|
93
95
|
this.localChanges = null;
|
|
94
96
|
this.history = createEmptyHistoryState();
|
|
@@ -104,6 +106,9 @@ export class FileHistoryViewController {
|
|
|
104
106
|
|
|
105
107
|
initialize() {
|
|
106
108
|
this.openEditorButton?.addEventListener('click', () => {
|
|
109
|
+
if (!this.isActive) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
107
112
|
if (!this.currentFilePath) {
|
|
108
113
|
return;
|
|
109
114
|
}
|
|
@@ -113,6 +118,9 @@ export class FileHistoryViewController {
|
|
|
113
118
|
|
|
114
119
|
this.modeButtons.forEach((button) => {
|
|
115
120
|
button.addEventListener('click', () => {
|
|
121
|
+
if (!this.isActive) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
116
124
|
const nextMode = button.getAttribute('data-diff-mode');
|
|
117
125
|
if (!nextMode || nextMode === this.diffMode) {
|
|
118
126
|
return;
|
|
@@ -123,6 +131,9 @@ export class FileHistoryViewController {
|
|
|
123
131
|
});
|
|
124
132
|
|
|
125
133
|
this.content?.addEventListener('click', (event) => {
|
|
134
|
+
if (!this.isActive) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
126
137
|
const selectionButton = event.target instanceof Element
|
|
127
138
|
? event.target.closest('[data-file-history-select]')
|
|
128
139
|
: null;
|
|
@@ -160,6 +171,7 @@ export class FileHistoryViewController {
|
|
|
160
171
|
}
|
|
161
172
|
|
|
162
173
|
hide() {
|
|
174
|
+
this.isActive = false;
|
|
163
175
|
this.page?.classList.add('hidden');
|
|
164
176
|
this.content?.classList.remove('diff-content--history');
|
|
165
177
|
if (this.content) {
|
|
@@ -175,7 +187,6 @@ export class FileHistoryViewController {
|
|
|
175
187
|
this.historyListElement = null;
|
|
176
188
|
this.historyListScrollTop = 0;
|
|
177
189
|
this.selectionRequestToken = 0;
|
|
178
|
-
this.syncToolbar();
|
|
179
190
|
}
|
|
180
191
|
|
|
181
192
|
getEntries() {
|
|
@@ -229,6 +240,7 @@ export class FileHistoryViewController {
|
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
async openFileHistory({ filePath }) {
|
|
243
|
+
this.isActive = true;
|
|
232
244
|
this.currentFilePath = String(filePath ?? '').trim() || null;
|
|
233
245
|
this.localChanges = null;
|
|
234
246
|
this.history = {
|
|
@@ -289,16 +301,11 @@ export class FileHistoryViewController {
|
|
|
289
301
|
return null;
|
|
290
302
|
}
|
|
291
303
|
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const data = await response.json();
|
|
298
|
-
if (!response.ok) {
|
|
299
|
-
throw new Error(data.error || 'Failed to load local file changes');
|
|
300
|
-
}
|
|
301
|
-
|
|
304
|
+
const data = await this.gitApiClient.readDiff({
|
|
305
|
+
metaOnly: true,
|
|
306
|
+
path: this.currentFilePath,
|
|
307
|
+
scope: 'all',
|
|
308
|
+
});
|
|
302
309
|
const file = data.files?.[0];
|
|
303
310
|
if (!file) {
|
|
304
311
|
return null;
|
|
@@ -322,16 +329,11 @@ export class FileHistoryViewController {
|
|
|
322
329
|
};
|
|
323
330
|
}
|
|
324
331
|
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const data = await response.json();
|
|
331
|
-
if (!response.ok) {
|
|
332
|
-
throw new Error(data.error || 'Failed to load file history');
|
|
333
|
-
}
|
|
334
|
-
|
|
332
|
+
const data = await this.gitApiClient.readFileHistory({
|
|
333
|
+
limit: HISTORY_PAGE_SIZE,
|
|
334
|
+
offset,
|
|
335
|
+
path: this.currentFilePath,
|
|
336
|
+
});
|
|
335
337
|
const commits = Array.isArray(data.commits) ? data.commits : [];
|
|
336
338
|
return {
|
|
337
339
|
commits,
|
|
@@ -350,30 +352,20 @@ export class FileHistoryViewController {
|
|
|
350
352
|
}
|
|
351
353
|
|
|
352
354
|
if (entry.type === 'local') {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const data = await response.json();
|
|
358
|
-
if (!response.ok) {
|
|
359
|
-
throw new Error(data.error || 'Failed to load local diff');
|
|
360
|
-
}
|
|
361
|
-
|
|
355
|
+
const data = await this.gitApiClient.readDiff({
|
|
356
|
+
path: this.currentFilePath || '',
|
|
357
|
+
scope: 'all',
|
|
358
|
+
});
|
|
362
359
|
return {
|
|
363
360
|
detail: data.files?.[0] ?? null,
|
|
364
361
|
summary: data.summary ?? entry.summary ?? null,
|
|
365
362
|
};
|
|
366
363
|
}
|
|
367
364
|
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const data = await response.json();
|
|
373
|
-
if (!response.ok) {
|
|
374
|
-
throw new Error(data.error || 'Failed to load commit diff');
|
|
375
|
-
}
|
|
376
|
-
|
|
365
|
+
const data = await this.gitApiClient.readCommit({
|
|
366
|
+
hash: entry.hash || '',
|
|
367
|
+
path: entry.pathAtCommit || '',
|
|
368
|
+
});
|
|
377
369
|
return {
|
|
378
370
|
commit: data.commit ?? null,
|
|
379
371
|
detail: data.files?.[0] ?? null,
|
|
@@ -564,6 +556,9 @@ export class FileHistoryViewController {
|
|
|
564
556
|
}
|
|
565
557
|
|
|
566
558
|
renderLoading() {
|
|
559
|
+
if (!this.isActive) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
567
562
|
this.page?.classList.remove('hidden');
|
|
568
563
|
if (this.content) {
|
|
569
564
|
this.content.classList.add('diff-content--history');
|
|
@@ -574,6 +569,9 @@ export class FileHistoryViewController {
|
|
|
574
569
|
}
|
|
575
570
|
|
|
576
571
|
renderEmpty(message) {
|
|
572
|
+
if (!this.isActive) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
577
575
|
this.page?.classList.remove('hidden');
|
|
578
576
|
if (this.content) {
|
|
579
577
|
this.content.classList.add('diff-content--history');
|
|
@@ -744,6 +742,9 @@ export class FileHistoryViewController {
|
|
|
744
742
|
}
|
|
745
743
|
|
|
746
744
|
render() {
|
|
745
|
+
if (!this.isActive) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
747
748
|
this.page?.classList.remove('hidden');
|
|
748
749
|
this.captureListScroll();
|
|
749
750
|
this.syncToolbar();
|