dioxus_dnd/
multiselect.rs1#![doc = include_str!("../docs/api/multiselect.md")]
2
3use dioxus::prelude::*;
4
5use crate::core::{use_dnd, DragMode, Draggable, DropEffect, ZoneId};
6
7fn suppress_click_for_mode(mode: DragMode) -> bool {
8 mode == DragMode::Pointer
9}
10
11pub struct Selection<K: Clone + PartialEq + 'static> {
13 items: Signal<Vec<K>>,
14 anchor: Option<Signal<Option<K>>>,
15}
16
17impl<K: Clone + PartialEq + 'static> Copy for Selection<K> {}
18impl<K: Clone + PartialEq + 'static> Clone for Selection<K> {
19 fn clone(&self) -> Self {
20 *self
21 }
22}
23impl<K: Clone + PartialEq + 'static> PartialEq for Selection<K> {
24 fn eq(&self, other: &Self) -> bool {
25 self.items == other.items && self.anchor == other.anchor
26 }
27}
28
29impl<K: Clone + PartialEq + 'static> Selection<K> {
30 pub fn from_signal(items: Signal<Vec<K>>) -> Self {
37 Self {
38 items,
39 anchor: None,
40 }
41 }
42
43 pub fn from_signals(items: Signal<Vec<K>>, anchor: Signal<Option<K>>) -> Self {
49 Self {
50 items,
51 anchor: Some(anchor),
52 }
53 }
54
55 pub fn is_selected(&self, key: &K) -> bool {
57 self.items.read().contains(key)
58 }
59
60 pub fn select_only(&mut self, key: K) {
62 self.items.set(vec![key.clone()]);
63 if let Some(mut anchor) = self.anchor {
64 anchor.set(Some(key));
65 }
66 }
67
68 pub fn toggle(&mut self, key: K) {
70 let mut items = self.items.write();
71 if let Some(ix) = items.iter().position(|k| *k == key) {
72 items.remove(ix);
73 } else {
74 items.push(key);
75 }
76 }
77
78 pub fn clear(&mut self) {
80 self.items.write().clear();
81 if let Some(mut anchor) = self.anchor {
82 anchor.set(None);
83 }
84 }
85
86 pub fn items(&self) -> Vec<K> {
88 self.items.read().clone()
89 }
90
91 pub fn len(&self) -> usize {
93 self.items.read().len()
94 }
95
96 pub fn is_empty(&self) -> bool {
98 self.items.read().is_empty()
99 }
100
101 pub fn click(&mut self, key: K, modifiers: Modifiers) {
104 if modifiers.contains(Modifiers::CONTROL) || modifiers.contains(Modifiers::META) {
105 self.toggle(key);
106 } else {
107 self.select_only(key);
108 }
109 }
110
111 pub fn select_range(&mut self, ordered: &[K], to: &K, additive: bool) -> bool {
115 let anchor = self
116 .anchor
117 .and_then(|anchor| anchor.peek().clone())
118 .or_else(|| self.items.peek().first().cloned())
119 .unwrap_or_else(|| to.clone());
120 let Some(from_index) = ordered.iter().position(|item| item == &anchor) else {
121 return false;
122 };
123 let Some(to_index) = ordered.iter().position(|item| item == to) else {
124 return false;
125 };
126 let (start, end) = if from_index <= to_index {
127 (from_index, to_index)
128 } else {
129 (to_index, from_index)
130 };
131 let range = &ordered[start..=end];
132 if additive {
133 let mut selected = self.items.write();
134 for item in range {
135 if !selected.contains(item) {
136 selected.push(item.clone());
137 }
138 }
139 } else {
140 self.items.set(range.to_vec());
141 }
142 if let Some(mut anchor_state) = self.anchor {
143 anchor_state.set(Some(anchor));
144 }
145 true
146 }
147
148 pub fn click_in_order(&mut self, key: K, modifiers: Modifiers, ordered: &[K]) {
150 if modifiers.contains(Modifiers::SHIFT) {
151 let additive =
152 modifiers.contains(Modifiers::CONTROL) || modifiers.contains(Modifiers::META);
153 if self.select_range(ordered, &key, additive) {
154 return;
155 }
156 }
157 self.click(key, modifiers);
158 }
159
160 pub fn keyboard_range(
163 &mut self,
164 ordered: &[K],
165 current: usize,
166 step: isize,
167 extend: bool,
168 ) -> Option<usize> {
169 if ordered.is_empty() {
170 return None;
171 }
172 let current = current.min(ordered.len() - 1);
173 let next = current.saturating_add_signed(step).min(ordered.len() - 1);
174 if extend {
175 if let Some(mut anchor) = self.anchor {
176 if anchor.peek().is_none() {
177 anchor.set(Some(ordered[current].clone()));
178 }
179 } else if self.items.peek().is_empty() {
180 self.items.set(vec![ordered[current].clone()]);
183 }
184 self.select_range(ordered, &ordered[next], false);
185 } else {
186 self.select_only(ordered[next].clone());
187 }
188 Some(next)
189 }
190}
191
192pub fn use_selection_from_signal<K: Clone + PartialEq + 'static>(
195 items: Signal<Vec<K>>,
196) -> Selection<K> {
197 Selection::from_signals(items, use_signal(|| None))
198}
199
200pub fn use_selection<K: Clone + PartialEq + 'static>() -> Selection<K> {
202 Selection {
203 items: use_signal(Vec::new),
204 anchor: Some(use_signal(|| None)),
205 }
206}
207
208#[component]
220pub fn SelectableDraggable<K: Clone + PartialEq + 'static>(
221 item: K,
223 selection: Selection<K>,
225 #[props(default)]
227 zone: Option<ZoneId>,
228 #[props(default)]
230 effect: DropEffect,
231 #[props(default)]
233 label: Option<String>,
234 #[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
235 children: Element,
236) -> Element {
237 let dnd = use_dnd::<Vec<K>>(); let selected = selection.is_selected(&item);
239 let payload = if selected {
242 selection.items()
243 } else {
244 vec![item.clone()]
245 };
246 let click_key = item.clone();
247 let mut selection = selection;
248 let mut suppress_pointer_click = use_signal(|| false);
253 let mut attributes = attributes;
254 crate::core::components::protect_attributes(
255 &mut attributes,
256 &["data-selected", "onclick", "onpointerdown"],
257 );
258
259 rsx! {
260 div {
261 "data-selected": if selected { "true" },
262 onclick: move |evt: MouseEvent| {
263 if *suppress_pointer_click.peek() {
264 suppress_pointer_click.set(false);
265 return;
266 }
267 selection.click(click_key.clone(), evt.modifiers());
268 },
269 ..attributes,
270 Draggable::<Vec<K>> {
271 payload,
272 zone,
273 effect,
274 label,
275 on_drag_start: move |_| {
276 if suppress_click_for_mode(dnd.mode()) {
277 suppress_pointer_click.set(true);
278 }
279 },
280 on_drag_end: move |dropped: bool| {
281 if !dropped {
282 suppress_pointer_click.set(false);
283 }
284 },
285 div {
286 onpointerdown: move |_| {
287 if !dnd.dragging() && *suppress_pointer_click.peek() {
292 suppress_pointer_click.set(false);
293 }
294 },
295 {children}
296 }
297 }
298 }
299 }
300}
301
302#[component]
305pub fn SelectionCount<K: Clone + PartialEq + 'static>(
306 #[props(default)]
308 phantom: std::marker::PhantomData<K>,
309) -> Element {
310 let _ = phantom;
311 let dnd = use_dnd::<Vec<K>>();
312 let strings = crate::core::use_dnd_strings();
313 let n = dnd.payload().map(|p| p.len()).unwrap_or(0);
314 let text = (strings.selection_count)(n);
315 rsx! {
316 span { "{text}" }
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn only_pointer_drags_arm_browser_click_suppression() {
326 assert!(suppress_click_for_mode(DragMode::Pointer));
327 assert!(!suppress_click_for_mode(DragMode::Keyboard));
328 }
329}