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

atinc / ngx-tethys / edbc1d43-1648-411a-a6bc-f24c9aa3f654

27 Mar 2025 06:13AM UTC coverage: 90.236% (+0.06%) from 90.179%
edbc1d43-1648-411a-a6bc-f24c9aa3f654

push

circleci

web-flow
Merge pull request #3282 from atinc/v19.0.0-next

5598 of 6865 branches covered (81.54%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 7 files covered. (100.0%)

157 existing lines in 46 files now uncovered.

13357 of 14141 relevant lines covered (94.46%)

992.51 hits per line

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

95.29
/src/date-picker/picker.component.ts
1
import { getFlexiblePositions, ThyPlacement } from 'ngx-tethys/core';
2
import { coerceBooleanProperty, 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
    inject,
13
    Input,
14
    OnChanges,
15
    Output,
16
    SimpleChanges,
17
    ViewChild
18
} from '@angular/core';
1✔
19

20
import { AsyncPipe, NgClass, NgTemplateOutlet } from '@angular/common';
159✔
21
import { scaleMotion, scaleXMotion, scaleYMotion } from 'ngx-tethys/core';
159✔
22
import { ThyI18nService } from 'ngx-tethys/i18n';
159✔
23
import { ThyIcon } from 'ngx-tethys/icon';
159✔
24
import { ThyInputDirective } from 'ngx-tethys/input';
159✔
25
import { ThyEnterDirective } from 'ngx-tethys/shared';
159✔
26
import { DateHelperService } from './date-helper.service';
159✔
27
import { CompatibleValue, RangePartType } from './inner-types';
159✔
28
import { getFlexibleAdvancedReadableValue } from './picker.util';
159✔
29
import { ThyDateGranularity } from './standard-types';
159✔
30

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

47
    @Input() isRange = false;
48
    @Input() open: boolean | undefined = undefined;
165✔
49
    @Input() disabled: boolean;
165✔
50
    @Input() placeholder: string | string[];
51
    @Input() readonly: boolean;
52
    @Input() allowClear: boolean;
656✔
53
    @Input() autoFocus: boolean;
54
    @Input() className: string;
55
    @Input() size: 'sm' | 'xs' | 'lg' | 'md' | 'default';
320✔
56
    @Input() suffixIcon: string;
320✔
57
    @Input() placement: ThyPlacement = 'bottomLeft';
312✔
58
    @Input() flexible: boolean = false;
59
    @Input() mode: string;
60
    @Input({ transform: coerceBooleanProperty }) hasBackdrop: boolean;
61
    @Input() separator: string;
62
    @Output() blur = new EventEmitter<Event>();
1,561✔
63
    @Output() readonly valueChange = new EventEmitter<TinyDate | TinyDate[] | null>();
64
    @Output() readonly openChange = new EventEmitter<boolean>(); // Emitted when overlay's open state change
65
    @Output() readonly inputChange = new EventEmitter<string>();
665✔
66

67
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
68
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
69
    @ViewChild('pickerInput', { static: true }) pickerInput: ElementRef;
337✔
70
    @ViewChild('overlayContainer', { static: false }) overlayContainer: ElementRef<HTMLElement>;
8✔
71

5✔
72
    @Input()
73
    get format() {
74
        return this.innerFormat;
3✔
75
    }
76

77
    set format(value: string) {
78
        this.innerFormat = value;
79
        this.updateReadableDate(this.innerValue);
159✔
80
    }
159✔
81

1✔
82
    @Input()
83
    get flexibleDateGranularity() {
84
        return this.innerflexibleDateGranularity;
85
    }
59✔
86

87
    set flexibleDateGranularity(granularity: ThyDateGranularity) {
88
        this.innerflexibleDateGranularity = granularity;
5✔
89
        this.updateReadableDate(this.innerValue);
5!
90
    }
5✔
91

92
    @Input()
5✔
93
    get value() {
94
        return this.innerValue;
95
    }
17✔
96

17✔
97
    set value(value: TinyDate | TinyDate[] | null) {
17✔
98
        this.innerValue = value;
99
        if (!this.entering) {
100
            this.updateReadableDate(this.innerValue);
8!
UNCOV
101
        }
×
102
    }
103

8!
104
    private innerflexibleDateGranularity: ThyDateGranularity;
8✔
105

106
    private innerFormat: string;
107

125✔
108
    private innerValue: TinyDate | TinyDate[] | null;
124✔
109

124✔
110
    entering = false;
124✔
111

124✔
112
    prefixCls = 'thy-calendar';
124!
113

124✔
114
    isShowDatePopup = false;
115

116
    overlayOpen = false; // Available when "open"=undefined
117

118
    overlayPositions = getFlexiblePositions(this.placement, 4);
119

111✔
120
    get realOpenState(): boolean {
57✔
121
        // The value that really decide the open state of overlay
57✔
122
        return this.isOpenHandledByUser() ? !!this.open : this.overlayOpen;
57✔
123
    }
57✔
124

125
    get readonlyState(): boolean {
126
        return this.isRange || this.readonly || this.mode !== 'date';
127
    }
129✔
128

129✔
129
    ngOnChanges(changes: SimpleChanges): void {
130
        // open by user
131
        if (changes.open && changes.open.currentValue !== undefined) {
132
            if (changes.open.currentValue) {
60✔
133
                this.showDatePopup();
60✔
134
            } else {
60✔
135
                this.closeDatePopup();
136
            }
137
        }
138
    }
128✔
139

125✔
140
    ngAfterViewInit(): void {
141
        this.overlayPositions = getFlexiblePositions(this.placement, 4);
142
        if (this.autoFocus) {
143
            this.focus();
12✔
144
        }
145
    }
146

52✔
147
    focus(): void {
148
        this.pickerInput.nativeElement.focus();
149
    }
128✔
150

151
    onBlur(event: FocusEvent) {
152
        this.blur.emit(event);
6✔
153
        if (this.entering) {
6✔
154
            this.valueChange.emit(this.pickerInput.nativeElement.value);
6✔
155
        }
6✔
156
        this.entering = false;
157
    }
UNCOV
158

×
159
    onInput(event: InputEvent) {
160
        this.entering = true;
161
        const inputValue = (event.target as HTMLElement)['value'];
656✔
162
        this.inputChange.emit(inputValue);
259✔
163
    }
164

397✔
165
    onEnter() {
249✔
166
        if (this.readonlyState) {
167
            return;
168
        }
148✔
169
        this.valueChange.emit(this.pickerInput.nativeElement.value || this.getReadableValue(new TinyDate()));
170
        this.entering = false;
171
    }
172

173
    showOverlay(): void {
1,689✔
174
        if (!this.realOpenState) {
175
            this.overlayOpen = true;
176
            this.showDatePopup();
177

687✔
178
            this.openChange.emit(this.overlayOpen);
264✔
179
            setTimeout(() => {
46✔
180
                if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
181
                    this.cdkConnectedOverlay.overlayRef.updatePosition();
182
                }
218✔
183
            });
218✔
184
        }
218✔
185
    }
186

187
    hideOverlay(): void {
188
        if (this.realOpenState) {
423✔
189
            this.overlayOpen = false;
423✔
190
            this.closeDatePopup();
191

192
            this.openChange.emit(this.overlayOpen);
193
            this.focus();
194
        }
195
    }
205✔
196

13✔
197
    showDatePopup() {
198
        this.isShowDatePopup = true;
199
        this.changeDetector.markForCheck();
192✔
200
    }
201

202
    closeDatePopup() {
203
        // Delay 200ms before destroying the date-popup, otherwise you will not see the closing animation.
656✔
204
        setTimeout(() => {
205
            this.isShowDatePopup = false;
206
            this.changeDetector.markForCheck();
207
        }, 200);
208
    }
638✔
209

638✔
210
    onClickInputBox(): void {
40✔
211
        if (!this.disabled && !this.readonly && !this.isOpenHandledByUser()) {
212
            this.showOverlay();
598✔
213
        }
214
    }
1✔
215

216
    onClickBackdrop(): void {
217
        this.hideOverlay();
218
    }
219

220
    onOverlayDetach(): void {
221
        this.hideOverlay();
222
    }
223

224
    onPositionChange(position: ConnectedOverlayPositionChange): void {
225
        this.changeDetector.detectChanges();
226
    }
227

228
    onClickClear(event: MouseEvent): void {
229
        event.preventDefault();
230
        event.stopPropagation();
231

232
        this.innerValue = this.isRange ? [] : null;
233
        this.valueChange.emit(this.innerValue);
234
    }
235

236
    getPartTypeIndex(partType: RangePartType): number {
237
        return { left: 0, right: 1 }[partType];
238
    }
239

240
    isEmptyValue(value: CompatibleValue | null): boolean {
241
        if (value === null) {
242
            return true;
243
        } else if (this.isRange) {
1✔
244
            return !value || !Array.isArray(value) || value.every(val => !val);
245
        } else {
246
            return !value;
247
        }
248
    }
249

250
    // Whether open state is permanently controlled by user himself
251
    isOpenHandledByUser(): boolean {
252
        return this.open !== undefined;
253
    }
254

255
    getReadableValue(tinyDate: TinyDate | TinyDate[]): string | null {
256
        let value: TinyDate;
257
        if (this.isRange) {
258
            if (this.flexible && this.innerflexibleDateGranularity !== 'day') {
259
                return getFlexibleAdvancedReadableValue(
260
                    tinyDate as TinyDate[],
261
                    this.innerflexibleDateGranularity,
262
                    this.separator,
263
                    this.i18n.getLocale()
264
                );
265
            } else {
266
                const start = tinyDate[0] ? this.formatDate(tinyDate[0]) : '';
267
                const end = tinyDate[1] ? this.formatDate(tinyDate[1]) : '';
268
                return start && end ? `${start}${this.separator}${end}` : null;
269
            }
270
        } else {
271
            value = tinyDate as TinyDate;
272
            return value ? this.formatDate(value) : null;
273
        }
274
    }
275

276
    formatDate(value: TinyDate) {
277
        // dateHelper.format() 使用的是 angular 的 format,不支持季度,修改的话,改动比较大。
278
        // 此处通过对 innerFormat 做下判断,如果是季度的 format,使用 date-fns 的 format()
279
        if (this.innerFormat && (this.innerFormat.includes('q') || this.innerFormat.includes('Q'))) {
280
            return value.format(this.innerFormat);
281
        } else {
282
            return this.dateHelper.format(value.nativeDate, this.innerFormat);
283
        }
284
    }
285

286
    getPlaceholder(): string {
287
        return this.isRange && this.placeholder && Array.isArray(this.placeholder)
288
            ? (this.placeholder as string[]).join(this.separator)
289
            : (this.placeholder as string);
290
    }
291

292
    private updateReadableDate(setValue: TinyDate | TinyDate[] | null) {
293
        const readableValue = this.getReadableValue(setValue);
294
        if (readableValue === this.pickerInput.nativeElement['value']) {
295
            return;
296
        }
297

298
        this.pickerInput.nativeElement.value = readableValue;
299
    }
300
}
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