tallyweb_components/
treeview.rs

1#![allow(unused_braces)]
2#![allow(non_snake_case)]
3#![allow(dead_code)]
4
5use core::fmt::Debug;
6use std::{collections::HashMap, hash::Hash};
7
8use leptos::{ev::MouseEvent, *};
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct SelectionModel<S, T>
12where
13    S: Clone + PartialEq + Eq + Hash + 'static,
14    T: Clone + 'static + Debug + PartialEq,
15{
16    items: HashMap<S, TreeNode<T, S>>,
17    selection: HashMap<S, bool>,
18    multi_select: bool,
19}
20
21impl<S, T> Default for SelectionModel<S, T>
22where
23    S: Clone + PartialEq + Eq + Hash + 'static,
24    T: Clone + 'static + Debug + PartialEq,
25{
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl<S, T> SelectionModel<S, T>
32where
33    S: Clone + PartialEq + Eq + Hash + 'static,
34    T: Clone + 'static + Debug + PartialEq,
35{
36    pub fn new() -> Self {
37        Self {
38            items: HashMap::new(),
39            selection: HashMap::new(),
40            multi_select: false,
41        }
42    }
43
44    pub fn set_multi_select(&mut self, multi_select: bool) {
45        self.multi_select = multi_select
46    }
47
48    pub fn get(&self, key: &S) -> Option<&T> {
49        Some(&self.items.get(key)?.row)
50    }
51
52    pub fn get_mut(&mut self, key: &S) -> Option<&mut T> {
53        Some(&mut self.items.get_mut(key)?.row)
54    }
55
56    pub fn get_node(&self, key: &S) -> Option<&TreeNode<T, S>> {
57        self.items.get(key)
58    }
59
60    pub fn get_node_mut(&mut self, key: &S) -> Option<&mut TreeNode<T, S>> {
61        self.items.get_mut(key)
62    }
63
64    pub fn clear_selection(&mut self) {
65        self.selection.clear();
66    }
67
68    pub fn select(&mut self, key: &S) {
69        if !self.multi_select {
70            self.selection.clear();
71        }
72        self.selection.insert(key.clone(), true);
73    }
74
75    pub fn toggle(&mut self, key: &S) {
76        let current_value = self.is_selected(key);
77        if !self.multi_select {
78            self.selection.clear();
79        }
80
81        self.selection.insert(key.clone(), !current_value);
82    }
83
84    pub fn selection_mut(&mut self) -> Vec<&mut T> {
85        let selected = self.selection.clone();
86        self.items
87            .iter_mut()
88            .filter(|(key, _)| selected.get(*key).cloned().unwrap_or_default())
89            .map(|(_, item)| &mut item.row)
90            .collect()
91    }
92
93    pub fn selection(&self) -> Vec<&T> {
94        self.selection
95            .iter()
96            .filter(|(_, b)| **b)
97            .filter_map(|(k, _)| self.items.get(k).map(|i| &i.row))
98            .collect()
99    }
100
101    pub fn get_selected_keys(&self) -> Vec<&S> {
102        self.selection
103            .iter()
104            .filter(|(k, b)| **b && self.items.contains_key(k))
105            .map(|(key, _)| key)
106            .collect()
107    }
108
109    pub fn get_owned_selected_keys(&self) -> Vec<S> {
110        self.selection
111            .iter()
112            .filter(|(k, b)| **b && self.items.contains_key(k))
113            .map(|(key, _)| key)
114            .cloned()
115            .collect()
116    }
117
118    pub fn remove_item(&mut self, key: &S) -> Option<T> {
119        Some(self.items.remove(key)?.row)
120    }
121
122    pub fn is_selected(&self, key: &S) -> bool {
123        return self.selection.get(key).cloned().unwrap_or_default();
124    }
125
126    pub fn is_empty(&self) -> bool {
127        self.selection.is_empty()
128    }
129}
130
131#[component]
132pub fn TreeViewWidget<T, F, S, FV, IV, EC>(
133    each: F,
134    key: fn(&T) -> S,
135    each_child: EC,
136    view: FV,
137    #[prop(default=create_signal(false).0.into(), into)] show_separator: Signal<bool>,
138    #[prop(default=create_rw_signal(SelectionModel::default()), into)] selection_model: RwSignal<
139        SelectionModel<S, T>,
140    >,
141    #[prop(optional)] on_click: Option<fn(&S, MouseEvent)>,
142) -> impl IntoView
143where
144    T: Debug + Clone + PartialEq + 'static,
145    S: Debug + Clone + PartialEq + Eq + Hash + ToString + 'static,
146    F: Fn() -> Vec<T> + Copy + 'static,
147    FV: Fn(&T) -> IV + Copy + 'static,
148    IV: IntoView,
149    EC: Fn(&T) -> Vec<T> + Copy + 'static,
150{
151    let nodes = create_memo(move |_| each());
152
153    create_isomorphic_effect(move |_| {
154        each().into_iter().for_each(move |c| {
155            let key_val = store_value(key(&c));
156            if selection_model
157                .get_untracked()
158                .get_node(&key_val())
159                .is_none()
160            {
161                let node = TreeNode::<T, S>::new(key, c, 0);
162                selection_model.update(move |s| {
163                    s.items.insert(key_val(), node);
164                });
165            }
166        })
167    });
168
169    let each = move || {
170        nodes()
171            .iter()
172            .filter_map(|n| selection_model.get_untracked().get_node(&key(n)).cloned())
173            .collect::<Vec<_>>()
174    };
175
176    view! {
177        <tree-view>
178            <ul>
179                <For
180                    each
181                    key=move |c| key(&c.row)
182                    children=move |item| {
183                        view! {
184                            <TreeViewRow
185                                item=item.row.clone()
186                                key
187                                selection_model
188                                view
189                                each_child
190                                on_click
191                            >
192                                {view(&item.row)}
193                            </TreeViewRow>
194                            <Show when=show_separator fallback=|| ()>
195                                <hr />
196                            </Show>
197                        }
198                    }
199                />
200
201            </ul>
202        </tree-view>
203    }
204    .into_view()
205}
206
207#[component]
208fn TreeViewRow<T, S, FV, IV, EC>(
209    children: ChildrenFn,
210    item: T,
211    key: fn(&T) -> S,
212    each_child: EC,
213    view: FV,
214    selection_model: RwSignal<SelectionModel<S, T>>,
215    on_click: Option<fn(&S, MouseEvent)>,
216) -> impl IntoView
217where
218    T: Debug + Clone + PartialEq + 'static,
219    S: Debug + Clone + PartialEq + Eq + Hash + ToString + 'static,
220    FV: Fn(&T) -> IV + Copy + 'static,
221    IV: IntoView,
222    EC: Fn(&T) -> Vec<T> + Copy + 'static,
223{
224    let key_val = store_value(key(&item));
225
226    let node = create_read_slice(selection_model, move |sm| sm.items.get(&key_val()).cloned());
227
228    let (is_expanded, toggle_expand) = create_slice(
229        selection_model,
230        move |model| {
231            model
232                .items
233                .get(&key_val())
234                .map(|n| n.is_expanded)
235                .unwrap_or_default()
236        },
237        move |model, _| {
238            if let Some(node) = model.items.get_mut(&key_val()) {
239                node.toggle_expand()
240            };
241        },
242    );
243
244    let (is_selected, set_selected) = create_slice(
245        selection_model,
246        move |model| model.is_selected(&key_val()),
247        move |model, _| model.select(&key_val()),
248    );
249
250    let caret_class = move || "caret fa-solid fa-caret-right";
251
252    let div_class = move || {
253        let mut class = String::from("selectable row");
254        if is_selected() {
255            class += " selected"
256        }
257
258        class
259    };
260
261    let background = create_memo(move |_| {
262        if is_selected() {
263            "var(--accent, #3584E4)"
264        } else {
265            "none"
266        }
267    });
268
269    let on_row_click = move |_: MouseEvent| set_selected(());
270
271    let on_caret_click = move |ev: MouseEvent| {
272        ev.stop_propagation();
273        toggle_expand(())
274    };
275
276    let depth = move || node().map(|n| n.depth).unwrap_or_default();
277
278    let depth_style = move || {
279        let margin = format!("{}em", 2.0 * depth() as f64);
280        let style = format!("padding-left:{};", margin);
281        style
282    };
283
284    let node_children = create_memo(move |_| each_child(&item));
285
286    create_isomorphic_effect(move |_| {
287        node_children().into_iter().for_each(|c| {
288            let key_val = store_value(key(&c));
289            if selection_model
290                .get_untracked()
291                .get_node(&key_val())
292                .is_none()
293            {
294                let node = TreeNode::<T, S>::new(key, c, depth() + 1);
295                selection_model.update(|s| {
296                    s.items.insert(key_val(), node);
297                });
298            }
299        });
300    });
301
302    let children = store_value(children);
303
304    view! {
305        <li style:display="block">
306            <div
307                style=depth_style
308                style:background=background
309                style:display="flex"
310                class=div_class
311                on:click=move |ev| {
312                    if let Some(f) = on_click {
313                        if let Some(k) = key_val.try_get_value() {
314                            f(&k, ev);
315                        }
316                    } else {
317                        on_row_click(ev);
318                    }
319                }
320            >
321
322                <Show when=move || {
323                    node.try_get_untracked()
324                        .flatten()
325                        .is_some_and(|c| !each_child(&c.row).is_empty())
326                }>
327                    <div
328                        class=caret_class
329                        style:transform=move || if is_expanded() { "rotate(90deg)" } else { "" }
330                        style:cursor="pointer"
331                        style:font-size="24px"
332                        style:transition="transform 0.24s"
333                        on:click=on_caret_click
334                    ></div>
335                </Show>
336                {children()}
337            </div>
338            <ul style:display=move || if is_expanded() { "block" } else { "none" }>
339                <For
340                    each=node_children
341                    key
342                    children=move |item| {
343                        view! {
344                            <TreeViewRow
345                                key
346                                item=item.clone()
347                                selection_model=selection_model
348                                each_child=each_child
349                                view=view
350                                on_click
351                            >
352                                {view(&item)}
353                            </TreeViewRow>
354                        }
355                    }
356                />
357
358            </ul>
359        </li>
360    }
361}
362
363#[derive(Debug, Clone, PartialEq)]
364pub struct TreeNode<T, S>
365where
366    T: Clone + 'static + Debug + PartialEq,
367    S: Clone + PartialEq + Eq + Hash + 'static,
368{
369    pub key: fn(&T) -> S,
370    pub row: T,
371    pub depth: usize,
372    pub is_expanded: bool,
373}
374
375impl<T, S> TreeNode<T, S>
376where
377    T: Clone + 'static + Debug + PartialEq,
378    S: Clone + PartialEq + Eq + Hash + 'static,
379{
380    pub fn new(key: fn(&T) -> S, item: T, depth: usize) -> Self {
381        Self {
382            key,
383            row: item.clone(),
384            depth,
385            is_expanded: false,
386        }
387    }
388
389    pub fn set_expand(&mut self, do_expand: bool) {
390        self.is_expanded = do_expand
391    }
392
393    pub fn toggle_expand(&mut self) {
394        self.is_expanded = !self.is_expanded
395    }
396}