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