vertigo_forms/
select_search.rs1use either::Either;
2use std::{collections::HashMap, hash::Hash};
3use vertigo::{
4 AttrGroup, Computed, KeyDownEvent, Value, bind, component, computed_tuple, css, dom,
5 dom_element, transaction,
6};
7
8pub struct SelectSearchParams {
9 pub min_chars: usize,
11 pub input_title: String,
12}
13
14impl Default for SelectSearchParams {
15 fn default() -> Self {
16 Self {
17 min_chars: 3,
18 input_title: "Enter phrase".to_string(),
19 }
20 }
21}
22
23#[component]
25pub fn SelectSearch<K, V>(
26 value: Value<K>,
28 options: Computed<HashMap<K, V>>,
30 params: SelectSearchParams,
32 c: AttrGroup,
34 i: AttrGroup,
36) where
37 K: Clone + ToString + PartialEq + Eq + Hash + 'static,
38 V: Clone + ToString + PartialEq + 'static,
39{
40 let filter = Value::<Option<String>>::default();
42 let dropdown_opened = Value::<bool>::default();
44 let item_selected = Value::<Option<K>>::default();
46
47 let items = computed_tuple!(options, filter).map(move |(inner_options, inner_filter)| {
49 if let Some(inner_filter) = inner_filter {
50 let inner_filter = inner_filter.to_lowercase();
51 if inner_filter.len() >= params.min_chars {
52 let mut filtered = inner_options
54 .into_iter()
55 .filter(|(_, opt_value)| {
56 opt_value.to_string().to_lowercase().contains(&inner_filter)
57 })
58 .collect::<Vec<_>>();
59 filtered.sort_by_cached_key(|(_, opt_value)| opt_value.to_string());
60 filtered
61 } else {
62 vec![]
63 }
64 } else {
65 vec![]
66 }
67 });
68
69 let dropdown_css = |visible| {
70 let display_value = if visible { "block" } else { "none" };
71 css! {"
72 display: {display_value};
73 position: fixed;
74 background-color: white;
75 box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.4);
76 border: 1px black solid;
77 "}
78 };
79
80 let list_deps = computed_tuple!(dropdown_opened, items, item_selected);
82 let list = bind!(
83 value,
84 filter,
85 dropdown_opened,
86 list_deps.render_value(move |(inner_dropdown_opened, inner_items, item_selected)| {
87 let item_css = |selected: bool| {
88 let bg_color = if selected { "#ccc" } else { "inherit" };
89
90 css! {"
91 cursor: pointer;
92 padding: 2px 4px;
93 background-color: {bg_color};
94
95 :hover {
96 background-color: #ccc;
97 };
98 "}
99 };
100
101 let list = dom_element! {
102 <div css={dropdown_css(inner_dropdown_opened)} />
103 };
104
105 if inner_dropdown_opened {
106 for (opt_key, opt_value) in &inner_items {
107 let on_mouse_down = || true;
109 let on_click = bind!(value, filter, dropdown_opened, opt_key, |_| {
110 value.set(opt_key.clone());
111 filter.set(None);
112 dropdown_opened.set(false);
113 });
114 list.add_child(dom! {
115 <div
116 id={opt_key.to_string()}
117 css={item_css(item_selected.as_ref() == Some(opt_key))}
118 {on_mouse_down} {on_click}
119 >
120 {opt_value.to_string()}
121 </div>
122 });
123 }
124 }
125
126 list.into()
127 })
128 );
129
130 let input_deps = computed_tuple!(value, options);
132 let input = input_deps.render_value(move |(inner_value, options_inner)| {
133 let i = i.clone();
134 let displayed_value = filter.to_computed().map(move |inner_filter| {
136 if let Some(inner_filter) = inner_filter {
137 inner_filter
138 } else {
139 options_inner
140 .get(&inner_value)
141 .map(|val| val.to_string())
142 .unwrap_or_default()
143 }
144 });
145
146 let on_input = bind!(filter, dropdown_opened, |new_value: String| {
147 if new_value.len() >= params.min_chars {
148 dropdown_opened.set(true);
149 }
150 filter.set(Some(new_value));
151 });
152
153 let on_blur = bind!(dropdown_opened, || dropdown_opened.set(false));
154
155 let hook_key_down = bind!(
157 value,
158 item_selected,
159 filter,
160 options,
161 items,
162 dropdown_opened,
163 |key_down: KeyDownEvent| {
164 if key_down.key == "ArrowDown" || key_down.key == "ArrowUp" {
165 transaction(|ctx| {
166 if filter.get(ctx).is_some() {
167 let mut items_iter = {
169 let iter = items.get(ctx).into_iter();
170 if key_down.key == "ArrowUp" {
171 Either::Left(iter.rev())
172 } else {
173 Either::Right(iter)
174 }
175 }
176 .peekable();
177
178 let first_key = items_iter.peek().map(|(key, _)| key).cloned();
180
181 if let Some(inner_item_selected) = item_selected.get(ctx) {
182 if let Some((next_key, _)) = items_iter
184 .skip_while(|(opt_key, _)| opt_key != &inner_item_selected)
185 .nth(1)
186 {
187 item_selected.set(Some(next_key));
188 } else {
189 item_selected.set(first_key);
191 }
192 } else if let Some((opt_key, _)) = items_iter.next() {
193 item_selected.set(Some(opt_key));
195 }
196 }
197 });
198 true
199 } else if key_down.key == "Enter" {
200 transaction(|ctx| {
201 if let Some(item_selected) = item_selected.get(ctx) {
202 dropdown_opened.set(false);
204 if let Some(opt_value) = options.get(ctx).get(&item_selected) {
206 filter.set(Some(opt_value.to_string()));
207 }
208 value.set(item_selected);
210 }
211 });
212 true
213 } else {
214 false
215 }
216 }
217 );
218
219 dom! {
220 <input
221 required="required"
222 title={¶ms.input_title}
223 value={displayed_value}
224 {on_input} {on_blur} {hook_key_down} {..i}
225 />
226 }
227 });
228
229 let dropdown_css = css! {"
230 position: relative;
231 "};
232
233 dom! {
234 <div css={dropdown_css} {..c}>
235 {input}
236 {list}
237 </div>
238 }
239}