adui_dioxus/components/
select_base.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use dioxus::{events::KeyboardEvent, prelude::*};
5
6use crate::components::form::{form_value_to_radio_key, form_value_to_string_vec};
7use crate::components::overlay::{OverlayKey, OverlayKind, use_overlay};
8
9/// Shared key type used by selector-like components.
10///
11/// We normalise all option keys to owned `String` values so they can be
12/// round-tripped through `serde_json::Value` and the Form store easily.
13pub type OptionKey = String;
14
15/// Flat option used by `Select` and `AutoComplete`.
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct SelectOption {
18    pub key: OptionKey,
19    pub label: String,
20    #[serde(default)]
21    pub disabled: bool,
22}
23
24/// Tree-shaped option node shared by `TreeSelect` and `Cascader`.
25#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26pub struct OptionNode {
27    pub key: OptionKey,
28    pub label: String,
29    #[serde(default)]
30    pub disabled: bool,
31    #[serde(default)]
32    pub children: Vec<OptionNode>,
33}
34
35/// Alias for clarity when used by `TreeSelect`.
36pub type TreeNode = OptionNode;
37
38/// Alias for clarity when used by `Cascader`.
39pub type CascaderNode = OptionNode;
40
41// ---- Value mapping helpers -------------------------------------------------
42
43/// Convert a `serde_json::Value` coming from Form into an optional `OptionKey`.
44///
45/// This delegates to `form_value_to_radio_key` so that selector components are
46/// consistent with Radio/Checkbox semantics.
47pub fn value_to_option_key(val: Option<Value>) -> Option<OptionKey> {
48    form_value_to_radio_key(val)
49}
50
51/// Convert a single key into a JSON value suitable for storing in Form.
52pub fn option_key_to_value(key: &OptionKey) -> Value {
53    Value::String(key.clone())
54}
55
56/// Convert a JSON value into a vector of option keys.
57///
58/// This is primarily used for multi-select scenarios and internally reuses
59/// `form_value_to_string_vec` (which expects an array of strings).
60pub fn value_to_option_keys(val: Option<Value>) -> Vec<OptionKey> {
61    form_value_to_string_vec(val)
62}
63
64/// Convert a slice of keys into a JSON array value.
65pub fn option_keys_to_value(keys: &[OptionKey]) -> Value {
66    let items = keys.iter().cloned().map(Value::String).collect();
67    Value::Array(items)
68}
69
70/// Convert a JSON value into a path of keys (used by Cascader).
71///
72/// Paths are represented as a simple array of strings such as
73/// `["zhejiang", "hangzhou"]`.
74pub fn value_to_path(val: Option<Value>) -> Vec<OptionKey> {
75    match val {
76        Some(Value::Array(items)) => items
77            .into_iter()
78            .filter_map(|v| v.as_str().map(|s| s.to_string()))
79            .collect(),
80        _ => Vec::new(),
81    }
82}
83
84/// Convert a path of keys into a JSON array value.
85pub fn path_to_value(path: &[OptionKey]) -> Value {
86    option_keys_to_value(path)
87}
88
89// ---- Option list helpers ---------------------------------------------------
90
91/// Filter options by label using a case-insensitive `contains` match.
92///
93/// An empty query returns the original options unchanged (cloned).
94pub fn filter_options_by_query(options: &[SelectOption], query: &str) -> Vec<SelectOption> {
95    let trimmed = query.trim();
96    if trimmed.is_empty() {
97        return options.to_vec();
98    }
99    let q = trimmed.to_lowercase();
100    options
101        .iter()
102        .filter(|opt| opt.label.to_lowercase().contains(&q))
103        .cloned()
104        .collect()
105}
106
107/// Toggle a single key within a set of keys.
108///
109/// If the key already exists it is removed, otherwise it is appended. This is
110/// the shared primitive for multi-select style components.
111pub fn toggle_option_key(current: &[OptionKey], key: &str) -> Vec<OptionKey> {
112    let mut next = current.to_vec();
113    if let Some(pos) = next.iter().position(|v| v == key) {
114        next.remove(pos);
115    } else {
116        next.push(key.to_string());
117    }
118    next
119}
120
121// ---- Dropdown layer & keyboard helpers ------------------------------------
122
123/// Lightweight handle describing an overlay entry reserved for a dropdown.
124#[derive(Clone, Copy)]
125pub struct DropdownLayer {
126    #[allow(dead_code)] // Kept for future overlay-key based control of dropdown layers.
127    pub key: Signal<Option<OverlayKey>>,
128    pub z_index: Signal<i32>,
129}
130
131/// Generic overlay handle for floating layers (tooltip, popover, dropdown, ...).
132#[derive(Clone, Copy)]
133pub struct FloatingLayer {
134    pub key: Signal<Option<OverlayKey>>,
135    pub z_index: Signal<i32>,
136}
137
138/// Internal helper: register/unregister a floating entry with the global
139/// `OverlayManager` for a given [`OverlayKind`].
140///
141/// - 当 `open` 为 true 时,确保存在对应 kind 的 overlay entry,并通过返回的
142///   [`FloatingLayer`] 暴露 z-index;
143/// - 当 `open` 变为 false 时,释放对应 entry,便于 z-index 复用;
144/// - 若当前树中不存在 OverlayProvider,则回退到固定 z-index(1000)。
145pub fn use_floating_layer(kind: OverlayKind, open: bool) -> FloatingLayer {
146    let overlay = use_overlay();
147    let entry_key: Signal<Option<OverlayKey>> = use_signal(|| None);
148    let z_index: Signal<i32> = use_signal(|| 1000);
149
150    {
151        let overlay = overlay.clone();
152        let mut key_signal = entry_key;
153        let mut z_signal = z_index;
154        use_effect(move || {
155            if let Some(handle) = overlay.clone() {
156                let current_key = *key_signal.read();
157                if open {
158                    if current_key.is_none() {
159                        let (key, meta) = handle.open(kind, false);
160                        z_signal.set(meta.z_index);
161                        key_signal.set(Some(key));
162                    }
163                } else if let Some(key) = current_key {
164                    handle.close(key);
165                    key_signal.set(None);
166                }
167            }
168        });
169    }
170
171    FloatingLayer {
172        key: entry_key,
173        z_index,
174    }
175}
176
177/// Hook: 专门为选择器系列组件保留的下拉浮层注册函数。
178///
179/// 目前等价于 `use_floating_layer(OverlayKind::Dropdown, open)`,单独保留函数是为了
180/// 保持现有 Select/TreeSelect/Cascader 等调用点稳定,也方便后续在下拉类 Overlay
181/// 上做额外元信息扩展。
182pub fn use_dropdown_layer(open: bool) -> DropdownLayer {
183    let FloatingLayer { key, z_index } = use_floating_layer(OverlayKind::Dropdown, open);
184
185    DropdownLayer { key, z_index }
186}
187
188/// Internal helper: compute the next active index in a linear option list.
189///
190/// This pure function is used by keyboard handlers and is easy to test :
191///
192/// - `current` is the currently highlighted index (if any)
193/// - `len` is the total number of options
194/// - `direction` is +1 for moving forward (ArrowDown) and -1 for moving
195///   backward (ArrowUp)
196pub fn next_active_index(current: Option<usize>, len: usize, direction: i32) -> Option<usize> {
197    if len == 0 {
198        return None;
199    }
200
201    let dir = if direction >= 0 { 1 } else { -1 };
202
203    match current {
204        None => {
205            if dir > 0 {
206                Some(0)
207            } else {
208                Some(len.saturating_sub(1))
209            }
210        }
211        Some(idx) => {
212            if len == 1 {
213                Some(0)
214            } else if dir > 0 {
215                Some((idx + 1) % len)
216            } else {
217                Some((idx + len - 1) % len)
218            }
219        }
220    }
221}
222
223/// Handle a key event for an option list and update the active index.
224///
225/// Returns `Some(index)` when the user confirms a selection via Enter; callers
226/// can then perform the corresponding option activation.
227pub fn handle_option_list_key_event(
228    evt: &KeyboardEvent,
229    options_len: usize,
230    active_index_signal: &Signal<Option<usize>>,
231) -> Option<usize> {
232    use dioxus::prelude::Key;
233
234    match evt.key() {
235        Key::ArrowDown => {
236            let mut signal = *active_index_signal;
237            let current = *signal.read();
238            let next = next_active_index(current, options_len, 1);
239            signal.set(next);
240            None
241        }
242        Key::ArrowUp => {
243            let mut signal = *active_index_signal;
244            let current = *signal.read();
245            let next = next_active_index(current, options_len, -1);
246            signal.set(next);
247            None
248        }
249        Key::Enter => *active_index_signal.read(),
250        Key::Escape => {
251            let mut signal = *active_index_signal;
252            signal.set(None);
253            None
254        }
255        _ => None,
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn value_to_and_from_single_key_round_trips() {
265        let original = OptionKey::from("foo");
266        let json = option_key_to_value(&original);
267        let parsed = value_to_option_key(Some(json));
268        assert_eq!(parsed, Some(original));
269    }
270
271    #[test]
272    fn value_to_and_from_multiple_keys_round_trips() {
273        let keys = vec!["a".to_string(), "b".to_string()];
274        let json = option_keys_to_value(&keys);
275        let parsed = value_to_option_keys(Some(json));
276        assert_eq!(parsed, keys);
277    }
278
279    #[test]
280    fn path_conversion_uses_string_array_representation() {
281        let path = vec!["root".to_string(), "child".to_string()];
282        let json = path_to_value(&path);
283        let parsed = value_to_path(Some(json));
284        assert_eq!(parsed, path);
285    }
286
287    #[test]
288    fn toggle_option_key_adds_and_removes_values() {
289        let current: Vec<OptionKey> = vec![];
290        let next = toggle_option_key(&current, "x");
291        assert_eq!(next, vec!["x".to_string()]);
292
293        let next2 = toggle_option_key(&next, "x");
294        assert!(next2.is_empty());
295    }
296
297    #[test]
298    fn filter_options_by_query_matches_label_case_insensitively() {
299        let options = vec![
300            SelectOption {
301                key: "1".into(),
302                label: "Apple".into(),
303                disabled: false,
304            },
305            SelectOption {
306                key: "2".into(),
307                label: "Banana".into(),
308                disabled: false,
309            },
310            SelectOption {
311                key: "3".into(),
312                label: "Cherry".into(),
313                disabled: false,
314            },
315        ];
316        let filtered = filter_options_by_query(&options, "an");
317        let labels: Vec<String> = filtered.into_iter().map(|o| o.label).collect();
318        assert_eq!(labels, vec!["Banana".to_string()]);
319    }
320
321    #[test]
322    fn next_active_index_wraps_and_handles_empty() {
323        // Empty list should always return None.
324        assert_eq!(next_active_index(None, 0, 1), None);
325        assert_eq!(next_active_index(Some(0), 0, 1), None);
326
327        // First move in non-empty list.
328        assert_eq!(next_active_index(None, 3, 1), Some(0));
329        assert_eq!(next_active_index(None, 3, -1), Some(2));
330
331        // Forward and backward wrapping.
332        assert_eq!(next_active_index(Some(0), 3, 1), Some(1));
333        assert_eq!(next_active_index(Some(2), 3, 1), Some(0));
334        assert_eq!(next_active_index(Some(0), 3, -1), Some(2));
335        assert_eq!(next_active_index(Some(1), 3, -1), Some(0));
336    }
337}