leptos_struct_table/components/
thead_drag.rs

1use std::{marker::PhantomData, sync::Arc};
2
3use leptos::prelude::*;
4use wasm_bindgen::JsCast;
5
6#[derive(Clone)]
7pub struct HeadDragHandler<Column>(pub(crate) Arc<dyn DragHandler<Column> + Send + Sync + 'static>);
8
9impl<Column> HeadDragHandler<Column>
10where
11    Column: Clone + PartialEq + Send + Sync + 'static,
12{
13    pub fn new<H>(handler: H) -> Self
14    where
15        H: DragHandler<Column> + Send + Sync + 'static,
16    {
17        Self(Arc::new(handler))
18    }
19}
20
21impl<Column> Default for HeadDragHandler<Column>
22where
23    Column: Clone + PartialEq + Send + Sync + 'static,
24{
25    fn default() -> Self {
26        Self(Arc::new(DefaultDragHandler::<Column>::default()))
27    }
28}
29
30/// Collection of event handlers needed to create a table-column dragging experience to reorder columns.
31pub trait DragHandler<Column>: Send + Sync
32where
33    Column: Clone + PartialEq + Send + Sync + 'static,
34{
35    /// Cursor is above **column** and dropped the column it was dragging.
36    fn received_drop(
37        &self,
38        drag_state: DragStateRwSignal<Column>,
39        columns: RwSignal<Vec<Column>>,
40        _column: Column,
41        _event: web_sys::DragEvent,
42    ) {
43        drag_state.update(|drag_state| {
44            if let Some(drag_state) = drag_state.take() {
45                columns.update(|columns| drag_state.reorder_columns(columns))
46            }
47        });
48    }
49
50    /// Cursor is moving above **column** while dragging.
51    fn dragging_over(
52        &self,
53        drag_state_carrier: DragStateRwSignal<Column>,
54        column: Column,
55        event: web_sys::DragEvent,
56    ) {
57        let Some(mut drag_state) = drag_state_carrier.get() else {
58            return;
59        };
60
61        // Prevent default stop to allow drop.
62        event.prevent_default();
63
64        let hovering_side = if let Some(target) = event.target() {
65            let Ok(thead) = target.dyn_into::<web_sys::HtmlTableCellElement>() else {
66                return;
67            };
68            let thead_rect = thead.get_bounding_client_rect();
69            let thead_center_x = thead_rect.x() + thead_rect.width() / 2.0;
70            let mouse_x = event.x();
71            if (mouse_x as f64) < thead_center_x {
72                DragSide::Left
73            } else {
74                DragSide::Right
75            }
76        } else {
77            // fallback
78            DragSide::Left
79        };
80
81        // Update state when the state changed.
82        if drag_state.hovering_over != column || drag_state.hovering_side != hovering_side {
83            drag_state.hovering_over = column;
84            drag_state.hovering_side = hovering_side;
85            *drag_state_carrier.write() = Some(drag_state);
86        }
87    }
88
89    /// Cursor moves out of **column**
90    fn drag_leave(
91        &self,
92        _drag_state: DragStateRwSignal<Column>,
93        _column: Column,
94        _event: web_sys::DragEvent,
95    ) {
96        // by default do nothing
97    }
98
99    /// Started dragging **column**.
100    fn drag_start(
101        &self,
102        drag_state: DragStateRwSignal<Column>,
103        column: Column,
104        _event: web_sys::DragEvent,
105    ) {
106        drag_state.set(Some(DragState {
107            grabbed: column.clone(),
108            hovering_over: column,
109            hovering_side: DragSide::Left,
110        }));
111    }
112
113    /// Dragging ended.
114    fn drag_end(
115        &self,
116        drag_state: DragStateRwSignal<Column>,
117        columns: RwSignal<Vec<Column>>,
118        _column: Column,
119        _event: web_sys::DragEvent,
120    ) {
121        drag_state.update(|drag_state| {
122            if let Some(drag_state) = drag_state.take() {
123                columns.update(|columns| drag_state.reorder_columns(columns))
124            }
125        });
126    }
127
128    /// Classes for columns.
129    /// Intended to react to drag events to show highlights via classes.
130    fn get_drag_classes(
131        &self,
132        drag_state: DragStateRwSignal<Column>,
133        column: Column,
134        columns: RwSignal<Vec<Column>>,
135    ) -> Signal<String> {
136        let grabbed_class = self.grabbed_class();
137        let hover_left_class = self.hover_left_class();
138        let hover_right_class = self.hover_right_class();
139
140        Signal::derive(move || {
141            let Some(drag_state) = drag_state.get() else {
142                return String::new();
143            };
144
145            if drag_state.grabbed == column {
146                grabbed_class.to_string()
147            } else if drag_state.hovering_over == column {
148                let mut resorted_cols = columns.get();
149
150                drag_state.reorder_columns(&mut resorted_cols);
151
152                if resorted_cols == *columns.read() {
153                    String::new()
154                } else {
155                    match drag_state.hovering_side {
156                        DragSide::Left => hover_left_class.to_string(),
157                        DragSide::Right => hover_right_class.to_string(),
158                    }
159                }
160            } else {
161                String::new()
162            }
163        })
164    }
165
166    fn grabbed_class(&self) -> &'static str {
167        "grabbed"
168    }
169
170    fn hover_left_class(&self) -> &'static str {
171        "hover-left"
172    }
173
174    fn hover_right_class(&self) -> &'static str {
175        "hover-right"
176    }
177}
178
179#[derive(Copy, Clone)]
180pub struct DefaultDragHandler<C>(PhantomData<C>);
181
182impl<C> DragHandler<C> for DefaultDragHandler<C> where C: Clone + PartialEq + Send + Sync + 'static {}
183
184impl<C> Default for DefaultDragHandler<C>
185where
186    C: Clone + PartialEq + Send + Sync + 'static,
187{
188    fn default() -> Self {
189        DefaultDragHandler(PhantomData)
190    }
191}
192
193pub type DragStateRwSignal<Column> = RwSignal<Option<DragState<Column>>>;
194
195#[derive(Clone, PartialEq)]
196pub struct DragState<Column> {
197    /// Column which is being dragged.
198    pub grabbed: Column,
199    /// Last column the cursor was over
200    pub hovering_over: Column,
201    /// On which column side of [hovering_over] the cursor is on.
202    /// Used for styling that side, e.g. as dropzone indicator.
203    pub hovering_side: DragSide,
204}
205
206impl<Column> DragState<Column>
207where
208    Column: Clone + PartialEq,
209{
210    pub fn reorder_columns(&self, columns: &mut Vec<Column>) {
211        let index = columns
212            .iter()
213            .position(|c| *c == self.hovering_over)
214            .unwrap();
215        let grabbed_index = columns.iter().position(|c| *c == self.grabbed).unwrap();
216
217        if grabbed_index == index {
218            return;
219        }
220
221        columns.remove(grabbed_index);
222
223        let mut index = match self.hovering_side {
224            DragSide::Left => index,
225            DragSide::Right => index + 1,
226        };
227        if index > grabbed_index {
228            index -= 1;
229        }
230
231        columns.insert(index, self.grabbed.clone());
232    }
233}
234
235/// The side of a column the cursor is in while dragging another column over it.
236/// Used for styling the matching side with a drop-zone highlight.
237#[derive(Clone, Copy, PartialEq, Debug)]
238pub enum DragSide {
239    Left,
240    Right,
241}