UNPKG

@taiga-ui/kit

Version:

Taiga UI Angular main components kit

207 lines (200 loc) 9.51 kB
import * as i0 from '@angular/core'; import { inject, computed, effect, untracked, signal, Directive, Input } from '@angular/core'; import * as i2 from '@maskito/angular'; import { MaskitoDirective } from '@maskito/angular'; import { maskitoTransform, MASKITO_DEFAULT_OPTIONS } from '@maskito/core'; import { maskitoPrefixPostprocessorGenerator, maskitoCaretGuard } from '@maskito/kit'; import { TUI_IDENTITY_VALUE_TRANSFORMER, TuiControl, tuiAsControl, tuiValueTransformerFrom } from '@taiga-ui/cdk/classes'; import { TUI_ALLOW_SIGNAL_WRITES } from '@taiga-ui/cdk/constants'; import { tuiInjectElement } from '@taiga-ui/cdk/utils/dom'; import * as i1 from '@taiga-ui/core/components/textfield'; import { TuiTextfieldDirective, TuiTextfieldComponent, TuiWithTextfield } from '@taiga-ui/core/components/textfield'; import { tuiMaskito } from '@taiga-ui/kit/utils'; import { tuiCreateOptions } from '@taiga-ui/cdk/utils/di'; const TUI_INPUT_PHONE_DEFAULT_OPTIONS = { mask: '+1 ### ###-####', allowText: false, valueTransformer: TUI_IDENTITY_VALUE_TRANSFORMER, }; const [TUI_INPUT_PHONE_OPTIONS, tuiInputPhoneOptionsProvider] = tuiCreateOptions(TUI_INPUT_PHONE_DEFAULT_OPTIONS); const countDigits = (value) => value.replaceAll(/\D/g, '').length; /** * `InputPhone` component sets country code as non-removable prefix. * This prefix appears on focus and cannot be erased. * But users sometimes can copy complete phone number (from any different source) * and try to insert the whole string inside our `InputPhone` textfield. * This preprocessor helps to avoid country prefix duplication on paste/drop/autofill events. */ function tuiCreateCompletePhoneInsertionPreprocessor(countryCode, phoneMaskAfterCountryCode) { const completePhoneLength = (countryCode + phoneMaskAfterCountryCode).replaceAll(/[^#\d]+/g, '').length; const trimCountryPrefix = (value) => countryCode === '+7' ? value.replace(/^(\+?\s*7?\s?8?)\s?/, '') : value.replace(new RegExp(`^(\\+?\\s*${countryCode.replace('+', '')}?)\\s?`), ''); return ({ elementState, data }) => { const { value, selection } = elementState; return { elementState: { selection, value: /** * The only possible case when `value` includes digits more * than mask expression allows – browser autofill. * It means that we are inside `input`-event * and mask are ready to reject "extra" characters. * We should cut leading country prefix to save trailing characters! */ countDigits(value) > completePhoneLength ? trimCountryPrefix(value) : value, }, data: countDigits(data) >= completePhoneLength || data.startsWith(countryCode) ? /** * User tries to insert/drop the complete phone number (with country prefix). * We should drop already existing non-removable prefix. */ trimCountryPrefix(data) : data, }; }; } /** * Create {@link https://maskito.dev/core-concepts/mask-expression pattern mask expression} for phone number * * @example * tuiCreatePhoneMaskExpression('+1', '(###) ###-####'); */ function tuiCreatePhoneMaskExpression(countryCode, phoneMaskAfterCountryCode) { return [ ...countryCode.split(''), ' ', ...phoneMaskAfterCountryCode .replaceAll(/[^#\- ()]+/g, '') .split('') .map((item) => (item === '#' ? /\d/ : item)), ]; } const MASK_SYMBOLS = /[ \-_()]/g; function isText(value) { return Number.isNaN(parseInt(value.replaceAll(MASK_SYMBOLS, ''), 10)); } class TuiInputPhone extends TuiControl { constructor() { super(...arguments); this.textfield = inject(TuiTextfieldDirective); this.host = inject(TuiTextfieldComponent); this.nonRemovablePrefix = computed(() => `${this.countryCode()} `); this.inputMode = computed(() => (this.allowText() ? 'text' : 'numeric')); this.valueEffect = effect(() => { const value = this.value() ?? ''; if (!value) { return; } this.textfield.value.set(maskitoTransform(value, this.mask())); }, TUI_ALLOW_SIGNAL_WRITES); this.blurEffect = effect(() => { const incomplete = untracked(() => !this.value()); if (!this.host.focused() && incomplete) { this.textfield.value.set(''); } if (this.host.focused() && incomplete && !this.allowText()) { this.textfield.value.set(this.nonRemovablePrefix()); } }, TUI_ALLOW_SIGNAL_WRITES); this.options = inject(TUI_INPUT_PHONE_OPTIONS); this.el = tuiInjectElement(); this.mask = computed(() => this.calculateMask(this.countryCode(), this.phoneMask(), this.nonRemovablePrefix(), this.allowText())); this.maskito = tuiMaskito(computed(() => this.mask())); this.countryCode = signal(extractCountryCode(this.options.mask)); this.allowText = signal(this.options.allowText); this.phoneMask = signal(extractMask(this.options.mask)); } // TODO(v5): replace with signal input set allowTextSetter(allow) { this.allowText.set(allow); } // TODO(v5): replace with signal input set maskSetter(mask) { this.countryCode.set(extractCountryCode(mask)); this.phoneMask.set(extractMask(mask)); } onInput(value) { if (!value && !this.allowText()) { this.textfield.value.set(this.nonRemovablePrefix()); } const parsed = isText(value) ? value : value.replaceAll(MASK_SYMBOLS, '').slice(0, this.maxPhoneLength); this.onChange(parsed === this.countryCode() || isText(parsed) ? '' : parsed); } get maxPhoneLength() { return (this.countryCode().length + this.phoneMask().replaceAll(/[^#]+/g, '').length); } calculateMask(countryCode, phoneMaskAfterCountryCode, nonRemovablePrefix, allowText) { const mask = tuiCreatePhoneMaskExpression(countryCode, phoneMaskAfterCountryCode); const preprocessors = [ tuiCreateCompletePhoneInsertionPreprocessor(countryCode, phoneMaskAfterCountryCode), ]; return allowText ? { mask: ({ value }) => isText(value) && value !== '+' ? MASKITO_DEFAULT_OPTIONS.mask : mask, preprocessors, } : { mask, preprocessors, postprocessors: [ maskitoPrefixPostprocessorGenerator(nonRemovablePrefix), ], plugins: [ maskitoCaretGuard((value, [from, to]) => [ from === to ? nonRemovablePrefix.length : 0, value.length, ]), ], }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TuiInputPhone, deps: null, target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: TuiInputPhone, isStandalone: true, selector: "input[tuiInputPhone]", inputs: { allowTextSetter: ["allowText", "allowTextSetter"], maskSetter: ["mask", "maskSetter"] }, host: { attributes: { "type": "tel" }, listeners: { "input": "onInput($event.target.value)" }, properties: { "inputMode": "inputMode()", "disabled": "disabled()" } }, providers: [ tuiAsControl(TuiInputPhone), tuiValueTransformerFrom(TUI_INPUT_PHONE_OPTIONS), ], usesInheritance: true, hostDirectives: [{ directive: i1.TuiWithTextfield }, { directive: i2.MaskitoDirective }], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TuiInputPhone, decorators: [{ type: Directive, args: [{ standalone: true, selector: 'input[tuiInputPhone]', providers: [ tuiAsControl(TuiInputPhone), tuiValueTransformerFrom(TUI_INPUT_PHONE_OPTIONS), ], hostDirectives: [TuiWithTextfield, MaskitoDirective], host: { type: 'tel', '[inputMode]': 'inputMode()', '[disabled]': 'disabled()', '(input)': 'onInput($event.target.value)', }, }] }], propDecorators: { allowTextSetter: [{ type: Input, args: ['allowText'] }], maskSetter: [{ type: Input, args: ['mask'] }] } }); function extractCountryCode(mask) { const match = /^(\+\d+)/.exec(mask); return match?.[1] || ''; } function extractMask(mask) { const match = /^\+\d+(.*)$/.exec(mask); return match?.[1]?.trim() || ''; } /** * Generated bundle index. Do not edit. */ export { TUI_INPUT_PHONE_DEFAULT_OPTIONS, TUI_INPUT_PHONE_OPTIONS, TuiInputPhone, tuiInputPhoneOptionsProvider }; //# sourceMappingURL=taiga-ui-kit-components-input-phone.mjs.map