libphonenumber-js
Version:
A simpler (and smaller) rewrite of Google Android's libphonenumber library in javascript
445 lines (425 loc) • 16.2 kB
JavaScript
import Metadata from './metadata.js';
import PhoneNumber from './PhoneNumber.js';
import AsYouTypeState from './AsYouTypeState.js';
import AsYouTypeFormatter, { DIGIT_PLACEHOLDER } from './AsYouTypeFormatter.js';
import AsYouTypeParser, { extractFormattedDigitsAndPlus } from './AsYouTypeParser.js';
import getCountryByCallingCode from './helpers/getCountryByCallingCode.js';
import getCountryByNationalNumber from './helpers/getCountryByNationalNumber.js';
import isObject from './helpers/isObject.js';
const USE_NON_GEOGRAPHIC_COUNTRY_CODE = false;
export default class AsYouType {
/**
* @param {(string|object)?} [optionsOrDefaultCountry] - The default country used for parsing non-international phone numbers. Can also be an `options` object.
* @param {Object} metadata
*/
constructor(optionsOrDefaultCountry, metadata) {
this.metadata = new Metadata(metadata);
const [defaultCountry, defaultCallingCode] = this.getCountryAndCallingCode(optionsOrDefaultCountry);
// `this.defaultCountry` and `this.defaultCallingCode` aren't required to be in sync.
// For example, `this.defaultCountry` could be `"AR"` and `this.defaultCallingCode` could be `undefined`.
// So `this.defaultCountry` and `this.defaultCallingCode` are totally independent.
this.defaultCountry = defaultCountry;
this.defaultCallingCode = defaultCallingCode;
this.reset();
}
getCountryAndCallingCode(optionsOrDefaultCountry) {
// Set `defaultCountry` and `defaultCallingCode` options.
let defaultCountry;
let defaultCallingCode;
// Turns out `null` also has type "object". Weird.
if (optionsOrDefaultCountry) {
if (isObject(optionsOrDefaultCountry)) {
defaultCountry = optionsOrDefaultCountry.defaultCountry;
defaultCallingCode = optionsOrDefaultCountry.defaultCallingCode;
} else {
defaultCountry = optionsOrDefaultCountry;
}
}
if (defaultCountry && !this.metadata.hasCountry(defaultCountry)) {
defaultCountry = undefined;
}
if (defaultCallingCode) {
/* istanbul ignore if */
if (USE_NON_GEOGRAPHIC_COUNTRY_CODE) {
if (this.metadata.isNonGeographicCallingCode(defaultCallingCode)) {
defaultCountry = '001';
}
}
}
return [defaultCountry, defaultCallingCode];
}
/**
* Inputs "next" phone number characters.
* @param {string} text
* @return {string} Formatted phone number characters that have been input so far.
*/
input(text) {
const {
digits,
justLeadingPlus
} = this.parser.input(text, this.state);
if (justLeadingPlus) {
this.formattedOutput = '+';
} else if (digits) {
this.determineTheCountryIfNeeded();
// Match the available formats by the currently available leading digits.
if (this.state.nationalSignificantNumber) {
this.formatter.narrowDownMatchingFormats(this.state);
}
let formattedNationalNumber;
if (this.metadata.hasSelectedNumberingPlan()) {
formattedNationalNumber = this.formatter.format(digits, this.state);
}
if (formattedNationalNumber === undefined) {
// See if another national (significant) number could be re-extracted.
if (this.parser.reExtractNationalSignificantNumber(this.state)) {
this.determineTheCountryIfNeeded();
// If it could, then re-try formatting the new national (significant) number.
const nationalDigits = this.state.getNationalDigits();
if (nationalDigits) {
formattedNationalNumber = this.formatter.format(nationalDigits, this.state);
}
}
}
this.formattedOutput = formattedNationalNumber ? this.getFullNumber(formattedNationalNumber) : this.getNonFormattedNumber();
}
return this.formattedOutput;
}
reset() {
this.state = new AsYouTypeState({
onCountryChange: country => {
// Before version `1.6.0`, the official `AsYouType` formatter API
// included a `.country` property on an `AsYouType` instance.
// Since that property (along with the others) have been moved to
// `this.state`, `this.country` property is emulated for compatibility
// with the old versions.
this.country = country;
},
onCallingCodeChange: (callingCode, country) => {
this.metadata.selectNumberingPlan(country, callingCode);
this.formatter.reset(this.metadata.numberingPlan, this.state);
this.parser.reset(this.metadata.numberingPlan);
}
});
this.formatter = new AsYouTypeFormatter({
state: this.state,
metadata: this.metadata
});
this.parser = new AsYouTypeParser({
defaultCountry: this.defaultCountry,
defaultCallingCode: this.defaultCallingCode,
metadata: this.metadata,
state: this.state,
onNationalSignificantNumberChange: () => {
this.determineTheCountryIfNeeded();
this.formatter.reset(this.metadata.numberingPlan, this.state);
}
});
this.state.reset({
country: this.defaultCountry,
callingCode: this.defaultCallingCode
});
this.formattedOutput = '';
return this;
}
/**
* Returns `true` if the phone number is being input in international format.
* In other words, returns `true` if and only if the parsed phone number starts with a `"+"`.
* @return {boolean}
*/
isInternational() {
return this.state.international;
}
/**
* Returns the "calling code" part of the phone number when it's being input
* in an international format.
* If no valid calling code has been entered so far, returns `undefined`.
* @return {string} [callingCode]
*/
getCallingCode() {
// If the number is being input in national format and some "default calling code"
// has been passed to `AsYouType` constructor, then `this.state.callingCode`
// is equal to that "default calling code".
//
// If the number is being input in national format and no "default calling code"
// has been passed to `AsYouType` constructor, then returns `undefined`,
// even if a "default country" has been passed to `AsYouType` constructor.
//
if (this.isInternational()) {
return this.state.callingCode;
}
}
// A legacy alias.
getCountryCallingCode() {
return this.getCallingCode();
}
/**
* Returns a two-letter country code of the phone number.
* Returns `undefined` for "non-geographic" phone numbering plans.
* Returns `undefined` if no phone number has been input yet.
* @return {string} [country]
*/
getCountry() {
const {
digits
} = this.state;
// Return `undefined` if no digits have been input yet.
if (digits) {
return this._getCountry();
}
}
/**
* Returns a two-letter country code of the phone number.
* Returns `undefined` for "non-geographic" phone numbering plans.
* @return {string} [country]
*/
_getCountry() {
const {
country
} = this.state;
/* istanbul ignore if */
if (USE_NON_GEOGRAPHIC_COUNTRY_CODE) {
// `AsYouType.getCountry()` returns `undefined`
// for "non-geographic" phone numbering plans.
if (country === '001') {
return;
}
}
return country;
}
determineTheCountryIfNeeded() {
// Suppose a user enters a phone number in international format,
// and there're several countries corresponding to that country calling code,
// and a country has been derived from the number, and then
// a user enters one more digit and the number is no longer
// valid for the derived country, so the country should be re-derived
// on every new digit in those cases.
//
// If the phone number is being input in national format,
// then it could be a case when `defaultCountry` wasn't specified
// when creating `AsYouType` instance, and just `defaultCallingCode` was specified,
// and that "calling code" could correspond to a "non-geographic entity",
// or there could be several countries corresponding to that country calling code.
// In those cases, `this.country` is `undefined` and should be derived
// from the number. Again, if country calling code is ambiguous, then
// `this.country` should be re-derived with each new digit.
//
if (!this.state.country || this.isCountryCallingCodeAmbiguous()) {
this.determineTheCountry();
}
}
// Prepends `+CountryCode ` in case of an international phone number
getFullNumber(formattedNationalNumber) {
if (this.isInternational()) {
const prefix = text => this.formatter.getInternationalPrefixBeforeCountryCallingCode(this.state, {
spacing: text ? true : false
}) + text;
const {
callingCode
} = this.state;
if (!callingCode) {
return prefix(`${this.state.getDigitsWithoutInternationalPrefix()}`);
}
if (!formattedNationalNumber) {
return prefix(callingCode);
}
return prefix(`${callingCode} ${formattedNationalNumber}`);
}
return formattedNationalNumber;
}
getNonFormattedNationalNumberWithPrefix() {
const {
nationalSignificantNumber,
prefixBeforeNationalSignificantNumberThatIsNotNationalPrefix,
nationalPrefix
} = this.state;
let number = nationalSignificantNumber;
const prefix = prefixBeforeNationalSignificantNumberThatIsNotNationalPrefix || nationalPrefix;
if (prefix) {
number = prefix + number;
}
return number;
}
getNonFormattedNumber() {
const {
nationalSignificantNumberIsModified
} = this.state;
return this.getFullNumber(nationalSignificantNumberIsModified ? this.state.getNationalDigits() : this.getNonFormattedNationalNumberWithPrefix());
}
getNonFormattedTemplate() {
const number = this.getNonFormattedNumber();
if (number) {
return number.replace(/[\+\d]/g, DIGIT_PLACEHOLDER);
}
}
isCountryCallingCodeAmbiguous() {
const {
callingCode
} = this.state;
const countryCodes = this.metadata.getCountryCodesForCallingCode(callingCode);
return countryCodes && countryCodes.length > 1;
}
// Determines the exact country of the phone number
// entered so far based on the country phone code
// and the national phone number.
determineTheCountry() {
this.state.setCountry(getCountryByCallingCode(this.isInternational() ? this.state.callingCode : this.defaultCallingCode, {
nationalNumber: this.state.nationalSignificantNumber,
metadata: this.metadata
}));
}
/**
* Returns a E.164 phone number value for the user's input.
*
* For example, for country `"US"` and input `"(222) 333-4444"`
* it will return `"+12223334444"`.
*
* For international phone number input, it will also auto-correct
* some minor errors such as using a national prefix when writing
* an international phone number. For example, if the user inputs
* `"+44 0 7400 000000"` then it will return an auto-corrected
* `"+447400000000"` phone number value.
*
* Will return `undefined` if no digits have been input,
* or when inputting a phone number in national format and no
* default country or default "country calling code" have been set.
*
* @return {string} [value]
*/
getNumberValue() {
const {
digits,
callingCode,
country,
nationalSignificantNumber
} = this.state;
// Will return `undefined` if no digits have been input.
if (!digits) {
return;
}
if (this.isInternational()) {
if (callingCode) {
return '+' + callingCode + nationalSignificantNumber;
} else {
return '+' + digits;
}
} else {
if (country || callingCode) {
const callingCode_ = country ? this.metadata.countryCallingCode() : callingCode;
return '+' + callingCode_ + nationalSignificantNumber;
}
}
}
/**
* Returns an instance of `PhoneNumber` class.
* Will return `undefined` if no national (significant) number
* digits have been entered so far, or if no `defaultCountry` has been
* set and the user enters a phone number not in international format.
*/
getNumber() {
const {
nationalSignificantNumber,
carrierCode,
callingCode
} = this.state;
// `this._getCountry()` is basically same as `this.state.country`
// with the only change that it return `undefined` in case of a
// "non-geographic" numbering plan instead of `"001"` "internal use" value.
let country = this._getCountry();
if (!nationalSignificantNumber) {
return;
}
// `state.country` and `state.callingCode` aren't required to be in sync.
// For example, `country` could be `"AR"` and `callingCode` could be `undefined`.
// So `country` and `callingCode` are totally independent.
if (!country && !callingCode) {
return;
}
// By default, if `defaultCountry` parameter was passed when
// creating `AsYouType` instance, `state.country` is gonna be
// that `defaultCountry`, which doesn't entirely conform with
// `parsePhoneNumber()`'s behavior where it attempts to determine
// the country more precisely in cases when multiple countries
// could correspond to the same `countryCallingCode`.
// https://gitlab.com/catamphetamine/libphonenumber-js/-/issues/103#note_1417192969
//
// Because `AsYouType.getNumber()` method is supposed to be a 1:1
// equivalent for `parsePhoneNumber(AsYouType.getNumberValue())`,
// then it should also behave accordingly in cases of `country` ambiguity.
// That's how users of this library would expect it to behave anyway.
//
if (country) {
if (country === this.defaultCountry) {
// `state.country` and `state.callingCode` aren't required to be in sync.
// For example, `state.country` could be `"AR"` and `state.callingCode` could be `undefined`.
// So `state.country` and `state.callingCode` are totally independent.
const metadata = new Metadata(this.metadata.metadata);
metadata.selectNumberingPlan(country);
const _callingCode = metadata.numberingPlan.callingCode();
const ambiguousCountries = this.metadata.getCountryCodesForCallingCode(_callingCode);
if (ambiguousCountries.length > 1) {
const exactCountry = getCountryByNationalNumber(nationalSignificantNumber, {
countries: ambiguousCountries,
metadata: this.metadata.metadata
});
if (exactCountry) {
country = exactCountry;
}
}
}
}
const phoneNumber = new PhoneNumber(country || callingCode, nationalSignificantNumber, this.metadata.metadata);
if (carrierCode) {
phoneNumber.carrierCode = carrierCode;
}
// Phone number extensions are not supported by "As You Type" formatter.
return phoneNumber;
}
/**
* Returns `true` if the phone number is "possible".
* Is just a shortcut for `PhoneNumber.isPossible()`.
* @return {boolean}
*/
isPossible() {
const phoneNumber = this.getNumber();
if (!phoneNumber) {
return false;
}
return phoneNumber.isPossible();
}
/**
* Returns `true` if the phone number is "valid".
* Is just a shortcut for `PhoneNumber.isValid()`.
* @return {boolean}
*/
isValid() {
const phoneNumber = this.getNumber();
if (!phoneNumber) {
return false;
}
return phoneNumber.isValid();
}
/**
* @deprecated
* This method is used in `react-phone-number-input/source/input-control.js`
* in versions before `3.0.16`.
*/
getNationalNumber() {
return this.state.nationalSignificantNumber;
}
/**
* Returns the phone number characters entered by the user.
* @return {string}
*/
getChars() {
return (this.state.international ? '+' : '') + this.state.digits;
}
/**
* Returns the template for the formatted phone number.
* @return {string}
*/
getTemplate() {
return this.formatter.getTemplate(this.state) || this.getNonFormattedTemplate() || '';
}
}
//# sourceMappingURL=AsYouType.js.map