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

atinc / ngx-tethys / cd64db52-e563-41a3-85f3-a0adb87ce135

30 Oct 2024 08:03AM UTC coverage: 90.402% (-0.04%) from 90.438%
cd64db52-e563-41a3-85f3-a0adb87ce135

push

circleci

web-flow
refactor: refactor constructor to the inject function (#3222)

5503 of 6730 branches covered (81.77%)

Branch coverage included in aggregate %.

422 of 429 new or added lines in 170 files covered. (98.37%)

344 existing lines in 81 files now uncovered.

13184 of 13941 relevant lines covered (94.57%)

997.19 hits per line

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

95.27
/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
    Input,
13
    OnChanges,
14
    Output,
15
    SimpleChanges,
16
    ViewChild,
17
    inject
1✔
18
} from '@angular/core';
19

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

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

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

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

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

75
    set format(value: string) {
76
        this.innerFormat = value;
77
        this.updateReadableDate(this.innerValue);
157✔
78
    }
157✔
79

1✔
80
    @Input()
81
    get flexibleDateGranularity() {
82
        return this.innerflexibleDateGranularity;
83
    }
57✔
84

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

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

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

8!
102
    private innerflexibleDateGranularity: ThyDateGranularity;
8✔
103

104
    private innerFormat: string;
105

123✔
106
    private innerValue: TinyDate | TinyDate[] | null;
122✔
107

122✔
108
    entering = false;
122✔
109

122✔
110
    prefixCls = 'thy-calendar';
122!
111

122✔
112
    isShowDatePopup = false;
113

114
    overlayOpen = false; // Available when "open"=undefined
115

116
    overlayPositions = getFlexiblePositions(this.placement, 4);
117

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

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

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

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

50✔
145
    focus(): void {
146
        this.pickerInput.nativeElement.focus();
147
    }
126✔
148

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

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

383✔
163
    onEnter() {
235✔
164
        if (this.readonlyState) {
165
            return;
166
        }
148✔
167
        this.valueChange.emit(this.pickerInput.nativeElement.value || this.getReadableValue(new TinyDate(new Date())));
168
        this.entering = false;
169
    }
170

171
    showOverlay(): void {
1,653✔
172
        if (!this.realOpenState) {
173
            this.overlayOpen = true;
174
            this.showDatePopup();
175

675✔
176
            this.openChange.emit(this.overlayOpen);
252✔
177
            setTimeout(() => {
46✔
178
                if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
179
                    this.cdkConnectedOverlay.overlayRef.updatePosition();
180
                }
206✔
181
            });
206✔
182
        }
206✔
183
    }
184

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

190
            this.openChange.emit(this.overlayOpen);
191
            this.focus();
192
        }
193
    }
197✔
194

13✔
195
    showDatePopup() {
196
        this.isShowDatePopup = true;
197
        this.changeDetector.markForCheck();
184✔
198
    }
199

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

628✔
208
    onClickInputBox(): void {
40✔
209
        if (!this.disabled && !this.readonly && !this.isOpenHandledByUser()) {
210
            this.showOverlay();
588✔
211
        }
212
    }
1✔
213

214
    onClickBackdrop(): void {
215
        this.hideOverlay();
216
    }
217

218
    onOverlayDetach(): void {
219
        this.hideOverlay();
220
    }
221

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

226
    onClickClear(event: MouseEvent): void {
227
        event.preventDefault();
228
        event.stopPropagation();
229

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

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

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

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

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

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

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

285
    private updateReadableDate(setValue: TinyDate | TinyDate[] | null) {
286
        const readableValue = this.getReadableValue(setValue);
287
        if (readableValue === this.pickerInput.nativeElement['value']) {
288
            return;
289
        }
290

291
        this.pickerInput.nativeElement.value = readableValue;
292
    }
293
}
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