From 782500c009f1f9456f36671a21d8b93f047e22ef Mon Sep 17 00:00:00 2001 From: Hlars Date: Sun, 27 Apr 2025 19:35:28 +0200 Subject: [PATCH] added mat currency input field --- angular.json | 5 +- .../currency-input.component.html | 6 + .../currency-input.component.scss | 16 + .../currency-input.component.spec.ts | 23 + .../currency-input.component.ts | 873 ++++++++++++++++++ 5 files changed, 922 insertions(+), 1 deletion(-) create mode 100644 src/app/currency-input/currency-input.component.html create mode 100644 src/app/currency-input/currency-input.component.scss create mode 100644 src/app/currency-input/currency-input.component.spec.ts create mode 100644 src/app/currency-input/currency-input.component.ts diff --git a/angular.json b/angular.json index 357b559..0dd7482 100644 --- a/angular.json +++ b/angular.json @@ -4,6 +4,9 @@ "newProjectRoot": "projects", "projects": { "material-mentions": { + "i18n": { + "sourceLocale": "de" + }, "projectType": "application", "schematics": { "@schematics/angular:component": { @@ -101,4 +104,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/app/currency-input/currency-input.component.html b/src/app/currency-input/currency-input.component.html new file mode 100644 index 0000000..f5d50b2 --- /dev/null +++ b/src/app/currency-input/currency-input.component.html @@ -0,0 +1,6 @@ +
+ +
\ No newline at end of file diff --git a/src/app/currency-input/currency-input.component.scss b/src/app/currency-input/currency-input.component.scss new file mode 100644 index 0000000..1eb6aca --- /dev/null +++ b/src/app/currency-input/currency-input.component.scss @@ -0,0 +1,16 @@ +div { + display: flex; + width: 100%; +} + +input { + flex-grow: 1; + + border: none; + background: none; + padding: 0; + outline: none; + font: inherit; + text-align: right; + color: currentColor; +} diff --git a/src/app/currency-input/currency-input.component.spec.ts b/src/app/currency-input/currency-input.component.spec.ts new file mode 100644 index 0000000..6a4b5b6 --- /dev/null +++ b/src/app/currency-input/currency-input.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CurrencyInputComponent } from './currency-input.component'; + +describe('CurrencyInputComponent', () => { + let component: CurrencyInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CurrencyInputComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CurrencyInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/currency-input/currency-input.component.ts b/src/app/currency-input/currency-input.component.ts new file mode 100644 index 0000000..d13000d --- /dev/null +++ b/src/app/currency-input/currency-input.component.ts @@ -0,0 +1,873 @@ +import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Component, ElementRef, HostBinding, Inject, Input, LOCALE_ID, Optional, Self, ViewChild } from '@angular/core'; +import { ControlValueAccessor, NgControl, Validators } from '@angular/forms'; +import { MatFormFieldControl } from '@angular/material/form-field'; +import { Subject } from 'rxjs'; + +interface FormatterOptions { + useGrouping: boolean, + minimumFractionDigits: number, + maximumFractionDigits: number +} + +@Component({ + selector: 'app-currency-input', + imports: [], + providers: [{ provide: MatFormFieldControl, useExisting: CurrencyInputComponent }], + templateUrl: './currency-input.component.html', + styleUrl: './currency-input.component.scss' +}) +export class CurrencyInputComponent implements MatFormFieldControl, ControlValueAccessor { + + constructor(@Optional() @Self() public ngControl: NgControl, @Inject(LOCALE_ID) public locale: string) { + // Replace the provider from above with this. + if (this.ngControl != null) { + // Setting the value accessor directly (instead of using + // the providers) to avoid running into a circular import. + this.ngControl.valueAccessor = this; + } + + this.constructParsers(); + } + + ngAfterViewInit(): void { + if (this.ngControl) + this.required = this.ngControl.control?.hasValidator(Validators.required); + // if (this.commentText !== '') { + // this._commentInputRef.nativeElement.innerHTML = this.commentText; + // this.onChange(this.commentText); + // this.stateChanges.next() + // } + } + + ngOnDestroy() { this.stateChanges.complete(); } + + // ================================================== + // => Everything related to custom form field control + // ================================================== + static nextId = 0; + @HostBinding() id = `currency-input-${CurrencyInputComponent.nextId++}`; + @HostBinding('class.floating') + + @Input('aria-describedby') userAriaDescribedBy: string = ''; + @Input() + get value(): number | null { + return this.inputValue; + } + set value(value: number | null) { + this.inputValue = value ?? 0; + } + @Input() + get placeholder() { + return this._placeholder; + } + set placeholder(plh) { + this._placeholder = plh; + this.stateChanges.next(); + } + get shouldLabelFloat() { + return this.focused || !this.empty; + } + @Input() + get required(): boolean { return this._required; } + set required(req: BooleanInput) { + this._required = coerceBooleanProperty(req); + this.stateChanges.next(); + } + @Input() + get disabled(): boolean { return this._disabled; } + set disabled(value: BooleanInput) { + this._disabled = coerceBooleanProperty(value); + this.stateChanges.next(); + } + + inputValue: number | null = null; + controlType = 'currency-input'; + stateChanges = new Subject(); + private _placeholder: string = ''; + private _required = false; + private _disabled = false; + focused = false; + touched = false; + + onChange = (value: number | null) => { }; + onTouched = () => { }; + + writeValue(value: number): void { + this.inputValue = value; + // throw new Error('Method not implemented.'); + } + registerOnChange(onChange: any): void { + this.onChange = onChange; + // throw new Error('Method not implemented.'); + } + registerOnTouched(onTouched: any): void { + this.onTouched = onTouched; + // throw new Error('Method not implemented.'); + } + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + onFocusIn(event: FocusEvent) { + if (!this.focused) { + this.focused = true; + this.stateChanges.next(); + } + } + + onFocusOut(event: FocusEvent) { + // if (!this._commentInputRef.nativeElement.contains(event.relatedTarget as Element)) { + // this.cleanEmptyElements(this._commentInputRef.nativeElement); + // this.commentText = this._commentInputRef.nativeElement.innerHTML; + // this.touched = true; + // this.focused = false; + // this.onTouched(); + // this.onChange(this.commentText); + // this.stateChanges.next(); + // } + this.touched = true; + this.focused = false; + this.onTouched(); + this.onChange(this.inputValue); + this.stateChanges.next(); + } + + setDescribedByIds(ids: string[]) { + const controlElement = this.input?.nativeElement; + controlElement?.setAttribute('aria-describedby', ids.join(' ')); + } + + onContainerClick(event: MouseEvent) { + this.input.nativeElement.focus(); + } + + get empty(): boolean { + return this.inputValue === null; + } + + get errorState(): boolean { + return this.touched && this.required && this.inputValue === null; + } + + + + + // ================================================== + // => Everything related to custom form field control + // ================================================== + + @ViewChild('input') input!: ElementRef; + + isSpecialChar: boolean | null | undefined = undefined; + lastValue: string | null | undefined = undefined; + min: number = 0; + groupChar: string = ''; + maxFractionDigits = 2; + maxLength?: number; + + _numeral: any; + _decimal: any; + _decimalChar: string = ''; + _group: any; + _minusSign: any; + _index: number | any; + + get allowMinusSign(): boolean { + return this.min == null || this.min < 0; + } + + get formatterOptions(): FormatterOptions { + return { + useGrouping: true, + minimumFractionDigits: 2, + maximumFractionDigits: 2 + } + } + + + constructParsers() { + 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._group = this.getGroupingExpression(); + this._minusSign = this.getMinusSignExpression(); + this._decimal = this.getDecimalExpression(); + this._decimalChar = this.getDecimalChar(); + this._index = (d: any) => index.get(d); + } + + getDecimalExpression(): RegExp { + const decimalChar = this.getDecimalChar(); + return new RegExp(`[${decimalChar}]`, 'g'); + } + + getDecimalChar(): string { + const formatter = new Intl.NumberFormat(this.locale, { useGrouping: false }); + return formatter + .format(1.1) + .trim() + .replace(this._numeral, ''); + } + + getGroupingExpression(): RegExp { + const formatter = new Intl.NumberFormat(this.locale, { useGrouping: true }); + this.groupChar = formatter.format(1000000).trim().replace(this._numeral, '').charAt(0); + return new RegExp(`[${this.groupChar}]`, 'g'); + } + + getMinusSignExpression(): RegExp { + const formatter = new Intl.NumberFormat(this.locale, { useGrouping: false }); + return new RegExp(`[${formatter.format(-1).trim().replace(this._numeral, '')}]`, 'g'); + } + + isMinusSign(char: string) { + if (this._minusSign.test(char) || char === '-') { + this._minusSign.lastIndex = 0; + return true; + } + + return false; + } + + isDecimalSign(char: string) { + if (this._decimal.test(char)) { + this._decimal.lastIndex = 0; + return true; + } + + return false; + } + + parseValue(text: any) { + let filteredText = text + .trim() + .replace(/\s/g, '') + .replace(this._group, '') + .replace(this._minusSign, '-') + .replace(this._decimal, '.') + .replace(this._numeral, this._index); + + if (filteredText) { + if (filteredText === '-') + // Minus sign + return filteredText; + + let parsedValue = +filteredText; + return isNaN(parsedValue) ? null : parsedValue; + } + + return null; + } + + getCharIndexes(val: string) { + const decimalCharIndex = val.search(this._decimal); + this._decimal.lastIndex = 0; + const minusCharIndex = val.search(this._minusSign); + this._minusSign.lastIndex = 0; + + return { decimalCharIndex, minusCharIndex }; + } + + + + onUserInput(event: Event) { + if (this.disabled) { + return; + } + + if (this.isSpecialChar) { + (event.target as HTMLInputElement).value = this.lastValue as string; + } + this.isSpecialChar = false; + } + + onInputKeyDown(event: KeyboardEvent) { + if (this.disabled) { + return; + } + + this.lastValue = (event.target as HTMLInputElement).value; + if ((event as KeyboardEvent).shiftKey || (event as KeyboardEvent).altKey || event.metaKey || event.ctrlKey) { + this.isSpecialChar = true; + return; + } + + let selectionStart = (event.target as HTMLInputElement).selectionStart as number; + let selectionEnd = (event.target as HTMLInputElement).selectionEnd as number; + let inputValue = (event.target as HTMLInputElement).value as string; + let newValueStr = null; + + if (event.altKey) { + event.preventDefault(); + } + + switch (event.key) { + case 'ArrowUp': + event.preventDefault(); + break; + + case 'ArrowDown': + event.preventDefault(); + break; + + case 'ArrowLeft': + for (let index = selectionStart; index <= inputValue.length; index++) { + const previousCharIndex = index === 0 ? 0 : index - 1; + if (this.isNumeralChar(inputValue.charAt(previousCharIndex))) { + this.input.nativeElement.setSelectionRange(index, index); + break; + } + } + break; + + case 'ArrowRight': + for (let index = selectionEnd; index >= 0; index--) { + if (this.isNumeralChar(inputValue.charAt(index))) { + this.input.nativeElement.setSelectionRange(index, index); + break; + } + } + break; + + case 'Tab': + case 'Enter': + newValueStr = this.validateValue(this.parseValue(this.input.nativeElement.value)); + this.input.nativeElement.value = this.formatValue(newValueStr); + this.input.nativeElement.setAttribute('aria-valuenow', newValueStr?.toString() ?? ''); + this.updateModel(event, newValueStr); + break; + + case 'Backspace': { + event.preventDefault(); + + if (selectionStart === selectionEnd) { + // if ((selectionStart == 1 && this.prefix) || (selectionStart == inputValue.length && this.suffix)) { + // break; + // } + + const deleteChar = inputValue.charAt(selectionStart - 1); + const { decimalCharIndex, decimalCharIndexWithoutPrefix } = this.getDecimalCharIndexes(inputValue); + + if (this.isNumeralChar(deleteChar)) { + const decimalLength = this.getDecimalLength(inputValue); + + 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; + + if (decimalLength) { + this.input?.nativeElement.setSelectionRange(selectionStart - 1, selectionStart - 1); + } else { + newValueStr = inputValue.slice(0, selectionStart - 1) + inputValue.slice(selectionStart); + } + } else if (decimalCharIndex > 0 && selectionStart > decimalCharIndex) { + const insertedText = this.isDecimalMode() && (this.formatterOptions.minimumFractionDigits || 0) < decimalLength ? '' : '0'; + newValueStr = inputValue.slice(0, selectionStart - 1) + insertedText + inputValue.slice(selectionStart); + } else if (decimalCharIndexWithoutPrefix === 1) { + newValueStr = inputValue.slice(0, selectionStart - 1) + '0' + inputValue.slice(selectionStart); + newValueStr = (this.parseValue(newValueStr) as number) > 0 ? newValueStr : ''; + } else { + newValueStr = inputValue.slice(0, selectionStart - 1) + inputValue.slice(selectionStart); + } + } + + this.updateValue(event, newValueStr, null, 'delete-single'); + } else { + newValueStr = this.deleteRange(inputValue, selectionStart, selectionEnd); + this.updateValue(event, newValueStr, null, 'delete-range'); + } + + break; + } + + case 'Delete': + event.preventDefault(); + + if (selectionStart === selectionEnd) { + // if ((selectionStart == 0 && this.prefix) || (selectionStart == inputValue.length - 1 && this.suffix)) { + // break; + // } + const deleteChar = inputValue.charAt(selectionStart); + const { decimalCharIndex, decimalCharIndexWithoutPrefix } = this.getDecimalCharIndexes(inputValue); + + if (this.isNumeralChar(deleteChar)) { + const decimalLength = this.getDecimalLength(inputValue); + + if (this._group.test(deleteChar)) { + this._group.lastIndex = 0; + newValueStr = inputValue.slice(0, selectionStart) + inputValue.slice(selectionStart + 2); + } else if (this._decimal.test(deleteChar)) { + this._decimal.lastIndex = 0; + + if (decimalLength) { + this.input?.nativeElement.setSelectionRange(selectionStart + 1, selectionStart + 1); + } else { + newValueStr = inputValue.slice(0, selectionStart) + inputValue.slice(selectionStart + 1); + } + } else if (decimalCharIndex > 0 && selectionStart > decimalCharIndex) { + const insertedText = this.isDecimalMode() && (this.formatterOptions.minimumFractionDigits || 0) < decimalLength ? '' : '0'; + newValueStr = inputValue.slice(0, selectionStart) + insertedText + inputValue.slice(selectionStart + 1); + } else if (decimalCharIndexWithoutPrefix === 1) { + newValueStr = inputValue.slice(0, selectionStart) + '0' + inputValue.slice(selectionStart + 1); + newValueStr = (this.parseValue(newValueStr) as number) > 0 ? newValueStr : ''; + } else { + newValueStr = inputValue.slice(0, selectionStart) + inputValue.slice(selectionStart + 1); + } + } + + this.updateValue(event, newValueStr as string, null, 'delete-back-single'); + } else { + newValueStr = this.deleteRange(inputValue, selectionStart, selectionEnd); + this.updateValue(event, newValueStr, null, 'delete-range'); + } + break; + + case 'Home': + if (this.min) { + this.updateModel(event, this.min); + event.preventDefault(); + } + break; + + case 'End': + // if (this.max) { + // this.updateModel(event, this.max); + // event.preventDefault(); + // } + break; + + default: + break; + } + + // this.onKeyDown.emit(event); + } + + + onInputKeyPress(event: KeyboardEvent) { + if (this.disabled) { + return; + } + + let code = event.which || event.keyCode; + let char = String.fromCharCode(code); + let isDecimalSign = this.isDecimalSign(char); + const isMinusSign = this.isMinusSign(char); + + if (code != 13) { + event.preventDefault(); + } + if (!isDecimalSign && event.code === 'NumpadDecimal') { + isDecimalSign = true; + char = this._decimalChar; + code = char.charCodeAt(0); + } + const { value, selectionStart, selectionEnd } = this.input.nativeElement; + const newValue = this.parseValue(value + char); + const newValueStr = newValue != null ? newValue.toString() : ''; + const selectedValue = value.substring(selectionStart ?? 0, selectionEnd ?? 0); + const selectedValueParsed = this.parseValue(selectedValue); + const selectedValueStr = selectedValueParsed != null ? selectedValueParsed.toString() : ''; + + if (selectionStart !== selectionEnd && selectedValueStr.length > 0) { + this.insert(event, char, { isDecimalSign, isMinusSign }); + return; + } + + if (this.maxLength && newValueStr.length > this.maxLength) { + return; + } + + if ((48 <= code && code <= 57) || isMinusSign || isDecimalSign) { + this.insert(event, char, { isDecimalSign, isMinusSign }); + } + } + + insert(event: Event, text: string, sign = { isDecimalSign: false, isMinusSign: false }) { + const minusCharIndexOnText = text.search(this._minusSign); + this._minusSign.lastIndex = 0; + if (!this.allowMinusSign && minusCharIndexOnText !== -1) { + return; + } + + let selectionStart = this.input?.nativeElement.selectionStart ?? 0; + let selectionEnd = this.input?.nativeElement.selectionEnd ?? 0; + let inputValue = this.input?.nativeElement.value.trim(); + const { decimalCharIndex, minusCharIndex } = this.getCharIndexes(inputValue); + let newValueStr; + + if (sign.isMinusSign) { + if (selectionStart === 0) { + newValueStr = inputValue; + if (minusCharIndex === -1 || selectionEnd !== 0) { + newValueStr = this.insertText(inputValue, text, 0, selectionEnd); + } + + this.updateValue(event, newValueStr, text, 'insert'); + } + } else if (sign.isDecimalSign) { + if (decimalCharIndex > 0 && selectionStart === decimalCharIndex) { + this.updateValue(event, inputValue, text, 'insert'); + } else if (decimalCharIndex > selectionStart && decimalCharIndex < selectionEnd) { + newValueStr = this.insertText(inputValue, text, selectionStart, selectionEnd); + this.updateValue(event, newValueStr, text, 'insert'); + } else if (decimalCharIndex === -1 && this.maxFractionDigits) { + newValueStr = this.insertText(inputValue, text, selectionStart, selectionEnd); + this.updateValue(event, newValueStr, text, 'insert'); + } + } else { + const maxFractionDigits = this.formatterOptions.maximumFractionDigits; // this.numberFormat.resolvedOptions().maximumFractionDigits; + const operation = selectionStart !== selectionEnd ? 'range-insert' : 'insert'; + + if (decimalCharIndex > 0 && selectionStart > decimalCharIndex) { + if (selectionStart + text.length - (decimalCharIndex + 1) <= maxFractionDigits) { + // const charIndex = currencyCharIndex >= selectionStart ? currencyCharIndex - 1 : suffixCharIndex >= selectionStart ? suffixCharIndex : inputValue.length; + const charIndex = inputValue.length; + newValueStr = inputValue.slice(0, selectionStart) + text + inputValue.slice(selectionStart + text.length, charIndex) + inputValue.slice(charIndex); + this.updateValue(event, newValueStr, text, operation); + } + } else { + newValueStr = this.insertText(inputValue, text, selectionStart, selectionEnd); + this.updateValue(event, newValueStr, text, operation); + } + } + } + + + insertText(value: string, text: string, start: number, end: number): string { + let textSplit = text === '.' ? text : text.split('.'); + + if (textSplit.length === 2) { + const decimalCharIndex = value.slice(start, end).search(this._decimal); + this._decimal.lastIndex = 0; + return decimalCharIndex > 0 ? value.slice(0, start) + this.formatValue(text) + value.slice(end) : value || this.formatValue(text); + } else if (end - start === value.length) { + return this.formatValue(text); + } else if (start === 0) { + return text + value.slice(end); + } else if (end === value.length) { + return value.slice(0, start) + text; + } else { + return value.slice(0, start) + text + value.slice(end); + } + } + + deleteRange(value: string, start: number, end: number) { + 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; + } + + updateValue(event: Event, valueStr?: null | string, insertedValueStr?: null | string, operation?: null | string) { + let currentValue = this.input?.nativeElement.value; + let newValue = null; + + if (valueStr != null) { + newValue = this.parseValue(valueStr); + // newValue = !newValue && !this.allowEmpty ? 0 : newValue; + this.updateInput(newValue, insertedValueStr, operation, valueStr); + + this.handleOnInput(event, currentValue, newValue); + } + } + + updateInput(value: any, insertedValueStr?: | null | string, operation?: | null | string, valueStr?: | null | string) { + insertedValueStr = insertedValueStr || ''; + + let inputValue = this.input?.nativeElement.value; + let newValue = this.formatValue(value); + let currentLength = inputValue.length; + + if (newValue !== valueStr) { + newValue = this.concatValues(newValue, valueStr as string); + } + + if (currentLength === 0) { + this.input.nativeElement.value = newValue; + this.input.nativeElement.setSelectionRange(0, 0); + const index = this.initCursor(); + const selectionEnd = index + insertedValueStr.length; + this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } else { + let selectionStart = this.input.nativeElement.selectionStart ?? 0; + let selectionEnd = this.input.nativeElement.selectionEnd ?? 0; + + if (this.maxLength && newValue.length > this.maxLength) { + newValue = newValue.slice(0, this.maxLength); + selectionStart = Math.min(selectionStart, this.maxLength); + selectionEnd = Math.min(selectionEnd, this.maxLength); + } + + if (this.maxLength && this.maxLength < newValue.length) { + return; + } + + this.input.nativeElement.value = newValue; + let newLength = newValue.length; + + if (operation === 'range-insert') { + const startValue = this.parseValue((inputValue || '').slice(0, selectionStart)); + const startValueStr = startValue !== null ? startValue.toString() : ''; + const startExpr = startValueStr.split('').join(`(${this.groupChar})?`); + const sRegex = new RegExp(startExpr, 'g'); + sRegex.test(newValue); + + const tExpr = insertedValueStr.split('').join(`(${this.groupChar})?`); + const tRegex = new RegExp(tExpr, 'g'); + tRegex.test(newValue.slice(sRegex.lastIndex)); + + selectionEnd = sRegex.lastIndex + tRegex.lastIndex; + this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } else if (newLength === currentLength) { + if (operation === 'insert' || operation === 'delete-back-single') 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' || operation === 'spin') this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } else if (operation === 'delete-back-single') { + let prevChar = inputValue.charAt(selectionEnd - 1); + let nextChar = inputValue.charAt(selectionEnd); + let diff = currentLength - newLength; + let isGroupChar = this._group.test(nextChar); + + if (isGroupChar && diff === 1) { + selectionEnd += 1; + } else if (!isGroupChar && this.isNumeralChar(prevChar)) { + selectionEnd += -1 * diff + 1; + } + + this._group.lastIndex = 0; + this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } else if (inputValue === '-' && operation === 'insert') { + this.input.nativeElement.setSelectionRange(0, 0); + const index = this.initCursor(); + const selectionEnd = index + insertedValueStr.length + 1; + this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } else { + selectionEnd = selectionEnd + (newLength - currentLength); + this.input.nativeElement.setSelectionRange(selectionEnd, selectionEnd); + } + } + + this.input.nativeElement.setAttribute('aria-valuenow', value); + } + + initCursor() { + let selectionStart = this.input?.nativeElement.selectionStart ?? 0; + let selectionEnd = this.input?.nativeElement.selectionEnd ?? 0; + let inputValue = this.input?.nativeElement.value; + let valueLength = inputValue.length; + let index = null; + + // remove prefix + let prefixLength = 0; + // let prefixLength = (this.prefixChar || '').length; + // inputValue = inputValue.replace(this._prefix, ''); + + // Will allow selecting whole prefix. But not a part of it. + // Negative values will trigger clauses after this to fix the cursor position. + if (selectionStart === selectionEnd || selectionStart !== 0 || selectionEnd < prefixLength) { + selectionStart -= prefixLength; + } + + let char = inputValue.charAt(selectionStart); + if (this.isNumeralChar(char)) { + return selectionStart + prefixLength; + } + + //left + let i = selectionStart - 1; + while (i >= 0) { + char = inputValue.charAt(i); + if (this.isNumeralChar(char)) { + index = i + prefixLength; + break; + } else { + i--; + } + } + + if (index !== null) { + this.input?.nativeElement.setSelectionRange(index + 1, index + 1); + } else { + i = selectionStart; + while (i < valueLength) { + char = inputValue.charAt(i); + if (this.isNumeralChar(char)) { + index = i + prefixLength; + break; + } else { + i++; + } + } + + if (index !== null) { + this.input?.nativeElement.setSelectionRange(index, index); + } + } + + return index || 0; + } + + handleOnInput(event: Event, currentValue: string, newValue: any) { + if (this.isValueChanged(currentValue, newValue)) { + (this.input as ElementRef).nativeElement.value = this.formatValue(newValue); + this.input?.nativeElement.setAttribute('aria-valuenow', newValue); + this.updateModel(event, newValue); + // this.onInput.emit({ originalEvent: event, value: newValue, formattedValue: currentValue }); + } + } + + concatValues(val1: string, val2: string) { + if (val1 && val2) { + let decimalCharIndex = val2.search(this._decimal); + this._decimal.lastIndex = 0; + + return decimalCharIndex !== -1 ? val1.split(this._decimal)[0] + val2.slice(decimalCharIndex) : val1; + + } + return val1; + } + + getDecimalLength(value: string) { + if (value) { + const valueSplit = value.split(this._decimal); + + if (valueSplit.length === 2) { + return valueSplit[1] + .trim() + .replace(/\s/g, '') + } + } + + return 0; + } + + onPaste(event: ClipboardEvent) { + if (!this.disabled && !this.disabled) { + event.preventDefault(); + let data = (event.clipboardData || (document as any).defaultView['clipboardData']).getData('Text'); + if (data) { + if (this.maxLength) { + data = data.toString().substring(0, this.maxLength); + } + + let filteredData = this.parseValue(data); + if (filteredData != null) { + this.insert(event, filteredData.toString()); + } + } + } + } + + isValueChanged(currentValue: string, newValue: string) { + if (newValue === null && currentValue !== null) { + return true; + } + + if (newValue != null) { + let parsedCurrentValue = typeof currentValue === 'string' ? this.parseValue(currentValue) : currentValue; + return newValue !== parsedCurrentValue; + } + + return false; + } + + isNumeralChar(char: string) { + 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; + } + + isDecimalMode(): boolean { + return true; + } + + getDecimalCharIndexes(val: string) { + let decimalCharIndex = val.search(this._decimal); + this._decimal.lastIndex = 0; + + const filteredVal = val + .trim() + .replace(/\s/g, '') + const decimalCharIndexWithoutPrefix = filteredVal.search(this._decimal); + this._decimal.lastIndex = 0; + + return { decimalCharIndex, decimalCharIndexWithoutPrefix }; + } + + validateValue(value: number | string) { + if (value === '-' || value == null) { + return null; + } + + if (this.min != null && (value as number) < this.min) { + return this.min; + } + + // if (this.max != null && (value as number) > this.max) { + // return this.max; + // } + + return value; + } + + resetRegex() { + this._numeral.lastIndex = 0; + this._decimal.lastIndex = 0; + this._group.lastIndex = 0; + this._minusSign.lastIndex = 0; + } + + formatValue(value: any) { + if (value != null) { + if (value === '-') { + // Minus sign + return value; + } + + // if (this.format) { + let formatter = new Intl.NumberFormat(this.locale, this.formatterOptions); + let formattedValue = formatter.format(value); + + return formattedValue; + // } + + return value.toString(); + } + + return ''; + } + + + formattedValue() { + const val = !this.value && !this.required ? 0 : this.value; + return this.formatValue(val); + } + + updateModel(event: Event, value: any) { + const isBlurUpdateOnMode = this.ngControl?.control?.updateOn === 'blur'; + + if (this.inputValue !== value) { + this.inputValue = value; + if (!(isBlurUpdateOnMode && this.focused)) { + // this.onModelChange(value); + this.stateChanges.next(); + } + } else if (isBlurUpdateOnMode) { + // this.onModelChange(value); + this.stateChanges.next(); + } + // this.onModelTouched(); + this.onTouched(); + } +}