yggdrasil/pages/admin/
note_drafts.rs1use dioxus::prelude::*;
3use serde::{de::DeserializeOwned, Serialize};
4
5pub(super) fn use_draft_protection<
6 T: Clone + PartialEq + Serialize + DeserializeOwned + 'static,
7>(
8 slot: String,
9 current: Signal<T>,
10 dirty: impl Fn() -> bool + Copy + 'static,
11) -> (Signal<Option<T>>, Signal<bool>) {
12 #[cfg(target_arch = "wasm32")]
13 {
14 use std::rc::Rc;
15 use wasm_bindgen::{closure::Closure, JsCast};
16 let user: crate::context::UserContext = use_context();
17 let key = use_hook(move || {
18 format!(
19 "ygg.note-draft.{}.{}",
20 user.user.peek().as_ref().map(|u| u.id).unwrap_or_default(),
21 slot
22 )
23 });
24 let read_key = key.clone();
25 let recovery = use_signal(move || {
26 let storage = web_sys::window()?.session_storage().ok()??;
27 let draft: T = serde_json::from_str(&storage.get_item(&read_key).ok()??).ok()?;
28 (draft != current()).then_some(draft)
29 });
30 let failed = use_signal(|| false);
31 let flush = use_hook(move || {
32 Rc::new(move || {
33 let mut failed = failed;
34 let unsaved = dirty();
35 let snapshot = current();
36 if recovery().is_some() {
37 return;
38 }
39 let result = (|| {
40 let storage = web_sys::window()?.session_storage().ok()??;
41 if unsaved {
42 storage
43 .set_item(&key, &serde_json::to_string(&snapshot).ok()?)
44 .ok()?;
45 } else {
46 storage.remove_item(&key).ok()?;
47 }
48 Some(())
49 })();
50 let error = result.is_none();
51 if *failed.peek() != error {
52 failed.set(error);
53 }
54 })
55 });
56 let cache = flush.clone();
57 use_effect(move || cache());
58 let listeners = use_hook(move || {
59 let cache = flush.clone();
60 let unload = Rc::new(Closure::<dyn FnMut(web_sys::Event)>::new(
61 move |event: web_sys::Event| {
62 cache();
63 if dirty() {
64 event.prevent_default();
65 let _ =
66 js_sys::Reflect::set(event.as_ref(), &"returnValue".into(), &"".into());
67 }
68 },
69 ));
70 let leave = Rc::new(Closure::<dyn FnMut() -> bool>::new(move || {
71 flush();
73 !dirty()
74 || web_sys::window().is_some_and(|w| {
75 w.confirm_with_message(
76 "还有未保存内容,或保存 / 上传尚未完成。确定离开?本地恢复副本不等于保存成功。"
77 ).unwrap_or(false)
78 })
79 }));
80 if let Some(window) = web_sys::window() {
81 let _ = window.add_event_listener_with_callback(
82 "beforeunload",
83 unload.as_ref().as_ref().unchecked_ref(),
84 );
85 }
86 let module = crate::bridges::navigation::wasm::get_module().map(Rc::new);
87 let id = module.as_ref().map(|m| m.set_leave_guard(&leave));
88 (unload, leave, module, id)
89 });
90 use_drop(move || {
91 let (unload, _leave, module, id) = listeners;
92 if let (Some(module), Some(id)) = (module, id) {
93 module.clear_leave_guard(id);
94 }
95 if let Some(window) = web_sys::window() {
96 let _ = window.remove_event_listener_with_callback(
97 "beforeunload",
98 unload.as_ref().as_ref().unchecked_ref(),
99 );
100 }
101 });
102 (recovery, failed)
103 }
104 #[cfg(not(target_arch = "wasm32"))]
105 {
106 let _ = (slot, current, dirty);
107 (use_signal(|| None), use_signal(|| false))
108 }
109}
110
111pub(super) fn clear_submitted(current: &mut String, submitted: &str) -> bool {
113 if current != submitted {
114 return false;
115 }
116 current.clear();
117 true
118}
119
120#[cfg(test)]
121mod tests {
122 use super::clear_submitted;
123
124 #[test]
125 fn quick_save_clears_only_the_submitted_snapshot() {
126 let mut current = "first".to_string();
127 assert!(clear_submitted(&mut current, "first"));
128 assert!(current.is_empty());
129 current = "first plus a new thought".to_string();
130 assert!(!clear_submitted(&mut current, "first"));
131 assert_eq!(current, "first plus a new thought");
132 current.clear();
133 assert!(!clear_submitted(&mut current, "first"));
134 }
135}