master
1---
2import { footerConfig } from '@/config';
3import { t } from '@utils/i18n';
4import { Icon } from 'astro-icon/components';
5import Button from './Button.astro';
6
7interface Props {
8 total: number;
9 current: number;
10 baseUrl: string;
11 specialPages?: { page: number; url: string }[];
12}
13
14const { total, current, baseUrl, specialPages } = Astro.props;
15
16let pages: { page: number; url: string }[] = [];
17
18function getPageUrl(page: number) {
19 return specialPages?.find((p) => p.page === page)?.url || `${baseUrl}/${page}`;
20}
21
22function pushPage(page: number) {
23 pages.push({ page, url: getPageUrl(page) });
24}
25
26if (total <= 5) Array.from({ length: total }).map((_, i) => pushPage(i + 1));
27else {
28 if (current <= 3) {
29 Array.from({ length: 3 }).map((_, i) => pushPage(i + 1));
30 pages.push({ page: -1, url: '' });
31 pushPage(total);
32 } else if (current >= total - 2) {
33 pushPage(1);
34 pages.push({ page: -1, url: '' });
35 Array.from({ length: 3 }).map((_, i) => pushPage(total - 2 + i));
36 } else {
37 pushPage(1);
38 pages.push({ page: -1, url: '' });
39 pushPage(current - 1);
40 pushPage(current);
41 pushPage(current + 1);
42 pages.push({ page: -1, url: '' });
43 pushPage(total);
44 }
45}
46---
47
48<nav
49 id="pagination"
50 class:list={[
51 'relative flex min-h-10 w-full justify-between max-md:px-2',
52 // Hide pagination if only one page and no footer columns
53 pages.length <= 1 && footerConfig.columns !== false && 'max-xs:hidden',
54 ]}
55>
56 {
57 current > 1 && (
58 <Button
59 id="prev-page-btn"
60 class:list={[
61 'btn-primary absolute left-2 md:left-0',
62 current < total ? 'max-xs:w-[calc(50%-1rem)]' : 'max-xs:w-[calc(100%-1rem)]',
63 ]}
64 href={getPageUrl(current - 1)}
65 title={t.navigation.prevPage()}
66 aria-label={t.navigation.prevPage()}
67 rel="prev"
68 >
69 <Icon name="material-symbols:chevron-left-rounded" class="my-1 text-2xl" />
70 <span class="xs:hidden">{t.navigation.prevPage()}</span>
71 </Button>
72 )
73 }
74 <div class="max-xs:hidden mx-auto flex items-center justify-center gap-2">
75 <div class={pages.length > 1 ? 'join' : undefined}>
76 {
77 pages.map((p) => {
78 return (
79 <Button
80 class:list={[
81 'join-item btn-soft btn-primary',
82 current === p.page && 'btn-active',
83 p.page === -1 && 'btn-disabled',
84 ]}
85 href={p.url}
86 >
87 <span class="mx-1">{`${p.page === -1 ? '...' : p.page}`}</span>
88 </Button>
89 );
90 })
91 }
92 </div>
93 {
94 total > 1 && (
95 <label
96 id="page-jumper"
97 class="input input-bordered max-xs:hidden mx-2 flex flex-row items-center gap-0 overflow-hidden px-0"
98 data-base-url={baseUrl}
99 data-special-pages={specialPages?.map((p) => `${p.page}:${p.url}`).join(',')}
100 >
101 <input
102 id="page-jumper-input"
103 type="number"
104 min="1"
105 max={total}
106 class="pr-2 pl-4 duration-300"
107 inert
108 />
109 <Button id="page-jumper-button" class="relative right-0 m-0 duration-300">
110 <Icon
111 name="material-symbols:keyboard-double-arrow-right-rounded"
112 class="my-1 text-xl"
113 />
114 </Button>
115 </label>
116 )
117 }
118 </div>
119 {
120 current < total && (
121 <Button
122 id="next-page-btn"
123 class:list={[
124 'btn-primary absolute right-2 md:right-0',
125 current > 1 ? 'max-xs:w-[calc(50%-1rem)]' : 'max-xs:w-[calc(100%-1rem)]',
126 ]}
127 href={getPageUrl(current + 1)}
128 title={t.navigation.nextPage()}
129 aria-label={t.navigation.nextPage()}
130 rel="next"
131 >
132 <span class="xs:hidden">{t.navigation.nextPage()}</span>
133 <Icon name="material-symbols:chevron-right-rounded" class="my-1 text-2xl" />
134 </Button>
135 )
136 }
137</nav>
138
139<style>
140 /* hide arrows from number input */
141 input::-webkit-outer-spin-button,
142 input::-webkit-inner-spin-button {
143 appearance: none;
144 }
145
146 input[type='number'] {
147 appearance: textfield;
148 }
149
150 input:focus {
151 outline: none;
152 }
153</style>
154
155<script>
156 function setup() {
157 const pageJumper = document.getElementById('page-jumper');
158 const pageJumperInput = document.getElementById(
159 'page-jumper-input'
160 ) as HTMLInputElement | null;
161 const pageJumperButton = document.getElementById('page-jumper-button');
162
163 function pageJumperMouseEnterCallback() {
164 if (pageJumperInput) pageJumperInput.style.width = '4rem';
165 pageJumperInput?.removeAttribute('inert');
166 pageJumperInput?.classList.add('pl-4');
167 pageJumperInput?.classList.add('pr-2');
168 pageJumperInput?.focus();
169 }
170
171 function pageJumperMouseLeaveCallback() {
172 if (pageJumperInput) pageJumperInput.style.width = '0px';
173 pageJumperInput?.setAttribute('inert', '');
174 pageJumperInput?.classList.remove('pl-4');
175 pageJumperInput?.classList.remove('pr-2');
176 pageJumperInput?.blur();
177 }
178
179 function getPageUrl(page: number) {
180 const baseUrl = pageJumper?.getAttribute('data-base-url');
181 const specialPagesStr = pageJumper?.getAttribute('data-special-pages');
182 const specialPagesArray = specialPagesStr?.split(',');
183 const specialPages = specialPagesArray?.map((p) => {
184 const [page, url] = p.split(':', 2);
185 return { page: Number(page), url: url };
186 });
187 return specialPages?.find((p) => p.page === page)?.url || `${baseUrl}/${page}`;
188 }
189
190 function pageJumperExecHandler() {
191 const page = pageJumperInput?.value;
192 if (page) {
193 const pageUrl = getPageUrl(Number(page));
194 window.swup?.navigate(pageUrl);
195 }
196 }
197
198 pageJumper?.addEventListener('mouseenter', pageJumperMouseEnterCallback);
199 pageJumper?.addEventListener('mouseleave', pageJumperMouseLeaveCallback);
200 pageJumperMouseLeaveCallback();
201
202 pageJumperInput?.addEventListener('keydown', (event) => {
203 if (event.key === 'Enter') {
204 pageJumperExecHandler();
205 }
206 });
207 pageJumperButton?.addEventListener('click', pageJumperExecHandler);
208 }
209 document.addEventListener('astro:page-load', setup);
210 setup();
211</script>