import { ARROW_LEFT, ARROW_RIGHT } from '../../constants/arrows'; import { ARIA_CONTROLS, ARIA_LABEL, ARIA_ORIENTATION, ARIA_SELECTED, ROLE, TAB_INDEX, } from '../../constants/attributes'; import { CLASS_ACTIVE, CLASS_PAGINATION } from '../../constants/classes'; import { TTB } from '../../constants/directions'; import { EVENT_END_INDEX_CHANGED, EVENT_MOVE, EVENT_PAGINATION_MOUNTED, EVENT_PAGINATION_UPDATED, EVENT_REFRESH, EVENT_SCROLL, EVENT_SCROLLED, EVENT_UPDATED, } from '../../constants/events'; import { BaseComponent, ComponentConstructor, Options } from '../../types'; import { addClass, apply, ceil, create, display, empty, focus, format, prevent, removeAttribute, removeClass, removeNode, setAttribute, slice, } from '@splidejs/utils'; /** * The interface for the Pagination component. * * @since 3.0.0 */ export interface PaginationComponent extends BaseComponent { readonly items: PaginationItem[]; getAt(index: number): PaginationItem; update(): void; } /** * The interface for data of the pagination. * * @since 3.0.0 */ export interface PaginationData { readonly list: HTMLUListElement; readonly items: PaginationItem[]; } /** * The interface for each pagination item. * * @since 3.0.0 */ export interface PaginationItem { readonly li: HTMLLIElement; readonly button: HTMLButtonElement; readonly page: number; } /** * The component for the pagination UI (a slide picker). * * @since 3.0.0 * * @param Splide - A Splide instance. * @param Components - A collection of components. * @param options - Options. * @param event - An EventInterface instance. * * @return A Pagination component object. */ export const Pagination: ComponentConstructor = (Splide, Components, options, event) => { const { on, emit, bind } = event; const { Slides, Elements, Controller } = Components; const { hasFocus, getIndex, go } = Controller; const { resolve } = Components.Direction; const { pagination: placeholder } = Elements; /** * Stores all pagination items. */ const items: PaginationItem[] = []; /** * The pagination element. */ let list: HTMLUListElement | null; /** * Holds modifier classes. */ let paginationClasses: string; /** * Called when the component is mounted. */ function mount(): void { destroy(); on([EVENT_UPDATED, EVENT_REFRESH, EVENT_END_INDEX_CHANGED], mount); const { pagination: enabled = true } = options; placeholder && display(placeholder, enabled ? '' : 'none'); if (enabled) { on([EVENT_MOVE, EVENT_SCROLL, EVENT_SCROLLED], update); createPagination(); update(); emit(EVENT_PAGINATION_MOUNTED, { list, items }, getAt(Splide.index)); } } /** * Destroys the component. */ function destroy(): void { if (list) { removeNode(placeholder ? slice(list.children) : list); removeClass(list, paginationClasses); empty(items); list = null; } event.destroy(); } /** * Creates the pagination element and appends it to the slider. */ function createPagination(): void { const { length } = Splide; const { classes, i18n, perPage, paginationKeyboard = true } = options; const max = hasFocus() ? Controller.getEnd() + 1 : ceil(length / perPage); const dir = getDirection(); list = placeholder || create('ul', classes.pagination, Elements.track.parentElement); addClass(list, (paginationClasses = `${ CLASS_PAGINATION }--${ dir }`)); setAttribute(list, ROLE, 'tablist'); setAttribute(list, ARIA_LABEL, i18n.select); setAttribute(list, ARIA_ORIENTATION, dir === TTB ? 'vertical' : ''); for (let i = 0; i < max; i++) { const li = create('li', null, list); const button = create('button', { class: classes.page, type: 'button' }, li); const controls = Slides.getIn(i).map(Slide => Slide.slide.id); const text = !hasFocus() && perPage > 1 ? i18n.pageX : i18n.slideX; bind(button, 'click', () => go(`>${ i }`)); paginationKeyboard && bind(button, 'keydown', apply(onKeydown, i)); setAttribute(li, ROLE, 'presentation'); setAttribute(button, ROLE, 'tab'); setAttribute(button, ARIA_CONTROLS, controls.join(' ')); setAttribute(button, ARIA_LABEL, format(text, i + 1)); setAttribute(button, TAB_INDEX, -1); items.push({ li, button, page: i }); } } /** * Called when any key is pressed on the pagination. * * @link https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/#keyboard-interaction-21 * * @param page - A page index. * @param e - A KeyboardEvent object. */ function onKeydown(page: number, e: KeyboardEvent): void { const { length } = items; const { key } = e; const dir = getDirection(); let nextPage = -1; if (key === resolve(ARROW_RIGHT, false, dir)) { nextPage = ++page % length; } else if (key === resolve(ARROW_LEFT, false, dir)) { nextPage = (--page + length) % length; } else if (key === 'Home') { nextPage = 0; } else if (key === 'End') { nextPage = length - 1; } const item = items[nextPage]; if (item) { focus(item.button); go(`>${ nextPage }`); prevent(e, true); } } /** * Returns the latest direction for pagination. * * @return The direction for pagination. */ function getDirection(): Options[ 'direction' ] { return options.paginationDirection || options.direction; } /** * Returns the pagination item at the specified index. * * @param index - An index. * * @return A pagination item object if available, or otherwise `undefined`. */ function getAt(index: number): PaginationItem | undefined { return items[Controller.toPage(index)]; } /** * Updates the pagination status. */ function update(): void { const prev = getAt(getIndex(true)); const curr = getAt(getIndex()); if (prev) { const { button } = prev; removeClass(button, CLASS_ACTIVE); removeAttribute(button, ARIA_SELECTED); setAttribute(button, TAB_INDEX, -1); } if (curr) { const { button } = curr; addClass(button, CLASS_ACTIVE); setAttribute(button, ARIA_SELECTED, true); setAttribute(button, TAB_INDEX, ''); } emit(EVENT_PAGINATION_UPDATED, { list, items }, prev, curr); } return { items, mount, destroy, getAt, update, }; };