refactor: move block delete to history-safe commands
This commit is contained in:
@@ -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}"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user