sortable_js/
lib.rs

1//! This crate provides rusty bindings to SortableJS.
2//!
3//! The documentation mostly lives with [the official SortableJS documentation](https://github.com/SortableJS/Sortable).
4//!
5//! Just adding this crate as a dependency should be enough to get everything working when using [trunk](https://trunkrs.dev/). Just be careful to keep alive the return value of `Sortable::apply`, or you will get JavaScript exceptions.
6//!
7//! You can find example usage of SortableJS from a pure-Rust WASM application
8//! in the `examples/` directory.
9
10use std::rc::Rc;
11
12use wasm_bindgen::closure::Closure;
13use wasm_bindgen::{JsCast, JsValue};
14
15mod js {
16    use wasm_bindgen::prelude::*;
17
18    #[wasm_bindgen(module = "/sortable.esm.js")]
19    extern "C" {
20        #[wasm_bindgen(extends = js_sys::Object)]
21        pub type Sortable;
22
23        #[wasm_bindgen(constructor)]
24        pub fn new(elt: &web_sys::Element, opts: &js_sys::Object) -> Sortable;
25
26        #[wasm_bindgen(method)]
27        pub fn destroy(item: &Sortable);
28    }
29}
30
31/// An event raised by one of the Sortable callbacks. See [the official documentation](https://github.com/SortableJS/Sortable#event-object-demo) for details about the fields.
32///
33/// `raw_event` contains the raw JS event, should additional non-documented
34/// fields be needed.
35#[derive(Clone, Debug)]
36pub struct Event {
37    pub raw_event: js_sys::Object,
38    pub to: web_sys::HtmlElement,
39    pub from: web_sys::HtmlElement,
40    pub item: web_sys::HtmlElement,
41    pub clone: web_sys::HtmlElement,
42    pub old_index: Option<usize>,
43    pub new_index: Option<usize>,
44    pub old_draggable_index: Option<usize>,
45    pub new_draggable_index: Option<usize>,
46    // TODO: pullMode
47}
48
49impl Event {
50    fn from_raw_event(raw_event: js_sys::Object) -> Event {
51        macro_rules! get {
52            ($field:expr) => {
53                js_sys::Reflect::get(&raw_event, &JsValue::from_str($field))
54                    .expect("failed retrieving field from raw event")
55                    .dyn_into()
56                    .expect("failed casting field of raw event to proper type")
57            };
58        }
59        macro_rules! get_optint {
60            ($field:expr) => {
61                js_sys::Reflect::get(&raw_event, &JsValue::from_str($field))
62                    .ok()
63                    .map(|evt| {
64                        let float = evt
65                            .as_f64()
66                            .expect("failed casting field of raw event to proper type");
67                        let int = float as usize;
68                        assert!(
69                            (int as f64 - float).abs() < 0.1,
70                            "received index that is not an integer: {}",
71                            float
72                        );
73                        int
74                    })
75            };
76        }
77        Event {
78            to: get!("to"),
79            from: get!("from"),
80            item: get!("item"),
81            clone: get!("clone"),
82            old_index: get_optint!("oldIndex"),
83            new_index: get_optint!("newIndex"),
84            old_draggable_index: get_optint!("oldDraggableIndex"),
85            new_draggable_index: get_optint!("newDraggableIndex"),
86            raw_event,
87        }
88    }
89}
90
91/// An event raised by one of the Sortable callbacks. See [the official documentation](https://github.com/SortableJS/Sortable/#move-event-object) for details about the fields.
92///
93/// `raw_event` contains the raw JS event, should additional non-documented
94/// fields be needed.
95#[derive(Clone, Debug)]
96pub struct MoveEvent {
97    pub raw_event: js_sys::Object,
98    pub to: web_sys::HtmlElement,
99    pub from: web_sys::HtmlElement,
100    pub dragged: web_sys::HtmlElement,
101    // TODO: cast fails? pub dragged_rect: web_sys::DomRect,
102    pub related: web_sys::HtmlElement,
103    // TODO: cast fails? pub related_rect: web_sys::DomRect,
104    pub will_insert_after: bool,
105}
106
107impl MoveEvent {
108    fn from_raw_event(raw_event: js_sys::Object) -> MoveEvent {
109        macro_rules! get {
110            ($field:expr) => {
111                js_sys::Reflect::get(&raw_event, &JsValue::from_str($field))
112                    .expect("failed retrieving field from raw event")
113                    .dyn_into()
114                    .expect("failed casting field of raw event to proper type")
115            };
116        }
117        let will_insert_after =
118            js_sys::Reflect::get(&raw_event, &JsValue::from_str("willInsertAfter"))
119                .expect("failed retrieving field from raw event")
120                .as_bool()
121                .expect("willInsertAfter was not a boolean");
122        MoveEvent {
123            to: get!("to"),
124            from: get!("from"),
125            dragged: get!("dragged"),
126            // dragged_rect: get!("draggedRect"),
127            related: get!("related"),
128            // related_rect: get!("relatedRect"),
129            will_insert_after,
130            raw_event,
131        }
132    }
133}
134
135pub enum MoveResponse {
136    Cancel,
137    InsertBefore,
138    InsertAfter,
139    InsertDefault,
140}
141
142#[repr(usize)]
143enum CallbackId {
144    Choose,
145    Unchoose,
146    Start,
147    End,
148    Add,
149    Update,
150    Sort,
151    Remove,
152    Filter,
153    Clone,
154    Change,
155    Spill,
156    _Total,
157}
158
159/// See https://github.com/SortableJS/Sortable for more documentation about available options
160pub struct Options {
161    options: js_sys::Object,
162    callbacks: [Option<Rc<Closure<dyn FnMut(js_sys::Object)>>>; CallbackId::_Total as usize],
163    on_move_cb: Option<Rc<Closure<dyn FnMut(js_sys::Object, js_sys::Object) -> JsValue>>>,
164}
165
166macro_rules! option {
167    ( $setter:ident, $jsname:expr, $typ:ty, $builder:ident ) => {
168        pub fn $setter(&mut self, value: $typ) -> &mut Options {
169            let res = js_sys::Reflect::set(
170                &self.options,
171                &JsValue::from_str($jsname),
172                &JsValue::$builder(value),
173            )
174            .expect("setting property on object failed");
175            assert!(res, "failed setting property on object");
176            self
177        }
178    };
179}
180
181macro_rules! callback {
182    ( $setter:ident, $jsname:expr, $id:ident ) => {
183        pub fn $setter(&mut self, mut cb: impl 'static + FnMut(Event)) -> &Options {
184            let cb = Closure::new(move |e: js_sys::Object| cb(Event::from_raw_event(e)));
185            let res = js_sys::Reflect::set(&self.options, &JsValue::from_str($jsname), cb.as_ref())
186                .expect("setting callback on object failed");
187            assert!(res, "failed setting callback on object");
188            self.callbacks[CallbackId::$id as usize] = Some(Rc::new(cb));
189            self
190        }
191    };
192}
193
194impl Options {
195    /// Create a builder for `Sortable`
196    ///
197    /// This builder allows for configuration options to be set. Once the
198    /// desired configuration is done, use `apply` to apply it to a list.
199    pub fn new() -> Options {
200        Options {
201            options: js_sys::Object::new(),
202            callbacks: std::array::from_fn(|_| None),
203            on_move_cb: None,
204        }
205    }
206
207    option!(group, "group", &str, from_str);
208    option!(sort, "sort", bool, from_bool);
209    option!(delay, "delay", f64, from_f64);
210    option!(delay_on_touch_only, "delayOnTouchOnly", bool, from_bool);
211    option!(touch_start_threshold, "touchStartThreshold", f64, from_f64);
212    option!(disabled, "disabled", bool, from_bool);
213    // TODO: consider supporting the Store option
214    option!(animation_ms, "animation", f64, from_f64);
215    option!(easing, "easing", &str, from_str);
216    option!(handle, "handle", &str, from_str);
217    option!(filter, "filter", &str, from_str);
218    option!(prevent_on_filter, "preventOnFilter", bool, from_bool);
219    option!(draggable, "draggable", &str, from_str);
220
221    option!(data_id_attr, "dataIdAttr", &str, from_str);
222
223    option!(ghost_class, "ghostClass", &str, from_str);
224    option!(chosen_class, "chosenClass", &str, from_str);
225    option!(drag_class, "dragClass", &str, from_str);
226
227    option!(swap_threshold, "swapThreshold", f64, from_f64);
228    option!(invert_swap, "invertSwap", bool, from_bool);
229    option!(
230        inverted_swap_threshold,
231        "invertedSwapThreshold",
232        f64,
233        from_f64
234    );
235    option!(direction, "direction", &str, from_str);
236
237    option!(force_fallback, "forceFallback", bool, from_bool);
238
239    option!(fallback_class, "fallbackClass", &str, from_str);
240    option!(fallback_on_body, "fallbackOnBody", bool, from_bool);
241    option!(fallback_tolerance, "fallbackTolerance", f64, from_f64);
242
243    option!(dragover_bubble, "dragoverBubble", bool, from_bool);
244    option!(remove_clone_on_hide, "removeCloneOnHide", bool, from_bool);
245    option!(
246        empty_insert_threshold,
247        "emptyInsertThreshold",
248        f64,
249        from_f64
250    );
251    option!(revert_dom, "revertDOM", bool, from_bool);
252
253    callback!(on_choose, "onChoose", Choose);
254    callback!(on_unchoose, "onUnchoose", Unchoose);
255    callback!(on_start, "onStart", Start);
256    callback!(on_end, "onEnd", End);
257    callback!(on_add, "onAdd", Add);
258    callback!(on_update, "onUpdate", Update);
259    callback!(on_sort, "onSort", Sort);
260    callback!(on_remove, "onRemove", Remove);
261    callback!(on_filter, "onFilter", Filter);
262    callback!(on_clone, "onClone", Clone);
263    callback!(on_change, "onChange", Change);
264
265    pub fn on_move(
266        &mut self,
267        mut cb: impl 'static + FnMut(MoveEvent, js_sys::Object) -> MoveResponse,
268    ) -> &Options {
269        let cb = Closure::new(move |evt: js_sys::Object, original_evt: js_sys::Object| {
270            match cb(MoveEvent::from_raw_event(evt), original_evt) {
271                MoveResponse::Cancel => JsValue::FALSE,
272                MoveResponse::InsertBefore => JsValue::from_f64(-1.),
273                MoveResponse::InsertAfter => JsValue::from_f64(1.),
274                MoveResponse::InsertDefault => JsValue::TRUE,
275            }
276        });
277        let res = js_sys::Reflect::set(&self.options, &JsValue::from_str("onMove"), cb.as_ref())
278            .expect("setting callback on object failed");
279        assert!(res, "failed setting callback on object");
280        self.on_move_cb = Some(Rc::new(cb));
281        self
282    }
283
284    // RevertOnSpill / RemoveOnSpill plugins
285    option!(revert_on_spill, "revertOnSpill", bool, from_bool);
286    option!(remove_on_spill, "removeOnSpill", bool, from_bool);
287    callback!(on_spill, "onSpill", Spill);
288
289    // AutoScroll plugin
290    option!(scroll, "scroll", bool, from_bool);
291    option!(force_autoscroll_fallback, "forceAutoscrollFallback", bool, from_bool);
292    option!(scroll_sensitivity_px, "scrollSensitivity", f64, from_f64);
293    option!(scroll_speed_px, "scrollSpeed", f64, from_f64);
294    option!(bubble_scroll, "bubbleScroll", bool, from_bool);
295    // TODO: scrollFn
296
297    /// Recover the javascript options that are being built in this object
298    ///
299    /// Note that you can set options on this object through `js_sys::Reflect`.
300    /// This allows setting options that are not planned for by
301    /// `sortable-js-rs`.
302    pub fn options(&self) -> &js_sys::Object {
303        &self.options
304    }
305
306    /// Apply the current configuration as a `Sortable` instance on `elt`
307    ///
308    /// Do not forget to keep the return value of this function alive for as
309    /// long as you want the callbacks to be callable, as JS code will error out
310    /// should an event happen after it was dropped.
311    pub fn apply(&self, elt: &web_sys::Element) -> Sortable {
312        let sortable = js::Sortable::new(elt, &self.options);
313        let object_ref: &js_sys::Object = sortable.as_ref();
314        let raw_object = object_ref.clone();
315        Sortable {
316            raw_object,
317            sortable,
318            _callbacks: self.callbacks.clone(),
319            _on_move_cb: self.on_move_cb.clone(),
320        }
321    }
322}
323
324/// Data related to the Sortable instance
325///
326/// When it is dropped, the list is made non-sortable again. This is required
327/// because callbacks could be called otherwise. If it is a problem for you, you
328/// can leak it, but be aware of the fact that it is a leak.
329pub struct Sortable {
330    /// Raw Sortable JS object, should this crate not expose the necessary
331    /// methods
332    pub raw_object: js_sys::Object,
333
334    /// raw_object but with the proper type
335    sortable: js::Sortable,
336
337    /// Keep the callbacks alive
338    _callbacks: [Option<Rc<Closure<dyn FnMut(js_sys::Object)>>>; CallbackId::_Total as usize],
339    _on_move_cb: Option<Rc<Closure<dyn FnMut(js_sys::Object, js_sys::Object) -> JsValue>>>,
340}
341
342impl Drop for Sortable {
343    fn drop(&mut self) {
344        self.sortable.destroy();
345    }
346}