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}"))
|
||||
}
|
||||
|
||||
@@ -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(¤t)
|
||||
.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(¤t_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),
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user