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