refactor: split tree shell picker runtime

This commit is contained in:
lix-2026
2026-05-26 02:56:16 +08:00
parent 19642cdd7b
commit 40c51c5cc6
5 changed files with 209 additions and 136 deletions
@@ -207,7 +207,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib ssr::pages::layout
- [x] D1. `tree-shell-state-runtime.js`state hydration、serialization、patch dispatch。
- [x] D2. `tree-shell-page-runtime.js`page tree keyboard、expand、focus、drag/drop intent。
- [x] D3. `tree-shell-filetree-runtime.js`filetree row normalization、resource meta、open target。
- [ ] D4. `tree-shell-picker-runtime.js`picker search/focus/pick root。
- [x] D4. `tree-shell-picker-runtime.js`picker search/focus/pick root。
- [ ] D5. `tree-shell-dom-runtime.js`DOM patch/render helpers。
- [x] D6. `tree-shell-icons-runtime.js`icon templates 和 resource kind badge。
- [ ] D7. entrypoint 少于 2,500 行。
@@ -0,0 +1,147 @@
// MNote debug /tree shell picker focus and selection reducers.
import { normalizeTreeShellText } from "./tree-shell-state-runtime.js";
export function getTreeShellVisiblePickerEntries(context) {
if (context.mode !== "picker") {
return [];
}
const visible = [];
if (context.allowRootPick) {
visible.push({
pickerItemKey: "__root__",
item: null,
});
}
const walk = (entries) => {
entries.forEach((item) => {
visible.push({
pickerItemKey: item.nodeId,
item,
});
if (item.childCount > 0 && context.expanded.has(item.nodeId)) {
walk(context.getSiblings(item.nodeId));
}
});
};
walk(context.roots);
return visible;
}
export function isTreeShellPickerEntryPickable(entry, context) {
if (!entry) return false;
if (entry.pickerItemKey === "__root__") {
return context.allowRootPick;
}
const documentId = normalizeTreeShellText(entry.item?.nodeId || entry.pickerItemKey);
return Boolean(documentId && !context.excludedIds.has(documentId));
}
export function normalizeTreeShellPickerItemKey(pickerItemKey, context) {
const normalizedItemKey = normalizeTreeShellText(pickerItemKey);
if (normalizedItemKey === "__root__" && context.allowRootPick) {
return "__root__";
}
if (
normalizedItemKey &&
context.itemById.has(normalizedItemKey) &&
!context.excludedIds.has(normalizedItemKey)
) {
return normalizedItemKey;
}
return "";
}
export function resolveTreeShellCurrentPickerItemKey(context) {
const fromActive = context.normalizePickerItemKey(context.currentActivePickerItemKey);
if (fromActive) return fromActive;
const fromDocument = context.normalizePickerItemKey(context.currentActiveDocumentId);
if (fromDocument) return fromDocument;
return context.getPickablePickerEntries()[0]?.pickerItemKey || "";
}
export function computeTreeShellPickerStateActionResult(context, action) {
const currentPickerItemKey = context.resolveCurrentPickerItemKey();
if (
context.mode !== "picker" ||
context.pickerStateReducerContractName !== "rust_picker_state_reducer_v1" ||
!context.pickerStateReducerActions.has(action?.kind || "")
) {
return {
nextItemKey: currentPickerItemKey,
pickedDocumentId:
action?.kind === "pick" && currentPickerItemKey !== "__root__"
? currentPickerItemKey || null
: null,
pickedRoot: action?.kind === "pick" && currentPickerItemKey === "__root__",
};
}
const pickable = context.getPickablePickerEntries();
if (pickable.length === 0) {
return {
nextItemKey: "",
pickedDocumentId: null,
pickedRoot: false,
};
}
const currentIndex = pickable.findIndex(
(entry) => entry.pickerItemKey === currentPickerItemKey,
);
const resolvedIndex = currentIndex >= 0 ? currentIndex : 0;
const actionKind = action.kind;
if (actionKind === "normalize") {
return {
nextItemKey: pickable[resolvedIndex]?.pickerItemKey || "",
pickedDocumentId: null,
pickedRoot: false,
};
}
if (actionKind === "focus") {
const nextItemKey = context.normalizePickerItemKey(action.itemKey);
return {
nextItemKey: nextItemKey || pickable[0]?.pickerItemKey || "",
pickedDocumentId: null,
pickedRoot: false,
};
}
if (actionKind === "pick") {
const target = pickable[resolvedIndex];
const targetKey = target?.pickerItemKey || "";
return {
nextItemKey: targetKey,
pickedDocumentId:
targetKey && targetKey !== "__root__" ? targetKey : null,
pickedRoot: targetKey === "__root__",
};
}
let nextIndex = resolvedIndex;
if (actionKind === "next") {
nextIndex = Math.min(pickable.length - 1, resolvedIndex + 1);
} else if (actionKind === "previous") {
nextIndex = Math.max(0, resolvedIndex - 1);
} else if (actionKind === "home") {
nextIndex = 0;
} else if (actionKind === "end") {
nextIndex = pickable.length - 1;
} else {
return {
nextItemKey: currentPickerItemKey,
pickedDocumentId: null,
pickedRoot: false,
};
}
return {
nextItemKey: pickable[nextIndex]?.pickerItemKey || "",
pickedDocumentId: null,
pickedRoot: false,
};
}
@@ -20,6 +20,13 @@ import {
getFileTreeRowOwnerDocumentId,
normalizeTreeShellTreeItems,
} from "./tree-shell-filetree-runtime.js";
import {
computeTreeShellPickerStateActionResult,
getTreeShellVisiblePickerEntries,
isTreeShellPickerEntryPickable,
normalizeTreeShellPickerItemKey,
resolveTreeShellCurrentPickerItemKey,
} from "./tree-shell-picker-runtime.js";
import {
normalizeTreeShellNumber as normalizeNumber,
normalizeTreeShellStringArray as normalizeStringArray,
@@ -1767,148 +1774,48 @@ function startTreeShellRuntime() {
return visible;
};
const getVisiblePickerEntries = () => {
if (mode !== "picker") {
return [];
}
const visible = [];
if (allowRootPick) {
visible.push({
pickerItemKey: "__root__",
item: null,
const getVisiblePickerEntries = () =>
getTreeShellVisiblePickerEntries({
mode,
allowRootPick,
roots,
expanded,
getSiblings,
});
}
const walk = (entries) => {
entries.forEach((item) => {
visible.push({
pickerItemKey: item.nodeId,
item,
const isPickerEntryPickable = (entry) =>
isTreeShellPickerEntryPickable(entry, {
allowRootPick,
excludedIds,
});
if (item.childCount > 0 && expanded.has(item.nodeId)) {
walk(getSiblings(item.nodeId));
}
});
};
walk(roots);
return visible;
};
const isPickerEntryPickable = (entry) => {
if (!entry) return false;
if (entry.pickerItemKey === "__root__") {
return allowRootPick;
}
const documentId = normalizeText(entry.item?.nodeId || entry.pickerItemKey);
return Boolean(documentId && !excludedIds.has(documentId));
};
const getPickablePickerEntries = () =>
getVisiblePickerEntries().filter((entry) => isPickerEntryPickable(entry));
const normalizePickerItemKey = (pickerItemKey) => {
const normalizedItemKey = normalizeText(pickerItemKey);
if (normalizedItemKey === "__root__" && allowRootPick) {
return "__root__";
}
if (normalizedItemKey && itemById.has(normalizedItemKey) && !excludedIds.has(normalizedItemKey)) {
return normalizedItemKey;
}
return "";
};
const normalizePickerItemKey = (pickerItemKey) =>
normalizeTreeShellPickerItemKey(pickerItemKey, {
allowRootPick,
itemById,
excludedIds,
});
const resolveCurrentPickerItemKey = () => {
const fromActive = normalizePickerItemKey(currentActivePickerItemKey);
if (fromActive) return fromActive;
const fromDocument = normalizePickerItemKey(currentActiveDocumentId);
if (fromDocument) return fromDocument;
return getPickablePickerEntries()[0]?.pickerItemKey || "";
};
const resolveCurrentPickerItemKey = () =>
resolveTreeShellCurrentPickerItemKey({
currentActivePickerItemKey,
currentActiveDocumentId,
normalizePickerItemKey,
getPickablePickerEntries,
});
const computePickerStateActionResult = (action) => {
const currentPickerItemKey = resolveCurrentPickerItemKey();
if (
mode !== "picker" ||
pickerStateReducerContractName !== "rust_picker_state_reducer_v1" ||
!pickerStateReducerActions.has(action?.kind || "")
) {
return {
nextItemKey: currentPickerItemKey,
pickedDocumentId:
action?.kind === "pick" && currentPickerItemKey !== "__root__"
? currentPickerItemKey || null
: null,
pickedRoot: action?.kind === "pick" && currentPickerItemKey === "__root__",
};
}
const pickable = getPickablePickerEntries();
if (pickable.length === 0) {
return {
nextItemKey: "",
pickedDocumentId: null,
pickedRoot: false,
};
}
const currentIndex = pickable.findIndex(
(entry) => entry.pickerItemKey === currentPickerItemKey,
);
const resolvedIndex = currentIndex >= 0 ? currentIndex : 0;
const actionKind = action.kind;
if (actionKind === "normalize") {
return {
nextItemKey: pickable[resolvedIndex]?.pickerItemKey || "",
pickedDocumentId: null,
pickedRoot: false,
};
}
if (actionKind === "focus") {
const nextItemKey = normalizePickerItemKey(action.itemKey);
return {
nextItemKey: nextItemKey || pickable[0]?.pickerItemKey || "",
pickedDocumentId: null,
pickedRoot: false,
};
}
if (actionKind === "pick") {
const target = pickable[resolvedIndex];
const targetKey = target?.pickerItemKey || "";
return {
nextItemKey: targetKey,
pickedDocumentId:
targetKey && targetKey !== "__root__" ? targetKey : null,
pickedRoot: targetKey === "__root__",
};
}
let nextIndex = resolvedIndex;
if (actionKind === "next") {
nextIndex = Math.min(pickable.length - 1, resolvedIndex + 1);
} else if (actionKind === "previous") {
nextIndex = Math.max(0, resolvedIndex - 1);
} else if (actionKind === "home") {
nextIndex = 0;
} else if (actionKind === "end") {
nextIndex = pickable.length - 1;
} else {
return {
nextItemKey: currentPickerItemKey,
pickedDocumentId: null,
pickedRoot: false,
};
}
return {
nextItemKey: pickable[nextIndex]?.pickerItemKey || "",
pickedDocumentId: null,
pickedRoot: false,
};
};
const computePickerStateActionResult = (action) =>
computeTreeShellPickerStateActionResult({
mode,
pickerStateReducerContractName,
pickerStateReducerActions,
resolveCurrentPickerItemKey,
getPickablePickerEntries,
normalizePickerItemKey,
}, action);
const focusNode = (nodeId) => {
if (!nodeId || !itemById.has(nodeId)) return;
+5
View File
@@ -182,6 +182,10 @@ pub fn build_router(state: AppState) -> Router {
"/api/mnote-browser-runtime/tree-shell-filetree-runtime.js",
get(web_shell::tree_shell_filetree_runtime_asset),
)
.route(
"/api/mnote-browser-runtime/tree-shell-picker-runtime.js",
get(web_shell::tree_shell_picker_runtime_asset),
)
.route(
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
get(web_shell::document_conflict_panel_runtime_asset),
@@ -637,6 +641,7 @@ mod tests {
"/api/mnote-browser-runtime/tree-shell-state-runtime.js",
"/api/mnote-browser-runtime/tree-shell-icons-runtime.js",
"/api/mnote-browser-runtime/tree-shell-filetree-runtime.js",
"/api/mnote-browser-runtime/tree-shell-picker-runtime.js",
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
"/api/mnote-browser-runtime/document-pane-runtime.js",
"/api/mnote-browser-runtime/document-mindmap-host-runtime.js",
@@ -994,6 +994,20 @@ pub async fn tree_shell_filetree_runtime_asset() -> Response {
.unwrap_or_else(|_| Response::new(Body::empty()))
}
pub async fn tree_shell_picker_runtime_asset() -> Response {
const JS: &str = include_str!("../../browser/tree-shell-picker-runtime.js");
Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
"application/javascript; charset=utf-8",
)
.header(header::CACHE_CONTROL, "no-store")
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
.body(Body::from(JS))
.unwrap_or_else(|_| Response::new(Body::empty()))
}
pub async fn document_conflict_panel_runtime_asset() -> Response {
// include_str! 路径相对于当前源文件 (src/routes/web_shell.rs -> ../../browser/)
const JS: &str = include_str!("../../browser/document-conflict-panel-runtime.js");