@bagelink/vue 0.0.451 → 0.0.458
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/Card.vue.d.ts +2 -2
- package/dist/components/Flag.vue.d.ts +9 -5
- package/dist/components/Flag.vue.d.ts.map +1 -1
- package/dist/components/MapEmbed.vue.d.ts +20 -0
- package/dist/components/MapEmbed.vue.d.ts.map +1 -0
- package/dist/components/Modal.vue.d.ts.map +1 -1
- package/dist/components/TableSchema.vue.d.ts +18 -6
- package/dist/components/TableSchema.vue.d.ts.map +1 -1
- package/dist/components/form/BglField.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/TelInput.vue.d.ts +203 -127
- package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/TextInput.vue.d.ts +5 -0
- package/dist/components/form/inputs/TextInput.vue.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/Layout.vue.d.ts +1 -1
- package/dist/index.cjs +30657 -16732
- package/dist/index.mjs +30657 -16732
- package/dist/plugins/bagel.d.ts +1 -1
- package/dist/plugins/modal.d.ts +1 -1
- package/dist/plugins/modal.d.ts.map +1 -1
- package/dist/style.css +813 -1129
- package/dist/types/BagelForm.d.ts +1 -0
- package/dist/types/BagelForm.d.ts.map +1 -1
- package/package.json +14 -12
- package/src/components/Flag.vue +45 -1341
- package/src/components/MapEmbed.vue +67 -0
- package/src/components/Modal.vue +10 -10
- package/src/components/form/BglField.vue +1 -0
- package/src/components/form/inputs/RichText.vue +2 -2
- package/src/components/form/inputs/SelectInput.vue +7 -8
- package/src/components/form/inputs/TelInput.vue +164 -108
- package/src/components/form/inputs/TextInput.vue +6 -9
- package/src/components/index.ts +1 -0
- package/src/plugins/modal.ts +13 -12
- package/src/styles/appearance.css +54 -19
- package/src/styles/layout.css +5 -0
- package/src/styles/mobilLayout.css +6 -0
- package/src/styles/modal.css +3 -2
- package/src/styles/text.css +32 -3
- package/src/types/BagelForm.ts +1 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="map" id="map" :style="{height: `${height || 400}px`}"/>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import L from 'leaflet';
|
|
7
|
+
import type { LatLngExpression, Map, Marker } from 'leaflet';
|
|
8
|
+
import 'leaflet/dist/leaflet.css';
|
|
9
|
+
import { onMounted, watch } from 'vue';
|
|
10
|
+
|
|
11
|
+
type MapProps = {
|
|
12
|
+
center?: LatLngExpression;
|
|
13
|
+
zoom?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
address?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const props = defineProps<MapProps>();
|
|
19
|
+
|
|
20
|
+
let map = $ref<Map>();
|
|
21
|
+
const markers = $ref<Marker[]>([]);
|
|
22
|
+
|
|
23
|
+
function initializeMap() {
|
|
24
|
+
map = L.map('map', {
|
|
25
|
+
center: props.center || [31.7683, 35.2137],
|
|
26
|
+
zoom: props.zoom || 13,
|
|
27
|
+
});
|
|
28
|
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18 }).addTo(map);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onMounted(initializeMap);
|
|
32
|
+
|
|
33
|
+
const markerSVG = '<svg height="32" width="32" xmlns="http://www.w3.org/2000/svg"><g><path d="m12 0c-4.4183 0-8 3.5817-8 8 0 1.421.3816 2.75 1.0312 3.906.1079.192.221.381.3438.563l6.625 11.531 6.625-11.531c.102-.151.19-.311.281-.469l.063-.094c.649-1.156 1.031-2.485 1.031-3.906 0-4.4183-3.582-8-8-8zm0 4c2.209 0 4 1.7909 4 4 0 2.209-1.791 4-4 4-2.2091 0-4-1.791-4-4 0-2.2091 1.7909-4 4-4z" fill="#e74c3c"/><path d="m12 3c-2.7614 0-5 2.2386-5 5 0 2.761 2.2386 5 5 5 2.761 0 5-2.239 5-5 0-2.7614-2.239-5-5-5zm0 2c1.657 0 3 1.3431 3 3s-1.343 3-3 3-3-1.3431-3-3 1.343-3 3-3z" fill="#c0392b"/></g></svg>';
|
|
34
|
+
|
|
35
|
+
function addMarker(latlng: LatLngExpression) {
|
|
36
|
+
const customIcon = L.icon({
|
|
37
|
+
iconUrl: `data:image/svg+xml;utf8,${encodeURIComponent(markerSVG)}`,
|
|
38
|
+
iconSize: [32, 32],
|
|
39
|
+
});
|
|
40
|
+
const marker = L.marker(latlng, { icon: customIcon }).addTo(map);
|
|
41
|
+
markers.push(marker);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function geocodeAddress(address:string) {
|
|
45
|
+
const geocodeUrl = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`;
|
|
46
|
+
const res = await fetch(geocodeUrl);
|
|
47
|
+
const data = await res.json() || [];
|
|
48
|
+
data.forEach((element) => addMarker([element.lat, element.lon]));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
watch(() => props.address, (address) => {
|
|
52
|
+
if (address) geocodeAddress(address).catch(console.error);
|
|
53
|
+
}, { immediate: true });
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
#map {
|
|
58
|
+
height: 100%;
|
|
59
|
+
}
|
|
60
|
+
.map {
|
|
61
|
+
filter: contrast(0.6) brightness(1.25) hue-rotate(12deg);
|
|
62
|
+
border-radius: var(--input-border-radius);
|
|
63
|
+
}
|
|
64
|
+
.leaflet-control-attribution{
|
|
65
|
+
display: none;
|
|
66
|
+
}
|
|
67
|
+
</style>
|
package/src/components/Modal.vue
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
@click="() => (dismissable ? closeModal() : '')"
|
|
6
6
|
@keydown.esc="closeModal"
|
|
7
7
|
>
|
|
8
|
-
<
|
|
8
|
+
<Card class="modal" @click.stop :style="{ ...maxWidth }">
|
|
9
9
|
<header v-if="slots['toolbar'] || title" class="tool-bar">
|
|
10
10
|
<slot name="toolbar" />
|
|
11
11
|
<Btn
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<Title class="modal-title" tag="h3" v-if="title" :label="title" />
|
|
18
18
|
</header>
|
|
19
19
|
|
|
20
|
-
<div v-else class="sticky
|
|
20
|
+
<div v-else class="sticky bg-white z-index-999 -mt-1 -ms-1 px-025 h-30px pt-025 modal-no-title">
|
|
21
21
|
<Btn
|
|
22
22
|
class="color-black"
|
|
23
23
|
:style="{ float: side ? 'left' : 'right' }"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
/>
|
|
39
39
|
<slot name="footer" />
|
|
40
40
|
</footer>
|
|
41
|
-
</
|
|
41
|
+
</Card>
|
|
42
42
|
</div>
|
|
43
43
|
</template>
|
|
44
44
|
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
onMounted, onUnmounted, watch, useSlots,
|
|
48
48
|
} from 'vue';
|
|
49
49
|
import {
|
|
50
|
-
type BtnOptions, Btn, useEscape, Title,
|
|
50
|
+
type BtnOptions, Btn, useEscape, Title, Card,
|
|
51
51
|
} from '@bagelink/vue';
|
|
52
52
|
import '../styles/modal.css';
|
|
53
53
|
|
|
@@ -82,11 +82,9 @@ const maxWidth = $computed(() => {
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
const emit = defineEmits(['update:visible']);
|
|
85
|
-
const modal = $ref<HTMLDialogElement | null>(null);
|
|
86
85
|
|
|
87
86
|
const closeModal = () => {
|
|
88
87
|
isVisible = false;
|
|
89
|
-
modal?.close();
|
|
90
88
|
setTimeout(() => emit('update:visible', false), 200);
|
|
91
89
|
};
|
|
92
90
|
|
|
@@ -95,10 +93,7 @@ defineExpose({ closeModal });
|
|
|
95
93
|
const escapeKeyClose = (e: KeyboardEvent) => props?.dismissable && useEscape(e, () => closeModal());
|
|
96
94
|
|
|
97
95
|
function openModal() {
|
|
98
|
-
setTimeout(() =>
|
|
99
|
-
modal?.showModal();
|
|
100
|
-
isVisible = true;
|
|
101
|
-
}, 1);
|
|
96
|
+
setTimeout(() => (isVisible = true), 1);
|
|
102
97
|
}
|
|
103
98
|
|
|
104
99
|
onMounted(() => document.addEventListener('keydown', escapeKeyClose));
|
|
@@ -141,4 +136,9 @@ onUnmounted(() => {
|
|
|
141
136
|
align-items: center;
|
|
142
137
|
gap: 0;
|
|
143
138
|
}
|
|
139
|
+
.modal-no-title{
|
|
140
|
+
width: calc(100% + 2rem);
|
|
141
|
+
border-radius: var(--card-border-radius);
|
|
142
|
+
|
|
143
|
+
}
|
|
144
144
|
</style>
|
|
@@ -111,7 +111,7 @@ const config: {
|
|
|
111
111
|
txtField('src', 'YouTube URL'),
|
|
112
112
|
{
|
|
113
113
|
$el: BglVideo,
|
|
114
|
-
attrs: { src: (_, { src }: { src: string }) => src || '' },
|
|
114
|
+
attrs: { src: (_: any, { src }: { src: string }) => src || '' },
|
|
115
115
|
},
|
|
116
116
|
],
|
|
117
117
|
onSubmit: (videoObj) => editor?.commands.setYoutubeVideo(videoObj),
|
|
@@ -198,7 +198,7 @@ const config: {
|
|
|
198
198
|
{
|
|
199
199
|
name: 'Table',
|
|
200
200
|
command: () => editor
|
|
201
|
-
|
|
201
|
+
?.chain()
|
|
202
202
|
.focus()
|
|
203
203
|
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
|
204
204
|
.run(),
|
|
@@ -75,7 +75,9 @@
|
|
|
75
75
|
import { watch } from 'vue';
|
|
76
76
|
import { Dropdown } from 'floating-vue';
|
|
77
77
|
import 'floating-vue/style.css';
|
|
78
|
-
import {
|
|
78
|
+
import {
|
|
79
|
+
TextInput, Card, Icon, type MaterialIcons,
|
|
80
|
+
} from '@bagelink/vue';
|
|
79
81
|
|
|
80
82
|
type Option =
|
|
81
83
|
| string
|
|
@@ -150,11 +152,9 @@ const getValue = (option?: Option) => {
|
|
|
150
152
|
return option.value;
|
|
151
153
|
};
|
|
152
154
|
|
|
153
|
-
const isSelected = (option: Option) =>
|
|
154
|
-
!!selectedItems.find((item) => getValue(option) === getValue(item));
|
|
155
|
+
const isSelected = (option: Option) => !!selectedItems.find((item) => getValue(option) === getValue(item));
|
|
155
156
|
|
|
156
|
-
const filteredOptions = $computed(() =>
|
|
157
|
-
props.options.filter((option) => {
|
|
157
|
+
const filteredOptions = $computed(() => props.options.filter((option) => {
|
|
158
158
|
const searchTerm = search
|
|
159
159
|
.split(/\s+/)
|
|
160
160
|
.filter(Boolean)
|
|
@@ -164,8 +164,7 @@ const filteredOptions = $computed(() =>
|
|
|
164
164
|
(searchTerm.every((s) => getLabel(option).match(s)) ||
|
|
165
165
|
searchTerm.length === 0)
|
|
166
166
|
);
|
|
167
|
-
})
|
|
168
|
-
);
|
|
167
|
+
}));
|
|
169
168
|
|
|
170
169
|
const select = (option: Option) => {
|
|
171
170
|
const existingIndex = selectedItems.findIndex(
|
|
@@ -205,7 +204,7 @@ watch(
|
|
|
205
204
|
(newVal: Option | Option[]) => {
|
|
206
205
|
if (!props.multiselect) {
|
|
207
206
|
const newOption =
|
|
208
|
-
props.options
|
|
207
|
+
props.options?.find((o) => getValue(o) === newVal) || newVal;
|
|
209
208
|
if (newOption && !isSelected(newOption)) selectedItems = [newOption];
|
|
210
209
|
} else {
|
|
211
210
|
const isSame = compareArrays([newVal].flat(), selectedItems);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="bagel-input"
|
|
4
|
+
:class="{ disabled: disabled }"
|
|
5
|
+
>
|
|
3
6
|
<label>
|
|
4
7
|
{{ label }}
|
|
5
8
|
<div
|
|
@@ -12,12 +15,20 @@
|
|
|
12
15
|
@keydown.esc="reset"
|
|
13
16
|
@keydown.tab="reset"
|
|
14
17
|
>
|
|
15
|
-
<Dropdown
|
|
16
|
-
|
|
18
|
+
<Dropdown
|
|
19
|
+
:placement="'bottom'"
|
|
20
|
+
@hide="open = false"
|
|
21
|
+
v-if="!computedDropDownOptions.hide"
|
|
22
|
+
:disabled="computedDropDownOptions.disabled"
|
|
23
|
+
>
|
|
24
|
+
<span
|
|
25
|
+
class="flex gap-05"
|
|
26
|
+
@click="open = true"
|
|
27
|
+
>
|
|
17
28
|
<Icon :icon="open ? 'collapse_all' : 'expand_all'" />
|
|
18
29
|
<Flag
|
|
19
|
-
v-if="
|
|
20
|
-
:country
|
|
30
|
+
v-if="computedDropDownOptions.showFlags && activeCountryCode"
|
|
31
|
+
:country="activeCountryCode"
|
|
21
32
|
/>
|
|
22
33
|
</span>
|
|
23
34
|
<template #popper="{ hide }">
|
|
@@ -29,6 +40,7 @@
|
|
|
29
40
|
icon="search"
|
|
30
41
|
v-model="searchQuery"
|
|
31
42
|
/>
|
|
43
|
+
|
|
32
44
|
<ul
|
|
33
45
|
ref="listEl"
|
|
34
46
|
class="overflow-y p-0"
|
|
@@ -43,25 +55,30 @@
|
|
|
43
55
|
:key="pb.iso2 + isPreferred(pb)"
|
|
44
56
|
tabindex="-1"
|
|
45
57
|
@click="
|
|
46
|
-
()
|
|
47
|
-
|
|
48
|
-
hide();
|
|
49
|
-
}
|
|
58
|
+
chooseCountry(pb.iso2);
|
|
59
|
+
hide();
|
|
50
60
|
"
|
|
51
61
|
@mousemove="selectedIndex = index"
|
|
52
|
-
:aria-selected="activeCountryCode === pb.iso2 && !isPreferred(pb)
|
|
62
|
+
:aria-selected="activeCountryCode === pb.iso2 && !isPreferred(pb)
|
|
63
|
+
"
|
|
53
64
|
>
|
|
54
|
-
<Flag
|
|
65
|
+
<Flag
|
|
66
|
+
v-if="computedDropDownOptions.showFlags"
|
|
67
|
+
:country="pb.iso2"
|
|
68
|
+
/>
|
|
55
69
|
<p class="tel-country">{{ pb.name }}</p>
|
|
56
|
-
<span v-if="
|
|
70
|
+
<span v-if="computedDropDownOptions.showDialCodeInList">
|
|
71
|
+
+{{ pb.dialCode }}
|
|
72
|
+
</span>
|
|
57
73
|
</li>
|
|
58
74
|
</ul>
|
|
59
75
|
</div>
|
|
60
76
|
</template>
|
|
61
77
|
</Dropdown>
|
|
78
|
+
|
|
62
79
|
<input
|
|
63
80
|
:required="required"
|
|
64
|
-
:placeholder="placeholder"
|
|
81
|
+
:placeholder="computedInputOptions.placeholder"
|
|
65
82
|
:disabled="disabled"
|
|
66
83
|
v-model="phone"
|
|
67
84
|
type="tel"
|
|
@@ -69,16 +86,20 @@
|
|
|
69
86
|
:autocomplete="autocomplete"
|
|
70
87
|
:class="['vti__input']"
|
|
71
88
|
:id="id"
|
|
72
|
-
:
|
|
73
|
-
:
|
|
74
|
-
:
|
|
75
|
-
:
|
|
76
|
-
:
|
|
89
|
+
:pattern="computedInputOptions.pattern"
|
|
90
|
+
:minlength="computedInputOptions.minlength"
|
|
91
|
+
:maxlength="computedInputOptions.maxlength"
|
|
92
|
+
:name="computedInputOptions.name"
|
|
93
|
+
:readonly="computedInputOptions.readonly"
|
|
94
|
+
:tabindex="computedInputOptions.tabindex"
|
|
95
|
+
:aria-describedby="computedInputOptions['aria-describedby']"
|
|
77
96
|
@blur="onBlur"
|
|
78
97
|
@focus="onFocus"
|
|
79
98
|
@keyup.enter="onEnter"
|
|
80
99
|
@keyup.space="onSpace"
|
|
81
|
-
|
|
100
|
+
@keydown="handleInput"
|
|
101
|
+
:style="computedInputOptions.style"
|
|
102
|
+
/>
|
|
82
103
|
</div>
|
|
83
104
|
</label>
|
|
84
105
|
</div>
|
|
@@ -86,11 +107,20 @@
|
|
|
86
107
|
|
|
87
108
|
<script lang="ts" setup>
|
|
88
109
|
import {
|
|
89
|
-
Icon,
|
|
90
|
-
|
|
110
|
+
Icon,
|
|
111
|
+
Dropdown,
|
|
112
|
+
TextInput,
|
|
113
|
+
Flag,
|
|
114
|
+
allCountries,
|
|
115
|
+
type Country,
|
|
116
|
+
debounce,
|
|
91
117
|
} from '@bagelink/vue';
|
|
92
|
-
import { onMounted, watch } from 'vue';
|
|
93
|
-
import {
|
|
118
|
+
import { type Raw, type StyleValue, onMounted, watch } from 'vue';
|
|
119
|
+
import {
|
|
120
|
+
parsePhoneNumberFromString,
|
|
121
|
+
type NumberFormat,
|
|
122
|
+
type CountryCode,
|
|
123
|
+
} from 'libphonenumber-js';
|
|
94
124
|
import axios from 'axios';
|
|
95
125
|
|
|
96
126
|
async function getIp() {
|
|
@@ -101,28 +131,34 @@ async function getIp() {
|
|
|
101
131
|
return data;
|
|
102
132
|
}
|
|
103
133
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
const defaultDropdownOptions = {
|
|
135
|
+
hide: false,
|
|
136
|
+
disabled: false,
|
|
137
|
+
showFlags: true,
|
|
138
|
+
showDialCodeInSelection: true,
|
|
139
|
+
showDialCodeInList: true,
|
|
140
|
+
searchable: true,
|
|
109
141
|
};
|
|
110
142
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
143
|
+
const defaultInputOptions = {
|
|
144
|
+
showDialCode: true,
|
|
145
|
+
autofocus: false,
|
|
146
|
+
'aria-describedby': '',
|
|
147
|
+
id: '',
|
|
148
|
+
maxlength: 25,
|
|
149
|
+
minlength: 9,
|
|
150
|
+
pattern: '',
|
|
151
|
+
name: '',
|
|
152
|
+
readonly: false,
|
|
153
|
+
tabindex: 0,
|
|
154
|
+
style: '' as Raw<StyleValue>,
|
|
155
|
+
placeholder: 'Enter a phone number',
|
|
120
156
|
};
|
|
121
157
|
|
|
122
158
|
type Props = {
|
|
123
159
|
label?: string;
|
|
124
160
|
id?: string;
|
|
125
|
-
autocomplete?: 'on' | 'off';
|
|
161
|
+
autocomplete?: 'on' | 'off' | 'tel';
|
|
126
162
|
placeholder?: string;
|
|
127
163
|
required?: boolean;
|
|
128
164
|
modelValue: string;
|
|
@@ -133,13 +169,14 @@ type Props = {
|
|
|
133
169
|
disabled?: boolean;
|
|
134
170
|
searchable?: boolean;
|
|
135
171
|
autoDefaultCountry?: boolean;
|
|
136
|
-
|
|
172
|
+
inputOptions?: Partial<typeof defaultInputOptions>;
|
|
173
|
+
dropdownOptions?: Partial<typeof defaultDropdownOptions>;
|
|
137
174
|
excludeCountries?: string[];
|
|
138
|
-
|
|
139
|
-
invalidMsg?: string;
|
|
140
|
-
mode?: 'auto' | 'international' | 'national';
|
|
175
|
+
mode?: NumberFormat;
|
|
141
176
|
onlyCountries?: string[];
|
|
142
177
|
preferredCountries?: string[];
|
|
178
|
+
parseArg?: { extract?: boolean };
|
|
179
|
+
debounceDelay?: number;
|
|
143
180
|
};
|
|
144
181
|
|
|
145
182
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -151,33 +188,27 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
151
188
|
defaultCountry: '',
|
|
152
189
|
placeholder: 'Enter a phone number',
|
|
153
190
|
searchable: true,
|
|
154
|
-
dropdownOptions: () => ({
|
|
155
|
-
disabled: false,
|
|
156
|
-
showFlags: true,
|
|
157
|
-
showDialCodeInSelection: true,
|
|
158
|
-
showDialCodeInList: true,
|
|
159
|
-
searchable: true,
|
|
160
|
-
}),
|
|
161
191
|
disabled: false,
|
|
162
192
|
autoDefaultCountry: true,
|
|
163
193
|
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
194
|
required: false,
|
|
175
|
-
|
|
176
|
-
mode: 'auto',
|
|
195
|
+
mode: 'INTERNATIONAL',
|
|
177
196
|
onlyCountries: () => [],
|
|
178
197
|
preferredCountries: () => [],
|
|
198
|
+
showDropdown: true,
|
|
199
|
+
debounceDelay: 300,
|
|
179
200
|
});
|
|
180
201
|
|
|
202
|
+
const computedDropDownOptions = $computed(() => ({
|
|
203
|
+
...defaultDropdownOptions,
|
|
204
|
+
...props.dropdownOptions,
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
const computedInputOptions = $computed(() => ({
|
|
208
|
+
...defaultInputOptions,
|
|
209
|
+
...props.inputOptions,
|
|
210
|
+
}));
|
|
211
|
+
|
|
181
212
|
let activeCountryCode = $ref<CountryCode>();
|
|
182
213
|
let open = $ref(false);
|
|
183
214
|
let selectedIndex = $ref<number>();
|
|
@@ -195,53 +226,29 @@ const emit = defineEmits([
|
|
|
195
226
|
'country-changed',
|
|
196
227
|
'enter',
|
|
197
228
|
'space',
|
|
229
|
+
'debounce',
|
|
198
230
|
]);
|
|
199
231
|
|
|
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
232
|
// Computed: activeCountry
|
|
225
|
-
const activeCountry = $computed(() =>
|
|
233
|
+
const activeCountry = $computed(() =>
|
|
234
|
+
props.allCountries.find(
|
|
235
|
+
(country) => country.iso2 === activeCountryCode?.toUpperCase(),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
226
238
|
|
|
227
|
-
const isPreferred = (country?: Country) =>
|
|
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
|
-
});
|
|
239
|
+
const isPreferred = (country?: Country) =>
|
|
240
|
+
props.preferredCountries.includes(country?.iso2 as CountryCode);
|
|
237
241
|
|
|
238
242
|
const filteredCountries = $computed(() => {
|
|
239
243
|
if (props.onlyCountries.length) {
|
|
240
|
-
return props.allCountries.filter(({ iso2 }) =>
|
|
244
|
+
return props.allCountries.filter(({ iso2 }) =>
|
|
245
|
+
props.onlyCountries.some((c) => c.toUpperCase() === iso2),
|
|
246
|
+
);
|
|
241
247
|
}
|
|
242
248
|
if (props.excludeCountries.length) {
|
|
243
249
|
return props.allCountries.filter(
|
|
244
|
-
({ iso2 }) =>
|
|
250
|
+
({ iso2 }) =>
|
|
251
|
+
!props.excludeCountries.includes(iso2.toUpperCase()) &&
|
|
245
252
|
!props.excludeCountries.includes(iso2.toLowerCase()),
|
|
246
253
|
);
|
|
247
254
|
}
|
|
@@ -252,16 +259,48 @@ const sortedCountries = $computed(() => {
|
|
|
252
259
|
const preferredCountries = getCountries(props.preferredCountries);
|
|
253
260
|
|
|
254
261
|
const countriesList = [...preferredCountries, ...filteredCountries];
|
|
255
|
-
const cleanInput = searchQuery.replace(
|
|
262
|
+
const cleanInput = searchQuery.replace(
|
|
263
|
+
/[~`!@#$%^&*()+={}[\];:'"<>.,/\\?-_]/g,
|
|
264
|
+
'',
|
|
265
|
+
);
|
|
256
266
|
return countriesList
|
|
257
267
|
.filter(
|
|
258
|
-
(c) =>
|
|
268
|
+
(c) =>
|
|
269
|
+
new RegExp(cleanInput, 'i').test(c?.name || '') ||
|
|
259
270
|
new RegExp(cleanInput, 'i').test(c?.iso2 || '') ||
|
|
260
271
|
new RegExp(cleanInput, 'i').test(c?.dialCode || ''),
|
|
261
272
|
)
|
|
262
273
|
.filter(Boolean);
|
|
263
274
|
});
|
|
264
275
|
|
|
276
|
+
const parseArgs = $computed(() => ({
|
|
277
|
+
...props?.parseArg,
|
|
278
|
+
defaultCountry: activeCountryCode,
|
|
279
|
+
}));
|
|
280
|
+
|
|
281
|
+
const phone = defineModel<string>('modelValue', {
|
|
282
|
+
default: '',
|
|
283
|
+
set: (value) => {
|
|
284
|
+
const formatted = formatPhone(`${value}`);
|
|
285
|
+
if (!formatted) return '';
|
|
286
|
+
|
|
287
|
+
emit('update:modelValue', formatted);
|
|
288
|
+
debounce(() => emit('debounce', formatted), props.debounceDelay);
|
|
289
|
+
return formatted;
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
function formatPhone(val: string): string {
|
|
294
|
+
const phoneNumber = parsePhoneNumberFromString(val, parseArgs);
|
|
295
|
+
if (!phoneNumber) {
|
|
296
|
+
const dialCode =
|
|
297
|
+
sortedCountries.find((c) => c.iso2 === activeCountryCode)?.dialCode || '';
|
|
298
|
+
if (props.mode === 'INTERNATIONAL') return `+${dialCode}`;
|
|
299
|
+
return dialCode;
|
|
300
|
+
}
|
|
301
|
+
return phoneNumber.format(props.mode)?.replaceAll(' ', '') || '';
|
|
302
|
+
}
|
|
303
|
+
|
|
265
304
|
// Watchers
|
|
266
305
|
watch(
|
|
267
306
|
() => activeCountry,
|
|
@@ -309,7 +348,8 @@ const initializeCountry = async () => {
|
|
|
309
348
|
|
|
310
349
|
onMounted(initializeCountry);
|
|
311
350
|
|
|
312
|
-
const findCountry = (iso: string) =>
|
|
351
|
+
const findCountry = (iso: string) =>
|
|
352
|
+
filteredCountries.find((country) => country.iso2 === iso?.toUpperCase());
|
|
313
353
|
|
|
314
354
|
const getCountries = (list: string[]): Country[] => {
|
|
315
355
|
const countryList: Country[] = [];
|
|
@@ -320,7 +360,8 @@ const getCountries = (list: string[]): Country[] => {
|
|
|
320
360
|
return countryList;
|
|
321
361
|
};
|
|
322
362
|
|
|
323
|
-
const findCountryByDialCode = (dialCode: number) =>
|
|
363
|
+
const findCountryByDialCode = (dialCode: number) =>
|
|
364
|
+
filteredCountries.find((country) => Number(country.dialCode) === dialCode);
|
|
324
365
|
|
|
325
366
|
const chooseCountry = (country?: string) => {
|
|
326
367
|
if (!country) return;
|
|
@@ -329,7 +370,7 @@ const chooseCountry = (country?: string) => {
|
|
|
329
370
|
activeCountryCode = parsedCountry.iso2;
|
|
330
371
|
|
|
331
372
|
if (props.inputOptions?.showDialCode && parsedCountry) {
|
|
332
|
-
phone =
|
|
373
|
+
phone.value = `+ ${parsedCountry.dialCode}`;
|
|
333
374
|
activeCountryCode = parsedCountry.iso2 || '';
|
|
334
375
|
return;
|
|
335
376
|
}
|
|
@@ -342,7 +383,7 @@ const onBlur = () => emit('blur');
|
|
|
342
383
|
|
|
343
384
|
const onFocus = () => emit('focus');
|
|
344
385
|
|
|
345
|
-
const onEnter = () => emit('enter');
|
|
386
|
+
const onEnter = (e: KeyboardEvent) => emit('enter');
|
|
346
387
|
|
|
347
388
|
const onSpace = () => emit('space');
|
|
348
389
|
|
|
@@ -350,9 +391,22 @@ const onSpace = () => emit('space');
|
|
|
350
391
|
|
|
351
392
|
// Method: reset
|
|
352
393
|
const reset = () => {
|
|
353
|
-
selectedIndex = sortedCountries.findIndex(
|
|
394
|
+
selectedIndex = sortedCountries.findIndex(
|
|
395
|
+
(c) => c.iso2 === activeCountryCode,
|
|
396
|
+
);
|
|
354
397
|
open = false;
|
|
355
398
|
};
|
|
399
|
+
|
|
400
|
+
function handleInput(e: KeyboardEvent) {
|
|
401
|
+
const keyVal = e.key;
|
|
402
|
+
if (keyVal?.length > 1 || e.metaKey) return;
|
|
403
|
+
|
|
404
|
+
const hasBadChars = RegExp(/[^+\d]/).test(keyVal);
|
|
405
|
+
|
|
406
|
+
if (!hasBadChars) return;
|
|
407
|
+
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
}
|
|
356
410
|
</script>
|
|
357
411
|
|
|
358
412
|
<style scoped>
|
|
@@ -372,19 +426,20 @@ const reset = () => {
|
|
|
372
426
|
outline: none;
|
|
373
427
|
box-shadow: inset 0 0 10px #00000012;
|
|
374
428
|
}
|
|
429
|
+
|
|
375
430
|
.tel-input input {
|
|
376
431
|
background: transparent;
|
|
377
432
|
}
|
|
378
|
-
|
|
379
|
-
color: transparent;
|
|
380
|
-
}
|
|
433
|
+
|
|
381
434
|
.tel-input input:focus-visible {
|
|
382
435
|
box-shadow: none;
|
|
383
436
|
}
|
|
437
|
+
|
|
384
438
|
.input_country-code {
|
|
385
439
|
font-size: var(--input-font-size);
|
|
386
440
|
color: var(--input-color);
|
|
387
441
|
}
|
|
442
|
+
|
|
388
443
|
.tel-country {
|
|
389
444
|
font-size: var(--input-font-size);
|
|
390
445
|
max-width: 200px;
|
|
@@ -394,6 +449,7 @@ const reset = () => {
|
|
|
394
449
|
margin-top: 0;
|
|
395
450
|
margin-bottom: 0;
|
|
396
451
|
}
|
|
452
|
+
|
|
397
453
|
.tel-countryp-dropdown {
|
|
398
454
|
direction: ltr;
|
|
399
455
|
text-align: left;
|