• 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

85.5
/src/date-picker/abstract-picker.component.ts
1
import { TabIndexDisabledControlValueAccessorMixin } from 'ngx-tethys/core';
2
import { coerceBooleanProperty, TinyDate } from 'ngx-tethys/util';
3
import {
4
    ChangeDetectorRef,
5
    computed,
6
    Directive,
7
    inject,
8
    Input,
9
    input,
10
    signal,
11
    effect,
12
    OnChanges,
13
    OnInit,
1✔
14
    output,
15
    Signal,
9✔
16
    SimpleChanges,
17
    viewChild,
18
    model,
116✔
19
    DestroyRef
20
} from '@angular/core';
21
import { ControlValueAccessor } from '@angular/forms';
348✔
22
import { injectLocale, ThyDatePickerLocale } from 'ngx-tethys/i18n';
23
import { SafeAny } from 'ngx-tethys/types';
24
import { ThyDatePickerConfigService } from './date-picker.service';
3,209✔
25
import { CompatibleValue, RangeAdvancedValue } from './inner-types';
26
import { ThyPicker } from './picker.component';
27
import { makeValue, setValueByTimestampPrecision, transformDateValue } from './picker.util';
13✔
28
import {
29
    ThyCompatibleDate,
30
    CompatiblePresets,
744✔
31
    DateEntry,
32
    DisabledDateFn,
33
    ThyDateChangeEvent,
156✔
34
    ThyDateGranularity,
35
    ThyDateRangeEntry,
36
    ThyPanelMode,
190✔
37
    ThyShortcutPosition
190✔
38
} from './standard-types';
190✔
39

190✔
40
/**
190✔
41
 * @private
190✔
42
 */
190✔
43
@Directive()
190✔
44
export abstract class AbstractPickerComponent
190✔
45
    extends TabIndexDisabledControlValueAccessorMixin
190✔
46
    implements OnInit, OnChanges, ControlValueAccessor
190✔
47
{
190✔
48
    protected destroyRef = inject(DestroyRef);
190✔
49

190✔
50
    cdr = inject(ChangeDetectorRef);
190✔
51

190✔
52
    locale: Signal<ThyDatePickerLocale> = injectLocale('datePicker');
190✔
53

190✔
54
    thyValue: CompatibleValue | null;
190✔
55

190!
56
    panelMode: ThyPanelMode | ThyPanelMode[];
190✔
57

190✔
58
    _panelMode: ThyPanelMode = 'date';
190✔
59

156✔
60
    private datePickerConfigService = inject(ThyDatePickerConfigService);
61

190✔
62
    /**
190✔
63
     * 模式
190✔
64
     * @type decade | year | month | date | week | flexible
190✔
65
     */
190!
66
    @Input() set thyMode(value: ThyPanelMode) {
190✔
67
        this._panelMode = value ?? 'date';
190✔
68
    }
83!
69

70
    get thyMode() {
190✔
71
        return this._panelMode;
190✔
72
    }
190✔
73

190✔
74
    /**
190✔
75
     * 是否显示清除按钮
190✔
76
     */
190✔
77
    readonly thyAllowClear = input(true, { transform: coerceBooleanProperty });
190✔
78

190✔
79
    /**
190✔
80
     * 是否自动获取焦点
190✔
81
     */
190✔
82
    readonly thyAutoFocus = input(false, { transform: coerceBooleanProperty });
191✔
83

84
    readonly open = signal(undefined);
190✔
85

189✔
86
    readonly thyOpen = input(undefined, { transform: coerceBooleanProperty });
37✔
87

88
    readonly thyDisabledDate = input<DisabledDateFn>();
89

190✔
90
    /**
191✔
91
     * 最小值
3✔
92
     */
93
    readonly thyMinDate = input<Date | number>();
94

95
    /**
96
     * 最大值
156✔
97
     */
156✔
98
    readonly thyMaxDate = input<Date | number>();
156✔
99

100
    /**
101
     * 输入框提示文字
43✔
102
     * @type string | string[]
103
     */
104
    readonly thyPlaceHolder = input<string | string[]>(undefined);
237✔
105

37✔
106
    readonly placeholder = signal<string | string[]>(undefined);
107

108
    /**
109
     * 是否只读
47✔
110
     */
111
    readonly thyReadonly = input(false, { transform: coerceBooleanProperty });
112

113
    /**
×
114
     * 选择器 className
115
     */
×
NEW
116
    readonly thyOriginClassName = input<string>();
×
117

118
    /**
×
UNCOV
119
     * 弹出层 className
×
120
     */
NEW
121
    readonly thyPanelClassName = input<string>();
×
UNCOV
122

×
123
    /**
UNCOV
124
     * 输入框的大小
×
UNCOV
125
     */
×
126
    readonly thySize = input<'lg' | 'md' | 'sm' | 'xs' | 'default'>('default');
UNCOV
127

×
UNCOV
128
    /**
×
129
     * 设置时间戳精度
130
     * @default seconds 10位
×
131
     */
132
    readonly thyTimestampPrecision = input<'seconds' | 'milliseconds'>(
133
        this.datePickerConfigService.config?.timestampPrecision || 'seconds'
72✔
134
    );
72✔
135

72✔
136
    /**
72✔
137
     * 展示的日期格式
72✔
138
     * @default yyyy-MM-dd
30✔
139
     */
30✔
140
    readonly thyFormat = model<string>();
30✔
141

27✔
142
    /**
27!
UNCOV
143
     * 区间分隔符,不传值默认为 "~"
×
144
     */
145
    readonly thySeparator = input<string>(this.datePickerConfigService.config?.separator);
146

27✔
147
    readonly separator: Signal<string> = computed(() => {
148
        return ` ${this.thySeparator()?.trim()} `;
149
    });
30✔
150

30✔
151
    /**
152
     * @description.en-us only for range picker, Whether to automatically take the beginning and ending unixTime of the day
153
     * @description.zh-cn 是否取值开始日期的00:00以及截止日期的24:00
42✔
154
     * @default false
42✔
155
     */
32✔
156
    readonly thyAutoStartAndEnd = input(false, { transform: coerceBooleanProperty });
157

42✔
158
    /**
39✔
159
     * 面板默认日期
160
     */
161
    readonly thyDefaultPickerValue = input<ThyCompatibleDate | number | null>(null);
3✔
162

163
    /**
164
     * 自定义的后缀图标
165
     */
166
    readonly thySuffixIcon = input('calendar');
458✔
167

74!
UNCOV
168
    /**
×
169
     * 是否展示快捷选项面板
170
     * @default false
171
     */
74!
NEW
172
    readonly thyShowShortcut = input(undefined, { transform: coerceBooleanProperty });
×
173

174
    /**
175
     * 快捷选项面板的显示位置
176
     * @type left | bottom
177
     */
NEW
178
    readonly thyShortcutPosition = input('left', { transform: (value: ThyShortcutPosition) => value || 'left' });
×
UNCOV
179

×
180
    /**
181
     * 自定义快捷选项
182
     * @type ThyShortcutPreset[]
386✔
183
     */
386✔
184
    readonly thyShortcutPresets = input<CompatiblePresets>();
386✔
185

386✔
186
    /**
3✔
187
     * 是否支持选择时间
2✔
188
     */
189
    readonly thyShowTime = input(false, {
190
        transform: (value: object | boolean) => (typeof value === 'object' ? value : coerceBooleanProperty(value))
386✔
191
    });
386✔
192

386✔
193
    /**
386✔
194
     * 是否展示时间(时、分)
386✔
195
     */
386✔
196
    readonly thyMustShowTime = input(false, { transform: coerceBooleanProperty });
197

198
    readonly showWeek = computed<boolean>(() => this.thyMode === 'week');
387✔
199

200
    readonly flexible = computed<boolean>(() => this.thyMode === 'flexible');
201

202
    /**
156✔
203
     * 日期变化的回调
204
     */
205
    readonly thyDateChange = output<ThyDateChangeEvent>();
206

72✔
207
    readonly thyOpenChange = output<boolean>();
10✔
208

209
    readonly picker = viewChild<ThyPicker>(ThyPicker);
210

211
    /**
224!
212
     * 是否禁用
224✔
213
     * @default false
91✔
214
     */
215
    @Input({ transform: coerceBooleanProperty })
216
    set thyDisabled(value: boolean) {
133✔
217
        this.disabled = value;
218
    }
219
    get thyDisabled(): boolean {
220
        return this.disabled;
185✔
221
    }
185✔
222

223
    /**
224
     * 时区,不传使用默认时区
156✔
225
     */
119✔
226
    readonly thyTimeZone = input<string>();
119✔
227

228
    disabled = false;
229

230
    isRange: boolean;
461✔
231

232
    withTime: boolean;
233

72✔
234
    flexibleDateGranularity: ThyDateGranularity;
235

1✔
236
    protected isCustomPlaceHolder = false;
1✔
237

238
    private onlyEmitDate = false;
239

240
    protected originWithTime: boolean;
241

242
    protected innerValue: ThyCompatibleDate;
243

244
    get realOpenState(): boolean {
245
        return this.picker().realOpenState;
246
    }
247

248
    get isShowDatePopup(): boolean {
249
        return this.picker().isShowDatePopup;
250
    }
251

252
    initValue(): void {
253
        this.thyValue = this.isRange ? [] : null;
254
    }
255

256
    constructor() {
257
        super();
258

259
        effect(() => {
260
            this.open.set(this.thyOpen());
261
        });
262

263
        effect(() => {
264
            if (this.isCustomPlaceHolder) {
265
                this.placeholder.set(this.thyPlaceHolder());
266
            }
267
        });
1✔
268

269
        effect(() => {
270
            if (this.thyTimeZone()) {
271
                this.setValue(this.innerValue);
272
            }
273
        });
274
    }
275

276
    ngOnInit(): void {
277
        this.setDefaultPlaceHolder();
278
        this.setDefaultTimePickerState();
279
        this.initValue();
280
    }
281

282
    onDateValueChange(event: ThyDateChangeEvent) {
283
        this.thyDateChange.emit(event);
284
    }
285

286
    ngOnChanges(changes: SimpleChanges): void {
287
        if (changes.thyPlaceHolder && changes.thyPlaceHolder.firstChange && typeof changes.thyPlaceHolder.currentValue !== 'undefined') {
288
            this.isCustomPlaceHolder = true;
289
        }
290
    }
291

292
    closeOverlay(): void {
293
        this.picker().hideOverlay();
294
    }
295

296
    getAutoStartAndEndValue(begin: TinyDate, end: TinyDate) {
297
        let value: { begin: number; end: number };
298
        switch (this.thyMode) {
299
            case 'date':
300
                value = { begin: begin.startOfDay().getUnixTime(), end: end.endOfDay().getUnixTime() };
301
                break;
302
            case 'week':
303
                value = { begin: begin.startOfWeek().getUnixTime(), end: end.endOfWeek().getUnixTime() };
304
                break;
305
            case 'month':
306
                value = { begin: begin.startOfMonth().getUnixTime(), end: end.endOfMonth().getUnixTime() };
307
                break;
308
            case 'year':
309
                value = { begin: begin.startOfYear().getUnixTime(), end: end.endOfYear().getUnixTime() };
310
                break;
311
            default:
312
                value = { begin: begin.startOfDay().getUnixTime(), end: end.endOfDay().getUnixTime() };
313
                break;
314
        }
315
        return value;
316
    }
317

318
    onValueChange(originalValue: CompatibleValue | RangeAdvancedValue): void {
319
        this.setFormatRule();
320
        const { value, withTime, flexibleDateGranularity } = transformDateValue(originalValue);
321
        this.flexibleDateGranularity = flexibleDateGranularity;
322
        this.setValue(value);
323
        if (this.isRange) {
324
            const vAsRange: any = this.thyValue;
325
            let value = { begin: null, end: null } as ThyDateRangeEntry;
326
            if (vAsRange.length) {
327
                const [begin, end] = vAsRange as TinyDate[];
328
                if (this.thyAutoStartAndEnd()) {
329
                    value = this.getAutoStartAndEndValue(begin, end);
330
                } else {
331
                    value = { begin: begin.getUnixTime(), end: end.getUnixTime() };
332
                }
333
            }
334
            const [beginUnixTime, endUnixTime] = this.setValueByPrecision(value) as number[];
335
            this.onChangeFn(
336
                Object.assign({ begin: beginUnixTime, end: endUnixTime }, this.flexible() ? { granularity: flexibleDateGranularity } : {})
337
            );
338
        } else {
339
            const value = { date: null, with_time: this.withTime ? 1 : 0 } as DateEntry;
340
            if (this.thyValue) {
341
                value.date = (this.thyValue as TinyDate).getUnixTime();
342
            }
343
            if (this.onlyEmitDate) {
344
                this.onChangeFn(this.setValueByPrecision(value.date) as number);
345
            } else {
346
                this.onChangeFn(Object.assign(value, { date: this.setValueByPrecision(value.date) as number }));
347
            }
348
        }
349
    }
350

351
    setFormatRule() {
352
        if (!this.thyFormat()) {
353
            if (this.withTime) {
354
                this.thyFormat.set('yyyy-MM-dd HH:mm');
355
            } else {
356
                if (!this.onlyEmitDate) {
357
                    this.thyFormat.set('yyyy-MM-dd');
358
                }
359
            }
360
        }
361
    }
362

363
    onOpenChange(open: boolean): void {
364
        this.open.set(open);
365
        this.thyOpenChange.emit(open);
366
    }
367

368
    onChangeFn: (val: ThyCompatibleDate | DateEntry | ThyDateRangeEntry | number | null) => void = () => void 0;
369

370
    writeValue(originalValue: ThyCompatibleDate | ThyDateRangeEntry): void {
371
        const { value, withTime, flexibleDateGranularity } = transformDateValue(originalValue);
372
        this.flexibleDateGranularity = flexibleDateGranularity;
373
        this.innerValue = value;
374
        if (this.flexible() && value && (value as Date[]).length) {
375
            if (!this.flexibleDateGranularity) {
376
                this.flexibleDateGranularity = 'day';
377
            }
378
        }
379

380
        this.setValue(value);
381
        this.setTimePickerState(withTime);
382
        this.onlyEmitDate = typeof withTime === 'undefined';
383
        this.originWithTime = withTime;
384
        this.setFormatRule();
385
        this.cdr.markForCheck();
386
    }
387

388
    setTimePickerState(withTime: boolean): void {
389
        this.withTime = withTime;
390
    }
391

392
    // Displays the time directly when the time must be displayed by default
393
    setDefaultTimePickerState() {
394
        this.withTime = this.thyMustShowTime();
395
    }
396

397
    // Restore after clearing time to select whether the original picker time is displayed or not
398
    restoreTimePickerState(value: CompatibleValue | null) {
399
        if (!value) {
400
            this.withTime = this.thyMustShowTime() || this.originWithTime;
401
        }
402
    }
403

404
    setPanelMode() {
405
        const mode = this.thyMode ?? 'date';
406
        if (this.isRange) {
407
            this.panelMode = this.flexible() ? ['date', 'date'] : [mode, mode];
408
        } else {
409
            this.panelMode = mode;
410
        }
411
    }
412

413
    setDisabledState(disabled: boolean): void {
414
        this.thyDisabled = disabled;
415
        this.cdr.markForCheck();
416
    }
417

418
    private setDefaultPlaceHolder(): void {
419
        if (!this.isCustomPlaceHolder) {
420
            const placeholder = this.isRange ? [this.locale().startDate, this.locale().endDate] : this.locale().placeholder;
421
            this.placeholder.set(placeholder);
422
        }
423
    }
424

425
    public setValue(value: ThyCompatibleDate): void {
426
        this.thyValue = makeValue(value, this.isRange, this.thyTimeZone());
427
    }
428

429
    private setValueByPrecision(value: ThyCompatibleDate | number | Date | DateEntry | ThyDateRangeEntry | SafeAny): number | number[] {
430
        return setValueByTimestampPrecision(value, this.isRange, this.thyTimestampPrecision(), this.thyTimeZone());
431
    }
432
}
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