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
12pub 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
55pub 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 position = 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,
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(position));
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 listener_options = UseEventListenerOptions::default().capture(true);
205
206 let _ = use_event_listener_with_options(
207 dragging_handle,
208 pointerdown,
209 on_pointer_down,
210 listener_options,
211 );
212 let _ = use_event_listener_with_options(
213 dragging_element,
214 pointermove,
215 on_pointer_move,
216 listener_options,
217 );
218 let _ = use_event_listener_with_options(
219 dragging_element,
220 pointerup,
221 on_pointer_up,
222 listener_options,
223 );
224
225 UseDraggableReturn {
226 x: Signal::derive(move || position.get().x),
227 y: Signal::derive(move || position.get().y),
228 position,
229 set_position,
230 is_dragging: Signal::derive(move || start_position.get().is_some()),
231 style: Signal::derive(move || {
232 let position = position.get();
233 format!("left: {}px; top: {}px;", position.x, position.y)
234 }),
235 }
236}
237
238#[derive(DefaultBuilder)]
240pub struct UseDraggableOptions<DragEl, DragM, HandleEl, HandleM>
241where
242 DragEl: IntoElementMaybeSignal<web_sys::EventTarget, DragM>,
243 HandleEl: IntoElementMaybeSignal<web_sys::EventTarget, HandleM>,
244{
245 #[builder(into)]
247 exact: Signal<bool>,
248
249 #[builder(into)]
251 prevent_default: Signal<bool>,
252
253 #[builder(into)]
255 stop_propagation: Signal<bool>,
256
257 dragging_element: DragEl,
259
260 handle: Option<HandleEl>,
262
263 pointer_types: Vec<PointerType>,
265
266 #[builder(into)]
268 initial_value: MaybeRwSignal<Position>,
269
270 target_offset: Arc<dyn Fn(web_sys::EventTarget) -> (f64, f64)>,
273
274 on_start: Arc<dyn Fn(UseDraggableCallbackArgs) -> bool + Send + Sync>,
276
277 on_move: Arc<dyn Fn(UseDraggableCallbackArgs) + Send + Sync>,
279
280 on_end: Arc<dyn Fn(UseDraggableCallbackArgs) + Send + Sync>,
282
283 #[builder(skip)]
284 _marker1: PhantomData<DragM>,
285 #[builder(skip)]
286 _marker2: PhantomData<HandleM>,
287}
288
289impl<DragM, HandleM> Default
290 for UseDraggableOptions<UseWindow, DragM, Option<web_sys::EventTarget>, HandleM>
291where
292 UseWindow: IntoElementMaybeSignal<web_sys::EventTarget, DragM>,
293 Option<web_sys::EventTarget>: IntoElementMaybeSignal<web_sys::EventTarget, HandleM>,
294{
295 fn default() -> Self {
296 Self {
297 exact: Signal::default(),
298 prevent_default: Signal::default(),
299 stop_propagation: Signal::default(),
300 dragging_element: use_window(),
301 handle: None,
302 pointer_types: vec![PointerType::Mouse, PointerType::Touch, PointerType::Pen],
303 initial_value: MaybeRwSignal::default(),
304 target_offset: Arc::new(|target: web_sys::EventTarget| {
305 let target: web_sys::Element = target.unchecked_into();
306 let rect = target.get_bounding_client_rect();
307 (rect.left(), rect.top())
308 }),
309 on_start: Arc::new(|_| true),
310 on_move: Arc::new(|_| {}),
311 on_end: Arc::new(|_| {}),
312 _marker1: PhantomData,
313 _marker2: PhantomData,
314 }
315 }
316}
317
318pub struct UseDraggableCallbackArgs {
320 pub position: Position,
322 pub event: PointerEvent,
324}
325
326pub struct UseDraggableReturn {
328 pub x: Signal<f64>,
330 pub y: Signal<f64>,
332 pub position: Signal<Position>,
334 pub set_position: WriteSignal<Position>,
336 pub is_dragging: Signal<bool>,
338 pub style: Signal<String>,
340}