@bagelink/vue 1.2.65 → 1.2.71

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.
Files changed (46) hide show
  1. package/bin/experimentalGenTypedRoutes.ts +309 -0
  2. package/dist/components/Alert.vue.d.ts +2 -0
  3. package/dist/components/Alert.vue.d.ts.map +1 -1
  4. package/dist/components/Carousel.vue.d.ts +12 -3
  5. package/dist/components/Carousel.vue.d.ts.map +1 -1
  6. package/dist/components/Carousel2.vue.d.ts +89 -0
  7. package/dist/components/Carousel2.vue.d.ts.map +1 -0
  8. package/dist/components/form/inputs/EmailInput.vue.d.ts +48 -0
  9. package/dist/components/form/inputs/EmailInput.vue.d.ts.map +1 -0
  10. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  11. package/dist/components/form/inputs/TelInput.vue.d.ts +60 -0
  12. package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -0
  13. package/dist/components/form/inputs/TextInput.vue.d.ts.map +1 -1
  14. package/dist/components/form/inputs/index.d.ts +2 -1
  15. package/dist/components/form/inputs/index.d.ts.map +1 -1
  16. package/dist/components/index.d.ts +1 -1
  17. package/dist/components/index.d.ts.map +1 -1
  18. package/dist/composables/useDevice.d.ts.map +1 -1
  19. package/dist/composables/useSchemaField.d.ts.map +1 -1
  20. package/dist/directives/pattern.d.ts.map +1 -1
  21. package/dist/index.cjs +11302 -10747
  22. package/dist/index.mjs +11303 -10748
  23. package/dist/style.css +1822 -1755
  24. package/dist/utils/BagelFormUtils.d.ts +7 -0
  25. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  26. package/dist/utils/constants.d.ts +1 -0
  27. package/dist/utils/constants.d.ts.map +1 -1
  28. package/package.json +7 -3
  29. package/src/components/Alert.vue +29 -7
  30. package/src/components/Carousel.vue +8 -0
  31. package/src/components/Carousel2.vue +1012 -0
  32. package/src/components/form/inputs/EmailInput.vue +476 -0
  33. package/src/components/form/inputs/SelectInput.vue +1 -1
  34. package/src/components/form/inputs/TelInput.vue +210 -350
  35. package/src/components/form/inputs/TextInput.vue +1 -1
  36. package/src/components/form/inputs/index.ts +2 -1
  37. package/src/components/index.ts +2 -1
  38. package/src/composables/useDevice.ts +1 -0
  39. package/src/composables/useSchemaField.ts +3 -1
  40. package/src/directives/pattern.ts +3 -2
  41. package/src/styles/inputs.css +137 -138
  42. package/src/styles/layout.css +1466 -1459
  43. package/src/styles/mobilLayout.css +11 -0
  44. package/src/utils/BagelFormUtils.ts +24 -0
  45. package/src/utils/constants.ts +1 -0
  46. package/src/components/form/inputs/PhoneInput.vue +0 -352
@@ -1,359 +1,199 @@
1
- <script lang="ts" setup>
1
+ <script setup lang="ts">
2
2
  import type { Country } from '@bagelink/vue'
3
- import type { CountryCode, NumberFormat } from 'libphonenumber-js'
4
- import type { Raw, StyleValue, Ref } from 'vue'
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, computed } from 'vue'
7
+ import { onMounted, watch, ref } from 'vue'
16
8
 
17
- export interface TelInputProps {
18
- label?: string
9
+ const props = defineProps<{
19
10
  id?: string
20
- class?: string
21
- autocomplete?: 'on' | 'off' | 'tel'
22
- placeholder?: string
23
- required?: boolean
11
+ label?: string
24
12
  modelValue: string
25
- allCountries?: Country[]
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
- preferredCountries?: string[]
38
- parseArg?: { extract?: boolean }
39
- }
16
+ required?: boolean
17
+ disabled?: boolean
18
+ }>()
40
19
 
41
- const props = withDefaults(defineProps<TelInputProps>(), {
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
- const emit = defineEmits([
61
- 'update:modelValue',
62
- 'change',
63
- 'input',
64
- 'blur',
65
- 'focus',
66
- 'country-changed',
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
- const defaultInputOptions = {
85
- 'showDialCode': true,
86
- 'autofocus': false,
87
- 'aria-describedby': '',
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
- const computedDropDownOptions = $computed(() => ({
100
- ...defaultDropdownOptions,
101
- ...props.dropdownOptions,
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
- async function initializeCountry() {
168
- if (props.defaultCountry) {
169
- if (typeof props.defaultCountry === 'string') {
170
- chooseCountry(props.defaultCountry)
171
- return
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
- function chooseCountry(country?: string) {
198
- if (!country) return
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
- watch(
207
- () => activeCountry.value,
208
- (value, oldValue) => {
209
- if (!value && oldValue?.iso2) {
210
- activeCountryCode.value = oldValue.iso2
211
- return
212
- }
213
- if (value?.iso2) emit('country-changed', value)
214
- },
215
- )
216
-
217
- return {
218
- activeCountryCode,
219
- activeCountry,
220
- selectedIndex,
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
- function usePhoneNumberFormatting(props: TelInputProps, activeCountryCode: Ref<CountryCode | undefined>) {
231
- const parseArgs = computed(() => ({
232
- ...props.parseArg,
233
- defaultCountry: activeCountryCode.value,
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
- if (!phoneNumber) {
253
- const dialCode = props.allCountries?.find(
254
- c => c.iso2 === activeCountryCode.value
255
- )?.dialCode || ''
87
+ // Parse and format the phone number
88
+ function parseAndFormatPhoneNumber(value: string): string {
89
+ if (!value) return value
256
90
 
257
- if (props.mode === 'INTERNATIONAL') return `+${dialCode}`
258
- return dialCode
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
- // Format the number according to the selected mode
262
- const formattedNumber = phoneNumber.format(props.mode || 'INTERNATIONAL')
102
+ // Validate the phone number and set custom validity
103
+ function validatePhoneNumber() {
104
+ if (!inputRef.value) return
263
105
 
264
- // For international format, ensure proper formatting with country code
265
- if (props.mode === 'INTERNATIONAL') {
266
- const countryCode = phoneNumber.countryCallingCode
267
- const { nationalNumber } = phoneNumber
268
- return `+${countryCode}${nationalNumber}`
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
- return formattedNumber.replaceAll(' ', '') || ''
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
- const debouncedEmit = useDebounceFn((maybeFormatted: string) => { emit('debounce', maybeFormatted) })
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 { formatPhone } = usePhoneNumberFormatting(props, activeCountryCode)
124
+ const digits = value.replace(/\D/g, '')
125
+ if (digits.length <= 1) return false
292
126
 
293
- const isPreferred = (country?: Country) => props.preferredCountries.includes(country?.iso2 as CountryCode) || false
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
- // Method: reset
296
- function reset() {
297
- if (!sortedCountries.value || !activeCountryCode.value) return
298
- selectedIndex.value = sortedCountries.value.findIndex(
299
- (c: Country) => c.iso2 === activeCountryCode.value
300
- )
301
- open.value = false
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
- const phone = defineModel<string>('modelValue', {
305
- default: '',
306
- set: (value) => {
307
- let maybeFormatted = value
308
- if (value.length > 5) {
309
- maybeFormatted = formatPhone(`${value}`)
310
- if (!maybeFormatted) {
311
- emit('update:modelValue', '')
312
- return ''
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
- emit('update:modelValue', maybeFormatted)
317
- debouncedEmit(maybeFormatted)
318
- return maybeFormatted
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
- if (!hasBadChars) return
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
- e.preventDefault()
179
+ validatePhoneNumber()
180
+ emit('blur', event)
337
181
  }
338
182
 
339
- async function getIp() {
340
- const apiData = sessionStorage.getItem('ipapi')
341
- if (apiData) return JSON.parse(apiData)
342
- const { data } = await axios.get('https://ipapi.co/json/')
343
- sessionStorage.setItem('ipapi', JSON.stringify(data))
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="computedDropDownOptions.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
- v-model="searchQuery"
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, index) in sortedCountries"
397
- :key="pb.iso2 + isPreferred(pb)"
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 && !isPreferred(pb)
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 v-if="computedDropDownOptions.showDialCodeInList">
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
- v-model="phone"
251
+ ref="inputRef"
252
+ v-model="phoneNumber"
253
+ v-pattern.tel
423
254
  :required="required"
424
- :placeholder="props.placeholder || 'Enter a phone number'"
255
+ :placeholder="placeholder || label || 'Phone Number'"
425
256
  :disabled="disabled"
426
257
  type="tel"
427
- :autocomplete="autocomplete"
428
- :pattern="computedInputOptions.pattern"
429
- :minlength="computedInputOptions.minlength"
430
- :maxlength="computedInputOptions.maxlength"
431
- :name="computedInputOptions.name"
432
- :readonly="computedInputOptions.readonly"
433
- :tabindex="computedInputOptions.tabindex"
434
- :aria-describedby="computedInputOptions['aria-describedby']"
435
- :style="computedInputOptions.style"
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
- .input_country-code {
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>
@@ -99,7 +99,7 @@ onMounted(() => {
99
99
  :title="title"
100
100
  >
101
101
  <label :for="id">
102
- {{ label }}
102
+ {{ label }} <span v-if="required && label">*</span>
103
103
 
104
104
  <input
105
105
  v-if="!multiline && !autoheight && !code && inputRows < 2"