diff --git a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md index a526ae12..87a0a021 100644 --- a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md +++ b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md @@ -250,13 +250,13 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tree -- -- - [x] E3. `runtime_bridge.rs`:迁出 `dispatch_runtime_event`、`dispatch_*_to_target`、host command listener install。 - [x] E4. `block_transform.rs`:迁出 `run_block_turn_into_action`、`top_level_block_matches_action`、page/block transform helpers。 - [x] E5. `slash_menu_view.rs`:迁出 slash menu Leptos view 和 click handling。 -- [ ] E6. `block_handle_menu_view.rs`:迁出 block handle menu 和 submenu view,作为最后一刀。 +- [x] E6. `block_handle_menu_view.rs`:迁出 block handle menu 和 submenu view,作为最后一刀。 验收标准: - [x] `lib.rs` 少于 5,000 行。 -- [ ] `block_handle_menu_view.rs` 可以超过 1,500 行,但必须有清晰入参 struct,避免二十多个 signal 直接散落。 -- [ ] 不改变 Tiptap command 行为,不新增编辑器功能。 +- [x] `block_handle_menu_view.rs` 可以超过 1,500 行,但必须有清晰入参 struct,避免二十多个 signal 直接散落。 +- [x] 不改变 Tiptap command 行为,不新增编辑器功能。 验收命令: diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_handle_menu_view.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_handle_menu_view.rs new file mode 100644 index 00000000..8276ffda --- /dev/null +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_handle_menu_view.rs @@ -0,0 +1,668 @@ +use crate::{ + current_page_anchor_url, editor_block_by_id, request_ai_edit_bridge, + write_mnote_text_to_clipboard, +}; +use crate::editor_runtime::{ + block_hover_state::{BlockMenuLayout, HoveredBlockState}, + block_transform::{ + apply_text_align, run_block_turn_into_action, run_block_turn_into_folded_heading_action, + run_block_turn_into_page_action, top_level_block_is_page, top_level_block_matches_action, + top_level_block_range, + }, + command_sync::{read_editor_snapshot, sync_persisted_editor_command}, + dom_events::trap_scroll_inside_menu, + dom_selection::runtime_block_id_from_index, + editor_focus::schedule_editor_focus, + persistence::{persist_document_state, persisted_document_identity, PersistedDocumentIdentity}, + slash_actions::{SlashActionKind, SLASH_ACTIONS}, +}; +use leptos::prelude::*; +use leptos_tiptap::{TiptapEditorHandle, TiptapSelectionState, TiptapTextAlign}; +use serde_json::Value; +use wasm_bindgen_futures::spawn_local; +use web_sys::{MouseEvent, WheelEvent}; + +#[derive(Clone, Copy)] +struct FoldedHeadingAction { + level: u8, + id: &'static str, + icon: &'static str, + label: &'static str, +} + +const FOLDED_HEADING_ACTIONS: [FoldedHeadingAction; 4] = [ + FoldedHeadingAction { + level: 1, + id: "1", + icon: "H1", + label: "折叠主标题", + }, + FoldedHeadingAction { + level: 2, + id: "2", + icon: "H2", + label: "折叠大标题", + }, + FoldedHeadingAction { + level: 3, + id: "3", + icon: "H3", + label: "折叠中标题", + }, + FoldedHeadingAction { + level: 4, + id: "4", + icon: "H4", + label: "折叠小标题", + }, +]; + +pub(crate) struct BlockHandleMenuViewProps { + pub(crate) editor: TiptapEditorHandle, + pub(crate) block: HoveredBlockState, + pub(crate) duplicate_block_label: String, + pub(crate) delete_block_label: String, + pub(crate) menu_layout: BlockMenuLayout, + pub(crate) signals: BlockHandleMenuSignals, +} + +#[derive(Clone, Copy)] +pub(crate) struct BlockHandleMenuSignals { + pub(crate) document_id: ReadSignal>, + pub(crate) workspace_id: ReadSignal>, + pub(crate) title: ReadSignal, + pub(crate) selection_state: ReadSignal, + pub(crate) block_turn_into_open: ReadSignal, + pub(crate) block_folded_title_open: ReadSignal, + pub(crate) set_ai_bridge_state: WriteSignal, + pub(crate) set_ai_bridge_message: WriteSignal, + pub(crate) set_dirty_count: WriteSignal, + pub(crate) set_html_output: WriteSignal, + pub(crate) set_document_json: WriteSignal, + pub(crate) set_json_output: WriteSignal, + pub(crate) set_command_feedback: WriteSignal, + pub(crate) set_block_menu_open: WriteSignal, + pub(crate) set_block_menu_anchor: WriteSignal>, + pub(crate) set_block_turn_into_open: WriteSignal, + pub(crate) set_block_folded_title_open: WriteSignal, + pub(crate) set_hovered_block: WriteSignal>, +} + +fn runtime_persisted_identity( + document_id: ReadSignal>, + workspace_id: ReadSignal>, +) -> PersistedDocumentIdentity { + persisted_document_identity(document_id.get_untracked(), workspace_id.get_untracked()) +} + +fn persist_block_transform_result( + editor: TiptapEditorHandle, + signals: BlockHandleMenuSignals, +) -> (String, Value, String) { + let (html, snapshot, json_text) = read_editor_snapshot(editor); + signals.set_dirty_count.update(|count| *count += 1); + signals.set_html_output.set(html.clone()); + signals.set_document_json.set(snapshot.clone()); + signals.set_json_output.set(json_text.clone()); + (html, snapshot, json_text) +} + +fn close_block_transform_menus(signals: BlockHandleMenuSignals) { + signals.set_block_menu_open.set(false); + signals.set_block_menu_anchor.set(None); + signals.set_block_turn_into_open.set(false); + signals.set_block_folded_title_open.set(false); +} + +fn handle_turn_into_action_click( + editor: TiptapEditorHandle, + block_index: usize, + kind: SlashActionKind, + label: &'static str, + signals: BlockHandleMenuSignals, +) { + match run_block_turn_into_action(editor, block_index, kind) { + Ok(message) => { + close_block_transform_menus(signals); + let (html, snapshot, _) = persist_block_transform_result(editor, signals); + match persist_document_state( + &runtime_persisted_identity(signals.document_id, signals.workspace_id), + &signals.title.get_untracked(), + &snapshot, + Some(html), + ) { + Ok(()) => signals.set_command_feedback.set(format!( + "{}:块 -> {},并已写入本地草稿", + message, label, + )), + Err(err) => signals.set_command_feedback.set(format!( + "{}:块 -> {},但本地保存失败:{}", + message, label, err, + )), + } + } + Err(err) => signals.set_command_feedback.set(err), + } +} + +fn handle_folded_heading_click( + editor: TiptapEditorHandle, + block_index: usize, + action: FoldedHeadingAction, + signals: BlockHandleMenuSignals, +) { + match run_block_turn_into_folded_heading_action(editor, block_index, action.level) { + Ok(message) => { + close_block_transform_menus(signals); + let (html, snapshot, _) = persist_block_transform_result(editor, signals); + match persist_document_state( + &runtime_persisted_identity(signals.document_id, signals.workspace_id), + &signals.title.get_untracked(), + &snapshot, + Some(html), + ) { + Ok(()) => signals + .set_command_feedback + .set(format!("{},并已写入本地草稿", message)), + Err(err) => signals + .set_command_feedback + .set(format!("{},但本地保存失败:{}", message, err)), + } + } + Err(err) => signals.set_command_feedback.set(err), + } +} + +fn block_transform_shortcut( + editor: TiptapEditorHandle, + block_index: usize, + kind: SlashActionKind, +) -> &'static str { + if top_level_block_matches_action(editor, block_index, kind) { + return "✓"; + } + + match kind { + SlashActionKind::Paragraph => "Ctrl+Alt+0", + SlashActionKind::Todo => "Ctrl+Shift+5", + SlashActionKind::Heading1 => "Ctrl+Shift+1", + SlashActionKind::Heading2 => "Ctrl+Shift+2", + SlashActionKind::Heading3 => "Ctrl+Shift+3", + SlashActionKind::BulletList => "Ctrl+Shift+6", + SlashActionKind::OrderedList => "Ctrl+Shift+7", + SlashActionKind::CodeBlock => "Ctrl+Alt+-", + SlashActionKind::Mindmap => "/dt", + _ => "", + } +} + +fn block_transform_submenu_view( + editor: TiptapEditorHandle, + block_index: usize, + signals: BlockHandleMenuSignals, +) -> impl IntoView { + let current_block_is_page = top_level_block_is_page(editor, block_index); + let page_current_marker = if current_block_is_page { + "✓" + } else { + "Ctrl+Shift+9" + }; + + view! { +
+ {SLASH_ACTIONS + .iter() + .filter(|action| { + !matches!( + action.kind, + SlashActionKind::Divider + | SlashActionKind::SimpleTable + | SlashActionKind::Image + | SlashActionKind::UploadAttachment + | SlashActionKind::Toc + | SlashActionKind::AiAssistant + | SlashActionKind::AiWrite + | SlashActionKind::ContinueWriting + | SlashActionKind::Summarize + | SlashActionKind::MoreAi + ) + }) + .map(|action| { + let kind = action.kind; + let label = action.label; + let icon = action.icon; + let is_current = top_level_block_matches_action(editor, block_index, kind); + let shortcut = block_transform_shortcut(editor, block_index, kind); + let testid = format!("block-transform-item-{}", action.id); + view! { + + } + }) + .collect_view()} + + + + + + + {move || { + if signals.block_folded_title_open.get() { + view! { +
+ {FOLDED_HEADING_ACTIONS + .iter() + .map(|folded| { + let action = *folded; + let testid = format!("block-transform-folded-heading-{}", action.id); + view! { + + } + }) + .collect_view()} +
+ }.into_any() + } else { + ().into_any() + } + }} +
+ } +} + +pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl IntoView { + let BlockHandleMenuViewProps { + editor, + block, + duplicate_block_label, + delete_block_label, + menu_layout, + signals, + } = props; + let block_index = block.index; + let menu_top = format!("{}px", menu_layout.top); + let menu_bottom = "auto"; + + view! { +
+ +
+
+ + +
+
+
+ + +
+
+
+ + + + + + + + +
+ + {move || { + if signals.block_turn_into_open.get() { + block_transform_submenu_view(editor, block_index, signals).into_any() + } else { + ().into_any() + } + }} +
+
+ } +} 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 daefff81..a3bfac38 100644 --- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod attachment_links; pub(crate) mod attachment_upload; pub(crate) mod block_dnd; pub(crate) mod block_hover_state; +pub(crate) mod block_handle_menu_view; pub(crate) mod block_menu_document; pub(crate) mod block_menu_overlay; pub(crate) mod block_transform; diff --git a/rust/spikes/leptos-tiptap-spike/src/lib.rs b/rust/spikes/leptos-tiptap-spike/src/lib.rs index 7dff2007..241742c9 100644 --- a/rust/spikes/leptos-tiptap-spike/src/lib.rs +++ b/rust/spikes/leptos-tiptap-spike/src/lib.rs @@ -11,7 +11,6 @@ use serde::Deserialize; use serde_json::{json, Value}; use std::fmt::Display; use wasm_bindgen::{closure::Closure, prelude::*, JsCast, JsValue}; -use wasm_bindgen_futures::spawn_local; use web_sys::{ window, CustomEvent, DragEvent, Element, Event, EventTarget, HtmlElement, MouseEvent, WheelEvent, @@ -26,13 +25,14 @@ use editor_runtime::block_dnd::{ use editor_runtime::block_hover_state::{ BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState, }; +use editor_runtime::block_handle_menu_view::{ + block_handle_menu_view, BlockHandleMenuSignals, BlockHandleMenuViewProps, +}; use editor_runtime::block_menu_overlay; use editor_runtime::block_transform::{ apply_block_delta_to_json, apply_highlight_color, apply_image_align, apply_text_align, apply_text_color, clear_text_color, insert_editor_paragraph_relative_to_block, - prosemirror_node_size, run_block_turn_into_action, run_block_turn_into_folded_heading_action, - run_block_turn_into_page_action, run_slash_action, top_level_block_boundary_position, - top_level_block_is_page, top_level_block_matches_action, top_level_block_range, + prosemirror_node_size, run_slash_action, top_level_block_boundary_position, }; use editor_runtime::bridge_dispatch::HostCommandKind; use editor_runtime::bridge_events::{ @@ -40,9 +40,7 @@ use editor_runtime::bridge_events::{ HostCommandPayload, HostStatusPayload, ReadyPayload, StatePayload, BLOCK_DELTA_EVENT, COMMAND_EVENT, HEIGHT_EVENT, PROTOCOL, RUNTIME_NAME, }; -use editor_runtime::command_sync::{ - read_editor_snapshot, sync_editor_outputs, sync_persisted_editor_command, -}; +use editor_runtime::command_sync::{sync_editor_outputs, sync_persisted_editor_command}; use editor_runtime::content_layout::{content_column_left, content_text_left}; use editor_runtime::dom_events::{ event_target_matches_selector, target_element, trap_scroll_inside_menu, @@ -339,41 +337,6 @@ const TOOLBAR_ALIGN_OPTIONS: [ToolbarAlignOption; 4] = [ }, ]; -#[derive(Clone, Copy)] -struct FoldedHeadingAction { - level: u8, - id: &'static str, - icon: &'static str, - label: &'static str, -} - -const FOLDED_HEADING_ACTIONS: [FoldedHeadingAction; 4] = [ - FoldedHeadingAction { - level: 1, - id: "1", - icon: "H1", - label: "折叠主标题", - }, - FoldedHeadingAction { - level: 2, - id: "2", - icon: "H2", - label: "折叠大标题", - }, - FoldedHeadingAction { - level: 3, - id: "3", - icon: "H3", - label: "折叠中标题", - }, - FoldedHeadingAction { - level: 4, - id: "4", - icon: "H4", - label: "折叠小标题", - }, -]; - #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] #[allow(dead_code)] @@ -3378,8 +3341,6 @@ fn App(mount_options: MountOptions) -> impl IntoView { .map(|current| current.index == block_index) .unwrap_or(false) { - let current_duplicate_label = duplicate_block_label.clone(); - let current_delete_label = delete_block_label.clone(); let menu_layout = editor_stage_element().and_then(|stage| { let rect = stage.get_bounding_client_rect(); block_menu_overlay::block_menu_layout(&block, rect.width(), rect.height(), rect.top(), rect.left()) @@ -3389,485 +3350,33 @@ fn App(mount_options: MountOptions) -> impl IntoView { top: 80.0, left: 80.0, }); - let menu_top = format!("{}px", menu_layout.top); - let menu_bottom = "auto"; - let current_block_is_page = top_level_block_is_page(editor, block_index); - let page_current_marker = if current_block_is_page { "✓" } else { "Ctrl+Shift+9" }; - view! { -
- -
- {move || { - let duplicate_feedback_label = current_duplicate_label.clone(); - let delete_feedback_label = current_delete_label.clone(); - view! { - <> -
- - -
-
-
- - -
-
-
- - - - - - - - -
- - {move || { - if block_turn_into_open.get() { - view! { -
- {SLASH_ACTIONS - .iter() - .filter(|action| !matches!(action.kind, SlashActionKind::Divider | SlashActionKind::SimpleTable | SlashActionKind::Image | SlashActionKind::UploadAttachment | SlashActionKind::Toc | SlashActionKind::AiAssistant | SlashActionKind::AiWrite | SlashActionKind::ContinueWriting | SlashActionKind::Summarize | SlashActionKind::MoreAi)) - .map(|action| { - let kind = action.kind; - let label = action.label; - let icon = action.icon; - let is_current = top_level_block_matches_action(editor, block_index, action.kind); - let shortcut = if is_current { - "✓" - } else { - match action.kind { - SlashActionKind::Paragraph => "Ctrl+Alt+0", - SlashActionKind::Todo => "Ctrl+Shift+5", - SlashActionKind::Heading1 => "Ctrl+Shift+1", - SlashActionKind::Heading2 => "Ctrl+Shift+2", - SlashActionKind::Heading3 => "Ctrl+Shift+3", - SlashActionKind::BulletList => "Ctrl+Shift+6", - SlashActionKind::OrderedList => "Ctrl+Shift+7", - SlashActionKind::CodeBlock => "Ctrl+Alt+-", - SlashActionKind::Mindmap => "/dt", - _ => "", - } - }; - let testid = format!("block-transform-item-{}", action.id); - let action_menu_label = "块".to_string(); - view! { - - } - }) - .collect_view()} - - - - - - - {move || { - if block_folded_title_open.get() { - view! { -
- {FOLDED_HEADING_ACTIONS - .iter() - .map(|folded| { - let action = *folded; - let testid = format!("block-transform-folded-heading-{}", action.id); - view! { - - } - }) - .collect_view()} -
- }.into_any() - } else { - ().into_any() - } - }} -
- }.into_any() - } else { - ().into_any() - } - }} - - } - }} -
-
- }.into_any() + block_handle_menu_view(BlockHandleMenuViewProps { + editor, + block: block.clone(), + duplicate_block_label: duplicate_block_label.clone(), + delete_block_label: delete_block_label.clone(), + menu_layout, + signals: BlockHandleMenuSignals { + document_id, + workspace_id, + title, + selection_state, + block_turn_into_open, + block_folded_title_open, + set_ai_bridge_state, + set_ai_bridge_message, + set_dirty_count, + set_html_output, + set_document_json, + set_json_output, + set_command_feedback, + set_block_menu_open, + set_block_menu_anchor, + set_block_turn_into_open, + set_block_folded_title_open, + set_hovered_block, + }, + }).into_any() } else { ().into_any() }