refactor: split tiptap runtime bridge
This commit is contained in:
@@ -48,8 +48,7 @@ pub(crate) 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 {
|
||||
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 {
|
||||
@@ -66,12 +65,10 @@ pub(crate) fn selected_local_attachment_link_in_editor_state() -> bool {
|
||||
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())
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
//! 也不分发 editor command;调用方负责保留编辑器状态编排。
|
||||
|
||||
use crate::{
|
||||
editor_root_element,
|
||||
editor_runtime::block_hover_state::{
|
||||
block_label_for_element, DropIndicatorState, DropPlacement, HoveredBlockState,
|
||||
},
|
||||
editor_root_element, editor_stage_element,
|
||||
editor_stage_element,
|
||||
};
|
||||
use web_sys::{window, Element, EventTarget};
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// 将 delta operations 应用到 Tiptap JSON 树(原地修改)。
|
||||
/// 返回 true 表示树发生了变更。
|
||||
pub(crate) fn apply_block_delta_to_json(content: &mut Value, operations: &[Value]) -> bool {
|
||||
let Some(content_arr) = content.get_mut("content").and_then(Value::as_array_mut) else {
|
||||
return false;
|
||||
};
|
||||
let mut changed = false;
|
||||
|
||||
for op_value in operations {
|
||||
let Some(op) = op_value.get("op").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
match op {
|
||||
"replace" => {
|
||||
let block_id = op_value.get("block_id").and_then(Value::as_str);
|
||||
let text = op_value.get("text").and_then(Value::as_str).unwrap_or("");
|
||||
let block_type = op_value.get("block_type").and_then(Value::as_str);
|
||||
if let Some(bid) = block_id {
|
||||
if apply_replace_block(content_arr, bid, text, block_type) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
"insert_after" => {
|
||||
let anchor = op_value.get("anchor_block_id").and_then(Value::as_str);
|
||||
let block_id = op_value.get("block_id").and_then(Value::as_str);
|
||||
let text = op_value.get("text").and_then(Value::as_str).unwrap_or("");
|
||||
if let (Some(aid), Some(bid)) = (anchor, block_id) {
|
||||
if apply_insert_block_after(content_arr, aid, bid, text) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
"delete" => {
|
||||
let block_id = op_value.get("block_id").and_then(Value::as_str);
|
||||
if let Some(bid) = block_id {
|
||||
if apply_delete_block(content_arr, bid) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
"move_after" => {
|
||||
let block_id = op_value.get("block_id").and_then(Value::as_str);
|
||||
let anchor = op_value.get("anchor_block_id").and_then(Value::as_str);
|
||||
if let (Some(bid), Some(aid)) = (block_id, anchor) {
|
||||
if apply_move_block_after(content_arr, bid, aid) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
fn find_block_index(blocks: &[Value], block_id: &str) -> Option<usize> {
|
||||
blocks.iter().position(|b| {
|
||||
b.get("attrs")
|
||||
.and_then(|a| a.get("block_id"))
|
||||
.and_then(Value::as_str)
|
||||
== Some(block_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_replace_block(
|
||||
blocks: &mut Vec<Value>,
|
||||
block_id: &str,
|
||||
text: &str,
|
||||
block_type: Option<&str>,
|
||||
) -> bool {
|
||||
let Some(idx) = find_block_index(blocks, block_id) else {
|
||||
return false;
|
||||
};
|
||||
let block = &mut blocks[idx];
|
||||
|
||||
if let Some(bt) = block_type {
|
||||
if let Some(b) = block.as_object_mut() {
|
||||
b.insert("type".into(), json!(bt));
|
||||
}
|
||||
}
|
||||
|
||||
let new_content: Value = if text.is_empty() {
|
||||
json!([])
|
||||
} else {
|
||||
json!([{ "type": "text", "text": text }])
|
||||
};
|
||||
|
||||
if let Some(b) = block.as_object_mut() {
|
||||
b.insert("content".into(), new_content);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn apply_insert_block_after(
|
||||
blocks: &mut Vec<Value>,
|
||||
anchor_block_id: &str,
|
||||
new_block_id: &str,
|
||||
text: &str,
|
||||
) -> bool {
|
||||
let Some(idx) = find_block_index(blocks, anchor_block_id) else {
|
||||
return false;
|
||||
};
|
||||
let new_block = json!({
|
||||
"type": "paragraph",
|
||||
"attrs": { "block_id": new_block_id },
|
||||
"content": if text.is_empty() {
|
||||
json!([])
|
||||
} else {
|
||||
json!([{ "type": "text", "text": text }])
|
||||
}
|
||||
});
|
||||
blocks.insert(idx + 1, new_block);
|
||||
true
|
||||
}
|
||||
|
||||
fn apply_delete_block(blocks: &mut Vec<Value>, block_id: &str) -> bool {
|
||||
let Some(idx) = find_block_index(blocks, block_id) else {
|
||||
return false;
|
||||
};
|
||||
blocks.remove(idx);
|
||||
true
|
||||
}
|
||||
|
||||
fn apply_move_block_after(blocks: &mut Vec<Value>, block_id: &str, anchor_block_id: &str) -> bool {
|
||||
let Some(block_idx) = find_block_index(blocks, block_id) else {
|
||||
return false;
|
||||
};
|
||||
let Some(anchor_idx) = find_block_index(blocks, anchor_block_id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if block_idx == anchor_idx || block_idx == anchor_idx + 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let block = blocks.remove(block_idx);
|
||||
let adjusted_anchor = if block_idx < anchor_idx {
|
||||
anchor_idx - 1
|
||||
} else {
|
||||
anchor_idx
|
||||
};
|
||||
blocks.insert(adjusted_anchor + 1, block);
|
||||
true
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
use leptos_tiptap::{
|
||||
TiptapContent, TiptapEditorHandle, TiptapInsertContentOptions, TiptapRange,
|
||||
};
|
||||
use leptos_tiptap::{TiptapContent, TiptapEditorHandle, TiptapInsertContentOptions, TiptapRange};
|
||||
use serde_json::json;
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
//!
|
||||
//! #[wasm_bindgen] 薄包装器保留在 lib.rs 中。
|
||||
|
||||
use leptos::prelude::*;
|
||||
use leptos::ev;
|
||||
use leptos::mount::mount_to;
|
||||
use leptos::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use serde_wasm_bindgen;
|
||||
@@ -30,7 +30,8 @@ pub(crate) const MINDMAP_SHELL_ACTION_EVENT: &str = "mnote:mindmap-shell:action"
|
||||
pub(crate) const MINDMAP_SHELL_PANEL_EVENT: &str = "mnote:mindmap-shell:panel";
|
||||
pub(crate) const MINDMAP_SHELL_MINIMAP_EVENT: &str = "mnote:mindmap-shell:minimap";
|
||||
pub(crate) const MINDMAP_SHELL_ZOOM_EVENT: &str = "mnote:mindmap-shell:zoom";
|
||||
pub(crate) const MINDMAP_SHELL_TOOLBAR_OVERFLOW_EVENT: &str = "mnote:mindmap-shell:toolbar-overflow";
|
||||
pub(crate) const MINDMAP_SHELL_TOOLBAR_OVERFLOW_EVENT: &str =
|
||||
"mnote:mindmap-shell:toolbar-overflow";
|
||||
pub(crate) struct MountedMindmapShell {
|
||||
// 保留 Leptos mount handle,避免 mindmap shell 被提前释放。
|
||||
#[allow(dead_code)]
|
||||
@@ -372,7 +373,10 @@ fn render_mindmap_sidebar_option(
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub(crate) fn MindmapShell(options: MindmapShellOptions, event_target: EventTarget) -> impl IntoView {
|
||||
pub(crate) fn MindmapShell(
|
||||
options: MindmapShellOptions,
|
||||
event_target: EventTarget,
|
||||
) -> impl IntoView {
|
||||
let toolbar_groups = options.toolbar_groups.clone();
|
||||
let sidebar_panels = options.sidebar_panels.clone();
|
||||
let navigator = options.navigator.clone();
|
||||
@@ -873,7 +877,10 @@ fn take_mindmap_shell_handle(id: u32) -> Option<MountedMindmapShell> {
|
||||
MOUNTED_MINDMAP_SHELLS.with(|registry| registry.borrow_mut().remove(&id))
|
||||
}
|
||||
|
||||
pub(crate) fn mount_mindmap_shell_impl(container: JsValue, options: JsValue) -> Result<u32, JsValue> {
|
||||
pub(crate) fn mount_mindmap_shell_impl(
|
||||
container: JsValue,
|
||||
options: JsValue,
|
||||
) -> Result<u32, JsValue> {
|
||||
console_error_panic_hook::set_once();
|
||||
let options = serde_wasm_bindgen::from_value::<MindmapShellOptions>(options)?;
|
||||
let target = container
|
||||
|
||||
@@ -4,6 +4,7 @@ pub(crate) mod block_dnd;
|
||||
pub(crate) mod block_hover_state;
|
||||
pub(crate) mod block_menu_document;
|
||||
pub(crate) mod block_menu_overlay;
|
||||
pub(crate) mod block_transform;
|
||||
pub(crate) mod bridge_dispatch;
|
||||
pub(crate) mod bridge_events;
|
||||
pub(crate) mod command_sync;
|
||||
@@ -15,6 +16,7 @@ pub(crate) mod history_safe_commands;
|
||||
pub(crate) mod mindmap_node_view;
|
||||
pub(crate) mod overlays;
|
||||
pub(crate) mod persistence;
|
||||
pub(crate) mod runtime_bridge;
|
||||
pub(crate) mod slash_actions;
|
||||
pub(crate) mod style;
|
||||
pub(crate) mod table_commands;
|
||||
|
||||
@@ -216,8 +216,7 @@ pub(crate) fn clamp_overlay_anchor(
|
||||
gap: f64,
|
||||
) -> (f64, f64) {
|
||||
let (viewport_width, viewport_height) = viewport_dimensions();
|
||||
let clamped_left =
|
||||
raw_left.clamp(gap, (viewport_width - overlay_width - gap).max(gap));
|
||||
let clamped_left = raw_left.clamp(gap, (viewport_width - overlay_width - gap).max(gap));
|
||||
let below_limit = viewport_height - gap;
|
||||
let clamped_top = if raw_top + overlay_height > below_limit {
|
||||
(raw_anchor_top - overlay_height - gap).clamp(gap, below_limit - 120.0)
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
//! 编辑器 runtime 与宿主之间的事件桥接 helper。
|
||||
//!
|
||||
//! 本模块只承接 envelope dispatch、listener registry 和低耦合 host command
|
||||
//! listener 安装逻辑;信号读写和编辑器命令处理仍留在 `lib.rs`。
|
||||
use std::{any::Any, collections::HashMap};
|
||||
|
||||
use leptos::mount::UnmountHandle;
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::{closure::Closure, JsCast};
|
||||
use web_sys::{CustomEvent, CustomEventInit, Event, EventTarget};
|
||||
|
||||
use crate::editor_runtime::bridge_dispatch::HostCommandKind;
|
||||
use crate::editor_runtime::bridge_events::{
|
||||
BridgeEnvelope, ChangePayload, HostCommandEnvelope, HostStatusPayload, ReadyPayload,
|
||||
StatePayload, CHANGE_EVENT, COMMAND_EVENT, EVENT_PREFIX, PROTOCOL, READY_EVENT, RUNTIME_NAME,
|
||||
RUNTIME_VERSION, SELECTION_EVENT, STATE_EVENT, STATUS_EVENT,
|
||||
};
|
||||
use crate::editor_runtime::dom_selection::SelectionPayload;
|
||||
|
||||
pub(crate) struct MountedRuntimeListener {
|
||||
target: EventTarget,
|
||||
listener: Closure<dyn FnMut(Event)>,
|
||||
}
|
||||
|
||||
pub(crate) struct MountedRuntime {
|
||||
listeners: Vec<MountedRuntimeListener>,
|
||||
// 保留 Leptos mount handle,避免挂载内容被提前释放。
|
||||
#[allow(dead_code)]
|
||||
mount_handle: Box<dyn Any>,
|
||||
}
|
||||
|
||||
impl Drop for MountedRuntime {
|
||||
fn drop(&mut self) {
|
||||
for listener in &self.listeners {
|
||||
let _ = listener.target.remove_event_listener_with_callback(
|
||||
COMMAND_EVENT,
|
||||
listener.listener.as_ref().unchecked_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static MOUNTED_HANDLES: std::cell::RefCell<HashMap<u32, MountedRuntime>> = std::cell::RefCell::new(HashMap::new());
|
||||
static PENDING_RUNTIME_LISTENERS: std::cell::RefCell<HashMap<u32, Vec<MountedRuntimeListener>>> = std::cell::RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_runtime_event<T>(
|
||||
target: Option<EventTarget>,
|
||||
event_name: &'static str,
|
||||
payload: &T,
|
||||
) where
|
||||
T: Serialize,
|
||||
{
|
||||
let Some(target) = target else {
|
||||
return;
|
||||
};
|
||||
let envelope = BridgeEnvelope {
|
||||
protocol: PROTOCOL,
|
||||
runtime: RUNTIME_NAME,
|
||||
version: RUNTIME_VERSION,
|
||||
source: EVENT_PREFIX,
|
||||
event: event_name,
|
||||
payload,
|
||||
};
|
||||
let Ok(detail_value) = serde_wasm_bindgen::to_value(&envelope) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let init = CustomEventInit::new();
|
||||
init.set_detail(&detail_value);
|
||||
if let Ok(event) = CustomEvent::new_with_event_init_dict(event_name, &init) {
|
||||
let _ = target.dispatch_event(&event);
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_custom_event_to_target<T>(target: &EventTarget, event_name: &'static str, payload: &T)
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let envelope = BridgeEnvelope {
|
||||
protocol: PROTOCOL,
|
||||
runtime: RUNTIME_NAME,
|
||||
version: RUNTIME_VERSION,
|
||||
source: EVENT_PREFIX,
|
||||
event: event_name,
|
||||
payload,
|
||||
};
|
||||
let Ok(detail_value) = serde_wasm_bindgen::to_value(&envelope) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let init = CustomEventInit::new();
|
||||
init.set_detail(&detail_value);
|
||||
init.set_bubbles(true);
|
||||
if let Ok(event) = CustomEvent::new_with_event_init_dict(event_name, &init) {
|
||||
let _ = target.dispatch_event(&event);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_ready_event_to_target(target: &EventTarget, payload: &ReadyPayload) {
|
||||
dispatch_custom_event_to_target(target, READY_EVENT, payload);
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_change_event_to_target(target: &EventTarget, payload: &ChangePayload) {
|
||||
dispatch_custom_event_to_target(target, CHANGE_EVENT, payload);
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_state_event_to_target(target: &EventTarget, payload: &StatePayload) {
|
||||
dispatch_custom_event_to_target(target, STATE_EVENT, payload);
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_status_event_to_target(target: &EventTarget, payload: &HostStatusPayload) {
|
||||
dispatch_custom_event_to_target(target, STATUS_EVENT, payload);
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_selection_event_to_target(target: &EventTarget, payload: &SelectionPayload) {
|
||||
dispatch_custom_event_to_target(target, SELECTION_EVENT, payload);
|
||||
}
|
||||
|
||||
pub(crate) fn register_unmount_handle<M: Any + leptos::prelude::Mountable + 'static>(
|
||||
id: u32,
|
||||
target: EventTarget,
|
||||
listener: Closure<dyn FnMut(Event)>,
|
||||
handle: UnmountHandle<M>,
|
||||
) {
|
||||
let mut listeners = vec![MountedRuntimeListener { target, listener }];
|
||||
PENDING_RUNTIME_LISTENERS.with(|registry| {
|
||||
if let Some(mut pending) = registry.borrow_mut().remove(&id) {
|
||||
listeners.append(&mut pending);
|
||||
}
|
||||
});
|
||||
MOUNTED_HANDLES.with(|registry| {
|
||||
registry.borrow_mut().insert(
|
||||
id,
|
||||
MountedRuntime {
|
||||
listeners,
|
||||
mount_handle: Box::new(handle),
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn register_runtime_listener(
|
||||
id: u32,
|
||||
target: EventTarget,
|
||||
listener: Closure<dyn FnMut(Event)>,
|
||||
) {
|
||||
MOUNTED_HANDLES.with(|registry| {
|
||||
if let Some(runtime) = registry.borrow_mut().get_mut(&id) {
|
||||
runtime
|
||||
.listeners
|
||||
.push(MountedRuntimeListener { target, listener });
|
||||
} else {
|
||||
PENDING_RUNTIME_LISTENERS.with(|pending_registry| {
|
||||
pending_registry
|
||||
.borrow_mut()
|
||||
.entry(id)
|
||||
.or_default()
|
||||
.push(MountedRuntimeListener { target, listener });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn take_unmount_handle(id: u32) -> Option<MountedRuntime> {
|
||||
PENDING_RUNTIME_LISTENERS.with(|registry| {
|
||||
registry.borrow_mut().remove(&id);
|
||||
});
|
||||
MOUNTED_HANDLES.with(|registry| registry.borrow_mut().remove(&id))
|
||||
}
|
||||
|
||||
pub(crate) fn build_command_listener(mount_id: u32) -> Closure<dyn FnMut(Event)> {
|
||||
Closure::<dyn FnMut(Event)>::wrap(Box::new(move |event: Event| {
|
||||
let Some(custom_event) = event.dyn_ref::<CustomEvent>() else {
|
||||
return;
|
||||
};
|
||||
let detail = custom_event.detail();
|
||||
let Ok(envelope) = serde_wasm_bindgen::from_value::<HostCommandEnvelope>(detail) else {
|
||||
return;
|
||||
};
|
||||
if envelope.protocol.as_deref() != Some(PROTOCOL) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(payload) = envelope.payload else {
|
||||
return;
|
||||
};
|
||||
let Some(command_kind) = HostCommandKind::from_payload(&payload) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = mount_id;
|
||||
let _ = command_kind;
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user