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

atinc / ngx-tethys / a94de615-1a97-46e8-b07e-aa98619b3649

13 Nov 2024 12:34PM UTC coverage: 90.366% (-0.03%) from 90.398%
a94de615-1a97-46e8-b07e-aa98619b3649

push

circleci

minlovehua
feat: support i18n

5517 of 6751 branches covered (81.72%)

Branch coverage included in aggregate %.

67 of 76 new or added lines in 19 files covered. (88.16%)

55 existing lines in 10 files now uncovered.

13225 of 13989 relevant lines covered (94.54%)

1000.62 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
    inject
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';
19
import { TinyDate, coerceBooleanProperty } from 'ngx-tethys/util';
1✔
20
import { ThyTimePanel } from './time-picker-panel.component';
21
import { ThyIcon } from 'ngx-tethys/icon';
18✔
22
import { NgTemplateOutlet, NgClass } from '@angular/common';
18✔
23
import { ThyInputDirective } from 'ngx-tethys/input';
18✔
24
import { scaleMotion, scaleXMotion, scaleYMotion } from 'ngx-tethys/core';
18✔
25
import { ThyI18nService, ThyI18nTranslate } from 'ngx-tethys/i18n';
18✔
26

18✔
27
export type TimePickerSize = 'xs' | 'sm' | 'md' | 'lg' | 'default';
18✔
28

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

68
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
69

2!
UNCOV
70
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
×
71

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

74
    @ViewChild('overlayContainer', { static: false }) overlayContainer: ElementRef<HTMLElement>;
75

3✔
76
    /**
3✔
77
     * 输入框大小
3✔
78
     * @type 'xs' | 'sm' | 'md' | 'lg' | 'default'
79
     */
80
    @Input() thySize: TimePickerSize = 'default';
1✔
81

1✔
82
    /**
83
     * 输入框提示文字
84
     * @type string
1✔
85
     * @default 选择时间
1✔
86
     */
1✔
87
    @Input() thyPlaceholder: string = this.i18n.translate('timePicker.placeholder');
1✔
88

89
    /**
90
     * 弹出位置
4✔
91
     * @type 'top' | 'topLeft'| 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight' | 'left' | 'leftTop' | 'leftBottom' | 'right' | 'rightTop' | 'rightBottom'
4✔
92
     */
93
    @Input() thyPlacement: ThyPlacement = 'bottomLeft';
94

1✔
95
    /**
1✔
96
     * 展示的日期格式,支持 'HH:mm:ss' | 'HH:mm' | 'mm:ss'
97
     * @type string
98
     * @default HH:mm:ss
1✔
99
     */
100
    @Input() set thyFormat(value: string) {
101
        this.format = value || 'HH:mm:ss';
14✔
102
        if (this.value && isValid(this.value)) {
103
            this.showText = new TinyDate(this.value).format(this.format);
UNCOV
104
        }
×
105
    }
106

107
    /**
7✔
108
     * 小时间隔步长
109
     * @type number
110
     */
6!
111
    @Input() thyHourStep: number = 1;
112

113
    /**
6✔
114
     * 分钟间隔步长
6✔
115
     * @type number
116
     */
117
    @Input() thyMinuteStep: number = 1;
118

15!
119
    /**
15✔
120
     * 秒间隔步长
121
     * @type number
122
     */
123
    @Input() thySecondStep: number = 1;
15!
UNCOV
124

×
125
    /**
126
     * 弹出层组件 class
15✔
127
     * @type string
15✔
128
     */
15✔
129
    @Input() thyPopupClass: string;
130

131
    /**
16✔
132
     * 是否显示弹出层遮罩
8✔
133
     * @type boolean
8✔
134
     * @default false
8✔
135
     */
8✔
136
    @Input({ transform: coerceBooleanProperty }) thyBackdrop: boolean;
7✔
137

1✔
138
    /**
139
     * 禁用
140
     * @type boolean
6✔
141
     * @default false
142
     */
143
    @Input({ transform: coerceBooleanProperty }) set thyDisabled(value: boolean) {
144
        this.disabled = value;
1!
145
    }
1✔
146

147
    get thyDisabled(): boolean {
148
        return this.disabled;
8✔
149
    }
150

151
    /**
152
     * 只读
1!
153
     * @type boolean
1✔
154
     * @default false
155
     */
156
    @Input({ transform: coerceBooleanProperty }) thyReadonly: boolean;
157

8!
158
    /**
8✔
159
     * 展示选择此刻
160
     * @type boolean
161
     */
162
    @Input({ transform: coerceBooleanProperty }) thyShowSelectNow = true;
37✔
163

9✔
164
    /**
9✔
165
     * 可清空值
166
     * @type boolean
167
     */
28✔
168
    @Input({ transform: coerceBooleanProperty }) thyAllowClear = true;
169

170
    /**
171
     * 打开/关闭弹窗事件
18✔
172
     * @type EventEmitter<boolean>
173
     */
174
    @Output() thyOpenChange = new EventEmitter<boolean>();
18✔
175

176
    prefixCls = 'thy-time-picker';
177

18✔
178
    overlayPositions = getFlexiblePositions(this.thyPlacement, 4);
18✔
179

180
    format: string = 'HH:mm:ss';
18✔
181

19✔
182
    disabled: boolean;
17✔
183

17✔
184
    showText: string = '';
16✔
185

186
    openState: boolean;
187

188
    value: Date = new TinyDate().setHms(0, 0, 0).nativeDate;
2✔
189

2✔
190
    originValue: Date;
191

19✔
192
    keepFocus: boolean;
193

194
    private isDisabledFirstChange = true;
2✔
195

2✔
196
    onValueChangeFn: (val: number | Date) => void = () => void 0;
2✔
197

198
    onTouchedFn: () => void = () => void 0;
199

8!
200
    ngOnInit() {}
8✔
201

202
    ngAfterViewInit() {
8!
203
        this.overlayPositions = getFlexiblePositions(this.thyPlacement, 4);
8✔
204
    }
205

206
    onInputPickerClick() {
207
        if (this.disabledUserOperation()) {
4✔
208
            return;
1✔
209
        }
210
        this.openOverlay();
4✔
211
    }
2✔
212

1✔
213
    onInputPickerBlur() {
1!
214
        if (this.keepFocus) {
1!
215
            this.focus();
1!
216
        } else {
1✔
217
            if (this.openState) {
1✔
218
                this.closeOverlay();
1✔
219
            }
220
        }
221
    }
222

2✔
223
    onPickTime(value: Date) {
1✔
224
        this.originValue = new Date(value);
1✔
225
        this.setValue(value);
1✔
226
        this.emitValue();
227
    }
228

1✔
229
    onPickTimeConfirm(value: Date) {
1✔
230
        this.originValue = new Date(value);
1✔
231
        this.confirmValue(value);
232
    }
233

234
    onClearTime(e: Event) {
235
        e.stopPropagation();
9✔
236
        this.originValue = null;
9✔
237
        this.setValue(null);
2✔
238
        this.emitValue();
239
    }
7✔
240

7✔
241
    onCustomizeInput(value: string) {
7✔
242
        this.formatInputValue(value);
243
        this.cdr.detectChanges();
21✔
244
    }
245

246
    onKeyupEnter() {
7✔
247
        this.confirmValue(this.value);
248
        this.closeOverlay();
249
    }
31✔
250

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

255
    onPositionChange(e: Event) {
256
        this.cdr.detectChanges();
257
    }
258

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

263
    onOverlayDetach() {
264
        this.closeOverlay();
265
    }
266

267
    onOutsideClick(event: Event) {
268
        if (
269
            this.openState &&
270
            !this.elementRef.nativeElement.contains(event.target) &&
271
            !this.overlayContainer.nativeElement.contains(event.target as Node)
272
        ) {
1✔
273
            this.closeOverlay();
274
            this.cdr.detectChanges();
275
        }
276
    }
277

278
    onOverlayAttach() {
279
        if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
280
            this.cdkConnectedOverlay.overlayRef.updatePosition();
281
        }
18✔
282
    }
283

284
    openOverlay() {
285
        if (this.disabledUserOperation()) {
286
            return;
287
        }
288
        this.keepFocus = true;
289
        this.openState = true;
290
        this.thyOpenChange.emit(this.openState);
291
    }
292

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

313
    focus() {
314
        if (this.inputRef) {
315
            this.inputRef.nativeElement.focus();
316
        }
317
    }
318

319
    blur() {
320
        if (this.inputRef) {
321
            this.inputRef.nativeElement.blur();
322
        }
323
    }
324

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

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

338
    registerOnTouched(fn: any): void {
339
        this.onTouchedFn = fn;
340
    }
341

342
    setDisabledState?(isDisabled: boolean): void {
343
        this.disabled = (this.isDisabledFirstChange && this.thyDisabled) || isDisabled;
344
        this.isDisabledFirstChange = false;
345
    }
346

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

360
    private confirmValue(value: Date) {
361
        this.setValue(value);
362
        this.emitValue();
363
        this.cdr.markForCheck();
364
    }
365

366
    private emitValue() {
367
        if (this.onValueChangeFn) {
368
            this.onValueChangeFn(this.value);
369
        }
370
        if (this.onTouchedFn) {
371
            this.onTouchedFn();
372
        }
373
    }
374

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

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

417
    private disabledUserOperation() {
418
        return this.disabled || this.thyReadonly;
419
    }
420
}
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