Pagination.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import { ARROW_LEFT, ARROW_RIGHT } from '../../constants/arrows';
  2. import {
  3. ARIA_CONTROLS,
  4. ARIA_LABEL,
  5. ARIA_ORIENTATION,
  6. ARIA_SELECTED,
  7. ROLE,
  8. TAB_INDEX,
  9. } from '../../constants/attributes';
  10. import { CLASS_ACTIVE, CLASS_PAGINATION } from '../../constants/classes';
  11. import { TTB } from '../../constants/directions';
  12. import {
  13. EVENT_END_INDEX_CHANGED,
  14. EVENT_MOVE,
  15. EVENT_PAGINATION_MOUNTED,
  16. EVENT_PAGINATION_UPDATED,
  17. EVENT_REFRESH,
  18. EVENT_SCROLL,
  19. EVENT_SCROLLED,
  20. EVENT_UPDATED,
  21. } from '../../constants/events';
  22. import { BaseComponent, ComponentConstructor, Options } from '../../types';
  23. import {
  24. addClass,
  25. apply,
  26. ceil,
  27. create,
  28. display,
  29. empty,
  30. focus,
  31. format,
  32. prevent,
  33. removeAttribute,
  34. removeClass,
  35. removeNode,
  36. setAttribute,
  37. slice,
  38. } from '@splidejs/utils';
  39. /**
  40. * The interface for the Pagination component.
  41. *
  42. * @since 3.0.0
  43. */
  44. export interface PaginationComponent extends BaseComponent {
  45. readonly items: PaginationItem[];
  46. getAt(index: number): PaginationItem;
  47. update(): void;
  48. }
  49. /**
  50. * The interface for data of the pagination.
  51. *
  52. * @since 3.0.0
  53. */
  54. export interface PaginationData {
  55. readonly list: HTMLUListElement;
  56. readonly items: PaginationItem[];
  57. }
  58. /**
  59. * The interface for each pagination item.
  60. *
  61. * @since 3.0.0
  62. */
  63. export interface PaginationItem {
  64. readonly li: HTMLLIElement;
  65. readonly button: HTMLButtonElement;
  66. readonly page: number;
  67. }
  68. /**
  69. * The component for the pagination UI (a slide picker).
  70. *
  71. * @since 3.0.0
  72. *
  73. * @param Splide - A Splide instance.
  74. * @param Components - A collection of components.
  75. * @param options - Options.
  76. * @param event - An EventInterface instance.
  77. *
  78. * @return A Pagination component object.
  79. */
  80. export const Pagination: ComponentConstructor<PaginationComponent> = (Splide, Components, options, event) => {
  81. const { on, emit, bind } = event;
  82. const { Slides, Elements, Controller } = Components;
  83. const { hasFocus, getIndex, go } = Controller;
  84. const { resolve } = Components.Direction;
  85. const { pagination: placeholder } = Elements;
  86. /**
  87. * Stores all pagination items.
  88. */
  89. const items: PaginationItem[] = [];
  90. /**
  91. * The pagination element.
  92. */
  93. let list: HTMLUListElement | null;
  94. /**
  95. * Holds modifier classes.
  96. */
  97. let paginationClasses: string;
  98. /**
  99. * Called when the component is mounted.
  100. */
  101. function mount(): void {
  102. destroy();
  103. on([EVENT_UPDATED, EVENT_REFRESH, EVENT_END_INDEX_CHANGED], mount);
  104. const { pagination: enabled = true } = options;
  105. placeholder && display(placeholder, enabled ? '' : 'none');
  106. if (enabled) {
  107. on([EVENT_MOVE, EVENT_SCROLL, EVENT_SCROLLED], update);
  108. createPagination();
  109. update();
  110. emit(EVENT_PAGINATION_MOUNTED, { list, items }, getAt(Splide.index));
  111. }
  112. }
  113. /**
  114. * Destroys the component.
  115. */
  116. function destroy(): void {
  117. if (list) {
  118. removeNode(placeholder ? slice(list.children) : list);
  119. removeClass(list, paginationClasses);
  120. empty(items);
  121. list = null;
  122. }
  123. event.destroy();
  124. }
  125. /**
  126. * Creates the pagination element and appends it to the slider.
  127. */
  128. function createPagination(): void {
  129. const { length } = Splide;
  130. const { classes, i18n, perPage, paginationKeyboard = true } = options;
  131. const max = hasFocus() ? Controller.getEnd() + 1 : ceil(length / perPage);
  132. const dir = getDirection();
  133. list = placeholder || create('ul', classes.pagination, Elements.track.parentElement);
  134. addClass(list, (paginationClasses = `${ CLASS_PAGINATION }--${ dir }`));
  135. setAttribute(list, ROLE, 'tablist');
  136. setAttribute(list, ARIA_LABEL, i18n.select);
  137. setAttribute(list, ARIA_ORIENTATION, dir === TTB ? 'vertical' : '');
  138. for (let i = 0; i < max; i++) {
  139. const li = create('li', null, list);
  140. const button = create('button', { class: classes.page, type: 'button' }, li);
  141. const controls = Slides.getIn(i).map(Slide => Slide.slide.id);
  142. const text = !hasFocus() && perPage > 1 ? i18n.pageX : i18n.slideX;
  143. bind(button, 'click', () => go(`>${ i }`));
  144. paginationKeyboard && bind(button, 'keydown', apply(onKeydown, i));
  145. setAttribute(li, ROLE, 'presentation');
  146. setAttribute(button, ROLE, 'tab');
  147. setAttribute(button, ARIA_CONTROLS, controls.join(' '));
  148. setAttribute(button, ARIA_LABEL, format(text, i + 1));
  149. setAttribute(button, TAB_INDEX, -1);
  150. items.push({ li, button, page: i });
  151. }
  152. }
  153. /**
  154. * Called when any key is pressed on the pagination.
  155. *
  156. * @link https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/#keyboard-interaction-21
  157. *
  158. * @param page - A page index.
  159. * @param e - A KeyboardEvent object.
  160. */
  161. function onKeydown(page: number, e: KeyboardEvent): void {
  162. const { length } = items;
  163. const { key } = e;
  164. const dir = getDirection();
  165. let nextPage = -1;
  166. if (key === resolve(ARROW_RIGHT, false, dir)) {
  167. nextPage = ++page % length;
  168. } else if (key === resolve(ARROW_LEFT, false, dir)) {
  169. nextPage = (--page + length) % length;
  170. } else if (key === 'Home') {
  171. nextPage = 0;
  172. } else if (key === 'End') {
  173. nextPage = length - 1;
  174. }
  175. const item = items[nextPage];
  176. if (item) {
  177. focus(item.button);
  178. go(`>${ nextPage }`);
  179. prevent(e, true);
  180. }
  181. }
  182. /**
  183. * Returns the latest direction for pagination.
  184. *
  185. * @return The direction for pagination.
  186. */
  187. function getDirection(): Options[ 'direction' ] {
  188. return options.paginationDirection || options.direction;
  189. }
  190. /**
  191. * Returns the pagination item at the specified index.
  192. *
  193. * @param index - An index.
  194. *
  195. * @return A pagination item object if available, or otherwise `undefined`.
  196. */
  197. function getAt(index: number): PaginationItem | undefined {
  198. return items[Controller.toPage(index)];
  199. }
  200. /**
  201. * Updates the pagination status.
  202. */
  203. function update(): void {
  204. const prev = getAt(getIndex(true));
  205. const curr = getAt(getIndex());
  206. if (prev) {
  207. const { button } = prev;
  208. removeClass(button, CLASS_ACTIVE);
  209. removeAttribute(button, ARIA_SELECTED);
  210. setAttribute(button, TAB_INDEX, -1);
  211. }
  212. if (curr) {
  213. const { button } = curr;
  214. addClass(button, CLASS_ACTIVE);
  215. setAttribute(button, ARIA_SELECTED, true);
  216. setAttribute(button, TAB_INDEX, '');
  217. }
  218. emit(EVENT_PAGINATION_UPDATED, { list, items }, prev, curr);
  219. }
  220. return {
  221. items,
  222. mount,
  223. destroy,
  224. getAt,
  225. update,
  226. };
  227. };