Skip to main content

orbital_base_components/tree/
dnd.rs

1use leptos::{leptos_dom::helpers::WindowListenerHandle, prelude::*};
2use wasm_bindgen::JsCast;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum TreeDropPosition {
6    Before,
7    After,
8}
9
10#[derive(Clone)]
11pub struct TreeDragState {
12    pub dragging_id: RwSignal<Option<String>>,
13    pub drop_target_id: RwSignal<Option<String>>,
14    pub drop_position: RwSignal<Option<TreeDropPosition>>,
15}
16
17impl TreeDragState {
18    pub fn new() -> Self {
19        Self {
20            dragging_id: RwSignal::new(None),
21            drop_target_id: RwSignal::new(None),
22            drop_position: RwSignal::new(None),
23        }
24    }
25
26    pub fn clear(&self) {
27        self.dragging_id.set(None);
28        self.drop_target_id.set(None);
29        self.drop_position.set(None);
30    }
31}
32
33impl Default for TreeDragState {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39/// Drag handle state for reorderable tree items.
40pub fn use_tree_item_drag(
41    item_id: Signal<String>,
42    reorderable: Signal<bool>,
43    drag_state: TreeDragState,
44) -> (Signal<bool>, Callback<leptos::ev::PointerEvent>) {
45    let dragging = Signal::derive({
46        let drag_state = drag_state.clone();
47        move || {
48            drag_state
49                .dragging_id
50                .get()
51                .is_some_and(|id| id == item_id.get())
52        }
53    });
54
55    let on_pointerdown = Callback::new({
56        let drag_state = drag_state.clone();
57        move |event: leptos::ev::PointerEvent| {
58            if !reorderable.get_untracked() {
59                return;
60            }
61            let from_handle = event
62                .target()
63                .and_then(|target| target.dyn_into::<web_sys::Element>().ok())
64                .is_some_and(|element| {
65                    element
66                        .closest(".orbital-tree-item-layout__drag-handle")
67                        .ok()
68                        .flatten()
69                        .is_some()
70                });
71            if !from_handle {
72                return;
73            }
74            event.stop_propagation();
75            drag_state.dragging_id.set(Some(item_id.get_untracked()));
76        }
77    });
78
79    (dragging, on_pointerdown)
80}
81
82/// Move a registered tree item element before or after its drop target sibling.
83pub fn apply_sibling_dom_reorder(
84    dom_registry: &crate::collection::state::CollectionItemDomRegistry,
85    source_id: &str,
86    target_id: &str,
87    order: usize,
88) {
89    let Some(source_el) = dom_registry.get_dom_element(source_id) else {
90        return;
91    };
92    let Some(target_el) = dom_registry.get_dom_element(target_id) else {
93        return;
94    };
95    let Some(parent) = source_el.parent_node() else {
96        return;
97    };
98
99    if order == 0 {
100        let _ = parent.insert_before(&source_el, Some(&target_el));
101        return;
102    }
103
104    if let Some(next) = target_el.next_sibling() {
105        let _ = parent.insert_before(&source_el, Some(&next));
106    } else {
107        let _ = parent.append_child(&source_el);
108    }
109}
110
111/// Document-level drag listeners installed by [`install_tree_drag_listeners`].
112pub struct TreeDragListenerHandle {
113    handles: Vec<WindowListenerHandle>,
114}
115
116impl TreeDragListenerHandle {
117    pub fn remove(self) {
118        for handle in self.handles {
119            handle.remove();
120        }
121    }
122}
123
124/// Install document-level pointer listeners for drag reorder. Call once at tree root and remove via [`TreeDragListenerHandle::remove`] when the tree unmounts.
125pub fn install_tree_drag_listeners(
126    drag_state: TreeDragState,
127    reorderable: Signal<bool>,
128    on_drop: Callback<(String, String, TreeDropPosition)>,
129) -> TreeDragListenerHandle {
130    let drag_state_for_move = drag_state.clone();
131    let drag_state_for_up = drag_state.clone();
132
133    let move_listener = leptos::leptos_dom::helpers::window_event_listener(
134        leptos::ev::pointermove,
135        move |event: leptos::ev::PointerEvent| {
136            if !reorderable.get_untracked() {
137                return;
138            }
139            if drag_state_for_move.dragging_id.get_untracked().is_none() {
140                return;
141            }
142            let target = event
143                .target()
144                .and_then(|t| t.dyn_into::<web_sys::Element>().ok());
145            if let Some(element) = target {
146                if let Ok(Some(row)) = element.closest(".orbital-tree-item") {
147                    if let Some(id) = row.get_attribute("data-item-id") {
148                        drag_state_for_move.drop_target_id.set(Some(id));
149                        let rect = row.get_bounding_client_rect();
150                        let mid = rect.top() + rect.height() / 2.0;
151                        let pos = if (event.client_y() as f64) < mid {
152                            TreeDropPosition::Before
153                        } else {
154                            TreeDropPosition::After
155                        };
156                        drag_state_for_move.drop_position.set(Some(pos));
157                    }
158                }
159            }
160        },
161    );
162
163    let up_listener = leptos::leptos_dom::helpers::window_event_listener(
164        leptos::ev::pointerup,
165        move |_event: leptos::ev::PointerEvent| {
166            if !reorderable.get_untracked() {
167                return;
168            }
169            let Some(source_id) = drag_state_for_up.dragging_id.get_untracked() else {
170                return;
171            };
172            if let (Some(target_id), Some(pos)) = (
173                drag_state_for_up.drop_target_id.get_untracked(),
174                drag_state_for_up.drop_position.get_untracked(),
175            ) {
176                if source_id != target_id {
177                    on_drop.run((source_id, target_id, pos));
178                }
179            }
180            drag_state_for_up.clear();
181        },
182    );
183
184    TreeDragListenerHandle {
185        handles: vec![move_listener, up_listener],
186    }
187}