patternfly_yew/components/pagination/
mod.rs1mod simple;
4pub use simple::*;
5
6use crate::prelude::{
7 AsClasses, Button, ButtonVariant, Dropdown, ExtendClasses, Icon, MenuAction, TextInput,
8 TextInputType, use_on_enter,
9};
10use yew::prelude::*;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum PaginationPosition {
14 Top,
15 Bottom,
16}
17
18impl AsClasses for PaginationPosition {
19 fn extend_classes(&self, classes: &mut Classes) {
20 match self {
21 Self::Top => {}
22 Self::Bottom => classes.push(classes!("pf-m-top")),
23 }
24 }
25}
26
27#[derive(Clone, PartialEq, Properties)]
29pub struct PaginationProperties {
30 #[prop_or_default]
31 pub total_entries: Option<usize>,
32 #[prop_or_default]
33 pub offset: usize,
34 #[prop_or(vec![10,25,50])]
35 pub entries_per_page_choices: Vec<usize>,
36 #[prop_or(25)]
37 pub selected_choice: usize,
38
39 #[prop_or_default]
41 pub onnavigation: Callback<Navigation>,
42
43 #[prop_or_default]
45 pub onlimit: Callback<usize>,
46
47 #[prop_or_default]
49 pub id: Option<AttrValue>,
50
51 #[prop_or_default]
53 pub style: AttrValue,
54
55 #[prop_or(PaginationPosition::Top)]
56 pub position: PaginationPosition,
57
58 #[prop_or_default]
60 pub disabled: bool,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum Navigation {
65 First,
66 Previous,
67 Next,
68 Last,
69 Page(usize),
71}
72
73#[function_component(Pagination)]
87pub fn pagination(props: &PaginationProperties) -> Html {
88 let mut menu_classes = classes!("pf-v6-c-pagination__page-menu");
90 menu_classes.extend_from(&props.position);
91
92 let empty = props
94 .total_entries
95 .map(|total| total == 0)
96 .unwrap_or_default();
97
98 let max_page = props
100 .total_entries
101 .map(|m| (m as f64 / props.selected_choice as f64).ceil() as usize);
102
103 let current_page = match empty {
105 true => 0,
106 false => (props.offset as f64 / props.selected_choice as f64).ceil() as usize,
107 };
108
109 let is_last_page = if let Some(max) = props.total_entries {
111 props.offset + props.selected_choice >= max
112 } else {
113 false
114 };
115
116 let total_entries = props
118 .total_entries
119 .map(|m| format!("{}", m))
120 .unwrap_or_else(|| String::from("many"));
121
122 let start = match empty {
124 true => 0,
125 false => props.offset + 1,
127 };
128
129 let mut end = props.offset + props.selected_choice;
130 if let Some(total) = props.total_entries {
131 end = end.min(total);
132 }
133 let showing = format!("{start} - {end}",);
134
135 let limit_choices = props.entries_per_page_choices.clone();
136
137 let node = use_node_ref();
138
139 let input = use_state_eq(|| 0);
143 let input_text = use_state_eq(|| Some((current_page + 1).to_string()));
145
146 if input_text.is_none() {
147 input_text.set(Some((current_page + 1).to_string()));
148 }
149
150 let onkeydown = use_on_enter(
151 (input.clone(), props.onnavigation.clone(), max_page),
152 |(input, onnavigation, max_page)| {
153 let mut page: usize = **input;
154 if let Some(max_page) = max_page {
155 if page > *max_page {
156 page = *max_page;
157 }
158 }
159 page = page.saturating_sub(1);
161 log::debug!("Emit page change: {page}");
162 onnavigation.emit(Navigation::Page(page));
163 },
164 );
165
166 let onchange = use_callback(
167 (input.clone(), input_text.clone(), max_page, current_page),
168 |text: String, (input, input_text, max_page, current_page)| {
169 input_text.set(Some(text.clone()));
170
171 let value = match text.parse::<usize>() {
172 Ok(value) => {
173 let max_page = max_page.unwrap_or(usize::MAX);
174 if value > 0 && value <= max_page {
175 Some(value)
176 } else {
177 None
178 }
179 }
180 Err(_) => None,
181 };
182
183 if let Some(value) = value {
184 input.set(value);
185 } else {
186 input.set(current_page.saturating_add(1));
188 }
189
190 log::debug!("New prepared page value: {:?} / {}", **input_text, **input);
191 },
192 );
193
194 let onblur = use_callback(input_text.clone(), |_, input_text| {
195 input_text.set(None);
196 });
197
198 let onnavigation = use_callback(
199 (props.onnavigation.clone(), input_text.clone()),
200 |nav, (onnavigation, input_text)| {
201 input_text.set(None);
202 onnavigation.emit(nav);
203 },
204 );
205
206 {
208 let input_text = input_text.clone();
209 use_effect_with(
210 (props.offset, props.selected_choice, props.total_entries),
211 move |(offset, selected, total)| {
212 let r = (*offset as f64 / *selected as f64).ceil() as usize;
213
214 if *total == Some(0) {
215 input_text.set(Some("0".to_string()));
216 } else {
217 input_text.set(Some((r + 1).to_string()));
218 }
219 },
220 );
221 }
222
223 let onlimit = use_callback(
225 (props.onlimit.clone(), input_text.clone()),
226 |limit, (onlimit, input_text)| {
227 input_text.set(None);
228 onlimit.emit(limit);
229 },
230 );
231
232 let pagination_classes = match &props.position {
234 PaginationPosition::Top => classes!("pf-v6-c-pagination"),
235 PaginationPosition::Bottom => classes!("pf-v6-c-pagination", "pf-m-bottom"),
236 };
237
238 let pagination_styles = format!(
239 "--pf-v6-c-pagination__nav-page-select--c-form-control--width-chars: {};",
240 max_page.unwrap_or_default().to_string().len().clamp(2, 10)
241 );
242
243 let unbound = props.total_entries.is_none();
246
247 html! (
248 <div
249 id={&props.id}
250 class={pagination_classes}
251 style={[pagination_styles, props.style.to_string()].join(" ")}
252 ref={node}
253 >
254 <div class="pf-v6-c-pagination__total-items">
256 <b>{ showing.clone() }</b> {"\u{00a0}of\u{00a0}"}
257 <b>{ total_entries.clone() }</b>
258 </div>
259
260 <Dropdown
261 text={html!(
262 <>
263 <b>{ showing }</b>{"\u{00a0}of\u{00a0}"}
264 <b>{ total_entries }</b>
265 </>
266 )}
267 disabled={props.disabled}
268 >
269 { for limit_choices.into_iter().map(|limit| {
270 let onlimit = onlimit.clone();
271 let onclick = Callback::from(move |_|{
272 log::warn!(">> Limit changed to {}", limit);
273 onlimit.emit(limit);
274 });
275
276 html_nested!(
277 <MenuAction {onclick} selected={props.selected_choice == limit}>
278 {limit} {" per page"} {props.selected_choice}
279 </MenuAction>
280 )
281 })}
282 </Dropdown>
283
284 <nav class="pf-v6-c-pagination__nav" aria-label="Pagination">
286 <div class="pf-v6-c-pagination__nav-control pf-m-first">
287 <Button
288 variant={ButtonVariant::Plain}
289 onclick={onnavigation.reform(|_|Navigation::First)}
290 disabled={ props.disabled || props.offset == 0 }
291 aria_label="Go to first page"
292 >
293 { Icon::AngleDoubleLeft }
294 </Button>
295 </div>
296 <div class="pf-v6-c-pagination__nav-control pf-m-prev">
297 <Button
298 aria_label="Go to previous page"
299 variant={ButtonVariant::Plain}
300 onclick={onnavigation.reform(|_|Navigation::Previous)}
301 disabled={ props.disabled || props.offset == 0 }
302 >
303 { Icon::AngleLeft }
304 </Button>
305 </div>
306 <div class="pf-v6-c-pagination__nav-page-select">
307 <TextInput
308 r#type={TextInputType::Number}
309 inputmode="number"
310 {onchange}
311 {onkeydown}
312 {onblur}
313 value={(*input_text).clone().unwrap_or_else(|| (current_page+1).to_string()) }
314 disabled={ props.disabled || empty }
315 />
316 if let Some(max_page) = max_page {
317 <span aria-hidden="true">{ "of "} { max_page }</span>
318 }
319 </div>
320
321 <div class="pf-v6-c-pagination__nav-control pf-m-next">
322 <Button
323 aria_label="Go to next page"
324 variant={ButtonVariant::Plain}
325 onclick={onnavigation.reform(|_|Navigation::Next)}
326 disabled={ props.disabled || is_last_page }
327 >
328 { Icon::AngleRight }
329 </Button>
330 </div>
331 <div class="pf-v6-c-pagination__nav-control pf-m-last">
332 <Button
333 aria_label="Go to last page"
334 variant={ButtonVariant::Plain}
335 onclick={onnavigation.reform(|_|Navigation::Last)}
336 disabled={ props.disabled || unbound || is_last_page }
337 >
338 { Icon::AngleDoubleRight }
339 </Button>
340 </div>
341 </nav>
342 </div>
343 )
344}