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 { }