refactor: split editor mount and sidebar page ai
This commit is contained in:
+3
-3
@@ -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
@@ -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()
|
||||
|
||||
@@ -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": []
|
||||
}))
|
||||
}
|
||||
@@ -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(|| {
|
||||
|
||||
Reference in New Issue
Block a user