refactor: split editor mount and sidebar page ai

This commit is contained in:
lix-2026
2026-05-26 04:01:52 +08:00
parent c65135f7ed
commit a8bc87173f
10 changed files with 3124 additions and 2973 deletions
@@ -20,7 +20,7 @@ use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{CustomEvent, CustomEventInit, EventTarget, HtmlElement, HtmlInputElement};
use crate::next_mount_id;
use crate::editor_runtime::mount::next_mount_id;
thread_local! {
static MOUNTED_MINDMAP_SHELLS: std::cell::RefCell<HashMap<u32, MountedMindmapShell>> = std::cell::RefCell::new(HashMap::new());
@@ -14,6 +14,7 @@ pub(crate) mod dom_selection;
pub(crate) mod editor_focus;
pub(crate) mod history_safe_commands;
pub(crate) mod mindmap_node_view;
pub(crate) mod mount;
pub(crate) mod overlays;
pub(crate) mod persistence;
pub(crate) mod runtime_bridge;
@@ -0,0 +1,165 @@
//! Leptos Tiptap island 挂载上下文与 embedded/standalone delivery mode。
use std::cell::Cell;
use leptos::mount::mount_to;
use leptos::prelude::*;
use leptos_tiptap::TiptapContent;
use serde::Deserialize;
use serde_json::Value;
use wasm_bindgen::JsCast;
use web_sys::{window, EventTarget, HtmlElement};
use crate::editor_runtime::bridge_events::{RuntimePageOptions, COMMAND_EVENT};
use crate::editor_runtime::runtime_bridge::{build_command_listener, register_unmount_handle};
use crate::App;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RuntimeDeliveryMode {
Standalone,
Embedded,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MountOptions {
pub(crate) document_id: Option<String>,
pub(crate) workspace_id: Option<String>,
pub(crate) title: Option<String>,
pub(crate) content: Option<Value>,
pub(crate) html: Option<String>,
pub(crate) editable: Option<bool>,
pub(crate) read_only: Option<bool>,
pub(crate) revision: Option<i64>,
pub(crate) conflict_detection_key: Option<String>,
pub(crate) page_options: Option<RuntimePageOptions>,
#[serde(default)]
pub(crate) standalone_object: Option<RuntimeStandaloneObject>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct RuntimeStandaloneObject {
pub(crate) kind: Option<String>,
pub(crate) document_id: Option<String>,
pub(crate) mindmap_id: Option<String>,
}
struct RuntimeMountContext {
id: u32,
target: EventTarget,
mode: RuntimeDeliveryMode,
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
struct RuntimeMountOptions {
options: MountOptions,
mode: RuntimeDeliveryMode,
}
thread_local! {
static RUNTIME_MOUNT_CONTEXT: std::cell::RefCell<Option<RuntimeMountContext>> = const { std::cell::RefCell::new(None) };
static RUNTIME_MOUNT_OPTIONS: std::cell::RefCell<Option<RuntimeMountOptions>> = const { std::cell::RefCell::new(None) };
static NEXT_MOUNT_ID: Cell<u32> = const { Cell::new(1) };
}
pub(crate) fn next_mount_id() -> u32 {
NEXT_MOUNT_ID.with(|cell| {
let next = cell.get();
cell.set(next.saturating_add(1).max(1));
next
})
}
pub(crate) fn set_runtime_mount_context(context: Option<(u32, EventTarget, RuntimeDeliveryMode)>) {
RUNTIME_MOUNT_CONTEXT.with(|cell| {
*cell.borrow_mut() = context.map(|(id, target, mode)| RuntimeMountContext {
id,
target,
mode,
});
});
}
pub(crate) fn runtime_mount_context() -> Option<(u32, EventTarget, RuntimeDeliveryMode)> {
RUNTIME_MOUNT_CONTEXT.with(|cell| {
cell.borrow()
.as_ref()
.map(|context| (context.id, context.target.clone(), context.mode))
})
}
pub(crate) fn runtime_delivery_mode() -> RuntimeDeliveryMode {
resolve_runtime_delivery_mode(
runtime_mount_context().map(|(_, _, mode)| mode),
url_requests_embedded_mode(),
)
}
pub(crate) fn set_runtime_mount_options(options: Option<MountOptions>, mode: RuntimeDeliveryMode) {
RUNTIME_MOUNT_OPTIONS.with(|cell| {
*cell.borrow_mut() = options.map(|options| RuntimeMountOptions { options, mode });
});
}
pub(crate) fn runtime_editor_instance_id(mount_id: Option<u32>) -> String {
mount_id
.filter(|value| *value > 0)
.map(|value| format!("mnote-leptos-tiptap-spike-{value}"))
.unwrap_or_else(|| "mnote-leptos-tiptap-spike-standalone".to_string())
}
fn url_requests_embedded_mode() -> bool {
window()
.and_then(|win| win.location().search().ok())
.map(|search| search.contains("embedded=1"))
.unwrap_or(false)
}
pub(crate) fn resolve_runtime_delivery_mode(
explicit_mode: Option<RuntimeDeliveryMode>,
url_embedded: bool,
) -> RuntimeDeliveryMode {
if let Some(mode) = explicit_mode {
return mode;
}
if url_embedded {
RuntimeDeliveryMode::Embedded
} else {
RuntimeDeliveryMode::Standalone
}
}
pub(crate) fn mount_app_into(
target: HtmlElement,
options: MountOptions,
mode: RuntimeDeliveryMode,
) -> u32 {
let mount_id = next_mount_id();
let context_target: EventTarget = target.clone().into();
set_runtime_mount_options(Some(options.clone()), mode);
set_runtime_mount_context(Some((mount_id, context_target.clone(), mode)));
let handle = mount_to(target, move || {
view! {
<App mount_options=options />
}
});
let command_listener = build_command_listener(mount_id);
let _ = context_target
.add_event_listener_with_callback(COMMAND_EVENT, command_listener.as_ref().unchecked_ref());
register_unmount_handle(mount_id, context_target, command_listener, handle);
mount_id
}
pub(crate) fn initial_content() -> TiptapContent {
TiptapContent::json(serde_json::json!({
"type": "doc",
"content": []
}))
}