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

atinc / ngx-tethys / d9ae709b-3c27-4b69-b125-b8b80b54f90b

pending completion
d9ae709b-3c27-4b69-b125-b8b80b54f90b

Pull #2757

circleci

mengshuicmq
fix: fix code review
Pull Request #2757: feat(color-picker): color-picker support disabled (#INFR-8645)

98 of 6315 branches covered (1.55%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

2392 of 13661 relevant lines covered (17.51%)

83.12 hits per line

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

3.85
/src/slider/slider.component.ts
1
import {
2
    Component,
3
    OnInit,
4
    OnChanges,
5
    OnDestroy,
6
    AfterViewInit,
7
    SimpleChanges,
8
    forwardRef,
9
    Input,
10
    EventEmitter,
11
    ChangeDetectorRef,
12
    ViewChild,
13
    ElementRef,
14
    HostBinding,
15
    Output,
16
    NgZone
17
} from '@angular/core';
1✔
18
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
19
import { Observable, Subscription, fromEvent } from 'rxjs';
×
20
import { clamp } from 'ngx-tethys/util';
×
21
import { tap, pluck, map, distinctUntilChanged, takeUntil } from 'rxjs/operators';
×
22
import { InputBoolean, InputNumber } from 'ngx-tethys/core';
23
import { useHostRenderer } from '@tethys/cdk/dom';
×
24
import { NgStyle } from '@angular/common';
×
25

26
export type ThySliderType = 'primary' | 'success' | 'info' | 'warning' | 'danger';
27

28
export type ThySliderSize = 'sm' | 'md' | 'lg';
×
29

×
30
/**
×
31
 * 滑动输入条组件
32
 * @name thy-slider
×
33
 * @order 10
×
34
 */
35
@Component({
36
    selector: 'thy-slider',
37
    templateUrl: './slider.component.html',
×
38
    providers: [
×
39
        {
×
40
            provide: NG_VALUE_ACCESSOR,
×
41
            useExisting: forwardRef(() => ThySliderComponent),
×
42
            multi: true
×
43
        }
×
44
    ],
×
45
    standalone: true,
×
46
    imports: [NgStyle]
×
47
})
×
48
export class ThySliderComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges, ControlValueAccessor {
×
49
    /**
×
50
     * 是否切换为纵轴模式
×
51
     */
×
52
    @HostBinding('class.slider-vertical')
×
53
    @Input()
54
    @InputBoolean()
55
    thyVertical = false;
×
56

×
57
    /**
×
58
     * 是否禁用
59
     */
×
60
    @HostBinding('class.slider-disabled')
×
61
    @Input()
×
62
    @InputBoolean()
63
    thyDisabled = false;
64

65
    @HostBinding('class.thy-slider') _thySlider = true;
×
66

×
67
    @HostBinding('class.cursor-pointer') _pointer = true;
68

69
    @ViewChild('sliderRail', { static: true }) sliderRail: ElementRef;
×
70

71
    @ViewChild('sliderTrack', { static: true }) sliderTrack: ElementRef;
72

×
73
    @ViewChild('sliderPointer', { static: true }) sliderPointer: ElementRef;
74

75
    /**
×
76
     * 最大值
77
     */
78
    @Input() @InputNumber() thyMax = 100;
×
79

×
80
    /**
×
81
     * 最小值
×
82
     */
83
    @Input() @InputNumber() thyMin = 0;
84

85
    /**
86
     * 步长,需要被 thyMax - thyMin 的差值整除。
×
87
     */
88
    @Input() @InputNumber() thyStep = 1;
89

×
90
    /**
×
91
     * 切换主题类型
92
     * @type primary | success | info | warning | danger
93
     * @default success
×
94
     */
95
    @Input() set thyType(type: ThySliderType) {
96
        if (type) {
97
            if (this.typeClassName) {
×
98
                this.hostRenderer.removeClass(this.typeClassName);
×
99
            }
×
100
            this.hostRenderer.addClass(type ? `thy-slider-${type}` : '');
101
            this.typeClassName = `thy-slider-${type}`;
×
102
        }
103
    }
104

×
105
    /**
×
106
     * 通过变量设置颜色
107
     */
×
108
    @Input() thyColor: string;
109

110
    /**
×
111
     * 滑动输入条大小
112
     * @type sm | md | lg
113
     * @default sm
×
114
     */
×
115
    @Input() set thySize(size: ThySliderSize) {
×
116
        if (size) {
117
            if (this.sizeClassName) {
118
                this.hostRenderer.removeClass(this.sizeClassName);
×
119
            }
120
            this.hostRenderer.addClass(size ? `thy-slider-${size}` : '');
121
            this.sizeClassName = `thy-slider-${size}`;
×
122
        }
×
123
    }
×
124

×
125
    /**
126
     * 移动结束后的回调,参数为当前值
×
127
     */
×
128
    @Output() thyAfterChange = new EventEmitter<{ value: number }>();
×
129

×
130
    public value: number;
131

×
132
    private typeClassName = '';
×
133

×
134
    private sizeClassName = '';
135

×
136
    private dragStartListener: Observable<number>;
×
137

×
138
    private dragMoveListener: Observable<number>;
139

140
    private dragEndListener: Observable<Event>;
×
141

×
142
    private dragStartHandler: Subscription | null;
×
143

144
    private dragMoveHandler: Subscription | null;
×
145

×
146
    private dragEndHandler: Subscription | null;
147

×
148
    private hostRenderer = useHostRenderer();
×
149

150
    private onChangeCallback = (v: any) => {};
151

152
    private onTouchedCallback = (v: any) => {};
×
153

×
154
    constructor(private cdr: ChangeDetectorRef, private ngZone: NgZone, private ref: ElementRef) {}
155

156
    ngOnInit() {
×
157
        if (typeof ngDevMode === 'undefined' || ngDevMode) {
×
158
            verifyMinAndMax(this);
159
            verifyStepValues(this);
160
        }
×
161

×
162
        this.toggleDisabled();
×
163
        if (this.value === null || this.value === undefined) {
164
            this.setValue(this.ensureValueInRange(null));
165
        }
×
166
    }
×
167

168
    ngAfterViewInit() {
169
        this.registerMouseEventsListeners();
×
170
        this.toggleDisabled();
171
    }
172

173
    writeValue(newValue: number) {
×
174
        this.setValue(this.ensureValueInRange(newValue));
×
175
    }
×
176

×
177
    registerOnChange(fn: any) {
×
178
        this.onChangeCallback = fn;
×
179
    }
180

×
181
    registerOnTouched(fn: any) {
×
182
        this.onTouchedCallback = fn;
183
    }
×
184

×
185
    ngOnChanges(changes: SimpleChanges) {
×
186
        if (typeof ngDevMode === 'undefined' || ngDevMode) {
×
187
            if (changes.hasOwnProperty('thyMin') || changes.hasOwnProperty('thyMax') || changes.hasOwnProperty('thyStep')) {
×
188
                verifyMinAndMax(this);
189
                verifyStepValues(this);
190
            }
191
        }
×
192
    }
×
193

×
194
    ngOnDestroy() {
×
195
        this.unsubscribeMouseListeners();
×
196
    }
197

198
    private toggleDisabled() {
×
199
        if (this.thyDisabled) {
×
200
            this.unsubscribeMouseListeners();
×
201
        } else {
×
202
            this.subscribeMouseListeners(['start']);
203
        }
204
    }
×
205

×
206
    private setValue(value: number) {
207
        if (this.value !== value) {
208
            this.value = value;
×
209
            this.updateTrackAndPointer();
×
210
        }
211
        this.onChangeCallback(this.value);
×
212
    }
213

214
    private ensureValueInRange(value: number): number {
×
215
        if (!this.valueMustBeValid(value)) {
×
216
            return this.thyMin;
×
217
        }
×
218
        return clamp(value, this.thyMin, this.thyMax);
219
    }
×
220

×
221
    private valueMustBeValid(value: number): boolean {
222
        return !isNaN(typeof value !== 'number' ? parseFloat(value) : value);
223
    }
×
224

225
    private updateTrackAndPointer() {
×
226
        const offset = this.valueToOffset(this.value);
227
        this.updateStyle(offset / 100);
228
        this.cdr.markForCheck();
×
229
    }
×
230

×
231
    private valueToOffset(value: number): number {
232
        return ((value - this.thyMin) * 100) / (this.thyMax - this.thyMin);
1✔
233
    }
234

235
    private updateStyle(offsetPercentage: number) {
236
        const percentage = Math.min(1, Math.max(0, offsetPercentage));
237
        const orientFields: string[] = this.thyVertical ? ['height', 'bottom'] : ['width', 'left'];
1✔
238
        this.sliderTrack.nativeElement.style[orientFields[0]] = `${percentage * 100}%`;
239
        this.sliderPointer.nativeElement.style[orientFields[1]] = `${percentage * 100}%`;
240
    }
241

242
    private unsubscribeMouseListeners(actions: string[] = ['start', 'move', 'end']) {
243
        if (actions.includes('start') && this.dragStartHandler) {
244
            this.dragStartHandler.unsubscribe();
245
            this.dragStartHandler = null;
246
        }
247
        if (actions.includes('move') && this.dragMoveHandler) {
248
            this.dragMoveHandler.unsubscribe();
249
            this.dragMoveHandler = null;
250
        }
251
        if (actions.includes('end') && this.dragEndHandler) {
252
            this.dragEndHandler.unsubscribe();
253
            this.dragEndHandler = null;
254
        }
1✔
255
    }
256

257
    private subscribeMouseListeners(actions: string[] = ['start', 'move', 'end']) {
258
        if (actions.includes('start') && this.dragStartListener && !this.dragStartHandler) {
1✔
259
            this.dragStartHandler = this.dragStartListener.subscribe(this.mouseStartMoving.bind(this));
260
        }
261

262
        if (actions.includes('move') && this.dragMoveListener && !this.dragMoveHandler) {
1✔
263
            this.dragMoveHandler = this.dragMoveListener.subscribe(this.mouseMoving.bind(this));
264
        }
265

266
        if (actions.includes('end') && this.dragEndListener && !this.dragEndHandler) {
1✔
267
            this.dragEndHandler = this.dragEndListener.subscribe(this.mouseStopMoving.bind(this));
268
        }
269
    }
270

1✔
271
    private mouseStartMoving(value: number) {
272
        this.pointerController(true);
273
        this.setValue(value);
274
    }
1✔
275

276
    private mouseMoving(value: number) {
277
        this.setValue(this.ensureValueInRange(value));
278
        this.cdr.markForCheck();
279
    }
280

281
    private mouseStopMoving(): void {
×
282
        this.pointerController(false);
283
        this.cdr.markForCheck();
284
        this.thyAfterChange.emit({ value: this.value });
285
    }
286

287
    private pointerController(movable: boolean) {
288
        if (movable) {
289
            this.subscribeMouseListeners(['move', 'end']);
290
        } else {
291
            this.unsubscribeMouseListeners(['move', 'end']);
292
        }
293
    }
294
    private registerMouseEventsListeners() {
×
295
        const orientField = this.thyVertical ? 'pageY' : 'pageX';
×
296

297
        this.dragStartListener = this.ngZone.runOutsideAngular(() => {
298
            return fromEvent(this.ref.nativeElement, 'mousedown').pipe(
299
                tap((e: Event) => {
×
300
                    e.stopPropagation();
×
301
                    e.preventDefault();
302
                }),
×
303
                pluck(orientField),
×
304
                map((position: number, index) => this.mousePositionToAdaptiveValue(position))
305
            );
306
        });
307

308
        this.dragEndListener = this.ngZone.runOutsideAngular(() => {
309
            return fromEvent(document, 'mouseup');
310
        });
311

312
        this.dragMoveListener = this.ngZone.runOutsideAngular(() => {
313
            return fromEvent(document, 'mousemove').pipe(
314
                tap((e: Event) => {
315
                    e.stopPropagation();
316
                    e.preventDefault();
317
                }),
318
                pluck(orientField),
319
                distinctUntilChanged(),
320
                map((position: number) => this.mousePositionToAdaptiveValue(position)),
321
                distinctUntilChanged(),
322
                takeUntil(this.dragEndListener)
323
            );
324
        });
325
    }
326

327
    private mousePositionToAdaptiveValue(position: number): number {
328
        const sliderStartPosition = this.getSliderPagePosition();
329
        const sliderLength = this.getRailLength();
330
        const ratio = this.convertPointerPositionToRatio(position, sliderStartPosition, sliderLength);
331
        const value = this.ratioToValue(ratio);
332
        return parseFloat(value.toFixed(this.getDecimals(this.thyStep)));
333
    }
334

335
    private getSliderPagePosition(): number {
336
        const rect = this.ref.nativeElement.getBoundingClientRect();
337
        const window = this.ref.nativeElement.ownerDocument.defaultView;
338
        const orientFields: string[] = this.thyVertical ? ['bottom', 'pageYOffset'] : ['left', 'pageXOffset'];
339
        return rect[orientFields[0]] + window[orientFields[1]];
340
    }
341

342
    private getRailLength() {
343
        const orientFiled = this.thyVertical ? 'clientHeight' : 'clientWidth';
344

345
        return this.sliderRail.nativeElement[orientFiled];
346
    }
347

348
    private convertPointerPositionToRatio(pointerPosition: number, startPosition: number, totalLength: number) {
349
        if (this.thyVertical) {
350
            return clamp((startPosition - pointerPosition) / totalLength, 0, 1);
351
        }
352
        return clamp((pointerPosition - startPosition) / totalLength, 0, 1);
353
    }
354

355
    private ratioToValue(ratio: number) {
356
        let value = (this.thyMax - this.thyMin) * ratio + this.thyMin;
357
        const step = this.thyStep;
358
        if (ratio === 0) {
359
            value = this.thyMin;
360
        } else if (ratio === 1) {
361
            value = this.thyMax;
362
        } else {
363
            value = Math.round(value / step) * step;
364
        }
365
        return clamp(value, this.thyMin, this.thyMax);
366
    }
367

368
    private getDecimals(value: number): number {
369
        const valueString = value.toString();
370
        const integerLength = valueString.indexOf('.') + 1;
371
        return integerLength >= 0 ? valueString.length - integerLength : 0;
372
    }
373
}
374

375
// Note: keep `verifyMinAndMax` and `verifyStepValues` as separate functions (not as class properties)
376
// so they're tree-shakable in production mode.
377

378
function verifyMinAndMax(ctx: ThySliderComponent): void | never {
379
    if (ctx.thyMin >= ctx.thyMax) {
380
        throw new Error('min value must less than max value.');
381
    }
382
}
383

384
function verifyStepValues(ctx: ThySliderComponent): void | never {
385
    if (ctx.thyStep <= 0 || !ctx.thyStep) {
386
        throw new Error('step value must be greater than 0.');
387
    } else if (Number.isInteger(ctx.thyStep) && (ctx.thyMax - ctx.thyMin) % ctx.thyStep) {
388
        throw new Error('(max - min) must be divisible by step.');
389
    }
390
}
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