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>