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

worktile / tethys-libs / 2ab684c8-3f25-4aef-bdc8-3ea6ff131a16

18 Nov 2025 09:33AM UTC coverage: 75.84% (-2.5%) from 78.302%
2ab684c8-3f25-4aef-bdc8-3ea6ff131a16

push

circleci

web-flow
build: bump angular 20 #TINFR-2655 (#195)

123 of 218 branches covered (56.42%)

361 of 476 relevant lines covered (75.84%)

9.48 hits per line

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

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

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

41
    @ViewChild('progressTrack', { static: true }) progressTrack: ElementRef | undefined;
42

43
    @ViewChild('progressBuffer', { static: true }) progressBuffer: ElementRef | undefined;
44

45
    /**
46
     * 进度值
47
     */
48
    @Input() set thyProgressValue(value: number) {
49
        this.setValue(value);
37✔
50
    }
51

52
    /**
53
     * 缓存值
54
     */
55
    @Input() set thyBufferedValue(value: number) {
56
        if (this.progressBuffer && value) {
23✔
57
            const validValue = value <= 0 ? 0 : value >= 100 ? 100 : value;
2!
58
            (this.progressBuffer as ElementRef).nativeElement.style[this.dimension] = `${validValue}%`;
2✔
59
        }
60
    }
61

62
    /**
63
     * 进度条方向
64
     */
65
    @Input() thyDirection: 'horizontal' | 'vertical' = 'horizontal';
31✔
66

67
    /**
68
     * 进度主题类型 primary | success | info | warning | danger
69
     */
70
    @Input() set thyProgressType(type: ThySliderType | undefined) {
71
        if (type) {
31✔
72
            if (this.typeClassName) {
17!
73
                this.hostRenderer.removeClass(this.typeClassName);
×
74
            }
75
            this.hostRenderer.addClass(type ? `thy-media-progress-${type}` : '');
17!
76
            this.typeClassName = `thy-media-progress-${type}`;
17✔
77
        }
78
    }
79

80
    /**
81
     * 移动结束后回调
82
     */
83
    @Output() thyAfterChange = new EventEmitter<number>();
31✔
84

85
    /**
86
     * 移动开始
87
     */
88
    @Output() thyMoveStart = new EventEmitter<void>();
31✔
89

90
    /**
91
     * 移动中
92
     */
93
    @Output() thyMove = new EventEmitter<void>();
31✔
94

95
    /**
96
     * 移动结束
97
     */
98
    @Output() thyMoveEnd = new EventEmitter<void>();
31✔
99

100
    get dimension() {
101
        return this.thyDirection === 'horizontal' ? 'width' : 'height';
16✔
102
    }
103

104
    private dragStartListener: Observable<number> | undefined;
105

106
    private dragMoveListener: Observable<number> | undefined;
107

108
    private dragEndListener: Observable<Event> | undefined;
109

110
    private dragStartHandler: Subscription | undefined;
111

112
    private dragMoveHandler: Subscription | undefined;
113

114
    private dragEndHandler: Subscription | undefined;
115

116
    private hostRenderer = useHostRenderer();
31✔
117

118
    typeClassName = '';
31✔
119

120
    progressValue = 0;
31✔
121

122
    constructor(private cdr: ChangeDetectorRef, private ref: ElementRef, private ngZone: NgZone) {
31✔
123
        super();
31✔
124
    }
125

126
    ngOnInit(): void {
127
        this.subscribeMouseListeners(['start']);
31✔
128
    }
129

130
    ngAfterViewInit() {
131
        this.registerMouseEventsListeners();
31✔
132
        this.subscribeMouseListeners(['start']);
31✔
133
    }
134

135
    private setValue(value: number) {
136
        if (this.progressValue !== value) {
39✔
137
            this.progressValue = value <= 0 ? 0 : value >= 100 ? 100 : value;
14✔
138
            this.updateTrackAndPointer();
14✔
139
        }
140
    }
141

142
    private updateTrackAndPointer() {
143
        (this.progressTrack as ElementRef).nativeElement.style[this.dimension] = `${this.progressValue}%`;
14✔
144
        this.cdr.markForCheck();
14✔
145
    }
146

147
    private unsubscribeMouseListeners(actions: string[] = ['start', 'move', 'end']) {
30✔
148
        if (actions.includes('start') && this.dragStartHandler) {
32✔
149
            this.dragStartHandler.unsubscribe();
30✔
150
            this.dragStartHandler = undefined;
30✔
151
        }
152
        if (actions.includes('move') && this.dragMoveHandler) {
32✔
153
            this.dragMoveHandler.unsubscribe();
2✔
154
            this.dragMoveHandler = undefined;
2✔
155
        }
156
        if (actions.includes('end') && this.dragEndHandler) {
32✔
157
            this.dragEndHandler.unsubscribe();
2✔
158
            this.dragEndHandler = undefined;
2✔
159
        }
160
    }
161

162
    private subscribeMouseListeners(actions: string[] = ['start', 'move', 'end']) {
×
163
        if (actions.includes('start') && this.dragStartListener && !this.dragStartHandler) {
64✔
164
            this.dragStartHandler = this.dragStartListener.subscribe(this.mouseStartMoving.bind(this));
31✔
165
        }
166

167
        if (actions.includes('move') && this.dragMoveListener && !this.dragMoveHandler) {
64✔
168
            this.dragMoveHandler = this.dragMoveListener.subscribe(this.mouseMoving.bind(this));
2✔
169
        }
170

171
        if (actions.includes('end') && this.dragEndListener && !this.dragEndHandler) {
64✔
172
            this.dragEndHandler = this.dragEndListener.subscribe(this.mouseStopMoving.bind(this));
2✔
173
        }
174
    }
175

176
    private mouseStartMoving(value: number) {
177
        this.pointerController(true);
2✔
178
        this.setValue(value);
2✔
179
    }
180

181
    private mouseMoving(value: number) {
182
        this.setValue(value);
×
183
        this.cdr.markForCheck();
×
184
    }
185

186
    private mouseStopMoving(): void {
187
        this.pointerController(false);
2✔
188
        this.cdr.markForCheck();
2✔
189
        this.thyAfterChange.emit(this.progressValue);
2✔
190
    }
191

192
    private pointerController(movable: boolean) {
193
        if (movable) {
4✔
194
            this.subscribeMouseListeners(['move', 'end']);
2✔
195
        } else {
196
            this.unsubscribeMouseListeners(['move', 'end']);
2✔
197
        }
198
    }
199

200
    private registerMouseEventsListeners() {
201
        const dimension = this.thyDirection === 'vertical' ? 'pageY' : 'pageX';
31✔
202
        this.dragStartListener = this.ngZone.runOutsideAngular(() => {
31✔
203
            return (fromEvent(this.ref.nativeElement, 'mousedown') as Observable<MouseEvent>).pipe(
31✔
204
                tap((e: MouseEvent) => {
205
                    e.stopPropagation();
2✔
206
                    e.preventDefault();
2✔
207
                }),
208
                pluck(dimension),
209
                map((position: number, index) => this.mousePositionToAdaptiveValue(position)),
2✔
210
                tap(() => {
211
                    this.thyMoveStart.emit();
2✔
212
                })
213
            );
214
        });
215

216
        this.dragEndListener = this.ngZone.runOutsideAngular(() => {
31✔
217
            return fromEvent(document, 'mouseup').pipe(
31✔
218
                tap(() => {
219
                    this.thyMoveEnd.emit();
4✔
220
                })
221
            );
222
        });
223

224
        this.dragMoveListener = this.ngZone.runOutsideAngular(() => {
31✔
225
            const dimension = this.thyDirection === 'vertical' ? 'pageY' : 'pageX';
31✔
226
            return (fromEvent(document, 'mousemove') as Observable<MouseEvent>).pipe(
31✔
227
                tap((e: MouseEvent) => {
228
                    e.stopPropagation();
×
229
                    e.preventDefault();
×
230
                    this.thyMove.emit();
×
231
                }),
232
                pluck(dimension),
233
                map((position: number) => this.mousePositionToAdaptiveValue(position)),
×
234
                distinctUntilChanged(),
235
                tap(() => {
236
                    this.thyMove.emit();
×
237
                }),
238
                takeUntil(this.dragEndListener as Observable<Event>)
239
            );
240
        });
241
    }
242

243
    private mousePositionToAdaptiveValue(position: number): number {
244
        const dimension = this.thyDirection === 'vertical' ? 'clientHeight' : 'clientWidth';
2!
245
        const progressStartPosition = this.getProgressPagePosition();
2✔
246
        const progressLength = (this.progressRail as ElementRef).nativeElement[dimension];
2✔
247
        const ratio = this.convertPointerPositionToRatio(position, progressStartPosition, progressLength);
2✔
248
        return parseFloat((ratio * 100).toFixed(2));
2✔
249
    }
250

251
    private getProgressPagePosition(): number {
252
        const rect = this.ref.nativeElement.getBoundingClientRect();
2✔
253
        const window = this.ref.nativeElement.ownerDocument.defaultView;
2✔
254
        const orientFields: string[] = this.thyDirection === 'vertical' ? ['bottom', 'pageYOffset'] : ['left', 'pageXOffset'];
2!
255
        // const orientFields: string[] = ['left', 'pageXOffset'];
256
        return rect[orientFields[0]] + window[orientFields[1]];
2✔
257
    }
258

259
    private convertPointerPositionToRatio(pointerPosition: number, startPosition: number, totalLength: number) {
260
        return clamp(Math.abs(pointerPosition - startPosition) / totalLength, 0, 1);
2✔
261
    }
262

263
    ngOnDestroy(): void {
264
        this.unsubscribeMouseListeners();
30✔
265
        super.ngOnDestroy();
30✔
266
    }
267
}
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