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

atinc / ngx-tethys / e1289f47-715a-4c2d-a6ec-393315def93d

06 Nov 2023 02:54AM UTC coverage: 90.192% (+0.02%) from 90.168%
e1289f47-715a-4c2d-a6ec-393315def93d

Pull #2886

circleci

su4g
feat(date-picker): date picker allowed input date
Pull Request #2886: feat(date-picker): date picker allowed input date

5226 of 6461 branches covered (0.0%)

Branch coverage included in aggregate %.

64 of 67 new or added lines in 3 files covered. (95.52%)

6 existing lines in 2 files now uncovered.

13157 of 13921 relevant lines covered (94.51%)

977.48 hits per line

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

94.31
/src/date-picker/picker.component.ts
1
import { getFlexiblePositions, ThyPlacement } from 'ngx-tethys/core';
2
import { TinyDate } from 'ngx-tethys/util';
3

4
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange } from '@angular/cdk/overlay';
5
import {
6
    AfterViewInit,
7
    ChangeDetectionStrategy,
8
    ChangeDetectorRef,
9
    Component,
10
    ElementRef,
11
    EventEmitter,
12
    Input,
13
    Output,
14
    ViewChild
15
} from '@angular/core';
16

17
import { AsyncPipe, NgClass, NgIf, NgTemplateOutlet } from '@angular/common';
18
import { ThyIconComponent } from 'ngx-tethys/icon';
19
import { ThyInputDirective } from 'ngx-tethys/input';
1✔
20
import { DateHelperService } from './date-helper.service';
UNCOV
21
import { CompatibleValue, RangePartType } from './inner-types';
×
22
import { getFlexibleAdvancedReadableValue, isValidDateString, parseFormatDate, transformDateValue } from './picker.util';
23
import { DisabledDateFn, ThyDateGranularity } from './standard-types';
24
import { ThyEnterDirective } from 'ngx-tethys/shared';
148✔
25
import { BehaviorSubject, Subject } from 'rxjs';
148✔
26
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
27
import { filter, map } from 'rxjs/operators';
28

669✔
29
/**
30
 * @private
31
 */
287✔
32
@Component({
287✔
33
    selector: 'thy-picker',
202✔
34
    exportAs: 'thyPicker',
35
    templateUrl: './picker.component.html',
287✔
36
    changeDetection: ChangeDetectionStrategy.OnPush,
278✔
37
    standalone: true,
38
    imports: [
39
        CdkOverlayOrigin,
40
        ThyInputDirective,
41
        ThyEnterDirective,
1,640✔
42
        AsyncPipe,
43
        NgTemplateOutlet,
44
        NgIf,
1,078✔
45
        ThyIconComponent,
46
        NgClass,
47
        CdkConnectedOverlay
478✔
48
    ]
49
})
50
export class ThyPickerComponent implements AfterViewInit {
142✔
51
    @Input() isRange = false;
142✔
52
    @Input() open: boolean | undefined = undefined;
142✔
53
    @Input() disabled: boolean;
142✔
54
    @Input() placeholder: string | string[];
142✔
55
    @Input() readonly: boolean;
142✔
56
    @Input() allowClear: boolean;
142✔
57
    @Input() autoFocus: boolean;
142✔
58
    @Input() className: string;
142✔
59
    @Input() format: string;
142✔
60
    @Input() size: 'sm' | 'xs' | 'lg' | 'md' | 'default';
142✔
61
    // @Input() value: TinyDate | TinyDate[] | null;
142✔
62
    @Input() suffixIcon: string;
142✔
63
    @Input() placement: ThyPlacement = 'bottomLeft';
142✔
64
    @Input() flexible: boolean = false;
142✔
65
    @Input() max: Date | number;
142✔
66
    @Input() min: Date | number;
142✔
67
    @Input() disabledDate: DisabledDateFn;
142✔
68
    @Output() blur = new EventEmitter<Event>();
142✔
69
    @Output() readonly valueChange = new EventEmitter<TinyDate | TinyDate[] | null>();
70
    @Output() readonly openChange = new EventEmitter<boolean>(); // Emitted when overlay's open state change
71
    @Output() readonly updateDate = new EventEmitter<TinyDate | TinyDate[] | null>();
142✔
72
    @Output() readonly enterChange = new EventEmitter<TinyDate | TinyDate[] | null>();
142✔
73

1✔
74
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
75
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
142✔
76
    @ViewChild('pickerInput', { static: true }) pickerInput: ElementRef;
77

13✔
78
    @Input()
1✔
79
    get flexibleDateGranularity() {
80
        return this._flexibleDateGranularity;
13✔
81
    }
13✔
82

13✔
83
    set flexibleDateGranularity(granularity: ThyDateGranularity) {
3✔
84
        this._flexibleDateGranularity = granularity;
85
        this.updateReadableDate(this._value);
13✔
86
    }
87

10✔
88
    @Input()
89
    get value() {
90
        return this._value;
10✔
91
    }
92

93
    set value(value: TinyDate | TinyDate[] | null) {
94
        this._value = value;
54✔
95
        if (!this._previousDate) {
96
            this._previousDate = this._value;
NEW
97
        }
×
98
        if (!this.onTuoched) {
99
            this.updateReadableDate(this._value);
100
        }
13✔
101
    }
13✔
102

13✔
103
    private _flexibleDateGranularity: ThyDateGranularity;
104
    private _value: TinyDate | TinyDate[] | null;
105
    private _inputDate$ = new Subject<string>();
4!
NEW
106
    private _previousDate: TinyDate | TinyDate[] | null;
×
107
    onTuoched = false;
108
    readableValue$ = new BehaviorSubject<string | null>(null);
4✔
109
    prefixCls = 'thy-calendar';
110
    animationOpenState = false;
1!
111
    overlayOpen = false; // Available when "open"=undefined
4✔
112
    overlayPositions = getFlexiblePositions(this.placement, 4);
4✔
113
    takeUntilDestroyed = takeUntilDestroyed();
114

115
    get realOpenState(): boolean {
111✔
116
        // The value that really decide the open state of overlay
110✔
117
        return this.isOpenHandledByUser() ? !!this.open : this.overlayOpen;
110!
118
    }
110✔
119

120
    get readonlyState(): boolean {
110✔
121
        return this.isRange || this.readonly || !this.format || !this.validFormat;
110✔
122
    }
110!
123

110✔
124
    get validFormat() {
125
        return this.format.includes('yyyy') && this.format.includes('MM') && this.format.includes('dd');
126
    }
127

128
    constructor(private changeDetector: ChangeDetectorRef, private dateHelper: DateHelperService) {}
129

106✔
130
    ngAfterViewInit(): void {
52✔
131
        this.overlayPositions = getFlexiblePositions(this.placement, 4);
52✔
132
        if (this.autoFocus) {
46✔
133
            this.focus();
134
        }
52✔
135

52✔
136
        this._inputDate$
137
            .pipe(
138
                this.takeUntilDestroyed,
139
                filter((str: string) => {
114✔
140
                    if (!str) {
111✔
141
                        this._previousDate = null;
142
                    }
143
                    const formatValid = isValidDateString(str, this.format);
144
                    const limitValid = this.isValidDateLimit(
10✔
145
                        new TinyDate(parseFormatDate(str, this.format)),
146
                        this.min,
147
                        this.max,
47✔
148
                        this.disabledDate
149
                    );
150
                    if (!formatValid || !limitValid) {
214✔
151
                        this.updateDate.emit(null);
152
                    }
153
                    return formatValid && limitValid;
5✔
154
                }),
5✔
155
                map(date => {
5✔
156
                    return new TinyDate(parseFormatDate(date, this.format));
5✔
157
                })
5✔
158
            )
159
            .subscribe((date: TinyDate) => {
NEW
160
                this.updateDate.emit(date);
×
161
            });
162
    }
163

669✔
164
    focus(): void {
261✔
165
        this.pickerInput.nativeElement.focus();
166
    }
408✔
167

269✔
168
    onBlur(event: FocusEvent) {
169
        this.blur.emit(event);
170
    }
139✔
171

172
    onInput(event: InputEvent) {
173
        this.onTuoched = true;
174
        const inputValue = (event.target as HTMLElement)['value'];
175
        this._inputDate$.next(inputValue);
2,423✔
176
    }
177

178
    onEnter() {
179
        if (this.readonlyState) {
430✔
180
            return;
184✔
181
        }
31✔
182
        const setValue =
183
            this._value ||
184
            this._previousDate ||
153✔
185
            (this.isValidDateLimit(new TinyDate(new Date()), this.min, this.max, this.disabledDate) ? new TinyDate(new Date()) : null);
153✔
186
        this.updateValue(setValue);
153✔
187
        this._previousDate = setValue;
188
    }
189

190
    showOverlay(): void {
246✔
191
        if (!this.realOpenState) {
246✔
192
            this.overlayOpen = true;
193
            if (this.realOpenState) {
194
                this.animationOpenState = true;
195
            }
669✔
196
            this.openChange.emit(this.overlayOpen);
197
            setTimeout(() => {
198
                if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
199
                    this.cdkConnectedOverlay.overlayRef.updatePosition();
4✔
200
                }
9✔
201
            });
4✔
202
        }
4✔
203
    }
204

205
    hideOverlay(): void {
5✔
206
        if (this.realOpenState) {
207
            this.overlayOpen = false;
208
            if (!this.realOpenState) {
209
                this.animationOpenState = false;
430✔
210
            }
430✔
211
            this.openChange.emit(this.overlayOpen);
28✔
212
            this.focus();
213
        }
402✔
214
    }
353✔
215

216
    onClickInputBox(): void {
217
        if (!this.disabled && !this.readonly && !this.isOpenHandledByUser()) {
49✔
218
            this.showOverlay();
49✔
219
        }
49✔
220
    }
221

222
    onClickBackdrop(): void {
223
        this.hideOverlay();
224
    }
14✔
225

14!
UNCOV
226
    onOverlayDetach(): void {
×
227
        this.hideOverlay();
228
    }
14✔
229

14!
230
    onPositionChange(position: ConnectedOverlayPositionChange): void {
14!
231
        this.changeDetector.detectChanges();
232
    }
233

234
    onClickClear(event: MouseEvent): void {
1✔
235
        event.preventDefault();
236
        event.stopPropagation();
237

238
        this._previousDate = null;
1✔
239
        this._value = this.isRange ? [] : null;
240
        this.updateValue(this._value, false);
241
    }
242

243
    getPartTypeIndex(partType: RangePartType): number {
244
        return { left: 0, right: 1 }[partType];
245
    }
246

247
    isEmptyValue(value: CompatibleValue | null): boolean {
248
        if (value === null) {
249
            return true;
250
        } else if (this.isRange) {
251
            return !value || !Array.isArray(value) || value.every(val => !val);
252
        } else {
253
            return !value;
254
        }
255
    }
256

257
    // Whether open state is permanently controlled by user himself
258
    isOpenHandledByUser(): boolean {
259
        return this.open !== undefined;
260
    }
261

262
    getReadableValue(tinyDate: TinyDate | TinyDate[]): string | null {
263
        let value: TinyDate;
264
        if (this.isRange) {
265
            if (this.flexible && this._flexibleDateGranularity !== 'day') {
266
                return getFlexibleAdvancedReadableValue(tinyDate as TinyDate[], this._flexibleDateGranularity);
267
            } else {
1✔
268
                const start = tinyDate[0] ? this.dateHelper.format(tinyDate[0].nativeDate, this.format) : '';
269
                const end = tinyDate[1] ? this.dateHelper.format(tinyDate[1].nativeDate, this.format) : '';
270
                return start && end ? `${start} ~ ${end}` : null;
271
            }
272
        } else {
273
            value = tinyDate as TinyDate;
274
            return value ? this.dateHelper.format(value.nativeDate, this.format) : null;
275
        }
276
    }
277

278
    getPlaceholder(): string {
279
        return this.isRange && this.placeholder && Array.isArray(this.placeholder)
280
            ? (this.placeholder as string[]).join(' ~ ')
281
            : (this.placeholder as string);
282
    }
283

284
    private updateValue(setValue: TinyDate | TinyDate[] | null, sourceEnter = true) {
285
        if (sourceEnter) {
286
            this.enterChange.emit(setValue);
287
            this.updateReadableDate(setValue);
288
        } else {
289
            this.valueChange.emit(setValue);
290
        }
291
    }
292

293
    private updateReadableDate(setValue: TinyDate | TinyDate[] | null) {
294
        const readableValue = this.getReadableValue(setValue);
295
        if (readableValue === this.pickerInput.nativeElement['value']) {
296
            return;
297
        }
298
        if (this.readonlyState) {
299
            this.readableValue$.next(readableValue);
300
        } else {
301
            this.readableValue$.next(null);
302
            setTimeout(() => {
303
                this.readableValue$.next(readableValue);
304
            }, 0);
305
        }
306
    }
307

308
    private isValidDateLimit(date: TinyDate, min: Date | number, max: Date | number, disabledDate: DisabledDateFn): boolean {
309
        let disable = false;
310
        if (disabledDate !== undefined) {
311
            disable = disabledDate(date.nativeDate);
312
        }
313
        const minDate = min ? new TinyDate(transformDateValue(min).value as Date) : null;
314
        const maxDate = max ? new TinyDate(transformDateValue(max).value as Date) : null;
315
        return (
316
            (!minDate || date.startOfDay().nativeDate >= minDate.startOfDay().nativeDate) &&
317
            (!maxDate || date.startOfDay().nativeDate <= maxDate.startOfDay().nativeDate) &&
318
            !disable
319
        );
320
    }
321
}
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