UNPKG

@taiga-ui/kit

Version:

Taiga UI Angular main components kit

201 lines (193 loc) 9.46 kB
import { TuiLabel } from '@taiga-ui/core/components/label'; import { TuiTextfieldComponent, TuiTextfieldOptionsDirective } from '@taiga-ui/core/components/textfield'; import { TuiDropdownContent } from '@taiga-ui/core/portals/dropdown'; import * as i0 from '@angular/core'; import { inject, computed, effect, untracked, input, Directive } from '@angular/core'; import * as i2 from '@maskito/angular'; import { MaskitoDirective } from '@maskito/angular'; import { maskitoTransform, MASKITO_DEFAULT_OPTIONS } from '@maskito/core'; import { maskitoCaretGuard, maskitoPrefixPostprocessorGenerator } from '@maskito/kit'; import { TUI_IDENTITY_VALUE_TRANSFORMER, TuiControl, tuiAsControl, tuiValueTransformerFrom } from '@taiga-ui/cdk/classes'; import { tuiInjectElement } from '@taiga-ui/cdk/utils/dom'; import * as i1 from '@taiga-ui/core/components/input'; import { TuiInputDirective, TuiWithInput } from '@taiga-ui/core/components/input'; 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(String.raw `^(\+?\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(Number.parseInt(value.replaceAll(MASK_SYMBOLS, ''), 10)); } class TuiInputPhoneDirective extends TuiControl { constructor() { super(...arguments); this.input = inject(TuiInputDirective); this.host = inject(TuiTextfieldComponent); this.options = inject(TUI_INPUT_PHONE_OPTIONS); this.el = tuiInjectElement(); this.nonRemovablePrefix = computed(() => `${this.countryCode()} `); this.inputMode = computed(() => this.allowText() ? 'text' : 'numeric'); this.valueEffect = effect(() => { const value = this.value(); if (value) { this.input.value.set(maskitoTransform(value ?? '', this.maskito())); } }); this.blurEffect = effect(() => { const incomplete = untracked(() => !this.value()); const prefix = incomplete && this.interactive() && !this.allowText(); if (!this.host.focused() && incomplete) { this.input.value.set(''); } else if (this.host.focused() && prefix) { this.input.value.set(untracked(this.nonRemovablePrefix)); } }); this.countryCode = computed(() => extractCode(this.mask())); this.phoneMask = computed(() => extractMask(this.mask())); this.maskito = tuiMaskito(computed(() => this.calculateMask(this.countryCode(), this.phoneMask(), this.nonRemovablePrefix(), this.allowText()))); this.allowText = input(this.options.allowText); this.mask = input(this.options.mask); } onInput(value) { if (!value && !this.allowText()) { this.input.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: "19.2.21", ngImport: i0, type: TuiInputPhoneDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.21", type: TuiInputPhoneDirective, isStandalone: true, selector: "input[tuiInputPhone]", inputs: { allowText: { classPropertyName: "allowText", publicName: "allowText", isSignal: true, isRequired: false, transformFunction: null }, mask: { classPropertyName: "mask", publicName: "mask", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "tel" }, listeners: { "input": "onInput($event.target.value)" }, properties: { "disabled": "disabled()", "inputMode": "inputMode()" } }, providers: [ tuiAsControl(TuiInputPhoneDirective), tuiValueTransformerFrom(TUI_INPUT_PHONE_OPTIONS), ], usesInheritance: true, hostDirectives: [{ directive: i1.TuiWithInput }, { directive: i2.MaskitoDirective }], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: TuiInputPhoneDirective, decorators: [{ type: Directive, args: [{ selector: 'input[tuiInputPhone]', providers: [ tuiAsControl(TuiInputPhoneDirective), tuiValueTransformerFrom(TUI_INPUT_PHONE_OPTIONS), ], hostDirectives: [TuiWithInput, MaskitoDirective], host: { type: 'tel', '[disabled]': 'disabled()', '[inputMode]': 'inputMode()', '(input)': 'onInput($event.target.value)', }, }] }] }); function extractCode(mask) { const match = /^(\+\d+)/.exec(mask); return match?.[1] || ''; } function extractMask(mask) { const match = /^\+\d+(\D.*)?$/.exec(mask); return match?.[1]?.trim() || ''; } const TuiInputPhone = [ TuiInputPhoneDirective, TuiLabel, TuiTextfieldComponent, TuiTextfieldOptionsDirective, TuiDropdownContent, ]; /** * Generated bundle index. Do not edit. */ export { TUI_INPUT_PHONE_DEFAULT_OPTIONS, TUI_INPUT_PHONE_OPTIONS, TuiInputPhone, TuiInputPhoneDirective, tuiInputPhoneOptionsProvider }; //# sourceMappingURL=taiga-ui-kit-components-input-phone.mjs.map