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

atinc / ngx-tethys / #94

12 Aug 2025 05:53AM UTC coverage: 90.345% (+0.02%) from 90.324%
#94

push

web-flow
Merge 79e13dd53 into aa9fa8ee2

5531 of 6813 branches covered (81.18%)

Branch coverage included in aggregate %.

350 of 378 new or added lines in 20 files covered. (92.59%)

61 existing lines in 11 files now uncovered.

13970 of 14772 relevant lines covered (94.57%)

904.12 hits per line

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

96.57
/src/date-picker/picker.component.ts
1
import { getFlexiblePositions, ThyPlacement } from 'ngx-tethys/core';
2
import { coerceBooleanProperty, TinyDate } from 'ngx-tethys/util';
3
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedOverlayPositionChange, ConnectionPositionPair } from '@angular/cdk/overlay';
4
import {
5
    AfterViewInit,
6
    ChangeDetectionStrategy,
7
    ChangeDetectorRef,
8
    Component,
9
    ElementRef,
10
    inject,
11
    input,
12
    effect,
13
    OnChanges,
14
    output,
15
    SimpleChanges,
16
    viewChild
17
} from '@angular/core';
18
import { NgClass, NgTemplateOutlet } from '@angular/common';
19
import { scaleMotion, scaleXMotion, scaleYMotion } from 'ngx-tethys/core';
1✔
20
import { ThyI18nService } from 'ngx-tethys/i18n';
21
import { ThyIcon } from 'ngx-tethys/icon';
22
import { ThyInputDirective } from 'ngx-tethys/input';
1,569✔
23
import { ThyEnterDirective } from 'ngx-tethys/shared';
24
import { DateHelperService } from './date-helper.service';
25
import { CompatibleValue, RangePartType } from './inner-types';
669✔
26
import { getFlexibleAdvancedReadableValue } from './picker.util';
27
import { ThyDateGranularity } from './standard-types';
28

160✔
29
/**
160✔
30
 * @private
160✔
31
 */
160✔
32
@Component({
160✔
33
    selector: 'thy-picker',
160✔
34
    exportAs: 'thyPicker',
160✔
35
    templateUrl: './picker.component.html',
160✔
36
    changeDetection: ChangeDetectionStrategy.OnPush,
160✔
37
    imports: [CdkOverlayOrigin, ThyInputDirective, ThyEnterDirective, NgTemplateOutlet, ThyIcon, NgClass, CdkConnectedOverlay],
160✔
38
    animations: [scaleXMotion, scaleYMotion, scaleMotion]
160✔
39
})
160✔
40
export class ThyPicker implements OnChanges, AfterViewInit {
160✔
41
    private changeDetector = inject(ChangeDetectorRef);
160✔
42

160✔
43
    private dateHelper = inject(DateHelperService);
160✔
44

160✔
45
    private i18n = inject(ThyI18nService);
160✔
46

160✔
47
    readonly isRange = input(false, { transform: coerceBooleanProperty });
160✔
48

160✔
49
    readonly open = input<boolean | undefined>(undefined);
160✔
50

160✔
51
    readonly disabled = input(false, { transform: coerceBooleanProperty });
160✔
52

160✔
53
    readonly placeholder = input<string | string[]>();
160✔
54

160✔
55
    readonly readonly = input(false, { transform: coerceBooleanProperty });
160✔
56

160✔
57
    readonly allowClear = input(false, { transform: coerceBooleanProperty });
160✔
58

160✔
59
    readonly autoFocus = input(false, { transform: coerceBooleanProperty });
160✔
60

160✔
61
    readonly className = input<string>();
160✔
62

160✔
63
    readonly size = input<'sm' | 'xs' | 'lg' | 'md' | 'default'>();
160✔
64

324✔
65
    readonly suffixIcon = input<string>();
324✔
66

216✔
67
    readonly placement = input<ThyPlacement>('bottomLeft');
208✔
68

69
    readonly flexible = input(false, { transform: coerceBooleanProperty });
70

71
    readonly mode = input<string>();
160✔
72

162✔
73
    readonly hasBackdrop = input(false, { transform: coerceBooleanProperty });
162!
74

162✔
75
    readonly separator = input<string>();
76

77
    readonly timeZone = input<string>();
160✔
78

170✔
79
    readonly blur = output<Event>();
170✔
80

8✔
81
    readonly valueChange = output<TinyDate | TinyDate[] | null>();
82

83
    readonly openChange = output<boolean>(); // Emitted when overlay's open state change
160✔
84

162✔
85
    readonly inputChange = output<string>();
3✔
86

87
    readonly origin = viewChild<CdkOverlayOrigin>('origin');
88

89
    readonly cdkConnectedOverlay = viewChild<CdkConnectedOverlay>(CdkConnectedOverlay);
90

91
    readonly pickerInput = viewChild<ElementRef>('pickerInput');
341✔
92

8✔
93
    readonly overlayContainer = viewChild<ElementRef<HTMLElement>>('overlayContainer');
5✔
94

95
    readonly format = input<string>();
96

3✔
97
    readonly flexibleDateGranularity = input<ThyDateGranularity>();
98

99
    readonly value = input<TinyDate | TinyDate[] | null>();
100

101
    private innerflexibleDateGranularity: ThyDateGranularity;
160✔
102

160✔
103
    private innerFormat: string;
1✔
104

105
    private innerValue: TinyDate | TinyDate[] | null;
106

107
    entering = false;
59✔
108

109
    prefixCls = 'thy-calendar';
110

5✔
111
    isShowDatePopup = false;
5!
112

5✔
113
    overlayOpen = false; // Available when "open"=undefined
114

5✔
115
    overlayPositions: ConnectionPositionPair[] = getFlexiblePositions(this.placement(), 4);
116

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

122
    get readonlyState(): boolean {
8!
NEW
123
        return this.isRange() || this.readonly() || this.mode() !== 'date';
×
124
    }
125

8!
126
    constructor() {
8✔
127
        effect(() => {
128
            this.innerValue = this.value();
129
            if (this.innerValue) {
125✔
130
                if (!this.entering) {
124✔
131
                    this.updateReadableDate(this.innerValue);
124✔
132
                }
124✔
133
            }
124✔
134
        });
124!
135

124✔
136
        effect(() => {
137
            this.innerFormat = this.format();
138
            if (this.innerFormat) {
139
                this.updateReadableDate(this.innerValue);
140
            }
141
        });
111✔
142

57✔
143
        effect(() => {
57✔
144
            this.innerflexibleDateGranularity = this.flexibleDateGranularity();
57✔
145
            if (this.innerflexibleDateGranularity) {
57✔
146
                this.updateReadableDate(this.innerValue);
147
            }
148
        });
149

129✔
150
        effect(() => {
129✔
151
            if (this.timeZone()) {
152
                this.formatDate(this.innerValue as TinyDate);
153
            }
154
        });
60✔
155
    }
60✔
156

60✔
157
    ngOnChanges(changes: SimpleChanges): void {
158
        // open by user
159
        if (changes.open && changes.open.currentValue !== undefined) {
160
            if (changes.open.currentValue) {
128✔
161
                this.showDatePopup();
125✔
162
            } else {
163
                this.closeDatePopup();
164
            }
165
        }
12✔
166
    }
167

168
    ngAfterViewInit(): void {
52✔
169
        this.overlayPositions = getFlexiblePositions(this.placement(), 4);
170
        if (this.autoFocus()) {
171
            this.focus();
128✔
172
        }
173
    }
174

6✔
175
    focus(): void {
6✔
176
        this.pickerInput()?.nativeElement.focus();
6✔
177
    }
6✔
178

179
    onBlur(event: FocusEvent) {
UNCOV
180
        this.blur.emit(event);
×
181
        if (this.entering) {
182
            this.valueChange.emit(this.pickerInput()?.nativeElement.value);
183
        }
660✔
184
        this.entering = false;
260✔
185
    }
186

400✔
187
    onInput(event: InputEvent) {
249✔
188
        this.entering = true;
189
        const inputValue = (event.target as HTMLElement)['value'];
190
        this.inputChange.emit(inputValue);
151✔
191
    }
192

193
    onEnter() {
194
        if (this.readonlyState) {
195
            return;
1,697✔
196
        }
197
        this.valueChange.emit(this.pickerInput()?.nativeElement.value || this.getReadableValue(new TinyDate(undefined, this.timeZone())));
198
        this.entering = false;
199
    }
427✔
200

207✔
201
    showOverlay(): void {
44✔
202
        if (!this.realOpenState) {
203
            this.overlayOpen = true;
204
            this.showDatePopup();
163✔
205

163✔
206
            this.openChange.emit(this.overlayOpen);
163✔
207
            setTimeout(() => {
208
                if (this.cdkConnectedOverlay() && this.cdkConnectedOverlay()?.overlayRef) {
209
                    this.cdkConnectedOverlay()?.overlayRef.updatePosition();
210
                }
220✔
211
            });
220✔
212
        }
213
    }
214

215
    hideOverlay(): void {
216
        if (this.realOpenState) {
217
            this.overlayOpen = false;
211✔
218
            this.closeDatePopup();
13✔
219

220
            this.openChange.emit(this.overlayOpen);
221
            this.focus();
198✔
222
        }
223
    }
224

225
    showDatePopup() {
660✔
226
        this.isShowDatePopup = true;
227
        this.changeDetector.markForCheck();
228
    }
229

230
    closeDatePopup() {
378✔
231
        // Delay 200ms before destroying the date-popup, otherwise you will not see the closing animation.
378✔
232
        setTimeout(() => {
38✔
233
            this.isShowDatePopup = false;
234
            this.changeDetector.markForCheck();
340✔
235
        }, 200);
236
    }
1✔
237

1✔
238
    onClickInputBox(): void {
239
        if (!this.disabled() && !this.readonly() && !this.isOpenHandledByUser()) {
240
            this.showOverlay();
241
        }
242
    }
243

244
    onClickBackdrop(): void {
245
        this.hideOverlay();
246
    }
247

248
    onOverlayDetach(): void {
249
        this.hideOverlay();
250
    }
251

252
    onPositionChange(position: ConnectedOverlayPositionChange): void {
253
        this.changeDetector.detectChanges();
254
    }
255

256
    onClickClear(event: MouseEvent): void {
257
        event.preventDefault();
258
        event.stopPropagation();
259

260
        this.innerValue = this.isRange() ? [] : null;
261
        this.valueChange.emit(this.innerValue);
262
    }
263

264
    getPartTypeIndex(partType: RangePartType): number {
265
        return { left: 0, right: 1 }[partType];
266
    }
267

1✔
268
    isEmptyValue(value: CompatibleValue | null): boolean {
269
        if (value === null) {
270
            return true;
271
        } else if (this.isRange()) {
272
            return !value || !Array.isArray(value) || value.every(val => !val);
273
        } else {
274
            return !value;
275
        }
276
    }
277

278
    // Whether open state is permanently controlled by user himself
279
    isOpenHandledByUser(): boolean {
280
        return this.open() !== undefined;
281
    }
282

283
    getReadableValue(tinyDate: TinyDate | TinyDate[]): string | null {
284
        let value: TinyDate;
285
        if (this.isRange()) {
286
            if (this.flexible() && this.innerflexibleDateGranularity !== 'day') {
287
                return getFlexibleAdvancedReadableValue(
288
                    tinyDate as TinyDate[],
289
                    this.innerflexibleDateGranularity,
290
                    this.separator(),
291
                    this.i18n.getLocale()
292
                );
293
            } else {
294
                const start = tinyDate[0] ? this.formatDate(tinyDate[0]) : '';
295
                const end = tinyDate[1] ? this.formatDate(tinyDate[1]) : '';
296
                return start && end ? `${start}${this.separator()}${end}` : null;
297
            }
298
        } else {
299
            value = tinyDate as TinyDate;
300
            return value ? this.formatDate(value) : null;
301
        }
302
    }
303

304
    formatDate(value: TinyDate) {
305
        // dateHelper.format() 使用的是 angular 的 format,不支持季度,修改的话,改动比较大。
306
        // 此处通过对 innerFormat 做下判断,如果是季度的 format,使用 date-fns 的 format()
307
        if (this.innerFormat && (this.innerFormat.includes('q') || this.innerFormat.includes('Q'))) {
308
            return value.format(this.innerFormat);
309
        } else {
310
            return this.dateHelper.format(value?.nativeDate, this.innerFormat);
311
        }
312
    }
313

314
    getPlaceholder(): string {
315
        return this.isRange() && this.placeholder() && Array.isArray(this.placeholder())
316
            ? (this.placeholder() as string[]).join(this.separator())
317
            : (this.placeholder() as string);
318
    }
319

320
    private updateReadableDate(setValue: TinyDate | TinyDate[] | null) {
321
        const readableValue = this.getReadableValue(setValue);
322
        if (readableValue === this.pickerInput()?.nativeElement['value']) {
323
            return;
324
        }
325

326
        this.pickerInput().nativeElement.value = readableValue;
327
    }
328
}
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