/// import { containsClass, throttle, forEach, mutate, isSwiping, getTouchCoords, PosCoordinates, getInitialItemsOrder, navAvailable, calcStepIndex } from './helpers'; import { step, swipeContinuous, swipeStarts, swipeEnds } from './logic'; import { afterInfiniteUpdated } from './update-infinite'; import * as SE from './side-effects'; const querySelector = HTMLElement.prototype.querySelector; export class Carousel implements ICarousel { element: CarouselElement; container: HTMLDivElement; swipeDir = undefined; isSwiping = undefined; busy: boolean = false; index: number = 0; offset: number = 0; itemsOrder: SlidesOrder; mode: CarouselMode = 'finite'; autoRotateInterval: number = 0; private autoRotateIntervalId: number; resizeListener: EventListener; touchStartListener: EventListener; touchMoveListener: EventListener; touchEndListener: EventListener; pagination: IPagination = { left: null, right: null, indicator: null }; touchStart: PosCoordinates; currentTouch: PosCoordinates; finalTouch: PosCoordinates; constructor(element: CarouselElement) { this.element = element; } attached() { this.touchStart = new PosCoordinates(0, 0); this.mode = this.element.getAttribute('loop') || 'finite'; if (navigator.userAgent.indexOf('Safari') >= 0 && navigator.userAgent.indexOf('OS 14_') >= 0) { this.mode = 'finite'; } this.autoRotateInterval = this.element.getAttribute('auto-rotate-interval') || 0; this.container = (querySelector.call(this.element, '[role="container"]')); this.container.addEventListener('transitionend', _ => this.busy = false); if (this.mode === 'infinite') { // Note: This event will not be always triggered! // When we move to the [right], first of all, we remove `no-transition` class from the container. // Thus, transition happens and we have the event. // However, when we move to the [left], we add the `no-transition` class to the Container. // Thus, the transition will not be happening and the callback will not be called. this.container.addEventListener('transitionend', _ => { mutate(this, afterInfiniteUpdated(this, false)); // this.busy = false; }); } // Create Listeners. this.resizeListener = throttle(step.bind(null, 0, this.mode, this), 100); this.touchStartListener = this.touchStartEventHandler.bind(this); this.touchMoveListener = this.touchMoveEventHandler.bind(this); this.touchEndListener = this.touchEndEventHandler.bind(this); // Add Listeners. window.addEventListener('resize', this.resizeListener); this.element.addEventListener('touchstart', this.touchStartListener); this.element.addEventListener('touchmove', this.touchMoveListener); this.element.addEventListener('touchend', this.touchEndListener); if (!('touchend' in window)) { forEach((btn: NavigationButton) => { const direction = btn.getAttribute('data-direction'); this.pagination[btn.getAttribute('data-direction')] = btn; btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); mutate(this, step(direction === 'left' ? -1 : 1, this)); }); }, this.element.querySelectorAll('[role="nav-button"]')); } this.pagination.indicator = this.element.querySelector('[role="indicator"]'); this.index = 0; mutate(this, step(0, this, false)); this.busy = false; if (this.autoRotateInterval > 0) this.setupAutoRotate(); } detached() { window.removeEventListener('resize', this.resizeListener, true); this.element.removeEventListener('touchstart', this.touchStartListener, true); this.element.removeEventListener('touchmove', this.touchMoveListener, true); this.element.removeEventListener('touchend', this.touchEndListener, true); } touchStartEventHandler(event: TouchEvent) { const target = event.target; if (target.classList.contains('as24-carousel__button')) { event.preventDefault(); event.stopPropagation(); } if (this.busy) return; if (target.classList.contains('as24-carousel__button')) { let btnDirection = target.dataset.direction; mutate(this, step(btnDirection === 'left' ? -1 : 1, this)); } else { const navButtons = Array.from(this.element.querySelectorAll('[role="nav-button"]')); if (!navAvailable(navButtons)) { return; } mutate(this, swipeStarts(getTouchCoords(event), this)); } } touchMoveEventHandler(event: TouchEvent) { const target = event.target; const navButtons = Array.from(this.element.querySelectorAll('[role="nav-button"]')); if (this.busy) return; if (this.isSwiping) { event.preventDefault(); } if (!navAvailable(navButtons)) { return; } if (!target.classList.contains('as24-carousel__button')) { mutate(this, swipeContinuous(getTouchCoords(event), this)); } } touchEndEventHandler(event: TouchEvent) { const navButtons = Array.from(this.element.querySelectorAll('[role="nav-button"]')); const finalTouch = getTouchCoords(event.changedTouches[0]); if (!navAvailable(navButtons) || (this.touchStart && this.touchStart.x === finalTouch.x)) { return; } mutate(this, swipeEnds(finalTouch, this)); } goTo(index: number, options: GoToOptions) { this.index = --index; this.index = calcStepIndex(0, this); this.touchStart = new PosCoordinates(0, 0); mutate(this, step(0, this, options.notify)); this.busy = false; } getIndex(): number { return this.index; } redraw(triggerNotifications = true): void { mutate(this, step(0, this, triggerNotifications)); this.busy = false; } removeItem(index: number): void { this.container.children[index].remove(); this.busy = false; this.goTo(1, { notify: false }); } private setupAutoRotate(): void { this.autoRotateIntervalId = setInterval(() => { mutate(this, step(1, this)); }, this.autoRotateInterval); this.addOneTimeEventListener(this.element, 'touchstart', this.stopAutoRotate.bind(this)); this.addOneTimeEventListener(this.element, 'click', this.stopAutoRotate.bind(this)); } private stopAutoRotate(): void { if (!this.autoRotateIntervalId) return; clearInterval(this.autoRotateIntervalId); this.autoRotateIntervalId = 0; } private addOneTimeEventListener(element: Element, eventName: string, listener: Function): void { const executeListenerAndRemove = () => { listener(); element.removeEventListener(eventName, executeListenerAndRemove, true); }; element.addEventListener(eventName, executeListenerAndRemove, true); } }