refactor: extract editor action modules
This commit is contained in:
@@ -186,7 +186,7 @@ codegraph sync .
|
||||
|----|------|
|
||||
| SIDEBAR_TREE_JS 外置 | 布局文件从 11,741 → ~1,100 行 |
|
||||
| browser/ 模块 | 11 个 JS 文件,总计 ~11,000 行 |
|
||||
| editor_runtime/ 模块 | 16 个 Rust 模块(另有 mod.rs) |
|
||||
| editor_runtime/ 模块 | 19 个 Rust 模块(另有 mod.rs) |
|
||||
| 浏览器 smoke | task479/488/489/451 全部 PASS |
|
||||
| 测试 | editor 13/13, web sidebar 10/10, web full 531/549 |
|
||||
| CodeGraph | 350+ 文件索引完成 |
|
||||
@@ -195,4 +195,4 @@ codegraph sync .
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| Batch C 全量验证 | 尚未运行 `cargo test -p mnote-web --lib` / `cargo test -p mnote-leptos-tiptap-spike` / task451 全套;本轮已完成 spike check/build、wasm-bindgen、task479/task488/task489 与直接编辑器交互回归 |
|
||||
| Batch C 全量验证 | 尚未运行 `cargo test -p mnote-web --lib` / `cargo test -p mnote-leptos-tiptap-spike` / task451 全套;本轮已完成 spike check/build、wasm-bindgen、task479/task488/task489、临时 slash/table Playwright 回归与直接编辑器交互回归 |
|
||||
|
||||
@@ -406,11 +406,14 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
||||
|
||||
| 文件 | 原大小 | 现大小 | 变化 |
|
||||
|------|--------|--------|------|
|
||||
| `lib.rs` | 9,873 行 | 8,671 行 | **-12%** |
|
||||
| `lib.rs` | 9,873 行 | 8,181 行 | **-17%** |
|
||||
| `editor_runtime/mindmap_node_view.rs` | — | 909 行 | 新建 |
|
||||
| `editor_runtime/bridge_events.rs` | — | 149 行 | 新建 |
|
||||
| `editor_runtime/slash_actions.rs` | — | 237 行 | 新建 |
|
||||
| `editor_runtime/table_commands.rs` | — | 180 行 | 新建 |
|
||||
| `editor_runtime/bridge_dispatch.rs` | — | 86 行 | 新建 |
|
||||
|
||||
### editor_runtime/ 最终模块清单(16 模块,另有 `mod.rs`)
|
||||
### editor_runtime/ 最终模块清单(19 模块,另有 `mod.rs`)
|
||||
|
||||
| 模块 | 说明 | 来源 |
|
||||
|------|------|------|
|
||||
@@ -420,6 +423,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
||||
| `block_hover_state.rs` | HoveredBlockState 共享类型 | 5-29 |
|
||||
| `block_menu_document.rs` | legacy JSON helper | 5-29 |
|
||||
| `block_menu_overlay.rs` | block menu 几何 | 5-29 |
|
||||
| `bridge_dispatch.rs` | host command kind 解析 | **本批新建** |
|
||||
| `bridge_events.rs` | 事件常量 + payload 类型 | **本批新建** |
|
||||
| `command_sync.rs` | 命令同步 | 5-28 P2 |
|
||||
| `content_layout.rs` | 内容布局 | 5-29 |
|
||||
@@ -430,6 +434,8 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
||||
| `mindmap_node_view.rs` | mindmap shell 完整实现 | **本批新建**(5-28 P5) |
|
||||
| `overlays.rs` | overlay 管理 | 5-29 |
|
||||
| `persistence.rs` | 持久化 | 5-28 P2 |
|
||||
| `slash_actions.rs` | slash 菜单 action 数据 | **本批新建** |
|
||||
| `table_commands.rs` | table toolbar / option 命令 | **本批新建** |
|
||||
|
||||
### 未完成(后续独立任务)
|
||||
|
||||
@@ -448,3 +454,8 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
||||
- 拖拽重排两条路径与块菜单“拷贝副本”不再读取/写回整文 HTML,改为 `move_top_level_block_with_history` / `duplicate_top_level_block_with_history` + `sync_persisted_editor_command`。
|
||||
- 删除本轮变成无调用方的 `block_menu_legacy_html.rs` 与 `apply_html_update`,generated island JS/WASM 已重新生成。
|
||||
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、`cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release`、`wasm-bindgen --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm`、`task489`、`task488`、`task479`、临时 Playwright 拷贝副本 undo/redo + 块拖拽重排回归。
|
||||
- 2026-05-25:Codex 继续完成低风险模块提取。
|
||||
- 新增 `slash_actions.rs`,迁出 `SlashActionKind` / `SlashAction` / `SLASH_ACTIONS`,`lib.rs` 继续持有 slash 命令执行与 Leptos view。
|
||||
- 新增 `table_commands.rs`,迁出 table toolbar / option action、命令执行和 checked 状态计算。
|
||||
- 新增 `bridge_dispatch.rs`,迁出 `HostCommandKind` 及 host command payload 解析;真实 command 执行 match 仍留在 `lib.rs`。
|
||||
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、`cargo test --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml command -- --test-threads=1`、wasm release build、`wasm-bindgen`、task489、task488、task479、临时 slash 插入简单表格 + 表格行 aux 命令 Playwright 回归。
|
||||
|
||||
+9
-9
@@ -1244,47 +1244,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: 1144, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1143, 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: 1196, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1195, 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: 972, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 971, 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: 1094, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1093, 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: 1144, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1143, 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: 598, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 879, 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: 1096, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1095, 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: 1111, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1110, 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: 1147, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1146, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c);
|
||||
return ret;
|
||||
},
|
||||
|
||||
BIN
Binary file not shown.
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user