@anglefeint/astro-theme 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { PaginationModel } from '../../utils/pagination';
|
|
3
|
+
import {
|
|
4
|
+
resolvePaginationItemVariant,
|
|
5
|
+
resolvePaginationVariant,
|
|
6
|
+
} from '../../utils/pagination-style';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
model: PaginationModel;
|
|
10
|
+
locale: string;
|
|
11
|
+
blogRoot: string;
|
|
12
|
+
pathname: string;
|
|
13
|
+
labels: {
|
|
14
|
+
previous: string;
|
|
15
|
+
next: string;
|
|
16
|
+
paginationAria: string;
|
|
17
|
+
jumpTo: string;
|
|
18
|
+
jumpInputLabel: string;
|
|
19
|
+
jumpGo: string;
|
|
20
|
+
};
|
|
21
|
+
config: {
|
|
22
|
+
jump: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
enterToGo: boolean;
|
|
25
|
+
};
|
|
26
|
+
style: {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
mode: 'random' | 'sequential' | 'fixed';
|
|
29
|
+
variants: number;
|
|
30
|
+
fixedVariant: number;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { model, locale, blogRoot, pathname, labels, config } = Astro.props as Props;
|
|
36
|
+
|
|
37
|
+
const pageHref = (pageNum: number) => (pageNum === 1 ? blogRoot : `${blogRoot}${pageNum}/`);
|
|
38
|
+
const styleEnabled = config.style.enabled;
|
|
39
|
+
const styleMode = config.style.mode;
|
|
40
|
+
const styleVariants = Math.max(1, Math.min(12, Math.floor(config.style.variants)));
|
|
41
|
+
const styleFixedVariant = Math.max(
|
|
42
|
+
1,
|
|
43
|
+
Math.min(styleVariants, Math.floor(config.style.fixedVariant))
|
|
44
|
+
);
|
|
45
|
+
const showJump = config.jump.enabled && model.showJump;
|
|
46
|
+
|
|
47
|
+
const wrapVariant = resolvePaginationVariant({
|
|
48
|
+
currentPage: model.currentPage,
|
|
49
|
+
totalPages: model.totalPages,
|
|
50
|
+
locale,
|
|
51
|
+
pathname,
|
|
52
|
+
config: {
|
|
53
|
+
ENABLED: styleEnabled,
|
|
54
|
+
MODE: styleMode,
|
|
55
|
+
VARIANTS: styleVariants,
|
|
56
|
+
FIXED_VARIANT: styleFixedVariant,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const itemVariantClass = (seed: string, index: number) => {
|
|
61
|
+
if (!styleEnabled) return 'pg-var-1';
|
|
62
|
+
if (styleMode === 'random') return 'pg-var-random';
|
|
63
|
+
return resolvePaginationItemVariant({
|
|
64
|
+
seed,
|
|
65
|
+
index,
|
|
66
|
+
config: {
|
|
67
|
+
ENABLED: styleEnabled,
|
|
68
|
+
MODE: styleMode,
|
|
69
|
+
VARIANTS: styleVariants,
|
|
70
|
+
FIXED_VARIANT: styleFixedVariant,
|
|
71
|
+
},
|
|
72
|
+
}).className;
|
|
73
|
+
};
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
model.totalPages > 1 && (
|
|
78
|
+
<div
|
|
79
|
+
class={`pagination-wrap ${wrapVariant.className}`}
|
|
80
|
+
data-pagination-component="cyber"
|
|
81
|
+
data-style-enabled={styleEnabled ? 'true' : 'false'}
|
|
82
|
+
data-style-mode={styleMode}
|
|
83
|
+
data-style-variants={String(styleVariants)}
|
|
84
|
+
data-style-fixed-variant={String(styleFixedVariant)}
|
|
85
|
+
>
|
|
86
|
+
<nav class="pagination" aria-label={labels.paginationAria}>
|
|
87
|
+
{model.currentPage > 1 && (
|
|
88
|
+
<a
|
|
89
|
+
class={`pg-item pg-nav ${itemVariantClass(`${locale}:prev:${model.currentPage}`, 0)}`}
|
|
90
|
+
href={pageHref(model.currentPage - 1)}
|
|
91
|
+
aria-label={labels.previous}
|
|
92
|
+
>
|
|
93
|
+
{labels.previous}
|
|
94
|
+
</a>
|
|
95
|
+
)}
|
|
96
|
+
{model.items.map((item, index) =>
|
|
97
|
+
item.kind === 'ellipsis' ? (
|
|
98
|
+
<span
|
|
99
|
+
class={`pg-item pg-ellipsis ${itemVariantClass(`${locale}:ellipsis:${item.id}`, index)}`}
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
>
|
|
102
|
+
...
|
|
103
|
+
</span>
|
|
104
|
+
) : (
|
|
105
|
+
<a
|
|
106
|
+
class={`pg-item ${itemVariantClass(`${locale}:page:${item.page}`, index)} ${item.page === model.currentPage ? 'current' : ''}`}
|
|
107
|
+
href={pageHref(item.page)}
|
|
108
|
+
aria-current={item.page === model.currentPage ? 'page' : undefined}
|
|
109
|
+
>
|
|
110
|
+
{item.page}
|
|
111
|
+
</a>
|
|
112
|
+
)
|
|
113
|
+
)}
|
|
114
|
+
{model.currentPage < model.totalPages && (
|
|
115
|
+
<a
|
|
116
|
+
class={`pg-item pg-nav ${itemVariantClass(`${locale}:next:${model.currentPage}`, model.items.length + 2)}`}
|
|
117
|
+
href={pageHref(model.currentPage + 1)}
|
|
118
|
+
aria-label={labels.next}
|
|
119
|
+
>
|
|
120
|
+
{labels.next}
|
|
121
|
+
</a>
|
|
122
|
+
)}
|
|
123
|
+
</nav>
|
|
124
|
+
{showJump && (
|
|
125
|
+
<form
|
|
126
|
+
class="pagination-jump"
|
|
127
|
+
data-blog-root={blogRoot}
|
|
128
|
+
data-total-pages={String(model.totalPages)}
|
|
129
|
+
data-enter-to-go={config.jump.enterToGo ? 'true' : 'false'}
|
|
130
|
+
aria-label={labels.jumpTo}
|
|
131
|
+
>
|
|
132
|
+
<label class="sr-only" for="pagination-jump-input">
|
|
133
|
+
{labels.jumpInputLabel}
|
|
134
|
+
</label>
|
|
135
|
+
<span class="pagination-jump-label">{labels.jumpTo}</span>
|
|
136
|
+
<input
|
|
137
|
+
id="pagination-jump-input"
|
|
138
|
+
class="pagination-jump-input"
|
|
139
|
+
type="text"
|
|
140
|
+
inputmode="numeric"
|
|
141
|
+
pattern="[0-9]*"
|
|
142
|
+
value={model.currentPage}
|
|
143
|
+
/>
|
|
144
|
+
<button type="submit" class="pagination-jump-btn">
|
|
145
|
+
{labels.jumpGo}
|
|
146
|
+
</button>
|
|
147
|
+
</form>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
<script is:inline>
|
|
154
|
+
document.querySelectorAll('[data-pagination-component="cyber"]').forEach((wrap) => {
|
|
155
|
+
if (!(wrap instanceof HTMLElement)) return;
|
|
156
|
+
const toInt = (value, fallback) => {
|
|
157
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
158
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
159
|
+
};
|
|
160
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
161
|
+
|
|
162
|
+
const styleEnabled = wrap.dataset.styleEnabled === 'true';
|
|
163
|
+
const styleMode = wrap.dataset.styleMode || 'random';
|
|
164
|
+
const variants = clamp(toInt(wrap.dataset.styleVariants, 9), 1, 12);
|
|
165
|
+
const fixedVariant = clamp(toInt(wrap.dataset.styleFixedVariant, 1), 1, variants);
|
|
166
|
+
|
|
167
|
+
const items = wrap.querySelectorAll('.pagination .pg-item');
|
|
168
|
+
|
|
169
|
+
if (!styleEnabled) {
|
|
170
|
+
items.forEach((item) => {
|
|
171
|
+
item.className = item.className
|
|
172
|
+
.replace(/\bpg-var-\d+\b/g, '')
|
|
173
|
+
.replace(/\bpg-var-random\b/g, '')
|
|
174
|
+
.trim();
|
|
175
|
+
item.classList.add('pg-var-1');
|
|
176
|
+
});
|
|
177
|
+
} else if (styleMode === 'random') {
|
|
178
|
+
items.forEach((item) => {
|
|
179
|
+
item.className = item.className
|
|
180
|
+
.replace(/\bpg-var-\d+\b/g, '')
|
|
181
|
+
.replace(/\bpg-var-random\b/g, '')
|
|
182
|
+
.trim();
|
|
183
|
+
const variant = Math.floor(Math.random() * variants) + 1;
|
|
184
|
+
item.classList.add(`pg-var-${variant}`);
|
|
185
|
+
});
|
|
186
|
+
} else if (styleMode === 'fixed') {
|
|
187
|
+
items.forEach((item) => {
|
|
188
|
+
item.className = item.className
|
|
189
|
+
.replace(/\bpg-var-\d+\b/g, '')
|
|
190
|
+
.replace(/\bpg-var-random\b/g, '')
|
|
191
|
+
.trim();
|
|
192
|
+
item.classList.add(`pg-var-${fixedVariant}`);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const form = wrap.querySelector('.pagination-jump');
|
|
197
|
+
if (!(form instanceof HTMLFormElement)) return;
|
|
198
|
+
|
|
199
|
+
const blogRoot = form.getAttribute('data-blog-root') || '/blog/';
|
|
200
|
+
const totalPages = Math.max(1, parseInt(form.getAttribute('data-total-pages') || '1', 10));
|
|
201
|
+
const enterToGo = form.getAttribute('data-enter-to-go') !== 'false';
|
|
202
|
+
const input = form.querySelector('.pagination-jump-input');
|
|
203
|
+
if (!(input instanceof HTMLInputElement)) return;
|
|
204
|
+
|
|
205
|
+
const normalizeRoot = (root) => {
|
|
206
|
+
if (!root) return '/blog/';
|
|
207
|
+
return root.endsWith('/') ? root : `${root}/`;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const toHref = (pageNum) => {
|
|
211
|
+
const root = normalizeRoot(blogRoot);
|
|
212
|
+
return pageNum === 1 ? root : `${root}${pageNum}/`;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const runJump = () => {
|
|
216
|
+
const normalized = (input.value || '').replace(/[^\d]/g, '');
|
|
217
|
+
const value = Number.parseInt(normalized || '1', 10);
|
|
218
|
+
if (!Number.isFinite(value)) return;
|
|
219
|
+
const target = Math.max(1, Math.min(totalPages, Math.round(value)));
|
|
220
|
+
window.location.assign(toHref(target));
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
form.addEventListener('submit', (event) => {
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
runJump();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (!enterToGo) {
|
|
229
|
+
input.addEventListener('keydown', (event) => {
|
|
230
|
+
if (event.key !== 'Enter') return;
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
</script>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { clamp, int } from './number';
|
|
2
|
+
|
|
3
|
+
type PaginationStyleMode = 'random' | 'sequential' | 'fixed';
|
|
4
|
+
|
|
5
|
+
interface PaginationStyleConfig {
|
|
6
|
+
ENABLED: boolean;
|
|
7
|
+
MODE: PaginationStyleMode;
|
|
8
|
+
VARIANTS: number;
|
|
9
|
+
FIXED_VARIANT: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ResolvePaginationVariantOptions {
|
|
13
|
+
currentPage: number;
|
|
14
|
+
totalPages: number;
|
|
15
|
+
locale: string;
|
|
16
|
+
pathname: string;
|
|
17
|
+
config: PaginationStyleConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function hashString(value: string): number {
|
|
21
|
+
let hash = 5381;
|
|
22
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
23
|
+
hash = (hash * 33) ^ value.charCodeAt(i);
|
|
24
|
+
}
|
|
25
|
+
return Math.abs(hash) >>> 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolvePaginationVariant(options: ResolvePaginationVariantOptions): {
|
|
29
|
+
className: string;
|
|
30
|
+
variant: number;
|
|
31
|
+
} {
|
|
32
|
+
const variants = clamp(int(options.config.VARIANTS), 1, 12);
|
|
33
|
+
if (!options.config.ENABLED) {
|
|
34
|
+
return { className: 'cyber-pg-v1', variant: 1 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const mode = options.config.MODE;
|
|
38
|
+
let variant = 1;
|
|
39
|
+
|
|
40
|
+
if (mode === 'fixed') {
|
|
41
|
+
variant = clamp(int(options.config.FIXED_VARIANT), 1, variants);
|
|
42
|
+
} else if (mode === 'sequential') {
|
|
43
|
+
variant = ((Math.max(1, int(options.currentPage)) - 1) % variants) + 1;
|
|
44
|
+
} else {
|
|
45
|
+
const seedKey = `${options.locale}:${options.pathname}:${options.currentPage}:${options.totalPages}`;
|
|
46
|
+
variant = (hashString(seedKey) % variants) + 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
className: `cyber-pg-v${variant}`,
|
|
51
|
+
variant,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ResolvePaginationItemVariantOptions {
|
|
56
|
+
seed: string;
|
|
57
|
+
index: number;
|
|
58
|
+
config: PaginationStyleConfig;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolvePaginationItemVariant(options: ResolvePaginationItemVariantOptions): {
|
|
62
|
+
className: string;
|
|
63
|
+
variant: number;
|
|
64
|
+
} {
|
|
65
|
+
const variants = clamp(int(options.config.VARIANTS), 1, 12);
|
|
66
|
+
if (!options.config.ENABLED) {
|
|
67
|
+
return { className: 'pg-var-1', variant: 1 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const mode = options.config.MODE;
|
|
71
|
+
let variant = 1;
|
|
72
|
+
if (mode === 'fixed') {
|
|
73
|
+
variant = clamp(int(options.config.FIXED_VARIANT), 1, variants);
|
|
74
|
+
} else if (mode === 'sequential') {
|
|
75
|
+
variant = (int(options.index) % variants) + 1;
|
|
76
|
+
} else {
|
|
77
|
+
variant = (hashString(options.seed) % variants) + 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { className: `pg-var-${variant}`, variant };
|
|
81
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { clamp, int } from './number';
|
|
2
|
+
|
|
3
|
+
export type PaginationItem = { kind: 'page'; page: number } | { kind: 'ellipsis'; id: string };
|
|
4
|
+
|
|
5
|
+
export interface PaginationModel {
|
|
6
|
+
currentPage: number;
|
|
7
|
+
totalPages: number;
|
|
8
|
+
showJump: boolean;
|
|
9
|
+
items: PaginationItem[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface BuildPaginationModelOptions {
|
|
13
|
+
currentPage: number;
|
|
14
|
+
totalPages: number;
|
|
15
|
+
windowSize?: number;
|
|
16
|
+
showJumpThreshold?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function addRange(set: Set<number>, start: number, end: number, totalPages: number) {
|
|
20
|
+
const s = clamp(int(start), 1, totalPages);
|
|
21
|
+
const e = clamp(int(end), 1, totalPages);
|
|
22
|
+
for (let page = s; page <= e; page += 1) set.add(page);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildPaginationModel(options: BuildPaginationModelOptions): PaginationModel {
|
|
26
|
+
const totalPages = Math.max(1, int(options.totalPages));
|
|
27
|
+
const currentPage = clamp(int(options.currentPage), 1, totalPages);
|
|
28
|
+
const windowSize = clamp(int(options.windowSize ?? 7), 5, 21);
|
|
29
|
+
const showJumpThreshold = Math.max(1, int(options.showJumpThreshold ?? 12));
|
|
30
|
+
const showJump = totalPages > showJumpThreshold;
|
|
31
|
+
|
|
32
|
+
if (totalPages <= windowSize) {
|
|
33
|
+
return {
|
|
34
|
+
currentPage,
|
|
35
|
+
totalPages,
|
|
36
|
+
showJump,
|
|
37
|
+
items: Array.from({ length: totalPages }, (_, i) => ({ kind: 'page', page: i + 1 })),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const boundaryCount = 1;
|
|
42
|
+
const siblingCount = Math.max(1, Math.floor((windowSize - (boundaryCount * 2 + 3)) / 2));
|
|
43
|
+
const nearEdgeSlots = boundaryCount + siblingCount * 2 + 2;
|
|
44
|
+
const picked = new Set<number>();
|
|
45
|
+
|
|
46
|
+
addRange(picked, 1, boundaryCount, totalPages);
|
|
47
|
+
addRange(picked, totalPages - boundaryCount + 1, totalPages, totalPages);
|
|
48
|
+
addRange(picked, currentPage - siblingCount, currentPage + siblingCount, totalPages);
|
|
49
|
+
|
|
50
|
+
if (currentPage <= nearEdgeSlots) addRange(picked, 1, nearEdgeSlots + 1, totalPages);
|
|
51
|
+
if (currentPage >= totalPages - nearEdgeSlots + 1) {
|
|
52
|
+
addRange(picked, totalPages - nearEdgeSlots, totalPages, totalPages);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const pages = [...picked].sort((a, b) => a - b);
|
|
56
|
+
const items: PaginationItem[] = [];
|
|
57
|
+
for (let i = 0; i < pages.length; i += 1) {
|
|
58
|
+
const page = pages[i];
|
|
59
|
+
const prev = pages[i - 1];
|
|
60
|
+
if (typeof prev === 'number' && page - prev > 1) {
|
|
61
|
+
items.push({ kind: 'ellipsis', id: `ellipsis-${prev}-${page}` });
|
|
62
|
+
}
|
|
63
|
+
items.push({ kind: 'page', page });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { currentPage, totalPages, showJump, items };
|
|
67
|
+
}
|