Pagination.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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 enabled = options.pagination;
  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 } = options;
  131. const max = hasFocus() ? Controller.getEnd() + 1 : ceil( length / perPage );
  132. list = placeholder || create( 'ul', classes.pagination, Elements.track.parentElement );
  133. addClass( list, ( paginationClasses = `${ CLASS_PAGINATION }--${ getDirection() }` ) );
  134. setAttribute( list, ROLE, 'tablist' );
  135. setAttribute( list, ARIA_LABEL, i18n.select );
  136. setAttribute( list, ARIA_ORIENTATION, getDirection() === TTB ? 'vertical' : '' );
  137. for ( let i = 0; i < max; i++ ) {
  138. const li = create( 'li', null, list );
  139. const button = create( 'button', { class: classes.page, type: 'button' }, li );
  140. const controls = Slides.getIn( i ).map( Slide => Slide.slide.id );
  141. const text = ! hasFocus() && perPage > 1 ? i18n.pageX : i18n.slideX;
  142. bind( button, 'click', apply( onClick, i ) );
  143. if ( options.paginationKeyboard ) {
  144. bind( button, 'keydown', apply( onKeydown, i ) );
  145. }
  146. setAttribute( li, ROLE, 'presentation' );
  147. setAttribute( button, ROLE, 'tab' );
  148. setAttribute( button, ARIA_CONTROLS, controls.join( ' ' ) );
  149. setAttribute( button, ARIA_LABEL, format( text, i + 1 ) );
  150. setAttribute( button, TAB_INDEX, -1 );
  151. items.push( { li, button, page: i } );
  152. }
  153. }
  154. /**
  155. * Called when the user clicks each pagination dot.
  156. * Moves the focus to the active slide for accessibility.
  157. *
  158. * @param page - A clicked page index.
  159. */
  160. function onClick( page: number ): void {
  161. go( `>${ page }`, true );
  162. }
  163. /**
  164. * Called when any key is pressed on the pagination.
  165. *
  166. * @link https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/#keyboard-interaction-21
  167. *
  168. * @param page - A page index.
  169. * @param e - A KeyboardEvent object.
  170. */
  171. function onKeydown( page: number, e: KeyboardEvent ): void {
  172. const { length } = items;
  173. const { key } = e;
  174. const dir = getDirection();
  175. let nextPage = -1;
  176. if ( key === resolve( ARROW_RIGHT, false, dir ) ) {
  177. nextPage = ++page % length;
  178. } else if ( key === resolve( ARROW_LEFT, false, dir ) ) {
  179. nextPage = ( --page + length ) % length;
  180. } else if ( key === 'Home' ) {
  181. nextPage = 0;
  182. } else if ( key === 'End' ) {
  183. nextPage = length - 1;
  184. }
  185. const item = items[ nextPage ];
  186. if ( item ) {
  187. focus( item.button );
  188. go( `>${ nextPage }` );
  189. prevent( e, true );
  190. }
  191. }
  192. /**
  193. * Returns the latest direction for pagination.
  194. */
  195. function getDirection(): Options['direction'] {
  196. return options.paginationDirection || options.direction;
  197. }
  198. /**
  199. * Returns the pagination item at the specified index.
  200. *
  201. * @param index - An index.
  202. *
  203. * @return A pagination item object if available, or otherwise `undefined`.
  204. */
  205. function getAt( index: number ): PaginationItem | undefined {
  206. return items[ Controller.toPage( index ) ];
  207. }
  208. /**
  209. * Updates the pagination status.
  210. */
  211. function update(): void {
  212. const prev = getAt( getIndex( true ) );
  213. const curr = getAt( getIndex() );
  214. if ( prev ) {
  215. const { button } = prev;
  216. removeClass( button, CLASS_ACTIVE );
  217. removeAttribute( button, ARIA_SELECTED );
  218. setAttribute( button, TAB_INDEX, -1 );
  219. }
  220. if ( curr ) {
  221. const { button } = curr;
  222. addClass( button, CLASS_ACTIVE );
  223. setAttribute( button, ARIA_SELECTED, true );
  224. setAttribute( button, TAB_INDEX, '' );
  225. }
  226. emit( EVENT_PAGINATION_UPDATED, { list, items }, prev, curr );
  227. }
  228. return {
  229. items,
  230. mount,
  231. destroy,
  232. getAt,
  233. update,
  234. };
  235. };