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

atinc / ngx-tethys / cff9a597-43ba-4b85-8a38-4b131b625bb3

07 Nov 2023 10:01AM UTC coverage: 90.187% (+0.02%) from 90.168%
cff9a597-43ba-4b85-8a38-4b131b625bb3

Pull #2886

circleci

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

5224 of 6459 branches covered (0.0%)

Branch coverage included in aggregate %.

50 of 53 new or added lines in 2 files covered. (94.34%)

6 existing lines in 2 files now uncovered.

13138 of 13901 relevant lines covered (94.51%)

978.74 hits per line

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

95.54
/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';
1✔
18
import { ThyIconComponent } from 'ngx-tethys/icon';
UNCOV
19
import { ThyInputDirective } from 'ngx-tethys/input';
×
20
import { DateHelperService } from './date-helper.service';
21
import { CompatibleValue, RangePartType } from './inner-types';
22
import { getFlexibleAdvancedReadableValue } from './picker.util';
148✔
23
import { ThyDateGranularity } from './standard-types';
148✔
24
import { ThyEnterDirective } from 'ngx-tethys/shared';
25
import { BehaviorSubject } from 'rxjs';
26

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

68
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
69
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
4✔
70
    @ViewChild('pickerInput', { static: true }) pickerInput: ElementRef;
4!
71

4✔
72
    @Input()
73
    get flexibleDateGranularity() {
4✔
74
        return this.innerflexibleDateGranularity;
75
    }
76

13✔
77
    set flexibleDateGranularity(granularity: ThyDateGranularity) {
13✔
78
        this.innerflexibleDateGranularity = granularity;
13✔
79
        this.updateReadableDate(this.innerValue);
80
    }
81

4!
NEW
82
    @Input()
×
83
    get value() {
84
        return this.innerValue;
4✔
85
    }
4✔
86

87
    set value(value: TinyDate | TinyDate[] | null) {
88
        this.innerValue = value;
111✔
89
        if (!this.entering) {
110✔
90
            this.updateReadableDate(this.innerValue);
110!
91
        }
110✔
92
    }
93

110✔
94
    private innerflexibleDateGranularity: ThyDateGranularity;
110✔
95

110!
96
    private innerValue: TinyDate | TinyDate[] | null;
110✔
97

98
    entering = false;
99

100
    readableValue$ = new BehaviorSubject<string | null>(null);
101

102
    prefixCls = 'thy-calendar';
101✔
103

52✔
104
    animationOpenState = false;
52✔
105

46✔
106
    overlayOpen = false; // Available when "open"=undefined
107

52✔
108
    overlayPositions = getFlexiblePositions(this.placement, 4);
52✔
109

110
    get realOpenState(): boolean {
111
        // The value that really decide the open state of overlay
112
        return this.isOpenHandledByUser() ? !!this.open : this.overlayOpen;
114✔
113
    }
111✔
114

115
    get readonlyState(): boolean {
116
        return this.isRange || this.readonly || this.mode !== 'date';
117
    }
10✔
118

119
    constructor(private changeDetector: ChangeDetectorRef, private dateHelper: DateHelperService) {}
120

47✔
121
    ngAfterViewInit(): void {
122
        this.overlayPositions = getFlexiblePositions(this.placement, 4);
123
        if (this.autoFocus) {
214✔
124
            this.focus();
125
        }
126
    }
5✔
127

5✔
128
    focus(): void {
5✔
129
        this.pickerInput.nativeElement.focus();
5✔
130
    }
131

UNCOV
132
    onBlur(event: FocusEvent) {
×
133
        this.blur.emit(event);
134
        if (this.entering) {
135
            this.valueChange.emit(this.pickerInput.nativeElement.value);
661✔
136
        }
258✔
137
        this.entering = false;
138
    }
403✔
139

264✔
140
    onInput(event: InputEvent) {
141
        this.entering = true;
142
        const inputValue = (event.target as HTMLElement)['value'];
139✔
143
        this.inputChange.emit(inputValue);
144
    }
145

146
    onEnter() {
147
        if (this.readonlyState) {
2,402✔
148
            return;
149
        }
150
        this.valueChange.emit(this.pickerInput.nativeElement.value || this.getReadableValue(new TinyDate(new Date())));
151
        this.entering = false;
475✔
152
    }
208✔
153

38✔
154
    showOverlay(): void {
155
        if (!this.realOpenState) {
156
            this.overlayOpen = true;
170✔
157
            if (this.realOpenState) {
170✔
158
                this.animationOpenState = true;
170✔
159
            }
160
            this.openChange.emit(this.overlayOpen);
161
            setTimeout(() => {
162
                if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
267✔
163
                    this.cdkConnectedOverlay.overlayRef.updatePosition();
267✔
164
                }
165
            });
166
        }
167
    }
661✔
168

169
    hideOverlay(): void {
170
        if (this.realOpenState) {
171
            this.overlayOpen = false;
172
            if (!this.realOpenState) {
426✔
173
                this.animationOpenState = false;
426✔
174
            }
26✔
175
            this.openChange.emit(this.overlayOpen);
176
            this.focus();
400✔
177
        }
353✔
178
    }
179

180
    onClickInputBox(): void {
47✔
181
        if (!this.disabled && !this.readonly && !this.isOpenHandledByUser()) {
47✔
182
            this.showOverlay();
47✔
183
        }
184
    }
185

186
    onClickBackdrop(): void {
1✔
187
        this.hideOverlay();
188
    }
189

190
    onOverlayDetach(): void {
1✔
191
        this.hideOverlay();
192
    }
193

194
    onPositionChange(position: ConnectedOverlayPositionChange): void {
195
        this.changeDetector.detectChanges();
196
    }
197

198
    onClickClear(event: MouseEvent): void {
199
        event.preventDefault();
200
        event.stopPropagation();
201

202
        this.innerValue = this.isRange ? [] : null;
203
        this.valueChange.emit(this.innerValue);
204
    }
205

206
    getPartTypeIndex(partType: RangePartType): number {
207
        return { left: 0, right: 1 }[partType];
208
    }
209

210
    isEmptyValue(value: CompatibleValue | null): boolean {
211
        if (value === null) {
212
            return true;
213
        } else if (this.isRange) {
214
            return !value || !Array.isArray(value) || value.every(val => !val);
215
        } else {
216
            return !value;
1✔
217
        }
218
    }
219

220
    // Whether open state is permanently controlled by user himself
221
    isOpenHandledByUser(): boolean {
222
        return this.open !== undefined;
223
    }
224

225
    getReadableValue(tinyDate: TinyDate | TinyDate[]): string | null {
226
        let value: TinyDate;
227
        if (this.isRange) {
228
            if (this.flexible && this.innerflexibleDateGranularity !== 'day') {
229
                return getFlexibleAdvancedReadableValue(tinyDate as TinyDate[], this.innerflexibleDateGranularity);
230
            } else {
231
                const start = tinyDate[0] ? this.dateHelper.format(tinyDate[0].nativeDate, this.format) : '';
232
                const end = tinyDate[1] ? this.dateHelper.format(tinyDate[1].nativeDate, this.format) : '';
233
                return start && end ? `${start} ~ ${end}` : null;
234
            }
235
        } else {
236
            value = tinyDate as TinyDate;
237
            return value ? this.dateHelper.format(value.nativeDate, this.format) : null;
238
        }
239
    }
240

241
    getPlaceholder(): string {
242
        return this.isRange && this.placeholder && Array.isArray(this.placeholder)
243
            ? (this.placeholder as string[]).join(' ~ ')
244
            : (this.placeholder as string);
245
    }
246

247
    private updateReadableDate(setValue: TinyDate | TinyDate[] | null) {
248
        const readableValue = this.getReadableValue(setValue);
249
        if (readableValue === this.pickerInput.nativeElement['value']) {
250
            return;
251
        }
252
        if (this.readonlyState) {
253
            this.readableValue$.next(readableValue);
254
        } else {
255
            this.readableValue$.next(null);
256
            setTimeout(() => {
257
                this.readableValue$.next(readableValue);
258
            }, 0);
259
        }
260
    }
261
}
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

© 2026 Coveralls, Inc