// 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; var activeFileTreeDropPosition = null; // ─── 纯 helper ───────────────────────────────────────── function filetreeDragPayload(rowIds) { return JSON.stringify({ type: 'mnote-file-tree-dnd', version: 1, rowIds: rowIds || draggingFileTreeRowIds, }); } /** * 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'; } function filetreeDropDetail(targetRow, fileTree, deps) { deps = deps || {}; var resolveWorkspaceId = deps.resolveWorkspaceId || function() { return ''; }; var fileTreeRowLocalUploadTargetRelativePath = deps.fileTreeRowLocalUploadTargetRelativePath || function() { return ''; }; var dropPosition = deps.dropPosition || null; 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', dropPosition: dropPosition || (targetRow ? 'inside' : 'inside'), }; } 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'); activeFileTreeDropRow.removeAttribute('data-drop-position'); activeFileTreeDropRow.setAttribute('data-drop-feedback', 'false'); } activeFileTreeDropRow = null; 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); } 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(); 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'; } 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(); 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'; } var detail = filetreeDropDetail(targetRow, fileTree, { resolveWorkspaceId: resolveWorkspaceId, fileTreeRowLocalUploadTargetRelativePath: fileTreeRowLocalUploadTargetRelativePath, dropPosition: dropPosition, }); 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, dropPosition: dropPosition, })); }); } draggingFileTreeRowIds = []; } // ─── 导出 ─────────────────────────────────────────────── window.__mnoteFileTreeDndRuntime = { FILETREE_DRAG_MIME: FILETREE_DRAG_MIME, get draggingFileTreeRowIds() { return draggingFileTreeRowIds; }, set draggingFileTreeRowIds(value) { draggingFileTreeRowIds = value; }, get activeFileTreeDropRow() { return activeFileTreeDropRow; }, get activeFileTreeDropPosition() { return activeFileTreeDropPosition; }, filetreeDragPayload: filetreeDragPayload, filetreeDropDetail: filetreeDropDetail, fileTreeDropPosition: fileTreeDropPosition, filetreeHasFiles: filetreeHasFiles, filetreeHasInternalDrag: filetreeHasInternalDrag, parseFileTreeDragPayload: parseFileTreeDragPayload, clearFileTreeDropFeedback: clearFileTreeDropFeedback, setFileTreeDropFeedback: setFileTreeDropFeedback, resetFileTreeDragState: resetFileTreeDragState, startFileTreeDrag: startFileTreeDrag, handleFileTreeDragOver: handleFileTreeDragOver, handleFileTreeDrop: handleFileTreeDrop, };