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}