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

atinc / ngx-tethys / 881c8997-29c3-4d01-9ef1-22092f16cec2

03 Apr 2024 03:31AM UTC coverage: 90.404% (-0.2%) from 90.585%
881c8997-29c3-4d01-9ef1-22092f16cec2

Pull #3062

circleci

minlovehua
refactor(all): use the transform attribute of @input() instead of @InputBoolean() and @InputNumber()
Pull Request #3062: refactor(all): use the transform attribute of @input() instead of @InputBoolean() and @InputNumber()

5411 of 6635 branches covered (81.55%)

Branch coverage included in aggregate %.

217 of 223 new or added lines in 82 files covered. (97.31%)

201 existing lines in 53 files now uncovered.

13176 of 13925 relevant lines covered (94.62%)

980.1 hits per line

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

91.96
/src/time-picker/time-picker.component.ts
1
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
2
import {
3
    AfterViewInit,
4
    booleanAttribute,
5
    ChangeDetectionStrategy,
6
    ChangeDetectorRef,
7
    Component,
8
    ElementRef,
9
    EventEmitter,
10
    forwardRef,
11
    Input,
12
    OnInit,
13
    Output,
14
    ViewChild
15
} from '@angular/core';
16
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
17
import { isValid } from 'date-fns';
18
import { getFlexiblePositions, ThyPlacement } from 'ngx-tethys/core';
1✔
19
import { TinyDate } from 'ngx-tethys/util';
20
import { ThyTimePanel } from './time-picker-panel.component';
22✔
21
import { ThyIcon } from 'ngx-tethys/icon';
22!
22
import { NgTemplateOutlet, NgIf, NgClass } from '@angular/common';
22✔
23
import { ThyInputDirective } from 'ngx-tethys/input';
24
import { scaleMotion, scaleXMotion, scaleYMotion } from 'ngx-tethys/core';
25

26
export type TimePickerSize = 'xs' | 'sm' | 'md' | 'lg' | 'default';
19✔
27

28
/**
29
 * 时间选择组件
18✔
30
 * @name thy-time-picker
31
 */
32
@Component({
18✔
33
    selector: 'thy-time-picker',
18✔
34
    templateUrl: './time-picker.component.html',
18✔
35
    changeDetection: ChangeDetectionStrategy.OnPush,
18✔
36
    providers: [
18✔
37
        {
18✔
38
            provide: NG_VALUE_ACCESSOR,
18✔
39
            multi: true,
18✔
40
            useExisting: forwardRef(() => ThyTimePicker)
18✔
41
        }
18✔
42
    ],
18✔
43
    host: {
18✔
44
        class: 'thy-time-picker',
18✔
45
        '[class.thy-time-picker-disabled]': `disabled`,
18✔
46
        '[class.thy-time-picker-readonly]': `thyReadonly`
18✔
47
    },
18✔
48
    standalone: true,
18✔
49
    imports: [
18✔
50
        CdkOverlayOrigin,
18✔
51
        ThyInputDirective,
52
        FormsModule,
53
        NgTemplateOutlet,
54
        NgIf,
18✔
55
        ThyIcon,
56
        NgClass,
57
        CdkConnectedOverlay,
16✔
58
        ThyTimePanel
2✔
59
    ],
60
    animations: [scaleXMotion, scaleYMotion, scaleMotion]
14✔
61
})
62
export class ThyTimePicker implements OnInit, AfterViewInit, ControlValueAccessor {
63
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
3✔
64

1✔
65
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
66

67
    @ViewChild('pickerInput', { static: true }) inputRef: ElementRef<HTMLInputElement>;
2!
UNCOV
68

×
69
    @ViewChild('overlayContainer', { static: false }) overlayContainer: ElementRef<HTMLElement>;
70

71
    /**
72
     * 输入框大小
73
     * @type 'xs' | 'sm' | 'md' | 'lg' | 'default'
3✔
74
     */
3✔
75
    @Input() thySize: TimePickerSize = 'default';
3✔
76

77
    /**
78
     * 输入框提示文字
1✔
79
     * @type string
1✔
80
     */
81
    @Input() thyPlaceholder: string = '选择时间';
82

1✔
83
    /**
1✔
84
     * 弹出位置
1✔
85
     * @type 'top' | 'topLeft'| 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight' | 'left' | 'leftTop' | 'leftBottom' | 'right' | 'rightTop' | 'rightBottom'
1✔
86
     */
87
    @Input() thyPlacement: ThyPlacement = 'bottomLeft';
88

4✔
89
    /**
4✔
90
     * 展示的日期格式,支持 'HH:mm:ss' | 'HH:mm' | 'mm:ss'
91
     * @type string
92
     * @default HH:mm:ss
1✔
93
     */
1✔
94
    @Input() set thyFormat(value: string) {
95
        this.format = value || 'HH:mm:ss';
96
        if (this.value && isValid(this.value)) {
1✔
97
            this.showText = new TinyDate(this.value).format(this.format);
98
        }
99
    }
14✔
100

101
    /**
UNCOV
102
     * 小时间隔步长
×
103
     * @type number
104
     */
105
    @Input() thyHourStep: number = 1;
7✔
106

107
    /**
108
     * 分钟间隔步长
6!
109
     * @type number
110
     */
111
    @Input() thyMinuteStep: number = 1;
6✔
112

6✔
113
    /**
114
     * 秒间隔步长
115
     * @type number
116
     */
15!
117
    @Input() thySecondStep: number = 1;
15✔
118

119
    /**
120
     * 弹出层组件 class
121
     * @type string
15!
UNCOV
122
     */
×
123
    @Input() thyPopupClass: string;
124

15✔
125
    /**
15✔
126
     * 是否显示弹出层遮罩
15✔
127
     * @type boolean
128
     * @default false
129
     */
16✔
130
    @Input({ transform: booleanAttribute }) thyBackdrop: boolean;
8✔
131

8✔
132
    /**
8✔
133
     * 禁用
8✔
134
     * @type boolean
7✔
135
     * @default false
1✔
136
     */
137
    @Input({ transform: booleanAttribute }) set thyDisabled(value: boolean) {
138
        this.disabled = value;
6✔
139
    }
140

141
    get thyDisabled(): boolean {
142
        return this.disabled;
1!
143
    }
1✔
144

145
    /**
146
     * 只读
8✔
147
     * @type boolean
148
     * @default false
149
     */
150
    @Input({ transform: booleanAttribute }) thyReadonly: boolean;
1!
151

1✔
152
    /**
153
     * 展示选择此刻
154
     * @type boolean
155
     */
8!
156
    @Input({ transform: booleanAttribute }) thyShowSelectNow = true;
8✔
157

158
    /**
159
     * 可清空值
160
     * @type boolean
37✔
161
     */
9✔
162
    @Input({ transform: booleanAttribute }) thyAllowClear = true;
9✔
163

164
    /**
165
     * 打开/关闭弹窗事件
28✔
166
     * @type EventEmitter<boolean>
167
     */
168
    @Output() thyOpenChange = new EventEmitter<boolean>();
169

18✔
170
    prefixCls = 'thy-time-picker';
171

172
    overlayPositions = getFlexiblePositions(this.thyPlacement, 4);
18✔
173

174
    format: string = 'HH:mm:ss';
175

18✔
176
    disabled: boolean;
18✔
177

178
    showText: string = '';
18✔
179

19✔
180
    openState: boolean;
17✔
181

17✔
182
    value: Date = new TinyDate().setHms(0, 0, 0).nativeDate;
16✔
183

184
    originValue: Date;
185

186
    keepFocus: boolean;
2✔
187

2✔
188
    private isDisabledFirstChange = true;
189

19✔
190
    onValueChangeFn: (val: number | Date) => void = () => void 0;
191

192
    onTouchedFn: () => void = () => void 0;
2✔
193

2✔
194
    constructor(private cdr: ChangeDetectorRef, private elementRef: ElementRef) {}
2✔
195

196
    ngOnInit() {}
197

8!
198
    ngAfterViewInit() {
8✔
199
        this.overlayPositions = getFlexiblePositions(this.thyPlacement, 4);
200
    }
8!
201

8✔
202
    onInputPickerClick() {
203
        if (this.disabledUserOperation()) {
204
            return;
205
        }
4✔
206
        this.openOverlay();
1✔
207
    }
208

4✔
209
    onInputPickerBlur() {
2✔
210
        if (this.keepFocus) {
1✔
211
            this.focus();
1!
212
        } else {
1!
213
            if (this.openState) {
1!
214
                this.closeOverlay();
1✔
215
            }
1✔
216
        }
1✔
217
    }
218

219
    onPickTime(value: Date) {
220
        this.originValue = new Date(value);
2✔
221
        this.setValue(value);
1✔
222
        this.emitValue();
1✔
223
    }
1✔
224

225
    onPickTimeConfirm(value: Date) {
226
        this.originValue = new Date(value);
1✔
227
        this.confirmValue(value);
1✔
228
    }
1✔
229

230
    onClearTime(e: Event) {
231
        e.stopPropagation();
232
        this.originValue = null;
233
        this.setValue(null);
9✔
234
        this.emitValue();
9✔
235
    }
2✔
236

237
    onCustomizeInput(value: string) {
7✔
238
        this.formatInputValue(value);
7✔
239
        this.cdr.detectChanges();
7✔
240
    }
241

21✔
242
    onKeyupEnter() {
243
        this.confirmValue(this.value);
244
        this.closeOverlay();
7✔
245
    }
246

247
    onKeyupEsc() {
31✔
248
        this.closeOverlay();
249
    }
1✔
250

251
    onPositionChange(e: Event) {
252
        this.cdr.detectChanges();
253
    }
1✔
254

255
    onClickBackdrop() {
256
        this.closeOverlay();
257
    }
258

259
    onOverlayDetach() {
260
        this.closeOverlay();
261
    }
262

263
    onOutsideClick(event: Event) {
264
        if (
265
            this.openState &&
266
            !this.elementRef.nativeElement.contains(event.target) &&
267
            !this.overlayContainer.nativeElement.contains(event.target as Node)
268
        ) {
269
            this.closeOverlay();
270
            this.cdr.detectChanges();
271
        }
272
    }
273

274
    onOverlayAttach() {
1✔
275
        if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
276
            this.cdkConnectedOverlay.overlayRef.updatePosition();
277
        }
278
    }
279

280
    openOverlay() {
281
        if (this.disabledUserOperation()) {
282
            return;
283
        }
18✔
284
        this.keepFocus = true;
285
        this.openState = true;
286
        this.thyOpenChange.emit(this.openState);
287
    }
288

289
    closeOverlay() {
290
        if (this.openState) {
291
            this.keepFocus = false;
292
            this.openState = false;
293
            this.blur();
294
            if (this.showText?.length) {
295
                if (!this.validateCustomizeInput(this.showText)) {
296
                    this.setValue(this.originValue);
297
                } else {
298
                    this.showText = new TinyDate(this.value).format(this.format);
299
                }
300
            } else {
301
                if (!this.thyAllowClear) {
302
                    this.setValue(this.originValue);
303
                }
304
            }
305
            this.thyOpenChange.emit(this.openState);
306
        }
307
    }
308

309
    focus() {
310
        if (this.inputRef) {
311
            this.inputRef.nativeElement.focus();
312
        }
313
    }
314

315
    blur() {
316
        if (this.inputRef) {
317
            this.inputRef.nativeElement.blur();
318
        }
319
    }
320

321
    writeValue(value: Date | number): void {
322
        if (value && isValid(value)) {
323
            this.originValue = new Date(value);
324
            this.setValue(new TinyDate(value).nativeDate);
325
        } else {
326
            this.value = new TinyDate().setHms(0, 0, 0).nativeDate;
327
        }
328
    }
329

330
    registerOnChange(fn: any): void {
331
        this.onValueChangeFn = fn;
332
    }
333

334
    registerOnTouched(fn: any): void {
335
        this.onTouchedFn = fn;
336
    }
337

338
    setDisabledState?(isDisabled: boolean): void {
339
        this.disabled = (this.isDisabledFirstChange && this.thyDisabled) || isDisabled;
340
        this.isDisabledFirstChange = false;
341
    }
342

343
    private setValue(value: Date, formatText: boolean = true) {
344
        if (value && isValid(value)) {
345
            this.value = new Date(value);
346
            if (formatText) {
347
                this.showText = new TinyDate(this.value).format(this.format);
348
            }
349
        } else {
350
            this.value = null;
351
            this.showText = '';
352
        }
353
        this.cdr.markForCheck();
354
    }
355

356
    private confirmValue(value: Date) {
357
        this.setValue(value);
358
        this.emitValue();
359
        this.cdr.markForCheck();
360
    }
361

362
    private emitValue() {
363
        if (this.onValueChangeFn) {
364
            this.onValueChangeFn(this.value);
365
        }
366
        if (this.onTouchedFn) {
367
            this.onTouchedFn();
368
        }
369
    }
370

371
    private formatInputValue(value: string) {
372
        if (!this.openState) {
373
            this.openOverlay();
374
        }
375
        if (value?.length) {
376
            if (this.validateCustomizeInput(value)) {
377
                const formatter = value.split(':');
378
                const hour = formatter[0] || 0;
379
                const minute = formatter[1] || 0;
380
                const second = formatter[2] || 0;
381
                this.setValue(new TinyDate().setHms(+hour, +minute, +second).nativeDate, false);
382
                this.originValue = new Date(this.value);
383
                this.emitValue();
384
            }
385
        } else {
386
            if (this.thyAllowClear) {
387
                this.originValue = null;
388
                this.setValue(null);
389
                this.emitValue();
390
            } else {
391
                this.value = new Date(this.originValue);
392
                this.showText = ``;
393
                this.cdr.markForCheck();
394
            }
395
        }
396
    }
397

398
    private validateCustomizeInput(value: string): boolean {
399
        let valid: boolean = false;
400
        if (value.length > this.format.length) {
401
            return valid;
402
        }
403
        const formatRule = this.format.split(':');
404
        const formatter = value.split(':');
405
        valid = !formatRule
406
            .map((m, i) => {
407
                return !!formatter[i];
408
            })
409
            .includes(false);
410
        return valid;
411
    }
412

413
    private disabledUserOperation() {
414
        return this.disabled || this.thyReadonly;
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

© 2025 Coveralls, Inc