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

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

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

20.73
/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts
1
import {
2
    Directive, Input, ElementRef,
3
    Renderer2, Output, EventEmitter, Inject,
4
    LOCALE_ID, OnChanges, SimpleChanges, HostListener, OnInit, booleanAttribute
5
} from '@angular/core';
6
import {
7
    ControlValueAccessor,
8
    Validator, AbstractControl, ValidationErrors, NG_VALIDATORS, NG_VALUE_ACCESSOR,
9
} from '@angular/forms';
10
import { DOCUMENT } from '@angular/common';
11
import { IgxMaskDirective } from '../mask/mask.directive';
12
import { MaskParsingService } from '../mask/mask-parsing.service';
13
import { isDate, PlatformUtil } from '../../core/utils';
14
import { IgxDateTimeEditorEventArgs, DatePartInfo, DatePart } from './date-time-editor.common';
15
import { noop } from 'rxjs';
16
import { DatePartDeltas } from './date-time-editor.common';
17
import { DateTimeUtil } from '../../date-common/util/date-time.util';
18

19
/**
20
 * Date Time Editor provides a functionality to input, edit and format date and time.
21
 *
22
 * @igxModule IgxDateTimeEditorModule
23
 *
24
 * @igxParent IgxInputGroup
25
 *
26
 * @igxTheme igx-input-theme
27
 *
28
 * @igxKeywords date, time, editor
29
 *
30
 * @igxGroup Scheduling
31
 *
32
 * @remarks
33
 *
34
 * The Ignite UI Date Time Editor Directive makes it easy for developers to manipulate date/time user input.
35
 * It requires input in a specified or default input format which is visible in the input element as a placeholder.
36
 * It allows the input of only date (ex: 'dd/MM/yyyy'), only time (ex:'HH:mm tt') or both at once, if needed.
37
 * Supports display format that may differ from the input format.
38
 * Provides methods to increment and decrement any specific/targeted `DatePart`.
39
 *
40
 * @example
41
 * ```html
42
 * <igx-input-group>
43
 *   <input type="text" igxInput [igxDateTimeEditor]="'dd/MM/yyyy'" [displayFormat]="'shortDate'" [(ngModel)]="date"/>
44
 * </igx-input-group>
45
 * ```
46
 */
47
@Directive({
48
    selector: '[igxDateTimeEditor]',
49
    exportAs: 'igxDateTimeEditor',
50
    providers: [
51
        { provide: NG_VALUE_ACCESSOR, useExisting: IgxDateTimeEditorDirective, multi: true },
52
        { provide: NG_VALIDATORS, useExisting: IgxDateTimeEditorDirective, multi: true }
53
    ],
54
    standalone: true
55
})
56
export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnChanges, OnInit, Validator, ControlValueAccessor {
2✔
57
    /**
58
     * Locale settings used for value formatting.
59
     *
60
     * @remarks
61
     * Uses Angular's `LOCALE_ID` by default. Affects both input mask and display format if those are not set.
62
     * If a `locale` is set, it must be registered via `registerLocaleData`.
63
     * Please refer to https://angular.io/guide/i18n#i18n-pipes.
64
     * If it is not registered, `Intl` will be used for formatting.
65
     *
66
     * @example
67
     * ```html
68
     * <input igxDateTimeEditor [locale]="'en'">
69
     * ```
70
     */
71
    @Input()
72
    public locale: string;
73

74
    /**
75
     * Minimum value required for the editor to remain valid.
76
     *
77
     * @remarks
78
     * If a `string` value is passed, it must be in the defined input format.
79
     *
80
     * @example
81
     * ```html
82
     * <input igxDateTimeEditor [minValue]="minDate">
83
     * ```
84
     */
85
    public get minValue(): string | Date {
UNCOV
86
        return this._minValue;
×
87
    }
88

89
    @Input()
90
    public set minValue(value: string | Date) {
91
        this._minValue = value;
2✔
92
        this._onValidatorChange();
2✔
93
    }
94

95
    /**
96
     * Maximum value required for the editor to remain valid.
97
     *
98
     * @remarks
99
     * If a `string` value is passed in, it must be in the defined input format.
100
     *
101
     * @example
102
     * ```html
103
     * <input igxDateTimeEditor [maxValue]="maxDate">
104
     * ```
105
     */
106
    public get maxValue(): string | Date {
UNCOV
107
        return this._maxValue;
×
108
    }
109

110
    @Input()
111
    public set maxValue(value: string | Date) {
112
        this._maxValue = value;
2✔
113
        this._onValidatorChange();
2✔
114
    }
115

116
    /**
117
     * Specify if the currently spun date segment should loop over.
118
     *
119
     * @example
120
     * ```html
121
     * <input igxDateTimeEditor [spinLoop]="false">
122
     * ```
123
     */
124
    @Input({ transform: booleanAttribute })
125
    public spinLoop = true;
4✔
126

127
    /**
128
     * Set both pre-defined format options such as `shortDate` and `longDate`,
129
     * as well as constructed format string using characters supported by `DatePipe`, e.g. `EE/MM/yyyy`.
130
     *
131
     * @example
132
     * ```html
133
     * <input igxDateTimeEditor [displayFormat]="'shortDate'">
134
     * ```
135
     */
136
    @Input()
137
    public set displayFormat(value: string) {
138
        this._displayFormat = value;
4✔
139
        this.updateDefaultFormat();
4✔
140
    }
141

142
    public get displayFormat(): string {
143
        return this._displayFormat || this.inputFormat;
2!
144
    }
145

146
    /**
147
     * Expected user input format (and placeholder).
148
     *
149
     * @example
150
     * ```html
151
     * <input [igxDateTimeEditor]="'dd/MM/yyyy'">
152
     * ```
153
     */
154
    @Input(`igxDateTimeEditor`)
155
    public set inputFormat(value: string) {
156
        if (value) {
4✔
157
            this.setMask(value);
2✔
158
            this._inputFormat = value;
2✔
159
        }
160
    }
161

162
    public get inputFormat(): string {
163
        return this._inputFormat || this._defaultInputFormat;
1,032✔
164
    }
165

166
    /**
167
     * Editor value.
168
     *
169
     * @example
170
     * ```html
171
     * <input igxDateTimeEditor [value]="date">
172
     * ```
173
     */
174
    @Input()
175
    public set value(value: Date | string | undefined | null) {
176
        this._value = value;
2✔
177
        this.setDateValue(value);
2✔
178
        this.onChangeCallback(value);
2✔
179
        this.updateMask();
2✔
180
    }
181

182
    public get value(): Date | string | undefined | null {
UNCOV
183
        return this._value;
×
184
    }
185

186
    /**
187
     * Specify the default input format type. Defaults to `date`, which includes
188
     * only date parts for editing. Other valid options are `time` and `dateTime`.
189
     *
190
     * @example
191
     * ```html
192
     * <input igxDateTimeEditor [defaultFormatType]="'dateTime'">
193
     * ```
194
     */
195
    @Input()
196
    public defaultFormatType: 'date' | 'time' | 'dateTime' = 'date';
4✔
197

198
    /**
199
     * Delta values used to increment or decrement each editor date part on spin actions.
200
     * All values default to `1`.
201
     *
202
     * @example
203
     * ```html
204
     * <input igxDateTimeEditor [spinDelta]="{date: 5, minute: 30}">
205
     * ```
206
     */
207
    @Input()
208
    public spinDelta: DatePartDeltas;
209

210
    /**
211
     * Emitted when the editor's value has changed.
212
     *
213
     * @example
214
     * ```html
215
     * <input igxDateTimeEditor (valueChange)="valueChange($event)"/>
216
     * ```
217
     */
218
    @Output()
219
    public valueChange = new EventEmitter<Date | string>();
4✔
220

221
    /**
222
     * Emitted when the editor is not within a specified range or when the editor's value is in an invalid state.
223
     *
224
     * @example
225
     * ```html
226
     * <input igxDateTimeEditor [minValue]="minDate" [maxValue]="maxDate" (validationFailed)="onValidationFailed($event)"/>
227
     * ```
228
     */
229
    @Output()
230
    public validationFailed = new EventEmitter<IgxDateTimeEditorEventArgs>();
4✔
231

232
    private _inputFormat: string;
233
    private _displayFormat: string;
234
    private _oldValue: Date;
235
    private _dateValue: Date;
236
    private _onClear: boolean;
237
    private document: Document;
238
    private _defaultInputFormat: string;
239
    private _value?: Date | string;
240
    private _minValue: Date | string;
241
    private _maxValue: Date | string;
242
    private _inputDateParts: DatePartInfo[];
243
    private _datePartDeltas: DatePartDeltas = {
4✔
244
        date: 1,
245
        month: 1,
246
        year: 1,
247
        hours: 1,
248
        minutes: 1,
249
        seconds: 1,
250
        fractionalSeconds: 1
251
    };
252

253
    private onChangeCallback: (...args: any[]) => void = noop;
4✔
254
    private _onValidatorChange: (...args: any[]) => void = noop;
4✔
255

256
    private get datePartDeltas(): DatePartDeltas {
UNCOV
257
        return Object.assign({}, this._datePartDeltas, this.spinDelta);
×
258
    }
259

260
    private get emptyMask(): string {
UNCOV
261
        return this.maskParser.applyMask(null, this.maskOptions);
×
262
    }
263

264
    private get targetDatePart(): DatePart {
265
        // V.K. May 16th, 2022 #11554 Get correct date part in shadow DOM
UNCOV
266
        if (this.document.activeElement === this.nativeElement ||
×
267
            this.document.activeElement?.shadowRoot?.activeElement === this.nativeElement) {
UNCOV
268
            return this._inputDateParts
×
UNCOV
269
                .find(p => p.start <= this.selectionStart && this.selectionStart <= p.end && p.type !== DatePart.Literal)?.type;
×
270
        } else {
UNCOV
271
            if (this._inputDateParts.some(p => p.type === DatePart.Date)) {
×
UNCOV
272
                return DatePart.Date;
×
UNCOV
273
            } else if (this._inputDateParts.some(p => p.type === DatePart.Hours)) {
×
UNCOV
274
                return DatePart.Hours;
×
275
            }
276
        }
277
    }
278

279
    private get hasDateParts(): boolean {
UNCOV
280
        return this._inputDateParts.some(
×
UNCOV
281
            p => p.type === DatePart.Date
×
282
                || p.type === DatePart.Month
283
                || p.type === DatePart.Year);
284
    }
285

286
    private get hasTimeParts(): boolean {
UNCOV
287
        return this._inputDateParts.some(
×
UNCOV
288
            p => p.type === DatePart.Hours
×
289
                || p.type === DatePart.Minutes
290
                || p.type === DatePart.Seconds
291
                || p.type === DatePart.FractionalSeconds);
292
    }
293

294
    private get dateValue(): Date {
295
        return this._dateValue;
10✔
296
    }
297

298
    constructor(
299
        renderer: Renderer2,
300
        elementRef: ElementRef,
301
        maskParser: MaskParsingService,
302
        platform: PlatformUtil,
303
        @Inject(DOCUMENT) private _document: any,
4✔
304
        @Inject(LOCALE_ID) private _locale: any) {
4✔
305
        super(elementRef, maskParser, renderer, platform);
4✔
306
        this.document = this._document as Document;
4✔
307
        this.locale = this.locale || this._locale;
4✔
308
    }
309

310
    @HostListener('wheel', ['$event'])
311
    public onWheel(event: WheelEvent): void {
UNCOV
312
        if (!this._focused) {
×
313
            return;
×
314
        }
UNCOV
315
        event.preventDefault();
×
UNCOV
316
        event.stopPropagation();
×
UNCOV
317
        if (event.deltaY > 0) {
×
UNCOV
318
            this.decrement();
×
319
        } else {
320
            this.increment();
×
321
        }
322
    }
323

324
    public override ngOnInit(): void {
325
        this.updateDefaultFormat();
4✔
326
        this.setMask(this.inputFormat);
4✔
327
        this.updateMask();
4✔
328
    }
329

330
    /** @hidden @internal */
331
    public ngOnChanges(changes: SimpleChanges): void {
332
        if (changes['locale'] && !changes['locale'].firstChange ||
4!
333
            changes['defaultFormatType'] && !changes['defaultFormatType'].firstChange
334
        ) {
UNCOV
335
            this.updateDefaultFormat();
×
UNCOV
336
            this.setMask(this.inputFormat);
×
UNCOV
337
            this.updateMask();
×
338
        }
339
        if (changes['inputFormat'] && !changes['inputFormat'].firstChange) {
4!
UNCOV
340
            this.updateMask();
×
341
        }
342
    }
343

344

345
    /** Clear the input element value. */
346
    public clear(): void {
UNCOV
347
        this._onClear = true;
×
UNCOV
348
        this.updateValue(null);
×
UNCOV
349
        this.setSelectionRange(0, this.inputValue.length);
×
UNCOV
350
        this._onClear = false;
×
351
    }
352

353
    /**
354
     * Increment specified DatePart.
355
     *
356
     * @param datePart The optional DatePart to increment. Defaults to Date or Hours (when Date is absent from the inputFormat - ex:'HH:mm').
357
     * @param delta The optional delta to increment by. Overrides `spinDelta`.
358
     */
359
    public increment(datePart?: DatePart, delta?: number): void {
UNCOV
360
        const targetPart = datePart || this.targetDatePart;
×
UNCOV
361
        if (!targetPart) {
×
362
            return;
×
363
        }
UNCOV
364
        const newValue = this.trySpinValue(targetPart, delta);
×
UNCOV
365
        this.updateValue(newValue);
×
366
    }
367

368
    /**
369
     * Decrement specified DatePart.
370
     *
371
     * @param datePart The optional DatePart to decrement. Defaults to Date or Hours (when Date is absent from the inputFormat - ex:'HH:mm').
372
     * @param delta The optional delta to decrement by. Overrides `spinDelta`.
373
     */
374
    public decrement(datePart?: DatePart, delta?: number): void {
UNCOV
375
        const targetPart = datePart || this.targetDatePart;
×
UNCOV
376
        if (!targetPart) {
×
377
            return;
×
378
        }
UNCOV
379
        const newValue = this.trySpinValue(targetPart, delta, true);
×
UNCOV
380
        this.updateValue(newValue);
×
381
    }
382

383
    /** @hidden @internal */
384
    public override writeValue(value: any): void {
385
        this._value = value;
4✔
386
        this.setDateValue(value);
4✔
387
        this.updateMask();
4✔
388
    }
389

390
    /** @hidden @internal */
391
    public validate(control: AbstractControl): ValidationErrors | null {
392
        if (!control.value) {
10✔
393
            return null;
10✔
394
        }
395
        // InvalidDate handling
UNCOV
396
        if (isDate(control.value) && !DateTimeUtil.isValidDate(control.value)) {
×
397
            return { value: true };
×
398
        }
399

UNCOV
400
        let errors = {};
×
UNCOV
401
        const value = DateTimeUtil.isValidDate(control.value) ? control.value : DateTimeUtil.parseIsoDate(control.value);
×
UNCOV
402
        const minValueDate = DateTimeUtil.isValidDate(this.minValue) ? this.minValue : this.parseDate(this.minValue);
×
UNCOV
403
        const maxValueDate = DateTimeUtil.isValidDate(this.maxValue) ? this.maxValue : this.parseDate(this.maxValue);
×
UNCOV
404
        if (minValueDate || maxValueDate) {
×
UNCOV
405
            errors = DateTimeUtil.validateMinMax(value,
×
406
                minValueDate, maxValueDate,
407
                this.hasTimeParts, this.hasDateParts);
408
        }
409

UNCOV
410
        return Object.keys(errors).length > 0 ? errors : null;
×
411
    }
412

413
    /** @hidden @internal */
414
    public registerOnValidatorChange?(fn: () => void): void {
415
        this._onValidatorChange = fn;
2✔
416
    }
417

418
    /** @hidden @internal */
419
    public override registerOnChange(fn: any): void {
420
        this.onChangeCallback = fn;
2✔
421
    }
422

423
    /** @hidden @internal */
424
    public override registerOnTouched(fn: any): void {
425
        this._onTouchedCallback = fn;
2✔
426
    }
427

428
    /** @hidden @internal */
429
    public setDisabledState?(_isDisabled: boolean): void { }
430

431
    /** @hidden @internal */
432
    public override onCompositionEnd(): void {
UNCOV
433
        super.onCompositionEnd();
×
434

UNCOV
435
        this.updateValue(this.parseDate(this.inputValue));
×
UNCOV
436
        this.updateMask();
×
437
    }
438

439
    /** @hidden @internal */
440
    public override onInputChanged(event): void {
UNCOV
441
        super.onInputChanged(event);
×
UNCOV
442
        if (this._composing) {
×
UNCOV
443
            return;
×
444
        }
445

UNCOV
446
        if (this.inputIsComplete()) {
×
UNCOV
447
            const parsedDate = this.parseDate(this.inputValue);
×
UNCOV
448
            if (DateTimeUtil.isValidDate(parsedDate)) {
×
UNCOV
449
                this.updateValue(parsedDate);
×
450
            } else {
UNCOV
451
                const oldValue = this.value && new Date(this.dateValue.getTime());
×
UNCOV
452
                const args: IgxDateTimeEditorEventArgs = { oldValue, newValue: parsedDate, userInput: this.inputValue };
×
UNCOV
453
                this.validationFailed.emit(args);
×
UNCOV
454
                if (DateTimeUtil.isValidDate(args.newValue)) {
×
455
                    this.updateValue(args.newValue);
×
456
                } else {
UNCOV
457
                    this.updateValue(null);
×
458
                }
459
            }
460
        } else {
UNCOV
461
            this.updateValue(null);
×
462
        }
463
    }
464

465
    /** @hidden @internal */
466
    public override onKeyDown(event: KeyboardEvent): void {
UNCOV
467
        if (this.nativeElement.readOnly) {
×
468
            return;
×
469
        }
UNCOV
470
        super.onKeyDown(event);
×
UNCOV
471
        const key = event.key;
×
472

UNCOV
473
        if (event.altKey) {
×
474
            return;
×
475
        }
476

UNCOV
477
        if (key === this.platform.KEYMAP.ARROW_DOWN || key === this.platform.KEYMAP.ARROW_UP) {
×
UNCOV
478
            this.spin(event);
×
UNCOV
479
            return;
×
480
        }
481

UNCOV
482
        if (event.ctrlKey && key === this.platform.KEYMAP.SEMICOLON) {
×
483
            this.updateValue(new Date());
×
484
        }
485

UNCOV
486
        this.moveCursor(event);
×
487
    }
488

489
    /** @hidden @internal */
490
    public override onFocus(): void {
UNCOV
491
        if (this.nativeElement.readOnly) {
×
UNCOV
492
            return;
×
493
        }
UNCOV
494
        this._focused = true;
×
UNCOV
495
        this._onTouchedCallback();
×
UNCOV
496
        this.updateMask();
×
UNCOV
497
        super.onFocus();
×
UNCOV
498
        this.nativeElement.select();
×
499
    }
500

501
    /** @hidden @internal */
502
    public override onBlur(value: string): void {
UNCOV
503
        this._focused = false;
×
UNCOV
504
        if (!this.inputIsComplete() && this.inputValue !== this.emptyMask) {
×
UNCOV
505
            this.updateValue(this.parseDate(this.inputValue));
×
506
        } else {
UNCOV
507
            this.updateMask();
×
508
        }
509

510
        // TODO: think of a better way to set displayValuePipe in mask directive
UNCOV
511
        if (this.displayValuePipe) {
×
UNCOV
512
            return;
×
513
        }
514

UNCOV
515
        super.onBlur(value);
×
516
    }
517

518
    // the date editor sets its own inputFormat as its placeholder if none is provided
519
    /** @hidden */
520
    protected override setPlaceholder(_value: string): void { }
521

522
    private updateDefaultFormat(): void {
523
        this._defaultInputFormat = DateTimeUtil.getNumericInputFormat(this.locale, this._displayFormat)
8✔
524
                                || DateTimeUtil.getDefaultInputFormat(this.locale, this.defaultFormatType);
525
        this.setMask(this.inputFormat);
8✔
526
    }
527

528
    private updateMask(): void {
529
        if (this._focused) {
10!
530
            // store the cursor position as it will be moved during masking
UNCOV
531
            const cursor = this.selectionEnd;
×
UNCOV
532
            this.inputValue = this.getMaskedValue();
×
UNCOV
533
            this.setSelectionRange(cursor);
×
534
        } else {
535
            if (!this.dateValue || !DateTimeUtil.isValidDate(this.dateValue)) {
10!
536
                this.inputValue = '';
10✔
537
                return;
10✔
538
            }
UNCOV
539
            if (this.displayValuePipe) {
×
540
                // TODO: remove when formatter func has been deleted
UNCOV
541
                this.inputValue = this.displayValuePipe.transform(this.value);
×
UNCOV
542
                return;
×
543
            }
UNCOV
544
            const format = this.displayFormat || this.inputFormat;
×
UNCOV
545
            if (format) {
×
UNCOV
546
                this.inputValue = DateTimeUtil.formatDate(this.dateValue, format.replace('tt', 'aa'), this.locale);
×
547
            } else {
UNCOV
548
                this.inputValue = this.dateValue.toLocaleString();
×
549
            }
550
        }
551
    }
552

553
    private setMask(inputFormat: string): void {
554
        const oldFormat = this._inputDateParts?.map(p => p.format).join('');
70✔
555
        this._inputDateParts = DateTimeUtil.parseDateTimeFormat(inputFormat);
14✔
556
        inputFormat = this._inputDateParts.map(p => p.format).join('');
98✔
557
        const mask = (inputFormat || this._defaultInputFormat)
14!
558
        .replace(new RegExp(/(?=[^at])[\w]/, 'g'), '0');
559
        this.mask = mask.replaceAll(/(a{1,2})|tt/g, match => 'L'.repeat(match.length === 1 ? 1 : 2));
14!
560
        const placeholder = this.nativeElement.placeholder;
14✔
561
        if (!placeholder || oldFormat === placeholder) {
14✔
562
            this.renderer.setAttribute(this.nativeElement, 'placeholder', inputFormat);
1✔
563
        }
564
    }
565

566
    private parseDate(val: string): Date | null {
UNCOV
567
        if (!val) {
×
UNCOV
568
            return null;
×
569
        }
570

UNCOV
571
        return DateTimeUtil.parseValueFromMask(val, this._inputDateParts, this.promptChar);
×
572
    }
573

574
    private getMaskedValue(): string {
UNCOV
575
        let mask = this.emptyMask;
×
UNCOV
576
        if (DateTimeUtil.isValidDate(this.value) || DateTimeUtil.parseIsoDate(this.value)) {
×
UNCOV
577
            for (const part of this._inputDateParts) {
×
UNCOV
578
                if (part.type === DatePart.Literal) {
×
UNCOV
579
                    continue;
×
580
                }
UNCOV
581
                const targetValue = this.getPartValue(part, part.format.length);
×
UNCOV
582
                mask = this.maskParser.replaceInMask(mask, targetValue, this.maskOptions, part.start, part.end).value;
×
583
            }
UNCOV
584
            return mask;
×
585
        }
UNCOV
586
        if (!this.inputIsComplete() || !this._onClear) {
×
UNCOV
587
            return this.inputValue;
×
588
        }
UNCOV
589
        return mask;
×
590
    }
591

592

593
    private valueInRange(value: Date): boolean {
UNCOV
594
        if (!value) {
×
595
            return false;
×
596
        }
597

UNCOV
598
        let errors = {};
×
UNCOV
599
        const minValueDate = DateTimeUtil.isValidDate(this.minValue) ? this.minValue : this.parseDate(this.minValue);
×
UNCOV
600
        const maxValueDate = DateTimeUtil.isValidDate(this.maxValue) ? this.maxValue : this.parseDate(this.maxValue);
×
UNCOV
601
        if (minValueDate || maxValueDate) {
×
UNCOV
602
            errors = DateTimeUtil.validateMinMax(value,
×
603
                this.minValue, this.maxValue,
604
                this.hasTimeParts, this.hasDateParts);
605
        }
606

UNCOV
607
        return Object.keys(errors).length === 0;
×
608
    }
609

610
    private spinValue(datePart: DatePart, delta: number): Date {
UNCOV
611
        if (!this.dateValue || !DateTimeUtil.isValidDate(this.dateValue)) {
×
612
            return null;
×
613
        }
UNCOV
614
        const newDate = new Date(this.dateValue.getTime());
×
UNCOV
615
        switch (datePart) {
×
616
            case DatePart.Date:
UNCOV
617
                DateTimeUtil.spinDate(delta, newDate, this.spinLoop);
×
UNCOV
618
                break;
×
619
            case DatePart.Month:
UNCOV
620
                DateTimeUtil.spinMonth(delta, newDate, this.spinLoop);
×
UNCOV
621
                break;
×
622
            case DatePart.Year:
UNCOV
623
                DateTimeUtil.spinYear(delta, newDate);
×
UNCOV
624
                break;
×
625
            case DatePart.Hours:
UNCOV
626
                DateTimeUtil.spinHours(delta, newDate, this.spinLoop);
×
UNCOV
627
                break;
×
628
            case DatePart.Minutes:
UNCOV
629
                DateTimeUtil.spinMinutes(delta, newDate, this.spinLoop);
×
UNCOV
630
                break;
×
631
            case DatePart.Seconds:
UNCOV
632
                DateTimeUtil.spinSeconds(delta, newDate, this.spinLoop);
×
UNCOV
633
                break;
×
634
            case DatePart.FractionalSeconds:
UNCOV
635
                DateTimeUtil.spinFractionalSeconds(delta, newDate, this.spinLoop);
×
UNCOV
636
                break;
×
637
            case DatePart.AmPm:
UNCOV
638
                const formatPart = this._inputDateParts.find(dp => dp.type === DatePart.AmPm);
×
UNCOV
639
                const amPmFromMask = this.inputValue.substring(formatPart.start, formatPart.end);
×
UNCOV
640
                return DateTimeUtil.spinAmPm(newDate, this.dateValue, amPmFromMask);
×
641
        }
642

UNCOV
643
        return newDate;
×
644
    }
645

646
    private trySpinValue(datePart: DatePart, delta?: number, negative = false): Date {
×
UNCOV
647
        if (!delta) {
×
648
            // default to 1 if a delta is set to 0 or any other falsy value
UNCOV
649
            delta = this.datePartDeltas[datePart] || 1;
×
650
        }
UNCOV
651
        const spinValue = negative ? -Math.abs(delta) : Math.abs(delta);
×
UNCOV
652
        return this.spinValue(datePart, spinValue) || new Date();
×
653
    }
654

655
    private setDateValue(value: Date | string): void {
656
        this._dateValue = DateTimeUtil.isValidDate(value)
6!
657
            ? value
658
            : DateTimeUtil.parseIsoDate(value);
659
    }
660

661
    private updateValue(newDate: Date): void {
UNCOV
662
        this._oldValue = this.dateValue;
×
UNCOV
663
        this.value = newDate;
×
664

665
        // TODO: should we emit events here?
UNCOV
666
        if (this.inputIsComplete() || this.inputValue === this.emptyMask) {
×
UNCOV
667
            this.valueChange.emit(this.dateValue);
×
668
        }
UNCOV
669
        if (this.dateValue && !this.valueInRange(this.dateValue)) {
×
UNCOV
670
            this.validationFailed.emit({ oldValue: this._oldValue, newValue: this.dateValue, userInput: this.inputValue });
×
671
        }
672
    }
673

674
    private toTwelveHourFormat(value: string): number {
UNCOV
675
        let hour = parseInt(value.replace(new RegExp(this.promptChar, 'g'), '0'), 10);
×
UNCOV
676
        if (hour > 12) {
×
UNCOV
677
            hour -= 12;
×
UNCOV
678
        } else if (hour === 0) {
×
UNCOV
679
            hour = 12;
×
680
        }
681

UNCOV
682
        return hour;
×
683
    }
684

685
    private getPartValue(datePartInfo: DatePartInfo, partLength: number): string {
686
        let maskedValue;
UNCOV
687
        const datePart = datePartInfo.type;
×
UNCOV
688
        switch (datePart) {
×
689
            case DatePart.Date:
UNCOV
690
                maskedValue = this.dateValue.getDate();
×
UNCOV
691
                break;
×
692
            case DatePart.Month:
693
                // months are zero based
UNCOV
694
                maskedValue = this.dateValue.getMonth() + 1;
×
UNCOV
695
                break;
×
696
            case DatePart.Year:
UNCOV
697
                if (partLength === 2) {
×
UNCOV
698
                    maskedValue = this.prependValue(
×
699
                        parseInt(this.dateValue.getFullYear().toString().slice(-2), 10), partLength, '0');
700
                } else {
UNCOV
701
                    maskedValue = this.dateValue.getFullYear();
×
702
                }
UNCOV
703
                break;
×
704
            case DatePart.Hours:
UNCOV
705
                if (datePartInfo.format.indexOf('h') !== -1) {
×
UNCOV
706
                    maskedValue = this.prependValue(
×
707
                        this.toTwelveHourFormat(this.dateValue.getHours().toString()), partLength, '0');
708
                } else {
UNCOV
709
                    maskedValue = this.dateValue.getHours();
×
710
                }
UNCOV
711
                break;
×
712
            case DatePart.Minutes:
UNCOV
713
                maskedValue = this.dateValue.getMinutes();
×
UNCOV
714
                break;
×
715
            case DatePart.Seconds:
UNCOV
716
                maskedValue = this.dateValue.getSeconds();
×
UNCOV
717
                break;
×
718
            case DatePart.FractionalSeconds:
UNCOV
719
                partLength = 3;
×
UNCOV
720
                maskedValue = this.prependValue(this.dateValue.getMilliseconds(), 3, '00');
×
UNCOV
721
                break;
×
722
            case DatePart.AmPm:
UNCOV
723
                maskedValue = DateTimeUtil.getAmPmValue(partLength, this.dateValue.getHours() < 12);
×
UNCOV
724
                break;
×
725
        }
726

UNCOV
727
        if (datePartInfo.type !== DatePart.AmPm) {
×
UNCOV
728
            return this.prependValue(maskedValue, partLength, '0');
×
729
        }
730

UNCOV
731
        return maskedValue;
×
732
    }
733

734
    private prependValue(value: number, partLength: number, prependChar: string): string {
UNCOV
735
        return (prependChar + value.toString()).slice(-partLength);
×
736
    }
737

738
    private spin(event: KeyboardEvent): void {
UNCOV
739
        event.preventDefault();
×
UNCOV
740
        switch (event.key) {
×
741
            case this.platform.KEYMAP.ARROW_UP:
UNCOV
742
                this.increment();
×
UNCOV
743
                break;
×
744
            case this.platform.KEYMAP.ARROW_DOWN:
UNCOV
745
                this.decrement();
×
UNCOV
746
                break;
×
747
        }
748
    }
749

750
    private inputIsComplete(): boolean {
UNCOV
751
        return this.inputValue.indexOf(this.promptChar) === -1;
×
752
    }
753

754
    private moveCursor(event: KeyboardEvent): void {
UNCOV
755
        const value = (event.target as HTMLInputElement).value;
×
UNCOV
756
        switch (event.key) {
×
757
            case this.platform.KEYMAP.ARROW_LEFT:
UNCOV
758
                if (event.ctrlKey) {
×
UNCOV
759
                    event.preventDefault();
×
UNCOV
760
                    this.setSelectionRange(this.getNewPosition(value));
×
761
                }
UNCOV
762
                break;
×
763
            case this.platform.KEYMAP.ARROW_RIGHT:
UNCOV
764
                if (event.ctrlKey) {
×
UNCOV
765
                    event.preventDefault();
×
UNCOV
766
                    this.setSelectionRange(this.getNewPosition(value, 1));
×
767
                }
UNCOV
768
                break;
×
769
        }
770
    }
771

772
    /**
773
     * Move the cursor in a specific direction until it reaches a date/time separator.
774
     * Then return its index.
775
     *
776
     * @param value The string it operates on.
777
     * @param direction 0 is left, 1 is right. Default is 0.
778
     */
779
    private getNewPosition(value: string, direction = 0): number {
×
UNCOV
780
        const literals = this._inputDateParts.filter(p => p.type === DatePart.Literal);
×
UNCOV
781
        let cursorPos = this.selectionStart;
×
UNCOV
782
        if (!direction) {
×
UNCOV
783
            do {
×
UNCOV
784
                cursorPos = cursorPos > 0 ? --cursorPos : cursorPos;
×
UNCOV
785
            } while (!literals.some(l => l.end === cursorPos) && cursorPos > 0);
×
UNCOV
786
            return cursorPos;
×
787
        } else {
UNCOV
788
            do {
×
UNCOV
789
                cursorPos++;
×
UNCOV
790
            } while (!literals.some(l => l.start === cursorPos) && cursorPos < value.length);
×
UNCOV
791
            return cursorPos;
×
792
        }
793
    }
794
}
795

796

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