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

atinc / ngx-tethys / #96

12 Aug 2025 06:20AM UTC coverage: 90.341% (+0.02%) from 90.324%
#96

push

web-flow
refactor(date-picker): migrate to signal for date-picker #TINFR-1463 (#3513)

* refactor(date-picker): migrate to signal for calendar header

* refactor(date-picker): migrate to signal for calendar footer

* refactor(date-picker): migrate to signal for calendar table

* refactor(date-picker): migrate to signal for date table cell

* refactor(date-picker): migrate to signal for date carousel

* refactor(date-picker): migrate to signal for inner-popup and date-popup

* refactor(date-picker): migrate to signal for pickers

5531 of 6813 branches covered (81.18%)

Branch coverage included in aggregate %.

342 of 367 new or added lines in 20 files covered. (93.19%)

66 existing lines in 11 files now uncovered.

13969 of 14772 relevant lines covered (94.56%)

904.1 hits per line

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

87.22
/src/date-picker/lib/date-carousel/date-carousel.component.ts
1
import { NgClass, NgTemplateOutlet } from '@angular/common';
2
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, inject, input, OnDestroy, OnInit, Signal } from '@angular/core';
3
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
4
import { ThyButton } from 'ngx-tethys/button';
5
import { injectLocale, ThyDatePickerLocale } from 'ngx-tethys/i18n';
6
import { ThyIcon } from 'ngx-tethys/icon';
7
import { TinyDate } from 'ngx-tethys/util';
8
import { Subject } from 'rxjs';
9
import { DateHelperService } from '../../date-helper.service';
10
import { AdvancedSelectableCell, RangeAdvancedValue } from '../../inner-types';
11
import { DatePickerAdvancedShowYearTipPipe } from '../../picker.pipes';
12
import { ThyDateGranularity } from '../../standard-types';
13
import { QUARTER_FORMAT } from '../../date-picker.config';
14

15
/**
16
 * @private
17
 */
18
@Component({
1✔
19
    // eslint-disable-next-line @angular-eslint/component-selector
20
    selector: 'date-carousel',
17✔
21
    templateUrl: './date-carousel.component.html',
17✔
22
    changeDetection: ChangeDetectionStrategy.OnPush,
17✔
23
    providers: [
17✔
24
        {
17✔
25
            provide: NG_VALUE_ACCESSOR,
17✔
26
            multi: true,
17✔
27
            useExisting: forwardRef(() => DateCarousel)
17✔
28
        }
29
    ],
30
    host: {
24✔
31
        class: 'thy-date-picker-advanced-carousel'
24✔
32
    },
24✔
33
    imports: [NgTemplateOutlet, ThyButton, ThyIcon, NgClass, DatePickerAdvancedShowYearTipPipe]
6✔
34
})
6✔
35
export class DateCarousel implements OnInit, ControlValueAccessor, OnDestroy {
36
    private cdr = inject(ChangeDetectorRef);
37

18✔
38
    private dateHelper = inject(DateHelperService);
39

24✔
40
    locale: Signal<ThyDatePickerLocale> = injectLocale('datePicker');
41

42
    readonly activeDate = input<TinyDate>();
17✔
43

14✔
44
    set defaultValue(value: RangeAdvancedValue) {
13✔
45
        this.dateGranularity = value.dateGranularity;
46
        this.buildSelectableData(value.begin);
42✔
47

56✔
48
        if (value.begin && value.end) {
56✔
49
            const shouldBeSelectValue = this.getShouldBeToggleValue(value.begin, value.end);
14✔
50
            this.select(...shouldBeSelectValue);
11✔
51
        } else {
1✔
52
            this.clearSelect(true);
53
        }
54
        this.initialized = true;
55
    }
56

57
    selectableData: { year?: AdvancedSelectableCell[]; quarter?: AdvancedSelectableCell[]; month?: AdvancedSelectableCell[] } = {};
58

10✔
59
    dateGranularity: ThyDateGranularity;
10✔
60

61
    selectedValue: AdvancedSelectableCell[] = [];
62

63
    private initialized = false;
64

65
    private selectedValueChange$ = new Subject<RangeAdvancedValue>();
66

67
    private _onChange: (value: RangeAdvancedValue) => void;
68

69
    private _onTouched: (value: RangeAdvancedValue) => void;
41✔
70

24✔
71
    ngOnInit(): void {
72
        this.selectedValueChange$.subscribe(() => {
73
            if (this.selectedValue.length) {
74
                this.buildSelectableData(this.selectedValue[0]?.startValue, this.dateGranularity);
17✔
75
            }
76
            this.selectableData.year.forEach(item => (item.classMap = this.getClassMap(item)));
77
            this.selectableData.quarter.forEach(item => (item.classMap = this.getClassMap(item)));
17✔
78
            this.selectableData.month.forEach(item => (item.classMap = this.getClassMap(item)));
79
            if (this.initialized) {
80
                if (this.isSelectEmpty()) {
192✔
81
                    this._onChange({
82
                        dateGranularity: null,
83
                        begin: null,
84
                        end: null
85
                    });
86
                } else {
87
                    const selctedValue = this.selectedValue;
88
                    this._onChange({
89
                        dateGranularity: this.dateGranularity,
192✔
90
                        begin: selctedValue[0]?.startValue,
91
                        end: selctedValue[selctedValue.length - 1]?.endValue
92
                    });
192✔
93
                }
56✔
94
            }
95
        });
96
    }
136✔
97

32✔
98
    writeValue(value: RangeAdvancedValue): void {
99
        if (value) {
100
            this.defaultValue = value;
104✔
101
        }
53✔
102
    }
78✔
103

104
    registerOnChange(fn: any): void {
105
        this._onChange = fn;
106
    }
107

383✔
108
    registerOnTouched(fn: any): void {
109
        this._onTouched = fn;
110
    }
22✔
111

112
    getClassMap(cell: AdvancedSelectableCell) {
113
        return {
27✔
114
            [`active`]: this.isSelected(cell),
115
            [`indeterminate`]: this.isCellIndeterminate(this.selectedValue, cell),
116
            [`type-active`]: this.isTypeActive(this.selectedValue, cell),
13✔
117
            ['in-hover-range']: cell.isInRange,
24✔
118
            ['out-range']: cell.isOutRange
13✔
119
        };
120
    }
121

13✔
122
    isTypeActive(originalValue: AdvancedSelectableCell[], value: AdvancedSelectableCell) {
13✔
123
        return originalValue?.length && originalValue[0].type === value.type;
124
    }
125

1✔
126
    isCellIndeterminate(originalValue: AdvancedSelectableCell[], value: AdvancedSelectableCell) {
1✔
127
        if (originalValue[0]?.type === value.type) {
128
            return false;
1✔
129
        } else {
1✔
130
            if (originalValue[0]?.type === 'year') {
131
                return !!originalValue.find(item => item.startValue.isSameYear(value.startValue));
132
            } else {
20✔
133
                return value.type === 'year'
20!
134
                    ? !!originalValue.find(item => item.startValue.isSameYear(value.startValue))
×
135
                    : !!originalValue.find(item => item.startValue.isSameQuarter(value.startValue));
136
            }
137
        }
138
    }
7✔
139

7✔
140
    isSelected(value: AdvancedSelectableCell) {
141
        return this.selectedValue.find(item => item.startValue.isSameDay(value.startValue)) && this.dateGranularity === value.type;
1✔
142
    }
1!
UNCOV
143

×
144
    isSelectEmpty() {
145
        return this.selectedValue.length == 0;
146
    }
1✔
147

1✔
148
    selectSort() {
1✔
149
        this.selectedValue.sort((a, b) => a.startValue.getTime() - b.startValue.getTime());
1✔
150
    }
151

152
    select(...value: AdvancedSelectableCell[]) {
1✔
153
        value.forEach(item => {
154
            if (!this.isSelected(item)) {
3✔
155
                this.selectedValue.push(...value);
3!
UNCOV
156
            }
×
157
        });
158
        this.selectSort();
159
        this.selectedValueChange$.next(undefined);
3✔
160
    }
3✔
161

7✔
162
    deselect(...value: AdvancedSelectableCell[]) {
7✔
163
        value.forEach(item => {
164
            this.selectedValue = this.selectedValue.filter(selected => !selected.startValue.isSameDay(item.startValue));
165
        });
3✔
166
        this.selectSort();
167
        this.selectedValueChange$.next(undefined);
3✔
168
    }
3!
UNCOV
169

×
170
    clearSelect(hidden?: boolean) {
171
        this.selectedValue = [];
172
        if (!hidden) {
3✔
173
            this.selectedValueChange$.next(undefined);
3✔
174
        }
3✔
175
    }
3✔
176

177
    getShouldBeToggleValue(begin: TinyDate, end: TinyDate) {
178
        let selectedValue: AdvancedSelectableCell[] = [];
179
        switch (this.dateGranularity) {
7✔
180
            case 'year':
181
                this.dateGranularity = 'year';
182
                if (begin.isSameYear(end)) {
111✔
183
                    selectedValue.push(this.getSelectableYear(begin));
37✔
184
                } else {
98✔
185
                    selectedValue.push(this.getSelectableYear(begin));
186
                    while (!begin.isSameYear(end)) {
33✔
187
                        begin = begin.addYears(1);
99✔
188
                        selectedValue.push(this.getSelectableYear(begin));
189
                    }
33✔
190
                }
191
                break;
33✔
192
            case 'month':
132✔
193
                this.dateGranularity = 'month';
194
                if (begin.isSameMonth(end)) {
33✔
195
                    selectedValue.push(this.getSelectableMonth(begin));
196
                } else {
32✔
197
                    selectedValue.push(this.getSelectableMonth(begin));
128✔
198
                    while (!begin.isSameMonth(end)) {
199
                        begin = begin.addMonths(1);
32✔
200
                        selectedValue.push(this.getSelectableMonth(begin));
201
                    }
202
                }
37✔
203
                break;
204
            case 'quarter':
2✔
205
                this.dateGranularity = 'quarter';
107✔
206
                if (begin.isSameQuarter(end)) {
107✔
207
                    selectedValue.push(this.getSelectableQuarter(begin));
208
                } else {
209
                    selectedValue.push(this.getSelectableQuarter(begin));
210
                    while (!begin.isSameQuarter(end)) {
211
                        begin = begin.addQuarters(1);
212
                        selectedValue.push(this.getSelectableQuarter(begin));
213
                    }
214
                }
6✔
215
        }
146✔
216
        return selectedValue;
146✔
217
    }
218

219
    buildSelectableData(startDate: TinyDate, excludeGranularity?: ThyDateGranularity) {
220
        const buildGranularity = ['year', 'month', 'quarter'].filter(item => item !== excludeGranularity);
221
        buildGranularity.forEach(granularity => {
222
            switch (granularity) {
223
                case 'year':
224
                    this.selectableData.year = [...Array(3).keys()].map((item, index) => {
10✔
225
                        return this.getSelectableYear(startDate, index);
146✔
226
                    });
227
                    break;
146✔
228
                case 'quarter':
229
                    this.selectableData.quarter = [...Array(4).keys()].map((item, index) => {
230
                        return this.getSelectableQuarter(startDate, index);
231
                    });
232
                    break;
233
                case 'month':
234
                    this.selectableData.month = [...Array(4).keys()].map((item, index) => {
146✔
235
                        return this.getSelectableMonth(startDate, index);
236
                    });
237
                    break;
3✔
238
            }
239
        });
3✔
240
        this.cdr.markForCheck();
1✔
241
    }
242

4✔
243
    getSelectableYear(currentDate: TinyDate, preOrNextcount: number = 0): AdvancedSelectableCell {
1✔
244
        currentDate = currentDate || this.activeDate() || new TinyDate().startOfYear();
245
        return {
4✔
246
            type: 'year',
247
            content: `${currentDate.addYears(preOrNextcount).getYear()}`,
11✔
248
            startValue: currentDate.addYears(preOrNextcount).startOfYear(),
249
            endValue: currentDate.addYears(preOrNextcount).endOfYear(),
250
            classMap: {}
3✔
251
        };
252
    }
3✔
253

1✔
254
    getSelectableQuarter(currentDate: TinyDate, preOrNextcount: number = 0): AdvancedSelectableCell {
255
        currentDate = currentDate || this.activeDate() || new TinyDate().startOfQuarter();
4✔
256
        return {
1✔
257
            type: 'quarter',
258
            content: `${currentDate.addQuarters(preOrNextcount).format(QUARTER_FORMAT)}`,
4✔
259
            startValue: currentDate.addQuarters(preOrNextcount).startOfQuarter(),
260
            endValue: currentDate.addQuarters(preOrNextcount).endOfQuarter(),
11✔
261
            classMap: {}
262
        };
263
    }
8✔
264

28✔
265
    getSelectableMonth(currentDate: TinyDate, preOrNextcount: number = 0): AdvancedSelectableCell {
28✔
266
        currentDate = currentDate || this.activeDate() || new TinyDate().startOfMonth();
267
        // Selectable months for advanced range selector
8✔
268
        const cell: AdvancedSelectableCell = {
4✔
269
            type: 'month',
4✔
270
            content: this.dateHelper.format(currentDate.addMonths(preOrNextcount).nativeDate, this.locale().monthFormat),
271
            startValue: currentDate.addMonths(preOrNextcount).startOfMonth(),
4✔
272
            endValue: currentDate.addMonths(preOrNextcount).endOfMonth(),
273
            classMap: {}
4✔
274
        };
1✔
275
        return cell;
1!
276
    }
1✔
277

278
    prevClick(type: ThyDateGranularity) {
1✔
279
        switch (type) {
280
            case 'year':
3✔
281
                this.selectableData.year = this.selectableData.year.map(item => this.getSelectableYear(item.startValue, -1));
1✔
282
                break;
1✔
283
            case 'quarter':
1✔
284
                this.selectableData.quarter = this.selectableData.quarter.map(item => this.getSelectableQuarter(item.startValue, -2));
285
                break;
286
            case 'month':
287
                this.selectableData.month = this.selectableData.month.map(item => this.getSelectableMonth(item.startValue, -2));
2✔
288
        }
2✔
289
        this.selectableData[type].forEach(item => (item.classMap = this.getClassMap(item)));
2✔
290
    }
291

292
    nextClick(type: ThyDateGranularity) {
293
        switch (type) {
294
            case 'year':
1!
295
                this.selectableData.year = this.selectableData.year.map(item => this.getSelectableYear(item.startValue, 1));
296
                break;
1✔
297
            case 'quarter':
298
                this.selectableData.quarter = this.selectableData.quarter.map(item => this.getSelectableQuarter(item.startValue, 2));
299
                break;
UNCOV
300
            case 'month':
×
UNCOV
301
                this.selectableData.month = this.selectableData.month.map(item => this.getSelectableMonth(item.startValue, 2));
×
UNCOV
302
        }
×
UNCOV
303
        this.selectableData[type].forEach(item => (item.classMap = this.getClassMap(item)));
×
304
    }
305

306
    selectDate(type: ThyDateGranularity, value: AdvancedSelectableCell) {
307
        this.selectableData[type].forEach(item => {
2!
UNCOV
308
            item.isInRange = false;
×
309
            item.isOutRange = false;
310
        });
2!
311

2✔
312
        if (this.isSelectEmpty()) {
2!
UNCOV
313
            this.dateGranularity = type;
×
314
            this.select(value);
315
            // this.selectedValueChange$.next();
316
            return;
2✔
317
        }
2✔
318
        if (this.isSelected(value)) {
2✔
319
            this.toggleSelect(value);
8✔
320
            if (this.isSelectEmpty()) {
4✔
321
                this.dateGranularity = null;
322
            }
323
            return;
4✔
324
        }
325

326
        if (this.dateGranularity === value.type) {
327
            const { rangeStart, rangeEnd } = this.getActualStartAndEnd(value);
328
            const shouldBeSelectValue = this.getShouldBeToggleValue(rangeStart, rangeEnd);
UNCOV
329
            this.select(...shouldBeSelectValue);
×
UNCOV
330
            // this.selectedValueChange$.next();
×
UNCOV
331
        } else {
×
UNCOV
332
            this.dateGranularity = type;
×
333
            this.clearSelect(true);
334
            this.select(value);
UNCOV
335
            // this.selectedValueChange$.next();
×
336
        }
337
    }
338
    toggleSelect(value: AdvancedSelectableCell) {
339
        if (value.startValue.isSameDay(this.selectedValue[0].startValue)) {
8✔
340
            // only deselect first one
341
            this.deselect(value);
342
        } else {
2!
343
            // deselect current and all after current
8✔
344
            const rangeStart = value.startValue;
345
            const rangeEnd = this.selectedValue[this.selectedValue.length - 1].endValue;
2!
346
            const shouldBeDeselectValue = this.getShouldBeToggleValue(rangeStart, rangeEnd);
8✔
347
            this.deselect(...shouldBeDeselectValue);
348
        }
8✔
349
    }
350

351
    onMouseenter(event: Event, type: ThyDateGranularity, value: AdvancedSelectableCell) {
1✔
352
        if (this.isSelectEmpty() || this.dateGranularity !== type) {
1✔
353
            return;
354
        }
1!
UNCOV
355
        if (this.isSelected(value)) {
×
UNCOV
356
            value.isInRange = true;
×
357
            if (value.startValue.isSameDay(this.selectedValue[0].startValue)) {
358
                value.isOutRange = true;
1!
359
            } else {
1✔
360
                const rangeStart = value.startValue;
1✔
361
                const rangeEnd = this.selectedValue[this.selectedValue.length - 1].endValue;
362
                this.selectableData[type].forEach((item: AdvancedSelectableCell) => {
1✔
363
                    if (item.startValue.getTime() >= rangeStart.getTime() && item.startValue.getTime() < rangeEnd.getTime()) {
364
                        item.isOutRange = true;
365
                    } else {
17✔
366
                        item.isOutRange = false;
367
                    }
1✔
368
                });
369
            }
370
        } else {
371
            const { rangeStart, rangeEnd } = this.getActualStartAndEnd(value);
1✔
372
            this.selectableData[type].forEach((item: AdvancedSelectableCell) => {
373
                if (item.startValue.getTime() >= rangeStart.getTime() && item.startValue.getTime() < rangeEnd.getTime()) {
374
                    item.isInRange = true;
375
                } else {
376
                    item.isInRange = false;
377
                }
378
            });
379
        }
380
        this.selectableData[type].forEach(item => (item.classMap = this.getClassMap(item)));
381
    }
17✔
382

383
    onMouseleave(event: Event, type: ThyDateGranularity, value: AdvancedSelectableCell) {
384
        if (value.isInRange) {
385
            this.selectableData[type].forEach(item => (item.isInRange = false));
386
        }
387
        if (value.isOutRange) {
388
            this.selectableData[type].forEach(item => (item.isOutRange = false));
389
        }
390
        this.selectableData[type].forEach(item => (item.classMap = this.getClassMap(item)));
391
    }
392

393
    getActualStartAndEnd(value: AdvancedSelectableCell) {
394
        const selectedStart = this.selectedValue[0].startValue;
395
        const selectedEnd = this.selectedValue[this.selectedValue.length - 1].endValue;
396
        let rangeStart: TinyDate, rangeEnd: TinyDate;
397
        if (value.startValue.isBeforeDay(selectedStart)) {
398
            rangeStart = value.startValue;
399
            rangeEnd = selectedStart;
400
        }
401
        if (value.startValue.isAfterDay(selectedEnd)) {
402
            rangeStart = selectedEnd;
403
            rangeEnd = value.endValue;
404
        }
405
        return { rangeStart, rangeEnd };
406
    }
407

408
    ngOnDestroy(): void {
409
        this.selectedValueChange$.complete();
410
    }
411
}
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