|
|
|
@@ -103,6 +103,12 @@ type TreeShellInlineRendererInput = {
|
|
|
|
|
channel: string;
|
|
|
|
|
commandNames: string[];
|
|
|
|
|
};
|
|
|
|
|
runtimeArtifact: {
|
|
|
|
|
contractName: "rust_tree_shell_runtime_artifact_v1";
|
|
|
|
|
inputFields: string[];
|
|
|
|
|
outputChannels: string[];
|
|
|
|
|
eventKinds: string[];
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type TreeShellIframeHostProps = {
|
|
|
|
@@ -212,6 +218,19 @@ const PICKER_STATE_REDUCER_ACTIONS = [
|
|
|
|
|
"end",
|
|
|
|
|
"pick",
|
|
|
|
|
] as const;
|
|
|
|
|
const TREE_SHELL_RUNTIME_ARTIFACT = {
|
|
|
|
|
contractName: "rust_tree_shell_runtime_artifact_v1",
|
|
|
|
|
inputFields: [
|
|
|
|
|
"rendererInput",
|
|
|
|
|
"projectionItems",
|
|
|
|
|
"expandedIds",
|
|
|
|
|
"selectedRowIds",
|
|
|
|
|
"activePickerItem",
|
|
|
|
|
"focusedId",
|
|
|
|
|
],
|
|
|
|
|
outputChannels: ["domPatch", "intentEvent", "commandDispatchEvent"],
|
|
|
|
|
eventKinds: ["focus", "keyboard", "expandCollapse", "selection", "contextMenu", "dragDrop", "pick"],
|
|
|
|
|
} as const satisfies TreeShellInlineRendererInput["runtimeArtifact"];
|
|
|
|
|
|
|
|
|
|
const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
@@ -706,6 +725,10 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
target: { documentId: nodeId },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (mode === "page" && usedRustInitialRenderer && patchPageTreeExpansionDom(nodeId)) {
|
|
|
|
|
focusRowElement(nodeId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
renderTree();
|
|
|
|
|
focusRowElement(nodeId);
|
|
|
|
|
};
|
|
|
|
@@ -735,6 +758,11 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
}
|
|
|
|
|
focusedNodeId = nextFocusId;
|
|
|
|
|
postPageFocusChange(nextFocusId);
|
|
|
|
|
if (usedRustInitialRenderer) {
|
|
|
|
|
patchPageTreeActiveDom();
|
|
|
|
|
focusRowElement(nextFocusId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
renderTree();
|
|
|
|
|
focusRowElement(nextFocusId);
|
|
|
|
|
};
|
|
|
|
@@ -773,11 +801,22 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (actionKind === "expand") {
|
|
|
|
|
if (item?.nodeId) toggleExpand(item.nodeId);
|
|
|
|
|
if (item?.childCount > 0 && !expanded.has(item.nodeId)) {
|
|
|
|
|
toggleExpand(item.nodeId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const firstChild = item ? getSiblings(item.nodeId)[0] : null;
|
|
|
|
|
if (firstChild) focusNode(firstChild.nodeId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (actionKind === "collapse") {
|
|
|
|
|
if (item?.nodeId) toggleExpand(item.nodeId);
|
|
|
|
|
if (item?.childCount > 0 && expanded.has(item.nodeId)) {
|
|
|
|
|
toggleExpand(item.nodeId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (item?.parentNodeId && itemById.has(item.parentNodeId)) {
|
|
|
|
|
focusNode(item.parentNodeId);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (actionKind === "open") {
|
|
|
|
@@ -1245,7 +1284,7 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
syncFileTreeSelectionDom();
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
const getVisiblePickerEntries = () => {
|
|
|
|
|
const getPickablePickerEntries = () => {
|
|
|
|
|
const visible = [];
|
|
|
|
|
if (allowRootPick) {
|
|
|
|
|
visible.push({ pickerItemKey: "__root__", documentId: null });
|
|
|
|
@@ -1277,7 +1316,7 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
if (fromActive) return fromActive;
|
|
|
|
|
const fromDocument = normalizePickerItemKey(currentActiveDocumentId);
|
|
|
|
|
if (fromDocument) return fromDocument;
|
|
|
|
|
return getVisiblePickerEntries()[0]?.pickerItemKey || "";
|
|
|
|
|
return getPickablePickerEntries()[0]?.pickerItemKey || "";
|
|
|
|
|
};
|
|
|
|
|
const computePickerStateActionResult = (action) => {
|
|
|
|
|
const currentPickerItemKey = resolveCurrentPickerItemKey();
|
|
|
|
@@ -1295,21 +1334,21 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
pickedRoot: action?.kind === "pick" && currentPickerItemKey === "__root__",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const visible = getVisiblePickerEntries();
|
|
|
|
|
if (visible.length === 0) {
|
|
|
|
|
const pickable = getPickablePickerEntries();
|
|
|
|
|
if (pickable.length === 0) {
|
|
|
|
|
return {
|
|
|
|
|
nextItemKey: "",
|
|
|
|
|
pickedDocumentId: null,
|
|
|
|
|
pickedRoot: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const currentIndex = visible.findIndex(
|
|
|
|
|
const currentIndex = pickable.findIndex(
|
|
|
|
|
(entry) => entry.pickerItemKey === currentPickerItemKey,
|
|
|
|
|
);
|
|
|
|
|
const baseIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
|
|
|
if (action.kind === "normalize") {
|
|
|
|
|
return {
|
|
|
|
|
nextItemKey: visible[baseIndex]?.pickerItemKey || "",
|
|
|
|
|
nextItemKey: pickable[baseIndex]?.pickerItemKey || "",
|
|
|
|
|
pickedDocumentId: null,
|
|
|
|
|
pickedRoot: false,
|
|
|
|
|
};
|
|
|
|
@@ -1317,13 +1356,13 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
if (action.kind === "focus") {
|
|
|
|
|
const nextItemKey = normalizePickerItemKey(action.itemKey);
|
|
|
|
|
return {
|
|
|
|
|
nextItemKey: nextItemKey || visible[0]?.pickerItemKey || "",
|
|
|
|
|
nextItemKey: nextItemKey || pickable[0]?.pickerItemKey || "",
|
|
|
|
|
pickedDocumentId: null,
|
|
|
|
|
pickedRoot: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (action.kind === "pick") {
|
|
|
|
|
const target = visible[baseIndex];
|
|
|
|
|
const target = pickable[baseIndex];
|
|
|
|
|
const targetKey = target?.pickerItemKey || "";
|
|
|
|
|
return {
|
|
|
|
|
nextItemKey: targetKey,
|
|
|
|
@@ -1338,20 +1377,35 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
: action.kind === "home"
|
|
|
|
|
? 0
|
|
|
|
|
: action.kind === "end"
|
|
|
|
|
? visible.length - 1
|
|
|
|
|
: Math.min(visible.length - 1, baseIndex + 1);
|
|
|
|
|
const target = visible[nextIndex];
|
|
|
|
|
? pickable.length - 1
|
|
|
|
|
: Math.min(pickable.length - 1, baseIndex + 1);
|
|
|
|
|
const target = pickable[nextIndex];
|
|
|
|
|
return {
|
|
|
|
|
nextItemKey: target?.pickerItemKey || "",
|
|
|
|
|
pickedDocumentId: null,
|
|
|
|
|
pickedRoot: false,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
const applyPickerFocusByItemKey = (pickerItemKey) => {
|
|
|
|
|
const postPickerPickResultToHost = (result) => {
|
|
|
|
|
if (mode !== "picker" || !result) return;
|
|
|
|
|
if (result.pickedRoot) {
|
|
|
|
|
postToHost("tree.pick.root", { documentId: null, target: { documentId: null } });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (result.pickedDocumentId) {
|
|
|
|
|
postToHost("tree.pick", {
|
|
|
|
|
documentId: result.pickedDocumentId,
|
|
|
|
|
itemKey: result.nextItemKey || result.pickedDocumentId,
|
|
|
|
|
target: { documentId: result.pickedDocumentId },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const applyPickerFocusByItemKey = (pickerItemKey, options = {}) => {
|
|
|
|
|
const result = computePickerStateActionResult({
|
|
|
|
|
kind: "focus",
|
|
|
|
|
itemKey: pickerItemKey,
|
|
|
|
|
});
|
|
|
|
|
const shouldFocusDom = options.focusDom === true;
|
|
|
|
|
currentActivePickerItemKey = result.nextItemKey || "";
|
|
|
|
|
currentActiveDocumentId =
|
|
|
|
|
result.nextItemKey && result.nextItemKey !== "__root__"
|
|
|
|
@@ -1364,10 +1418,11 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
});
|
|
|
|
|
if (usedRustInitialRenderer) {
|
|
|
|
|
patchPickerActiveDom();
|
|
|
|
|
focusPickerRowElement(currentActivePickerItemKey);
|
|
|
|
|
if (shouldFocusDom) focusPickerRowElement(currentActivePickerItemKey);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
renderTree();
|
|
|
|
|
if (shouldFocusDom) focusPickerRowElement(currentActivePickerItemKey);
|
|
|
|
|
};
|
|
|
|
|
const applyPickerStateAction = (action) => {
|
|
|
|
|
const result = computePickerStateActionResult(action);
|
|
|
|
@@ -1380,7 +1435,8 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
if (!(row instanceof HTMLElement)) return;
|
|
|
|
|
row.dataset.focused = String(currentActivePickerItemKey === "__root__");
|
|
|
|
|
row.addEventListener("click", () => {
|
|
|
|
|
postToHost("tree.pick.root", { documentId: null, target: { documentId: null } });
|
|
|
|
|
applyPickerFocusByItemKey("__root__", { focusDom: true });
|
|
|
|
|
postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
const bindPickerRowEvents = (row, item) => {
|
|
|
|
@@ -1391,7 +1447,10 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
currentActivePickerItemKey === item.nodeId ||
|
|
|
|
|
(!currentActivePickerItemKey && currentActiveDocumentId === item.nodeId),
|
|
|
|
|
);
|
|
|
|
|
row.addEventListener("click", () => handleNavigate(item.nodeId));
|
|
|
|
|
row.addEventListener("click", () => {
|
|
|
|
|
applyPickerFocusByItemKey(item.nodeId, { focusDom: true });
|
|
|
|
|
postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
const focusPickerRowElement = (pickerItemKey) => {
|
|
|
|
|
const normalizedItemKey = normalizeText(pickerItemKey);
|
|
|
|
@@ -1481,6 +1540,12 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
if (!(element instanceof HTMLElement)) return;
|
|
|
|
|
element.addEventListener("click", (event) => {
|
|
|
|
|
const action = normalizeText(element.dataset.rustAction);
|
|
|
|
|
if (action === "toggle") {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
toggleExpand(item.nodeId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (action === "open") {
|
|
|
|
|
handleNavigate(item.nodeId);
|
|
|
|
|
return;
|
|
|
|
@@ -1491,6 +1556,32 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
void runTreeCommand({ action: "create", parentId: item.nodeId });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (action === "rename") {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
const nextTitle = window.prompt("重命名页面", item.title);
|
|
|
|
|
const title = normalizeText(nextTitle);
|
|
|
|
|
if (!title || title === item.title) return;
|
|
|
|
|
void runTreeCommand({
|
|
|
|
|
action: "rename",
|
|
|
|
|
documentId: item.nodeId,
|
|
|
|
|
title,
|
|
|
|
|
})
|
|
|
|
|
.then(() => {
|
|
|
|
|
item.title = title;
|
|
|
|
|
const titleElement = row.querySelector(".tree-link-title");
|
|
|
|
|
if (titleElement) titleElement.textContent = title;
|
|
|
|
|
setLastAction(\`已重命名为 \${title}\`);
|
|
|
|
|
postToHost("tree.node.renamed", {
|
|
|
|
|
documentId: item.nodeId,
|
|
|
|
|
target: { documentId: item.nodeId },
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
setLastAction(error instanceof Error ? error.message : "重命名失败", "error");
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (action === "menu") {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
@@ -1504,6 +1595,141 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
const renderHydratedPageNode = (item) => {
|
|
|
|
|
if (!item) return null;
|
|
|
|
|
const children = getSiblings(item.nodeId);
|
|
|
|
|
const hasChildren = item.childCount > 0 && children.length > 0;
|
|
|
|
|
const nodeElement = document.createElement("li");
|
|
|
|
|
nodeElement.className = "tree-node";
|
|
|
|
|
nodeElement.dataset.nodeId = item.nodeId;
|
|
|
|
|
|
|
|
|
|
const row = document.createElement("div");
|
|
|
|
|
row.className = "tree-row";
|
|
|
|
|
row.setAttribute("role", "treeitem");
|
|
|
|
|
row.setAttribute("aria-level", String(item.depth + 1));
|
|
|
|
|
row.setAttribute("aria-expanded", hasChildren ? String(expanded.has(item.nodeId)) : "false");
|
|
|
|
|
row.setAttribute("data-testid", "tree-node-open");
|
|
|
|
|
row.dataset.rustRenderedRow = "page";
|
|
|
|
|
row.dataset.nodeId = item.nodeId;
|
|
|
|
|
row.dataset.shellMode = "page";
|
|
|
|
|
row.dataset.active = String(item.nodeId === currentActiveDocumentId);
|
|
|
|
|
row.dataset.focused = String(item.nodeId === focusedNodeId);
|
|
|
|
|
row.dataset.draggable = "true";
|
|
|
|
|
row.draggable = true;
|
|
|
|
|
|
|
|
|
|
if (hasChildren) {
|
|
|
|
|
const toggleButton = document.createElement("button");
|
|
|
|
|
toggleButton.type = "button";
|
|
|
|
|
toggleButton.className = "tree-toggle";
|
|
|
|
|
toggleButton.setAttribute("data-testid", "tree-node-toggle");
|
|
|
|
|
toggleButton.dataset.rustAction = "toggle";
|
|
|
|
|
toggleButton.dataset.nodeId = item.nodeId;
|
|
|
|
|
toggleButton.setAttribute("aria-label", \`\${expanded.has(item.nodeId) ? "折叠" : "展开"} \${item.title}\`);
|
|
|
|
|
toggleButton.textContent = expanded.has(item.nodeId) ? "▾" : "▸";
|
|
|
|
|
row.appendChild(toggleButton);
|
|
|
|
|
} else {
|
|
|
|
|
const spacer = document.createElement("span");
|
|
|
|
|
spacer.className = "tree-spacer";
|
|
|
|
|
spacer.setAttribute("aria-hidden", "true");
|
|
|
|
|
row.appendChild(spacer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
row.appendChild(createKindBadge("page"));
|
|
|
|
|
|
|
|
|
|
const linkButton = document.createElement("button");
|
|
|
|
|
linkButton.type = "button";
|
|
|
|
|
linkButton.className = "tree-link";
|
|
|
|
|
linkButton.setAttribute("data-testid", "tree-node-open");
|
|
|
|
|
linkButton.dataset.rustAction = "open";
|
|
|
|
|
linkButton.dataset.nodeId = item.nodeId;
|
|
|
|
|
const titleElement = document.createElement("span");
|
|
|
|
|
titleElement.className = "tree-link-title";
|
|
|
|
|
titleElement.textContent = item.title;
|
|
|
|
|
linkButton.appendChild(titleElement);
|
|
|
|
|
row.appendChild(linkButton);
|
|
|
|
|
|
|
|
|
|
const actions = document.createElement("div");
|
|
|
|
|
actions.className = "tree-actions";
|
|
|
|
|
[
|
|
|
|
|
["create", "tree-action-create", "+", "新建子页面"],
|
|
|
|
|
["rename", "tree-action-rename", "✎", "重命名"],
|
|
|
|
|
["menu", "tree-action-menu", "…", "更多操作"],
|
|
|
|
|
].forEach(([action, testId, label, ariaLabel]) => {
|
|
|
|
|
const button = document.createElement("button");
|
|
|
|
|
button.type = "button";
|
|
|
|
|
button.className = "tree-action";
|
|
|
|
|
button.setAttribute("data-testid", testId);
|
|
|
|
|
button.dataset.rustAction = action;
|
|
|
|
|
button.dataset.nodeId = item.nodeId;
|
|
|
|
|
button.setAttribute("aria-label", ariaLabel);
|
|
|
|
|
button.textContent = label;
|
|
|
|
|
actions.appendChild(button);
|
|
|
|
|
});
|
|
|
|
|
row.appendChild(actions);
|
|
|
|
|
nodeElement.appendChild(row);
|
|
|
|
|
bindPageRowEvents(row, item);
|
|
|
|
|
|
|
|
|
|
if (hasChildren && expanded.has(item.nodeId)) {
|
|
|
|
|
const childrenList = document.createElement("ul");
|
|
|
|
|
childrenList.className = "tree-children";
|
|
|
|
|
children.forEach((child) => {
|
|
|
|
|
const childNode = renderHydratedPageNode(child);
|
|
|
|
|
if (childNode) childrenList.appendChild(childNode);
|
|
|
|
|
});
|
|
|
|
|
nodeElement.appendChild(childrenList);
|
|
|
|
|
}
|
|
|
|
|
return nodeElement;
|
|
|
|
|
};
|
|
|
|
|
const patchPageTreeExpansionDom = (nodeId) => {
|
|
|
|
|
if (mode !== "page") return false;
|
|
|
|
|
const normalizedNodeId = normalizeText(nodeId);
|
|
|
|
|
if (!normalizedNodeId) return false;
|
|
|
|
|
const item = itemById.get(normalizedNodeId);
|
|
|
|
|
if (!item) return false;
|
|
|
|
|
const nodeElement = appElement.querySelector(
|
|
|
|
|
\`.tree-node[data-node-id="\${CSS.escape(normalizedNodeId)}"]\`,
|
|
|
|
|
);
|
|
|
|
|
if (!(nodeElement instanceof HTMLElement)) return false;
|
|
|
|
|
const row = nodeElement.querySelector(
|
|
|
|
|
\`:scope > .tree-row[data-node-id="\${CSS.escape(normalizedNodeId)}"]\`,
|
|
|
|
|
);
|
|
|
|
|
const children = getSiblings(normalizedNodeId);
|
|
|
|
|
const hasChildren = item.childCount > 0 && children.length > 0;
|
|
|
|
|
const isExpanded = hasChildren && expanded.has(normalizedNodeId);
|
|
|
|
|
if (row instanceof HTMLElement) {
|
|
|
|
|
row.setAttribute("aria-expanded", hasChildren ? String(isExpanded) : "false");
|
|
|
|
|
const toggleButton = row.querySelector('[data-testid="tree-node-toggle"]');
|
|
|
|
|
if (toggleButton instanceof HTMLButtonElement) {
|
|
|
|
|
toggleButton.textContent = isExpanded ? "▾" : "▸";
|
|
|
|
|
toggleButton.setAttribute("aria-label", \`\${isExpanded ? "折叠" : "展开"} \${item.title}\`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!hasChildren) {
|
|
|
|
|
patchPageTreeActiveDom();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
let childrenList = Array.from(nodeElement.children).find(
|
|
|
|
|
(child) => child instanceof HTMLElement && child.classList.contains("tree-children"),
|
|
|
|
|
);
|
|
|
|
|
if (isExpanded) {
|
|
|
|
|
if (!(childrenList instanceof HTMLElement)) {
|
|
|
|
|
childrenList = document.createElement("ul");
|
|
|
|
|
childrenList.className = "tree-children";
|
|
|
|
|
children.forEach((child) => {
|
|
|
|
|
const childNode = renderHydratedPageNode(child);
|
|
|
|
|
if (childNode) childrenList.appendChild(childNode);
|
|
|
|
|
});
|
|
|
|
|
nodeElement.appendChild(childrenList);
|
|
|
|
|
}
|
|
|
|
|
childrenList.hidden = false;
|
|
|
|
|
childrenList.style.display = "";
|
|
|
|
|
} else if (childrenList instanceof HTMLElement) {
|
|
|
|
|
childrenList.hidden = true;
|
|
|
|
|
childrenList.style.display = "none";
|
|
|
|
|
}
|
|
|
|
|
patchPageTreeActiveDom();
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
const patchPageTreeActiveDom = () => {
|
|
|
|
|
if (mode !== "page") return;
|
|
|
|
|
appElement.querySelectorAll('[data-rust-rendered-row="page"]').forEach((row) => {
|
|
|
|
@@ -1744,7 +1970,14 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
linkButton.className = "tree-link";
|
|
|
|
|
linkButton.setAttribute("data-testid", mode === "picker" ? "tree-picker-row" : "tree-node-open");
|
|
|
|
|
linkButton.setAttribute("aria-label", \`打开 \${item.title}\`);
|
|
|
|
|
linkButton.addEventListener("click", () => handleNavigate(item.nodeId));
|
|
|
|
|
linkButton.addEventListener("click", () => {
|
|
|
|
|
if (mode === "picker") {
|
|
|
|
|
applyPickerFocusByItemKey(item.nodeId, { focusDom: true });
|
|
|
|
|
postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
handleNavigate(item.nodeId);
|
|
|
|
|
});
|
|
|
|
|
const titleElement = document.createElement("span");
|
|
|
|
|
titleElement.className = "tree-link-title";
|
|
|
|
|
titleElement.textContent = item.title;
|
|
|
|
@@ -1857,9 +2090,11 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
rootButton.className = "tree-row";
|
|
|
|
|
rootButton.setAttribute("data-testid", "tree-picker-root");
|
|
|
|
|
rootButton.dataset.focused = String(currentActivePickerItemKey === "__root__");
|
|
|
|
|
rootButton.tabIndex = currentActivePickerItemKey === "__root__" ? 0 : -1;
|
|
|
|
|
rootButton.textContent = "根目录";
|
|
|
|
|
rootButton.addEventListener("click", () => {
|
|
|
|
|
postToHost("tree.pick.root", { documentId: null, target: { documentId: null } });
|
|
|
|
|
applyPickerFocusByItemKey("__root__", { focusDom: true });
|
|
|
|
|
postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" }));
|
|
|
|
|
});
|
|
|
|
|
root.appendChild(rootButton);
|
|
|
|
|
}
|
|
|
|
@@ -1881,11 +2116,7 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
}
|
|
|
|
|
if (normalizedCommand === "pick") {
|
|
|
|
|
const result = applyPickerStateAction({ kind: "pick" });
|
|
|
|
|
if (result.pickedRoot) {
|
|
|
|
|
postToHost("tree.pick.root", { documentId: null });
|
|
|
|
|
} else if (result.pickedDocumentId) {
|
|
|
|
|
handleNavigate(result.pickedDocumentId);
|
|
|
|
|
}
|
|
|
|
|
postPickerPickResultToHost(result);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
applyPickerStateAction({ kind: normalizedCommand });
|
|
|
|
@@ -1916,7 +2147,6 @@ const LOCAL_TREE_SHELL_TEMPLATE = `<!doctype html>
|
|
|
|
|
}
|
|
|
|
|
if (mode === "picker" && usedRustInitialRenderer) {
|
|
|
|
|
patchPickerActiveDom();
|
|
|
|
|
focusPickerRowElement(currentActivePickerItemKey);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
renderTree();
|
|
|
|
@@ -2188,6 +2418,12 @@ function buildInlineRendererInput(input: {
|
|
|
|
|
channel: normalizeString(input.channel),
|
|
|
|
|
commandNames: [...TREE_SHELL_COMMAND_NAMES],
|
|
|
|
|
};
|
|
|
|
|
const runtimeArtifact = {
|
|
|
|
|
contractName: TREE_SHELL_RUNTIME_ARTIFACT.contractName,
|
|
|
|
|
inputFields: [...TREE_SHELL_RUNTIME_ARTIFACT.inputFields],
|
|
|
|
|
outputChannels: [...TREE_SHELL_RUNTIME_ARTIFACT.outputChannels],
|
|
|
|
|
eventKinds: [...TREE_SHELL_RUNTIME_ARTIFACT.eventKinds],
|
|
|
|
|
};
|
|
|
|
|
if (input.mode === "filetree") {
|
|
|
|
|
const filetreeSelection = buildInlineFiletreeSelection(input.activeDocumentId);
|
|
|
|
|
return {
|
|
|
|
@@ -2203,6 +2439,7 @@ function buildInlineRendererInput(input: {
|
|
|
|
|
activePickerItem: null,
|
|
|
|
|
excludedPickerIds: [],
|
|
|
|
|
commandDispatcher,
|
|
|
|
|
runtimeArtifact,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (input.mode === "picker") {
|
|
|
|
@@ -2220,6 +2457,7 @@ function buildInlineRendererInput(input: {
|
|
|
|
|
actions: [...PICKER_STATE_REDUCER_ACTIONS],
|
|
|
|
|
},
|
|
|
|
|
commandDispatcher,
|
|
|
|
|
runtimeArtifact,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
@@ -2235,6 +2473,7 @@ function buildInlineRendererInput(input: {
|
|
|
|
|
activePickerItem: null,
|
|
|
|
|
excludedPickerIds: [],
|
|
|
|
|
commandDispatcher,
|
|
|
|
|
runtimeArtifact,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2250,11 +2489,14 @@ function buildInlinePageTreeHtml(input: {
|
|
|
|
|
const children = childrenByParent.get(item.nodeId) ?? [];
|
|
|
|
|
const expandable = item.childCount > 0 && children.length > 0;
|
|
|
|
|
const expanded = expandable && item.expandedByDefault;
|
|
|
|
|
const toggleHtml = expandable
|
|
|
|
|
? `<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="${escapeInlineHtml(item.nodeId)}" aria-label="${expanded ? "折叠" : "展开"} ${escapeInlineHtml(item.title)}">${expanded ? "▾" : "▸"}</button>`
|
|
|
|
|
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
|
|
|
|
const childHtml =
|
|
|
|
|
expanded && children.length > 0
|
|
|
|
|
? `<ul class="tree-children">${children.map(renderRow).join("")}</ul>`
|
|
|
|
|
: "";
|
|
|
|
|
return `<li class="tree-node" data-node-id="${escapeInlineHtml(item.nodeId)}"><div class="tree-row" role="treeitem" aria-level="${item.depth + 1}" aria-expanded="${expanded}" data-rust-rendered-row="page" data-testid="tree-node-open" data-node-id="${escapeInlineHtml(item.nodeId)}" data-shell-mode="page" data-active="${item.nodeId === activeDocumentId}" data-focused="${item.nodeId === focusedDocumentId}" data-draggable="true" draggable="true" tabindex="${item.nodeId === focusedDocumentId ? "0" : "-1"}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="${escapeInlineHtml(item.nodeId)}"><span class="tree-link-title">${escapeInlineHtml(item.title)}</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="${escapeInlineHtml(item.nodeId)}" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="${escapeInlineHtml(item.nodeId)}" aria-label="重命名">✎</button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="${escapeInlineHtml(item.nodeId)}" aria-label="更多操作">…</button></div></div>${childHtml}</li>`;
|
|
|
|
|
return `<li class="tree-node" data-node-id="${escapeInlineHtml(item.nodeId)}"><div class="tree-row" role="treeitem" aria-level="${item.depth + 1}" aria-expanded="${expanded}" data-rust-rendered-row="page" data-testid="tree-node-open" data-node-id="${escapeInlineHtml(item.nodeId)}" data-shell-mode="page" data-active="${item.nodeId === activeDocumentId}" data-focused="${item.nodeId === focusedDocumentId}" data-draggable="true" draggable="true" tabindex="${item.nodeId === focusedDocumentId ? "0" : "-1"}">${toggleHtml}<span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="${escapeInlineHtml(item.nodeId)}"><span class="tree-link-title">${escapeInlineHtml(item.title)}</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="${escapeInlineHtml(item.nodeId)}" aria-label="新建子页面">+</button><button type="button" class="tree-action" data-testid="tree-action-rename" data-rust-action="rename" data-node-id="${escapeInlineHtml(item.nodeId)}" aria-label="重命名">✎</button><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="${escapeInlineHtml(item.nodeId)}" aria-label="更多操作">…</button></div></div>${childHtml}</li>`;
|
|
|
|
|
};
|
|
|
|
|
const roots = childrenByParent.get("") ?? [];
|
|
|
|
|
if (roots.length === 0) {
|
|
|
|
@@ -2304,8 +2546,9 @@ function buildInlinePickerHtml(input: {
|
|
|
|
|
const childrenByParent = buildInlineChildrenByParent(input.items);
|
|
|
|
|
const activeDocumentId = normalizeString(input.activeDocumentId);
|
|
|
|
|
const activePickerItemKey = normalizeString(input.activePickerItemKey);
|
|
|
|
|
const rootActive = activePickerItemKey === "__root__";
|
|
|
|
|
const rootHtml = input.allowRootPick
|
|
|
|
|
? `<li class="tree-node" data-node-id="__root__"><button type="button" class="tree-row" data-testid="tree-picker-root" data-rust-rendered-row="picker-root" data-rust-action="pick-root" data-focused="${activePickerItemKey === "__root__"}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">根目录</span></span></button></li>`
|
|
|
|
|
? `<li class="tree-node" data-node-id="__root__"><button type="button" class="tree-row" data-testid="tree-picker-root" data-rust-rendered-row="picker-root" data-rust-action="pick-root" data-focused="${rootActive}" tabindex="${rootActive ? "0" : "-1"}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">根目录</span></span></button></li>`
|
|
|
|
|
: "";
|
|
|
|
|
const renderRow = (item: TreeShellInlineProjectionItem): string => {
|
|
|
|
|
const children = childrenByParent.get(item.nodeId) ?? [];
|
|
|
|
@@ -2318,7 +2561,7 @@ function buildInlinePickerHtml(input: {
|
|
|
|
|
expanded && children.length > 0
|
|
|
|
|
? `<ul class="tree-children">${children.map(renderRow).join("")}</ul>`
|
|
|
|
|
: "";
|
|
|
|
|
return `<li class="tree-node" data-node-id="${escapeInlineHtml(item.nodeId)}"><button type="button" class="tree-row" role="treeitem" aria-level="${item.depth + 1}" aria-expanded="${expanded}" data-rust-rendered-row="picker" data-testid="tree-picker-row" data-node-id="${escapeInlineHtml(item.nodeId)}" data-shell-mode="picker" data-focused="${active}" data-rust-action="pick"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">${escapeInlineHtml(item.title)}</span></span></button>${childHtml}</li>`;
|
|
|
|
|
return `<li class="tree-node" data-node-id="${escapeInlineHtml(item.nodeId)}"><button type="button" class="tree-row" role="treeitem" aria-level="${item.depth + 1}" aria-expanded="${expanded}" data-rust-rendered-row="picker" data-testid="tree-picker-row" data-node-id="${escapeInlineHtml(item.nodeId)}" data-shell-mode="picker" data-focused="${active}" data-rust-action="pick" tabindex="${active ? "0" : "-1"}"><span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="page" aria-hidden="true"></span><span class="tree-link"><span class="tree-link-title">${escapeInlineHtml(item.title)}</span></span></button>${childHtml}</li>`;
|
|
|
|
|
};
|
|
|
|
|
const roots = childrenByParent.get("") ?? [];
|
|
|
|
|
return `<ul class="tree-root" role="tree" data-rust-picker-renderer="initial_v1">${rootHtml}${roots.map(renderRow).join("")}</ul>`;
|
|
|
|
|