yggdrasil/pages/admin/write/
editor.rs1use 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#[cfg(target_arch = "wasm32")]
11const PLACEHOLDER: &str = "开始写作,或输入 / 插入代码、图表、公式和图片";
12
13#[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#[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 let generation = StoredValue::new(0_u64);
56 Effect::new(move |_| {
57 state.reload.track();
58 if !library_ready(state.library) {
59 return;
60 }
61 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 let markdown = state.form.with_untracked(|form| form.content.clone());
76 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 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 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 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}