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

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM UTC 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

23.02
/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts
1
import {
2
    Directive, ElementRef, EventEmitter, HostListener,
3
    Output, PipeTransform, Renderer2,
4
    Input, OnInit, AfterViewChecked, booleanAttribute,
5
} from '@angular/core';
6
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
7
import { MaskParsingService, MaskOptions, parseMask } from './mask-parsing.service';
8
import { IBaseEventArgs, PlatformUtil } from '../../core/utils';
9
import { noop } from 'rxjs';
10

11
@Directive({
12
    providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: IgxMaskDirective, multi: true }],
13
    selector: '[igxMask]',
14
    exportAs: 'igxMask',
15
    standalone: true
16
})
17
export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueAccessor {
2✔
18
    /**
19
     * Sets the input mask.
20
     * ```html
21
     * <input [igxMask] = "'00/00/0000'">
22
     * ```
23
     */
24
    @Input('igxMask')
25
    public get mask(): string {
26
        return this._mask || this.defaultMask;
4✔
27
    }
28

29
    public set mask(val: string) {
30
        // B.P. 9th June 2021 #7490
31
        if (val !== this._mask) {
14✔
32
            const cleanInputValue = this.maskParser.parseValueFromMask(this.inputValue, this.maskOptions);
4✔
33
            this.setPlaceholder(val);
4✔
34
            this._mask = val;
4✔
35
            this.updateInputValue(cleanInputValue);
4✔
36
        }
37
    }
38

39
    /**
40
     * Sets the character representing a fillable spot in the input mask.
41
     * Default value is "'_'".
42
     * ```html
43
     * <input [promptChar] = "'/'">
44
     * ```
45
     */
46
    @Input()
47
    public promptChar = '_';
4✔
48

49
    /**
50
     * Specifies if the bound value includes the formatting symbols.
51
     * ```html
52
     * <input [includeLiterals] = "true">
53
     * ```
54
     */
55
    @Input({ transform: booleanAttribute })
56
    public includeLiterals: boolean;
57

58
    /**
59
     * Specifies a pipe to be used on blur.
60
     * ```html
61
     * <input [displayValuePipe] = "displayFormatPipe">
62
     * ```
63
     */
64
    @Input()
65
    public displayValuePipe: PipeTransform;
66

67
    /**
68
     * Specifies a pipe to be used on focus.
69
     * ```html
70
     * <input [focusedValuePipe] = "inputFormatPipe">
71
     * ```
72
     */
73
    @Input()
74
    public focusedValuePipe: PipeTransform;
75

76
    /**
77
     * Emits an event each time the value changes.
78
     * Provides `rawValue: string` and `formattedValue: string` as event arguments.
79
     * ```html
80
     * <input (valueChanged) = "valueChanged(rawValue: string, formattedValue: string)">
81
     * ```
82
     */
83
    @Output()
84
    public valueChanged = new EventEmitter<IMaskEventArgs>();
4✔
85

86
    /** @hidden */
87
    public get nativeElement(): HTMLInputElement {
88
        return this.elementRef.nativeElement;
61✔
89
    }
90

91
    /** @hidden @internal; */
92
    protected get inputValue(): string {
93
        return this.nativeElement.value;
32✔
94
    }
95

96
    /** @hidden @internal */
97
    protected set inputValue(val: string) {
98
        this.nativeElement.value = val;
14✔
99
    }
100

101
    /** @hidden */
102
    protected get maskOptions(): MaskOptions {
103
        const format = this.mask || this.defaultMask;
4!
104
        const promptChar = this.promptChar && this.promptChar.substring(0, 1);
4✔
105
        return { format, promptChar };
4✔
106
    }
107

108
    /** @hidden */
109
    protected get selectionStart(): number {
110
        // Edge(classic) and FF don't select text on drop
UNCOV
111
        return this.nativeElement.selectionStart === this.nativeElement.selectionEnd && this._hasDropAction ?
×
112
            this.nativeElement.selectionEnd - this._droppedData.length :
113
            this.nativeElement.selectionStart;
114
    }
115

116
    /** @hidden */
117
    protected get selectionEnd(): number {
UNCOV
118
        return this.nativeElement.selectionEnd;
×
119
    }
120

121
    /** @hidden */
122
    protected get start(): number {
123
        return this._start;
×
124
    }
125

126
    /** @hidden */
127
    protected get end(): number {
128
        return this._end;
×
129
    }
130

131
    protected _composing: boolean;
132
    protected _compositionStartIndex: number;
133
    protected _focused = false;
4✔
134
    private _compositionValue: string;
135
    private _end = 0;
4✔
136
    private _start = 0;
4✔
137
    private _key: string;
138
    private _mask: string;
139
    private _oldText = '';
4✔
140
    private _dataValue = '';
4✔
141
    private _droppedData: string;
142
    private _hasDropAction: boolean;
143

144
    private readonly defaultMask = 'CCCCCCCCCC';
4✔
145

146
    protected _onTouchedCallback: () => void = noop;
4✔
147
    protected _onChangeCallback: (_: any) => void = noop;
4✔
148

149
    constructor(
150
        protected elementRef: ElementRef<HTMLInputElement>,
4✔
151
        protected maskParser: MaskParsingService,
4✔
152
        protected renderer: Renderer2,
4✔
153
        protected platform: PlatformUtil) { }
4✔
154

155
    /** @hidden */
156
    @HostListener('keydown', ['$event'])
157
    public onKeyDown(event: KeyboardEvent): void {
UNCOV
158
        const key = event.key;
×
UNCOV
159
        if (!key) {
×
UNCOV
160
            return;
×
161
        }
162

UNCOV
163
        if ((event.ctrlKey && (key === this.platform.KEYMAP.Z || key === this.platform.KEYMAP.Y))) {
×
164
            event.preventDefault();
×
165
        }
166

UNCOV
167
        this._key = key;
×
UNCOV
168
        this._start = this.selectionStart;
×
UNCOV
169
        this._end = this.selectionEnd;
×
170
    }
171

172
    /** @hidden @internal */
173
    @HostListener('compositionstart')
174
    public onCompositionStart(): void {
UNCOV
175
        if (!this._composing) {
×
UNCOV
176
            this._compositionStartIndex = this._start;
×
UNCOV
177
            this._composing = true;
×
178
        }
179
    }
180

181
    /** @hidden @internal */
182
    @HostListener('compositionend')
183
    public onCompositionEnd(): void {
UNCOV
184
        this._start = this._compositionStartIndex;
×
UNCOV
185
        const end = this.selectionEnd;
×
UNCOV
186
        const valueToParse = this.inputValue.substring(this._start, end);
×
UNCOV
187
        this.updateInput(valueToParse);
×
UNCOV
188
        this._end = this.selectionEnd;
×
UNCOV
189
        this._compositionValue = this.inputValue;
×
190
    }
191

192
    /** @hidden @internal */
193
    @HostListener('input', ['$event'])
194
    public onInputChanged(event): void {
195
        /**
196
         * '!this._focused' is a fix for #8165
197
         * On page load IE triggers input events before focus events and
198
         * it does so for every single input on the page.
199
         * The mask needs to be prevented from doing anything while this is happening because
200
         * the end user will be unable to blur the input.
201
         * https://stackoverflow.com/questions/21406138/input-event-triggered-on-internet-explorer-when-placeholder-changed
202
         */
203

UNCOV
204
        if (this._composing) {
×
UNCOV
205
            if (this.inputValue.length < this._oldText.length) {
×
206
                // software keyboard input delete
UNCOV
207
                this._key = this.platform.KEYMAP.BACKSPACE;
×
208
            }
UNCOV
209
            return;
×
210
        }
211

212
        // After the compositionend event Chromium triggers input events of type 'deleteContentBackward' and
213
        // we need to adjust the start and end indexes to include mask literals
UNCOV
214
        if (event.inputType === 'deleteContentBackward' && this._key !== this.platform.KEYMAP.BACKSPACE) {
×
215
            const isInputComplete = this._compositionStartIndex === 0 && this._end === this.mask.length;
×
216
            let numberOfMaskLiterals = 0;
×
217
            const literalPos = parseMask(this.maskOptions.format).literals.keys();
×
218
            for (const index of literalPos) {
×
219
                if (index >= this._compositionStartIndex && index <= this._end) {
×
220
                    numberOfMaskLiterals++;
×
221
                }
222
            }
223
            this.inputValue = isInputComplete ?
×
224
                this.inputValue.substring(0, this.selectionEnd - numberOfMaskLiterals) + this.inputValue.substring(this.selectionEnd)
225
                : this._compositionValue?.substring(0, this._compositionStartIndex) || this.inputValue;
×
226

227
            if (this._compositionValue) {
×
228
                this._start = this.selectionStart;
×
229
                this._end = this.selectionEnd;
×
230
                this.nativeElement.selectionStart = isInputComplete ? this._start - numberOfMaskLiterals : this._compositionStartIndex;
×
231
                this.nativeElement.selectionEnd = this._end - numberOfMaskLiterals;
×
232
                this.nativeElement.selectionEnd = this._end;
×
233
                this._start = this.selectionStart;
×
234
                this._end = this.selectionEnd;
×
235
            }
236
        }
237

UNCOV
238
        if (this._hasDropAction) {
×
239
            this._start = this.selectionStart;
×
240
        }
241

UNCOV
242
        let valueToParse = '';
×
UNCOV
243
        switch (this._key) {
×
244
            case this.platform.KEYMAP.DELETE:
UNCOV
245
                this._end = this._start === this._end ? ++this._end : this._end;
×
UNCOV
246
                break;
×
247
            case this.platform.KEYMAP.BACKSPACE:
248
                this._start = this.selectionStart;
×
249
                break;
×
250
            default:
UNCOV
251
                valueToParse = this.inputValue.substring(this._start, this.selectionEnd);
×
UNCOV
252
                break;
×
253
        }
254

UNCOV
255
        this.updateInput(valueToParse);
×
256
    }
257

258
    /** @hidden */
259
    @HostListener('paste')
260
    public onPaste(): void {
UNCOV
261
        this._oldText = this.inputValue;
×
UNCOV
262
        this._start = this.selectionStart;
×
263
    }
264

265
    /** @hidden */
266
    @HostListener('focus')
267
    public onFocus(): void {
UNCOV
268
        if (this.nativeElement.readOnly) {
×
UNCOV
269
            return;
×
270
        }
UNCOV
271
        this._focused = true;
×
UNCOV
272
        this.showMask(this.inputValue);
×
273
    }
274

275
    /** @hidden */
276
    @HostListener('blur', ['$event.target.value'])
277
    public onBlur(value: string): void {
UNCOV
278
        this._focused = false;
×
UNCOV
279
        this.showDisplayValue(value);
×
UNCOV
280
        this._onTouchedCallback();
×
281
    }
282

283
    /** @hidden */
284
    @HostListener('dragenter')
285
    public onDragEnter(): void {
UNCOV
286
        if (!this._focused && !this._dataValue) {
×
UNCOV
287
            this.showMask(this._dataValue);
×
288
        }
289
    }
290

291
    /** @hidden */
292
    @HostListener('dragleave')
293
    public onDragLeave(): void {
UNCOV
294
        if (!this._focused) {
×
UNCOV
295
            this.showDisplayValue(this.inputValue);
×
296
        }
297
    }
298

299
    /** @hidden */
300
    @HostListener('drop', ['$event'])
301
    public onDrop(event: DragEvent): void {
UNCOV
302
        this._hasDropAction = true;
×
UNCOV
303
        this._droppedData = event.dataTransfer.getData('text');
×
304
    }
305

306
    /** @hidden */
307
    public ngOnInit(): void {
UNCOV
308
        this.setPlaceholder(this.maskOptions.format);
×
309
    }
310

311
    /**
312
     * TODO: Remove after date/time picker integration refactor
313
     *
314
     * @hidden
315
     */
316
    public ngAfterViewChecked(): void {
317
        if (this._composing) {
24!
318
            return;
×
319
        }
320
        this._oldText = this.inputValue;
24✔
321
    }
322

323
    /** @hidden */
324
    public writeValue(value: string): void {
UNCOV
325
        if (this.promptChar && this.promptChar.length > 1) {
×
326
            this.maskOptions.promptChar = this.promptChar.substring(0, 1);
×
327
        }
328

UNCOV
329
        this.inputValue = value ? this.maskParser.applyMask(value, this.maskOptions) : '';
×
UNCOV
330
        if (this.displayValuePipe) {
×
UNCOV
331
            this.inputValue = this.displayValuePipe.transform(this.inputValue);
×
332
        }
333

UNCOV
334
        this._dataValue = this.includeLiterals ? this.inputValue : value;
×
335

UNCOV
336
        this.valueChanged.emit({ rawValue: value, formattedValue: this.inputValue });
×
337
    }
338

339
    /** @hidden */
340
    public registerOnChange(fn: (_: any) => void): void {
UNCOV
341
        this._onChangeCallback = fn;
×
342
    }
343

344
    /** @hidden */
345
    public registerOnTouched(fn: () => void): void {
UNCOV
346
        this._onTouchedCallback = fn;
×
347
    }
348

349
    /** @hidden */
350
    protected showMask(value: string): void {
UNCOV
351
        if (this.focusedValuePipe) {
×
352
            // TODO(D.P.): focusedValuePipe should be deprecated or force-checked to match mask format
UNCOV
353
            this.inputValue = this.focusedValuePipe.transform(value);
×
354
        } else {
UNCOV
355
            this.inputValue = this.maskParser.applyMask(value, this.maskOptions);
×
356
        }
357

UNCOV
358
        this._oldText = this.inputValue;
×
359
    }
360

361
    /** @hidden */
362
    protected setSelectionRange(start: number, end: number = start): void {
×
UNCOV
363
        this.nativeElement.setSelectionRange(start, end);
×
364
    }
365

366
    /** @hidden */
367
    protected afterInput(): void {
UNCOV
368
        this._oldText = this.inputValue;
×
UNCOV
369
        this._hasDropAction = false;
×
UNCOV
370
        this._start = 0;
×
UNCOV
371
        this._end = 0;
×
UNCOV
372
        this._key = null;
×
UNCOV
373
        this._composing = false;
×
374
    }
375

376
    /** @hidden */
377
    protected setPlaceholder(value: string): void {
UNCOV
378
        const placeholder = this.nativeElement.placeholder;
×
UNCOV
379
        if (!placeholder || placeholder === this.mask) {
×
UNCOV
380
            this.renderer.setAttribute(this.nativeElement, 'placeholder', parseMask(value ?? '').mask || this.defaultMask);
×
381
        }
382
    }
383

384
    private updateInputValue(value: string) {
385
        if (this._focused) {
4!
386
            this.showMask(value);
×
387
        } else if (!this.displayValuePipe) {
4✔
388
            this.inputValue = this.inputValue ? this.maskParser.applyMask(value, this.maskOptions) : '';
4!
389
        }
390
    }
391

392
    private updateInput(valueToParse: string) {
UNCOV
393
        const replacedData = this.maskParser.replaceInMask(this._oldText, valueToParse, this.maskOptions, this._start, this._end);
×
UNCOV
394
        this.inputValue = replacedData.value;
×
UNCOV
395
        if (this._key === this.platform.KEYMAP.BACKSPACE) {
×
UNCOV
396
            replacedData.end = this._start;
×
397
        }
398

UNCOV
399
        this.setSelectionRange(replacedData.end);
×
400

UNCOV
401
        const rawVal = this.maskParser.parseValueFromMask(this.inputValue, this.maskOptions);
×
UNCOV
402
        this._dataValue = this.includeLiterals ? this.inputValue : rawVal;
×
UNCOV
403
        this._onChangeCallback(this._dataValue);
×
404

UNCOV
405
        this.valueChanged.emit({ rawValue: rawVal, formattedValue: this.inputValue });
×
UNCOV
406
        this.afterInput();
×
407
    }
408

409
    private showDisplayValue(value: string) {
UNCOV
410
        if (this.displayValuePipe) {
×
UNCOV
411
            this.inputValue = this.displayValuePipe.transform(value);
×
UNCOV
412
        } else if (value === this.maskParser.applyMask(null, this.maskOptions)) {
×
UNCOV
413
            this.inputValue = '';
×
414
        }
415
    }
416
}
417

418
/**
419
 * The IgxMaskModule provides the {@link IgxMaskDirective} inside your application.
420
 */
421
export interface IMaskEventArgs extends IBaseEventArgs {
422
    rawValue: string;
423
    formattedValue: string;
424
}
425

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