• 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

89.8
/src/date-picker/base-picker.component.ts
1
import { ThyClickDispatcher, ThyPlacement } from 'ngx-tethys/core';
2
import { elementMatchClosest, FunctionProp, TinyDate } from 'ngx-tethys/util';
3
import {
4
    Component,
5
    ElementRef,
6
    inject,
7
    input,
8
    Input,
9
    NgZone,
10
    OnChanges,
11
    OnInit,
12
    output,
13
    PLATFORM_ID,
14
    TemplateRef,
15
    viewChild
1✔
16
} from '@angular/core';
17
import { coerceBooleanProperty } from '@angular/cdk/coercion';
156✔
18
import { isPlatformBrowser } from '@angular/common';
156✔
19
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
156✔
20
import { AbstractPickerComponent } from './abstract-picker.component';
156✔
21
import { CompatibleValue, RangeAdvancedValue } from './inner-types';
156✔
22
import { ThyPicker } from './picker.component';
156✔
23
import { hasTimeInStringDate, isValidStringDate, parseStringDate, transformDateValue } from './picker.util';
156✔
24
import { ThyCompatibleDate, ThyPanelMode } from './standard-types';
156✔
25
import { QUARTER_FORMAT } from './date-picker.config';
156✔
26

156✔
27
/**
156✔
28
 * @private
156✔
29
 */
30
@Component({
31
    template: ``,
158✔
32
    host: {
158✔
33
        '[attr.tabindex]': `tabIndex`,
8✔
34
        '(focus)': 'onFocus($event)',
8✔
35
        '(blur)': 'onBlur($event)'
36
    }
37
})
38
export class BasePicker extends AbstractPickerComponent implements OnInit, OnChanges {
1,306✔
39
    protected element = inject(ElementRef);
40

41
    initialized: boolean;
156✔
42

156✔
43
    private innerPreviousDate: string;
156✔
44

156✔
45
    private thyPicker = viewChild<ThyPicker>('thyPicker');
156!
46

156✔
47
    readonly thyDateRender = input<FunctionProp<TemplateRef<Date> | string>>();
48

49
    @Input() set thyMode(value: ThyPanelMode) {
50
        this._panelMode = value ?? 'date';
226✔
51
        if (this.initialized) {
52
            this.setPanelMode();
53
            this.setFormat();
54
        }
55
    }
1✔
56

1✔
57
    get thyMode() {
1✔
58
        return this._panelMode;
59
    }
60

61
    /**
62
     * 是否有幕布
63
     */
64
    readonly thyHasBackdrop = input(true, { transform: coerceBooleanProperty });
49✔
65

49✔
66
    /**
49✔
67
     * 弹出位置
49✔
68
     * @type top | topLeft | topRight | bottom | bottomLeft | bottomRight | left | leftTop | leftBottom | right | rightTop | rightBottom
41✔
69
     */
70
    readonly thyPlacement = input<ThyPlacement>('bottomLeft');
49✔
71

72
    readonly thyOnPanelChange = output<ThyPanelMode | ThyPanelMode[]>();
73

14✔
74
    readonly thyOnCalendarChange = output<Date[]>();
6✔
75

6✔
76
    readonly thyOnOk = output<ThyCompatibleDate | null>();
6✔
77

6✔
78
    thyClickDispatcher = inject(ThyClickDispatcher);
79

8✔
80
    platformId = inject(PLATFORM_ID);
8✔
81

8!
82
    ngZone = inject(NgZone);
8✔
83

5✔
84
    ngOnInit(): void {
85
        super.ngOnInit();
86
        this.setPanelMode();
3✔
87
        this.setFormat();
88
        this.initialized = true;
8✔
89

5✔
90
        if (isPlatformBrowser(this.platformId)) {
91
            this.thyClickDispatcher
92
                .clicked(0)
93
                .pipe(takeUntilDestroyed(this.destroyRef))
8✔
94
                .subscribe((event: Event) => {
8✔
95
                    if (
96
                        !this.element.nativeElement.contains(event.target) &&
97
                        !this.thyPicker()
164✔
98
                            ?.overlayContainer()
124✔
99
                            ?.nativeElement.contains(event.target as Node) &&
100
                        this.realOpenState
101
                    ) {
102
                        this.ngZone.run(() => {
103
                            this.closeOverlay();
124✔
104
                            this.cdr.markForCheck();
105
                        });
124✔
106
                    }
107
                });
108
        }
109
    }
110

14!
111
    onValueChange(value: CompatibleValue | RangeAdvancedValue): void {
21✔
112
        this.thyPicker().entering = false;
14✔
113
        this.restoreTimePickerState(value as CompatibleValue);
114
        super.onValueChange(value);
115
        if (!this.flexible()) {
116
            this.closeOverlay();
46✔
117
        }
118
        this.innerPreviousDate = this.thyPicker().getReadableValue(this.thyValue);
119
    }
5!
UNCOV
120

×
UNCOV
121
    onInputValueChange(formatDate: string | null | Array<null>) {
×
UNCOV
122
        if (!formatDate || !formatDate.length) {
×
123
            const compatibleValue = formatDate ? (formatDate as CompatibleValue) : null;
124
            this.restoreTimePickerState(compatibleValue);
UNCOV
125
            super.onValueChange(compatibleValue);
×
126
            return;
127
        }
128
        let value = formatDate as string;
129
        const valueValid = isValidStringDate(value, this.thyTimeZone());
5!
130
        const valueLimitValid = valueValid ? this.isValidDateLimit(parseStringDate(value, this.thyTimeZone())) : false;
5✔
131
        if (valueValid && valueLimitValid) {
132
            this.innerPreviousDate = value;
UNCOV
133
        } else {
×
134
            value = this.innerPreviousDate;
135
        }
136
        const tinyDate = value
5✔
137
            ? this.thyShowTime()
138
                ? parseStringDate(value, this.thyTimeZone())
139
                : parseStringDate(value, this.thyTimeZone()).startOfDay()
181✔
140
            : null;
181✔
141
        this.restoreTimePickerState(tinyDate);
57✔
142
        super.onValueChange(tinyDate);
143
    }
144

145
    setFormat() {
1✔
146
        if (!this.thyFormat()) {
147
            const inputFormats: { [key in ThyPanelMode]?: string } = {
148
                year: 'yyyy',
149
                quarter: `yyyy-${QUARTER_FORMAT}`,
5✔
150
                month: 'yyyy-MM',
1✔
151
                week: this.locale().weekThFormat,
152
                date: this.thyShowTime() ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd'
4✔
153
            };
154
            this.thyFormat.set(this.flexible() ? inputFormats['date'] : inputFormats[this.thyMode]);
155
        }
13!
156
    }
13✔
157

4✔
158
    // Emit thyOnCalendarChange when select date by thy-range-picker
159
    onCalendarChange(value: TinyDate[]): void {
13✔
160
        if (this.isRange) {
161
            const rangeValue = value.map(x => x.nativeDate);
162
            this.thyOnCalendarChange.emit(rangeValue);
163
        }
8✔
164
    }
8!
UNCOV
165

×
166
    onShowTimePickerChange(show: boolean): void {
167
        this.withTime = show;
8✔
168
    }
8✔
169

8✔
170
    onResultOk(): void {
171
        if (this.isRange) {
172
            const value = this.thyValue as TinyDate[];
173
            if (value.length) {
1✔
174
                this.thyOnOk.emit([value[0].nativeDate, value[1].nativeDate]);
175
            } else {
176
                this.thyOnOk.emit([]);
177
            }
178
        } else {
179
            if (this.thyValue) {
180
                this.thyOnOk.emit((this.thyValue as TinyDate).nativeDate);
181
            } else {
182
                this.thyOnOk.emit(null);
183
            }
184
        }
1✔
185
        this.closeOverlay();
186
    }
187

188
    onOpenChange(open: boolean): void {
189
        this.thyOpenChange.emit(open);
190
        if (!open) {
191
            this.onTouchedFn();
192
        }
193
    }
194

195
    onFocus(event: Event) {
196
        this.picker().focus();
197
    }
198

199
    onBlur(event?: FocusEvent) {
200
        // Tab 聚焦后自动聚焦到 input 输入框,此分支下直接返回,无需触发 onTouchedFn
201
        if (elementMatchClosest(event?.relatedTarget as HTMLElement, ['date-popup', 'thy-picker'])) {
202
            return;
203
        }
204
        this.onTouchedFn();
205
    }
206

207
    onInputDate(value: string) {
208
        if (value && isValidStringDate(value, this.thyTimeZone())) {
209
            if (this.thyShowTime()) {
210
                this.withTime = hasTimeInStringDate(value, this.thyTimeZone());
211
            }
212
            this.thyValue = parseStringDate(value, this.thyTimeZone());
213
        }
214
    }
215

216
    private isValidDateLimit(date: TinyDate): boolean {
217
        let disable = false;
218
        if (this.thyDisabledDate() !== undefined) {
219
            disable = this.thyDisabledDate()(date.nativeDate);
220
        }
221
        const minDate = this.thyMinDate() ? new TinyDate(transformDateValue(this.thyMinDate()).value as Date, this.thyTimeZone()) : null;
222
        const maxDate = this.thyMaxDate() ? new TinyDate(transformDateValue(this.thyMaxDate()).value as Date, this.thyTimeZone()) : null;
223
        return (
224
            (!minDate || date.startOfDay().nativeDate >= minDate.startOfDay().nativeDate) &&
225
            (!maxDate || date.startOfDay().nativeDate <= maxDate.startOfDay().nativeDate) &&
226
            !disable
227
        );
228
    }
229
}
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