@bagelink/vue 1.0.47 → 1.0.50
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/dist/components/form/FieldArray.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/PhoneInput.vue.d.ts +116 -0
- package/dist/components/form/inputs/PhoneInput.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/index.d.ts +1 -1
- package/dist/components/form/inputs/index.d.ts.map +1 -1
- package/dist/composables/index.d.ts +2 -2
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/directives/pattern.d.ts.map +1 -1
- package/dist/index.cjs +9774 -9897
- package/dist/index.mjs +9774 -9897
- package/dist/style.css +206 -177
- package/dist/types/BagelForm.d.ts +1 -1
- package/dist/types/BagelForm.d.ts.map +1 -1
- package/dist/types/TableSchema.d.ts +1 -1
- package/dist/types/TableSchema.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/DataTable/useTableData.ts +2 -2
- package/src/components/form/FieldArray.vue +3 -2
- package/src/components/form/inputs/PhoneInput.vue +352 -0
- package/src/components/form/inputs/index.ts +1 -1
- package/src/composables/index.ts +8 -15
- package/src/directives/pattern.ts +1 -0
- package/src/styles/inputs.css +139 -140
- package/src/types/BagelForm.ts +1 -1
- package/src/types/TableSchema.ts +1 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Country } from '@bagelink/vue'
|
|
3
|
+
import type { CountryCode } from 'libphonenumber-js'
|
|
4
|
+
import { Dropdown, Flag, Icon, TextInput, allCountries } from '@bagelink/vue'
|
|
5
|
+
import axios from 'axios'
|
|
6
|
+
import { parsePhoneNumberFromString } from 'libphonenumber-js'
|
|
7
|
+
import { onMounted, watch, ref } from 'vue'
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
id?: string
|
|
11
|
+
label?: string
|
|
12
|
+
modelValue: string
|
|
13
|
+
placeholder?: string
|
|
14
|
+
excludeCountries?: string[]
|
|
15
|
+
onlyCountries?: string[]
|
|
16
|
+
required?: boolean
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits(['update:modelValue', 'blur', 'focus', 'keydown', 'input', 'paste'])
|
|
21
|
+
|
|
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)
|
|
29
|
+
|
|
30
|
+
// Watch for changes to phoneNumber and emit update events
|
|
31
|
+
watch(() => phoneNumber, (newValue) => {
|
|
32
|
+
emit('update:modelValue', newValue)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Watch for changes to the modelValue prop
|
|
36
|
+
watch(() => props.modelValue, (newValue) => {
|
|
37
|
+
if (newValue !== phoneNumber) {
|
|
38
|
+
phoneNumber = newValue
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
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()))
|
|
47
|
+
}
|
|
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()))
|
|
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)
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
return filteredCountries
|
|
60
|
+
})
|
|
61
|
+
|
|
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)
|
|
77
|
+
}
|
|
78
|
+
const { country_code } = JSON.parse(apiData)
|
|
79
|
+
selectCountry(countries.find(c => c.iso2 === country_code) ?? countries[0])
|
|
80
|
+
}
|
|
81
|
+
|
|
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
|
+
}
|
|
86
|
+
|
|
87
|
+
// Parse and format the phone number
|
|
88
|
+
function parseAndFormatPhoneNumber(value: string): string {
|
|
89
|
+
if (!value) return value
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const parsedNumber = parsePhoneNumberFromString(value, getCountryCode())
|
|
93
|
+
if (parsedNumber && parsedNumber.isValid()) {
|
|
94
|
+
return parsedNumber.formatInternational()
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Error parsing phone number:', error)
|
|
98
|
+
}
|
|
99
|
+
return value
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Validate the phone number and set custom validity
|
|
103
|
+
function validatePhoneNumber() {
|
|
104
|
+
if (!inputRef.value) return
|
|
105
|
+
|
|
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
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
inputRef.value.setCustomValidity('Please enter a valid phone number')
|
|
117
|
+
isValid = false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function detectCountryFromNumber(value: string): boolean {
|
|
122
|
+
if (!value.startsWith('+')) return false
|
|
123
|
+
|
|
124
|
+
const digits = value.replace(/\D/g, '')
|
|
125
|
+
if (digits.length <= 1) return false
|
|
126
|
+
|
|
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
|
+
}
|
|
135
|
+
|
|
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()
|
|
149
|
+
}
|
|
150
|
+
|
|
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
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
phoneNumber = value
|
|
167
|
+
emit('input', event)
|
|
168
|
+
validatePhoneNumber()
|
|
169
|
+
}
|
|
170
|
+
|
|
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
|
+
}
|
|
178
|
+
|
|
179
|
+
validatePhoneNumber()
|
|
180
|
+
emit('blur', event)
|
|
181
|
+
}
|
|
182
|
+
|
|
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)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
onMounted(initializeCountry)
|
|
191
|
+
</script>
|
|
192
|
+
|
|
193
|
+
<template>
|
|
194
|
+
<div class="bagel-input text-input" :class="{ invalid: !isValid }">
|
|
195
|
+
<label>
|
|
196
|
+
{{ label }}
|
|
197
|
+
<div
|
|
198
|
+
dir="ltr"
|
|
199
|
+
class="flex gap-05 tel-input"
|
|
200
|
+
tabindex="-1"
|
|
201
|
+
aria-label="Country Code Selector"
|
|
202
|
+
aria-haspopup="listbox"
|
|
203
|
+
:aria-expanded="open"
|
|
204
|
+
>
|
|
205
|
+
<Dropdown
|
|
206
|
+
v-model:shown="open"
|
|
207
|
+
placement="bottom-start"
|
|
208
|
+
:disabled="disableDropdown"
|
|
209
|
+
@show="focusSearchInput"
|
|
210
|
+
>
|
|
211
|
+
<template #trigger>
|
|
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" />
|
|
215
|
+
</span>
|
|
216
|
+
</template>
|
|
217
|
+
<div class="p-075 tel-countryp-dropdown">
|
|
218
|
+
<TextInput
|
|
219
|
+
v-if="searchable"
|
|
220
|
+
ref="searchInput"
|
|
221
|
+
v-model="search"
|
|
222
|
+
aria-label="Search by country name or country code"
|
|
223
|
+
placeholder="Search"
|
|
224
|
+
icon="search"
|
|
225
|
+
/>
|
|
226
|
+
|
|
227
|
+
<ul
|
|
228
|
+
class="overflow-y p-0 max-h-300px"
|
|
229
|
+
role="listbox"
|
|
230
|
+
>
|
|
231
|
+
<li
|
|
232
|
+
v-for="(pb) in countries"
|
|
233
|
+
:key="pb.iso2"
|
|
234
|
+
role="option"
|
|
235
|
+
class="flex gap-075 pointer hover"
|
|
236
|
+
tabindex="-1"
|
|
237
|
+
:aria-selected="activeCountryCode === pb.iso2"
|
|
238
|
+
@click="selectCountry(pb)"
|
|
239
|
+
>
|
|
240
|
+
<Flag :country="pb.iso2" />
|
|
241
|
+
<p class="tel-country">{{ pb.name }}</p>
|
|
242
|
+
<span>
|
|
243
|
+
+{{ pb.dialCode }}
|
|
244
|
+
</span>
|
|
245
|
+
</li>
|
|
246
|
+
</ul>
|
|
247
|
+
</div>
|
|
248
|
+
</Dropdown>
|
|
249
|
+
<input
|
|
250
|
+
:id="id"
|
|
251
|
+
ref="inputRef"
|
|
252
|
+
v-model="phoneNumber"
|
|
253
|
+
v-pattern.tel
|
|
254
|
+
:required="required"
|
|
255
|
+
:placeholder="placeholder || label || 'Phone Number'"
|
|
256
|
+
:disabled="disabled"
|
|
257
|
+
type="tel"
|
|
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)"
|
|
267
|
+
>
|
|
268
|
+
</div>
|
|
269
|
+
</label>
|
|
270
|
+
</div>
|
|
271
|
+
</template>
|
|
272
|
+
|
|
273
|
+
<style scoped>
|
|
274
|
+
.tel-input {
|
|
275
|
+
direction: ltr;
|
|
276
|
+
text-align: left;
|
|
277
|
+
background: var(--input-bg);
|
|
278
|
+
border: none;
|
|
279
|
+
padding-inline-start: 0.7rem;
|
|
280
|
+
border-radius: var(--input-border-radius);
|
|
281
|
+
color: var(--input-color);
|
|
282
|
+
min-width: calc(var(--input-height) * 3);
|
|
283
|
+
width: 100%;
|
|
284
|
+
display: flex;
|
|
285
|
+
align-items: center;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.tel-input:focus-within {
|
|
289
|
+
outline: none;
|
|
290
|
+
box-shadow: inset 0 0 10px #00000012;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.tel-input input {
|
|
294
|
+
background: transparent;
|
|
295
|
+
text-align: left;
|
|
296
|
+
flex: 1;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.tel-input input:focus-visible {
|
|
300
|
+
box-shadow: none;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.country-code-display {
|
|
304
|
+
align-items: center;
|
|
305
|
+
white-space: nowrap;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.dial-code {
|
|
309
|
+
font-size: var(--input-font-size);
|
|
310
|
+
color: var(--input-color);
|
|
311
|
+
opacity: 0.6;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.tel-country {
|
|
315
|
+
font-size: var(--input-font-size);
|
|
316
|
+
max-width: 200px;
|
|
317
|
+
white-space: nowrap;
|
|
318
|
+
text-overflow: ellipsis;
|
|
319
|
+
overflow: hidden;
|
|
320
|
+
margin-top: 0;
|
|
321
|
+
margin-bottom: 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.tel-countryp-dropdown {
|
|
325
|
+
direction: ltr;
|
|
326
|
+
text-align: left;
|
|
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
|
+
}
|
|
352
|
+
</style>
|
|
@@ -10,6 +10,7 @@ export { default as JSONInput } from './JSONInput.vue'
|
|
|
10
10
|
export { default as NumberInput } from './NumberInput.vue'
|
|
11
11
|
export { default as OTP } from './OTP.vue'
|
|
12
12
|
export { default as PasswordInput } from './PasswordInput.vue'
|
|
13
|
+
export { default as TelInput } from './PhoneInput.vue'
|
|
13
14
|
export { default as RadioGroup } from './RadioGroup.vue'
|
|
14
15
|
export { default as RadioPillsInput } from './RadioPillsInput.vue'
|
|
15
16
|
export { default as RangeInput } from './RangeInput.vue'
|
|
@@ -17,7 +18,6 @@ export { default as RichText } from './RichText/index.vue'
|
|
|
17
18
|
export { default as SelectInput } from './SelectInput.vue'
|
|
18
19
|
export { default as SignaturePad } from './SignaturePad.vue'
|
|
19
20
|
export { default as TableField } from './TableField.vue'
|
|
20
|
-
export { default as TelInput } from './TelInput.vue'
|
|
21
21
|
export { default as TextInput } from './TextInput.vue'
|
|
22
22
|
export { default as ToggleInput } from './ToggleInput.vue'
|
|
23
23
|
export { default as UploadInput } from './Upload/UploadInput.vue'
|
package/src/composables/index.ts
CHANGED
|
@@ -4,29 +4,22 @@ import type { Ref, UnwrapRef } from 'vue'
|
|
|
4
4
|
import { getFallbackSchema } from '@bagelink/vue'
|
|
5
5
|
import { ref, watch } from 'vue'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
interface UseBglSchemaParamsT<T> {
|
|
8
11
|
schema?: BglFormSchemaFnT<T>
|
|
9
12
|
columns?: string[]
|
|
10
13
|
data?: any[]
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
export
|
|
14
|
-
{ schema, columns, data }:
|
|
15
|
-
):
|
|
16
|
+
export function useBglSchema<T = { [key: string]: unknown }>(
|
|
17
|
+
{ schema, columns, data }: UseBglSchemaParamsT<T> = {}
|
|
18
|
+
): BglFormSchemaT<T> {
|
|
16
19
|
let _schema = schema
|
|
17
|
-
|
|
18
|
-
// Handle async schema functions
|
|
19
20
|
if (typeof _schema === 'function') {
|
|
20
|
-
|
|
21
|
-
if (result instanceof Promise) {
|
|
22
|
-
_schema = await result
|
|
23
|
-
} else {
|
|
24
|
-
_schema = result
|
|
25
|
-
}
|
|
26
|
-
} else if (_schema instanceof Promise) {
|
|
27
|
-
_schema = await _schema
|
|
21
|
+
_schema = _schema()
|
|
28
22
|
}
|
|
29
|
-
|
|
30
23
|
if (_schema) {
|
|
31
24
|
return (
|
|
32
25
|
columns && columns.length > 0
|
|
@@ -9,6 +9,7 @@ const DEFAULT_PATTERNS: Record<string, PatternValue> = {
|
|
|
9
9
|
pint: { pattern: /\d/ },
|
|
10
10
|
int: { pattern: /[\d-]/ },
|
|
11
11
|
pnum: { pattern: /[\d.]/ },
|
|
12
|
+
tel: { pattern: /[\d.\-()\s+]/ },
|
|
12
13
|
money: { pattern: /[\d.\s,]/ },
|
|
13
14
|
number: { pattern: /[\d\-.]/ },
|
|
14
15
|
num: { pattern: /[\d\-.]/ },
|