diff --git a/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md b/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md index 1e67497a..e42b5550 100644 --- a/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md +++ b/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md @@ -98,3 +98,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - Codex 已读取 Hindsight recall、`process-handoff.md/json`、`result.json`,并核对 worktree `git status --short` / `git diff --stat` 为空;runner 已产出 completed handoff 但进程未自行退出,主控已终止残留 runner。 - 审计结论:当前不宜直接把 `sync_editor_overlay_state` / `try_sync_editor_overlay_state` 迁入 `overlays.rs`,因为它们仍强依赖 `HoveredBlockState`、slash/block menu signals、`active_editor_stage` 和 8/18 overlay signal 的局部关闭策略;`on_selection_change` 仍有一套内联 signal 同步逻辑,直接迁出会造成两套 overlay reconciler。 - 下一刀建议:先做类型/边界准备或更窄 helper,而不是迁整个 sync 函数;候选是抽出 block/overlay shared types(例如 `HoveredBlockState` 及相关 anchor 类型)或把 `active_editor_stage` 的 DOM 判断收口到 runtime helper,之后再考虑 `close_editor_floating_overlays` 整簇迁移。 +- 2026-05-25:Codex 主控完成 editor focus helper 第一刀:新增 `editor_runtime/editor_focus.rs`,迁出 `schedule_editor_focus` 与 `active_editor_stage`;`lib.rs` 仍保留 overlay / block menu signal 编排和调用点。 + - 修改:`rust/spikes/leptos-tiptap-spike/src/editor_runtime/editor_focus.rs`、`rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs`、`rust/spikes/leptos-tiptap-spike/src/lib.rs`、generated island wasm/js。 + - 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、`node scripts/task489-block-menu-delete-undo-smoke.js`、`node scripts/task488-local-attachment-link-delete-undo-smoke.js`。 + - 未完成:`sync_editor_overlay_state` / `try_sync_editor_overlay_state`、`HoveredBlockState` shared type、block menu signal 仍未拆出。 diff --git a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js index aaab68e9..b172c264 100644 --- a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js +++ b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island.js @@ -1284,7 +1284,7 @@ function __wbg_get_imports() { return ret; }, __wbindgen_cast_0000000000000006: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 882, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 724, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf); return ret; }, diff --git a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm index 37b83f47..c75734e2 100644 Binary files a/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm and b/rust/spikes/leptos-tiptap-spike/generated/island/mnote-leptos-tiptap-spike-island_bg.wasm differ diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/editor_focus.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/editor_focus.rs new file mode 100644 index 00000000..499844c7 --- /dev/null +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/editor_focus.rs @@ -0,0 +1,51 @@ +use leptos_tiptap::TiptapEditorHandle; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::window; + +use crate::{ + editor_root_element, editor_runtime::dom_selection::node_is_within_root, EDITOR_STAGE_SELECTOR, +}; + +pub(crate) fn schedule_editor_focus(editor: TiptapEditorHandle) { + let Some(win) = window() else { + return; + }; + let callback = Closure::::new(move || { + let _ = editor.focus(); + }); + let _ = win.set_timeout_with_callback_and_timeout_and_arguments_0( + callback.as_ref().unchecked_ref(), + 0, + ); + callback.forget(); +} + +pub(crate) fn active_editor_stage() -> bool { + let active_element_inside_stage = window() + .and_then(|win| win.document()) + .and_then(|document| document.active_element()) + .and_then(|element| element.closest(EDITOR_STAGE_SELECTOR).ok().flatten()) + .is_some(); + + if active_element_inside_stage { + return true; + } + + let Some(root) = editor_root_element() else { + return false; + }; + let Some(selection) = window().and_then(|win| win.get_selection().ok().flatten()) else { + return false; + }; + + let anchor_inside = selection + .anchor_node() + .map(|node| node_is_within_root(node, &root)) + .unwrap_or(false); + let focus_inside = selection + .focus_node() + .map(|node| node_is_within_root(node, &root)) + .unwrap_or(false); + + anchor_inside || focus_inside +} diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs index caffebd5..61a1c9f7 100644 --- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod block_menu_document; pub(crate) mod block_menu_legacy_html; pub(crate) mod command_sync; pub(crate) mod dom_selection; +pub(crate) mod editor_focus; pub(crate) mod history_safe_commands; pub(crate) mod overlays; pub(crate) mod persistence; diff --git a/rust/spikes/leptos-tiptap-spike/src/lib.rs b/rust/spikes/leptos-tiptap-spike/src/lib.rs index 3aa1f747..ac38bf5f 100644 --- a/rust/spikes/leptos-tiptap-spike/src/lib.rs +++ b/rust/spikes/leptos-tiptap-spike/src/lib.rs @@ -28,8 +28,9 @@ use editor_runtime::command_sync::{ read_editor_snapshot, sync_editor_outputs, sync_persisted_editor_command, }; use editor_runtime::dom_selection::{ - has_active_text_selection, node_is_within_root, selection_summary, + has_active_text_selection, selection_summary, }; +use editor_runtime::editor_focus::{active_editor_stage, schedule_editor_focus}; use editor_runtime::overlays::{ floating_toolbar_anchor_from_selection, sync_text_selection_overlay, FloatingToolbarAnchor, }; @@ -3655,20 +3656,6 @@ fn schedule_scroll_mnote_block_anchor_from_hash_retry(remaining: u8) { callback.forget(); } -fn schedule_editor_focus(editor: TiptapEditorHandle) { - let Some(win) = window() else { - return; - }; - let callback = Closure::::new(move || { - let _ = editor.focus(); - }); - let _ = win.set_timeout_with_callback_and_timeout_and_arguments_0( - callback.as_ref().unchecked_ref(), - 0, - ); - callback.forget(); -} - fn current_viewport_scroll() -> Option<(f64, f64)> { let win = window()?; let x = win.scroll_x().ok()?; @@ -6310,36 +6297,6 @@ fn p0_extensions() -> Vec { ] } -fn active_editor_stage() -> bool { - let active_element_inside_stage = window() - .and_then(|win| win.document()) - .and_then(|document| document.active_element()) - .and_then(|element| element.closest(EDITOR_STAGE_SELECTOR).ok().flatten()) - .is_some(); - - if active_element_inside_stage { - return true; - } - - let Some(root) = editor_root_element() else { - return false; - }; - let Some(selection) = window().and_then(|win| win.get_selection().ok().flatten()) else { - return false; - }; - - let anchor_inside = selection - .anchor_node() - .map(|node| node_is_within_root(node, &root)) - .unwrap_or(false); - let focus_inside = selection - .focus_node() - .map(|node| node_is_within_root(node, &root)) - .unwrap_or(false); - - anchor_inside || focus_inside -} - fn current_block_info_from_index(index: Option) -> CurrentBlockInfo { index .map(|value| CurrentBlockInfo {