Skip to content

Commit

Permalink
feat(carousel): Changed data structure to linked list.
Browse files Browse the repository at this point in the history
  • Loading branch information
macroorganizm committed Dec 21, 2016
1 parent 76d6558 commit 35102e6
Show file tree
Hide file tree
Showing 9 changed files with 552 additions and 100 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<div class="row">
<div class="col-md-4">
<carousel [interval]="myInterval" [noWrap]="noWrapSlides">
<slide *ngFor="let slidez of slides; let index=index"
[active]="slidez.active">
<carousel [interval]="myInterval" [noWrap]="noWrapSlides" (activeSlideChanged)="activeSlideChanged($event)">
<slide *ngFor="let slidez of slides; let index=index">
<img [src]="slidez.image" style="margin:auto;">

<div class="carousel-caption">
Expand All @@ -18,10 +17,13 @@ <h4>Slide {{index}}</h4>
<button type="button" class="btn btn-info"
(click)="addSlide()">Add Slide
</button>
<!-- issue: removing currently active slide should not lead to white screen? -->
<!--<button type="button" class="btn btn-info"-->
<!--(click)="removeSlide(2)">Remove Slide#3-->
<!--</button>-->

<button type="button" class="btn btn-info"
(click)="removeSlide()">Remove Current Slide
</button>
<button type="button" class="btn btn-info"
(click)="removeSlide(2)">Remove Slide#3
</button>
<br>

<div class="checkbox">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class CarouselDemoComponent {
public myInterval:number = 5000;
public noWrapSlides:boolean = false;
public slides:any[] = [];
public currentSlideIndex: number;

public constructor() {
for (let i = 0; i < 4; i++) {
Expand All @@ -24,7 +25,12 @@ export class CarouselDemoComponent {
});
}

public removeSlide(index:number):void {
this.slides.splice(index, 1);
public activeSlideChanged(index: number): void {
this.currentSlideIndex = index;
}

public removeSlide(index?: number):void {
const toRemove = index ? index : this.currentSlideIndex;
this.slides.splice(toRemove, 1);
}
}
184 changes: 101 additions & 83 deletions src/carousel/carousel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
* 4) default interval should be equal 5000
*/

import { Component, Input, OnDestroy } from '@angular/core';
import { Component, Input, OnDestroy, Output, EventEmitter } from '@angular/core';

import { isBs3 } from '../utils/ng2-bootstrap-config';
import { SlideComponent } from './slide.component';
import { CarouselConfig } from './carousel.config';

import LinkedList from './../utils/linked-list.class';

export enum Direction {UNKNOWN, NEXT, PREV}

Expand All @@ -28,14 +31,14 @@ export enum Direction {UNKNOWN, NEXT, PREV}
template: `
<div (mouseenter)="pause()" (mouseleave)="play()" class="carousel slide">
<ol class="carousel-indicators" *ngIf="slides.length > 1">
<li *ngFor="let slidez of slides" [class.active]="slidez.active === true" (click)="select(slidez)"></li>
<li *ngFor="let slidez of slides; let i = index;" [class.active]="slidez.active === true" (click)="selectSlide(i)"></li>
</ol>
<div class="carousel-inner"><ng-content></ng-content></div>
<a class="left carousel-control" (click)="prev()" *ngIf="slides.length && (currentSlide?.hasPreviousSibling || currentSlide.index === 0)">
<a class="left carousel-control" [class.disabled]="getCurrentSlideIndex() === 0 && noWrap" (click)="previousSlide()" *ngIf="slides.length > 1">
<span class="icon-prev" aria-hidden="true"></span>
<span *ngIf="isBs4" class="sr-only">Previous</span>
</a>
<a class="right carousel-control" (click)="next()" *ngIf="slides.length && (currentSlide?.hasNextSibling || currentSlide.index === slides.length - 1)">
<a class="right carousel-control" (click)="nextSlide()" [class.disabled]="isLast(getCurrentSlideIndex()) && noWrap" *ngIf="slides.length > 1">
<span class="icon-next" aria-hidden="true"></span>
<span *ngIf="isBs4" class="sr-only">Next</span>
</a>
Expand All @@ -47,8 +50,6 @@ export class CarouselComponent implements OnDestroy {
@Input() public noWrap:boolean;
/** if `true` will disable pausing on carousel mouse hover */
@Input() public noPause:boolean;
/** if `true` will disable transitions on the carousel */
@Input() public noTransition:boolean;

/**
* Amount of time in milliseconds to delay between automatically
Expand All @@ -58,38 +59,76 @@ export class CarouselComponent implements OnDestroy {
public get interval():number {
return this._interval;
}

public set interval(value:number) {
this._interval = value;
this.restartTimer();
}

public slides:SlideComponent[] = [];
@Output() public activeSlideChanged: EventEmitter <any> = new EventEmitter<any>(false);

protected _slides: LinkedList<SlideComponent> = new LinkedList<SlideComponent>();
public get slides(): SlideComponent[] {
return this._slides.toArray();
}

protected currentInterval:any;
protected isPlaying:boolean;
protected destroyed:boolean = false;
protected currentSlide:SlideComponent;
protected _interval:number = 5000;
private _lastAddedSlide:SlideComponent;
protected _interval:number;

public get isBs4():boolean {
return !isBs3();
}

public constructor(config: CarouselConfig) {
Object.assign(this, config);
}

public ngOnDestroy():void {
this.destroyed = true;
}

public select(nextSlide: SlideComponent):void {
let nextIndex = nextSlide.index;
let direction = nextIndex > this.getCurrentIndex() ? Direction.NEXT : Direction.PREV;
// Prevent this user-triggered transition from occurring if there is
// already one in progress
if (nextSlide && nextSlide !== this.currentSlide) {
this.goNext(nextSlide, direction);
public addSlide(slide: SlideComponent): void {
this._slides.add(slide);
if (this._slides.length === 1) {
slide.active = true;
this.play();
}
}

public removeSlide(slide: SlideComponent): void {
const remIndex = this._slides.indexOf(slide);

if (this.getCurrentSlideIndex() === remIndex) {

// behavior in case removing of a current active slide
if (this._slides.length > 1) {
if (this.isLast(remIndex) && this.noWrap) {

// last slide and looping is disabled - step backward
this._select(Direction.PREV, undefined, true);
} else {
this._select(Direction.NEXT, undefined, true);
}
}
}

this._slides.remove(remIndex);
this.activeSlideChanged.emit(this.getCurrentSlideIndex());
}

public nextSlide(): void {
this._select(Direction.NEXT);
}

public previousSlide(): void {
this._select(Direction.PREV);
}

public selectSlide(index: number): void {
this._select(undefined, index, true);
}

public play():void {
if (!this.isPlaying) {
this.isPlaying = true;
Expand All @@ -104,93 +143,72 @@ export class CarouselComponent implements OnDestroy {
}
}

public next():any {
let newIndex = (this.getCurrentIndex() + 1) % this.slides.length;

if (newIndex === 0 && this.noWrap) {
this.pause();
return;
}

return this.select(this.getSlideByIndex(newIndex));
}

public prev():any {
let newIndex = this.getCurrentIndex() - 1 < 0
? this.slides.length - 1
: this.getCurrentIndex() - 1;

if (this.noWrap && newIndex === this.slides.length - 1) {
this.pause();
return;
}

return this.select(this.getSlideByIndex(newIndex));
public getCurrentSlideIndex(): number {
return this._slides.findIndex((slide: SlideComponent) => slide.active);
}

public addSlide(slide:SlideComponent):void {
slide.index = this.slides.length;
this.slides.push(slide);
if (this.slides.length === 1 || slide.active) {
this.select(slide);
this.play();
}
slide.previousSiblingSlide = this._lastAddedSlide;
if (this._lastAddedSlide) {
this._lastAddedSlide.nextSiblingSlide = slide;
}
this._lastAddedSlide = slide;
public isLast(index: number): boolean {
return index + 1 >= this._slides.length;
}

public removeSlide(slide:SlideComponent):void {
this.slides.splice(slide.index, 1);
/**
* Select slide
* @param direction: {Direction}
* @param nextIndex: {number}(optional) - index of next active slide
* @param force: boolean {optional} - if true, selection will ignore this.noWrap flag(for jumping after removing a current slide)
* @private
*/
private _select(direction: Direction = 0, nextIndex?: number, force: boolean = false): void {
const currentSlideIndex = this.getCurrentSlideIndex();

if (this.slides.length === 0) {
this.currentSlide = void 0;
// if this is last slide, need to going forward but looping is disabled
if (!force && (this.isLast(currentSlideIndex) && direction && direction !== Direction.PREV && this.noWrap)) {
this.pause();
return;
}

for (let i = 0; i < this.slides.length; i++) {
this.slides[i].index = i;
let currentSlide = this._slides.get(currentSlideIndex);
let nextSlideIndex: number = !isNaN(nextIndex) ? nextIndex : undefined;

if (direction !== undefined && direction !== Direction.UNKNOWN) {
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(currentSlideIndex)) ? currentSlideIndex + 1 :
(!force && this.noWrap ) ? currentSlideIndex : 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 = (currentSlideIndex > 0) ? currentSlideIndex - 1 :
(!force && this.noWrap ) ? currentSlideIndex : this._slides.length - 1;
break;
default:
throw new Error('Wrong direction');
}
}
}

protected goNext(slide:SlideComponent, direction:Direction):void {
if (this.destroyed) {
if (nextSlideIndex === currentSlideIndex) {
return;
}

slide.direction = direction;
slide.active = true;
this.currentSlide = slide;

// every time you change slides, reset the timer
this.restartTimer();
}

protected getSlideByIndex(index:number):any {
let len = this.slides.length;
for (let i = 0; i < len; ++i) {
if (this.slides[i].index === index) {
return this.slides[i];
}
}
return void 0;
}
let nextSlide = this._slides.get(nextSlideIndex);
currentSlide.active = false;
nextSlide.active = true;
this.activeSlideChanged.emit(nextSlideIndex);

protected getCurrentIndex():number {
return !this.currentSlide ? 0 : this.currentSlide.index;
}

protected restartTimer():any {
private restartTimer():any {
this.resetTimer();
let interval = +this.interval;
if (!isNaN(interval) && interval > 0) {
this.currentInterval = setInterval(
() => {
let nInterval = +this.interval;
if (this.isPlaying && !isNaN(this.interval) && nInterval > 0 && this.slides.length) {
this.next();
this._select(Direction.NEXT);
} else {
this.pause();
}
Expand All @@ -199,7 +217,7 @@ export class CarouselComponent implements OnDestroy {
}
}

protected resetTimer():void {
private resetTimer():void {
if (this.currentInterval) {
clearInterval(this.currentInterval);
this.currentInterval = void 0;
Expand Down
8 changes: 8 additions & 0 deletions src/carousel/carousel.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Injectable } from '@angular/core';

@Injectable()
export class CarouselConfig {
public interval: number = 5000;
public noPause: boolean = false;
public noWrap: boolean = false;
}
4 changes: 3 additions & 1 deletion src/carousel/carousel.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { NgModule, ModuleWithProviders } from '@angular/core';

import { CarouselComponent } from './carousel.component';
import { SlideComponent } from './slide.component';
import { CarouselConfig } from './carousel.config';

@NgModule({
imports: [CommonModule],
declarations: [SlideComponent, CarouselComponent],
exports: [SlideComponent, CarouselComponent]
exports: [SlideComponent, CarouselComponent],
providers: [CarouselConfig]
})
export class CarouselModule {
public static forRoot(): ModuleWithProviders {
Expand Down
1 change: 1 addition & 0 deletions src/carousel/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { CarouselComponent } from './carousel.component';
export { CarouselModule } from './carousel.module';
export { SlideComponent } from './slide.component';
export { CarouselConfig } from './carousel.config';
12 changes: 5 additions & 7 deletions src/spec/carousel.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,7 @@ describe('Component: Carousel', () => {
expect(indicators.length).toBe(3);
});

// TODO:
xit('should hide navigation when only one slide', () => {
it('should hide navigation when only one slide', () => {
context.slides.splice(0, 2);
fixture.detectChanges();
expect(context.slides.length).toBe(1);
Expand All @@ -112,18 +111,17 @@ describe('Component: Carousel', () => {
expect(next.length).toBe(0);
});

// TODO:
xit('should disable prev button when slide index is 0 and noWrap is truthy', () => {
it('should disable prev button when slide index is 0 and noWrap is truthy', () => {
context.noWrapSlides = true;
fixture.detectChanges();
let prev = element.querySelector('a.left');
expect(prev.classList).toContain('disabled');
});

// TODO:
xit('should disable next button when last slide is active and noWrap is truthy', () => {
it('should disable next button when last slide is active and noWrap is truthy', () => {
context.noWrapSlides = true;
context.slides[2].active = true;
let indicators = element.querySelectorAll('ol.carousel-indicators > li');
indicators[2].click();
fixture.detectChanges();
let next = element.querySelector('a.right');
expect(next.classList).toContain('disabled');
Expand Down
Loading

0 comments on commit 35102e6

Please sign in to comment.