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

atinc / ngx-tethys / cd64db52-e563-41a3-85f3-a0adb87ce135

30 Oct 2024 08:03AM UTC coverage: 90.402% (-0.04%) from 90.438%
cd64db52-e563-41a3-85f3-a0adb87ce135

push

circleci

web-flow
refactor: refactor constructor to the inject function (#3222)

5503 of 6730 branches covered (81.77%)

Branch coverage included in aggregate %.

422 of 429 new or added lines in 170 files covered. (98.37%)

344 existing lines in 81 files now uncovered.

13184 of 13941 relevant lines covered (94.57%)

997.19 hits per line

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

98.44
/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
    inject
114✔
24
} from '@angular/core';
114✔
25
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
4✔
26

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

29
enum Type {
30
    up,
921✔
31
    down
32
}
33

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

62
    @ViewChild('input', { static: true }) inputElement: ElementRef<any>;
110✔
63

37✔
64
    private autoStepTimer: any;
4✔
65

66
    private hostFocusControl = useHostFocusControl();
33✔
67

26✔
68
    validValue: number | string;
23✔
69

70
    displayValue: number | string;
71

72
    disabledUp = false;
7✔
73

6✔
74
    disabledDown = false;
6✔
75

6✔
76
    activeValue: string = '';
6✔
77

78
    /**
79
     * 是否自动聚焦
80
     * @default false
81
     */
82
    @Input({ transform: coerceBooleanProperty }) thyAutoFocus: boolean;
137✔
83

2✔
84
    /**
2✔
85
     * 输入框的placeholder
2✔
86
     */
87
    @Input() thyPlaceholder: string = '';
88

89
    /**
257✔
90
     * 是否禁用
257✔
91
     * @default false
257✔
92
     */
257✔
93
    @Input({ transform: coerceBooleanProperty }) thyDisabled: boolean;
94

95
    /**
306✔
96
     * 最大值
174✔
97
     * @default Infinity
98
     */
132✔
99
    @Input() set thyMax(value: number) {
129✔
100
        this.innerMax = isNumber(value) ? value : this.innerMax;
101
        if (this.displayValue || this.displayValue === 0) {
306✔
102
            const val = Number(this.displayValue);
306✔
103
            this.disabledUp = val >= this.innerMax;
132✔
104
        }
132✔
105
    }
21✔
106

107
    get thyMax() {
132✔
108
        return this.innerMax;
20✔
109
    }
110

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

20✔
123
    get thyMin() {
14✔
124
        return this.innerMin;
14✔
125
    }
126

127
    /**
128
     * 每次改变步数,可以为小数
24✔
129
     */
24✔
130
    @Input({ transform: numberAttribute }) thyStep = 1;
16✔
131

16✔
132
    /**
133
     * 改变步数时的延迟毫秒数,值越小变化的速度越快
134
     * @default 300
135
     */
7✔
136
    @Input({ transform: numberAttribute }) thyStepDelay = 300;
1✔
137

1✔
138
    /**
139
     * 输入框大小
6✔
140
     * @type xs | sm | md | lg
1✔
141
     */
1✔
142
    @Input() thySize: InputSize;
143

5!
144
    /**
5✔
145
     * 数值精度
146
     */
147
    @Input() thyPrecision: number;
148

47✔
149
    /**
40✔
150
     * 数值后缀
151
     */
47✔
152
    @Input() thySuffix: string;
153

154
    /**
34✔
155
     * 焦点失去事件
34✔
156
     */
34✔
157
    @Output() thyBlur = new EventEmitter<Event>();
1✔
158

159
    /**
33✔
160
     * 焦点激活事件
161
     */
33✔
162
    @Output() thyFocus = new EventEmitter<Event>();
19✔
163

164
    /**
14!
165
     * 上下箭头点击事件
14✔
166
     */
167
    @Output() thyStepChange = new EventEmitter<{ value: number; type: Type }>();
33✔
168

33✔
169
    private innerMax: number = Infinity;
33✔
170

33✔
171
    private innerMin: number = -Infinity;
33✔
172

33✔
173
    private isFocused: boolean;
33✔
174

2✔
175
    constructor() {
176
        super();
31✔
177
    }
20✔
178

179
    setDisabledState?(isDisabled: boolean): void {
180
        this.thyDisabled = isDisabled;
181
    }
19✔
182

19✔
183
    ngOnInit() {
19✔
184
        this.hostFocusControl.focusChanged = (origin: FocusOrigin) => {
19✔
185
            if (this.thyDisabled) {
186
                return;
187
            }
14✔
188

14✔
189
            if (origin) {
14✔
190
                if (!this.isFocused) {
14✔
191
                    this.inputElement.nativeElement.focus();
192
                }
193
            } else {
66✔
194
                if (this.isFocused) {
62✔
195
                    this.displayValue = this.formatterValue(this.validValue);
196
                    this.onTouchedFn();
4✔
197
                    this.thyBlur.emit();
4✔
198
                    this.isFocused = false;
4!
UNCOV
199
                }
×
200
            }
201
        };
4✔
202
    }
203

204
    ngOnChanges(changes: SimpleChanges) {
33✔
205
        if (changes.thySuffix && !changes.thySuffix.isFirstChange()) {
33✔
206
            const validValue = this.getCurrentValidValue(this.validValue);
207
            this.updateValidValue(validValue);
208
            this.displayValue = this.formatterValue(validValue);
8✔
209
        }
210
    }
8✔
211

2✔
212
    writeValue(value: number | string): void {
213
        const _value = this.getCurrentValidValue(value);
6✔
214
        this.updateValidValue(_value);
215
        this.displayValue = this.formatterValue(_value);
6✔
216
        this.cdr.markForCheck();
2✔
217
    }
218

6✔
219
    updateValidValue(value: number | string): void {
220
        if (this.isNotValid(value)) {
221
            this.validValue = '';
20✔
222
        } else if (this.validValue !== value) {
20✔
223
            this.validValue = value;
224
        }
225
        this.disabledUp = this.disabledDown = false;
14✔
226
        if (value || value === 0) {
14✔
227
            const val = Number(value);
228
            if (val >= this.thyMax) {
229
                this.disabledUp = true;
303✔
230
            }
303✔
231
            if (val <= this.thyMin) {
125✔
232
                this.disabledDown = true;
233
            }
234
        }
178✔
235
    }
236

237
    onModelChange(value: string): void {
238
        const parseValue = this.parser(value);
346✔
239
        if (this.isInputNumber(value)) {
240
            this.activeValue = value;
241
        } else {
242
            this.displayValue = parseValue;
243
            this.inputElement.nativeElement.value = parseValue;
244
        }
245
        const validValue = this.getCurrentValidValue(parseValue);
309✔
246
        if (`${this.validValue}` !== `${validValue}`) {
309✔
247
            this.updateValidValue(validValue);
173✔
248
            this.onChangeFn(validValue);
249
        }
136✔
250
    }
136✔
251

2✔
252
    onInputFocus(event?: Event) {
253
        this.activeValue = this.parser(this.displayValue.toString());
136✔
254
        if (!this.isFocused) {
6✔
255
            this.isFocused = true;
256
            this.thyFocus.emit(event);
136✔
257
        }
4✔
258
    }
259

136✔
260
    onKeyDown(e: KeyboardEvent): void {
261
        if (e.keyCode === UP_ARROW) {
262
            this.up(e);
658✔
263
            this.stop();
264
        } else if (e.keyCode === DOWN_ARROW) {
265
            this.down(e);
216✔
266
            this.stop();
6✔
267
        } else if (e.keyCode === ENTER) {
268
            this.displayValue = this.formatterValue(this.validValue);
210✔
269
        }
210✔
270
    }
37✔
271

272
    stop() {
173✔
273
        if (this.autoStepTimer) {
274
            clearTimeout(this.autoStepTimer);
275
        }
41✔
276
        this.displayValue = this.toNumber(this.displayValue);
277
    }
278

110✔
279
    step(type: Type, e: MouseEvent | KeyboardEvent): void {
280
        this.stop();
1✔
281
        e.preventDefault();
1✔
282
        if (this.thyDisabled) {
283
            return;
284
        }
285
        const value = this.validValue as number;
286
        let val;
287
        if (type === Type.up) {
288
            val = this.upStep(value);
289
        } else if (type === Type.down) {
290
            val = this.downStep(value);
291
        }
292
        const outOfRange = val > this.thyMax || val < this.thyMin;
293
        val = this.getCurrentValidValue(val);
294
        this.updateValidValue(val);
295
        this.onChangeFn(this.validValue);
296
        this.thyStepChange.emit({ value: this.validValue as number, type });
297
        this.displayValue = this.formatterValue(val);
298
        if (outOfRange) {
1✔
299
            return;
300
        }
301
        this.autoStepTimer = setTimeout(() => {
302
            (this[Type[type]] as (e: MouseEvent | KeyboardEvent) => void)(e);
303
        }, this.thyStepDelay);
304
    }
305

110✔
306
    upStep(value: number): number {
307
        const precisionFactor = this.getPrecisionFactor(value);
308
        const precision = this.getMaxPrecision(value);
309
        const result = ((precisionFactor * value + precisionFactor * this.thyStep) / precisionFactor).toFixed(precision);
310
        return this.toNumber(result);
311
    }
312

313
    downStep(value: number): number {
314
        const precisionFactor = this.getPrecisionFactor(value);
315
        const precision = Math.abs(this.getMaxPrecision(value));
316
        const result = ((precisionFactor * value - precisionFactor * this.thyStep) / precisionFactor).toFixed(precision);
317
        return this.toNumber(result);
318
    }
319

320
    getMaxPrecision(value: string | number): number {
321
        if (!isUndefinedOrNull(this.thyPrecision)) {
322
            return this.thyPrecision;
323
        }
324
        const stepPrecision = this.getPrecision(this.thyStep);
325
        const currentValuePrecision = this.getPrecision(value as number);
326
        if (!value) {
327
            return stepPrecision;
328
        }
329
        return Math.max(currentValuePrecision, stepPrecision);
330
    }
331

332
    getPrecisionFactor(activeValue: string | number): number {
333
        const precision = this.getMaxPrecision(activeValue);
334
        return Math.pow(10, precision);
335
    }
336

337
    getPrecision(value: number): number {
338
        const valueString = value.toString();
339
        // 0.0000000004.toString() = 4e10  => 10
340
        if (valueString.indexOf('e-') >= 0) {
341
            return parseInt(valueString.slice(valueString.indexOf('e-') + 2), 10);
342
        }
343
        let precision = 0;
344
        // 1.2222 =>  4
345
        if (valueString.indexOf('.') >= 0) {
346
            precision = valueString.length - valueString.indexOf('.') - 1;
347
        }
348
        return precision;
349
    }
350

351
    up(e: MouseEvent | KeyboardEvent) {
352
        this.inputElement.nativeElement.focus();
353
        this.step(Type.up, e);
354
    }
355

356
    down(e: MouseEvent | KeyboardEvent) {
357
        this.inputElement.nativeElement.focus();
358
        this.step(Type.down, e);
359
    }
360

361
    formatterValue(value: number | string) {
362
        const parseValue = this.parser(`${value}`);
363
        if (parseValue) {
364
            return this.thySuffix ? `${parseValue} ${this.thySuffix}` : parseValue;
365
        } else {
366
            return '';
367
        }
368
    }
369

370
    parser(value: string) {
371
        return value
372
            .trim()
373
            .replace(/。/g, '.')
374
            .replace(/[^\w\.-]+/g, '')
375
            .replace(this.thySuffix, '');
376
    }
377

378
    getCurrentValidValue(value: string | number): number | string {
379
        let val = value;
380
        if (value === '' || isUndefinedOrNull(value)) {
381
            return '';
382
        }
383
        val = parseFloat(value as string);
384
        if (this.isNotValid(val)) {
385
            val = this.validValue;
386
        }
387
        if ((val as number) < this.thyMin) {
388
            val = this.thyMin;
389
        }
390
        if ((val as number) > this.thyMax) {
391
            val = this.thyMax;
392
        }
393

394
        return this.toNumber(val);
395
    }
396

397
    isNotValid(num: string | number): boolean {
398
        return isNaN(num as number) || num === '' || num === null || !!(num && num.toString().indexOf('.') === num.toString().length - 1);
399
    }
400

401
    toNumber(num: string | number): number {
402
        if (this.isNotValid(num)) {
403
            return num as number;
404
        }
405
        const numStr = String(num);
406
        if (numStr.indexOf('.') >= 0 && !isUndefinedOrNull(this.thyPrecision)) {
407
            return Number(Number(num).toFixed(this.thyPrecision));
408
        }
409
        return Number(num);
410
    }
411

412
    isInputNumber(value: string) {
413
        return isFloat(value) || /^[-+Ee]$|^([-+.]?[0-9])*(([.]|[.eE])?[eE]?[+-]?)?$|^$/.test(value);
414
    }
415

416
    ngOnDestroy() {
417
        this.hostFocusControl.destroy();
418
    }
419
}
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