• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

atinc / ngx-tethys / #55

30 Jul 2025 07:08AM UTC coverage: 9.866% (-80.4%) from 90.297%
#55

push

why520crazy
feat(empty): add setMessage for update display text #TINFR-2616

92 of 6794 branches covered (1.35%)

Branch coverage included in aggregate %.

2014 of 14552 relevant lines covered (13.84%)

6.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.26
/src/carousel/carousel.component.ts
1
import { Platform } from '@angular/cdk/platform';
2
import { NgTemplateOutlet } from '@angular/common';
3
import {
4
    AfterContentInit,
5
    AfterViewInit,
6
    ChangeDetectionStrategy,
7
    ChangeDetectorRef,
8
    Component,
9
    ElementRef,
10
    inject,
11
    NgZone,
12
    numberAttribute,
13
    OnDestroy,
14
    OnInit,
15
    Renderer2,
16
    TemplateRef,
17
    ViewEncapsulation,
18
    input,
19
    output,
20
    viewChild,
1✔
21
    QueryList,
22
    computed,
×
23
    ContentChildren,
×
24
    effect
×
25
} from '@angular/core';
×
26
import { ThyDot } from 'ngx-tethys/dot';
×
27
import { ThyIcon } from 'ngx-tethys/icon';
×
28
import { coerceBooleanProperty, isNumber, TinyDate } from 'ngx-tethys/util';
×
29
import { fromEvent, Subject } from 'rxjs';
×
30
import { debounceTime, takeUntil } from 'rxjs/operators';
×
31
import { ThyCarouselItemDirective } from './carousel-item.directive';
×
32
import { ThyCarouselService } from './carousel.service';
×
33
import { ThyCarouselFadeEngine, ThyCarouselNoopEngine, ThyCarouselSlideEngine } from './engine';
×
34
import { ThyCarouselEffect, ThyCarouselPause, ThyCarouselSwitchData, ThyCarouselTrigger, ThyDistanceVector } from './typings';
×
35
import { IThyCarouselComponent, THY_CAROUSEL_COMPONENT } from './carousel.token';
×
36

×
37
/**
×
38
 * 走马灯组件
×
39
 * @name thy-carousel
×
40
 */
×
41
@Component({
×
42
    selector: 'thy-carousel',
×
43
    templateUrl: './carousel.component.html',
×
44
    changeDetection: ChangeDetectionStrategy.OnPush,
×
45
    encapsulation: ViewEncapsulation.None,
×
46
    preserveWhitespaces: false,
47
    host: {
×
48
        class: 'thy-carousel'
49
    },
×
50
    imports: [NgTemplateOutlet, ThyDot, ThyIcon],
51
    providers: [
×
52
        {
53
            provide: THY_CAROUSEL_COMPONENT,
54
            useExisting: ThyCarousel
×
55
        }
×
56
    ]
×
57
})
×
58
export class ThyCarousel implements IThyCarouselComponent, OnInit, AfterViewInit, AfterContentInit, OnDestroy {
×
59
    protected renderer = inject(Renderer2);
×
60
    private cdr = inject(ChangeDetectorRef);
×
61
    private ngZone = inject(NgZone);
×
62
    private readonly carouselService = inject(ThyCarouselService);
×
63
    private readonly platform = inject(Platform);
×
64

65
    /**
66
     * @private
×
67
     */
×
68
    @ContentChildren(ThyCarouselItemDirective) carouselItems!: QueryList<ThyCarouselItemDirective>;
×
69

70
    /**
71
     * @private
×
72
     */
×
73
    readonly carouselWrapper = viewChild<ElementRef<HTMLElement>>('carouselWrapper');
×
74

75
    /**
76
     * 是否自动切换
×
77
     */
78
    readonly thyAutoPlay = input(false, { transform: coerceBooleanProperty });
79

80
    /**
81
     * 自动切换时间间隔(毫秒)
×
82
     */
×
83
    readonly thyAutoPlayInterval = input(3000, { transform: numberAttribute });
×
84

×
85
    /**
×
86
     * 切换动画样式
×
87
     * @type slide | fade | noop
×
88
     */
×
89
    readonly thyEffect = input<ThyCarouselEffect>('slide');
×
90

91
    /**
92
     * 是否显示切换指示器
×
93
     */
×
94
    readonly thyIndicators = input(true, { transform: coerceBooleanProperty });
×
95

×
96
    /**
97
     * 指示器 Item 的渲染模板
×
98
     */
99
    readonly thyIndicatorRender = input<
×
100
        TemplateRef<{
101
            $implicit: boolean;
102
        }>
103
    >();
×
104

×
105
    /**
×
106
     * 是否显示左右切换
×
107
     */
108
    readonly thyControls = input(true, { transform: coerceBooleanProperty });
109

110
    /**
×
111
     * 上一个控制器渲染模板
×
112
     */
113
    readonly thyControlPrev = input<TemplateRef<any>>();
114

115
    /**
×
116
     * 下一个控制器渲染模板
×
117
     */
×
118
    readonly thyControlNext = input<TemplateRef<any>>();
×
119

120
    /**
121
     * 是否支持手势滑动
122
     */
123
    readonly thyTouchable = input(true, { transform: coerceBooleanProperty });
×
124

×
125
    /**
×
126
     * 指示点切换的触发条件
127
     * @type click | hover
128
     */
129
    readonly thyTrigger = input<ThyCarouselTrigger>('click');
×
130

×
131
    /**
132
     * 鼠标移动到指示器时是否暂停播放
×
133
     * @type false | hover
×
134
     */
×
135
    readonly thyPause = input<ThyCarouselPause>('hover');
×
136

×
137
    /**
×
138
     * 触发切换帧之前,返回 `{from: number, to: number}`
×
139
     */
140
    readonly thyBeforeChange = output<ThyCarouselSwitchData>();
×
141

×
142
    /**
×
143
     * 切换帧之后的回调,返回当前帧索引
144
     */
145
    readonly thyAfterChange = output<number>();
146

×
147
    private isDragging = false;
148

×
149
    private isTransitioning = false;
150

151
    private pointerVector: ThyDistanceVector = { x: 0, y: 0 };
×
152

153
    readonly engine = computed(() => {
154
        switch (this.thyEffect()) {
×
155
            case 'slide':
×
156
                return new ThyCarouselSlideEngine(this, this.cdr, this.renderer, this.platform);
157

158
            case 'fade':
159
                return new ThyCarouselFadeEngine(this, this.cdr, this.renderer, this.platform);
160

×
161
            default:
×
162
                return new ThyCarouselNoopEngine(this, this.cdr, this.renderer, this.platform);
163
        }
164
    });
165

×
166
    private _trigger$ = new Subject<number | null>();
×
167

×
168
    private _destroy$ = new Subject<void>();
169

×
170
    wrapperDomRect: DOMRect;
×
171

172
    activeIndex: number = 0;
173

174
    wrapperEl: HTMLElement;
×
175

×
176
    transitionTimer: any = null;
×
177

178
    playTime: number = 400;
179

180
    isPause: boolean = false;
×
181

182
    constructor() {
183
        effect(() => {
×
184
            if (this.thyEffect()) {
185
                this.markContentActive(0);
186
                this.setInitialValue();
×
187
            }
×
188
        });
×
189

190
        effect(() => {
191
            if (this.thyTouchable()) {
×
192
                this.renderer.setStyle(this.wrapperEl, 'cursor', this.thyTouchable() ? 'grab' : 'default');
193
            }
194
        });
195

196
        effect(() => {
×
197
            if (!this.thyAutoPlay() || !this.thyAutoPlayInterval()) {
×
198
                this.clearScheduledTransition();
×
199
            } else {
200
                this.scheduleNextTransition();
×
201
            }
×
202
        });
×
203
    }
×
204

205
    private moveTo(index: number): void {
206
        const carouselItems = this.carouselItems;
207
        if (carouselItems && carouselItems.length && !this.isTransitioning) {
×
208
            this.setInitialValue();
×
209
            const len = carouselItems.length;
×
210
            const from = this.activeIndex;
211
            const to = (index + len) % len;
212
            this.thyBeforeChange.emit({ from, to });
213
            this.isTransitioning = true;
214
            this.engine()
×
215
                ?.switch(index, this.activeIndex)
×
216
                .subscribe(
×
217
                    () => {
×
218
                        this.activeIndex = to;
×
219
                        this.markContentActive(this.activeIndex);
220
                        this.scheduleNextTransition();
1✔
221
                        this.thyAfterChange.emit(this.activeIndex);
1✔
222
                    },
223
                    () => {},
224
                    () => {
225
                        this.isTransitioning = false;
226
                    }
227
                );
228
            this.cdr.markForCheck();
229
        }
230
    }
231

232
    private markContentActive(index: number) {
233
        this.activeIndex = index;
234
        this.cdr.markForCheck();
235
        this.carouselItems?.forEach((carouselContent: ThyCarouselItemDirective, i: number) => {
236
            carouselContent.isActive = index === i;
237
        });
238
    }
239

1✔
240
    private setInitialValue(): void {
241
        if (this.engine() && this.carouselItems) {
242
            this.engine()?.initializeCarouselContents(this.carouselItems);
243
        }
244
    }
245

246
    private scheduleNextTransition(): void {
247
        this.clearScheduledTransition();
248
        if (this.thyAutoPlay() && !this.isPause) {
249
            this.transitionTimer = setTimeout(() => {
250
                this.moveTo(this.activeIndex + 1);
251
            }, this.thyAutoPlayInterval());
252
        }
253
    }
254

255
    private clearScheduledTransition(): void {
256
        if (this.transitionTimer) {
257
            clearTimeout(this.transitionTimer);
258
            this.transitionTimer = null;
259
        }
260
    }
261

262
    onDrag(event: TouchEvent | MouseEvent): void {
263
        if (!this.isDragging && !this.isTransitioning && this.thyTouchable()) {
264
            const mouseDownTime = new TinyDate().getTime();
265
            let mouseUpTime: number;
266
            this.clearScheduledTransition();
267
            this.wrapperDomRect = this.wrapperEl.getBoundingClientRect();
268
            this.carouselService.registerDrag(event).subscribe(
269
                pointerVector => {
270
                    this.renderer.setStyle(this.wrapperEl, 'cursor', 'grabbing');
271
                    this.pointerVector = pointerVector;
272
                    this.isDragging = true;
273
                    this.engine()?.dragging(this.pointerVector, this.wrapperDomRect);
274
                },
275
                () => {},
276
                () => {
277
                    if (this.isDragging) {
278
                        mouseUpTime = new TinyDate().getTime();
279
                        const holdDownTime = mouseUpTime - mouseDownTime;
280
                        // Fast enough to switch to the next frame
281
                        // or
282
                        // If the pointerVector is more than one third switch to the next frame
283
                        if (
284
                            Math.abs(this.pointerVector.x) > this.wrapperDomRect.width / 3 ||
285
                            Math.abs(this.pointerVector.x) / holdDownTime >= 1
286
                        ) {
287
                            this.moveTo(this.pointerVector.x > 0 ? this.activeIndex - 1 : this.activeIndex + 1);
288
                        } else {
289
                            this.moveTo(this.activeIndex);
290
                        }
291
                    }
292
                    this.isDragging = false;
293
                    this.renderer.setStyle(this.wrapperEl, 'cursor', 'grab');
294
                }
295
            );
296
        }
297
    }
298

299
    indicatorHandleClick(index: number): void {
300
        if (this.thyTrigger() === 'click') {
301
            this.moveTo(index);
302
        }
303
    }
304

305
    indicatorHandleTrigger(index: number): void {
306
        if (this.thyPause() === 'hover') {
307
            this.isPause = true;
308
            this.clearScheduledTransition();
309
        }
310
        if (this.thyTrigger() === 'hover') {
311
            this._trigger$.next(index);
312
        }
313
    }
314

315
    indicatorHandleLeave() {
316
        if (this.thyPause() === 'hover') {
317
            this.isPause = false;
318
            this.scheduleNextTransition();
319
        }
320
    }
321

322
    next(): void {
323
        this.moveTo(this.activeIndex + 1);
324
    }
325

326
    pre(): void {
327
        this.moveTo(this.activeIndex - 1);
328
    }
329

330
    ngOnInit(): void {
331
        this.wrapperEl = this.carouselWrapper()!.nativeElement;
332
        this.ngZone.runOutsideAngular(() => {
333
            fromEvent(window, 'resize')
334
                .pipe(takeUntil(this._destroy$), debounceTime(100))
335
                .subscribe(() => {
336
                    this.engine()?.correctionOffset();
337
                });
338
        });
339
    }
340

341
    ngAfterViewInit(): void {
342
        this.carouselItems.changes.subscribe(() => {
343
            this.markContentActive(0);
344
            this.setInitialValue();
345
        });
346
        this.markContentActive(0);
347
        this.setInitialValue();
348
        if (!this.thyTouchable()) {
349
            this.renderer.setStyle(this.wrapperEl, 'cursor', 'default');
350
        }
351
    }
352

353
    ngAfterContentInit() {
354
        this._trigger$.pipe(takeUntil(this._destroy$), debounceTime(this.playTime)).subscribe(index => {
355
            if (isNumber(index)) {
356
                this.moveTo(index);
357
            }
358
        });
359
    }
360

361
    ngOnDestroy() {
362
        this.clearScheduledTransition();
363
        this._trigger$.next(null);
364
        this._trigger$.complete();
365
        this._destroy$.next();
366
        this._destroy$.complete();
367
    }
368
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc