import {
Component, ElementRef, OnInit, OnDestroy, Input, forwardRef, Output, EventEmitter,
ViewChild, ChangeDetectionStrategy, ViewEncapsulation, ChangeDetectorRef
} from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
export const INPUTMASK_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputMaskedComponent),
multi: true
};
@Component({
selector: 'farris-input-masked, f-input-masked',
template: ``,
providers: [INPUTMASK_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class InputMaskedComponent implements OnInit, OnDestroy, ControlValueAccessor {
@Input() type = 'text';
@Input() slotChar = '_';
@Input() autoClear = 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 = '[A-Za-z]';
@Input() autoFocus: boolean;
@Input() autocomplete: string;
@ViewChild('input') inputViewChild: ElementRef;
@Output() completed: EventEmitter = new EventEmitter();
@Output() focus: EventEmitter = new EventEmitter();
@Output() blur: EventEmitter = new EventEmitter();
@Output() input: EventEmitter = new EventEmitter();
@Output() keydown: EventEmitter = new EventEmitter();
value: any;
_mask: string;
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;
onModelChange = (val) => { };
onModelTouched = () => { };
constructor(public el: ElementRef, private cd: ChangeDetectorRef) { }
ngOnInit() {
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]`
};
const maskTokens = this.mask.split('');
for (let i = 0; i < maskTokens.length; i++) {
const 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++) {
const 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): void {
this.onModelChange = fn;
}
registerOnTouched(fn): void {
this.onModelTouched = fn;
}
setDisabledState(val: boolean): void {
this.disabled = val;
this.cd.markForCheck();
}
caret(first?: number, last?: number) {
let range, begin, end;
if (!this.inputViewChild.nativeElement.offsetParent ||
this.inputViewChild.nativeElement !== this.inputViewChild.nativeElement.ownerDocument.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, end };
}
}
isCompleted(): 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) {
const curVal = this.inputViewChild.nativeElement.value;
const 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.completed.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.completed.emit();
}
}, 0);
}
}
onInputBlur(e) {
this.focused = false;
this.onModelTouched();
this.checkVal();
this.updateFilledState();
this.blur.emit(e);
if (this.inputViewChild.nativeElement.value !== this.focusText || this.inputViewChild.nativeElement.value !== this.value) {
this.updateModel(e);
const event = document.createEvent('HTMLEvents');
event.initEvent('change', true, false);
this.inputViewChild.nativeElement.dispatchEvent(event);
}
}
onInputKeydown(e) {
if (this.readonly) {
return;
}
const k = e.which || e.keyCode;
let pos;
let begin;
let end;
this.oldVal = this.inputViewChild.nativeElement.value;
this.keydown.emit(e);
if (k === 8 || k === 46) {
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.input.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;
}
const k = e.which || e.keyCode;
const pos = this.caret();
let p;
let c;
let next;
let 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);
this.caret(next);
if (pos.begin <= this.lastRequiredNonMaskPos) {
completed = this.isCompleted();
}
this.input.emit(e);
}
}
e.preventDefault();
}
this.updateModel(e);
this.updateFilledState();
if (completed) {
this.completed.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
const test = this.inputViewChild.nativeElement.value;
let lastMatch = -1;
let i;
let c;
let 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 !== this.inputViewChild.nativeElement.ownerDocument.activeElement) {
return;
}
this.writeBuffer();
if (pos == this.mask.replace('?', '').length) {
this.caret(0, pos);
} else {
this.caret(pos);
}
}, 10);
this.focus.emit(event);
}
onInputChange(event) {
if (this.androidChrome) {
this.handleAndroidInput(event);
} else {
this.handleInputChange(event);
}
this.input.emit(event);
}
handleInputChange(event) {
if (this.readonly) {
return;
}
setTimeout(() => {
const pos = this.checkVal(true);
this.caret(pos);
this.updateModel(event);
if (this.isCompleted()) {
this.completed.emit();
}
}, 0);
}
getUnmaskedValue() {
const unmaskedBuffer = [];
for (let i = 0; i < this.buffer.length; i++) {
const 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 != '';
}
setFocus() {
this.inputViewChild.nativeElement.focus();
}
ngOnDestroy() {
}
}