orbital_base_components/collection/state/
focus.rs1use 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 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}