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:
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user