From ca7c0f06d3e0569f7c836fc1029dbfba30b345df Mon Sep 17 00:00:00 2001 From: Wendell Date: Thu, 18 Apr 2019 15:33:35 +0800 Subject: [PATCH] refactor(module:carousel): refactor (#3062) * refactor(module:carousel): refactor * fix: move some listeners to body * fix: fix vertical context preparing dragging from first to last * fix: opacity strategy transition * fix(module:breadcrumb): add input boolean * feat: support SSR * fix: lint * fix: fix active index not reset when content changes close #2468, close #2218 --- angular.json | 3 - .../breadcrumb/nz-breadcrumb.component.ts | 4 +- components/carousel/doc/index.en-US.md | 2 +- components/carousel/doc/index.zh-CN.md | 2 +- .../carousel/nz-carousel-content.directive.ts | 72 +--- .../carousel/nz-carousel-definitions.ts | 24 ++ .../carousel/nz-carousel.component.html | 21 +- components/carousel/nz-carousel.component.ts | 338 +++++++++--------- components/carousel/nz-carousel.spec.ts | 263 ++++++++++---- .../carousel/strategies/base-strategy.ts | 73 ++++ .../carousel/strategies/opacity-strategy.ts | 48 +++ .../carousel/strategies/transform-strategy.ts | 166 +++++++++ components/core/util/dom.ts | 12 + package.json | 2 - 14 files changed, 695 insertions(+), 335 deletions(-) create mode 100644 components/carousel/nz-carousel-definitions.ts create mode 100644 components/carousel/strategies/base-strategy.ts create mode 100644 components/carousel/strategies/opacity-strategy.ts create mode 100644 components/carousel/strategies/transform-strategy.ts diff --git a/angular.json b/angular.json index 2d48431d8ff..4514530a098 100644 --- a/angular.json +++ b/angular.json @@ -30,9 +30,6 @@ "styles": [ "site/doc/styles.less" ], - "scripts": [ - "node_modules/hammerjs/hammer.min.js" - ], "es5BrowserSupport": true }, "configurations": { diff --git a/components/breadcrumb/nz-breadcrumb.component.ts b/components/breadcrumb/nz-breadcrumb.component.ts index 767c3f9146d..8bab4a34c94 100755 --- a/components/breadcrumb/nz-breadcrumb.component.ts +++ b/components/breadcrumb/nz-breadcrumb.component.ts @@ -16,6 +16,8 @@ import { ActivatedRoute, Params, PRIMARY_OUTLET, Router } from '@angular/router' import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { InputBoolean } from '../core/util'; + export const NZ_ROUTE_DATA_BREADCRUMB = 'breadcrumb'; export interface BreadcrumbOption { @@ -39,7 +41,7 @@ export interface BreadcrumbOption { ] }) export class NzBreadCrumbComponent implements OnInit, OnDestroy { - @Input() nzAutoGenerate = false; + @Input() @InputBoolean() nzAutoGenerate = false; @Input() nzSeparator: string | TemplateRef = '/'; breadcrumbs: BreadcrumbOption[] | undefined = []; diff --git a/components/carousel/doc/index.en-US.md b/components/carousel/doc/index.en-US.md index 265223a1f9a..e656c3f7395 100644 --- a/components/carousel/doc/index.en-US.md +++ b/components/carousel/doc/index.en-US.md @@ -26,7 +26,7 @@ A carousel component. Scales with its container. | `[nzVertical]` | Whether to use a vertical display | `boolean` | `false` | | `(nzAfterChange)` | Callback function called after the current index changes | `EventEmitter` | - | | `(nzBeforeChange)` | Callback function called before the current index changes | `EventEmitter{ from: number; to: number }>` | - | -| `[nzEnableSwipe]` | Whether to support swipe gesture (would work if only you import hammer.js in your project) | `boolean` | `true` | +| `[nzEnableSwipe]` | Whether to support swipe gesture | `boolean` | `true` | #### Methods diff --git a/components/carousel/doc/index.zh-CN.md b/components/carousel/doc/index.zh-CN.md index 94df14d2a85..6cde2a1c3c7 100644 --- a/components/carousel/doc/index.zh-CN.md +++ b/components/carousel/doc/index.zh-CN.md @@ -27,7 +27,7 @@ subtitle: 走马灯 | `[nzVertical]` | 垂直显示 | `boolean` | `false` | | `(nzAfterChange)` | 切换面板的回调 | `EventEmitter` | - | | `(nzBeforeChange)` | 切换面板的回调 | `EventEmitter<{ from: number; to: number }>` | - | -| `[nzEnableSwipe]` | 是否支持手势划动切换,仅在自行引入 hammer.js 的情形下生效 | `boolean` | `true` | +| `[nzEnableSwipe]` | 是否支持手势划动切换 | `boolean` | `true` | #### 方法 | 名称 | 描述 | diff --git a/components/carousel/nz-carousel-content.directive.ts b/components/carousel/nz-carousel-content.directive.ts index 9c191c3cd7e..cbcf958caf9 100755 --- a/components/carousel/nz-carousel-content.directive.ts +++ b/components/carousel/nz-carousel-content.directive.ts @@ -1,57 +1,13 @@ -import { Directive, ElementRef, OnInit, Renderer2 } from '@angular/core'; - -import { isNotNil } from '../core/util/check'; +import { Directive, ElementRef, Renderer2 } from '@angular/core'; @Directive({ selector: '[nz-carousel-content]' }) -export class NzCarouselContentDirective implements OnInit { +export class NzCarouselContentDirective { el: HTMLElement = this.elementRef.nativeElement; - private _active = false; - private _width: number = 0; - private _left: number | null; - private _top: number | null; - private _fadeMode = false; - - set width(value: number) { - this._width = value; - this.renderer.setStyle(this.el, 'width', `${this.width}px`); - } - - get width(): number { - return this._width; - } - - set left(value: number | null) { - this._left = value; - if (isNotNil(this.left)) { - this.renderer.setStyle(this.el, 'left', `${this.left}px`); - } else { - this.renderer.removeStyle(this.el, 'left'); - } - } - - get left(): number | null { - return this._left; - } - - set top(value: number | null) { - this._top = value; - if (isNotNil(this.top)) { - this.renderer.setStyle(this.el, 'top', `${this.top}px`); - } else { - this.renderer.removeStyle(this.el, 'top'); - } - } - - get top(): number | null { - return this._top; - } - set isActive(value: boolean) { this._active = value; - this.updateOpacity(); if (this.isActive) { this.renderer.addClass(this.el, 'slick-active'); } else { @@ -63,31 +19,9 @@ export class NzCarouselContentDirective implements OnInit { return this._active; } - set fadeMode(value: boolean) { - this._fadeMode = value; - if (this.fadeMode) { - this.renderer.setStyle(this.el, 'position', 'relative'); - } else { - this.renderer.removeStyle(this.el, 'position'); - } - this.updateOpacity(); - } - - get fadeMode(): boolean { - return this._fadeMode; - } + private _active = false; constructor(private elementRef: ElementRef, private renderer: Renderer2) { renderer.addClass(elementRef.nativeElement, 'slick-slide'); } - - ngOnInit(): void { - this.renderer.setStyle(this.el, 'transition', 'opacity 500ms ease'); - } - - private updateOpacity(): void { - if (this.fadeMode) { - this.renderer.setStyle(this.el, 'opacity', this.isActive ? 1 : 0); - } - } } diff --git a/components/carousel/nz-carousel-definitions.ts b/components/carousel/nz-carousel-definitions.ts new file mode 100644 index 00000000000..5acf223e4f5 --- /dev/null +++ b/components/carousel/nz-carousel-definitions.ts @@ -0,0 +1,24 @@ +import { QueryList } from '@angular/core'; +import { NzCarouselContentDirective } from './nz-carousel-content.directive'; + +export type NzCarouselEffects = 'fade' | 'scrollx'; + +export interface NzCarouselComponentAsSource { + carouselContents: QueryList; + el: HTMLElement; + nzTransitionSpeed: number; + nzVertical: boolean; + slickListEl: HTMLElement; + slickTrackEl: HTMLElement; + activeIndex: number; +} + +export interface PointerVector { + x: number; + y: number; +} + +export interface FromToInterface { + from: number; + to: number; +} diff --git a/components/carousel/nz-carousel.component.html b/components/carousel/nz-carousel.component.html index 7ec643c66f1..f00c2e4199e 100755 --- a/components/carousel/nz-carousel.component.html +++ b/components/carousel/nz-carousel.component.html @@ -1,23 +1,30 @@
+ (mousedown)="pointerDown($event)" + (touchstart)="pointerDown($event)" + > +
+
    -
  • - +
  • + +
- + diff --git a/components/carousel/nz-carousel.component.ts b/components/carousel/nz-carousel.component.ts index 0e358cfb1ef..6bb4fee2f49 100755 --- a/components/carousel/nz-carousel.component.ts +++ b/components/carousel/nz-carousel.component.ts @@ -1,4 +1,6 @@ import { LEFT_ARROW, RIGHT_ARROW } from '@angular/cdk/keycodes'; +import { Platform } from '@angular/cdk/platform'; +import { DOCUMENT } from '@angular/common'; import { AfterContentInit, AfterViewInit, @@ -8,6 +10,7 @@ import { ContentChildren, ElementRef, EventEmitter, + Inject, Input, NgZone, OnChanges, @@ -20,14 +23,17 @@ import { ViewChild, ViewEncapsulation } from '@angular/core'; -import { fromEvent, Subscription } from 'rxjs'; -import { debounceTime } from 'rxjs/operators'; -import { InputBoolean, InputNumber } from '../core/util/convert'; -import { NzCarouselContentDirective } from './nz-carousel-content.directive'; +import { fromEvent, Subject } from 'rxjs'; +import { take, takeUntil, throttleTime } from 'rxjs/operators'; -export type NzCarouselEffects = 'fade' | 'scrollx'; +import { InputBoolean, InputNumber } from '../core/util/convert'; +import { isTouchEvent } from '../core/util/dom'; -export type SwipeDirection = 'swipeleft' | 'swiperight'; +import { NzCarouselContentDirective } from './nz-carousel-content.directive'; +import { FromToInterface, NzCarouselEffects, PointerVector } from './nz-carousel-definitions'; +import { NzCarouselBaseStrategy } from './strategies/base-strategy'; +import { NzCarouselOpacityStrategy } from './strategies/opacity-strategy'; +import { NzCarouselTransformStrategy } from './strategies/transform-strategy'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -54,249 +60,233 @@ export type SwipeDirection = 'swipeleft' | 'swiperight'; .slick-track { opacity: 1; - transition: all 0.5s ease; - } - - .slick-slide { - transition: opacity 500ms ease; } ` ] }) -export class NzCarouselComponent implements AfterViewInit, AfterContentInit, OnDestroy, OnChanges { - @ContentChildren(NzCarouselContentDirective) slideContents: QueryList; +export class NzCarouselComponent implements AfterContentInit, AfterViewInit, OnDestroy, OnChanges { + @ContentChildren(NzCarouselContentDirective) carouselContents: QueryList; + @ViewChild('slickList') slickList: ElementRef; @ViewChild('slickTrack') slickTrack: ElementRef; - @Input() nzTransitionSpeed = 500; // Not exposed. @Input() nzDotRender: TemplateRef<{ $implicit: number }>; @Input() nzEffect: NzCarouselEffects = 'scrollx'; @Input() @InputBoolean() nzEnableSwipe = true; @Input() @InputBoolean() nzDots: boolean = true; @Input() @InputBoolean() nzVertical: boolean = false; @Input() @InputBoolean() nzAutoPlay = false; - @Input() @InputNumber() nzAutoPlaySpeed = 3000; // Should be nzAutoPlayDuration, but changing this is breaking. + @Input() @InputNumber() nzAutoPlaySpeed = 3000; + @Input() @InputNumber() nzTransitionSpeed = 500; - @Output() readonly nzAfterChange: EventEmitter = new EventEmitter(); - @Output() readonly nzBeforeChange: EventEmitter<{ from: number; to: number }> = new EventEmitter(); + @Output() readonly nzBeforeChange = new EventEmitter(); + @Output() readonly nzAfterChange = new EventEmitter(); activeIndex = 0; - transform = 'translate3d(0px, 0px, 0px)'; - transitionAction: number | null; - - private el = this.elementRef.nativeElement; - private subs_ = new Subscription(); + el: HTMLElement; + slickListEl: HTMLElement; + slickTrackEl: HTMLElement; + strategy: NzCarouselBaseStrategy; + transitionInProgress: number | null; - get nextIndex(): number { - return this.activeIndex < this.slideContents.length - 1 ? this.activeIndex + 1 : 0; - } - - get prevIndex(): number { - return this.activeIndex > 0 ? this.activeIndex - 1 : this.slideContents.length - 1; - } + private destroy$ = new Subject(); + private document: Document; + private gestureRect: ClientRect | null = null; + private pointerDelta: PointerVector | null = null; + private pointerPosition: PointerVector | null = null; + private isTransiting = false; + private isDragging = false; constructor( - public elementRef: ElementRef, + elementRef: ElementRef, + @Inject(DOCUMENT) document: any, // tslint:disable-line:no-any private renderer: Renderer2, private cdr: ChangeDetectorRef, - private ngZone: NgZone + private ngZone: NgZone, + private platform: Platform ) { - renderer.addClass(elementRef.nativeElement, 'ant-carousel'); + this.document = document; + this.renderer.addClass(elementRef.nativeElement, 'ant-carousel'); + this.el = elementRef.nativeElement; } ngAfterContentInit(): void { - if (this.slideContents && this.slideContents.length) { - this.slideContents.first.isActive = true; - } + this.markContentActive(0); } ngAfterViewInit(): void { - // Re-render when content changes. - this.subs_.add( - this.slideContents.changes.subscribe(() => { - this.renderContent(); - }) - ); + this.slickListEl = this.slickList.nativeElement; + this.slickTrackEl = this.slickTrack.nativeElement; + + this.carouselContents.changes.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.markContentActive(0); + this.strategy.withCarouselContents(this.carouselContents); + }); this.ngZone.runOutsideAngular(() => { - this.subs_.add( - fromEvent(window, 'resize') - .pipe(debounceTime(50)) - .subscribe(() => { - this.renderContent(); - this.setTransition(); - }) - ); + fromEvent(window, 'resize') + .pipe( + takeUntil(this.destroy$), + throttleTime(16) + ) + .subscribe(() => { + this.strategy.withCarouselContents(this.carouselContents); + }); }); - // When used in modals (drawers maybe too), it should render itself asynchronously. - // Refer to https://github.com/NG-ZORRO/ng-zorro-antd/issues/2387 - Promise.resolve().then(() => { - this.renderContent(); + this.ngZone.onStable.pipe(take(1)).subscribe(() => { + this.switchStrategy(); + this.strategy.withCarouselContents(this.carouselContents); }); } ngOnChanges(changes: SimpleChanges): void { - if (changes.nzAutoPlay || changes.nzAutoPlaySpeed) { - this.setUpNextScroll(); + const { nzEffect } = changes; + + if (nzEffect && !nzEffect.isFirstChange()) { + this.switchStrategy(); } - if (changes.nzEffect) { - this.updateMode(); + + if (!this.nzAutoPlay || !this.nzAutoPlaySpeed) { + this.clearScheduledTransition(); + } else { + this.scheduleNextTransition(); } } ngOnDestroy(): void { - this.subs_.unsubscribe(); - this.clearTimeout(); - } + this.clearScheduledTransition(); + this.strategy.dispose(); + this.dispose(); - setContentActive(index: number): void { - if (this.slideContents && this.slideContents.length) { - this.nzBeforeChange.emit({ from: this.slideContents.toArray().findIndex(slide => slide.isActive), to: index }); - this.activeIndex = index; - this.setTransition(); - this.slideContents.forEach((slide, i) => (slide.isActive = index === i)); - this.setUpNextScroll(); - this.cdr.markForCheck(); - // Should trigger the following when animation is done. The transition takes 0.5 seconds according to the CSS. - setTimeout(() => this.nzAfterChange.emit(index), this.nzTransitionSpeed); - } + this.destroy$.next(); + this.destroy$.complete(); } - private setTransition(): void { - this.transform = - this.nzEffect === 'fade' - ? 'translate3d(0px, 0px, 0px)' - : this.nzVertical - ? // `Scrollx` mode. - `translate3d(0px, ${-this.activeIndex * this.el.offsetHeight}px, 0px)` - : `translate3d(${-this.activeIndex * this.el.offsetWidth}px, 0px, 0px)`; - if (this.slickTrack) { - this.renderer.setStyle(this.slickTrack.nativeElement, 'transform', this.transform); + onKeyDown(e: KeyboardEvent): void { + if (e.keyCode === LEFT_ARROW) { + e.preventDefault(); + this.pre(); + } else if (e.keyCode === RIGHT_ARROW) { + this.next(); + e.preventDefault(); } } next(): void { - this.setContentActive(this.nextIndex); + this.goTo(this.activeIndex + 1); } pre(): void { - this.setContentActive(this.prevIndex); + this.goTo(this.activeIndex - 1); } goTo(index: number): void { - if (index >= 0 && index <= this.slideContents.length - 1) { - this.setContentActive(index); + if (this.carouselContents && this.carouselContents.length && !this.isTransiting) { + const length = this.carouselContents.length; + const from = this.activeIndex; + const to = (index + length) % length; + this.isTransiting = true; + this.nzBeforeChange.emit({ from, to }); + this.strategy.switch(this.activeIndex, index).subscribe(() => { + this.scheduleNextTransition(); + this.nzAfterChange.emit(index); + this.isTransiting = false; + }); + this.markContentActive(to); + this.cdr.markForCheck(); } } - onKeyDown(e: KeyboardEvent): void { - if (e.keyCode === LEFT_ARROW) { - // Left - this.pre(); - e.preventDefault(); - } else if (e.keyCode === RIGHT_ARROW) { - // Right - this.next(); - e.preventDefault(); + private switchStrategy(): void { + if (this.strategy) { + this.strategy.dispose(); } + + this.strategy = + this.nzEffect === 'scrollx' + ? new NzCarouselTransformStrategy(this, this.cdr, this.renderer) + : new NzCarouselOpacityStrategy(this, this.cdr, this.renderer); + + this.markContentActive(0); + this.strategy.withCarouselContents(this.carouselContents); } - swipe(action: SwipeDirection = 'swipeleft'): void { - if (!this.nzEnableSwipe) { - return; - } - if (action === 'swipeleft') { - this.next(); - } - if (action === 'swiperight') { - this.pre(); + private scheduleNextTransition(): void { + this.clearScheduledTransition(); + if (this.nzAutoPlay && this.nzAutoPlaySpeed > 0 && this.platform.isBrowser) { + this.transitionInProgress = setTimeout(() => { + this.goTo(this.activeIndex + 1); + }, this.nzAutoPlaySpeed); } } - /* tslint:disable-next-line:no-any */ - swipeInProgress(e: any): void { - if (this.nzEffect === 'scrollx') { - const final = e.isFinal; - const scrollWidth = final ? 0 : e.deltaX * 1.2; - const totalWidth = this.el.offsetWidth; - if (this.nzVertical) { - const totalHeight = this.el.offsetHeight; - const scrollPercent = scrollWidth / totalWidth; - const scrollHeight = scrollPercent * totalHeight; - this.transform = `translate3d(0px, ${-this.activeIndex * totalHeight + scrollHeight}px, 0px)`; - } else { - this.transform = `translate3d(${-this.activeIndex * totalWidth + scrollWidth}px, 0px, 0px)`; - } - if (this.slickTrack) { - this.renderer.setStyle(this.slickTrack.nativeElement, 'transform', this.transform); - } - } - if (e.isFinal) { - this.setUpNextScroll(); - } else { - this.clearTimeout(); + private clearScheduledTransition(): void { + if (this.transitionInProgress) { + clearTimeout(this.transitionInProgress); + this.transitionInProgress = null; } } - clearTimeout(): void { - if (this.transitionAction) { - clearTimeout(this.transitionAction); - this.transitionAction = null; + private markContentActive(index: number): void { + this.activeIndex = index; + + if (this.carouselContents) { + this.carouselContents.forEach((slide, i) => { + slide.isActive = index === i; + }); } + + this.cdr.markForCheck(); } - /** - * Make a carousel scroll to `this.nextIndex` after `this.nzAutoPlaySpeed` milliseconds. - */ - private setUpNextScroll(): void { - this.clearTimeout(); - if (this.nzAutoPlay && this.nzAutoPlaySpeed > 0) { - this.transitionAction = setTimeout(() => { - this.setContentActive(this.nextIndex); - }, this.nzAutoPlaySpeed); + pointerDown = (event: TouchEvent | MouseEvent) => { + if (!this.isDragging && !this.isTransiting && this.nzEnableSwipe) { + const point = isTouchEvent(event) ? event.touches[0] || event.changedTouches[0] : event; + this.isDragging = true; + this.clearScheduledTransition(); + this.gestureRect = this.slickListEl.getBoundingClientRect(); + this.pointerPosition = { x: point.clientX, y: point.clientY }; + + this.document.addEventListener('mousemove', this.pointerMove); + this.document.addEventListener('touchmove', this.pointerMove); + this.document.addEventListener('mouseup', this.pointerUp); + this.document.addEventListener('touchend', this.pointerUp); } - } + }; - private updateMode(): void { - if (this.slideContents && this.slideContents.length) { - this.renderContent(); - this.setContentActive(0); + pointerMove = (event: TouchEvent | MouseEvent) => { + if (this.isDragging) { + const point = isTouchEvent(event) ? event.touches[0] || event.changedTouches[0] : event; + this.pointerDelta = { x: point.clientX - this.pointerPosition!.x, y: point.clientY - this.pointerPosition!.y }; + if (Math.abs(this.pointerDelta.x) > 5) { + this.strategy.dragging(this.pointerDelta); + } } - } + }; - private renderContent(): void { - const slickTrackElement = this.slickTrack.nativeElement; - const slickListElement = this.slickList.nativeElement; - if (this.slideContents && this.slideContents.length) { - this.slideContents.forEach((content, i) => { - content.width = this.el.offsetWidth; - if (this.nzEffect === 'fade') { - content.fadeMode = true; - if (this.nzVertical) { - content.top = -i * this.el.offsetHeight; - } else { - content.left = -i * content.width; - } - } else { - content.fadeMode = false; - content.left = null; - content.top = null; - } - }); - if (this.nzVertical) { - this.renderer.removeStyle(slickTrackElement, 'width'); - this.renderer.removeStyle(slickListElement, 'width'); - this.renderer.setStyle(slickListElement, 'height', `${this.slideContents.first.el.offsetHeight}px`); - this.renderer.setStyle(slickTrackElement, 'height', `${this.slideContents.length * this.el.offsetHeight}px`); + pointerUp = () => { + if (this.isDragging && this.nzEnableSwipe) { + const delta = this.pointerDelta ? this.pointerDelta.x : 0; + + // Switch to another slide if delta is third of the width. + if (Math.abs(delta) > this.gestureRect!.width / 3) { + this.goTo(delta > 0 ? this.activeIndex - 1 : this.activeIndex + 1); } else { - this.renderer.removeStyle(slickTrackElement, 'height'); - this.renderer.removeStyle(slickListElement, 'height'); - this.renderer.removeStyle(slickTrackElement, 'width'); // This is necessary to prevent carousel items to overflow. - this.renderer.setStyle(slickTrackElement, 'width', `${this.slideContents.length * this.el.offsetWidth}px`); + this.goTo(this.activeIndex); } - this.setUpNextScroll(); - this.cdr.markForCheck(); + + this.gestureRect = null; + this.pointerDelta = null; + this.isDragging = false; + this.dispose(); } + }; + + private dispose(): void { + this.document.removeEventListener('mousemove', this.pointerMove); + this.document.removeEventListener('touchmove', this.pointerMove); + this.document.removeEventListener('touchend', this.pointerMove); + this.document.removeEventListener('mouseup', this.pointerMove); } } diff --git a/components/carousel/nz-carousel.spec.ts b/components/carousel/nz-carousel.spec.ts index e0c522b8f4b..5d8ba4c67c6 100644 --- a/components/carousel/nz-carousel.spec.ts +++ b/components/carousel/nz-carousel.spec.ts @@ -12,16 +12,18 @@ import { NzCarouselModule } from './nz-carousel.module'; describe('carousel', () => { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [NzCarouselModule], - declarations: [NzTestCarouselBasicComponent] + imports : [ NzCarouselModule ], + declarations: [ NzTestCarouselBasicComponent ] }); TestBed.compileComponents(); })); + describe('carousel basic', () => { let fixture: ComponentFixture; let testComponent: NzTestCarouselBasicComponent; let carouselWrapper: DebugElement; let carouselContents: DebugElement[]; + beforeEach(() => { fixture = TestBed.createComponent(NzTestCarouselBasicComponent); fixture.detectChanges(); @@ -29,12 +31,14 @@ describe('carousel', () => { carouselWrapper = fixture.debugElement.query(By.directive(NzCarouselComponent)); carouselContents = fixture.debugElement.queryAll(By.directive(NzCarouselContentDirective)); }); + it('should className correct', () => { fixture.detectChanges(); expect(carouselWrapper.nativeElement.classList).toContain('ant-carousel'); expect(carouselContents.every(content => content.nativeElement.classList.contains('slick-slide'))).toBe(true); - expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); }); + it('should dynamic change content work', fakeAsync(() => { fixture.detectChanges(); tick(); @@ -47,6 +51,7 @@ describe('carousel', () => { carouselContents = fixture.debugElement.queryAll(By.directive(NzCarouselContentDirective)); expect(carouselContents.length).toBe(0); })); + it('should nzDots work', () => { fixture.detectChanges(); expect(testComponent.dots).toBe(true); @@ -55,140 +60,233 @@ describe('carousel', () => { fixture.detectChanges(); expect(carouselWrapper.nativeElement.querySelector('.slick-dots')).toBeNull(); }); + it('should nzDotRender work', () => { fixture.detectChanges(); expect(testComponent.dots).toBe(true); expect(carouselWrapper.nativeElement.querySelector('.slick-dots').children.length).toBe(4); expect(carouselWrapper.nativeElement.querySelector('.slick-dots').firstElementChild.innerText).toBe('1'); expect(carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.innerText).toBe('4'); - expect( - carouselWrapper.nativeElement.querySelector('.slick-dots').firstElementChild.firstElementChild.tagName - ).toBe('A'); + expect(carouselWrapper.nativeElement.querySelector('.slick-dots').firstElementChild.firstElementChild.tagName).toBe('A'); }); + it('should click content change', () => { fixture.detectChanges(); - expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.click(); fixture.detectChanges(); - expect(carouselContents[3].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[ 3 ].nativeElement.classList).toContain('slick-active'); }); - it('should keydown change content work', () => { + + it('should keydown change content work', fakeAsync(() => { fixture.detectChanges(); - expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); const list = carouselWrapper.nativeElement.querySelector('.slick-list'); + + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + dispatchKeyboardEvent(list, 'keydown', LEFT_ARROW); - fixture.detectChanges(); - expect(carouselContents[3].nativeElement.classList).toContain('slick-active'); + tickATransition(fixture); + expect(carouselContents[ 3 ].nativeElement.classList).toContain('slick-active'); dispatchKeyboardEvent(list, 'keydown', LEFT_ARROW); - fixture.detectChanges(); - expect(carouselContents[2].nativeElement.classList).toContain('slick-active'); + tickATransition(fixture); + expect(carouselContents[ 2 ].nativeElement.classList).toContain('slick-active'); dispatchKeyboardEvent(list, 'keydown', RIGHT_ARROW); - fixture.detectChanges(); - expect(carouselContents[3].nativeElement.classList).toContain('slick-active'); + tickATransition(fixture); + expect(carouselContents[ 3 ].nativeElement.classList).toContain('slick-active'); dispatchKeyboardEvent(list, 'keydown', RIGHT_ARROW); - fixture.detectChanges(); - expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); - }); + tickATransition(fixture); + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + })); + it('should vertical work', () => { fixture.detectChanges(); + expect(carouselWrapper.nativeElement.firstElementChild!.classList).toContain('slick-initialized'); expect(carouselWrapper.nativeElement.firstElementChild!.classList).toContain('slick-slider'); expect(carouselWrapper.nativeElement.firstElementChild!.classList).not.toContain('slick-vertical'); testComponent.vertical = true; fixture.detectChanges(); + expect(carouselWrapper.nativeElement.firstElementChild!.classList).toContain('slick-initialized'); expect(carouselWrapper.nativeElement.firstElementChild!.classList).toContain('slick-slider'); expect(carouselWrapper.nativeElement.firstElementChild!.classList).toContain('slick-vertical'); }); - it('should effect change work', () => { + + it('should effect change work', fakeAsync(() => { fixture.detectChanges(); - expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).toBe(''); - carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.click(); + tick(1000); fixture.detectChanges(); + expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).toBe('translate3d(0px, 0px, 0px)'); + carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.click(); + tickATransition(fixture); expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).not.toBe(''); + testComponent.effect = 'fade'; testComponent.vertical = true; fixture.detectChanges(); - expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.click(); - fixture.detectChanges(); - expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).toBe( - 'translate3d(0px, 0px, 0px)' - ); + tickATransition(fixture); + expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).toBe(''); + testComponent.effect = 'scrollx'; fixture.detectChanges(); - expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.click(); - fixture.detectChanges(); - expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).not.toBe( - 'translate3d(0px, 0px, 0px)' - ); - }); + tickATransition(fixture); + expect(carouselWrapper.nativeElement.querySelector('.slick-track').style.transform).not.toBe('translate3d(0px, 0px, 0px)'); + })); + it('should autoplay work', fakeAsync(() => { testComponent.autoPlay = true; fixture.detectChanges(); - expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); fixture.detectChanges(); tick(5000); fixture.detectChanges(); - expect(carouselContents[1].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); carouselWrapper.nativeElement.querySelector('.slick-dots').lastElementChild.click(); fixture.detectChanges(); tick(5000); fixture.detectChanges(); - testComponent.nzCarouselComponent.clearTimeout(); // Manually stop the auto play to quit this test. - expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); + testComponent.autoPlay = false; + fixture.detectChanges(); + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); })); + it('should autoplay speed work', fakeAsync(() => { testComponent.autoPlay = true; testComponent.autoPlaySpeed = 1000; fixture.detectChanges(); - expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); fixture.detectChanges(); tick(1000 + 10); fixture.detectChanges(); - expect(carouselContents[1].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); testComponent.autoPlaySpeed = 0; fixture.detectChanges(); tick(2000 + 10); fixture.detectChanges(); - expect(carouselContents[1].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); })); - it('should func work', () => { + + it('should func work', fakeAsync(() => { fixture.detectChanges(); - expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); testComponent.nzCarouselComponent.next(); - fixture.detectChanges(); - expect(carouselContents[1].nativeElement.classList).toContain('slick-active'); + tickATransition(fixture); + expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); testComponent.nzCarouselComponent.pre(); - fixture.detectChanges(); - expect(carouselContents[0].nativeElement.classList).toContain('slick-active'); + tickATransition(fixture); + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); testComponent.nzCarouselComponent.goTo(2); - fixture.detectChanges(); - expect(carouselContents[2].nativeElement.classList).toContain('slick-active'); - }); + tickATransition(fixture); + expect(carouselContents[ 2 ].nativeElement.classList).toContain('slick-active'); + })); + it('should resize content after window resized', fakeAsync(() => { - // @ts-ignore - const resizeSpy = spyOn(testComponent.nzCarouselComponent, 'renderContent'); + const resizeSpy = spyOn(testComponent.nzCarouselComponent.strategy, 'withCarouselContents'); window.dispatchEvent(new Event('resize')); - tick(200); - expect(resizeSpy).toHaveBeenCalled(); + tick(16); + expect(resizeSpy).toHaveBeenCalledTimes(1); })); - it('should swipe work', fakeAsync(() => { - fixture.detectChanges(); - testComponent.nzCarouselComponent.swipe('swipeleft'); - tick(1000); - fixture.detectChanges(); - expect(carouselContents[1].nativeElement.classList).toContain('slick-active'); + + it('should support swiping to switch', fakeAsync(() => { + swipe(testComponent.nzCarouselComponent, 500); + tickATransition(fixture); + expect(carouselContents[ 0 ].nativeElement.classList).not.toContain('slick-active'); + expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); + + swipe(testComponent.nzCarouselComponent, -500); + tickATransition(fixture); + swipe(testComponent.nzCarouselComponent, -500); + tickATransition(fixture); + expect(carouselContents[ 0 ].nativeElement.classList).not.toContain('slick-active'); + expect(carouselContents[ 3 ].nativeElement.classList).toContain('slick-active'); })); - it('should swipeInProgress work', () => { - fixture.detectChanges(); - fixture.detectChanges(); - testComponent.nzCarouselComponent.swipeInProgress({ isFinal: false, deltaX: 100 }); - expect(testComponent.nzCarouselComponent.transform).toBe('translate3d(120px, 0px, 0px)'); - testComponent.nzCarouselComponent.swipeInProgress({ isFinal: true }); + + it('should prevent swipes that are not long enough', fakeAsync(() => { + swipe(testComponent.nzCarouselComponent, 2); + tickATransition(fixture); + expect(carouselContents[ 0 ].nativeElement.classList).toContain('slick-active'); + })); + }); + + describe('strategies', () => { + let fixture: ComponentFixture; + let testComponent: NzTestCarouselBasicComponent; + let carouselContents: DebugElement[]; + + beforeEach(() => { + fixture = TestBed.createComponent(NzTestCarouselBasicComponent); fixture.detectChanges(); - expect(testComponent.nzCarouselComponent.transform).toBe('translate3d(0px, 0px, 0px)'); + testComponent = fixture.debugElement.componentInstance; + carouselContents = fixture.debugElement.queryAll(By.directive(NzCarouselContentDirective)); + }); + + describe('transform strategy', () => { + it('should set transform', fakeAsync(() => { + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).toBe(`translate3d(0px, 0px, 0px)`); + + swipe(testComponent.nzCarouselComponent, 500); + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).not.toBe(`translate3d(0px, 0px, 0px)`); + + tickATransition(fixture); + + swipe(testComponent.nzCarouselComponent, -500); + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).toBe(`translate3d(0px, 0px, 0px)`); + tickATransition(fixture); + + // From first to last. + swipe(testComponent.nzCarouselComponent, -500); + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).not.toBe(`translate3d(0px, 0px, 0px)`); + tickATransition(fixture); + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).not.toBe(`translate3d(0px, 0px, 0px)`); + + // From last to first. + swipe(testComponent.nzCarouselComponent, 500); + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).not.toBe(`translate3d(0px, 0px, 0px)`); + tickATransition(fixture); + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).toBe(`translate3d(0px, 0px, 0px)`); + })); + + it('vertical', fakeAsync(() => { + testComponent.vertical = true; + fixture.detectChanges(); + + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).toBe(`translate3d(0px, 0px, 0px)`); + + swipe(testComponent.nzCarouselComponent, 500); + expect(testComponent.nzCarouselComponent.el.style.transform).not.toBe(`translate3d(0px, 0px, 0px)`); + tickATransition(fixture); + + swipe(testComponent.nzCarouselComponent, -500); + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).toBe(`translate3d(0px, 0px, 0px)`); + tickATransition(fixture); + + // From first to last. + swipe(testComponent.nzCarouselComponent, -500); + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).not.toBe(`translate3d(0px, 0px, 0px)`); + tickATransition(fixture); + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).not.toBe(`translate3d(0px, 0px, 0px)`); + + // From last to first. + swipe(testComponent.nzCarouselComponent, 500); + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).not.toBe(`translate3d(0px, 0px, 0px)`); + tickATransition(fixture); + expect(testComponent.nzCarouselComponent.slickTrackEl.style.transform).toBe(`translate3d(0px, 0px, 0px)`); + })); + + it('should disable dragging during transitioning', fakeAsync(() => { + testComponent.nzCarouselComponent.goTo(1); + swipe(testComponent.nzCarouselComponent, 500); + tickATransition(fixture); + expect(carouselContents[ 1 ].nativeElement.classList).toContain('slick-active'); + })); + }); + + // Already covered in components specs. + describe('opacity strategy', () => { }); }); }); @@ -204,25 +302,36 @@ describe('carousel', () => { [nzAutoPlay]="autoPlay" [nzAutoPlaySpeed]="autoPlaySpeed" (nzAfterChange)="afterChange($event)" - (nzBeforeChange)="beforeChange($event)" - > -
-

{{ index }}

-
- {{ index + 1 }} - - ` + (nzBeforeChange)="beforeChange($event)"> +

{{index}}

+ {{index + 1}} + ` }) export class NzTestCarouselBasicComponent { @ViewChild(NzCarouselComponent) nzCarouselComponent: NzCarouselComponent; dots = true; vertical = false; effect = 'scrollx'; - array = [1, 2, 3, 4]; + array = [ 1, 2, 3, 4 ]; autoPlay = false; autoPlaySpeed = 3000; afterChange = jasmine.createSpy('afterChange callback'); beforeChange = jasmine.createSpy('beforeChange callback'); } + +function tickATransition(fixture: ComponentFixture): void { + fixture.detectChanges(); + tick(700); + fixture.detectChanges(); +} + +/* + * Swipe a carousel. + * @param carousel: Carousel component. + * @Distance: Positive to right. Nagetive to left. + */ +function swipe(carousel: NzCarouselComponent, distance: number): void { + carousel.pointerDown(new MouseEvent('mousedown', { clientX: 500, clientY: 0 })); + carousel.pointerMove(new MouseEvent('mousemove', { clientX: 500 - distance, clientY: 0 })); + carousel.pointerUp(); +} diff --git a/components/carousel/strategies/base-strategy.ts b/components/carousel/strategies/base-strategy.ts new file mode 100644 index 00000000000..75c6bf6f796 --- /dev/null +++ b/components/carousel/strategies/base-strategy.ts @@ -0,0 +1,73 @@ +import { ChangeDetectorRef, QueryList, Renderer2 } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { NzCarouselContentDirective } from '../nz-carousel-content.directive'; +import { FromToInterface, NzCarouselComponentAsSource, PointerVector } from '../nz-carousel-definitions'; + +export abstract class NzCarouselBaseStrategy { + // Properties that strategies may want to use. + protected carouselComponent: NzCarouselComponentAsSource | null; + protected contents: NzCarouselContentDirective[]; + protected slickListEl: HTMLElement; + protected slickTrackEl: HTMLElement; + protected length: number; + protected unitWidth: number; + protected unitHeight: number; + + protected get maxIndex(): number { + return this.length - 1; + } + + protected get firstEl(): HTMLElement { + return this.contents[0].el; + } + + protected get lastEl(): HTMLElement { + return this.contents[this.maxIndex].el; + } + + constructor( + carouselComponent: NzCarouselComponentAsSource, + protected cdr: ChangeDetectorRef, + protected renderer: Renderer2 + ) { + this.carouselComponent = carouselComponent; + } + + /** + * Initialize dragging sequences. + * @param contents + */ + withCarouselContents(contents: QueryList | null): void { + // TODO: carousel and its contents should be separated. + const carousel = this.carouselComponent!; + const rect = carousel.el.getBoundingClientRect(); + this.slickListEl = carousel.slickListEl; + this.slickTrackEl = carousel.slickTrackEl; + this.unitWidth = rect.width; + this.unitHeight = rect.height; + this.contents = contents ? contents.toArray() : []; + this.length = this.contents.length; + } + + /** + * Trigger transition. + */ + abstract switch(_f: number, _t: number): Observable; + + /** + * When user drag the carousel component. + * @optional + */ + dragging(_vector: PointerVector): void {} + + /** + * Destroy a scroll strategy. + */ + dispose(): void {} + + protected getFromToInBoundary(f: number, t: number): FromToInterface { + const length = this.maxIndex + 1; + return { from: (f + length) % length, to: (t + length) % length }; + } +} diff --git a/components/carousel/strategies/opacity-strategy.ts b/components/carousel/strategies/opacity-strategy.ts new file mode 100644 index 00000000000..1651acbcac2 --- /dev/null +++ b/components/carousel/strategies/opacity-strategy.ts @@ -0,0 +1,48 @@ +import { QueryList } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; + +import { NzCarouselContentDirective } from '../nz-carousel-content.directive'; + +import { NzCarouselBaseStrategy } from './base-strategy'; + +export class NzCarouselOpacityStrategy extends NzCarouselBaseStrategy { + withCarouselContents(contents: QueryList | null): void { + super.withCarouselContents(contents); + + if (this.contents) { + this.slickTrackEl.style.width = `${this.length * this.unitWidth}px`; + + this.contents.forEach((content: NzCarouselContentDirective, i: number) => { + this.renderer.setStyle(content.el, 'opacity', this.carouselComponent!.activeIndex === i ? '1' : '0'); + this.renderer.setStyle(content.el, 'position', 'relative'); + this.renderer.setStyle(content.el, 'width', `${this.unitWidth}px`); + this.renderer.setStyle(content.el, 'left', `${-this.unitWidth * i}px`); + this.renderer.setStyle(content.el, 'transition', ['opacity 500ms ease 0s', 'visibility 500ms ease 0s']); + }); + } + } + + switch(_f: number, _t: number): Observable { + const { to: t } = this.getFromToInBoundary(_f, _t); + const complete$ = new Subject(); + + this.contents.forEach((content: NzCarouselContentDirective, i: number) => { + this.renderer.setStyle(content.el, 'opacity', t === i ? '1' : '0'); + }); + + setTimeout(() => { + complete$.next(); + complete$.complete(); + }, this.carouselComponent!.nzTransitionSpeed); + + return complete$; + } + + dispose(): void { + this.contents.forEach((content: NzCarouselContentDirective) => { + this.renderer.setStyle(content.el, 'transition', null); + }); + + super.dispose(); + } +} diff --git a/components/carousel/strategies/transform-strategy.ts b/components/carousel/strategies/transform-strategy.ts new file mode 100644 index 00000000000..9db68d50609 --- /dev/null +++ b/components/carousel/strategies/transform-strategy.ts @@ -0,0 +1,166 @@ +import { QueryList } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; + +import { NzCarouselContentDirective } from '../nz-carousel-content.directive'; +import { PointerVector } from '../nz-carousel-definitions'; + +import { NzCarouselBaseStrategy } from './base-strategy'; + +export class NzCarouselTransformStrategy extends NzCarouselBaseStrategy { + private isDragging = false; + private isTransitioning = false; + + private get vertical(): boolean { + return this.carouselComponent!.nzVertical; + } + + dispose(): void { + super.dispose(); + this.renderer.setStyle(this.slickTrackEl, 'transform', null); + } + + withCarouselContents(contents: QueryList | null): void { + super.withCarouselContents(contents); + + const carousel = this.carouselComponent!; + const activeIndex = carousel.activeIndex; + + if (this.contents.length) { + if (this.vertical) { + this.renderer.setStyle(this.slickListEl, 'height', `${this.unitHeight}px`); + this.renderer.setStyle(this.slickTrackEl, 'height', `${this.length * this.unitHeight}px`); + this.renderer.setStyle( + this.slickTrackEl, + 'transform', + `translate3d(0, ${-activeIndex * this.unitHeight}px, 0)` + ); + } else { + this.renderer.setStyle(this.slickTrackEl, 'width', `${this.length * this.unitWidth}px`); + this.renderer.setStyle(this.slickTrackEl, 'transform', `translate3d(${-activeIndex * this.unitWidth}px, 0, 0)`); + } + + this.contents.forEach((content: NzCarouselContentDirective) => { + this.renderer.setStyle(content.el, 'position', 'relative'); + this.renderer.setStyle(content.el, 'width', `${this.unitWidth}px`); + }); + } + } + + switch(_f: number, _t: number): Observable { + const { to: t } = this.getFromToInBoundary(_f, _t); + const complete$ = new Subject(); + + this.renderer.setStyle(this.slickTrackEl, 'transition', 'transform 500ms ease'); + + if (this.vertical) { + this.verticalTransform(_f, _t); + } else { + this.horizontalTransform(_f, _t); + } + + this.isTransitioning = true; + this.isDragging = false; + + setTimeout(() => { + this.renderer.setStyle(this.slickTrackEl, 'transition', null); + this.contents.forEach((content: NzCarouselContentDirective) => { + this.renderer.setStyle(content.el, this.vertical ? 'top' : 'left', null); + }); + + if (this.vertical) { + this.renderer.setStyle(this.slickTrackEl, 'transform', `translate3d(0, ${-t * this.unitHeight}px, 0)`); + } else { + this.renderer.setStyle(this.slickTrackEl, 'transform', `translate3d(${-t * this.unitWidth}px, 0, 0)`); + } + + this.isTransitioning = false; + + complete$.next(); + complete$.complete(); + }, this.carouselComponent!.nzTransitionSpeed); + + return complete$.asObservable(); + } + + dragging(_vector: PointerVector): void { + if (this.isTransitioning) { + return; + } + + const activeIndex = this.carouselComponent!.activeIndex; + + if (this.carouselComponent!.nzVertical) { + if (!this.isDragging && this.length > 2) { + if (activeIndex === this.maxIndex) { + this.prepareVerticalContext(true); + } else if (activeIndex === 0) { + this.prepareVerticalContext(false); + } + } + this.renderer.setStyle( + this.slickTrackEl, + 'transform', + `translate3d(0, ${-activeIndex * this.unitHeight + _vector.x}px, 0)` + ); + } else { + if (!this.isDragging && this.length > 2) { + if (activeIndex === this.maxIndex) { + this.prepareHorizontalContext(true); + } else if (activeIndex === 0) { + this.prepareHorizontalContext(false); + } + } + this.renderer.setStyle( + this.slickTrackEl, + 'transform', + `translate3d(${-activeIndex * this.unitWidth + _vector.x}px, 0, 0)` + ); + } + + this.isDragging = true; + } + + private verticalTransform(_f: number, _t: number): void { + const { from: f, to: t } = this.getFromToInBoundary(_f, _t); + const needToAdjust = this.length > 2 && _t !== t; + + if (needToAdjust) { + this.prepareVerticalContext(t < f); + this.renderer.setStyle(this.slickTrackEl, 'transform', `translate3d(0, ${-_t * this.unitHeight}px, 0)`); + } else { + this.renderer.setStyle(this.slickTrackEl, 'transform', `translate3d(0, ${-t * this.unitHeight}px, 0`); + } + } + + private horizontalTransform(_f: number, _t: number): void { + const { from: f, to: t } = this.getFromToInBoundary(_f, _t); + const needToAdjust = this.length > 2 && _t !== t; + + if (needToAdjust) { + this.prepareHorizontalContext(t < f); + this.renderer.setStyle(this.slickTrackEl, 'transform', `translate3d(${-_t * this.unitWidth}px, 0, 0)`); + } else { + this.renderer.setStyle(this.slickTrackEl, 'transform', `translate3d(${-t * this.unitWidth}px, 0, 0`); + } + } + + private prepareVerticalContext(lastToFirst: boolean): void { + if (lastToFirst) { + this.renderer.setStyle(this.firstEl, 'top', `${this.length * this.unitHeight}px`); + this.renderer.setStyle(this.lastEl, 'top', null); + } else { + this.renderer.setStyle(this.firstEl, 'top', null); + this.renderer.setStyle(this.lastEl, 'top', `${-this.unitHeight * this.length}px`); + } + } + + private prepareHorizontalContext(lastToFirst: boolean): void { + if (lastToFirst) { + this.renderer.setStyle(this.firstEl, 'left', `${this.length * this.unitWidth}px`); + this.renderer.setStyle(this.lastEl, 'left', null); + } else { + this.renderer.setStyle(this.firstEl, 'left', null); + this.renderer.setStyle(this.lastEl, 'left', `${-this.unitWidth * this.length}px`); + } + } +} diff --git a/components/core/util/dom.ts b/components/core/util/dom.ts index 0b41a1066c3..8b986dc480c 100644 --- a/components/core/util/dom.ts +++ b/components/core/util/dom.ts @@ -1,3 +1,8 @@ +/** + * This module provides utility functions to query DOM information or + * set properties. + */ + import { Observable } from 'rxjs'; import { filterNotEmptyNode } from './check'; @@ -57,6 +62,13 @@ export function reverseChildNodes(parent: HTMLElement): void { } } +/** + * Investigate if an event is a `TouchEvent`. + */ +export function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent { + return event.type.startsWith('touch'); +} + export interface MouseTouchObserverConfig { end: string; move: string; diff --git a/package.json b/package.json index d4ffd5efde0..f8efcd6063a 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "@schematics/angular": "~7.2.0", "@stackblitz/sdk": "^1.1.1", "@types/fs-extra": "^5.0.4", - "@types/hammerjs": "^2.0.35", "@types/jasmine": "~2.8.8", "@types/jasminewd2": "~2.0.3", "antd-theme-generator": "^1.0.7", @@ -73,7 +72,6 @@ "conventional-changelog-cli": "^2.0.1", "core-js": "^2.5.4", "fs-extra": "^6.0.1", - "hammerjs": "^2.0.8", "husky": "^1.0.1", "jasmine-core": "~2.99.1", "karma-chrome-launcher": "~2.2.0",