主编辑区改造准备
This commit is contained in:
+1
@@ -971,6 +971,7 @@ name = "mnote-leptos-tiptap-spike"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
"leptos",
|
||||
"leptos-tiptap",
|
||||
"leptos_dom",
|
||||
|
||||
@@ -11,10 +11,11 @@ leptos = { version = "0.8.19", features = ["csr"] }
|
||||
leptos_dom = "0.8.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
js-sys = "0.3.77"
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3.77", features = ["DataTransfer", "Document", "DomRect", "DragEvent", "Element", "EventTarget", "HtmlElement", "MouseEvent", "Node", "Range", "Selection", "Storage", "Window"] }
|
||||
|
||||
[dependencies.leptos-tiptap]
|
||||
path = "../../../design/blocknote/reference-code/leptos-tiptap"
|
||||
path = "../../../design/05-editor-mainline/reference-code/leptos-tiptap"
|
||||
default-features = false
|
||||
features = ["component", "full"]
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# 8123 leptos-tiptap runtime bridge
|
||||
|
||||
这是 `rust/spikes/leptos-tiptap-spike` 的宿主桥接说明,面向前端 host / QA。
|
||||
|
||||
## runtime 识别
|
||||
|
||||
宿主可用下面特征识别这套 runtime:
|
||||
|
||||
- `data-mnote-runtime="8123-leptos-tiptap-runtime"`
|
||||
- `data-mnote-runtime-bridge="true"`
|
||||
- `data-testid="mnote-leptos-tiptap-host"`
|
||||
|
||||
稳定锚点:
|
||||
|
||||
- root: `data-testid="mnote-leptos-tiptap-host"`
|
||||
- stage: `#editor-stage` / `data-testid="mnote-leptos-tiptap-editor-stage"`
|
||||
- editor root: `.editor-surface .ProseMirror` / `data-testid="mnote-leptos-tiptap-editor-root"`
|
||||
- toolbar: `data-testid="mnote-leptos-tiptap-toolbar"`
|
||||
- slash menu: `data-testid="mnote-leptos-tiptap-slash-menu"`
|
||||
- handle: `data-testid="mnote-leptos-tiptap-handle"`
|
||||
|
||||
## 事件协议
|
||||
|
||||
所有事件都走同一 envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"protocol": "mnote.leptos_tiptap.bridge.v1",
|
||||
"runtime": "8123-leptos-tiptap-runtime",
|
||||
"version": "1.0.0",
|
||||
"source": "mnote:leptos-tiptap-spike",
|
||||
"payload": {}
|
||||
}
|
||||
```
|
||||
|
||||
### `mnote:leptos-tiptap-spike:ready`
|
||||
|
||||
首次挂载完成时发送,告诉 host 这套 runtime 可以被嵌入。
|
||||
|
||||
payload 里至少包含:
|
||||
|
||||
- `runtime_name`
|
||||
- `selectors`
|
||||
- `supported_commands`
|
||||
- `supports_embedded_mode`
|
||||
|
||||
### `mnote:leptos-tiptap-spike:state`
|
||||
|
||||
发送当前最小可观测状态。
|
||||
|
||||
payload 字段:
|
||||
|
||||
- `document_id`
|
||||
- `title`
|
||||
- `dirty_count`
|
||||
- `selected_block_index`
|
||||
- `editor_focused`
|
||||
- `slash_open`
|
||||
- `toolbar_open`
|
||||
|
||||
### `mnote:leptos-tiptap-spike:change`
|
||||
|
||||
内容变化时发送结构化文档快照。
|
||||
|
||||
payload 结构:
|
||||
|
||||
- `title`: 文档标题
|
||||
- `content`: 当前文档 JSON
|
||||
- `meta`: 附加运行态信息(如 dirty_count / selection / focus)
|
||||
|
||||
### `mnote:leptos-tiptap-spike:save-request`
|
||||
|
||||
保存路径或 host 明确要求落盘时发送,payload 与 `change` 一致,但语义是“请宿主处理保存”。
|
||||
|
||||
### `mnote:leptos-tiptap-spike:command`
|
||||
|
||||
预留宿主到 runtime 的命令通道。当前 spike 只声明了最小命令集合,还没有把完整命令分发做成正式 host API。
|
||||
|
||||
## 选择器清单
|
||||
|
||||
建议 host/QA 只依赖这些稳定选择器:
|
||||
|
||||
- root: `[data-testid="mnote-leptos-tiptap-host"]`
|
||||
- stage: `#editor-stage` 或 `[data-testid="mnote-leptos-tiptap-editor-stage"]`
|
||||
- editor: `.editor-surface .ProseMirror` 或 `[data-testid="mnote-leptos-tiptap-editor-root"]`
|
||||
- toolbar: `[data-testid="mnote-leptos-tiptap-toolbar"]`
|
||||
- slash menu: `[data-testid="mnote-leptos-tiptap-slash-menu"]`
|
||||
- handle: `[data-testid="mnote-leptos-tiptap-handle"]`
|
||||
|
||||
## 推荐嵌入方式
|
||||
|
||||
推荐把这套 runtime 作为 iframe 或独立容器挂载,然后由 host 监听 `document` 上的 `CustomEvent`:
|
||||
|
||||
1. 先等待 `ready`。
|
||||
2. 从 `ready.payload.selectors` 取稳定锚点。
|
||||
3. 监听 `change` / `state` / `save-request`。
|
||||
4. 需要操作时再发 `command`。
|
||||
|
||||
最小监听示例:
|
||||
|
||||
```ts
|
||||
const onReady = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
// detail.protocol / detail.payload.selectors
|
||||
};
|
||||
|
||||
document.addEventListener('mnote:leptos-tiptap-spike:ready', onReady as EventListener);
|
||||
document.addEventListener('mnote:leptos-tiptap-spike:change', onReady as EventListener);
|
||||
document.addEventListener('mnote:leptos-tiptap-spike:state', onReady as EventListener);
|
||||
document.addEventListener('mnote:leptos-tiptap-spike:save-request', onReady as EventListener);
|
||||
```
|
||||
|
||||
## 已知限制
|
||||
|
||||
- 这仍是 8123 spike,不是正式主编辑器切流完成态。
|
||||
- 目前桥接只保证稳定事件与稳定 selector,不保证 host 命令已经全量实现。
|
||||
- 仍保留独立运行能力,host 不应依赖 debug 文案。
|
||||
- 保存仍以 spike 本地草稿逻辑为主,真正主链保存合同还没迁移完。
|
||||
@@ -181,6 +181,36 @@ const SPIKE_STYLE: &str = r#"
|
||||
padding: 28px 20px 72px;
|
||||
}
|
||||
|
||||
.app-shell-embedded {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-shell-embedded .hero-bar,
|
||||
.app-shell-embedded .title-input,
|
||||
.app-shell-embedded .editor-subcopy,
|
||||
.app-shell-embedded .footer-strip,
|
||||
.app-shell-embedded .debug-drawer,
|
||||
.app-shell-embedded .editor-crumbs:first-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-shell-embedded .editor-card {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.app-shell-embedded .editor-topbar {
|
||||
padding: 8px 12px 0;
|
||||
}
|
||||
|
||||
.app-shell-embedded .editor-stage {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.hero-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -1209,10 +1239,7 @@ fn target_element(target: web_sys::EventTarget) -> Option<Element> {
|
||||
})
|
||||
}
|
||||
|
||||
fn event_target_matches_selector(
|
||||
target: Option<web_sys::EventTarget>,
|
||||
selector: &str,
|
||||
) -> bool {
|
||||
fn event_target_matches_selector(target: Option<web_sys::EventTarget>, selector: &str) -> bool {
|
||||
target
|
||||
.and_then(target_element)
|
||||
.and_then(|element| element.closest(selector).ok().flatten())
|
||||
@@ -2260,8 +2287,7 @@ fn App() -> impl IntoView {
|
||||
let (text_selection_active, set_text_selection_active) = signal(false);
|
||||
let (floating_toolbar_anchor, set_floating_toolbar_anchor) =
|
||||
signal(None::<FloatingToolbarAnchor>);
|
||||
let (locked_toolbar_anchor, set_locked_toolbar_anchor) =
|
||||
signal(None::<FloatingToolbarAnchor>);
|
||||
let (locked_toolbar_anchor, set_locked_toolbar_anchor) = signal(None::<FloatingToolbarAnchor>);
|
||||
let (slash_open, set_slash_open) = signal(false);
|
||||
let (slash_index, set_slash_index) = signal(0_usize);
|
||||
let (turn_into_open, set_turn_into_open) = signal(false);
|
||||
@@ -2596,6 +2622,10 @@ fn App() -> impl IntoView {
|
||||
};
|
||||
|
||||
let selection_text = move || selection_summary(&selection_state.get());
|
||||
let is_embedded = window()
|
||||
.and_then(|win| win.location().search().ok())
|
||||
.map(|search| search.contains("embedded=1"))
|
||||
.unwrap_or(false);
|
||||
|
||||
let on_link_click = {
|
||||
let editor = editor;
|
||||
@@ -2633,7 +2663,7 @@ fn App() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<style>{SPIKE_STYLE}</style>
|
||||
<main class="app-shell">
|
||||
<main class=move || if is_embedded { "app-shell app-shell-embedded" } else { "app-shell" }>
|
||||
<section class="hero-bar">
|
||||
<div class="hero-meta">
|
||||
<span class="hero-tag">"P0 / Tiptap-like 主编辑器体验"</span>
|
||||
@@ -2750,6 +2780,18 @@ fn App() -> impl IntoView {
|
||||
|
||||
set_hovered_block.set(None);
|
||||
}
|
||||
on:mouseover=move |event: MouseEvent| {
|
||||
if block_menu_open.get_untracked() || dragging_block_index.get_untracked().is_some() {
|
||||
return;
|
||||
}
|
||||
let Some(target) = event.target() else {
|
||||
return;
|
||||
};
|
||||
if let Some(block) = hovered_block_from_target(target) {
|
||||
set_editor_focused.set(true);
|
||||
set_hovered_block.set(Some(block));
|
||||
}
|
||||
}
|
||||
on:mouseleave=move |_| {
|
||||
if !block_menu_open.get_untracked() && dragging_block_index.get_untracked().is_none() {
|
||||
set_hovered_block.set(None);
|
||||
@@ -3717,19 +3759,10 @@ fn App() -> impl IntoView {
|
||||
}
|
||||
|
||||
fn initial_content() -> TiptapContent {
|
||||
TiptapContent::html(
|
||||
r#"
|
||||
<h1>准备接管主编辑器体验</h1>
|
||||
<p>这页现在先只在 <strong>8123</strong> 做 P0 体验收口:你可以输入 <code>/</code> 打开命令菜单,也可以直接试试选中文字后的浮动工具条。</p>
|
||||
<p>目标不是继续堆 runtime debug,而是先把页面做得更像可以真实检验的块编辑器。</p>
|
||||
<ul>
|
||||
<li>标题、段落、引用、代码块</li>
|
||||
<li>无序列表 / 有序列表 / Todo 列表</li>
|
||||
<li>分割线、行内样式、链接和高亮</li>
|
||||
</ul>
|
||||
<p>请把光标放到这里,然后按 <code>/</code>。</p>
|
||||
"#,
|
||||
)
|
||||
TiptapContent::json(serde_json::json!({
|
||||
"type": "doc",
|
||||
"content": []
|
||||
}))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
Reference in New Issue
Block a user