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 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#[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 #[builder(into)]
244 exact: Signal<bool>,
245
246 #[builder(into)]
248 prevent_default: Signal<bool>,
249
250 #[builder(into)]
252 stop_propagation: Signal<bool>,
253
254 dragging_element: DragEl,
256
257 handle: Option<HandleEl>,
259
260 pointer_types: Vec<PointerType>,
262
263 #[builder(into)]
265 initial_value: MaybeRwSignal<Position>,
266
267 on_start: Arc<dyn Fn(UseDraggableCallbackArgs) -> bool + Send + Sync>,
269
270 on_move: Arc<dyn Fn(UseDraggableCallbackArgs) + Send + Sync>,
272
273 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
306pub struct UseDraggableCallbackArgs {
308 pub position: Position,
310 pub event: PointerEvent,
312}
313
314pub struct UseDraggableReturn {
316 pub x: Signal<f64>,
318 pub y: Signal<f64>,
320 pub position: Signal<Position>,
322 pub set_position: WriteSignal<Position>,
324 pub is_dragging: Signal<bool>,
326 pub style: Signal<String>,
328}