svelte-tel-input
Version:
svelte-tel-input
288 lines (287 loc) • 11.4 kB
JavaScript
import { AsYouType, Metadata, getCountryCallingCode, getExampleNumber } from 'libphonenumber-js/max';
import { examplePhoneNumbers } from '../assets/index.js';
export const capitalize = (str) => {
if (!str)
return '';
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
};
// Use carefully, it can be rate limited.
export const getCurrentCountry = async () => {
try {
const response = await (await fetch('https://ip2c.org/s')).text();
const result = (response || '').toString();
if (!result || result[0] !== '1') {
console.warn('Unable to fetch the country');
return;
}
return result.substring(2, 4);
}
catch (error) {
console.warn('Unable to fetch the country');
return;
}
};
export const isNumber = (value) => {
return typeof value === 'number' && isFinite(value);
};
export const normalizeTelInput = (input) => {
const filteredResult = Object.fromEntries(Object.entries({
countryCode: input ? input.country : null,
isValid: input ? input.isValid() : false,
isPossible: input ? input.isPossible() : false,
phoneNumber: input ? input.number : null,
countryCallingCode: input ? input.countryCallingCode : null,
formattedNumber: input ? new AsYouType().input(input.number) : null,
nationalNumber: input ? input.nationalNumber : null,
formatInternational: input ? new AsYouType().input(input.number) : null,
formatOriginal: input
? new AsYouType()
.input(input.number)
.slice(input.countryCallingCode.length + 1)
.trim()
: null,
formatNational: input ? new AsYouType(input.country).input(input.number) : null,
uri: input ? input.getURI() : null,
e164: input ? input.number : null
}).filter(([, value]) => value !== null));
return filteredResult;
};
export const generatePlaceholder = (country, { format, spaces } = {
format: 'national',
spaces: true
}) => {
const examplePhoneNumber = getExampleNumber(country, examplePhoneNumbers);
if (examplePhoneNumber) {
switch (format) {
case 'international':
return spaces
? examplePhoneNumber.formatInternational()
: examplePhoneNumber.number;
default:
return spaces
? examplePhoneNumber
.formatInternational()
.slice(examplePhoneNumber.countryCallingCode.length + 1)
.trim()
: examplePhoneNumber.nationalNumber;
}
}
else {
throw new Error(`No country found with this country code: ${country}`);
}
};
export const isSelected = (itemToSelect, selectedItem) => {
if (!selectedItem) {
return false;
}
if (typeof selectedItem === 'object' && typeof itemToSelect === 'object') {
return selectedItem.id === itemToSelect.id;
}
return itemToSelect === selectedItem;
};
export const getInternationalPhoneNumberPrefix = (country) => {
const ONLY_DIGITS_REGEXP = /^\d+$/;
// Standard international phone number prefix: "+" and "country calling code".
let prefix = '+' + getCountryCallingCode(country);
// Get "leading digits" for a phone number of the country.
// If there're "leading digits" then they can be part of the prefix too.
const newMetadata = new Metadata();
const leadingDigits = newMetadata.numberingPlan?.leadingDigits();
if (leadingDigits && ONLY_DIGITS_REGEXP.test(leadingDigits)) {
prefix += leadingDigits;
}
return prefix;
};
/**
* Trims phone number digits if they exceed the maximum possible length
* for a national (significant) number for the country.
* @param {string} number - A possibly incomplete phone number digits string. Can be a possibly incomplete E.164 phone number.
* @param {string} country
* @return {string} Can be empty.
*/
export const trimNumber = (number, country) => {
const nationalSignificantNumberPart = getNationalSignificantNumberDigits(number, country);
if (nationalSignificantNumberPart) {
const overflowDigitsCount = nationalSignificantNumberPart.length - getMaxNumberLength(country);
if (overflowDigitsCount > 0) {
return number.slice(0, number.length - overflowDigitsCount);
}
}
return number;
};
export const getMaxNumberLength = (country) => {
// Get "possible lengths" for a phone number of the country.
const newMetadata = new Metadata();
newMetadata.selectNumberingPlan(country);
// Return the last "possible length".
if (newMetadata.numberingPlan) {
return newMetadata.numberingPlan.possibleLengths()[newMetadata.numberingPlan.possibleLengths().length - 1];
}
else {
throw new Error('There is no metadata object.');
}
};
/**
* If the phone number being input is an international one
* then tries to derive the country from the phone number.
* (regardless of whether there's any country currently selected)
* @param {string} partialE164Number - A possibly incomplete E.164 phone number.
* @param {string?} country - Currently selected country.
* @param {string[]?} countries - A list of available countries. If not passed then "all countries" are assumed.
* @return {string?}
*/
export const getCountryForPartialE164Number = (partialE164Number, { country, countries, required } = {}) => {
if (partialE164Number === '+') {
// Don't change the currently selected country yet.
return country;
}
const derived_country = getCountryFromPossiblyIncompleteInternationalPhoneNumber(partialE164Number);
// If a phone number is being input in international form
// and the country can already be derived from it,
// then select that country.
if (derived_country && (!countries || countries.indexOf(derived_country) >= 0)) {
return derived_country;
}
// If "International" country option has not been disabled
// and the international phone number entered doesn't correspond
// to the currently selected country then reset the currently selected country.
else if (country && !required && !couldNumberBelongToCountry(partialE164Number, country)) {
return undefined;
}
// Don't change the currently selected country.
return country;
};
/**
* Determines the country for a given (possibly incomplete) E.164 phone number.
* @param {string} number - A possibly incomplete E.164 phone number.
* @return {string?}
*/
export const getCountryFromPossiblyIncompleteInternationalPhoneNumber = (number) => {
const formatter = new AsYouType();
formatter.input(number);
// // `001` is a special "non-geograpical entity" code
// // in Google's `libphonenumber` library.
// if (formatter.getCountry() === '001') {
// return
// }
return formatter.getCountry();
};
/**
* Parses a partially entered national phone number digits
* (or a partially entered E.164 international phone number)
* and returns the national significant number part.
* National significant number returned doesn't come with a national prefix.
* @param {string} number - National number digits. Or possibly incomplete E.164 phone number.
* @param {string?} country
* @return {string} [result]
*/
export const getNationalSignificantNumberDigits = (number, country) => {
// Create "as you type" formatter.
const formatter = new AsYouType(country);
// Input partial national phone number.
formatter.input(number);
// Return the parsed partial national phone number.
const phoneNumber = formatter.getNumber();
return phoneNumber && phoneNumber.nationalNumber;
};
/**
* Checks if a partially entered E.164 phone number could belong to a country.
* @param {string} number
* @param {CountryCode} country
* @return {boolean}
*/
export const couldNumberBelongToCountry = (number, country) => {
const intlPhoneNumberPrefix = getInternationalPhoneNumberPrefix(country);
let i = 0;
while (i < number.length && i < intlPhoneNumberPrefix.length) {
if (number[i] !== intlPhoneNumberPrefix[i]) {
return false;
}
i++;
}
return true;
};
export const isSupportedCountry = (country, metadata) => {
return metadata.countries[country] !== undefined;
};
/**
* These mappings map a character (key) to a specific digit that should
* replace it for normalization purposes.
* @param {string} character
* @returns {string}
*/
export const allowedCharacters = (character, { spaces } = {
spaces: true
}) => {
const DIGITS = {
'0': '0',
'1': '1',
'2': '2',
'3': '3',
'4': '4',
'5': '5',
'6': '6',
'7': '7',
'8': '8',
'9': '9',
'\uFF10': '0', // Fullwidth digit 0
'\uFF11': '1', // Fullwidth digit 1
'\uFF12': '2', // Fullwidth digit 2
'\uFF13': '3', // Fullwidth digit 3
'\uFF14': '4', // Fullwidth digit 4
'\uFF15': '5', // Fullwidth digit 5
'\uFF16': '6', // Fullwidth digit 6
'\uFF17': '7', // Fullwidth digit 7
'\uFF18': '8', // Fullwidth digit 8
'\uFF19': '9', // Fullwidth digit 9
'\u0660': '0', // Arabic-indic digit 0
'\u0661': '1', // Arabic-indic digit 1
'\u0662': '2', // Arabic-indic digit 2
'\u0663': '3', // Arabic-indic digit 3
'\u0664': '4', // Arabic-indic digit 4
'\u0665': '5', // Arabic-indic digit 5
'\u0666': '6', // Arabic-indic digit 6
'\u0667': '7', // Arabic-indic digit 7
'\u0668': '8', // Arabic-indic digit 8
'\u0669': '9', // Arabic-indic digit 9
'\u06F0': '0', // Eastern-Arabic digit 0
'\u06F1': '1', // Eastern-Arabic digit 1
'\u06F2': '2', // Eastern-Arabic digit 2
'\u06F3': '3', // Eastern-Arabic digit 3
'\u06F4': '4', // Eastern-Arabic digit 4
'\u06F5': '5', // Eastern-Arabic digit 5
'\u06F6': '6', // Eastern-Arabic digit 6
'\u06F7': '7', // Eastern-Arabic digit 7
'\u06F8': '8', // Eastern-Arabic digit 8
'\u06F9': '9' // Eastern-Arabic digit 9,
};
// Allow spaces
if (spaces) {
const regex = new RegExp('[\\t\\n\\v\\f\\r \\u00a0\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u200b\\u2028\\u2029\\u3000]', 'g');
if (regex.test(character)) {
return character;
}
}
// Allow digits
return DIGITS[character];
};
export const inputParser = (text, { allowSpaces, parseCharacter, disallowPlusSign }) => {
let value = '';
for (let index = 0; index < text.length; index++) {
const character = parseCharacter(text[index], value, allowSpaces, disallowPlusSign);
if (character !== undefined) {
value += character;
}
}
return value;
};
export const inspectAllowedChars = (character, value, allowSpaces, disallowPlusSign) => {
// Leading plus is allowed
if (!disallowPlusSign && character === '+') {
if (!value) {
return character;
}
}
// Allowed characters
return allowedCharacters(character, { spaces: allowSpaces });
};