collabmd 0.1.31 → 0.1.32
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-Br4qyh3r.js} +1 -1
- package/dist/client/assets/drawioEditor-ClmnTx5J.js +2 -0
- package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Cf-iV5-3.js} +2 -2
- package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
- package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CXAxVhlw.js} +2 -2
- package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DoH_kMdu.js} +2 -2
- package/dist/client/assets/exportDocument-9wTrBZNS.css +1 -0
- package/dist/client/assets/{index-kPgEYvEX.js → index-C8QZYHvV.js} +2 -2
- package/dist/client/assets/index-CxSbUTs2.css +1 -0
- 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-BjHHMlv0.js +1249 -0
- package/dist/client/assets/vault-api-client-Bf9tB_GW.js +1 -0
- package/dist/client/assets/vault-api-client-D7Ck0CvR.css +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 +4 -1
- 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 +176 -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 +2 -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 +67 -0
- package/src/client/presentation/backlinks-panel.js +6 -9
- package/src/client/presentation/bases-preview-controller.js +26 -7
- 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/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 +103 -28
- 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/bases/base-definition.js +255 -0
- package/src/server/domain/bases/base-expression-runtime.js +1159 -0
- package/src/server/domain/bases/base-index-snapshot-store.js +418 -0
- package/src/server/domain/bases/base-query-results.js +205 -0
- package/src/server/domain/bases/base-query-service.js +68 -1965
- 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 +109 -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-Bia2hI25.js} +0 -0
|
@@ -23,6 +23,12 @@ function getPathLeaf(path) {
|
|
|
23
23
|
.pop() || '';
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function getParentPath(pathValue) {
|
|
27
|
+
const normalized = String(pathValue ?? '').replace(/\/+$/u, '');
|
|
28
|
+
const separatorIndex = normalized.lastIndexOf('/');
|
|
29
|
+
return separatorIndex >= 0 ? normalized.slice(0, separatorIndex) : '';
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
function replacePathPrefix(pathValue, oldPrefix, newPrefix) {
|
|
27
33
|
if (pathValue === oldPrefix) {
|
|
28
34
|
return newPrefix;
|
|
@@ -201,6 +207,10 @@ export class FileActionController {
|
|
|
201
207
|
label: 'Rename / move',
|
|
202
208
|
onSelect: () => this.handleRenameDirectory(directoryPath),
|
|
203
209
|
},
|
|
210
|
+
{
|
|
211
|
+
label: 'Download',
|
|
212
|
+
onSelect: () => this.handleDownloadDirectory(directoryPath),
|
|
213
|
+
},
|
|
204
214
|
{
|
|
205
215
|
label: 'Delete',
|
|
206
216
|
danger: true,
|
|
@@ -215,6 +225,10 @@ export class FileActionController {
|
|
|
215
225
|
label: 'Rename / move',
|
|
216
226
|
onSelect: () => this.handleRenameFile(filePath),
|
|
217
227
|
},
|
|
228
|
+
{
|
|
229
|
+
label: 'Download',
|
|
230
|
+
onSelect: () => this.handleDownloadFile(filePath),
|
|
231
|
+
},
|
|
218
232
|
{
|
|
219
233
|
label: 'Delete',
|
|
220
234
|
danger: true,
|
|
@@ -627,6 +641,81 @@ export class FileActionController {
|
|
|
627
641
|
}
|
|
628
642
|
}
|
|
629
643
|
|
|
644
|
+
planMoveEntryByDrop({ destinationDirectory = '', sourcePath = '', sourceType = 'file' } = {}) {
|
|
645
|
+
const normalizedSourcePath = normalizeVaultPathInput(sourcePath);
|
|
646
|
+
const normalizedDestinationDirectory = normalizeVaultPathInput(destinationDirectory);
|
|
647
|
+
const currentParentPath = getParentPath(normalizedSourcePath);
|
|
648
|
+
const entryName = getPathLeaf(normalizedSourcePath);
|
|
649
|
+
|
|
650
|
+
if (!normalizedSourcePath || !entryName) {
|
|
651
|
+
return { ok: false, reason: 'Invalid item to move' };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (currentParentPath === normalizedDestinationDirectory) {
|
|
655
|
+
return { ok: false, reason: 'Item is already in that folder' };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (sourceType === 'directory') {
|
|
659
|
+
if (normalizedDestinationDirectory === normalizedSourcePath) {
|
|
660
|
+
return { ok: false, reason: 'A folder cannot be moved into itself' };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (normalizedDestinationDirectory.startsWith(`${normalizedSourcePath}/`)) {
|
|
664
|
+
return { ok: false, reason: 'A folder cannot be moved into one of its descendants' };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
nextPath: composeVaultChildPath(normalizedDestinationDirectory, entryName),
|
|
669
|
+
ok: true,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const extension = getVaultFileExtension(normalizedSourcePath) || '.md';
|
|
674
|
+
return {
|
|
675
|
+
extension,
|
|
676
|
+
nextPath: composeVaultChildPath(normalizedDestinationDirectory, entryName),
|
|
677
|
+
ok: true,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
canMoveEntryByDrop(payload = {}) {
|
|
682
|
+
return this.planMoveEntryByDrop(payload).ok;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async moveEntryByDrop(payload = {}) {
|
|
686
|
+
const plan = this.planMoveEntryByDrop(payload);
|
|
687
|
+
if (!plan.ok) {
|
|
688
|
+
this.showToast(plan.reason);
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (payload.sourceType === 'directory') {
|
|
693
|
+
return this.renameDirectory(payload.sourcePath, plan.nextPath);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return this.renameVaultFile(payload.sourcePath, plan.nextPath, plan.extension);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async downloadFileEntry(filePath) {
|
|
700
|
+
try {
|
|
701
|
+
await this.vaultClient.downloadFile(filePath);
|
|
702
|
+
return true;
|
|
703
|
+
} catch (error) {
|
|
704
|
+
this.showToast(error?.message || 'Failed to download file');
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async downloadDirectoryEntry(directoryPath) {
|
|
710
|
+
try {
|
|
711
|
+
await this.vaultClient.downloadDirectory(directoryPath);
|
|
712
|
+
return true;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
this.showToast(error?.message || 'Failed to download folder');
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
630
719
|
getCreateContext(parentDir = '') {
|
|
631
720
|
const normalizedParentDir = normalizeVaultPathInput(parentDir);
|
|
632
721
|
|
|
@@ -908,4 +997,12 @@ export class FileActionController {
|
|
|
908
997
|
onSubmit: () => this.deleteDirectory(directoryPath, { recursive: hasDescendants }),
|
|
909
998
|
});
|
|
910
999
|
}
|
|
1000
|
+
|
|
1001
|
+
handleDownloadFile(filePath) {
|
|
1002
|
+
void this.downloadFileEntry(filePath);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
handleDownloadDirectory(directoryPath) {
|
|
1006
|
+
void this.downloadDirectoryEntry(directoryPath);
|
|
1007
|
+
}
|
|
911
1008
|
}
|
|
@@ -29,6 +29,7 @@ export class FileExplorerController {
|
|
|
29
29
|
this.state.toggleDirectory(pathValue);
|
|
30
30
|
this.renderTree();
|
|
31
31
|
},
|
|
32
|
+
onEntryDrop: (payload) => this.actionController.moveEntryByDrop(payload),
|
|
32
33
|
onFileContextMenu: (event, payload) => {
|
|
33
34
|
if (payload.type === 'directory') {
|
|
34
35
|
this.view.showContextMenu(event, this.actionController.getDirectoryContextMenuItems(payload.directoryPath));
|
|
@@ -40,6 +41,7 @@ export class FileExplorerController {
|
|
|
40
41
|
onFileSelect: (filePath) => {
|
|
41
42
|
this.onFileSelect?.(filePath);
|
|
42
43
|
},
|
|
44
|
+
onValidateDrop: (payload) => this.actionController.canMoveEntryByDrop(payload),
|
|
43
45
|
onSearchChange: (value) => {
|
|
44
46
|
this.state.setSearchQuery(value);
|
|
45
47
|
this.renderTree();
|
|
@@ -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);
|