use crate::core::{IntoElementMaybeSignal, MaybeRwSignal, PointerType, Position};
use crate::{use_event_listener_with_options, use_window, UseEventListenerOptions, UseWindow};
use default_struct_builder::DefaultBuilder;
use leptos::ev::{pointerdown, pointermove, pointerup};
use leptos::prelude::*;
use leptos::reactive::wrappers::read::Signal;
use std::marker::PhantomData;
use std::sync::Arc;
use wasm_bindgen::JsCast;
use web_sys::PointerEvent;
pub fn use_draggable<El, M>(target: El) -> UseDraggableReturn
where
El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
{
use_draggable_with_options::<El, M, _, _, _, _>(target, UseDraggableOptions::default())
}
pub fn use_draggable_with_options<El, M, DragEl, DragM, HandleEl, HandleM>(
target: El,
options: UseDraggableOptions<DragEl, DragM, HandleEl, HandleM>,
) -> UseDraggableReturn
where
El: IntoElementMaybeSignal<web_sys::EventTarget, M>,
DragEl: IntoElementMaybeSignal<web_sys::EventTarget, DragM>,
HandleEl: IntoElementMaybeSignal<web_sys::EventTarget, HandleM>,
{
let UseDraggableOptions {
exact,
prevent_default,
stop_propagation,
dragging_element,
handle,
pointer_types,
initial_value,
on_start,
on_move,
on_end,
..
} = options;
let target = target.into_element_maybe_signal();
let dragging_handle = if let Some(handle) = handle {
handle.into_element_maybe_signal()
} else {
target
};
let (position, set_position) = initial_value.into_signal();
let (start_position, set_start_position) = signal(None::<Position>);
let filter_event = move |event: &PointerEvent| {
let ty = event.pointer_type();
pointer_types.iter().any(|p| p.to_string() == ty)
};
let handle_event = move |event: PointerEvent| {
if prevent_default.get_untracked() {
event.prevent_default();
}
if stop_propagation.get_untracked() {
event.stop_propagation();
}
};
let on_pointer_down = {
let filter_event = filter_event.clone();
move |event: PointerEvent| {
if !filter_event(&event) {
return;
}
if let Some(target) = target.get_untracked() {
let target: web_sys::Element = target.unchecked_into();
if exact.get_untracked() && event_target::<web_sys::Element>(&event) != target {
return;
}
let rect = target.get_bounding_client_rect();
let position = Position {
x: event.client_x() as f64 - rect.left(),
y: event.client_y() as f64 - rect.top(),
};
#[cfg(debug_assertions)]
let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
if !on_start(UseDraggableCallbackArgs {
position,
event: event.clone(),
}) {
#[cfg(debug_assertions)]
drop(zone);
return;
}
#[cfg(debug_assertions)]
drop(zone);
set_start_position.set(Some(position));
handle_event(event);
}
}
};
let on_pointer_move = {
let filter_event = filter_event.clone();
move |event: PointerEvent| {
if !filter_event(&event) {
return;
}
if let Some(start_position) = start_position.get_untracked() {
let position = Position {
x: event.client_x() as f64 - start_position.x,
y: event.client_y() as f64 - start_position.y,
};
set_position.set(position);
#[cfg(debug_assertions)]
let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
on_move(UseDraggableCallbackArgs {
position,
event: event.clone(),
});
#[cfg(debug_assertions)]
drop(zone);
handle_event(event);
}
}
};
let on_pointer_up = move |event: PointerEvent| {
if !filter_event(&event) {
return;
}
if start_position.get_untracked().is_none() {
return;
}
set_start_position.set(None);
#[cfg(debug_assertions)]
let zone = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
on_end(UseDraggableCallbackArgs {
position: position.get_untracked(),
event: event.clone(),
});
#[cfg(debug_assertions)]
drop(zone);
handle_event(event);
};
let dragging_element = dragging_element.into_element_maybe_signal();
let listener_options = UseEventListenerOptions::default().capture(true);
let _ = use_event_listener_with_options(
dragging_handle,
pointerdown,
on_pointer_down,
listener_options,
);
let _ = use_event_listener_with_options(
dragging_element,
pointermove,
on_pointer_move,
listener_options,
);
let _ = use_event_listener_with_options(
dragging_element,
pointerup,
on_pointer_up,
listener_options,
);
UseDraggableReturn {
x: Signal::derive(move || position.get().x),
y: Signal::derive(move || position.get().y),
position,
set_position,
is_dragging: Signal::derive(move || start_position.get().is_some()),
style: Signal::derive(move || {
let position = position.get();
format!("left: {}px; top: {}px;", position.x, position.y)
}),
}
}
#[derive(DefaultBuilder)]
pub struct UseDraggableOptions<DragEl, DragM, HandleEl, HandleM>
where
DragEl: IntoElementMaybeSignal<web_sys::EventTarget, DragM>,
HandleEl: IntoElementMaybeSignal<web_sys::EventTarget, HandleM>,
{
#[builder(into)]
exact: Signal<bool>,
#[builder(into)]
prevent_default: Signal<bool>,
#[builder(into)]
stop_propagation: Signal<bool>,
dragging_element: DragEl,
handle: Option<HandleEl>,
pointer_types: Vec<PointerType>,
#[builder(into)]
initial_value: MaybeRwSignal<Position>,
on_start: Arc<dyn Fn(UseDraggableCallbackArgs) -> bool + Send + Sync>,
on_move: Arc<dyn Fn(UseDraggableCallbackArgs) + Send + Sync>,
on_end: Arc<dyn Fn(UseDraggableCallbackArgs) + Send + Sync>,
#[builder(skip)]
_marker1: PhantomData<DragM>,
#[builder(skip)]
_marker2: PhantomData<HandleM>,
}
impl<DragM, HandleM> Default
for UseDraggableOptions<UseWindow, DragM, Option<web_sys::EventTarget>, HandleM>
where
UseWindow: IntoElementMaybeSignal<web_sys::EventTarget, DragM>,
Option<web_sys::EventTarget>: IntoElementMaybeSignal<web_sys::EventTarget, HandleM>,
{
fn default() -> Self {
Self {
exact: Signal::default(),
prevent_default: Signal::default(),
stop_propagation: Signal::default(),
dragging_element: use_window(),
handle: None,
pointer_types: vec![PointerType::Mouse, PointerType::Touch, PointerType::Pen],
initial_value: MaybeRwSignal::default(),
on_start: Arc::new(|_| true),
on_move: Arc::new(|_| {}),
on_end: Arc::new(|_| {}),
_marker1: PhantomData,
_marker2: PhantomData,
}
}
}
pub struct UseDraggableCallbackArgs {
pub position: Position,
pub event: PointerEvent,
}
pub struct UseDraggableReturn {
pub x: Signal<f64>,
pub y: Signal<f64>,
pub position: Signal<Position>,
pub set_position: WriteSignal<Position>,
pub is_dragging: Signal<bool>,
pub style: Signal<String>,
}