@ciri/ngx-carousel
Version:
A simple angular carousel component.
778 lines (768 loc) • 23.2 kB
JavaScript
import { Directive, TemplateRef, InjectionToken, Component, ViewEncapsulation, ChangeDetectionStrategy, ElementRef, ChangeDetectorRef, Inject, ContentChild, EventEmitter, forwardRef, Renderer2, Input, Output, ViewChild, ContentChildren, NgModule } from '@angular/core';
import { Observable, Subject, BehaviorSubject, timer, interval } from 'rxjs';
import { takeUntil, startWith, debounceTime, skip, filter, distinctUntilChanged } from 'rxjs/operators';
import { animationFrame } from 'rxjs/internal/scheduler/animationFrame';
import { DomSanitizer, HammerGestureConfig, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser';
import ResizeObserver from 'resize-observer-polyfill';
import { CommonModule } from '@angular/common';
import 'hammerjs';
/**
* @fileoverview added by tsickle
* Generated from: lib/lazy-render.directive.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class LazyRenderDirective {
/**
* @param {?} content
*/
constructor(content) {
this.content = content;
}
}
LazyRenderDirective.decorators = [
{ type: Directive, args: [{
selector: '[lazyRender]'
},] }
];
/** @nocollapse */
LazyRenderDirective.ctorParameters = () => [
{ type: TemplateRef }
];
if (false) {
/** @type {?} */
LazyRenderDirective.prototype.content;
}
/**
* @fileoverview added by tsickle
* Generated from: utils.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* 监听元素大小变动
* \@param target 被监听元素
* @type {?}
*/
const resize = (/**
* @param {?} target
* @return {?}
*/
(target) => {
return new Observable((/**
* @param {?} observer
* @return {?}
*/
observer => {
/** @type {?} */
const ro = new ResizeObserver((/**
* @param {?} entries
* @return {?}
*/
entries => {
observer.next(entries);
}));
ro.observe(target);
return (/**
* @return {?}
*/
() => {
ro.disconnect();
});
}));
});
/**
* @param {?} value
* @param {?} min
* @param {?} max
* @return {?}
*/
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
/**
* @param {?} number
* @param {?} start
* @param {?} end
* @return {?}
*/
function inRange(number, start, end) {
return number >= start && number <= end;
}
/** @type {?} */
const CAROUSEL = new InjectionToken('CarouselToken');
/**
* @fileoverview added by tsickle
* Generated from: lib/carousel-item/carousel-item.component.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class CarouselItemComponent {
/**
* @param {?} elRef
* @param {?} cdr
* @param {?} sanitizer
* @param {?} parent
*/
constructor(elRef, cdr, sanitizer, parent // 之所以不声明具体类型是因为会警告循环引用,虽然它并未发生
) {
this.elRef = elRef;
this.cdr = cdr;
this.sanitizer = sanitizer;
this.parent = parent;
this.rendered = false;
this.destroy$ = new Subject();
}
// 这种方式不兼容 ie11,废弃掉此方案
// @HostBinding('style')
// get style() {
// return this.sanitizer.bypassSecurityTrustStyle(`
// width: ${this.parent.width}px;
// `)
// }
/**
* @return {?}
*/
get isLazyRender() {
return !!this.lazyContent;
}
/**
* @return {?}
*/
get shouldRender() {
return !this.isLazyRender || this.rendered;
}
/**
* @return {?}
*/
ngOnInit() { }
/**
* @return {?}
*/
ngAfterViewInit() {
const { active$, cache, lazyRenderOffset: offset } = (/** @type {?} */ (this.parent));
active$.pipe(takeUntil(this.destroy$)).subscribe((/**
* @param {?} index
* @return {?}
*/
index => {
this.rendered =
(cache && this.rendered) || inRange(this.index, index - offset, index + offset);
this.cdr.markForCheck();
}));
}
/**
* @return {?}
*/
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
CarouselItemComponent.decorators = [
{ type: Component, args: [{
selector: 'ngx-carousel-item',
template: "<ng-container *ngIf=\"shouldRender\" [ngTemplateOutlet]=\"lazyContent && lazyContent.content\">\n <ng-content></ng-content>\n</ng-container>\n",
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class.ngx-carousel__item]': `true`
},
styles: [".ngx-carousel__item{display:inline-block;vertical-align:top}.ngx-carousel__item.pre-mirror-node{position:absolute;left:0;transform:translateX(-100%)}.ngx-carousel__item.post-mirror-node{position:absolute;right:0;transform:translateX(100%)}"]
}] }
];
/** @nocollapse */
CarouselItemComponent.ctorParameters = () => [
{ type: ElementRef },
{ type: ChangeDetectorRef },
{ type: DomSanitizer },
{ type: undefined, decorators: [{ type: Inject, args: [CAROUSEL,] }] }
];
CarouselItemComponent.propDecorators = {
lazyContent: [{ type: ContentChild, args: [LazyRenderDirective, { static: false },] }]
};
if (false) {
/** @type {?} */
CarouselItemComponent.prototype.lazyContent;
/** @type {?} */
CarouselItemComponent.prototype.index;
/** @type {?} */
CarouselItemComponent.prototype.rendered;
/**
* @type {?}
* @private
*/
CarouselItemComponent.prototype.destroy$;
/** @type {?} */
CarouselItemComponent.prototype.elRef;
/**
* @type {?}
* @private
*/
CarouselItemComponent.prototype.cdr;
/**
* @type {?}
* @private
*/
CarouselItemComponent.prototype.sanitizer;
/**
* @type {?}
* @private
*/
CarouselItemComponent.prototype.parent;
}
/**
* @fileoverview added by tsickle
* Generated from: lib/carousel/carousel.component.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class CarouselComponent {
// 后镜像节点
/**
* @param {?} renderer
* @param {?} hostElRef
* @param {?} cdr
*/
constructor(renderer, hostElRef, cdr) {
this.renderer = renderer;
this.hostElRef = hostElRef;
this.cdr = cdr;
/**
* 是否开启无缝模式
*/
this.loop = false;
/**
* 切换速度(ms)
*/
this.speed = 300;
/**
* 自动轮播时间间隔,0 代表关闭自动轮播
*/
this.autoplay = 0;
/**
* 是否跟随手指滑动,设为 false 代表只在松手后进行移动判断
*/
this.followFinger = true;
/**
* 是否允许手动滑动,设为 false 代表只能通过 api 翻页
*/
this.allowTouchMove = true;
/**
* 默认激活项
*/
this.initialIndex = 0;
/**
* lazyRender 模式下预渲染个数,1 代表左右多渲染一个,2 代表左右多渲染两个,...
*/
this.lazyRenderOffset = 0;
/**
* 是否缓存 lazyRender 模式下渲染过的 item,不从 dom 树中删除
*/
this.cache = false;
/**
* 索引变动时触发
*/
this.indexChange = new EventEmitter();
this.active$ = new BehaviorSubject(null);
this.destroy$ = new Subject();
this.percent = 0; // 手指滑动距离所占宽度总和百分比
// 手指滑动距离所占宽度总和百分比
this.offset = 0; // 偏移量(%)
// 偏移量(%)
this.animating = false; // 是否处于过渡效果中
}
/**
* @return {?}
*/
get active() {
return this.active$.value;
}
/**
* @return {?}
*/
get count() {
return (this.items || []).length;
}
/**
* @return {?}
*/
get viewport() {
return this.hostElRef.nativeElement;
}
/**
* @return {?}
*/
get width() {
return this.viewport.offsetWidth;
}
/**
* @return {?}
*/
get canMove() {
return this.allowTouchMove && !this.animating;
}
/**
* @return {?}
*/
get data() {
return {
active: this.active,
count: this.count,
offset: this.offset,
animating: this.animating,
atFirst: this.active === 0,
atLast: this.active === this.count - 1
};
}
/**
* @return {?}
*/
ngOnInit() { }
/**
* @return {?}
*/
ngAfterViewInit() {
this.items.changes
.pipe(takeUntil(this.destroy$), startWith(null), debounceTime(0, animationFrame))
.subscribe((/**
* @return {?}
*/
() => {
this.init();
}));
this.active$
.pipe(takeUntil(this.destroy$), skip(1), filter((/**
* @param {?} v
* @return {?}
*/
v => v !== null && inRange(v, 0, this.count - 1))), distinctUntilChanged())
.subscribe((/**
* @param {?} res
* @return {?}
*/
res => {
this.indexChange.emit(res);
this.cdr.markForCheck();
}));
// resize 功能待开发
// resize(this.viewport)
// .pipe(takeUntil(this.destroy$), debounceTime(0, animationFrame))
// .subscribe(() => {
// // this.updateWidth()
// // this.goTo(this.active, true)
// })
}
/**
* @return {?}
*/
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
/**
* @param {?} e
* @return {?}
*/
onPanStart(e) {
this.stopAutoplay();
}
/**
* @param {?} e
* @return {?}
*/
onPanMove(e) {
if (!this.canMove) {
return;
}
/** @type {?} */
const deltaX = this.getSafeDeltaX(e.deltaX);
this.percent = ((100 / this.count) * deltaX) / this.width;
if (this.followFinger) {
/** @type {?} */
const offset = this.percent - (100 / this.count) * this.active;
this.move(offset, true);
}
}
/**
* @param {?} e
* @return {?}
*/
onPanEnd(e) {
if (!this.canMove) {
return;
}
// 轻拂或者滑动距离大于等于一个节点宽度的 50% 才进行跳转
/** @type {?} */
let newActive = this.active;
/** @type {?} */
const isSwipeLeft = e.direction === Hammer.DIRECTION_LEFT && e.velocityX < -0.3;
/** @type {?} */
const isSwipeRight = e.direction === Hammer.DIRECTION_RIGHT && e.velocityX > 0.3;
if (isSwipeLeft || this.percent <= -50 / this.count) {
newActive++;
}
else if (isSwipeRight || this.percent >= 50 / this.count) {
newActive--;
}
this.goTo(newActive);
this.startAutoplay();
}
/**
* @param {?=} target
* @param {?=} immediate
* @return {?}
*/
goTo(target = 0, immediate = false) {
if (this.animating) {
return;
}
/** @type {?} */
const active = this.getSafeActive(target);
/** @type {?} */
const realActive = this.getRealActive(active);
this.active$.next(realActive);
// 到达第一个或最后一个时更新镜像节点
if (this.loop && (realActive === 0 || realActive === this.count - 1)) {
this.handleMirrorNodes();
}
this.animating = true;
this.move(-(100 / this.count) * active, immediate).subscribe((/**
* @return {?}
*/
() => {
this.animating = false;
if (active === -1 || active === this.count) {
this.goTo(realActive, true);
}
}));
}
/**
* @return {?}
*/
prev() {
this.goTo(this.active - 1);
}
/**
* @return {?}
*/
next() {
this.goTo(this.active + 1);
}
/**
* @private
* @return {?}
*/
init() {
if (this.items.length === 0) {
return;
}
this.items.forEach((/**
* @param {?} el
* @param {?} index
* @return {?}
*/
(el, index) => {
el.index = index;
this.renderer.setStyle(el.elRef.nativeElement, 'width', `${this.width}px`);
}));
this.goTo(this.getSafeActive(this.initialIndex, true), true);
this.startAutoplay();
}
/**
* @private
* @param {?} deltaX
* @return {?}
*/
getSafeDeltaX(deltaX) {
/** @type {?} */
const w = this.width;
return clamp(deltaX, -w, w);
}
/**
* @private
* @param {?} active
* @param {?=} strict
* @return {?}
*/
getSafeActive(active, strict = false) {
/** @type {?} */
const min = this.loop && !strict ? -1 : 0;
/** @type {?} */
const max = this.loop && !strict ? this.count : this.count - 1;
return clamp(active, min, max);
}
// 计算真实索引
// 由于 loop 模式下拷贝了俩节点,所以 active 有误差
// 假设有三个节点,那么 active 非 loop 模式下为 0 ~ 2,loop 模式下为 -1 ~ 3
/**
* @private
* @param {?} active
* @return {?}
*/
getRealActive(active) {
return (active + this.count) % this.count;
}
// loop 模式下首尾拷贝一个节点,模拟无缝轮播
// 0 1 2 => 2 0 1 2 0
// TODO: 也许能找到一个不用手动复制 dom,并且可以自动更新内容的方式
/**
* @private
* @return {?}
*/
handleMirrorNodes() {
/** @type {?} */
const trackEl = this.track.nativeElement
// 清理镜像节点
;
// 清理镜像节点
try {
this.renderer.removeChild(trackEl, this.preMirrorNode);
this.renderer.removeChild(trackEl, this.postMirrorNode);
}
catch (e) { }
const { first, last } = this.items;
this.preMirrorNode = last.elRef.nativeElement.cloneNode(true);
this.postMirrorNode = first.elRef.nativeElement.cloneNode(true);
this.renderer.addClass(this.preMirrorNode, 'pre-mirror-node');
this.renderer.addClass(this.postMirrorNode, 'post-mirror-node');
this.renderer.insertBefore(trackEl, this.preMirrorNode, first.elRef.nativeElement);
this.renderer.appendChild(trackEl, this.postMirrorNode);
}
/**
* @private
* @param {?} offset
* @param {?=} immediate
* @return {?}
*/
move(offset, immediate = false) {
/** @type {?} */
const el = this.track.nativeElement;
/** @type {?} */
const oldOffset = this.offset;
/** @type {?} */
const newOffset = (this.offset = offset);
this.renderer.setStyle(el, 'transition', immediate ? 'none' : `transform ${this.speed}ms`);
this.renderer.setStyle(el, 'transform', `translate3d(${offset}%, 0, 0)`);
return timer(immediate || newOffset === oldOffset ? 0 : this.speed).pipe(takeUntil(this.destroy$));
}
/**
* @private
* @return {?}
*/
startAutoplay() {
if (!this.autoplay || this.count <= 1) {
return;
}
this.stopAutoplay();
this.intervalSub = interval(this.autoplay + this.speed)
.pipe(takeUntil(this.destroy$))
.subscribe((/**
* @return {?}
*/
() => {
/** @type {?} */
const oldActive = this.active;
/** @type {?} */
const newActive = this.loop ? oldActive + 1 : this.getRealActive(oldActive + 1);
this.goTo(newActive);
}));
}
/**
* @private
* @return {?}
*/
stopAutoplay() {
this.intervalSub && this.intervalSub.unsubscribe();
}
}
CarouselComponent.decorators = [
{ type: Component, args: [{
selector: 'ngx-carousel',
template: "<div\n class=\"ngx-carousel__track\"\n #track\n (dragstart)=\"$event.preventDefault()\"\n (panstart)=\"onPanStart($event)\"\n (panmove)=\"onPanMove($event)\"\n (panend)=\"onPanEnd($event)\"\n (pancancel)=\"onPanEnd($event)\"\n>\n <ng-content></ng-content>\n</div>\n\n<div class=\"ngx-carousel__indicator\" *ngIf=\"!indicator\">\n <div\n *ngFor=\"let item of items; let i = index\"\n [class.active]=\"i === active\"\n ></div>\n</div>\n\n<ng-container *ngTemplateOutlet=\"indicator; context: { $implicit: data }\"></ng-container>\n",
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class.ngx-carousel]': `true`
},
providers: [
{
provide: CAROUSEL,
useExisting: forwardRef((/**
* @return {?}
*/
() => CarouselComponent))
}
],
styles: [".ngx-carousel{position:relative;display:block;overflow:hidden}.ngx-carousel__track{position:relative;display:inline-block;white-space:nowrap}.ngx-carousel__indicator{position:absolute;bottom:10px;width:100%;text-align:center;white-space:nowrap;font-size:0;pointer-events:none}.ngx-carousel__indicator div{display:inline-block;width:6px;height:6px;margin:0 3px;border-radius:50%;background:rgba(0,0,0,.25);pointer-events:auto}.ngx-carousel__indicator div.active{background:rgba(0,0,0,.75)}"]
}] }
];
/** @nocollapse */
CarouselComponent.ctorParameters = () => [
{ type: Renderer2 },
{ type: ElementRef },
{ type: ChangeDetectorRef }
];
CarouselComponent.propDecorators = {
loop: [{ type: Input }],
speed: [{ type: Input }],
autoplay: [{ type: Input }],
followFinger: [{ type: Input }],
allowTouchMove: [{ type: Input }],
indicator: [{ type: Input }],
initialIndex: [{ type: Input }],
lazyRenderOffset: [{ type: Input }],
cache: [{ type: Input }],
indexChange: [{ type: Output }],
track: [{ type: ViewChild, args: ['track', { static: false },] }],
items: [{ type: ContentChildren, args: [CarouselItemComponent,] }]
};
if (false) {
/**
* 是否开启无缝模式
* @type {?}
*/
CarouselComponent.prototype.loop;
/**
* 切换速度(ms)
* @type {?}
*/
CarouselComponent.prototype.speed;
/**
* 自动轮播时间间隔,0 代表关闭自动轮播
* @type {?}
*/
CarouselComponent.prototype.autoplay;
/**
* 是否跟随手指滑动,设为 false 代表只在松手后进行移动判断
* @type {?}
*/
CarouselComponent.prototype.followFinger;
/**
* 是否允许手动滑动,设为 false 代表只能通过 api 翻页
* @type {?}
*/
CarouselComponent.prototype.allowTouchMove;
/**
* 自定义指示器
* @type {?}
*/
CarouselComponent.prototype.indicator;
/**
* 默认激活项
* @type {?}
*/
CarouselComponent.prototype.initialIndex;
/**
* lazyRender 模式下预渲染个数,1 代表左右多渲染一个,2 代表左右多渲染两个,...
* @type {?}
*/
CarouselComponent.prototype.lazyRenderOffset;
/**
* 是否缓存 lazyRender 模式下渲染过的 item,不从 dom 树中删除
* @type {?}
*/
CarouselComponent.prototype.cache;
/**
* 索引变动时触发
* @type {?}
*/
CarouselComponent.prototype.indexChange;
/** @type {?} */
CarouselComponent.prototype.track;
/** @type {?} */
CarouselComponent.prototype.items;
/** @type {?} */
CarouselComponent.prototype.active$;
/**
* @type {?}
* @private
*/
CarouselComponent.prototype.destroy$;
/**
* @type {?}
* @private
*/
CarouselComponent.prototype.intervalSub;
/**
* @type {?}
* @private
*/
CarouselComponent.prototype.percent;
/**
* @type {?}
* @private
*/
CarouselComponent.prototype.offset;
/**
* @type {?}
* @private
*/
CarouselComponent.prototype.animating;
/**
* @type {?}
* @private
*/
CarouselComponent.prototype.preMirrorNode;
/**
* @type {?}
* @private
*/
CarouselComponent.prototype.postMirrorNode;
/**
* @type {?}
* @private
*/
CarouselComponent.prototype.renderer;
/**
* @type {?}
* @private
*/
CarouselComponent.prototype.hostElRef;
/**
* @type {?}
* @private
*/
CarouselComponent.prototype.cdr;
}
/**
* @fileoverview added by tsickle
* Generated from: lib/hammer.config.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class HammerConfig extends HammerGestureConfig {
/**
* @param {?} element
* @return {?}
*/
buildHammer(element) {
/** @type {?} */
let mc = new Hammer(element, {
inputClass: Hammer.TouchMouseInput
});
return mc;
}
}
/**
* @fileoverview added by tsickle
* Generated from: lib/carousel.module.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class CarouselModule {
}
CarouselModule.decorators = [
{ type: NgModule, args: [{
declarations: [CarouselComponent, CarouselItemComponent, LazyRenderDirective],
imports: [CommonModule],
exports: [CarouselComponent, CarouselItemComponent, LazyRenderDirective],
providers: [{ provide: HAMMER_GESTURE_CONFIG, useClass: HammerConfig }]
},] }
];
/**
* @fileoverview added by tsickle
* Generated from: public-api.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* Generated from: ciri-ngx-carousel.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
export { CarouselComponent, CarouselItemComponent, CarouselModule, LazyRenderDirective, CAROUSEL as ɵa, HammerConfig as ɵb };
//# sourceMappingURL=ciri-ngx-carousel.js.map