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

atinc / ngx-tethys / 7983a0b7-759c-497d-94f5-a448f6913ad6

22 Nov 2023 10:48AM UTC coverage: 90.279% (+0.008%) from 90.271%
7983a0b7-759c-497d-94f5-a448f6913ad6

Pull #2911

circleci

minlovehua
fix(time-picker): the switch thyDisabled does not take effect when using ngModel #INFR-10583
Pull Request #2911: fix(time-picker): the switch thyDisabled does not take effect when using ngModel #INFR-10583

5296 of 6526 branches covered (0.0%)

Branch coverage included in aggregate %.

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

3 existing lines in 1 file now uncovered.

13203 of 13965 relevant lines covered (94.54%)

976.83 hits per line

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

92.16
/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, InputBoolean, ThyPlacement } from 'ngx-tethys/core';
18
import { TinyDate } from 'ngx-tethys/util';
1✔
19
import { ThyTimePanelComponent } from './time-picker-panel.component';
20
import { ThyIconComponent } from 'ngx-tethys/icon';
22✔
21
import { NgTemplateOutlet, NgIf, NgClass } from '@angular/common';
22!
22
import { ThyInputDirective } from 'ngx-tethys/input';
22✔
23

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

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

62
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
63

3✔
64
    @ViewChild('pickerInput', { static: true }) inputRef: ElementRef<HTMLInputElement>;
1✔
65

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

2!
UNCOV
68
    /**
×
69
     * 输入框大小
70
     * @type 'xs' | 'sm' | 'md' | 'lg' | 'default'
71
     */
72
    @Input() thySize: TimePickerSize = 'default';
73

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

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

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

98
    /**
99
     * 小时间隔步长
15✔
100
     * @type number
101
     */
UNCOV
102
    @Input() thyHourStep: number = 1;
×
103

104
    /**
105
     * 分钟间隔步长
7✔
106
     * @type number
107
     */
108
    @Input() thyMinuteStep: number = 1;
6!
109

110
    /**
111
     * 秒间隔步长
6✔
112
     * @type number
6✔
113
     */
114
    @Input() thySecondStep: number = 1;
115

116
    /**
15!
117
     * 弹出层组件 class
15✔
118
     * @type string
119
     */
120
    @Input() thyPopupClass: string;
121

15!
UNCOV
122
    /**
×
123
     * 是否显示弹出层遮罩
124
     * @type boolean
15✔
125
     * @default false
15✔
126
     */
15✔
127
    @Input() @InputBoolean() thyBackdrop: boolean;
128

129
    /**
16✔
130
     * 禁用
8✔
131
     * @type boolean
8✔
132
     * @default false
8✔
133
     */
8✔
134
    @Input() @InputBoolean() set thyDisabled(value: boolean) {
7✔
135
        this.disabled = value;
1✔
136
    }
137

138
    /**
6✔
139
     * 只读
140
     * @type boolean
141
     * @default false
142
     */
1!
143
    @Input() @InputBoolean() set thyReadonly(value: boolean) {
1✔
144
        this.readonly = value;
145
    }
146

8✔
147
    /**
148
     * 展示选择此刻
149
     * @type boolean
150
     */
1!
151
    @Input() @InputBoolean() thyShowSelectNow = true;
1✔
152

153
    /**
154
     * 可清空值
155
     * @type boolean
8!
156
     */
8✔
157
    @Input() @InputBoolean() thyAllowClear = true;
158

159
    /**
160
     * 打开/关闭弹窗事件
37✔
161
     * @type EventEmitter<boolean>
9✔
162
     */
9✔
163
    @Output() thyOpenChange = new EventEmitter<boolean>();
164

165
    prefixCls = 'thy-time-picker';
28✔
166

167
    overlayPositions = getFlexiblePositions(this.thyPlacement, 4);
168

169
    format: string = 'HH:mm:ss';
18✔
170

171
    disabled: boolean;
172

18✔
173
    readonly: boolean;
174

175
    showText: string = '';
20✔
176

20✔
177
    openState: boolean;
178

18✔
179
    value: Date = new TinyDate().setHms(0, 0, 0).nativeDate;
19✔
180

17✔
181
    originValue: Date;
17✔
182

16✔
183
    keepFocus: boolean;
184

185
    private isDisabledFirstChange = true;
186

2✔
187
    onValueChangeFn: (val: number | Date) => void = () => void 0;
2✔
188

189
    onTouchedFn: () => void = () => void 0;
19✔
190

191
    constructor(private cdr: ChangeDetectorRef, private elementRef: ElementRef) {}
192

2✔
193
    ngOnInit() {}
2✔
194

2✔
195
    ngAfterViewInit() {
196
        this.overlayPositions = getFlexiblePositions(this.thyPlacement, 4);
197
    }
8!
198

8✔
199
    onInputPickerClick() {
200
        if (this.disabledUserOperation()) {
8!
201
            return;
8✔
202
        }
203
        this.openOverlay();
204
    }
205

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

1✔
216
    onPickTime(value: Date) {
1✔
217
        this.originValue = new Date(value);
218
        this.setValue(value);
219
        this.emitValue();
220
    }
2✔
221

1✔
222
    onPickTimeConfirm(value: Date) {
1✔
223
        this.originValue = new Date(value);
1✔
224
        this.confirmValue(value);
225
    }
226

1✔
227
    onClearTime(e: Event) {
1✔
228
        e.stopPropagation();
1✔
229
        this.originValue = null;
230
        this.setValue(null);
231
        this.emitValue();
232
    }
233

9✔
234
    onCustomizeInput(value: string) {
9✔
235
        this.formatInputValue(value);
2✔
236
        this.cdr.detectChanges();
237
    }
7✔
238

7✔
239
    onKeyupEnter() {
7✔
240
        this.confirmValue(this.value);
241
        this.closeOverlay();
21✔
242
    }
243

244
    onKeyupEsc() {
7✔
245
        this.closeOverlay();
246
    }
247

31✔
248
    onPositionChange(e: Event) {
249
        this.cdr.detectChanges();
1✔
250
    }
251

252
    onClickBackdrop() {
253
        this.closeOverlay();
1✔
254
    }
255

256
    onOverlayDetach() {
257
        this.closeOverlay();
258
    }
259

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

271
    onOverlayAttach() {
272
        if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
273
            this.cdkConnectedOverlay.overlayRef.updatePosition();
274
        }
1✔
275
    }
276

277
    openOverlay() {
278
        if (this.disabledUserOperation()) {
1✔
279
            return;
280
        }
281
        this.keepFocus = true;
282
        this.openState = true;
283
        this.thyOpenChange.emit(this.openState);
1✔
284
    }
285

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

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

312
    blur() {
313
        if (this.inputRef) {
314
            this.inputRef.nativeElement.blur();
315
        }
316
    }
317

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

327
    registerOnChange(fn: any): void {
328
        this.onValueChangeFn = fn;
329
    }
330

331
    registerOnTouched(fn: any): void {
332
        this.onTouchedFn = fn;
333
    }
334

335
    setDisabledState?(isDisabled: boolean): void {
336
        this.disabled = (this.isDisabledFirstChange && this.thyDisabled) || isDisabled;
337
        this.isDisabledFirstChange = false;
338
    }
339

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

353
    private confirmValue(value: Date) {
354
        this.setValue(value);
355
        this.emitValue();
356
        this.cdr.markForCheck();
357
    }
358

359
    private emitValue() {
360
        if (this.onValueChangeFn) {
361
            this.onValueChangeFn(this.value);
362
        }
363
        if (this.onTouchedFn) {
364
            this.onTouchedFn();
365
        }
366
    }
367

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

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

410
    private disabledUserOperation() {
411
        return this.disabled || this.readonly;
412
    }
413
}
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