leptos_use/
use_draggable.rs

1use crate::core::{IntoElementMaybeSignal, MaybeRwSignal, PointerType, Position};
2use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions, UseWindow};
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        on_start,
74        on_move,
75        on_end,
76        ..
77    } = options;
78
79    let target = target.into_element_maybe_signal();
80
81    let dragging_handle = if let Some(handle) = handle {
82        handle.into_element_maybe_signal()
83    } else {
84        target
85    };
86
87    let (position, set_position) = initial_value.into_signal();
88    let (start_position, set_start_position) = signal(None::<Position>);
89
90    let filter_event = move |event: &PointerEvent| {
91        let ty = event.pointer_type();
92        pointer_types.iter().any(|p| p.to_string() == ty)
93    };
94
95    let handle_event = move |event: PointerEvent| {
96        if prevent_default.get_untracked() {
97            event.prevent_default();
98        }
99        if stop_propagation.get_untracked() {
100            event.stop_propagation();
101        }
102    };
103
104    let on_pointer_down = {
105        let filter_event = filter_event.clone();
106
107        move |event: PointerEvent| {
108            if !filter_event(&event) {
109                return;
110            }
111
112            if let Some(target) = target.get_untracked() {
113                let target: web_sys::Element = target.unchecked_into();
114
115                if exact.get_untracked() && event_target::<web_sys::Element>(&event) != target {
116                    return;
117                }
118
119                let rect = target.get_bounding_client_rect();
120                let position = Position {
121                    x: event.client_x() as f64 - rect.left(),
122                    y: event.client_y() as f64 - rect.top(),
123                };
124
125                #[cfg(debug_assertions)]
126                let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
127
128                if !on_start(UseDraggableCallbackArgs {
129                    position,
130                    event: event.clone(),
131                }) {
132                    #[cfg(debug_assertions)]
133                    drop(zone);
134                    return;
135                }
136
137                #[cfg(debug_assertions)]
138                drop(zone);
139
140                set_start_position.set(Some(position));
141                handle_event(event);
142            }
143        }
144    };
145
146    let on_pointer_move = {
147        let filter_event = filter_event.clone();
148
149        move |event: PointerEvent| {
150            if !filter_event(&event) {
151                return;
152            }
153            if let Some(start_position) = start_position.get_untracked() {
154                let position = Position {
155                    x: event.client_x() as f64 - start_position.x,
156                    y: event.client_y() as f64 - start_position.y,
157                };
158                set_position.set(position);
159
160                #[cfg(debug_assertions)]
161                let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
162
163                on_move(UseDraggableCallbackArgs {
164                    position,
165                    event: event.clone(),
166                });
167
168                #[cfg(debug_assertions)]
169                drop(zone);
170
171                handle_event(event);
172            }
173        }
174    };
175
176    let on_pointer_up = move |event: PointerEvent| {
177        if !filter_event(&event) {
178            return;
179        }
180        if start_position.get_untracked().is_none() {
181            return;
182        }
183        set_start_position.set(None);
184
185        #[cfg(debug_assertions)]
186        let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
187
188        on_end(UseDraggableCallbackArgs {
189            position: position.get_untracked(),
190            event: event.clone(),
191        });
192
193        #[cfg(debug_assertions)]
194        drop(zone);
195
196        handle_event(event);
197    };
198
199    let dragging_element = dragging_element.into_element_maybe_signal();
200
201    let listener_options = UseEventListenerOptions::default().capture(true);
202
203    let _ = use_event_listener_with_options(
204        dragging_handle,
205        pointerdown,
206        on_pointer_down,
207        listener_options,
208    );
209    let _ = use_event_listener_with_options(
210        dragging_element,
211        pointermove,
212        on_pointer_move,
213        listener_options,
214    );
215    let _ = use_event_listener_with_options(
216        dragging_element,
217        pointerup,
218        on_pointer_up,
219        listener_options,
220    );
221
222    UseDraggableReturn {
223        x: Signal::derive(move || position.get().x),
224        y: Signal::derive(move || position.get().y),
225        position,
226        set_position,
227        is_dragging: Signal::derive(move || start_position.get().is_some()),
228        style: Signal::derive(move || {
229            let position = position.get();
230            format!("left: {}px; top: {}px;", position.x, position.y)
231        }),
232    }
233}
234
235/// Options for [`use_draggable_with_options`].
236#[derive(DefaultBuilder)]
237pub struct UseDraggableOptions<DragEl, DragM, HandleEl, HandleM>
238where
239    DragEl: IntoElementMaybeSignal<web_sys::EventTarget, DragM>,
240    HandleEl: IntoElementMaybeSignal<web_sys::EventTarget, HandleM>,
241{
242    /// Only start the dragging when click on the element directly. Defaults to `false`.
243    #[builder(into)]
244    exact: Signal<bool>,
245
246    /// Prevent events defaults. Defaults to `false`.
247    #[builder(into)]
248    prevent_default: Signal<bool>,
249
250    /// Prevent events propagation. Defaults to `false`.
251    #[builder(into)]
252    stop_propagation: Signal<bool>,
253
254    /// Element to attach `pointermove` and `pointerup` events to. Defaults to `window`.
255    dragging_element: DragEl,
256
257    /// Handle that triggers the drag event. Defaults to `target`.
258    handle: Option<HandleEl>,
259
260    /// Pointer types that listen to. Defaults to `[Mouse, Touch, Pen]`.
261    pointer_types: Vec<PointerType>,
262
263    /// Initial position of the element. Defaults to `{ x: 0, y: 0 }`.
264    #[builder(into)]
265    initial_value: MaybeRwSignal<Position>,
266
267    /// Callback when the dragging starts. Return `false` to prevent dragging.
268    on_start: Arc<dyn Fn(UseDraggableCallbackArgs) -> bool + Send + Sync>,
269
270    /// Callback during dragging.
271    on_move: Arc<dyn Fn(UseDraggableCallbackArgs) + Send + Sync>,
272
273    /// Callback when dragging end.
274    on_end: Arc<dyn Fn(UseDraggableCallbackArgs) + Send + Sync>,
275
276    #[builder(skip)]
277    _marker1: PhantomData<DragM>,
278    #[builder(skip)]
279    _marker2: PhantomData<HandleM>,
280}
281
282impl<DragM, HandleM> Default
283    for UseDraggableOptions<UseWindow, DragM, Option<web_sys::EventTarget>, HandleM>
284where
285    UseWindow: IntoElementMaybeSignal<web_sys::EventTarget, DragM>,
286    Option<web_sys::EventTarget>: IntoElementMaybeSignal<web_sys::EventTarget, HandleM>,
287{
288    fn default() -> Self {
289        Self {
290            exact: Signal::default(),
291            prevent_default: Signal::default(),
292            stop_propagation: Signal::default(),
293            dragging_element: use_window(),
294            handle: None,
295            pointer_types: vec![PointerType::Mouse, PointerType::Touch, PointerType::Pen],
296            initial_value: MaybeRwSignal::default(),
297            on_start: Arc::new(|_| true),
298            on_move: Arc::new(|_| {}),
299            on_end: Arc::new(|_| {}),
300            _marker1: PhantomData,
301            _marker2: PhantomData,
302        }
303    }
304}
305
306/// Argument for the `on_...` handler functions of [`UseDraggableOptions`].
307pub struct UseDraggableCallbackArgs {
308    /// Position of the `target` element
309    pub position: Position,
310    /// Original `PointerEvent` from the event listener
311    pub event: PointerEvent,
312}
313
314/// Return type of [`use_draggable`].
315pub struct UseDraggableReturn {
316    /// X coordinate of the element
317    pub x: Signal<f64>,
318    /// Y coordinate of the element
319    pub y: Signal<f64>,
320    /// Position of the element
321    pub position: Signal<Position>,
322    /// Set the position of the element manually
323    pub set_position: WriteSignal<Position>,
324    /// Whether the element is being dragged
325    pub is_dragging: Signal<bool>,
326    /// Style attribute "left: {x}px; top: {y}px;"
327    pub style: Signal<String>,
328}