Files
mnote/rust/crates/mnote-web/browser/tree-shell-filetree-menu-runtime.js
T

354 lines
12 KiB
JavaScript

// 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,
};
}