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..ad933d0444 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 activeSlideIndex: 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 selectSlide(index: number): void {
+ this.activeSlideIndex = index;
+ }
+
+ public removeSlide(index?: number):void {
+ const toRemove = index ? index : this.activeSlideIndex;
+ this.slides.splice(toRemove, 1);
}
}
diff --git a/demo/src/ng-api-doc.ts b/demo/src/ng-api-doc.ts
index 61ede4ae23..20565c4119 100644
--- a/demo/src/ng-api-doc.ts
+++ b/demo/src/ng-api-doc.ts
@@ -186,6 +186,11 @@ export const ngdoc = {
"description": "
Base element to create carousel
\n",
"selector": "carousel",
"inputs": [
+ {
+ "name": "activeSlide",
+ "type": "number",
+ "description": "
Index of currently displayed slide(started for 0)
\n"
+ },
{
"name": "interval",
"type": "number",
@@ -197,44 +202,163 @@ export const ngdoc = {
"description": "
If true
— will disable pausing on carousel mouse hover
\n"
},
{
- "name": "noTransition",
+ "name": "noWrap",
"type": "boolean",
- "description": "
If true
— will disable carousel transitions
\n"
+ "description": "
If true
— carousel will not cycle continuously and will have hard stops (prevent looping)
\n"
+ }
+ ],
+ "outputs": [
+ {
+ "name": "activeSlideChange",
+ "description": "
Will be emitted when active slide has been changed. Part of two-way-bindable [(activeSlide)] property
\n"
+ }
+ ],
+ "properties": [
+ {
+ "name": "activeSlide",
+ "type": "number",
+ "description": "
Index of currently displayed slide(started for 0)
\n"
+ }
+ ],
+ "methods": [
+ {
+ "name": "addSlide",
+ "description": "
Adds new slide. If this slide is first in collection - set it as active and starts auto changing
\n",
+ "args": [
+ {
+ "name": "slide",
+ "type": "SlideComponent"
+ }
+ ],
+ "returnType": "void"
+ },
+ {
+ "name": "removeSlide",
+ "description": "
Removes specified slide. If this slide is active - will roll to another slide
\n",
+ "args": [
+ {
+ "name": "slide",
+ "type": "SlideComponent"
+ }
+ ],
+ "returnType": "void"
+ },
+ {
+ "name": "nextSlide",
+ "description": "
Rolling to next slide
\n",
+ "args": [
+ {
+ "name": "force",
+ "type": "boolean"
+ }
+ ],
+ "returnType": "void"
+ },
+ {
+ "name": "previousSlide",
+ "description": "
Rolling to previous slide
\n",
+ "args": [
+ {
+ "name": "force",
+ "type": "boolean"
+ }
+ ],
+ "returnType": "void"
+ },
+ {
+ "name": "selectSlide",
+ "description": "
Rolling to specified slide
\n",
+ "args": [
+ {
+ "name": "index",
+ "type": "number"
+ }
+ ],
+ "returnType": "void"
+ },
+ {
+ "name": "play",
+ "description": "
Starts a auto changing of slides
\n",
+ "args": [],
+ "returnType": "void"
+ },
+ {
+ "name": "pause",
+ "description": "
Stops a auto changing of slides
\n",
+ "args": [],
+ "returnType": "void"
+ },
+ {
+ "name": "getCurrentSlideIndex",
+ "description": "
Finds and returns index of currently displayed slide\n@returns {number}
\n",
+ "args": [],
+ "returnType": "number"
+ },
+ {
+ "name": "isLast",
+ "description": "
Defines, whether the specified index is last in collection\n@returns {boolean}
\n",
+ "args": [
+ {
+ "name": "index",
+ "type": "number"
+ }
+ ],
+ "returnType": "boolean"
+ }
+ ]
+ },
+ "CarouselConfig": {
+ "fileName": "src/carousel/carousel.config.ts",
+ "className": "CarouselConfig",
+ "description": "",
+ "methods": [],
+ "properties": [
+ {
+ "name": "interval",
+ "defaultValue": "5000",
+ "type": "number",
+ "description": "
Default interval of auto changing of slides
\n"
+ },
+ {
+ "name": "noPause",
+ "defaultValue": "false",
+ "type": "boolean",
+ "description": "
Is loop of auto changing of slides can be paused
\n"
},
{
"name": "noWrap",
+ "defaultValue": "false",
"type": "boolean",
- "description": "
If true
— carousel will not cycle continuously and will have hard stops (prevent looping)
\n"
+ "description": "
Is slides can wrap from the last to the first slide
\n"
}
- ],
- "outputs": [],
- "properties": [],
- "methods": []
+ ]
},
"SlideComponent": {
"fileName": "src/carousel/slide.component.ts",
"className": "SlideComponent",
- "description": "
Wrap your content with slide
component
\n",
+ "description": "",
"selector": "slide",
"inputs": [
{
"name": "active",
"type": "boolean",
"description": "
Is current slide active
\n"
- },
+ }
+ ],
+ "outputs": [],
+ "properties": [
{
- "name": "direction",
- "type": "Direction",
- "description": ""
+ "name": "addClass",
+ "defaultValue": "true",
+ "type": "boolean",
+ "description": "
Wraps element by appropriate CSS classes
\n"
},
{
- "name": "index",
- "type": "number",
- "description": "
Index of slide in carousel's slides
\n"
+ "name": "carousel",
+ "type": "CarouselComponent",
+ "description": "
Link to Parent(container-collection) component
\n"
}
],
- "outputs": [],
- "properties": [],
"methods": []
},
"CollapseDirective": {
@@ -252,11 +376,11 @@ export const ngdoc = {
"outputs": [
{
"name": "collapsed",
- "description": "
This event fired as soon as content is collapsed
\n"
+ "description": "
This event fires as soon as content collapses
\n"
},
{
"name": "expanded",
- "description": "
This event fired as soon as content becomes visible
\n"
+ "description": "
This event fires as soon as content becomes visible
\n"
}
],
"properties": [
diff --git a/package.json b/package.json
index ef8f79b0cb..50ce865b3d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ng2-bootstrap-base",
- "version": "1.1.16-9",
+ "version": "1.1.16-10",
"description": "Native Angular Bootstrap Components",
"private": true,
"scripts": {
diff --git a/src/carousel/carousel.component.ts b/src/carousel/carousel.component.ts
index 02f6473887..51a7cfcf79 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
@@ -44,153 +47,227 @@ export enum Direction {UNKNOWN, NEXT, PREV}
})
export class CarouselComponent implements OnDestroy {
/** If `true` — carousel will not cycle continuously and will have hard stops (prevent looping) */
- @Input() public noWrap:boolean;
+ @Input() public noWrap: boolean;
/** If `true` — will disable pausing on carousel mouse hover */
- @Input() public noPause:boolean;
- /** If `true` — will disable carousel transitions */
- @Input() public noTransition:boolean;
+ @Input() public noPause: boolean;
+
+ protected _currentActiveSlide: number;
+
+ /** Will be emitted when active slide has been changed. Part of two-way-bindable [(activeSlide)] property */
+ @Output() public activeSlideChange: EventEmitter
= new EventEmitter(false);
+
+ /** Index of currently displayed slide(started for 0) */
+ @Input()
+ public set activeSlide(index: number) {
+ if (this._slides.length && index !== this._currentActiveSlide) {
+ this._select(index);
+ }
+ }
+ public get activeSlide(): number {
+ return this._currentActiveSlide;
+ }
+
+ protected _interval: number;
/**
* Delay of item cycling in milliseconds. If false, carousel won't cycle automatically.
*/
@Input()
- public get interval():number {
+ public get interval(): number {
return this._interval;
}
-
- public set interval(value:number) {
+ public set interval(value: number) {
this._interval = value;
this.restartTimer();
}
- public slides:SlideComponent[] = [];
- protected currentInterval:any;
- protected isPlaying:boolean;
- protected destroyed:boolean = false;
- protected currentSlide:SlideComponent;
- protected _interval:number;
+ protected _slides: LinkedList = new LinkedList();
+ public get slides(): SlideComponent[] {
+ return this._slides.toArray();
+ }
+
+ protected currentInterval: any;
+ protected isPlaying: boolean;
+ protected destroyed: boolean = false;
public get isBs4():boolean {
return !isBs3();
}
- public ngOnDestroy():void {
+ public constructor(config: CarouselConfig) {
+ Object.assign(this, config);
+ }
+
+ public ngOnDestroy(): void {
this.destroyed = true;
}
- public select(nextSlide:SlideComponent, direction:Direction = Direction.UNKNOWN):void {
- let nextIndex = nextSlide.index;
- if (direction === Direction.UNKNOWN) {
- direction = nextIndex > this.getCurrentIndex()
- ? Direction.NEXT
- : Direction.PREV;
+ /**
+ * Adds new slide. If this slide is first in collection - set it as active and starts auto changing
+ * @param slide
+ */
+ public 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
+ */
+ public 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);
- // Prevent this user-triggered transition from occurring if there is
- // already one in progress
- if (nextSlide && nextSlide !== this.currentSlide) {
- this.goNext(nextSlide, direction);
}
}
- public play():void {
+ /**
+ * Rolling to next slide
+ * @param force: {boolean} if true - will ignore noWrap flag
+ */
+ public nextSlide(force: boolean = false): void {
+ this.activeSlide = this.findNextSlideIndex(Direction.NEXT, force);
+ }
+
+ /**
+ * Rolling to previous slide
+ * @param force: {boolean} if true - will ignore noWrap flag
+ */
+ public previousSlide(force: boolean = false): void {
+ this.activeSlide = this.findNextSlideIndex(Direction.PREV, force);
+ }
+
+ /**
+ * Rolling to specified slide
+ * @param index: {number} index of slide, which must be shown
+ */
+ public selectSlide(index: number): void {
+ this.activeSlide = index;
+ }
+
+ /**
+ * Starts a auto changing of slides
+ */
+ public play(): void {
if (!this.isPlaying) {
this.isPlaying = true;
this.restartTimer();
}
}
- public pause():void {
+ /**
+ * Stops a auto changing of slides
+ */
+ public pause(): void {
if (!this.noPause) {
this.isPlaying = false;
this.resetTimer();
}
}
- 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), Direction.NEXT);
- }
-
- 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), Direction.PREV);
+ /**
+ * Finds and returns index of currently displayed slide
+ * @returns {number}
+ */
+ 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(this.slides[this.slides.length - 1]);
- if (this.slides.length === 1) {
- this.play();
- }
- } else {
- slide.active = false;
- }
+ /**
+ * Defines, whether the specified index is last in collection
+ * @param index
+ * @returns {boolean}
+ */
+ public isLast(index: number): boolean {
+ return index + 1 >= this._slides.length;
}
- public removeSlide(slide:SlideComponent):void {
- this.slides.splice(slide.index, 1);
+ /**
+ * 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: number = 0;
- if (this.slides.length === 0) {
- this.currentSlide = void 0;
- return;
+ if (!force && (this.isLast(this.activeSlide) && direction !== Direction.PREV && this.noWrap)) {
+ return void 0;
}
- for (let i = 0; i < this.slides.length; i++) {
- this.slides[i].index = i;
+ 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;
}
- protected goNext(slide:SlideComponent, direction:Direction):void {
- if (this.destroyed) {
+ /**
+ * Sets a slide, which specified through index, as active
+ * @param index
+ * @private
+ */
+ private _select(index: number): void {
+ if (isNaN(index)) {
+ this.pause();
return;
}
-
- slide.direction = direction;
- slide.active = true;
-
- if (this.currentSlide) {
- this.currentSlide.direction = direction;
- this.currentSlide.active = false;
+ let currentSlide = this._slides.get(this._currentActiveSlide);
+ if (currentSlide) {
+ currentSlide.active = false;
}
-
- 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];
- }
+ let nextSlide = this._slides.get(index);
+ if (nextSlide) {
+ this._currentActiveSlide = index;
+ nextSlide.active = true;
+ this.activeSlide = index;
+ this.activeSlideChange.emit(index);
}
- return void 0;
- }
-
- protected getCurrentIndex():number {
- return !this.currentSlide ? 0 : this.currentSlide.index;
}
- protected restartTimer():any {
+ /**
+ * Starts loop of auto changing of slides
+ */
+ private restartTimer(): any {
this.resetTimer();
let interval = +this.interval;
if (!isNaN(interval) && interval > 0) {
@@ -198,7 +275,7 @@ export class CarouselComponent implements OnDestroy {
() => {
let nInterval = +this.interval;
if (this.isPlaying && !isNaN(this.interval) && nInterval > 0 && this.slides.length) {
- this.next();
+ this.nextSlide();
} else {
this.pause();
}
@@ -207,7 +284,10 @@ export class CarouselComponent implements OnDestroy {
}
}
- protected resetTimer():void {
+ /**
+ * Stops loop of auto changing of slides
+ */
+ 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..e6a8dafac4
--- /dev/null
+++ b/src/carousel/carousel.config.ts
@@ -0,0 +1,13 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class CarouselConfig {
+ /** Default interval of auto changing of slides */
+ public interval: number = 5000;
+
+ /** Is loop of auto changing of slides can be paused */
+ public noPause: boolean = false;
+
+ /** Is slides can wrap from the last to the first slide */
+ 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/carousel/slide.component.ts b/src/carousel/slide.component.ts
index 20489e86f5..f0659c640f 100644
--- a/src/carousel/slide.component.ts
+++ b/src/carousel/slide.component.ts
@@ -1,13 +1,7 @@
-/***
- * todo:
- * direction (?string) (not yet supported)
- actual (not yet supported) (?any) - will be bind to slider context, to be used from template
- */
-import { Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
+import { Component, HostBinding, OnDestroy, Input, OnInit } from '@angular/core';
-import { CarouselComponent, Direction } from './carousel.component';
+import { CarouselComponent } from './carousel.component';
-/** Wrap your content with `slide` component */
@Component({
selector: 'slide',
template: `
@@ -17,28 +11,29 @@ import { CarouselComponent, Direction } from './carousel.component';
`
})
export class SlideComponent implements OnInit, OnDestroy {
- /** Index of slide in carousel's slides */
- @Input() public index:number;
- @Input() public direction:Direction;
/** Is current slide active */
@HostBinding('class.active')
@Input() public active:boolean;
+ /** Wraps element by appropriate CSS classes */
@HostBinding('class.item')
@HostBinding('class.carousel-item')
public addClass:boolean = true;
+ /** Link to Parent(container-collection) component */
protected carousel:CarouselComponent;
public constructor(carousel:CarouselComponent) {
this.carousel = carousel;
}
+ /** Fires changes in container collection after adding a new slide instance */
public ngOnInit():void {
this.carousel.addSlide(this);
}
+ /** Fires changes in container collection after removing of this slide instance */
public ngOnDestroy():void {
this.carousel.removeSlide(this);
}
diff --git a/src/collapse/collapse.directive.ts b/src/collapse/collapse.directive.ts
index 579a797c7a..45e3b5cfa6 100644
--- a/src/collapse/collapse.directive.ts
+++ b/src/collapse/collapse.directive.ts
@@ -18,9 +18,9 @@ import {
]*/
})
export class CollapseDirective {
- /** This event fired as soon as content is collapsed */
+ /** This event fires as soon as content collapses */
@Output() public collapsed: EventEmitter = new EventEmitter();
- /** This event fired as soon as content becomes visible */
+ /** This event fires as soon as content becomes visible */
@Output() public expanded: EventEmitter = new EventEmitter();
@HostBinding('style.display')
diff --git a/src/package.json b/src/package.json
index 693013d458..bea7180555 100644
--- a/src/package.json
+++ b/src/package.json
@@ -1,6 +1,6 @@
{
"name": "ng2-bootstrap",
- "version": "1.1.16-9",
+ "version": "1.1.16-10",
"dependencies": {
"moment": "*"
},
diff --git a/src/positioning/ng-positioning.ts b/src/positioning/ng-positioning.ts
index c9cd5dc442..9fcf9a754c 100644
--- a/src/positioning/ng-positioning.ts
+++ b/src/positioning/ng-positioning.ts
@@ -83,8 +83,8 @@ export class Positioning {
bottom: hostElPosition.top + hostElPosition.height
};
const targetElBCR = targetElement.getBoundingClientRect();
- const placementPrimary = placement.split('-')[0] || 'top';
- const placementSecondary = placement.split('-')[1] || 'center';
+ const placementPrimary = placement.split(' ')[0] || 'top';
+ const placementSecondary = placement.split(' ')[1] || 'center';
let targetElPosition: ClientRect = {
height: targetElBCR.height || targetElement.offsetHeight,
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/carousel.spec.ts b/src/spec/ng-bootstrap/carousel.spec.ts
new file mode 100644
index 0000000000..71e4825f04
--- /dev/null
+++ b/src/spec/ng-bootstrap/carousel.spec.ts
@@ -0,0 +1,418 @@
+/* tslint:disable:max-classes-per-file max-file-line-count component-class-suffix */
+/**
+ * @copyright Angular ng-bootstrap team
+ */
+import { fakeAsync, discardPeriodicTasks, tick, TestBed, ComponentFixture, inject } from '@angular/core/testing';
+import { createGenericTestComponent } from './test/common';
+
+import { By } from '@angular/platform-browser';
+import { Component } from '@angular/core';
+
+import { CarouselModule, CarouselComponent, CarouselConfig } from '../../carousel';
+
+const createTestComponent = (html: string) =>
+ createGenericTestComponent(html, TestComponent) as ComponentFixture;
+
+function expectActiveSlides(nativeEl: HTMLDivElement, active: boolean[]): void {
+ const slideElms = nativeEl.querySelectorAll('.carousel-item');
+ const indicatorElms = nativeEl.querySelectorAll('ol.carousel-indicators > li');
+
+ expect(slideElms.length).toBe(active.length);
+ expect(indicatorElms.length).toBe(active.length);
+
+ for (let i = 0; i < active.length; i++) {
+ if (active[i]) {
+ expect(slideElms[i]).toHaveCssClass('active');
+ expect(indicatorElms[i]).toHaveCssClass('active');
+ } else {
+ expect(slideElms[i]).not.toHaveCssClass('active');
+ expect(indicatorElms[i]).not.toHaveCssClass('active');
+ }
+ }
+}
+
+describe('ngb-carousel', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({declarations: [TestComponent], imports: [CarouselModule.forRoot()]});
+ });
+
+ it('should initialize inputs with default values', () => {
+ const defaultConfig = new CarouselConfig();
+ const carousel = new CarouselComponent(new CarouselConfig());
+
+ expect(carousel.interval).toBe(defaultConfig.interval);
+ expect(carousel.noWrap).toBe(defaultConfig.noWrap);
+ // expect(carousel.keyboard).toBe(defaultConfig.keyboard);
+ });
+
+ it('should render slides and navigation indicators', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+ const fixture = createTestComponent(html);
+
+ const slideElms = fixture.nativeElement.querySelectorAll('.carousel-item');
+ expect(slideElms.length).toBe(2);
+ expect(slideElms[0].textContent).toMatch(/slide1/);
+ expect(slideElms[1].textContent).toMatch(/slide2/);
+
+ expect(fixture.nativeElement.querySelectorAll('ol.carousel-indicators > li').length).toBe(2);
+
+ discardPeriodicTasks();
+ }));
+
+ it('should mark the first slide as active by default', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ discardPeriodicTasks();
+ }));
+
+ it('should mark the requested slide as active', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+
+ fixture.componentInstance.activeSlideIndex = 1;
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+
+ discardPeriodicTasks();
+ }));
+
+ it('should auto-correct when slide index is undefined', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ discardPeriodicTasks();
+ }));
+
+ it('should change slide on indicator click', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+ const indicatorElms = fixture.nativeElement.querySelectorAll('ol.carousel-indicators > li');
+
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ indicatorElms[1].click();
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+
+ discardPeriodicTasks();
+ }));
+
+ it('should change slide on carousel control click', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+
+ const controlElms = fixture.nativeElement.querySelectorAll('.carousel-control');
+
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ controlElms[1].click(); // next
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+
+ controlElms[0].click(); // prev
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ discardPeriodicTasks();
+ }));
+
+ it('should change slide on time passage (default interval value)', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ tick(6000);
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+
+ discardPeriodicTasks();
+ }));
+
+ it('should change slide on time passage (custom interval value)', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ tick(1000);
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ tick(1200);
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+
+ discardPeriodicTasks();
+ }));
+
+ it('should not change slide on time passage (custom interval value is zero)', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ tick(1000);
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ tick(1200);
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ discardPeriodicTasks();
+ }));
+
+ it('should pause / resume slide change with time passage on mouse enter / leave', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+
+ const carouselDebugEl = fixture.debugElement.query(By.directive(CarouselComponent));
+
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ carouselDebugEl.children[0].triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ tick(6000);
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ carouselDebugEl.children[0].triggerEventHandler('mouseleave', {});
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ tick(6000);
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+ discardPeriodicTasks();
+ }));
+
+ it('should wrap slide changes by default', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+
+ const controlElms = fixture.nativeElement.querySelectorAll('.carousel-control');
+
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ controlElms[1].click(); // next
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+
+ controlElms[1].click(); // next
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ controlElms[0].click(); // prev
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+
+ discardPeriodicTasks();
+ }));
+
+ it('should not wrap slide changes by when requested', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+
+ const controlElms = fixture.nativeElement.querySelectorAll('.carousel-control');
+
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ controlElms[0].click(); // prev
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ controlElms[1].click(); // next
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+
+ controlElms[1].click(); // next
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+
+ discardPeriodicTasks();
+ }));
+
+ xit('should change on key arrowRight and arrowLeft', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ fixture.debugElement.query(By.directive(CarouselComponent)).triggerEventHandler('keydown.arrowRight', {}); // next()
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+
+ fixture.debugElement.query(By.directive(CarouselComponent)).triggerEventHandler('keydown.arrowLeft', {}); // prev()
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ fixture.componentInstance.keyboard = false;
+ fixture.detectChanges();
+ fixture.debugElement.query(By.directive(CarouselComponent)).triggerEventHandler('keydown.arrowRight', {}); // prev()
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ discardPeriodicTasks();
+
+ }));
+
+ xit('should listen to keyevents based on keyboard attribute', fakeAsync(() => {
+ const html = `
+
+ slide1
+ slide2
+
+ `;
+
+ const fixture = createTestComponent(html);
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ fixture.componentInstance.keyboard = false;
+ fixture.detectChanges();
+ fixture.debugElement.query(By.directive(CarouselComponent)).triggerEventHandler('keydown.arrowRight', {}); // prev()
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [true, false]);
+
+ fixture.componentInstance.keyboard = true;
+ fixture.detectChanges();
+ fixture.debugElement.query(By.directive(CarouselComponent)).triggerEventHandler('keydown.arrowRight', {}); // next()
+ fixture.detectChanges();
+ expectActiveSlides(fixture.nativeElement, [false, true]);
+
+ discardPeriodicTasks();
+
+ }));
+
+ describe('Custom config', () => {
+ let config: CarouselConfig;
+
+ beforeEach(() => { TestBed.configureTestingModule({imports: [CarouselModule.forRoot()]}); });
+
+ beforeEach(inject([CarouselConfig], (c: CarouselConfig) => {
+ config = c;
+ config.interval = 1000;
+ config.noWrap = true;
+ // config.keyboard = false;
+ }));
+
+ it('should initialize inputs with provided config', () => {
+ const fixture = TestBed.createComponent(CarouselComponent);
+ fixture.detectChanges();
+
+ const carousel = fixture.componentInstance;
+ expect(carousel.interval).toBe(config.interval);
+ expect(carousel.noWrap).toBe(config.noWrap);
+ // expect(carousel.keyboard).toBe(config.keyboard);
+ });
+ });
+
+ describe('Custom config as provider', () => {
+ const config = new CarouselConfig();
+ config.interval = 1000;
+ config.noWrap = true;
+ // config.keyboard = false;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule(
+ {imports: [CarouselModule.forRoot()], providers: [{provide: CarouselConfig, useValue: config}]});
+ });
+
+ it('should initialize inputs with provided config as provider', () => {
+ const fixture = TestBed.createComponent(CarouselComponent);
+ fixture.detectChanges();
+
+ const carousel = fixture.componentInstance;
+ expect(carousel.interval).toBe(config.interval);
+ expect(carousel.noWrap).toBe(config.noWrap);
+ // expect(carousel.keyboard).toBe(config.keyboard);
+ });
+ });
+
+});
+
+@Component({selector: 'test-cmp', template: ''})
+class TestComponent {
+ public activeSlideIndex: number;
+ // keyboard = true;
+}
diff --git a/src/spec/utils/linkedlist.spec.ts b/src/spec/utils/linkedlist.spec.ts
new file mode 100644
index 0000000000..84378b5bbb
--- /dev/null
+++ b/src/spec/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/tabs/tab.directive.ts b/src/tabs/tab.directive.ts
index 31947970d5..71926db536 100644
--- a/src/tabs/tab.directive.ts
+++ b/src/tabs/tab.directive.ts
@@ -1,28 +1,25 @@
-import {
- Directive, EventEmitter, HostBinding, Input, OnDestroy, Output, TemplateRef, OnInit
-} from '@angular/core';
-
+import { Directive, EventEmitter, HostBinding, Input, Output, TemplateRef, OnInit } from '@angular/core';
import { TabsetComponent } from './tabset.component';
@Directive({selector: 'tab, [tab]'})
-export class TabDirective implements OnDestroy, OnInit {
+export class TabDirective implements OnInit {
/** tab header text */
- @Input() public heading:string;
+ @Input() public heading: string;
/** if true tab can not be activated */
- @Input() public disabled:boolean;
+ @Input() public disabled: boolean;
/** if true tab can be removable, additional button will appear */
- @Input() public removable:boolean;
+ @Input() public removable: boolean;
/** if set, will be added to the tab's class atribute */
- @Input() public customClass:string;
+ @Input() public customClass: string;
/** tab active state toggle */
@HostBinding('class.active')
@Input()
- public get active():boolean {
+ public get active(): boolean {
return this._active;
}
- public set active(active:boolean) {
+ public set active(active: boolean) {
if (this.disabled && active || !active) {
if (!active) {
this._active = active;
@@ -34,7 +31,7 @@ export class TabDirective implements OnDestroy, OnInit {
this._active = active;
this.select.emit(this);
- this.tabset.tabs.forEach((tab:TabDirective) => {
+ this.tabset.tabs.forEach((tab: TabDirective) => {
if (tab !== this) {
tab.active = false;
}
@@ -42,28 +39,24 @@ export class TabDirective implements OnDestroy, OnInit {
}
/** fired when tab became active, $event:Tab equals to selected instance of Tab component */
- @Output() public select:EventEmitter = new EventEmitter();
+ @Output() public select: EventEmitter = new EventEmitter();
/** fired when tab became inactive, $event:Tab equals to deselected instance of Tab component */
- @Output() public deselect:EventEmitter = new EventEmitter();
+ @Output() public deselect: EventEmitter = new EventEmitter();
/** fired before tab will be removed */
- @Output() public removed:EventEmitter = new EventEmitter();
+ @Output() public removed: EventEmitter = new EventEmitter();
- @HostBinding('class.tab-pane') public addClass:boolean = true;
+ @HostBinding('class.tab-pane') public addClass: boolean = true;
- public headingRef:TemplateRef;
- public tabset:TabsetComponent;
- protected _active:boolean;
+ public headingRef: TemplateRef;
+ public tabset: TabsetComponent;
+ protected _active: boolean;
- public constructor(tabset:TabsetComponent) {
+ public constructor(tabset: TabsetComponent) {
this.tabset = tabset;
this.tabset.addTab(this);
}
- public ngOnInit():void {
- this.removable = !!this.removable;
- }
-
- public ngOnDestroy():void {
- this.tabset.removeTab(this);
+ public ngOnInit(): void {
+ this.removable = this.removable;
}
}
diff --git a/src/typeahead/typeahead-container.component.ts b/src/typeahead/typeahead-container.component.ts
index 9d5a826b9b..d92e54e7b6 100644
--- a/src/typeahead/typeahead-container.component.ts
+++ b/src/typeahead/typeahead-container.component.ts
@@ -60,7 +60,7 @@ import { TypeaheadMatch } from './typeahead-match.class';
`,
// tslint:disable-next-line
- host: {'[class]': '"dropdown open"'},
+ host: {'class': 'dropdown open', style: 'position: absolute;' },
encapsulation: ViewEncapsulation.None
})
export class TypeaheadContainerComponent {
diff --git a/src/typeahead/typeahead.directive.ts b/src/typeahead/typeahead.directive.ts
index 5cabc8a686..c8669a9a8a 100644
--- a/src/typeahead/typeahead.directive.ts
+++ b/src/typeahead/typeahead.directive.ts
@@ -217,8 +217,8 @@ export class TypeaheadDirective implements OnInit, OnDestroy {
this._typeahead
.attach(TypeaheadContainerComponent)
// todo: add append to body, after updating positioning service
- // .to(this.container)
- // .position({attachment: 'bottom left'})
+ .to(this.container)
+ .position({attachment: 'bottom left'})
.show({
typeaheadRef: this,
placement: this.placement,
diff --git a/src/utils/linked-list.class.ts b/src/utils/linked-list.class.ts
new file mode 100644
index 0000000000..6093df856c
--- /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) {
+ return void 0;
+ }
+
+ 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
+}