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 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#[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 #[builder(into)]
299 exact: Signal<bool>,
300
301 #[builder(into)]
303 prevent_default: Signal<bool>,
304
305 #[builder(into)]
307 stop_propagation: Signal<bool>,
308
309 dragging_element: DragEl,
311
312 handle: Option<HandleEl>,
314
315 pointer_types: Vec<PointerType>,
317
318 #[builder(into)]
320 initial_value: MaybeRwSignal<Position>,
321
322 target_offset: Arc<dyn Fn(web_sys::EventTarget) -> (f64, f64)>,
325
326 on_start: Arc<dyn Fn(UseDraggableCallbackArgs) -> bool + Send + Sync>,
328
329 on_move: Arc<dyn Fn(UseDraggableCallbackArgs) + Send + Sync>,
331
332 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
370pub struct UseDraggableCallbackArgs {
372 pub position: Position,
374 pub event: PointerEvent,
376}
377
378pub struct UseDraggableReturn {
380 pub x: Signal<f64>,
382 pub y: Signal<f64>,
384 pub position: Signal<Position>,
386 pub set_position: WriteSignal<Position>,
388 pub is_dragging: Signal<bool>,
390 pub style: Signal<String>,
392}