Land password-vault dedicated workbench and mnote-vault-core/CLI, agent token read path design, vault transport split, and retire obsolete filetree smokes. Ignore local vault reimport scripts that trip secret scanners.
243 lines
10 KiB
JavaScript
243 lines
10 KiB
JavaScript
// 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,
|
|
};
|