yew_bs/components/
pagination.rs1use yew::prelude::*;
2use crate::components::common::Size;
3#[derive(Clone, Copy, PartialEq, Debug)]
4pub enum PaginationAlignment {
5 Start,
6 Center,
7 End,
8}
9impl PaginationAlignment {
10 pub fn as_str(&self) -> &'static str {
11 match self {
12 PaginationAlignment::Start => "justify-content-start",
13 PaginationAlignment::Center => "justify-content-center",
14 PaginationAlignment::End => "justify-content-end",
15 }
16 }
17}
18#[derive(Properties, PartialEq)]
20pub struct PaginationProps {
21 pub active_page: usize,
23 pub total_pages: usize,
25 #[prop_or_default]
27 pub on_page_change: Callback<usize>,
28 #[prop_or_default]
30 pub size: Option<Size>,
31 #[prop_or_default]
33 pub alignment: Option<PaginationAlignment>,
34 #[prop_or(7)]
36 pub max_page_buttons: usize,
37 #[prop_or(false)]
39 pub show_first_last: bool,
40 #[prop_or("Previous".into())]
42 pub prev_text: AttrValue,
43 #[prop_or("Next".into())]
45 pub next_text: AttrValue,
46 #[prop_or("First".into())]
48 pub first_text: AttrValue,
49 #[prop_or("Last".into())]
51 pub last_text: AttrValue,
52 #[prop_or("Page navigation".into())]
54 pub aria_label: AttrValue,
55 #[prop_or_default]
57 pub class: Option<AttrValue>,
58}
59#[function_component(Pagination)]
61pub fn pagination(props: &PaginationProps) -> Html {
62 if props.total_pages <= 1 {
63 return html! {};
64 }
65 let mut classes_vec = vec!["pagination".to_string()];
66 if let Some(size) = &props.size {
67 classes_vec.push(format!("pagination-{}", size.as_str()));
68 }
69 if let Some(class) = &props.class {
70 classes_vec.push(class.to_string());
71 }
72 let classes = classes!(classes_vec);
73 let on_page_change = props.on_page_change.clone();
74 let page_numbers = calculate_visible_pages(
75 props.active_page,
76 props.total_pages,
77 props.max_page_buttons,
78 );
79 html! {
80 < nav aria - label = { props.aria_label.clone() } > < ul class = { classes } > if
81 props.show_first_last { < li class = { classes!("page-item", if props.active_page
82 <= 1 { "disabled" } else { "" }) } > < a class = "page-link" href = "#" onclick =
83 { Callback::from({ let on_page_change = on_page_change.clone(); let active_page =
84 props.active_page; move | e : MouseEvent | { e.prevent_default(); if active_page
85 > 1 { on_page_change.emit(1); } } }) } aria - label = "First page" > { props
86 .first_text.clone() } </ a > </ li > } < li class = { classes!("page-item", if
87 props.active_page <= 1 { "disabled" } else { "" }) } > < a class = "page-link"
88 href = "#" onclick = { Callback::from({ let on_page_change = on_page_change
89 .clone(); let active_page = props.active_page; move | e : MouseEvent | { e
90 .prevent_default(); if active_page > 1 { on_page_change.emit(active_page - 1); }
91 } }) } aria - label = "Previous page" > { props.prev_text.clone() } </ a > </ li
92 > { page_numbers.into_iter().map(| page_num | { let is_active = page_num == props
93 .active_page; let is_ellipsis = page_num == 0; if is_ellipsis { html! { < li
94 class = "page-item disabled" > < span class = "page-link" > { "…" } </ span >
95 </ li > } } else { let on_click = { let on_page_change = props.on_page_change
96 .clone(); Callback::from(move | e : MouseEvent | { e.prevent_default();
97 on_page_change.emit(page_num); }) }; html! { < li class = { classes!("page-item",
98 is_active.then_some("active")) } > < a class = "page-link" href = "#" onclick = {
99 on_click } aria - label = { format!("Page {}", page_num) } aria - current = {
100 is_active.then_some("page") } > { page_num.to_string() } </ a > </ li > } } })
101 .collect::< Html > () } < li class = { classes!("page-item", if props.active_page
102 >= props.total_pages { "disabled" } else { "" }) } > < a class = "page-link" href
103 = "#" onclick = { Callback::from({ let on_page_change = on_page_change.clone();
104 let active_page = props.active_page; let total_pages = props.total_pages; move |
105 e : MouseEvent | { e.prevent_default(); if active_page < total_pages {
106 on_page_change.emit(active_page + 1); } } }) } aria - label = "Next page" > {
107 props.next_text.clone() } </ a > </ li > if props.show_first_last { < li class =
108 { classes!("page-item", if props.active_page >= props.total_pages { "disabled" }
109 else { "" }) } > < a class = "page-link" href = "#" onclick = { Callback::from({
110 let on_page_change = on_page_change.clone(); let active_page = props.active_page;
111 let total_pages = props.total_pages; move | e : MouseEvent | { e
112 .prevent_default(); if active_page < total_pages { on_page_change
113 .emit(total_pages); } } }) } aria - label = "Last page" > { props.last_text
114 .clone() } </ a > </ li > } </ ul > </ nav >
115 }
116}
117fn calculate_visible_pages(
119 active_page: usize,
120 total_pages: usize,
121 max_buttons: usize,
122) -> Vec<usize> {
123 if total_pages <= max_buttons {
124 return (1..=total_pages).collect();
125 }
126 let mut pages = Vec::new();
127 let half_max = max_buttons / 2;
128 pages.push(1);
129 let start = (active_page.saturating_sub(half_max)).max(2);
130 let end = (active_page + half_max).min(total_pages - 1);
131 if start > 2 {
132 pages.push(0);
133 }
134 for page in start..=end {
135 pages.push(page);
136 }
137 if end < total_pages - 1 {
138 pages.push(0);
139 }
140 if total_pages > 1 {
141 pages.push(total_pages);
142 }
143 pages
144}
145#[cfg(test)]
146mod tests {
147 use super::*;
148 #[test]
149 fn test_calculate_visible_pages_small() {
150 assert_eq!(calculate_visible_pages(1, 5, 7), vec![1, 2, 3, 4, 5]);
151 }
152 #[test]
153 fn test_calculate_visible_pages_start() {
154 assert_eq!(calculate_visible_pages(2, 10, 5), vec![1, 2, 3, 4, 0, 10]);
155 }
156 #[test]
157 fn test_calculate_visible_pages_middle() {
158 assert_eq!(calculate_visible_pages(5, 10, 5), vec![1, 0, 3, 4, 5, 6, 7, 0, 10]);
159 }
160 #[test]
161 fn test_calculate_visible_pages_end() {
162 assert_eq!(calculate_visible_pages(9, 10, 5), vec![1, 0, 7, 8, 9, 10]);
163 }
164}