fix: preserve undo for attachment link deletion

This commit is contained in:
lix-2026
2026-05-24 19:55:37 +08:00
parent 9c4bb58257
commit cafd29a963
9 changed files with 401 additions and 82 deletions
+1
View File
@@ -975,6 +975,7 @@ dependencies = [
"leptos",
"leptos-tiptap",
"leptos_dom",
"send_wrapper",
"serde",
"serde-wasm-bindgen",
"serde_json",
@@ -20,6 +20,7 @@ leptos_dom = "0.8.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde-wasm-bindgen = "0.6"
send_wrapper = "0.6"
js-sys = "0.3.77"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
@@ -71,6 +71,7 @@ export interface InitOutput {
readonly wasm_bindgen__convert__closures_____invoke__had771ddc65647798: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c: (a: number, b: number) => void;
@@ -297,6 +297,9 @@ function __wbg_get_imports() {
const ret = arg0.activeElement;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
},
__wbg_addEventListener_2ed1344165a839a7: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
arg0.addEventListener(getStringFromWasm0(arg1, arg2), arg3, arg4 !== 0);
}, arguments); },
__wbg_addEventListener_8176dab41b09531c: function() { return handleError(function (arg0, arg1, arg2, arg3) {
arg0.addEventListener(getStringFromWasm0(arg1, arg2), arg3);
}, arguments); },
@@ -1022,6 +1025,9 @@ function __wbg_get_imports() {
const ret = arg0.removeChild(arg1);
return ret;
}, arguments); },
__wbg_removeEventListener_0069d57d090a1674: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
arg0.removeEventListener(getStringFromWasm0(arg1, arg2), arg3, arg4 !== 0);
}, arguments); },
__wbg_removeEventListener_7bdf07404d9b24bd: function() { return handleError(function (arg0, arg1, arg2, arg3) {
arg0.removeEventListener(getStringFromWasm0(arg1, arg2), arg3);
}, arguments); },
@@ -1169,6 +1175,9 @@ function __wbg_get_imports() {
const ret = arg0.status;
return ret;
},
__wbg_stopImmediatePropagation_2228838acf177dc6: function(arg0) {
arg0.stopImmediatePropagation();
},
__wbg_stopPropagation_e088fca8231e68c4: function(arg0) {
arg0.stopPropagation();
},
@@ -1242,61 +1251,66 @@ function __wbg_get_imports() {
}
}, arguments); },
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1128, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1134, 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: 1180, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1186, 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: 956, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 962, 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: 1078, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1084, 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: 1128, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1134, 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: [], shim_idx: 1080, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f);
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 297, 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: 1095, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5);
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1086, 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: 1131, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1101, 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: 1137, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c);
return ret;
},
__wbindgen_cast_0000000000000009: function(arg0) {
__wbindgen_cast_000000000000000a: function(arg0) {
// Cast intrinsic for `F64 -> Externref`.
const ret = arg0;
return ret;
},
__wbindgen_cast_000000000000000a: function(arg0) {
__wbindgen_cast_000000000000000b: function(arg0) {
// Cast intrinsic for `I64 -> Externref`.
const ret = arg0;
return ret;
},
__wbindgen_cast_000000000000000b: function(arg0, arg1) {
__wbindgen_cast_000000000000000c: function(arg0, arg1) {
// Cast intrinsic for `Ref(String) -> Externref`.
const ret = getStringFromWasm0(arg0, arg1);
return ret;
},
__wbindgen_cast_000000000000000c: function(arg0) {
__wbindgen_cast_000000000000000d: function(arg0) {
// Cast intrinsic for `U64 -> Externref`.
const ret = BigInt.asUintN(64, arg0);
return ret;
@@ -1349,6 +1363,10 @@ function wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4(arg0, a
wasm.wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4(arg0, arg1, arg2);
}
function wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf(arg0, arg1, arg2);
}
function wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12(arg0, arg1, arg2) {
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12(arg0, arg1, arg2);
if (ret[1]) {
@@ -24,6 +24,7 @@ export const wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5: (a:
export const wasm_bindgen__convert__closures_____invoke__had771ddc65647798: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f: (a: number, b: number) => void;
export const wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5: (a: number, b: number) => void;
export const wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c: (a: number, b: number) => void;
+104 -68
View File
@@ -9,6 +9,7 @@ use leptos_tiptap::{
TiptapNodeName, TiptapRange, TiptapSchemaTarget, TiptapSelectionState, TiptapTextAlign,
TiptapTocNodeAttrs,
};
use send_wrapper::SendWrapper;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use std::{any::Any, cell::Cell, collections::HashMap, fmt::Display};
@@ -5261,14 +5262,19 @@ fn hovered_block_from_selection() -> Option<HoveredBlockState> {
block_state_from_index(index)
}
fn selected_local_attachment_block_index() -> Option<usize> {
let selection = window().and_then(|win| win.get_selection().ok().flatten())?;
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 None;
return false;
}
let root = editor_root_element()?;
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 {
@@ -5278,15 +5284,49 @@ fn selected_local_attachment_block_index() -> Option<usize> {
if !href.contains("/api/local-folder/files/open") {
continue;
}
let text = link.text_content().unwrap_or_default();
if text.trim() != selected_text {
continue;
if link.text_content().unwrap_or_default().trim() == selected_text {
return true;
}
let block = direct_block_from_element(link, &root)?;
return top_level_block_index(&root, &block);
}
None
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> {
@@ -8736,9 +8776,7 @@ fn App(mount_options: MountOptions) -> impl IntoView {
let editor = editor;
let document_id = document_id;
let workspace_id = workspace_id;
let dirty_count = dirty_count;
let title = title;
let editor_focused = editor_focused;
let set_dirty_count = set_dirty_count;
let set_html_output = set_html_output;
let set_document_json = set_document_json;
@@ -8759,9 +8797,6 @@ fn App(mount_options: MountOptions) -> impl IntoView {
let set_color_menu_open = set_color_menu_open;
let more_menu_open = more_menu_open;
let set_more_menu_open = set_more_menu_open;
let revision = revision;
let conflict_detection_key = conflict_detection_key;
let read_only = read_only;
let set_locked_toolbar_anchor = set_locked_toolbar_anchor;
let set_text_selection_active = set_text_selection_active;
let set_floating_toolbar_anchor = set_floating_toolbar_anchor;
@@ -8779,65 +8814,66 @@ fn App(mount_options: MountOptions) -> impl IntoView {
let set_block_turn_into_open = set_block_turn_into_open;
let set_block_folded_title_open = set_block_folded_title_open;
let set_command_feedback = set_command_feedback;
let keydown_capture_closure =
SendWrapper::new(Closure::wrap(Box::new(move |event: ev::KeyboardEvent| {
if !active_editor_stage()
|| event.ctrl_key()
|| 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())
{
return;
}
event.prevent_default();
event.stop_immediate_propagation();
match editor.delete_selection() {
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,
"已删除附件引用",
);
let _ = set_hovered_block.try_set(None);
let _ = set_block_menu_open.try_set(false);
let _ = set_block_menu_anchor.try_set(None);
}
Err(err) => {
let _ = set_command_feedback.try_set(format!("删除附件引用失败:{err}"));
}
}
}) as Box<dyn FnMut(_)>));
if let Some(win) = window() {
let _ = win.add_event_listener_with_callback_and_bool(
"keydown",
keydown_capture_closure.as_ref().unchecked_ref(),
true,
);
}
on_cleanup(move || {
if let Some(win) = window() {
let _ = win.remove_event_listener_with_callback_and_bool(
"keydown",
keydown_capture_closure.as_ref().unchecked_ref(),
true,
);
}
drop(keydown_capture_closure);
});
let keydown_handle = window_event_listener(ev::keydown, move |event| {
let focused = active_editor_stage();
if !focused {
return;
}
if !event.ctrl_key()
&& !event.meta_key()
&& !event.alt_key()
&& matches!(event.key().as_str(), "Backspace" | "Delete")
{
if let Some(block_index) = selected_local_attachment_block_index() {
event.prevent_default();
match editor.get_html() {
Ok(current_html) => {
match delete_top_level_block_html(&current_html, block_index) {
Ok(next_html) => {
apply_html_update(
editor,
&runtime_persisted_identity(document_id, workspace_id),
next_html,
document_id,
workspace_id,
dirty_count,
set_dirty_count,
set_html_output,
set_document_json,
set_json_output,
title,
hovered_block,
editor_focused,
slash_open,
turn_into_open,
color_menu_open,
more_menu_open,
revision,
conflict_detection_key,
read_only,
set_command_feedback,
"已删除附件引用",
);
let _ = set_hovered_block.try_set(None);
let _ = set_block_menu_open.try_set(false);
let _ = set_block_menu_anchor.try_set(None);
}
Err(err) => {
let _ = set_command_feedback.try_set(err);
}
}
}
Err(err) => {
let _ =
set_command_feedback.try_set(format!("读取当前 HTML 失败:{err}"));
}
}
return;
}
}
if (event.ctrl_key() || event.meta_key())
&& event.shift_key()
&& event.key().eq_ignore_ascii_case("u")