From 15e37c43bf7ab131be58a7b77b5a40c66b9da6fe Mon Sep 17 00:00:00 2001 From: Boris Date: Wed, 19 Feb 2020 18:04:45 +0200 Subject: [PATCH 1/6] refactor(igxMask): cleanup/streamline parsing and input handling - input parsing go through one method - add drag and drop functionality - set default value on mask prop --- .../igniteui-angular/src/lib/core/utils.ts | 9 +- .../date-picker/date-picker.component.spec.ts | 12 +- .../src/lib/date-picker/date-picker.pipes.ts | 7 +- .../directives/mask/mask-parsing.service.ts | 287 +++----------- .../directives/mask/mask.directive.spec.ts | 142 +++++-- .../src/lib/directives/mask/mask.directive.ts | 366 ++++++++---------- .../lib/grids/grid/grid-cell-editing.spec.ts | 2 + .../lib/test-utils/ui-interactions.spec.ts | 7 + .../time-picker/time-picker.component.html | 2 +- .../src/lib/time-picker/time-picker.pipes.ts | 2 + src/app/mask/mask.sample.html | 90 ++--- 11 files changed, 388 insertions(+), 538 deletions(-) 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..d578d2f7668 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'); @@ -939,7 +939,6 @@ describe('IgxDatePicker', () => { // initial input value is 20-10-11 / dd-MM-yy // focus the day part, position the caret at the beginning - input.nativeElement.focus(); input.nativeElement.setSelectionRange(0, 0); // press arrow up @@ -984,7 +983,6 @@ describe('IgxDatePicker', () => { // initial input value is 20-10-11 / dd-MM-yy // focus the day part, position the caret at the beginning - input.nativeElement.focus(); input.nativeElement.setSelectionRange(0, 0); // press arrow down @@ -1036,7 +1034,6 @@ describe('IgxDatePicker', () => { // initial input value is 20-10-11 / dd-MM-yy // focus the day part, position the caret at the beginning - input.nativeElement.focus(); input.nativeElement.setSelectionRange(0, 0); // up @@ -1149,7 +1146,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/date-picker/date-picker.pipes.ts b/projects/igniteui-angular/src/lib/date-picker/date-picker.pipes.ts index 1ac31332707..32a8e9c0cdd 100644 --- a/projects/igniteui-angular/src/lib/date-picker/date-picker.pipes.ts +++ b/projects/igniteui-angular/src/lib/date-picker/date-picker.pipes.ts @@ -31,13 +31,18 @@ export class DatePickerDisplayValuePipe implements PipeTransform { export class DatePickerInputValuePipe implements PipeTransform { constructor(@Inject(IGX_DATE_PICKER_COMPONENT) private _datePicker: IDatePicker) { } transform(value: any, args?: any): any { + /** + * TODO(D.P.): This plugs into the mask, but constantly received display strings it can't handle at all + * Those are almost immediately overridden by the pickers onFocus handling anyway; Refactor ASAP + */ if (this._datePicker.invalidDate !== '') { return this._datePicker.invalidDate; } else { if (this._datePicker.value === null || this._datePicker.value === undefined) { return DatePickerUtil.maskToPromptChars(this._datePicker.inputMask); } else { - return DatePickerUtil.addPromptCharsEditMode(this._datePicker.dateFormatParts, this._datePicker.value, value); + return (this._datePicker as any)._getEditorDate(this._datePicker.value); + // return DatePickerUtil.addPromptCharsEditMode(this._datePicker.dateFormatParts, this._datePicker.value, value); } } } 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..232ed32c129 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,225 @@ 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 */ + public ngAfterViewChecked(): void { + this._oldText = this.inputValue; + } + + /** @hidden */ @HostListener('keydown', ['$event']) - public onKeydown(event): void { + public onKeyDown(event): void { const key = event.keyCode || event.charCode; + if (!key) { return; } 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)) { + 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; - } - } - - /** - *@hidden - */ - @HostListener('paste', ['$event']) - public onPaste(event): void { - this._paste = true; - - this._valOnPaste = this.value; - this._cursorOnPaste = this.getCursorPosition(); + this._start = this.selectionStart; + this._end = this.selectionEnd; } - /** - *@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 +317,7 @@ export interface IMaskEventArgs extends IBaseEventArgs { formattedValue: string; } -/** - * @hidden - */ +/** @hidden */ @NgModule({ declarations: [IgxMaskDirective], exports: [IgxMaskDirective], diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-cell-editing.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-cell-editing.spec.ts index 9c4a0f1de68..72a7f9803d8 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-cell-editing.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-cell-editing.spec.ts @@ -211,6 +211,8 @@ describe('IgxGrid - Cell Editing #grid', () => { expect(datePicker).toBeDefined(); const editTemplate = cellDomDate.query(By.css('.igx-date-picker__input-date')); + editTemplate.triggerEventHandler('focus', { target: editTemplate.nativeElement }); + fixture.detectChanges(); UIInteractions.sendInput(editTemplate, editValue); fixture.detectChanges(); 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/projects/igniteui-angular/src/lib/time-picker/time-picker.component.html b/projects/igniteui-angular/src/lib/time-picker/time-picker.component.html index d5cf9dbfa07..5ba13de658b 100644 --- a/projects/igniteui-angular/src/lib/time-picker/time-picker.component.html +++ b/projects/igniteui-angular/src/lib/time-picker/time-picker.component.html @@ -4,7 +4,7 @@ access_time - diff --git a/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts b/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts index 11fb564d32f..a9938265933 100644 --- a/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts +++ b/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts @@ -120,6 +120,8 @@ export class TimeInputFormatPipe implements PipeTransform { mask = this.timePicker.parseMask(); } + // TODO: Pending refactoring. + value = value ? value : (this.timePicker as any).displayValue; if (!value || value === mask) { return mask; } 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}} + +
+
+
+
+ From 1768ad2a92e1f0c99cd8afcd1e42c8f368a385fd Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Thu, 27 Feb 2020 16:30:04 +0200 Subject: [PATCH 2/6] chore(mask): keep edit cursor to actual value replace, regardless of selection --- .../src/lib/directives/mask/mask-parsing.service.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 b7a39f93da6..f5b347bf6c0 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 @@ -95,17 +95,20 @@ export class MaskParsingService { for (let i = start; i < end || (chars.length && i < maskedValue.length); i++) { if (literalsPositions.indexOf(i) !== -1) { if (chars[0] === maskedValue[i]) { + cursor = i + 1; chars.shift(); } - cursor++; continue; } if (chars[0] && !this.validateCharOnPosition(chars[0], i, maskOptions.format)) { break; } - cursor++; - const char = chars.length ? chars.shift() : maskOptions.promptChar; + let char = maskOptions.promptChar; + if (chars.length) { + cursor = i + 1; + char = chars.shift(); + } maskedValue = this.replaceCharAt(maskedValue, i, char); } From 6ff800f55982126a702c609345269c88f294176c Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Fri, 28 Feb 2020 12:01:17 +0200 Subject: [PATCH 3/6] refactor(mask): ignore self drag events --- .../src/lib/directives/mask/mask.directive.ts | 12 ++++++++++-- src/app/mask/mask.sample.html | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) 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 232ed32c129..e30d4a3b9a3 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts @@ -125,6 +125,7 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA private _key: number; private _oldText = ''; private _dataValue = ''; + private _focused = false; private _droppedData: string; private _hasDropAction: boolean; private _stopPropagation: boolean; @@ -222,12 +223,14 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA /** @hidden */ @HostListener('focus') public onFocus(): void { + this._focused = true; this.showMask(this._dataValue); } /** @hidden */ @HostListener('blur', ['$event.target.value']) public onBlur(value: string): void { + this._focused = false; this.showDisplayValue(value); this._onTouchedCallback(); } @@ -235,13 +238,17 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA /** @hidden */ @HostListener('dragenter') public onDragEnter(): void { - this.showMask(this._dataValue); + if (!this._focused) { + this.showMask(this._dataValue); + } } /** @hidden */ @HostListener('dragleave') public onDragLeave(): void { - this.showDisplayValue(this.inputValue); + if (!this._focused) { + this.showDisplayValue(this.inputValue); + } } /** @hidden */ @@ -257,6 +264,7 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA if (isIE()) { this._stopPropagation = true; } + // TODO(D.P.): focusedValuePipe should be deprecated or force-checked to match mask format this.inputValue = this.focusedValuePipe.transform(value); } else { this.inputValue = this.maskParser.applyMask(this.inputValue, this.maskOptions); diff --git a/src/app/mask/mask.sample.html b/src/app/mask/mask.sample.html index 1359f124d85..3e3e64f6f8d 100644 --- a/src/app/mask/mask.sample.html +++ b/src/app/mask/mask.sample.html @@ -12,17 +12,17 @@

Personal Data

- - - From dcd49b1bd089b9dcded9b49dd614c70351d63f38 Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Fri, 28 Feb 2020 13:04:07 +0200 Subject: [PATCH 4/6] chore(time-picker): update input pipe --- .../igniteui-angular/src/lib/time-picker/time-picker.pipes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts b/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts index a9938265933..8c3b5e27bda 100644 --- a/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts +++ b/projects/igniteui-angular/src/lib/time-picker/time-picker.pipes.ts @@ -121,7 +121,7 @@ export class TimeInputFormatPipe implements PipeTransform { } // TODO: Pending refactoring. - value = value ? value : (this.timePicker as any).displayValue; + value = (this.timePicker as any).displayValue; if (!value || value === mask) { return mask; } From 19127db4a2e58d1fa0ab5e4ed6341c00ec7760bf Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Fri, 28 Feb 2020 17:42:40 +0200 Subject: [PATCH 5/6] fix(mask): don't call model change on model write #6783 --- .../directives/mask/mask.directive.spec.ts | 50 ++++++++++++++++++- .../src/lib/directives/mask/mask.directive.ts | 1 - src/app/mask/mask.sample.html | 2 +- 3 files changed, 50 insertions(+), 3 deletions(-) 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 c6a7ce6fb5b..f4992d68928 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 @@ -7,10 +7,11 @@ import { } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { IgxInputGroupModule } from '../../input-group/input-group.component'; -import { IgxMaskModule } from './mask.directive'; +import { IgxMaskModule, IgxMaskDirective } from './mask.directive'; import { configureTestSuite } from '../../test-utils/configure-suite'; import { UIInteractions } from '../../test-utils/ui-interactions.spec'; +import { Replaced } from './mask-parsing.service'; describe('igxMask', () => { configureTestSuite(); @@ -399,6 +400,53 @@ describe('igxMask', () => { })); }); +describe('igxMaskDirective ControlValueAccessor Unit', () => { + let mask: IgxMaskDirective; + it('Should correctly implement interface methods', () => { + const mockNgControl = jasmine.createSpyObj('NgControl', ['registerOnChangeCb', 'registerOnTouchedCb']); + + const mockParser = jasmine.createSpyObj('MaskParsingService', { + applyMask: 'test____', + replaceInMask: { value: 'test_2__', end: 6 } as Replaced, + parseValueFromMask: 'test2' + }); + const format = 'CCCCCCCC'; + + // init + mask = new IgxMaskDirective(null, mockParser, null); + mask.mask = format; + mask.registerOnChange(mockNgControl.registerOnChangeCb); + mask.registerOnTouched(mockNgControl.registerOnTouchedCb); + spyOn(mask.onValueChange, 'emit'); + const inputGet = spyOnProperty(mask as any, 'inputValue', 'get'); + const inputSet = spyOnProperty(mask as any, 'inputValue', 'set'); + + // writeValue + inputGet.and.returnValue('formatted'); + mask.writeValue('test'); + expect(mockParser.applyMask).toHaveBeenCalledWith('test', jasmine.objectContaining({ format })); + expect(inputSet).toHaveBeenCalledWith('test____'); + expect(mockNgControl.registerOnChangeCb).not.toHaveBeenCalled(); + expect(mask.onValueChange.emit).toHaveBeenCalledWith({ rawValue: 'test', formattedValue: 'formatted' }); + + // OnChange callback + inputGet.and.returnValue('test_2___'); + spyOnProperty(mask as any, 'selectionEnd').and.returnValue(6); + const setSelectionSpy = spyOn(mask as any, 'setSelectionRange'); + mask.onInputChanged(); + expect(mockParser.replaceInMask).toHaveBeenCalledWith('', 'test_2', jasmine.objectContaining({ format }), 0, 0); + expect(inputSet).toHaveBeenCalledWith('test_2__'); + expect(setSelectionSpy).toHaveBeenCalledWith(6); + expect(mockNgControl.registerOnChangeCb).toHaveBeenCalledWith('test2'); + + // OnTouched callback + mask.onFocus(); + expect(mockNgControl.registerOnTouchedCb).not.toHaveBeenCalled(); + mask.onBlur(''); + expect(mockNgControl.registerOnTouchedCb).toHaveBeenCalledTimes(1); + }); +}); + @Component({ template: ` 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 e30d4a3b9a3..43059a67cd1 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts @@ -305,7 +305,6 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA } this._dataValue = this.includeLiterals ? this.inputValue : value; - this._onChangeCallback(this._dataValue); this.onValueChange.emit({ rawValue: value, formattedValue: this.inputValue }); } diff --git a/src/app/mask/mask.sample.html b/src/app/mask/mask.sample.html index 3e3e64f6f8d..adc9cb22b48 100644 --- a/src/app/mask/mask.sample.html +++ b/src/app/mask/mask.sample.html @@ -18,7 +18,7 @@

Personal Data

+ [(ngModel)]="person.socialSecurityNumber" (blur)="validateSSN(ssn, snackbar)" required/> From 21283c49ccf5918b50c61272848e7a16147504d9 Mon Sep 17 00:00:00 2001 From: Damyan Petev Date: Mon, 2 Mar 2020 09:55:05 +0200 Subject: [PATCH 6/6] refactor(mask): udpate test trying to assign model value --- .../src/lib/directives/mask/mask.directive.spec.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 f4992d68928..8e069e65d14 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 @@ -129,20 +129,17 @@ describe('igxMask', () => { it('Enter value with a preset mask and value', fakeAsync(() => { const fixture = TestBed.createComponent(MaskComponent); fixture.detectChanges(); + tick(); // NgModel updateValue Promise const comp = fixture.componentInstance; const input = comp.input; - input.nativeElement.dispatchEvent(new Event('input')); - tick(); expect(input.nativeElement.value).toEqual('(123) 4567-890'); expect(comp.value).toEqual('1234567890'); comp.value = '7777'; fixture.detectChanges(); - - input.nativeElement.dispatchEvent(new Event('input')); tick(); expect(input.nativeElement.value).toEqual('(777) 7___-___');