Skip to main content

nightshade_api/web/
list_editor.rs

1//! An editable, reorderable list component ([`OrderedList`]) and its [`ListItem`] entry.
2
3use leptos::prelude::*;
4
5/// An entry in an [`OrderedList`], identified by `id` with a display `label`.
6#[derive(Clone)]
7pub struct ListItem {
8    /// The stable identifier passed to the list's callbacks.
9    pub id: String,
10    /// The text shown for this row.
11    pub label: String,
12}
13
14impl ListItem {
15    /// Creates a [`ListItem`] from an id and its display label.
16    pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
17        Self {
18            id: id.into(),
19            label: label.into(),
20        }
21    }
22}
23
24/// A reorderable list of [`ListItem`]s, showing move-up, move-down, and remove actions
25/// only for the callbacks that are provided (up/down are disabled at the ends). Each
26/// callback receives the affected item's id, and `on_select` fires when a row's label is
27/// clicked. Renders an "Empty" placeholder when there are no items.
28#[component]
29pub fn OrderedList(
30    #[prop(into)] items: Signal<Vec<ListItem>>,
31    #[prop(optional)] on_move_up: Option<Callback<String>>,
32    #[prop(optional)] on_move_down: Option<Callback<String>>,
33    #[prop(optional)] on_remove: Option<Callback<String>>,
34    #[prop(optional)] on_select: Option<Callback<String>>,
35) -> impl IntoView {
36    view! {
37        <div class="nightshade-ordered-list" role="list">
38            {move || {
39                let rows = items.get();
40                let count = rows.len();
41                if count == 0 {
42                    return view! { <div class="nightshade-ordered-empty">"Empty"</div> }.into_any();
43                }
44                rows.into_iter()
45                    .enumerate()
46                    .map(|(index, item)| {
47                        let select_id = item.id.clone();
48                        let up_id = item.id.clone();
49                        let down_id = item.id.clone();
50                        let remove_id = item.id.clone();
51                        let is_first = index == 0;
52                        let is_last = index + 1 == count;
53                        view! {
54                            <div class="nightshade-ordered-row" role="listitem">
55                                <button
56                                    class="nightshade-ordered-label"
57                                    on:click=move |_| {
58                                        if let Some(callback) = on_select {
59                                            callback.run(select_id.clone());
60                                        }
61                                    }
62                                >
63                                    {item.label}
64                                </button>
65                                <div class="nightshade-ordered-actions">
66                                    {on_move_up
67                                        .map(|callback| {
68                                            view! {
69                                                <button
70                                                    class="nightshade-ordered-action"
71                                                    aria-label="Move up"
72                                                    disabled=is_first
73                                                    on:click=move |_| callback.run(up_id.clone())
74                                                >
75                                                    "\u{25b2}"
76                                                </button>
77                                            }
78                                        })}
79                                    {on_move_down
80                                        .map(|callback| {
81                                            view! {
82                                                <button
83                                                    class="nightshade-ordered-action"
84                                                    aria-label="Move down"
85                                                    disabled=is_last
86                                                    on:click=move |_| callback.run(down_id.clone())
87                                                >
88                                                    "\u{25bc}"
89                                                </button>
90                                            }
91                                        })}
92                                    {on_remove
93                                        .map(|callback| {
94                                            view! {
95                                                <button
96                                                    class="nightshade-ordered-action danger"
97                                                    aria-label="Remove"
98                                                    on:click=move |_| callback.run(remove_id.clone())
99                                                >
100                                                    "\u{00d7}"
101                                                </button>
102                                            }
103                                        })}
104                                </div>
105                            </div>
106                        }
107                    })
108                    .collect_view()
109                    .into_any()
110            }}
111        </div>
112    }
113}