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

atinc / ngx-tethys / e62d3b10-1466-49c3-aabd-707148681fc8

14 Jun 2024 08:24AM UTC coverage: 90.422%. Remained the same
e62d3b10-1466-49c3-aabd-707148681fc8

push

circleci

minlovehua
feat: use the ngx-tethys/util's coerceBooleanProperty instead of booleanAttribute #INFR-12648

5467 of 6692 branches covered (81.69%)

Branch coverage included in aggregate %.

117 of 120 new or added lines in 66 files covered. (97.5%)

183 existing lines in 46 files now uncovered.

13216 of 13970 relevant lines covered (94.6%)

985.91 hits per line

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

97.25
/src/input-number/input-number.component.ts
1
import { TabIndexDisabledControlValueAccessorMixin, useHostFocusControl } from 'ngx-tethys/core';
2
import { ThyMaxDirective, ThyMinDirective } from 'ngx-tethys/form';
3
import { ThyIcon } from 'ngx-tethys/icon';
4
import { ThyInputDirective } from 'ngx-tethys/input';
5
import { ThyAutofocusDirective } from 'ngx-tethys/shared';
6
import { coerceBooleanProperty, DOWN_ARROW, ENTER, isFloat, isNumber, isUndefinedOrNull, UP_ARROW } from 'ngx-tethys/util';
7

8
import { FocusOrigin } from '@angular/cdk/a11y';
9
import {
10
    ChangeDetectorRef,
11
    Component,
12
    ElementRef,
1✔
13
    EventEmitter,
1✔
14
    forwardRef,
1✔
15
    Input,
2✔
16
    numberAttribute,
17
    OnChanges,
18
    OnDestroy,
19
    OnInit,
20
    Output,
21
    SimpleChanges,
1✔
22
    ViewChild
23
} from '@angular/core';
106✔
24
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
106✔
25

4✔
26
type InputSize = 'xs' | 'sm' | 'md' | 'lg' | '';
4✔
27

28
enum Type {
29
    up,
30
    down
928✔
31
}
32

33
/**
106✔
34
 * 数字输入框
106✔
35
 * @name thy-input-number
4✔
36
 * @order 10
4✔
37
 */
38
@Component({
39
    selector: 'thy-input-number',
40
    templateUrl: './input-number.component.html',
930✔
41
    providers: [
42
        {
43
            provide: NG_VALUE_ACCESSOR,
102✔
44
            useExisting: forwardRef(() => ThyInputNumber),
102✔
45
            multi: true
102✔
46
        }
102✔
47
    ],
102✔
48
    standalone: true,
102✔
49
    imports: [ThyIcon, ThyInputDirective, ThyAutofocusDirective, FormsModule, ThyMinDirective, ThyMaxDirective],
102✔
50
    host: {
102✔
51
        class: 'thy-input-number',
102✔
52
        '[attr.tabindex]': 'tabIndex'
102✔
53
    }
102✔
54
})
102✔
55
export class ThyInputNumber
102✔
56
    extends TabIndexDisabledControlValueAccessorMixin
57
    implements ControlValueAccessor, OnChanges, OnInit, OnDestroy
58
{
102✔
59
    @ViewChild('input', { static: true }) inputElement: ElementRef<any>;
60

61
    private autoStepTimer: any;
102✔
62

36✔
63
    private hostFocusControl = useHostFocusControl();
4✔
64

65
    validValue: number | string;
32✔
66

25✔
67
    displayValue: number | string;
22✔
68

69
    disabledUp = false;
70

71
    disabledDown = false;
7✔
72

6✔
73
    activeValue: string = '';
6✔
74

6✔
75
    /**
6✔
76
     * 是否自动聚焦
77
     * @default false
78
     */
79
    @Input({ transform: coerceBooleanProperty }) thyAutoFocus: boolean;
80

81
    /**
129✔
82
     * 输入框的placeholder
2✔
83
     */
2✔
84
    @Input() thyPlaceholder: string = '';
2✔
85

86
    /**
87
     * 是否禁用
88
     * @default false
241✔
89
     */
241✔
90
    @Input({ transform: coerceBooleanProperty }) thyDisabled: boolean;
241✔
91

241✔
92
    /**
93
     * 最大值
94
     * @default Infinity
268✔
95
     */
162✔
96
    @Input() set thyMax(value: number) {
97
        this.innerMax = isNumber(value) ? value : this.innerMax;
106✔
98
        if (this.displayValue || this.displayValue === 0) {
105✔
99
            const val = Number(this.displayValue);
100
            this.disabledUp = val >= this.innerMax;
268✔
101
        }
268✔
102
    }
106✔
103

106✔
104
    get thyMax() {
19✔
105
        return this.innerMax;
106
    }
106✔
107

18✔
108
    /**
109
     * 最小值
110
     * @default -Infinity
111
     */
112
    @Input() set thyMin(value: number) {
20✔
113
        this.innerMin = isNumber(value) ? value : this.innerMin;
20✔
114
        if (this.displayValue || this.displayValue === 0) {
18✔
115
            const val = Number(this.displayValue);
116
            this.disabledDown = val <= this.innerMin;
117
        }
2✔
118
    }
2✔
119

120
    get thyMin() {
20✔
121
        return this.innerMin;
20✔
122
    }
14✔
123

14✔
124
    /**
125
     * 每次改变步数,可以为小数
126
     */
127
    @Input({ transform: numberAttribute }) thyStep = 1;
23✔
128

23✔
129
    /**
15✔
130
     * 改变步数时的延迟毫秒数,值越小变化的速度越快
15✔
131
     * @default 300
132
     */
133
    @Input({ transform: numberAttribute }) thyStepDelay = 300;
134

7✔
135
    /**
1✔
136
     * 输入框大小
1✔
137
     * @type xs | sm | md | lg
138
     */
6✔
139
    @Input() thySize: InputSize;
1✔
140

1✔
141
    /**
142
     * 数值精度
5!
143
     */
5✔
144
    @Input() thyPrecision: number;
145

146
    /**
147
     * 数值后缀
25✔
148
     */
19✔
149
    @Input() thySuffix: string;
150

25✔
151
    /**
152
     * 焦点失去事件
153
     */
12✔
154
    @Output() thyBlur = new EventEmitter<Event>();
12✔
155

12✔
156
    /**
1✔
157
     * 焦点激活事件
158
     */
11✔
159
    @Output() thyFocus = new EventEmitter<Event>();
160

11✔
161
    private innerMax: number = Infinity;
8✔
162

163
    private innerMin: number = -Infinity;
3!
164

3✔
165
    private isFocused: boolean;
166

11✔
167
    constructor(private cdr: ChangeDetectorRef) {
11✔
168
        super();
11✔
169
    }
11✔
170

11✔
171
    setDisabledState?(isDisabled: boolean): void {
11!
UNCOV
172
        this.thyDisabled = isDisabled;
×
173
    }
174

11✔
UNCOV
175
    ngOnInit() {
×
176
        this.hostFocusControl.focusChanged = (origin: FocusOrigin) => {
177
            if (this.thyDisabled) {
178
                return;
179
            }
8✔
180

8✔
181
            if (origin) {
8✔
182
                if (!this.isFocused) {
8✔
183
                    this.inputElement.nativeElement.focus();
184
                }
185
            } else {
3✔
186
                if (this.isFocused) {
3✔
187
                    this.displayValue = this.formatterValue(this.validValue);
3✔
188
                    this.onTouchedFn();
3✔
189
                    this.thyBlur.emit();
190
                    this.isFocused = false;
191
                }
22✔
192
            }
18✔
193
        };
194
    }
4✔
195

4✔
196
    ngOnChanges(changes: SimpleChanges) {
4!
UNCOV
197
        if (changes.thySuffix && !changes.thySuffix.isFirstChange()) {
×
198
            const validValue = this.getCurrentValidValue(this.validValue);
199
            this.updateValidValue(validValue);
4✔
200
            this.displayValue = this.formatterValue(validValue);
201
        }
202
    }
11✔
203

11✔
204
    writeValue(value: number | string): void {
205
        const _value = this.getCurrentValidValue(value);
206
        this.updateValidValue(_value);
8✔
207
        this.displayValue = this.formatterValue(_value);
208
        this.cdr.markForCheck();
8✔
209
    }
2✔
210

211
    updateValidValue(value: number | string): void {
6✔
212
        if (this.isNotValid(value)) {
213
            this.validValue = '';
6✔
214
        } else if (this.validValue !== value) {
2✔
215
            this.validValue = value;
216
        }
6✔
217
        this.disabledUp = this.disabledDown = false;
218
        if (value || value === 0) {
219
            const val = Number(value);
9✔
220
            if (val >= this.thyMax) {
9✔
221
                this.disabledUp = true;
222
            }
223
            if (val <= this.thyMin) {
3✔
224
                this.disabledDown = true;
3✔
225
            }
226
        }
227
    }
265✔
228

265✔
229
    onModelChange(value: string): void {
164✔
230
        const parseValue = this.parser(value);
231
        if (this.isInputNumber(value)) {
232
            this.activeValue = value;
101✔
233
        } else {
234
            this.displayValue = parseValue;
235
            this.inputElement.nativeElement.value = parseValue;
236
        }
307✔
237
        const validValue = this.getCurrentValidValue(parseValue);
238
        if (`${this.validValue}` !== `${validValue}`) {
239
            this.updateValidValue(validValue);
240
            this.onChangeFn(validValue);
241
        }
242
    }
243

272✔
244
    onInputFocus(event?: Event) {
272✔
245
        this.activeValue = this.parser(this.displayValue.toString());
60✔
246
        if (!this.isFocused) {
247
            this.isFocused = true;
212✔
248
            this.thyFocus.emit(event);
212✔
249
        }
104✔
250
    }
251

212✔
252
    onKeyDown(e: KeyboardEvent): void {
5✔
253
        if (e.keyCode === UP_ARROW) {
254
            this.up(e);
212✔
255
            this.stop();
3✔
256
        } else if (e.keyCode === DOWN_ARROW) {
257
            this.down(e);
212✔
258
            this.stop();
259
        } else if (e.keyCode === ENTER) {
260
            this.displayValue = this.formatterValue(this.validValue);
728✔
261
        }
262
    }
263

248✔
264
    stop() {
107✔
265
        if (this.autoStepTimer) {
266
            clearTimeout(this.autoStepTimer);
141✔
267
        }
141✔
268
        this.displayValue = this.toNumber(this.displayValue);
15✔
269
    }
270

126✔
271
    step(type: Type, e: MouseEvent | KeyboardEvent): void {
272
        this.stop();
273
        e.preventDefault();
41✔
274
        if (this.thyDisabled) {
275
            return;
276
        }
102✔
277
        const value = this.validValue as number;
278
        let val;
1✔
279
        if (type === Type.up) {
280
            val = this.upStep(value);
281
        } else if (type === Type.down) {
1✔
282
            val = this.downStep(value);
283
        }
284
        const outOfRange = val > this.thyMax || val < this.thyMin;
285
        val = this.getCurrentValidValue(val);
286
        this.updateValidValue(val);
287
        this.onChangeFn(this.validValue);
288
        this.displayValue = this.formatterValue(val);
289
        if (outOfRange) {
290
            return;
291
        }
292
        this.autoStepTimer = setTimeout(() => {
293
            (this[Type[type]] as (e: MouseEvent | KeyboardEvent) => void)(e);
294
        }, this.thyStepDelay);
295
    }
296

297
    upStep(value: number): number {
1✔
298
        const precisionFactor = this.getPrecisionFactor(value);
299
        const precision = this.getMaxPrecision(value);
300
        const result = ((precisionFactor * value + precisionFactor * this.thyStep) / precisionFactor).toFixed(precision);
301
        return this.toNumber(result);
302
    }
303

304
    downStep(value: number): number {
102✔
305
        const precisionFactor = this.getPrecisionFactor(value);
306
        const precision = Math.abs(this.getMaxPrecision(value));
307
        const result = ((precisionFactor * value - precisionFactor * this.thyStep) / precisionFactor).toFixed(precision);
308
        return this.toNumber(result);
309
    }
310

311
    getMaxPrecision(value: string | number): number {
312
        if (!isUndefinedOrNull(this.thyPrecision)) {
313
            return this.thyPrecision;
314
        }
315
        const stepPrecision = this.getPrecision(this.thyStep);
316
        const currentValuePrecision = this.getPrecision(value as number);
317
        if (!value) {
318
            return stepPrecision;
319
        }
320
        return Math.max(currentValuePrecision, stepPrecision);
321
    }
322

323
    getPrecisionFactor(activeValue: string | number): number {
324
        const precision = this.getMaxPrecision(activeValue);
325
        return Math.pow(10, precision);
326
    }
327

328
    getPrecision(value: number): number {
329
        const valueString = value.toString();
330
        // 0.0000000004.toString() = 4e10  => 10
331
        if (valueString.indexOf('e-') >= 0) {
332
            return parseInt(valueString.slice(valueString.indexOf('e-') + 2), 10);
333
        }
334
        let precision = 0;
335
        // 1.2222 =>  4
336
        if (valueString.indexOf('.') >= 0) {
337
            precision = valueString.length - valueString.indexOf('.') - 1;
338
        }
339
        return precision;
340
    }
341

342
    up(e: MouseEvent | KeyboardEvent) {
343
        this.inputElement.nativeElement.focus();
344
        this.step(Type.up, e);
345
    }
346

347
    down(e: MouseEvent | KeyboardEvent) {
348
        this.inputElement.nativeElement.focus();
349
        this.step(Type.down, e);
350
    }
351

352
    formatterValue(value: number | string) {
353
        const parseValue = this.parser(`${value}`);
354
        if (parseValue) {
355
            return this.thySuffix ? `${parseValue} ${this.thySuffix}` : parseValue;
356
        } else {
357
            return '';
358
        }
359
    }
360

361
    parser(value: string) {
362
        return value
363
            .trim()
364
            .replace(/。/g, '.')
365
            .replace(/[^\w\.-]+/g, '')
366
            .replace(this.thySuffix, '');
367
    }
368

369
    getCurrentValidValue(value: string | number): number | string {
370
        let val = value;
371
        if (value === '' || value === undefined) {
372
            return '';
373
        }
374
        val = parseFloat(value as string);
375
        if (this.isNotValid(val)) {
376
            val = this.validValue;
377
        }
378
        if ((val as number) < this.thyMin) {
379
            val = this.thyMin;
380
        }
381
        if ((val as number) > this.thyMax) {
382
            val = this.thyMax;
383
        }
384

385
        return this.toNumber(val);
386
    }
387

388
    isNotValid(num: string | number): boolean {
389
        return isNaN(num as number) || num === '' || num === null || !!(num && num.toString().indexOf('.') === num.toString().length - 1);
390
    }
391

392
    toNumber(num: string | number): number {
393
        if (this.isNotValid(num)) {
394
            return num as number;
395
        }
396
        const numStr = String(num);
397
        if (numStr.indexOf('.') >= 0 && !isUndefinedOrNull(this.thyPrecision)) {
398
            return Number(Number(num).toFixed(this.thyPrecision));
399
        }
400
        return Number(num);
401
    }
402

403
    isInputNumber(value: string) {
404
        return isFloat(value) || /^[-+Ee]$|^([-+.]?[0-9])*(([.]|[.eE])?[eE]?[+-]?)?$|^$/.test(value);
405
    }
406

407
    ngOnDestroy() {
408
        this.hostFocusControl.destroy();
409
    }
410
}
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