Paginator.svelte 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. <script lang="ts">
  2. import { createEventDispatcher } from 'svelte';
  3. // Event Dispatcher
  4. type PaginatorEvent = {
  5. amount: number;
  6. page: number;
  7. };
  8. const dispatch = createEventDispatcher<PaginatorEvent>();
  9. // Props
  10. /** Pass the page setting object. */
  11. export let settings = { page: 0, limit: 5, size: 0, amounts: [1, 2, 5, 10] };
  12. /** Sets selection and buttons to disabled state on-demand. */
  13. export let disabled = false;
  14. /** Show Previous and Next buttons. */
  15. export let showPreviousNextButtons = true;
  16. /** Show First and Last buttons. */
  17. export let showFirstLastButtons = false;
  18. /** Displays a numeric row of page buttons. */
  19. export let showNumerals = false;
  20. /** Maximum number of active page siblings in the numeric row.*/
  21. export let maxNumerals = 1;
  22. /** Provide classes to set flexbox justification. */
  23. export let justify: string = 'justify-between';
  24. // Props (select)
  25. /** Set the text for the amount selection input. */
  26. export let amountText = 'Items';
  27. // Props (buttons)
  28. /** Provide arbitrary classes to the active page buttons. */
  29. export let active: string = 'bg-gray-100 dark:bg-gray-700';
  30. /*** Set the base button classes. */
  31. export let buttonClasses: string = '!px-3 !py-1.5';
  32. /** Set the label for the pages separator. */
  33. export let separatorText = 'of';
  34. // Base Classes
  35. const cBase = 'flex flex-col md:flex-row items-center space-y-4 md:space-y-0 md:space-x-4';
  36. const cLabel = 'w-full md:w-auto';
  37. // Local
  38. let lastPage = Math.max(0, Math.ceil(settings.size / settings.limit - 1));
  39. let controlPages: number[] = getNumerals();
  40. function onChangeLength(): void {
  41. /** @event {{ length: number }} amount - Fires when the amount selection input changes. */
  42. dispatch('amount', settings.limit);
  43. lastPage = Math.max(0, Math.ceil(settings.size / settings.limit - 1));
  44. // ensure page in limit range
  45. if (settings.page > lastPage) {
  46. settings.page = lastPage;
  47. }
  48. controlPages = getNumerals();
  49. }
  50. function gotoPage(page: number) {
  51. if (page < 0) return;
  52. settings.page = page;
  53. /** @event {{ page: number }} page Fires when the next/back buttons are pressed. */
  54. dispatch('page', settings.page);
  55. controlPages = getNumerals();
  56. }
  57. // Full row - no ellipsis
  58. function getFullNumerals() {
  59. const pages = [];
  60. for (let index = 0; index <= lastPage; index++) {
  61. pages.push(index);
  62. }
  63. return pages;
  64. }
  65. function getNumerals() {
  66. if (lastPage <= maxNumerals * 2 + 1) return getFullNumerals();
  67. const pages = [];
  68. const isWithinLeftSection = settings.page < maxNumerals + 2;
  69. const isWithinRightSection = settings.page > lastPage - (maxNumerals + 2);
  70. pages.push(0);
  71. if (!isWithinLeftSection) pages.push(-1);
  72. if (isWithinLeftSection || isWithinRightSection) {
  73. // mid section - with only one ellipsis
  74. const sectionStart = isWithinLeftSection ? 1 : lastPage - (maxNumerals + 2);
  75. const sectionEnd = isWithinRightSection ? lastPage - 1 : maxNumerals + 2;
  76. for (let i = sectionStart; i <= sectionEnd; i++) {
  77. pages.push(i);
  78. }
  79. } else {
  80. // mid section - with both ellipses
  81. for (let i = settings.page - maxNumerals; i <= settings.page + maxNumerals; i++) {
  82. pages.push(i);
  83. }
  84. }
  85. if (!isWithinRightSection) pages.push(-1);
  86. pages.push(lastPage);
  87. return pages;
  88. }
  89. function updateSize(size: number) {
  90. lastPage = Math.max(0, Math.ceil(size / settings.limit - 1));
  91. controlPages = getNumerals();
  92. }
  93. // State
  94. $: classesButtonActive = (page: number) => {
  95. return page === settings.page ? `${active}` : '';
  96. };
  97. $: maxNumerals, onChangeLength();
  98. $: updateSize(settings.size);
  99. // Reactive Classes
  100. $: classesBase = `${cBase} ${justify} ${$$props.class ?? ''}`;
  101. </script>
  102. <div class={classesBase}>
  103. <!-- Select Amount -->
  104. {#if settings.amounts.length}
  105. <select
  106. bind:value={settings.limit}
  107. on:change={onChangeLength}
  108. class="dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-sm bg-transparent outline-none"
  109. {disabled}
  110. >
  111. {#each settings.amounts as amount}
  112. <option value={amount}>{amount} {amountText}</option>
  113. {/each}
  114. </select>
  115. {/if}
  116. <!-- Controls -->
  117. <div>
  118. <!-- Button: First -->
  119. {#if showFirstLastButtons}
  120. <button
  121. type="button"
  122. class={buttonClasses}
  123. on:click={() => {
  124. gotoPage(0);
  125. }}
  126. disabled={disabled || settings.page === 0}
  127. >
  128. <svg
  129. xmlns="http://www.w3.org/2000/svg"
  130. viewBox="0 0 512 512"
  131. fill="currentColor"
  132. class="w-4 h-4"
  133. >
  134. <path
  135. d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160zm352-160l-160 160c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L301.3 256 438.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0z"
  136. />
  137. </svg>
  138. </button>
  139. {/if}
  140. <!-- Button: Back -->
  141. {#if showPreviousNextButtons}
  142. <button
  143. type="button"
  144. aria-label="Previous Page"
  145. class={buttonClasses}
  146. on:click={() => {
  147. gotoPage(settings.page - 1);
  148. }}
  149. disabled={disabled || settings.page === 0}
  150. >
  151. <svg
  152. xmlns="http://www.w3.org/2000/svg"
  153. viewBox="0 0 448 512"
  154. fill="currentColor"
  155. class="w-4 h-4"
  156. >
  157. <path
  158. d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.2 288 416 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-306.7 0L214.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"
  159. />
  160. </svg>
  161. </button>
  162. {/if}
  163. <!-- Center -->
  164. {#if showNumerals === false}
  165. <!-- Details -->
  166. <button type="button" class="{buttonClasses} !text-sm">
  167. {settings.page * settings.limit + 1}-{Math.min(
  168. settings.page * settings.limit + settings.limit,
  169. settings.size
  170. )}&nbsp;<span class="opacity-50">{separatorText} {settings.size}</span>
  171. </button>
  172. {:else}
  173. <!-- Numeric Row -->
  174. {#each controlPages as page}
  175. <button
  176. type="button"
  177. {disabled}
  178. class="{buttonClasses} {classesButtonActive(page)}"
  179. on:click={() => gotoPage(page)}
  180. >
  181. {page >= 0 ? page + 1 : '...'}
  182. </button>
  183. {/each}
  184. {/if}
  185. <!-- Button: Next -->
  186. {#if showPreviousNextButtons}
  187. <button
  188. type="button"
  189. class={buttonClasses}
  190. on:click={() => {
  191. gotoPage(settings.page + 1);
  192. }}
  193. disabled={disabled || (settings.page + 1) * settings.limit >= settings.size}
  194. >
  195. <svg
  196. xmlns="http://www.w3.org/2000/svg"
  197. viewBox="0 0 448 512"
  198. fill="currentColor"
  199. class="w-4 h-4"
  200. >
  201. <path
  202. d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"
  203. />
  204. </svg>
  205. </button>
  206. {/if}
  207. <!-- Button: last -->
  208. {#if showFirstLastButtons}
  209. <button
  210. type="button"
  211. class={buttonClasses}
  212. on:click={() => {
  213. gotoPage(lastPage);
  214. }}
  215. disabled={disabled || (settings.page + 1) * settings.limit >= settings.size}
  216. >
  217. <svg
  218. xmlns="http://www.w3.org/2000/svg"
  219. viewBox="0 0 512 512"
  220. fill="currentColor"
  221. class="w-4 h-4"
  222. >
  223. <path
  224. d="M470.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L402.7 256 265.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160zm-352 160l160-160c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L210.7 256 73.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0z"
  225. />
  226. </svg>
  227. </button>
  228. {/if}
  229. </div>
  230. </div>