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
@@ -244,7 +244,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tree -- --
拆分项:
- [x] E1. `style.rs`:迁出 `SPIKE_STYLE`,根 `lib.rs` 只引用常量。
- [ ] E2. `mount.rs`:迁出 mount context、mounted handles、`mount_app_into` 周边可独立部分;`#[wasm_bindgen]` wrapper 可暂留根文件。
- [x] E2. `mount.rs`:迁出 mount context、mounted handles、`mount_app_into` 周边可独立部分;`#[wasm_bindgen]` wrapper 可暂留根文件。
- [x] E3. `runtime_bridge.rs`:迁出 `dispatch_runtime_event``dispatch_*_to_target`、host command listener install。
- [x] E4. `block_transform.rs`:迁出 `run_block_turn_into_action``top_level_block_matches_action`、page/block transform helpers。
- [ ] E5. `slash_menu_view.rs`:迁出 slash menu Leptos view 和 click handling。
@@ -272,8 +272,8 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::web_shell
拆分项:
- [ ] F1. 审计 `sidebar-tree-runtime.js` 中所有 Page AI / ACP / Hermes / Reasonix 字符串和函数。
- [ ] F2. 纯 UI panel host 可以迁入 `browser/page-ai-panel-runtime.js`
- [x] F1. 审计 `sidebar-tree-runtime.js` 中所有 Page AI / ACP / Hermes / Reasonix 字符串和函数。
- [x] F2. 纯 UI panel host 迁入 `browser/sidebar-page-ai-runtime.js`
- [ ] F3. ACP/Hermes session 行为不得迁入 tree runtime;需要新建或更新 `design/07-ai/process/*` checklist 后再实施。
验收命令:
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5
View File
@@ -154,6 +154,10 @@ pub fn build_router(state: AppState) -> Router {
"/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js",
get(web_shell::sidebar_attachment_open_runtime_asset),
)
.route(
"/api/mnote-browser-runtime/sidebar-page-ai-runtime.js",
get(web_shell::sidebar_page_ai_runtime_asset),
)
.route(
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
get(web_shell::sidebar_tree_runtime_asset),
@@ -650,6 +654,7 @@ mod tests {
"/api/mnote-browser-runtime/sidebar-filetree-command-runtime.js",
"/api/mnote-browser-runtime/sidebar-filetree-upload-runtime.js",
"/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js",
"/api/mnote-browser-runtime/sidebar-page-ai-runtime.js",
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
"/api/mnote-browser-runtime/tree-live-controller.js",
"/api/mnote-browser-runtime/tree-shell-runtime.js",
@@ -728,6 +728,20 @@ pub async fn sidebar_tree_runtime_asset() -> Response {
.unwrap_or_else(|_| Response::new(Body::empty()))
}
pub async fn sidebar_page_ai_runtime_asset() -> Response {
const JS: &str = include_str!("../../browser/sidebar-page-ai-runtime.js");
Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
"application/javascript; charset=utf-8",
)
.header(header::CACHE_CONTROL, "no-store")
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
.body(Body::from(JS))
.unwrap_or_else(|_| Response::new(Body::empty()))
}
pub async fn sidebar_shell_runtime_asset() -> Response {
const JS: &str = include_str!("../../browser/sidebar-shell-runtime.js");
Response::builder()
+45 -43
View File
@@ -215,6 +215,8 @@ mod tests {
include_str!("../../../browser/sidebar-workspace-runtime.js");
const SIDEBAR_PAGE_TREE_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-tree-runtime.js");
const SIDEBAR_PAGE_AI_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-runtime.js");
const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str =
include_str!("../../../browser/filetree-context-menu-runtime.js");
const FILETREE_DND_RUNTIME_JS: &str = include_str!("../../../browser/filetree-dnd-runtime.js");
@@ -352,7 +354,7 @@ mod tests {
#[test]
fn page_ai_fast_path_is_not_local_first_main_path() {
assert!(
SIDEBAR_TREE_RUNTIME_JS
SIDEBAR_PAGE_AI_RUNTIME_JS
.contains("if (currentSourceKind() === 'local_folder') return false;"),
"local-first 页面 AI 不应继续加厚 page-ai fast-path;本地编辑应走受控文件工具"
);
@@ -361,15 +363,15 @@ mod tests {
#[test]
fn page_ai_local_source_passes_file_reference_fields_to_agent_run() {
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS.contains("function currentRootUri()"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sourceKind: currentSourceKind()"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("rootUri: currentRootUri()"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("sourceKind: currentSourceKind()"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("rootUri: currentRootUri()"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("if (sourceKind && !payload.sourceKind) payload.sourceKind = sourceKind"));
assert!(SIDEBAR_WORKSPACE_RUNTIME_JS
.contains("if (rootUri && !payload.rootUri) payload.rootUri = rootUri"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageContext: scopedContext.pageContext"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageContext: scopedContext.pageContext"));
assert!(
SIDEBAR_TREE_RUNTIME_JS
SIDEBAR_PAGE_AI_RUNTIME_JS
.contains("if (currentSourceKind() === 'local_folder') return false;"),
"local source 不应进入 page-ai fast-path,后端会把 run 收敛为文件引用 scope"
);
@@ -377,70 +379,70 @@ mod tests {
#[test]
fn page_ai_uses_backend_acp_session_runtime_store() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiLoadBackendSessions"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/hermes/client/sessions?"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("params.set('source', 'acp')"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiLoadBackendSessionDetail"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiSearchBackendSessions"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-rename"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-delete"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-resume"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-session-search"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiUsageSummary"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("usage.updated"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("thought.delta"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("permission.requested"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-permission-action=\"allow\""));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-permission-action=\"deny\""));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiHidePermissionDialog"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (!message.resolved)"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiLoadBackendSessions"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("/api/hermes/client/sessions?"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("params.set('source', 'acp')"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiLoadBackendSessionDetail"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiSearchBackendSessions"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-rename"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-delete"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-resume"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-session-search"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiUsageSummary"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("usage.updated"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("thought.delta"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("permission.requested"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-permission-action=\"allow\""));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-permission-action=\"deny\""));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiHidePermissionDialog"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("if (!message.resolved)"));
assert!(
!SIDEBAR_TREE_RUNTIME_JS.contains("(item.resolved ? ' disabled' : '')"),
!SIDEBAR_PAGE_AI_RUNTIME_JS.contains("(item.resolved ? ' disabled' : '')"),
"已决 ACP permission 事件不能继续展示假审批按钮"
);
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("session.info.updated"),
SIDEBAR_PAGE_AI_RUNTIME_JS.contains("session.info.updated"),
"ACP SessionInfoUpdate 事件应通过 session.info.updated SSE 转发到前端"
);
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("plan.updated"),
SIDEBAR_PAGE_AI_RUNTIME_JS.contains("plan.updated"),
"ACP PlanUpdate 事件应通过 plan.updated SSE 转发到前端"
);
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("data-page-ai-plan"),
SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-plan"),
"plan 消息应渲染为 data-page-ai-plan 标记的轻量系统状态面板"
);
assert!(
SIDEBAR_TREE_RUNTIME_JS.contains("执行计划 · "),
SIDEBAR_PAGE_AI_RUNTIME_JS.contains("执行计划 · "),
"plan 面板标题应显示执行计划和步数"
);
}
#[test]
fn page_ai_session_ui_labels_local_shared_and_cloud_storage() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiSessionStorageLabel"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("local_private"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("local_shared"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("convex_acp_runtime_store"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("本地私有"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("共享会话"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("云端会话"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sessionStorage:"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("permissionLevel:"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("shareId:"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiSessionStorageLabel"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("local_private"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("local_shared"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("convex_acp_runtime_store"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("本地私有"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("共享会话"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("云端会话"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("sessionStorage:"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("permissionLevel:"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("shareId:"));
}
#[test]
fn page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch() {
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("var PAGE_AI_SESSION_STORAGE_VERSION = 3"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("var PAGE_AI_SESSION_STORAGE_VERSION = 3"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiAcpRuntime: 'reasonix'"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function pageAiNormalizeAcpRuntimes"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("return ['reasonix', 'hermes']"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiNormalizeAcpRuntimes(acpRuntimes)"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiNormalizeAcpRuntimes"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("return ['reasonix', 'hermes']"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiNormalizeAcpRuntimes(acpRuntimes)"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pageAiAcpRuntimeSelect.value || 'reasonix'"));
assert!(SIDEBAR_TREE_RUNTIME_JS
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS
.contains("activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix'"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("默认 (Hermes HTTP)"));
assert!(!SIDEBAR_PAGE_AI_RUNTIME_JS.contains("默认 (Hermes HTTP)"));
}
#[test]
@@ -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": []
}))
}
+14 -158
View File
@@ -1,5 +1,5 @@
use leptos::ev;
use leptos::mount::{mount_to, mount_to_body};
use leptos::mount::mount_to_body;
use leptos::prelude::*;
use leptos_dom::helpers::window_event_listener;
use leptos_tiptap::{
@@ -9,7 +9,7 @@ use leptos_tiptap::{
use send_wrapper::SendWrapper;
use serde::Deserialize;
use serde_json::{json, Value};
use std::{cell::Cell, fmt::Display};
use std::fmt::Display;
use wasm_bindgen::{closure::Closure, prelude::*, JsCast, JsValue};
use wasm_bindgen_futures::spawn_local;
use web_sys::{
@@ -37,8 +37,8 @@ use editor_runtime::block_transform::{
use editor_runtime::bridge_dispatch::HostCommandKind;
use editor_runtime::bridge_events::{
BridgeSelectorsPayload, ChangeMetaPayload, ChangePayload, HeightPayload, HostCommandEnvelope,
HostCommandPayload, HostStatusPayload, ReadyPayload, RuntimePageOptions, StatePayload,
BLOCK_DELTA_EVENT, COMMAND_EVENT, HEIGHT_EVENT, PROTOCOL, RUNTIME_NAME,
HostCommandPayload, HostStatusPayload, ReadyPayload, StatePayload, BLOCK_DELTA_EVENT,
COMMAND_EVENT, HEIGHT_EVENT, PROTOCOL, RUNTIME_NAME,
};
use editor_runtime::command_sync::{
read_editor_snapshot, sync_editor_outputs, sync_persisted_editor_command,
@@ -53,6 +53,13 @@ use editor_runtime::dom_selection::{
};
use editor_runtime::editor_focus::{active_editor_stage, body_has_focus, schedule_editor_focus};
use editor_runtime::mindmap_node_view::{mount_mindmap_shell_impl, unmount_mindmap_shell_impl};
use editor_runtime::mount::{
initial_content, mount_app_into, runtime_delivery_mode, runtime_editor_instance_id,
runtime_mount_context, set_runtime_mount_context, set_runtime_mount_options, MountOptions,
RuntimeDeliveryMode, RuntimeStandaloneObject,
};
#[cfg(test)]
use editor_runtime::mount::resolve_runtime_delivery_mode;
use editor_runtime::overlays::{
clamp_overlay_anchor, close_editor_floating_overlays_if_escape, image_element_from_target,
image_toolbar_anchor_from_image, open_block_menu_overlay, open_image_toolbar_overlay,
@@ -68,10 +75,9 @@ use editor_runtime::persistence::{
persisted_document_identity, PersistedDocumentIdentity,
};
use editor_runtime::runtime_bridge::{
build_command_listener, dispatch_change_event_to_target, dispatch_ready_event_to_target,
dispatch_change_event_to_target, dispatch_ready_event_to_target,
dispatch_runtime_event, dispatch_selection_event_to_target, dispatch_state_event_to_target,
dispatch_status_event_to_target, register_runtime_listener, register_unmount_handle,
take_unmount_handle,
dispatch_status_event_to_target, register_runtime_listener, take_unmount_handle,
};
use editor_runtime::slash_actions::{SlashActionKind, SLASH_ACTIONS};
use editor_runtime::style::SPIKE_STYLE;
@@ -515,98 +521,6 @@ mod tests {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RuntimeDeliveryMode {
Standalone,
Embedded,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MountOptions {
document_id: Option<String>,
workspace_id: Option<String>,
title: Option<String>,
content: Option<Value>,
html: Option<String>,
editable: Option<bool>,
read_only: Option<bool>,
revision: Option<i64>,
conflict_detection_key: Option<String>,
page_options: Option<RuntimePageOptions>,
#[serde(default)]
standalone_object: Option<RuntimeStandaloneObject>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct RuntimeStandaloneObject {
kind: Option<String>,
document_id: Option<String>,
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) };
}
fn next_mount_id() -> u32 {
NEXT_MOUNT_ID.with(|cell| {
let next = cell.get();
cell.set(next.saturating_add(1).max(1));
next
})
}
fn set_runtime_mount_context(context: Option<RuntimeMountContext>) {
RUNTIME_MOUNT_CONTEXT.with(|cell| {
*cell.borrow_mut() = context;
});
}
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))
})
}
fn runtime_delivery_mode() -> RuntimeDeliveryMode {
resolve_runtime_delivery_mode(
runtime_mount_context().map(|(_, _, mode)| mode),
url_requests_embedded_mode(),
)
}
fn set_runtime_mount_options(options: Option<RuntimeMountOptions>) {
RUNTIME_MOUNT_OPTIONS.with(|cell| {
*cell.borrow_mut() = options;
});
}
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 editor_block_by_id(block_id: &str) -> Option<Element> {
find_mnote_block_anchor(block_id)
}
@@ -728,7 +642,7 @@ pub fn unmount(mount_id: u32) -> Result<(), JsValue> {
if let Some(handle) = take_unmount_handle(mount_id) {
if runtime_mount_context().map(|(id, _, _)| id) == Some(mount_id) {
set_runtime_mount_context(None);
set_runtime_mount_options(None);
set_runtime_mount_options(None, RuntimeDeliveryMode::Embedded);
}
drop(handle);
Ok(())
@@ -737,35 +651,6 @@ pub fn unmount(mount_id: u32) -> Result<(), JsValue> {
}
}
fn mount_app_into(target: HtmlElement, options: MountOptions, mode: RuntimeDeliveryMode) -> u32 {
let mount_id = next_mount_id();
let context_target: EventTarget = target.clone().into();
let mount_options = RuntimeMountOptions {
options: options.clone(),
mode,
};
set_runtime_mount_options(Some(mount_options.clone()));
set_runtime_mount_context(Some(RuntimeMountContext {
id: mount_id,
target: 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
}
fn default_title() -> String {
"Leptos Tiptap 主编辑器 P0".to_string()
}
@@ -818,28 +703,6 @@ fn runtime_persisted_identity(
persisted_document_identity(document_id.get_untracked(), workspace_id.get_untracked())
}
fn url_requests_embedded_mode() -> bool {
window()
.and_then(|win| win.location().search().ok())
.map(|search| search.contains("embedded=1"))
.unwrap_or(false)
}
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
}
}
fn is_embedded_mode() -> bool {
runtime_delivery_mode() == RuntimeDeliveryMode::Embedded
}
@@ -4386,13 +4249,6 @@ fn App(mount_options: MountOptions) -> impl IntoView {
}
}
fn initial_content() -> TiptapContent {
TiptapContent::json(serde_json::json!({
"type": "doc",
"content": []
}))
}
pub fn standalone_main() {
console_error_panic_hook::set_once();
mount_to_body(|| {