@taiga-ui/kit
Version:
Taiga UI Angular main components kit
207 lines (200 loc) • 9.51 kB
JavaScript
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