@bagelink/vue 0.0.439 → 0.0.441
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/AccordionItem.vue.d.ts.map +1 -1
- package/dist/components/DataPreview.vue.d.ts +6 -0
- package/dist/components/DataPreview.vue.d.ts.map +1 -1
- package/dist/components/Flag.vue.d.ts +16 -0
- package/dist/components/Flag.vue.d.ts.map +1 -0
- package/dist/components/Modal.vue.d.ts.map +1 -1
- package/dist/components/TableSchema.vue.d.ts +6 -5
- package/dist/components/TableSchema.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/FileUpload.vue.d.ts +164 -2
- package/dist/components/form/inputs/FileUpload.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/TelInput.vue.d.ts +165 -0
- package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/TextInput.vue.d.ts +2 -5
- package/dist/components/form/inputs/TextInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/index.d.ts +1 -0
- 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/components/layout/Tabs.vue.d.ts.map +1 -1
- package/dist/index.cjs +6502 -1540
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +6502 -1540
- package/dist/style.css +1877 -93
- package/dist/utils/BagelFormUtils.d.ts.map +1 -1
- package/dist/utils/allCountries.d.ts +11 -0
- package/dist/utils/allCountries.d.ts.map +1 -0
- package/package.json +3 -4
- package/src/components/AccordionItem.vue +8 -5
- package/src/components/DataPreview.vue +14 -4
- package/src/components/Flag.vue +1349 -0
- package/src/components/Modal.vue +26 -9
- package/src/components/ModalForm.vue +1 -1
- package/src/components/PageTitle.vue +8 -1
- package/src/components/TableSchema.vue +34 -2
- package/src/components/form/inputs/FileUpload.vue +28 -11
- package/src/components/form/inputs/SelectInput.vue +6 -0
- package/src/components/form/inputs/TelInput.vue +401 -0
- package/src/components/form/inputs/TextInput.vue +15 -53
- package/src/components/form/inputs/index.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/components/layout/Tabs.vue +1 -0
- package/src/index.ts +1 -0
- package/src/styles/appearance.css +610 -0
- package/src/styles/buttons.css +8 -0
- package/src/styles/layout.css +46 -1
- package/src/styles/mobilLayout.css +37 -0
- package/src/styles/modal.css +5 -2
- package/src/styles/text.css +39 -0
- package/src/utils/BagelFormUtils.ts +2 -0
- package/src/utils/allCountries.ts +337 -0
- package/src/components/whatsapp/form/MsgTemplate.vue +0 -224
- package/src/components/whatsapp/form/TextVariableExamples.vue +0 -79
- package/src/components/whatsapp/index.ts +0 -2
- package/src/components/whatsapp/interfaces.ts +0 -58
package/src/components/Modal.vue
CHANGED
|
@@ -3,17 +3,30 @@
|
|
|
3
3
|
class="bg-dark"
|
|
4
4
|
:class="{ 'is-side': side, 'is-active': isVisible, 'bg-lignt': false }"
|
|
5
5
|
@click="() => (dismissable ? closeModal() : '')"
|
|
6
|
-
@keydown.esc="closeModal"
|
|
6
|
+
@keydown.esc="closeModal"
|
|
7
|
+
>
|
|
7
8
|
<Card class="modal" @click.stop :style="{ ...maxWidth }">
|
|
8
|
-
<header class="tool-bar">
|
|
9
|
+
<header v-if="slots['toolbar'] || title" class="tool-bar">
|
|
9
10
|
<slot name="toolbar" />
|
|
10
11
|
<Btn
|
|
11
12
|
:style="{ float: side ? 'left' : 'right' }"
|
|
12
13
|
flat
|
|
13
14
|
icon="close"
|
|
14
|
-
@click="closeModal"
|
|
15
|
+
@click="closeModal"
|
|
16
|
+
/>
|
|
15
17
|
<Title class="modal-title" tag="h3" v-if="title" :label="title" />
|
|
16
18
|
</header>
|
|
19
|
+
|
|
20
|
+
<div v-else class="sticky h-10 bg-white z-index-999 -mt-1 -me-075 h-40px pt-025 round">
|
|
21
|
+
<Btn
|
|
22
|
+
class="color-black"
|
|
23
|
+
:style="{ float: side ? 'left' : 'right' }"
|
|
24
|
+
icon="close"
|
|
25
|
+
thin
|
|
26
|
+
color="white"
|
|
27
|
+
@click="closeModal"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
17
30
|
<slot />
|
|
18
31
|
<footer v-if="slots['footer'] || actions?.length" class="modal-footer mt-3">
|
|
19
32
|
<Btn
|
|
@@ -21,7 +34,8 @@
|
|
|
21
34
|
:key="i"
|
|
22
35
|
@click="closeModal"
|
|
23
36
|
color="gray"
|
|
24
|
-
v-bind="action"
|
|
37
|
+
v-bind="action"
|
|
38
|
+
/>
|
|
25
39
|
<slot name="footer" />
|
|
26
40
|
</footer>
|
|
27
41
|
</Card>
|
|
@@ -29,8 +43,12 @@
|
|
|
29
43
|
</template>
|
|
30
44
|
|
|
31
45
|
<script lang="ts" setup>
|
|
32
|
-
import {
|
|
33
|
-
|
|
46
|
+
import {
|
|
47
|
+
onMounted, onUnmounted, watch, useSlots,
|
|
48
|
+
} from 'vue';
|
|
49
|
+
import {
|
|
50
|
+
type BtnOptions, Btn, useEscape, Title, Card,
|
|
51
|
+
} from '@bagelink/vue';
|
|
34
52
|
import '../styles/modal.css';
|
|
35
53
|
|
|
36
54
|
const props = defineProps<{
|
|
@@ -53,7 +71,7 @@ watch(
|
|
|
53
71
|
if (val) openModal();
|
|
54
72
|
else closeModal();
|
|
55
73
|
},
|
|
56
|
-
{ immediate: true }
|
|
74
|
+
{ immediate: true },
|
|
57
75
|
);
|
|
58
76
|
|
|
59
77
|
const maxWidth = $computed(() => {
|
|
@@ -72,8 +90,7 @@ const closeModal = () => {
|
|
|
72
90
|
|
|
73
91
|
defineExpose({ closeModal });
|
|
74
92
|
|
|
75
|
-
const escapeKeyClose = (e: KeyboardEvent) =>
|
|
76
|
-
props?.dismissable && useEscape(e, () => closeModal());
|
|
93
|
+
const escapeKeyClose = (e: KeyboardEvent) => props?.dismissable && useEscape(e, () => closeModal());
|
|
77
94
|
|
|
78
95
|
function openModal() {
|
|
79
96
|
setTimeout(() => (isVisible = true), 1);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="page-top">
|
|
3
|
-
<h1 class="top-title">
|
|
3
|
+
<h1 class="top-title m-0">
|
|
4
4
|
<slot /> {{ value }}
|
|
5
5
|
</h1>
|
|
6
6
|
</div>
|
|
@@ -19,6 +19,7 @@ defineProps({
|
|
|
19
19
|
.top-title {
|
|
20
20
|
font-weight: 600;
|
|
21
21
|
font-size: 20px;
|
|
22
|
+
line-height: 1;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
.page-top {
|
|
@@ -26,4 +27,10 @@ defineProps({
|
|
|
26
27
|
align-items: center;
|
|
27
28
|
gap: 1rem;
|
|
28
29
|
}
|
|
30
|
+
|
|
31
|
+
@media screen and (max-width: 910px) {
|
|
32
|
+
.top-title {
|
|
33
|
+
font-size: 16px;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
29
36
|
</style>
|
|
@@ -71,11 +71,32 @@ const slots = useSlots();
|
|
|
71
71
|
const loading = $ref(true);
|
|
72
72
|
|
|
73
73
|
const props = defineProps<{
|
|
74
|
+
selecteItems?: string[];
|
|
74
75
|
data: any[];
|
|
75
76
|
schema?: BglFormSchemaT | (() => BglFormSchemaT);
|
|
76
77
|
showFields?: string[];
|
|
77
78
|
}>();
|
|
78
79
|
|
|
80
|
+
const emit = defineEmits(['update:selecteItems', 'orderBy', 'select']);
|
|
81
|
+
|
|
82
|
+
let selecteItems = $ref<string[]>(props.selecteItems || []);
|
|
83
|
+
let selected = $computed({
|
|
84
|
+
get: () => selecteItems,
|
|
85
|
+
set: (value: string[]) => {
|
|
86
|
+
selecteItems = value;
|
|
87
|
+
emit('update:selecteItems', value);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
function selectAll(event: PointerEvent) {
|
|
92
|
+
const value = (event.target as HTMLInputElement).checked;
|
|
93
|
+
if (!value) {
|
|
94
|
+
selected = [];
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
selected.push(...computedData.map((d) => d.id));
|
|
98
|
+
}
|
|
99
|
+
|
|
79
100
|
const computedSchema = $computed(() => {
|
|
80
101
|
if (typeof props.schema === 'function') {
|
|
81
102
|
return props.schema();
|
|
@@ -97,8 +118,6 @@ const computedSchema = $computed(() => {
|
|
|
97
118
|
: schema;
|
|
98
119
|
});
|
|
99
120
|
|
|
100
|
-
const emit = defineEmits(['select', 'orderBy']);
|
|
101
|
-
|
|
102
121
|
const selectElement = (item: Record<string, any>) => emit('select', item);
|
|
103
122
|
|
|
104
123
|
let sortDirection = $ref('ASC');
|
|
@@ -131,6 +150,12 @@ const computedData = $computed(() => {
|
|
|
131
150
|
return dta.sort((a, b) => {
|
|
132
151
|
const aValue = a[sortField];
|
|
133
152
|
const bValue = b[sortField];
|
|
153
|
+
const numAValue = parseInt(aValue);
|
|
154
|
+
const numBValue = parseInt(bValue);
|
|
155
|
+
if (!isNaN(numAValue) && !isNaN(numBValue)) {
|
|
156
|
+
if (sortDirection === 'ASC') return numAValue - numBValue;
|
|
157
|
+
return numBValue - numAValue;
|
|
158
|
+
}
|
|
134
159
|
if (typeof aValue === 'string') {
|
|
135
160
|
if (sortDirection === 'ASC') return aValue.localeCompare(bValue);
|
|
136
161
|
return bValue.localeCompare(aValue);
|
|
@@ -197,6 +222,9 @@ th {
|
|
|
197
222
|
height: 50px;
|
|
198
223
|
vertical-align: bottom;
|
|
199
224
|
}
|
|
225
|
+
.row.first-row input[type=checkbox]{
|
|
226
|
+
margin-bottom: 0.7rem;
|
|
227
|
+
}
|
|
200
228
|
|
|
201
229
|
.row.first-row::after {
|
|
202
230
|
content: '';
|
|
@@ -267,6 +295,10 @@ th {
|
|
|
267
295
|
.row-item:hover {
|
|
268
296
|
background: var(--bgl-gray-light);
|
|
269
297
|
}
|
|
298
|
+
.row-item input[type=checkbox]{
|
|
299
|
+
margin-top: 0.45rem !important;
|
|
300
|
+
accent-color: var(--bgl-primary);
|
|
301
|
+
}
|
|
270
302
|
|
|
271
303
|
.infinite-wrapper {
|
|
272
304
|
overflow-y: auto;
|
|
@@ -14,12 +14,16 @@
|
|
|
14
14
|
dragover: isDragOver,
|
|
15
15
|
}"
|
|
16
16
|
>
|
|
17
|
+
<slot name="files" :files="storageFiles" :fileQueue>
|
|
17
18
|
<template v-for="file in storageFiles" :key="file.id">
|
|
18
|
-
<div class="
|
|
19
|
-
<img class="preview" :src="file.url" alt="" />
|
|
19
|
+
<div class="single-image" v-if="!multiple">
|
|
20
|
+
<img class="single preview" :src="file.url" alt="" />
|
|
21
|
+
<!-- <p class="no-margin">
|
|
22
|
+
{{ file.name }}
|
|
23
|
+
</p> -->
|
|
20
24
|
</div>
|
|
21
|
-
<div class="previewName">
|
|
22
|
-
<img
|
|
25
|
+
<div class="multi-image-item previewName" v-if="multiple">
|
|
26
|
+
<img class="preview" :src="file.url" alt="" />
|
|
23
27
|
<p class="no-margin">
|
|
24
28
|
{{ file.name }}
|
|
25
29
|
</p>
|
|
@@ -60,6 +64,10 @@
|
|
|
60
64
|
</div>
|
|
61
65
|
</div>
|
|
62
66
|
</template>
|
|
67
|
+
</slot>
|
|
68
|
+
<slot v-if="(!storageFiles.length && !fileQueue.length) || multiple" name="placeholder">
|
|
69
|
+
<p>Drop files here or click to upload</p>
|
|
70
|
+
</slot>
|
|
63
71
|
</div>
|
|
64
72
|
</div>
|
|
65
73
|
</template>
|
|
@@ -224,8 +232,7 @@ const drop = (e: DragEvent) => {
|
|
|
224
232
|
cursor: pointer;
|
|
225
233
|
transition: var(--bgl-transition);
|
|
226
234
|
position: relative;
|
|
227
|
-
|
|
228
|
-
max-height: 500px;
|
|
235
|
+
height: calc(var(--width) / 1.5);
|
|
229
236
|
font-size: var(--input-font-size);
|
|
230
237
|
overflow-y: auto;
|
|
231
238
|
background: var(--input-bg);
|
|
@@ -256,11 +263,24 @@ const drop = (e: DragEvent) => {
|
|
|
256
263
|
}
|
|
257
264
|
|
|
258
265
|
img.preview {
|
|
259
|
-
|
|
266
|
+
/* max-height: calc(var(--width) / 2); */
|
|
260
267
|
/* max-width: calc(var(--width) * 2); */
|
|
268
|
+
width: 40px;
|
|
269
|
+
height: 40px;
|
|
261
270
|
border-radius: var(--input-border-radius);
|
|
262
271
|
object-fit: cover;
|
|
263
|
-
background: var(--bgl-
|
|
272
|
+
background: var(--bgl-gray-light);
|
|
273
|
+
box-shadow: 0 0 10px #00000012;
|
|
274
|
+
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
img.preview.single{
|
|
278
|
+
max-width: var(--width);
|
|
279
|
+
width: auto;
|
|
280
|
+
object-fit: contain;
|
|
281
|
+
height: auto;
|
|
282
|
+
max-height: calc(var(--width) / 2);
|
|
283
|
+
margin: 1rem;
|
|
264
284
|
}
|
|
265
285
|
|
|
266
286
|
.fileUploadWrap.dragover,
|
|
@@ -276,9 +296,6 @@ img.preview {
|
|
|
276
296
|
color: var(--bgl-gray);
|
|
277
297
|
}
|
|
278
298
|
|
|
279
|
-
.bagel-input .fileUploadWrap.fileDropZone:after {
|
|
280
|
-
content: 'Drop files here or click to upload';
|
|
281
|
-
}
|
|
282
299
|
|
|
283
300
|
.pie {
|
|
284
301
|
width: 30px;
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="bagel-input" :class="{ disabled: disabled }">
|
|
3
|
+
<label>
|
|
4
|
+
{{ label }}
|
|
5
|
+
<div
|
|
6
|
+
dir="ltr"
|
|
7
|
+
class="flex gap-05 tel-input"
|
|
8
|
+
tabindex="-1"
|
|
9
|
+
aria-label="Country Code Selector"
|
|
10
|
+
aria-haspopup="listbox"
|
|
11
|
+
:aria-expanded="open"
|
|
12
|
+
@keydown.esc="reset"
|
|
13
|
+
@keydown.tab="reset"
|
|
14
|
+
>
|
|
15
|
+
<Dropdown :placement="'bottom'" @hide="open = false">
|
|
16
|
+
<span class="flex gap-05" @click="open = true">
|
|
17
|
+
<Icon :icon="open ? 'collapse_all' : 'expand_all'" />
|
|
18
|
+
<Flag
|
|
19
|
+
v-if="dropdownOptions.showFlags && activeCountryCode"
|
|
20
|
+
:country-code="activeCountryCode?.toLowerCase()"
|
|
21
|
+
/>
|
|
22
|
+
</span>
|
|
23
|
+
<template #popper="{ hide }">
|
|
24
|
+
<div class="p-075 tel-countryp-dropdown">
|
|
25
|
+
<TextInput
|
|
26
|
+
v-if="searchable"
|
|
27
|
+
aria-label="Search by country name or country code"
|
|
28
|
+
placeholder="Search"
|
|
29
|
+
icon="search"
|
|
30
|
+
v-model="searchQuery"
|
|
31
|
+
/>
|
|
32
|
+
<ul
|
|
33
|
+
ref="listEl"
|
|
34
|
+
class="overflow-y p-0"
|
|
35
|
+
:style="{ 'max-height': '400px' }"
|
|
36
|
+
:class="dropdownOpenDirection"
|
|
37
|
+
role="listbox"
|
|
38
|
+
>
|
|
39
|
+
<li
|
|
40
|
+
v-for="(pb, index) in sortedCountries"
|
|
41
|
+
role="option"
|
|
42
|
+
class="flex gap-075"
|
|
43
|
+
:key="pb.iso2 + isPreferred(pb)"
|
|
44
|
+
tabindex="-1"
|
|
45
|
+
@click="
|
|
46
|
+
() => {
|
|
47
|
+
chooseCountry(pb.iso2);
|
|
48
|
+
hide();
|
|
49
|
+
}
|
|
50
|
+
"
|
|
51
|
+
@mousemove="selectedIndex = index"
|
|
52
|
+
:aria-selected="activeCountryCode === pb.iso2 && !isPreferred(pb)"
|
|
53
|
+
>
|
|
54
|
+
<Flag v-if="dropdownOptions.showFlags" :country-code="pb.iso2.toLowerCase()" />
|
|
55
|
+
<p class="tel-country">{{ pb.name }}</p>
|
|
56
|
+
<span v-if="dropdownOptions.showDialCodeInList"> +{{ pb.dialCode }} </span>
|
|
57
|
+
</li>
|
|
58
|
+
</ul>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
</Dropdown>
|
|
62
|
+
<input
|
|
63
|
+
:required="required"
|
|
64
|
+
:placeholder="placeholder"
|
|
65
|
+
:disabled="disabled"
|
|
66
|
+
v-model="phone"
|
|
67
|
+
type="tel"
|
|
68
|
+
ref="refInput"
|
|
69
|
+
:autocomplete="autocomplete"
|
|
70
|
+
:class="['vti__input']"
|
|
71
|
+
:id="id"
|
|
72
|
+
:maxlength="inputOptions.maxlength"
|
|
73
|
+
:name="inputOptions.name"
|
|
74
|
+
:readonly="inputOptions.readonly"
|
|
75
|
+
:tabindex="inputOptions.tabindex"
|
|
76
|
+
:aria-describedby="inputOptions['aria-describedby']"
|
|
77
|
+
@blur="onBlur"
|
|
78
|
+
@focus="onFocus"
|
|
79
|
+
@keyup.enter="onEnter"
|
|
80
|
+
@keyup.space="onSpace"
|
|
81
|
+
>
|
|
82
|
+
</div>
|
|
83
|
+
</label>
|
|
84
|
+
</div>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<script lang="ts" setup>
|
|
88
|
+
import {
|
|
89
|
+
Icon, Dropdown, TextInput, Flag,
|
|
90
|
+
allCountries, type Country,
|
|
91
|
+
} from '@bagelink/vue';
|
|
92
|
+
import { onMounted, watch } from 'vue';
|
|
93
|
+
import { parsePhoneNumberFromString, type CountryCode, type PhoneNumber } from 'libphonenumber-js';
|
|
94
|
+
import axios from 'axios';
|
|
95
|
+
|
|
96
|
+
async function getIp() {
|
|
97
|
+
const apiData = sessionStorage?.getItem('ipapi');
|
|
98
|
+
if (apiData) return JSON.parse(apiData);
|
|
99
|
+
const { data } = await axios.get('https://ipapi.co/json/');
|
|
100
|
+
sessionStorage.setItem('ipapi', JSON.stringify(data));
|
|
101
|
+
return data;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type DropdownOptions = {
|
|
105
|
+
disabled?: boolean;
|
|
106
|
+
showFlags?: boolean;
|
|
107
|
+
showDialCodeInSelection?: boolean;
|
|
108
|
+
showDialCodeInList?: boolean;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type InputOptions = {
|
|
112
|
+
autofocus?: boolean;
|
|
113
|
+
'aria-describedby'?: string;
|
|
114
|
+
maxlength?: number;
|
|
115
|
+
name?: string;
|
|
116
|
+
placeholder?: string;
|
|
117
|
+
readonly?: boolean;
|
|
118
|
+
tabindex?: number;
|
|
119
|
+
showDialCode?: boolean;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type Props = {
|
|
123
|
+
label?: string;
|
|
124
|
+
id?: string;
|
|
125
|
+
autocomplete?: 'on' | 'off';
|
|
126
|
+
placeholder?: string;
|
|
127
|
+
required?: boolean;
|
|
128
|
+
modelValue: string;
|
|
129
|
+
allCountries?: Country[];
|
|
130
|
+
autoFormat?: boolean;
|
|
131
|
+
customValidate?: boolean | RegExp;
|
|
132
|
+
defaultCountry?: string | number;
|
|
133
|
+
disabled?: boolean;
|
|
134
|
+
searchable?: boolean;
|
|
135
|
+
autoDefaultCountry?: boolean;
|
|
136
|
+
dropdownOptions?: DropdownOptions;
|
|
137
|
+
excludeCountries?: string[];
|
|
138
|
+
inputOptions?: InputOptions;
|
|
139
|
+
invalidMsg?: string;
|
|
140
|
+
mode?: 'auto' | 'international' | 'national';
|
|
141
|
+
onlyCountries?: string[];
|
|
142
|
+
preferredCountries?: string[];
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
146
|
+
modelValue: '',
|
|
147
|
+
autocomplete: 'on',
|
|
148
|
+
allCountries: () => allCountries,
|
|
149
|
+
autoFormat: true,
|
|
150
|
+
customValidate: false,
|
|
151
|
+
defaultCountry: '',
|
|
152
|
+
placeholder: 'Enter a phone number',
|
|
153
|
+
searchable: true,
|
|
154
|
+
dropdownOptions: () => ({
|
|
155
|
+
disabled: false,
|
|
156
|
+
showFlags: true,
|
|
157
|
+
showDialCodeInSelection: true,
|
|
158
|
+
showDialCodeInList: true,
|
|
159
|
+
searchable: true,
|
|
160
|
+
}),
|
|
161
|
+
disabled: false,
|
|
162
|
+
autoDefaultCountry: true,
|
|
163
|
+
excludeCountries: () => [],
|
|
164
|
+
inputOptions: () => ({
|
|
165
|
+
showDialCode: true,
|
|
166
|
+
autofocus: false,
|
|
167
|
+
'aria-describedby': '',
|
|
168
|
+
id: '',
|
|
169
|
+
maxlength: 25,
|
|
170
|
+
name: '',
|
|
171
|
+
readonly: false,
|
|
172
|
+
tabindex: 0,
|
|
173
|
+
}),
|
|
174
|
+
required: false,
|
|
175
|
+
invalidMsg: 'Invalid phone number',
|
|
176
|
+
mode: 'auto',
|
|
177
|
+
onlyCountries: () => [],
|
|
178
|
+
preferredCountries: () => [],
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
let activeCountryCode = $ref<CountryCode>();
|
|
182
|
+
let open = $ref(false);
|
|
183
|
+
let selectedIndex = $ref<number>();
|
|
184
|
+
const dropdownOpenDirection = $ref('below');
|
|
185
|
+
const searchQuery = $ref('');
|
|
186
|
+
|
|
187
|
+
const refInput = $ref<HTMLInputElement | null>(null);
|
|
188
|
+
|
|
189
|
+
const emit = defineEmits([
|
|
190
|
+
'update:modelValue',
|
|
191
|
+
'change',
|
|
192
|
+
'input',
|
|
193
|
+
'blur',
|
|
194
|
+
'focus',
|
|
195
|
+
'country-changed',
|
|
196
|
+
'enter',
|
|
197
|
+
'space',
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
function formatPhone(val: string) {
|
|
201
|
+
let result: PhoneNumber | undefined;
|
|
202
|
+
if (val?.[0] === '+') result = parsePhoneNumberFromString(val);
|
|
203
|
+
else result = parsePhoneNumberFromString(val, activeCountryCode);
|
|
204
|
+
const valid = result?.isValid?.();
|
|
205
|
+
refInput?.setCustomValidity?.(valid ? '' : props.invalidMsg);
|
|
206
|
+
const formatted = valid ? result?.format?.(parsedMode.toUpperCase() as any) : val;
|
|
207
|
+
return formatted;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const cleanStr = (str?: string) => {
|
|
211
|
+
const phoneNumber = (str?.match(/[()\-+0-9\s]*/g) || []).join('');
|
|
212
|
+
return phoneNumber.trim();
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
let phone = $computed<string>({
|
|
216
|
+
get: () => props.modelValue,
|
|
217
|
+
set: (value) => {
|
|
218
|
+
const clean = cleanStr(value);
|
|
219
|
+
const formatted = formatPhone(clean) || '';
|
|
220
|
+
emit('update:modelValue', formatted);
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Computed: activeCountry
|
|
225
|
+
const activeCountry = $computed(() => props.allCountries.find((country) => country.iso2 === activeCountryCode?.toUpperCase()));
|
|
226
|
+
|
|
227
|
+
const isPreferred = (country?: Country) => props.preferredCountries.includes(country?.iso2 as string);
|
|
228
|
+
|
|
229
|
+
// Computed: parsedMode
|
|
230
|
+
const parsedMode = $computed(() => {
|
|
231
|
+
if (props.mode === 'auto') {
|
|
232
|
+
if (!phone || phone[0] !== '+') return 'national';
|
|
233
|
+
return 'international';
|
|
234
|
+
}
|
|
235
|
+
return props.mode || 'international';
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const filteredCountries = $computed(() => {
|
|
239
|
+
if (props.onlyCountries.length) {
|
|
240
|
+
return props.allCountries.filter(({ iso2 }) => props.onlyCountries.some((c) => c.toUpperCase() === iso2));
|
|
241
|
+
}
|
|
242
|
+
if (props.excludeCountries.length) {
|
|
243
|
+
return props.allCountries.filter(
|
|
244
|
+
({ iso2 }) => !props.excludeCountries.includes(iso2.toUpperCase()) &&
|
|
245
|
+
!props.excludeCountries.includes(iso2.toLowerCase()),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return props.allCountries;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const sortedCountries = $computed(() => {
|
|
252
|
+
const preferredCountries = getCountries(props.preferredCountries);
|
|
253
|
+
|
|
254
|
+
const countriesList = [...preferredCountries, ...filteredCountries];
|
|
255
|
+
const cleanInput = searchQuery.replace(/[~`!@#$%^&*()+={}\[\];:'"<>.,\/\\\?-_]/g, '');
|
|
256
|
+
return countriesList
|
|
257
|
+
.filter(
|
|
258
|
+
(c) => new RegExp(cleanInput, 'i').test(c?.name || '') ||
|
|
259
|
+
new RegExp(cleanInput, 'i').test(c?.iso2 || '') ||
|
|
260
|
+
new RegExp(cleanInput, 'i').test(c?.dialCode || ''),
|
|
261
|
+
)
|
|
262
|
+
.filter(Boolean);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Watchers
|
|
266
|
+
watch(
|
|
267
|
+
() => activeCountry,
|
|
268
|
+
(value, oldValue) => {
|
|
269
|
+
if (!value && oldValue?.iso2) {
|
|
270
|
+
activeCountryCode = oldValue.iso2;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (value?.iso2) emit('country-changed', value);
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const initializeCountry = async () => {
|
|
278
|
+
// if (phone?.[0] === '+') return;
|
|
279
|
+
// 2. Use default country if passed from parent
|
|
280
|
+
if (props.defaultCountry) {
|
|
281
|
+
if (typeof props.defaultCountry === 'string') {
|
|
282
|
+
chooseCountry(props.defaultCountry);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (typeof props.defaultCountry === 'number') {
|
|
286
|
+
const country = findCountryByDialCode(props.defaultCountry);
|
|
287
|
+
if (country) {
|
|
288
|
+
chooseCountry(country.iso2);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const fallbackCountry = sortedCountries[0];
|
|
295
|
+
|
|
296
|
+
if (props.autoDefaultCountry) {
|
|
297
|
+
try {
|
|
298
|
+
const res = (await getIp()).country;
|
|
299
|
+
chooseCountry(res || activeCountryCode);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.warn(error);
|
|
302
|
+
chooseCountry(fallbackCountry?.iso2);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
// 4. Use the first country from preferred list (if available) or all countries list
|
|
306
|
+
chooseCountry(fallbackCountry?.iso2);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
onMounted(initializeCountry);
|
|
311
|
+
|
|
312
|
+
const findCountry = (iso: string) => filteredCountries.find((country) => country.iso2 === iso?.toUpperCase());
|
|
313
|
+
|
|
314
|
+
const getCountries = (list: string[]): Country[] => {
|
|
315
|
+
const countryList: Country[] = [];
|
|
316
|
+
list.forEach((countryCode) => {
|
|
317
|
+
const country = findCountry(countryCode);
|
|
318
|
+
if (country) countryList.push(country);
|
|
319
|
+
});
|
|
320
|
+
return countryList;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const findCountryByDialCode = (dialCode: number) => filteredCountries.find((country) => Number(country.dialCode) === dialCode);
|
|
324
|
+
|
|
325
|
+
const chooseCountry = (country?: string) => {
|
|
326
|
+
if (!country) return;
|
|
327
|
+
const parsedCountry = findCountry(country);
|
|
328
|
+
if (!parsedCountry) return;
|
|
329
|
+
activeCountryCode = parsedCountry.iso2;
|
|
330
|
+
|
|
331
|
+
if (props.inputOptions?.showDialCode && parsedCountry) {
|
|
332
|
+
phone = `+${parsedCountry.dialCode}`;
|
|
333
|
+
activeCountryCode = parsedCountry.iso2 || '';
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
activeCountryCode = parsedCountry.iso2 || '';
|
|
338
|
+
// emitInput(phone);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const onBlur = () => emit('blur');
|
|
342
|
+
|
|
343
|
+
const onFocus = () => emit('focus');
|
|
344
|
+
|
|
345
|
+
const onEnter = () => emit('enter');
|
|
346
|
+
|
|
347
|
+
const onSpace = () => emit('space');
|
|
348
|
+
|
|
349
|
+
// const focus = () => refInput?.focus();
|
|
350
|
+
|
|
351
|
+
// Method: reset
|
|
352
|
+
const reset = () => {
|
|
353
|
+
selectedIndex = sortedCountries.findIndex((c) => c.iso2 === activeCountryCode);
|
|
354
|
+
open = false;
|
|
355
|
+
};
|
|
356
|
+
</script>
|
|
357
|
+
|
|
358
|
+
<style scoped>
|
|
359
|
+
.tel-input {
|
|
360
|
+
direction: ltr;
|
|
361
|
+
text-align: left;
|
|
362
|
+
background: var(--input-bg);
|
|
363
|
+
border: none;
|
|
364
|
+
padding-inline-start: 0.7rem;
|
|
365
|
+
border-radius: var(--input-border-radius);
|
|
366
|
+
color: var(--input-color);
|
|
367
|
+
min-width: calc(var(--input-height) * 3);
|
|
368
|
+
width: 100%;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.tel-input:focus-within {
|
|
372
|
+
outline: none;
|
|
373
|
+
box-shadow: inset 0 0 10px #00000012;
|
|
374
|
+
}
|
|
375
|
+
.tel-input input {
|
|
376
|
+
background: transparent;
|
|
377
|
+
}
|
|
378
|
+
.tel-input input::placeholder {
|
|
379
|
+
color: transparent;
|
|
380
|
+
}
|
|
381
|
+
.tel-input input:focus-visible {
|
|
382
|
+
box-shadow: none;
|
|
383
|
+
}
|
|
384
|
+
.input_country-code {
|
|
385
|
+
font-size: var(--input-font-size);
|
|
386
|
+
color: var(--input-color);
|
|
387
|
+
}
|
|
388
|
+
.tel-country {
|
|
389
|
+
font-size: var(--input-font-size);
|
|
390
|
+
max-width: 200px;
|
|
391
|
+
white-space: nowrap;
|
|
392
|
+
text-overflow: ellipsis;
|
|
393
|
+
overflow: hidden;
|
|
394
|
+
margin-top: 0;
|
|
395
|
+
margin-bottom: 0;
|
|
396
|
+
}
|
|
397
|
+
.tel-countryp-dropdown {
|
|
398
|
+
direction: ltr;
|
|
399
|
+
text-align: left;
|
|
400
|
+
}
|
|
401
|
+
</style>
|