#![doc = include_str!("../docs/api/multiselect.md")]
use dioxus::prelude::*;
use crate::core::{use_dnd, DragMode, Draggable, DropEffect, ZoneId};
fn suppress_click_for_mode(mode: DragMode) -> bool {
mode == DragMode::Pointer
}
pub struct Selection<K: Clone + PartialEq + 'static> {
items: Signal<Vec<K>>,
anchor: Option<Signal<Option<K>>>,
}
impl<K: Clone + PartialEq + 'static> Copy for Selection<K> {}
impl<K: Clone + PartialEq + 'static> Clone for Selection<K> {
fn clone(&self) -> Self {
*self
}
}
impl<K: Clone + PartialEq + 'static> PartialEq for Selection<K> {
fn eq(&self, other: &Self) -> bool {
self.items == other.items && self.anchor == other.anchor
}
}
impl<K: Clone + PartialEq + 'static> Selection<K> {
pub fn from_signal(items: Signal<Vec<K>>) -> Self {
Self {
items,
anchor: None,
}
}
pub fn from_signals(items: Signal<Vec<K>>, anchor: Signal<Option<K>>) -> Self {
Self {
items,
anchor: Some(anchor),
}
}
pub fn is_selected(&self, key: &K) -> bool {
self.items.read().contains(key)
}
pub fn select_only(&mut self, key: K) {
self.items.set(vec![key.clone()]);
if let Some(mut anchor) = self.anchor {
anchor.set(Some(key));
}
}
pub fn toggle(&mut self, key: K) {
let mut items = self.items.write();
if let Some(ix) = items.iter().position(|k| *k == key) {
items.remove(ix);
} else {
items.push(key);
}
}
pub fn clear(&mut self) {
self.items.write().clear();
if let Some(mut anchor) = self.anchor {
anchor.set(None);
}
}
pub fn items(&self) -> Vec<K> {
self.items.read().clone()
}
pub fn len(&self) -> usize {
self.items.read().len()
}
pub fn is_empty(&self) -> bool {
self.items.read().is_empty()
}
pub fn click(&mut self, key: K, modifiers: Modifiers) {
if modifiers.contains(Modifiers::CONTROL) || modifiers.contains(Modifiers::META) {
self.toggle(key);
} else {
self.select_only(key);
}
}
pub fn select_range(&mut self, ordered: &[K], to: &K, additive: bool) -> bool {
let anchor = self
.anchor
.and_then(|anchor| anchor.peek().clone())
.or_else(|| self.items.peek().first().cloned())
.unwrap_or_else(|| to.clone());
let Some(from_index) = ordered.iter().position(|item| item == &anchor) else {
return false;
};
let Some(to_index) = ordered.iter().position(|item| item == to) else {
return false;
};
let (start, end) = if from_index <= to_index {
(from_index, to_index)
} else {
(to_index, from_index)
};
let range = &ordered[start..=end];
if additive {
let mut selected = self.items.write();
for item in range {
if !selected.contains(item) {
selected.push(item.clone());
}
}
} else {
self.items.set(range.to_vec());
}
if let Some(mut anchor_state) = self.anchor {
anchor_state.set(Some(anchor));
}
true
}
pub fn click_in_order(&mut self, key: K, modifiers: Modifiers, ordered: &[K]) {
if modifiers.contains(Modifiers::SHIFT) {
let additive =
modifiers.contains(Modifiers::CONTROL) || modifiers.contains(Modifiers::META);
if self.select_range(ordered, &key, additive) {
return;
}
}
self.click(key, modifiers);
}
pub fn keyboard_range(
&mut self,
ordered: &[K],
current: usize,
step: isize,
extend: bool,
) -> Option<usize> {
if ordered.is_empty() {
return None;
}
let current = current.min(ordered.len() - 1);
let next = current.saturating_add_signed(step).min(ordered.len() - 1);
if extend {
if let Some(mut anchor) = self.anchor {
if anchor.peek().is_none() {
anchor.set(Some(ordered[current].clone()));
}
} else if self.items.peek().is_empty() {
self.items.set(vec![ordered[current].clone()]);
}
self.select_range(ordered, &ordered[next], false);
} else {
self.select_only(ordered[next].clone());
}
Some(next)
}
}
pub fn use_selection_from_signal<K: Clone + PartialEq + 'static>(
items: Signal<Vec<K>>,
) -> Selection<K> {
Selection::from_signals(items, use_signal(|| None))
}
pub fn use_selection<K: Clone + PartialEq + 'static>() -> Selection<K> {
Selection {
items: use_signal(Vec::new),
anchor: Some(use_signal(|| None)),
}
}
#[component]
pub fn SelectableDraggable<K: Clone + PartialEq + 'static>(
item: K,
selection: Selection<K>,
#[props(default)]
zone: Option<ZoneId>,
#[props(default)]
effect: DropEffect,
#[props(default)]
label: Option<String>,
#[props(extends = div, extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let dnd = use_dnd::<Vec<K>>(); let selected = selection.is_selected(&item);
let payload = if selected {
selection.items()
} else {
vec![item.clone()]
};
let click_key = item.clone();
let mut selection = selection;
let mut suppress_pointer_click = use_signal(|| false);
let mut attributes = attributes;
crate::core::components::protect_attributes(
&mut attributes,
&["data-selected", "onclick", "onpointerdown"],
);
rsx! {
div {
"data-selected": if selected { "true" },
onclick: move |evt: MouseEvent| {
if *suppress_pointer_click.peek() {
suppress_pointer_click.set(false);
return;
}
selection.click(click_key.clone(), evt.modifiers());
},
..attributes,
Draggable::<Vec<K>> {
payload,
zone,
effect,
label,
on_drag_start: move |_| {
if suppress_click_for_mode(dnd.mode()) {
suppress_pointer_click.set(true);
}
},
on_drag_end: move |dropped: bool| {
if !dropped {
suppress_pointer_click.set(false);
}
},
div {
onpointerdown: move |_| {
if !dnd.dragging() && *suppress_pointer_click.peek() {
suppress_pointer_click.set(false);
}
},
{children}
}
}
}
}
}
#[component]
pub fn SelectionCount<K: Clone + PartialEq + 'static>(
#[props(default)]
phantom: std::marker::PhantomData<K>,
) -> Element {
let _ = phantom;
let dnd = use_dnd::<Vec<K>>();
let strings = crate::core::use_dnd_strings();
let n = dnd.payload().map(|p| p.len()).unwrap_or(0);
let text = (strings.selection_count)(n);
rsx! {
span { "{text}" }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_pointer_drags_arm_browser_click_suppression() {
assert!(suppress_click_for_mode(DragMode::Pointer));
assert!(!suppress_click_for_mode(DragMode::Keyboard));
}
}