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

atinc / ngx-tethys / 68ef226c-f83e-44c1-b8ed-e420a83c5d84

28 May 2025 10:31AM UTC coverage: 10.352% (-80.0%) from 90.316%
68ef226c-f83e-44c1-b8ed-e420a83c5d84

Pull #3460

circleci

pubuzhixing8
chore: xxx
Pull Request #3460: refactor(icon): migrate signal input #TINFR-1476

132 of 6823 branches covered (1.93%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 1 file covered. (71.43%)

11648 existing lines in 344 files now uncovered.

2078 of 14525 relevant lines covered (14.31%)

6.69 hits per line

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

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

×
UNCOV
26
export type TimePickerSize = 'xs' | 'sm' | 'md' | 'lg' | 'default';
×
UNCOV
27

×
UNCOV
28
/**
×
UNCOV
29
 * 时间选择组件
×
UNCOV
30
 * @name thy-time-picker
×
UNCOV
31
 */
×
UNCOV
32
@Component({
×
UNCOV
33
    selector: 'thy-time-picker',
×
UNCOV
34
    templateUrl: './time-picker.component.html',
×
UNCOV
35
    changeDetection: ChangeDetectionStrategy.OnPush,
×
UNCOV
36
    providers: [
×
UNCOV
37
        {
×
UNCOV
38
            provide: NG_VALUE_ACCESSOR,
×
39
            multi: true,
40
            useExisting: forwardRef(() => ThyTimePicker)
UNCOV
41
        }
×
UNCOV
42
    ],
×
UNCOV
43
    host: {
×
44
        class: 'thy-time-picker',
45
        '[class.thy-time-picker-disabled]': `disabled`,
46
        '[class.thy-time-picker-readonly]': `thyReadonly`
UNCOV
47
    },
×
48
    imports: [CdkOverlayOrigin, ThyInputDirective, FormsModule, NgTemplateOutlet, ThyIcon, NgClass, CdkConnectedOverlay, ThyTimePanel],
49
    animations: [scaleXMotion, scaleYMotion, scaleMotion]
UNCOV
50
})
×
51
export class ThyTimePicker implements OnInit, AfterViewInit, ControlValueAccessor {
52
    private cdr = inject(ChangeDetectorRef);
53
    private elementRef = inject(ElementRef);
UNCOV
54
    locale: Signal<ThyTimePickerLocale> = injectLocale('timePicker');
×
55

56
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
UNCOV
57

×
UNCOV
58
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
×
59

UNCOV
60
    @ViewChild('pickerInput', { static: true }) inputRef: ElementRef<HTMLInputElement>;
×
61

62
    @ViewChild('overlayContainer', { static: false }) overlayContainer: ElementRef<HTMLElement>;
UNCOV
63

×
UNCOV
64
    /**
×
65
     * 输入框大小
66
     * @type 'xs' | 'sm' | 'md' | 'lg' | 'default'
UNCOV
67
     */
×
68
    @Input() thySize: TimePickerSize = 'default';
×
69

70
    /**
71
     * 输入框提示文字
72
     * @type string
UNCOV
73
     */
×
UNCOV
74
    @Input() thyPlaceholder: string = this.locale().placeholder;
×
UNCOV
75

×
76
    /**
77
     * 弹出位置
UNCOV
78
     * @type 'top' | 'topLeft'| 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight' | 'left' | 'leftTop' | 'leftBottom' | 'right' | 'rightTop' | 'rightBottom'
×
UNCOV
79
     */
×
80
    @Input() thyPlacement: ThyPlacement = 'bottomLeft';
81

UNCOV
82
    /**
×
UNCOV
83
     * 展示的日期格式,支持 'HH:mm:ss' | 'HH:mm' | 'mm:ss'
×
UNCOV
84
     * @type string
×
UNCOV
85
     * @default HH:mm:ss
×
86
     */
87
    @Input() set thyFormat(value: string) {
UNCOV
88
        this.format = value || 'HH:mm:ss';
×
UNCOV
89
        if (this.value && isValid(this.value)) {
×
90
            this.showText = new TinyDate(this.value).format(this.format);
91
        }
UNCOV
92
    }
×
UNCOV
93

×
94
    /**
95
     * 小时间隔步长
UNCOV
96
     * @type number
×
97
     */
98
    @Input() thyHourStep: number = 1;
UNCOV
99

×
100
    /**
101
     * 分钟间隔步长
102
     * @type number
×
103
     */
104
    @Input() thyMinuteStep: number = 1;
UNCOV
105

×
106
    /**
107
     * 秒间隔步长
UNCOV
108
     * @type number
×
109
     */
110
    @Input() thySecondStep: number = 1;
UNCOV
111

×
UNCOV
112
    /**
×
113
     * 弹出层组件 class
114
     * @type string
115
     */
UNCOV
116
    @Input() thyPopupClass: string;
×
UNCOV
117

×
118
    /**
119
     * 是否显示弹出层遮罩
120
     * @type boolean
UNCOV
121
     * @default false
×
122
     */
×
123
    @Input({ transform: coerceBooleanProperty }) thyBackdrop: boolean;
UNCOV
124

×
UNCOV
125
    /**
×
UNCOV
126
     * 禁用
×
127
     * @type boolean
128
     * @default false
UNCOV
129
     */
×
UNCOV
130
    @Input({ transform: coerceBooleanProperty }) set thyDisabled(value: boolean) {
×
UNCOV
131
        this.disabled = value;
×
UNCOV
132
    }
×
UNCOV
133

×
UNCOV
134
    get thyDisabled(): boolean {
×
UNCOV
135
        return this.disabled;
×
136
    }
137

UNCOV
138
    /**
×
139
     * 只读
140
     * @type boolean
141
     * @default false
UNCOV
142
     */
×
UNCOV
143
    @Input({ transform: coerceBooleanProperty }) thyReadonly: boolean;
×
144

145
    /**
UNCOV
146
     * 展示选择此刻
×
147
     * @type boolean
148
     */
149
    @Input({ transform: coerceBooleanProperty }) thyShowSelectNow = true;
UNCOV
150

×
UNCOV
151
    /**
×
152
     * 可清空值
153
     * @type boolean
154
     */
UNCOV
155
    @Input({ transform: coerceBooleanProperty }) thyAllowClear = true;
×
UNCOV
156

×
157
    /**
158
     * 打开/关闭弹窗事件
159
     * @type EventEmitter<boolean>
UNCOV
160
     */
×
UNCOV
161
    @Output() thyOpenChange = new EventEmitter<boolean>();
×
UNCOV
162

×
163
    prefixCls = 'thy-time-picker';
164

UNCOV
165
    overlayPositions: ConnectionPositionPair[] = getFlexiblePositions(this.thyPlacement, 4);
×
166

167
    format: string = 'HH:mm:ss';
168

UNCOV
169
    disabled: boolean;
×
170

171
    showText: string = '';
UNCOV
172

×
173
    openState: boolean;
174

UNCOV
175
    value: Date = new TinyDate().setHms(0, 0, 0).nativeDate;
×
UNCOV
176

×
177
    originValue: Date;
178

×
UNCOV
179
    keepFocus: boolean;
×
UNCOV
180

×
UNCOV
181
    private isDisabledFirstChange = true;
×
UNCOV
182

×
183
    onValueChangeFn: (val: number | Date) => void = () => void 0;
184

185
    onTouchedFn: () => void = () => void 0;
UNCOV
186

×
UNCOV
187
    ngOnInit() {}
×
188

UNCOV
189
    ngAfterViewInit() {
×
190
        this.overlayPositions = getFlexiblePositions(this.thyPlacement, 4);
191
    }
UNCOV
192

×
UNCOV
193
    onInputPickerClick() {
×
UNCOV
194
        if (this.disabledUserOperation()) {
×
195
            return;
196
        }
UNCOV
197
        this.openOverlay();
×
UNCOV
198
    }
×
199

UNCOV
200
    onInputPickerBlur() {
×
UNCOV
201
        if (this.keepFocus) {
×
202
            this.focus();
203
        } else {
204
            if (this.openState) {
UNCOV
205
                this.closeOverlay();
×
UNCOV
206
            }
×
207
        }
UNCOV
208
    }
×
UNCOV
209

×
UNCOV
210
    onPickTime(value: Date) {
×
UNCOV
211
        this.originValue = new TinyDate(value)?.nativeDate;
×
UNCOV
212
        this.setValue(value);
×
UNCOV
213
        this.emitValue();
×
UNCOV
214
    }
×
UNCOV
215

×
UNCOV
216
    onPickTimeConfirm(value: Date) {
×
217
        this.originValue = new TinyDate(value)?.nativeDate;
218
        this.confirmValue(value);
219
    }
UNCOV
220

×
UNCOV
221
    onClearTime(e: Event) {
×
UNCOV
222
        e.stopPropagation();
×
UNCOV
223
        this.originValue = null;
×
224
        this.setValue(null);
225
        this.emitValue();
UNCOV
226
    }
×
UNCOV
227

×
UNCOV
228
    onCustomizeInput(value: string) {
×
229
        this.formatInputValue(value);
230
        this.cdr.detectChanges();
231
    }
232

UNCOV
233
    onKeyupEnter() {
×
UNCOV
234
        this.confirmValue(this.value);
×
UNCOV
235
        this.closeOverlay();
×
236
    }
UNCOV
237

×
UNCOV
238
    onKeyupEsc() {
×
UNCOV
239
        this.closeOverlay();
×
240
    }
UNCOV
241

×
242
    onPositionChange(e: Event) {
243
        this.cdr.detectChanges();
UNCOV
244
    }
×
245

246
    onClickBackdrop() {
UNCOV
247
        this.closeOverlay();
×
248
    }
249

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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