import {NgModule,Component,ChangeDetectionStrategy, Input, ElementRef, ViewChild, OnInit, EventEmitter, Output, forwardRef, ViewEncapsulation, ChangeDetectorRef} from '@angular/core';
import {CommonModule} from '@angular/common';
import {InputTextModule} from 'mirra-ui/inputtext';
import { ButtonModule } from 'mirra-ui/button';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
export const INPUTNUMBER_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputNumber),
multi: true
};
@Component({
selector: 'p-inputNumber',
template: `
`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [INPUTNUMBER_VALUE_ACCESSOR],
encapsulation: ViewEncapsulation.None,
styleUrls: ['./inputnumber.scss']
})
export class InputNumber implements OnInit,ControlValueAccessor {
@Input() showButtons: boolean = false;
@Input() format: boolean = true;
@Input() buttonLayout: string = "stacked";
@Input() prefix: string;
@Input() locale: string;
@Input() suffix: string;
@Input() localeMatcher: string;
@Input() currency: string;
@Input() currencyDisplay: string;
@Input() useGrouping: boolean = true;
@Input() disabled: boolean;
@Input() inputId: string;
@Input() styleClass: string;
@Input() style: any;
@Input() placeholder: string;
@Input() size: number;
@Input() maxlength: number;
@Input() tabindex: string;
@Input() title: string;
@Input() ariaLabel: string;
@Input() ariaRequired: boolean;
@Input() name: string;
@Input() required: boolean;
@Input() autocomplete: string;
@Input() min: number;
@Input() max: number;
@Input() minFractionDigits: number;
@Input() maxFractionDigits: number;
@Input() incrementButtonClass: string;
@Input() decrementButtonClass: string;
@Input() incrementButtonIcon: string = 'far fa-angle-up';
@Input() decrementButtonIcon: string = 'far fa-angle-down';
@Input() mode: string = "decimal";
@Input() step: number = 1;
@Input() inputStyle: any;
@Input() inputStyleClass: string;
@ViewChild('input') input: ElementRef;
@Output() onFocus: EventEmitter = new EventEmitter();
@Output() onBlur: EventEmitter = new EventEmitter();
value: number;
onModelChange: Function = () => {};
onModelTouched: Function = () => {};
focused: boolean;
isSpecialChar: boolean;
timer: any;
lastValue: string;
_numeral: any;
numberFormat: any;
_decimal: any;
_group: any;
_minusSign: any;
_currency: any;
_prefix: any;
_suffix: any;
_index: any;
constructor(public el: ElementRef, private cd: ChangeDetectorRef) { }
ngOnInit() {
this.numberFormat = new Intl.NumberFormat(this.locale, this.getOptions());
const numerals = [...new Intl.NumberFormat(this.locale, {useGrouping: false}).format(9876543210)].reverse();
const index = new Map(numerals.map((d, i) => [d, i]));
this._numeral = new RegExp(`[${numerals.join('')}]`, 'g');
this._decimal = this.getDecimalExpression();
this._group = this.getGroupingExpression();
this._minusSign = this.getMinusSignExpression();
this._currency = this.getCurrencyExpression();
this._suffix = new RegExp(`[${this.suffix||''}]`, 'g');
this._prefix = new RegExp(`[${this.prefix||''}]`, 'g');
this._index = d => index.get(d);
}
formatValue(value) {
if (value != null) {
if (this.format) {
let formatter = new Intl.NumberFormat(this.locale, this.getOptions());
let formattedValue = formatter.format(value);
if (this.prefix) {
formattedValue = this.prefix + formattedValue;
}
if (this.suffix) {
formattedValue = formattedValue + this.suffix;
}
return formattedValue;
}
return value;
}
return '';
}
formattedValue() {
return this.formatValue(this.value);
}
onInput(event) {
if (this.isSpecialChar) {
event.target.value = this.lastValue;
}
this.isSpecialChar = false;
}
onInputKeyDown(event) {
this.lastValue = event.target.value;
if (event.shiftKey || event.altKey) {
this.isSpecialChar = true;
return;
}
let selectionStart = event.target.selectionStart;
let selectionEnd = event.target.selectionEnd;
let inputValue = event.target.value;
if (event.altKey) {
event.preventDefault();
}
switch (event.which) {
//up
case 38:
this.spin(event, 1);
event.preventDefault();
break;
//down
case 40:
this.spin(event, -1);
event.preventDefault();
break;
//left
case 37:
if (!this.isNumeralChar(inputValue.charAt(selectionStart - 1))) {
event.preventDefault();
}
break;
//right
case 39:
if (!this.isNumeralChar(inputValue.charAt(selectionStart))) {
event.preventDefault();
}
break;
//backspace
case 8: {
event.preventDefault();
let newValueStr = null;
if (selectionStart === selectionEnd) {
let deleteChar = inputValue.charAt(selectionStart - 1);
let decimalCharIndex = inputValue.search(this._decimal);
this._decimal.lastIndex = 0;
if (this.isNumeralChar(deleteChar)) {
if (this._group.test(deleteChar)) {
this._group.lastIndex = 0;
newValueStr = inputValue.slice(0, selectionStart - 2) + inputValue.slice(selectionStart - 1);
}
else if (this._decimal.test(deleteChar)) {
this._decimal.lastIndex = 0;
this.input.nativeElement.setSelectionRange(selectionStart - 1, selectionStart - 1);
}
else if (decimalCharIndex > 0 && selectionStart > decimalCharIndex) {
newValueStr = inputValue.slice(0, selectionStart - 1) + '0' + inputValue.slice(selectionStart);
}
else {
newValueStr = inputValue.slice(0, selectionStart - 1) + inputValue.slice(selectionStart);
}
}
if (newValueStr != null) {
this.updateValue(event, newValueStr, 'delete-single');
}
}
else {
newValueStr = this.deleteRange(inputValue, selectionStart, selectionEnd);
this.updateValue(event, newValueStr, 'delete-range');
}
break;
}
default:
break;
}
}
onInputKeyPress(event) {
event.preventDefault();
let code = event.which || event.keyCode;
let char = String.fromCharCode(code);
if ((48 <= code && code <= 57) || this.isMinusSign(char)) {
this.insert(event, char);
}
}
onPaste(event) {
event.preventDefault();
let data = (event.clipboardData || window['clipboardData']).getData('Text');
if (data) {
let filteredData = this.parseValue(data);
if (filteredData != null) {
this.insert(event, filteredData.toString());
}
}
}
onInputClick() {
this.initCursor();
}
onInputFocus(event) {
this.focused = true;
this.onFocus.emit(event);
}
onInputBlur(event) {
this.focused = false;
let newValue = this.validateValue(this.parseValue(this.input.nativeElement.value));
this.input.nativeElement.value = this.formatValue(newValue);
this.input.nativeElement.setAttribute('aria-valuenow', newValue);
this.updateModel(event, newValue);
this.onBlur.emit(event);
}
onUpButtonMouseDown(event) {
this.input.nativeElement.focus();
this.repeat(event, null, 1);
event.preventDefault();
}
onUpButtonMouseUp() {
this.clearTimer();
}
onUpButtonMouseLeave() {
this.clearTimer();
}
onUpButtonKeyDown(event) {
if (event.keyCode === 32 || event.keyCode === 13) {
this.repeat(event, null, 1);
}
}
onUpButtonKeyUp() {
this.clearTimer();
}
onDownButtonMouseDown(event) {
this.input.nativeElement.focus();
this.repeat(event, null, -1);
event.preventDefault();
}
onDownButtonMouseUp() {
this.clearTimer();
}
onDownButtonMouseLeave() {
this.clearTimer();
}
onDownButtonKeyUp() {
this.clearTimer();
}
onDownButtonKeyDown(event) {
if (event.keyCode === 32 || event.keyCode === 13) {
this.repeat(event, null, -1);
}
}
spin(event, dir) {
let step = this.step * dir;
let currentValue = this.parseValue(this.input.nativeElement.value) || 0;
let newValue = this.validateValue(currentValue + step);
if (this.maxlength && this.maxlength < this.formatValue(newValue).length) {
return;
}
this.updateInput(newValue, 'spin');
this.updateModel(event, newValue);
}
repeat(event, interval, dir) {
let i = interval || 500;
this.clearTimer();
this.timer = setTimeout(() => {
this.repeat(event, 40, dir);
}, i);
this.spin(event, dir);
}
clearTimer() {
if (this.timer) {
clearInterval(this.timer);
}
}
insert(event, text) {
let selectionStart = this.input.nativeElement.selectionStart;
let selectionEnd = this.input.nativeElement.selectionEnd;
let inputValue = this.input.nativeElement.value.trim();
let maxFractionDigits = this.numberFormat.resolvedOptions().maximumFractionDigits;
let newValueStr;
let decimalCharIndex = inputValue.search(this._decimal);
this._decimal.lastIndex = 0;
if (decimalCharIndex > 0 && selectionStart > decimalCharIndex) {
if ((selectionStart + text.length - (decimalCharIndex + 1)) <= maxFractionDigits) {
newValueStr = inputValue.slice(0, selectionStart) + text + inputValue.slice(selectionStart + text.length);
this.updateValue(event, newValueStr, 'insert');
}
}
else {
newValueStr = this.insertText(inputValue, text, selectionStart, selectionEnd);
this.updateValue(event, newValueStr, 'insert');
}
}
insertText(value, text, start, end) {
let newValueStr;
if ((end - start) === value.length)
newValueStr = text;
else if (start === 0)
newValueStr = text + value.slice(end);
else if (end === value.length)
newValueStr = value.slice(0, start) + text;
else
newValueStr = value.slice(0, start) + text + value.slice(end);
return newValueStr;
}
initCursor() {
let selectionStart = this.input.nativeElement.selectionStart;
let inputValue = this.input.nativeElement.value;
let valueLength = inputValue.length;
let index = null;
let char = inputValue.charAt(selectionStart);
if (this.isNumeralChar(char)) {
return;
}
//left
let i = selectionStart - 1;
while (i >= 0) {
char = inputValue.charAt(i);
if (this.isNumeralChar(char)) {
index = i;
break;
}
else {
i--;
}
}
if (index !== null) {
this.input.nativeElement.setSelectionRange(index + 1, index + 1);
}
else {
i = selectionStart + 1;
while (i < valueLength) {
char = inputValue.charAt(i);
if (this.isNumeralChar(char)) {
index = i;
break;
}
else {
i++;
}
}
if (index !== null) {
this.input.nativeElement.setSelectionRange(index, index);
}
}
}
updateInput(value, operation) {
let currentLength = this.input.nativeElement.value.length;
if (currentLength === 0) {
this.input.nativeElement.value = this.formatValue(value);
this.input.nativeElement.setSelectionRange(0, 0);
this.initCursor();
this.input.nativeElement.setSelectionRange(this.input.nativeElement.selectionStart + 1, this.input.nativeElement.selectionStart + 1);
}
else {
let selectionStart = this.input.nativeElement.selectionEnd;
let selectionEnd = this.input.nativeElement.selectionEnd;
let formattedValue = this.formatValue(value);
if (this.maxlength && this.maxlength < formattedValue.length) {
return;
}
this.input.nativeElement.value = this.formatValue(value);
let newLength = this.input.nativeElement.value.length;
if (newLength === currentLength) {
if (operation === 'insert')
this.input.nativeElement.setSelectionRange(selectionEnd + 1, selectionEnd + 1);
else if (operation === 'delete-single')
this.input.nativeElement.setSelectionRange(selectionEnd - 1, selectionEnd - 1);
else if (operation === 'delete-range')
this.input.nativeElement.setSelectionRange(selectionStart, selectionStart);
else if (operation === 'spin')
this.input.nativeElement.setSelectionRange(selectionStart, selectionEnd);
}
else {
selectionEnd = selectionEnd + (newLength - currentLength);
this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd);
}
}
this.input.nativeElement.setAttribute('aria-valuenow', value);
}
updateModel(event, value) {
this.value = value;
this.onModelChange(value);
}
updateValue(event, valueStr, operation) {
if (valueStr != null) {
let newValue = this.parseValue(valueStr);
this.updateInput(newValue, operation);
}
}
validateValue(value) {
if (this.min !== null && value < this.min) {
return this.min;
}
if (this.max !== null && value > this.max) {
return this.max;
}
return value;
}
deleteRange(value, start, end) {
let newValueStr;
if ((end - start) === value.length)
newValueStr = '';
else if (start === 0)
newValueStr = value.slice(end);
else if (end === value.length)
newValueStr = value.slice(0, start);
else
newValueStr = value.slice(0, start) + value.slice(end);
return newValueStr;
}
isNumeralChar(char) {
if (char.length === 1 && (this._numeral.test(char) || this._decimal.test(char) || this._group.test(char) || this._minusSign.test(char))) {
this.resetRegex();
return true;
}
return false;
}
isMinusSign(char) {
if (this._minusSign.test(char)) {
this._minusSign.lastIndex = 0;
return true;
}
return false;
}
parseValue(text) {
let filteredText = text.trim()
.replace(/\s/g, '')
.replace(this._currency, '')
.replace(this._group, '')
.replace(this._suffix, '')
.replace(this._prefix, '')
.replace(this._minusSign, '-')
.replace(this._decimal, '.')
.replace(this._numeral, this._index);
if (filteredText) {
let parsedValue = +filteredText;
return isNaN(parsedValue) ? null : parsedValue;
}
return null;
}
writeValue(value: any) : void {
this.value = value;
this.cd.markForCheck();
}
registerOnChange(fn: Function): void {
this.onModelChange = fn;
}
registerOnTouched(fn: Function): void {
this.onModelTouched = fn;
}
setDisabledState(val: boolean): void {
this.disabled = val;
}
getOptions() {
return {
localeMatcher: this.localeMatcher,
style: this.mode,
currency: this.currency,
currencyDisplay: this.currencyDisplay,
useGrouping: this.useGrouping,
minimumFractionDigits: this.minFractionDigits,
maximumFractionDigits: this.maxFractionDigits
};
}
getDecimalExpression() {
const formatter = new Intl.NumberFormat(this.locale, {useGrouping: false});
return new RegExp(`[${formatter.format(1.1).trim().replace(this._numeral, '')}]`, 'g');
}
getGroupingExpression() {
const formatter = new Intl.NumberFormat(this.locale, {useGrouping: true});
return new RegExp(`[${formatter.format(1000).trim().replace(this._numeral, '')}]`, 'g');
}
getMinusSignExpression() {
const formatter = new Intl.NumberFormat(this.locale, {useGrouping: false});
return new RegExp(`[${formatter.format(-1).trim().replace(this._numeral, '')}]`, 'g');
}
getCurrencyExpression() {
if (this.currency) {
const formatter = new Intl.NumberFormat(this.locale, {style: 'currency', currency: this.currency, currencyDisplay: this.currencyDisplay});
return new RegExp(`[${formatter.format(1).replace(/\s/g, '').replace(this._numeral, '').replace(this._decimal, '').replace(this._group, '')}]`, 'g');
}
return new RegExp(`[]`,'g');
}
filled() {
return (this.value != null && this.value.toString().length > 0)
}
resetRegex() {
this._numeral.lastIndex = 0;
this._decimal.lastIndex = 0;
this._group.lastIndex = 0;
this._minusSign.lastIndex = 0;
}
}
@NgModule({
imports: [CommonModule,InputTextModule, ButtonModule],
exports: [InputNumber],
declarations: [InputNumber]
})
export class InputNumberModule { }