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