src/app/experimental/swiper/swiper.component.ts
/**
* @license
* Copyright Workylab. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://raw.githubusercontent.com/workylab/materialize-angular/master/LICENSE
*/
import {
AfterContentInit,
Component,
ContentChildren,
ElementRef,
EventEmitter,
Input,
Output,
QueryList,
ViewChild
} from '@angular/core';
import { SwiperEventsModel, SwiperModel } from './swiper.model';
import { getBooleanValue } from '../../utils/get-boolean-value.util';
import { SwiperItemComponent } from '../swiper-item/swiper-item.component';
@Component({
selector: 'materialize-swiper',
styleUrls: ['./swiper.component.scss'],
templateUrl: './swiper.component.html'
})
export class SwiperComponent implements AfterContentInit, SwiperModel {
static readonly defaultProps: SwiperModel = {
animationMs: 300,
autoplayMs: 1000,
className: '',
displayControls: true,
displayDots: true,
isAutoplay: false,
isCarousel: false,
isChangePerPage: false,
isReverse: false,
itemSwipePercentAdjust: 5,
maxSwipeOutPercent: 5
};
static readonly TOUCH_EVENTS: SwiperEventsModel = {
click: 'touchend',
down: 'touchstart',
move: 'touchmove',
out: 'touchend',
up: 'touchend'
};
static readonly MOUSE_EVENTS: SwiperEventsModel = {
click: 'click',
down: 'mousedown',
move: 'mousemove',
out: 'mouseout',
up: 'mouseup'
};
@ViewChild('swiperContainer', { static: true }) swiperContainerRef: ElementRef;
@ViewChild('swiper', { static: true }) swiperRef: ElementRef;
@ContentChildren(SwiperItemComponent) itemsQuery: QueryList<SwiperItemComponent>;
@Output('onChange') onChangeEmitter: EventEmitter<number>;
@Input('animationMs') animationMsInput: number;
@Input('autoplayMs') autoplayMsInput: number;
@Input('className') classNameInput: string;
@Input('displayControls') displayControlsInput: boolean;
@Input('displayDots') displayDotsInput: boolean;
@Input('isAutoplay') isAutoplayInput: boolean;
@Input('isCarousel') isCarouselInput: boolean;
@Input('isChangePerPage') isChangePerPageInput: boolean;
@Input('isReverse') isReverseInput: boolean;
@Input('itemSwipePercentAdjust') itemSwipePercentAdjustInput: number;
@Input('maxSwipeOutPercent') maxSwipeOutPercentInput: number;
public animationMs: number;
public autoplayMs: number;
public className: string;
public displayControls: boolean;
public dots: Array<number> = [];
public isAutoplay: boolean;
public isCarousel: boolean;
public isChangePerPage: boolean;
public isReverse: boolean;
public displayDots: boolean;
public itemSwipePercentAdjust: number;
public maxSwipeOutPercent: number;
// private pages: number[];
private firstPointX: number;
private firstPointY: number;
private initDistance: number;
private interval: number;
private lastIndexToDisplay: number;
private traveledDistance: number;
public container: HTMLElement;
public index: number;
public items: Array<HTMLElement>;
public nextClonedItems: number;
public prevClonedItems: number;
public supportedEvents: SwiperEventsModel;
public swiper: HTMLElement;
constructor() {
this.onChangeEmitter = new EventEmitter();
this.actionDown = this.actionDown.bind(this);
this.actionUp = this.actionUp.bind(this);
this.activateSwipe = this.activateSwipe.bind(this);
this.animate = this.animate.bind(this);
this.autoplay = this.autoplay.bind(this);
this.cancelRedirect = this.cancelRedirect.bind(this);
this.showNext = this.showNext.bind(this);
this.showPrev = this.showPrev.bind(this);
this.stopAutoplay = this.stopAutoplay.bind(this);
this.swipe = this.swipe.bind(this);
this.update = this.update.bind(this);
}
supportTouchEvents() {
return 'ontouchstart' in window;
}
ngAfterContentInit() {
setTimeout(() => {
this.initValues();
}, 300);
}
initValues() {
const { defaultProps } = SwiperComponent;
this.index = 0;
this.initDistance = 0;
this.traveledDistance = 0;
this.supportedEvents = this.supportTouchEvents()
? SwiperComponent.TOUCH_EVENTS
: SwiperComponent.MOUSE_EVENTS;
this.animationMs = this.animationMsInput || defaultProps.animationMs;
this.className = this.classNameInput || defaultProps.className;
this.displayControls = getBooleanValue(this.displayControlsInput, defaultProps.displayControls);
this.displayDots = getBooleanValue(this.displayDotsInput, defaultProps.displayDots);
this.isAutoplay = getBooleanValue(this.isAutoplayInput, defaultProps.isAutoplay);
this.isCarousel = getBooleanValue(this.isCarouselInput, defaultProps.isCarousel);
this.isChangePerPage = getBooleanValue(this.isChangePerPageInput, defaultProps.isChangePerPage);
this.isReverse = getBooleanValue(this.isReverseInput, defaultProps.isReverse);
this.itemSwipePercentAdjust = this.itemSwipePercentAdjustInput || defaultProps.itemSwipePercentAdjust;
this.maxSwipeOutPercent = this.maxSwipeOutPercentInput || defaultProps.maxSwipeOutPercent;
this.swiper = this.swiperRef.nativeElement;
this.container = this.swiperContainerRef.nativeElement;
this.items = this.itemsQuery.toArray().map(item => item.swiperItemContainer);
this.lastIndexToDisplay = this.getLastIndexToDisplay(this.items);
// this.pages = this.getItemsPerPage();
this.swiper.addEventListener(this.supportedEvents.down, this.actionDown);
this.swiper.addEventListener(this.supportedEvents.click, this.cancelRedirect);
window.addEventListener('resize', () => {
setTimeout(this.update, 100);
});
if (this.displayDots) {
this.createDots();
}
if (this.isCarousel) {
this.createClones();
if (this.isAutoplay) {
this.startAutoplay();
}
}
}
update() {
this.lastIndexToDisplay = this.getLastIndexToDisplay(this.items);
// this.pages = this.getItemsPerPage();
this.goToItem(this.index, false);
if (this.displayDots) {
this.createDots();
}
if (this.isCarousel) {
this.createClones();
}
}
animate(distance: number, velocity: number) {
const translate = `translate3d(${ -1 * distance }px, 0px, 0px)`;
this.container.style.transform = translate;
this.container.style.transitionDuration = `${ velocity }ms`;
}
containerFullWidth(): number {
return this.container.scrollWidth - this.container.offsetWidth;
}
autoplay() {
this.swiper.removeEventListener(this.supportedEvents.out, this.autoplay);
this.interval = window.setInterval(() => {
if (this.isReverse) {
this.showPrev();
} else {
this.showNext();
}
}, this.autoplayMs);
}
startAutoplay() {
this.autoplay();
this.swiper.addEventListener(this.supportedEvents.move, this.stopAutoplay);
}
stopAutoplay() {
clearInterval(this.interval);
this.swiper.addEventListener(this.supportedEvents.out, this.autoplay);
}
actionDown(downEvent: any) {
if (!this.supportTouchEvents()) {
// downEvent.preventDefault();
}
this.firstPointY = this.supportTouchEvents()
? downEvent.touches[0].clientY
: downEvent.screenY;
this.firstPointX = this.supportTouchEvents()
? downEvent.touches[0].clientX
: downEvent.screenX;
let transform = this.container.style.transform;
if (transform) {
transform = transform.split('(')[1];
transform = transform.split(')')[0];
transform = transform.split(',')[0];
transform = transform.replace('-', '');
transform = transform.replace('px', '');
this.initDistance = Number(transform);
} else {
this.initDistance = 0;
}
this.swiper.addEventListener(this.supportedEvents.move, this.activateSwipe);
this.swiper.addEventListener(this.supportedEvents.up, () => {
this.swiper.removeEventListener(
this.supportedEvents.move, this.activateSwipe
);
});
}
activateSwipe(moveEvent: any) {
const distanceY = this.supportTouchEvents()
? moveEvent.touches[0].clientY
: moveEvent.screenY;
if (Math.abs(this.firstPointY - distanceY) < 10) {
this.swiper.addEventListener(this.supportedEvents.move, this.swipe);
this.swiper.addEventListener(this.supportedEvents.up, this.actionUp);
this.swiper.removeEventListener(this.supportedEvents.move, this.activateSwipe);
}
}
actionUp(upEvent: any) {
const distanceEvent = this.supportTouchEvents()
? upEvent.changedTouches[0].clientX
: upEvent.screenX;
this.traveledDistance = this.firstPointX - distanceEvent;
const distance = this.traveledDistance + this.initDistance;
for (let i = 0; i <= this.lastIndexToDisplay; i++) {
const ajustDistance = (this.items[i].offsetWidth * this.itemSwipePercentAdjust) / 100;
const minDistance = this.traveledDistance > 0
? this.items[i].offsetLeft + ajustDistance
: this.items[i].offsetLeft + this.items[i].offsetWidth - ajustDistance;
if (minDistance > distance) {
this.goToItem(i, true);
break;
}
}
this.swiper.removeEventListener(this.supportedEvents.move, this.swipe);
this.swiper.removeEventListener(this.supportedEvents.up, this.actionUp);
}
swipe(moveEvent: any) {
if (moveEvent.cancelable) {
moveEvent.preventDefault();
}
const distanceEvent = this.supportTouchEvents()
? moveEvent.touches[0].clientX
: moveEvent.screenX;
let distance = this.firstPointX - distanceEvent + this.initDistance;
const containerWidth = this.container.offsetWidth;
const outRange = containerWidth / 100 * this.maxSwipeOutPercent;
const minDistance = outRange * -1;
const maxDistance = outRange + this.containerFullWidth();
if (distance < minDistance) {
distance = minDistance;
} else if (distance > maxDistance) {
distance = maxDistance;
}
this.animate(distance, 0);
}
getLastIndexToDisplay(items: Array<HTMLElement>): number {
let distance = 0;
const totalItems = items.length - 1;
for (let i = totalItems; i >= 0; i--) {
distance = distance + items[i].offsetWidth;
if (i === totalItems && distance > this.container.offsetWidth) {
return i;
}
if (distance > this.container.offsetWidth) {
return i + 1;
}
if (distance === this.container.offsetWidth) {
return i;
}
}
return totalItems;
}
cancelRedirect(event: any) {
const distanceEvent = this.supportTouchEvents()
? event.changedTouches[0].clientX
: event.screenX;
this.traveledDistance = this.firstPointX - distanceEvent;
if (this.traveledDistance !== 0 && !this.supportTouchEvents()) {
event.preventDefault();
}
}
updateIndex(newIndex: number) {
this.items[this.index].classList.remove('active');
this.index = newIndex;
this.items[newIndex].classList.add('active');
this.onChangeEmitter.emit(this.index);
}
preventAutoplay(event: any) {
if (event && this.isCarousel && this.supportTouchEvents()) {
clearInterval(this.interval);
this.autoplay();
}
}
showPrev(event?: Event) {
this.preventAutoplay(event);
if (this.isChangePerPage) {
// const currentPage = this.getPageByIndex(this.index);
// this.goToPage(currentPage - 1);
} else {
this.goToItem(this.index - 1, true);
}
}
showNext(event?: Event) {
this.preventAutoplay(event);
if (this.isChangePerPage) {
// const page = this.getPageByIndex(this.index);
// this.goToPage(page + 1);
} else {
this.goToItem(this.index + 1, true);
}
}
cloneItem(index: number): HTMLElement {
const clonedItem = this.items[index].cloneNode(true) as HTMLElement;
clonedItem.classList.add('cloned');
return clonedItem;
}
createClones() {
this.deleteClones();
this.items = Array.from(this.container.querySelectorAll('.swiper-item-wrapper'));
this.nextClonedItems = this.cloneDisplayedItemsInFirstPage();
this.prevClonedItems = this.cloneDisplayedItemsInLastPage();
this.items = Array.from(this.container.querySelectorAll('.swiper-item-wrapper'));
this.lastIndexToDisplay = this.getLastIndexToDisplay(this.items);
// this.pages = this.getItemsPerPage();
this.goToItem(this.prevClonedItems, false);
this.createDots();
}
deleteClones() {
const clons: any = this.container.querySelectorAll('.cloned');
for (const item of clons) {
if (item.parentNode) {
item.parentNode.removeChild(item);
}
}
}
goToItem(index: number, hasAnimation: boolean) {
const animationMs = hasAnimation
? this.animationMs
: 0;
if (index >= 0 && index <= this.lastIndexToDisplay) {
this.updateIndex(index);
this.animate(this.items[index].offsetLeft, animationMs);
const realLastIndex = this.items.length - this.nextClonedItems;
if (this.isCarousel && index >= realLastIndex) {
this.moveToRealInit(index);
} else if (this.isCarousel && index < this.prevClonedItems) {
this.moveToRealEnd(index);
}
}
if (index >= this.lastIndexToDisplay && !this.isCarousel) {
this.updateIndex(this.lastIndexToDisplay);
this.animate(this.containerFullWidth(), animationMs);
}
}
moveToRealInit(index: number) {
setTimeout(() => {
const realLastIndex = this.items.length - this.nextClonedItems;
const realCurrentIndex = index + this.prevClonedItems;
const initIndex = realCurrentIndex - realLastIndex;
this.updateIndex(initIndex);
this.animate(this.items[initIndex].offsetLeft, 0);
}, this.animationMs);
}
moveToRealEnd(index: number) {
setTimeout(() => {
const realLastIndex = this.items.length - this.nextClonedItems;
const realCurrentIndex = this.prevClonedItems - index;
const endIndex = realLastIndex - realCurrentIndex;
this.updateIndex(endIndex);
this.animate(this.items[endIndex].offsetLeft, 0);
}, this.animationMs);
}
cloneDisplayedItemsInFirstPage(): number {
let distance = 0;
for (let i = 0; i < this.items.length; i++) {
distance = distance + this.items[i].offsetWidth;
this.container.appendChild(this.cloneItem(i));
if (distance >= this.container.offsetWidth) {
return i + 1;
}
}
return this.items.length;
}
cloneDisplayedItemsInLastPage(): number {
const totalItems = this.items.length - 1;
let itemsCounter = 1;
let distance = 0;
for (let i = totalItems; i >= 0; i--) {
distance = distance + this.items[i].offsetWidth;
this.container.insertBefore(this.cloneItem(i), this.container.firstChild);
if (distance >= this.container.offsetWidth) {
return itemsCounter;
}
itemsCounter = itemsCounter + 1;
}
return totalItems;
}
onClickDot(index: number) {
this.goToItem(index, true);
}
createDots() {
this.dots = [];
let firstDot = 0;
let lastDot = 0;
if (this.isChangePerPage) {
//
} else {
firstDot = this.isCarousel
? this.prevClonedItems
: 0;
lastDot = this.isCarousel
? this.items.length - this.nextClonedItems
: this.items.length;
}
for (let i = firstDot; i < lastDot; i++) {
this.dots.push(i);
}
}
// getItemsPerPage(): Array<number> {
// const pageItems = [];
// let distance = 0;
// let itemsCounter = 1;
// for (let i = 0; i < this.items.length; i++) {
// distance = distance + this.items[i].offsetWidth;
// if (distance > this.container.offsetWidth) {
// if (distance < this.container.offsetWidth + this.items.length) {
// pageItems.push(itemsCounter);
// distance = 0;
// itemsCounter = 0;
// } else {
// pageItems.push(itemsCounter - 1);
// distance = this.items[i].offsetWidth;
// itemsCounter = 1;
// }
// }
// if (i === this.items.length - 1 && itemsCounter > 0) {
// pageItems.push(itemsCounter);
// }
// itemsCounter++;
// }
// return pageItems;
// }
// goToPage(pageNumber: number) {
// let firstItemPage = 0;
// for (let i = 0; i < pageNumber; i++) {
// firstItemPage = firstItemPage + this.pages[i];
// }
// this.goToItem(firstItemPage, true);
// }
// getPageByIndex(index: number): number {
// let itemsInCurrentPage = 0;
// const nextIndex = index + 1;
// for (let i = 0; i < this.items.length; i++) {
// itemsInCurrentPage = itemsInCurrentPage + this.pages[i];
// if (itemsInCurrentPage >= nextIndex) {
// return i;
// }
// }
// return 0;
// }
}