1use ironhtml::typed::Element;
7use ironhtml_elements::{Li, Nav, Span, Ul, A};
8
9extern crate alloc;
10use alloc::format;
11use alloc::string::String;
12use alloc::string::ToString;
13
14#[derive(Clone, Copy, Default)]
16pub enum PaginationSize {
17 Small,
18 #[default]
19 Default,
20 Large,
21}
22
23impl PaginationSize {
24 const fn class(self) -> &'static str {
25 match self {
26 Self::Small => "pagination-sm",
27 Self::Default => "",
28 Self::Large => "pagination-lg",
29 }
30 }
31}
32
33pub struct PageItem {
35 pub label: String,
36 pub href: String,
37 pub active: bool,
38 pub disabled: bool,
39}
40
41impl PageItem {
42 #[must_use]
44 pub fn page(number: u32, href: impl Into<String>) -> Self {
45 Self {
46 label: number.to_string(),
47 href: href.into(),
48 active: false,
49 disabled: false,
50 }
51 }
52
53 #[must_use]
55 pub fn labeled(label: impl Into<String>, href: impl Into<String>) -> Self {
56 Self {
57 label: label.into(),
58 href: href.into(),
59 active: false,
60 disabled: false,
61 }
62 }
63
64 #[must_use]
66 pub const fn active(mut self) -> Self {
67 self.active = true;
68 self
69 }
70
71 #[must_use]
73 pub const fn disabled(mut self) -> Self {
74 self.disabled = true;
75 self
76 }
77}
78
79#[must_use]
96pub fn pagination(items: &[PageItem]) -> Element<Nav> {
97 Element::<Nav>::new()
98 .attr("aria-label", "Page navigation")
99 .child::<Ul, _>(|ul| {
100 items.iter().fold(ul.class("pagination"), |ul, item| {
101 ul.child::<Li, _>(|_| page_item(item))
102 })
103 })
104}
105
106#[must_use]
108pub fn pagination_sized(items: &[PageItem], size: PaginationSize) -> Element<Nav> {
109 let class = if size.class().is_empty() {
110 "pagination".to_string()
111 } else {
112 format!("pagination {}", size.class())
113 };
114
115 Element::<Nav>::new()
116 .attr("aria-label", "Page navigation")
117 .child::<Ul, _>(|ul| {
118 items.iter().fold(ul.class(&class), |ul, item| {
119 ul.child::<Li, _>(|_| page_item(item))
120 })
121 })
122}
123
124#[must_use]
126pub fn pagination_with_nav(
127 items: &[PageItem],
128 prev_href: Option<&str>,
129 next_href: Option<&str>,
130) -> Element<Nav> {
131 Element::<Nav>::new()
132 .attr("aria-label", "Page navigation")
133 .child::<Ul, _>(|ul| {
134 let ul = ul.class("pagination").child::<Li, _>(|li| {
136 let li_class = if prev_href.is_none() {
137 "page-item disabled"
138 } else {
139 "page-item"
140 };
141 let href = prev_href.unwrap_or("#");
142 li.class(li_class)
143 .child::<A, _>(|a| a.class("page-link").attr("href", href).text("Previous"))
144 });
145
146 let ul = items
148 .iter()
149 .fold(ul, |ul, item| ul.child::<Li, _>(|_| page_item(item)));
150
151 ul.child::<Li, _>(|li| {
153 let li_class = if next_href.is_none() {
154 "page-item disabled"
155 } else {
156 "page-item"
157 };
158 let href = next_href.unwrap_or("#");
159 li.class(li_class)
160 .child::<A, _>(|a| a.class("page-link").attr("href", href).text("Next"))
161 })
162 })
163}
164
165#[must_use]
167pub fn pagination_with_arrows(
168 items: &[PageItem],
169 prev_href: Option<&str>,
170 next_href: Option<&str>,
171) -> Element<Nav> {
172 Element::<Nav>::new()
173 .attr("aria-label", "Page navigation")
174 .child::<Ul, _>(|ul| {
175 let ul = ul.class("pagination").child::<Li, _>(|li| {
177 let li_class = if prev_href.is_none() {
178 "page-item disabled"
179 } else {
180 "page-item"
181 };
182 let href = prev_href.unwrap_or("#");
183 li.class(li_class).child::<A, _>(|a| {
184 a.class("page-link")
185 .attr("href", href)
186 .attr("aria-label", "Previous")
187 .child::<Span, _>(|s| s.attr("aria-hidden", "true").text("\u{ab}"))
188 })
189 });
190
191 let ul = items
193 .iter()
194 .fold(ul, |ul, item| ul.child::<Li, _>(|_| page_item(item)));
195
196 ul.child::<Li, _>(|li| {
198 let li_class = if next_href.is_none() {
199 "page-item disabled"
200 } else {
201 "page-item"
202 };
203 let href = next_href.unwrap_or("#");
204 li.class(li_class).child::<A, _>(|a| {
205 a.class("page-link")
206 .attr("href", href)
207 .attr("aria-label", "Next")
208 .child::<Span, _>(|s| s.attr("aria-hidden", "true").text("\u{bb}"))
209 })
210 })
211 })
212}
213
214#[must_use]
216pub fn pagination_centered(items: &[PageItem]) -> Element<Nav> {
217 Element::<Nav>::new()
218 .attr("aria-label", "Page navigation")
219 .child::<Ul, _>(|ul| {
220 items
221 .iter()
222 .fold(ul.class("pagination justify-content-center"), |ul, item| {
223 ul.child::<Li, _>(|_| page_item(item))
224 })
225 })
226}
227
228#[must_use]
230pub fn pagination_end(items: &[PageItem]) -> Element<Nav> {
231 Element::<Nav>::new()
232 .attr("aria-label", "Page navigation")
233 .child::<Ul, _>(|ul| {
234 items
235 .iter()
236 .fold(ul.class("pagination justify-content-end"), |ul, item| {
237 ul.child::<Li, _>(|_| page_item(item))
238 })
239 })
240}
241
242fn page_item(item: &PageItem) -> Element<Li> {
244 let mut class = String::from("page-item");
245 if item.active {
246 class.push_str(" active");
247 }
248 if item.disabled {
249 class.push_str(" disabled");
250 }
251
252 let mut li = Element::<Li>::new().class(&class);
253
254 if item.active {
255 li = li.attr("aria-current", "page").child::<A, _>(|a| {
256 a.class("page-link")
257 .attr("href", &item.href)
258 .text(&item.label)
259 });
260 } else {
261 li = li.child::<A, _>(|a| {
262 a.class("page-link")
263 .attr("href", &item.href)
264 .text(&item.label)
265 });
266 }
267
268 li
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use alloc::vec;
275
276 #[test]
277 fn test_pagination() {
278 let items = vec![
279 PageItem::page(1, "#").active(),
280 PageItem::page(2, "#"),
281 PageItem::page(3, "#"),
282 ];
283 let nav = pagination(&items);
284 let html = nav.render();
285 assert!(html.contains("pagination"));
286 assert!(html.contains("page-item active"));
287 assert!(html.contains("page-link"));
288 }
289
290 #[test]
291 fn test_pagination_sizes() {
292 let items = vec![PageItem::page(1, "#")];
293
294 let small = pagination_sized(&items, PaginationSize::Small);
295 assert!(small.render().contains("pagination-sm"));
296
297 let large = pagination_sized(&items, PaginationSize::Large);
298 assert!(large.render().contains("pagination-lg"));
299 }
300
301 #[test]
302 fn test_pagination_with_nav() {
303 let items = vec![PageItem::page(1, "#").active(), PageItem::page(2, "#")];
304 let nav = pagination_with_nav(&items, Some("#"), Some("#page2"));
305 let html = nav.render();
306 assert!(html.contains("Previous"));
307 assert!(html.contains("Next"));
308 }
309}