@@ -4,72 +4,22 @@ import {FormContext} from "antd/es/form/context";
4
4
import Select from "antd/es/select" ;
5
5
import Input from "antd/es/input" ;
6
6
7
+ import {
8
+ checkValidity ,
9
+ cleanInput ,
10
+ displayFormat ,
11
+ getCountry ,
12
+ getDefaultISO2Code ,
13
+ getMetadata ,
14
+ getRawValue ,
15
+ parsePhoneNumber ,
16
+ usePhone ,
17
+ } from "react-phone-hooks" ;
18
+
19
+ import { injectMergedStyles } from "./styles" ;
7
20
import { PhoneInputProps , PhoneNumber } from "./types" ;
8
21
9
- import styleInject from "./styles" ;
10
- import timezones from "./metadata/timezones.json" ;
11
- import countries from "./metadata/countries.json" ;
12
- import validations from "./metadata/validations.json" ;
13
-
14
- styleInject ( "styles.css" ) ;
15
-
16
- const slots = new Set ( "." ) ;
17
-
18
- const getMetadata = ( rawValue : string , countriesList : typeof countries = countries , country : any = null ) => {
19
- country = country == null && rawValue . startsWith ( "44" ) ? "gb" : country ;
20
- if ( country != null ) {
21
- countriesList = countriesList . filter ( ( c ) => c [ 0 ] === country ) ;
22
- countriesList = countriesList . sort ( ( a , b ) => b [ 2 ] . length - a [ 2 ] . length ) ;
23
- }
24
- return countriesList . find ( ( c ) => rawValue . startsWith ( c [ 2 ] ) ) ;
25
- }
26
-
27
- const getRawValue = ( value : PhoneNumber | string ) => {
28
- if ( typeof value === "string" ) return value . replaceAll ( / \D / g, "" ) ;
29
- return [ value ?. countryCode , value ?. areaCode , value ?. phoneNumber ] . filter ( Boolean ) . join ( "" ) ;
30
- }
31
-
32
- const displayFormat = ( value : string ) => {
33
- return value . replace ( / [ . \s \D ] + $ / , "" ) . replace ( / ( \( \d + ) $ / , "$1)" ) ;
34
- }
35
-
36
- const cleanInput = ( input : any , pattern : string ) => {
37
- input = input . match ( / \d / g) || [ ] ;
38
- return Array . from ( pattern , c => input [ 0 ] === c || slots . has ( c ) ? input . shift ( ) || c : c ) ;
39
- }
40
-
41
- const checkValidity = ( metadata : PhoneNumber , strict : boolean = false ) => {
42
- /** Checks if both the area code and phone number match the validation pattern */
43
- const pattern = ( validations as any ) [ metadata . isoCode as keyof typeof validations ] [ Number ( strict ) ] ;
44
- return new RegExp ( pattern ) . test ( [ metadata . areaCode , metadata . phoneNumber ] . filter ( Boolean ) . join ( "" ) ) ;
45
- }
46
-
47
- const getDefaultISO2Code = ( ) => {
48
- /** Returns the default ISO2 code, based on the user's timezone */
49
- return ( timezones [ Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone as keyof typeof timezones ] || "" ) || "us" ;
50
- }
51
-
52
- const parsePhoneNumber = ( formattedNumber : string , countriesList : typeof countries = countries , country : any = null ) : PhoneNumber => {
53
- const value = getRawValue ( formattedNumber ) ;
54
- const isoCode = getMetadata ( value , countriesList , country ) ?. [ 0 ] || getDefaultISO2Code ( ) ;
55
- const countryCodePattern = / \+ \d + / ;
56
- const areaCodePattern = / \( ( \d + ) \) / ;
57
-
58
- /** Parses the matching partials of the phone number by predefined regex patterns */
59
- const countryCodeMatch = formattedNumber ? ( formattedNumber . match ( countryCodePattern ) || [ ] ) : [ ] ;
60
- const areaCodeMatch = formattedNumber ? ( formattedNumber . match ( areaCodePattern ) || [ ] ) : [ ] ;
61
-
62
- /** Converts the parsed values of the country and area codes to integers if values present */
63
- const countryCode = countryCodeMatch . length > 0 ? parseInt ( countryCodeMatch [ 0 ] ) : null ;
64
- const areaCode = areaCodeMatch . length > 1 ? areaCodeMatch [ 1 ] : null ;
65
-
66
- /** Parses the phone number by removing the country and area codes from the formatted value */
67
- const phoneNumberPattern = new RegExp ( `^${ countryCode } ${ ( areaCode || "" ) } (\\d+)` ) ;
68
- const phoneNumberMatch = value ? ( value . match ( phoneNumberPattern ) || [ ] ) : [ ] ;
69
- const phoneNumber = phoneNumberMatch . length > 1 ? phoneNumberMatch [ 1 ] : null ;
70
-
71
- return { countryCode, areaCode, phoneNumber, isoCode} ;
72
- }
22
+ injectMergedStyles ( ) ;
73
23
74
24
const PhoneInput = ( {
75
25
value : initialValue = "" ,
@@ -87,67 +37,34 @@ const PhoneInput = ({
87
37
onKeyDown : handleKeyDown = ( ) => null ,
88
38
...antInputProps
89
39
} : PhoneInputProps ) => {
90
- const defaultValue = getRawValue ( initialValue ) ;
91
- const defaultMetadata = getMetadata ( defaultValue ) || countries . find ( ( [ iso ] ) => iso === country ) ;
92
- const defaultValueState = defaultValue || countries . find ( ( [ iso ] ) => iso === defaultMetadata ?. [ 0 ] ) ?. [ 2 ] as string ;
93
-
94
40
const formInstance = useFormInstance ( ) ;
95
41
const formContext = useContext ( FormContext ) ;
96
42
const backRef = useRef < boolean > ( false ) ;
97
43
const initiatedRef = useRef < boolean > ( false ) ;
98
44
const [ query , setQuery ] = useState < string > ( "" ) ;
99
- const [ value , setValue ] = useState < string > ( defaultValueState ) ;
100
45
const [ minWidth , setMinWidth ] = useState < number > ( 0 ) ;
101
46
const [ countryCode , setCountryCode ] = useState < string > ( country ) ;
102
47
103
- const countriesOnly = useMemo ( ( ) => {
104
- const allowList = onlyCountries . length > 0 ? onlyCountries : countries . map ( ( [ iso ] ) => iso ) ;
105
- return countries . map ( ( [ iso ] ) => iso ) . filter ( ( iso ) => {
106
- return allowList . includes ( iso ) && ! excludeCountries . includes ( iso ) ;
107
- } ) ;
108
- } , [ onlyCountries , excludeCountries ] )
109
-
110
- const countriesList = useMemo ( ( ) => {
111
- const filteredCountries = countries . filter ( ( [ iso , name , _1 , dial ] ) => {
112
- return countriesOnly . includes ( iso ) && (
113
- name . toLowerCase ( ) . startsWith ( query . toLowerCase ( ) ) || dial . includes ( query )
114
- ) ;
115
- } ) ;
116
- return [
117
- ...filteredCountries . filter ( ( [ iso ] ) => preferredCountries . includes ( iso ) ) ,
118
- ...filteredCountries . filter ( ( [ iso ] ) => ! preferredCountries . includes ( iso ) ) ,
119
- ] ;
120
- } , [ countriesOnly , preferredCountries , query ] )
121
-
122
- const metadata = useMemo ( ( ) => {
123
- const calculatedMetadata = getMetadata ( getRawValue ( value ) , countriesList , countryCode ) ;
124
- if ( countriesList . find ( ( [ iso ] ) => iso === calculatedMetadata ?. [ 0 ] || iso === defaultMetadata ?. [ 0 ] ) ) {
125
- return calculatedMetadata || defaultMetadata ;
126
- }
127
- return countriesList [ 0 ] ;
128
- } , [ countriesList , countryCode , defaultMetadata , value ] )
129
-
130
- const pattern = useMemo ( ( ) => {
131
- return metadata ?. [ 3 ] || defaultMetadata ?. [ 3 ] || "" ;
132
- } , [ defaultMetadata , metadata ] )
133
-
134
- const clean = useCallback ( ( input : any ) => {
135
- return cleanInput ( input , pattern . replaceAll ( / \d / g, "." ) ) ;
136
- } , [ pattern ] )
137
-
138
- const first = useMemo ( ( ) => {
139
- return [ ...pattern ] . findIndex ( c => slots . has ( c ) ) ;
140
- } , [ pattern ] )
141
-
142
- const prev = useMemo ( ( j = 0 ) => {
143
- return Array . from ( pattern . replaceAll ( / \d / g, "." ) , ( c , i ) => {
144
- return slots . has ( c ) ? j = i + 1 : j ;
145
- } ) ;
146
- } , [ pattern ] )
48
+ const {
49
+ clean,
50
+ value,
51
+ format,
52
+ metadata,
53
+ setValue,
54
+ countriesList,
55
+ } = usePhone ( {
56
+ query,
57
+ country,
58
+ countryCode,
59
+ initialValue,
60
+ onlyCountries,
61
+ excludeCountries,
62
+ preferredCountries,
63
+ } ) ;
147
64
148
65
const selectValue = useMemo ( ( ) => {
149
66
let metadata = getMetadata ( getRawValue ( value ) , countriesList ) ;
150
- metadata = metadata || countries . find ( ( [ iso ] ) => iso === countryCode ) ;
67
+ metadata = metadata || getCountry ( countryCode as any ) ;
151
68
return ( { ...metadata } ) ?. [ 0 ] + ( { ...metadata } ) ?. [ 2 ] ;
152
69
} , [ countriesList , countryCode , value ] )
153
70
@@ -164,17 +81,6 @@ const PhoneInput = ({
164
81
}
165
82
} , [ antInputProps , formContext , formInstance ] )
166
83
167
- const format = useCallback ( ( { target} : ChangeEvent < HTMLInputElement > ) => {
168
- const [ i , j ] = [ target . selectionStart , target . selectionEnd ] . map ( ( i : any ) => {
169
- i = clean ( target . value . slice ( 0 , i ) ) . findIndex ( c => slots . has ( c ) ) ;
170
- return i < 0 ? prev [ prev . length - 1 ] : backRef . current ? prev [ i - 1 ] || first : i ;
171
- } ) ;
172
- target . value = displayFormat ( clean ( target . value ) . join ( "" ) ) ;
173
- target . setSelectionRange ( i , j ) ;
174
- backRef . current = false ;
175
- setValue ( target . value ) ;
176
- } , [ clean , first , prev ] )
177
-
178
84
const onKeyDown = useCallback ( ( event : KeyboardEvent < HTMLInputElement > ) => {
179
85
backRef . current = event . key === "Backspace" ;
180
86
handleKeyDown ( event ) ;
@@ -206,16 +112,17 @@ const PhoneInput = ({
206
112
const formattedNumber = displayFormat ( clean ( initialValue ) . join ( "" ) ) ;
207
113
const phoneMetadata = parsePhoneNumber ( formattedNumber , countriesList ) ;
208
114
onMount ( { ...phoneMetadata , valid : ( strict : boolean ) => checkValidity ( phoneMetadata , strict ) } ) ;
209
- setCountryCode ( phoneMetadata . isoCode as keyof typeof validations ) ;
115
+ setCountryCode ( phoneMetadata . isoCode as any ) ;
210
116
setValue ( formattedNumber ) ;
211
- } , [ clean , countriesList , metadata , onMount , value ] )
117
+ } , [ clean , countriesList , metadata , onMount , setValue , value ] )
212
118
213
119
const countriesSelect = useMemo ( ( ) => (
214
120
< Select
215
121
suffixIcon = { null }
216
122
value = { selectValue }
217
123
open = { disableDropdown ? false : undefined }
218
- onSelect = { ( selectedOption , { key : mask } ) => {
124
+ onSelect = { ( selectedOption , { key} ) => {
125
+ const [ _ , mask ] = key . split ( "_" ) ;
219
126
if ( selectValue === selectedOption ) return ;
220
127
const selectedCountryCode = selectedOption . slice ( 0 , 2 ) ;
221
128
const formattedNumber = displayFormat ( cleanInput ( mask , mask ) . join ( "" ) ) ;
@@ -241,8 +148,8 @@ const PhoneInput = ({
241
148
>
242
149
{ countriesList . map ( ( [ iso , name , dial , mask ] ) => (
243
150
< Select . Option
244
- key = { iso + mask }
245
151
value = { iso + dial }
152
+ key = { `${ iso } _${ mask } ` }
246
153
label = { < div className = { `flag ${ iso } ` } /> }
247
154
children = { < div className = "ant-phone-input-select-item" >
248
155
< div className = { `flag ${ iso } ` } />
@@ -251,7 +158,7 @@ const PhoneInput = ({
251
158
/>
252
159
) ) }
253
160
</ Select >
254
- ) , [ selectValue , disableDropdown , minWidth , searchNotFound , countriesList , setFieldValue , enableSearch , searchPlaceholder ] )
161
+ ) , [ selectValue , disableDropdown , minWidth , searchNotFound , countriesList , setFieldValue , setValue , enableSearch , searchPlaceholder ] )
255
162
256
163
return (
257
164
< div className = "ant-phone-input-wrapper"
0 commit comments