diff --git a/assets/scss/partials/pagination.scss b/assets/scss/partials/pagination.scss index b2fffa4..108c34f 100644 --- a/assets/scss/partials/pagination.scss +++ b/assets/scss/partials/pagination.scss @@ -3,22 +3,44 @@ @include card-style; overflow: hidden; + .hide-on-mobile, + .pagination-link { + display: none; + + @include respond(md) { + display: flex; + } + } + .page-link { - padding: 16px 0; + padding: var(--spacing-sm) 0; margin: 0; + border: 0; + background: none; + font: inherit; border-radius: 0; display: flex; align-items: center; justify-content: center; flex: 1 1 auto; - min-width: 48px; + min-width: 36px; color: var(--card-text-color-secondary); text-decoration: none; + @include respond(md) { + min-width: 48px; + padding: var(--spacing-md) 0; + } + &:first-child, &:last-child { - flex: 0 0 48px; - padding: 16px; + flex: 0 0 var(--spacing-3xl); + padding: var(--spacing-sm); + + @include respond(md) { + flex: 0 0 48px; + padding: var(--spacing-md); + } } &:hover:not(.current):not(.disabled) { @@ -44,5 +66,194 @@ width: 20px; height: 20px; } + + &.pagination-jump-trigger { + cursor: pointer; + } } -} \ No newline at end of file +} + +#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; + } + } +} diff --git a/assets/ts/main.ts b/assets/ts/main.ts index 3b6c3c4..ee54000 100644 --- a/assets/ts/main.ts +++ b/assets/ts/main.ts @@ -10,6 +10,7 @@ import createElement from './createElement'; import StackColorScheme from './colorScheme'; import { setupScrollspy } from './scrollspy'; import { setupSmoothAnchors } from './smoothAnchors'; +import { setupPaginationJump } from './pagination'; let Stack = { init: () => { @@ -24,6 +25,8 @@ let Stack = { setupScrollspy(); } + setupPaginationJump(); + /** * Add copy button to code block */ diff --git a/assets/ts/pagination.ts b/assets/ts/pagination.ts new file mode 100644 index 0000000..aa93f9f --- /dev/null +++ b/assets/ts/pagination.ts @@ -0,0 +1,104 @@ +export function setupPaginationJump() { + const triggers = document.querySelectorAll('.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(); + }); +} diff --git a/demo/config/_default/hugo.toml b/demo/config/_default/hugo.toml index d2f615a..58faa84 100644 --- a/demo/config/_default/hugo.toml +++ b/demo/config/_default/hugo.toml @@ -12,7 +12,7 @@ hasCJKLanguage = false path = "github.com/CaiJimmy/hugo-theme-stack/v3" [pagination] - pagerSize = 5 + pagerSize = 3 [permalinks] post = "/p/:slug/" diff --git a/demo/content/post/pagination-test-01.md b/demo/content/post/pagination-test-01.md new file mode 100644 index 0000000..8f070d4 --- /dev/null +++ b/demo/content/post/pagination-test-01.md @@ -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**. diff --git a/demo/content/post/pagination-test-02.md b/demo/content/post/pagination-test-02.md new file mode 100644 index 0000000..97ecbd6 --- /dev/null +++ b/demo/content/post/pagination-test-02.md @@ -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**. diff --git a/demo/content/post/pagination-test-03.md b/demo/content/post/pagination-test-03.md new file mode 100644 index 0000000..365ca06 --- /dev/null +++ b/demo/content/post/pagination-test-03.md @@ -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**. diff --git a/demo/content/post/pagination-test-04.md b/demo/content/post/pagination-test-04.md new file mode 100644 index 0000000..6e109b8 --- /dev/null +++ b/demo/content/post/pagination-test-04.md @@ -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**. diff --git a/demo/content/post/pagination-test-05.md b/demo/content/post/pagination-test-05.md new file mode 100644 index 0000000..8f1ef6e --- /dev/null +++ b/demo/content/post/pagination-test-05.md @@ -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**. diff --git a/demo/content/post/pagination-test-06.md b/demo/content/post/pagination-test-06.md new file mode 100644 index 0000000..63c94c1 --- /dev/null +++ b/demo/content/post/pagination-test-06.md @@ -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**. diff --git a/demo/content/post/pagination-test-07.md b/demo/content/post/pagination-test-07.md new file mode 100644 index 0000000..b7ab7d2 --- /dev/null +++ b/demo/content/post/pagination-test-07.md @@ -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**. diff --git a/demo/content/post/pagination-test-08.md b/demo/content/post/pagination-test-08.md new file mode 100644 index 0000000..e9eb51a --- /dev/null +++ b/demo/content/post/pagination-test-08.md @@ -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**. diff --git a/demo/content/post/pagination-test-09.md b/demo/content/post/pagination-test-09.md new file mode 100644 index 0000000..b8a49a2 --- /dev/null +++ b/demo/content/post/pagination-test-09.md @@ -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**. diff --git a/demo/content/post/pagination-test-10.md b/demo/content/post/pagination-test-10.md new file mode 100644 index 0000000..fc97c0c --- /dev/null +++ b/demo/content/post/pagination-test-10.md @@ -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**. diff --git a/demo/content/post/pagination-test-11.md b/demo/content/post/pagination-test-11.md new file mode 100644 index 0000000..a428121 --- /dev/null +++ b/demo/content/post/pagination-test-11.md @@ -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**. diff --git a/demo/content/post/pagination-test-12.md b/demo/content/post/pagination-test-12.md new file mode 100644 index 0000000..3273c0a --- /dev/null +++ b/demo/content/post/pagination-test-12.md @@ -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**. diff --git a/i18n/en.toml b/i18n/en.toml index 5b0f118..8aacb6b 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -76,3 +76,8 @@ darkMode = "Dark Mode" [cookies.functional] title = "Functional" text = "These cookies enable features like comments and embedded content." + +[pagination] + jumpToPage = "Jump to page" + jump = "Go" + pressEnter = "Press Enter to jump" diff --git a/i18n/ja.toml b/i18n/ja.toml index 64a14cd..dd17782 100644 --- a/i18n/ja.toml +++ b/i18n/ja.toml @@ -67,3 +67,8 @@ darkMode = "ダークモード" [cookies.functional] title = "機能的" text = "これらのCookieは、コメントや埋め込みコンテンツなどの機能を有効にします." + +[pagination] + jumpToPage = "ページにジャンプ" + jump = "ジャンプ" + pressEnter = "Enter キーでジャンプ" diff --git a/i18n/zh-hant-hk.toml b/i18n/zh-hant-hk.toml index f68d205..9adddca 100644 --- a/i18n/zh-hant-hk.toml +++ b/i18n/zh-hant-hk.toml @@ -67,3 +67,8 @@ darkMode = "深色模式" [cookies.functional] title = "功能性" text = "這些 Cookie 啟用評論和嵌入內容等功能." + +[pagination] + jumpToPage = "跳轉到頁碼" + jump = "跳轉" + pressEnter = "按 Enter 鍵跳轉" diff --git a/i18n/zh-hant-tw.toml b/i18n/zh-hant-tw.toml index 4284c2f..c687c46 100644 --- a/i18n/zh-hant-tw.toml +++ b/i18n/zh-hant-tw.toml @@ -67,3 +67,8 @@ darkMode = "夜晚模式" [cookies.functional] title = "功能性" text = "這些 Cookie 啟用評論和嵌入內容等功能." + +[pagination] + jumpToPage = "跳轉到頁碼" + jump = "跳轉" + pressEnter = "按 Enter 鍵跳轉" diff --git a/i18n/zh.toml b/i18n/zh.toml index 6a539a5..1cac4e6 100644 --- a/i18n/zh.toml +++ b/i18n/zh.toml @@ -67,3 +67,8 @@ darkMode = "暗色模式" [cookies.functional] title = "功能性" text = "这些 Cookie 启用评论和嵌入内容等功能." + +[pagination] + jumpToPage = "跳转到页码" + jump = "跳转" + pressEnter = "按 Enter 键跳转" diff --git a/layouts/_partials/pagination.html b/layouts/_partials/pagination.html index a5abdb6..b0e70f2 100644 --- a/layouts/_partials/pagination.html +++ b/layouts/_partials/pagination.html @@ -1,70 +1,138 @@ {{- $pag := $.Paginator -}} {{- if gt $pag.TotalPages 1 -}} - + + +
+
+ + (1 - {{ $pag.TotalPages }}) +
+
+ + +
+
+ Enter + {{ i18n "pagination.pressEnter" | default "Press Enter to jump" }} +
+
+
+{{- end -}}