@bagelink/vue 1.2.63 → 1.2.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/experimentalGenTypedRoutes.ts +309 -0
- package/dist/components/Alert.vue.d.ts +2 -0
- package/dist/components/Alert.vue.d.ts.map +1 -1
- package/dist/components/Carousel.vue.d.ts +12 -3
- package/dist/components/Carousel.vue.d.ts.map +1 -1
- package/dist/components/Carousel2.vue.d.ts +89 -0
- package/dist/components/Carousel2.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/EmailInput.vue.d.ts +48 -0
- package/dist/components/form/inputs/EmailInput.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/TelInput.vue.d.ts +60 -0
- package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/TextInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/index.d.ts +2 -1
- package/dist/components/form/inputs/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/composables/useDevice.d.ts.map +1 -1
- package/dist/composables/useSchemaField.d.ts.map +1 -1
- package/dist/directives/pattern.d.ts.map +1 -1
- package/dist/index.cjs +11370 -10336
- package/dist/index.mjs +11371 -10337
- package/dist/style.css +1839 -1701
- package/dist/utils/BagelFormUtils.d.ts +7 -0
- package/dist/utils/BagelFormUtils.d.ts.map +1 -1
- package/dist/utils/constants.d.ts +1 -0
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/search.d.ts +2 -2
- package/dist/utils/search.d.ts.map +1 -1
- package/package.json +7 -3
- package/src/components/Alert.vue +29 -7
- package/src/components/Carousel.vue +8 -0
- package/src/components/Carousel2.vue +1012 -0
- package/src/components/form/inputs/EmailInput.vue +476 -0
- package/src/components/form/inputs/SelectInput.vue +2 -2
- package/src/components/form/inputs/TelInput.vue +210 -350
- package/src/components/form/inputs/TextInput.vue +1 -1
- package/src/components/form/inputs/index.ts +2 -1
- package/src/components/index.ts +1 -0
- package/src/composables/useDevice.ts +1 -0
- package/src/composables/useSchemaField.ts +3 -1
- package/src/directives/pattern.ts +3 -2
- package/src/styles/inputs.css +137 -138
- package/src/styles/layout.css +1466 -1459
- package/src/styles/mobilLayout.css +11 -0
- package/src/utils/BagelFormUtils.ts +24 -0
- package/src/utils/constants.ts +1 -0
- package/src/utils/search.ts +3 -3
- package/src/components/form/inputs/PhoneInput.vue +0 -352
|
@@ -1,359 +1,199 @@
|
|
|
1
|
-
<script lang="ts"
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
import type { Country } from '@bagelink/vue'
|
|
3
|
-
import type { CountryCode
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
Dropdown,
|
|
7
|
-
Flag,
|
|
8
|
-
Icon,
|
|
9
|
-
TextInput,
|
|
10
|
-
allCountries,
|
|
11
|
-
useDebounceFn
|
|
12
|
-
} from '@bagelink/vue'
|
|
3
|
+
import type { CountryCode } from 'libphonenumber-js'
|
|
4
|
+
import { Dropdown, Flag, Icon, TextInput, allCountries } from '@bagelink/vue'
|
|
13
5
|
import axios from 'axios'
|
|
14
6
|
import { parsePhoneNumberFromString } from 'libphonenumber-js'
|
|
15
|
-
import { onMounted, watch, ref
|
|
7
|
+
import { onMounted, watch, ref } from 'vue'
|
|
16
8
|
|
|
17
|
-
|
|
18
|
-
label?: string
|
|
9
|
+
const props = defineProps<{
|
|
19
10
|
id?: string
|
|
20
|
-
|
|
21
|
-
autocomplete?: 'on' | 'off' | 'tel'
|
|
22
|
-
placeholder?: string
|
|
23
|
-
required?: boolean
|
|
11
|
+
label?: string
|
|
24
12
|
modelValue: string
|
|
25
|
-
|
|
26
|
-
autoFormat?: boolean
|
|
27
|
-
customValidate?: boolean | RegExp
|
|
28
|
-
defaultCountry?: string | number
|
|
29
|
-
disabled?: boolean
|
|
30
|
-
searchable?: boolean
|
|
31
|
-
autoDefaultCountry?: boolean
|
|
32
|
-
inputOptions?: Partial<typeof defaultInputOptions>
|
|
33
|
-
dropdownOptions?: Partial<typeof defaultDropdownOptions>
|
|
13
|
+
placeholder?: string
|
|
34
14
|
excludeCountries?: string[]
|
|
35
|
-
mode?: NumberFormat
|
|
36
15
|
onlyCountries?: string[]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
16
|
+
required?: boolean
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
}>()
|
|
40
19
|
|
|
41
|
-
const
|
|
42
|
-
modelValue: '',
|
|
43
|
-
autocomplete: 'on',
|
|
44
|
-
allCountries: () => allCountries,
|
|
45
|
-
autoFormat: true,
|
|
46
|
-
customValidate: false,
|
|
47
|
-
defaultCountry: '',
|
|
48
|
-
placeholder: 'Enter a phone number',
|
|
49
|
-
searchable: true,
|
|
50
|
-
disabled: false,
|
|
51
|
-
autoDefaultCountry: true,
|
|
52
|
-
excludeCountries: () => [],
|
|
53
|
-
required: false,
|
|
54
|
-
mode: 'INTERNATIONAL',
|
|
55
|
-
onlyCountries: () => [],
|
|
56
|
-
preferredCountries: () => [],
|
|
57
|
-
showDropdown: true,
|
|
58
|
-
})
|
|
20
|
+
const emit = defineEmits(['update:modelValue', 'blur', 'focus', 'keydown', 'input', 'paste'])
|
|
59
21
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'enter',
|
|
68
|
-
'space',
|
|
69
|
-
'debounce',
|
|
70
|
-
])
|
|
71
|
-
|
|
72
|
-
const open = ref(false)
|
|
73
|
-
const dropdownOpenDirection = ref('below')
|
|
74
|
-
|
|
75
|
-
const defaultDropdownOptions = {
|
|
76
|
-
hide: false,
|
|
77
|
-
disabled: false,
|
|
78
|
-
showFlags: true,
|
|
79
|
-
showDialCodeInSelection: true,
|
|
80
|
-
showDialCodeInList: true,
|
|
81
|
-
searchable: true,
|
|
82
|
-
}
|
|
22
|
+
let phoneNumber = $ref(props.modelValue)
|
|
23
|
+
let search = $ref('')
|
|
24
|
+
let activeCountry = $ref<Country>()
|
|
25
|
+
const searchInput = $ref<InstanceType<typeof TextInput>>()
|
|
26
|
+
let open = $ref(false)
|
|
27
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
28
|
+
let isValid = $ref(true)
|
|
83
29
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
'
|
|
87
|
-
|
|
88
|
-
'id': '',
|
|
89
|
-
'maxlength': 25,
|
|
90
|
-
'minlength': 9,
|
|
91
|
-
'pattern': undefined as string | undefined,
|
|
92
|
-
'name': '',
|
|
93
|
-
'readonly': false,
|
|
94
|
-
'tabindex': 0,
|
|
95
|
-
'style': '' as Raw<StyleValue>,
|
|
96
|
-
// 'placeholder': 'Enter a phone number',
|
|
97
|
-
}
|
|
30
|
+
// Watch for changes to phoneNumber and emit update events
|
|
31
|
+
watch(() => phoneNumber, (newValue) => {
|
|
32
|
+
emit('update:modelValue', newValue)
|
|
33
|
+
})
|
|
98
34
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const computedInputOptions = $computed(() => ({
|
|
105
|
-
...defaultInputOptions,
|
|
106
|
-
...props.inputOptions,
|
|
107
|
-
}))
|
|
108
|
-
|
|
109
|
-
// Composables
|
|
110
|
-
function useCountrySelection(props: TelInputProps, emit: any) {
|
|
111
|
-
const activeCountryCode = ref<CountryCode>()
|
|
112
|
-
const selectedIndex = ref<number>()
|
|
113
|
-
const searchQuery = ref('')
|
|
114
|
-
|
|
115
|
-
const activeCountry = computed(() => props.allCountries?.find(
|
|
116
|
-
country => country.iso2 === activeCountryCode.value?.toUpperCase(),
|
|
117
|
-
))
|
|
118
|
-
|
|
119
|
-
const filteredCountries = computed(() => {
|
|
120
|
-
const countries = props.allCountries || []
|
|
121
|
-
const onlyCountries = props.onlyCountries || []
|
|
122
|
-
const excludeCountries = props.excludeCountries || []
|
|
123
|
-
|
|
124
|
-
if (onlyCountries.length > 0) {
|
|
125
|
-
return countries.filter(({ iso2 }) => onlyCountries.some(c => c.toUpperCase() === iso2))
|
|
126
|
-
}
|
|
127
|
-
if (excludeCountries.length > 0) {
|
|
128
|
-
return countries.filter(
|
|
129
|
-
({ iso2 }) => !excludeCountries.includes(iso2.toUpperCase())
|
|
130
|
-
&& !excludeCountries.includes(iso2.toLowerCase()),
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
return countries
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
const sortedCountries = computed(() => {
|
|
137
|
-
const preferredCountries = getCountries(props.preferredCountries || [])
|
|
138
|
-
const countriesList = [...preferredCountries, ...filteredCountries.value]
|
|
139
|
-
const cleanInput = searchQuery.value.replaceAll(
|
|
140
|
-
/[~`!#$%&*()+={};:'"<>.,\\/@-]/g,
|
|
141
|
-
'',
|
|
142
|
-
).toLowerCase()
|
|
143
|
-
return countriesList
|
|
144
|
-
.filter(
|
|
145
|
-
c => new RegExp(cleanInput, 'i').test(c.name || '')
|
|
146
|
-
|| new RegExp(cleanInput, 'i').test(c.iso2 || '')
|
|
147
|
-
|| new RegExp(cleanInput, 'i').test(c.dialCode || ''),
|
|
148
|
-
)
|
|
149
|
-
.filter(Boolean)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
const findCountry = (iso: string) => filteredCountries.value.find(
|
|
153
|
-
country => country.iso2 === iso.toUpperCase()
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
const findCountryByDialCode = (dialCode: number) => filteredCountries.value.find((country: Country) => Number(country.dialCode) === dialCode)
|
|
157
|
-
|
|
158
|
-
function getCountries(list: string[]): Country[] {
|
|
159
|
-
const countryList: Country[] = []
|
|
160
|
-
list.forEach((countryCode) => {
|
|
161
|
-
const country = findCountry(countryCode)
|
|
162
|
-
if (country) countryList.push(country)
|
|
163
|
-
})
|
|
164
|
-
return countryList
|
|
35
|
+
// Watch for changes to the modelValue prop
|
|
36
|
+
watch(() => props.modelValue, (newValue) => {
|
|
37
|
+
if (newValue !== phoneNumber) {
|
|
38
|
+
phoneNumber = newValue
|
|
165
39
|
}
|
|
40
|
+
})
|
|
166
41
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
if (typeof props.defaultCountry === 'number') {
|
|
174
|
-
const country = findCountryByDialCode(props.defaultCountry)
|
|
175
|
-
if (country) {
|
|
176
|
-
chooseCountry(country.iso2)
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const fallbackCountry = sortedCountries.value[0]
|
|
183
|
-
|
|
184
|
-
if (props.autoDefaultCountry) {
|
|
185
|
-
try {
|
|
186
|
-
const res = (await getIp()).country
|
|
187
|
-
chooseCountry(res || activeCountryCode.value)
|
|
188
|
-
} catch (error) {
|
|
189
|
-
console.warn(error)
|
|
190
|
-
chooseCountry(fallbackCountry.iso2)
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
chooseCountry(fallbackCountry.iso2)
|
|
194
|
-
}
|
|
42
|
+
const countries = $computed(() => {
|
|
43
|
+
let filteredCountries = allCountries
|
|
44
|
+
if (props.excludeCountries && props.excludeCountries.length) {
|
|
45
|
+
const excludeCountries = props.excludeCountries.map(c => c.toLowerCase())
|
|
46
|
+
filteredCountries = filteredCountries.filter(c => !excludeCountries.includes(c.iso2.toLowerCase()))
|
|
195
47
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const parsedCountry = findCountry(country)
|
|
200
|
-
if (!parsedCountry) return
|
|
201
|
-
activeCountryCode.value = parsedCountry.iso2
|
|
202
|
-
emit('country-changed', parsedCountry)
|
|
203
|
-
open.value = false
|
|
48
|
+
if (props.onlyCountries && props.onlyCountries.length) {
|
|
49
|
+
const onlyCountries = props.onlyCountries.map(c => c.toLowerCase())
|
|
50
|
+
filteredCountries = filteredCountries.filter(c => onlyCountries.includes(c.iso2.toLowerCase()))
|
|
204
51
|
}
|
|
52
|
+
if (search.length) {
|
|
53
|
+
const lowerCaseSearch = search.toLowerCase()
|
|
54
|
+
filteredCountries = filteredCountries.filter(c => c.name.toLowerCase().includes(lowerCaseSearch)
|
|
55
|
+
|| c.iso2.toLowerCase().includes(lowerCaseSearch)
|
|
56
|
+
|| c.dialCode.includes(search.replace('+', ''))
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
return filteredCountries
|
|
60
|
+
})
|
|
205
61
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
searchQuery,
|
|
222
|
-
sortedCountries,
|
|
223
|
-
chooseCountry,
|
|
224
|
-
initializeCountry,
|
|
225
|
-
findCountry,
|
|
226
|
-
findCountryByDialCode
|
|
62
|
+
const activeCountryCode = $computed(() => activeCountry?.iso2)
|
|
63
|
+
|
|
64
|
+
function selectCountry(country: Country) {
|
|
65
|
+
activeCountry = country
|
|
66
|
+
open = false
|
|
67
|
+
search = ''
|
|
68
|
+
if (!phoneNumber) phoneNumber = `+${activeCountry.dialCode}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function getIp() {
|
|
72
|
+
let apiData = sessionStorage.getItem('ipapi')
|
|
73
|
+
if (!apiData) {
|
|
74
|
+
apiData = (await axios.get('https://ipapi.co/json/')).data
|
|
75
|
+
apiData = JSON.stringify(apiData)
|
|
76
|
+
sessionStorage.setItem('ipapi', apiData)
|
|
227
77
|
}
|
|
78
|
+
const { country_code } = JSON.parse(apiData)
|
|
79
|
+
selectCountry(countries.find(c => c.iso2 === country_code) ?? countries[0])
|
|
228
80
|
}
|
|
229
81
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}))
|
|
235
|
-
|
|
236
|
-
function formatPhone(val: string): string {
|
|
237
|
-
// First, try to parse the number as is
|
|
238
|
-
let phoneNumber = parsePhoneNumberFromString(val, parseArgs.value)
|
|
239
|
-
|
|
240
|
-
// If parsing failed and there's a + at the start, try removing any existing country code
|
|
241
|
-
if (!phoneNumber && val.startsWith('+')) {
|
|
242
|
-
const currentCountry = props.allCountries?.find(c => c.iso2 === activeCountryCode.value)
|
|
243
|
-
if (currentCountry) {
|
|
244
|
-
// Remove the current country code if it exists
|
|
245
|
-
const { dialCode } = currentCountry
|
|
246
|
-
const withoutDialCode = val.replace(new RegExp(`^\\+${dialCode}`), '')
|
|
247
|
-
// Try parsing again with the cleaned number
|
|
248
|
-
phoneNumber = parsePhoneNumberFromString(withoutDialCode, parseArgs.value)
|
|
249
|
-
}
|
|
250
|
-
}
|
|
82
|
+
// Get the country code for use with libphonenumber-js
|
|
83
|
+
function getCountryCode(): CountryCode | undefined {
|
|
84
|
+
return activeCountry?.iso2 ? activeCountry.iso2.toUpperCase() as CountryCode : undefined
|
|
85
|
+
}
|
|
251
86
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
)?.dialCode || ''
|
|
87
|
+
// Parse and format the phone number
|
|
88
|
+
function parseAndFormatPhoneNumber(value: string): string {
|
|
89
|
+
if (!value) return value
|
|
256
90
|
|
|
257
|
-
|
|
258
|
-
|
|
91
|
+
try {
|
|
92
|
+
const parsedNumber = parsePhoneNumberFromString(value, getCountryCode())
|
|
93
|
+
if (parsedNumber && parsedNumber.isValid()) {
|
|
94
|
+
return parsedNumber.formatInternational()
|
|
259
95
|
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Error parsing phone number:', error)
|
|
98
|
+
}
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
260
101
|
|
|
261
|
-
|
|
262
|
-
|
|
102
|
+
// Validate the phone number and set custom validity
|
|
103
|
+
function validatePhoneNumber() {
|
|
104
|
+
if (!inputRef.value) return
|
|
263
105
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
106
|
+
try {
|
|
107
|
+
const parsedNumber = parsePhoneNumberFromString(phoneNumber, getCountryCode())
|
|
108
|
+
if (parsedNumber && parsedNumber.isValid()) {
|
|
109
|
+
inputRef.value.setCustomValidity('')
|
|
110
|
+
isValid = true
|
|
111
|
+
} else {
|
|
112
|
+
inputRef.value.setCustomValidity('Please enter a valid phone number')
|
|
113
|
+
isValid = false
|
|
269
114
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return {
|
|
275
|
-
formatPhone,
|
|
276
|
-
parseArgs
|
|
115
|
+
} catch (error) {
|
|
116
|
+
inputRef.value.setCustomValidity('Please enter a valid phone number')
|
|
117
|
+
isValid = false
|
|
277
118
|
}
|
|
278
119
|
}
|
|
279
120
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const {
|
|
283
|
-
activeCountryCode,
|
|
284
|
-
selectedIndex,
|
|
285
|
-
searchQuery,
|
|
286
|
-
sortedCountries,
|
|
287
|
-
chooseCountry,
|
|
288
|
-
initializeCountry
|
|
289
|
-
} = useCountrySelection(props, emit)
|
|
121
|
+
function detectCountryFromNumber(value: string): boolean {
|
|
122
|
+
if (!value.startsWith('+')) return false
|
|
290
123
|
|
|
291
|
-
const
|
|
124
|
+
const digits = value.replace(/\D/g, '')
|
|
125
|
+
if (digits.length <= 1) return false
|
|
292
126
|
|
|
293
|
-
|
|
127
|
+
for (const country of countries) {
|
|
128
|
+
if (digits.startsWith(country.dialCode) && country !== activeCountry) {
|
|
129
|
+
selectCountry(country)
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
294
135
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
(
|
|
300
|
-
|
|
301
|
-
|
|
136
|
+
async function initializeCountry() {
|
|
137
|
+
if (phoneNumber) {
|
|
138
|
+
detectCountryFromNumber(phoneNumber)
|
|
139
|
+
} else {
|
|
140
|
+
await getIp()
|
|
141
|
+
}
|
|
142
|
+
const formatted = parseAndFormatPhoneNumber(phoneNumber)
|
|
143
|
+
if (formatted !== phoneNumber) {
|
|
144
|
+
phoneNumber = formatted
|
|
145
|
+
emit('input', phoneNumber)
|
|
146
|
+
validatePhoneNumber()
|
|
147
|
+
}
|
|
148
|
+
validatePhoneNumber()
|
|
302
149
|
}
|
|
303
150
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
151
|
+
function handlePhoneInput(event: Event) {
|
|
152
|
+
const input = event.target as HTMLInputElement
|
|
153
|
+
const { value } = input
|
|
154
|
+
detectCountryFromNumber(value)
|
|
155
|
+
|
|
156
|
+
if (value.startsWith('+')) {
|
|
157
|
+
const formatted = parseAndFormatPhoneNumber(value)
|
|
158
|
+
if (formatted !== value) {
|
|
159
|
+
phoneNumber = formatted
|
|
160
|
+
emit('input', event)
|
|
161
|
+
validatePhoneNumber()
|
|
162
|
+
return
|
|
314
163
|
}
|
|
164
|
+
}
|
|
315
165
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
get: value => value,
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
function onBlur() { emit('blur') }
|
|
324
|
-
function onFocus() { emit('focus') }
|
|
325
|
-
function onEnter() { emit('enter') }
|
|
326
|
-
function onSpace() { emit('space') }
|
|
327
|
-
|
|
328
|
-
function handleInput(e: KeyboardEvent) {
|
|
329
|
-
const keyVal = (e.key as string | undefined) ?? ''
|
|
330
|
-
if (keyVal.length > 1 || e.metaKey) return
|
|
331
|
-
|
|
332
|
-
const hasBadChars = /[^+\d]/.test(keyVal)
|
|
166
|
+
phoneNumber = value
|
|
167
|
+
emit('input', event)
|
|
168
|
+
validatePhoneNumber()
|
|
169
|
+
}
|
|
333
170
|
|
|
334
|
-
|
|
171
|
+
function handleBlur(event: Event) {
|
|
172
|
+
if (phoneNumber && !phoneNumber.startsWith('+') && activeCountry) {
|
|
173
|
+
const nationalNumber = phoneNumber.replace(/^0+/, '')
|
|
174
|
+
phoneNumber = `+${activeCountry.dialCode} ${nationalNumber}`
|
|
175
|
+
} else if (phoneNumber) {
|
|
176
|
+
phoneNumber = parseAndFormatPhoneNumber(phoneNumber)
|
|
177
|
+
}
|
|
335
178
|
|
|
336
|
-
|
|
179
|
+
validatePhoneNumber()
|
|
180
|
+
emit('blur', event)
|
|
337
181
|
}
|
|
338
182
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return data
|
|
183
|
+
const disableDropdown = $computed(() => countries.length === 1 && !search)
|
|
184
|
+
const searchable = $computed(() => countries.length > 7 || search)
|
|
185
|
+
|
|
186
|
+
function focusSearchInput() {
|
|
187
|
+
setTimeout(() => searchInput?.focus(), 100)
|
|
345
188
|
}
|
|
346
189
|
|
|
347
190
|
onMounted(initializeCountry)
|
|
348
191
|
</script>
|
|
349
192
|
|
|
350
193
|
<template>
|
|
351
|
-
<div
|
|
352
|
-
class="bagel-input"
|
|
353
|
-
:class="{ disabled, [props.class || '']: true }"
|
|
354
|
-
>
|
|
194
|
+
<div class="bagel-input text-input" :class="{ invalid: !isValid }">
|
|
355
195
|
<label>
|
|
356
|
-
{{ label }}
|
|
196
|
+
{{ label }} <span v-if="required && label">*</span>
|
|
357
197
|
<div
|
|
358
198
|
dir="ltr"
|
|
359
199
|
class="flex gap-05 tel-input"
|
|
@@ -361,27 +201,24 @@ onMounted(initializeCountry)
|
|
|
361
201
|
aria-label="Country Code Selector"
|
|
362
202
|
aria-haspopup="listbox"
|
|
363
203
|
:aria-expanded="open"
|
|
364
|
-
@keydown.esc="reset"
|
|
365
|
-
@keydown.tab="reset"
|
|
366
204
|
>
|
|
367
205
|
<Dropdown
|
|
368
206
|
v-model:shown="open"
|
|
369
207
|
placement="bottom-start"
|
|
370
|
-
:disabled="
|
|
208
|
+
:disabled="disableDropdown"
|
|
209
|
+
@show="focusSearchInput"
|
|
371
210
|
>
|
|
372
211
|
<template #trigger>
|
|
373
|
-
<span class="flex gap-05">
|
|
374
|
-
<Icon :icon="open ? 'collapse_all' : 'expand_all'" />
|
|
375
|
-
<Flag
|
|
376
|
-
v-if="computedDropDownOptions.showFlags && activeCountryCode"
|
|
377
|
-
:country="activeCountryCode"
|
|
378
|
-
/>
|
|
212
|
+
<span class="flex gap-05 country-code-display">
|
|
213
|
+
<Icon v-if="!disableDropdown" :icon="open ? 'collapse_all' : 'expand_all'" />
|
|
214
|
+
<Flag v-if="activeCountryCode" :country="activeCountryCode" />
|
|
379
215
|
</span>
|
|
380
216
|
</template>
|
|
381
217
|
<div class="p-075 tel-countryp-dropdown">
|
|
382
218
|
<TextInput
|
|
383
219
|
v-if="searchable"
|
|
384
|
-
|
|
220
|
+
ref="searchInput"
|
|
221
|
+
v-model="search"
|
|
385
222
|
aria-label="Search by country name or country code"
|
|
386
223
|
placeholder="Search"
|
|
387
224
|
icon="search"
|
|
@@ -389,55 +226,44 @@ onMounted(initializeCountry)
|
|
|
389
226
|
|
|
390
227
|
<ul
|
|
391
228
|
class="overflow-y p-0 max-h-300px"
|
|
392
|
-
:class="dropdownOpenDirection"
|
|
393
229
|
role="listbox"
|
|
394
230
|
>
|
|
395
231
|
<li
|
|
396
|
-
v-for="(pb
|
|
397
|
-
:key="pb.iso2
|
|
232
|
+
v-for="(pb) in countries"
|
|
233
|
+
:key="pb.iso2"
|
|
398
234
|
role="option"
|
|
399
235
|
class="flex gap-075 pointer hover"
|
|
400
236
|
tabindex="-1"
|
|
401
|
-
:aria-selected="activeCountryCode === pb.iso2
|
|
402
|
-
"
|
|
403
|
-
@click="chooseCountry(pb.iso2)"
|
|
404
|
-
@mousemove="selectedIndex = index"
|
|
237
|
+
:aria-selected="activeCountryCode === pb.iso2"
|
|
238
|
+
@click="selectCountry(pb)"
|
|
405
239
|
>
|
|
406
|
-
<Flag
|
|
407
|
-
v-if="computedDropDownOptions.showFlags"
|
|
408
|
-
:country="pb.iso2"
|
|
409
|
-
/>
|
|
240
|
+
<Flag :country="pb.iso2" />
|
|
410
241
|
<p class="tel-country">{{ pb.name }}</p>
|
|
411
|
-
<span
|
|
242
|
+
<span>
|
|
412
243
|
+{{ pb.dialCode }}
|
|
413
244
|
</span>
|
|
414
245
|
</li>
|
|
415
246
|
</ul>
|
|
416
247
|
</div>
|
|
417
248
|
</Dropdown>
|
|
418
|
-
|
|
419
|
-
<!-- ref="refInput" -->
|
|
420
249
|
<input
|
|
421
250
|
:id="id"
|
|
422
|
-
|
|
251
|
+
ref="inputRef"
|
|
252
|
+
v-model="phoneNumber"
|
|
253
|
+
v-pattern.tel
|
|
423
254
|
:required="required"
|
|
424
|
-
:placeholder="
|
|
255
|
+
:placeholder="placeholder || label || 'Phone Number'"
|
|
425
256
|
:disabled="disabled"
|
|
426
257
|
type="tel"
|
|
427
|
-
|
|
428
|
-
:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
@blur="onBlur"
|
|
437
|
-
@focus="onFocus"
|
|
438
|
-
@keyup.enter="onEnter"
|
|
439
|
-
@keyup.space="onSpace"
|
|
440
|
-
@keydown="handleInput"
|
|
258
|
+
autocomplete="tel"
|
|
259
|
+
:name="id"
|
|
260
|
+
tabindex="0"
|
|
261
|
+
class="national-number-input"
|
|
262
|
+
@blur="handleBlur($event)"
|
|
263
|
+
@focus="emit('focus', $event)"
|
|
264
|
+
@keydown="emit('keydown', $event)"
|
|
265
|
+
@input="handlePhoneInput($event)"
|
|
266
|
+
@paste="emit('paste', $event)"
|
|
441
267
|
>
|
|
442
268
|
</div>
|
|
443
269
|
</label>
|
|
@@ -455,6 +281,8 @@ onMounted(initializeCountry)
|
|
|
455
281
|
color: var(--input-color);
|
|
456
282
|
min-width: calc(var(--input-height) * 3);
|
|
457
283
|
width: 100%;
|
|
284
|
+
display: flex;
|
|
285
|
+
align-items: center;
|
|
458
286
|
}
|
|
459
287
|
|
|
460
288
|
.tel-input:focus-within {
|
|
@@ -464,15 +292,23 @@ onMounted(initializeCountry)
|
|
|
464
292
|
|
|
465
293
|
.tel-input input {
|
|
466
294
|
background: transparent;
|
|
295
|
+
text-align: left;
|
|
296
|
+
flex: 1;
|
|
467
297
|
}
|
|
468
298
|
|
|
469
299
|
.tel-input input:focus-visible {
|
|
470
300
|
box-shadow: none;
|
|
471
301
|
}
|
|
472
302
|
|
|
473
|
-
.
|
|
303
|
+
.country-code-display {
|
|
304
|
+
align-items: center;
|
|
305
|
+
white-space: nowrap;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.dial-code {
|
|
474
309
|
font-size: var(--input-font-size);
|
|
475
310
|
color: var(--input-color);
|
|
311
|
+
opacity: 0.6;
|
|
476
312
|
}
|
|
477
313
|
|
|
478
314
|
.tel-country {
|
|
@@ -489,4 +325,28 @@ onMounted(initializeCountry)
|
|
|
489
325
|
direction: ltr;
|
|
490
326
|
text-align: left;
|
|
491
327
|
}
|
|
328
|
+
|
|
329
|
+
.national-number-input {
|
|
330
|
+
margin-left: 4px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.country-changed {
|
|
334
|
+
animation: highlight-country 1.5s ease-in-out;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.invalid input {
|
|
338
|
+
border-color: var(--error-color, red);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
@keyframes highlight-country {
|
|
342
|
+
0%, 100% {
|
|
343
|
+
background-color: transparent;
|
|
344
|
+
}
|
|
345
|
+
30% {
|
|
346
|
+
background-color: var(--primary-color-light, rgba(0, 123, 255, 0.2));
|
|
347
|
+
}
|
|
348
|
+
70% {
|
|
349
|
+
background-color: var(--primary-color-light, rgba(0, 123, 255, 0.2));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
492
352
|
</style>
|