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
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.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#[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 #[builder(into)]
245 exact: Signal<bool>,
246
247 #[builder(into)]
249 prevent_default: Signal<bool>,
250
251 #[builder(into)]
253 stop_propagation: Signal<bool>,
254
255 dragging_element: DragEl,
257
258 handle: Option<HandleEl>,
260
261 pointer_types: Vec<PointerType>,
263
264 #[builder(into)]
266 initial_value: MaybeRwSignal<Position>,
267
268 target_offset: Arc<dyn Fn(web_sys::EventTarget) -> (f64, f64)>,
271
272 on_start: Arc<dyn Fn(UseDraggableCallbackArgs) -> bool + Send + Sync>,
274
275 on_move: Arc<dyn Fn(UseDraggableCallbackArgs) + Send + Sync>,
277
278 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
316pub struct UseDraggableCallbackArgs {
318 pub position: Position,
320 pub event: PointerEvent,
322}
323
324pub struct UseDraggableReturn {
326 pub x: Signal<f64>,
328 pub y: Signal<f64>,
330 pub position: Signal<Position>,
332 pub set_position: WriteSignal<Position>,
334 pub is_dragging: Signal<bool>,
336 pub style: Signal<String>,
338}