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

atinc / ngx-tethys / 5fa9630c-19a1-4ee7-af3d-6a0c3535952a

08 Oct 2024 08:24AM UTC coverage: 90.438% (+0.007%) from 90.431%
5fa9630c-19a1-4ee7-af3d-6a0c3535952a

push

circleci

minlovehua
refactor: refactor all control-flow directives to new control-flow #TINFR-381

5511 of 6738 branches covered (81.79%)

Branch coverage included in aggregate %.

98 of 104 new or added lines in 58 files covered. (94.23%)

113 existing lines in 17 files now uncovered.

13253 of 14010 relevant lines covered (94.6%)

991.73 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
    ChangeDetectionStrategy,
5
    ChangeDetectorRef,
6
    Component,
7
    ElementRef,
8
    EventEmitter,
9
    forwardRef,
10
    Input,
11
    OnInit,
12
    Output,
13
    ViewChild
14
} from '@angular/core';
15
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
16
import { isValid } from 'date-fns';
17
import { getFlexiblePositions, ThyPlacement } from 'ngx-tethys/core';
18
import { TinyDate, coerceBooleanProperty } from 'ngx-tethys/util';
1✔
19
import { ThyTimePanel } from './time-picker-panel.component';
20
import { ThyIcon } from 'ngx-tethys/icon';
22✔
21
import { NgTemplateOutlet, NgClass } from '@angular/common';
22!
22
import { ThyInputDirective } from 'ngx-tethys/input';
22✔
23
import { scaleMotion, scaleXMotion, scaleYMotion } from 'ngx-tethys/core';
24

25
export type TimePickerSize = 'xs' | 'sm' | 'md' | 'lg' | 'default';
26

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

54
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
18✔
55

56
    @ViewChild('pickerInput', { static: true }) inputRef: ElementRef<HTMLInputElement>;
57

16✔
58
    @ViewChild('overlayContainer', { static: false }) overlayContainer: ElementRef<HTMLElement>;
2✔
59

60
    /**
14✔
61
     * 输入框大小
62
     * @type 'xs' | 'sm' | 'md' | 'lg' | 'default'
63
     */
3✔
64
    @Input() thySize: TimePickerSize = 'default';
1✔
65

66
    /**
67
     * 输入框提示文字
2!
UNCOV
68
     * @type string
×
69
     */
70
    @Input() thyPlaceholder: string = '选择时间';
71

72
    /**
73
     * 弹出位置
3✔
74
     * @type 'top' | 'topLeft'| 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight' | 'left' | 'leftTop' | 'leftBottom' | 'right' | 'rightTop' | 'rightBottom'
3✔
75
     */
3✔
76
    @Input() thyPlacement: ThyPlacement = 'bottomLeft';
77

78
    /**
1✔
79
     * 展示的日期格式,支持 'HH:mm:ss' | 'HH:mm' | 'mm:ss'
1✔
80
     * @type string
81
     * @default HH:mm:ss
82
     */
1✔
83
    @Input() set thyFormat(value: string) {
1✔
84
        this.format = value || 'HH:mm:ss';
1✔
85
        if (this.value && isValid(this.value)) {
1✔
86
            this.showText = new TinyDate(this.value).format(this.format);
87
        }
88
    }
4✔
89

4✔
90
    /**
91
     * 小时间隔步长
92
     * @type number
1✔
93
     */
1✔
94
    @Input() thyHourStep: number = 1;
95

96
    /**
1✔
97
     * 分钟间隔步长
98
     * @type number
99
     */
14✔
100
    @Input() thyMinuteStep: number = 1;
101

UNCOV
102
    /**
×
103
     * 秒间隔步长
104
     * @type number
105
     */
7✔
106
    @Input() thySecondStep: number = 1;
107

108
    /**
6!
109
     * 弹出层组件 class
110
     * @type string
111
     */
6✔
112
    @Input() thyPopupClass: string;
6✔
113

114
    /**
115
     * 是否显示弹出层遮罩
116
     * @type boolean
15!
117
     * @default false
15✔
118
     */
119
    @Input({ transform: coerceBooleanProperty }) thyBackdrop: boolean;
120

121
    /**
15!
UNCOV
122
     * 禁用
×
123
     * @type boolean
124
     * @default false
15✔
125
     */
15✔
126
    @Input({ transform: coerceBooleanProperty }) set thyDisabled(value: boolean) {
15✔
127
        this.disabled = value;
128
    }
129

16✔
130
    get thyDisabled(): boolean {
8✔
131
        return this.disabled;
8✔
132
    }
8✔
133

8✔
134
    /**
7✔
135
     * 只读
1✔
136
     * @type boolean
137
     * @default false
138
     */
6✔
139
    @Input({ transform: coerceBooleanProperty }) thyReadonly: boolean;
140

141
    /**
142
     * 展示选择此刻
1!
143
     * @type boolean
1✔
144
     */
145
    @Input({ transform: coerceBooleanProperty }) thyShowSelectNow = true;
146

8✔
147
    /**
148
     * 可清空值
149
     * @type boolean
150
     */
1!
151
    @Input({ transform: coerceBooleanProperty }) thyAllowClear = true;
1✔
152

153
    /**
154
     * 打开/关闭弹窗事件
155
     * @type EventEmitter<boolean>
8!
156
     */
8✔
157
    @Output() thyOpenChange = new EventEmitter<boolean>();
158

159
    prefixCls = 'thy-time-picker';
160

37✔
161
    overlayPositions = getFlexiblePositions(this.thyPlacement, 4);
9✔
162

9✔
163
    format: string = 'HH:mm:ss';
164

165
    disabled: boolean;
28✔
166

167
    showText: string = '';
168

169
    openState: boolean;
18✔
170

171
    value: Date = new TinyDate().setHms(0, 0, 0).nativeDate;
172

18✔
173
    originValue: Date;
174

175
    keepFocus: boolean;
18✔
176

18✔
177
    private isDisabledFirstChange = true;
178

18✔
179
    onValueChangeFn: (val: number | Date) => void = () => void 0;
19✔
180

17✔
181
    onTouchedFn: () => void = () => void 0;
17✔
182

16✔
183
    constructor(
184
        private cdr: ChangeDetectorRef,
185
        private elementRef: ElementRef
186
    ) {}
2✔
187

2✔
188
    ngOnInit() {}
189

19✔
190
    ngAfterViewInit() {
191
        this.overlayPositions = getFlexiblePositions(this.thyPlacement, 4);
192
    }
2✔
193

2✔
194
    onInputPickerClick() {
2✔
195
        if (this.disabledUserOperation()) {
196
            return;
197
        }
8!
198
        this.openOverlay();
8✔
199
    }
200

8!
201
    onInputPickerBlur() {
8✔
202
        if (this.keepFocus) {
203
            this.focus();
204
        } else {
205
            if (this.openState) {
4✔
206
                this.closeOverlay();
1✔
207
            }
208
        }
4✔
209
    }
2✔
210

1✔
211
    onPickTime(value: Date) {
1!
212
        this.originValue = new Date(value);
1!
213
        this.setValue(value);
1!
214
        this.emitValue();
1✔
215
    }
1✔
216

1✔
217
    onPickTimeConfirm(value: Date) {
218
        this.originValue = new Date(value);
219
        this.confirmValue(value);
220
    }
2✔
221

1✔
222
    onClearTime(e: Event) {
1✔
223
        e.stopPropagation();
1✔
224
        this.originValue = null;
225
        this.setValue(null);
226
        this.emitValue();
1✔
227
    }
1✔
228

1✔
229
    onCustomizeInput(value: string) {
230
        this.formatInputValue(value);
231
        this.cdr.detectChanges();
232
    }
233

9✔
234
    onKeyupEnter() {
9✔
235
        this.confirmValue(this.value);
2✔
236
        this.closeOverlay();
237
    }
7✔
238

7✔
239
    onKeyupEsc() {
7✔
240
        this.closeOverlay();
241
    }
21✔
242

243
    onPositionChange(e: Event) {
244
        this.cdr.detectChanges();
7✔
245
    }
246

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

251
    onOverlayDetach() {
252
        this.closeOverlay();
253
    }
1✔
254

255
    onOutsideClick(event: Event) {
256
        if (
257
            this.openState &&
258
            !this.elementRef.nativeElement.contains(event.target) &&
259
            !this.overlayContainer.nativeElement.contains(event.target as Node)
260
        ) {
261
            this.closeOverlay();
262
            this.cdr.detectChanges();
263
        }
264
    }
265

266
    onOverlayAttach() {
267
        if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
268
            this.cdkConnectedOverlay.overlayRef.updatePosition();
269
        }
270
    }
271

272
    openOverlay() {
273
        if (this.disabledUserOperation()) {
274
            return;
1✔
275
        }
276
        this.keepFocus = true;
277
        this.openState = true;
278
        this.thyOpenChange.emit(this.openState);
279
    }
280

281
    closeOverlay() {
282
        if (this.openState) {
283
            this.keepFocus = false;
18✔
284
            this.openState = false;
285
            this.blur();
286
            if (this.showText?.length) {
287
                if (!this.validateCustomizeInput(this.showText)) {
288
                    this.setValue(this.originValue);
289
                } else {
290
                    this.showText = new TinyDate(this.value).format(this.format);
291
                }
292
            } else {
293
                if (!this.thyAllowClear) {
294
                    this.setValue(this.originValue);
295
                }
296
            }
297
            this.thyOpenChange.emit(this.openState);
298
        }
299
    }
300

301
    focus() {
302
        if (this.inputRef) {
303
            this.inputRef.nativeElement.focus();
304
        }
305
    }
306

307
    blur() {
308
        if (this.inputRef) {
309
            this.inputRef.nativeElement.blur();
310
        }
311
    }
312

313
    writeValue(value: Date | number): void {
314
        if (value && isValid(value)) {
315
            this.originValue = new Date(value);
316
            this.setValue(new TinyDate(value).nativeDate);
317
        } else {
318
            this.value = new TinyDate().setHms(0, 0, 0).nativeDate;
319
        }
320
    }
321

322
    registerOnChange(fn: any): void {
323
        this.onValueChangeFn = fn;
324
    }
325

326
    registerOnTouched(fn: any): void {
327
        this.onTouchedFn = fn;
328
    }
329

330
    setDisabledState?(isDisabled: boolean): void {
331
        this.disabled = (this.isDisabledFirstChange && this.thyDisabled) || isDisabled;
332
        this.isDisabledFirstChange = false;
333
    }
334

335
    private setValue(value: Date, formatText: boolean = true) {
336
        if (value && isValid(value)) {
337
            this.value = new Date(value);
338
            if (formatText) {
339
                this.showText = new TinyDate(this.value).format(this.format);
340
            }
341
        } else {
342
            this.value = null;
343
            this.showText = '';
344
        }
345
        this.cdr.markForCheck();
346
    }
347

348
    private confirmValue(value: Date) {
349
        this.setValue(value);
350
        this.emitValue();
351
        this.cdr.markForCheck();
352
    }
353

354
    private emitValue() {
355
        if (this.onValueChangeFn) {
356
            this.onValueChangeFn(this.value);
357
        }
358
        if (this.onTouchedFn) {
359
            this.onTouchedFn();
360
        }
361
    }
362

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

390
    private validateCustomizeInput(value: string): boolean {
391
        let valid: boolean = false;
392
        if (value.length > this.format.length) {
393
            return valid;
394
        }
395
        const formatRule = this.format.split(':');
396
        const formatter = value.split(':');
397
        valid = !formatRule
398
            .map((m, i) => {
399
                return !!formatter[i];
400
            })
401
            .includes(false);
402
        return valid;
403
    }
404

405
    private disabledUserOperation() {
406
        return this.disabled || this.thyReadonly;
407
    }
408
}
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