ironhtml_bootstrap/
pagination.rs

1//! Bootstrap pagination components.
2//!
3//! Provides type-safe Bootstrap pagination matching the
4//! [Bootstrap pagination documentation](https://getbootstrap.com/docs/5.3/components/pagination/).
5
6use 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/// Pagination size options.
15#[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
33/// A pagination item.
34pub struct PageItem {
35    pub label: String,
36    pub href: String,
37    pub active: bool,
38    pub disabled: bool,
39}
40
41impl PageItem {
42    /// Create a regular page item.
43    #[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    /// Create a page item with custom label.
54    #[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    /// Mark this item as active.
65    #[must_use]
66    pub const fn active(mut self) -> Self {
67        self.active = true;
68        self
69    }
70
71    /// Mark this item as disabled.
72    #[must_use]
73    pub const fn disabled(mut self) -> Self {
74        self.disabled = true;
75        self
76    }
77}
78
79/// Create a Bootstrap pagination.
80///
81/// ## Example
82///
83/// ```rust
84/// use ironhtml_bootstrap::pagination::{pagination, PageItem};
85///
86/// let items = vec![
87///     PageItem::page(1, "#").active(),
88///     PageItem::page(2, "#"),
89///     PageItem::page(3, "#"),
90/// ];
91///
92/// let nav = pagination(&items);
93/// assert!(nav.render().contains("pagination"));
94/// ```
95#[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/// Create a sized pagination.
107#[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/// Create a pagination with previous/next buttons.
125#[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            // Previous button
135            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            // Page items
147            let ul = items
148                .iter()
149                .fold(ul, |ul, item| ul.child::<Li, _>(|_| page_item(item)));
150
151            // Next button
152            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/// Create a pagination with icon arrows.
166#[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            // Previous arrow
176            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            // Page items
192            let ul = items
193                .iter()
194                .fold(ul, |ul, item| ul.child::<Li, _>(|_| page_item(item)));
195
196            // Next arrow
197            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/// Create a centered pagination.
215#[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/// Create a right-aligned pagination.
229#[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
242/// Create a simple page item element.
243fn 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}