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 2ea52871..fb0616b2 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 @@ -314,10 +314,6 @@ function __wbg_get_imports() { const ret = arg0.anchorNode; return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); }, - __wbg_appendChild_e95c8b3b936d250a: function() { return handleError(function (arg0, arg1) { - const ret = arg0.appendChild(arg1); - return ret; - }, arguments); }, __wbg_assign_d4fed0f8abb71719: function() { return handleError(function (arg0, arg1, arg2) { arg0.assign(getStringFromWasm0(arg1, arg2)); }, arguments); }, @@ -608,13 +604,6 @@ function __wbg_get_imports() { __wbg_init_bridge_runtime_777169bfaa015be2: function() { return handleError(function () { init_bridge_runtime(); }, arguments); }, - __wbg_innerHTML_df4703c8599a85c4: function(arg0, arg1) { - const ret = arg1.innerHTML; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, __wbg_innerHeight_72e7bb88c4b9ede8: function() { return handleError(function (arg0) { const ret = arg0.innerHeight; return ret; @@ -1029,10 +1018,6 @@ function __wbg_get_imports() { __wbg_removeAttribute_c75ac657c944b3f1: function() { return handleError(function (arg0, arg1, arg2) { arg0.removeAttribute(getStringFromWasm0(arg1, arg2)); }, arguments); }, - __wbg_removeChild_172df530ec85cc8a: function() { return handleError(function (arg0, arg1) { - const ret = arg0.removeChild(arg1); - return ret; - }, arguments); }, __wbg_removeEventListener_0069d57d090a1674: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { arg0.removeEventListener(getStringFromWasm0(arg1, arg2), arg3, arg4 !== 0); }, arguments); }, @@ -1259,47 +1244,47 @@ function __wbg_get_imports() { } }, arguments); }, __wbindgen_cast_0000000000000001: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1145, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1144, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5); return ret; }, __wbindgen_cast_0000000000000002: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1197, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1196, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12); return ret; }, __wbindgen_cast_0000000000000003: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 973, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 972, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__had771ddc65647798); return ret; }, __wbindgen_cast_0000000000000004: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1095, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1094, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a); return ret; }, __wbindgen_cast_0000000000000005: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1145, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1144, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4); return ret; }, __wbindgen_cast_0000000000000006: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 294, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 598, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf); return ret; }, __wbindgen_cast_0000000000000007: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1097, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1096, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f); return ret; }, __wbindgen_cast_0000000000000008: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1112, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1111, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`. const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5); return ret; }, __wbindgen_cast_0000000000000009: function(arg0, arg1) { - // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1148, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1147, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c); 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 fc7075fd..572ae85d 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/block_menu_legacy_html.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_legacy_html.rs deleted file mode 100644 index a8a71706..00000000 --- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/block_menu_legacy_html.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! 基于 HTML 字符串的 block menu legacy helper。 -//! -//! 这些函数通过浏览器 DOM API 操作编辑器完整 HTML 字符串: -//! `window.document.createElement("div") + innerHTML 解析 -> DOM 修改 -> -//! innerHTML 回读`。这是 history-safe Tiptap command 之前的过渡路径, -//! 新代码应优先使用 command-based mutation,不要继续扩展整文 HTML 替换。 -//! -//! # History 风险 -//! -//! HTML 字符串往返会丢失 ProseMirror 内部状态,包括 marks、decorations、 -//! selection 和 undo history。调用方随后必须通过 `apply_html_update` 用原始 -//! HTML 重置编辑器,因此也会破坏撤销历史。本模块只用于隔离待替换的 legacy -//! helper。 - -use wasm_bindgen::JsCast; -use web_sys::{window, Element}; - -use crate::editor_runtime::block_hover_state::DropPlacement; - -/// 复制第 `index` 个顶层 HTML 块并插入其后。 -/// -/// 纯 DOM 操作:解析 `current_html` 为 `
` 子元素,克隆目标块, -/// 插入到下一个同级节点前(或末尾),返回序列化 HTML。 -pub(crate) fn duplicate_top_level_block_html( - current_html: &str, - index: usize, -) -> Result { - let document = window() - .and_then(|win| win.document()) - .ok_or_else(|| "浏览器 document 不可用".to_string())?; - let container = document - .create_element("div") - .map_err(|err| format!("创建 HTML 容器失败:{err:?}"))?; - container.set_inner_html(current_html); - - let children = container.children(); - let current = children - .item(index as u32) - .ok_or_else(|| format!("找不到第 {index} 个 HTML 顶层块"))?; - let cloned = current - .clone_node_with_deep(true) - .map_err(|err| format!("复制 HTML 块失败:{err:?}"))? - .dyn_into::() - .map_err(|_| "复制出的节点不是 Element".to_string())?; - - if let Some(next_sibling) = children.item(index as u32 + 1) { - container - .insert_before(&cloned, Some(&next_sibling)) - .map_err(|err| format!("插入复制块失败:{err:?}"))?; - } else { - container - .append_child(&cloned) - .map_err(|err| format!("追加复制块失败:{err:?}"))?; - } - - Ok(container.inner_html()) -} - -/// 在 HTML 层将第 `source` 个顶层块移动到第 `target` 个块的前/后。 -/// -/// 纯 DOM 操作:解析 `current_html`,克隆拖拽块,移除原始块, -/// 在调整后的位置插入克隆,返回序列化 HTML。 -pub(crate) fn reorder_top_level_block_html( - current_html: &str, - source: usize, - target: usize, - placement: DropPlacement, -) -> Result { - let document = window() - .and_then(|win| win.document()) - .ok_or_else(|| "浏览器 document 不可用".to_string())?; - let container = document - .create_element("div") - .map_err(|err| format!("创建 HTML 容器失败:{err:?}"))?; - container.set_inner_html(current_html); - - let initial_children = container.children(); - let block_count = initial_children.length() as usize; - if source >= block_count || target >= block_count { - return Err("拖拽目标超出当前顶层块范围".to_string()); - } - - let current = initial_children - .item(source as u32) - .ok_or_else(|| format!("找不到第 {source} 个 HTML 顶层块"))?; - let moving = current - .clone_node_with_deep(true) - .map_err(|err| format!("复制拖拽块失败:{err:?}"))? - .dyn_into::() - .map_err(|_| "复制出的拖拽块不是 Element".to_string())?; - - container - .remove_child(¤t) - .map_err(|err| format!("移除原始拖拽块失败:{err:?}"))?; - - let mut insert_at = match placement { - DropPlacement::Before => target, - DropPlacement::After => target + 1, - }; - if source < insert_at { - insert_at = insert_at.saturating_sub(1); - } - - let current_children = container.children(); - let current_len = current_children.length() as usize; - if insert_at >= current_len { - container - .append_child(&moving) - .map_err(|err| format!("追加拖拽块失败:{err:?}"))?; - } else { - let next_sibling = current_children - .item(insert_at as u32) - .ok_or_else(|| format!("找不到第 {insert_at} 个 HTML 插入位置"))?; - container - .insert_before(&moving, Some(&next_sibling)) - .map_err(|err| format!("插入拖拽块失败:{err:?}"))?; - } - - Ok(container.inner_html()) -} diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/history_safe_commands.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/history_safe_commands.rs index 324f0e19..8a8f76b7 100644 --- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/history_safe_commands.rs +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/history_safe_commands.rs @@ -122,7 +122,6 @@ pub(crate) fn move_top_level_block_with_history( target_index: usize, placement: DropPlacement, ) -> Result<(), String> { - // 读取并克隆源块内容 let document = editor .get_json() .map_err(|err| format!("读取当前 JSON 失败:{err}"))?; @@ -135,32 +134,47 @@ pub(crate) fn move_top_level_block_with_history( } let source_block = content[source_index].clone(); - // 删除源块 + let mut insert_at = match placement { + DropPlacement::Before => target_index, + DropPlacement::After => target_index + 1, + }; + if source_index < insert_at { + insert_at = insert_at.saturating_sub(1); + } + + if insert_at == source_index { + return editor + .focus() + .map_err(|err| format!("聚焦编辑器失败:{err}")); + } + delete_top_level_block_with_history(editor, source_index)?; - // 重新计算目标位置(删除后索引可能偏移) - let adjusted_target = - if source_index < target_index { - target_index - 1 - } else { - target_index - }; let document = editor .get_json() .map_err(|err| format!("重新读取 JSON 失败:{err}"))?; - let range = top_level_block_full_range(&document, adjusted_target) - .ok_or_else(|| format!("无法计算第 {adjusted_target} 个块的插入位置"))?; - - let insert_at = match placement { - DropPlacement::Before => range.from, - DropPlacement::After => range.to, + let content_len_after_move = document + .get("content") + .and_then(Value::as_array) + .map(Vec::len) + .unwrap_or(0); + let insert_position = if content_len_after_move == 0 { + 0 + } else if insert_at >= content_len_after_move { + top_level_block_full_range(&document, content_len_after_move - 1) + .map(|range| range.to) + .ok_or_else(|| "无法计算文档末尾插入位置".to_string())? + } else { + top_level_block_full_range(&document, insert_at) + .map(|range| range.from) + .ok_or_else(|| format!("无法计算第 {insert_at} 个块的插入位置"))? }; editor .insert_content_at( TiptapRange { - from: insert_at, - to: insert_at, + from: insert_position, + to: insert_position, }, TiptapContent::json(source_block), Some(TiptapInsertContentOptions { diff --git a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mindmap_node_view.rs b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mindmap_node_view.rs index 40a49200..537aac6a 100644 --- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mindmap_node_view.rs +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mindmap_node_view.rs @@ -11,7 +11,7 @@ use leptos::prelude::*; use leptos::ev; -use leptos::mount::{mount_to, UnmountHandle}; +use leptos::mount::mount_to; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use serde_wasm_bindgen; 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 181ea406..2e5d48ce 100644 --- a/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs +++ b/rust/spikes/leptos-tiptap-spike/src/editor_runtime/mod.rs @@ -4,12 +4,13 @@ pub(crate) mod block_dnd; pub(crate) mod block_hover_state; pub(crate) mod block_menu_document; pub(crate) mod block_menu_overlay; -pub(crate) mod block_menu_legacy_html; +pub(crate) mod bridge_events; pub(crate) mod command_sync; pub(crate) mod content_layout; pub(crate) mod dom_events; pub(crate) mod dom_selection; pub(crate) mod editor_focus; pub(crate) mod history_safe_commands; +pub(crate) mod mindmap_node_view; 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 da71f33a..f28ecb2b 100644 --- a/rust/spikes/leptos-tiptap-spike/src/lib.rs +++ b/rust/spikes/leptos-tiptap-spike/src/lib.rs @@ -17,7 +17,7 @@ use wasm_bindgen::{closure::Closure, prelude::*, JsCast, JsValue}; use wasm_bindgen_futures::{spawn_local, JsFuture}; use web_sys::{ window, CustomEvent, CustomEventInit, DragEvent, Element, Event, EventTarget, HtmlElement, - HtmlInputElement, MouseEvent, Node, RequestInit, RequestMode, Response, WheelEvent, + MouseEvent, Node, RequestInit, RequestMode, Response, WheelEvent, }; use editor_runtime::attachment_upload::dispatch_editor_upload_request; @@ -31,12 +31,16 @@ use editor_runtime::block_menu_overlay; use editor_runtime::block_menu_document::{ collect_plain_text, mindmap_paragraph_node, paragraph_node, }; -use editor_runtime::block_menu_legacy_html::{ - duplicate_top_level_block_html, reorder_top_level_block_html, -}; use editor_runtime::block_hover_state::{ BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState, }; +use editor_runtime::bridge_events::{ + BridgeEnvelope, BridgeSelectorsPayload, ChangeMetaPayload, ChangePayload, HeightPayload, + HostCommandEnvelope, HostCommandPayload, HostStatusPayload, ReadyPayload, RuntimePageOptions, + StatePayload, BLOCK_DELTA_EVENT, CHANGE_EVENT, COMMAND_EVENT, EVENT_PREFIX, HEIGHT_EVENT, + PROTOCOL, READY_EVENT, RUNTIME_NAME, RUNTIME_VERSION, SELECTION_EVENT, STATE_EVENT, + STATUS_EVENT, +}; use editor_runtime::command_sync::{ read_editor_snapshot, sync_editor_outputs, sync_persisted_editor_command, }; @@ -49,6 +53,9 @@ use editor_runtime::dom_selection::{ selection_payload, selection_summary, SelectionPayload, }; use editor_runtime::editor_focus::{active_editor_stage, body_has_focus, schedule_editor_focus}; +use editor_runtime::mindmap_node_view::{ + mount_mindmap_shell_impl, unmount_mindmap_shell_impl, +}; use editor_runtime::overlays::{ clamp_overlay_anchor, close_editor_floating_overlays_if_escape, current_table_overlay_anchor, @@ -74,23 +81,6 @@ const HANDLE_TRIGGER_WIDTH: f64 = 22.0; const HANDLE_TRIGGER_GAP: f64 = 4.0; const HANDLE_MENU_GAP: f64 = 0.0; const HANDLE_TEXT_ALIGN_OFFSET: f64 = 96.0; -const RUNTIME_NAME: &str = "8123-leptos-tiptap-runtime"; -const RUNTIME_VERSION: &str = "1.1.0"; -const PROTOCOL: &str = "mnote.leptos_tiptap.bridge.v1"; -const EVENT_PREFIX: &str = "mnote:leptos-tiptap-spike"; -const READY_EVENT: &str = "mnote:leptos-tiptap-spike:ready"; -const CHANGE_EVENT: &str = "mnote:leptos-tiptap-spike:change"; -const STATE_EVENT: &str = "mnote:leptos-tiptap-spike:state"; -const STATUS_EVENT: &str = "mnote:leptos-tiptap-spike:status"; -const SELECTION_EVENT: &str = "mnote:leptos-tiptap-spike:selection"; -const COMMAND_EVENT: &str = "mnote:leptos-tiptap-spike:command"; -const BLOCK_DELTA_EVENT: &str = "mnote:editor:block-delta"; -const HEIGHT_EVENT: &str = "mnote:leptos-tiptap-spike:height"; -const MINDMAP_SHELL_ACTION_EVENT: &str = "mnote:mindmap-shell:action"; -const MINDMAP_SHELL_PANEL_EVENT: &str = "mnote:mindmap-shell:panel"; -const MINDMAP_SHELL_MINIMAP_EVENT: &str = "mnote:mindmap-shell:minimap"; -const MINDMAP_SHELL_ZOOM_EVENT: &str = "mnote:mindmap-shell:zoom"; -const MINDMAP_SHELL_TOOLBAR_OVERFLOW_EVENT: &str = "mnote:mindmap-shell:toolbar-overflow"; mod editor_runtime; #[wasm_bindgen(inline_js = r#" @@ -3032,141 +3022,6 @@ const SLASH_ACTIONS: [SlashAction; 22] = [ }, ]; -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct BridgeEnvelope -where - T: Serialize, -{ - protocol: &'static str, - runtime: &'static str, - version: &'static str, - source: &'static str, - event: &'static str, - payload: T, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct BridgeSelectorsPayload { - root: &'static str, - stage: &'static str, - editor: &'static str, - toolbar: &'static str, - slash_menu: &'static str, - handle: &'static str, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct HostStatusPayload { - document_id: Option, - workspace_id: Option, - title: String, - dirty_count: u32, - selected_block_index: Option, - current_block_id: Option, - editor_focused: bool, - read_only: bool, - slash_open: bool, - toolbar_open: bool, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct ReadyPayload { - runtime_name: &'static str, - selectors: BridgeSelectorsPayload, - supported_commands: Vec<&'static str>, - supports_embedded_mode: bool, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct StatePayload { - document_id: Option, - workspace_id: Option, - title: String, - dirty_count: u32, - selected_block_index: Option, - editor_focused: bool, - slash_open: bool, - toolbar_open: bool, - read_only: bool, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct ChangeMetaPayload { - dirty_count: u32, - editor_focused: bool, - slash_open: bool, - toolbar_open: bool, - selected_block_index: Option, - revision: Option, - conflict_detection_key: Option, - read_only: bool, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct ChangePayload { - document_id: Option, - workspace_id: Option, - title: String, - content: Value, - meta: ChangeMetaPayload, -} - -#[derive(Clone, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct HeightPayload { - height: f64, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct HostCommandEnvelope { - protocol: Option, - runtime: Option, - version: Option, - source: Option, - event: Option, - payload: Option, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RuntimePageOptions { - wide_layout: Option, - small_text: Option, - layout_density: Option, - show_heading_numbers: Option, - embed_default_block_id: Option, -} - -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct HostCommandPayload { - command: Option, - document_id: Option, - workspace_id: Option, - title: Option, - content: Option, - editable: Option, - page_options: Option, - block_id: Option, - block_index: Option, - text: Option, - reference_document_id: Option, - reference_block_id: Option, - current_block_id: Option, - selection: Option, - revision: Option, - conflict_detection_key: Option, - read_only: Option, -} - #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct LegacyHostEnvelope { @@ -3449,9 +3304,6 @@ struct MountedRuntime { mount_handle: Box, } -struct MountedMindmapShell { - mount_handle: Box, -} impl Drop for MountedRuntime { fn drop(&mut self) { @@ -3469,7 +3321,6 @@ thread_local! { static RUNTIME_MOUNT_OPTIONS: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; static MOUNTED_HANDLES: std::cell::RefCell> = std::cell::RefCell::new(HashMap::new()); static PENDING_RUNTIME_LISTENERS: std::cell::RefCell>> = std::cell::RefCell::new(HashMap::new()); - static MOUNTED_MINDMAP_SHELLS: std::cell::RefCell> = std::cell::RefCell::new(HashMap::new()); static NEXT_MOUNT_ID: Cell = const { Cell::new(1) }; } @@ -3671,26 +3522,14 @@ fn dispatch_ready_event_to_target(target: &EventTarget, payload: &ReadyPayload) dispatch_custom_event_to_target(target, READY_EVENT, payload); } -fn dispatch_change_event(payload: &ChangePayload) { - dispatch_runtime_event(CHANGE_EVENT, payload); -} - fn dispatch_change_event_to_target(target: &EventTarget, payload: &ChangePayload) { dispatch_custom_event_to_target(target, CHANGE_EVENT, payload); } -fn dispatch_state_event(payload: &StatePayload) { - dispatch_runtime_event(STATE_EVENT, payload); -} - fn dispatch_state_event_to_target(target: &EventTarget, payload: &StatePayload) { dispatch_custom_event_to_target(target, STATE_EVENT, payload); } -fn dispatch_status_event(payload: &HostStatusPayload) { - dispatch_runtime_event(STATUS_EVENT, payload); -} - fn dispatch_status_event_to_target(target: &EventTarget, payload: &HostStatusPayload) { dispatch_custom_event_to_target(target, STATUS_EVENT, payload); } @@ -3747,875 +3586,15 @@ fn take_unmount_handle(id: u32) -> Option { MOUNTED_HANDLES.with(|registry| registry.borrow_mut().remove(&id)) } -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellAction { - id: String, - label: String, - #[serde(default)] - long_label: Option, - icon: String, - #[serde(default)] - icon_key: Option, - #[serde(default)] - priority: Option, - #[serde(default)] - overflow_group: Option, - #[serde(default)] - cluster: Option, - disabled: bool, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellToolbarGroup { - id: String, - label: String, - actions: Vec, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellSidebarPanel { - id: String, - kind: String, - label: String, - icon: String, - active: bool, - body_title: String, - body_caption: String, - #[serde(default)] - options: Vec, - #[serde(default)] - body_items: Vec, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellSidebarOption { - id: String, - label: String, - action_id: Option, - value: Value, - control_type: String, - #[serde(default)] - preview: Option, - #[serde(default)] - description: Option, - #[serde(default)] - readonly: bool, - compat_path: Option, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellNavigator { - word_count: u32, - node_count: u32, - zoom_percent: u32, - readonly: bool, - #[serde(default)] - minimap_open: bool, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellToolbarOverflowState { - available_width: Option, - #[serde(default)] - visible_action_ids: Vec, - #[serde(default)] - overflow_action_ids: Vec, - #[serde(default)] - more_open: bool, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellFullscreenState { - mode: String, - is_fullscreen: bool, - target: String, - #[serde(default = "default_true")] - api_available: bool, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellSidebarState { - trigger_visible: bool, - panel_open: bool, - active_panel_id: Option, - drawer_width: u32, - collapsed_by_toggle: bool, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellNavigatorState { - search_open: bool, - minimap_open: bool, - readonly: bool, - zoom_percent: u32, - mouse_behavior: String, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellInteractionState { - chrome_visibility: String, - toolbar_overflow: MindmapShellToolbarOverflowState, - fullscreen: MindmapShellFullscreenState, - sidebar: MindmapShellSidebarState, - navigator: MindmapShellNavigatorState, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellOptions { - mindmap_id: String, - toolbar_groups: Vec, - sidebar_panels: Vec, - navigator: MindmapShellNavigator, - shell: MindmapShellInteractionState, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellActionPayload<'a> { - action_id: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - value: Option<&'a Value>, - #[serde(skip_serializing_if = "Option::is_none")] - compat_path: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - source: Option<&'a str>, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellPanelPayload<'a> { - event: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - panel_id: Option<&'a str>, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellMinimapPayload { - open: bool, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellZoomPayload { - percent: u32, -} - -fn default_true() -> bool { - true -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct MindmapShellToolbarOverflowPayload { - more_open: bool, -} - -fn dispatch_mindmap_shell_event(target: &EventTarget, event_name: &'static str, payload: &T) -where - T: Serialize, -{ - let Ok(detail_value) = serde_wasm_bindgen::to_value(payload) else { - return; - }; - let init = CustomEventInit::new(); - init.set_detail(&detail_value); - init.set_bubbles(true); - if let Ok(event) = CustomEvent::new_with_event_init_dict(event_name, &init) { - let _ = target.dispatch_event(&event); - } -} - -fn render_mindmap_toolbar_action( - action: MindmapShellAction, - class_name: &'static str, - source: &'static str, - action_target: EventTarget, -) -> impl IntoView { - let action_id = action.id.clone(); - let action_label_title = action - .long_label - .clone() - .filter(|label| !label.is_empty()) - .unwrap_or_else(|| action.label.clone()); - let action_label_aria = action_label_title.clone(); - let action_label_text = action.label.clone(); - let action_icon_text = action.icon.clone(); - let action_icon_key = action.icon_key.clone().unwrap_or_default(); - let action_overflow_group = action.overflow_group.clone().unwrap_or_default(); - let action_cluster = action.cluster.clone().unwrap_or_default(); - let action_priority = action.priority.unwrap_or(u32::MAX).to_string(); - let action_disabled = action.disabled; - - view! { - - } -} - -fn render_mindmap_sidebar_option( - option: MindmapShellSidebarOption, - action_target: EventTarget, -) -> impl IntoView { - let option_id = option.id.clone(); - let option_label = option.label.clone(); - let option_control_type = option.control_type.clone(); - let action_id = option.action_id.clone(); - let option_value = option.value.clone(); - let compat_path = option.compat_path.clone(); - let option_preview = option.preview.clone().unwrap_or_default(); - let option_description = option.description.clone().unwrap_or_default(); - let option_readonly = option.readonly; - let option_action_id_attr = action_id.clone().unwrap_or_default(); - - if option_readonly || action_id.is_none() { - return view! { -
- {option_label} - {if option_description.is_empty() { - ().into_any() - } else { - view! { {option_description} }.into_any() - }} -
- } - .into_any(); - } - - let button_class = if option_control_type == "layoutCard" { - "mnote-mindmap-side-option mnote-mindmap-side-option-layout-card" - } else if option_control_type == "swatch" { - "mnote-mindmap-side-option mnote-mindmap-side-option-swatch" - } else if option_control_type == "treeItem" { - "mnote-mindmap-side-option mnote-mindmap-side-option-tree-item" - } else { - "mnote-mindmap-side-option" - }; - - view! { - - } - .into_any() -} - -#[component] -fn MindmapShell(options: MindmapShellOptions, event_target: EventTarget) -> impl IntoView { - let toolbar_groups = options.toolbar_groups.clone(); - let sidebar_panels = options.sidebar_panels.clone(); - let navigator = options.navigator.clone(); - let shell = options.shell.clone(); - let minimap_open = shell.navigator.minimap_open; - let search_open = shell.navigator.search_open; - let sidebar_panel_open = shell.sidebar.panel_open; - let sidebar_trigger_visible = shell.sidebar.trigger_visible; - let active_panel_id = shell.sidebar.active_panel_id.clone(); - let sidebar_drawer_width = shell.sidebar.drawer_width.to_string(); - let chrome_visibility = shell.chrome_visibility.clone(); - let fullscreen_mode = shell.fullscreen.mode.clone(); - let fullscreen_target = shell.fullscreen.target.clone(); - let navigator_mouse_behavior = shell.navigator.mouse_behavior.clone(); - let toolbar_available_width = shell - .toolbar_overflow - .available_width - .map(|width| width.round().to_string()) - .unwrap_or_else(|| "".to_string()); - let chrome_visible = shell.chrome_visibility == "visible"; - let toolbar_visible_actions = shell.toolbar_overflow.visible_action_ids.join(","); - let toolbar_overflow_actions = shell.toolbar_overflow.overflow_action_ids.join(","); - let sidebar_drawer_width_value = shell.sidebar.drawer_width; - let navigator_right_offset = if chrome_visible && sidebar_panel_open && sidebar_trigger_visible - { - sidebar_drawer_width_value + 112 - } else if chrome_visible && sidebar_trigger_visible { - 108 - } else { - 28 - }; - let navigator_right_style = format!("right: {}px;", navigator_right_offset); - let minimap_right_style = format!("right: {}px;", navigator_right_offset); - let mut primary_toolbar_actions: Vec = Vec::new(); - let mut overflow_toolbar_actions: Vec = Vec::new(); - let mut file_toolbar_actions: Vec = Vec::new(); - - for group in toolbar_groups.into_iter() { - match group.id.as_str() { - "overflow" => overflow_toolbar_actions.extend(group.actions), - "file" => file_toolbar_actions.extend(group.actions), - _ => primary_toolbar_actions.extend(group.actions), - } - } - primary_toolbar_actions.sort_by_key(|action| action.priority.unwrap_or(u32::MAX)); - overflow_toolbar_actions.sort_by_key(|action| action.priority.unwrap_or(u32::MAX)); - file_toolbar_actions.sort_by_key(|action| action.priority.unwrap_or(u32::MAX)); - let has_toolbar_overflow = !overflow_toolbar_actions.is_empty(); - let toolbar_more_open = shell.toolbar_overflow.more_open && has_toolbar_overflow; - - let action_target = event_target.clone(); - let panel_target = event_target; - - view! { -
- {if chrome_visible { - view! {
-
-
- {primary_toolbar_actions - .into_iter() - .map(|action| render_mindmap_toolbar_action(action, "mnote-mindmap-tool", "toolbar", action_target.clone())) - .collect_view()} -
- {if has_toolbar_overflow { - let overflow_toggle_target = action_target.clone(); - let overflow_menu_actions = overflow_toolbar_actions.clone(); - view! { -
- - {if toolbar_more_open { - view! { - - }.into_any() - } else { - ().into_any() - }} -
- }.into_any() - } else { - ().into_any() - }} -
- {file_toolbar_actions - .into_iter() - .map(|action| render_mindmap_toolbar_action(action, "mnote-mindmap-tool", "toolbar", action_target.clone())) - .collect_view()} -
-
-
}.into_any() - } else { - ().into_any() - }} - {if chrome_visible { - view! { }.into_any() - } else { - ().into_any() - }} -
- {format!("字数 {}", navigator.word_count)} - {format!("节点 {}", navigator.node_count)} -
- {if chrome_visible { - view! {
-
- -
- - {if search_open { - view! { - - }.into_any() - } else { - ().into_any() - }} -
- - -
-
- - () { - dispatch_mindmap_shell_event(&action_target, MINDMAP_SHELL_ZOOM_EVENT, &MindmapShellZoomPayload { percent }); - } else if let Some(target) = event.target() { - if let Ok(input) = target.dyn_into::() { - input.set_value(&format!("{}%", navigator.zoom_percent)); - } - } - } - } - on:keydown=move |event: ev::KeyboardEvent| { - if event.key() == "Escape" { - if let Some(target) = event.target() { - if let Ok(input) = target.dyn_into::() { - input.set_value(&format!("{}%", navigator.zoom_percent)); - input.blur().ok(); - } - } - } - } - /> - - - -
-
}.into_any() - } else { - ().into_any() - }} - {if minimap_open { - view! { -
-
- - - -
-
-
- }.into_any() - } else { - ().into_any() - }} -
- } -} - -fn take_mindmap_shell_handle(id: u32) -> Option { - MOUNTED_MINDMAP_SHELLS.with(|registry| registry.borrow_mut().remove(&id)) -} - #[wasm_bindgen] pub fn mount_mindmap_shell(container: Element, options: JsValue) -> Result { console_error_panic_hook::set_once(); - let options = serde_wasm_bindgen::from_value::(options)?; - let target = container - .dyn_into::() - .map_err(|_| JsValue::from_str("mindmap shell mount 目标必须是 HTML 元素"))?; - let mount_id = next_mount_id(); - let event_target: EventTarget = target.clone().into(); - let handle = mount_to(target, move || { - view! { - - } - }); - MOUNTED_MINDMAP_SHELLS.with(|registry| { - registry.borrow_mut().insert( - mount_id, - MountedMindmapShell { - mount_handle: Box::new(handle), - }, - ); - }); - Ok(mount_id) + mount_mindmap_shell_impl(container.into(), options) } #[wasm_bindgen] pub fn unmount_mindmap_shell(mount_id: u32) -> Result<(), JsValue> { - if take_mindmap_shell_handle(mount_id).is_some() { - Ok(()) - } else { - Err(JsValue::from_str("找不到对应的 mindmap shell 挂载句柄")) - } + unmount_mindmap_shell_impl(mount_id) } #[wasm_bindgen] @@ -4963,91 +3942,6 @@ fn next_mindmap_id() -> String { ) } -fn apply_html_update( - editor: TiptapEditorHandle, - persisted_identity: &PersistedDocumentIdentity, - next_html: String, - document_id: ReadSignal>, - workspace_id: ReadSignal>, - dirty_count: ReadSignal, - set_dirty_count: WriteSignal, - set_html_output: WriteSignal, - set_document_json: WriteSignal, - set_json_output: WriteSignal, - title: ReadSignal, - hovered_block: ReadSignal>, - editor_focused: ReadSignal, - slash_open: ReadSignal, - turn_into_open: ReadSignal, - color_menu_open: ReadSignal, - more_menu_open: ReadSignal, - revision: ReadSignal>, - conflict_detection_key: ReadSignal>, - read_only: ReadSignal, - set_command_feedback: WriteSignal, - success_message: impl Into, -) { - let success_message = success_message.into(); - match editor.set_content(TiptapContent::html(next_html)) { - Ok(()) => { - set_dirty_count.update(|count| *count += 1); - let (html, snapshot, json_text) = read_editor_snapshot(editor); - set_html_output.set(html.clone()); - set_document_json.set(snapshot.clone()); - set_json_output.set(json_text); - dispatch_change_event(&ChangePayload { - document_id: document_id.get_untracked(), - workspace_id: workspace_id.get_untracked(), - title: title.get_untracked(), - content: snapshot.clone(), - meta: ChangeMetaPayload { - dirty_count: dirty_count.get_untracked(), - editor_focused: editor_focused.get_untracked(), - slash_open: slash_open.get_untracked(), - toolbar_open: toolbar_overlay_locked( - turn_into_open.get_untracked(), - color_menu_open.get_untracked(), - more_menu_open.get_untracked(), - ), - selected_block_index: hovered_block.get_untracked().map(|block| block.index), - revision: revision.get_untracked(), - conflict_detection_key: conflict_detection_key.get_untracked(), - read_only: read_only.get_untracked(), - }, - }); - dispatch_runtime_state( - document_id.get_untracked(), - workspace_id.get_untracked(), - title.get_untracked(), - dirty_count.get_untracked(), - hovered_block.get_untracked(), - editor_focused.get_untracked(), - slash_open.get_untracked(), - turn_into_open.get_untracked(), - color_menu_open.get_untracked(), - more_menu_open.get_untracked(), - read_only.get_untracked(), - ); - match persist_document_state( - persisted_identity, - &title.get_untracked(), - &snapshot, - Some(html), - ) { - Ok(()) => { - set_command_feedback.set(format!("{success_message},并已写入本地草稿")); - } - Err(err) => { - set_command_feedback.set(format!("{success_message},但本地保存失败:{err}")); - } - } - } - Err(err) => { - set_command_feedback.set(format!("更新文档失败:{err}")); - } - } -} - fn top_level_block_boundary_position(document: &Value, index: usize, before: bool) -> Option { let content = document.get("content").and_then(Value::as_array)?; let mut position = 0_u32; @@ -5222,48 +4116,6 @@ fn status_payload( } } -fn dispatch_runtime_state( - document_id: Option, - workspace_id: Option, - title: String, - dirty_count: u32, - hovered_block: Option, - editor_focused: bool, - slash_open: bool, - turn_into_open: bool, - color_menu_open: bool, - more_menu_open: bool, - read_only: bool, -) { - let state = state_payload( - document_id.clone(), - workspace_id.clone(), - title.clone(), - dirty_count, - hovered_block.clone(), - editor_focused, - slash_open, - turn_into_open, - color_menu_open, - more_menu_open, - read_only, - ); - dispatch_state_event(&state); - dispatch_status_event(&status_payload( - document_id, - workspace_id, - title, - dirty_count, - hovered_block, - editor_focused, - slash_open, - turn_into_open, - color_menu_open, - more_menu_open, - read_only, - )); -} - fn dispatch_runtime_state_to_target( target: &EventTarget, document_id: Option, @@ -6814,35 +5666,21 @@ fn App(mount_options: MountOptions) -> impl IntoView { if let Some(source_index) = dragging_block_index.try_get_untracked().flatten() { let indicator = drop_indicator_from_point(event.client_x(), event.client_y()); if let Some(indicator) = indicator { - match editor.get_html() { - Ok(current_html) => match reorder_top_level_block_html( - ¤t_html, + match editor_runtime::history_safe_commands::move_top_level_block_with_history( + &editor, source_index, indicator.index, indicator.placement, ) { - Ok(next_html) => { - apply_html_update( + Ok(()) => { + sync_persisted_editor_command( editor, &persisted_identity, - next_html, - document_id, - workspace_id, - dirty_count, set_dirty_count, set_html_output, set_document_json, set_json_output, title, - hovered_block, - editor_focused, - slash_open, - turn_into_open, - color_menu_open, - more_menu_open, - revision, - conflict_detection_key, - read_only, set_command_feedback, "已通过块手柄拖拽重排", ); @@ -6850,12 +5688,7 @@ fn App(mount_options: MountOptions) -> impl IntoView { Err(err) => { set_command_feedback.set(err); } - }, - Err(err) => { - let _ = - set_command_feedback.try_set(format!("读取当前 HTML 失败:{err}")); } - } } else { let _ = set_command_feedback.try_set("块拖拽已取消".to_string()); } @@ -7701,35 +6534,21 @@ fn App(mount_options: MountOptions) -> impl IntoView { return; }; - match editor.get_html() { - Ok(current_html) => match reorder_top_level_block_html( - ¤t_html, + match editor_runtime::history_safe_commands::move_top_level_block_with_history( + &editor, source_index, indicator.index, indicator.placement, ) { - Ok(next_html) => { - apply_html_update( + Ok(()) => { + sync_persisted_editor_command( editor, &runtime_persisted_identity(document_id, workspace_id), - next_html, - document_id, - workspace_id, - dirty_count, set_dirty_count, set_html_output, set_document_json, set_json_output, title, - hovered_block, - editor_focused, - slash_open, - turn_into_open, - color_menu_open, - more_menu_open, - revision, - conflict_detection_key, - read_only, set_command_feedback, "已通过块手柄拖拽重排", ); @@ -7737,11 +6556,7 @@ fn App(mount_options: MountOptions) -> impl IntoView { Err(err) => { set_command_feedback.set(err); } - }, - Err(err) => { - set_command_feedback.set(format!("读取当前 HTML 失败:{err}")); } - } set_dragging_block_index.set(None); set_dragging_block_anchor.set(None); @@ -8876,30 +7691,16 @@ fn App(mount_options: MountOptions) -> impl IntoView { data-testid="block-drag-menu-item-duplicate" on:click=move |event: MouseEvent| { event.stop_propagation(); - match editor.get_html() { - Ok(current_html) => match duplicate_top_level_block_html(¤t_html, block_index) { - Ok(next_html) => { - apply_html_update( + match editor_runtime::history_safe_commands::duplicate_top_level_block_with_history(&editor, block_index) { + Ok(()) => { + sync_persisted_editor_command( editor, &runtime_persisted_identity(document_id, workspace_id), - next_html, - document_id, - workspace_id, - dirty_count, set_dirty_count, set_html_output, set_document_json, set_json_output, title, - hovered_block, - editor_focused, - slash_open, - turn_into_open, - color_menu_open, - more_menu_open, - revision, - conflict_detection_key, - read_only, set_command_feedback, format!("已复制 {}", duplicate_feedback_label.clone()), ); @@ -8907,8 +7708,6 @@ fn App(mount_options: MountOptions) -> impl IntoView { set_block_menu_anchor.set(None); } Err(err) => set_command_feedback.set(err), - }, - Err(err) => set_command_feedback.set(format!("读取当前 HTML 失败:{err}")), } } >