refactor: externalize local upload helpers

Add local-upload-runtime.js as a browser runtime module for upload and attachment helper functions, with sidebar fallback wrappers and a fixed runtime asset route.

While validating task479, fix the editor attachment delete path so DOM-selected local attachment links are mapped back to a Tiptap text selection before delete_selection(), preserving undo history and adjacent links.
This commit is contained in:
lix-2026
2026-05-24 22:42:53 +08:00
parent 418f8ff8b0
commit 0a03572601
10 changed files with 353 additions and 19 deletions
@@ -360,6 +360,10 @@ function __wbg_get_imports() {
const ret = arg0.call(arg1);
return ret;
}, arguments); },
__wbg_call_86e39d65afc3d9db: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
const ret = arg0.call(arg1, arg2, arg3, arg4);
return ret;
}, arguments); },
__wbg_call_a24592a6f349a97e: function() { return handleError(function (arg0, arg1, arg2) {
const ret = arg0.call(arg1, arg2);
return ret;
@@ -846,6 +850,10 @@ function __wbg_get_imports() {
state0.a = 0;
}
},
__wbg_new_with_args_57b505df22335acd: function(arg0, arg1, arg2, arg3) {
const ret = new Function(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3));
return ret;
},
__wbg_new_with_byte_offset_and_length_01848e8d6a3d49ad: function(arg0, arg1, arg2) {
const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0);
return ret;
@@ -1251,47 +1259,47 @@ function __wbg_get_imports() {
}
}, arguments); },
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1136, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1135, 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: 1188, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1187, 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: 964, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 963, 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: 1086, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1085, 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: 1136, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1135, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4);
return ret;
},
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 297, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 745, 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: 1088, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1087, 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: 1103, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1102, 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: 1139, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1138, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c);
return ret;
},
@@ -1,3 +1,4 @@
use leptos_tiptap::TiptapRange;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{window, Element};
@@ -9,20 +10,17 @@ fn editor_root_element() -> Option<Element> {
.and_then(|document| document.query_selector(EDITOR_ROOT_SELECTOR).ok().flatten())
}
/// 当前 DOM 选中文本匹配本地附件链接文本时返回 `true`。
pub(crate) fn selected_local_attachment_link_text() -> bool {
fn selected_local_attachment_link_dom_info() -> Option<(String, String)> {
let selection = match window().and_then(|win| win.get_selection().ok().flatten()) {
Some(selection) if !selection.is_collapsed() => selection,
_ => return false,
_ => return None,
};
let selected_text = String::from(selection.to_string()).trim().to_string();
if selected_text.is_empty() {
return false;
return None;
}
let Some(root) = editor_root_element() else {
return false;
};
let root = editor_root_element()?;
let links = root.get_elements_by_tag_name("a");
for index in 0..links.length() {
let Some(link) = links.item(index) else {
@@ -33,11 +31,16 @@ pub(crate) fn selected_local_attachment_link_text() -> bool {
continue;
}
if link.text_content().unwrap_or_default().trim() == selected_text {
return true;
return Some((selected_text, href));
}
}
false
None
}
/// 当前 DOM 选中文本匹配本地附件链接文本时返回 `true`。
pub(crate) fn selected_local_attachment_link_text() -> bool {
selected_local_attachment_link_dom_info().is_some()
}
/// 当前非空选区命中本地附件链接 mark 时返回 `true`。
@@ -85,3 +88,54 @@ pub(crate) fn selected_local_attachment_link_in_editor_state() -> bool {
pub(crate) fn is_local_attachment_link_selected() -> bool {
selected_local_attachment_link_in_editor_state() || selected_local_attachment_link_text()
}
pub(crate) fn selected_local_attachment_link_range() -> Option<TiptapRange> {
let (selected_text, selected_href) = selected_local_attachment_link_dom_info()?;
let root = editor_root_element()?;
let editor_value = js_sys::Reflect::get(root.as_ref(), &JsValue::from_str("editor")).ok()?;
let find_range = js_sys::Function::new_with_args(
"editor, selectedText, selectedHref",
r#"
if (!editor || !editor.state || !editor.state.doc) return null;
var text = String(selectedText || '').trim();
var hrefNeedle = String(selectedHref || '').trim();
if (!text) return null;
var match = null;
editor.state.doc.descendants(function(node, pos) {
if (match || !node || !node.isText) return;
var nodeText = String(node.text || '');
var offset = nodeText.indexOf(text);
if (offset < 0) return;
var hasAttachmentLink = Array.isArray(node.marks) && node.marks.some(function(mark) {
var href = mark && mark.attrs ? String(mark.attrs.href || '') : '';
return mark.type && mark.type.name === 'link'
&& href.indexOf('/api/local-folder/files/open') >= 0
&& (!hrefNeedle || href === hrefNeedle);
});
if (!hasAttachmentLink) return;
match = { from: pos + offset, to: pos + offset + text.length };
});
return match;
"#,
);
let range_value = find_range
.call3(
&JsValue::NULL,
&editor_value,
&JsValue::from_str(&selected_text),
&JsValue::from_str(&selected_href),
)
.ok()?;
if range_value.is_null() || range_value.is_undefined() {
return None;
}
let from = js_sys::Reflect::get(&range_value, &JsValue::from_str("from"))
.ok()
.and_then(|value| value.as_f64())
.filter(|value| value.is_finite() && *value >= 0.0)? as u32;
let to = js_sys::Reflect::get(&range_value, &JsValue::from_str("to"))
.ok()
.and_then(|value| value.as_f64())
.filter(|value| value.is_finite() && *value >= from as f64)? as u32;
Some(TiptapRange { from, to })
}
@@ -12,6 +12,9 @@ pub(crate) fn delete_selected_attachment_link_with_history(
if !attachment_links::is_local_attachment_link_selected() {
return Err("当前选区不是本地附件链接".to_string());
}
if let Some(range) = attachment_links::selected_local_attachment_link_range() {
let _ = editor.set_text_selection(range);
}
// 继续走 Tiptap 命令路径,保留撤销/重做历史。
editor.delete_selection().map_err(|error| error.to_string())
}