Skip to main content

orbital_base_components/collection/state/
focus.rs

1use leptos::{html, prelude::*};
2use std::collections::HashMap;
3
4const TYPEAHEAD_RESET_MS: f64 = 500.0;
5
6#[derive(Clone)]
7pub struct CollectionFocusState {
8    pub focused_item: RwSignal<Option<String>>,
9    pub typeahead: RwSignal<String>,
10    pub typeahead_deadline: RwSignal<Option<f64>>,
11}
12
13impl Default for CollectionFocusState {
14    fn default() -> Self {
15        Self::new()
16    }
17}
18
19impl CollectionFocusState {
20    pub fn new() -> Self {
21        Self {
22            focused_item: RwSignal::new(None),
23            typeahead: RwSignal::new(String::new()),
24            typeahead_deadline: RwSignal::new(None),
25        }
26    }
27
28    pub fn focus_item(&self, item_id: String) {
29        self.focused_item.set(Some(item_id));
30    }
31
32    pub fn append_typeahead(&self, key: String) -> String {
33        let now = web_sys::window()
34            .and_then(|w| w.performance())
35            .map(|p| p.now())
36            .unwrap_or(0.0);
37        let prefix = if self
38            .typeahead_deadline
39            .get_untracked()
40            .is_some_and(|deadline| now > deadline)
41        {
42            String::new()
43        } else {
44            self.typeahead.get_untracked()
45        };
46        let next = format!("{prefix}{key}");
47        self.typeahead.set(next.clone());
48        self.typeahead_deadline.set(Some(now + TYPEAHEAD_RESET_MS));
49        next
50    }
51
52    pub fn reset_typeahead(&self) {
53        self.typeahead.set(String::new());
54        self.typeahead_deadline.set(None);
55    }
56}
57
58#[derive(Clone)]
59pub struct CollectionItemDomRegistry {
60    pub item_refs: RwSignal<HashMap<String, NodeRef<html::Div>>>,
61}
62
63impl Default for CollectionItemDomRegistry {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl CollectionItemDomRegistry {
70    pub fn new() -> Self {
71        Self {
72            item_refs: RwSignal::new(HashMap::new()),
73        }
74    }
75
76    pub fn register(&self, item_id: String, node_ref: NodeRef<html::Div>) {
77        self.item_refs.update(|refs| {
78            refs.insert(item_id, node_ref);
79        });
80    }
81
82    pub fn unregister(&self, item_id: &str) {
83        self.item_refs.update(|refs| {
84            refs.remove(item_id);
85        });
86    }
87
88    pub fn get_dom_element(&self, item_id: &str) -> Option<web_sys::HtmlElement> {
89        self.item_refs.with_untracked(|refs| {
90            refs.get(item_id)
91                .and_then(|node_ref| node_ref.get_untracked())
92                .map(|element| element.into())
93        })
94    }
95
96    pub fn focus_dom_element(&self, item_id: &str) {
97        if let Some(element) = self.get_dom_element(item_id) {
98            let _ = element.focus();
99        }
100    }
101
102    /// Item ids sorted by on-screen position for range selection.
103    pub fn visible_ids_in_dom_order(&self) -> Vec<String> {
104        self.item_refs.with_untracked(|refs| {
105            let mut positioned: Vec<(String, f64, f64)> = refs
106                .iter()
107                .filter_map(|(id, node_ref)| {
108                    node_ref.get_untracked().map(|element| {
109                        let rect = element.get_bounding_client_rect();
110                        (id.clone(), rect.top(), rect.left())
111                    })
112                })
113                .collect();
114            positioned.sort_by(|a, b| {
115                a.1.partial_cmp(&b.1)
116                    .unwrap_or(std::cmp::Ordering::Equal)
117                    .then_with(|| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))
118                    .then_with(|| a.0.cmp(&b.0))
119            });
120            positioned.into_iter().map(|(id, _, _)| id).collect()
121        })
122    }
123}