1
- import { ChangeEvent , useCallback , useMemo , useRef , useState } from "react" ;
1
+ import { ChangeEvent , KeyboardEvent , useCallback , useMemo , useRef , useState } from "react" ;
2
2
3
3
import { PhoneNumber , usePhoneOptions } from "./types" ;
4
4
@@ -9,149 +9,159 @@ import validations from "./metadata/validations.json";
9
9
const slots = new Set ( "." ) ;
10
10
11
11
export const getMetadata = ( rawValue : string , countriesList : typeof countries = countries , country : any = null ) => {
12
- country = country == null && rawValue . startsWith ( "44" ) ? "gb" : country ;
13
- if ( country != null ) {
14
- countriesList = countriesList . filter ( ( c ) => c [ 0 ] === country ) ;
15
- countriesList = countriesList . sort ( ( a , b ) => b [ 2 ] . length - a [ 2 ] . length ) ;
16
- }
17
- return countriesList . find ( ( c ) => rawValue . startsWith ( c [ 2 ] ) ) ;
12
+ country = country == null && rawValue . startsWith ( "44" ) ? "gb" : country ;
13
+ if ( country != null ) {
14
+ countriesList = countriesList . filter ( ( c ) => c [ 0 ] === country ) ;
15
+ countriesList = countriesList . sort ( ( a , b ) => b [ 2 ] . length - a [ 2 ] . length ) ;
16
+ }
17
+ return countriesList . find ( ( c ) => rawValue . startsWith ( c [ 2 ] ) ) ;
18
18
}
19
19
20
20
export const getCountry = ( countryCode : keyof typeof countries ) => {
21
- return countries . find ( ( [ iso ] ) => iso === countryCode ) ;
21
+ return countries . find ( ( [ iso ] ) => iso === countryCode ) ;
22
22
}
23
23
24
24
export const getRawValue = ( value : PhoneNumber | string ) => {
25
- if ( typeof value === "string" ) return value . replaceAll ( / \D / g, "" ) ;
26
- return [ value ?. countryCode , value ?. areaCode , value ?. phoneNumber ] . filter ( Boolean ) . join ( "" ) ;
25
+ if ( typeof value === "string" ) return value . replaceAll ( / \D / g, "" ) ;
26
+ return [ value ?. countryCode , value ?. areaCode , value ?. phoneNumber ] . filter ( Boolean ) . join ( "" ) ;
27
27
}
28
28
29
29
export const displayFormat = ( value : string ) => {
30
- /** Returns the formatted value that can be displayed as an actual input value */
31
- return value . replace ( / [ . \s \D ] + $ / , "" ) . replace ( / ( \( \d + ) $ / , "$1)" ) ;
30
+ /** Returns the formatted value that can be displayed as an actual input value */
31
+ return value . replace ( / [ . \s \D ] + $ / , "" ) . replace ( / ( \( \d + ) $ / , "$1)" ) ;
32
32
}
33
33
34
34
export const cleanInput = ( input : any , pattern : string ) => {
35
- input = input . match ( / \d / g) || [ ] ;
36
- return Array . from ( pattern , c => input [ 0 ] === c || slots . has ( c ) ? input . shift ( ) || c : c ) ;
35
+ input = input . match ( / \d / g) || [ ] ;
36
+ return Array . from ( pattern , c => input [ 0 ] === c || slots . has ( c ) ? input . shift ( ) || c : c ) ;
37
37
}
38
38
39
39
export const getFormattedNumber = ( rawValue : any , pattern : string ) => {
40
- /** Returns the reformatted input value based on the given pattern */
41
- return displayFormat ( cleanInput ( rawValue , pattern . replaceAll ( / \d / g, "." ) ) . join ( "" ) ) ;
40
+ /** Returns the reformatted input value based on the given pattern */
41
+ return displayFormat ( cleanInput ( rawValue , pattern . replaceAll ( / \d / g, "." ) ) . join ( "" ) ) ;
42
42
}
43
43
44
44
export const checkValidity = ( metadata : PhoneNumber , strict : boolean = false ) => {
45
- /** Checks if both the area code and phone number match the validation pattern */
46
- const pattern = ( validations as any ) [ metadata . isoCode as keyof typeof validations ] [ Number ( strict ) ] ;
47
- return new RegExp ( pattern ) . test ( [ metadata . areaCode , metadata . phoneNumber ] . filter ( Boolean ) . join ( "" ) ) ;
45
+ /** Checks if both the area code and phone number match the validation pattern */
46
+ const pattern = ( validations as any ) [ metadata . isoCode as keyof typeof validations ] [ Number ( strict ) ] ;
47
+ return new RegExp ( pattern ) . test ( [ metadata . areaCode , metadata . phoneNumber ] . filter ( Boolean ) . join ( "" ) ) ;
48
48
}
49
49
50
50
export const getDefaultISO2Code = ( ) => {
51
- /** Returns the default ISO2 code, based on the user's timezone */
52
- return ( timezones [ Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone as keyof typeof timezones ] || "" ) || "us" ;
51
+ /** Returns the default ISO2 code, based on the user's timezone */
52
+ return ( timezones [ Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone as keyof typeof timezones ] || "" ) || "us" ;
53
53
}
54
54
55
55
export const parsePhoneNumber = ( formattedNumber : string , countriesList : typeof countries = countries , country : any = null ) : PhoneNumber => {
56
- const value = getRawValue ( formattedNumber ) ;
57
- const isoCode = getMetadata ( value , countriesList , country ) ?. [ 0 ] || getDefaultISO2Code ( ) ;
58
- const countryCodePattern = / \+ \d + / ;
59
- const areaCodePattern = / \( ( \d + ) \) / ;
56
+ const value = getRawValue ( formattedNumber ) ;
57
+ const isoCode = getMetadata ( value , countriesList , country ) ?. [ 0 ] || getDefaultISO2Code ( ) ;
58
+ const countryCodePattern = / \+ \d + / ;
59
+ const areaCodePattern = / \( ( \d + ) \) / ;
60
60
61
- /** Parses the matching partials of the phone number by predefined regex patterns */
62
- const countryCodeMatch = formattedNumber ? ( formattedNumber . match ( countryCodePattern ) || [ ] ) : [ ] ;
63
- const areaCodeMatch = formattedNumber ? ( formattedNumber . match ( areaCodePattern ) || [ ] ) : [ ] ;
61
+ /** Parses the matching partials of the phone number by predefined regex patterns */
62
+ const countryCodeMatch = formattedNumber ? ( formattedNumber . match ( countryCodePattern ) || [ ] ) : [ ] ;
63
+ const areaCodeMatch = formattedNumber ? ( formattedNumber . match ( areaCodePattern ) || [ ] ) : [ ] ;
64
64
65
- /** Converts the parsed values of the country and area codes to integers if values present */
66
- const countryCode = countryCodeMatch . length > 0 ? parseInt ( countryCodeMatch [ 0 ] ) : null ;
67
- const areaCode = areaCodeMatch . length > 1 ? areaCodeMatch [ 1 ] : null ;
65
+ /** Converts the parsed values of the country and area codes to integers if values present */
66
+ const countryCode = countryCodeMatch . length > 0 ? parseInt ( countryCodeMatch [ 0 ] ) : null ;
67
+ const areaCode = areaCodeMatch . length > 1 ? areaCodeMatch [ 1 ] : null ;
68
68
69
- /** Parses the phone number by removing the country and area codes from the formatted value */
70
- const phoneNumberPattern = new RegExp ( `^${ countryCode } ${ ( areaCode || "" ) } (\\d+)` ) ;
71
- const phoneNumberMatch = value ? ( value . match ( phoneNumberPattern ) || [ ] ) : [ ] ;
72
- const phoneNumber = phoneNumberMatch . length > 1 ? phoneNumberMatch [ 1 ] : null ;
69
+ /** Parses the phone number by removing the country and area codes from the formatted value */
70
+ const phoneNumberPattern = new RegExp ( `^${ countryCode } ${ ( areaCode || "" ) } (\\d+)` ) ;
71
+ const phoneNumberMatch = value ? ( value . match ( phoneNumberPattern ) || [ ] ) : [ ] ;
72
+ const phoneNumber = phoneNumberMatch . length > 1 ? phoneNumberMatch [ 1 ] : null ;
73
73
74
- return { countryCode, areaCode, phoneNumber, isoCode} ;
74
+ return { countryCode, areaCode, phoneNumber, isoCode} ;
75
+ }
76
+
77
+ export const useMask = ( pattern : string ) => {
78
+ const backRef = useRef < boolean > ( false ) ;
79
+
80
+ const clean = useCallback ( ( input : any ) => {
81
+ return cleanInput ( input , pattern . replaceAll ( / \d / g, "." ) ) ;
82
+ } , [ pattern ] )
83
+
84
+ const first = useMemo ( ( ) => {
85
+ return [ ...pattern ] . findIndex ( c => slots . has ( c ) ) ;
86
+ } , [ pattern ] )
87
+
88
+ const prev = useMemo ( ( j = 0 ) => {
89
+ return Array . from ( pattern . replaceAll ( / \d / g, "." ) , ( c , i ) => {
90
+ return slots . has ( c ) ? j = i + 1 : j ;
91
+ } ) ;
92
+ } , [ pattern ] )
93
+
94
+ const onKeyDown = useCallback ( ( event : KeyboardEvent < HTMLInputElement > ) => {
95
+ backRef . current = event . key === "Backspace" ;
96
+ } , [ ] )
97
+
98
+ const onInput = useCallback ( ( { target} : ChangeEvent < HTMLInputElement > ) => {
99
+ const [ i , j ] = [ target . selectionStart , target . selectionEnd ] . map ( ( i : any ) => {
100
+ i = clean ( target . value . slice ( 0 , i ) ) . findIndex ( c => slots . has ( c ) ) ;
101
+ return i < 0 ? prev [ prev . length - 1 ] : backRef . current ? prev [ i - 1 ] || first : i ;
102
+ } ) ;
103
+ target . value = getFormattedNumber ( target . value , pattern ) ;
104
+ target . setSelectionRange ( i , j ) ;
105
+ backRef . current = false ;
106
+ } , [ clean , first , pattern , prev ] )
107
+
108
+ return {
109
+ onInput,
110
+ onKeyDown,
111
+ }
75
112
}
76
113
77
114
export const usePhone = ( {
78
- query = "" ,
79
- country = "" ,
80
- countryCode = "" ,
81
- initialValue = "" ,
82
- onlyCountries = [ ] ,
83
- excludeCountries = [ ] ,
84
- preferredCountries = [ ] ,
85
- } : usePhoneOptions ) => {
86
- const defaultValue = getRawValue ( initialValue ) ;
87
- const defaultMetadata = getMetadata ( defaultValue ) || countries . find ( ( [ iso ] ) => iso === country ) ;
88
- const defaultValueState = defaultValue || countries . find ( ( [ iso ] ) => iso === defaultMetadata ?. [ 0 ] ) ?. [ 2 ] as string ;
89
-
90
- const backRef = useRef < boolean > ( false ) ;
91
- const [ value , setValue ] = useState < string > ( defaultValueState ) ;
92
-
93
- const countriesOnly = useMemo ( ( ) => {
94
- const allowList = onlyCountries . length > 0 ? onlyCountries : countries . map ( ( [ iso ] ) => iso ) ;
95
- return countries . map ( ( [ iso ] ) => iso ) . filter ( ( iso ) => {
96
- return allowList . includes ( iso ) && ! excludeCountries . includes ( iso ) ;
97
- } ) ;
98
- } , [ onlyCountries , excludeCountries ] )
99
-
100
- const countriesList = useMemo ( ( ) => {
101
- const filteredCountries = countries . filter ( ( [ iso , name , _1 , dial ] ) => {
102
- return countriesOnly . includes ( iso ) && (
103
- name . toLowerCase ( ) . startsWith ( query . toLowerCase ( ) ) || dial . includes ( query )
104
- ) ;
105
- } ) ;
106
- return [
107
- ...filteredCountries . filter ( ( [ iso ] ) => preferredCountries . includes ( iso ) ) ,
108
- ...filteredCountries . filter ( ( [ iso ] ) => ! preferredCountries . includes ( iso ) ) ,
109
- ] ;
110
- } , [ countriesOnly , preferredCountries , query ] )
111
-
112
- const metadata = useMemo ( ( ) => {
113
- const calculatedMetadata = getMetadata ( getRawValue ( value ) , countriesList , countryCode ) ;
114
- if ( countriesList . find ( ( [ iso ] ) => iso === calculatedMetadata ?. [ 0 ] || iso === defaultMetadata ?. [ 0 ] ) ) {
115
- return calculatedMetadata || defaultMetadata ;
116
- }
117
- return countriesList [ 0 ] ;
118
- } , [ countriesList , countryCode , defaultMetadata , value ] )
119
-
120
- const pattern = useMemo ( ( ) => {
121
- return metadata ?. [ 3 ] || defaultMetadata ?. [ 3 ] || "" ;
122
- } , [ defaultMetadata , metadata ] )
123
-
124
- const clean = useCallback ( ( input : any ) => {
125
- return cleanInput ( input , pattern . replaceAll ( / \d / g, "." ) ) ;
126
- } , [ pattern ] )
127
-
128
- const first = useMemo ( ( ) => {
129
- return [ ...pattern ] . findIndex ( c => slots . has ( c ) ) ;
130
- } , [ pattern ] )
131
-
132
- const prev = useMemo ( ( j = 0 ) => {
133
- return Array . from ( pattern . replaceAll ( / \d / g, "." ) , ( c , i ) => {
134
- return slots . has ( c ) ? j = i + 1 : j ;
135
- } ) ;
136
- } , [ pattern ] )
137
-
138
- const format = useCallback ( ( { target} : ChangeEvent < HTMLInputElement > ) => {
139
- const [ i , j ] = [ target . selectionStart , target . selectionEnd ] . map ( ( i : any ) => {
140
- i = clean ( target . value . slice ( 0 , i ) ) . findIndex ( c => slots . has ( c ) ) ;
141
- return i < 0 ? prev [ prev . length - 1 ] : backRef . current ? prev [ i - 1 ] || first : i ;
142
- } ) ;
143
- target . value = getFormattedNumber ( target . value , pattern ) ;
144
- target . setSelectionRange ( i , j ) ;
145
- backRef . current = false ;
146
- setValue ( target . value ) ;
147
- } , [ clean , first , pattern , prev ] )
148
-
149
- return {
150
- clean,
151
- value,
152
- format,
153
- metadata,
154
- setValue,
155
- countriesList,
156
- }
115
+ query = "" ,
116
+ country = "" ,
117
+ countryCode = "" ,
118
+ initialValue = "" ,
119
+ onlyCountries = [ ] ,
120
+ excludeCountries = [ ] ,
121
+ preferredCountries = [ ] ,
122
+ } : usePhoneOptions ) => {
123
+ const defaultValue = getRawValue ( initialValue ) ;
124
+ const defaultMetadata = getMetadata ( defaultValue ) || countries . find ( ( [ iso ] ) => iso === country ) ;
125
+ const defaultValueState = defaultValue || countries . find ( ( [ iso ] ) => iso === defaultMetadata ?. [ 0 ] ) ?. [ 2 ] as string ;
126
+
127
+ const [ value , setValue ] = useState < string > ( defaultValueState ) ;
128
+
129
+ const countriesOnly = useMemo ( ( ) => {
130
+ const allowList = onlyCountries . length > 0 ? onlyCountries : countries . map ( ( [ iso ] ) => iso ) ;
131
+ return countries . map ( ( [ iso ] ) => iso ) . filter ( ( iso ) => {
132
+ return allowList . includes ( iso ) && ! excludeCountries . includes ( iso ) ;
133
+ } ) ;
134
+ } , [ onlyCountries , excludeCountries ] )
135
+
136
+ const countriesList = useMemo ( ( ) => {
137
+ const filteredCountries = countries . filter ( ( [ iso , name , _1 , dial ] ) => {
138
+ return countriesOnly . includes ( iso ) && (
139
+ name . toLowerCase ( ) . startsWith ( query . toLowerCase ( ) ) || dial . includes ( query )
140
+ ) ;
141
+ } ) ;
142
+ return [
143
+ ...filteredCountries . filter ( ( [ iso ] ) => preferredCountries . includes ( iso ) ) ,
144
+ ...filteredCountries . filter ( ( [ iso ] ) => ! preferredCountries . includes ( iso ) ) ,
145
+ ] ;
146
+ } , [ countriesOnly , preferredCountries , query ] )
147
+
148
+ const metadata = useMemo ( ( ) => {
149
+ const calculatedMetadata = getMetadata ( getRawValue ( value ) , countriesList , countryCode ) ;
150
+ if ( countriesList . find ( ( [ iso ] ) => iso === calculatedMetadata ?. [ 0 ] || iso === defaultMetadata ?. [ 0 ] ) ) {
151
+ return calculatedMetadata || defaultMetadata ;
152
+ }
153
+ return countriesList [ 0 ] ;
154
+ } , [ countriesList , countryCode , defaultMetadata , value ] )
155
+
156
+ const pattern = useMemo ( ( ) => {
157
+ return metadata ?. [ 3 ] || defaultMetadata ?. [ 3 ] || "" ;
158
+ } , [ defaultMetadata , metadata ] )
159
+
160
+ return {
161
+ value,
162
+ pattern,
163
+ metadata,
164
+ setValue,
165
+ countriesList,
166
+ }
157
167
}
0 commit comments