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

atinc / ngx-tethys / 6515bfb6-f9c0-4b6e-ba42-bbe6479d178f

01 Jul 2024 11:20AM UTC coverage: 90.433% (+0.01%) from 90.422%
6515bfb6-f9c0-4b6e-ba42-bbe6479d178f

push

circleci

web-flow
feat(inputNumber): add thyStepChange event emitter for click arrow #INFR-… (#3109)

5468 of 6692 branches covered (81.71%)

Branch coverage included in aggregate %.

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

2 existing lines in 2 files now uncovered.

13219 of 13972 relevant lines covered (94.61%)

985.43 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
} from '@angular/core';
108✔
24
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
108✔
25

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

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

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

61
    private autoStepTimer: any;
62

104✔
63
    private hostFocusControl = useHostFocusControl();
37✔
64

4✔
65
    validValue: number | string;
66

33✔
67
    displayValue: number | string;
26✔
68

23✔
69
    disabledUp = false;
70

71
    disabledDown = false;
72

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

14✔
166
    private innerMax: number = Infinity;
167

33✔
168
    private innerMin: number = -Infinity;
33✔
169

33✔
170
    private isFocused: boolean;
33✔
171

33✔
172
    constructor(private cdr: ChangeDetectorRef) {
33✔
173
        super();
33✔
174
    }
2✔
175

176
    setDisabledState?(isDisabled: boolean): void {
31✔
177
        this.thyDisabled = isDisabled;
20✔
178
    }
179

180
    ngOnInit() {
181
        this.hostFocusControl.focusChanged = (origin: FocusOrigin) => {
19✔
182
            if (this.thyDisabled) {
19✔
183
                return;
19✔
184
            }
19✔
185

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

201
    ngOnChanges(changes: SimpleChanges) {
4✔
202
        if (changes.thySuffix && !changes.thySuffix.isFirstChange()) {
203
            const validValue = this.getCurrentValidValue(this.validValue);
204
            this.updateValidValue(validValue);
33✔
205
            this.displayValue = this.formatterValue(validValue);
33✔
206
        }
207
    }
208

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

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

234
    onModelChange(value: string): void {
103✔
235
        const parseValue = this.parser(value);
236
        if (this.isInputNumber(value)) {
237
            this.activeValue = value;
238
        } else {
334✔
239
            this.displayValue = parseValue;
240
            this.inputElement.nativeElement.value = parseValue;
241
        }
242
        const validValue = this.getCurrentValidValue(parseValue);
243
        if (`${this.validValue}` !== `${validValue}`) {
244
            this.updateValidValue(validValue);
245
            this.onChangeFn(validValue);
298✔
246
        }
298✔
247
    }
62✔
248

249
    onInputFocus(event?: Event) {
236✔
250
        this.activeValue = this.parser(this.displayValue.toString());
236✔
251
        if (!this.isFocused) {
106✔
252
            this.isFocused = true;
253
            this.thyFocus.emit(event);
236✔
254
        }
6✔
255
    }
256

236✔
257
    onKeyDown(e: KeyboardEvent): void {
4✔
258
        if (e.keyCode === UP_ARROW) {
259
            this.up(e);
236✔
260
            this.stop();
261
        } else if (e.keyCode === DOWN_ARROW) {
262
            this.down(e);
846✔
263
            this.stop();
264
        } else if (e.keyCode === ENTER) {
265
            this.displayValue = this.formatterValue(this.validValue);
316✔
266
        }
110✔
267
    }
268

206✔
269
    stop() {
206✔
270
        if (this.autoStepTimer) {
37✔
271
            clearTimeout(this.autoStepTimer);
272
        }
169✔
273
        this.displayValue = this.toNumber(this.displayValue);
274
    }
275

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

303
    upStep(value: number): number {
304
        const precisionFactor = this.getPrecisionFactor(value);
305
        const precision = this.getMaxPrecision(value);
306
        const result = ((precisionFactor * value + precisionFactor * this.thyStep) / precisionFactor).toFixed(precision);
307
        return this.toNumber(result);
104✔
308
    }
309

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

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

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

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

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

353
    down(e: MouseEvent | KeyboardEvent) {
354
        this.inputElement.nativeElement.focus();
355
        this.step(Type.down, e);
356
    }
357

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

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

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

391
        return this.toNumber(val);
392
    }
393

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

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

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

413
    ngOnDestroy() {
414
        this.hostFocusControl.destroy();
415
    }
416
}
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