some ui fix and debug input field, separate price field

This commit is contained in:
2026-06-02 12:22:32 +03:30
parent ae963a60ce
commit 1f9166bed3
34 changed files with 335 additions and 282 deletions
@@ -17,7 +17,7 @@ import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { InputGroupModule } from 'primeng/inputgroup';
import { InputGroupAddon } from 'primeng/inputgroupaddon';
import { InputMaskModule } from 'primeng/inputmask';
import { InputNumberInputEvent, InputNumberModule } from 'primeng/inputnumber';
import { InputNumberModule } from 'primeng/inputnumber';
import { InputTextModule } from 'primeng/inputtext';
import { KeyFilterModule } from 'primeng/keyfilter';
import { ToggleSwitch } from 'primeng/toggleswitch';
@@ -51,8 +51,7 @@ export class InputComponent {
| 'email'
| 'checkbox'
| 'switch'
| 'number'
| 'price' = 'simple';
| 'number' = 'simple';
@Input() control!: FormControl<Maybe<any>>;
@Input() name!: string;
@Input() label: string = '';
@@ -80,17 +79,8 @@ export class InputComponent {
private readonly toastService = inject(ToastService);
// onInput(ev: Event) {
// const v = (ev.target as HTMLInputElement).value;
// const newLocal = v || v == '0';
// // this.valueChange.emit(this.type === 'price' && newLocal ? parseFloat(v) : v);
// }
preparedSuffix = computed(() => {
if (this.type === 'price') {
return 'ریال';
} else return this.suffix;
return this.suffix;
});
get preparedPlaceholder(): string | null {
@@ -106,30 +96,34 @@ export class InputComponent {
return 'شماره تلفن';
case 'email':
return 'آدرس ایمیل خود را وارد کنید';
case 'price':
return 'مبلغ';
default:
return '';
}
}
get inputMode(): string | null {
get inputMode(): string {
switch (this.type) {
case 'price':
return 'decimal';
case 'number':
case 'mobile':
case 'phone':
return 'decimal';
case 'postalCode':
case 'nationalId':
return 'numeric';
case 'mobile':
case 'phone':
return 'tel';
case 'email':
return 'email';
case 'checkbox':
return 'checkbox';
case 'switch':
return 'radio';
default:
return 'text';
}
}
get isNumeric(): boolean {
return this.inputMode === 'numeric';
return ['number', 'decimal'].includes(this.inputMode);
}
get preparedMaxLength(): number | null {
@@ -153,7 +147,6 @@ export class InputComponent {
switch (this.type) {
case 'mobile':
case 'phone':
case 'price':
case 'number':
inputClass.push('ltrInput');
break;
@@ -174,14 +167,12 @@ export class InputComponent {
switch (this.type) {
case 'email':
return 'email';
case 'mobile':
case 'phone':
case 'postalCode':
case 'nationalId':
return 'number';
case 'price':
// case 'mobile':
// case 'phone':
// case 'postalCode':
// case 'nationalId':
case 'number':
return 'text';
return 'number';
case 'checkbox':
return 'checkbox';
case 'switch':
@@ -195,10 +186,6 @@ export class InputComponent {
return this.required || this.control.hasValidator(Validators.required);
}
onPriceInput($event: InputNumberInputEvent) {
this.onInput($event.originalEvent, true);
}
private toEnglishDigits(value: string): string {
return value
.replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - 1776))
@@ -207,6 +194,7 @@ export class InputComponent {
private sanitizeNumericValue(value: string, allowDecimal: boolean): string {
const normalized = this.toEnglishDigits(value).replace(/,/g, '');
const cleaned = normalized.replace(allowDecimal ? /[^0-9.]/g : /[^0-9]/g, '');
if (!allowDecimal) return cleaned;
@@ -245,76 +233,82 @@ export class InputComponent {
return value.replace(/^0+(?=\d)/, '');
}
onInput($event: Event, isPriceFormat?: boolean) {
if (this.type !== 'price' && this.type !== 'number') {
return;
private enforceMaxLength(value: string): string {
if (!this.preparedMaxLength || value.length <= this.preparedMaxLength) return value;
return value.slice(0, this.preparedMaxLength);
}
private parseNumericValue(value: string): number {
return Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
}
private isOutOfRange(numericValue: number): { min: boolean; max: boolean } {
return {
min: !!((this.min || this.min === 0) && numericValue < this.min),
max: !!((this.max || this.max === 0) && numericValue > this.max),
};
}
private notifyRangeError(range: { min: boolean; max: boolean }) {
if (range.max) {
this.toastService.warn({
text: `مقدار ${this.label} نمی‌تواند بیشتر از ${this.max} باشد.`,
});
}
if (range.min) {
this.toastService.warn({
text: `مقدار ${this.label} نمی‌تواند کمتر از ${this.min} باشد.`,
});
}
}
private isIdentifierField(): boolean {
return ['mobile', 'phone', 'postalCode', 'nationalId'].includes(this.type);
}
private writeControlAndViewValue(target: HTMLInputElement, value: string, numericValue: number) {
const isIdentifierField = this.isIdentifierField();
const controlValue = isIdentifierField ? value : value === '' ? '' : numericValue;
this.control.setValue(controlValue);
target.value = value;
}
onInput($event: Event) {
const target = $event.target as HTMLInputElement | null;
if (!target) return;
let value = target.value ?? '';
const allowDecimal = this.type === 'price' || this.type === 'number';
const allowDecimal = this.type === 'number';
const isNumericMode = this.isNumeric;
if (this.inputMode === 'numeric' || this.inputMode === 'decimal' || isPriceFormat) {
if (isNumericMode) {
value = this.sanitizeNumericValue(value, allowDecimal);
value = this.applyFixed(value);
value = this.normalizeLeadingZeros(value, allowDecimal);
}
if (this.preparedMaxLength && value.length > this.preparedMaxLength) {
value = value.slice(0, this.preparedMaxLength);
}
value = this.enforceMaxLength(value);
let numericValue = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
const minValidator = (this.min || this.min === 0) && numericValue < this.min!;
const maxValidator = (this.max || this.max === 0) && numericValue > this.max!;
if (
(this.inputMode === 'numeric' || this.inputMode === 'decimal') &&
value !== '' &&
(minValidator || maxValidator)
) {
if (maxValidator) {
this.toastService.warn({
text: `مقدار ${this.label} نمی‌تواند بیشتر از ${this.max} باشد.`,
});
let numericValue = this.parseNumericValue(value);
const range = this.isOutOfRange(numericValue);
if (value !== '' && (range.min || range.max)) {
this.notifyRangeError(range);
value = this.lastValidNumericValue;
numericValue = this.parseNumericValue(value);
this.writeControlAndViewValue(target, value, numericValue);
return;
} else {
this.lastValidNumericValue = value;
}
if (minValidator) {
this.toastService.warn({
text: `مقدار ${this.label} نمی‌تواند کمتر از ${this.min} باشد.`,
});
this.writeControlAndViewValue(target, value, numericValue);
if (this.valueChange.observed) {
this.valueChange.emit(this.isIdentifierField() ? value : numericValue);
}
value = this.lastValidNumericValue;
numericValue = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
// const isIdentifierField = ['mobile', 'phone', 'postalCode', 'nationalId'].includes(this.type);
const isIdentifierField = !this.numericValue;
const restoredControlValue = isIdentifierField ? value : value === '' ? '' : numericValue;
this.control.setValue(restoredControlValue);
target.value = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value;
return;
} else if (this.inputMode === 'numeric' || this.inputMode === 'decimal') {
this.lastValidNumericValue = value;
}
const isIdentifierField = ['mobile', 'phone', 'postalCode', 'nationalId'].includes(this.type);
const controlValue = isIdentifierField ? value : value === '' ? '' : numericValue;
this.control.setValue(controlValue);
const viewValue = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value;
target.value = viewValue;
if (
(this.inputMode === 'numeric' || this.inputMode === 'decimal') &&
this.valueChange.observed
) {
this.valueChange.emit(isIdentifierField ? value : numericValue);
}
}
ngOnInit() {
const allowDecimal = this.type === 'price' || this.type === 'number';
const allowDecimal = this.type === 'number';
const initial = this.sanitizeNumericValue(`${this.control?.value ?? ''}`, allowDecimal);
this.lastValidNumericValue = initial;
}