2026-05-25 17:36:17 +08:00
|
|
|
// MNote filetree DND 运行时外置模块。
|
|
|
|
|
// 承接 filetree dragstart/dragover/drop/dragend 的业务逻辑,
|
|
|
|
|
// 不处理 page tree DND(留在 layout.rs)。
|
|
|
|
|
// inline script 保留完整 fallback。
|
|
|
|
|
|
|
|
|
|
// ─── 常量 ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
var FILETREE_DRAG_MIME = 'application/x-mnote-file-tree';
|
|
|
|
|
|
|
|
|
|
// ─── 状态 ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
var draggingFileTreeRowIds = [];
|
|
|
|
|
var activeFileTreeDropRow = null;
|
2026-07-24 11:36:06 +08:00
|
|
|
var activeFileTreeDropPosition = null;
|
2026-05-25 17:36:17 +08:00
|
|
|
|
|
|
|
|
// ─── 纯 helper ─────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function filetreeDragPayload(rowIds) {
|
|
|
|
|
return JSON.stringify({
|
|
|
|
|
type: 'mnote-file-tree-dnd',
|
|
|
|
|
version: 1,
|
|
|
|
|
rowIds: rowIds || draggingFileTreeRowIds,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-24 11:36:06 +08:00
|
|
|
/**
|
|
|
|
|
* Wolai-style drop zones on a row:
|
|
|
|
|
* - top 25% → before (sibling reorder line)
|
|
|
|
|
* - bottom 25% → after
|
|
|
|
|
* - middle → inside folders/dirs; leaf rows treat middle as after
|
|
|
|
|
*/
|
|
|
|
|
function fileTreeDropPosition(event, row) {
|
|
|
|
|
if (!(row instanceof HTMLElement) || !event) return 'inside';
|
|
|
|
|
var rect = row.getBoundingClientRect();
|
|
|
|
|
var ratio = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5;
|
|
|
|
|
if (ratio < 0.25) return 'before';
|
|
|
|
|
if (ratio > 0.75) return 'after';
|
|
|
|
|
var kind = String(row.getAttribute('data-row-kind') || '').trim();
|
|
|
|
|
if (kind === 'folder' || kind === 'directory' || kind === 'doc' || kind === 'document') {
|
|
|
|
|
// documents can host children in page/file tree hybrid; folders always accept inside
|
|
|
|
|
if (kind === 'folder' || kind === 'directory') return 'inside';
|
|
|
|
|
// expandable document rows also accept nest-into
|
|
|
|
|
if (row.getAttribute('aria-expanded') != null && row.querySelector('.tree-toggle')) return 'inside';
|
|
|
|
|
}
|
|
|
|
|
// non-container leaf: middle zone still sorts after the row
|
|
|
|
|
return 'after';
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 17:36:17 +08:00
|
|
|
function filetreeDropDetail(targetRow, fileTree, deps) {
|
|
|
|
|
deps = deps || {};
|
|
|
|
|
var resolveWorkspaceId = deps.resolveWorkspaceId || function() { return ''; };
|
|
|
|
|
var fileTreeRowLocalUploadTargetRelativePath = deps.fileTreeRowLocalUploadTargetRelativePath || function() { return ''; };
|
2026-07-24 11:36:06 +08:00
|
|
|
var dropPosition = deps.dropPosition || null;
|
2026-05-25 17:36:17 +08:00
|
|
|
return {
|
|
|
|
|
workspaceId: resolveWorkspaceId(targetRow || fileTree),
|
|
|
|
|
targetRowId: targetRow ? targetRow.getAttribute('data-row-id') : null,
|
|
|
|
|
targetRowKind: targetRow ? targetRow.getAttribute('data-row-kind') : 'root',
|
|
|
|
|
documentId: targetRow
|
|
|
|
|
? targetRow.getAttribute('data-document-id') || targetRow.getAttribute('data-doc-id')
|
|
|
|
|
: null,
|
|
|
|
|
assetId: targetRow ? targetRow.getAttribute('data-asset-id') : null,
|
|
|
|
|
targetRelativePath: targetRow ? fileTreeRowLocalUploadTargetRelativePath(targetRow) : '',
|
|
|
|
|
uploadIntent: 'filetree.folder.drop',
|
2026-07-24 11:36:06 +08:00
|
|
|
dropPosition: dropPosition || (targetRow ? 'inside' : 'inside'),
|
2026-05-25 17:36:17 +08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filetreeHasFiles(dataTransfer) {
|
|
|
|
|
return dataTransfer && Array.prototype.indexOf.call(dataTransfer.types || [], 'Files') >= 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filetreeHasInternalDrag(dataTransfer) {
|
|
|
|
|
return dataTransfer && Array.prototype.indexOf.call(dataTransfer.types || [], FILETREE_DRAG_MIME) >= 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseFileTreeDragPayload(raw) {
|
|
|
|
|
if (!raw) return [];
|
|
|
|
|
try {
|
|
|
|
|
var parsed = JSON.parse(raw);
|
|
|
|
|
if (Array.isArray(parsed.rowIds)) return parsed.rowIds;
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearFileTreeDropFeedback() {
|
|
|
|
|
if (activeFileTreeDropRow instanceof HTMLElement) {
|
|
|
|
|
activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
|
2026-07-24 11:36:06 +08:00
|
|
|
activeFileTreeDropRow.removeAttribute('data-drop-position');
|
|
|
|
|
activeFileTreeDropRow.setAttribute('data-drop-feedback', 'false');
|
2026-05-25 17:36:17 +08:00
|
|
|
}
|
|
|
|
|
activeFileTreeDropRow = null;
|
2026-07-24 11:36:06 +08:00
|
|
|
activeFileTreeDropPosition = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setFileTreeDropFeedback(row, position) {
|
|
|
|
|
clearFileTreeDropFeedback();
|
|
|
|
|
if (!(row instanceof HTMLElement)) return;
|
|
|
|
|
activeFileTreeDropRow = row;
|
|
|
|
|
activeFileTreeDropPosition = position || 'inside';
|
|
|
|
|
activeFileTreeDropRow.setAttribute('data-drop-target', 'true');
|
|
|
|
|
activeFileTreeDropRow.setAttribute('data-drop-feedback', 'true');
|
|
|
|
|
activeFileTreeDropRow.setAttribute('data-drop-position', activeFileTreeDropPosition);
|
2026-05-25 17:36:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetFileTreeDragState() {
|
|
|
|
|
draggingFileTreeRowIds = [];
|
|
|
|
|
clearFileTreeDropFeedback();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── 文件树 DND 开始 ───────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function startFileTreeDrag(fileRow, deps) {
|
|
|
|
|
deps = deps || {};
|
|
|
|
|
var selectedSidebarFileTreeRowIdsForDrag = deps.selectedSidebarFileTreeRowIdsForDrag || function(r) {
|
|
|
|
|
return [r.getAttribute('data-row-id') || ''].filter(Boolean);
|
|
|
|
|
};
|
|
|
|
|
draggingFileTreeRowIds = selectedSidebarFileTreeRowIdsForDrag(fileRow);
|
|
|
|
|
return {
|
|
|
|
|
payload: filetreeDragPayload(draggingFileTreeRowIds),
|
|
|
|
|
effectAllowed: 'copyMove',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── 文件树 DND 悬浮 ───────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function handleFileTreeDragOver(event, deps) {
|
|
|
|
|
deps = deps || {};
|
|
|
|
|
var closestAction = deps.closestAction || function() { return null; };
|
|
|
|
|
var fileTree = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
if (!fileTree || !fileTree.contains(event.target)) return false;
|
|
|
|
|
|
|
|
|
|
var hasFiles = filetreeHasFiles(event.dataTransfer);
|
|
|
|
|
var hasInternal = filetreeHasInternalDrag(event.dataTransfer);
|
|
|
|
|
if (!hasFiles && !hasInternal && draggingFileTreeRowIds.length === 0) return false;
|
|
|
|
|
|
|
|
|
|
event.preventDefault();
|
2026-07-24 11:36:06 +08:00
|
|
|
var row = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
|
|
|
|
|
if (row instanceof HTMLElement) {
|
|
|
|
|
var position = hasFiles ? 'inside' : fileTreeDropPosition(event, row);
|
|
|
|
|
// external file drops only nest into containers
|
|
|
|
|
if (hasFiles) {
|
|
|
|
|
var kind = String(row.getAttribute('data-row-kind') || '').trim();
|
|
|
|
|
if (kind !== 'folder' && kind !== 'directory' && kind !== 'doc' && kind !== 'document') {
|
|
|
|
|
// drop onto leaf file → treat as after (parent folder context resolved at drop)
|
|
|
|
|
position = 'after';
|
|
|
|
|
} else if (kind === 'folder' || kind === 'directory') {
|
|
|
|
|
position = 'inside';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setFileTreeDropFeedback(row, position);
|
|
|
|
|
} else {
|
|
|
|
|
clearFileTreeDropFeedback();
|
|
|
|
|
activeFileTreeDropRow = fileTree;
|
|
|
|
|
if (activeFileTreeDropRow instanceof HTMLElement) {
|
|
|
|
|
activeFileTreeDropRow.setAttribute('data-drop-target', 'true');
|
|
|
|
|
}
|
|
|
|
|
activeFileTreeDropPosition = 'inside';
|
2026-05-25 17:36:17 +08:00
|
|
|
}
|
|
|
|
|
if (event.dataTransfer) {
|
|
|
|
|
var copyModifier = event.altKey || event.ctrlKey || event.metaKey;
|
|
|
|
|
event.dataTransfer.dropEffect = hasFiles || copyModifier ? 'copy' : 'move';
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── 文件树 DND 释放 ───────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function handleFileTreeDrop(event, deps) {
|
|
|
|
|
deps = deps || {};
|
|
|
|
|
var closestAction = deps.closestAction || function() { return null; };
|
|
|
|
|
var resolveWorkspaceId = deps.resolveWorkspaceId || function() { return ''; };
|
|
|
|
|
var fileTreeRowLocalUploadTargetRelativePath = deps.fileTreeRowLocalUploadTargetRelativePath
|
|
|
|
|
|| function() { return ''; };
|
|
|
|
|
var ensureFileTreeWritableTarget = deps.ensureFileTreeWritableTarget || function() { return Promise.resolve(true); };
|
|
|
|
|
var dispatchSidebarEvent = deps.dispatchSidebarEvent || function() {};
|
|
|
|
|
var fileTree = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
if (!fileTree || !fileTree.contains(event.target)) return;
|
|
|
|
|
|
|
|
|
|
var targetRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
|
|
|
|
|
var files = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : [];
|
|
|
|
|
var raw = event.dataTransfer
|
|
|
|
|
? event.dataTransfer.getData(FILETREE_DRAG_MIME)
|
|
|
|
|
|| event.dataTransfer.getData('application/x-mnote-file-tree')
|
|
|
|
|
|| ''
|
|
|
|
|
: '';
|
|
|
|
|
var rowIds = draggingFileTreeRowIds.slice();
|
|
|
|
|
if (raw) {
|
|
|
|
|
rowIds = parseFileTreeDragPayload(raw);
|
|
|
|
|
}
|
|
|
|
|
if (!files.length && !rowIds.length) return;
|
|
|
|
|
|
|
|
|
|
event.preventDefault();
|
2026-07-24 11:36:06 +08:00
|
|
|
var dropPosition = activeFileTreeDropPosition
|
|
|
|
|
|| (targetRow ? fileTreeDropPosition(event, targetRow) : 'inside');
|
|
|
|
|
if (files.length) {
|
|
|
|
|
var fileKind = targetRow ? String(targetRow.getAttribute('data-row-kind') || '').trim() : '';
|
|
|
|
|
if (fileKind === 'folder' || fileKind === 'directory') dropPosition = 'inside';
|
|
|
|
|
}
|
2026-05-25 17:36:17 +08:00
|
|
|
var detail = filetreeDropDetail(targetRow, fileTree, {
|
|
|
|
|
resolveWorkspaceId: resolveWorkspaceId,
|
|
|
|
|
fileTreeRowLocalUploadTargetRelativePath: fileTreeRowLocalUploadTargetRelativePath,
|
2026-07-24 11:36:06 +08:00
|
|
|
dropPosition: dropPosition,
|
2026-05-25 17:36:17 +08:00
|
|
|
});
|
|
|
|
|
clearFileTreeDropFeedback();
|
|
|
|
|
if (files.length) {
|
|
|
|
|
dispatchSidebarEvent('tree.filetree.external-drop', Object.assign({}, detail, { files: files }));
|
|
|
|
|
} else {
|
|
|
|
|
var copyModifier = event.altKey === true || event.ctrlKey === true || event.metaKey === true;
|
|
|
|
|
void Promise.resolve(ensureFileTreeWritableTarget('drop', targetRow, rowIds, copyModifier))
|
|
|
|
|
.then(function(writable) {
|
|
|
|
|
if (!writable) return;
|
|
|
|
|
dispatchSidebarEvent('tree.filetree.internal-drop', Object.assign({}, detail, {
|
|
|
|
|
rowIds: rowIds,
|
|
|
|
|
copy: copyModifier,
|
2026-07-24 11:36:06 +08:00
|
|
|
dropPosition: dropPosition,
|
2026-05-25 17:36:17 +08:00
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
draggingFileTreeRowIds = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── 导出 ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
window.__mnoteFileTreeDndRuntime = {
|
|
|
|
|
FILETREE_DRAG_MIME: FILETREE_DRAG_MIME,
|
2026-07-24 11:36:06 +08:00
|
|
|
get draggingFileTreeRowIds() { return draggingFileTreeRowIds; },
|
|
|
|
|
set draggingFileTreeRowIds(value) { draggingFileTreeRowIds = value; },
|
|
|
|
|
get activeFileTreeDropRow() { return activeFileTreeDropRow; },
|
|
|
|
|
get activeFileTreeDropPosition() { return activeFileTreeDropPosition; },
|
2026-05-25 17:36:17 +08:00
|
|
|
filetreeDragPayload: filetreeDragPayload,
|
|
|
|
|
filetreeDropDetail: filetreeDropDetail,
|
2026-07-24 11:36:06 +08:00
|
|
|
fileTreeDropPosition: fileTreeDropPosition,
|
2026-05-25 17:36:17 +08:00
|
|
|
filetreeHasFiles: filetreeHasFiles,
|
|
|
|
|
filetreeHasInternalDrag: filetreeHasInternalDrag,
|
|
|
|
|
parseFileTreeDragPayload: parseFileTreeDragPayload,
|
|
|
|
|
clearFileTreeDropFeedback: clearFileTreeDropFeedback,
|
2026-07-24 11:36:06 +08:00
|
|
|
setFileTreeDropFeedback: setFileTreeDropFeedback,
|
2026-05-25 17:36:17 +08:00
|
|
|
resetFileTreeDragState: resetFileTreeDragState,
|
|
|
|
|
startFileTreeDrag: startFileTreeDrag,
|
|
|
|
|
handleFileTreeDragOver: handleFileTreeDragOver,
|
|
|
|
|
handleFileTreeDrop: handleFileTreeDrop,
|
|
|
|
|
};
|