refactor: split tree shell picker runtime
This commit is contained in:
+1
-1
@@ -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] 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] 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。
|
- [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。
|
- [ ] D5. `tree-shell-dom-runtime.js`:DOM patch/render helpers。
|
||||||
- [x] D6. `tree-shell-icons-runtime.js`:icon templates 和 resource kind badge。
|
- [x] D6. `tree-shell-icons-runtime.js`:icon templates 和 resource kind badge。
|
||||||
- [ ] D7. entrypoint 少于 2,500 行。
|
- [ ] 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,
|
getFileTreeRowOwnerDocumentId,
|
||||||
normalizeTreeShellTreeItems,
|
normalizeTreeShellTreeItems,
|
||||||
} from "./tree-shell-filetree-runtime.js";
|
} from "./tree-shell-filetree-runtime.js";
|
||||||
|
import {
|
||||||
|
computeTreeShellPickerStateActionResult,
|
||||||
|
getTreeShellVisiblePickerEntries,
|
||||||
|
isTreeShellPickerEntryPickable,
|
||||||
|
normalizeTreeShellPickerItemKey,
|
||||||
|
resolveTreeShellCurrentPickerItemKey,
|
||||||
|
} from "./tree-shell-picker-runtime.js";
|
||||||
import {
|
import {
|
||||||
normalizeTreeShellNumber as normalizeNumber,
|
normalizeTreeShellNumber as normalizeNumber,
|
||||||
normalizeTreeShellStringArray as normalizeStringArray,
|
normalizeTreeShellStringArray as normalizeStringArray,
|
||||||
@@ -1767,148 +1774,48 @@ function startTreeShellRuntime() {
|
|||||||
return visible;
|
return visible;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVisiblePickerEntries = () => {
|
const getVisiblePickerEntries = () =>
|
||||||
if (mode !== "picker") {
|
getTreeShellVisiblePickerEntries({
|
||||||
return [];
|
mode,
|
||||||
}
|
allowRootPick,
|
||||||
|
roots,
|
||||||
|
expanded,
|
||||||
|
getSiblings,
|
||||||
|
});
|
||||||
|
|
||||||
const visible = [];
|
const isPickerEntryPickable = (entry) =>
|
||||||
if (allowRootPick) {
|
isTreeShellPickerEntryPickable(entry, {
|
||||||
visible.push({
|
allowRootPick,
|
||||||
pickerItemKey: "__root__",
|
excludedIds,
|
||||||
item: null,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const walk = (entries) => {
|
|
||||||
entries.forEach((item) => {
|
|
||||||
visible.push({
|
|
||||||
pickerItemKey: item.nodeId,
|
|
||||||
item,
|
|
||||||
});
|
|
||||||
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 = () =>
|
const getPickablePickerEntries = () =>
|
||||||
getVisiblePickerEntries().filter((entry) => isPickerEntryPickable(entry));
|
getVisiblePickerEntries().filter((entry) => isPickerEntryPickable(entry));
|
||||||
|
|
||||||
const normalizePickerItemKey = (pickerItemKey) => {
|
const normalizePickerItemKey = (pickerItemKey) =>
|
||||||
const normalizedItemKey = normalizeText(pickerItemKey);
|
normalizeTreeShellPickerItemKey(pickerItemKey, {
|
||||||
if (normalizedItemKey === "__root__" && allowRootPick) {
|
allowRootPick,
|
||||||
return "__root__";
|
itemById,
|
||||||
}
|
excludedIds,
|
||||||
if (normalizedItemKey && itemById.has(normalizedItemKey) && !excludedIds.has(normalizedItemKey)) {
|
});
|
||||||
return normalizedItemKey;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveCurrentPickerItemKey = () => {
|
const resolveCurrentPickerItemKey = () =>
|
||||||
const fromActive = normalizePickerItemKey(currentActivePickerItemKey);
|
resolveTreeShellCurrentPickerItemKey({
|
||||||
if (fromActive) return fromActive;
|
currentActivePickerItemKey,
|
||||||
const fromDocument = normalizePickerItemKey(currentActiveDocumentId);
|
currentActiveDocumentId,
|
||||||
if (fromDocument) return fromDocument;
|
normalizePickerItemKey,
|
||||||
return getPickablePickerEntries()[0]?.pickerItemKey || "";
|
getPickablePickerEntries,
|
||||||
};
|
});
|
||||||
|
|
||||||
const computePickerStateActionResult = (action) => {
|
const computePickerStateActionResult = (action) =>
|
||||||
const currentPickerItemKey = resolveCurrentPickerItemKey();
|
computeTreeShellPickerStateActionResult({
|
||||||
if (
|
mode,
|
||||||
mode !== "picker" ||
|
pickerStateReducerContractName,
|
||||||
pickerStateReducerContractName !== "rust_picker_state_reducer_v1" ||
|
pickerStateReducerActions,
|
||||||
!pickerStateReducerActions.has(action?.kind || "")
|
resolveCurrentPickerItemKey,
|
||||||
) {
|
getPickablePickerEntries,
|
||||||
return {
|
normalizePickerItemKey,
|
||||||
nextItemKey: currentPickerItemKey,
|
}, action);
|
||||||
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 focusNode = (nodeId) => {
|
const focusNode = (nodeId) => {
|
||||||
if (!nodeId || !itemById.has(nodeId)) return;
|
if (!nodeId || !itemById.has(nodeId)) return;
|
||||||
|
|||||||
@@ -182,6 +182,10 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
"/api/mnote-browser-runtime/tree-shell-filetree-runtime.js",
|
"/api/mnote-browser-runtime/tree-shell-filetree-runtime.js",
|
||||||
get(web_shell::tree_shell_filetree_runtime_asset),
|
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(
|
.route(
|
||||||
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
|
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
|
||||||
get(web_shell::document_conflict_panel_runtime_asset),
|
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-state-runtime.js",
|
||||||
"/api/mnote-browser-runtime/tree-shell-icons-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-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-conflict-panel-runtime.js",
|
||||||
"/api/mnote-browser-runtime/document-pane-runtime.js",
|
"/api/mnote-browser-runtime/document-pane-runtime.js",
|
||||||
"/api/mnote-browser-runtime/document-mindmap-host-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()))
|
.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 {
|
pub async fn document_conflict_panel_runtime_asset() -> Response {
|
||||||
// include_str! 路径相对于当前源文件 (src/routes/web_shell.rs -> ../../browser/)
|
// include_str! 路径相对于当前源文件 (src/routes/web_shell.rs -> ../../browser/)
|
||||||
const JS: &str = include_str!("../../browser/document-conflict-panel-runtime.js");
|
const JS: &str = include_str!("../../browser/document-conflict-panel-runtime.js");
|
||||||
|
|||||||
Reference in New Issue
Block a user