1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
use crate::FromTransfer;
use sycamore::{prelude::*, web::html::ev};
use web_sys::DragEvent;
/// The builder for the [`create_droppable`] options
pub struct DroppableBuilder<'cx, G: Html, T: FromTransfer + 'static = ()> {
scope: Scope<'cx>,
on_drop: Option<Box<dyn Fn(T) + 'cx>>,
#[allow(clippy::type_complexity)]
accept: Option<Box<dyn Fn(&T) -> bool + 'cx>>,
hovering_class: String,
node_ref: Option<&'cx NodeRef<G>>,
}
impl<'cx, G: Html, T: FromTransfer + 'static> DroppableBuilder<'cx, G, T> {
fn new(scope: Scope<'cx>) -> Self {
Self {
scope,
on_drop: None,
accept: None,
hovering_class: Default::default(),
node_ref: None,
}
}
/// Sets a callback to run when an item is dropped on this droppable element.
/// The argument is parsed from the item's [`DataTransfer`](web_sys::DataTransfer).
pub fn on_drop(mut self, f: impl Fn(T) + 'cx) -> Self {
self.on_drop = Some(Box::new(f));
self
}
/// A callback to check if the incoming [`DataTransfer`](web_sys::DataTransfer) should be accepted.
/// The argument is parsed from the item's [`DataTransfer`](web_sys::DataTransfer).
pub fn accept(mut self, f: impl Fn(&T) -> bool + 'cx) -> Self {
self.accept = Some(Box::new(f));
self
}
/// A class or list of classes to set when a valid item is hovering over the element.
/// They are automatically removed when the item leaves or is dropped.
pub fn hovering_class(mut self, class: impl Into<String>) -> Self {
self.hovering_class = class.into();
self
}
/// An existing [`NodeRef`] to use instead of creating a new one. Useful for combining drag and
/// drop on one element, or using your own logic that requires [`NodeRef`].
pub fn node_ref(mut self, node_ref: &'cx NodeRef<G>) -> Self {
self.node_ref = Some(node_ref);
self
}
/// Create the droppable logic. Returns a [`NodeRef`] that needs to be set as the element's `ref`
/// attribute.
pub fn build(self) -> &'cx NodeRef<G> {
let node = self.node_ref.unwrap_or_else(|| create_node_ref(self.scope));
create_droppable_effect(self.scope, self, node);
node
}
}
/// Create a drop zone for an element. The [`DroppableBuilder`] can be used to further configure the
/// drop zone.
///
/// # Example
///
/// ```
/// # use sycamore::prelude::*;
/// # use sycamore_dnd::*;
/// #[component]
/// fn DropZone<G: Html>(cx: Scope) -> View<G> {
/// let drop = create_droppable(cx)
/// .on_drop(move |name: String| {
/// log::trace!("{name} was dropped here");
/// })
/// .build();
///
/// view! { cx,
/// div(class = "drop-zone", ref = drop) {
/// "Drop here"
/// }
/// }
/// }
/// ```
pub fn create_droppable<G: Html, T: FromTransfer>(cx: Scope<'_>) -> DroppableBuilder<'_, G, T> {
DroppableBuilder::new(cx)
}
fn create_droppable_effect<'cx, G: Html, T: FromTransfer + 'static>(
cx: Scope<'cx>,
options: DroppableBuilder<'cx, G, T>,
node_ref: &'cx NodeRef<G>,
) {
// SAFETY: This is safe as long as the builder has no custom `Drop` implementation
// See documentation for `create_ref_unsafe`.
let options = unsafe { create_ref_unsafe(cx, options) };
create_effect(cx, move || {
if let Some(node) = node_ref.try_get_raw() {
let on_drag_enter = {
let node = node.clone();
move |e: DragEvent| {
log::trace!("Drag enter");
e.prevent_default();
let should_accept = options
.accept
.as_ref()
.map(|accept| {
if let Some(data) = T::from_transfer(&e.data_transfer().unwrap()) {
accept(&data)
} else {
false
}
})
.unwrap_or(true);
if should_accept {
node.add_class(&options.hovering_class);
}
}
};
let on_drag_leave = {
let node = node.clone();
move |e: DragEvent| {
e.prevent_default();
node.remove_class(&options.hovering_class);
log::trace!("Drag leave");
}
};
let on_drag_over = |e: DragEvent| {
let should_accept = if let Some(accept) = options.accept.as_ref() {
if let Some(data) = T::from_transfer(&e.data_transfer().unwrap()) {
accept(&data)
} else {
false
}
} else {
true
};
if should_accept {
e.prevent_default();
}
};
let on_drop = {
let node = node.clone();
move |e: DragEvent| {
log::trace!("Dropping");
node.remove_class(&options.hovering_class);
if let Some((on_drop, data)) = options
.on_drop
.as_ref()
.zip(T::from_transfer(&e.data_transfer().unwrap()))
{
if options
.accept
.as_ref()
.map(|accept| accept(&data))
.unwrap_or(true)
{
log::trace!("Data found and accepted, calling `on_drop`");
e.prevent_default();
on_drop(data);
}
}
}
};
node.event(cx, ev::dragenter, on_drag_enter);
node.event(cx, ev::dragleave, on_drag_leave);
node.event(cx, ev::dragover, on_drag_over);
node.event(cx, ev::drop, on_drop);
}
});
}