2025-12-04 21:07:18 +03:30
|
|
|
|
import { Maybe } from '@/core';
|
2026-03-14 16:26:22 +03:30
|
|
|
|
import { ToastService } from '@/core/services/toast.service';
|
2026-04-30 16:27:42 +03:30
|
|
|
|
import { UikitLabelComponent } from '@/uikit';
|
2025-12-04 21:07:18 +03:30
|
|
|
|
import { UikitFieldComponent } from '@/uikit/uikit-field.component';
|
|
|
|
|
|
import { CommonModule } from '@angular/common';
|
2026-03-29 18:07:10 +03:30
|
|
|
|
import {
|
|
|
|
|
|
Component,
|
|
|
|
|
|
computed,
|
|
|
|
|
|
ContentChild,
|
|
|
|
|
|
EventEmitter,
|
|
|
|
|
|
inject,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Output,
|
|
|
|
|
|
TemplateRef,
|
|
|
|
|
|
} from '@angular/core';
|
2025-12-04 21:07:18 +03:30
|
|
|
|
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
2025-12-27 20:35:02 +03:30
|
|
|
|
import { InputGroupModule } from 'primeng/inputgroup';
|
|
|
|
|
|
import { InputGroupAddon } from 'primeng/inputgroupaddon';
|
2026-03-14 16:26:22 +03:30
|
|
|
|
import { InputMaskModule } from 'primeng/inputmask';
|
2026-03-29 18:07:10 +03:30
|
|
|
|
import { InputNumberInputEvent, InputNumberModule } from 'primeng/inputnumber';
|
2025-12-04 21:07:18 +03:30
|
|
|
|
import { InputTextModule } from 'primeng/inputtext';
|
2026-03-14 16:26:22 +03:30
|
|
|
|
import { KeyFilterModule } from 'primeng/keyfilter';
|
2025-12-04 23:34:00 +03:30
|
|
|
|
import { ToggleSwitch } from 'primeng/toggleswitch';
|
2025-12-04 21:07:18 +03:30
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
|
selector: 'app-input',
|
|
|
|
|
|
standalone: true,
|
2025-12-27 20:35:02 +03:30
|
|
|
|
imports: [
|
|
|
|
|
|
CommonModule,
|
|
|
|
|
|
ReactiveFormsModule,
|
|
|
|
|
|
InputTextModule,
|
|
|
|
|
|
InputGroupModule,
|
|
|
|
|
|
UikitFieldComponent,
|
|
|
|
|
|
ToggleSwitch,
|
|
|
|
|
|
InputGroupAddon,
|
|
|
|
|
|
InputNumberModule,
|
2026-03-14 16:26:22 +03:30
|
|
|
|
KeyFilterModule,
|
|
|
|
|
|
InputMaskModule,
|
2026-04-30 16:27:42 +03:30
|
|
|
|
UikitLabelComponent,
|
2025-12-27 20:35:02 +03:30
|
|
|
|
],
|
2025-12-04 21:07:18 +03:30
|
|
|
|
templateUrl: './input.component.html',
|
|
|
|
|
|
})
|
|
|
|
|
|
export class InputComponent {
|
2025-12-27 20:35:02 +03:30
|
|
|
|
@Input() type:
|
|
|
|
|
|
| 'simple'
|
|
|
|
|
|
| 'postalCode'
|
2026-03-29 18:07:10 +03:30
|
|
|
|
| 'nationalId'
|
2025-12-27 20:35:02 +03:30
|
|
|
|
| 'mobile'
|
|
|
|
|
|
| 'phone'
|
|
|
|
|
|
| 'email'
|
|
|
|
|
|
| 'checkbox'
|
|
|
|
|
|
| 'switch'
|
2026-03-14 16:26:22 +03:30
|
|
|
|
| 'number'
|
2025-12-27 20:35:02 +03:30
|
|
|
|
| 'price' = 'simple';
|
2025-12-04 21:07:18 +03:30
|
|
|
|
@Input() control!: FormControl<Maybe<any>>;
|
|
|
|
|
|
@Input() name!: string;
|
|
|
|
|
|
@Input() label: string = '';
|
2026-03-14 16:26:22 +03:30
|
|
|
|
@Input() placeholder?: string;
|
2025-12-04 21:07:18 +03:30
|
|
|
|
@Input() required = false;
|
|
|
|
|
|
@Input() disabled = false;
|
|
|
|
|
|
@Input() size?: 'small' | 'large';
|
|
|
|
|
|
@Input() autocomplete?: string = 'off';
|
2026-03-14 16:26:22 +03:30
|
|
|
|
@Input() showErrors = true;
|
2025-12-27 20:35:02 +03:30
|
|
|
|
@Input() hint?: string;
|
2026-03-14 16:26:22 +03:30
|
|
|
|
@Input() isLtrInput = false;
|
|
|
|
|
|
@Input() suffix: string = '';
|
|
|
|
|
|
@Input() maxLength?: number;
|
|
|
|
|
|
@Input() min?: number;
|
|
|
|
|
|
@Input() max?: number;
|
2026-05-06 22:01:20 +03:30
|
|
|
|
@Input() fixed?: number;
|
2026-05-10 09:44:30 +03:30
|
|
|
|
@Input() numericValue?: number;
|
2025-12-27 20:35:02 +03:30
|
|
|
|
@Output() valueChange = new EventEmitter<string | number>();
|
2026-01-04 13:45:45 +03:30
|
|
|
|
@Output() blur = new EventEmitter<void>();
|
2025-12-04 21:07:18 +03:30
|
|
|
|
|
2026-03-29 18:07:10 +03:30
|
|
|
|
@ContentChild('suffixTemp', { static: true }) suffixTemp!: TemplateRef<any> | null;
|
2026-04-30 16:27:42 +03:30
|
|
|
|
@ContentChild('labelSuffix', { static: false }) labelSuffix!: TemplateRef<any> | null;
|
|
|
|
|
|
@ContentChild('labelTextView', { static: false }) labelTextView!: TemplateRef<any> | null;
|
2026-03-29 18:07:10 +03:30
|
|
|
|
|
2026-03-14 16:26:22 +03:30
|
|
|
|
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);
|
|
|
|
|
|
// }
|
2025-12-04 21:07:18 +03:30
|
|
|
|
|
2026-03-14 16:26:22 +03:30
|
|
|
|
preparedSuffix = computed(() => {
|
|
|
|
|
|
if (this.type === 'price') {
|
|
|
|
|
|
return 'ریال';
|
|
|
|
|
|
} else return this.suffix;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
get preparedPlaceholder(): string | null {
|
|
|
|
|
|
if (this.placeholder) return this.placeholder;
|
2025-12-04 21:07:18 +03:30
|
|
|
|
switch (this.type) {
|
|
|
|
|
|
case 'mobile':
|
|
|
|
|
|
return '09xxxxxxxx';
|
|
|
|
|
|
case 'postalCode':
|
|
|
|
|
|
return 'کد پستی (۱۰ رقم)';
|
2026-03-29 18:07:10 +03:30
|
|
|
|
case 'nationalId':
|
|
|
|
|
|
return 'کد ملی (۱۰ رقم)';
|
2025-12-04 21:07:18 +03:30
|
|
|
|
case 'phone':
|
|
|
|
|
|
return 'شماره تلفن';
|
|
|
|
|
|
case 'email':
|
|
|
|
|
|
return 'آدرس ایمیل خود را وارد کنید';
|
2025-12-27 20:35:02 +03:30
|
|
|
|
case 'price':
|
|
|
|
|
|
return 'مبلغ';
|
2025-12-04 21:07:18 +03:30
|
|
|
|
default:
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get inputMode(): string | null {
|
|
|
|
|
|
switch (this.type) {
|
2026-05-06 22:01:20 +03:30
|
|
|
|
case 'price':
|
|
|
|
|
|
return 'decimal';
|
|
|
|
|
|
case 'number':
|
2025-12-04 21:07:18 +03:30
|
|
|
|
case 'mobile':
|
|
|
|
|
|
case 'phone':
|
|
|
|
|
|
case 'postalCode':
|
2026-03-29 18:07:10 +03:30
|
|
|
|
case 'nationalId':
|
2025-12-04 21:07:18 +03:30
|
|
|
|
return 'numeric';
|
|
|
|
|
|
default:
|
|
|
|
|
|
return 'text';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-14 16:26:22 +03:30
|
|
|
|
get isNumeric(): boolean {
|
|
|
|
|
|
return this.inputMode === 'numeric';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get preparedMaxLength(): number | null {
|
2025-12-04 21:07:18 +03:30
|
|
|
|
switch (this.type) {
|
|
|
|
|
|
case 'mobile':
|
|
|
|
|
|
return 11;
|
|
|
|
|
|
case 'postalCode':
|
2026-03-29 18:07:10 +03:30
|
|
|
|
case 'nationalId':
|
2025-12-04 21:07:18 +03:30
|
|
|
|
return 10;
|
|
|
|
|
|
case 'phone':
|
|
|
|
|
|
return 11;
|
|
|
|
|
|
default:
|
2026-03-14 16:26:22 +03:30
|
|
|
|
return this.maxLength || null;
|
2025-12-04 21:07:18 +03:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get inputClass(): string {
|
2026-03-14 16:26:22 +03:30
|
|
|
|
let inputClass = [];
|
2025-12-04 21:07:18 +03:30
|
|
|
|
switch (this.type) {
|
|
|
|
|
|
case 'mobile':
|
|
|
|
|
|
case 'phone':
|
2025-12-27 20:35:02 +03:30
|
|
|
|
case 'price':
|
2026-03-14 16:26:22 +03:30
|
|
|
|
case 'number':
|
|
|
|
|
|
inputClass.push('ltrInput');
|
|
|
|
|
|
break;
|
2025-12-04 21:07:18 +03:30
|
|
|
|
case 'email':
|
|
|
|
|
|
case 'postalCode':
|
2026-03-29 18:07:10 +03:30
|
|
|
|
case 'nationalId':
|
2026-03-14 16:26:22 +03:30
|
|
|
|
inputClass.push('ltrInput', 'rtlPlaceholder');
|
|
|
|
|
|
break;
|
2025-12-04 21:07:18 +03:30
|
|
|
|
}
|
2026-03-14 16:26:22 +03:30
|
|
|
|
|
|
|
|
|
|
if (this.isLtrInput) {
|
|
|
|
|
|
inputClass.push('ltrInput', 'rtlPlaceholder');
|
|
|
|
|
|
}
|
|
|
|
|
|
return inputClass.join(' ');
|
2025-12-04 21:07:18 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get htmlType(): string {
|
|
|
|
|
|
switch (this.type) {
|
|
|
|
|
|
case 'email':
|
|
|
|
|
|
return 'email';
|
2026-03-14 16:26:22 +03:30
|
|
|
|
case 'mobile':
|
|
|
|
|
|
case 'phone':
|
|
|
|
|
|
case 'postalCode':
|
2026-03-29 18:07:10 +03:30
|
|
|
|
case 'nationalId':
|
2026-03-14 16:26:22 +03:30
|
|
|
|
return 'number';
|
2026-05-06 22:01:20 +03:30
|
|
|
|
case 'price':
|
|
|
|
|
|
case 'number':
|
|
|
|
|
|
return 'text';
|
2026-03-14 16:26:22 +03:30
|
|
|
|
case 'checkbox':
|
|
|
|
|
|
return 'checkbox';
|
|
|
|
|
|
case 'switch':
|
|
|
|
|
|
return 'radio';
|
2025-12-04 21:07:18 +03:30
|
|
|
|
default:
|
|
|
|
|
|
return 'text';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
get isRequired(): boolean {
|
|
|
|
|
|
return this.required || this.control.hasValidator(Validators.required);
|
|
|
|
|
|
}
|
2026-03-14 16:26:22 +03:30
|
|
|
|
|
2026-03-29 18:07:10 +03:30
|
|
|
|
onPriceInput($event: InputNumberInputEvent) {
|
|
|
|
|
|
this.onInput($event.originalEvent, true);
|
|
|
|
|
|
}
|
2026-05-06 22:01:20 +03:30
|
|
|
|
|
|
|
|
|
|
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}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-29 18:07:10 +03:30
|
|
|
|
onInput($event: Event, isPriceFormat?: boolean) {
|
2026-05-06 22:01:20 +03:30
|
|
|
|
const target = $event.target as HTMLInputElement | null;
|
|
|
|
|
|
if (!target) return;
|
2026-03-29 18:07:10 +03:30
|
|
|
|
|
2026-05-06 22:01:20 +03:30
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.preparedMaxLength && value.length > this.preparedMaxLength) {
|
|
|
|
|
|
value = value.slice(0, this.preparedMaxLength);
|
2026-03-29 18:07:10 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 22:01:20 +03:30
|
|
|
|
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} باشد.`,
|
|
|
|
|
|
});
|
2026-03-14 16:26:22 +03:30
|
|
|
|
}
|
2026-05-06 22:01:20 +03:30
|
|
|
|
if (minValidator) {
|
|
|
|
|
|
this.toastService.warn({
|
|
|
|
|
|
text: `مقدار ${this.label} نمیتواند کمتر از ${this.min} باشد.`,
|
|
|
|
|
|
});
|
2026-04-18 12:14:39 +03:30
|
|
|
|
}
|
2026-05-06 22:01:20 +03:30
|
|
|
|
value = value.slice(0, -1);
|
|
|
|
|
|
numericValue = Number.isNaN(parseFloat(value)) ? 0 : parseFloat(value);
|
2026-03-14 16:26:22 +03:30
|
|
|
|
}
|
2026-04-18 12:14:39 +03:30
|
|
|
|
|
2026-05-10 09:44:30 +03:30
|
|
|
|
const isIdentifierField = !this.numericValue;
|
2026-05-06 22:01:20 +03:30
|
|
|
|
this.control.setValue(isIdentifierField ? value : value === '' ? '' : numericValue);
|
|
|
|
|
|
|
|
|
|
|
|
const viewValue = isPriceFormat && value !== '' ? numericValue.toLocaleString('en-US') : value;
|
2026-04-30 16:27:42 +03:30
|
|
|
|
|
2026-05-06 22:01:20 +03:30
|
|
|
|
target.value = viewValue;
|
|
|
|
|
|
if (
|
|
|
|
|
|
(this.inputMode === 'numeric' || this.inputMode === 'decimal') &&
|
|
|
|
|
|
this.valueChange.observed
|
|
|
|
|
|
) {
|
|
|
|
|
|
this.valueChange.emit(isIdentifierField ? value : numericValue);
|
2026-04-18 12:14:39 +03:30
|
|
|
|
}
|
2026-03-14 16:26:22 +03:30
|
|
|
|
}
|
2025-12-04 21:07:18 +03:30
|
|
|
|
}
|