import { Maybe } from '@/core'; import { ToastService } from '@/core/services/toast.service'; import { UikitLabelComponent } from '@/uikit'; import { UikitFieldComponent } from '@/uikit/uikit-field.component'; import { CommonModule } from '@angular/common'; import { Component, computed, ContentChild, EventEmitter, inject, Input, Output, TemplateRef, } from '@angular/core'; 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 { InputTextModule } from 'primeng/inputtext'; import { KeyFilterModule } from 'primeng/keyfilter'; import { ToggleSwitch } from 'primeng/toggleswitch'; @Component({ selector: 'app-input', standalone: true, imports: [ CommonModule, ReactiveFormsModule, InputTextModule, InputGroupModule, UikitFieldComponent, ToggleSwitch, InputGroupAddon, InputNumberModule, KeyFilterModule, InputMaskModule, UikitLabelComponent, ], templateUrl: './input.component.html', }) export class InputComponent { private lastValidNumericValue = ''; @Input() type: | 'simple' | 'postalCode' | 'nationalId' | 'mobile' | 'phone' | 'email' | 'checkbox' | 'switch' | 'number' | 'price' = 'simple'; @Input() control!: FormControl>; @Input() name!: string; @Input() label: string = ''; @Input() placeholder?: string; @Input() required = false; @Input() disabled = false; @Input() size?: 'small' | 'large'; @Input() autocomplete?: string = 'off'; @Input() showErrors = true; @Input() hint?: string; @Input() isLtrInput = false; @Input() suffix: string = ''; @Input() maxLength?: number; @Input() min?: number; @Input() max?: number; @Input() fixed?: number; @Input() numericValue?: number; @Output() valueChange = new EventEmitter(); @Output() blur = new EventEmitter(); @ContentChild('suffixTemp', { static: true }) suffixTemp!: TemplateRef | null; @ContentChild('labelSuffix', { static: false }) labelSuffix!: TemplateRef | null; @ContentChild('labelTextView', { static: false }) labelTextView!: TemplateRef | null; 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; }); get preparedPlaceholder(): string | null { if (this.placeholder) return this.placeholder; switch (this.type) { case 'mobile': return '09xxxxxxxx'; case 'postalCode': return 'کد پستی (۱۰ رقم)'; case 'nationalId': return 'کد ملی (۱۰ رقم)'; case 'phone': return 'شماره تلفن'; case 'email': return 'آدرس ایمیل خود را وارد کنید'; case 'price': return 'مبلغ'; default: return ''; } } get inputMode(): string | null { switch (this.type) { case 'price': return 'decimal'; case 'number': case 'mobile': case 'phone': case 'postalCode': case 'nationalId': return 'numeric'; default: return 'text'; } } get isNumeric(): boolean { return this.inputMode === 'numeric'; } get preparedMaxLength(): number | null { switch (this.type) { case 'mobile': return 11; case 'postalCode': case 'nationalId': return 10; case 'phone': return 11; default: return this.maxLength || null; } } get inputClass(): string { let inputClass = []; switch (this.type) { case 'mobile': case 'phone': case 'price': case 'number': inputClass.push('ltrInput'); break; case 'email': case 'postalCode': case 'nationalId': inputClass.push('ltrInput', 'rtlPlaceholder'); break; } if (this.isLtrInput) { inputClass.push('ltrInput', 'rtlPlaceholder'); } return inputClass.join(' '); } get htmlType(): string { switch (this.type) { case 'email': return 'email'; case 'mobile': case 'phone': case 'postalCode': case 'nationalId': return 'number'; case 'price': case 'number': return 'text'; case 'checkbox': return 'checkbox'; case 'switch': return 'radio'; default: return 'text'; } } get isRequired(): boolean { 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)) .replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - 1632)); } 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; const [integerPart = '', ...decimalParts] = cleaned.split('.'); const mergedDecimals = decimalParts.join(''); return decimalParts.length ? `${integerPart}.${mergedDecimals}` : integerPart; } private applyFixed(value: string): string { if (this.fixed === undefined || this.fixed === null || this.fixed < 0 || value === '') { return value; } if (this.fixed === 0) { return value.split('.')[0] || '0'; } const [integerPart = '0', decimalPart = ''] = value.split('.'); const normalizedInteger = integerPart === '' ? '0' : integerPart; const fixedDecimal = decimalPart.slice(0, this.fixed).padEnd(this.fixed, '0'); return `${normalizedInteger}.${fixedDecimal}`; } private normalizeLeadingZeros(value: string, allowDecimal: boolean): string { if (!value) return value; if (!allowDecimal) { return value.replace(/^0+(?=\d)/, ''); } if (value.includes('.')) { const [integerPart = '0', decimalPart = ''] = value.split('.'); const normalizedInteger = integerPart.replace(/^0+(?=\d)/, '') || '0'; return `${normalizedInteger}.${decimalPart}`; } return value.replace(/^0+(?=\d)/, ''); } onInput($event: Event, isPriceFormat?: boolean) { if ((this.inputMode !== 'numeric' && this.inputMode !== 'decimal') || !isPriceFormat) { return; } const target = $event.target as HTMLInputElement | null; if (!target) return; let value = target.value ?? ''; const allowDecimal = this.type === 'price' || this.type === 'number'; if (this.inputMode === 'numeric' || this.inputMode === 'decimal' || isPriceFormat) { 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); } 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} باشد.`, }); } if (minValidator) { this.toastService.warn({ text: `مقدار ${this.label} نمی‌تواند کمتر از ${this.min} باشد.`, }); } 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 initial = this.sanitizeNumericValue(`${this.control?.value ?? ''}`, allowDecimal); this.lastValidNumericValue = initial; } }