Skip to main content

yggdrasil/pages/admin/write/
editor.rs

1//! 文章编辑器的初始化与清理。文章在本模块运行前已取回,表单以服务端内容起步,
2//! 本地恢复副本才能与之比较。
3use super::state::{EditorPhase, PostDraft, SaveState, WriteEditorState};
4use crate::bridges::library::{use_browser_library, BrowserLibrary};
5use crate::bridges::tiptap::UploadsInFlight;
6use chrono::{DateTime, Utc};
7use leptos::prelude::*;
8
9/// 正文为空时编辑器里的提示。
10#[cfg(target_arch = "wasm32")]
11const PLACEHOLDER: &str = "开始写作,或输入 / 插入代码、图表、公式和图片";
12
13/// 编辑器的 Tiptap 回调与句柄只活在写作页的所有者里:页面卸载时 `editor` 随之释放,
14/// 触发 JS 端 destroy。
15#[cfg_attr(not(target_arch = "wasm32"), allow(unused_variables))]
16pub(super) fn use_write_editor(
17    post_id: Option<i32>,
18    initial: PostDraft,
19    published_at: Option<DateTime<Utc>>,
20) -> WriteEditorState {
21    let baseline = initial.clone();
22    let state = WriteEditorState {
23        post_id,
24        form: RwSignal::new(initial),
25        saved: RwSignal::new(baseline),
26        saved_at: RwSignal::new(None),
27        published_at: RwSignal::new(published_at),
28        phase: RwSignal::new(EditorPhase::Initializing),
29        save: RwSignal::new(SaveState::default()),
30        library: use_browser_library(BrowserLibrary::Tiptap, || true),
31        uploads_in_flight: RwSignal::new(UploadsInFlight::default()),
32        upload_errors: RwSignal::new(Vec::new()),
33        cover_uploading: RwSignal::new(false),
34        body_picker_visible: RwSignal::new(false),
35        body_picker_uploading: RwSignal::new(false),
36        reload: RwSignal::new(0),
37        container: NodeRef::new(),
38        #[cfg(target_arch = "wasm32")]
39        editor: StoredValue::new_local(None),
40    };
41    #[cfg(target_arch = "wasm32")]
42    install_editor(state);
43    state
44}
45
46/// 库就绪后创建编辑器,并在往返检查结束后把页面置为就绪。
47#[cfg(target_arch = "wasm32")]
48fn install_editor(state: WriteEditorState) {
49    use crate::bridges::library::library_ready;
50    use crate::bridges::tiptap::{consume_upload_event, EditorHandle, EditorOptions};
51    use wasm_bindgen::closure::Closure;
52
53    let ready = RwSignal::new(false);
54    // 每次(重新)创建递增世代号:迟到的帧回调与往返检查结果只认最新一次。
55    let generation = StoredValue::new(0_u64);
56    Effect::new(move |_| {
57        state.reload.track();
58        if !library_ready(state.library) {
59            return;
60        }
61        // 跟踪读取:容器挂载(节点提交进文档)后本 Effect 会重跑。
62        if state.container.get().is_none() {
63            return;
64        }
65        let Some(current) = generation.try_update_value(|g| {
66            *g += 1;
67            *g
68        }) else {
69            return;
70        };
71        ready.set(false);
72        state.editor.set_value(None);
73        state.phase.set(EditorPhase::Initializing);
74        // 重试时保留失败前已输入的内容。
75        let markdown = state.form.with_untracked(|form| form.content.clone());
76        // 推迟一帧:等容器提交进文档。
77        request_animation_frame(move || {
78            if generation.try_get_value() != Some(current) {
79                return;
80            }
81            let Some(container) = state.container.get_untracked() else {
82                state
83                    .phase
84                    .set(EditorPhase::Failed("编辑器容器未就绪".into()));
85                return;
86            };
87            let on_update: Closure<dyn FnMut(String)> = Closure::new(move |md: String| {
88                let _ = state.form.try_update(|form| form.content = md);
89            });
90            let on_check_roundtrip = crate::bridges::tiptap::make_roundtrip_check_closure();
91            let on_image_upload = crate::bridges::tiptap::make_upload_closure();
92            let on_upload_event: Closure<dyn FnMut(crate::bridges::tiptap::UploadEventJs)> =
93                Closure::new(move |event: crate::bridges::tiptap::UploadEventJs| {
94                    consume_upload_event(&event, state.uploads_in_flight, state.upload_errors);
95                });
96            let on_run_code = crate::bridges::tiptap::make_run_code_closure();
97            let on_render_mermaid = crate::bridges::tiptap::make_render_mermaid_closure();
98            let on_pick_from_library: Closure<dyn FnMut()> = Closure::new(move || {
99                let _ = state.body_picker_visible.try_set(true);
100            });
101            let options = EditorOptions::default();
102            options.set_placeholder(PLACEHOLDER);
103            options.set_on_update(&on_update);
104            options.set_on_image_upload(&on_image_upload);
105            options.set_on_upload_event(&on_upload_event);
106            options.set_on_run_code(&on_run_code);
107            options.set_on_pick_from_library(&on_pick_from_library);
108            options.set_check_roundtrip(&on_check_roundtrip);
109            options.set_on_render_mermaid(&on_render_mermaid);
110            match crate::bridges::tiptap::create(&container, &options) {
111                Ok(instance) => {
112                    let settled = (!markdown.is_empty()).then(|| instance.set_markdown(&markdown));
113                    state.editor.set_value(Some(
114                        EditorHandle::new(
115                            instance,
116                            on_update,
117                            on_image_upload,
118                            // 写作页的就绪以 setMarkdown 往返检查结束为准,不监听 onReady
119                            Closure::new(|| {}),
120                            on_upload_event,
121                            on_run_code,
122                            on_pick_from_library,
123                        )
124                        .with_roundtrip_check(on_check_roundtrip)
125                        .with_render_mermaid(on_render_mermaid),
126                    ));
127                    // 往返检查结束、视图模式确定后才就绪:此前骨架屏遮挡,保存不可用。
128                    leptos::task::spawn_local(async move {
129                        if let Some(settled) = settled {
130                            let _ = wasm_bindgen_futures::JsFuture::from(settled).await;
131                        }
132                        if generation.try_get_value() == Some(current) {
133                            let _ = ready.try_set(true);
134                        }
135                    });
136                }
137                Err(error) => {
138                    web_sys::console::error_1(&error);
139                    state
140                        .phase
141                        .set(EditorPhase::Failed("编辑器加载失败,请刷新页面重试".into()));
142                }
143            }
144        });
145    });
146
147    let baseline_done = StoredValue::new(false);
148    Effect::new(move |_| {
149        if !ready.get() {
150            return;
151        }
152        // 编辑器加载时会规范化 Markdown;首次就绪时把它的结果采纳为已保存基线,
153        // 免得没动过的文章显示成“已修改”。此前正文无法输入。
154        state.editor.with_value(|handle| {
155            if let Some(handle) = handle {
156                let markdown = handle.instance().get_markdown();
157                if !baseline_done.get_value() {
158                    baseline_done.set_value(true);
159                    state.saved.update(|saved| saved.content = markdown.clone());
160                }
161                state.form.update(|form| form.content = markdown);
162            }
163        });
164        state.phase.set(EditorPhase::Ready);
165    });
166}