refactor: extract attachment link history commands
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{window, Element};
|
||||
|
||||
const EDITOR_ROOT_SELECTOR: &str = ".editor-surface .ProseMirror";
|
||||
|
||||
fn editor_root_element() -> Option<Element> {
|
||||
window()
|
||||
.and_then(|win| win.document())
|
||||
.and_then(|document| document.query_selector(EDITOR_ROOT_SELECTOR).ok().flatten())
|
||||
}
|
||||
|
||||
/// 当前 DOM 选中文本匹配本地附件链接文本时返回 `true`。
|
||||
pub(crate) fn selected_local_attachment_link_text() -> bool {
|
||||
let selection = match window().and_then(|win| win.get_selection().ok().flatten()) {
|
||||
Some(selection) if !selection.is_collapsed() => selection,
|
||||
_ => return false,
|
||||
};
|
||||
let selected_text = String::from(selection.to_string()).trim().to_string();
|
||||
if selected_text.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(root) = editor_root_element() else {
|
||||
return false;
|
||||
};
|
||||
let links = root.get_elements_by_tag_name("a");
|
||||
for index in 0..links.length() {
|
||||
let Some(link) = links.item(index) else {
|
||||
continue;
|
||||
};
|
||||
let href = link.get_attribute("href").unwrap_or_default();
|
||||
if !href.contains("/api/local-folder/files/open") {
|
||||
continue;
|
||||
}
|
||||
if link.text_content().unwrap_or_default().trim() == selected_text {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// 当前非空选区命中本地附件链接 mark 时返回 `true`。
|
||||
pub(crate) fn selected_local_attachment_link_in_editor_state() -> bool {
|
||||
let Some(root) = editor_root_element() else {
|
||||
return false;
|
||||
};
|
||||
let Ok(editor_value) = js_sys::Reflect::get(root.as_ref(), &JsValue::from_str("editor"))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Ok(state_value) = js_sys::Reflect::get(&editor_value, &JsValue::from_str("state")) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(selection_value) = js_sys::Reflect::get(&state_value, &JsValue::from_str("selection"))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let is_empty = js_sys::Reflect::get(&selection_value, &JsValue::from_str("empty"))
|
||||
.ok()
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(true);
|
||||
if is_empty {
|
||||
return false;
|
||||
}
|
||||
let Some(get_attributes) = js_sys::Reflect::get(
|
||||
&editor_value,
|
||||
&JsValue::from_str("getAttributes"),
|
||||
)
|
||||
.ok()
|
||||
.and_then(|value| value.dyn_into::<js_sys::Function>().ok())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Ok(attributes) = get_attributes.call1(&editor_value, &JsValue::from_str("link")) else {
|
||||
return false;
|
||||
};
|
||||
js_sys::Reflect::get(&attributes, &JsValue::from_str("href"))
|
||||
.ok()
|
||||
.and_then(|value| value.as_string())
|
||||
.is_some_and(|href| href.contains("/api/local-folder/files/open"))
|
||||
}
|
||||
|
||||
/// DOM selection 或 Tiptap editor state 任一路径命中本地附件链接即可。
|
||||
pub(crate) fn is_local_attachment_link_selected() -> bool {
|
||||
selected_local_attachment_link_in_editor_state() || selected_local_attachment_link_text()
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use leptos_tiptap::TiptapEditorHandle;
|
||||
|
||||
use crate::editor_runtime::attachment_links;
|
||||
|
||||
/// 通过 Tiptap `delete_selection()` 删除当前选中的本地附件链接,保留撤销/重做历史。
|
||||
///
|
||||
/// 删除成功返回 `Ok(())`;当前选区不是本地附件链接或删除失败时返回 `Err(message)`。
|
||||
pub(crate) fn delete_selected_attachment_link_with_history(
|
||||
editor: &TiptapEditorHandle,
|
||||
) -> Result<(), String> {
|
||||
// 先确认当前选区确实是本地附件链接。
|
||||
if !attachment_links::is_local_attachment_link_selected() {
|
||||
return Err("当前选区不是本地附件链接".to_string());
|
||||
}
|
||||
// 继续走 Tiptap 命令路径,保留撤销/重做历史。
|
||||
editor.delete_selection().map_err(|error| error.to_string())
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod attachment_links;
|
||||
pub(crate) mod history_safe_commands;
|
||||
@@ -54,6 +54,9 @@ const STANDALONE_ROOT_ID: &str = "mnote-leptos-tiptap-standalone-root";
|
||||
const E24_IMAGE_PLACEHOLDER_SRC: &str = "/api/editor/image-placeholder.svg";
|
||||
const E24_IMAGE_PLACEHOLDER_ALT: &str = "E24 图片占位";
|
||||
const E24_IMAGE_PLACEHOLDER_TITLE: &str = "E24 图片";
|
||||
|
||||
mod editor_runtime;
|
||||
|
||||
#[wasm_bindgen(inline_js = r#"
|
||||
export function write_mnote_text_to_clipboard(text) {
|
||||
try {
|
||||
@@ -5262,73 +5265,6 @@ fn hovered_block_from_selection() -> Option<HoveredBlockState> {
|
||||
block_state_from_index(index)
|
||||
}
|
||||
|
||||
fn selected_local_attachment_link_text() -> bool {
|
||||
let selection = match window().and_then(|win| win.get_selection().ok().flatten()) {
|
||||
Some(selection) if !selection.is_collapsed() => selection,
|
||||
_ => return false,
|
||||
};
|
||||
let selected_text = String::from(selection.to_string()).trim().to_string();
|
||||
if selected_text.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(root) = editor_root_element() else {
|
||||
return false;
|
||||
};
|
||||
let links = root.get_elements_by_tag_name("a");
|
||||
for index in 0..links.length() {
|
||||
let Some(link) = links.item(index) else {
|
||||
continue;
|
||||
};
|
||||
let href = link.get_attribute("href").unwrap_or_default();
|
||||
if !href.contains("/api/local-folder/files/open") {
|
||||
continue;
|
||||
}
|
||||
if link.text_content().unwrap_or_default().trim() == selected_text {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn selected_local_attachment_link_in_editor_state() -> bool {
|
||||
let Some(root) = editor_root_element() else {
|
||||
return false;
|
||||
};
|
||||
let Ok(editor_value) = js_sys::Reflect::get(root.as_ref(), &JsValue::from_str("editor"))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Ok(state_value) = js_sys::Reflect::get(&editor_value, &JsValue::from_str("state")) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(selection_value) = js_sys::Reflect::get(&state_value, &JsValue::from_str("selection"))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let is_empty = js_sys::Reflect::get(&selection_value, &JsValue::from_str("empty"))
|
||||
.ok()
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(true);
|
||||
if is_empty {
|
||||
return false;
|
||||
}
|
||||
let Some(get_attributes) = js_sys::Reflect::get(&editor_value, &JsValue::from_str("getAttributes"))
|
||||
.ok()
|
||||
.and_then(|value| value.dyn_into::<js_sys::Function>().ok())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Ok(attributes) = get_attributes.call1(&editor_value, &JsValue::from_str("link")) else {
|
||||
return false;
|
||||
};
|
||||
js_sys::Reflect::get(&attributes, &JsValue::from_str("href"))
|
||||
.ok()
|
||||
.and_then(|value| value.as_string())
|
||||
.is_some_and(|href| href.contains("/api/local-folder/files/open"))
|
||||
}
|
||||
|
||||
fn block_state_from_index(index: usize) -> Option<HoveredBlockState> {
|
||||
let root = editor_root_element()?;
|
||||
let stage = editor_stage_element()?;
|
||||
@@ -8821,15 +8757,16 @@ fn App(mount_options: MountOptions) -> impl IntoView {
|
||||
|| event.meta_key()
|
||||
|| event.alt_key()
|
||||
|| !matches!(event.key().as_str(), "Backspace" | "Delete")
|
||||
|| !(selected_local_attachment_link_in_editor_state()
|
||||
|| selected_local_attachment_link_text())
|
||||
|| !editor_runtime::attachment_links::is_local_attachment_link_selected()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
event.prevent_default();
|
||||
event.stop_immediate_propagation();
|
||||
match editor.delete_selection() {
|
||||
match editor_runtime::history_safe_commands::delete_selected_attachment_link_with_history(
|
||||
&editor,
|
||||
) {
|
||||
Ok(()) => {
|
||||
sync_persisted_editor_command(
|
||||
editor,
|
||||
|
||||
Reference in New Issue
Block a user