refactor: extract editor action modules

This commit is contained in:
lix-2026
2026-05-25 19:50:50 +08:00
parent 7d5319606c
commit 4537b1a3d6
9 changed files with 536 additions and 509 deletions
@@ -0,0 +1,86 @@
use crate::editor_runtime::bridge_events::HostCommandPayload;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum HostCommandKind {
Undo,
Redo,
ReplaceContent,
SetPageOptions,
InsertInlineReference,
InsertEmbedReference,
RequestCurrentBlockId,
SetEditable,
Focus,
Bootstrap,
}
impl HostCommandKind {
pub(crate) fn from_event_name(event_name: &str) -> Option<Self> {
match event_name {
"undo" => Some(Self::Undo),
"redo" => Some(Self::Redo),
"replaceContent" | "replace-document" => Some(Self::ReplaceContent),
"setPageOptions" | "set-page-options" => Some(Self::SetPageOptions),
"insertInlineReference" => Some(Self::InsertInlineReference),
"insertEmbedReference" => Some(Self::InsertEmbedReference),
"requestCurrentBlockId" => Some(Self::RequestCurrentBlockId),
"setEditable" | "set-editable" => Some(Self::SetEditable),
"focus" => Some(Self::Focus),
"bootstrap" => Some(Self::Bootstrap),
_ => None,
}
}
pub(crate) fn from_payload(payload: &HostCommandPayload) -> Option<Self> {
payload.command.as_deref().and_then(Self::from_event_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_command_aliases_and_canonical_names() {
assert_eq!(
HostCommandKind::from_event_name("replace-document"),
Some(HostCommandKind::ReplaceContent)
);
assert_eq!(
HostCommandKind::from_event_name("replaceContent"),
Some(HostCommandKind::ReplaceContent)
);
assert_eq!(
HostCommandKind::from_event_name("requestCurrentBlockId"),
Some(HostCommandKind::RequestCurrentBlockId)
);
}
#[test]
fn reads_command_from_payload_field() {
let payload = HostCommandPayload {
command: Some("setEditable".to_string()),
document_id: None,
workspace_id: None,
title: None,
content: None,
editable: Some(true),
page_options: None,
block_id: None,
block_index: None,
text: None,
reference_document_id: None,
reference_block_id: None,
current_block_id: None,
selection: None,
revision: None,
conflict_detection_key: None,
read_only: None,
};
assert_eq!(
HostCommandKind::from_payload(&payload),
Some(HostCommandKind::SetEditable)
);
}
}
@@ -4,6 +4,7 @@ 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 bridge_dispatch;
pub(crate) mod bridge_events;
pub(crate) mod command_sync;
pub(crate) mod content_layout;
@@ -14,3 +15,5 @@ pub(crate) mod history_safe_commands;
pub(crate) mod mindmap_node_view;
pub(crate) mod overlays;
pub(crate) mod persistence;
pub(crate) mod slash_actions;
pub(crate) mod table_commands;
@@ -0,0 +1,237 @@
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum SlashActionKind {
AiAssistant,
AiWrite,
ContinueWriting,
Summarize,
MoreAi,
Paragraph,
Heading1,
Heading2,
Heading3,
Heading4,
BulletList,
OrderedList,
Todo,
AdvancedTodo,
Quote,
CodeBlock,
Divider,
SimpleTable,
Mindmap,
Image,
UploadAttachment,
Toc,
}
#[derive(Clone, Copy)]
pub(crate) struct SlashAction {
pub(crate) kind: SlashActionKind,
pub(crate) id: &'static str,
pub(crate) category: &'static str,
pub(crate) icon: &'static str,
pub(crate) label: &'static str,
pub(crate) description: &'static str,
pub(crate) shortcut: &'static str,
}
pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
SlashAction {
kind: SlashActionKind::AiAssistant,
id: "ai-assistant",
category: "AI 助理",
icon: "",
label: "AI 助理",
description: "按 Wolai 基线保留 AI 入口",
shortcut: "/ai",
},
SlashAction {
kind: SlashActionKind::AiWrite,
id: "ai-write",
category: "AI 助理",
icon: "",
label: "用 AI 写作",
description: "唤起写作辅助入口",
shortcut: "/yaixz",
},
SlashAction {
kind: SlashActionKind::ContinueWriting,
id: "continue-writing",
category: "AI 助理",
icon: "",
label: "续写",
description: "按当前上下文继续写作",
shortcut: "/xx",
},
SlashAction {
kind: SlashActionKind::Summarize,
id: "summarize",
category: "AI 助理",
icon: "",
label: "总结",
description: "对当前内容生成摘要",
shortcut: "/zj",
},
SlashAction {
kind: SlashActionKind::MoreAi,
id: "more-ai",
category: "AI 助理",
icon: "",
label: "更多",
description: "更多 AI 命令",
shortcut: "",
},
SlashAction {
kind: SlashActionKind::Paragraph,
id: "paragraph",
category: "基础块列表",
icon: "Aa",
label: "文本",
description: "普通正文块",
shortcut: "/wb",
},
SlashAction {
kind: SlashActionKind::Todo,
id: "todo",
category: "基础块列表",
icon: "",
label: "待办列表",
description: "创建可勾选任务",
shortcut: "/dblb",
},
SlashAction {
kind: SlashActionKind::AdvancedTodo,
id: "advanced-todo",
category: "基础块列表",
icon: "",
label: "高级待办列表",
description: "保留 Wolai 高级待办入口",
shortcut: "/gjdblb",
},
SlashAction {
kind: SlashActionKind::Heading1,
id: "heading-1",
category: "基础块列表",
icon: "H1",
label: "主标题",
description: "一级标题",
shortcut: "/h1",
},
SlashAction {
kind: SlashActionKind::Heading2,
id: "heading-2",
category: "基础块列表",
icon: "H2",
label: "大标题",
description: "二级标题",
shortcut: "/h2",
},
SlashAction {
kind: SlashActionKind::Heading3,
id: "heading-3",
category: "基础块列表",
icon: "H3",
label: "中标题",
description: "三级标题",
shortcut: "/h3",
},
SlashAction {
kind: SlashActionKind::Heading4,
id: "heading-4",
category: "基础块列表",
icon: "H4",
label: "小标题",
description: "四级标题",
shortcut: "/h4",
},
SlashAction {
kind: SlashActionKind::BulletList,
id: "bullet",
category: "基础块列表",
icon: "",
label: "列表",
description: "记录普通要点",
shortcut: "/lb",
},
SlashAction {
kind: SlashActionKind::OrderedList,
id: "ordered",
category: "基础块列表",
icon: "1.",
label: "数字列表",
description: "记录步骤顺序",
shortcut: "/szlb",
},
SlashAction {
kind: SlashActionKind::Quote,
id: "quote",
category: "基础块列表",
icon: "",
label: "引述文字",
description: "包住注释与摘录",
shortcut: "/ys",
},
SlashAction {
kind: SlashActionKind::CodeBlock,
id: "code-block",
category: "基础块列表",
icon: "<> ",
label: "代码片段",
description: "插入带语义的代码块",
shortcut: "/dm",
},
SlashAction {
kind: SlashActionKind::Divider,
id: "divider",
category: "基础块列表",
icon: "",
label: "分割线",
description: "插入分隔线",
shortcut: "/fgx",
},
SlashAction {
kind: SlashActionKind::SimpleTable,
id: "simple-table",
category: "进阶块列表",
icon: "",
label: "简单表格",
description: "插入基础表格",
shortcut: "/jdbg",
},
SlashAction {
kind: SlashActionKind::Mindmap,
id: "mindmap",
category: "进阶块列表",
icon: "",
label: "思维导图",
description: "插入 kernel projection 导图块",
shortcut: "/dt",
},
SlashAction {
kind: SlashActionKind::Image,
id: "image",
category: "媒体与附件",
icon: "",
label: "图片",
description: "上传并插入图片",
shortcut: "/tp",
},
SlashAction {
kind: SlashActionKind::UploadAttachment,
id: "upload-attachment",
category: "媒体与附件",
icon: "",
label: "上传附件",
description: "上传 Office、PDF 或其他文件",
shortcut: "/fj",
},
SlashAction {
kind: SlashActionKind::Toc,
id: "toc",
category: "进阶块列表",
icon: "",
label: "页面目录",
description: "根据当前标题生成目录",
shortcut: "/toc",
},
];
@@ -0,0 +1,180 @@
use leptos_tiptap::TiptapEditorHandle;
use serde_json::Value;
#[derive(Clone, Copy)]
pub(crate) enum TableToolbarAction {
AddRowAfter,
AddColumnAfter,
DeleteRow,
DeleteColumn,
ClearCell,
DeleteTable,
}
#[derive(Clone, Copy)]
pub(crate) enum TableOptionAction {
ToggleHeaderRow,
ToggleHeaderColumn,
ToggleHiddenBorders,
}
impl TableToolbarAction {
pub(crate) fn id(self) -> &'static str {
match self {
Self::AddRowAfter => "add-row-after",
Self::AddColumnAfter => "add-column-after",
Self::DeleteRow => "delete-row",
Self::DeleteColumn => "delete-column",
Self::ClearCell => "clear-cell",
Self::DeleteTable => "delete-table",
}
}
pub(crate) fn icon(self) -> &'static str {
match self {
Self::AddRowAfter => "+R",
Self::AddColumnAfter => "+C",
Self::DeleteRow => "-R",
Self::DeleteColumn => "-C",
Self::ClearCell => "",
Self::DeleteTable => "×",
}
}
pub(crate) fn title(self) -> &'static str {
match self {
Self::AddRowAfter => "下方插入行",
Self::AddColumnAfter => "右侧插入列",
Self::DeleteRow => "删除当前行",
Self::DeleteColumn => "删除当前列",
Self::ClearCell => "清空当前单元格",
Self::DeleteTable => "删除表格",
}
}
}
pub(crate) const TABLE_TOOLBAR_ACTIONS: [TableToolbarAction; 6] = [
TableToolbarAction::AddRowAfter,
TableToolbarAction::AddColumnAfter,
TableToolbarAction::ClearCell,
TableToolbarAction::DeleteRow,
TableToolbarAction::DeleteColumn,
TableToolbarAction::DeleteTable,
];
impl TableOptionAction {
pub(crate) fn id(self) -> &'static str {
match self {
Self::ToggleHeaderRow => "toggle-header-row",
Self::ToggleHeaderColumn => "toggle-header-column",
Self::ToggleHiddenBorders => "toggle-hidden-borders",
}
}
pub(crate) fn title(self) -> &'static str {
match self {
Self::ToggleHeaderRow => "标题行",
Self::ToggleHeaderColumn => "标题列",
Self::ToggleHiddenBorders => "隐藏边框线",
}
}
}
pub(crate) const TABLE_OPTION_ACTIONS: [TableOptionAction; 3] = [
TableOptionAction::ToggleHeaderRow,
TableOptionAction::ToggleHeaderColumn,
TableOptionAction::ToggleHiddenBorders,
];
pub(crate) fn run_table_toolbar_action(
editor: TiptapEditorHandle,
action: TableToolbarAction,
) -> Result<&'static str, String> {
let result = match action {
TableToolbarAction::AddRowAfter => editor.add_table_row_after(),
TableToolbarAction::AddColumnAfter => editor.add_table_column_after(),
TableToolbarAction::DeleteRow => editor.delete_table_row(),
TableToolbarAction::DeleteColumn => editor.delete_table_column(),
TableToolbarAction::ClearCell => editor.clear_table_cell(),
TableToolbarAction::DeleteTable => editor.delete_table(),
};
result
.map(|_| action.title())
.map_err(|err| format!("命令执行失败:{err}"))
}
pub(crate) fn run_table_option_action(
editor: TiptapEditorHandle,
action: TableOptionAction,
) -> Result<&'static str, String> {
let result = match action {
TableOptionAction::ToggleHeaderRow => editor.toggle_table_header_row(),
TableOptionAction::ToggleHeaderColumn => editor.toggle_table_header_column(),
TableOptionAction::ToggleHiddenBorders => editor.toggle_table_hidden_borders(),
};
result
.map(|_| action.title())
.map_err(|err| format!("命令执行失败:{err}"))
}
fn first_table_node(value: &Value) -> Option<&Value> {
if value.get("type").and_then(Value::as_str) == Some("table") {
return Some(value);
}
if let Some(table) = value
.get("props")
.and_then(|props| props.get("tiptapTable"))
.and_then(first_table_node)
{
return Some(table);
}
for key in ["content", "children"] {
if let Some(children) = value.get(key).and_then(Value::as_array) {
if let Some(table) = children.iter().find_map(first_table_node) {
return Some(table);
}
}
}
None
}
pub(crate) fn table_option_checked(document: &Value, action: TableOptionAction) -> bool {
let Some(table) = first_table_node(document) else {
return false;
};
match action {
TableOptionAction::ToggleHiddenBorders => table
.get("attrs")
.and_then(|attrs| attrs.get("hiddenBorders"))
.and_then(Value::as_bool)
.unwrap_or(false),
TableOptionAction::ToggleHeaderRow => table
.get("content")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("content"))
.and_then(Value::as_array)
.map(|cells| {
!cells.is_empty()
&& cells
.iter()
.all(|cell| cell.get("type").and_then(Value::as_str) == Some("tableHeader"))
})
.unwrap_or(false),
TableOptionAction::ToggleHeaderColumn => table
.get("content")
.and_then(Value::as_array)
.map(|rows| {
!rows.is_empty()
&& rows.iter().all(|row| {
row.get("content")
.and_then(Value::as_array)
.and_then(|cells| cells.first())
.and_then(|cell| cell.get("type"))
.and_then(Value::as_str)
== Some("tableHeader")
})
})
.unwrap_or(false),
}
}
+6 -496
View File
@@ -34,6 +34,7 @@ use editor_runtime::block_menu_document::{
use editor_runtime::block_hover_state::{
BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState,
};
use editor_runtime::bridge_dispatch::HostCommandKind;
use editor_runtime::bridge_events::{
BridgeEnvelope, BridgeSelectorsPayload, ChangeMetaPayload, ChangePayload, HeightPayload,
HostCommandEnvelope, HostCommandPayload, HostStatusPayload, ReadyPayload, RuntimePageOptions,
@@ -70,6 +71,11 @@ use editor_runtime::persistence::{
load_persisted_document, normalize_identity_value, persist_document_state,
persisted_document_identity, PersistedDocumentIdentity,
};
use editor_runtime::slash_actions::{SlashActionKind, SLASH_ACTIONS};
use editor_runtime::table_commands::{
run_table_option_action, run_table_toolbar_action, table_option_checked, TableToolbarAction,
TABLE_OPTION_ACTIONS, TABLE_TOOLBAR_ACTIONS,
};
#[cfg(test)]
use editor_runtime::persistence::persisted_document_storage_key;
@@ -2749,43 +2755,6 @@ const SPIKE_STYLE: &str = r#"
}
"#;
#[derive(Clone, Copy, PartialEq, Eq)]
enum SlashActionKind {
AiAssistant,
AiWrite,
ContinueWriting,
Summarize,
MoreAi,
Paragraph,
Heading1,
Heading2,
Heading3,
Heading4,
BulletList,
OrderedList,
Todo,
AdvancedTodo,
Quote,
CodeBlock,
Divider,
SimpleTable,
Mindmap,
Image,
UploadAttachment,
Toc,
}
#[derive(Clone, Copy)]
struct SlashAction {
kind: SlashActionKind,
id: &'static str,
category: &'static str,
icon: &'static str,
label: &'static str,
description: &'static str,
shortcut: &'static str,
}
#[derive(Clone, Copy)]
struct FoldedHeadingAction {
level: u8,
@@ -2821,207 +2790,6 @@ const FOLDED_HEADING_ACTIONS: [FoldedHeadingAction; 4] = [
},
];
const SLASH_ACTIONS: [SlashAction; 22] = [
SlashAction {
kind: SlashActionKind::AiAssistant,
id: "ai-assistant",
category: "AI 助理",
icon: "",
label: "AI 助理",
description: "按 Wolai 基线保留 AI 入口",
shortcut: "/ai",
},
SlashAction {
kind: SlashActionKind::AiWrite,
id: "ai-write",
category: "AI 助理",
icon: "",
label: "用 AI 写作",
description: "唤起写作辅助入口",
shortcut: "/yaixz",
},
SlashAction {
kind: SlashActionKind::ContinueWriting,
id: "continue-writing",
category: "AI 助理",
icon: "",
label: "续写",
description: "按当前上下文继续写作",
shortcut: "/xx",
},
SlashAction {
kind: SlashActionKind::Summarize,
id: "summarize",
category: "AI 助理",
icon: "",
label: "总结",
description: "对当前内容生成摘要",
shortcut: "/zj",
},
SlashAction {
kind: SlashActionKind::MoreAi,
id: "more-ai",
category: "AI 助理",
icon: "",
label: "更多",
description: "更多 AI 命令",
shortcut: "",
},
SlashAction {
kind: SlashActionKind::Paragraph,
id: "paragraph",
category: "基础块列表",
icon: "Aa",
label: "文本",
description: "普通正文块",
shortcut: "/wb",
},
SlashAction {
kind: SlashActionKind::Todo,
id: "todo",
category: "基础块列表",
icon: "",
label: "待办列表",
description: "创建可勾选任务",
shortcut: "/dblb",
},
SlashAction {
kind: SlashActionKind::AdvancedTodo,
id: "advanced-todo",
category: "基础块列表",
icon: "",
label: "高级待办列表",
description: "保留 Wolai 高级待办入口",
shortcut: "/gjdblb",
},
SlashAction {
kind: SlashActionKind::Heading1,
id: "heading-1",
category: "基础块列表",
icon: "H1",
label: "主标题",
description: "一级标题",
shortcut: "/h1",
},
SlashAction {
kind: SlashActionKind::Heading2,
id: "heading-2",
category: "基础块列表",
icon: "H2",
label: "大标题",
description: "二级标题",
shortcut: "/h2",
},
SlashAction {
kind: SlashActionKind::Heading3,
id: "heading-3",
category: "基础块列表",
icon: "H3",
label: "中标题",
description: "三级标题",
shortcut: "/h3",
},
SlashAction {
kind: SlashActionKind::Heading4,
id: "heading-4",
category: "基础块列表",
icon: "H4",
label: "小标题",
description: "四级标题",
shortcut: "/h4",
},
SlashAction {
kind: SlashActionKind::BulletList,
id: "bullet",
category: "基础块列表",
icon: "",
label: "列表",
description: "记录普通要点",
shortcut: "/lb",
},
SlashAction {
kind: SlashActionKind::OrderedList,
id: "ordered",
category: "基础块列表",
icon: "1.",
label: "数字列表",
description: "记录步骤顺序",
shortcut: "/szlb",
},
SlashAction {
kind: SlashActionKind::Quote,
id: "quote",
category: "基础块列表",
icon: "",
label: "引述文字",
description: "包住注释与摘录",
shortcut: "/ys",
},
SlashAction {
kind: SlashActionKind::CodeBlock,
id: "code-block",
category: "基础块列表",
icon: "<> ",
label: "代码片段",
description: "插入带语义的代码块",
shortcut: "/dm",
},
SlashAction {
kind: SlashActionKind::Divider,
id: "divider",
category: "基础块列表",
icon: "",
label: "分割线",
description: "插入分隔线",
shortcut: "/fgx",
},
SlashAction {
kind: SlashActionKind::SimpleTable,
id: "simple-table",
category: "进阶块列表",
icon: "",
label: "简单表格",
description: "插入基础表格",
shortcut: "/jdbg",
},
SlashAction {
kind: SlashActionKind::Mindmap,
id: "mindmap",
category: "进阶块列表",
icon: "",
label: "思维导图",
description: "插入 kernel projection 导图块",
shortcut: "/dt",
},
SlashAction {
kind: SlashActionKind::Image,
id: "image",
category: "媒体与附件",
icon: "",
label: "图片",
description: "上传并插入图片",
shortcut: "/tp",
},
SlashAction {
kind: SlashActionKind::UploadAttachment,
id: "upload-attachment",
category: "媒体与附件",
icon: "",
label: "上传附件",
description: "上传 Office、PDF 或其他文件",
shortcut: "/fj",
},
SlashAction {
kind: SlashActionKind::Toc,
id: "toc",
category: "进阶块列表",
icon: "",
label: "页面目录",
description: "根据当前标题生成目录",
shortcut: "/toc",
},
];
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LegacyHostEnvelope {
@@ -3045,90 +2813,10 @@ struct HostDocumentPayload {
read_only: Option<bool>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum HostCommandKind {
Undo,
Redo,
ReplaceContent,
SetPageOptions,
InsertInlineReference,
InsertEmbedReference,
RequestCurrentBlockId,
SetEditable,
Focus,
Bootstrap,
}
impl HostCommandKind {
fn from_event_name(event_name: &str) -> Option<Self> {
match event_name {
"undo" => Some(Self::Undo),
"redo" => Some(Self::Redo),
"replaceContent" | "replace-document" => Some(Self::ReplaceContent),
"setPageOptions" | "set-page-options" => Some(Self::SetPageOptions),
"insertInlineReference" => Some(Self::InsertInlineReference),
"insertEmbedReference" => Some(Self::InsertEmbedReference),
"requestCurrentBlockId" => Some(Self::RequestCurrentBlockId),
"setEditable" | "set-editable" => Some(Self::SetEditable),
"focus" => Some(Self::Focus),
"bootstrap" => Some(Self::Bootstrap),
_ => None,
}
}
fn from_payload(payload: &HostCommandPayload) -> Option<Self> {
payload.command.as_deref().and_then(Self::from_event_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_command_aliases_and_canonical_names() {
assert_eq!(
HostCommandKind::from_event_name("replace-document"),
Some(HostCommandKind::ReplaceContent)
);
assert_eq!(
HostCommandKind::from_event_name("replaceContent"),
Some(HostCommandKind::ReplaceContent)
);
assert_eq!(
HostCommandKind::from_event_name("requestCurrentBlockId"),
Some(HostCommandKind::RequestCurrentBlockId)
);
}
#[test]
fn reads_command_from_payload_field() {
let payload = HostCommandPayload {
command: Some("setEditable".to_string()),
document_id: None,
workspace_id: None,
title: None,
content: None,
editable: Some(true),
page_options: None,
block_id: None,
block_index: None,
text: None,
reference_document_id: None,
reference_block_id: None,
current_block_id: None,
selection: None,
revision: None,
conflict_detection_key: None,
read_only: None,
};
assert_eq!(
HostCommandKind::from_payload(&payload),
Some(HostCommandKind::SetEditable)
);
}
#[test]
fn resolves_embedded_mode_from_explicit_runtime_mode_first() {
assert_eq!(
@@ -4292,184 +3980,6 @@ fn apply_image_align(
.map_err(|err| format!("命令执行失败:{err}"))
}
#[derive(Clone, Copy)]
enum TableToolbarAction {
AddRowAfter,
AddColumnAfter,
DeleteRow,
DeleteColumn,
ClearCell,
DeleteTable,
}
#[derive(Clone, Copy)]
enum TableOptionAction {
ToggleHeaderRow,
ToggleHeaderColumn,
ToggleHiddenBorders,
}
impl TableToolbarAction {
fn id(self) -> &'static str {
match self {
Self::AddRowAfter => "add-row-after",
Self::AddColumnAfter => "add-column-after",
Self::DeleteRow => "delete-row",
Self::DeleteColumn => "delete-column",
Self::ClearCell => "clear-cell",
Self::DeleteTable => "delete-table",
}
}
fn icon(self) -> &'static str {
match self {
Self::AddRowAfter => "+R",
Self::AddColumnAfter => "+C",
Self::DeleteRow => "-R",
Self::DeleteColumn => "-C",
Self::ClearCell => "",
Self::DeleteTable => "×",
}
}
fn title(self) -> &'static str {
match self {
Self::AddRowAfter => "下方插入行",
Self::AddColumnAfter => "右侧插入列",
Self::DeleteRow => "删除当前行",
Self::DeleteColumn => "删除当前列",
Self::ClearCell => "清空当前单元格",
Self::DeleteTable => "删除表格",
}
}
}
const TABLE_TOOLBAR_ACTIONS: [TableToolbarAction; 6] = [
TableToolbarAction::AddRowAfter,
TableToolbarAction::AddColumnAfter,
TableToolbarAction::ClearCell,
TableToolbarAction::DeleteRow,
TableToolbarAction::DeleteColumn,
TableToolbarAction::DeleteTable,
];
impl TableOptionAction {
fn id(self) -> &'static str {
match self {
Self::ToggleHeaderRow => "toggle-header-row",
Self::ToggleHeaderColumn => "toggle-header-column",
Self::ToggleHiddenBorders => "toggle-hidden-borders",
}
}
fn title(self) -> &'static str {
match self {
Self::ToggleHeaderRow => "标题行",
Self::ToggleHeaderColumn => "标题列",
Self::ToggleHiddenBorders => "隐藏边框线",
}
}
}
const TABLE_OPTION_ACTIONS: [TableOptionAction; 3] = [
TableOptionAction::ToggleHeaderRow,
TableOptionAction::ToggleHeaderColumn,
TableOptionAction::ToggleHiddenBorders,
];
fn run_table_toolbar_action(
editor: TiptapEditorHandle,
action: TableToolbarAction,
) -> Result<&'static str, String> {
let result = match action {
TableToolbarAction::AddRowAfter => editor.add_table_row_after(),
TableToolbarAction::AddColumnAfter => editor.add_table_column_after(),
TableToolbarAction::DeleteRow => editor.delete_table_row(),
TableToolbarAction::DeleteColumn => editor.delete_table_column(),
TableToolbarAction::ClearCell => editor.clear_table_cell(),
TableToolbarAction::DeleteTable => editor.delete_table(),
};
result
.map(|_| action.title())
.map_err(|err| format!("命令执行失败:{err}"))
}
fn run_table_option_action(
editor: TiptapEditorHandle,
action: TableOptionAction,
) -> Result<&'static str, String> {
let result = match action {
TableOptionAction::ToggleHeaderRow => editor.toggle_table_header_row(),
TableOptionAction::ToggleHeaderColumn => editor.toggle_table_header_column(),
TableOptionAction::ToggleHiddenBorders => editor.toggle_table_hidden_borders(),
};
result
.map(|_| action.title())
.map_err(|err| format!("命令执行失败:{err}"))
}
fn first_table_node(value: &Value) -> Option<&Value> {
if value.get("type").and_then(Value::as_str) == Some("table") {
return Some(value);
}
if let Some(table) = value
.get("props")
.and_then(|props| props.get("tiptapTable"))
.and_then(first_table_node)
{
return Some(table);
}
for key in ["content", "children"] {
if let Some(children) = value.get(key).and_then(Value::as_array) {
if let Some(table) = children.iter().find_map(first_table_node) {
return Some(table);
}
}
}
None
}
fn table_option_checked(document: &Value, action: TableOptionAction) -> bool {
let Some(table) = first_table_node(document) else {
return false;
};
match action {
TableOptionAction::ToggleHiddenBorders => table
.get("attrs")
.and_then(|attrs| attrs.get("hiddenBorders"))
.and_then(Value::as_bool)
.unwrap_or(false),
TableOptionAction::ToggleHeaderRow => table
.get("content")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("content"))
.and_then(Value::as_array)
.map(|cells| {
!cells.is_empty()
&& cells
.iter()
.all(|cell| cell.get("type").and_then(Value::as_str) == Some("tableHeader"))
})
.unwrap_or(false),
TableOptionAction::ToggleHeaderColumn => table
.get("content")
.and_then(Value::as_array)
.map(|rows| {
!rows.is_empty()
&& rows.iter().all(|row| {
row.get("content")
.and_then(Value::as_array)
.and_then(|cells| cells.first())
.and_then(|cell| cell.get("type"))
.and_then(Value::as_str)
== Some("tableHeader")
})
})
.unwrap_or(false),
}
}
fn run_slash_action(
editor: TiptapEditorHandle,
action: SlashActionKind,