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

319 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Maybe<any>>;
@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<string | number>();
@Output() blur = new EventEmitter<void>();
@ContentChild('suffixTemp', { static: true }) suffixTemp!: TemplateRef<any> | null;
@ContentChild('labelSuffix', { static: false }) labelSuffix!: TemplateRef<any> | null;
@ContentChild('labelTextView', { static: false }) labelTextView!: TemplateRef<any> | 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;
}
}