use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
mod js {
use wasm_bindgen::prelude::*;
#[wasm_bindgen(module = "/sortable.esm.js")]
extern "C" {
#[wasm_bindgen(extends = js_sys::Object)]
pub type Sortable;
#[wasm_bindgen(constructor)]
pub fn new(elt: &web_sys::Element, opts: &js_sys::Object) -> Sortable;
#[wasm_bindgen(method)]
pub fn destroy(item: &Sortable);
}
}
#[derive(Clone, Debug)]
pub struct Event {
pub raw_event: js_sys::Object,
pub to: web_sys::HtmlElement,
pub from: web_sys::HtmlElement,
pub item: web_sys::HtmlElement,
pub clone: web_sys::HtmlElement,
pub old_index: Option<usize>,
pub new_index: Option<usize>,
pub old_draggable_index: Option<usize>,
pub new_draggable_index: Option<usize>,
}
impl Event {
fn from_raw_event(raw_event: js_sys::Object) -> Event {
macro_rules! get {
($field:expr) => {
js_sys::Reflect::get(&raw_event, &JsValue::from_str($field))
.expect("failed retrieving field from raw event")
.dyn_into()
.expect("failed casting field of raw event to proper type")
};
}
macro_rules! get_optint {
($field:expr) => {
js_sys::Reflect::get(&raw_event, &JsValue::from_str($field))
.ok()
.map(|evt| {
let float = evt
.as_f64()
.expect("failed casting field of raw event to proper type");
let int = float as usize;
assert!(
(int as f64 - float).abs() < 0.1,
"received index that is not an integer: {}",
float
);
int
})
};
}
Event {
to: get!("to"),
from: get!("from"),
item: get!("item"),
clone: get!("clone"),
old_index: get_optint!("oldIndex"),
new_index: get_optint!("newIndex"),
old_draggable_index: get_optint!("oldDraggableIndex"),
new_draggable_index: get_optint!("newDraggableIndex"),
raw_event,
}
}
}
#[derive(Clone, Debug)]
pub struct MoveEvent {
pub raw_event: js_sys::Object,
pub to: web_sys::HtmlElement,
pub from: web_sys::HtmlElement,
pub dragged: web_sys::HtmlElement,
pub related: web_sys::HtmlElement,
pub will_insert_after: bool,
}
impl MoveEvent {
fn from_raw_event(raw_event: js_sys::Object) -> MoveEvent {
macro_rules! get {
($field:expr) => {
js_sys::Reflect::get(&raw_event, &JsValue::from_str($field))
.expect("failed retrieving field from raw event")
.dyn_into()
.expect("failed casting field of raw event to proper type")
};
}
let will_insert_after =
js_sys::Reflect::get(&raw_event, &JsValue::from_str("willInsertAfter"))
.expect("failed retrieving field from raw event")
.as_bool()
.expect("willInsertAfter was not a boolean");
MoveEvent {
to: get!("to"),
from: get!("from"),
dragged: get!("dragged"),
related: get!("related"),
will_insert_after,
raw_event,
}
}
}
pub enum MoveResponse {
Cancel,
InsertBefore,
InsertAfter,
InsertDefault,
}
#[repr(usize)]
enum CallbackId {
Choose,
Unchoose,
Start,
End,
Add,
Update,
Sort,
Remove,
Filter,
Clone,
Change,
Spill,
_Total,
}
pub struct Options {
options: js_sys::Object,
callbacks: [Option<Rc<Closure<dyn FnMut(js_sys::Object)>>>; CallbackId::_Total as usize],
on_move_cb: Option<Rc<Closure<dyn FnMut(js_sys::Object, js_sys::Object) -> JsValue>>>,
}
macro_rules! option {
( $setter:ident, $jsname:expr, $typ:ty, $builder:ident ) => {
pub fn $setter(&mut self, value: $typ) -> &mut Options {
let res = js_sys::Reflect::set(
&self.options,
&JsValue::from_str($jsname),
&JsValue::$builder(value),
)
.expect("setting property on object failed");
assert!(res, "failed setting property on object");
self
}
};
}
macro_rules! callback {
( $setter:ident, $jsname:expr, $id:ident ) => {
pub fn $setter(&mut self, mut cb: impl 'static + FnMut(Event)) -> &Options {
let cb = Closure::new(move |e: js_sys::Object| cb(Event::from_raw_event(e)));
let res = js_sys::Reflect::set(&self.options, &JsValue::from_str($jsname), cb.as_ref())
.expect("setting callback on object failed");
assert!(res, "failed setting callback on object");
self.callbacks[CallbackId::$id as usize] = Some(Rc::new(cb));
self
}
};
}
impl Options {
pub fn new() -> Options {
Options {
options: js_sys::Object::new(),
callbacks: std::array::from_fn(|_| None),
on_move_cb: None,
}
}
option!(group, "group", &str, from_str);
option!(sort, "sort", bool, from_bool);
option!(delay, "delay", f64, from_f64);
option!(delay_on_touch_only, "delayOnTouchOnly", bool, from_bool);
option!(touch_start_threshold, "touchStartThreshold", f64, from_f64);
option!(disabled, "disabled", bool, from_bool);
option!(animation_ms, "animation", f64, from_f64);
option!(easing, "easing", &str, from_str);
option!(handle, "handle", &str, from_str);
option!(filter, "filter", &str, from_str);
option!(prevent_on_filter, "preventOnFilter", bool, from_bool);
option!(draggable, "draggable", &str, from_str);
option!(data_id_attr, "dataIdAttr", &str, from_str);
option!(ghost_class, "ghostClass", &str, from_str);
option!(chosen_class, "chosenClass", &str, from_str);
option!(drag_class, "dragClass", &str, from_str);
option!(swap_threshold, "swapThreshold", f64, from_f64);
option!(invert_swap, "invertSwap", bool, from_bool);
option!(
inverted_swap_threshold,
"invertedSwapThreshold",
f64,
from_f64
);
option!(direction, "direction", &str, from_str);
option!(force_fallback, "forceFallback", bool, from_bool);
option!(fallback_class, "fallbackClass", &str, from_str);
option!(fallback_on_body, "fallbackOnBody", bool, from_bool);
option!(fallback_tolerance, "fallbackTolerance", f64, from_f64);
option!(dragover_bubble, "dragoverBubble", bool, from_bool);
option!(remove_clone_on_hide, "removeCloneOnHide", bool, from_bool);
option!(
empty_insert_threshold,
"emptyInsertThreshold",
f64,
from_f64
);
option!(revert_dom, "revertDOM", bool, from_bool);
callback!(on_choose, "onChoose", Choose);
callback!(on_unchoose, "onUnchoose", Unchoose);
callback!(on_start, "onStart", Start);
callback!(on_end, "onEnd", End);
callback!(on_add, "onAdd", Add);
callback!(on_update, "onUpdate", Update);
callback!(on_sort, "onSort", Sort);
callback!(on_remove, "onRemove", Remove);
callback!(on_filter, "onFilter", Filter);
callback!(on_clone, "onClone", Clone);
callback!(on_change, "onChange", Change);
pub fn on_move(
&mut self,
mut cb: impl 'static + FnMut(MoveEvent, js_sys::Object) -> MoveResponse,
) -> &Options {
let cb = Closure::new(move |evt: js_sys::Object, original_evt: js_sys::Object| {
match cb(MoveEvent::from_raw_event(evt), original_evt) {
MoveResponse::Cancel => JsValue::FALSE,
MoveResponse::InsertBefore => JsValue::from_f64(-1.),
MoveResponse::InsertAfter => JsValue::from_f64(1.),
MoveResponse::InsertDefault => JsValue::TRUE,
}
});
let res = js_sys::Reflect::set(&self.options, &JsValue::from_str("onMove"), cb.as_ref())
.expect("setting callback on object failed");
assert!(res, "failed setting callback on object");
self.on_move_cb = Some(Rc::new(cb));
self
}
option!(revert_on_spill, "revertOnSpill", bool, from_bool);
option!(remove_on_spill, "removeOnSpill", bool, from_bool);
callback!(on_spill, "onSpill", Spill);
option!(scroll, "scroll", bool, from_bool);
option!(force_autoscroll_fallback, "forceAutoscrollFallback", bool, from_bool);
option!(scroll_sensitivity_px, "scrollSensitivity", f64, from_f64);
option!(scroll_speed_px, "scrollSpeed", f64, from_f64);
option!(bubble_scroll, "bubbleScroll", bool, from_bool);
pub fn options(&self) -> &js_sys::Object {
&self.options
}
pub fn apply(&self, elt: &web_sys::Element) -> Sortable {
let sortable = js::Sortable::new(elt, &self.options);
let object_ref: &js_sys::Object = sortable.as_ref();
let raw_object = object_ref.clone();
Sortable {
raw_object,
sortable,
_callbacks: self.callbacks.clone(),
_on_move_cb: self.on_move_cb.clone(),
}
}
}
pub struct Sortable {
pub raw_object: js_sys::Object,
sortable: js::Sortable,
_callbacks: [Option<Rc<Closure<dyn FnMut(js_sys::Object)>>>; CallbackId::_Total as usize],
_on_move_cb: Option<Rc<Closure<dyn FnMut(js_sys::Object, js_sys::Object) -> JsValue>>>,
}
impl Drop for Sortable {
fn drop(&mut self) {
self.sortable.destroy();
}
}