fix editor page transform true source

This commit is contained in:
lix-2026
2026-04-30 22:45:43 +08:00
parent c7cfe85d87
commit 015ad5bedd
6 changed files with 278 additions and 37 deletions
+1
View File
@@ -979,6 +979,7 @@ dependencies = [
"serde-wasm-bindgen",
"serde_json",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
+2 -1
View File
@@ -22,7 +22,8 @@ serde_json = "1"
serde-wasm-bindgen = "0.6"
js-sys = "0.3.77"
wasm-bindgen = "0.2"
web-sys = { version = "0.3.77", features = ["CustomEvent", "CustomEventInit", "DataTransfer", "Document", "DomRect", "DragEvent", "Element", "EventTarget", "HtmlElement", "MessageEvent", "MouseEvent", "Node", "Range", "Selection", "Storage", "Window"] }
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3.77", features = ["CustomEvent", "CustomEventInit", "DataTransfer", "Document", "DomRect", "DragEvent", "Element", "EventTarget", "HtmlElement", "MessageEvent", "MouseEvent", "Node", "Range", "RequestInit", "RequestMode", "Response", "Selection", "Storage", "Window"] }
[dependencies.leptos-tiptap]
path = "../../../design/05-editor-mainline/reference-code/leptos-tiptap"
@@ -284,6 +284,9 @@ function __wbg_get_imports() {
const ret = arg0.appendChild(arg1);
return ret;
}, arguments); },
__wbg_assign_d4fed0f8abb71719: function() { return handleError(function (arg0, arg1, arg2) {
arg0.assign(getStringFromWasm0(arg1, arg2));
}, arguments); },
__wbg_body_c7b35a55457167ba: function(arg0) {
const ret = arg0.body;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
@@ -461,6 +464,10 @@ function __wbg_get_imports() {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
},
__wbg_fetch_3f6f4a922f97ca32: function(arg0, arg1, arg2, arg3) {
const ret = arg0.fetch(getStringFromWasm0(arg1, arg2), arg3);
return ret;
},
__wbg_firstElementChild_f67647a589d437a2: function(arg0) {
const ret = arg0.firstElementChild;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
@@ -601,6 +608,16 @@ function __wbg_get_imports() {
const ret = result;
return ret;
},
__wbg_instanceof_Response_9b2d111407865ff2: function(arg0) {
let result;
try {
result = arg0 instanceof Response;
} catch (_) {
result = false;
}
const ret = result;
return ret;
},
__wbg_instanceof_ShadowRoot_d26d95cd2363a2c1: function(arg0) {
let result;
try {
@@ -659,6 +676,10 @@ function __wbg_get_imports() {
const ret = Symbol.iterator;
return ret;
},
__wbg_json_cc3f4ccd040a504c: function() { return handleError(function (arg0) {
const ret = arg0.json();
return ret;
}, arguments); },
__wbg_key_2cbc38fa83cdb336: function(arg0, arg1) {
const ret = arg1.key;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
@@ -752,6 +773,10 @@ function __wbg_get_imports() {
const ret = arg0.next;
return ret;
},
__wbg_ok_f7783a2e6ac7fe17: function(arg0) {
const ret = arg0.ok;
return ret;
},
__wbg_parentElement_d1271cca94202d1f: function(arg0) {
const ret = arg0.parentElement;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
@@ -963,6 +988,9 @@ function __wbg_get_imports() {
__wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
arg0[arg1] = arg2;
},
__wbg_set_body_be11680f34217f75: function(arg0, arg1) {
arg0.body = arg1;
},
__wbg_set_detail_68bec5c91196ba57: function(arg0, arg1) {
arg0.detail = arg1;
},
@@ -973,6 +1001,12 @@ function __wbg_get_imports() {
__wbg_set_innerHTML_a3c82996073b31ea: function(arg0, arg1, arg2) {
arg0.innerHTML = getStringFromWasm0(arg1, arg2);
},
__wbg_set_method_c9f1f985f6b6c427: function(arg0, arg1, arg2) {
arg0.method = getStringFromWasm0(arg1, arg2);
},
__wbg_set_mode_5e08d503428c06b9: function(arg0, arg1) {
arg0.mode = __wbindgen_enum_RequestMode[arg1];
},
__wbg_set_nodeValue_f39ed00fc286b285: function(arg0, arg1, arg2) {
arg0.nodeValue = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
},
@@ -1006,6 +1040,10 @@ function __wbg_get_imports() {
const ret = typeof window === 'undefined' ? null : window;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
},
__wbg_status_43e0d2f15b22d69f: function(arg0) {
const ret = arg0.status;
return ret;
},
__wbg_stopPropagation_e088fca8231e68c4: function(arg0) {
arg0.stopPropagation();
},
@@ -1024,6 +1062,10 @@ function __wbg_get_imports() {
const ret = arg0.target;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
},
__wbg_then_792e0c862b060889: function(arg0, arg1, arg2) {
const ret = arg0.then(arg1, arg2);
return ret;
},
__wbg_then_8e16ee11f05e4827: function(arg0, arg1) {
const ret = arg0.then(arg1);
return ret;
@@ -1058,37 +1100,37 @@ function __wbg_get_imports() {
return ret;
},
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1111, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1120, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h1ea2b89742286ce6);
return ret;
},
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1366, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1382, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2593b18d2d6f8a75);
return ret;
},
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1457, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1473, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h36e788bd1cd1f0f7);
return ret;
},
__wbindgen_cast_0000000000000004: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1292, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1307, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h03deb55668e1377f);
return ret;
},
__wbindgen_cast_0000000000000005: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1368, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1384, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h837fba73fce77300);
return ret;
},
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1305, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1320, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd17f30facfdd737f);
return ret;
},
__wbindgen_cast_0000000000000007: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1367, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1383, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hf726e22cfd1ee0a9);
return ret;
},
@@ -1172,6 +1214,9 @@ function wasm_bindgen__convert__closures_____invoke__hb0ba6117a7ec12e8(arg0, arg
const __wbindgen_enum_ReadableStreamType = ["bytes"];
const __wbindgen_enum_RequestMode = ["same-origin", "no-cors", "cors", "navigate"];
const IntoUnderlyingByteSourceFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_intounderlyingbytesource_free(ptr >>> 0, 1));
+206 -24
View File
@@ -12,9 +12,10 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use std::{any::Any, cell::Cell, collections::HashMap, fmt::Display};
use wasm_bindgen::{closure::Closure, prelude::*, JsCast, JsValue};
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{
window, CustomEvent, CustomEventInit, DragEvent, Element, Event, EventTarget, HtmlElement,
MouseEvent, Node, Storage, WheelEvent,
MouseEvent, Node, RequestInit, RequestMode, Response, Storage, WheelEvent,
};
const EDITOR_STAGE_SELECTOR: &str = "#editor-stage";
@@ -4034,6 +4035,126 @@ fn normalized_page_block_title(text: &str) -> String {
}
}
#[derive(Debug, Deserialize)]
struct TreeCommandHttpResponse {
result: Option<TreeCommandResult>,
message: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TreeCommandResult {
document_id: Option<String>,
workspace_id: Option<String>,
title: Option<String>,
parent_id: Option<String>,
}
fn dispatch_tree_local_command(body: &Value, result: &Value) {
let Some(win) = window() else {
return;
};
let Ok(detail) = serde_wasm_bindgen::to_value(&json!({
"body": body,
"result": result,
})) else {
return;
};
let init = CustomEventInit::new();
init.set_detail(&detail);
if let Ok(event) = CustomEvent::new_with_event_init_dict("tree:local-command", &init) {
let _ = win.dispatch_event(&event);
}
}
async fn create_child_page_via_tree_command(
parent_id: String,
workspace_id: Option<String>,
title: String,
) -> Result<TreeCommandResult, String> {
let body = json!({
"action": "create",
"workspaceId": workspace_id,
"parentId": parent_id,
"title": title,
"accessScope": "private",
"content": [],
});
let body_string =
serde_json::to_string(&body).map_err(|err| format!("序列化创建子页面请求失败:{err}"))?;
let request_init = RequestInit::new();
request_init.set_method("POST");
request_init.set_mode(RequestMode::SameOrigin);
request_init.set_body(&JsValue::from_str(&body_string));
let headers = js_sys::Object::new();
js_sys::Reflect::set(
&headers,
&JsValue::from_str("content-type"),
&JsValue::from_str("application/json"),
)
.map_err(|_| "设置创建子页面请求头失败".to_string())?;
js_sys::Reflect::set(
request_init.as_ref(),
&JsValue::from_str("headers"),
&headers,
)
.map_err(|_| "设置创建子页面请求头失败".to_string())?;
let win = window().ok_or_else(|| "当前浏览器窗口不可用".to_string())?;
let response_value =
JsFuture::from(win.fetch_with_str_and_init("/api/tree/commands", &request_init))
.await
.map_err(|err| format!("创建子页面请求失败:{err:?}"))?;
let response: Response = response_value
.dyn_into()
.map_err(|_| "创建子页面响应类型错误".to_string())?;
let status = response.status();
let json_value = JsFuture::from(
response
.json()
.map_err(|_| "读取创建子页面响应失败".to_string())?,
)
.await
.map_err(|err| format!("解析创建子页面响应失败:{err:?}"))?;
let parsed: TreeCommandHttpResponse = serde_wasm_bindgen::from_value(json_value.clone())
.map_err(|err| format!("创建子页面响应 JSON 不符合预期:{err}"))?;
if !response.ok() {
return Err(parsed
.message
.unwrap_or_else(|| format!("创建子页面失败:HTTP {status}")));
}
let result = parsed
.result
.ok_or_else(|| "创建子页面响应缺少 result".to_string())?;
let document_id = result
.document_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "创建子页面响应缺少 documentId".to_string())?;
if document_id == parent_id {
return Err("创建子页面返回了当前页面 id".to_string());
}
if let Some(created_parent_id) = result
.parent_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
if created_parent_id != parent_id {
return Err(format!(
"创建子页面父节点不符合预期:期望 {parent_id},实际 {created_parent_id}"
));
}
}
dispatch_tree_local_command(
&body,
&serde_wasm_bindgen::from_value(json_value).unwrap_or(Value::Null)["result"],
);
Ok(result)
}
fn block_plain_text_from_index(
editor: TiptapEditorHandle,
block_index: usize,
@@ -4093,9 +4214,14 @@ fn top_level_block_is_page(editor: TiptapEditorHandle, block_index: usize) -> bo
.unwrap_or(false)
}
fn page_reference_paragraph_node(page_id: &str, title: &str) -> Value {
fn page_reference_paragraph_node(page_id: &str, workspace_id: Option<&str>, title: &str) -> Value {
let href = if page_id.trim().is_empty() {
"#".to_string()
} else if let Some(workspace_id) = workspace_id
.map(str::trim)
.filter(|value| !value.is_empty())
{
format!("/documents/{page_id}?workspaceId={workspace_id}")
} else {
format!("/documents/{page_id}")
};
@@ -4118,6 +4244,7 @@ fn replace_block_with_page_reference(
editor: TiptapEditorHandle,
block_index: usize,
page_id: &str,
workspace_id: Option<&str>,
title: &str,
) -> Result<(), String> {
let mut document = editor
@@ -4127,7 +4254,7 @@ fn replace_block_with_page_reference(
if block_index >= content.len() {
return Err(format!("找不到第 {} 个块", block_index + 1));
}
content[block_index] = page_reference_paragraph_node(page_id, title);
content[block_index] = page_reference_paragraph_node(page_id, workspace_id, title);
let next_payload = document
.get("content")
.and_then(Value::as_array)
@@ -4163,28 +4290,59 @@ fn run_block_turn_into_page_action(
return;
}
};
let page_id = document_id
.get_untracked()
.unwrap_or_else(|| format!("page-block-{}", block_index + 1));
match replace_block_with_page_reference(editor, block_index, &page_id, &page_title) {
Ok(()) => {
set_block_menu_open.set(false);
set_block_menu_anchor.set(None);
set_block_turn_into_open.set(false);
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!("已转换为页面:{page_title}"),
);
let Some(parent_document_id) = document_id.get_untracked() else {
set_command_feedback.set("转换为页面失败:当前页面缺少 documentId".to_string());
return;
};
let workspace_id_value = workspace_id.get_untracked();
set_command_feedback.set(format!("正在创建子页面:{page_title}"));
spawn_local(async move {
match create_child_page_via_tree_command(
parent_document_id.clone(),
workspace_id_value.clone(),
page_title.clone(),
)
.await
{
Ok(created) => {
let Some(child_page_id) = created.document_id.as_deref() else {
set_command_feedback.set("转换为页面失败:创建响应缺少子页面 id".to_string());
return;
};
let effective_workspace_id = created
.workspace_id
.as_deref()
.or(workspace_id_value.as_deref());
let effective_title = created.title.as_deref().unwrap_or(&page_title);
match replace_block_with_page_reference(
editor,
block_index,
child_page_id,
effective_workspace_id,
effective_title,
) {
Ok(()) => {
set_block_menu_open.set(false);
set_block_menu_anchor.set(None);
set_block_turn_into_open.set(false);
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!("已转换为子页面:{effective_title}"),
);
}
Err(err) => set_command_feedback.set(err),
}
}
Err(err) => set_command_feedback.set(err),
}
Err(err) => set_command_feedback.set(err),
}
});
}
fn apply_feedback_result<E>(setter: WriteSignal<String>, result: Result<&'static str, E>)
@@ -4931,6 +5089,30 @@ fn App(mount_options: MountOptions) -> impl IntoView {
on_cleanup(move || drop(keydown_handle));
}
{
let click_handle = window_event_listener(ev::click, move |event: MouseEvent| {
let Some(element) = event.target().and_then(target_element) else {
return;
};
let Some(anchor) = element.closest("a.mnote-page-block-link").ok().flatten() else {
return;
};
let Some(href) = anchor
.get_attribute("href")
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty() && value != "#")
else {
return;
};
event.prevent_default();
event.stop_propagation();
if let Some(win) = window() {
let _ = win.location().assign(&href);
}
});
on_cleanup(move || drop(click_handle));
}
let has_task_schema = move || {
let snapshot = json_output.get();
snapshot.contains("\"taskList\"") && snapshot.contains("\"taskItem\"")