1use std::cell::RefCell;
2use std::rc::Rc;
3
4use dioxus::prelude::*;
5use nucleo_matcher::{Config, Matcher};
6
7use crate::filter;
8use crate::navigation::{self, Direction};
9use crate::types::*;
10
11#[derive(Clone, Copy)]
15#[allow(dead_code)]
16pub struct SelectContext {
17 pub(crate) value: Signal<String>,
19 pub(crate) controlled_value: Option<Signal<String>>,
20
21 pub(crate) values: Signal<Vec<String>>,
23 pub(crate) controlled_values: Option<Signal<Vec<String>>>,
24
25 pub(crate) open: Signal<bool>,
27 pub(crate) controlled_open: Option<Signal<bool>>,
28
29 pub(crate) search_query: Signal<String>,
31 pub(crate) scored_items: Memo<Vec<ScoredItem>>,
32 pub(crate) visible_values: Memo<Vec<String>>,
33
34 pub(crate) highlighted: Signal<Option<String>>,
36
37 pub(crate) items: Signal<Vec<ItemEntry>>,
39 pub(crate) groups: Signal<Vec<GroupEntry>>,
40
41 pub(crate) multiple: bool,
43 pub(crate) disabled: bool,
44 pub(crate) autocomplete: AutoComplete,
45 pub(crate) open_on_focus: bool,
46 pub(crate) custom_filter: Signal<Option<CustomFilter>>,
47
48 pub(crate) on_value_change: Option<EventHandler<String>>,
50 pub(crate) on_values_change: Option<EventHandler<Vec<String>>>,
51 pub(crate) on_open_change: Option<EventHandler<bool>>,
52
53 pub(crate) instance_id: u32,
55
56 pub(crate) has_input: Signal<bool>,
58}
59
60impl SelectContext {
61 pub fn current_value(&self) -> String {
65 match self.controlled_value {
66 Some(sig) => (sig)(),
67 None => (self.value)(),
68 }
69 }
70
71 pub fn current_values(&self) -> Vec<String> {
73 match self.controlled_values {
74 Some(sig) => (sig)(),
75 None => (self.values)(),
76 }
77 }
78
79 pub fn current_values_peek(&self) -> Vec<String> {
81 match self.controlled_values {
82 Some(sig) => sig.peek().clone(),
83 None => self.values.peek().clone(),
84 }
85 }
86
87 pub fn select_single(&mut self, val: &str) {
89 if self.disabled {
90 return;
91 }
92 if let Some(mut controlled) = self.controlled_value {
93 controlled.set(val.to_string());
94 } else {
95 self.value.set(val.to_string());
96 }
97 if let Some(handler) = &self.on_value_change {
98 handler.call(val.to_string());
99 }
100 self.set_open(false);
101 self.search_query.set(String::new());
102 }
103
104 pub fn toggle_value(&mut self, val: &str) {
107 if self.disabled {
108 return;
109 }
110 let mut current = self.current_values();
111 if let Some(pos) = current.iter().position(|v| v == val) {
112 current.remove(pos);
113 } else {
114 current.push(val.to_string());
115 }
116 if let Some(mut controlled) = self.controlled_values {
117 controlled.set(current.clone());
118 } else {
119 self.values.set(current.clone());
120 }
121 if let Some(handler) = &self.on_values_change {
122 handler.call(current);
123 }
124 }
125
126 pub fn is_selected(&self, val: &str) -> bool {
128 if self.multiple {
129 self.current_values().iter().any(|v| v == val)
130 } else {
131 self.current_value() == val
132 }
133 }
134
135 pub fn is_open(&self) -> bool {
139 match self.controlled_open {
140 Some(sig) => (sig)(),
141 None => (self.open)(),
142 }
143 }
144
145 pub fn set_open(&mut self, is_open: bool) {
147 if let Some(mut controlled) = self.controlled_open {
148 controlled.set(is_open);
149 } else {
150 self.open.set(is_open);
151 }
152 if let Some(handler) = &self.on_open_change {
153 handler.call(is_open);
154 }
155 if !is_open {
156 self.highlighted.set(None);
157 }
158 }
159
160 pub fn toggle_open(&mut self) {
162 let current = self.is_open();
163 self.set_open(!current);
164 }
165
166 pub fn highlight_next(&mut self) {
170 let visible = self.visible_values.read();
171 let items = self.items.read();
172 let current = self.highlighted.read();
173 let next = navigation::navigate(&items, &visible, current.as_deref(), Direction::Forward);
174 drop(visible);
175 drop(items);
176 drop(current);
177 self.highlighted.set(next.clone());
178 if let Some(ref val) = next {
179 self.scroll_item_into_view(val);
180 }
181 }
182
183 pub fn highlight_prev(&mut self) {
185 let visible = self.visible_values.read();
186 let items = self.items.read();
187 let current = self.highlighted.read();
188 let prev = navigation::navigate(&items, &visible, current.as_deref(), Direction::Backward);
189 drop(visible);
190 drop(items);
191 drop(current);
192 self.highlighted.set(prev.clone());
193 if let Some(ref val) = prev {
194 self.scroll_item_into_view(val);
195 }
196 }
197
198 pub fn highlight_first(&mut self) {
200 let visible = self.visible_values.read();
201 let items = self.items.read();
202 let target = navigation::first(&items, &visible);
203 drop(visible);
204 drop(items);
205 self.highlighted.set(target.clone());
206 if let Some(ref val) = target {
207 self.scroll_item_into_view(val);
208 }
209 }
210
211 pub fn highlight_last(&mut self) {
213 let visible = self.visible_values.read();
214 let items = self.items.read();
215 let target = navigation::last(&items, &visible);
216 drop(visible);
217 drop(items);
218 self.highlighted.set(target.clone());
219 if let Some(ref val) = target {
220 self.scroll_item_into_view(val);
221 }
222 }
223
224 pub fn type_ahead(&mut self, prefix: &str) {
226 let visible = self.visible_values.read();
227 let items = self.items.read();
228 let current = self.highlighted.read();
229 let target = navigation::type_ahead(&items, &visible, current.as_deref(), prefix);
230 drop(visible);
231 drop(items);
232 drop(current);
233 if let Some(ref val) = target {
234 self.highlighted.set(Some(val.clone()));
235 self.scroll_item_into_view(val);
236 }
237 }
238
239 pub fn confirm_highlighted(&mut self) {
241 let highlighted = self.highlighted.read().clone();
242 if let Some(val) = highlighted {
243 let is_disabled = self
245 .items
246 .read()
247 .iter()
248 .any(|e| e.value == val && e.disabled);
249 if is_disabled {
250 return;
251 }
252 if self.multiple {
253 self.toggle_value(&val);
254 } else {
255 self.select_single(&val);
256 }
257 }
258 }
259
260 pub fn register_item(&mut self, entry: ItemEntry) {
264 let mut items = self.items.write();
265 if !items.iter().any(|e| e.value == entry.value) {
266 items.push(entry);
267 }
268 }
269
270 pub fn deregister_item(&mut self, value: &str) {
272 let mut items = self.items.write();
273 items.retain(|e| e.value != value);
274 }
275
276 pub fn register_group(&mut self, entry: GroupEntry) {
278 let mut groups = self.groups.write();
279 if !groups.iter().any(|g| g.id == entry.id) {
280 groups.push(entry);
281 }
282 }
283
284 pub fn deregister_group(&mut self, id: &str) {
286 let mut groups = self.groups.write();
287 groups.retain(|g| g.id != id);
288 }
289
290 pub fn mark_has_input(&mut self) {
292 self.has_input.set(true);
293 }
294
295 pub fn has_search_input(&self) -> bool {
297 (self.has_input)()
298 }
299
300 pub fn trigger_id(&self) -> String {
304 format!("nox-select-{}-trigger", self.instance_id)
305 }
306
307 pub fn listbox_id(&self) -> String {
309 format!("nox-select-{}-listbox", self.instance_id)
310 }
311
312 pub fn item_id(&self, value: &str) -> String {
314 format!("nox-select-{}-item-{}", self.instance_id, value)
315 }
316
317 pub fn input_id(&self) -> String {
319 format!("nox-select-{}-input", self.instance_id)
320 }
321
322 pub fn group_label_id(&self, group_id: &str) -> String {
324 format!("nox-select-{}-group-{}", self.instance_id, group_id)
325 }
326
327 pub fn active_descendant(&self) -> String {
329 match self.highlighted.read().as_ref() {
330 Some(val) => self.item_id(val),
331 None => String::new(),
332 }
333 }
334
335 pub fn autocomplete(&self) -> AutoComplete {
339 self.autocomplete
340 }
341
342 pub fn open_on_focus(&self) -> bool {
344 self.open_on_focus
345 }
346
347 pub fn is_multiple(&self) -> bool {
349 self.multiple
350 }
351
352 pub fn has_highlighted(&self) -> bool {
354 self.highlighted.read().is_some()
355 }
356
357 pub fn highlighted_value(&self) -> Option<String> {
359 self.highlighted.read().clone()
360 }
361
362 pub fn set_search_query(&mut self, query: String) {
364 self.search_query.set(query);
365 }
366
367 fn scroll_item_into_view(&self, value: &str) {
371 #[cfg(target_arch = "wasm32")]
372 {
373 let id = self.item_id(value);
374 spawn(async move {
375 let js = format!(
376 "document.getElementById('{}')?.scrollIntoView({{block:'nearest'}})",
377 id.replace('\'', "\\'")
378 );
379 _ = document::eval(&js).await;
380 });
381 }
382 #[cfg(not(target_arch = "wasm32"))]
383 {
384 _ = value;
385 }
386 }
387
388 pub(crate) fn focus_combobox(&self) {
390 #[cfg(target_arch = "wasm32")]
391 {
392 let id = if self.has_search_input() {
393 self.input_id()
394 } else {
395 self.trigger_id()
396 };
397 spawn(async move {
398 let js = format!(
399 "document.getElementById('{}')?.focus()",
400 id.replace('\'', "\\'")
401 );
402 _ = document::eval(&js).await;
403 });
404 }
405 }
406}
407
408#[allow(clippy::too_many_arguments)]
412pub(crate) fn init_select_context(
413 default_value: Option<String>,
414 controlled_value: Option<Signal<String>>,
415 on_value_change: Option<EventHandler<String>>,
416 default_values: Option<Vec<String>>,
417 controlled_values: Option<Signal<Vec<String>>>,
418 on_values_change: Option<EventHandler<Vec<String>>>,
419 multiple: bool,
420 disabled: bool,
421 default_open: bool,
422 controlled_open: Option<Signal<bool>>,
423 on_open_change: Option<EventHandler<bool>>,
424 autocomplete: AutoComplete,
425 open_on_focus: bool,
426 custom_filter: Option<CustomFilter>,
427) -> SelectContext {
428 let instance_id = use_hook(next_instance_id);
429
430 let value = use_signal(|| default_value.unwrap_or_default());
431 let values = use_signal(|| default_values.unwrap_or_default());
432 let open = use_signal(|| default_open);
433 let search_query = use_signal(String::new);
434 let highlighted = use_signal(|| None::<String>);
435 let items: Signal<Vec<ItemEntry>> = use_signal(Vec::new);
436 let groups: Signal<Vec<GroupEntry>> = use_signal(Vec::new);
437 let has_input = use_signal(|| false);
438 let custom_filter_sig = use_signal(|| custom_filter);
439
440 let matcher = use_hook(|| Rc::new(RefCell::new(Matcher::new(Config::DEFAULT))));
442
443 let scored_items = use_memo(move || {
445 let query = search_query.read().clone();
446 let all_items = items.read();
447 let cf = custom_filter_sig.read();
448 let mut m = matcher.borrow_mut();
449 filter::score_items(&all_items, &query, cf.as_ref(), &mut m)
450 });
451
452 let visible_values = use_memo(move || filter::visible_values(&scored_items.read()));
453
454 let ctx = SelectContext {
455 value,
456 controlled_value,
457 values,
458 controlled_values,
459 open,
460 controlled_open,
461 search_query,
462 scored_items,
463 visible_values,
464 highlighted,
465 items,
466 groups,
467 multiple,
468 disabled,
469 autocomplete,
470 open_on_focus,
471 custom_filter: custom_filter_sig,
472 on_value_change,
473 on_values_change,
474 on_open_change,
475 instance_id,
476 has_input,
477 };
478
479 use_context_provider(|| ctx);
480
481 ctx
482}