import {NgModule,Component,ElementRef,OnInit,OnDestroy,Input,forwardRef,Output,EventEmitter,ViewChild,ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core'; import {CommonModule} from '@angular/common'; import {DomHandler} from 'mirra-ui/dom'; import {InputTextModule} from 'mirra-ui/inputtext'; import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms'; export const INPUTMASK_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputMask), multi: true }; @Component({ selector: 'p-inputMask', template: ``, host: { '[class.p-inputwrapper-filled]': 'filled', '[class.p-inputwrapper-focus]': 'focused' }, providers: [INPUTMASK_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None }) export class InputMask implements OnInit,OnDestroy,ControlValueAccessor { @Input() type: string = 'text'; @Input() slotChar: string = '_'; @Input() autoClear: boolean = true; @Input() style: any; @Input() inputId: string; @Input() styleClass: string; @Input() placeholder: string; @Input() size: number; @Input() maxlength: number; @Input() tabindex: string; @Input() title: string; @Input() ariaLabel: string; @Input() ariaRequired: boolean; @Input() disabled: boolean; @Input() readonly: boolean; @Input() unmask: boolean; @Input() name: string; @Input() required: boolean; @Input() characterPattern: string = '[A-Za-z]'; @Input() autoFocus: boolean; @Input() autocomplete: string; @ViewChild('input', { static: true }) inputViewChild: ElementRef; @Output() onComplete: EventEmitter = new EventEmitter(); @Output() onFocus: EventEmitter = new EventEmitter(); @Output() onBlur: EventEmitter = new EventEmitter(); @Output() onInput: EventEmitter = new EventEmitter(); value: any; _mask: string; onModelChange: Function = () => {}; onModelTouched: Function = () => {}; input: HTMLInputElement; filled: boolean; defs: any; tests: any[]; partialPosition: any; firstNonMaskPos: number; lastRequiredNonMaskPos: any; len: number; oldVal: string; buffer: any; defaultBuffer: string; focusText: string; caretTimeoutId: any; androidChrome: boolean; focused: boolean; constructor(public el: ElementRef) {} ngOnInit() { let ua = DomHandler.getUserAgent(); this.androidChrome = /chrome/i.test(ua) && /android/i.test(ua); this.initMask(); } @Input() get mask(): string { return this._mask; } set mask(val:string) { this._mask = val; this.initMask(); this.writeValue(''); this.onModelChange(this.value); } initMask() { this.tests = []; this.partialPosition = this.mask.length; this.len = this.mask.length; this.firstNonMaskPos = null; this.defs = { '9': '[0-9]', 'a': this.characterPattern, '*': `${this.characterPattern}|[0-9]` }; let maskTokens = this.mask.split(''); for(let i = 0; i < maskTokens.length; i++) { let c = maskTokens[i]; if (c == '?') { this.len--; this.partialPosition = i; } else if (this.defs[c]) { this.tests.push(new RegExp(this.defs[c])); if (this.firstNonMaskPos === null) { this.firstNonMaskPos = this.tests.length - 1; } if (i < this.partialPosition){ this.lastRequiredNonMaskPos = this.tests.length - 1; } } else { this.tests.push(null); } } this.buffer = []; for(let i = 0; i < maskTokens.length; i++) { let c = maskTokens[i]; if (c != '?') { if (this.defs[c]) this.buffer.push(this.getPlaceholder(i)); else this.buffer.push(c); } } this.defaultBuffer = this.buffer.join(''); } writeValue(value: any) : void { this.value = value; if (this.inputViewChild && this.inputViewChild.nativeElement) { if (this.value == undefined || this.value == null) this.inputViewChild.nativeElement.value = ''; else this.inputViewChild.nativeElement.value = this.value; this.checkVal(); this.focusText = this.inputViewChild.nativeElement.value; this.updateFilledState(); } } registerOnChange(fn: Function): void { this.onModelChange = fn; } registerOnTouched(fn: Function): void { this.onModelTouched = fn; } setDisabledState(val: boolean): void { this.disabled = val; } caret(first?: number, last?: number) { let range, begin, end; if (!this.inputViewChild.nativeElement.offsetParent||this.inputViewChild.nativeElement !== document.activeElement) { return; } if (typeof first == 'number') { begin = first; end = (typeof last === 'number') ? last : begin; if (this.inputViewChild.nativeElement.setSelectionRange) { this.inputViewChild.nativeElement.setSelectionRange(begin, end); } else if (this.inputViewChild.nativeElement['createTextRange']) { range = this.inputViewChild.nativeElement['createTextRange'](); range.collapse(true); range.moveEnd('character', end); range.moveStart('character', begin); range.select(); } } else { if (this.inputViewChild.nativeElement.setSelectionRange) { begin = this.inputViewChild.nativeElement.selectionStart; end = this.inputViewChild.nativeElement.selectionEnd; } else if (document['selection'] && document['selection'].createRange) { range = document['selection'].createRange(); begin = 0 - range.duplicate().moveStart('character', -100000); end = begin + range.text.length; } return {begin: begin, end: end}; } } isCompleted(): boolean { let completed: boolean; for (let i = this.firstNonMaskPos; i <= this.lastRequiredNonMaskPos; i++) { if (this.tests[i] && this.buffer[i] === this.getPlaceholder(i)) { return false; } } return true; } getPlaceholder(i: number) { if (i < this.slotChar.length) { return this.slotChar.charAt(i); } return this.slotChar.charAt(0); } seekNext(pos) { while (++pos < this.len && !this.tests[pos]); return pos; } seekPrev(pos) { while (--pos >= 0 && !this.tests[pos]); return pos; } shiftL(begin:number,end:number) { let i, j; if (begin<0) { return; } for (i = begin, j = this.seekNext(end); i < this.len; i++) { if (this.tests[i]) { if (j < this.len && this.tests[i].test(this.buffer[j])) { this.buffer[i] = this.buffer[j]; this.buffer[j] = this.getPlaceholder(j); } else { break; } j = this.seekNext(j); } } this.writeBuffer(); this.caret(Math.max(this.firstNonMaskPos, begin)); } shiftR(pos) { let i, c, j, t; for (i = pos, c = this.getPlaceholder(pos); i < this.len; i++) { if (this.tests[i]) { j = this.seekNext(i); t = this.buffer[i]; this.buffer[i] = c; if (j < this.len && this.tests[j].test(t)) { c = t; } else { break; } } } } handleAndroidInput(e) { var curVal = this.inputViewChild.nativeElement.value; var pos = this.caret(); if (this.oldVal && this.oldVal.length && this.oldVal.length > curVal.length ) { // a deletion or backspace happened this.checkVal(true); while (pos.begin > 0 && !this.tests[pos.begin-1]) pos.begin--; if (pos.begin === 0) { while (pos.begin < this.firstNonMaskPos && !this.tests[pos.begin]) pos.begin++; } setTimeout(() => { this.caret(pos.begin, pos.begin); this.updateModel(e); if (this.isCompleted()) { this.onComplete.emit(); } }, 0); } else { this.checkVal(true); while (pos.begin < this.len && !this.tests[pos.begin]) pos.begin++; setTimeout(() => { this.caret(pos.begin, pos.begin); this.updateModel(e); if (this.isCompleted()) { this.onComplete.emit(); } }, 0); } } onInputBlur(e) { this.focused = false; this.onModelTouched(); this.checkVal(); this.updateFilledState(); this.onBlur.emit(e); if (this.inputViewChild.nativeElement.value != this.focusText || this.inputViewChild.nativeElement.value != this.value) { this.updateModel(e); let event = document.createEvent('HTMLEvents'); event.initEvent('change', true, false); this.inputViewChild.nativeElement.dispatchEvent(event); } } onKeyDown(e) { if (this.readonly) { return; } let k = e.which||e.keyCode, pos, begin, end; let iPhone = /iphone/i.test(DomHandler.getUserAgent()); this.oldVal = this.inputViewChild.nativeElement.value; //backspace, delete, and escape get special treatment if (k === 8 || k === 46 || (iPhone && k === 127)) { pos = this.caret(); begin = pos.begin; end = pos.end; if (end - begin === 0) { begin=k!==46?this.seekPrev(begin):(end=this.seekNext(begin-1)); end=k===46?this.seekNext(end):end; } this.clearBuffer(begin, end); this.shiftL(begin, end - 1); this.updateModel(e); this.onInput.emit(e); e.preventDefault(); } else if ( k === 13 ) { // enter this.onInputBlur(e); this.updateModel(e); } else if (k === 27) { // escape this.inputViewChild.nativeElement.value = this.focusText; this.caret(0, this.checkVal()); this.updateModel(e); e.preventDefault(); } } onKeyPress(e) { if (this.readonly){ return; } var k = e.which || e.keyCode, pos = this.caret(), p, c, next, completed; if (e.ctrlKey || e.altKey || e.metaKey || k < 32 || (k > 34 && k < 41)) {//Ignore return; } else if ( k && k !== 13 ) { if (pos.end - pos.begin !== 0){ this.clearBuffer(pos.begin, pos.end); this.shiftL(pos.begin, pos.end-1); } p = this.seekNext(pos.begin - 1); if (p < this.len) { c = String.fromCharCode(k); if (this.tests[p].test(c)) { this.shiftR(p); this.buffer[p] = c; this.writeBuffer(); next = this.seekNext(p); if (/android/i.test(DomHandler.getUserAgent())) { //Path for CSP Violation on FireFox OS 1.1 let proxy = () => { this.caret(next); }; setTimeout(proxy,0); } else { this.caret(next); } if (pos.begin <= this.lastRequiredNonMaskPos) { completed = this.isCompleted(); } this.onInput.emit(e); } } e.preventDefault(); } this.updateModel(e); this.updateFilledState(); if (completed) { this.onComplete.emit(); } } clearBuffer(start, end) { let i; for (i = start; i < end && i < this.len; i++) { if (this.tests[i]) { this.buffer[i] = this.getPlaceholder(i); } } } writeBuffer() { this.inputViewChild.nativeElement.value = this.buffer.join(''); } checkVal(allow?: boolean) { //try to place characters where they belong let test = this.inputViewChild.nativeElement.value, lastMatch = -1, i, c, pos; for (i = 0, pos = 0; i < this.len; i++) { if (this.tests[i]) { this.buffer[i] = this.getPlaceholder(i); while (pos++ < test.length) { c = test.charAt(pos - 1); if (this.tests[i].test(c)) { this.buffer[i] = c; lastMatch = i; break; } } if (pos > test.length) { this.clearBuffer(i + 1, this.len); break; } } else { if (this.buffer[i] === test.charAt(pos)) { pos++; } if ( i < this.partialPosition){ lastMatch = i; } } } if (allow) { this.writeBuffer(); } else if (lastMatch + 1 < this.partialPosition) { if (this.autoClear || this.buffer.join('') === this.defaultBuffer) { // Invalid value. Remove it and replace it with the // mask, which is the default behavior. if (this.inputViewChild.nativeElement.value) this.inputViewChild.nativeElement.value = ''; this.clearBuffer(0, this.len); } else { // Invalid value, but we opt to show the value to the // user and allow them to correct their mistake. this.writeBuffer(); } } else { this.writeBuffer(); this.inputViewChild.nativeElement.value = this.inputViewChild.nativeElement.value.substring(0, lastMatch + 1); } return (this.partialPosition ? i : this.firstNonMaskPos); } onInputFocus(event) { if (this.readonly){ return; } this.focused = true; clearTimeout(this.caretTimeoutId); let pos; this.focusText = this.inputViewChild.nativeElement.value; pos = this.checkVal(); this.caretTimeoutId = setTimeout(() => { if (this.inputViewChild.nativeElement !== document.activeElement){ return; } this.writeBuffer(); if (pos == this.mask.replace("?","").length) { this.caret(0, pos); } else { this.caret(pos); } }, 10); this.onFocus.emit(event); } onInputChange(event) { if (this.androidChrome) this.handleAndroidInput(event); else this.handleInputChange(event); this.onInput.emit(event); } handleInputChange(event) { if (this.readonly){ return; } setTimeout(() => { var pos = this.checkVal(true); this.caret(pos); this.updateModel(event); if (this.isCompleted()) { this.onComplete.emit(); } }, 0); } getUnmaskedValue() { let unmaskedBuffer = []; for(let i = 0; i < this.buffer.length; i++) { let c = this.buffer[i]; if (this.tests[i] && c != this.getPlaceholder(i)) { unmaskedBuffer.push(c); } } return unmaskedBuffer.join(''); } updateModel(e) { const updatedValue = this.unmask ? this.getUnmaskedValue() : e.target.value; if (updatedValue !== null || updatedValue !== undefined) { this.value = updatedValue; this.onModelChange(this.value); } } updateFilledState() { this.filled = this.inputViewChild.nativeElement && this.inputViewChild.nativeElement.value != ''; } focus() { this.inputViewChild.nativeElement.focus(); } ngOnDestroy() { } } @NgModule({ imports: [CommonModule,InputTextModule], exports: [InputMask], declarations: [InputMask] }) export class InputMaskModule { }