react-phone-number-input
Version:
Telephone number input React component
658 lines (555 loc) • 26.4 kB
JavaScript
import { parsePhoneNumberFromString, getCountryCallingCode, AsYouType, Metadata } from 'libphonenumber-js/core';
import getInternationalPhoneNumberPrefix from './getInternationalPhoneNumberPrefix';
/**
* Decides which country should be pre-selected
* when the phone number input component is first mounted.
* @param {object?} phoneNumber - An instance of `PhoneNumber` class.
* @param {string?} country - Pre-defined country (two-letter code).
* @param {string[]?} countries - A list of countries available.
* @param {object} metadata - `libphonenumber-js` metadata
* @return {string?}
*/
export function getPreSelectedCountry(_ref) {
var phoneNumber = _ref.phoneNumber,
defaultCountry = _ref.defaultCountry,
countries = _ref.countries,
required = _ref.required,
metadata = _ref.metadata;
var country = defaultCountry; // If can get country from E.164 phone number
// then it overrides the `country` passed (or not passed).
if (phoneNumber && phoneNumber.country) {
// `country` will be left `undefined` in case of non-detection.
country = phoneNumber.country;
} // Only pre-select a country if it's in the available `countries` list.
if (countries && countries.indexOf(country) < 0) {
country = undefined;
} // If there will be no "International" option
// then some `country` must be selected.
// It will still be the wrong country though.
// But still country `<select/>` can't be left in a broken state.
if (!country && required && countries && countries.length > 0) {
country = countries[0];
}
return country;
}
/**
* Generates a sorted list of country `<select/>` options.
* @param {string[]} countries - A list of two-letter ("ISO 3166-1 alpha-2") country codes.
* @param {object} labels - Custom country labels. E.g. `{ RU: 'Россия', US: 'США', ... }`.
* @param {boolean} addInternationalOption - Whether should include "International" option at the top of the list.
* @return {object[]} A list of objects having shape `{ value : string, label : string }`.
*/
export function getCountrySelectOptions(_ref2) {
var countries = _ref2.countries,
countryNames = _ref2.countryNames,
addInternationalOption = _ref2.addInternationalOption,
compareStringsLocales = _ref2.compareStringsLocales,
_compareStrings = _ref2.compareStrings;
// Default country name comparator uses `String.localeCompare()`.
if (!_compareStrings) {
_compareStrings = compareStrings;
} // Generates a `<Select/>` option for each country.
var countrySelectOptions = countries.map(function (country) {
return {
value: country,
// All `locale` country names included in this library
// include all countries (this is checked at build time).
// The only case when a country name might be missing
// is when a developer supplies their own `labels` property.
// To guard against such cases, a missing country name
// is substituted by country code.
label: countryNames[country] || country
};
}); // Sort the list of countries alphabetically.
countrySelectOptions.sort(function (a, b) {
return _compareStrings(a.label, b.label, compareStringsLocales);
}); // Add the "International" option to the country list (if suitable)
if (addInternationalOption) {
countrySelectOptions.unshift({
label: countryNames.ZZ
});
}
return countrySelectOptions;
}
/**
* Parses a E.164 phone number to an instance of `PhoneNumber` class.
* @param {string?} value = E.164 phone number.
* @param {object} metadata - `libphonenumber-js` metadata
* @return {object} Object having shape `{ country: string?, countryCallingCode: string, number: string }`. `PhoneNumber`: https://gitlab.com/catamphetamine/libphonenumber-js#phonenumber.
* @example
* parsePhoneNumber('+78005553535')
*/
export function parsePhoneNumber(value, metadata) {
return parsePhoneNumberFromString(value || '', metadata);
}
/**
* Generates national number digits for a parsed phone.
* May prepend national prefix.
* The phone number must be a complete and valid phone number.
* @param {object} phoneNumber - An instance of `PhoneNumber` class.
* @param {object} metadata - `libphonenumber-js` metadata
* @return {string}
* @example
* getNationalNumberDigits({ country: 'RU', phone: '8005553535' })
* // returns '88005553535'
*/
export function generateNationalNumberDigits(phoneNumber) {
return phoneNumber.formatNational().replace(/\D/g, '');
}
/**
* Migrates parsed `<input/>` `value` for the newly selected `country`.
* @param {string?} phoneDigits - Phone number digits (and `+`) parsed from phone number `<input/>` (it's not the same as the `value` property).
* @param {string?} prevCountry - Previously selected country.
* @param {string?} newCountry - Newly selected country. Can't be same as previously selected country.
* @param {object} metadata - `libphonenumber-js` metadata.
* @param {boolean} useNationalFormat - whether should attempt to convert from international to national number for the new country.
* @return {string?}
*/
export function getPhoneDigitsForNewCountry(phoneDigits, _ref3) {
var prevCountry = _ref3.prevCountry,
newCountry = _ref3.newCountry,
metadata = _ref3.metadata,
useNationalFormat = _ref3.useNationalFormat;
// If `parsed_input` is empty
// then no need to migrate anything.
if (!phoneDigits) {
if (useNationalFormat) {
return '';
} else {
// If `phoneDigits` is empty then set `phoneDigits` to
// `+{getCountryCallingCode(newCountry)}`.
return getInternationalPhoneNumberPrefix(newCountry, metadata);
}
} // If switching to some country.
// (from "International" or another country)
// If switching from "International" then `phoneDigits` starts with a `+`.
// Otherwise it may or may not start with a `+`.
if (newCountry) {
// If the phone number was entered in international format
// then migrate it to the newly selected country.
// The phone number may be incomplete.
// The phone number entered not necessarily starts with
// the previously selected country phone prefix.
if (phoneDigits[0] === '+') {
// If the international phone number is for the new country
// then convert it to local if required.
if (useNationalFormat) {
// // If a phone number is being input in international form
// // and the country can already be derived from it,
// // and if it is the new country, then format as a national number.
// const derived_country = getCountryFromPossiblyIncompleteInternationalPhoneNumber(phoneDigits, metadata)
// if (derived_country === newCountry) {
// return stripCountryCallingCode(phoneDigits, derived_country, metadata)
// }
// Actually, the two countries don't necessarily need to match:
// the condition could be looser here, because several countries
// might share the same international phone number format
// (for example, "NANPA" countries like US, Canada, etc).
// The looser condition would be just "same nternational phone number format"
// which would mean "same country calling code" in the context of `libphonenumber-js`.
if (phoneDigits.indexOf('+' + getCountryCallingCode(newCountry, metadata)) === 0) {
return stripCountryCallingCode(phoneDigits, newCountry, metadata);
} // Simply discard the previously entered international phone number,
// because otherwise any "smart" transformation like getting the
// "national (significant) number" part and then prepending the
// newly selected country's "country calling code" to it
// would just be confusing for a user without being actually useful.
return ''; // // Simply strip the leading `+` character
// // therefore simply converting all digits into a "local" phone number.
// // https://github.com/catamphetamine/react-phone-number-input/issues/287
// return phoneDigits.slice(1)
}
if (prevCountry) {
var newCountryPrefix = getInternationalPhoneNumberPrefix(newCountry, metadata);
if (phoneDigits.indexOf(newCountryPrefix) === 0) {
return phoneDigits;
} else {
return newCountryPrefix;
}
} else {
var defaultValue = getInternationalPhoneNumberPrefix(newCountry, metadata); // If `phoneDigits`'s country calling code part is the same
// as for the new `country`, then leave `phoneDigits` as is.
if (phoneDigits.indexOf(defaultValue) === 0) {
return phoneDigits;
} // If `phoneDigits`'s country calling code part is not the same
// as for the new `country`, then set `phoneDigits` to
// `+{getCountryCallingCode(newCountry)}`.
return defaultValue;
} // // If the international phone number already contains
// // any country calling code then trim the country calling code part.
// // (that could also be the newly selected country phone code prefix as well)
// // `phoneDigits` doesn't neccessarily belong to `prevCountry`.
// // (e.g. if a user enters an international number
// // not belonging to any of the reduced `countries` list).
// phoneDigits = stripCountryCallingCode(phoneDigits, prevCountry, metadata)
// // Prepend country calling code prefix
// // for the newly selected country.
// return e164(phoneDigits, newCountry, metadata) || `+${getCountryCallingCode(newCountry, metadata)}`
}
} // If switching to "International" from a country.
else {
// If the phone number was entered in national format.
if (phoneDigits[0] !== '+') {
// Format the national phone number as an international one.
// The phone number entered not necessarily even starts with
// the previously selected country phone prefix.
// Even if the phone number belongs to whole another country
// it will still be parsed into some national phone number.
return e164(phoneDigits, prevCountry, metadata) || '';
}
}
return phoneDigits;
}
/**
* Converts phone number digits to a (possibly incomplete) E.164 phone number.
* @param {string?} number - A possibly incomplete phone number digits string. Can be a possibly incomplete E.164 phone number.
* @param {string?} country
* @param {[object} metadata - `libphonenumber-js` metadata.
* @return {string?}
*/
export function e164(number, country, metadata) {
if (!number) {
return;
} // If the phone number is being input in international format.
if (number[0] === '+') {
// If it's just the `+` sign then return nothing.
if (number === '+') {
return;
} // If there are any digits then the `value` is returned as is.
return number;
} // For non-international phone numbers
// an accompanying country code is required.
if (!country) {
return;
}
var partial_national_significant_number = getNationalSignificantNumberDigits(number, country, metadata);
if (partial_national_significant_number) {
return "+".concat(getCountryCallingCode(country, metadata)).concat(partial_national_significant_number);
}
}
/**
* 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
* @param {object} metadata - `libphonenumber-js` metadata.
* @return {string} Can be empty.
*/
export function trimNumber(number, country, metadata) {
var nationalSignificantNumberPart = getNationalSignificantNumberDigits(number, country, metadata);
if (nationalSignificantNumberPart) {
var overflowDigitsCount = nationalSignificantNumberPart.length - getMaxNumberLength(country, metadata);
if (overflowDigitsCount > 0) {
return number.slice(0, number.length - overflowDigitsCount);
}
}
return number;
}
function getMaxNumberLength(country, metadata) {
// Get "possible lengths" for a phone number of the country.
metadata = new Metadata(metadata);
metadata.country(country); // Return the last "possible length".
return metadata.numberingPlan.possibleLengths()[metadata.numberingPlan.possibleLengths().length - 1];
} // 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.
* @param {object} metadata - `libphonenumber-js` metadata.
* @return {string?}
*/
export function getCountryForPartialE164Number(partialE164Number, _ref4) {
var country = _ref4.country,
countries = _ref4.countries,
required = _ref4.required,
metadata = _ref4.metadata;
if (partialE164Number === '+') {
// Don't change the currently selected country yet.
return country;
}
var derived_country = getCountryFromPossiblyIncompleteInternationalPhoneNumber(partialE164Number, metadata); // 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, metadata)) {
return undefined;
} // Don't change the currently selected country.
return country;
}
/**
* Parses `<input/>` value. Derives `country` from `input`. Derives an E.164 `value`.
* @param {string?} phoneDigits — Parsed `<input/>` value. Examples: `""`, `"+"`, `"+123"`, `"123"`.
* @param {string?} prevPhoneDigits — Previous parsed `<input/>` value. Examples: `""`, `"+"`, `"+123"`, `"123"`.
* @param {string?} country - Currently selected country.
* @param {string[]?} countries - A list of available countries. If not passed then "all countries" are assumed.
* @param {boolean} international - Set to `true` to force international phone number format (leading `+`). Set to `false` to force "national" phone number format. Is `undefined` by default.
* @param {boolean} limitMaxLength — Whether to enable limiting phone number max length.
* @param {object} metadata - `libphonenumber-js` metadata.
* @return {object} An object of shape `{ input, country, value }`.
*/
export function onPhoneDigitsChange(phoneDigits, _ref5) {
var prevPhoneDigits = _ref5.prevPhoneDigits,
country = _ref5.country,
defaultCountry = _ref5.defaultCountry,
countries = _ref5.countries,
international = _ref5.international,
limitMaxLength = _ref5.limitMaxLength,
countryCallingCodeEditable = _ref5.countryCallingCodeEditable,
metadata = _ref5.metadata;
if (international && countryCallingCodeEditable === false) {
var prefix = getInternationalPhoneNumberPrefix(country, metadata); // The `<input/>` value must start with the country calling code.
if (phoneDigits.indexOf(prefix) !== 0) {
// If a user tabs into a phone number input field
// that is `international` and `withCountryCallingCode`,
// and then starts inputting local phone number digits,
// the first digit would get "swallowed" without this `appendDigits` fix.
// https://gitlab.com/catamphetamine/react-phone-number-input/-/issues/43
if (phoneDigits && phoneDigits[0] !== '+') {
phoneDigits = prefix + phoneDigits;
} else {
phoneDigits = prefix;
}
return {
phoneDigits: phoneDigits,
value: undefined,
country: country
};
}
} // If `international` property is `false`, then it means
// "enforce national-only format during input",
// so, if that's the case, then remove all `+` characters,
// but only if some country is currently selected.
// (not if "International" country is selected).
if (international === false && country && phoneDigits && phoneDigits[0] === '+') {
phoneDigits = convertInternationalPhoneDigitsToNational(phoneDigits, country, metadata);
} // Trim the input to not exceed the maximum possible number length.
if (phoneDigits && country && limitMaxLength) {
phoneDigits = trimNumber(phoneDigits, country, metadata);
} // If this `onChange()` event was triggered
// as a result of selecting "International" country,
// then force-prepend a `+` sign if the phone number
// `<input/>` value isn't in international format.
// Also, force-prepend a `+` sign if international
// phone number input format is set.
if (phoneDigits && phoneDigits[0] !== '+' && (!country || international)) {
phoneDigits = '+' + phoneDigits;
} // If the previously entered phone number
// has been entered in international format
// and the user decides to erase it,
// then also reset the `country`
// because it was most likely automatically selected
// while the user was typing in the phone number
// in international format.
// This fixes the issue when a user is presented
// with a phone number input with no country selected
// and then types in their local phone number
// then discovers that the input's messed up
// (a `+` has been prepended at the start of their input
// and a random country has been selected),
// decides to undo it all by erasing everything
// and then types in their local phone number again
// resulting in a seemingly correct phone number
// but in reality that phone number has incorrect country.
// https://github.com/catamphetamine/react-phone-number-input/issues/273
if (!phoneDigits && prevPhoneDigits && prevPhoneDigits[0] === '+') {
if (international) {
country = undefined;
} else {
country = defaultCountry;
}
} // Also resets such "randomly" selected country
// as soon as the user erases the number
// digit-by-digit up to the leading `+` sign.
if (phoneDigits === '+' && prevPhoneDigits && prevPhoneDigits[0] === '+' && prevPhoneDigits.length > '+'.length) {
country = undefined;
} // Generate the new `value` property.
var value;
if (phoneDigits) {
if (phoneDigits[0] === '+') {
if (phoneDigits === '+') {
value = undefined;
} else if (country && getInternationalPhoneNumberPrefix(country, metadata).indexOf(phoneDigits) === 0) {
value = undefined;
} else {
value = phoneDigits;
}
} else {
value = e164(phoneDigits, country, metadata);
}
} // Derive the country from the phone number.
// (regardless of whether there's any country currently selected,
// because there could be several countries corresponding to one country calling code)
if (value) {
country = getCountryForPartialE164Number(value, {
country: country,
countries: countries,
metadata: metadata
}); // If `international` property is `false`, then it means
// "enforce national-only format during input",
// so, if that's the case, then remove all `+` characters,
// but only if some country is currently selected.
// (not if "International" country is selected).
if (international === false && country && phoneDigits && phoneDigits[0] === '+') {
phoneDigits = convertInternationalPhoneDigitsToNational(phoneDigits, country, metadata); // Re-calculate `value` because `phoneDigits` has changed.
value = e164(phoneDigits, country, metadata);
}
}
return {
phoneDigits: phoneDigits,
country: country,
value: value
};
}
function convertInternationalPhoneDigitsToNational(input, country, metadata) {
// Handle the case when a user might have pasted
// a phone number in international format.
if (input.indexOf(getInternationalPhoneNumberPrefix(country, metadata)) === 0) {
// Create "as you type" formatter.
var formatter = new AsYouType(country, metadata); // Input partial national phone number.
formatter.input(input); // Return the parsed partial national phone number.
var phoneNumber = formatter.getNumber();
if (phoneNumber) {
// Transform the number to a national one,
// and remove all non-digits.
return phoneNumber.formatNational().replace(/\D/g, '');
} else {
return '';
}
} else {
// Just remove the `+` sign.
return input.replace(/\D/g, '');
}
}
/**
* Determines the country for a given (possibly incomplete) E.164 phone number.
* @param {string} number - A possibly incomplete E.164 phone number.
* @param {object} metadata - `libphonenumber-js` metadata.
* @return {string?}
*/
export function getCountryFromPossiblyIncompleteInternationalPhoneNumber(number, metadata) {
var formatter = new AsYouType(null, metadata);
formatter.input(number); // // `001` is a special "non-geograpical entity" code
// // in Google's `libphonenumber` library.
// if (formatter.getCountry() === '001') {
// return
// }
return formatter.getCountry();
}
/**
* Compares two strings.
* A helper for `Array.sort()`.
* @param {string} a — First string.
* @param {string} b — Second string.
* @param {(string[]|string)} [locales] — The `locales` argument of `String.localeCompare`.
*/
export function compareStrings(a, b, locales) {
// Use `String.localeCompare` if it's available.
// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
// Which means everyone except IE <= 10 and Safari <= 10.
// `localeCompare()` is available in latest Node.js versions.
/* istanbul ignore else */
if (String.prototype.localeCompare) {
return a.localeCompare(b, locales);
}
/* istanbul ignore next */
return a < b ? -1 : a > b ? 1 : 0;
}
/**
* Strips `+${countryCallingCode}` prefix from an E.164 phone number.
* @param {string} number - (possibly incomplete) E.164 phone number.
* @param {string?} country - A possible country for this phone number.
* @param {object} metadata - `libphonenumber-js` metadata.
* @return {string}
*/
export function stripCountryCallingCode(number, country, metadata) {
// Just an optimization, so that it
// doesn't have to iterate through all country calling codes.
if (country) {
var countryCallingCodePrefix = '+' + getCountryCallingCode(country, metadata); // If `country` fits the actual `number`.
if (number.length < countryCallingCodePrefix.length) {
if (countryCallingCodePrefix.indexOf(number) === 0) {
return '';
}
} else {
if (number.indexOf(countryCallingCodePrefix) === 0) {
return number.slice(countryCallingCodePrefix.length);
}
}
} // If `country` doesn't fit the actual `number`.
// Try all available country calling codes.
for (var _i = 0, _Object$keys = Object.keys(metadata.country_calling_codes); _i < _Object$keys.length; _i++) {
var country_calling_code = _Object$keys[_i];
if (number.indexOf(country_calling_code) === '+'.length) {
return number.slice('+'.length + country_calling_code.length);
}
}
return '';
}
/**
* 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
* @param {object} metadata - `libphonenumber-js` metadata.
* @return {string} [result]
*/
export function getNationalSignificantNumberDigits(number, country, metadata) {
// Create "as you type" formatter.
var formatter = new AsYouType(country, metadata); // Input partial national phone number.
formatter.input(number); // Return the parsed partial national phone number.
var 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 {string} country
* @return {boolean}
*/
export function couldNumberBelongToCountry(number, country, metadata) {
var intlPhoneNumberPrefix = getInternationalPhoneNumberPrefix(country, metadata);
var i = 0;
while (i < number.length && i < intlPhoneNumberPrefix.length) {
if (number[i] !== intlPhoneNumberPrefix[i]) {
return false;
}
i++;
}
return true;
}
/**
* Gets initial "phone digits" (including `+`, if using international format).
* @return {string} [phoneDigits] Returns `undefined` if there should be no initial "phone digits".
*/
export function getInitialPhoneDigits(_ref6) {
var value = _ref6.value,
phoneNumber = _ref6.phoneNumber,
defaultCountry = _ref6.defaultCountry,
international = _ref6.international,
useNationalFormat = _ref6.useNationalFormat,
metadata = _ref6.metadata;
// If the `value` (E.164 phone number)
// belongs to the currently selected country
// and `useNationalFormat` is `true`
// then convert `value` (E.164 phone number)
// to a local phone number digits.
// E.g. '+78005553535' -> '88005553535'.
if ((international === false || useNationalFormat) && phoneNumber && phoneNumber.country) {
return generateNationalNumberDigits(phoneNumber);
} // If `international` property is `true`,
// meaning "enforce international phone number format",
// then always show country calling code in the input field.
if (!value && international && defaultCountry) {
return getInternationalPhoneNumberPrefix(defaultCountry, metadata);
}
return value;
}
//# sourceMappingURL=phoneInputHelpers.js.map