orbital_base_components/tree/
dnd.rs1use 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
39pub 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
82pub 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
111pub 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
124pub 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}