leptos_use/
use_drop_zone.rs

1use crate::core::IntoElementMaybeSignal;
2use cfg_if::cfg_if;
3use default_struct_builder::DefaultBuilder;
4use leptos::prelude::*;
5use leptos::reactive::wrappers::read::Signal;
6use send_wrapper::SendWrapper;
7use std::fmt::{Debug, Formatter};
8use std::sync::Arc;
9
10cfg_if! { if #[cfg(not(feature = "ssr"))] {
11    use crate::use_event_listener;
12    use leptos::ev::{dragenter, dragleave, dragover, drop};
13}}
14
15/// Create a zone where files can be dropped.
16///
17/// ## Demo
18///
19/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_drop_zone)
20///
21/// ## Usage
22///
23/// ```
24/// # use leptos::prelude::*;
25/// # use leptos::html::Div;
26/// # use leptos_use::{use_drop_zone_with_options, UseDropZoneOptions, UseDropZoneReturn};
27/// #
28/// # #[component]
29/// # fn Demo() -> impl IntoView {
30/// let drop_zone_el = NodeRef::<Div>::new();
31///
32/// let on_drop = |event| {
33///     // called when files are dropped on zone
34/// };
35///
36/// let UseDropZoneReturn {
37///     is_over_drop_zone,
38///     ..
39/// } = use_drop_zone_with_options(
40///     drop_zone_el,
41///     UseDropZoneOptions::default().on_drop(on_drop)
42/// );
43///
44/// view! {
45///     <div node_ref=drop_zone_el>
46///         "Drop files here"
47///     </div>
48/// }
49/// # }
50/// ```
51///
52/// ## Server-Side Rendering
53///
54/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
55///
56/// On the server the returned `file` signal always contains an empty `Vec` and
57/// `is_over_drop_zone` contains always `false`
58pub fn use_drop_zone<El, M>(target: El) -> UseDropZoneReturn
59where
60    El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
61{
62    use_drop_zone_with_options(target, UseDropZoneOptions::default())
63}
64
65/// Version of [`use_drop_zone`] that takes a `UseDropZoneOptions`. See [`use_drop_zone`] for how to use.
66#[cfg_attr(feature = "ssr", allow(unused_variables))]
67pub fn use_drop_zone_with_options<El, M>(
68    target: El,
69    options: UseDropZoneOptions,
70) -> UseDropZoneReturn
71where
72    El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
73{
74    let (is_over_drop_zone, set_over_drop_zone) = signal(false);
75    let (files, set_files) = signal(Vec::<SendWrapper<web_sys::File>>::new());
76
77    #[cfg(not(feature = "ssr"))]
78    {
79        use std::ops::Deref;
80
81        let UseDropZoneOptions {
82            on_drop,
83            on_enter,
84            on_leave,
85            on_over,
86        } = options;
87
88        let counter = StoredValue::new(0_usize);
89
90        let update_files = move |event: &web_sys::DragEvent| {
91            if let Some(data_transfer) = event.data_transfer() {
92                let files: Vec<_> = data_transfer
93                    .files()
94                    .map(|f| js_sys::Array::from(&f).to_vec())
95                    .unwrap_or_default()
96                    .into_iter()
97                    .map(web_sys::File::from)
98                    .map(SendWrapper::new)
99                    .collect();
100
101                set_files.update(move |f| *f = files);
102            }
103        };
104
105        let target = target.into_element_maybe_signal();
106
107        let use_drop_zone_event = move |event| UseDropZoneEvent {
108            files: files
109                .read_untracked()
110                .iter()
111                .map(|f| f.deref().clone())
112                .collect(),
113            event,
114        };
115
116        let _ = use_event_listener(target, dragenter, move |event| {
117            event.prevent_default();
118            counter.update_value(|counter| *counter += 1);
119            set_over_drop_zone.set(true);
120
121            update_files(&event);
122
123            #[cfg(debug_assertions)]
124            let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
125
126            on_enter(use_drop_zone_event(event));
127        });
128
129        let _ = use_event_listener(target, dragover, move |event| {
130            event.prevent_default();
131            update_files(&event);
132
133            #[cfg(debug_assertions)]
134            let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
135
136            on_over(use_drop_zone_event(event));
137        });
138
139        let _ = use_event_listener(target, dragleave, move |event| {
140            event.prevent_default();
141            counter.update_value(|counter| *counter -= 1);
142            if counter.get_value() == 0 {
143                set_over_drop_zone.set(false);
144            }
145
146            update_files(&event);
147
148            #[cfg(debug_assertions)]
149            let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
150
151            on_leave(use_drop_zone_event(event));
152        });
153
154        let _ = use_event_listener(target, drop, move |event| {
155            event.prevent_default();
156            counter.update_value(|counter| *counter = 0);
157            set_over_drop_zone.set(false);
158
159            update_files(&event);
160
161            #[cfg(debug_assertions)]
162            let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
163
164            on_drop(use_drop_zone_event(event));
165        });
166    }
167
168    UseDropZoneReturn {
169        files: files.into(),
170        is_over_drop_zone: is_over_drop_zone.into(),
171    }
172}
173
174/// Options for [`use_drop_zone_with_options`].
175#[derive(DefaultBuilder, Clone)]
176#[cfg_attr(feature = "ssr", allow(dead_code))]
177pub struct UseDropZoneOptions {
178    /// Event handler for the [`drop`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event) event
179    on_drop: Arc<dyn Fn(UseDropZoneEvent) + Send + Sync>,
180    /// Event handler for the [`dragenter`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dragenter_event) event
181    on_enter: Arc<dyn Fn(UseDropZoneEvent) + Send + Sync>,
182    /// Event handler for the [`dragleave`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dragleave_event) event
183    on_leave: Arc<dyn Fn(UseDropZoneEvent) + Send + Sync>,
184    /// Event handler for the [`dragover`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dragover_event) event
185    on_over: Arc<dyn Fn(UseDropZoneEvent) + Send + Sync>,
186}
187
188impl Default for UseDropZoneOptions {
189    fn default() -> Self {
190        Self {
191            on_drop: Arc::new(|_| {}),
192            on_enter: Arc::new(|_| {}),
193            on_leave: Arc::new(|_| {}),
194            on_over: Arc::new(|_| {}),
195        }
196    }
197}
198
199impl Debug for UseDropZoneOptions {
200    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
201        write!(f, "UseDropZoneOptions")
202    }
203}
204
205/// Event passed as argument to the event handler functions of `UseDropZoneOptions`.
206#[derive(Clone, Debug)]
207pub struct UseDropZoneEvent {
208    /// Files being handled
209    pub files: Vec<web_sys::File>,
210    /// The original drag event
211    pub event: web_sys::DragEvent,
212}
213
214/// Return type of [`use_drop_zone`].
215#[derive(Clone, Copy)]
216pub struct UseDropZoneReturn {
217    /// Files being handled
218    pub files: Signal<Vec<SendWrapper<web_sys::File>>>,
219    /// Whether the files (dragged by the pointer) are over the drop zone
220    pub is_over_drop_zone: Signal<bool>,
221}