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

atinc / ngx-tethys / 44babf5a-ba67-4c9a-b2f0-5ed6c0ed4f4d

22 Dec 2023 06:36AM UTC coverage: 90.345% (+0.01%) from 90.332%
44babf5a-ba67-4c9a-b2f0-5ed6c0ed4f4d

Pull #2985

circleci

minlovehua
fix(date-picker): when the date picker component's thyHasBackdrop value is false, clicking outside should close #INFR-11053
Pull Request #2985: fix(date-picker): when the date picker directive's and component's thyHasBackdrop value is false, clicking outside should close #INFR-11053

5338 of 6569 branches covered (0.0%)

Branch coverage included in aggregate %.

15 of 19 new or added lines in 7 files covered. (78.95%)

5 existing lines in 3 files now uncovered.

13284 of 14043 relevant lines covered (94.6%)

975.88 hits per line

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

90.48
/src/date-picker/base-picker.component.ts
1
import { InputBoolean, ThyClickDispatcher, ThyPlacement } from 'ngx-tethys/core';
2
import { coerceBooleanProperty, elementMatchClosest, FunctionProp, TinyDate } from 'ngx-tethys/util';
3

4
import {
5
    ChangeDetectorRef,
6
    Component,
7
    ElementRef,
8
    EventEmitter,
9
    Inject,
10
    Input,
11
    NgZone,
12
    OnChanges,
13
    OnInit,
1✔
14
    Output,
15
    PLATFORM_ID,
141✔
16
    TemplateRef,
141✔
17
    ViewChild
5✔
18
} from '@angular/core';
19

20
import { AbstractPickerComponent } from './abstract-picker.component';
21
import { CompatibleValue, RangeAdvancedValue } from './inner-types';
736✔
22
import { CompatibleDate, ThyPanelMode } from './standard-types';
23
import { ThyPickerComponent } from './picker.component';
24
import { hasTimeInStringDate, isValidStringDate, parseStringDate, transformDateValue } from './picker.util';
481✔
25
import { isPlatformBrowser } from '@angular/common';
26
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
27

65!
28
/**
29
 * @private
30
 */
142✔
31
@Component({
142✔
32
    template: ``,
142✔
33
    standalone: true,
142✔
34
    host: {
142✔
35
        '[attr.tabindex]': `tabIndex`,
142✔
36
        '(focus)': 'onFocus($event)',
142✔
37
        '(blur)': 'onBlur($event)'
142✔
38
    }
142✔
39
})
142✔
40
export class BasePickerComponent extends AbstractPickerComponent implements OnInit, OnChanges {
142✔
41
    showWeek = false;
142✔
42

142✔
43
    panelMode: ThyPanelMode | ThyPanelMode[];
44

45
    initialized: boolean;
142✔
46

142✔
47
    private innerPreviousDate: string;
142✔
48

142!
49
    @ViewChild('thyPicker', { static: true }) thyPicker: ThyPickerComponent;
142✔
50

51
    @Input() thyDateRender: FunctionProp<TemplateRef<Date> | string>;
52

53
    @Input() set thyMode(value: ThyPanelMode) {
209✔
54
        this._panelMode = value ?? 'date';
55
        if (this.initialized) {
56
            this.setDefaultTimePickerState(this._panelMode);
1✔
57
        }
1✔
58
    }
1✔
59

60
    get thyMode() {
61
        return this._panelMode;
62
    }
63

64
    /**
65
     * 是否有幕布
47✔
66
     * @default true
47✔
67
     */
47✔
68
    @Input() @InputBoolean() thyHasBackdrop = true;
47✔
69

39✔
70
    /**
71
     * @type EventEmitter<ThyPanelMode | ThyPanelMode[]>
47✔
72
     */
73
    @Output() readonly thyOnPanelChange = new EventEmitter<ThyPanelMode | ThyPanelMode[]>();
74

13✔
75
    /**
5✔
76
     * @type EventEmitter<Date[]>
5✔
77
     */
5✔
78
    @Output() readonly thyOnCalendarChange = new EventEmitter<Date[]>();
5✔
79

80
    private _showTime: object | boolean;
8✔
81

8✔
82
    /**
8!
83
     * 增加时间选择功能
8✔
84
     * @default false
5✔
85
     */
86
    @Input() get thyShowTime(): object | boolean {
87
        return this._showTime;
3✔
88
    }
89
    set thyShowTime(value: object | boolean) {
8✔
90
        this._showTime = typeof value === 'object' ? value : coerceBooleanProperty(value);
8✔
91
    }
8✔
92

93
    /**
94
     * 是否展示时间(时、分)
95
     * @default false
147✔
96
     */
147✔
97
    @Input() @InputBoolean() thyMustShowTime = false;
58✔
98

99
    /**
100
     * 弹出位置
89✔
101
     * @type top | topLeft | topRight | bottom | bottomLeft | bottomRight | left | leftTop | leftBottom | right | rightTop | rightBottom
102
     */
147✔
103
    @Input() thyPlacement: ThyPlacement = 'bottomLeft';
147✔
104

120✔
105
    /**
106
     * @type EventEmitter<CompatibleDate | null>
107
     */
108
    @Output() readonly thyOnOk = new EventEmitter<CompatibleDate | null>();
120✔
109

110
    takeUntilDestroyed = takeUntilDestroyed();
120✔
111

112
    constructor(
113
        cdr: ChangeDetectorRef,
114
        protected element: ElementRef,
115
        protected thyClickDispatcher: ThyClickDispatcher,
60✔
116
        @Inject(PLATFORM_ID) protected platformId: string,
9✔
117
        protected ngZone: NgZone
118
    ) {
119
        super(cdr);
120
    }
121

10!
122
    ngOnInit(): void {
15✔
123
        super.ngOnInit();
10✔
124
        this.setDefaultTimePickerState(this._panelMode);
125
        this.initialized = true;
126

127
        if (isPlatformBrowser(this.platformId)) {
62✔
128
            this.thyClickDispatcher
129
                .clicked(0)
130
                .pipe(this.takeUntilDestroyed)
5!
NEW
131
                .subscribe((event: Event) => {
×
NEW
132
                    if (
×
NEW
133
                        !this.element.nativeElement.contains(event.target) &&
×
134
                        !this.thyPicker?.overlayContainer?.nativeElement.contains(event.target as Node) &&
135
                        this.realOpenState
NEW
136
                    ) {
×
137
                        this.ngZone.run(() => {
138
                            this.closeOverlay();
139
                            this.cdr.markForCheck();
140
                        });
5!
141
                    }
5✔
142
                });
143
        }
UNCOV
144
    }
×
145

146
    onValueChange(value: CompatibleValue | RangeAdvancedValue): void {
147
        this.thyPicker.entering = false;
5✔
148
        this.restoreTimePickerState(value as CompatibleValue);
149
        super.onValueChange(value);
150
        if (!this.flexible) {
168✔
151
            this.closeOverlay();
168✔
152
        }
54✔
153
        this.innerPreviousDate = this.thyPicker.getReadableValue(this.thyValue);
154
    }
155

156
    onInputValueChange(formatDate: string | null | Array<null>) {
1✔
157
        if (!formatDate || !formatDate.length) {
158
            const compatibleValue = formatDate ? (formatDate as CompatibleValue) : null;
159
            this.restoreTimePickerState(compatibleValue);
160
            super.onValueChange(compatibleValue);
5✔
161
            return;
1✔
162
        }
163
        let value = formatDate as string;
4✔
164
        const valueValid = isValidStringDate(value);
165
        const valueLimitValid = valueValid ? this.isValidDateLimit(parseStringDate(value)) : false;
166
        if (valueValid && valueLimitValid) {
13!
167
            this.innerPreviousDate = value;
13✔
168
        } else {
4✔
169
            value = this.innerPreviousDate;
170
        }
13✔
171
        const tinyDate = value ? (this.thyShowTime ? parseStringDate(value) : parseStringDate(value).startOfDay()) : null;
172
        this.restoreTimePickerState(tinyDate);
173
        super.onValueChange(tinyDate);
174
    }
8✔
175

8!
UNCOV
176
    // Displays the time directly when the time must be displayed by default
×
177
    setDefaultTimePickerState(value: ThyPanelMode) {
178
        this.withTime = this.thyMustShowTime;
8✔
179
        if (this.isRange) {
8✔
180
            this.panelMode = this.flexible ? ['date', 'date'] : [value, value];
8✔
181
        } else {
182
            this.panelMode = value;
183
        }
184
        this.showWeek = value === 'week';
1✔
185
        if (!this.thyFormat) {
186
            const inputFormats: { [key in ThyPanelMode]?: string } = {
187
                year: 'yyyy',
188
                month: 'yyyy-MM',
189
                week: 'yyyy-ww周',
190
                date: this.thyShowTime ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd'
191
            };
1✔
192
            this.thyFormat = this.flexible ? inputFormats['date'] : inputFormats[value];
193
        }
194
    }
195

196
    // Restore after clearing time to select whether the original picker time is displayed or not
197
    restoreTimePickerState(value: CompatibleValue | null) {
198
        if (!value) {
199
            this.withTime = this.thyMustShowTime || this.originWithTime;
200
        }
201
    }
202

203
    // Emit thyOnCalendarChange when select date by thy-range-picker
204
    onCalendarChange(value: TinyDate[]): void {
1✔
205
        if (this.isRange) {
206
            const rangeValue = value.map(x => x.nativeDate);
207
            this.thyOnCalendarChange.emit(rangeValue);
208
        }
1✔
209
    }
210

211
    onShowTimePickerChange(show: boolean): void {
212
        this.withTime = show;
1✔
213
    }
214

215
    onResultOk(): void {
216
        if (this.isRange) {
217
            const value = this.thyValue as TinyDate[];
218
            if (value.length) {
219
                this.thyOnOk.emit([value[0].nativeDate, value[1].nativeDate]);
220
            } else {
221
                this.thyOnOk.emit([]);
222
            }
223
        } else {
224
            if (this.thyValue) {
225
                this.thyOnOk.emit((this.thyValue as TinyDate).nativeDate);
226
            } else {
227
                this.thyOnOk.emit(null);
228
            }
229
        }
230
        this.closeOverlay();
231
    }
232

233
    onOpenChange(open: boolean): void {
234
        this.thyOpenChange.emit(open);
235
        if (!open) {
236
            this.onTouchedFn();
237
        }
238
    }
239

240
    onFocus(event: Event) {
241
        this.picker.focus();
242
    }
243

244
    onBlur(event?: FocusEvent) {
245
        // Tab 聚焦后自动聚焦到 input 输入框,此分支下直接返回,无需触发 onTouchedFn
246
        if (elementMatchClosest(event?.relatedTarget as HTMLElement, ['date-popup', 'thy-picker'])) {
247
            return;
248
        }
249
        this.onTouchedFn();
250
    }
251

252
    onInputDate(value: string) {
253
        if (value && isValidStringDate(value)) {
254
            if (this.thyShowTime) {
255
                this.withTime = hasTimeInStringDate(value);
256
            }
257
            this.thyValue = parseStringDate(value);
258
        }
259
    }
260

261
    private isValidDateLimit(date: TinyDate): boolean {
262
        let disable = false;
263
        if (this.thyDisabledDate !== undefined) {
264
            disable = this.thyDisabledDate(date.nativeDate);
265
        }
266
        const minDate = this.thyMinDate ? new TinyDate(transformDateValue(this.thyMinDate).value as Date) : null;
267
        const maxDate = this.thyMaxDate ? new TinyDate(transformDateValue(this.thyMaxDate).value as Date) : null;
268
        return (
269
            (!minDate || date.startOfDay().nativeDate >= minDate.startOfDay().nativeDate) &&
270
            (!maxDate || date.startOfDay().nativeDate <= maxDate.startOfDay().nativeDate) &&
271
            !disable
272
        );
273
    }
274
}
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