diff --git a/projects/igniteui-angular/src/lib/core/utils.ts b/projects/igniteui-angular/src/lib/core/utils.ts index 1dca59ee17e..f73b0b3363f 100644 --- a/projects/igniteui-angular/src/lib/core/utils.ts +++ b/projects/igniteui-angular/src/lib/core/utils.ts @@ -139,7 +139,14 @@ export const enum KEYCODES { RIGHT_ARROW = 39, DOWN_ARROW = 40, F2 = 113, - TAB = 9 + TAB = 9, + CTRL = 17, + Z = 90, + Y = 89, + X = 88, + BACKSPACE = 8, + DELETE = 46, + INPUT_METHOD = 229 } /** diff --git a/projects/igniteui-angular/src/lib/date-picker/date-picker.component.spec.ts b/projects/igniteui-angular/src/lib/date-picker/date-picker.component.spec.ts index a353f962089..80582432995 100644 --- a/projects/igniteui-angular/src/lib/date-picker/date-picker.component.spec.ts +++ b/projects/igniteui-angular/src/lib/date-picker/date-picker.component.spec.ts @@ -1,6 +1,6 @@ import { Component, ViewChild, ElementRef } from '@angular/core'; import { async, fakeAsync, TestBed, tick, flush, ComponentFixture } from '@angular/core/testing'; -import { FormsModule, FormGroup, FormBuilder, FormControl, ReactiveFormsModule} from '@angular/forms'; +import { FormsModule, FormGroup, FormBuilder, FormControl, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { IgxDatePickerComponent, IgxDatePickerModule } from './date-picker.component'; @@ -206,7 +206,7 @@ describe('IgxDatePicker', () => { expect(input).toEqual(document.activeElement); })); - it('When a modal datepicker is closed via outside click, the focus should remain on the input', + it('When a modal datepicker is closed via outside click, the focus should remain on the input', fakeAsync(() => { const datePickerDom = fixture.debugElement.query(By.css('igx-date-picker')); let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper--modal'); @@ -230,7 +230,7 @@ describe('IgxDatePicker', () => { expect(input).toEqual(document.activeElement); })); - it('When datepicker is closed upon selecting a date, the focus should remain on the input', + it('When datepicker is closed upon selecting a date, the focus should remain on the input', fakeAsync(() => { const datePickerDom = fixture.debugElement.query(By.css('igx-date-picker')); let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper--modal'); @@ -1149,7 +1149,8 @@ describe('IgxDatePicker', () => { expect(input).toBeDefined(); datePicker.isSpinLoop = false; - input.nativeElement.focus(); + input.triggerEventHandler('focus', {}); + fixture.detectChanges(); // bound transformedDate assign UIInteractions.sendInput(input, '31-03-19'); expect(input.nativeElement.value).toBe('31-03-19'); diff --git a/projects/igniteui-angular/src/lib/directives/mask/mask-parsing.service.ts b/projects/igniteui-angular/src/lib/directives/mask/mask-parsing.service.ts index 4523927b25d..b7a39f93da6 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask-parsing.service.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask-parsing.service.ts @@ -1,120 +1,32 @@ import { Injectable } from '@angular/core'; -/** - * @hidden - */ -export const MASK_FLAGS = [ 'C', '&', 'a', 'A', '?', 'L', '9', '0', '#' ]; +/** @hidden */ +export const MASK_FLAGS = ['C', '&', 'a', 'A', '?', 'L', '9', '0', '#']; -/** - * @hidden - */ -export const KEYS = { - Ctrl : 17, - Z : 90, - Y : 89, - X : 88, - BACKSPACE : 8, - DELETE : 46 - }; +/** @hidden */ +export interface MaskOptions { + format: string; + promptChar: string; +} +/** @hidden */ +export interface Replaced { + value: string; + end: number; +} -/** - * @hidden - */ +/** @hidden */ @Injectable({ providedIn: 'root' }) export class MaskParsingService { - private _cursor; - public get cursor() { - return this._cursor; - } - public data: boolean; - - public parseValueByMask(value, maskOptions, cursor): string { - let inputValue: string = value; - const mask: string = maskOptions.format; - const literals: Map = this.getMaskLiterals(mask); - const literalKeys: number[] = Array.from(literals.keys()); - const nonLiteralIndeces: number[] = this.getNonLiteralIndeces(mask, literalKeys); - - if (inputValue.length < mask.length) { // BACKSPACE, DELETE - if (inputValue === '' && cursor === -1) { - this._cursor = 0; - return this.parseValueByMaskOnInit(value, maskOptions); - } // workaround for IE 'x' button - - if (nonLiteralIndeces.indexOf(cursor + 1) !== -1) { - inputValue = this.insertCharAt(inputValue, cursor + 1, maskOptions.promptChar); - this._cursor = cursor + 1; - } else { - inputValue = this.insertCharAt(inputValue, cursor + 1, mask[cursor + 1]); - this._cursor = cursor + 1; - for (let i = this._cursor; i < 0; i--) { - if (literalKeys.indexOf(this._cursor) !== -1) { - this._cursor--; - } else { - break; - } - } - } - } else { - const char = inputValue[cursor]; - let isCharValid = this.validateCharOnPostion(char, cursor, mask); - if (nonLiteralIndeces.indexOf(cursor) !== -1) { - inputValue = this.replaceCharAt(inputValue, cursor, ''); - if (isCharValid) { - inputValue = this.replaceCharAt(inputValue, cursor, char); - this._cursor = cursor + 1; - } else { - this._cursor = cursor; - } - } else { - inputValue = this.replaceCharAt(inputValue, cursor, ''); - this._cursor = ++cursor; - for (let i = cursor; i < mask.length; i++) { - if (literalKeys.indexOf(this._cursor) !== -1) { - this._cursor = ++cursor; - } else { - isCharValid = this.validateCharOnPostion(char, cursor, mask); - if (isCharValid) { - inputValue = this.replaceCharAt(inputValue, cursor, char); - this._cursor = ++cursor; - break; - } else { - break; - } - } - } - } - } - - return inputValue; - } - - public parseMask(maskOptions): string { - let outputVal = ''; - const mask: string = maskOptions.format; - const literals: Map = this.getMaskLiterals(mask); - - for (const maskSym of mask) { - outputVal += maskOptions.promptChar; - } - - literals.forEach((val: string, key: number) => { - outputVal = this.replaceCharAt(outputVal, key, val); - }); - - return outputVal; - } - - public parseValueByMaskOnInit(inputVal, maskOptions): string { + public applyMask(inputVal: string, maskOptions: MaskOptions): string { let outputVal = ''; let value = ''; const mask: string = maskOptions.format; const literals: Map = this.getMaskLiterals(mask); const literalKeys: number[] = Array.from(literals.keys()); - const nonLiteralIndeces: number[] = this.getNonLiteralIndeces(mask, literalKeys); + const nonLiteralIndices: number[] = this.getNonLiteralIndices(mask, literalKeys); const literalValues: string[] = Array.from(literals.values()); if (inputVal != null) { @@ -137,33 +49,33 @@ export class MaskParsingService { for (let i = 0; i < nonLiteralValues.length; i++) { const char = nonLiteralValues[i]; - const isCharValid = this.validateCharOnPostion(char, nonLiteralIndeces[i], mask); + const isCharValid = this.validateCharOnPosition(char, nonLiteralIndices[i], mask); if (!isCharValid && char !== maskOptions.promptChar) { nonLiteralValues[i] = maskOptions.promptChar; } } - if (nonLiteralValues.length > nonLiteralIndeces.length) { - nonLiteralValues.splice(nonLiteralIndeces.length); + if (nonLiteralValues.length > nonLiteralIndices.length) { + nonLiteralValues.splice(nonLiteralIndices.length); } let pos = 0; for (const nonLiteralValue of nonLiteralValues) { const char = nonLiteralValue; - outputVal = this.replaceCharAt(outputVal, nonLiteralIndeces[pos++], char); + outputVal = this.replaceCharAt(outputVal, nonLiteralIndices[pos++], char); } return outputVal; } - public restoreValueFromMask(value, maskOptions): string { + public parseValueFromMask(maskedValue: string, maskOptions: MaskOptions): string { let outputVal = ''; const mask: string = maskOptions.format; const literals: Map = this.getMaskLiterals(mask); const literalValues: string[] = Array.from(literals.values()); - for (const val of value) { + for (const val of maskedValue) { if (literalValues.indexOf(val) === -1) { if (val !== maskOptions.promptChar) { outputVal += val; @@ -174,144 +86,40 @@ export class MaskParsingService { return outputVal; } - public parseValueByMaskUponSelection(value, maskOptions, cursor, selection): string { - let isCharValid: boolean; - let inputValue: string = value; - const char: string = inputValue[cursor]; - const mask: string = maskOptions.format; - const literals: Map = this.getMaskLiterals(mask); - const literalKeys: number[] = Array.from(literals.keys()); - const nonLiteralIndeces: number[] = this.getNonLiteralIndeces(mask, literalKeys); + public replaceInMask(maskedValue: string, value: string, maskOptions: MaskOptions, start: number, end: number): Replaced { + const literalsPositions: number[] = Array.from(this.getMaskLiterals(maskOptions.format).keys()); + const chars = Array.from(value); + let cursor = start; + end = Math.min(end, maskedValue.length); - if (!this.data) { - this._cursor = cursor < 0 ? ++cursor : cursor; - if (nonLiteralIndeces.indexOf(this._cursor) !== -1) { - isCharValid = this.validateCharOnPostion(char, this._cursor, mask); - inputValue = isCharValid ? this.replaceCharAt(inputValue, this._cursor++, char) : - inputValue = this.replaceCharAt(inputValue, this._cursor++, maskOptions.promptChar); - selection--; - if (selection > 0) { - for (let i = 0; i < selection; i++) { - cursor++; - inputValue = nonLiteralIndeces.indexOf(cursor) !== -1 ? - this.insertCharAt(inputValue, cursor, maskOptions.promptChar) : - this.insertCharAt(inputValue, cursor, mask[cursor]); - } - } - } else { - inputValue = this.replaceCharAt(inputValue, this._cursor, mask[this._cursor]); - this._cursor++; - selection--; - let isMarked = false; - if (selection > 0) { - cursor = this._cursor; - for (let i = 0; i < selection; i++) { - if (nonLiteralIndeces.indexOf(cursor) !== -1) { - isCharValid = this.validateCharOnPostion(char, cursor, mask); - if (isCharValid && !isMarked) { - inputValue = this.insertCharAt(inputValue, cursor, char); - cursor++; - this._cursor++; - isMarked = true; - } else { - inputValue = this.insertCharAt(inputValue, cursor, maskOptions.promptChar); - cursor++; - } - } else { - inputValue = this.insertCharAt(inputValue, cursor, mask[cursor]); - if (cursor === this._cursor) { - this._cursor++; - } - cursor++; - } - } + for (let i = start; i < end || (chars.length && i < maskedValue.length); i++) { + if (literalsPositions.indexOf(i) !== -1) { + if (chars[0] === maskedValue[i]) { + chars.shift(); } - } - } else { - if (inputValue === '' && cursor === -1) { - this._cursor = 0; - return this.parseValueByMaskOnInit(value, maskOptions); - } // workaround for IE 'x' button - - if (this._cursor < 0) { - this._cursor++; cursor++; + continue; } - cursor++; - this._cursor = cursor; - for (let i = 0; i < selection; i++) { - if (nonLiteralIndeces.indexOf(cursor) !== -1) { - inputValue = this.insertCharAt(inputValue, cursor, maskOptions.promptChar); - cursor++; - } else { - inputValue = this.insertCharAt(inputValue, cursor, mask[cursor]); - cursor++; - } - } - } - - return inputValue; - } - - public parseValueByMaskUponCopyPaste(value, maskOptions, cursor, clipboardData, selection): string { - let inputValue: string = value; - const mask: string = maskOptions.format; - const literals: Map = this.getMaskLiterals(mask); - const literalKeys: number[] = Array.from(literals.keys()); - const nonLiteralIndeces: number[] = this.getNonLiteralIndeces(mask, literalKeys); - - const selectionEnd = cursor + selection; - - this._cursor = cursor; - for (const clipboardSym of clipboardData) { - const char = clipboardSym; - - if (this._cursor > mask.length) { - return inputValue; - } - - if (nonLiteralIndeces.indexOf(this._cursor) !== -1) { - const isCharValid = this.validateCharOnPostion(char, this._cursor, mask); - if (isCharValid) { - inputValue = this.replaceCharAt(inputValue, this._cursor++, char); - } - } else { - for (let i = cursor; i < mask.length; i++) { - if (literalKeys.indexOf(this._cursor) !== -1) { - this._cursor++; - } else { - const isCharValid = this.validateCharOnPostion(char, this._cursor, mask); - if (isCharValid) { - inputValue = this.replaceCharAt(inputValue, this._cursor++, char); - } - break; - } - } + if (chars[0] && !this.validateCharOnPosition(chars[0], i, maskOptions.format)) { + break; } + cursor++; - selection--; - } - - if (selection > 0) { - for (let i = this._cursor; i < selectionEnd; i++) { - if (literalKeys.indexOf(this._cursor) !== -1) { - this._cursor++; - } else { - inputValue = this.replaceCharAt(inputValue, this._cursor++, maskOptions.promptChar); - } - } + const char = chars.length ? chars.shift() : maskOptions.promptChar; + maskedValue = this.replaceCharAt(maskedValue, i, char); } - return inputValue; + return { value: maskedValue, end: cursor }; } - private validateCharOnPostion(inputChar: string, position: number, mask: string): boolean { + /** Validates only non literal positions. */ + private validateCharOnPosition(inputChar: string, position: number, mask: string): boolean { let regex: RegExp; let isValid: boolean; const letterOrDigitRegEx = '[\\d\\u00C0-\\u1FFF\\u2C00-\\uD7FFa-zA-Z]'; const letterDigitOrSpaceRegEx = '[\\d\\u00C0-\\u1FFF\\u2C00-\\uD7FFa-zA-Z\\u0020]'; const letterRegEx = '[\\u00C0-\\u1FFF\\u2C00-\\uD7FFa-zA-Z]'; - const letteSpaceRegEx = '[\\u00C0-\\u1FFF\\u2C00-\\uD7FFa-zA-Z\\u0020]'; + const letterSpaceRegEx = '[\\u00C0-\\u1FFF\\u2C00-\\uD7FFa-zA-Z\\u0020]'; const digitRegEx = '[\\d]'; const digitSpaceRegEx = '[\\d\\u0020]'; const digitSpecialRegEx = '[\\d-\\+]'; @@ -333,7 +141,7 @@ export class MaskParsingService { isValid = regex.test(inputChar); break; case '?': - regex = new RegExp(letteSpaceRegEx); + regex = new RegExp(letterSpaceRegEx); isValid = regex.test(inputChar); break; case 'L': @@ -364,11 +172,6 @@ export class MaskParsingService { return strValue.substring(0, index) + char + strValue.substring(index + 1); } } - private insertCharAt(strValue: string, index: number, char: string): string { - if (strValue !== undefined) { - return strValue.substring(0, index) + char + strValue.substring(index); - } - } private getMaskLiterals(mask: string): Map { const literals = new Map(); @@ -381,16 +184,16 @@ export class MaskParsingService { return literals; } - private getNonLiteralIndeces(mask: string, literalKeys: number[]): number[] { - const nonLiteralsIndeces: number[] = new Array(); + private getNonLiteralIndices(mask: string, literalKeys: number[]): number[] { + const nonLiteralsIndices: number[] = new Array(); for (let i = 0; i < mask.length; i++) { if (literalKeys.indexOf(i) === -1) { - nonLiteralsIndeces.push(i); + nonLiteralsIndices.push(i); } } - return nonLiteralsIndeces; + return nonLiteralsIndices; } private getNonLiteralValues(value: string, literalValues: string[]): string[] { const nonLiteralValues: string[] = new Array(); diff --git a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts index 5211ef9847d..c6a7ce6fb5b 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.spec.ts @@ -1,15 +1,16 @@ import { Component, Input, ViewChild, ElementRef, Pipe, PipeTransform } from '@angular/core'; import { - async, - fakeAsync, - TestBed, - tick + async, + fakeAsync, + TestBed, + tick } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { IgxInputGroupModule } from '../../input-group/input-group.component'; import { IgxMaskModule } from './mask.directive'; import { configureTestSuite } from '../../test-utils/configure-suite'; +import { UIInteractions } from '../../test-utils/ui-interactions.spec'; describe('igxMask', () => { configureTestSuite(); @@ -27,7 +28,8 @@ describe('igxMask', () => { MaskComponent, OneWayBindComponent, PipesMaskComponent, - PlaceholderMaskComponent + PlaceholderMaskComponent, + EmptyMaskTestComponent ], imports: [ FormsModule, @@ -35,13 +37,12 @@ describe('igxMask', () => { IgxMaskModule ] }) - .compileComponents(); + .compileComponents(); })); it('Initializes an input with default mask', fakeAsync(() => { const fixture = TestBed.createComponent(DefMaskComponent); fixture.detectChanges(); - const input = fixture.componentInstance.input; expect(input.nativeElement.value).toEqual(''); @@ -51,6 +52,7 @@ describe('igxMask', () => { tick(); input.nativeElement.value = '@#$YUA123'; + fixture.detectChanges(); input.nativeElement.dispatchEvent(new Event('input')); tick(); @@ -146,28 +148,50 @@ describe('igxMask', () => { expect(comp.value).toEqual('7777'); })); - it('Enter incorrect value with a preset mask', fakeAsync(() => { + it('Should handle the input of invalid values', fakeAsync(() => { const fixture = TestBed.createComponent(MaskComponent); fixture.detectChanges(); - const input = fixture.componentInstance.input; input.nativeElement.dispatchEvent(new Event('focus')); tick(); input.nativeElement.value = 'abc4569d12'; + fixture.detectChanges(); input.nativeElement.dispatchEvent(new Event('input')); tick(); input.nativeElement.dispatchEvent(new Event('focus')); tick(); + fixture.detectChanges(); expect(input.nativeElement.value).toEqual('(___) 4569-_12'); + })); + + it('Enter incorrect value with a preset mask', fakeAsync(() => { + pending('This must be remade into a typing test.'); + const fixture = TestBed.createComponent(MaskComponent); + fixture.detectChanges(); + const input = fixture.componentInstance.input; + + input.nativeElement.dispatchEvent(new Event('focus')); + tick(); + + input.nativeElement.value = 'abc4569d12'; + input.nativeElement.dispatchEvent(new Event('input')); + tick(); + + input.nativeElement.dispatchEvent(new Event('focus')); + tick(); + fixture.detectChanges(); + + expect(input.nativeElement.value).toEqual('(456) 912_-___'); input.nativeElement.dispatchEvent(new Event('focus')); tick(); input.nativeElement.value = '1111111111111111111'; + fixture.detectChanges(); input.nativeElement.dispatchEvent(new Event('input')); tick(); @@ -199,6 +223,7 @@ describe('igxMask', () => { tick(); input.nativeElement.value = '123'; + fixture.detectChanges(); input.nativeElement.dispatchEvent(new Event('input')); tick(); @@ -225,6 +250,7 @@ describe('igxMask', () => { expect(comp.value).toEqual(3456); input.nativeElement.value = 'A'; + fixture.detectChanges(); input.nativeElement.dispatchEvent(new Event('input')); tick(); @@ -246,7 +272,7 @@ describe('igxMask', () => { input.nativeElement.select(); tick(); - const keyEvent = new KeyboardEvent('keydown', {key : '57'}); + const keyEvent = new KeyboardEvent('keydown', { key: '57' }); input.nativeElement.dispatchEvent(keyEvent); tick(); @@ -263,7 +289,6 @@ describe('igxMask', () => { it('Enter value over literal', fakeAsync(() => { const fixture = TestBed.createComponent(MaskComponent); fixture.detectChanges(); - const input = fixture.componentInstance.input; input.nativeElement.focus(); @@ -272,11 +297,8 @@ describe('igxMask', () => { input.nativeElement.select(); tick(); - const keyEvent = new KeyboardEvent('keydown', {key : '8'}); - input.nativeElement.dispatchEvent(keyEvent); - tick(); - input.nativeElement.value = ''; + fixture.detectChanges(); input.nativeElement.dispatchEvent(new Event('input')); tick(); @@ -286,7 +308,9 @@ describe('igxMask', () => { expect(input.nativeElement.value).toEqual('(___) ____-___'); input.nativeElement.value = '6666'; + fixture.detectChanges(); input.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); tick(); input.nativeElement.dispatchEvent(new Event('focus')); @@ -295,6 +319,42 @@ describe('igxMask', () => { expect(input.nativeElement.value).toEqual('(666) 6___-___'); })); + it('Should successfully drop text in the input', fakeAsync(() => { + const fixture = TestBed.createComponent(MaskComponent); + fixture.detectChanges(); + const input = fixture.componentInstance.input; + + input.nativeElement.focus(); + tick(); + input.nativeElement.select(); + tick(); + + input.nativeElement.value = '4576'; + UIInteractions.simulateDropEvent(input.nativeElement, '4576', 'text'); + fixture.detectChanges(); + tick(); + + input.nativeElement.dispatchEvent(new Event('focus')); + tick(); + + expect(input.nativeElement.value).toEqual('(457) 6___-___'); + })); + + it('Should display mask on dragenter and remove it on dragleave', fakeAsync(() => { + const fixture = TestBed.createComponent(EmptyMaskTestComponent); + fixture.detectChanges(); + const input = fixture.componentInstance.input; + + expect(input.nativeElement.value).toEqual(''); + expect(input.nativeElement.placeholder).toEqual('CCCCCCCCCC'); + + input.nativeElement.dispatchEvent(new DragEvent('dragenter')); + expect(input.nativeElement.value).toEqual('__________'); + + input.nativeElement.dispatchEvent(new DragEvent('dragleave')); + expect(input.nativeElement.value).toEqual(''); + })); + it('Apply display and input pipes on blur and focus.', fakeAsync(() => { const fixture = TestBed.createComponent(PipesMaskComponent); fixture.detectChanges(); @@ -339,7 +399,8 @@ describe('igxMask', () => { })); }); -@Component({ template: ` +@Component({ + template: ` ` }) class DefMaskComponent { @@ -350,7 +411,8 @@ class DefMaskComponent { public input: ElementRef; } -@Component({ template: ` +@Component({ + template: ` ` }) class MaskComponent { @@ -361,7 +423,8 @@ class MaskComponent { public input: ElementRef; } -@Component({ template: ` +@Component({ + template: ` @@ -378,7 +441,8 @@ class IncludeLiteralsComponent { public input1: ElementRef; } -@Component({ template: ` +@Component({ + template: ` ` }) class DigitSpaceMaskComponent { @@ -389,7 +453,8 @@ class DigitSpaceMaskComponent { public input: ElementRef; } -@Component({ template: ` +@Component({ + template: ` ` }) class DigitPlusMinusMaskComponent { @@ -400,7 +465,8 @@ class DigitPlusMinusMaskComponent { public input: ElementRef; } -@Component({ template: ` +@Component({ + template: ` ` }) class LetterSpaceMaskComponent { @@ -411,7 +477,8 @@ class LetterSpaceMaskComponent { public input: ElementRef; } -@Component({ template: ` +@Component({ + template: ` ` }) class AlphanumSpaceMaskComponent { @@ -422,7 +489,8 @@ class AlphanumSpaceMaskComponent { public input: ElementRef; } -@Component({ template: ` +@Component({ + template: ` ` }) class AnyCharMaskComponent { @@ -433,7 +501,8 @@ class AnyCharMaskComponent { public input: ElementRef; } -@Component({ template: ` +@Component({ + template: ` ` }) @@ -452,7 +521,8 @@ class EventFiringComponent { } } -@Component({ template: ` +@Component({ + template: ` +@Component({ + template: ` +@Component({ + template: ` + + + ` +}) +class EmptyMaskTestComponent { + @ViewChild('input', { static: true }) + public input: ElementRef; +} + @Pipe({ name: 'inputFormat' }) export class InputFormatPipe implements PipeTransform { - transform(value: any): string { + transform(value: any): string { return value.toUpperCase(); } } @Pipe({ name: 'displayFormat' }) export class DisplayFormatPipe implements PipeTransform { - transform(value: any): string { + transform(value: any): string { return value.toLowerCase(); } } diff --git a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts index 31b4f5f1adc..cdcca91b294 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts @@ -1,32 +1,27 @@ import { CommonModule } from '@angular/common'; import { - Directive, - ElementRef, - EventEmitter, - HostListener, - Input, - NgModule, - OnInit, - Output, - PipeTransform + Directive, ElementRef, EventEmitter, HostListener, + Output, PipeTransform, Renderer2, + Input, NgModule, OnInit, AfterViewChecked, } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { KEYS, MaskParsingService } from './mask-parsing.service'; -import { isIE, IBaseEventArgs } from '../../core/utils'; +import { DeprecateProperty } from '../../core/deprecateDecorators'; +import { MaskParsingService, MaskOptions } from './mask-parsing.service'; +import { isIE, IBaseEventArgs, KEYCODES } from '../../core/utils'; const noop = () => { }; @Directive({ providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: IgxMaskDirective, multi: true }], - selector: '[igxMask]' + selector: '[igxMask]', + exportAs: 'igxMask' }) -export class IgxMaskDirective implements OnInit, ControlValueAccessor { +export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueAccessor { /** * Sets the input mask. * ```html * * ``` - * @memberof IgxMaskDirective */ @Input('igxMask') public mask: string; @@ -37,17 +32,15 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { * ```html * * ``` - * @memberof IgxMaskDirective */ @Input() - public promptChar: string; + public promptChar = '_'; /** * Specifies if the bound value includes the formatting symbols. * ```html * * ``` - * @memberof IgxMaskDirective */ @Input() public includeLiterals: boolean; @@ -57,16 +50,14 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { * ```html * * ``` - * @memberof IgxMaskDirective */ - @Input() + @DeprecateProperty('"placeholder" is deprecated, use native placeholder instead.') public set placeholder(val: string) { - this._placeholder = val; - this.nativeElement.setAttribute('placeholder', this._placeholder); + this.renderer.setAttribute(this.nativeElement, 'placeholder', val); } public get placeholder(): string { - return this._placeholder; + return this.nativeElement.placeholder; } /** @@ -74,7 +65,6 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { * ```html * * ``` - * @memberof IgxMaskDirective */ @Input() public displayValuePipe: PipeTransform; @@ -84,17 +74,10 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { * ```html * * ``` - * @memberof IgxMaskDirective */ @Input() public focusedValuePipe: PipeTransform; - /** - *@hidden - */ - @Input() - private dataValue: string; - /** * Emits an event each time the value changes. * Provides `rawValue: string` and `formattedValue: string` as event arguments. @@ -105,264 +88,223 @@ export class IgxMaskDirective implements OnInit, ControlValueAccessor { @Output() public onValueChange = new EventEmitter(); - /** - *@hidden - */ - private get value() { + /** @hidden @internal; */ + protected get inputValue(): string { return this.nativeElement.value; } - /** - *@hidden - */ - private set value(val) { + /** @hidden @internal */ + protected set inputValue(val) { this.nativeElement.value = val; } - /** - *@hidden - */ - private get nativeElement() { - return this.elementRef.nativeElement; + /** @hidden */ + protected get maskOptions(): MaskOptions { + const format = this.mask || 'CCCCCCCCCC'; + const promptChar = this.promptChar && this.promptChar.substring(0, 1); + return { format, promptChar }; } - /** - *@hidden - */ - private get selectionStart() { - return this.nativeElement.selectionStart; + private get selectionStart(): number { + // Edge(classic) and FF don't select text on drop + return this.nativeElement.selectionStart === this.nativeElement.selectionEnd && this._hasDropAction ? + this.nativeElement.selectionEnd - this._droppedData.length : + this.nativeElement.selectionStart; } - /** - *@hidden - */ - private get selectionEnd() { + private get selectionEnd(): number { return this.nativeElement.selectionEnd; } - /** - *@hidden - */ - private _ctrlDown: boolean; - - /** - *@hidden - */ - private _paste: boolean; - - /** - *@hidden - */ - private _selection: number; - - /** - *@hidden - */ - private _placeholder: string; - - /** - *@hidden - */ - private _maskOptions = { - format: '', - promptChar: '' - }; - - /** - *@hidden - */ - private _key; - - /** - *@hidden - */ - private _cursorOnPaste; - - /** - *@hidden - */ - private _valOnPaste; + private get nativeElement(): HTMLInputElement { + return this.elementRef.nativeElement; + } + private _end = 0; + private _start = 0; + private _key: number; + private _oldText = ''; + private _dataValue = ''; + private _droppedData: string; + private _hasDropAction: boolean; private _stopPropagation: boolean; - /** - *@hidden - */ private _onTouchedCallback: () => void = noop; - - /** - *@hidden - */ private _onChangeCallback: (_: any) => void = noop; - constructor(private elementRef: ElementRef, private maskParser: MaskParsingService) { } + constructor( + protected elementRef: ElementRef, + protected maskParser: MaskParsingService, + protected renderer: Renderer2) { } - /** - *@hidden - */ + /** @hidden */ public ngOnInit(): void { - if (this.promptChar && this.promptChar.length > 1) { - this._maskOptions.promptChar = this.promptChar = this.promptChar.substring(0, 1); - } - - this._maskOptions.format = this.mask ? this.mask : 'CCCCCCCCCC'; - this._maskOptions.promptChar = this.promptChar ? this.promptChar : '_'; - this.nativeElement.setAttribute('placeholder', this.placeholder ? this.placeholder : this._maskOptions.format); + this.renderer.setAttribute(this.nativeElement, 'placeholder', + this.placeholder ? this.placeholder : this.maskOptions.format); } /** - *@hidden + * TODO: Remove after date/time picker integration refactor + * @hidden */ - @HostListener('keydown', ['$event']) - public onKeydown(event): void { - const key = event.keyCode || event.charCode; + public ngAfterViewChecked(): void { + this._oldText = this.inputValue; + } + /** @hidden */ + @HostListener('keydown', ['$event']) + public onKeyDown(event): void { if (isIE() && this._stopPropagation) { this._stopPropagation = false; } - if (key === KEYS.Ctrl) { - this._ctrlDown = true; - } - - if ((this._ctrlDown && key === KEYS.Z) || (this._ctrlDown && key === KEYS.Y)) { + const key = event.keyCode || event.charCode; + if ((key === KEYCODES.CTRL && key === KEYCODES.Z) || (key === KEYCODES.CTRL && key === KEYCODES.Y)) { event.preventDefault(); } this._key = key; - this._selection = Math.abs(this.selectionEnd - this.selectionStart); - } - - /** - *@hidden - */ - @HostListener('keyup', ['$event']) - public onKeyup(event): void { - const key = event.keyCode || event.charCode; - - if (key === KEYS.Ctrl) { - this._ctrlDown = false; - } + this._start = this.selectionStart; + this._end = this.selectionEnd; } - /** - *@hidden - */ - @HostListener('paste', ['$event']) - public onPaste(event): void { - this._paste = true; - - this._valOnPaste = this.value; - this._cursorOnPaste = this.getCursorPosition(); - } - - /** - *@hidden - */ - @HostListener('input', ['$event']) - public onInputChanged(event): void { + /** @hidden */ + @HostListener('input') + public onInputChanged(): void { if (isIE() && this._stopPropagation) { this._stopPropagation = false; return; } - if (this._paste) { - this._paste = false; + let valueToParse = ''; + if (this._hasDropAction) { + this._start = this.selectionStart; + } + if (this.inputValue.length < this._oldText.length && this._key === KEYCODES.INPUT_METHOD) { + // software keyboard input delete + this._key = KEYCODES.BACKSPACE; + } - const clipboardData = this.value.substring(this._cursorOnPaste, this.getCursorPosition()); - this.value = this.maskParser.parseValueByMaskUponCopyPaste( - this._valOnPaste, this._maskOptions, this._cursorOnPaste, clipboardData, this._selection); + switch (this._key) { + case KEYCODES.DELETE: + this._end = this._start === this._end ? ++this._end : this._end; + break; + case KEYCODES.BACKSPACE: + this._start = this.selectionStart; + break; + default: + valueToParse = this.inputValue.substring(this._start, this.selectionEnd); + break; + } - this.setCursorPosition(this.maskParser.cursor); - } else { - const currentCursorPos = this.getCursorPosition(); + const replacedData = this.maskParser.replaceInMask(this._oldText, valueToParse, this.maskOptions, this._start, this._end); + this.inputValue = replacedData.value; + if (this._key === KEYCODES.BACKSPACE) { replacedData.end = this._start; } + this.setSelectionRange(replacedData.end); - this.maskParser.data = (this._key === KEYS.BACKSPACE) || (this._key === KEYS.DELETE); + const rawVal = this.maskParser.parseValueFromMask(this.inputValue, this.maskOptions); + this._dataValue = this.includeLiterals ? this.inputValue : rawVal; + this._onChangeCallback(this._dataValue); - this.value = this._selection && this._selection !== 0 ? - this.maskParser.parseValueByMaskUponSelection(this.value, this._maskOptions, currentCursorPos - 1, this._selection) : - this.maskParser.parseValueByMask(this.value, this._maskOptions, currentCursorPos - 1); + this.onValueChange.emit({ rawValue: rawVal, formattedValue: this.inputValue }); + this.afterInput(); + } - this.setCursorPosition(this.maskParser.cursor); - } + /** @hidden */ + @HostListener('paste') + public onPaste(): void { + this._oldText = this.inputValue; + this._start = this.selectionStart; + } - const rawVal = this.maskParser.restoreValueFromMask(this.value, this._maskOptions); + /** @hidden */ + @HostListener('focus') + public onFocus(): void { + this.showMask(this._dataValue); + } - this.dataValue = this.includeLiterals ? this.value : rawVal; - this._onChangeCallback(this.dataValue); + /** @hidden */ + @HostListener('blur', ['$event.target.value']) + public onBlur(value: string): void { + this.showDisplayValue(value); + this._onTouchedCallback(); + } - this.onValueChange.emit({ rawValue: rawVal, formattedValue: this.value }); + /** @hidden */ + @HostListener('dragenter') + public onDragEnter(): void { + this.showMask(this._dataValue); } - /** - *@hidden - */ - @HostListener('focus', ['$event.target.value']) - public onFocus(value) { + /** @hidden */ + @HostListener('dragleave') + public onDragLeave(): void { + this.showDisplayValue(this.inputValue); + } + + /** @hidden */ + @HostListener('drop', ['$event']) + public onDrop(event: DragEvent): void { + this._hasDropAction = true; + this._droppedData = event.dataTransfer.getData('text'); + } + + /** @hidden */ + protected showMask(value: string) { if (this.focusedValuePipe) { if (isIE()) { this._stopPropagation = true; } - this.value = this.focusedValuePipe.transform(value); + this.inputValue = this.focusedValuePipe.transform(value); } else { - this.value = this.maskParser.parseValueByMaskOnInit(this.value, this._maskOptions); + this.inputValue = this.maskParser.applyMask(this.inputValue, this.maskOptions); } + + this._oldText = this.inputValue; } - /** - *@hidden - */ - @HostListener('blur', ['$event.target.value']) - public onBlur(value) { + private showDisplayValue(value: string) { if (this.displayValuePipe) { - this.value = this.displayValuePipe.transform(value); - } else if (value === this.maskParser.parseMask(this._maskOptions)) { - this.value = ''; + this.inputValue = this.displayValuePipe.transform(value); + } else if (value === this.maskParser.applyMask(null, this.maskOptions)) { + this.inputValue = ''; } } - /** - *@hidden - */ - private getCursorPosition(): number { - return this.nativeElement.selectionStart; + private setSelectionRange(start: number, end: number = start): void { + this.nativeElement.setSelectionRange(start, end); } - /** - *@hidden - */ - private setCursorPosition(start: number, end: number = start): void { - this.nativeElement.setSelectionRange(start, end); + private afterInput() { + this._oldText = this.inputValue; + this._hasDropAction = false; + this._start = 0; + this._end = 0; + this._key = null; } - /** - *@hidden - */ - public writeValue(value) { + /** @hidden */ + public writeValue(value: string): void { if (this.promptChar && this.promptChar.length > 1) { - this._maskOptions.promptChar = this.promptChar.substring(0, 1); + this.maskOptions.promptChar = this.promptChar.substring(0, 1); } - this.value = value ? this.maskParser.parseValueByMaskOnInit(value, this._maskOptions) : ''; + this.inputValue = value ? this.maskParser.applyMask(value, this.maskOptions) : ''; if (this.displayValuePipe) { - this.value = this.displayValuePipe.transform(this.value); + this.inputValue = this.displayValuePipe.transform(this.inputValue); } - this.dataValue = this.includeLiterals ? this.value : value; - this._onChangeCallback(this.dataValue); + this._dataValue = this.includeLiterals ? this.inputValue : value; + this._onChangeCallback(this._dataValue); - this.onValueChange.emit({ rawValue: value, formattedValue: this.value }); + this.onValueChange.emit({ rawValue: value, formattedValue: this.inputValue }); } - /** - *@hidden - */ - public registerOnChange(fn: (_: any) => void) { this._onChangeCallback = fn; } + /** @hidden */ + public registerOnChange(fn: (_: any) => void): void { this._onChangeCallback = fn; } - /** - *@hidden - */ - public registerOnTouched(fn: () => void) { this._onTouchedCallback = fn; } + /** @hidden */ + public registerOnTouched(fn: () => void): void { this._onTouchedCallback = fn; } } /** @@ -373,9 +315,7 @@ export interface IMaskEventArgs extends IBaseEventArgs { formattedValue: string; } -/** - * @hidden - */ +/** @hidden */ @NgModule({ declarations: [IgxMaskDirective], exports: [IgxMaskDirective], diff --git a/projects/igniteui-angular/src/lib/test-utils/ui-interactions.spec.ts b/projects/igniteui-angular/src/lib/test-utils/ui-interactions.spec.ts index 34bb4202b01..685908bc553 100644 --- a/projects/igniteui-angular/src/lib/test-utils/ui-interactions.spec.ts +++ b/projects/igniteui-angular/src/lib/test-utils/ui-interactions.spec.ts @@ -181,6 +181,13 @@ export class UIInteractions { cell.nativeElement.dispatchEvent(new PointerEvent('pointerup', { button: 2 })); } + public static simulateDropEvent(nativeElement: HTMLElement, data: any, format: string) { + const dataTransfer = new DataTransfer(); + dataTransfer.setData(format, data); + + nativeElement.dispatchEvent(new DragEvent('drop', { dataTransfer: dataTransfer })); + } + public static clearOverlay() { const overlays = document.getElementsByClassName('igx-overlay') as HTMLCollectionOf; Array.from(overlays).forEach(element => { diff --git a/src/app/mask/mask.sample.html b/src/app/mask/mask.sample.html index 4501f3628e7..1359f124d85 100644 --- a/src/app/mask/mask.sample.html +++ b/src/app/mask/mask.sample.html @@ -1,46 +1,46 @@
- - Provide means for controlling user input and formatting the visible value based on a configurable mask rules. - -
-
-
-

Personal Data

-
- - - - - - - - - - - - - - - - - -
-
-
-
-

Mask Using Pipes

-
- - - - model value: {{value}} - -
-
-
-
-
+ + Provide means for controlling user input and formatting the visible value based on a configurable mask rules. + +
+
+
+

Personal Data

+
+ + + + + + + + + + + + + + + + + +
+
+
+
+

Mask Using Pipes

+
+ + + + model value: {{value}} + +
+
+
+
+