diff --git a/demo/src/app/components/carousel/demos/carousel-demo.component.ts b/demo/src/app/components/carousel/demos/carousel-demo.component.ts
index 028fd5c8b8..648209b7d3 100644
--- a/demo/src/app/components/carousel/demos/carousel-demo.component.ts
+++ b/demo/src/app/components/carousel/demos/carousel-demo.component.ts
@@ -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++) {
@@ -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);
}
}
diff --git a/src/carousel/carousel.component.ts b/src/carousel/carousel.component.ts
index baab643280..f9a92b9a1a 100644
--- a/src/carousel/carousel.component.ts
+++ b/src/carousel/carousel.component.ts
@@ -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}
@@ -28,14 +31,14 @@ export enum Direction {UNKNOWN, NEXT, PREV}
template: `
1">
-
+
-
+ 1">
Previous
-
+ 1">
Next
@@ -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
@@ -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
= new EventEmitter(false);
+
+ protected _slides: LinkedList = new LinkedList();
+ 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;
@@ -104,85 +143,64 @@ 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) {
@@ -190,7 +208,7 @@ export class CarouselComponent implements OnDestroy {
() => {
let nInterval = +this.interval;
if (this.isPlaying && !isNaN(this.interval) && nInterval > 0 && this.slides.length) {
- this.next();
+ this._select(Direction.NEXT);
} else {
this.pause();
}
@@ -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;
diff --git a/src/carousel/carousel.config.ts b/src/carousel/carousel.config.ts
new file mode 100644
index 0000000000..32e2702d10
--- /dev/null
+++ b/src/carousel/carousel.config.ts
@@ -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;
+}
diff --git a/src/carousel/carousel.module.ts b/src/carousel/carousel.module.ts
index 23fa0c7671..777217cd51 100644
--- a/src/carousel/carousel.module.ts
+++ b/src/carousel/carousel.module.ts
@@ -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 {
diff --git a/src/carousel/index.ts b/src/carousel/index.ts
index 476bb3b7a3..322baeaabb 100644
--- a/src/carousel/index.ts
+++ b/src/carousel/index.ts
@@ -1,3 +1,4 @@
export { CarouselComponent } from './carousel.component';
export { CarouselModule } from './carousel.module';
export { SlideComponent } from './slide.component';
+export { CarouselConfig } from './carousel.config';
diff --git a/src/spec/carousel.component.spec.ts b/src/spec/carousel.component.spec.ts
index b1a7e582a0..d88c970521 100644
--- a/src/spec/carousel.component.spec.ts
+++ b/src/spec/carousel.component.spec.ts
@@ -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);
@@ -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');
diff --git a/src/spec/ng-bootstrap/utils/linkedlist.spec.ts b/src/spec/ng-bootstrap/utils/linkedlist.spec.ts
new file mode 100644
index 0000000000..71af9f8bb8
--- /dev/null
+++ b/src/spec/ng-bootstrap/utils/linkedlist.spec.ts
@@ -0,0 +1,159 @@
+import LinkedList from '../../../utils/linked-list.class';
+
+let list: LinkedList;
+
+describe('Linked List. Base functions.', () => {
+
+ beforeEach(() => {
+
+ list = new LinkedList();
+ list.add('1');
+ list.add('2');
+ list.add('3');
+ list.add('4');
+ list.add('5');
+ });
+
+ it('List length must available', () => {
+ expect(list.length).toEqual(5);
+ });
+
+ it('Check get()', () => {
+ expect(list.length).toEqual(5);
+ expect(list.get(2)).toEqual('3');
+ });
+
+ it('Check toArray function', () => {
+ expect(list.toArray().join()).toEqual('1,2,3,4,5');
+ });
+
+ it('Check remove(0)', () => {
+ list.remove(0);
+ expect(list.toArray().join()).toEqual('2,3,4,5');
+ });
+
+ it('Check remove()', () => {
+ list.remove(0);
+ expect(list.toArray().join()).toEqual('2,3,4,5');
+ });
+
+ it('Check remove(2)', () => {
+ list.remove(2);
+ expect(list.toArray().join()).toEqual('1,2,4,5');
+ });
+
+ it('Check remove(4)', () => {
+ list.remove(4);
+ expect(list.toArray().join()).toEqual('1,2,3,4');
+ });
+
+ it('Check add(4, 2)', () => {
+ list.add(4, 2);
+ expect(list.toArray().join()).toEqual('1,2,4,3,4,5');
+ });
+
+ it('Check set(4, 2)', () => {
+ list.set(4, 2);
+ expect(list.toArray().join()).toEqual('1,2,3,4,2');
+ });
+});
+
+describe('Linked List. Overridden (from Array) methods.', () => {
+
+ beforeEach(() => {
+ list = new LinkedList();
+ list.add('1');
+ list.add('2');
+ list.add('3');
+ list.add('4');
+ list.add('5');
+ });
+ it('Check instance of Array', () => {
+ expect(list instanceof Array).toBeTruthy();
+ });
+
+ it('Check instance of indexOf', () => {
+ expect(list.indexOf('3')).toEqual(2);
+ });
+
+ it('Check push(6, 7, 8)', () => {
+ expect(list.push(6, 7, 8)).toEqual(8);
+ expect(list.toArray().join()).toEqual('1,2,3,4,5,6,7,8');
+ });
+
+ it('Check pop()', () => {
+ expect(list.pop()).toEqual('5');
+ expect(list.toArray().join()).toEqual('1,2,3,4');
+ });
+
+ it('Check unshift(a, b, c)', () => {
+ expect(list.unshift('a', 'b', 'c')).toEqual(8);
+ expect(list.toArray().join()).toEqual('a,b,c,1,2,3,4,5');
+ });
+
+ it('Check shift()', () => {
+ expect(list.shift()).toEqual('1');
+ expect(list.toArray().join()).toEqual('2,3,4,5');
+ });
+
+ it('Check forEach', () => {
+ list.forEach((item: string, index: number) => {
+ list.set(index, 'new_' + item);
+ });
+ expect(list.toArray().join()).toEqual('new_1,new_2,new_3,new_4,new_5');
+ });
+
+ it('Check toString', () => {
+ expect(list.toString()).toEqual('[Linked List]');
+ });
+
+ it('Positive check some()', () => {
+ expect(list.some((item: string) => item === '2'));
+ });
+
+ it('Negative check some()', () => {
+ expect(list.some((item: string) => item === '6')).toBeFalsy();
+ });
+
+ it('Negative check every()', () => {
+ expect(list.every((item: string) => item === '2')).toBeFalsy();
+ });
+
+ it('Positive check every()', () => {
+ expect(list.every((item: string) => item !== '0')).toBeTruthy();
+ });
+
+});
+
+describe('Linked List. Working with objects.', () => {
+
+ beforeEach(() => {
+ list = new LinkedList();
+ list.add({stringProperty: 'String1', numberProperty: 10});
+ list.add({stringProperty: 'String2', numberProperty: 20});
+ list.add({stringProperty: 'String3', numberProperty: 30});
+ list.add({stringProperty: 'String4', numberProperty: 40});
+ list.add({stringProperty: 'String5', numberProperty: 50});
+ });
+
+ it('Check find', () => {
+ let result: any = list.find((item: any) => item.numberProperty === 20);
+ expect(result.stringProperty).toEqual('String2');
+
+ result = list.find((item: any, index: number) => index === 2);
+ expect(result.stringProperty).toEqual('String3');
+ });
+
+ it('Check findIndex', () => {
+ let result: number = list.findIndex((item: any) => item.numberProperty === 20);
+ expect(result).toEqual(1);
+
+ result = list.findIndex((item: any, index: number) => index === 2);
+ expect(result).toEqual(2);
+ });
+
+ it('Check findAll', () => {
+ const result: any = list.findAll((item: any) => item.numberProperty > 20);
+ expect(result.length).toEqual(3);
+ });
+});
diff --git a/src/utils/linked-list.class.ts b/src/utils/linked-list.class.ts
new file mode 100644
index 0000000000..a63b5cddd6
--- /dev/null
+++ b/src/utils/linked-list.class.ts
@@ -0,0 +1,258 @@
+export default class LinkedList extends Array {
+
+ public length: number = 0;
+ protected head: any;
+ protected tail: any;
+ protected current: any;
+ protected asArray: T[] = [];
+
+ protected getNode(position: number): any {
+ if (this.length === 0 || position < 0 || position >= this.length) {
+ throw new Error('Position is out of the list');
+ }
+
+ let current = this.head;
+
+ for (let index = 0; index < position; index++) {
+ current = current.next;
+ }
+ return current;
+ }
+
+ protected createInternalArrayRepresentation(): void {
+ let outArray: any[] = [];
+ let current = this.head;
+
+ while (current) {
+ outArray.push(current.value);
+ current = current.next;
+ }
+ this.asArray = outArray;
+ }
+
+ public get(position: number): T {
+ if (this.length === 0 || position < 0 || position >= this.length) {
+ throw new Error('Position is out of the list');
+ }
+
+ let current = this.head;
+
+ for (let index = 0; index < position; index++) {
+ current = current.next;
+ }
+ return current.value;
+ }
+
+ public add(value: T, position: number = this.length): void {
+ if (position < 0 || position > this.length) {
+ throw new Error('Position is out of the list');
+ }
+
+ let node = {
+ value: value as any,
+ next: undefined as any,
+ previous: undefined as any
+ };
+
+ if (this.length === 0) {
+ this.head = node;
+ this.tail = node;
+ this.current = node;
+ } else {
+ if (position === 0) {
+ // first node
+ node.next = this.head;
+ this.head.previous = node;
+ this.head = node;
+ } else if (position === this.length) {
+ // last node
+ this.tail.next = node;
+ node.previous = this.tail;
+ this.tail = node;
+ } else {
+ // node in middle
+ let currentPreviousNode = this.getNode(position - 1);
+ let currentNextNode = currentPreviousNode.next;
+
+ currentPreviousNode.next = node;
+ currentNextNode.previous = node;
+
+ node.previous = currentPreviousNode;
+ node.next = currentNextNode;
+ }
+
+ }
+ this.length++;
+ this.createInternalArrayRepresentation();
+ }
+
+ public remove(position: number = 0): void {
+ if (this.length === 0 || position < 0 || position >= this.length) {
+ throw new Error('Position is out of the list');
+ }
+
+ if (position === 0) {
+ // first node
+ this.head = this.head.next;
+
+ if (this.head) {
+ // there is no second node
+ this.head.previous = undefined;
+ } else {
+ // there is no second node
+ this.tail = undefined;
+ }
+ } else if (position === this.length - 1) {
+ // last node
+ this.tail = this.tail.previous;
+ this.tail.next = undefined;
+ } else {
+ // middle node
+ let removedNode = this.getNode(position);
+ removedNode.next.previous = removedNode.previous;
+ removedNode.previous.next = removedNode.next;
+ }
+
+ this.length--;
+ this.createInternalArrayRepresentation();
+ }
+
+ public set(position: number, value: T): void {
+ if (this.length === 0 || position < 0 || position >= this.length) {
+ throw new Error('Position is out of the list');
+ }
+
+ let node = this.getNode(position);
+ node.value = value;
+ this.createInternalArrayRepresentation();
+ }
+
+ public toArray(): T[] {
+ return this.asArray;
+ }
+
+ public findAll(fn: any): any[] {
+ let current = this.head;
+ let result: any[] = [];
+ for (let index = 0; index < this.length; index++) {
+ if (fn(current.value, index)) {
+ result.push({index, value: current.value});
+ }
+ current = current.next;
+ }
+ return result;
+ }
+ // Array methods overriding start
+ public push(...args: T[]): number {
+ args.forEach((arg: any) => {
+ this.add(arg);
+ });
+ return this.length;
+ }
+
+ public pop(): T {
+ if (this.length === 0) {
+ return undefined;
+ }
+ const last = this.tail;
+ this.remove(this.length - 1);
+ return last.value;
+ }
+
+ public unshift(...args: T[]): number {
+ args.reverse();
+ args.forEach((arg: any) => {
+ this.add(arg, 0);
+ });
+ return this.length;
+ }
+
+ public shift(): T {
+ if (this.length === 0) {
+ return undefined;
+ }
+ const lastItem = this.head.value;
+ this.remove();
+ return lastItem;
+ }
+
+ public forEach(fn: any): void {
+ let current = this.head;
+ for (let index = 0; index < this.length; index++) {
+ fn(current.value, index);
+ current = current.next;
+ }
+ }
+
+ public indexOf(value: T): number {
+ let current = this.head;
+ let position = 0;
+
+ for (let index = 0; index < this.length; index++) {
+ if (current.value === value) {
+
+ position = index;
+ break;
+ }
+ current = current.next;
+ }
+ return position;
+ }
+
+ public some(fn: any): boolean {
+ let current = this.head;
+ let result = false;
+ while (current && !result) {
+ if (fn(current.value)) {
+ result = true;
+ break;
+ }
+ current = current.next;
+ }
+ return result;
+ }
+
+ public every(fn: any): boolean {
+ let current = this.head;
+ let result = true;
+ while (current && result) {
+ if (!fn(current.value)) {
+ result = false;
+ }
+ current = current.next;
+ }
+ return result;
+ }
+
+ public toString(): string {
+ return '[Linked List]';
+ }
+
+ public find(fn: any): T {
+ let current = this.head;
+ let result: T;
+ for (let index = 0; index < this.length; index++) {
+ if (fn(current.value, index)) {
+ result = current.value;
+ break;
+ }
+ current = current.next;
+ }
+ return result;
+ }
+
+ public findIndex(fn: any): number {
+ let current = this.head;
+ let result: number;
+ for (let index = 0; index < this.length; index++) {
+ if (fn(current.value, index)) {
+ result = index;
+ break;
+ }
+ current = current.next;
+ }
+ return result;
+ }
+
+ // Array methods overriding END
+}