refactor: split tiptap block handle menu view
This commit is contained in:
+3
-3
@@ -250,13 +250,13 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tree -- --
|
||||
- [x] E3. `runtime_bridge.rs`:迁出 `dispatch_runtime_event`、`dispatch_*_to_target`、host command listener install。
|
||||
- [x] E4. `block_transform.rs`:迁出 `run_block_turn_into_action`、`top_level_block_matches_action`、page/block transform helpers。
|
||||
- [x] E5. `slash_menu_view.rs`:迁出 slash menu Leptos view 和 click handling。
|
||||
- [ ] E6. `block_handle_menu_view.rs`:迁出 block handle menu 和 submenu view,作为最后一刀。
|
||||
- [x] E6. `block_handle_menu_view.rs`:迁出 block handle menu 和 submenu view,作为最后一刀。
|
||||
|
||||
验收标准:
|
||||
|
||||
- [x] `lib.rs` 少于 5,000 行。
|
||||
- [ ] `block_handle_menu_view.rs` 可以超过 1,500 行,但必须有清晰入参 struct,避免二十多个 signal 直接散落。
|
||||
- [ ] 不改变 Tiptap command 行为,不新增编辑器功能。
|
||||
- [x] `block_handle_menu_view.rs` 可以超过 1,500 行,但必须有清晰入参 struct,避免二十多个 signal 直接散落。
|
||||
- [x] 不改变 Tiptap command 行为,不新增编辑器功能。
|
||||
|
||||
验收命令:
|
||||
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
use crate::{
|
||||
current_page_anchor_url, editor_block_by_id, request_ai_edit_bridge,
|
||||
write_mnote_text_to_clipboard,
|
||||
};
|
||||
use crate::editor_runtime::{
|
||||
block_hover_state::{BlockMenuLayout, HoveredBlockState},
|
||||
block_transform::{
|
||||
apply_text_align, run_block_turn_into_action, run_block_turn_into_folded_heading_action,
|
||||
run_block_turn_into_page_action, top_level_block_is_page, top_level_block_matches_action,
|
||||
top_level_block_range,
|
||||
},
|
||||
command_sync::{read_editor_snapshot, sync_persisted_editor_command},
|
||||
dom_events::trap_scroll_inside_menu,
|
||||
dom_selection::runtime_block_id_from_index,
|
||||
editor_focus::schedule_editor_focus,
|
||||
persistence::{persist_document_state, persisted_document_identity, PersistedDocumentIdentity},
|
||||
slash_actions::{SlashActionKind, SLASH_ACTIONS},
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
use leptos_tiptap::{TiptapEditorHandle, TiptapSelectionState, TiptapTextAlign};
|
||||
use serde_json::Value;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::{MouseEvent, WheelEvent};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct FoldedHeadingAction {
|
||||
level: u8,
|
||||
id: &'static str,
|
||||
icon: &'static str,
|
||||
label: &'static str,
|
||||
}
|
||||
|
||||
const FOLDED_HEADING_ACTIONS: [FoldedHeadingAction; 4] = [
|
||||
FoldedHeadingAction {
|
||||
level: 1,
|
||||
id: "1",
|
||||
icon: "H1",
|
||||
label: "折叠主标题",
|
||||
},
|
||||
FoldedHeadingAction {
|
||||
level: 2,
|
||||
id: "2",
|
||||
icon: "H2",
|
||||
label: "折叠大标题",
|
||||
},
|
||||
FoldedHeadingAction {
|
||||
level: 3,
|
||||
id: "3",
|
||||
icon: "H3",
|
||||
label: "折叠中标题",
|
||||
},
|
||||
FoldedHeadingAction {
|
||||
level: 4,
|
||||
id: "4",
|
||||
icon: "H4",
|
||||
label: "折叠小标题",
|
||||
},
|
||||
];
|
||||
|
||||
pub(crate) struct BlockHandleMenuViewProps {
|
||||
pub(crate) editor: TiptapEditorHandle,
|
||||
pub(crate) block: HoveredBlockState,
|
||||
pub(crate) duplicate_block_label: String,
|
||||
pub(crate) delete_block_label: String,
|
||||
pub(crate) menu_layout: BlockMenuLayout,
|
||||
pub(crate) signals: BlockHandleMenuSignals,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct BlockHandleMenuSignals {
|
||||
pub(crate) document_id: ReadSignal<Option<String>>,
|
||||
pub(crate) workspace_id: ReadSignal<Option<String>>,
|
||||
pub(crate) title: ReadSignal<String>,
|
||||
pub(crate) selection_state: ReadSignal<TiptapSelectionState>,
|
||||
pub(crate) block_turn_into_open: ReadSignal<bool>,
|
||||
pub(crate) block_folded_title_open: ReadSignal<bool>,
|
||||
pub(crate) set_ai_bridge_state: WriteSignal<String>,
|
||||
pub(crate) set_ai_bridge_message: WriteSignal<String>,
|
||||
pub(crate) set_dirty_count: WriteSignal<u32>,
|
||||
pub(crate) set_html_output: WriteSignal<String>,
|
||||
pub(crate) set_document_json: WriteSignal<Value>,
|
||||
pub(crate) set_json_output: WriteSignal<String>,
|
||||
pub(crate) set_command_feedback: WriteSignal<String>,
|
||||
pub(crate) set_block_menu_open: WriteSignal<bool>,
|
||||
pub(crate) set_block_menu_anchor: WriteSignal<Option<HoveredBlockState>>,
|
||||
pub(crate) set_block_turn_into_open: WriteSignal<bool>,
|
||||
pub(crate) set_block_folded_title_open: WriteSignal<bool>,
|
||||
pub(crate) set_hovered_block: WriteSignal<Option<HoveredBlockState>>,
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
fn persist_block_transform_result(
|
||||
editor: TiptapEditorHandle,
|
||||
signals: BlockHandleMenuSignals,
|
||||
) -> (String, Value, String) {
|
||||
let (html, snapshot, json_text) = read_editor_snapshot(editor);
|
||||
signals.set_dirty_count.update(|count| *count += 1);
|
||||
signals.set_html_output.set(html.clone());
|
||||
signals.set_document_json.set(snapshot.clone());
|
||||
signals.set_json_output.set(json_text.clone());
|
||||
(html, snapshot, json_text)
|
||||
}
|
||||
|
||||
fn close_block_transform_menus(signals: BlockHandleMenuSignals) {
|
||||
signals.set_block_menu_open.set(false);
|
||||
signals.set_block_menu_anchor.set(None);
|
||||
signals.set_block_turn_into_open.set(false);
|
||||
signals.set_block_folded_title_open.set(false);
|
||||
}
|
||||
|
||||
fn handle_turn_into_action_click(
|
||||
editor: TiptapEditorHandle,
|
||||
block_index: usize,
|
||||
kind: SlashActionKind,
|
||||
label: &'static str,
|
||||
signals: BlockHandleMenuSignals,
|
||||
) {
|
||||
match run_block_turn_into_action(editor, block_index, kind) {
|
||||
Ok(message) => {
|
||||
close_block_transform_menus(signals);
|
||||
let (html, snapshot, _) = persist_block_transform_result(editor, signals);
|
||||
match persist_document_state(
|
||||
&runtime_persisted_identity(signals.document_id, signals.workspace_id),
|
||||
&signals.title.get_untracked(),
|
||||
&snapshot,
|
||||
Some(html),
|
||||
) {
|
||||
Ok(()) => signals.set_command_feedback.set(format!(
|
||||
"{}:块 -> {},并已写入本地草稿",
|
||||
message, label,
|
||||
)),
|
||||
Err(err) => signals.set_command_feedback.set(format!(
|
||||
"{}:块 -> {},但本地保存失败:{}",
|
||||
message, label, err,
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(err) => signals.set_command_feedback.set(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_folded_heading_click(
|
||||
editor: TiptapEditorHandle,
|
||||
block_index: usize,
|
||||
action: FoldedHeadingAction,
|
||||
signals: BlockHandleMenuSignals,
|
||||
) {
|
||||
match run_block_turn_into_folded_heading_action(editor, block_index, action.level) {
|
||||
Ok(message) => {
|
||||
close_block_transform_menus(signals);
|
||||
let (html, snapshot, _) = persist_block_transform_result(editor, signals);
|
||||
match persist_document_state(
|
||||
&runtime_persisted_identity(signals.document_id, signals.workspace_id),
|
||||
&signals.title.get_untracked(),
|
||||
&snapshot,
|
||||
Some(html),
|
||||
) {
|
||||
Ok(()) => signals
|
||||
.set_command_feedback
|
||||
.set(format!("{},并已写入本地草稿", message)),
|
||||
Err(err) => signals
|
||||
.set_command_feedback
|
||||
.set(format!("{},但本地保存失败:{}", message, err)),
|
||||
}
|
||||
}
|
||||
Err(err) => signals.set_command_feedback.set(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn block_transform_shortcut(
|
||||
editor: TiptapEditorHandle,
|
||||
block_index: usize,
|
||||
kind: SlashActionKind,
|
||||
) -> &'static str {
|
||||
if top_level_block_matches_action(editor, block_index, kind) {
|
||||
return "✓";
|
||||
}
|
||||
|
||||
match kind {
|
||||
SlashActionKind::Paragraph => "Ctrl+Alt+0",
|
||||
SlashActionKind::Todo => "Ctrl+Shift+5",
|
||||
SlashActionKind::Heading1 => "Ctrl+Shift+1",
|
||||
SlashActionKind::Heading2 => "Ctrl+Shift+2",
|
||||
SlashActionKind::Heading3 => "Ctrl+Shift+3",
|
||||
SlashActionKind::BulletList => "Ctrl+Shift+6",
|
||||
SlashActionKind::OrderedList => "Ctrl+Shift+7",
|
||||
SlashActionKind::CodeBlock => "Ctrl+Alt+-",
|
||||
SlashActionKind::Mindmap => "/dt",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn block_transform_submenu_view(
|
||||
editor: TiptapEditorHandle,
|
||||
block_index: usize,
|
||||
signals: BlockHandleMenuSignals,
|
||||
) -> impl IntoView {
|
||||
let current_block_is_page = top_level_block_is_page(editor, block_index);
|
||||
let page_current_marker = if current_block_is_page {
|
||||
"✓"
|
||||
} else {
|
||||
"Ctrl+Shift+9"
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="block-drag-submenu" data-testid="block-transform-submenu" data-e30-testid="block-turn-into-menu">
|
||||
{SLASH_ACTIONS
|
||||
.iter()
|
||||
.filter(|action| {
|
||||
!matches!(
|
||||
action.kind,
|
||||
SlashActionKind::Divider
|
||||
| SlashActionKind::SimpleTable
|
||||
| SlashActionKind::Image
|
||||
| SlashActionKind::UploadAttachment
|
||||
| SlashActionKind::Toc
|
||||
| SlashActionKind::AiAssistant
|
||||
| SlashActionKind::AiWrite
|
||||
| SlashActionKind::ContinueWriting
|
||||
| SlashActionKind::Summarize
|
||||
| SlashActionKind::MoreAi
|
||||
)
|
||||
})
|
||||
.map(|action| {
|
||||
let kind = action.kind;
|
||||
let label = action.label;
|
||||
let icon = action.icon;
|
||||
let is_current = top_level_block_matches_action(editor, block_index, kind);
|
||||
let shortcut = block_transform_shortcut(editor, block_index, kind);
|
||||
let testid = format!("block-transform-item-{}", action.id);
|
||||
view! {
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid=testid
|
||||
data-current=is_current.to_string()
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
handle_turn_into_action_click(editor, block_index, kind, label, signals);
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">{icon}</span>
|
||||
<span>{label}</span>
|
||||
<span class="block-drag-menu-shortcut">{shortcut}</span>
|
||||
</button>
|
||||
}
|
||||
})
|
||||
.collect_view()}
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-transform-item-page"
|
||||
data-current=current_block_is_page.to_string()
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
run_block_turn_into_page_action(
|
||||
editor,
|
||||
block_index,
|
||||
signals.document_id,
|
||||
signals.workspace_id,
|
||||
signals.title,
|
||||
signals.set_dirty_count,
|
||||
signals.set_html_output,
|
||||
signals.set_document_json,
|
||||
signals.set_json_output,
|
||||
signals.set_command_feedback,
|
||||
signals.set_block_menu_open,
|
||||
signals.set_block_menu_anchor,
|
||||
signals.set_block_turn_into_open,
|
||||
);
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"▣"</span>
|
||||
<span>"页面"</span>
|
||||
<span class="block-drag-menu-shortcut">{page_current_marker}</span>
|
||||
</button>
|
||||
<button class="block-drag-menu-item" data-testid="block-transform-item-folded-list" disabled=true>
|
||||
<span class="block-drag-menu-icon">"≡"</span>
|
||||
<span>"折叠列表"</span>
|
||||
<span class="block-drag-menu-shortcut">"Ctrl+Shift+8"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-transform-item-folded-title"
|
||||
on:mouseenter=move |_| {
|
||||
signals.set_block_folded_title_open.set(true);
|
||||
}
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
signals.set_block_folded_title_open.set(true);
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"H1"</span>
|
||||
<span>"折叠标题"</span>
|
||||
<span class="block-drag-menu-arrow">"›"</span>
|
||||
</button>
|
||||
<button class="block-drag-menu-item" data-testid="block-transform-item-folded-todo" disabled=true>
|
||||
<span class="block-drag-menu-icon">"☑"</span>
|
||||
<span>"折叠待办"</span>
|
||||
<span class="block-drag-menu-arrow">"›"</span>
|
||||
</button>
|
||||
<button class="block-drag-menu-item" data-testid="block-transform-item-emphasis" disabled=true>
|
||||
<span class="block-drag-menu-icon">"Aa"</span>
|
||||
<span>"着重文字"</span>
|
||||
</button>
|
||||
<button class="block-drag-menu-item" data-testid="block-transform-item-math" disabled=true>
|
||||
<span class="block-drag-menu-icon">"f(x)"</span>
|
||||
<span>"数学公式"</span>
|
||||
<span class="block-drag-menu-shortcut">"Ctrl+Alt++"</span>
|
||||
</button>
|
||||
{move || {
|
||||
if signals.block_folded_title_open.get() {
|
||||
view! {
|
||||
<div class="block-drag-tertiary-submenu" data-testid="block-transform-folded-title-submenu">
|
||||
{FOLDED_HEADING_ACTIONS
|
||||
.iter()
|
||||
.map(|folded| {
|
||||
let action = *folded;
|
||||
let testid = format!("block-transform-folded-heading-{}", action.id);
|
||||
view! {
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid=testid
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
handle_folded_heading_click(editor, block_index, action, signals);
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">{action.icon}</span>
|
||||
<span>{action.label}</span>
|
||||
</button>
|
||||
}
|
||||
})
|
||||
.collect_view()}
|
||||
</div>
|
||||
}.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn block_handle_menu_view(props: BlockHandleMenuViewProps) -> impl IntoView {
|
||||
let BlockHandleMenuViewProps {
|
||||
editor,
|
||||
block,
|
||||
duplicate_block_label,
|
||||
delete_block_label,
|
||||
menu_layout,
|
||||
signals,
|
||||
} = props;
|
||||
let block_index = block.index;
|
||||
let menu_top = format!("{}px", menu_layout.top);
|
||||
let menu_bottom = "auto";
|
||||
|
||||
view! {
|
||||
<div
|
||||
class="block-drag-menu"
|
||||
data-testid="block-drag-menu"
|
||||
style:top=menu_top
|
||||
style:bottom=menu_bottom
|
||||
style:left=format!("{}px", menu_layout.left)
|
||||
style:max-height=format!("{}px", menu_layout.max_height)
|
||||
on:mousedown=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
}
|
||||
on:mouseup=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
}
|
||||
on:mousemove=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
}
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
}
|
||||
on:wheel=move |event: WheelEvent| {
|
||||
trap_scroll_inside_menu(&event);
|
||||
}
|
||||
>
|
||||
<input
|
||||
class="block-drag-command-input"
|
||||
data-testid="block-drag-command-input"
|
||||
readonly=true
|
||||
placeholder="请输入指令"
|
||||
/>
|
||||
<div class="block-drag-menu-actions">
|
||||
<div class="block-drag-menu-section">
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-ai"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
request_ai_edit_bridge(
|
||||
editor,
|
||||
"ask_ai",
|
||||
signals.document_id,
|
||||
signals.workspace_id,
|
||||
Some(block_index),
|
||||
runtime_block_id_from_index(block_index),
|
||||
signals.selection_state,
|
||||
signals.set_ai_bridge_state,
|
||||
signals.set_ai_bridge_message,
|
||||
signals.set_dirty_count,
|
||||
signals.set_html_output,
|
||||
signals.set_document_json,
|
||||
signals.set_json_output,
|
||||
signals.title,
|
||||
signals.set_command_feedback,
|
||||
);
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"✦"</span>
|
||||
<span>"AI 助理"</span>
|
||||
<span class="block-drag-menu-shortcut">"Ctrl+J"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-turn-into"
|
||||
on:mouseenter=move |_| {
|
||||
signals.set_block_turn_into_open.set(true);
|
||||
}
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
signals.set_block_turn_into_open.set(true);
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"↻"</span>
|
||||
<span>"转换为"</span>
|
||||
<span class="block-drag-menu-arrow">"›"</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="block-drag-menu-divider"></div>
|
||||
<div class="block-drag-menu-section">
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-duplicate"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
match crate::editor_runtime::history_safe_commands::duplicate_top_level_block_with_history(&editor, block_index) {
|
||||
Ok(()) => {
|
||||
sync_persisted_editor_command(
|
||||
editor,
|
||||
&runtime_persisted_identity(signals.document_id, signals.workspace_id),
|
||||
signals.set_dirty_count,
|
||||
signals.set_html_output,
|
||||
signals.set_document_json,
|
||||
signals.set_json_output,
|
||||
signals.title,
|
||||
signals.set_command_feedback,
|
||||
format!("已复制 {}", duplicate_block_label.clone()),
|
||||
);
|
||||
signals.set_block_menu_open.set(false);
|
||||
signals.set_block_menu_anchor.set(None);
|
||||
}
|
||||
Err(err) => signals.set_command_feedback.set(err),
|
||||
}
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"▣"</span>
|
||||
<span>"拷贝副本"</span>
|
||||
<span class="block-drag-menu-shortcut">"Ctrl+D"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-delete"
|
||||
on:mousedown=move |event: MouseEvent| {
|
||||
event.prevent_default();
|
||||
event.stop_propagation();
|
||||
}
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
match crate::editor_runtime::history_safe_commands::delete_top_level_block_with_history(&editor, block_index) {
|
||||
Ok(()) => {
|
||||
sync_persisted_editor_command(
|
||||
editor,
|
||||
&runtime_persisted_identity(signals.document_id, signals.workspace_id),
|
||||
signals.set_dirty_count,
|
||||
signals.set_html_output,
|
||||
signals.set_document_json,
|
||||
signals.set_json_output,
|
||||
signals.title,
|
||||
signals.set_command_feedback,
|
||||
format!("已删除 {}", delete_block_label.clone()),
|
||||
);
|
||||
signals.set_block_menu_open.set(false);
|
||||
signals.set_block_menu_anchor.set(None);
|
||||
signals.set_hovered_block.set(None);
|
||||
schedule_editor_focus(editor);
|
||||
}
|
||||
Err(err) => signals.set_command_feedback.set(err),
|
||||
}
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"⌫"</span>
|
||||
<span>"删除"</span>
|
||||
<span class="block-drag-menu-shortcut">"Del"</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="block-drag-menu-divider"></div>
|
||||
<div class="block-drag-menu-section">
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-copy-link"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
let Some(block_id) = runtime_block_id_from_index(block_index) else {
|
||||
signals
|
||||
.set_command_feedback
|
||||
.set("当前块缺少 Rust block id,无法复制锚点链接".to_string());
|
||||
return;
|
||||
};
|
||||
let Some(anchor_url) = current_page_anchor_url(&block_id) else {
|
||||
signals
|
||||
.set_command_feedback
|
||||
.set("当前页面 URL 不可用,无法复制锚点链接".to_string());
|
||||
return;
|
||||
};
|
||||
if let Some(block_element) = editor_block_by_id(&block_id) {
|
||||
let _ = block_element.scroll_into_view_with_bool(true);
|
||||
}
|
||||
spawn_local(async move {
|
||||
match write_mnote_text_to_clipboard(anchor_url.clone()).await {
|
||||
Ok(value) if value.as_bool().unwrap_or(false) => {
|
||||
signals.set_command_feedback.set(format!("已复制块链接:{}", block_id));
|
||||
}
|
||||
_ => {
|
||||
signals
|
||||
.set_command_feedback
|
||||
.set(format!("块链接已生成但剪贴板写入失败:{}", anchor_url));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"🔗"</span>
|
||||
<span>"复制链接"</span>
|
||||
<span class="block-drag-menu-arrow">"›"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-move"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
signals
|
||||
.set_command_feedback
|
||||
.set("移动/嵌入流程后续接 Page Aggregate 命令".to_string());
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"↗"</span>
|
||||
<span>"移动/嵌入到..."</span>
|
||||
<span class="block-drag-menu-shortcut">"Alt+Shift+M/G"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-history"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
signals
|
||||
.set_command_feedback
|
||||
.set("块历史入口已记录,恢复流程后续接版本链路".to_string());
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"◴"</span>
|
||||
<span>"块历史..."</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-comment"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
signals.set_command_feedback.set("评论入口已打开".to_string());
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"◉"</span>
|
||||
<span>"评论"</span>
|
||||
<span class="block-drag-menu-shortcut">"Ctrl+Alt+M"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-color"
|
||||
on:mouseenter=move |_| {
|
||||
signals
|
||||
.set_command_feedback
|
||||
.set("颜色子菜单后续按 Wolai 继续补齐".to_string());
|
||||
}
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
signals
|
||||
.set_command_feedback
|
||||
.set("颜色子菜单后续按 Wolai 继续补齐".to_string());
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"▧"</span>
|
||||
<span>"颜色"</span>
|
||||
<span class="block-drag-menu-arrow">"›"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-align-center"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
let result = (|| -> Result<(), String> {
|
||||
let document = editor
|
||||
.get_json()
|
||||
.map_err(|err| format!("读取当前 JSON 失败:{err}"))?;
|
||||
let range = top_level_block_range(&document, block_index)
|
||||
.ok_or_else(|| format!("找不到第 {} 个块的选择范围", block_index + 1))?;
|
||||
editor
|
||||
.set_text_selection(range)
|
||||
.map_err(|err| format!("选中当前块失败:{err}"))?;
|
||||
apply_text_align(editor, TiptapTextAlign::Center)?;
|
||||
let (html, snapshot, json_text) = read_editor_snapshot(editor);
|
||||
signals.set_dirty_count.update(|count| *count += 1);
|
||||
signals.set_html_output.set(html.clone());
|
||||
signals.set_document_json.set(snapshot.clone());
|
||||
signals.set_json_output.set(json_text);
|
||||
persist_document_state(
|
||||
&runtime_persisted_identity(signals.document_id, signals.workspace_id),
|
||||
&signals.title.get_untracked(),
|
||||
&snapshot,
|
||||
Some(html),
|
||||
)
|
||||
.map_err(|err| format!("文字居中已执行,但本地保存失败:{err}"))?;
|
||||
Ok(())
|
||||
})();
|
||||
match result {
|
||||
Ok(()) => signals
|
||||
.set_command_feedback
|
||||
.set("已切到居中,并已写入本地草稿".to_string()),
|
||||
Err(err) => signals.set_command_feedback.set(err),
|
||||
}
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"≡"</span>
|
||||
<span>"文字居中"</span>
|
||||
<span class="block-drag-menu-switch"></span>
|
||||
</button>
|
||||
<button class="block-drag-menu-item" data-testid="block-drag-menu-item-translate" disabled=true>
|
||||
<span class="block-drag-menu-icon">"文"</span>
|
||||
<span>"文字翻译"</span>
|
||||
<span class="block-drag-menu-arrow">"›"</span>
|
||||
</button>
|
||||
<button class="block-drag-menu-item" data-testid="block-drag-menu-item-poster" disabled=true>
|
||||
<span class="block-drag-menu-icon">"▤"</span>
|
||||
<span>"生成海报"</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="block-drag-menu-footer" data-testid="block-drag-menu-footer">
|
||||
<span>"anonymous"</span>
|
||||
<span>"最后编辑于 刚刚"</span>
|
||||
</div>
|
||||
{move || {
|
||||
if signals.block_turn_into_open.get() {
|
||||
block_transform_submenu_view(editor, block_index, signals).into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub(crate) mod attachment_links;
|
||||
pub(crate) mod attachment_upload;
|
||||
pub(crate) mod block_dnd;
|
||||
pub(crate) mod block_hover_state;
|
||||
pub(crate) mod block_handle_menu_view;
|
||||
pub(crate) mod block_menu_document;
|
||||
pub(crate) mod block_menu_overlay;
|
||||
pub(crate) mod block_transform;
|
||||
|
||||
@@ -11,7 +11,6 @@ use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::fmt::Display;
|
||||
use wasm_bindgen::{closure::Closure, prelude::*, JsCast, JsValue};
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use web_sys::{
|
||||
window, CustomEvent, DragEvent, Element, Event, EventTarget, HtmlElement, MouseEvent,
|
||||
WheelEvent,
|
||||
@@ -26,13 +25,14 @@ use editor_runtime::block_dnd::{
|
||||
use editor_runtime::block_hover_state::{
|
||||
BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState,
|
||||
};
|
||||
use editor_runtime::block_handle_menu_view::{
|
||||
block_handle_menu_view, BlockHandleMenuSignals, BlockHandleMenuViewProps,
|
||||
};
|
||||
use editor_runtime::block_menu_overlay;
|
||||
use editor_runtime::block_transform::{
|
||||
apply_block_delta_to_json, apply_highlight_color, apply_image_align, apply_text_align,
|
||||
apply_text_color, clear_text_color, insert_editor_paragraph_relative_to_block,
|
||||
prosemirror_node_size, run_block_turn_into_action, run_block_turn_into_folded_heading_action,
|
||||
run_block_turn_into_page_action, run_slash_action, top_level_block_boundary_position,
|
||||
top_level_block_is_page, top_level_block_matches_action, top_level_block_range,
|
||||
prosemirror_node_size, run_slash_action, top_level_block_boundary_position,
|
||||
};
|
||||
use editor_runtime::bridge_dispatch::HostCommandKind;
|
||||
use editor_runtime::bridge_events::{
|
||||
@@ -40,9 +40,7 @@ use editor_runtime::bridge_events::{
|
||||
HostCommandPayload, HostStatusPayload, ReadyPayload, StatePayload, BLOCK_DELTA_EVENT,
|
||||
COMMAND_EVENT, HEIGHT_EVENT, PROTOCOL, RUNTIME_NAME,
|
||||
};
|
||||
use editor_runtime::command_sync::{
|
||||
read_editor_snapshot, sync_editor_outputs, sync_persisted_editor_command,
|
||||
};
|
||||
use editor_runtime::command_sync::{sync_editor_outputs, sync_persisted_editor_command};
|
||||
use editor_runtime::content_layout::{content_column_left, content_text_left};
|
||||
use editor_runtime::dom_events::{
|
||||
event_target_matches_selector, target_element, trap_scroll_inside_menu,
|
||||
@@ -339,41 +337,6 @@ const TOOLBAR_ALIGN_OPTIONS: [ToolbarAlignOption; 4] = [
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct FoldedHeadingAction {
|
||||
level: u8,
|
||||
id: &'static str,
|
||||
icon: &'static str,
|
||||
label: &'static str,
|
||||
}
|
||||
|
||||
const FOLDED_HEADING_ACTIONS: [FoldedHeadingAction; 4] = [
|
||||
FoldedHeadingAction {
|
||||
level: 1,
|
||||
id: "1",
|
||||
icon: "H1",
|
||||
label: "折叠主标题",
|
||||
},
|
||||
FoldedHeadingAction {
|
||||
level: 2,
|
||||
id: "2",
|
||||
icon: "H2",
|
||||
label: "折叠大标题",
|
||||
},
|
||||
FoldedHeadingAction {
|
||||
level: 3,
|
||||
id: "3",
|
||||
icon: "H3",
|
||||
label: "折叠中标题",
|
||||
},
|
||||
FoldedHeadingAction {
|
||||
level: 4,
|
||||
id: "4",
|
||||
icon: "H4",
|
||||
label: "折叠小标题",
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[allow(dead_code)]
|
||||
@@ -3378,8 +3341,6 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
||||
.map(|current| current.index == block_index)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let current_duplicate_label = duplicate_block_label.clone();
|
||||
let current_delete_label = delete_block_label.clone();
|
||||
let menu_layout = editor_stage_element().and_then(|stage| {
|
||||
let rect = stage.get_bounding_client_rect();
|
||||
block_menu_overlay::block_menu_layout(&block, rect.width(), rect.height(), rect.top(), rect.left())
|
||||
@@ -3389,485 +3350,33 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
||||
top: 80.0,
|
||||
left: 80.0,
|
||||
});
|
||||
let menu_top = format!("{}px", menu_layout.top);
|
||||
let menu_bottom = "auto";
|
||||
let current_block_is_page = top_level_block_is_page(editor, block_index);
|
||||
let page_current_marker = if current_block_is_page { "✓" } else { "Ctrl+Shift+9" };
|
||||
view! {
|
||||
<div
|
||||
class="block-drag-menu"
|
||||
data-testid="block-drag-menu"
|
||||
style:top=menu_top
|
||||
style:bottom=menu_bottom
|
||||
style:left=format!("{}px", menu_layout.left)
|
||||
style:max-height=format!("{}px", menu_layout.max_height)
|
||||
on:mousedown=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
}
|
||||
on:mouseup=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
}
|
||||
on:mousemove=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
}
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
}
|
||||
on:wheel=move |event: WheelEvent| {
|
||||
trap_scroll_inside_menu(&event);
|
||||
}
|
||||
>
|
||||
<input
|
||||
class="block-drag-command-input"
|
||||
data-testid="block-drag-command-input"
|
||||
readonly=true
|
||||
placeholder="请输入指令"
|
||||
/>
|
||||
<div class="block-drag-menu-actions">
|
||||
{move || {
|
||||
let duplicate_feedback_label = current_duplicate_label.clone();
|
||||
let delete_feedback_label = current_delete_label.clone();
|
||||
view! {
|
||||
<>
|
||||
<div class="block-drag-menu-section">
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-ai"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
request_ai_edit_bridge(
|
||||
editor,
|
||||
"ask_ai",
|
||||
document_id,
|
||||
workspace_id,
|
||||
Some(block_index),
|
||||
runtime_block_id_from_index(block_index),
|
||||
selection_state,
|
||||
set_ai_bridge_state,
|
||||
set_ai_bridge_message,
|
||||
set_dirty_count,
|
||||
set_html_output,
|
||||
set_document_json,
|
||||
set_json_output,
|
||||
title,
|
||||
set_command_feedback,
|
||||
);
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"✦"</span>
|
||||
<span>"AI 助理"</span>
|
||||
<span class="block-drag-menu-shortcut">"Ctrl+J"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-turn-into"
|
||||
on:mouseenter=move |_| {
|
||||
set_block_turn_into_open.set(true);
|
||||
}
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
set_block_turn_into_open.set(true);
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"↻"</span>
|
||||
<span>"转换为"</span>
|
||||
<span class="block-drag-menu-arrow">"›"</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="block-drag-menu-divider"></div>
|
||||
<div class="block-drag-menu-section">
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-duplicate"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
match editor_runtime::history_safe_commands::duplicate_top_level_block_with_history(&editor, block_index) {
|
||||
Ok(()) => {
|
||||
sync_persisted_editor_command(
|
||||
editor,
|
||||
&runtime_persisted_identity(document_id, workspace_id),
|
||||
set_dirty_count,
|
||||
set_html_output,
|
||||
set_document_json,
|
||||
set_json_output,
|
||||
title,
|
||||
set_command_feedback,
|
||||
format!("已复制 {}", duplicate_feedback_label.clone()),
|
||||
);
|
||||
set_block_menu_open.set(false);
|
||||
set_block_menu_anchor.set(None);
|
||||
}
|
||||
Err(err) => set_command_feedback.set(err),
|
||||
}
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"▣"</span>
|
||||
<span>"拷贝副本"</span>
|
||||
<span class="block-drag-menu-shortcut">"Ctrl+D"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-delete"
|
||||
on:mousedown=move |event: MouseEvent| {
|
||||
event.prevent_default();
|
||||
event.stop_propagation();
|
||||
}
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
match editor_runtime::history_safe_commands::delete_top_level_block_with_history(&editor, block_index) {
|
||||
Ok(()) => {
|
||||
sync_persisted_editor_command(
|
||||
editor,
|
||||
&runtime_persisted_identity(document_id, workspace_id),
|
||||
set_dirty_count,
|
||||
set_html_output,
|
||||
set_document_json,
|
||||
set_json_output,
|
||||
title,
|
||||
set_command_feedback,
|
||||
format!("已删除 {}", delete_feedback_label.clone()),
|
||||
);
|
||||
set_block_menu_open.set(false);
|
||||
set_block_menu_anchor.set(None);
|
||||
set_hovered_block.set(None);
|
||||
schedule_editor_focus(editor);
|
||||
}
|
||||
Err(err) => set_command_feedback.set(err),
|
||||
}
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"⌫"</span>
|
||||
<span>"删除"</span>
|
||||
<span class="block-drag-menu-shortcut">"Del"</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="block-drag-menu-divider"></div>
|
||||
<div class="block-drag-menu-section">
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-copy-link"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
let Some(block_id) = runtime_block_id_from_index(block_index) else {
|
||||
set_command_feedback.set("当前块缺少 Rust block id,无法复制锚点链接".to_string());
|
||||
return;
|
||||
};
|
||||
let Some(anchor_url) = current_page_anchor_url(&block_id) else {
|
||||
set_command_feedback.set("当前页面 URL 不可用,无法复制锚点链接".to_string());
|
||||
return;
|
||||
};
|
||||
if let Some(block_element) = editor_block_by_id(&block_id) {
|
||||
let _ = block_element.scroll_into_view_with_bool(true);
|
||||
}
|
||||
spawn_local(async move {
|
||||
match write_mnote_text_to_clipboard(anchor_url.clone()).await {
|
||||
Ok(value) if value.as_bool().unwrap_or(false) => {
|
||||
set_command_feedback.set(format!("已复制块链接:{}", block_id));
|
||||
}
|
||||
_ => {
|
||||
set_command_feedback.set(format!("块链接已生成但剪贴板写入失败:{}", anchor_url));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"🔗"</span>
|
||||
<span>"复制链接"</span>
|
||||
<span class="block-drag-menu-arrow">"›"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-move"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
set_command_feedback.set("移动/嵌入流程后续接 Page Aggregate 命令".to_string());
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"↗"</span>
|
||||
<span>"移动/嵌入到..."</span>
|
||||
<span class="block-drag-menu-shortcut">"Alt+Shift+M/G"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-history"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
set_command_feedback.set("块历史入口已记录,恢复流程后续接版本链路".to_string());
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"◴"</span>
|
||||
<span>"块历史..."</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-comment"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
set_command_feedback.set("评论入口已打开".to_string());
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"◉"</span>
|
||||
<span>"评论"</span>
|
||||
<span class="block-drag-menu-shortcut">"Ctrl+Alt+M"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-color"
|
||||
on:mouseenter=move |_| {
|
||||
set_command_feedback.set("颜色子菜单后续按 Wolai 继续补齐".to_string());
|
||||
}
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
set_command_feedback.set("颜色子菜单后续按 Wolai 继续补齐".to_string());
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"▧"</span>
|
||||
<span>"颜色"</span>
|
||||
<span class="block-drag-menu-arrow">"›"</span>
|
||||
</button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-drag-menu-item-align-center"
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
let result = (|| -> Result<(), String> {
|
||||
let document = editor
|
||||
.get_json()
|
||||
.map_err(|err| format!("读取当前 JSON 失败:{err}"))?;
|
||||
let range = top_level_block_range(&document, block_index)
|
||||
.ok_or_else(|| format!("找不到第 {} 个块的选择范围", block_index + 1))?;
|
||||
editor
|
||||
.set_text_selection(range)
|
||||
.map_err(|err| format!("选中当前块失败:{err}"))?;
|
||||
apply_text_align(editor, TiptapTextAlign::Center)?;
|
||||
let (html, snapshot, json_text) = read_editor_snapshot(editor);
|
||||
set_dirty_count.update(|count| *count += 1);
|
||||
set_html_output.set(html.clone());
|
||||
set_document_json.set(snapshot.clone());
|
||||
set_json_output.set(json_text);
|
||||
persist_document_state(
|
||||
&runtime_persisted_identity(document_id, workspace_id),
|
||||
&title.get_untracked(),
|
||||
&snapshot,
|
||||
Some(html),
|
||||
)
|
||||
.map_err(|err| format!("文字居中已执行,但本地保存失败:{err}"))?;
|
||||
Ok(())
|
||||
})();
|
||||
match result {
|
||||
Ok(()) => set_command_feedback.set("已切到居中,并已写入本地草稿".to_string()),
|
||||
Err(err) => set_command_feedback.set(err),
|
||||
}
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"≡"</span>
|
||||
<span>"文字居中"</span>
|
||||
<span class="block-drag-menu-switch"></span>
|
||||
</button>
|
||||
<button class="block-drag-menu-item" data-testid="block-drag-menu-item-translate" disabled=true>
|
||||
<span class="block-drag-menu-icon">"文"</span>
|
||||
<span>"文字翻译"</span>
|
||||
<span class="block-drag-menu-arrow">"›"</span>
|
||||
</button>
|
||||
<button class="block-drag-menu-item" data-testid="block-drag-menu-item-poster" disabled=true>
|
||||
<span class="block-drag-menu-icon">"▤"</span>
|
||||
<span>"生成海报"</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="block-drag-menu-footer" data-testid="block-drag-menu-footer">
|
||||
<span>"anonymous"</span>
|
||||
<span>"最后编辑于 刚刚"</span>
|
||||
</div>
|
||||
{move || {
|
||||
if block_turn_into_open.get() {
|
||||
view! {
|
||||
<div class="block-drag-submenu" data-testid="block-transform-submenu" data-e30-testid="block-turn-into-menu">
|
||||
{SLASH_ACTIONS
|
||||
.iter()
|
||||
.filter(|action| !matches!(action.kind, SlashActionKind::Divider | SlashActionKind::SimpleTable | SlashActionKind::Image | SlashActionKind::UploadAttachment | SlashActionKind::Toc | SlashActionKind::AiAssistant | SlashActionKind::AiWrite | SlashActionKind::ContinueWriting | SlashActionKind::Summarize | SlashActionKind::MoreAi))
|
||||
.map(|action| {
|
||||
let kind = action.kind;
|
||||
let label = action.label;
|
||||
let icon = action.icon;
|
||||
let is_current = top_level_block_matches_action(editor, block_index, action.kind);
|
||||
let shortcut = if is_current {
|
||||
"✓"
|
||||
} else {
|
||||
match action.kind {
|
||||
SlashActionKind::Paragraph => "Ctrl+Alt+0",
|
||||
SlashActionKind::Todo => "Ctrl+Shift+5",
|
||||
SlashActionKind::Heading1 => "Ctrl+Shift+1",
|
||||
SlashActionKind::Heading2 => "Ctrl+Shift+2",
|
||||
SlashActionKind::Heading3 => "Ctrl+Shift+3",
|
||||
SlashActionKind::BulletList => "Ctrl+Shift+6",
|
||||
SlashActionKind::OrderedList => "Ctrl+Shift+7",
|
||||
SlashActionKind::CodeBlock => "Ctrl+Alt+-",
|
||||
SlashActionKind::Mindmap => "/dt",
|
||||
_ => "",
|
||||
}
|
||||
};
|
||||
let testid = format!("block-transform-item-{}", action.id);
|
||||
let action_menu_label = "块".to_string();
|
||||
view! {
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid=testid
|
||||
data-current=is_current.to_string()
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
match run_block_turn_into_action(editor, block_index, kind) {
|
||||
Ok(message) => {
|
||||
set_block_menu_open.set(false);
|
||||
set_block_menu_anchor.set(None);
|
||||
set_block_turn_into_open.set(false);
|
||||
set_block_folded_title_open.set(false);
|
||||
let (html, snapshot, json_text) = read_editor_snapshot(editor);
|
||||
set_dirty_count.update(|count| *count += 1);
|
||||
set_html_output.set(html.clone());
|
||||
set_document_json.set(snapshot.clone());
|
||||
set_json_output.set(json_text);
|
||||
match persist_document_state(
|
||||
&runtime_persisted_identity(document_id, workspace_id),
|
||||
&title.get_untracked(),
|
||||
&snapshot,
|
||||
Some(html),
|
||||
) {
|
||||
Ok(()) => set_command_feedback.set(format!(
|
||||
"{}:{} -> {},并已写入本地草稿",
|
||||
message,
|
||||
action_menu_label.clone(),
|
||||
label,
|
||||
)),
|
||||
Err(err) => set_command_feedback.set(format!(
|
||||
"{}:{} -> {},但本地保存失败:{}",
|
||||
message,
|
||||
action_menu_label.clone(),
|
||||
label,
|
||||
err,
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(err) => set_command_feedback.set(err),
|
||||
}
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">{icon}</span>
|
||||
<span>{label}</span>
|
||||
<span class="block-drag-menu-shortcut">{shortcut}</span>
|
||||
</button>
|
||||
}
|
||||
})
|
||||
.collect_view()}
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-transform-item-page"
|
||||
data-current=current_block_is_page.to_string()
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
run_block_turn_into_page_action(
|
||||
editor,
|
||||
block_index,
|
||||
document_id,
|
||||
workspace_id,
|
||||
title,
|
||||
set_dirty_count,
|
||||
set_html_output,
|
||||
set_document_json,
|
||||
set_json_output,
|
||||
set_command_feedback,
|
||||
set_block_menu_open,
|
||||
set_block_menu_anchor,
|
||||
set_block_turn_into_open,
|
||||
);
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"▣"</span>
|
||||
<span>"页面"</span>
|
||||
<span class="block-drag-menu-shortcut">{page_current_marker}</span>
|
||||
</button>
|
||||
<button class="block-drag-menu-item" data-testid="block-transform-item-folded-list" disabled=true><span class="block-drag-menu-icon">"≡"</span><span>"折叠列表"</span><span class="block-drag-menu-shortcut">"Ctrl+Shift+8"</span></button>
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid="block-transform-item-folded-title"
|
||||
on:mouseenter=move |_| {
|
||||
set_block_folded_title_open.set(true);
|
||||
}
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
set_block_folded_title_open.set(true);
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">"H1"</span>
|
||||
<span>"折叠标题"</span>
|
||||
<span class="block-drag-menu-arrow">"›"</span>
|
||||
</button>
|
||||
<button class="block-drag-menu-item" data-testid="block-transform-item-folded-todo" disabled=true><span class="block-drag-menu-icon">"☑"</span><span>"折叠待办"</span><span class="block-drag-menu-arrow">"›"</span></button>
|
||||
<button class="block-drag-menu-item" data-testid="block-transform-item-emphasis" disabled=true><span class="block-drag-menu-icon">"Aa"</span><span>"着重文字"</span></button>
|
||||
<button class="block-drag-menu-item" data-testid="block-transform-item-math" disabled=true><span class="block-drag-menu-icon">"f(x)"</span><span>"数学公式"</span><span class="block-drag-menu-shortcut">"Ctrl+Alt++"</span></button>
|
||||
{move || {
|
||||
if block_folded_title_open.get() {
|
||||
view! {
|
||||
<div class="block-drag-tertiary-submenu" data-testid="block-transform-folded-title-submenu">
|
||||
{FOLDED_HEADING_ACTIONS
|
||||
.iter()
|
||||
.map(|folded| {
|
||||
let action = *folded;
|
||||
let testid = format!("block-transform-folded-heading-{}", action.id);
|
||||
view! {
|
||||
<button
|
||||
class="block-drag-menu-item"
|
||||
data-testid=testid
|
||||
on:click=move |event: MouseEvent| {
|
||||
event.stop_propagation();
|
||||
match run_block_turn_into_folded_heading_action(editor, block_index, action.level) {
|
||||
Ok(message) => {
|
||||
set_block_menu_open.set(false);
|
||||
set_block_menu_anchor.set(None);
|
||||
set_block_turn_into_open.set(false);
|
||||
set_block_folded_title_open.set(false);
|
||||
let (html, snapshot, json_text) = read_editor_snapshot(editor);
|
||||
set_dirty_count.update(|count| *count += 1);
|
||||
set_html_output.set(html.clone());
|
||||
set_document_json.set(snapshot.clone());
|
||||
set_json_output.set(json_text);
|
||||
match persist_document_state(
|
||||
&runtime_persisted_identity(document_id, workspace_id),
|
||||
&title.get_untracked(),
|
||||
&snapshot,
|
||||
Some(html),
|
||||
) {
|
||||
Ok(()) => set_command_feedback.set(format!("{},并已写入本地草稿", message)),
|
||||
Err(err) => set_command_feedback.set(format!("{},但本地保存失败:{}", message, err)),
|
||||
}
|
||||
}
|
||||
Err(err) => set_command_feedback.set(err),
|
||||
}
|
||||
}
|
||||
>
|
||||
<span class="block-drag-menu-icon">{action.icon}</span>
|
||||
<span>{action.label}</span>
|
||||
</button>
|
||||
}
|
||||
})
|
||||
.collect_view()}
|
||||
</div>
|
||||
}.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
}.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
}}
|
||||
</>
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
}.into_any()
|
||||
block_handle_menu_view(BlockHandleMenuViewProps {
|
||||
editor,
|
||||
block: block.clone(),
|
||||
duplicate_block_label: duplicate_block_label.clone(),
|
||||
delete_block_label: delete_block_label.clone(),
|
||||
menu_layout,
|
||||
signals: BlockHandleMenuSignals {
|
||||
document_id,
|
||||
workspace_id,
|
||||
title,
|
||||
selection_state,
|
||||
block_turn_into_open,
|
||||
block_folded_title_open,
|
||||
set_ai_bridge_state,
|
||||
set_ai_bridge_message,
|
||||
set_dirty_count,
|
||||
set_html_output,
|
||||
set_document_json,
|
||||
set_json_output,
|
||||
set_command_feedback,
|
||||
set_block_menu_open,
|
||||
set_block_menu_anchor,
|
||||
set_block_turn_into_open,
|
||||
set_block_folded_title_open,
|
||||
set_hovered_block,
|
||||
},
|
||||
}).into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user