refactor: move block delete to history-safe commands

This commit is contained in:
lix-2026
2026-05-24 23:56:25 +08:00
parent 437c690a39
commit f2a7de9896
6 changed files with 343 additions and 76 deletions
@@ -1259,47 +1259,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: 1135, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1138, 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: 1187, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1190, 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: 963, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 966, 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: 1085, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1088, 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: 1135, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1138, 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: 745, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 504, 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: 1087, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1090, 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: 1102, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1105, 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: 1138, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1141, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c);
return ret;
},
@@ -1,4 +1,8 @@
use leptos_tiptap::TiptapEditorHandle;
use leptos_tiptap::{
TiptapContent, TiptapEditorHandle, TiptapInsertContentOptions, TiptapRange,
};
use serde_json::json;
use serde_json::Value;
use crate::editor_runtime::attachment_links;
@@ -18,3 +22,92 @@ pub(crate) fn delete_selected_attachment_link_with_history(
// 继续走 Tiptap 命令路径,保留撤销/重做历史。
editor.delete_selection().map_err(|error| error.to_string())
}
fn prosemirror_node_size(node: &Value) -> Option<u32> {
match node.get("type").and_then(Value::as_str) {
Some("text") => Some(
node.get("text")
.and_then(Value::as_str)
.map(|text| text.encode_utf16().count() as u32)
.unwrap_or(0),
),
Some("hardBreak") | Some("horizontalRule") => Some(1),
_ => {
let Some(children) = node.get("content").and_then(Value::as_array) else {
return Some(match node.get("type").and_then(Value::as_str) {
Some("paragraph") | Some("heading") | Some("blockquote")
| Some("codeBlock") | Some("bulletList") | Some("orderedList")
| Some("taskList") | Some("listItem") | Some("taskItem") | Some("table")
| Some("tableRow") | Some("tableCell") | Some("tableHeader") => 2,
_ => 1,
});
};
let content_size = children.iter().try_fold(0_u32, |acc, child| {
prosemirror_node_size(child).map(|size| acc + size)
})?;
Some(content_size + 2)
}
}
}
fn top_level_block_full_range(document: &Value, index: usize) -> Option<TiptapRange> {
let content = document.get("content").and_then(Value::as_array)?;
let mut position = 0_u32;
for (current_index, node) in content.iter().enumerate() {
let node_size = prosemirror_node_size(node)?;
if current_index == index {
return Some(TiptapRange {
from: position,
to: position + node_size,
});
}
position += node_size;
}
None
}
/// 通过 Tiptap `delete_range()` 删除顶层块,避免使用 `set_content()` 整文替换绕过 history。
pub(crate) fn delete_top_level_block_with_history(
editor: &TiptapEditorHandle,
index: usize,
) -> Result<(), String> {
let document = editor
.get_json()
.map_err(|err| format!("读取当前 JSON 失败:{err}"))?;
let content_len = document
.get("content")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0);
if index >= content_len {
return Err(format!("找不到第 {index} 个块"));
}
let range = top_level_block_full_range(&document, index)
.ok_or_else(|| format!("找不到第 {} 个块的删除范围", index + 1))?;
if content_len <= 1 {
editor
.insert_content_at(
range,
TiptapContent::json(json!({ "type": "paragraph" })),
Some(TiptapInsertContentOptions {
update_selection: Some(true),
..Default::default()
}),
)
.map_err(|err| format!("删除当前块失败:{err}"))?;
} else {
editor
.delete_range(range)
.map_err(|err| format!("删除当前块失败:{err}"))?;
}
editor
.focus()
.map_err(|err| format!("聚焦编辑器失败:{err}"))
}
+18 -58
View File
@@ -6409,30 +6409,6 @@ fn focus_top_level_block_start(editor: TiptapEditorHandle, index: usize) -> Resu
.map_err(|err| format!("定位新块失败:{err}"))
}
fn delete_top_level_block_html(current_html: &str, index: usize) -> Result<String, String> {
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();
if children.length() <= 1 {
container.set_inner_html("<p></p>");
return Ok(container.inner_html());
}
let current = children
.item(index as u32)
.ok_or_else(|| format!("找不到第 {index} 个 HTML 顶层块"))?;
container
.remove_child(&current)
.map_err(|err| format!("删除 HTML 块失败:{err:?}"))?;
Ok(container.inner_html())
}
fn reorder_top_level_block_html(
current_html: &str,
source: usize,
@@ -10389,40 +10365,24 @@ fn App(mount_options: MountOptions) -> impl IntoView {
data-testid="block-drag-menu-item-delete"
on:click=move |event: MouseEvent| {
event.stop_propagation();
match editor.get_html() {
Ok(current_html) => match delete_top_level_block_html(&current_html, block_index) {
Ok(next_html) => {
apply_html_update(
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!("已删除 {}", delete_feedback_label.clone()),
);
set_block_menu_open.set(false);
set_block_menu_anchor.set(None);
set_hovered_block.set(None);
}
Err(err) => set_command_feedback.set(err),
},
Err(err) => set_command_feedback.set(format!("读取当前 HTML 失败:{err}")),
match editor_runtime::history_safe_commands::delete_top_level_block_with_history(&editor, block_index) {
Ok(()) => {
sync_persisted_editor_command(
editor,
&runtime_persisted_identity(document_id, workspace_id),
set_dirty_count,
set_html_output,
set_document_json,
set_json_output,
title,
set_command_feedback,
format!("已删除 {}", delete_feedback_label.clone()),
);
set_block_menu_open.set(false);
set_block_menu_anchor.set(None);
set_hovered_block.set(None);
}
Err(err) => set_command_feedback.set(err),
}
}
>