feat: Add jump-to-page dialog and improve pagination logic and styling (#1303)
* feat: add jump-to-page dialog on pagination ellipsis - Replace pagination windowing algorithm with a tighter 5-item window (curr-1 / curr / curr+1, clamped at edges) and hide-on-mobile for pages more than 1 step away from the current page - Ellipsis spans now carry the pagination-jump-trigger class and open a native <dialog> on click instead of being static decorations - Add <dialog id="pagination-jump-dialog"> with number input, Go button, and Enter-key hint; data-total / data-first-url / data-format-url attributes on <nav> give the JS all it needs to build the target URL - Add assets/ts/pagination.ts: setupPaginationJump() with Fisher-Yates backdrop-click and ESC handling, animated open/close via .closing class - Wire setupPaginationJump() into Stack.init() in main.ts - Extend pagination.scss with .hide-on-mobile, .pagination-jump-trigger hover style, and the full #pagination-jump-dialog styling + keyframe scale-up/scale-down animations - Add [pagination] i18n keys (jumpToPage / jump / pressEnter) to en / zh / zh-hant-tw / zh-hant-hk / ja * style: align pagination code with upstream conventions - pagination.html: collapse multi-line span/class/aria attrs to single lines; use HTML5 void element syntax; fix hint div indentation - pagination.scss: 2-space indent → 4-space indent - pagination.ts: tab indent → 4-space indent; export const arrow fn → export function declaration * chore: update layouts/_partials/pagination.html Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: add aria-labelledby attrributes for better accesibility Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: use { once: true } for the listener and guard against re-entry when .closing is already set. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * style: replace hardcoded values with design tokens and improve dialog UX - Replace all hardcoded px spacing in .page-link with --spacing-* tokens - Use @include card-style in #pagination-jump-dialog; override with --shadow-l2 - Replace padding: 24px with var(--card-padding) for responsive dialog padding - Align header title/subtitle to space-between for cleaner layout - Replace hardcoded gaps/padding in form with spacing tokens - Fix input/button border-radius from 8px to 6px (better proportion for 44px height) - Fix focus ring: use color-mix(accent-color 20%) so it's visible in dark mode - Add :focus-visible + outline:none to fully suppress native browser outline - Remove translateY from button hover for more restrained interaction - Scope transition to specific properties (border-color, box-shadow, background-color) * chore: add pagination test articles for easier debugging * style: enhance backdrop animations for pagination jump dialog * style: replace pagination ellipsis spans with buttons for improved accessibility * fix: check if browser supports dialog API before continue with pagination jump setup * fix: retain focus on last active element when closing pagination jump dialog * fix: style for page-link button (pagination-jump) * style: adjust width of pagination jump dialog, add container padding * style: simplify * refactor: simplify pagination link logic --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jimmy Cai <jimmy@cai.im>
This commit is contained in:
co-authored by
Copilot
Jimmy Cai
parent
8e3687431f
commit
37f6a77a1a
@@ -3,22 +3,44 @@
|
|||||||
@include card-style;
|
@include card-style;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.hide-on-mobile,
|
||||||
|
.pagination-link {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@include respond(md) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.page-link {
|
.page-link {
|
||||||
padding: 16px 0;
|
padding: var(--spacing-sm) 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
font: inherit;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 48px;
|
min-width: 36px;
|
||||||
color: var(--card-text-color-secondary);
|
color: var(--card-text-color-secondary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
|
@include respond(md) {
|
||||||
|
min-width: 48px;
|
||||||
|
padding: var(--spacing-md) 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:first-child,
|
&:first-child,
|
||||||
&:last-child {
|
&:last-child {
|
||||||
|
flex: 0 0 var(--spacing-3xl);
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
|
||||||
|
@include respond(md) {
|
||||||
flex: 0 0 48px;
|
flex: 0 0 48px;
|
||||||
padding: 16px;
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(.current):not(.disabled) {
|
&:hover:not(.current):not(.disabled) {
|
||||||
@@ -44,5 +66,194 @@
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.pagination-jump-trigger {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pagination-jump-dialog {
|
||||||
|
border: none;
|
||||||
|
@include card-style;
|
||||||
|
box-shadow: var(--shadow-l2);
|
||||||
|
color: var(--body-text-color);
|
||||||
|
margin: auto; // Center dialog
|
||||||
|
width: calc(100% - (var(--container-padding) * 2));
|
||||||
|
max-width: 320px;
|
||||||
|
padding: var(--card-padding);
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
opacity: 0;
|
||||||
|
animation: backdrop-fade-in 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
animation: scale-up 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.closing {
|
||||||
|
animation: scale-down 0.2s ease forwards;
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
animation: backdrop-fade-out 0.2s ease forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-jump-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
|
||||||
|
label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--card-text-color-main);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: var(--card-text-color-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-jump-input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 var(--spacing-md);
|
||||||
|
height: 44px; // Explicit height for perfect alignment
|
||||||
|
border: 1.5px solid var(--card-separator-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--body-background);
|
||||||
|
color: var(--card-text-color-main);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
/* Hide spinner */
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
&::-webkit-outer-spin-button,
|
||||||
|
&::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color) 20%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 var(--spacing-xl);
|
||||||
|
height: 44px; // Match input height
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: var(--accent-color-text);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--accent-color-darker);
|
||||||
|
box-shadow: var(--shadow-l1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-jump-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: var(--card-text-color-tertiary);
|
||||||
|
margin-top: calc(var(--spacing-xs) * -1);
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
background-color: var(--body-background);
|
||||||
|
border: 1px solid var(--card-separator-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-family: var(--base-font-family);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
box-shadow: 0 2px 0 var(--card-separator-color);
|
||||||
|
color: var(--card-text-color-main);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale-down {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes backdrop-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes backdrop-fade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import createElement from './createElement';
|
|||||||
import StackColorScheme from './colorScheme';
|
import StackColorScheme from './colorScheme';
|
||||||
import { setupScrollspy } from './scrollspy';
|
import { setupScrollspy } from './scrollspy';
|
||||||
import { setupSmoothAnchors } from './smoothAnchors';
|
import { setupSmoothAnchors } from './smoothAnchors';
|
||||||
|
import { setupPaginationJump } from './pagination';
|
||||||
|
|
||||||
let Stack = {
|
let Stack = {
|
||||||
init: () => {
|
init: () => {
|
||||||
@@ -24,6 +25,8 @@ let Stack = {
|
|||||||
setupScrollspy();
|
setupScrollspy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupPaginationJump();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add copy button to code block
|
* Add copy button to code block
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
export function setupPaginationJump() {
|
||||||
|
const triggers = document.querySelectorAll<HTMLButtonElement>('.pagination-jump-trigger');
|
||||||
|
const dialog = document.getElementById('pagination-jump-dialog') as HTMLDialogElement;
|
||||||
|
|
||||||
|
if (!dialog || triggers.length === 0) return;
|
||||||
|
|
||||||
|
const nav = document.querySelector('.pagination') as HTMLElement;
|
||||||
|
const input = document.getElementById('pagination-jump-input') as HTMLInputElement;
|
||||||
|
const form = dialog.querySelector('.pagination-jump-form') as HTMLFormElement;
|
||||||
|
const supportsDialog = typeof dialog.showModal === 'function' && typeof dialog.close === 'function';
|
||||||
|
let lastFocusedElement: HTMLElement | null = null;
|
||||||
|
|
||||||
|
if (!supportsDialog || !nav || !input || !form) return;
|
||||||
|
|
||||||
|
const closeDialog = (): void => {
|
||||||
|
if (dialog.classList.contains('closing')) return;
|
||||||
|
dialog.classList.add('closing');
|
||||||
|
dialog.addEventListener(
|
||||||
|
'animationend',
|
||||||
|
() => {
|
||||||
|
dialog.classList.remove('closing');
|
||||||
|
dialog.close();
|
||||||
|
if (lastFocusedElement?.isConnected) {
|
||||||
|
lastFocusedElement.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open dialog when triggers are clicked
|
||||||
|
triggers.forEach((trigger) => {
|
||||||
|
trigger.addEventListener('click', () => {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
lastFocusedElement = activeElement instanceof HTMLElement ? activeElement : trigger;
|
||||||
|
dialog.showModal();
|
||||||
|
input.value = '';
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle ESC key closing the dialog
|
||||||
|
dialog.addEventListener('cancel', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
closeDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close dialog when clicking backdrop
|
||||||
|
dialog.addEventListener('click', (e) => {
|
||||||
|
const rect = dialog.getBoundingClientRect();
|
||||||
|
const isInDialog =
|
||||||
|
rect.top <= e.clientY &&
|
||||||
|
e.clientY <= rect.top + rect.height &&
|
||||||
|
rect.left <= e.clientX &&
|
||||||
|
e.clientX <= rect.left + rect.width;
|
||||||
|
if (!isInDialog) {
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow Enter key to trigger validation and submit
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (form.reportValidity()) {
|
||||||
|
form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
form.reportValidity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPage = parseInt(input.value);
|
||||||
|
if (isNaN(targetPage) || targetPage < 1) return;
|
||||||
|
|
||||||
|
const totalPages = parseInt(nav.dataset.total || '0');
|
||||||
|
if (targetPage > totalPages) return;
|
||||||
|
|
||||||
|
const firstUrl = nav.dataset.firstUrl || '';
|
||||||
|
const formatUrl = nav.dataset.formatUrl || '';
|
||||||
|
|
||||||
|
let targetUrl = '';
|
||||||
|
if (targetPage === 1) {
|
||||||
|
targetUrl = firstUrl;
|
||||||
|
} else {
|
||||||
|
// formatUrl is the URL for page 2. E.g., /tags/page/2/ or /page/2/
|
||||||
|
// Replace the '2' before the trailing slash or .html with the target page number
|
||||||
|
targetUrl = formatUrl.replace(/2([^\d]*)$/, `${targetPage}$1`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUrl) {
|
||||||
|
window.location.href = targetUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDialog();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ hasCJKLanguage = false
|
|||||||
path = "github.com/CaiJimmy/hugo-theme-stack/v3"
|
path = "github.com/CaiJimmy/hugo-theme-stack/v3"
|
||||||
|
|
||||||
[pagination]
|
[pagination]
|
||||||
pagerSize = 5
|
pagerSize = 3
|
||||||
|
|
||||||
[permalinks]
|
[permalinks]
|
||||||
post = "/p/:slug/"
|
post = "/p/:slug/"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 01
|
||||||
|
date: 2000-01-01
|
||||||
|
description: Test post 01 for pagination behavior.
|
||||||
|
slug: pagination-test-01
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **01**.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 02
|
||||||
|
date: 2000-01-02
|
||||||
|
description: Test post 02 for pagination behavior.
|
||||||
|
slug: pagination-test-02
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **02**.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 03
|
||||||
|
date: 2000-01-03
|
||||||
|
description: Test post 03 for pagination behavior.
|
||||||
|
slug: pagination-test-03
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **03**.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 04
|
||||||
|
date: 2000-01-04
|
||||||
|
description: Test post 04 for pagination behavior.
|
||||||
|
slug: pagination-test-04
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **04**.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 05
|
||||||
|
date: 2000-01-05
|
||||||
|
description: Test post 05 for pagination behavior.
|
||||||
|
slug: pagination-test-05
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **05**.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 06
|
||||||
|
date: 2000-01-06
|
||||||
|
description: Test post 06 for pagination behavior.
|
||||||
|
slug: pagination-test-06
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **06**.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 07
|
||||||
|
date: 2000-01-07
|
||||||
|
description: Test post 07 for pagination behavior.
|
||||||
|
slug: pagination-test-07
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **07**.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 08
|
||||||
|
date: 2000-01-08
|
||||||
|
description: Test post 08 for pagination behavior.
|
||||||
|
slug: pagination-test-08
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **08**.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 09
|
||||||
|
date: 2000-01-09
|
||||||
|
description: Test post 09 for pagination behavior.
|
||||||
|
slug: pagination-test-09
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **09**.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 10
|
||||||
|
date: 2000-01-10
|
||||||
|
description: Test post 10 for pagination behavior.
|
||||||
|
slug: pagination-test-10
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **10**.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 11
|
||||||
|
date: 2000-01-11
|
||||||
|
description: Test post 11 for pagination behavior.
|
||||||
|
slug: pagination-test-11
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **11**.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Pagination Test 12
|
||||||
|
date: 2000-01-12
|
||||||
|
description: Test post 12 for pagination behavior.
|
||||||
|
slug: pagination-test-12
|
||||||
|
tags:
|
||||||
|
- pagination
|
||||||
|
- test
|
||||||
|
categories:
|
||||||
|
- Testing
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is pagination test post **12**.
|
||||||
@@ -76,3 +76,8 @@ darkMode = "Dark Mode"
|
|||||||
[cookies.functional]
|
[cookies.functional]
|
||||||
title = "Functional"
|
title = "Functional"
|
||||||
text = "These cookies enable features like comments and embedded content."
|
text = "These cookies enable features like comments and embedded content."
|
||||||
|
|
||||||
|
[pagination]
|
||||||
|
jumpToPage = "Jump to page"
|
||||||
|
jump = "Go"
|
||||||
|
pressEnter = "Press Enter to jump"
|
||||||
|
|||||||
@@ -67,3 +67,8 @@ darkMode = "ダークモード"
|
|||||||
[cookies.functional]
|
[cookies.functional]
|
||||||
title = "機能的"
|
title = "機能的"
|
||||||
text = "これらのCookieは、コメントや埋め込みコンテンツなどの機能を有効にします."
|
text = "これらのCookieは、コメントや埋め込みコンテンツなどの機能を有効にします."
|
||||||
|
|
||||||
|
[pagination]
|
||||||
|
jumpToPage = "ページにジャンプ"
|
||||||
|
jump = "ジャンプ"
|
||||||
|
pressEnter = "Enter キーでジャンプ"
|
||||||
|
|||||||
@@ -67,3 +67,8 @@ darkMode = "深色模式"
|
|||||||
[cookies.functional]
|
[cookies.functional]
|
||||||
title = "功能性"
|
title = "功能性"
|
||||||
text = "這些 Cookie 啟用評論和嵌入內容等功能."
|
text = "這些 Cookie 啟用評論和嵌入內容等功能."
|
||||||
|
|
||||||
|
[pagination]
|
||||||
|
jumpToPage = "跳轉到頁碼"
|
||||||
|
jump = "跳轉"
|
||||||
|
pressEnter = "按 Enter 鍵跳轉"
|
||||||
|
|||||||
@@ -67,3 +67,8 @@ darkMode = "夜晚模式"
|
|||||||
[cookies.functional]
|
[cookies.functional]
|
||||||
title = "功能性"
|
title = "功能性"
|
||||||
text = "這些 Cookie 啟用評論和嵌入內容等功能."
|
text = "這些 Cookie 啟用評論和嵌入內容等功能."
|
||||||
|
|
||||||
|
[pagination]
|
||||||
|
jumpToPage = "跳轉到頁碼"
|
||||||
|
jump = "跳轉"
|
||||||
|
pressEnter = "按 Enter 鍵跳轉"
|
||||||
|
|||||||
@@ -67,3 +67,8 @@ darkMode = "暗色模式"
|
|||||||
[cookies.functional]
|
[cookies.functional]
|
||||||
title = "功能性"
|
title = "功能性"
|
||||||
text = "这些 Cookie 启用评论和嵌入内容等功能."
|
text = "这些 Cookie 启用评论和嵌入内容等功能."
|
||||||
|
|
||||||
|
[pagination]
|
||||||
|
jumpToPage = "跳转到页码"
|
||||||
|
jump = "跳转"
|
||||||
|
pressEnter = "按 Enter 键跳转"
|
||||||
|
|||||||
@@ -1,70 +1,138 @@
|
|||||||
{{- $pag := $.Paginator -}}
|
{{- $pag := $.Paginator -}}
|
||||||
{{- if gt $pag.TotalPages 1 -}}
|
{{- if gt $pag.TotalPages 1 -}}
|
||||||
<nav class="pagination" role="navigation" aria-label="pagination">
|
{{- /* Find page 2's URL to extract the pagination base path for JavaScript */ -}}
|
||||||
|
{{- $page2Url := "" -}}
|
||||||
|
{{- range $pag.Pagers -}}
|
||||||
|
{{- if eq .PageNumber 2 -}}
|
||||||
|
{{- $page2Url = .URL -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
<nav
|
||||||
|
class="pagination"
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
data-total="{{ $pag.TotalPages }}"
|
||||||
|
data-first-url="{{ $pag.First.URL }}"
|
||||||
|
data-format-url="{{ $page2Url }}">
|
||||||
{{- /* Previous page button */ -}}
|
{{- /* Previous page button */ -}}
|
||||||
<a class="page-link {{ if not $pag.Prev }}disabled{{ end }}"
|
<a
|
||||||
{{- if $pag.Prev -}}href="{{ $pag.Prev.URL }}" {{- end -}}
|
class="page-link {{ if not $pag.Prev }}disabled{{ end }}"
|
||||||
|
{{- if $pag.Prev -}}href="{{ $pag.Prev.URL }}"{{- end -}}
|
||||||
aria-label="Previous page"
|
aria-label="Previous page"
|
||||||
{{- if not $pag.Prev -}}aria-disabled="true"{{- end -}}>
|
{{- if not $pag.Prev -}}aria-disabled="true"{{- end -}}>
|
||||||
{{ partial "helper/icon" "chevron-left" }}
|
{{ partial "helper/icon" "chevron-left" }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{{- /* Page numbers logic */ -}}
|
{{- /* Page numbers logic */ -}}
|
||||||
{{- $window := 4 -}}
|
{{- /* Compute visible middle window [start, end] (excluding first and last page). */ -}}
|
||||||
{{- $showFirst := true -}}
|
{{- /* Keep 3 middle pages when possible; if total <= 5, show all middle pages. */ -}}
|
||||||
{{- $showLast := true -}}
|
{{- $curr := $pag.PageNumber -}}
|
||||||
|
{{- $total := $pag.TotalPages -}}
|
||||||
|
{{- $middleMinPage := 2 -}}
|
||||||
|
{{- $middleMaxPage := sub $total 1 -}}
|
||||||
|
{{- $start := $middleMinPage -}}
|
||||||
|
{{- $end := $middleMaxPage -}}
|
||||||
|
|
||||||
|
{{- if gt $total 5 -}}
|
||||||
|
{{- $start = sub $curr 1 -}}
|
||||||
|
{{- if lt $start $middleMinPage -}}
|
||||||
|
{{- $start = $middleMinPage -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $end = add $start 2 -}}
|
||||||
|
{{- if gt $end $middleMaxPage -}}
|
||||||
|
{{- $end = $middleMaxPage -}}
|
||||||
|
{{- $start = sub $end 2 -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
{{- /* First page */ -}}
|
{{- /* First page */ -}}
|
||||||
<a class="page-link {{ if eq $pag.PageNumber 1 }}current{{ end }}" aria-label="Page 1"
|
<a
|
||||||
{{- if eq $pag.PageNumber 1 -}}aria-current="page" {{- end -}}
|
class="page-link {{ if eq $curr 1 }}current{{ end }}"
|
||||||
{{- if ne $pag.PageNumber 1 -}}href="{{ $pag.First.URL }}"{{- end -}}>
|
aria-label="Page 1"
|
||||||
|
{{- if eq $curr 1 -}}aria-current="page"{{- end -}}
|
||||||
|
{{- if ne $curr 1 -}}href="{{ $pag.First.URL }}"{{- end -}}>
|
||||||
1
|
1
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{{- /* Left ellipsis - show if current page is > 3 */ -}}
|
{{- /* Left ellipsis - show if the window start is beyond page 2 */ -}}
|
||||||
{{- if gt $pag.PageNumber 3 -}}
|
{{- if gt $start $middleMinPage -}}
|
||||||
<span class="page-link pagination-link">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="page-link pagination-link pagination-jump-trigger"
|
||||||
|
aria-label="{{ i18n "pagination.jumpToPage" | default "Jump to page" }}">
|
||||||
{{ partial "helper/icon" "dots" }}
|
{{ partial "helper/icon" "dots" }}
|
||||||
</span>
|
</button>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{- /* Middle pages - show current and neighbors */ -}}
|
{{- /* Middle pages - show pages in [start, end] */ -}}
|
||||||
{{- range $pag.Pagers -}}
|
{{- range $pag.Pagers -}}
|
||||||
{{- if and (gt .PageNumber 1) (lt .PageNumber $pag.TotalPages) -}}
|
{{- if and (ge .PageNumber $start) (le .PageNumber $end) -}}
|
||||||
{{- if and (ge .PageNumber (sub $pag.PageNumber $window)) (le .PageNumber (add $pag.PageNumber $window)) -}}
|
{{- $hideOnMobile := and (ne .PageNumber $curr) (ne .PageNumber (sub $curr 1)) (ne .PageNumber (add $curr 1)) -}}
|
||||||
<a class="page-link {{ if eq .PageNumber $pag.PageNumber }}current{{ end }}"
|
<a
|
||||||
{{- if eq .PageNumber $pag.PageNumber -}}aria-current="page" {{- end -}} href="{{ .URL }}"
|
class="page-link {{ if eq .PageNumber $curr }}current{{ end }} {{ if $hideOnMobile }}hide-on-mobile{{ end }}"
|
||||||
|
{{- if eq .PageNumber $curr -}}aria-current="page"{{- end -}}
|
||||||
|
href="{{ .URL }}"
|
||||||
aria-label="Page {{ .PageNumber }}">
|
aria-label="Page {{ .PageNumber }}">
|
||||||
{{ .PageNumber }}
|
{{ .PageNumber }}
|
||||||
</a>
|
</a>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{- /* Right ellipsis - show if current page is < last-2 */ -}}
|
{{- /* Right ellipsis - show if the window end is before the second-to-last page */ -}}
|
||||||
{{- if lt $pag.PageNumber (sub $pag.TotalPages 2) -}}
|
{{- if lt $end $middleMaxPage -}}
|
||||||
<span class="page-link pagination-link">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="page-link pagination-link pagination-jump-trigger"
|
||||||
|
aria-label="{{ i18n "pagination.jumpToPage" | default "Jump to page" }}">
|
||||||
{{ partial "helper/icon" "dots" }}
|
{{ partial "helper/icon" "dots" }}
|
||||||
</span>
|
</button>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{- /* Last page (if more than 1 page total) */ -}}
|
{{- /* Last page */ -}}
|
||||||
|
<a
|
||||||
{{- if gt $pag.TotalPages 1 -}}
|
class="page-link {{ if eq $curr $total }}current{{ end }}"
|
||||||
<a class="page-link {{ if eq $pag.PageNumber $pag.TotalPages }}current{{ end }}"
|
{{- if ne $curr $total -}}href="{{ $pag.Last.URL }}"{{- end -}}
|
||||||
{{- if ne $pag.PageNumber $pag.TotalPages -}}href="{{ $pag.Last.URL }}" {{- end -}}
|
aria-label="Page {{ $total }}"
|
||||||
aria-label="Page {{ $pag.TotalPages }}"
|
{{- if eq $curr $total -}}aria-current="page"{{- end -}}>
|
||||||
{{- if eq $pag.PageNumber $pag.TotalPages -}}aria-current="page"{{- end -}}>
|
{{ $total }}
|
||||||
{{ $pag.TotalPages }}
|
|
||||||
</a>
|
</a>
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{- /* Next page button */ -}}
|
{{- /* Next page button */ -}}
|
||||||
<a class="page-link {{ if not $pag.Next }}disabled{{ end }}"
|
<a
|
||||||
{{- if $pag.Next -}}href="{{ $pag.Next.URL }}" {{- end -}}
|
class="page-link {{ if not $pag.Next }}disabled{{ end }}"
|
||||||
|
{{- if $pag.Next -}}href="{{ $pag.Next.URL }}"{{- end -}}
|
||||||
aria-label="Next page"
|
aria-label="Next page"
|
||||||
{{- if not $pag.Next -}}aria-disabled="true"{{- end -}}>
|
{{- if not $pag.Next -}}aria-disabled="true"{{- end -}}>
|
||||||
{{ partial "helper/icon" "chevron-right" }}
|
{{ partial "helper/icon" "chevron-right" }}
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<dialog id="pagination-jump-dialog" aria-labelledby="pagination-jump-title">
|
||||||
|
<form method="dialog" class="pagination-jump-form">
|
||||||
|
<header>
|
||||||
|
<label id="pagination-jump-title" class="title" for="pagination-jump-input">
|
||||||
|
{{ i18n "pagination.jumpToPage" | default "Jump to page:" }}
|
||||||
|
</label>
|
||||||
|
<span class="subtitle">(1 - {{ $pag.TotalPages }})</span>
|
||||||
|
</header>
|
||||||
|
<div class="pagination-jump-input-group">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="pagination-jump-input"
|
||||||
|
min="1"
|
||||||
|
max="{{ $pag.TotalPages }}"
|
||||||
|
autofocus
|
||||||
|
autocomplete="off"
|
||||||
|
required>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{{ i18n "pagination.jump" | default "Go" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-jump-hint">
|
||||||
|
<kbd>Enter</kbd>
|
||||||
|
<span>{{ i18n "pagination.pressEnter" | default "Press Enter to jump" }}</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
Reference in New Issue
Block a user