1use std::collections::HashSet;
2use std::sync::Arc;
3
4use js_sys::Reflect;
5use wasm_bindgen::prelude::wasm_bindgen;
6use wasm_bindgen::JsValue;
7
8use yrs::branch::BranchPtr;
9use yrs::undo::{EventKind, UndoManager};
10use yrs::{Doc, Transact};
11
12use crate::doc::YDoc;
13use crate::js::{Callback, Js, Shared};
14use crate::transaction::YTransaction;
15use crate::Result;
16
17#[wasm_bindgen]
18#[repr(transparent)]
19pub struct YUndoManager(UndoManager<JsValue>);
20
21impl YUndoManager {
22 fn get_scope(doc: &Doc, js: &JsValue) -> Result<BranchPtr> {
23 let shared = Shared::from_ref(js)?;
24 let branch_id = if let Some(id) = shared.branch_id() {
25 id
26 } else {
27 return Err(JsValue::from_str(crate::js::errors::INVALID_PRELIM_OP));
28 };
29 let txn = doc.transact();
30 match branch_id.get_branch(&txn) {
31 Some(branch) if !branch.is_deleted() => Ok(branch),
32 _ => Err(JsValue::from_str(crate::js::errors::REF_DISPOSED)),
33 }
34 }
35}
36
37#[wasm_bindgen]
38impl YUndoManager {
39 #[wasm_bindgen(constructor)]
40 pub fn new(doc: &YDoc, scope: JsValue, options: JsValue) -> Result<YUndoManager> {
41 let doc = &doc.0;
42 let scope = Self::get_scope(doc, &scope)?;
43 let mut o = yrs::undo::Options {
44 capture_timeout_millis: 500,
45 tracked_origins: HashSet::new(),
46 capture_transaction: None,
47 timestamp: Arc::new(crate::awareness::JsClock),
48 };
49 if options.is_object() {
50 if let Ok(js) = Reflect::get(&options, &JsValue::from_str("captureTimeout")) {
51 if let Some(millis) = js.as_f64() {
52 o.capture_timeout_millis = millis as u64;
53 }
54 }
55 if let Ok(js) = Reflect::get(&options, &JsValue::from_str("trackedOrigins")) {
56 if js_sys::Array::is_array(&js) {
57 let array = js_sys::Array::from(&js);
58 for js in array.iter() {
59 let v = Js::from(js);
60 o.tracked_origins.insert(v.into());
61 }
62 }
63 }
64 }
65 Ok(YUndoManager(UndoManager::with_scope_and_options(
66 doc, &scope, o,
67 )))
68 }
69
70 #[wasm_bindgen(js_name = addToScope)]
71 pub fn add_to_scope(&mut self, ytypes: js_sys::Array) -> Result<()> {
72 for js in ytypes.iter() {
73 let scope = Self::get_scope(self.0.doc(), &js)?;
74 self.0.expand_scope(&scope);
75 }
76 Ok(())
77 }
78
79 #[wasm_bindgen(js_name = addTrackedOrigin)]
80 pub fn add_tracked_origin(&mut self, origin: JsValue) {
81 self.0.include_origin(Js::from(origin))
82 }
83
84 #[wasm_bindgen(js_name = removeTrackedOrigin)]
85 pub fn remove_tracked_origin(&mut self, origin: JsValue) {
86 self.0.exclude_origin(Js::from(origin))
87 }
88
89 #[wasm_bindgen(js_name = clear)]
90 pub fn clear(&mut self) -> Result<()> {
91 if let Err(_) = self.0.clear() {
92 Err(JsValue::from_str(crate::js::errors::ANOTHER_TX))
93 } else {
94 Ok(())
95 }
96 }
97
98 #[wasm_bindgen(js_name = stopCapturing)]
99 pub fn stop_capturing(&mut self) {
100 self.0.reset()
101 }
102
103 #[wasm_bindgen(js_name = undo)]
104 pub fn undo(&mut self) -> Result<()> {
105 if let Err(_) = self.0.undo() {
106 Err(JsValue::from_str(crate::js::errors::ANOTHER_TX))
107 } else {
108 Ok(())
109 }
110 }
111
112 #[wasm_bindgen(js_name = redo)]
113 pub fn redo(&mut self) -> Result<()> {
114 if let Err(_) = self.0.redo() {
115 Err(JsValue::from_str(crate::js::errors::ANOTHER_TX))
116 } else {
117 Ok(())
118 }
119 }
120
121 #[wasm_bindgen(getter, js_name = canUndo)]
122 pub fn can_undo(&mut self) -> bool {
123 self.0.can_undo()
124 }
125
126 #[wasm_bindgen(getter, js_name = canRedo)]
127 pub fn can_redo(&mut self) -> bool {
128 self.0.can_redo()
129 }
130
131 #[wasm_bindgen(js_name = on)]
132 pub fn on(&mut self, event: &str, callback: js_sys::Function) -> crate::Result<()> {
133 let abi = callback.subscription_key();
134 match event {
135 "stack-item-added" => self.0.observe_item_added_with(abi, move |txn, e| {
136 let event: JsValue = YUndoEvent::new(e).into();
137 let txn: JsValue = YTransaction::from_ref(txn).into();
138 callback.call2(&JsValue::UNDEFINED, &event, &txn).unwrap();
139 let meta =
140 Reflect::get(&event, &JsValue::from_str("meta")).unwrap_or(JsValue::UNDEFINED);
141 *e.meta_mut() = meta;
142 }),
143 "stack-item-popped" => self.0.observe_item_popped_with(abi, move |txn, e| {
144 let event: JsValue = YUndoEvent::new(e).into();
145 let txn: JsValue = YTransaction::from_ref(txn).into();
146 callback.call2(&JsValue::UNDEFINED, &event, &txn).unwrap();
147 let meta =
148 Reflect::get(&event, &JsValue::from_str("meta")).unwrap_or(JsValue::UNDEFINED);
149 *e.meta_mut() = meta;
150 }),
151 "stack-item-updated" => self.0.observe_item_updated_with(abi, move |txn, e| {
152 let event: JsValue = YUndoEvent::new(e).into();
153 let txn: JsValue = YTransaction::from_ref(txn).into();
154 callback.call2(&JsValue::UNDEFINED, &event, &txn).unwrap();
155 let meta =
156 Reflect::get(&event, &JsValue::from_str("meta")).unwrap_or(JsValue::UNDEFINED);
157 *e.meta_mut() = meta;
158 }),
159 unknown => return Err(JsValue::from_str(&format!("Unknown event: {}", unknown))),
160 }
161 Ok(())
162 }
163
164 #[wasm_bindgen(js_name = off)]
165 pub fn off(&mut self, event: &str, callback: js_sys::Function) -> crate::Result<bool> {
166 let abi = callback.subscription_key();
167 match event {
168 "stack-item-added" => Ok(self.0.unobserve_item_added(abi)),
169 "stack-item-popped" => Ok(self.0.unobserve_item_popped(abi)),
170 "stack-item-updated" => Ok(self.0.unobserve_item_updated(abi)),
171 unknown => Err(JsValue::from_str(&format!("Unknown event: {}", unknown))),
172 }
173 }
174}
175
176#[wasm_bindgen]
177pub struct YUndoEvent {
178 origin: JsValue,
179 kind: JsValue,
180 meta: JsValue,
181}
182
183#[wasm_bindgen]
184impl YUndoEvent {
185 #[wasm_bindgen(getter, js_name = origin)]
186 pub fn origin(&self) -> JsValue {
187 self.origin.clone()
188 }
189 #[wasm_bindgen(getter, js_name = kind)]
190 pub fn kind(&self) -> JsValue {
191 self.kind.clone()
192 }
193
194 #[wasm_bindgen(getter, js_name = meta)]
195 pub fn meta(&self) -> JsValue {
196 self.meta.clone()
197 }
198
199 #[wasm_bindgen(setter, js_name = meta)]
200 pub fn set_meta(&mut self, value: &JsValue) {
201 self.meta = value.clone();
202 }
203
204 fn new(e: &yrs::undo::Event<JsValue>) -> Self {
205 let origin = if let Some(origin) = e.origin() {
206 Js::from(origin).into()
207 } else {
208 JsValue::UNDEFINED
209 };
210 YUndoEvent {
211 meta: e.meta().clone(),
212 origin,
213 kind: match e.kind() {
214 EventKind::Undo => JsValue::from_str("undo"),
215 EventKind::Redo => JsValue::from_str("redo"),
216 },
217 }
218 }
219}