Files
psp_panel/src/app/shared/components/input/input.component.ts
T

319 lines
9.5 KiB
TypeScript
Raw Normal View History

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';
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';
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';
import { ToggleSwitch } from 'primeng/toggleswitch';
2025-12-04 21:07:18 +03:30
@Component({
selector: 'app-input',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
InputTextModule,
InputGroupModule,
UikitFieldComponent,
ToggleSwitch,
InputGroupAddon,
InputNumberModule,
2026-03-14 16:26:22 +03:30
KeyFilterModule,
InputMaskModule,
UikitLabelComponent,
],
2025-12-04 21:07:18 +03:30
templateUrl: './input.component.html',
})
export class InputComponent {
private lastValidNumericValue = '';
@Input() type:
| 'simple'
| 'postalCode'
2026-03-29 18:07:10 +03:30
| 'nationalId'
| 'mobile'
| 'phone'
| 'email'
| 'checkbox'
| 'switch'
2026-03-14 16:26:22 +03:30
| 'number'
| '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;
@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;
@Input() fixed?: number;
2026-05-10 09:44:30 +03:30
@Input() numericValue?: number;
@Output() valueChange = new EventEmitter<string | number>();
@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;
@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 'آدرس ایمیل خود را وارد کنید';
case 'price':
return 'مبلغ';
2025-12-04 21:07:18 +03:30
default:
return '';
}
}
get inputMode(): string | null {
switch (this.type) {
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':
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';
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);
}
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)/, '');
}
2026-03-29 18:07:10 +03:30
onInput($event: Event, isPriceFormat?: boolean) {
2026-05-16 23:02:36 +03:30
if (this.type !== 'price' && this.type !== 'number') {
return;
}
const target = $event.target as HTMLInputElement | null;
if (!target) return;
2026-03-29 18:07:10 +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);
value = this.normalizeLeadingZeros(value, allowDecimal);
}
if (this.preparedMaxLength && value.length > this.preparedMaxLength) {
value = value.slice(0, this.preparedMaxLength);
2026-03-29 18:07:10 +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
}
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;
2026-03-14 16:26:22 +03:30
}
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);
}
2026-03-14 16:26:22 +03:30
}
ngOnInit() {
const allowDecimal = this.type === 'price' || this.type === 'number';
const initial = this.sanitizeNumericValue(`${this.control?.value ?? ''}`, allowDecimal);
this.lastValidNumericValue = initial;
}
2025-12-04 21:07:18 +03:30
}