// MNote debug /tree shell filetree context menu runtime. import { getFileTreeRowAssetId, getFileTreeRowDocumentId, } from "./tree-shell-filetree-runtime.js"; import { normalizeTreeShellText as normalizeText } from "./tree-shell-state-runtime.js"; const createFileTreeMenuItem = (kind, label, options = {}) => ({ kind, label, disabled: options.disabled === true, reason: normalizeText(options.reason), separatorBefore: options.separatorBefore === true, }); function buildFileTreeMenuTarget(context, rowId, rowKind, documentId, assetId) { const fileTreeRowById = context.getFileTreeRowById(); const item = rowId ? fileTreeRowById.get(rowId) : null; return { item, rowId: rowId || null, rowKind: rowKind || item?.rowKind || "root", documentId: documentId || getFileTreeRowDocumentId(item) || null, assetId: assetId || getFileTreeRowAssetId(item) || null, }; } export function buildTreeShellFileTreeContextMenuProfile(context, target) { const selectedFileTreeRowIds = context.getSelectedFileTreeRowIds(); const fileTreeClipboard = context.getFileTreeClipboard(); const selectedCount = selectedFileTreeRowIds.size; const isMulti = selectedCount > 1 && target.rowId && selectedFileTreeRowIds.has(target.rowId); const item = target.item; const rowKind = target.rowKind || "root"; const canPaste = Boolean(fileTreeClipboard.action && fileTreeClipboard.rowIds.length > 0); const hasDocument = Boolean(getFileTreeRowDocumentId(item)); const hasAsset = Boolean(getFileTreeRowAssetId(item)); const localSource = context.sourceKind === "local_folder"; const canCreateFolder = localSource; const canCreatePage = rowKind === "root" || rowKind === "folder" || rowKind === "document"; const canRename = rowKind === "markdown" || rowKind === "document" || rowKind === "folder" || rowKind === "index"; const canCopyCut = rowKind === "markdown" || rowKind === "document" || rowKind === "index"; const canDelete = rowKind === "markdown" || rowKind === "document" || rowKind === "index" || (localSource && rowKind === "asset"); if (rowKind === "root") { return [ createFileTreeMenuItem("newPage", "新建页面", { disabled: !canCreatePage, reason: "当前 root 不支持新建页面", }), createFileTreeMenuItem("newFolder", "新建文件夹", { disabled: !canCreateFolder, reason: "仅 local_folder 支持新建文件夹", }), createFileTreeMenuItem("upload", "上传/导入", { disabled: true, reason: localSource ? "外部文件请拖入 Explorer" : "仅 local_folder 支持上传", }), createFileTreeMenuItem("refresh", "刷新", { separatorBefore: true }), createFileTreeMenuItem("collapseAll", "全部折叠"), ]; } if (isMulti) { return [ createFileTreeMenuItem("copy", "复制"), createFileTreeMenuItem("cut", "剪切"), createFileTreeMenuItem("delete", "删除", { disabled: false }), createFileTreeMenuItem("moveTo", "移动到", { separatorBefore: true, disabled: true, reason: "目标选择器尚未接入 filetree 菜单", }), ]; } const items = []; if (rowKind === "folder") { items.push(createFileTreeMenuItem("newPage", "新建页面", { disabled: !canCreatePage, reason: "当前文件夹不支持新建页面", })); items.push(createFileTreeMenuItem("newFolder", "新建文件夹", { disabled: !canCreateFolder, reason: "仅 local_folder 支持新建文件夹", })); items.push(createFileTreeMenuItem("paste", "粘贴", { disabled: !canPaste, reason: "剪贴板为空", })); items.push(createFileTreeMenuItem("rename", "重命名", { separatorBefore: true, disabled: !canRename, reason: "当前文件夹不支持重命名", })); items.push(createFileTreeMenuItem("delete", "删除", { disabled: true, reason: "目录删除尚未收口到统一 executor", })); return items; } items.push(createFileTreeMenuItem("open", "打开", { disabled: !hasDocument && !hasAsset, reason: "当前行没有可打开资源", })); items.push(createFileTreeMenuItem("rename", "重命名", { disabled: !canRename, reason: "当前资源暂不支持重命名", })); items.push(createFileTreeMenuItem("copy", "复制", { disabled: !canCopyCut, reason: "当前资源暂不支持复制", })); items.push(createFileTreeMenuItem("cut", "剪切", { disabled: !canCopyCut, reason: "当前资源暂不支持剪切", })); items.push(createFileTreeMenuItem("paste", "粘贴", { disabled: !canPaste, reason: "剪贴板为空", })); items.push(createFileTreeMenuItem("moveTo", "移动到", { disabled: true, reason: "目标选择器尚未接入 filetree 菜单", })); items.push(createFileTreeMenuItem("delete", "删除", { disabled: !canDelete, reason: "当前资源暂不支持删除", })); items.push(createFileTreeMenuItem("reveal", "Reveal", { separatorBefore: true })); if (hasAsset) { items.push(createFileTreeMenuItem("download", "下载", { disabled: true, reason: "下载 executor 尚未接入", })); } if (item?.capabilities?.includes("share")) { items.push(createFileTreeMenuItem("share", "分享", { separatorBefore: true })); } if (item?.capabilities?.includes("publish")) { items.push(createFileTreeMenuItem("publish", "发布")); } return items; } export function createTreeShellFileTreeMenuRuntime(context) { let activeFileTreeMenuElement = null; const closeFileTreeContextMenu = () => { if (!activeFileTreeMenuElement) return; activeFileTreeMenuElement.remove(); activeFileTreeMenuElement = null; }; const executeFileTreeContextMenuAction = async (kind, target) => { closeFileTreeContextMenu(); const item = target.item; if (kind === "open") { if (item) context.openHydratedFileTreeItem(item); return; } if (kind === "rename") { if (target.rowId && getFileTreeRowDocumentId(item)) { context.beginInlineRename("filetree", target.rowId); } return; } if (kind === "copy" || kind === "cut") { const selectedFileTreeRowIds = context.getSelectedFileTreeRowIds(); if (target.rowId && !selectedFileTreeRowIds.has(target.rowId)) { context.commitFileTreeSelection({ selectedRowIds: [target.rowId], anchorRowId: target.rowId, focusedRowId: target.rowId, }); context.syncFileTreeSelectionDom(); } context.setFileTreeClipboard(kind); return; } if (kind === "paste") { await context.pasteFileTreeClipboardInto(target.rowId); return; } if (kind === "delete") { await context.runFileTreeDelete(target.rowId); return; } if (kind === "newPage") { const parentId = item && item.rowKind === "folder" ? item.nodeId : item && item.rowKind === "document" ? item.nodeId : null; await context.handleCreate(parentId); return; } if (kind === "newFolder") { const parentId = item && item.rowKind === "folder" ? item.nodeId : null; const result = await context.sendCommand({ action: "createFolder", workspaceId: context.workspaceId, documentId: "", parentId, title: "新建文件夹", }); context.setStatus("创建文件夹成功"); context.setLastAction("已创建新建文件夹"); context.scheduleRefresh({ renameRowId: context.localCreatedRowIdFromCommandResult(result, "folder"), }); return result; } if (kind === "refresh") { await context.refreshLocalFolderSnapshot(); return; } if (kind === "collapseAll") { context.expanded.clear(); context.renderTree(); return; } if (kind === "reveal") { context.setLastAction(`Reveal ${target.documentId || target.assetId || target.rowId || "root"}`); context.postToHost("tree.filetree.reveal", { documentId: target.documentId, assetId: target.assetId, rowId: target.rowId, payload: target, }); return; } context.postToHost(`tree.filetree.${kind}`, { documentId: target.documentId, assetId: target.assetId, rowId: target.rowId, payload: target, }); }; const openFileTreeContextMenu = ({ documentId, assetId, rowId, rowKind, clientX, clientY, }) => { closeFileTreeContextMenu(); const target = buildFileTreeMenuTarget(context, rowId, rowKind, documentId, assetId); const profile = buildTreeShellFileTreeContextMenuProfile(context, target); const menu = document.createElement("div"); menu.className = "tree-context-menu"; menu.dataset.testid = "filetree-context-menu"; menu.dataset.sourceKind = context.sourceKind; menu.dataset.rowKind = target.rowKind; menu.setAttribute("role", "menu"); profile.forEach((entry) => { if (entry.separatorBefore) { const separator = document.createElement("div"); separator.className = "tree-menu-separator"; separator.setAttribute("role", "separator"); menu.appendChild(separator); } const button = document.createElement("button"); button.type = "button"; button.className = "tree-menu-item"; button.dataset.menuAction = entry.kind; button.setAttribute("role", "menuitem"); button.textContent = entry.label; if (entry.disabled) { button.disabled = true; if (entry.reason) { button.title = entry.reason; button.dataset.disabledReason = entry.reason; } } else { button.addEventListener("click", () => { void executeFileTreeContextMenuAction(entry.kind, target); }); } menu.appendChild(button); }); const closeOnOutside = (event) => { if (activeFileTreeMenuElement && !activeFileTreeMenuElement.contains(event.target)) { closeFileTreeContextMenu(); document.removeEventListener("mousedown", closeOnOutside, true); document.removeEventListener("keydown", closeOnKeyDown, true); } }; const closeOnKeyDown = (event) => { if (event.key === "Escape") { closeFileTreeContextMenu(); document.removeEventListener("mousedown", closeOnOutside, true); document.removeEventListener("keydown", closeOnKeyDown, true); } }; document.body.appendChild(menu); const x = Number.isFinite(clientX) ? clientX : 16; const y = Number.isFinite(clientY) ? clientY : 16; const rect = menu.getBoundingClientRect(); menu.style.left = `${Math.max(4, Math.min(x, window.innerWidth - rect.width - 4))}px`; menu.style.top = `${Math.max(4, Math.min(y, window.innerHeight - rect.height - 4))}px`; activeFileTreeMenuElement = menu; window.setTimeout(() => { document.addEventListener("mousedown", closeOnOutside, true); document.addEventListener("keydown", closeOnKeyDown, true); }, 0); context.setLastAction( assetId ? `已打开资源 ${assetId} 的更多操作` : `已打开文件树节点 ${documentId || rowId || "unknown"} 的更多操作`, ); context.postToHost("tree.filetree.context-menu", { documentId, assetId, rowId, rowKind, payload: { documentId, assetId, rowId, rowKind, x: clientX, y: clientY, }, x: clientX, y: clientY, }); }; return { closeFileTreeContextMenu, executeFileTreeContextMenuAction, openFileTreeContextMenu, }; }