UNPKG

11.3 kBJavaScriptView Raw
1import Metadata from './metadata'
2import PhoneNumber from './PhoneNumber'
3import AsYouTypeState from './AsYouTypeState'
4import AsYouTypeFormatter, { DIGIT_PLACEHOLDER } from './AsYouTypeFormatter'
5import AsYouTypeParser, { extractFormattedDigitsAndPlus } from './AsYouTypeParser'
6import getCountryByCallingCode from './helpers/getCountryByCallingCode'
7
8const USE_NON_GEOGRAPHIC_COUNTRY_CODE = false
9
10export default class AsYouType {
11 /**
12 * @param {(string|object)?} [optionsOrDefaultCountry] - The default country used for parsing non-international phone numbers. Can also be an `options` object.
13 * @param {Object} metadata
14 */
15 constructor(optionsOrDefaultCountry, metadata) {
16 this.metadata = new Metadata(metadata)
17 const [defaultCountry, defaultCallingCode] = this.getCountryAndCallingCode(optionsOrDefaultCountry)
18 this.defaultCountry = defaultCountry
19 this.defaultCallingCode = defaultCallingCode
20 this.reset()
21 }
22
23 getCountryAndCallingCode(optionsOrDefaultCountry) {
24 // Set `defaultCountry` and `defaultCallingCode` options.
25 let defaultCountry
26 let defaultCallingCode
27 // Turns out `null` also has type "object". Weird.
28 if (optionsOrDefaultCountry) {
29 if (typeof optionsOrDefaultCountry === 'object') {
30 defaultCountry = optionsOrDefaultCountry.defaultCountry
31 defaultCallingCode = optionsOrDefaultCountry.defaultCallingCode
32 } else {
33 defaultCountry = optionsOrDefaultCountry
34 }
35 }
36 if (defaultCountry && !this.metadata.hasCountry(defaultCountry)) {
37 defaultCountry = undefined
38 }
39 if (defaultCallingCode) {
40 /* istanbul ignore if */
41 if (USE_NON_GEOGRAPHIC_COUNTRY_CODE) {
42 if (this.metadata.isNonGeographicCallingCode(defaultCallingCode)) {
43 defaultCountry = '001'
44 }
45 }
46 }
47 return [defaultCountry, defaultCallingCode]
48 }
49
50 /**
51 * Inputs "next" phone number characters.
52 * @param {string} text
53 * @return {string} Formatted phone number characters that have been input so far.
54 */
55 input(text) {
56 const {
57 digits,
58 justLeadingPlus
59 } = this.parser.input(text, this.state)
60 if (justLeadingPlus) {
61 this.formattedOutput = '+'
62 } else if (digits) {
63 this.determineTheCountryIfNeeded()
64 // Match the available formats by the currently available leading digits.
65 if (this.state.nationalSignificantNumber) {
66 this.formatter.narrowDownMatchingFormats(this.state)
67 }
68 let formattedNationalNumber
69 if (this.metadata.hasSelectedNumberingPlan()) {
70 formattedNationalNumber = this.formatter.format(digits, this.state)
71 }
72 if (formattedNationalNumber === undefined) {
73 // See if another national (significant) number could be re-extracted.
74 if (this.parser.reExtractNationalSignificantNumber(this.state)) {
75 this.determineTheCountryIfNeeded()
76 // If it could, then re-try formatting the new national (significant) number.
77 const nationalDigits = this.state.getNationalDigits()
78 if (nationalDigits) {
79 formattedNationalNumber = this.formatter.format(nationalDigits, this.state)
80 }
81 }
82 }
83 this.formattedOutput = formattedNationalNumber
84 ? this.getFullNumber(formattedNationalNumber)
85 : this.getNonFormattedNumber()
86 }
87 return this.formattedOutput
88 }
89
90 reset() {
91 this.state = new AsYouTypeState({
92 onCountryChange: (country) => {
93 // Before version `1.6.0`, the official `AsYouType` formatter API
94 // included the `.country` property of an `AsYouType` instance.
95 // Since that property (along with the others) have been moved to
96 // `this.state`, `this.country` property is emulated for compatibility
97 // with the old versions.
98 this.country = country
99 },
100 onCallingCodeChange: (country, callingCode) => {
101 this.metadata.selectNumberingPlan(country, callingCode)
102 this.formatter.reset(this.metadata.numberingPlan, this.state)
103 this.parser.reset(this.metadata.numberingPlan)
104 }
105 })
106 this.formatter = new AsYouTypeFormatter({
107 state: this.state,
108 metadata: this.metadata
109 })
110 this.parser = new AsYouTypeParser({
111 defaultCountry: this.defaultCountry,
112 defaultCallingCode: this.defaultCallingCode,
113 metadata: this.metadata,
114 state: this.state,
115 onNationalSignificantNumberChange: () => {
116 this.determineTheCountryIfNeeded()
117 this.formatter.reset(this.metadata.numberingPlan, this.state)
118 }
119 })
120 this.state.reset(this.defaultCountry, this.defaultCallingCode)
121 this.formattedOutput = ''
122 return this
123 }
124
125 /**
126 * Returns `true` if the phone number is being input in international format.
127 * In other words, returns `true` if and only if the parsed phone number starts with a `"+"`.
128 * @return {boolean}
129 */
130 isInternational() {
131 return this.state.international
132 }
133
134 /**
135 * Returns the "country calling code" part of the phone number.
136 * Returns `undefined` if the number is not being input in international format.
137 * Returns "country calling code" for "non-geographic" phone numbering plans too.
138 * @return {string} [callingCode]
139 */
140 getCallingCode() {
141 return this.state.callingCode
142 }
143
144 // A legacy alias.
145 getCountryCallingCode() {
146 return this.getCallingCode()
147 }
148
149 /**
150 * Returns a two-letter country code of the phone number.
151 * Returns `undefined` for "non-geographic" phone numbering plans.
152 * Returns `undefined` if no phone number has been input yet.
153 * @return {string} [country]
154 */
155 getCountry() {
156 const { digits, country } = this.state
157 // If no digits have been input yet,
158 // then `this.country` is the `defaultCountry`.
159 // Won't return the `defaultCountry` in such case.
160 if (!digits) {
161 return
162 }
163 let countryCode = country
164 /* istanbul ignore if */
165 if (USE_NON_GEOGRAPHIC_COUNTRY_CODE) {
166 // `AsYouType.getCountry()` returns `undefined`
167 // for "non-geographic" phone numbering plans.
168 if (countryCode === '001') {
169 countryCode = undefined
170 }
171 }
172 return countryCode
173 }
174
175 determineTheCountryIfNeeded() {
176 // Suppose a user enters a phone number in international format,
177 // and there're several countries corresponding to that country calling code,
178 // and a country has been derived from the number, and then
179 // a user enters one more digit and the number is no longer
180 // valid for the derived country, so the country should be re-derived
181 // on every new digit in those cases.
182 //
183 // If the phone number is being input in national format,
184 // then it could be a case when `defaultCountry` wasn't specified
185 // when creating `AsYouType` instance, and just `defaultCallingCode` was specified,
186 // and that "calling code" could correspond to a "non-geographic entity",
187 // or there could be several countries corresponding to that country calling code.
188 // In those cases, `this.country` is `undefined` and should be derived
189 // from the number. Again, if country calling code is ambiguous, then
190 // `this.country` should be re-derived with each new digit.
191 //
192 if (!this.state.country || this.isCountryCallingCodeAmbiguous()) {
193 this.determineTheCountry()
194 }
195 }
196
197 // Prepends `+CountryCode ` in case of an international phone number
198 getFullNumber(formattedNationalNumber) {
199 if (this.isInternational()) {
200 const prefix = (text) => this.formatter.getInternationalPrefixBeforeCountryCallingCode(this.state, {
201 spacing: text ? true : false
202 }) + text
203 const { callingCode } = this.state
204 if (!callingCode) {
205 return prefix(`${this.state.getDigitsWithoutInternationalPrefix()}`)
206 }
207 if (!formattedNationalNumber) {
208 return prefix(callingCode)
209 }
210 return prefix(`${callingCode} ${formattedNationalNumber}`)
211 }
212 return formattedNationalNumber
213 }
214
215 getNonFormattedNationalNumberWithPrefix() {
216 const {
217 nationalSignificantNumber,
218 complexPrefixBeforeNationalSignificantNumber,
219 nationalPrefix
220 } = this.state
221 let number = nationalSignificantNumber
222 const prefix = complexPrefixBeforeNationalSignificantNumber || nationalPrefix
223 if (prefix) {
224 number = prefix + number
225 }
226 return number
227 }
228
229 getNonFormattedNumber() {
230 const { nationalSignificantNumberMatchesInput } = this.state
231 return this.getFullNumber(
232 nationalSignificantNumberMatchesInput
233 ? this.getNonFormattedNationalNumberWithPrefix()
234 : this.state.getNationalDigits()
235 )
236 }
237
238 getNonFormattedTemplate() {
239 const number = this.getNonFormattedNumber()
240 if (number) {
241 return number.replace(/[\+\d]/g, DIGIT_PLACEHOLDER)
242 }
243 }
244
245 isCountryCallingCodeAmbiguous() {
246 const { callingCode } = this.state
247 const countryCodes = this.metadata.getCountryCodesForCallingCode(callingCode)
248 return countryCodes && countryCodes.length > 1
249 }
250
251 // Determines the country of the phone number
252 // entered so far based on the country phone code
253 // and the national phone number.
254 determineTheCountry() {
255 this.state.setCountry(getCountryByCallingCode(
256 this.isInternational() ? this.state.callingCode : this.defaultCallingCode,
257 this.state.nationalSignificantNumber,
258 this.metadata
259 ))
260 }
261
262 /**
263 * Returns an instance of `PhoneNumber` class.
264 * Will return `undefined` if no national (significant) number
265 * digits have been entered so far, or if no `defaultCountry` has been
266 * set and the user enters a phone number not in international format.
267 */
268 getNumber() {
269 let {
270 nationalSignificantNumber,
271 carrierCode
272 } = this.state
273 if (this.isInternational()) {
274 if (!this.state.callingCode) {
275 return
276 }
277 } else {
278 if (!this.state.country && !this.defaultCallingCode) {
279 return
280 }
281 }
282 if (!nationalSignificantNumber) {
283 return
284 }
285 const countryCode = this.getCountry()
286 const callingCode = this.getCountryCallingCode() || this.defaultCallingCode
287 const phoneNumber = new PhoneNumber(
288 countryCode || callingCode,
289 nationalSignificantNumber,
290 this.metadata.metadata
291 )
292 if (carrierCode) {
293 phoneNumber.carrierCode = carrierCode
294 }
295 // Phone number extensions are not supported by "As You Type" formatter.
296 return phoneNumber
297 }
298
299 /**
300 * Returns `true` if the phone number is "possible".
301 * Is just a shortcut for `PhoneNumber.isPossible()`.
302 * @return {boolean}
303 */
304 isPossible() {
305 const phoneNumber = this.getNumber()
306 if (!phoneNumber) {
307 return false
308 }
309 return phoneNumber.isPossible()
310 }
311
312 /**
313 * Returns `true` if the phone number is "valid".
314 * Is just a shortcut for `PhoneNumber.isValid()`.
315 * @return {boolean}
316 */
317 isValid() {
318 const phoneNumber = this.getNumber()
319 if (!phoneNumber) {
320 return false
321 }
322 return phoneNumber.isValid()
323 }
324
325 /**
326 * @deprecated
327 * This method is used in `react-phone-number-input/source/input-control.js`
328 * in versions before `3.0.16`.
329 */
330 getNationalNumber() {
331 return this.state.nationalSignificantNumber
332 }
333
334 /**
335 * Returns the phone number characters entered by the user.
336 * @return {string}
337 */
338 getChars() {
339 return (this.state.international ? '+' : '') + this.state.digits
340 }
341
342 /**
343 * Returns the template for the formatted phone number.
344 * @return {string}
345 */
346 getTemplate() {
347 return this.formatter.getTemplate(this.state) || this.getNonFormattedTemplate() || ''
348 }
349}
\No newline at end of file