fc_dioxus/components/
pagination.rs1use crate::{
3 core::{assets, attrs::component_attributes},
4 prelude::*,
5 theme::use_stylesheets,
6};
7const BEM: Bem = Bem::new("fc-pagination");
8const LINK: Bem = Bem::new("fc-pagination__link");
9fc_modifier! { enum PageDirection for LINK { Page => None, Prev => "prev", Next => "next" } }
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum PageItem {
14 Page(u32),
16 Gap,
18}
19
20#[must_use]
32pub fn page_window(page: u32, pages: u32, siblings: u32) -> Vec<PageItem> {
33 if pages == 0 {
34 return Vec::new();
35 }
36 let page = page.clamp(1, pages);
37 let low = page.saturating_sub(siblings).max(1);
38 let high = page.saturating_add(siblings).min(pages);
39 let mut shown: Vec<u32> = vec![1];
40 shown.extend(low..=high);
41 shown.push(pages);
42 shown.sort_unstable();
43 shown.dedup();
44 let mut items = Vec::with_capacity(shown.len() + 2);
45 let mut previous = 0;
46 for number in shown {
47 match number - previous {
48 1 => {}
49 2 => items.push(PageItem::Page(previous + 1)),
50 _ => items.push(PageItem::Gap),
51 }
52 items.push(PageItem::Page(number));
53 previous = number;
54 }
55 items
56}
57
58#[component]
63pub fn Pagination(
64 label: AccessibleName,
65 page: u32,
66 pages: u32,
67 #[props(default = 1)] siblings: u32,
68 #[props(default)] items: Option<Vec<PageItem>>,
69 #[props(default)] href: Option<Callback<u32, String>>,
70 #[props(default)] onchange: Option<EventHandler<u32>>,
71 #[props(default = "Previous".to_owned())] previous: String,
72 #[props(default = "Next".to_owned())] next: String,
73 #[props(default)] class: Option<String>,
74 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
75) -> Element {
76 let attributes = component_attributes(
77 attributes,
78 ClassList::new(BEM.block()).extend(class.as_deref()),
79 &["aria-label"],
80 );
81 let items = items.unwrap_or_else(|| page_window(page, pages, siblings));
82 let link =
83 move |target: u32, direction: PageDirection, enabled: bool, current: bool, text: String| {
84 let class = ClassList::new(LINK.block())
85 .with_modifier(LINK, direction)
86 .to_string();
87 let availability = if enabled {
88 Availability::Enabled
89 } else {
90 Availability::Disabled
91 };
92 let url = href.map(|href| href.call(target));
93 let onclick = onchange.map(|onchange| {
94 EventHandler::new(move |event: MouseEvent| {
95 event.prevent_default();
98 onchange.call(target);
99 })
100 });
101 let url = url.or_else(|| onclick.is_some().then(|| format!("#page-{target}")));
102 super::nav_target(
103 class,
104 url,
105 onclick,
106 current,
107 &availability,
108 Vec::new(),
109 rsx! { "{text}" },
110 )
111 };
112 rsx! {
113 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_PAGINATION_CSS } }
114 nav { aria_label: label.to_string(), ..attributes,
115 ul { class: BEM.element("list"),
116 li { {link(page.saturating_sub(1), PageDirection::Prev, page > 1, false, previous)} }
117 for (index, item) in items.into_iter().enumerate() {
118 li { key: "{index}",
119 match item {
120 PageItem::Page(number) => link(number, PageDirection::Page, true, number == page, number.to_string()),
121 PageItem::Gap => rsx! { span { class: BEM.element("gap"), aria_hidden: "true", "…" } },
122 }
123 }
124 }
125 li { {link(page.saturating_add(1), PageDirection::Next, page < pages, false, next)} }
126 }
127 }
128 }
129}