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

atinc / ngx-tethys / c0ef8457-a839-451f-8b72-80fd73106231

02 Apr 2024 02:27PM UTC coverage: 90.524% (-0.06%) from 90.585%
c0ef8457-a839-451f-8b72-80fd73106231

Pull #3062

circleci

minlovehua
refactor(all): use the transform attribute of @Input() instead of @InputBoolean() and @InputNumber()
Pull Request #3062: refactor(all): use the transform attribute of @input() instead of @InputBoolean() and @InputNumber()

4987 of 6108 branches covered (81.65%)

Branch coverage included in aggregate %.

217 of 223 new or added lines in 82 files covered. (97.31%)

202 existing lines in 53 files now uncovered.

12246 of 12929 relevant lines covered (94.72%)

1055.59 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 { TinyDate } from 'ngx-tethys/util';
3

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

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

162✔
30
/**
162✔
31
 * @private
32
 */
33
@Component({
637✔
34
    selector: 'thy-picker',
35
    exportAs: 'thyPicker',
36
    templateUrl: './picker.component.html',
312✔
37
    changeDetection: ChangeDetectionStrategy.OnPush,
312✔
38
    standalone: true,
304✔
39
    imports: [
40
        CdkOverlayOrigin,
41
        ThyInputDirective,
42
        ThyEnterDirective,
43
        AsyncPipe,
877✔
44
        NgTemplateOutlet,
45
        NgIf,
46
        ThyIcon,
646✔
47
        NgClass,
48
        CdkConnectedOverlay
49
    ],
156✔
50
    animations: [scaleXMotion, scaleYMotion, scaleMotion]
156✔
51
})
156✔
52
export class ThyPicker implements OnChanges, AfterViewInit {
156✔
53
    @Input() isRange = false;
156✔
54
    @Input() open: boolean | undefined = undefined;
156✔
55
    @Input() disabled: boolean;
156✔
56
    @Input() placeholder: string | string[];
156✔
57
    @Input() readonly: boolean;
156✔
58
    @Input() allowClear: boolean;
156✔
59
    @Input() autoFocus: boolean;
156✔
60
    @Input() className: string;
156✔
61
    @Input() size: 'sm' | 'xs' | 'lg' | 'md' | 'default';
156✔
62
    @Input() suffixIcon: string;
156✔
63
    @Input() placement: ThyPlacement = 'bottomLeft';
156✔
64
    @Input() flexible: boolean = false;
65
    @Input() mode: string;
66
    @Input({ transform: booleanAttribute }) hasBackdrop: boolean;
67
    @Output() blur = new EventEmitter<Event>();
327✔
68
    @Output() readonly valueChange = new EventEmitter<TinyDate | TinyDate[] | null>();
8✔
69
    @Output() readonly openChange = new EventEmitter<boolean>(); // Emitted when overlay's open state change
5✔
70
    @Output() readonly inputChange = new EventEmitter<string>();
71

72
    @ViewChild('origin', { static: true }) origin: CdkOverlayOrigin;
3✔
73
    @ViewChild(CdkConnectedOverlay, { static: true }) cdkConnectedOverlay: CdkConnectedOverlay;
74
    @ViewChild('pickerInput', { static: true }) pickerInput: ElementRef;
75
    @ViewChild('overlayContainer', { static: false }) overlayContainer: ElementRef<HTMLElement>;
76

77
    @Input()
156✔
78
    get format() {
156✔
79
        return this.innerFormat;
1✔
80
    }
81

82
    set format(value: string) {
83
        this.innerFormat = value;
56✔
84
        this.updateReadableDate(this.innerValue);
85
    }
86

5✔
87
    @Input()
5!
88
    get flexibleDateGranularity() {
5✔
89
        return this.innerflexibleDateGranularity;
90
    }
5✔
91

92
    set flexibleDateGranularity(granularity: ThyDateGranularity) {
93
        this.innerflexibleDateGranularity = granularity;
17✔
94
        this.updateReadableDate(this.innerValue);
17✔
95
    }
17✔
96

97
    @Input()
98
    get value() {
8!
UNCOV
99
        return this.innerValue;
×
100
    }
101

8!
102
    set value(value: TinyDate | TinyDate[] | null) {
8✔
103
        this.innerValue = value;
104
        if (!this.entering) {
105
            this.updateReadableDate(this.innerValue);
122✔
106
        }
121✔
107
    }
121✔
108

121✔
109
    private innerflexibleDateGranularity: ThyDateGranularity;
121✔
110

121!
111
    private innerFormat: string;
121✔
112

113
    private innerValue: TinyDate | TinyDate[] | null;
114

115
    entering = false;
116

117
    prefixCls = 'thy-calendar';
105✔
118

54✔
119
    isShowDatePopup = false;
54✔
120

54✔
121
    overlayOpen = false; // Available when "open"=undefined
54✔
122

123
    overlayPositions = getFlexiblePositions(this.placement, 4);
124

125
    get realOpenState(): boolean {
126✔
126
        // The value that really decide the open state of overlay
126✔
127
        return this.isOpenHandledByUser() ? !!this.open : this.overlayOpen;
128
    }
129

130
    get readonlyState(): boolean {
57✔
131
        return this.isRange || this.readonly || this.mode !== 'date';
57✔
132
    }
57✔
133

134
    constructor(private changeDetector: ChangeDetectorRef, private dateHelper: DateHelperService) {}
135

136
    ngOnChanges(changes: SimpleChanges): void {
125✔
137
        // open by user
122✔
138
        if (changes.open && changes.open.currentValue !== undefined) {
139
            if (changes.open.currentValue) {
140
                this.showDatePopup();
141
            } else {
12✔
142
                this.closeDatePopup();
143
            }
144
        }
49✔
145
    }
146

147
    ngAfterViewInit(): void {
125✔
148
        this.overlayPositions = getFlexiblePositions(this.placement, 4);
149
        if (this.autoFocus) {
150
            this.focus();
6✔
151
        }
6✔
152
    }
6✔
153

6✔
154
    focus(): void {
155
        this.pickerInput.nativeElement.focus();
UNCOV
156
    }
×
157

158
    onBlur(event: FocusEvent) {
159
        this.blur.emit(event);
637✔
160
        if (this.entering) {
256✔
161
            this.valueChange.emit(this.pickerInput.nativeElement.value);
162
        }
381✔
163
        this.entering = false;
235✔
164
    }
165

166
    onInput(event: InputEvent) {
146✔
167
        this.entering = true;
168
        const inputValue = (event.target as HTMLElement)['value'];
169
        this.inputChange.emit(inputValue);
170
    }
171

1,002✔
172
    onEnter() {
173
        if (this.readonlyState) {
174
            return;
175
        }
670✔
176
        this.valueChange.emit(this.pickerInput.nativeElement.value || this.getReadableValue(new TinyDate(new Date())));
252✔
177
        this.entering = false;
46✔
178
    }
179

180
    showOverlay(): void {
206✔
181
        if (!this.realOpenState) {
206✔
182
            this.overlayOpen = true;
206✔
183
            this.showDatePopup();
184

185
            this.openChange.emit(this.overlayOpen);
186
            setTimeout(() => {
418✔
187
                if (this.cdkConnectedOverlay && this.cdkConnectedOverlay.overlayRef) {
418✔
188
                    this.cdkConnectedOverlay.overlayRef.updatePosition();
189
                }
190
            });
191
        }
192
    }
193

195✔
194
    hideOverlay(): void {
13✔
195
        if (this.realOpenState) {
196
            this.overlayOpen = false;
197
            this.closeDatePopup();
182✔
198

199
            this.openChange.emit(this.overlayOpen);
200
            this.focus();
201
        }
637✔
202
    }
203

204
    showDatePopup() {
205
        this.isShowDatePopup = true;
206
        this.changeDetector.markForCheck();
624✔
207
    }
624✔
208

40✔
209
    closeDatePopup() {
210
        // Delay 200ms before destroying the date-popup, otherwise you will not see the closing animation.
584✔
211
        setTimeout(() => {
212
            this.isShowDatePopup = false;
1✔
213
            this.changeDetector.markForCheck();
214
        }, 200);
215
    }
216

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

223
    onClickBackdrop(): void {
224
        this.hideOverlay();
225
    }
226

227
    onOverlayDetach(): void {
228
        this.hideOverlay();
229
    }
230

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

235
    onClickClear(event: MouseEvent): void {
236
        event.preventDefault();
237
        event.stopPropagation();
238

239
        this.innerValue = this.isRange ? [] : null;
240
        this.valueChange.emit(this.innerValue);
241
    }
242

243
    getPartTypeIndex(partType: RangePartType): number {
244
        return { left: 0, right: 1 }[partType];
1✔
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.innerflexibleDateGranularity !== 'day') {
266
                return getFlexibleAdvancedReadableValue(tinyDate as TinyDate[], this.innerflexibleDateGranularity);
267
            } else {
268
                const start = tinyDate[0] ? this.formatDate(tinyDate[0]) : '';
269
                const end = tinyDate[1] ? this.formatDate(tinyDate[1]) : '';
270
                return start && end ? `${start} ~ ${end}` : null;
271
            }
272
        } else {
273
            value = tinyDate as TinyDate;
274
            return value ? this.formatDate(value) : null;
275
        }
276
    }
277

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

288
    getPlaceholder(): string {
289
        return this.isRange && this.placeholder && Array.isArray(this.placeholder)
290
            ? (this.placeholder as string[]).join(' ~ ')
291
            : (this.placeholder as string);
292
    }
293

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

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