// tslint:disable:max-file-line-count /*** * pause (not yet supported) (?string='hover') - event group name which pauses * the cycling of the carousel, if hover pauses on mouseenter and resumes on * mouseleave keyboard (not yet supported) (?boolean=true) - if false * carousel will not react to keyboard events * note: swiping not yet supported */ /**** * Problems: * 1) if we set an active slide via model changes, .active class remains on a * current slide. * 2) if we have only one slide, we shouldn't show prev/next nav buttons * 3) if first or last slide is active and noWrap is true, there should be * "disabled" class on the nav buttons. * 4) default interval should be equal 5000 */ import { Component, EventEmitter, Input, NgZone, OnDestroy, Output } from '@angular/core'; import { isBs3, LinkedList } from '../utils/index'; import { SlideComponent } from './slide.component'; import { CarouselConfig } from './carousel.config'; export enum Direction { UNKNOWN, NEXT, PREV } /** * Base element to create carousel */ @Component({ selector: 'carousel', templateUrl: './carousel.component.html' }) export class CarouselComponent implements OnDestroy { /** If `true` — carousel will not cycle continuously and will have hard stops (prevent looping) */ @Input() noWrap: boolean; /** If `true` — will disable pausing on carousel mouse hover */ @Input() noPause: boolean; /** If `true` — carousel-indicators are visible */ @Input() showIndicators: boolean; /** Will be emitted when active slide has been changed. Part of two-way-bindable [(activeSlide)] property */ @Output() activeSlideChange: EventEmitter = new EventEmitter(false); /** Index of currently displayed slide(started for 0) */ @Input() set activeSlide(index: number) { if (this._slides.length && index !== this._currentActiveSlide) { this._select(index); } } get activeSlide(): number { return this._currentActiveSlide; } /** * Delay of item cycling in milliseconds. If false, carousel won't cycle * automatically. */ @Input() get interval(): number { return this._interval; } set interval(value: number) { this._interval = value; this.restartTimer(); } get slides(): SlideComponent[] { return this._slides.toArray(); } protected _currentActiveSlide: number; protected _interval: number; protected _slides: LinkedList = new LinkedList(); protected currentInterval: any; protected isPlaying: boolean; protected destroyed = false; get isBs4(): boolean { return !isBs3(); } constructor(config: CarouselConfig, private ngZone: NgZone) { Object.assign(this, config); } ngOnDestroy(): void { this.destroyed = true; } /** * Adds new slide. If this slide is first in collection - set it as active * and starts auto changing * @param slide */ addSlide(slide: SlideComponent): void { this._slides.add(slide); if (this._slides.length === 1) { this._currentActiveSlide = void 0; this.activeSlide = 0; this.play(); } } /** * Removes specified slide. If this slide is active - will roll to another * slide * @param slide */ removeSlide(slide: SlideComponent): void { const remIndex = this._slides.indexOf(slide); if (this._currentActiveSlide === remIndex) { // removing of active slide let nextSlideIndex: number = void 0; if (this._slides.length > 1) { // if this slide last - will roll to first slide, if noWrap flag is // FALSE or to previous, if noWrap is TRUE in case, if this slide in // middle of collection, index of next slide is same to removed nextSlideIndex = !this.isLast(remIndex) ? remIndex : this.noWrap ? remIndex - 1 : 0; } this._slides.remove(remIndex); // prevents exception with changing some value after checking setTimeout(() => { this._select(nextSlideIndex); }, 0); } else { this._slides.remove(remIndex); const currentSlideIndex = this.getCurrentSlideIndex(); setTimeout(() => { // after removing, need to actualize index of current active slide this._currentActiveSlide = currentSlideIndex; this.activeSlideChange.emit(this._currentActiveSlide); }, 0); } } /** * Rolling to next slide * @param force: {boolean} if true - will ignore noWrap flag */ nextSlide(force = false): void { this.activeSlide = this.findNextSlideIndex(Direction.NEXT, force); } /** * Rolling to previous slide * @param force: {boolean} if true - will ignore noWrap flag */ previousSlide(force = false): void { this.activeSlide = this.findNextSlideIndex(Direction.PREV, force); } /** * Rolling to specified slide * @param index: {number} index of slide, which must be shown */ selectSlide(index: number): void { this.activeSlide = index; } /** * Starts a auto changing of slides */ play(): void { if (!this.isPlaying) { this.isPlaying = true; this.restartTimer(); } } /** * Stops a auto changing of slides */ pause(): void { if (!this.noPause) { this.isPlaying = false; this.resetTimer(); } } /** * Finds and returns index of currently displayed slide * @returns {number} */ getCurrentSlideIndex(): number { return this._slides.findIndex((slide: SlideComponent) => slide.active); } /** * Defines, whether the specified index is last in collection * @param index * @returns {boolean} */ isLast(index: number): boolean { return index + 1 >= this._slides.length; } /** * Defines next slide index, depending of direction * @param direction: Direction(UNKNOWN|PREV|NEXT) * @param force: {boolean} if TRUE - will ignore noWrap flag, else will * return undefined if next slide require wrapping * @returns {any} */ private findNextSlideIndex(direction: Direction, force: boolean): number { let nextSlideIndex = 0; if ( !force && (this.isLast(this.activeSlide) && direction !== Direction.PREV && this.noWrap) ) { return void 0; } switch (direction) { case Direction.NEXT: // if this is last slide, not force, looping is disabled // and need to going forward - select current slide, as a next nextSlideIndex = !this.isLast(this._currentActiveSlide) ? this._currentActiveSlide + 1 : !force && this.noWrap ? this._currentActiveSlide : 0; break; case Direction.PREV: // if this is first slide, not force, looping is disabled // and need to going backward - select current slide, as a next nextSlideIndex = this._currentActiveSlide > 0 ? this._currentActiveSlide - 1 : !force && this.noWrap ? this._currentActiveSlide : this._slides.length - 1; break; default: throw new Error('Unknown direction'); } return nextSlideIndex; } /** * Sets a slide, which specified through index, as active * @param index * @private */ private _select(index: number): void { if (isNaN(index)) { this.pause(); return; } const currentSlide = this._slides.get(this._currentActiveSlide); if (currentSlide) { currentSlide.active = false; } const nextSlide = this._slides.get(index); if (nextSlide) { this._currentActiveSlide = index; nextSlide.active = true; this.activeSlide = index; this.activeSlideChange.emit(index); } } /** * Starts loop of auto changing of slides */ private restartTimer(): any { this.resetTimer(); const interval = +this.interval; if (!isNaN(interval) && interval > 0) { this.currentInterval = this.ngZone.runOutsideAngular(() => { return setInterval(() => { const nInterval = +this.interval; this.ngZone.run(() => { if ( this.isPlaying && !isNaN(this.interval) && nInterval > 0 && this.slides.length ) { this.nextSlide(); } else { this.pause(); } }); }, interval); }); } } /** * Stops loop of auto changing of slides */ private resetTimer(): void { if (this.currentInterval) { clearInterval(this.currentInterval); this.currentInterval = void 0; } } }