Skip to main content

leptos_use/
use_draggable.rs

1use crate::core::{IntoElementMaybeSignal, MaybeRwSignal, PointerType, Position};
2use crate::{UseEventListenerOptions, UseWindow, use_event_listener_with_options, use_window};
3use default_struct_builder::DefaultBuilder;
4use leptos::ev::{pointerdown, pointermove, pointerup};
5use leptos::prelude::*;
6use leptos::reactive::wrappers::read::Signal;
7use std::marker::PhantomData;
8use std::sync::Arc;
9use wasm_bindgen::JsCast;
10use web_sys::PointerEvent;
11
12/// Make elements draggable.
13///
14/// ## Demo
15///
16/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_draggable)
17///
18/// ## Usage
19///
20/// ```
21/// # use leptos::prelude::*;
22/// # use leptos::html::Div;
23/// # use leptos_use::{use_draggable_with_options, UseDraggableOptions, UseDraggableReturn};
24/// # use leptos_use::core::Position;
25/// #
26/// # #[component]
27/// # fn Demo() -> impl IntoView {
28/// let el = NodeRef::<Div>::new();
29///
30/// // `style` is a helper string "left: {x}px; top: {y}px;"
31/// let UseDraggableReturn {
32///     x,
33///     y,
34///     style,
35///     ..
36/// } = use_draggable_with_options(
37///     el,
38///     UseDraggableOptions::default().initial_value(Position { x: 40.0, y: 40.0 }),
39/// );
40///
41/// view! {
42///     <div node_ref=el style=move || format!("position: fixed; {}", style.get())>
43///         Drag me! I am at { x }, { y }
44///     </div>
45/// }
46/// # }
47/// ```
48pub fn use_draggable<El, M>(target: El) -> UseDraggableReturn
49where
50    El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
51{
52    use_draggable_with_options::<El, M, _, _, _, _>(target, UseDraggableOptions::default())
53}
54
55/// Version of [`use_draggable`] that takes a `UseDraggableOptions`. See [`use_draggable`] for how to use.
56pub fn use_draggable_with_options<El, M, DragEl, DragM, HandleEl, HandleM>(
57    target: El,
58    options: UseDraggableOptions<DragEl, DragM, HandleEl, HandleM>,
59) -> UseDraggableReturn
60where
61    El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
62    DragEl: IntoElementMaybeSignal<web_sys::EventTarget, DragM>,
63    HandleEl: IntoElementMaybeSignal<web_sys::EventTarget, HandleM>,
64{
65    let UseDraggableOptions {
66        exact,
67        prevent_default,
68        stop_propagation,
69        dragging_element,
70        handle,
71        pointer_types,
72        initial_value,
73        target_offset,
74        on_start,
75        on_move,
76        on_end,
77        ..
78    } = options;
79
80    let target = target.into_element_maybe_signal();
81
82    let dragging_handle = if let Some(handle) = handle {
83        handle.into_element_maybe_signal()
84    } else {
85        target
86    };
87
88    let (position, set_position) = initial_value.into_signal();
89    let (start_position, set_start_position) = signal(None::<Position>);
90
91    let filter_event = move |event: &PointerEvent| {
92        let ty = event.pointer_type();
93        pointer_types.iter().any(|p| p.to_string() == ty)
94    };
95
96    let handle_event = move |event: PointerEvent| {
97        if prevent_default.get_untracked() {
98            event.prevent_default();
99        }
100        if stop_propagation.get_untracked() {
101            event.stop_propagation();
102        }
103    };
104
105    let on_pointer_down = {
106        let filter_event = filter_event.clone();
107
108        move |event: PointerEvent| {
109            if !filter_event(&event) {
110                return;
111            }
112
113            if let Some(target) = target.get_untracked() {
114                let (x, y) = target_offset(target.unchecked_ref::<web_sys::EventTarget>().clone());
115                let target: &web_sys::Element = target.unchecked_ref();
116
117                if exact.get_untracked() && event_target::<web_sys::Element>(&event) != *target {
118                    return;
119                }
120
121                #[allow(clippy::unnecessary_cast)]
122                let grab_offset = Position {
123                    x: event.client_x() as f64 - x,
124                    y: event.client_y() as f64 - y,
125                };
126
127                #[cfg(debug_assertions)]
128                let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
129
130                if !on_start(UseDraggableCallbackArgs {
131                    position: Position { x, y },
132                    event: event.clone(),
133                }) {
134                    #[cfg(debug_assertions)]
135                    drop(zone);
136                    return;
137                }
138
139                #[cfg(debug_assertions)]
140                drop(zone);
141
142                set_start_position.set(Some(grab_offset));
143                handle_event(event);
144            }
145        }
146    };
147
148    let on_pointer_move = {
149        let filter_event = filter_event.clone();
150
151        move |event: PointerEvent| {
152            if !filter_event(&event) {
153                return;
154            }
155            if let Some(start_position) = start_position.get_untracked() {
156                #[allow(clippy::unnecessary_cast)]
157                let position = Position {
158                    x: event.client_x() as f64 - start_position.x,
159                    y: event.client_y() as f64 - start_position.y,
160                };
161                set_position.set(position);
162
163                #[cfg(debug_assertions)]
164                let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
165
166                on_move(UseDraggableCallbackArgs {
167                    position,
168                    event: event.clone(),
169                });
170
171                #[cfg(debug_assertions)]
172                drop(zone);
173
174                handle_event(event);
175            }
176        }
177    };
178
179    let on_pointer_up = move |event: PointerEvent| {
180        if !filter_event(&event) {
181            return;
182        }
183        if start_position.get_untracked().is_none() {
184            return;
185        }
186        set_start_position.set(None);
187
188        #[cfg(debug_assertions)]
189        let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
190
191        on_end(UseDraggableCallbackArgs {
192            position: position.get_untracked(),
193            event: event.clone(),
194        });
195
196        #[cfg(debug_assertions)]
197        drop(zone);
198
199        handle_event(event);
200    };
201
202    let dragging_element = dragging_element.into_element_maybe_signal();
203
204    let passive_dragging_handle = Signal::derive(move || {
205        if prevent_default.get() {
206            None
207        } else {
208            dragging_handle.get()
209        }
210    });
211    let non_passive_dragging_handle = Signal::derive(move || {
212        if prevent_default.get() {
213            dragging_handle.get()
214        } else {
215            None
216        }
217    });
218    let passive_dragging_element = Signal::derive(move || {
219        if prevent_default.get() {
220            None
221        } else {
222            dragging_element.get()
223        }
224    });
225    let non_passive_dragging_element = Signal::derive(move || {
226        if prevent_default.get() {
227            dragging_element.get()
228        } else {
229            None
230        }
231    });
232
233    let passive_listener_options = UseEventListenerOptions::default()
234        .capture(true)
235        .passive(true);
236    let non_passive_listener_options = UseEventListenerOptions::default()
237        .capture(true)
238        .passive(false);
239
240    let _ = use_event_listener_with_options(
241        passive_dragging_handle,
242        pointerdown,
243        on_pointer_down.clone(),
244        passive_listener_options,
245    );
246    let _ = use_event_listener_with_options(
247        non_passive_dragging_handle,
248        pointerdown,
249        on_pointer_down,
250        non_passive_listener_options,
251    );
252    let _ = use_event_listener_with_options(
253        passive_dragging_element,
254        pointermove,
255        on_pointer_move.clone(),
256        passive_listener_options,
257    );
258    let _ = use_event_listener_with_options(
259        non_passive_dragging_element,
260        pointermove,
261        on_pointer_move,
262        non_passive_listener_options,
263    );
264    let _ = use_event_listener_with_options(
265        passive_dragging_element,
266        pointerup,
267        on_pointer_up.clone(),
268        passive_listener_options,
269    );
270    let _ = use_event_listener_with_options(
271        non_passive_dragging_element,
272        pointerup,
273        on_pointer_up,
274        non_passive_listener_options,
275    );
276
277    UseDraggableReturn {
278        x: Signal::derive(move || position.get().x),
279        y: Signal::derive(move || position.get().y),
280        position,
281        set_position,
282        is_dragging: Signal::derive(move || start_position.get().is_some()),
283        style: Signal::derive(move || {
284            let position = position.get();
285            format!("left: {}px; top: {}px;", position.x, position.y)
286        }),
287    }
288}
289
290/// Options for [`use_draggable_with_options`].
291#[derive(DefaultBuilder)]
292pub struct UseDraggableOptions<DragEl, DragM, HandleEl, HandleM>
293where
294    DragEl: IntoElementMaybeSignal<web_sys::EventTarget, DragM>,
295    HandleEl: IntoElementMaybeSignal<web_sys::EventTarget, HandleM>,
296{
297    /// Only start the dragging when click on the element directly. Defaults to `false`.
298    #[builder(into)]
299    exact: Signal<bool>,
300
301    /// Prevent events defaults. Defaults to `false`.
302    #[builder(into)]
303    prevent_default: Signal<bool>,
304
305    /// Prevent events propagation. Defaults to `false`.
306    #[builder(into)]
307    stop_propagation: Signal<bool>,
308
309    /// Element to attach `pointermove` and `pointerup` events to. Defaults to `window`.
310    dragging_element: DragEl,
311
312    /// Handle that triggers the drag event. Defaults to `target`.
313    handle: Option<HandleEl>,
314
315    /// Pointer types that listen to. Defaults to `[Mouse, Touch, Pen]`.
316    pointer_types: Vec<PointerType>,
317
318    /// Initial position of the element. Defaults to `{ x: 0, y: 0 }`.
319    #[builder(into)]
320    initial_value: MaybeRwSignal<Position>,
321
322    /// Computes the initial offset of the target element for drag positioning.
323    /// Defaults to using its bounding client rectangle's left and top values.
324    target_offset: Arc<dyn Fn(web_sys::EventTarget) -> (f64, f64)>,
325
326    /// Callback when the dragging starts. Return `false` to prevent dragging.
327    on_start: Arc<dyn Fn(UseDraggableCallbackArgs) -> bool + Send + Sync>,
328
329    /// Callback during dragging.
330    on_move: Arc<dyn Fn(UseDraggableCallbackArgs) + Send + Sync>,
331
332    /// Callback when dragging end.
333    on_end: Arc<dyn Fn(UseDraggableCallbackArgs) + Send + Sync>,
334
335    #[builder(skip)]
336    _marker1: PhantomData<DragM>,
337    #[builder(skip)]
338    _marker2: PhantomData<HandleM>,
339}
340
341impl<DragM, HandleM> Default
342    for UseDraggableOptions<UseWindow, DragM, Option<web_sys::EventTarget>, HandleM>
343where
344    UseWindow: IntoElementMaybeSignal<web_sys::EventTarget, DragM>,
345    Option<web_sys::EventTarget>: IntoElementMaybeSignal<web_sys::EventTarget, HandleM>,
346{
347    fn default() -> Self {
348        Self {
349            exact: Signal::default(),
350            prevent_default: Signal::default(),
351            stop_propagation: Signal::default(),
352            dragging_element: use_window(),
353            handle: None,
354            pointer_types: vec![PointerType::Mouse, PointerType::Touch, PointerType::Pen],
355            initial_value: MaybeRwSignal::default(),
356            target_offset: Arc::new(|target: web_sys::EventTarget| {
357                let target: web_sys::Element = target.unchecked_into();
358                let rect = target.get_bounding_client_rect();
359                (rect.left(), rect.top())
360            }),
361            on_start: Arc::new(|_| true),
362            on_move: Arc::new(|_| {}),
363            on_end: Arc::new(|_| {}),
364            _marker1: PhantomData,
365            _marker2: PhantomData,
366        }
367    }
368}
369
370/// Argument for the `on_...` handler functions of [`UseDraggableOptions`].
371pub struct UseDraggableCallbackArgs {
372    /// Position of the `target` element
373    pub position: Position,
374    /// Original `PointerEvent` from the event listener
375    pub event: PointerEvent,
376}
377
378/// Return type of [`use_draggable`].
379pub struct UseDraggableReturn {
380    /// X coordinate of the element
381    pub x: Signal<f64>,
382    /// Y coordinate of the element
383    pub y: Signal<f64>,
384    /// Position of the element
385    pub position: Signal<Position>,
386    /// Set the position of the element manually
387    pub set_position: WriteSignal<Position>,
388    /// Whether the element is being dragged
389    pub is_dragging: Signal<bool>,
390    /// Style attribute "left: {x}px; top: {y}px;"
391    pub style: Signal<String>,
392}