feat: Pagination (#212)

* feat: Pagination

* feat: optimize Pagination component

* fix: build nightly

---------

Co-authored-by: luoxiao <luoxiaozero@163.com>
This commit is contained in:
Raiyol 2024-07-14 17:45:47 +02:00 committed by GitHub
parent 68f81a04eb
commit 96f74a122e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 253 additions and 0 deletions

View file

@ -73,6 +73,7 @@ fn TheRouter(is_routing: RwSignal<bool>) -> impl IntoView {
<Route path="/menu" view=MenuMdPage/> <Route path="/menu" view=MenuMdPage/>
<Route path="/message" view=MessageMdPage/> <Route path="/message" view=MessageMdPage/>
<Route path="/modal" view=ModalMdPage/> <Route path="/modal" view=ModalMdPage/>
<Route path="/pagination" view=PaginationMdPage/>
<Route path="/popover" view=PopoverMdPage/> <Route path="/popover" view=PopoverMdPage/>
<Route path="/progress" view=ProgressMdPage/> <Route path="/progress" view=ProgressMdPage/>
<Route path="/radio" view=RadioMdPage/> <Route path="/radio" view=RadioMdPage/>

View file

@ -241,6 +241,10 @@ pub(crate) fn gen_menu_data() -> Vec<MenuGroupOption> {
value: "menu".into(), value: "menu".into(),
label: "Menu".into(), label: "Menu".into(),
}, },
MenuItemOption {
value: "pagination".into(),
label: "Pagination".into(),
},
MenuItemOption { MenuItemOption {
value: "tabs".into(), value: "tabs".into(),
label: "Tabs".into(), label: "Tabs".into(),

View file

@ -0,0 +1,49 @@
# Pagination
```rust demo
let page = create_rw_signal(1);
view! {
<Space vertical=true>
<div>"Page: " {move || page.get()}</div>
<Pagination page count=10 />
</Space>
}
```
### Size
```rust demo
view! {
<Space vertical=true>
<Pagination count=100 size=ButtonSize::Tiny />
<Pagination count=100 size=ButtonSize::Small />
<Pagination count=100 size=ButtonSize::Medium />
<Pagination count=100 size=ButtonSize::Large />
</Space>
}
```
### Pagination ranges
```rust demo
view! {
<Space vertical=true>
<Pagination count=100 sibling_count=0 />
<Pagination count=100 sibling_count=1 />
<Pagination count=100 sibling_count=2 />
<Pagination count=100 sibling_count=3 />
</Space>
}
```
### Pagination Props
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Additional classes. |
| page | `Model<usize>` | `1` | The current page starts from 1. |
| count | `MaybeSignal<usize>` | | The total numbers of pages. |
| sibling_count | `MaybeSignal<usize>` | `1` | Number of visible pages after and before the current page. |
| size | `MaybeSignal<ButtonSize>` | `ButtonSize::Medium` | Button size. |
| on_change | `Option<Callback<usize>>` | `None` | Callback fired when the page is changed. |

View file

@ -54,6 +54,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
"MenuMdPage" => "../docs/menu/mod.md", "MenuMdPage" => "../docs/menu/mod.md",
"MessageMdPage" => "../docs/message/mod.md", "MessageMdPage" => "../docs/message/mod.md",
"ModalMdPage" => "../docs/modal/mod.md", "ModalMdPage" => "../docs/modal/mod.md",
"PaginationMdPage" => "../docs/pagination/mod.md",
"PopoverMdPage" => "../docs/popover/mod.md", "PopoverMdPage" => "../docs/popover/mod.md",
"ProgressMdPage" => "../docs/progress/mod.md", "ProgressMdPage" => "../docs/progress/mod.md",
"RadioMdPage" => "../docs/radio/mod.md", "RadioMdPage" => "../docs/radio/mod.md",

View file

@ -28,6 +28,7 @@ mod menu;
mod message; mod message;
pub mod mobile; pub mod mobile;
mod modal; mod modal;
mod pagination;
mod popover; mod popover;
mod progress; mod progress;
mod radio; mod radio;
@ -75,6 +76,7 @@ pub use loading_bar::*;
pub use menu::*; pub use menu::*;
pub use message::*; pub use message::*;
pub use modal::*; pub use modal::*;
pub use pagination::*;
pub use popover::*; pub use popover::*;
pub use progress::*; pub use progress::*;
pub use radio::*; pub use radio::*;

179
thaw/src/pagination/mod.rs Normal file
View file

@ -0,0 +1,179 @@
use crate::{Button, ButtonSize, ButtonVariant};
use leptos::*;
use std::cmp::min;
use thaw_utils::{class_list, mount_style, Model, OptionalProp, StoredMaybeSignal};
fn range(start: usize, end: usize) -> Vec<PaginationItem> {
let mut ret = vec![];
for idx in start..=end {
ret.push(PaginationItem::Number(idx));
}
ret
}
enum PaginationItem {
DotLeft,
DotRight,
Number(usize),
}
fn use_pagination(page: usize, count: usize, sibling_count: usize) -> Vec<PaginationItem> {
// Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2*DOTS
let total_page_numbers = sibling_count + 5;
// Case 1:
// If the number of pages is less than the page numbers we want to show in our
// paginationComponent, we return the range [1..totalPageCount]
if total_page_numbers >= count {
return range(1, count);
}
let current_page = page;
// Calculate left and right sibling index and make sure they are within range 1 and totalPageCount
let left_sibling_index = if current_page > sibling_count + 1 {
current_page - sibling_count
} else {
1
};
let right_sibling_index = min(current_page + sibling_count, count);
// We do not show dots just when there is just one page number to be inserted between the extremes of sibling and the page limits i.e 1 and totalPageCount.
// Hence we are using leftSiblingIndex > 2 and rightSiblingIndex < totalPageCount - 2
let should_show_left_dots = left_sibling_index > 2;
let should_show_right_dots = right_sibling_index < count - 2;
let first_page_index = 1;
let last_page_index = count;
// Case 2: No left dots to show, but rights dots to be shown
if !should_show_left_dots && should_show_right_dots {
let left_item_count = 3 + 2 * sibling_count;
let mut left_range = range(1, left_item_count);
left_range.push(PaginationItem::DotRight);
left_range.push(PaginationItem::Number(count));
left_range
} else if should_show_left_dots && !should_show_right_dots {
// Case 3: No right dots to show, but left dots to be shown
let right_item_count = 3 + 2 * sibling_count;
let mut right_range = range(count - right_item_count + 1, count);
let mut ret = vec![
PaginationItem::Number(first_page_index),
PaginationItem::DotLeft,
];
ret.append(&mut right_range);
ret
} else {
// Case 4: Both left and right dots to be shown
let mut middle_range = range(left_sibling_index, right_sibling_index);
let mut range = vec![
PaginationItem::Number(first_page_index),
PaginationItem::DotLeft,
];
range.append(&mut middle_range);
range.append(&mut vec![
PaginationItem::DotRight,
PaginationItem::Number(last_page_index),
]);
range
}
}
#[component]
pub fn Pagination(
#[prop(default = 1.into(), into)] page: Model<usize>,
#[prop(into)] count: MaybeSignal<usize>,
#[prop(default = 1.into(), into)] sibling_count: MaybeSignal<usize>,
#[prop(optional, into)] on_change: Option<Callback<usize>>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional, into)] size: MaybeSignal<ButtonSize>,
) -> impl IntoView {
mount_style("pagination", include_str!("./pagination.css"));
let size: StoredMaybeSignal<_> = size.into();
let no_next = Memo::new(move |_| page.get() == count.get());
let no_previous = Memo::new(move |_| page.get() == 1);
let on_click_previous = Callback::<ev::MouseEvent>::new(move |_| {
page.update(|val| *val -= 1);
if let Some(callback) = on_change.as_ref() {
callback.call(page.get())
}
});
let on_click_next = Callback::<ev::MouseEvent>::new(move |_| {
page.update(|val| *val += 1);
if let Some(callback) = on_change.as_ref() {
callback.call(page.get())
}
});
view! {
<nav class=class_list!["thaw-pagination", class.map(| c | move || c.get())]>
<ul>
<li>
<Button
size=size
on_click=on_click_previous
variant=ButtonVariant::Text
icon=icondata_ai::AiLeftOutlined
disabled=no_previous
circle=true
/>
</li>
<For
each=move || use_pagination(page.get(), count.get(), sibling_count.get())
key=|item| match item {
PaginationItem::DotLeft => -2,
PaginationItem::DotRight => -1,
PaginationItem::Number(nb) => nb.clone() as i64
}
let:item
>
{
if let PaginationItem::Number(nb) = item {
view! {
<li>
<Button
size=size
style=Memo::new(move |_| if page.get() == nb {
"color: var(--thaw-font-color-hover); border-color: var(--thaw-border-color-hover);".to_string()
} else {
"".to_string()
})
variant=Memo::new(move |_| if page.get() == nb {
ButtonVariant::Outlined
} else {
ButtonVariant::Text
})
on_click=Callback::new(move |_: ev::MouseEvent| {
if page.get() != nb {
page.set(nb)
}
if let Some(callback) = on_change.as_ref() {
callback.call(page.get())
}
})
round=true
>
{nb}
</Button>
</li>
}
} else {
view! {
<li>"..."</li>
}
}
}
</For>
<li>
<Button
size
on_click=on_click_next
variant=ButtonVariant::Text
icon=icondata_ai::AiRightOutlined disabled=no_next
circle=true
/>
</li>
</ul>
</nav>
}
}

View file

@ -0,0 +1,8 @@
.thaw-pagination > ul {
list-style-type: none;
display: flex;
padding: 0;
margin: 0;
column-gap: 5px;
align-items: center;
}

View file

@ -94,3 +94,12 @@ impl<T> From<MaybeSignal<T>> for StoredMaybeSignal<T> {
} }
} }
} }
impl<T: Clone> From<StoredMaybeSignal<T>> for MaybeSignal<T> {
fn from(value: StoredMaybeSignal<T>) -> Self {
match value {
StoredMaybeSignal::StoredValue(value) => MaybeSignal::Static(value.get_value()),
StoredMaybeSignal::Signal(singal) => MaybeSignal::Dynamic(singal),
}
}
}