refactor: extract table toolbar view
This commit is contained in:
@@ -406,14 +406,15 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
|||||||
|
|
||||||
| 文件 | 原大小 | 现大小 | 变化 |
|
| 文件 | 原大小 | 现大小 | 变化 |
|
||||||
|------|--------|--------|------|
|
|------|--------|--------|------|
|
||||||
| `lib.rs` | 9,873 行 | 8,189 行 | **-17%** |
|
| `lib.rs` | 9,873 行 | 7,951 行 | **-19%** |
|
||||||
| `editor_runtime/mindmap_node_view.rs` | — | 906 行 | 新建 |
|
| `editor_runtime/mindmap_node_view.rs` | — | 906 行 | 新建 |
|
||||||
| `editor_runtime/bridge_events.rs` | — | 154 行 | 新建 |
|
| `editor_runtime/bridge_events.rs` | — | 154 行 | 新建 |
|
||||||
| `editor_runtime/slash_actions.rs` | — | 237 行 | 新建 |
|
| `editor_runtime/slash_actions.rs` | — | 237 行 | 新建 |
|
||||||
| `editor_runtime/table_commands.rs` | — | 180 行 | 新建 |
|
| `editor_runtime/table_commands.rs` | — | 180 行 | 新建 |
|
||||||
|
| `editor_runtime/table_toolbar_view.rs` | — | 371 行 | 新建 |
|
||||||
| `editor_runtime/bridge_dispatch.rs` | — | 86 行 | 新建 |
|
| `editor_runtime/bridge_dispatch.rs` | — | 86 行 | 新建 |
|
||||||
|
|
||||||
### editor_runtime/ 最终模块清单(19 模块,另有 `mod.rs`)
|
### editor_runtime/ 最终模块清单(20 模块,另有 `mod.rs`)
|
||||||
|
|
||||||
| 模块 | 说明 | 来源 |
|
| 模块 | 说明 | 来源 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
@@ -436,6 +437,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
|||||||
| `persistence.rs` | 持久化 | 5-28 P2 |
|
| `persistence.rs` | 持久化 | 5-28 P2 |
|
||||||
| `slash_actions.rs` | slash 菜单 action 数据 | **本批新建** |
|
| `slash_actions.rs` | slash 菜单 action 数据 | **本批新建** |
|
||||||
| `table_commands.rs` | table toolbar / option 命令 | **本批新建** |
|
| `table_commands.rs` | table toolbar / option 命令 | **本批新建** |
|
||||||
|
| `table_toolbar_view.rs` | table controls / toolbar / options view | **本批新建** |
|
||||||
|
|
||||||
### 未完成(后续独立任务)
|
### 未完成(后续独立任务)
|
||||||
|
|
||||||
@@ -464,3 +466,8 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
|
|||||||
- 将 `should_auto_close_toolbar_overlays` 接回两个实际 auto-close 判断点,避免 helper 外置后仍有重复手写判定。
|
- 将 `should_auto_close_toolbar_overlays` 接回两个实际 auto-close 判断点,避免 helper 外置后仍有重复手写判定。
|
||||||
- 对必须持有生命周期但 Rust 编译器无法直接观察用途的 mount handle、host envelope 反序列化字段加局部 `#[allow(dead_code)]`,不扩大到模块级。
|
- 对必须持有生命周期但 Rust 编译器无法直接观察用途的 mount handle、host envelope 反序列化字段加局部 `#[allow(dead_code)]`,不扩大到模块级。
|
||||||
- 已验证:`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml` 无 warning,`cargo test --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml -- --test-threads=1` 13/13 通过。
|
- 已验证:`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml` 无 warning,`cargo test --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml -- --test-threads=1` 13/13 通过。
|
||||||
|
- 2026-05-25:Codex 完成 `table_toolbar_view.rs` 第一刀。
|
||||||
|
- 新增 `editor_runtime/table_toolbar_view.rs`,迁出 table controls、table toolbar 和 table options view;`lib.rs` 保留 signal 创建与单一调用点。
|
||||||
|
- 边界:表格命令仍由 `table_commands.rs` 提供,持久化同步仍委托 `command_sync::sync_persisted_editor_command`,未修改 Tiptap table bridge 或保存语义。
|
||||||
|
- 已验证:`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 -- --test-threads=1` 13/13、wasm release build、`wasm-bindgen`。
|
||||||
|
- 浏览器验证:`task158-e30-menu-state-smoke.js` 受旧 Convex mutation `127.0.0.1:3210` 503 阻塞;已用 Playwright 在 `http://127.0.0.1:3000` 登录测试账号后直接设置表格 fixture,确认 table toolbar、options menu、row aux、col aux 均可见。
|
||||||
|
|||||||
+9
-9
@@ -1244,47 +1244,47 @@ function __wbg_get_imports() {
|
|||||||
}
|
}
|
||||||
}, arguments); },
|
}, arguments); },
|
||||||
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1143, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1148, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
||||||
// 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`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1200, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 971, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 976, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
||||||
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__had771ddc65647798);
|
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__had771ddc65647798);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000004: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000004: function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1093, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1098, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000005: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000005: function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1143, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1148, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 879, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 747, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000007: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000007: function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1095, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1100, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000008: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000008: function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1110, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1115, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
|
||||||
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5);
|
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
__wbindgen_cast_0000000000000009: function(arg0, arg1) {
|
__wbindgen_cast_0000000000000009: function(arg0, arg1) {
|
||||||
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1146, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1151, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
||||||
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c);
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -17,3 +17,4 @@ pub(crate) mod overlays;
|
|||||||
pub(crate) mod persistence;
|
pub(crate) mod persistence;
|
||||||
pub(crate) mod slash_actions;
|
pub(crate) mod slash_actions;
|
||||||
pub(crate) mod table_commands;
|
pub(crate) mod table_commands;
|
||||||
|
pub(crate) mod table_toolbar_view;
|
||||||
|
|||||||
@@ -0,0 +1,371 @@
|
|||||||
|
use crate::editor_runtime::{
|
||||||
|
command_sync::sync_persisted_editor_command,
|
||||||
|
overlays::{
|
||||||
|
current_table_overlay_anchor, table_col_aux_style, table_row_aux_style,
|
||||||
|
table_selection_overlay_style, TableOverlayAnchor, TableSelectionKind,
|
||||||
|
TableSelectionOverlayState,
|
||||||
|
},
|
||||||
|
persistence::{persisted_document_identity, PersistedDocumentIdentity},
|
||||||
|
table_commands::{
|
||||||
|
run_table_option_action, run_table_toolbar_action, table_option_checked,
|
||||||
|
TableToolbarAction, TABLE_OPTION_ACTIONS, TABLE_TOOLBAR_ACTIONS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_tiptap::TiptapEditorHandle;
|
||||||
|
use serde_json::Value;
|
||||||
|
use web_sys::MouseEvent;
|
||||||
|
|
||||||
|
fn runtime_persisted_identity(
|
||||||
|
document_id: ReadSignal<Option<String>>,
|
||||||
|
workspace_id: ReadSignal<Option<String>>,
|
||||||
|
) -> PersistedDocumentIdentity {
|
||||||
|
persisted_document_identity(document_id.get_untracked(), workspace_id.get_untracked())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) fn table_toolbar_view(
|
||||||
|
editor: TiptapEditorHandle,
|
||||||
|
table_toolbar_open: ReadSignal<bool>,
|
||||||
|
set_table_toolbar_open: WriteSignal<bool>,
|
||||||
|
table_options_open: ReadSignal<bool>,
|
||||||
|
set_table_options_open: WriteSignal<bool>,
|
||||||
|
table_overlay_anchor: ReadSignal<Option<TableOverlayAnchor>>,
|
||||||
|
set_table_overlay_anchor: WriteSignal<Option<TableOverlayAnchor>>,
|
||||||
|
table_selection_overlay: ReadSignal<Option<TableSelectionOverlayState>>,
|
||||||
|
set_table_selection_overlay: WriteSignal<Option<TableSelectionOverlayState>>,
|
||||||
|
slash_open: ReadSignal<bool>,
|
||||||
|
block_menu_open: ReadSignal<bool>,
|
||||||
|
dragging_block_index: ReadSignal<Option<usize>>,
|
||||||
|
document_json: ReadSignal<Value>,
|
||||||
|
set_document_json: WriteSignal<Value>,
|
||||||
|
set_dirty_count: WriteSignal<u32>,
|
||||||
|
set_html_output: WriteSignal<String>,
|
||||||
|
set_json_output: WriteSignal<String>,
|
||||||
|
title: ReadSignal<String>,
|
||||||
|
document_id: ReadSignal<Option<String>>,
|
||||||
|
workspace_id: ReadSignal<Option<String>>,
|
||||||
|
set_command_feedback: WriteSignal<String>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<>
|
||||||
|
{move || {
|
||||||
|
if !table_toolbar_open.get()
|
||||||
|
|| slash_open.get()
|
||||||
|
|| block_menu_open.get()
|
||||||
|
|| dragging_block_index.get().is_some()
|
||||||
|
{
|
||||||
|
return ().into_any();
|
||||||
|
}
|
||||||
|
let Some(anchor) = table_overlay_anchor.get() else {
|
||||||
|
return ().into_any();
|
||||||
|
};
|
||||||
|
let controls_style = format!(
|
||||||
|
"left:{:.1}px;top:{:.1}px;width:{:.1}px;height:{:.1}px;",
|
||||||
|
anchor.left, anchor.top, anchor.width, anchor.height
|
||||||
|
);
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="table-controls"
|
||||||
|
data-testid="mnote-leptos-tiptap-table-controls"
|
||||||
|
style=controls_style
|
||||||
|
>
|
||||||
|
{move || {
|
||||||
|
table_selection_overlay
|
||||||
|
.get()
|
||||||
|
.map(|selection| {
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="table-selection-overlay"
|
||||||
|
data-testid="table-selection-overlay"
|
||||||
|
data-selection-kind=selection.kind.id()
|
||||||
|
data-selection-index=selection.index.to_string()
|
||||||
|
style=table_selection_overlay_style(&anchor, &selection)
|
||||||
|
></div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_any()
|
||||||
|
}}
|
||||||
|
{(0..anchor.rows).map(|index| {
|
||||||
|
let active = table_selection_overlay
|
||||||
|
.get()
|
||||||
|
.map(|selection| {
|
||||||
|
selection.kind == TableSelectionKind::Row && selection.index == index
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
view! {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="table-aux-btn table-row-aux"
|
||||||
|
data-testid=format!("table-row-aux-{index}")
|
||||||
|
data-active=active.to_string()
|
||||||
|
style=table_row_aux_style(&anchor, index)
|
||||||
|
title="点击插入一行"
|
||||||
|
aria-label="点击插入一行"
|
||||||
|
on:mousedown=move |event: MouseEvent| {
|
||||||
|
event.prevent_default();
|
||||||
|
event.stop_propagation();
|
||||||
|
}
|
||||||
|
on:click=move |event: MouseEvent| {
|
||||||
|
event.prevent_default();
|
||||||
|
event.stop_propagation();
|
||||||
|
set_table_toolbar_open.set(true);
|
||||||
|
set_table_options_open.set(false);
|
||||||
|
match run_table_toolbar_action(editor, TableToolbarAction::AddRowAfter) {
|
||||||
|
Ok(message) => {
|
||||||
|
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,
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
if let Err(err) = editor.select_table_row(index as u32) {
|
||||||
|
set_command_feedback.set(format!(
|
||||||
|
"行已插入,但表格行选区同步失败:{err}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
set_table_overlay_anchor
|
||||||
|
.set(current_table_overlay_anchor());
|
||||||
|
set_table_selection_overlay.set(Some(
|
||||||
|
TableSelectionOverlayState {
|
||||||
|
kind: TableSelectionKind::Row,
|
||||||
|
index,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(err) => set_command_feedback.set(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>"+"</button>
|
||||||
|
}
|
||||||
|
}).collect_view()}
|
||||||
|
{(0..anchor.cols).map(|index| {
|
||||||
|
let active = table_selection_overlay
|
||||||
|
.get()
|
||||||
|
.map(|selection| {
|
||||||
|
selection.kind == TableSelectionKind::Column
|
||||||
|
&& selection.index == index
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
view! {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="table-aux-btn table-col-aux"
|
||||||
|
data-testid=format!("table-col-aux-{index}")
|
||||||
|
data-active=active.to_string()
|
||||||
|
style=table_col_aux_style(&anchor, index)
|
||||||
|
title="点击插入一列"
|
||||||
|
aria-label="点击插入一列"
|
||||||
|
on:mousedown=move |event: MouseEvent| {
|
||||||
|
event.prevent_default();
|
||||||
|
event.stop_propagation();
|
||||||
|
}
|
||||||
|
on:click=move |event: MouseEvent| {
|
||||||
|
event.prevent_default();
|
||||||
|
event.stop_propagation();
|
||||||
|
set_table_toolbar_open.set(true);
|
||||||
|
set_table_options_open.set(false);
|
||||||
|
match run_table_toolbar_action(
|
||||||
|
editor,
|
||||||
|
TableToolbarAction::AddColumnAfter,
|
||||||
|
) {
|
||||||
|
Ok(message) => {
|
||||||
|
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,
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
if let Err(err) = editor.select_table_column(index as u32) {
|
||||||
|
set_command_feedback.set(format!(
|
||||||
|
"列已插入,但表格列选区同步失败:{err}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
set_table_overlay_anchor
|
||||||
|
.set(current_table_overlay_anchor());
|
||||||
|
set_table_selection_overlay.set(Some(
|
||||||
|
TableSelectionOverlayState {
|
||||||
|
kind: TableSelectionKind::Column,
|
||||||
|
index,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(err) => set_command_feedback.set(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>"+"</button>
|
||||||
|
}
|
||||||
|
}).collect_view()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}}
|
||||||
|
{move || {
|
||||||
|
if !table_toolbar_open.get()
|
||||||
|
|| slash_open.get()
|
||||||
|
|| block_menu_open.get()
|
||||||
|
|| dragging_block_index.get().is_some()
|
||||||
|
{
|
||||||
|
return ().into_any();
|
||||||
|
}
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="table-toolbar"
|
||||||
|
data-testid="mnote-leptos-tiptap-table-toolbar"
|
||||||
|
on:mousedown=move |event: MouseEvent| {
|
||||||
|
event.prevent_default();
|
||||||
|
event.stop_propagation();
|
||||||
|
}
|
||||||
|
on:click=move |event: MouseEvent| {
|
||||||
|
event.stop_propagation();
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{TABLE_TOOLBAR_ACTIONS.iter().enumerate().map(|(index, action)| {
|
||||||
|
let action = *action;
|
||||||
|
let testid = format!("table-toolbar-{}", action.id());
|
||||||
|
view! {
|
||||||
|
<>
|
||||||
|
{if index == 3 {
|
||||||
|
view! {
|
||||||
|
<span
|
||||||
|
class="table-toolbar-separator"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
} else {
|
||||||
|
().into_any()
|
||||||
|
}}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="table-toolbar-btn"
|
||||||
|
data-testid=testid
|
||||||
|
title=action.title()
|
||||||
|
aria-label=action.title()
|
||||||
|
on:click=move |_| {
|
||||||
|
match run_table_toolbar_action(editor, action) {
|
||||||
|
Ok(message) => {
|
||||||
|
if matches!(action, TableToolbarAction::DeleteTable)
|
||||||
|
{
|
||||||
|
set_table_toolbar_open.set(false);
|
||||||
|
set_table_options_open.set(false);
|
||||||
|
set_table_overlay_anchor.set(None);
|
||||||
|
set_table_selection_overlay.set(None);
|
||||||
|
} else {
|
||||||
|
set_table_selection_overlay.set(None);
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
if !matches!(
|
||||||
|
action,
|
||||||
|
TableToolbarAction::DeleteTable
|
||||||
|
) {
|
||||||
|
set_table_overlay_anchor
|
||||||
|
.set(current_table_overlay_anchor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => set_command_feedback.set(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{action.icon()}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}).collect_view()}
|
||||||
|
<span class="table-toolbar-separator" aria-hidden="true"></span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="table-toolbar-btn"
|
||||||
|
data-testid="table-toolbar-options"
|
||||||
|
title="选项"
|
||||||
|
aria-label="选项"
|
||||||
|
aria-expanded=move || table_options_open.get().to_string()
|
||||||
|
on:click=move |_| {
|
||||||
|
set_table_options_open.update(|open| *open = !*open);
|
||||||
|
}
|
||||||
|
>
|
||||||
|
"选项"
|
||||||
|
</button>
|
||||||
|
{move || {
|
||||||
|
if !table_options_open.get() {
|
||||||
|
return ().into_any();
|
||||||
|
}
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="table-options-menu"
|
||||||
|
data-testid="table-toolbar-options-menu"
|
||||||
|
>
|
||||||
|
{TABLE_OPTION_ACTIONS.iter().map(|action| {
|
||||||
|
let action = *action;
|
||||||
|
let testid = format!("table-option-{}", action.id());
|
||||||
|
view! {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="table-option-btn"
|
||||||
|
data-testid=testid
|
||||||
|
role="switch"
|
||||||
|
aria-checked=move || {
|
||||||
|
table_option_checked(&document_json.get(), action)
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
on:click=move |_| {
|
||||||
|
match run_table_option_action(editor, action) {
|
||||||
|
Ok(message) => {
|
||||||
|
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,
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => set_command_feedback.set(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{action.title()}</span>
|
||||||
|
<span
|
||||||
|
class="table-option-switch"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}).collect_view()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,23 +58,19 @@ use editor_runtime::mindmap_node_view::{
|
|||||||
};
|
};
|
||||||
use editor_runtime::overlays::{
|
use editor_runtime::overlays::{
|
||||||
clamp_overlay_anchor,
|
clamp_overlay_anchor,
|
||||||
close_editor_floating_overlays_if_escape, current_table_overlay_anchor,
|
close_editor_floating_overlays_if_escape,
|
||||||
image_element_from_target, image_toolbar_anchor_from_image, open_block_menu_overlay,
|
image_element_from_target, image_toolbar_anchor_from_image, open_block_menu_overlay,
|
||||||
open_image_toolbar_overlay, open_slash_menu_overlay, selection_bounding_rect,
|
open_image_toolbar_overlay, open_slash_menu_overlay, selection_bounding_rect,
|
||||||
should_auto_close_toolbar_overlays, sync_overlays_on_selection_change, table_col_aux_style,
|
should_auto_close_toolbar_overlays, sync_overlays_on_selection_change,
|
||||||
table_overlay_anchor_from_table, table_row_aux_style, table_selection_overlay_style,
|
table_overlay_anchor_from_table, toolbar_overlay_locked, try_sync_editor_overlay_state,
|
||||||
toolbar_overlay_locked, try_sync_editor_overlay_state, FloatingToolbarAnchor, ImageToolbarAnchor,
|
FloatingToolbarAnchor, ImageToolbarAnchor, TableOverlayAnchor, TableSelectionOverlayState,
|
||||||
TableOverlayAnchor, TableSelectionKind, TableSelectionOverlayState,
|
|
||||||
};
|
};
|
||||||
use editor_runtime::persistence::{
|
use editor_runtime::persistence::{
|
||||||
load_persisted_document, normalize_identity_value, persist_document_state,
|
load_persisted_document, normalize_identity_value, persist_document_state,
|
||||||
persisted_document_identity, PersistedDocumentIdentity,
|
persisted_document_identity, PersistedDocumentIdentity,
|
||||||
};
|
};
|
||||||
use editor_runtime::slash_actions::{SlashActionKind, SLASH_ACTIONS};
|
use editor_runtime::slash_actions::{SlashActionKind, SLASH_ACTIONS};
|
||||||
use editor_runtime::table_commands::{
|
use editor_runtime::table_toolbar_view::table_toolbar_view;
|
||||||
run_table_option_action, run_table_toolbar_action, table_option_checked, TableToolbarAction,
|
|
||||||
TABLE_OPTION_ACTIONS, TABLE_TOOLBAR_ACTIONS,
|
|
||||||
};
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use editor_runtime::persistence::persisted_document_storage_key;
|
use editor_runtime::persistence::persisted_document_storage_key;
|
||||||
|
|
||||||
@@ -6078,263 +6074,29 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
|||||||
set_hovered_block.set(None);
|
set_hovered_block.set(None);
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{move || {
|
{table_toolbar_view(
|
||||||
if !table_toolbar_open.get() || slash_open.get() || block_menu_open.get() || dragging_block_index.get().is_some() {
|
|
||||||
return ().into_any();
|
|
||||||
}
|
|
||||||
let Some(anchor) = table_overlay_anchor.get() else {
|
|
||||||
return ().into_any();
|
|
||||||
};
|
|
||||||
let controls_style = format!(
|
|
||||||
"left:{:.1}px;top:{:.1}px;width:{:.1}px;height:{:.1}px;",
|
|
||||||
anchor.left,
|
|
||||||
anchor.top,
|
|
||||||
anchor.width,
|
|
||||||
anchor.height
|
|
||||||
);
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="table-controls"
|
|
||||||
data-testid="mnote-leptos-tiptap-table-controls"
|
|
||||||
style=controls_style
|
|
||||||
>
|
|
||||||
{move || {
|
|
||||||
table_selection_overlay.get().map(|selection| {
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="table-selection-overlay"
|
|
||||||
data-testid="table-selection-overlay"
|
|
||||||
data-selection-kind=selection.kind.id()
|
|
||||||
data-selection-index=selection.index.to_string()
|
|
||||||
style=table_selection_overlay_style(&anchor, &selection)
|
|
||||||
></div>
|
|
||||||
}
|
|
||||||
}).into_any()
|
|
||||||
}}
|
|
||||||
{(0..anchor.rows).map(|index| {
|
|
||||||
let active = table_selection_overlay.get()
|
|
||||||
.map(|selection| selection.kind == TableSelectionKind::Row && selection.index == index)
|
|
||||||
.unwrap_or(false);
|
|
||||||
view! {
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="table-aux-btn table-row-aux"
|
|
||||||
data-testid=format!("table-row-aux-{index}")
|
|
||||||
data-active=active.to_string()
|
|
||||||
style=table_row_aux_style(&anchor, index)
|
|
||||||
title="点击插入一行"
|
|
||||||
aria-label="点击插入一行"
|
|
||||||
on:mousedown=move |event: MouseEvent| {
|
|
||||||
event.prevent_default();
|
|
||||||
event.stop_propagation();
|
|
||||||
}
|
|
||||||
on:click=move |event: MouseEvent| {
|
|
||||||
event.prevent_default();
|
|
||||||
event.stop_propagation();
|
|
||||||
set_table_toolbar_open.set(true);
|
|
||||||
set_table_options_open.set(false);
|
|
||||||
match run_table_toolbar_action(editor, TableToolbarAction::AddRowAfter) {
|
|
||||||
Ok(message) => {
|
|
||||||
sync_persisted_editor_command(
|
|
||||||
editor,
|
editor,
|
||||||
&runtime_persisted_identity(document_id, workspace_id),
|
table_toolbar_open,
|
||||||
|
set_table_toolbar_open,
|
||||||
|
table_options_open,
|
||||||
|
set_table_options_open,
|
||||||
|
table_overlay_anchor,
|
||||||
|
set_table_overlay_anchor,
|
||||||
|
table_selection_overlay,
|
||||||
|
set_table_selection_overlay,
|
||||||
|
slash_open,
|
||||||
|
block_menu_open,
|
||||||
|
dragging_block_index,
|
||||||
|
document_json,
|
||||||
|
set_document_json,
|
||||||
set_dirty_count,
|
set_dirty_count,
|
||||||
set_html_output,
|
set_html_output,
|
||||||
set_document_json,
|
|
||||||
set_json_output,
|
set_json_output,
|
||||||
title,
|
title,
|
||||||
|
document_id,
|
||||||
|
workspace_id,
|
||||||
set_command_feedback,
|
set_command_feedback,
|
||||||
message,
|
)}
|
||||||
);
|
|
||||||
if let Err(err) = editor.select_table_row(index as u32) {
|
|
||||||
set_command_feedback.set(format!("行已插入,但表格行选区同步失败:{err}"));
|
|
||||||
}
|
|
||||||
set_table_overlay_anchor.set(current_table_overlay_anchor());
|
|
||||||
set_table_selection_overlay.set(Some(TableSelectionOverlayState { kind: TableSelectionKind::Row, index }));
|
|
||||||
}
|
|
||||||
Err(err) => set_command_feedback.set(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>"+"</button>
|
|
||||||
}
|
|
||||||
}).collect_view()}
|
|
||||||
{(0..anchor.cols).map(|index| {
|
|
||||||
let active = table_selection_overlay.get()
|
|
||||||
.map(|selection| selection.kind == TableSelectionKind::Column && selection.index == index)
|
|
||||||
.unwrap_or(false);
|
|
||||||
view! {
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="table-aux-btn table-col-aux"
|
|
||||||
data-testid=format!("table-col-aux-{index}")
|
|
||||||
data-active=active.to_string()
|
|
||||||
style=table_col_aux_style(&anchor, index)
|
|
||||||
title="点击插入一列"
|
|
||||||
aria-label="点击插入一列"
|
|
||||||
on:mousedown=move |event: MouseEvent| {
|
|
||||||
event.prevent_default();
|
|
||||||
event.stop_propagation();
|
|
||||||
}
|
|
||||||
on:click=move |event: MouseEvent| {
|
|
||||||
event.prevent_default();
|
|
||||||
event.stop_propagation();
|
|
||||||
set_table_toolbar_open.set(true);
|
|
||||||
set_table_options_open.set(false);
|
|
||||||
match run_table_toolbar_action(editor, TableToolbarAction::AddColumnAfter) {
|
|
||||||
Ok(message) => {
|
|
||||||
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,
|
|
||||||
message,
|
|
||||||
);
|
|
||||||
if let Err(err) = editor.select_table_column(index as u32) {
|
|
||||||
set_command_feedback.set(format!("列已插入,但表格列选区同步失败:{err}"));
|
|
||||||
}
|
|
||||||
set_table_overlay_anchor.set(current_table_overlay_anchor());
|
|
||||||
set_table_selection_overlay.set(Some(TableSelectionOverlayState { kind: TableSelectionKind::Column, index }));
|
|
||||||
}
|
|
||||||
Err(err) => set_command_feedback.set(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>"+"</button>
|
|
||||||
}
|
|
||||||
}).collect_view()}
|
|
||||||
</div>
|
|
||||||
}.into_any()
|
|
||||||
}}
|
|
||||||
{move || {
|
|
||||||
if !table_toolbar_open.get() || slash_open.get() || block_menu_open.get() || dragging_block_index.get().is_some() {
|
|
||||||
return ().into_any();
|
|
||||||
}
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="table-toolbar"
|
|
||||||
data-testid="mnote-leptos-tiptap-table-toolbar"
|
|
||||||
on:mousedown=move |event: MouseEvent| {
|
|
||||||
event.prevent_default();
|
|
||||||
event.stop_propagation();
|
|
||||||
}
|
|
||||||
on:click=move |event: MouseEvent| {
|
|
||||||
event.stop_propagation();
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{TABLE_TOOLBAR_ACTIONS.iter().enumerate().map(|(index, action)| {
|
|
||||||
let action = *action;
|
|
||||||
let testid = format!("table-toolbar-{}", action.id());
|
|
||||||
view! {
|
|
||||||
<>
|
|
||||||
{if index == 3 {
|
|
||||||
view! { <span class="table-toolbar-separator" aria-hidden="true"></span> }.into_any()
|
|
||||||
} else {
|
|
||||||
().into_any()
|
|
||||||
}}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="table-toolbar-btn"
|
|
||||||
data-testid=testid
|
|
||||||
title=action.title()
|
|
||||||
aria-label=action.title()
|
|
||||||
on:click=move |_| {
|
|
||||||
match run_table_toolbar_action(editor, action) {
|
|
||||||
Ok(message) => {
|
|
||||||
if matches!(action, TableToolbarAction::DeleteTable) {
|
|
||||||
set_table_toolbar_open.set(false);
|
|
||||||
set_table_options_open.set(false);
|
|
||||||
set_table_overlay_anchor.set(None);
|
|
||||||
set_table_selection_overlay.set(None);
|
|
||||||
} else {
|
|
||||||
set_table_selection_overlay.set(None);
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
message,
|
|
||||||
);
|
|
||||||
if !matches!(action, TableToolbarAction::DeleteTable) {
|
|
||||||
set_table_overlay_anchor.set(current_table_overlay_anchor());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => set_command_feedback.set(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{action.icon()}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}).collect_view()}
|
|
||||||
<span class="table-toolbar-separator" aria-hidden="true"></span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="table-toolbar-btn"
|
|
||||||
data-testid="table-toolbar-options"
|
|
||||||
title="选项"
|
|
||||||
aria-label="选项"
|
|
||||||
aria-expanded=move || table_options_open.get().to_string()
|
|
||||||
on:click=move |_| {
|
|
||||||
set_table_options_open.update(|open| *open = !*open);
|
|
||||||
}
|
|
||||||
>
|
|
||||||
"选项"
|
|
||||||
</button>
|
|
||||||
{move || {
|
|
||||||
if !table_options_open.get() {
|
|
||||||
return ().into_any();
|
|
||||||
}
|
|
||||||
view! {
|
|
||||||
<div class="table-options-menu" data-testid="table-toolbar-options-menu">
|
|
||||||
{TABLE_OPTION_ACTIONS.iter().map(|action| {
|
|
||||||
let action = *action;
|
|
||||||
let testid = format!("table-option-{}", action.id());
|
|
||||||
view! {
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="table-option-btn"
|
|
||||||
data-testid=testid
|
|
||||||
role="switch"
|
|
||||||
aria-checked=move || table_option_checked(&document_json.get(), action).to_string()
|
|
||||||
on:click=move |_| {
|
|
||||||
match run_table_option_action(editor, action) {
|
|
||||||
Ok(message) => {
|
|
||||||
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,
|
|
||||||
message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => set_command_feedback.set(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>{action.title()}</span>
|
|
||||||
<span class="table-option-switch" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
}).collect_view()}
|
|
||||||
</div>
|
|
||||||
}.into_any()
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
}.into_any()
|
|
||||||
}}
|
|
||||||
{move || {
|
{move || {
|
||||||
let Some(anchor) = image_toolbar_anchor.get() else {
|
let Some(anchor) = image_toolbar_anchor.get() else {
|
||||||
return ().into_any();
|
return ().into_any();
|
||||||
|
|||||||
Reference in New Issue
Block a user