• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

worktile / tethys-libs / aad409c8-d164-4cdb-933b-206daea3c62c

28 Nov 2025 06:15AM UTC coverage: 78.03% (+2.2%) from 75.84%
aad409c8-d164-4cdb-933b-206daea3c62c

Pull #199

circleci

xinglu01
feat(components): handle test
Pull Request #199: feat(components): migrate to signal

122 of 216 branches covered (56.48%)

113 of 122 new or added lines in 10 files covered. (92.62%)

7 existing lines in 3 files now uncovered.

412 of 528 relevant lines covered (78.03%)

10.71 hits per line

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

91.92
/packages/components/media/progress.component.ts
1
import {
2
    AfterViewInit,
3
    ChangeDetectionStrategy,
4
    ChangeDetectorRef,
5
    Component,
6
    ElementRef,
7
    NgZone,
8
    OnInit,
9
    effect,
10
    input,
11
    numberAttribute,
12
    output,
13
    viewChild
14
} from '@angular/core';
15
import { useHostRenderer } from '@tethys/cdk/dom';
16
import { MixinBase, mixinUnsubscribe } from 'ngx-tethys/core';
17
import { ThySliderType } from 'ngx-tethys/slider';
18
import { clamp } from 'ngx-tethys/util';
19
import { Observable, Subscription, distinctUntilChanged, fromEvent, map, pluck, takeUntil, tap } from 'rxjs';
20

21
@Component({
22
    selector: 'thy-media-progress',
23
    host: {
24
        class: 'thy-media-progress',
25
        '[class.thy-media-progress-vertical]': 'thyDirection() === "vertical"',
26
        '[class.thy-media-progress-horizontal]': 'thyDirection() === "horizontal"'
27
    },
28
    changeDetection: ChangeDetectionStrategy.OnPush,
29
    template: `
30
        <div class="thy-media-progress-rail" #progressRail>
31
            <div class="thy-media-progress-track" #progressTrack>
32
                <div class="thy-media-progress-pointer" #progressPointer></div>
33
            </div>
34
            <div class="thy-media-progress-buffer" #progressBuffer></div>
35
        </div>
36
    `,
37
    standalone: true
38
})
39
export class ThyMediaProgressComponent extends mixinUnsubscribe(MixinBase) implements OnInit, AfterViewInit {
1✔
40
    readonly progressRail = viewChild<ElementRef>('progressRail');
31✔
41

42
    readonly progressTrack = viewChild<ElementRef>('progressTrack');
31✔
43

44
    readonly progressBuffer = viewChild<ElementRef>('progressBuffer');
31✔
45

46
    /**
47
     * 进度值
48
     */
49
    readonly thyProgressValue = input<number, unknown>(0, { transform: numberAttribute });
31✔
50

51
    /**
52
     * 缓存值
53
     */
54
    readonly thyBufferedValue = input<number, unknown>(0, { transform: numberAttribute });
31✔
55

56
    /**
57
     * 进度条方向
58
     */
59
    readonly thyDirection = input<'horizontal' | 'vertical'>('horizontal');
31✔
60

61
    /**
62
     * 进度主题类型 primary | success | info | warning | danger
63
     */
64
    readonly thyProgressType = input<ThySliderType | undefined>();
31✔
65

66
    /**
67
     * 移动结束后回调
68
     */
69
    readonly thyAfterChange = output<number>();
31✔
70

71
    /**
72
     * 移动开始
73
     */
74
    readonly thyMoveStart = output<void>();
31✔
75

76
    /**
77
     * 移动中
78
     */
79
    readonly thyMove = output<void>();
31✔
80

81
    /**
82
     * 移动结束
83
     */
84
    readonly thyMoveEnd = output<void>();
31✔
85

86
    get dimension() {
87
        return this.thyDirection() === 'horizontal' ? 'width' : 'height';
16✔
88
    }
89

90
    private dragStartListener: Observable<number> | undefined;
91

92
    private dragMoveListener: Observable<number> | undefined;
93

94
    private dragEndListener: Observable<Event> | undefined;
95

96
    private dragStartHandler: Subscription | undefined;
97

98
    private dragMoveHandler: Subscription | undefined;
99

100
    private dragEndHandler: Subscription | undefined;
101

102
    private hostRenderer = useHostRenderer();
31✔
103

104
    typeClassName = '';
31✔
105

106
    progressValue = 0;
31✔
107

108
    constructor(
109
        private cdr: ChangeDetectorRef,
31✔
110
        private ref: ElementRef,
31✔
111
        private ngZone: NgZone
31✔
112
    ) {
113
        super();
31✔
114
        effect(() => {
31✔
115
            const value = this.thyProgressValue() as number;
37✔
116
            this.setValue(value);
37✔
117
        });
118

119
        effect(() => {
31✔
120
            const progressBuffer = this.progressBuffer();
33✔
121
            const bufferedValue = this.thyBufferedValue() as number;
33✔
122
            if (progressBuffer && bufferedValue) {
33✔
123
                const validValue = bufferedValue <= 0 ? 0 : bufferedValue >= 100 ? 100 : bufferedValue;
2!
124
                (progressBuffer as ElementRef).nativeElement.style[this.dimension] = `${validValue}%`;
2✔
125
            }
126
        });
127

128
        effect(() => {
31✔
129
            const type = this.thyProgressType() as ThySliderType;
31✔
130
            if (type) {
31✔
131
                if (this.typeClassName) {
31!
NEW
132
                    this.hostRenderer.removeClass(this.typeClassName);
×
133
                }
134
                this.hostRenderer.addClass(type ? `thy-media-progress-${type}` : '');
31!
135
                this.typeClassName = `thy-media-progress-${type}`;
31✔
136
            }
137
        });
138
    }
139

140
    ngOnInit(): void {
141
        this.subscribeMouseListeners(['start']);
31✔
142
    }
143

144
    ngAfterViewInit() {
145
        this.registerMouseEventsListeners();
31✔
146
        this.subscribeMouseListeners(['start']);
31✔
147
    }
148

149
    private setValue(value: number) {
150
        if (this.progressValue !== value) {
39✔
151
            this.progressValue = value <= 0 ? 0 : value >= 100 ? 100 : value;
14✔
152
            this.updateTrackAndPointer();
14✔
153
        }
154
    }
155

156
    private updateTrackAndPointer() {
157
        (this.progressTrack() as ElementRef).nativeElement.style[this.dimension] = `${this.progressValue}%`;
14✔
158
        this.cdr.markForCheck();
14✔
159
    }
160

161
    private unsubscribeMouseListeners(actions: string[] = ['start', 'move', 'end']) {
31✔
162
        if (actions.includes('start') && this.dragStartHandler) {
33✔
163
            this.dragStartHandler.unsubscribe();
31✔
164
            this.dragStartHandler = undefined;
31✔
165
        }
166
        if (actions.includes('move') && this.dragMoveHandler) {
33✔
167
            this.dragMoveHandler.unsubscribe();
2✔
168
            this.dragMoveHandler = undefined;
2✔
169
        }
170
        if (actions.includes('end') && this.dragEndHandler) {
33✔
171
            this.dragEndHandler.unsubscribe();
2✔
172
            this.dragEndHandler = undefined;
2✔
173
        }
174
    }
175

176
    private subscribeMouseListeners(actions: string[] = ['start', 'move', 'end']) {
×
177
        if (actions.includes('start') && this.dragStartListener && !this.dragStartHandler) {
64✔
178
            this.dragStartHandler = this.dragStartListener.subscribe(this.mouseStartMoving.bind(this));
31✔
179
        }
180

181
        if (actions.includes('move') && this.dragMoveListener && !this.dragMoveHandler) {
64✔
182
            this.dragMoveHandler = this.dragMoveListener.subscribe(this.mouseMoving.bind(this));
2✔
183
        }
184

185
        if (actions.includes('end') && this.dragEndListener && !this.dragEndHandler) {
64✔
186
            this.dragEndHandler = this.dragEndListener.subscribe(this.mouseStopMoving.bind(this));
2✔
187
        }
188
    }
189

190
    private mouseStartMoving(value: number) {
191
        this.pointerController(true);
2✔
192
        this.setValue(value);
2✔
193
    }
194

195
    private mouseMoving(value: number) {
196
        this.setValue(value);
×
197
        this.cdr.markForCheck();
×
198
    }
199

200
    private mouseStopMoving(): void {
201
        this.pointerController(false);
2✔
202
        this.cdr.markForCheck();
2✔
203
        this.thyAfterChange.emit(this.progressValue);
2✔
204
    }
205

206
    private pointerController(movable: boolean) {
207
        if (movable) {
4✔
208
            this.subscribeMouseListeners(['move', 'end']);
2✔
209
        } else {
210
            this.unsubscribeMouseListeners(['move', 'end']);
2✔
211
        }
212
    }
213

214
    private registerMouseEventsListeners() {
215
        const dimension = this.thyDirection() === 'vertical' ? 'pageY' : 'pageX';
31✔
216
        this.dragStartListener = this.ngZone.runOutsideAngular(() => {
31✔
217
            return (fromEvent(this.ref.nativeElement, 'mousedown') as Observable<MouseEvent>).pipe(
31✔
218
                tap((e: MouseEvent) => {
219
                    e.stopPropagation();
2✔
220
                    e.preventDefault();
2✔
221
                }),
222
                pluck(dimension),
223
                map((position: number, index) => this.mousePositionToAdaptiveValue(position)),
2✔
224
                tap(() => {
225
                    this.thyMoveStart.emit();
2✔
226
                })
227
            );
228
        });
229

230
        this.dragEndListener = this.ngZone.runOutsideAngular(() => {
31✔
231
            return fromEvent(document, 'mouseup').pipe(
31✔
232
                tap(() => {
233
                    this.thyMoveEnd.emit();
4✔
234
                })
235
            );
236
        });
237

238
        this.dragMoveListener = this.ngZone.runOutsideAngular(() => {
31✔
239
            const dimension = this.thyDirection() === 'vertical' ? 'pageY' : 'pageX';
31✔
240
            return (fromEvent(document, 'mousemove') as Observable<MouseEvent>).pipe(
31✔
241
                tap((e: MouseEvent) => {
242
                    e.stopPropagation();
×
243
                    e.preventDefault();
×
244
                    this.thyMove.emit();
×
245
                }),
246
                pluck(dimension),
247
                map((position: number) => this.mousePositionToAdaptiveValue(position)),
×
248
                distinctUntilChanged(),
249
                tap(() => {
250
                    this.thyMove.emit();
×
251
                }),
252
                takeUntil(this.dragEndListener as Observable<Event>)
253
            );
254
        });
255
    }
256

257
    private mousePositionToAdaptiveValue(position: number): number {
258
        const dimension = this.thyDirection() === 'vertical' ? 'clientHeight' : 'clientWidth';
2!
259
        const progressStartPosition = this.getProgressPagePosition();
2✔
260
        const progressLength = (this.progressRail() as ElementRef).nativeElement[dimension];
2✔
261
        const ratio = this.convertPointerPositionToRatio(position, progressStartPosition, progressLength);
2✔
262
        return parseFloat((ratio * 100).toFixed(2));
2✔
263
    }
264

265
    private getProgressPagePosition(): number {
266
        const rect = this.ref.nativeElement.getBoundingClientRect();
2✔
267
        const window = this.ref.nativeElement.ownerDocument.defaultView;
2✔
268
        const orientFields: string[] = this.thyDirection() === 'vertical' ? ['bottom', 'pageYOffset'] : ['left', 'pageXOffset'];
2!
269
        // const orientFields: string[] = ['left', 'pageXOffset'];
270
        return rect[orientFields[0]] + window[orientFields[1]];
2✔
271
    }
272

273
    private convertPointerPositionToRatio(pointerPosition: number, startPosition: number, totalLength: number) {
274
        return clamp(Math.abs(pointerPosition - startPosition) / totalLength, 0, 1);
2✔
275
    }
276

277
    ngOnDestroy(): void {
278
        this.unsubscribeMouseListeners();
31✔
279
        super.ngOnDestroy();
31✔
280
    }
281
}
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

© 2026 Coveralls, Inc