ywasm/
undo.rs

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}