refactor: extract block menu html helpers

This commit is contained in:
lix-2026
2026-05-25 01:22:26 +08:00
parent ac15424328
commit a4e50c1d19
7 changed files with 136 additions and 94 deletions
@@ -1284,7 +1284,7 @@ function __wbg_get_imports() {
return ret;
},
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 506, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 713, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf);
return ret;
},
@@ -0,0 +1,120 @@
//! 基于 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::DropPlacement;
/// 复制第 `index` 个顶层 HTML 块并插入其后。
///
/// 纯 DOM 操作:解析 `current_html` 为 `<div>` 子元素,克隆目标块,
/// 插入到下一个同级节点前(或末尾),返回序列化 HTML。
pub(crate) fn duplicate_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();
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::<Element>()
.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<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 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::<Element>()
.map_err(|_| "复制出的拖拽块不是 Element".to_string())?;
container
.remove_child(&current)
.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())
}
@@ -1,5 +1,6 @@
pub(crate) mod attachment_links;
pub(crate) mod attachment_upload;
pub(crate) mod block_menu_legacy_html;
pub(crate) mod command_sync;
pub(crate) mod dom_selection;
pub(crate) mod history_safe_commands;
+4 -92
View File
@@ -21,6 +21,9 @@ use web_sys::{
};
use editor_runtime::attachment_upload::dispatch_editor_upload_request;
use editor_runtime::block_menu_legacy_html::{
duplicate_top_level_block_html, reorder_top_level_block_html,
};
use editor_runtime::command_sync::{
read_editor_snapshot, sync_editor_outputs, sync_persisted_editor_command,
};
@@ -4834,7 +4837,7 @@ struct BlockMenuLayout {
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DropPlacement {
pub(crate) enum DropPlacement {
Before,
After,
}
@@ -6229,38 +6232,6 @@ fn apply_html_update(
}
}
fn duplicate_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();
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::<Element>()
.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())
}
fn top_level_block_boundary_position(document: &Value, index: usize, before: bool) -> Option<u32> {
let content = document.get("content").and_then(Value::as_array)?;
let mut position = 0_u32;
@@ -6343,65 +6314,6 @@ fn focus_top_level_block_start(editor: TiptapEditorHandle, index: usize) -> Resu
.map_err(|err| format!("定位新块失败:{err}"))
}
fn reorder_top_level_block_html(
current_html: &str,
source: usize,
target: usize,
placement: DropPlacement,
) -> 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 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::<Element>()
.map_err(|_| "复制出的拖拽块不是 Element".to_string())?;
container
.remove_child(&current)
.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())
}
fn p0_extensions() -> Vec<TiptapExtension> {
vec![
TiptapExtension::Document,