add editor transform to page parity

This commit is contained in:
lix-2026
2026-04-30 22:14:37 +08:00
parent 7565976810
commit c7cfe85d87
4 changed files with 243 additions and 10 deletions
@@ -1058,37 +1058,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: 1102, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1111, 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: 1357, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1366, 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: 1448, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
// 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`.
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: 1283, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1292, 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: 1359, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1368, 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: 1296, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1305, 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: 1358, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1367, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hf726e22cfd1ee0a9);
return ret;
},
+230 -1
View File
@@ -1196,6 +1196,45 @@ const SPIKE_STYLE: &str = r#"
min-height: 1.6em;
}
.editor-surface .ProseMirror p:has(> a.mnote-page-block-link) {
width: 100%;
margin: 0 0 8px;
padding: 2px 6px;
border-radius: 4px;
background: rgba(255, 71, 71, 0.12);
}
.editor-surface .ProseMirror a.mnote-page-block-link {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 100%;
padding: 1px 4px;
border-radius: 4px;
color: #37352f;
font-weight: 600;
line-height: 1.45;
text-decoration: none;
cursor: pointer;
}
.editor-surface .ProseMirror a.mnote-page-block-link::before {
content: "";
width: 14px;
height: 14px;
flex: 0 0 auto;
border: 1.4px solid #c8c5c0;
border-radius: 3px;
background: linear-gradient(180deg, #ffffff 0%, #f6f4f1 100%);
box-shadow: inset 0 -2px 0 rgba(55, 53, 47, 0.08);
}
.editor-surface .ProseMirror a.mnote-page-block-link:hover {
color: #37352f;
background: rgba(55, 53, 47, 0.08);
text-decoration: none;
}
.footer-strip {
display: flex;
justify-content: space-between;
@@ -3986,6 +4025,168 @@ fn run_block_turn_into_action(
}
}
fn normalized_page_block_title(text: &str) -> String {
let trimmed = text.trim();
if trimmed.is_empty() {
"未命名页面".to_string()
} else {
trimmed.to_string()
}
}
fn block_plain_text_from_index(
editor: TiptapEditorHandle,
block_index: usize,
) -> Result<String, String> {
let document = editor
.get_json()
.map_err(|err| format!("读取当前 JSON 失败:{err}"))?;
let block = document
.get("content")
.and_then(Value::as_array)
.and_then(|content| content.get(block_index))
.ok_or_else(|| format!("找不到第 {} 个块", block_index + 1))?;
Ok(normalized_page_block_title(&collect_plain_text(block)))
}
fn inline_has_page_block_link(node: &Value) -> bool {
if node
.get("marks")
.and_then(Value::as_array)
.map(|marks| {
marks.iter().any(|mark| {
mark.get("type").and_then(Value::as_str) == Some("link")
&& mark
.get("attrs")
.and_then(|attrs| attrs.get("class"))
.and_then(Value::as_str)
.map(|class_name| {
class_name
.split_whitespace()
.any(|part| part == "mnote-page-block-link")
})
.unwrap_or(false)
})
})
.unwrap_or(false)
{
return true;
}
node.get("content")
.and_then(Value::as_array)
.map(|children| children.iter().any(inline_has_page_block_link))
.unwrap_or(false)
}
fn top_level_block_is_page(editor: TiptapEditorHandle, block_index: usize) -> bool {
editor
.get_json()
.ok()
.and_then(|document| {
document
.get("content")
.and_then(Value::as_array)
.and_then(|content| content.get(block_index).cloned())
})
.map(|block| inline_has_page_block_link(&block))
.unwrap_or(false)
}
fn page_reference_paragraph_node(page_id: &str, title: &str) -> Value {
let href = if page_id.trim().is_empty() {
"#".to_string()
} else {
format!("/documents/{page_id}")
};
paragraph_node(vec![json!({
"type": "text",
"text": normalized_page_block_title(title),
"marks": [{
"type": "link",
"attrs": {
"href": href,
"target": null,
"rel": "noopener noreferrer nofollow",
"class": "mnote-page-block-link",
}
}]
})])
}
fn replace_block_with_page_reference(
editor: TiptapEditorHandle,
block_index: usize,
page_id: &str,
title: &str,
) -> Result<(), String> {
let mut document = editor
.get_json()
.map_err(|err| format!("读取当前 JSON 失败:{err}"))?;
let content = document_content_mut(&mut document)?;
if block_index >= content.len() {
return Err(format!("找不到第 {} 个块", block_index + 1));
}
content[block_index] = page_reference_paragraph_node(page_id, title);
let next_payload = document
.get("content")
.and_then(Value::as_array)
.cloned()
.map(Value::Array)
.unwrap_or(document);
editor
.set_content(TiptapContent::json(next_payload))
.map_err(|err| format!("转换为页面失败:{err}"))?;
focus_top_level_block_start(editor, block_index)?;
Ok(())
}
fn run_block_turn_into_page_action(
editor: TiptapEditorHandle,
block_index: usize,
document_id: ReadSignal<Option<String>>,
workspace_id: ReadSignal<Option<String>>,
title: ReadSignal<String>,
set_dirty_count: WriteSignal<u32>,
set_html_output: WriteSignal<String>,
set_document_json: WriteSignal<Value>,
set_json_output: WriteSignal<String>,
set_command_feedback: WriteSignal<String>,
set_block_menu_open: WriteSignal<bool>,
set_block_menu_anchor: WriteSignal<Option<HoveredBlockState>>,
set_block_turn_into_open: WriteSignal<bool>,
) {
let page_title = match block_plain_text_from_index(editor, block_index) {
Ok(value) => value,
Err(err) => {
set_command_feedback.set(err);
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}"),
);
}
Err(err) => set_command_feedback.set(err),
}
}
fn apply_feedback_result<E>(setter: WriteSignal<String>, result: Result<&'static str, E>)
where
E: Display,
@@ -5616,6 +5817,8 @@ fn App(mount_options: MountOptions) -> impl IntoView {
});
let menu_top = format!("{}px", menu_layout.top);
let menu_bottom = "auto";
let current_block_is_page = top_level_block_is_page(editor, block_index);
let page_current_marker = if current_block_is_page { "" } else { "Ctrl+Shift+9" };
view! {
<div
class="block-drag-menu"
@@ -5940,7 +6143,33 @@ fn App(mount_options: MountOptions) -> impl IntoView {
}
})
.collect_view()}
<button class="block-drag-menu-item" data-testid="block-transform-item-page" disabled=true><span class="block-drag-menu-icon">""</span><span>"页面"</span><span class="block-drag-menu-shortcut">"Ctrl+Shift+9"</span></button>
<button
class="block-drag-menu-item"
data-testid="block-transform-item-page"
data-current=current_block_is_page.to_string()
on:click=move |event: MouseEvent| {
event.stop_propagation();
run_block_turn_into_page_action(
editor,
block_index,
document_id,
workspace_id,
title,
set_dirty_count,
set_html_output,
set_document_json,
set_json_output,
set_command_feedback,
set_block_menu_open,
set_block_menu_anchor,
set_block_turn_into_open,
);
}
>
<span class="block-drag-menu-icon">""</span>
<span>"页面"</span>
<span class="block-drag-menu-shortcut">{page_current_marker}</span>
</button>
<button class="block-drag-menu-item" data-testid="block-transform-item-folded-list" disabled=true><span class="block-drag-menu-icon">""</span><span>"折叠列表"</span><span class="block-drag-menu-shortcut">"Ctrl+Shift+8"</span></button>
<button class="block-drag-menu-item" data-testid="block-transform-item-folded-title" disabled=true><span class="block-drag-menu-icon">"H1"</span><span>"折叠标题"</span><span class="block-drag-menu-arrow">""</span></button>
<button class="block-drag-menu-item" data-testid="block-transform-item-folded-todo" disabled=true><span class="block-drag-menu-icon">""</span><span>"折叠待办"</span><span class="block-drag-menu-arrow">""</span></button>