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 inner_options
54 .into_iter()
55 .filter(|(_, opt_value)| {
56 opt_value.to_string().to_lowercase().contains(&inner_filter)
57 })
58 .collect::<Vec<_>>()
59 } else {
60 vec![]
61 }
62 } else {
63 vec![]
64 }
65 });
66
67 let dropdown_css = |visible| {
68 let display_value = if visible { "block" } else { "none" };
69 css! {"
70 display: {display_value};
71 position: absolute;
72 background-color: white;
73 box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.4);
74 border: 1px black solid;
75 z-index: 1;
76 "}
77 };
78
79 let list_deps = computed_tuple!(dropdown_opened, items, item_selected);
81 let list = bind!(
82 value,
83 filter,
84 dropdown_opened,
85 list_deps.render_value(move |(inner_dropdown_opened, inner_items, item_selected)| {
86 let item_css = |selected: bool| {
87 let bg_color = if selected { "#ccc" } else { "inherit" };
88
89 css! {"
90 cursor: pointer;
91 padding: 2px 4px;
92 background-color: {bg_color};
93
94 :hover {
95 background-color: #ccc;
96 };
97 "}
98 };
99
100 let list = dom_element! {
101 <div css={dropdown_css(inner_dropdown_opened)} />
102 };
103
104 if inner_dropdown_opened {
105 for (opt_key, opt_value) in &inner_items {
106 let on_mouse_down = || true;
108 let on_click = bind!(value, filter, dropdown_opened, opt_key, |_| {
109 value.set(opt_key.clone());
110 filter.set(None);
111 dropdown_opened.set(false);
112 });
113 list.add_child(dom! {
114 <div
115 id={opt_key.to_string()}
116 css={item_css(item_selected.as_ref() == Some(opt_key))}
117 {on_mouse_down} {on_click}
118 >
119 {opt_value.to_string()}
120 </div>
121 });
122 }
123 }
124
125 list.into()
126 })
127 );
128
129 let input_deps = computed_tuple!(value, options);
131 let input = input_deps.render_value(move |(inner_value, options_inner)| {
132 let i = i.clone();
133 let displayed_value = filter.to_computed().map(move |inner_filter| {
135 if let Some(inner_filter) = inner_filter {
136 inner_filter
137 } else {
138 options_inner
139 .get(&inner_value)
140 .map(|val| val.to_string())
141 .unwrap_or_default()
142 }
143 });
144
145 let on_input = bind!(filter, dropdown_opened, |new_value: String| {
146 if new_value.len() >= params.min_chars {
147 dropdown_opened.set(true);
148 }
149 filter.set(Some(new_value));
150 });
151
152 let on_blur = bind!(dropdown_opened, || dropdown_opened.set(false));
153
154 let hook_key_down = bind!(
156 value,
157 item_selected,
158 filter,
159 options,
160 items,
161 dropdown_opened,
162 |key_down: KeyDownEvent| {
163 if key_down.key == "ArrowDown" || key_down.key == "ArrowUp" {
164 transaction(|ctx| {
165 if filter.get(ctx).is_some() {
166 let mut items_iter = {
168 let iter = items.get(ctx).into_iter();
169 if key_down.key == "ArrowUp" {
170 Either::Left(iter.rev())
171 } else {
172 Either::Right(iter)
173 }
174 }
175 .peekable();
176
177 let first_key = items_iter.peek().map(|(key, _)| key).cloned();
179
180 if let Some(inner_item_selected) = item_selected.get(ctx) {
181 if let Some((next_key, _)) = items_iter
183 .skip_while(|(opt_key, _)| opt_key != &inner_item_selected)
184 .nth(1)
185 {
186 item_selected.set(Some(next_key));
187 } else {
188 item_selected.set(first_key);
190 }
191 } else if let Some((opt_key, _)) = items_iter.next() {
192 item_selected.set(Some(opt_key));
194 }
195 }
196 });
197 true
198 } else if key_down.key == "Enter" {
199 transaction(|ctx| {
200 if let Some(item_selected) = item_selected.get(ctx) {
201 dropdown_opened.set(false);
203 if let Some(opt_value) = options.get(ctx).get(&item_selected) {
205 filter.set(Some(opt_value.to_string()));
206 }
207 value.set(item_selected);
209 }
210 });
211 true
212 } else {
213 false
214 }
215 }
216 );
217
218 dom! {
219 <input
220 required="required"
221 title={¶ms.input_title}
222 value={displayed_value}
223 {on_input} {on_blur} {hook_key_down} {..i}
224 />
225 }
226 });
227
228 let dropdown_css = css! {"
229 position: relative;
230 "};
231
232 dom! {
233 <div css={dropdown_css} {..c}>
234 {input}
235 {list}
236 </div>
237 }
238}