use crate::core::{ElementMaybeSignal, MaybeRwSignal, PointerType, Position}; use crate::use_event_listener_with_options; use crate::utils::{CloneableFnMutWithArg, CloneableFnWithArgAndReturn}; use default_struct_builder::DefaultBuilder; use leptos::ev::{pointerdown, pointermove, pointerup}; use leptos::*; use std::marker::PhantomData; use wasm_bindgen::JsCast; use web_sys::PointerEvent; /// /// /// ## Demo /// /// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_draggable) /// /// ## Usage /// /// ``` /// # use leptos::*; /// use leptos::html::Div; /// # use leptos_use::{use_draggable_with_options, UseDraggableOptions, UseDraggableReturn}; /// use leptos_use::core::Position; /// # /// # #[component] /// # fn Demo(cx: Scope) -> impl IntoView { /// let el = create_node_ref::
(cx); /// /// // `style` is a helper string "left: {x}px; top: {y}px;" /// let UseDraggableReturn { /// x, /// y, /// style, /// .. /// } = use_draggable_with_options( /// cx, /// el, /// UseDraggableOptions::default().initial_value(Position { x: 40.0, y: 40.0 }), /// ); /// /// view! { cx, ///
/// Drag me! I am at { x }, { y } ///
/// } /// # } /// ``` pub fn use_draggable(cx: Scope, target: El) -> UseDraggableReturn where (Scope, El): Into>, T: Into + Clone + 'static, { use_draggable_with_options::< El, T, web_sys::EventTarget, web_sys::EventTarget, web_sys::EventTarget, web_sys::EventTarget, >(cx, target, UseDraggableOptions::default()) } /// Version of [`use_draggable`] that takes a `UseDraggableOptions`. See [`use_draggable`] for how to use. pub fn use_draggable_with_options( cx: Scope, target: El, options: UseDraggableOptions, ) -> UseDraggableReturn where (Scope, El): Into>, T: Into + Clone + 'static, DragEl: Clone, (Scope, DragEl): Into>, DragT: Into + Clone + 'static, (Scope, HandleEl): Into>, HandleT: Into + Clone + 'static, { let UseDraggableOptions { exact, prevent_default, stop_propagation, dragging_element, handle, pointer_types, initial_value, on_start, on_move, on_end, .. } = options; let target = (cx, target).into(); let dragging_handle = if let Some(handle) = handle { let handle = (cx, handle).into(); Signal::derive(cx, move || handle.get().map(|handle| handle.into())) } else { let target = target.clone(); Signal::derive(cx, move || target.get().map(|target| target.into())) }; let (position, set_position) = initial_value.into_signal(cx); let (start_position, set_start_position) = create_signal(cx, None::); 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.into().unchecked_into(); if exact.get_untracked() && event_target::(&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(), }; if !on_start.clone()(UseDraggableCallbackArgs { position, event: event.clone(), }) { return; } 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); on_move.clone()(UseDraggableCallbackArgs { position, event: event.clone(), }); 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); on_end.clone()(UseDraggableCallbackArgs { position: position.get_untracked(), event: event.clone(), }); handle_event(event); }; let mut listener_options = web_sys::AddEventListenerOptions::new(); listener_options.capture(true); let _ = use_event_listener_with_options( cx, dragging_handle, pointerdown, on_pointer_down, listener_options.clone(), ); let _ = use_event_listener_with_options( cx, dragging_element.clone(), pointermove, on_pointer_move, listener_options.clone(), ); let _ = use_event_listener_with_options( cx, dragging_element, pointerup, on_pointer_up, listener_options, ); UseDraggableReturn { x: Signal::derive(cx, move || position.get().x), y: Signal::derive(cx, move || position.get().y), position, set_position, is_dragging: Signal::derive(cx, move || start_position.get().is_some()), style: Signal::derive(cx, move || { let position = position.get(); format!("left: {}px; top: {}px;", position.x, position.y) }), } } /// Options for [`use_draggable_with_options`]. #[derive(DefaultBuilder)] pub struct UseDraggableOptions where (Scope, DragEl): Into>, DragT: Into + Clone + 'static, (Scope, HandleEl): Into>, HandleT: Into + Clone + 'static, { /// Only start the dragging when click on the element directly. Defaults to `false`. #[builder(into)] exact: MaybeSignal, /// Prevent events defaults. Defaults to `false`. #[builder(into)] prevent_default: MaybeSignal, /// Prevent events propagation. Defaults to `false`. #[builder(into)] stop_propagation: MaybeSignal, /// Element to attach `pointermove` and `pointerup` events to. Defaults to `window`. dragging_element: DragEl, /// Handle that triggers the drag event. Defaults to `target`. handle: Option, /// Pointer types that listen to. Defaults to `[Mouse, Touch, Pen]`. pointer_types: Vec, /// Initial position of the element. Defaults to `{ x: 0, y: 0 }`. #[builder(into)] initial_value: MaybeRwSignal, /// Callback when the dragging starts. Return `false` to prevent dragging. on_start: Box>, /// Callback during dragging. on_move: Box>, /// Callback when dragging end. on_end: Box>, #[builder(skip)] _marker1: PhantomData, #[builder(skip)] _marker2: PhantomData, } impl Default for UseDraggableOptions< web_sys::EventTarget, web_sys::EventTarget, web_sys::EventTarget, web_sys::EventTarget, > { fn default() -> Self { Self { exact: MaybeSignal::default(), prevent_default: MaybeSignal::default(), stop_propagation: MaybeSignal::default(), dragging_element: window().into(), handle: None, pointer_types: vec![PointerType::Mouse, PointerType::Touch, PointerType::Pen], initial_value: MaybeRwSignal::default(), on_start: Box::new(|_| true), on_move: Box::new(|_| {}), on_end: Box::new(|_| {}), _marker1: PhantomData, _marker2: PhantomData, } } } /// Argument for the `on_...` handler functions of [`UseDraggableOptions`]. pub struct UseDraggableCallbackArgs { /// Position of the `target` element pub position: Position, /// Original `PointerEvent` from the event listener pub event: PointerEvent, } /// Return type of [`use_draggable`]. pub struct UseDraggableReturn { /// X coordinate of the element pub x: Signal, /// Y coordinate of the element pub y: Signal, /// Position of the element pub position: Signal, /// Set the position of the element manually pub set_position: WriteSignal, /// Whether the element is being dragged pub is_dragging: Signal, /// Style attribute "left: {x}px; top: {y}px;" pub style: Signal, }