js-kompo-utils 1.0.0
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/README.md +1 -0
- package/app.js +22 -0
- package/components/VlApexChart.vue +89 -0
- package/components/VlCKEditorTemplate.vue +52 -0
- package/components/VlCKEditorWithVars.vue +80 -0
- package/components/VlCollapsible.vue +91 -0
- package/components/VlCopyBox.vue +61 -0
- package/components/VlCreditCardInput.vue +104 -0
- package/components/VlDateMonthDay.vue +327 -0
- package/components/VlDateTextInput.vue +151 -0
- package/components/VlInternationalPhoneInput.vue +902 -0
- package/components/VlPartialDate.vue +283 -0
- package/components/VlPasswordInput.vue +358 -0
- package/components/VlQrCodeScanner.vue +91 -0
- package/components/VlSwipeableTab.vue +35 -0
- package/components/VlSwipeableTabs.vue +325 -0
- package/components/VlText.vue +72 -0
- package/components/VlTutorialStepBuilder.vue +671 -0
- package/components/VlValidatedInput.vue +262 -0
- package/components/data/countries.js +253 -0
- package/components/getAllDefaultComponents.js +12 -0
- package/docs/architecture.md +71 -0
- package/docs/tutorials.md +247 -0
- package/docs/usage.md +128 -0
- package/functions/index.js +7 -0
- package/functions/inputs.js +64 -0
- package/functions/loading.js +20 -0
- package/kompo-ckeditor-1.0.17/index.js +5 -0
- package/kompo-ckeditor-1.0.17/komponents/CKEditor.vue +36 -0
- package/kompo-ckeditor-1.0.17/komponents/CKEditorExtended.vue +66 -0
- package/kompo-ckeditor-1.0.17/komponents/EditableCKEditor.vue +42 -0
- package/kompo-ckeditor-1.0.17/komponents/TranslatableEditor.vue +28 -0
- package/kompo-ckeditor-1.0.17/mixins/CKEditor.js +291 -0
- package/kompo-ckeditor-1.0.17/mixins/CKEditorUtilities.js +52 -0
- package/kompo-ckeditor-1.0.17/mixins/CkUploadAdapter.js +95 -0
- package/kompo-ckeditor-1.0.17/mixins/translations/en.js +1 -0
- package/kompo-ckeditor-1.0.17/mixins/translations/fr.js +1 -0
- package/kompo-ckeditor-1.0.17/plugins/JsonBlockCommand.js +100 -0
- package/kompo-ckeditor-1.0.17/plugins/JsonBlockEditing.js +225 -0
- package/kompo-ckeditor-1.0.17/plugins/JsonBlockUi.js +21 -0
- package/kompo-ckeditor-1.0.17/plugins/MentionPlugin.js +118 -0
- package/orchestrator/KompoJsBuilder.js +333 -0
- package/package.json +25 -0
- package/raw_animations/count-up-animation.js +220 -0
- package/raw_animations/magic-poof.js +210 -0
- package/tutorials/TutorialConfig.js +34 -0
- package/tutorials/TutorialEngine.js +448 -0
- package/tutorials/index.js +2 -0
- package/tutorials/modules/AutoNext.js +86 -0
- package/tutorials/modules/Avatar.js +94 -0
- package/tutorials/modules/Bubble.js +617 -0
- package/tutorials/modules/Cursor.js +315 -0
- package/tutorials/modules/Highlight.js +282 -0
- package/tutorials/modules/HoverForcer.js +250 -0
- package/tutorials/modules/Mobile.js +148 -0
- package/tutorials/modules/Options.js +98 -0
- package/tutorials/modules/Overlay.js +142 -0
- package/tutorials/modules/Scroll.js +37 -0
- package/tutorials/modules/Typewriter.js +132 -0
- package/tutorials/step-builder/step-builder-bridge.js +436 -0
- package/tutorials/step-builder/utils/bezier-editor.js +379 -0
- package/tutorials/step-builder/utils/code-generator.js +127 -0
- package/tutorials/step-builder/utils/element-picker.js +125 -0
- package/tutorials/tutorial.scss +816 -0
- package/tutorials/utils/selectors.js +225 -0
- package/tutorials/utils/stability.js +76 -0
- package/tutorials/utils/svg.js +46 -0
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<vl-form-field v-bind="$_wrapperAttributes">
|
|
3
|
+
<div :class="inputGroupClass">
|
|
4
|
+
<!-- Country Selector -->
|
|
5
|
+
<div class="country-selector" ref="countrySelector">
|
|
6
|
+
<button
|
|
7
|
+
type="button"
|
|
8
|
+
class="country-trigger"
|
|
9
|
+
:class="{ 'is-open': isDropdownOpen }"
|
|
10
|
+
@click="toggleDropdown"
|
|
11
|
+
@keydown="onTriggerKeydown"
|
|
12
|
+
>
|
|
13
|
+
<img v-if="currentCountry.flag && !isDetectingCountry" :src="currentCountry.flag" :alt="currentCountry.name" class="flag-img" />
|
|
14
|
+
<span v-else-if="isDetectingCountry" class="flag-loading">⏳</span>
|
|
15
|
+
<span v-else class="flag-fallback">🌍</span>
|
|
16
|
+
<span class="code">+{{ currentCountry.dialCode }}</span>
|
|
17
|
+
<span class="arrow">▼</span>
|
|
18
|
+
</button>
|
|
19
|
+
|
|
20
|
+
<div
|
|
21
|
+
v-if="isDropdownOpen"
|
|
22
|
+
class="country-dropdown"
|
|
23
|
+
@click.stop
|
|
24
|
+
>
|
|
25
|
+
<div class="search-container">
|
|
26
|
+
<input
|
|
27
|
+
ref="searchInput"
|
|
28
|
+
v-model="searchTerm"
|
|
29
|
+
type="text"
|
|
30
|
+
placeholder="Search countries..."
|
|
31
|
+
class="search-input"
|
|
32
|
+
@keydown="onSearchKeydown"
|
|
33
|
+
@input="onSearchInput"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="countries-list" ref="countriesList">
|
|
38
|
+
<button
|
|
39
|
+
v-for="country in filteredCountries"
|
|
40
|
+
:key="country.code"
|
|
41
|
+
type="button"
|
|
42
|
+
class="country-option"
|
|
43
|
+
:class="{ 'is-selected': country.code === selectedCountryCode }"
|
|
44
|
+
@click="selectCountry(country)"
|
|
45
|
+
>
|
|
46
|
+
<img v-if="country.flag" :src="country.flag" :alt="country.name" class="flag-img" />
|
|
47
|
+
<span v-else class="flag-fallback">🌍</span>
|
|
48
|
+
<span class="name">{{ country.name }}</span>
|
|
49
|
+
<span class="dial-code">+{{ country.dialCode }}</span>
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<!-- Phone Input -->
|
|
56
|
+
<input
|
|
57
|
+
ref="phoneInput"
|
|
58
|
+
class="phone-input"
|
|
59
|
+
:value="formattedValue"
|
|
60
|
+
v-bind="$_attributes"
|
|
61
|
+
v-on="$_events"
|
|
62
|
+
@input="onPhoneInput"
|
|
63
|
+
@keydown="onPhoneKeydown"
|
|
64
|
+
@focus="onPhoneFocus"
|
|
65
|
+
@blur="onPhoneBlur"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</vl-form-field>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
import Field from 'vue-kompo/js/form/mixins/Field'
|
|
73
|
+
import HasInputAttributes from 'vue-kompo/js/form/mixins/HasInputAttributes'
|
|
74
|
+
import { parsePhoneNumberFromString, getCountries, getCountryCallingCode, AsYouType } from 'libphonenumber-js'
|
|
75
|
+
import { COUNTRIES_DATA } from './data/countries.js'
|
|
76
|
+
|
|
77
|
+
export default {
|
|
78
|
+
name: 'VlInternationalPhoneInput',
|
|
79
|
+
mixins: [Field, HasInputAttributes],
|
|
80
|
+
|
|
81
|
+
data() {
|
|
82
|
+
return {
|
|
83
|
+
// Phone number state
|
|
84
|
+
phoneValue: this.component?.value || '',
|
|
85
|
+
|
|
86
|
+
// Country selection state
|
|
87
|
+
selectedCountryCode: this.$_config('country') || this.$_config('defaultCountry'),
|
|
88
|
+
isDropdownOpen: false,
|
|
89
|
+
searchTerm: '',
|
|
90
|
+
|
|
91
|
+
// Input state
|
|
92
|
+
isFocused: false,
|
|
93
|
+
|
|
94
|
+
// Countries data
|
|
95
|
+
countriesData: COUNTRIES_DATA,
|
|
96
|
+
|
|
97
|
+
// Geolocation state
|
|
98
|
+
isDetectingCountry: false,
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
computed: {
|
|
103
|
+
$_attributes() {
|
|
104
|
+
return {
|
|
105
|
+
...this.$_defaultFieldAttributes,
|
|
106
|
+
...this.$_defaultInputAttributes,
|
|
107
|
+
type: 'tel',
|
|
108
|
+
inputmode: 'tel',
|
|
109
|
+
autocomplete: 'tel',
|
|
110
|
+
placeholder: this.placeholder,
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
placeholder() {
|
|
115
|
+
return this.$_config('placeholder') || 'Phone number'
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
inputGroupClass() {
|
|
119
|
+
const baseClass = 'vlInputGroup rounded-lg flex items-stretch'
|
|
120
|
+
const errorClass = this.showError ? (this.$_config('invalidClass') || '!border !border-red-600') : ''
|
|
121
|
+
return `${baseClass} ${errorClass}`
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// Current country info
|
|
125
|
+
currentCountry() {
|
|
126
|
+
const country = this.countries.find(c => c.code === this.selectedCountryCode)
|
|
127
|
+
return country || this.countries[0] // Fallback to first country
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// All countries with metadata - static data
|
|
131
|
+
countries() {
|
|
132
|
+
return this.countriesData
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// Filtered countries based on search
|
|
136
|
+
filteredCountries() {
|
|
137
|
+
if (!this.searchTerm.trim()) {
|
|
138
|
+
return this.countries
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const query = this.searchTerm.toLowerCase()
|
|
142
|
+
return this.countries.filter(country => {
|
|
143
|
+
// Search by country name
|
|
144
|
+
if (country.name.toLowerCase().includes(query)) return true
|
|
145
|
+
|
|
146
|
+
// Search by country code
|
|
147
|
+
if (country.code.toLowerCase().includes(query)) return true
|
|
148
|
+
|
|
149
|
+
// Search by dial code (with or without +)
|
|
150
|
+
const dialCode = country.dialCode
|
|
151
|
+
const dialCodeWithPlus = `+${dialCode}`
|
|
152
|
+
if (dialCode.includes(query) || dialCodeWithPlus.includes(query)) return true
|
|
153
|
+
|
|
154
|
+
// Search by dial code without the + prefix
|
|
155
|
+
if (query.startsWith('+')) {
|
|
156
|
+
const queryWithoutPlus = query.substring(1)
|
|
157
|
+
if (dialCode.includes(queryWithoutPlus)) return true
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return false
|
|
161
|
+
})
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// Formatted phone value for display
|
|
165
|
+
formattedValue() {
|
|
166
|
+
if (!this.phoneValue) return ''
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
// If phone value starts with the country code, show it formatted
|
|
170
|
+
if (this.phoneValue.startsWith('+')) {
|
|
171
|
+
const asYouType = new AsYouType()
|
|
172
|
+
const formatted = asYouType.input(this.phoneValue)
|
|
173
|
+
return formatted || this.phoneValue
|
|
174
|
+
} else {
|
|
175
|
+
// For national numbers, format with country context
|
|
176
|
+
const asYouType = new AsYouType(this.selectedCountryCode)
|
|
177
|
+
const formatted = asYouType.input(this.phoneValue)
|
|
178
|
+
return formatted || this.phoneValue
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return this.phoneValue
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// Validation state
|
|
186
|
+
isValidPhone() {
|
|
187
|
+
if (!this.phoneValue) return true // Empty is valid
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const phoneNumber = parsePhoneNumberFromString(this.phoneValue, this.selectedCountryCode)
|
|
191
|
+
return phoneNumber ? phoneNumber.isValid() : false
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
showError() {
|
|
198
|
+
const validateFront = this.$_config('validateFront')
|
|
199
|
+
if (!validateFront) return false
|
|
200
|
+
|
|
201
|
+
return this.phoneValue && !this.isValidPhone && !this.isFocused
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
watch: {
|
|
206
|
+
'component.value'(newValue) {
|
|
207
|
+
this.phoneValue = newValue || ''
|
|
208
|
+
|
|
209
|
+
// Auto-detect country from phone number if no country is explicitly set
|
|
210
|
+
if (newValue && !this.$_config('country')) {
|
|
211
|
+
this.autoDetectCountryFromPhone(newValue)
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
selectedCountryCode(newCode, oldCode) {
|
|
216
|
+
if (newCode !== oldCode && this.phoneValue) {
|
|
217
|
+
this.reformatWithCountry(newCode)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
mounted() {
|
|
223
|
+
// Close dropdown when clicking outside
|
|
224
|
+
document.addEventListener('click', this.handleClickOutside)
|
|
225
|
+
|
|
226
|
+
// Set initial value
|
|
227
|
+
// this.component.value = this.phoneValue
|
|
228
|
+
|
|
229
|
+
// Auto-detect country if no explicit country is set and no phone value exists
|
|
230
|
+
if (this.phoneValue) {
|
|
231
|
+
this.autoDetectCountryFromPhone(this.phoneValue)
|
|
232
|
+
} else if (!this.selectedCountryCode) {
|
|
233
|
+
this.detectCountryByLocation()
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
beforeDestroy() {
|
|
238
|
+
document.removeEventListener('click', this.handleClickOutside)
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
methods: {
|
|
242
|
+
// Country selection methods
|
|
243
|
+
toggleDropdown() {
|
|
244
|
+
this.isDropdownOpen = !this.isDropdownOpen
|
|
245
|
+
if (this.isDropdownOpen) {
|
|
246
|
+
this.$nextTick(() => {
|
|
247
|
+
this.focusSearchInput()
|
|
248
|
+
})
|
|
249
|
+
} else {
|
|
250
|
+
this.searchTerm = ''
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
selectCountry(country) {
|
|
255
|
+
const previousCountryCode = this.selectedCountryCode;
|
|
256
|
+
const previousCountry = this.countries.find(c => c.code === previousCountryCode);
|
|
257
|
+
this.selectedCountryCode = country.code
|
|
258
|
+
this.isDropdownOpen = false
|
|
259
|
+
this.searchTerm = ''
|
|
260
|
+
|
|
261
|
+
// Get current and new dial codes
|
|
262
|
+
const previousDialCode = previousCountry ? previousCountry.dialCode : '';
|
|
263
|
+
const newDialCode = country.dialCode;
|
|
264
|
+
|
|
265
|
+
// Check if current value is just a country code (like +54, +1, etc.)
|
|
266
|
+
const currentDialCode = `+${previousDialCode}`
|
|
267
|
+
const isOnlyCountryCode = !this.phoneValue ||
|
|
268
|
+
this.phoneValue === currentDialCode ||
|
|
269
|
+
this.phoneValue === previousDialCode ||
|
|
270
|
+
this.phoneValue.replace(/\D/g, '') === previousDialCode
|
|
271
|
+
|
|
272
|
+
if (isOnlyCountryCode) {
|
|
273
|
+
// Set new country code and ensure component.value is complete
|
|
274
|
+
this.phoneValue = `+${newDialCode}`
|
|
275
|
+
this.component.value = `+${newDialCode}`
|
|
276
|
+
|
|
277
|
+
// Update input display immediately
|
|
278
|
+
this.$nextTick(() => {
|
|
279
|
+
if (this.$refs.phoneInput) {
|
|
280
|
+
this.$refs.phoneInput.value = this.formattedValue
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
} else {
|
|
284
|
+
// Extract national number and reformat with new country
|
|
285
|
+
let nationalNumber = this.phoneValue;
|
|
286
|
+
console.log(nationalNumber, previousDialCode);
|
|
287
|
+
// Remove previous country code if it exists
|
|
288
|
+
if (nationalNumber.startsWith(`+${previousDialCode}`)) {
|
|
289
|
+
nationalNumber = nationalNumber.substring((`+${previousDialCode}`).length).trim();
|
|
290
|
+
} else if (nationalNumber.startsWith(previousDialCode)) {
|
|
291
|
+
nationalNumber = nationalNumber.substring(previousDialCode.length).trim();
|
|
292
|
+
} else if (nationalNumber.startsWith('+')) {
|
|
293
|
+
// Try to parse and extract national number
|
|
294
|
+
try {
|
|
295
|
+
const parsed = parsePhoneNumberFromString(nationalNumber);
|
|
296
|
+
if (parsed && parsed.nationalNumber) {
|
|
297
|
+
nationalNumber = parsed.nationalNumber;
|
|
298
|
+
}
|
|
299
|
+
} catch (error) {
|
|
300
|
+
// Keep original if parsing fails
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Set new phone value with new country code
|
|
305
|
+
this.phoneValue = `+${newDialCode}${nationalNumber}`;
|
|
306
|
+
this.component.value = this.phoneValue;
|
|
307
|
+
|
|
308
|
+
// Try to format properly
|
|
309
|
+
try {
|
|
310
|
+
const phoneNumber = parsePhoneNumberFromString(this.phoneValue);
|
|
311
|
+
if (phoneNumber && phoneNumber.isValid()) {
|
|
312
|
+
this.component.value = phoneNumber.number;
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
// Keep current value if parsing fails
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Focus back to phone input
|
|
320
|
+
this.$nextTick(() => {
|
|
321
|
+
this.focusPhoneInput()
|
|
322
|
+
})
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
reformatWithCountry(phoneValue, countryCode) {
|
|
327
|
+
try {
|
|
328
|
+
const phoneNumber = parsePhoneNumberFromString(phoneValue, countryCode)
|
|
329
|
+
if (phoneNumber && phoneNumber.isValid()) {
|
|
330
|
+
// Always keep the full international number in component.value
|
|
331
|
+
this.phoneValue = phoneNumber.number
|
|
332
|
+
this.component.value = phoneNumber.number
|
|
333
|
+
} else {
|
|
334
|
+
// Fallback: ensure we have at least the country code
|
|
335
|
+
const country = this.countries.find(c => c.code === countryCode);
|
|
336
|
+
if (country) {
|
|
337
|
+
const cleanValue = phoneValue.replace(/\D/g, '');
|
|
338
|
+
this.phoneValue = `+${country.dialCode}${cleanValue}`;
|
|
339
|
+
this.component.value = this.phoneValue;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch (error) {
|
|
343
|
+
// Keep current value if parsing fails
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
// Search methods
|
|
348
|
+
focusSearchInput() {
|
|
349
|
+
if (this.$refs.searchInput) {
|
|
350
|
+
this.$refs.searchInput.focus()
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
onSearchInput() {
|
|
355
|
+
// Search is reactive via computed property
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
onSearchKeydown(event) {
|
|
359
|
+
switch (event.key) {
|
|
360
|
+
case 'Escape':
|
|
361
|
+
this.closeDropdown()
|
|
362
|
+
this.focusPhoneInput()
|
|
363
|
+
break
|
|
364
|
+
case 'ArrowDown':
|
|
365
|
+
event.preventDefault()
|
|
366
|
+
this.focusFirstCountry()
|
|
367
|
+
break
|
|
368
|
+
case 'Enter':
|
|
369
|
+
event.preventDefault()
|
|
370
|
+
if (this.filteredCountries.length > 0) {
|
|
371
|
+
this.selectCountry(this.filteredCountries[0])
|
|
372
|
+
}
|
|
373
|
+
break
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
focusFirstCountry() {
|
|
378
|
+
this.$nextTick(() => {
|
|
379
|
+
const firstOption = this.$refs.countriesList?.querySelector('.country-option')
|
|
380
|
+
if (firstOption) {
|
|
381
|
+
firstOption.focus()
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
// Phone input methods
|
|
387
|
+
onPhoneInput(event) {
|
|
388
|
+
const input = event.target
|
|
389
|
+
const newValue = input.value
|
|
390
|
+
|
|
391
|
+
// Check if input contains country code (starts with +)
|
|
392
|
+
if (newValue.startsWith('+')) {
|
|
393
|
+
this.handleInputWithCountryCode(newValue)
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Clean the input value
|
|
398
|
+
const cleanedValue = this.cleanPhoneInput(newValue)
|
|
399
|
+
|
|
400
|
+
// Check if value exceeds maximum length
|
|
401
|
+
if (!this.isWithinMaxLength(cleanedValue)) {
|
|
402
|
+
// Restore previous value
|
|
403
|
+
input.value = this.formattedValue
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
this.updateInput(input, cleanedValue)
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
updateInput(input, cleanedValue = null) {
|
|
411
|
+
// Always ensure we have the country code in the phone value
|
|
412
|
+
const country = this.countries.find(c => c.code === this.selectedCountryCode);
|
|
413
|
+
let fullPhoneValue = cleanedValue;
|
|
414
|
+
|
|
415
|
+
if (cleanedValue && country) {
|
|
416
|
+
// If the cleaned value doesn't start with the country code, add it
|
|
417
|
+
if (!cleanedValue.startsWith(`+${country.dialCode}`) && !cleanedValue.startsWith(country.dialCode)) {
|
|
418
|
+
fullPhoneValue = `+${country.dialCode}${cleanedValue.replace(/^\+/, '')}`;
|
|
419
|
+
} else if (cleanedValue.startsWith(country.dialCode) && !cleanedValue.startsWith(`+${country.dialCode}`)) {
|
|
420
|
+
fullPhoneValue = `+${cleanedValue}`;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Update internal state - phoneValue for display, component.value for the complete number
|
|
425
|
+
this.phoneValue = fullPhoneValue || cleanedValue
|
|
426
|
+
this.component.value = this.getE164Number(this.phoneValue)
|
|
427
|
+
|
|
428
|
+
// Update display value with proper formatting
|
|
429
|
+
this.$nextTick(() => {
|
|
430
|
+
const formatted = this.formattedValue
|
|
431
|
+
input.value = formatted
|
|
432
|
+
|
|
433
|
+
// Maintain cursor position
|
|
434
|
+
const cursorPos = Math.min(input.selectionStart, formatted.length)
|
|
435
|
+
input.setSelectionRange(cursorPos, cursorPos)
|
|
436
|
+
})
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
handleInputWithCountryCode(inputValue) {
|
|
440
|
+
try {
|
|
441
|
+
// Parse the full international number
|
|
442
|
+
const phoneNumber = parsePhoneNumberFromString(inputValue)
|
|
443
|
+
|
|
444
|
+
if (phoneNumber && phoneNumber.country) {
|
|
445
|
+
// Update country if detected
|
|
446
|
+
this.selectedCountryCode = phoneNumber.country
|
|
447
|
+
|
|
448
|
+
// Store the full international number to preserve country code in display
|
|
449
|
+
this.phoneValue = inputValue
|
|
450
|
+
this.component.value = phoneNumber.number || inputValue
|
|
451
|
+
} else {
|
|
452
|
+
// If can't parse, treat as regular input
|
|
453
|
+
const cleanedValue = this.cleanPhoneInput(inputValue)
|
|
454
|
+
this.phoneValue = cleanedValue
|
|
455
|
+
this.component.value = this.getE164Number(cleanedValue)
|
|
456
|
+
}
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.warn('Error parsing phone with country code:', error)
|
|
459
|
+
// Fallback to regular input handling
|
|
460
|
+
const cleanedValue = this.cleanPhoneInput(inputValue)
|
|
461
|
+
this.phoneValue = cleanedValue
|
|
462
|
+
this.component.value = this.getE164Number(cleanedValue)
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
onPhoneKeydown(event) {
|
|
467
|
+
// Allow control keys
|
|
468
|
+
const controlKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Tab', 'Escape', 'Enter']
|
|
469
|
+
if (controlKeys.includes(event.key)) return
|
|
470
|
+
|
|
471
|
+
// Allow copy/paste shortcuts
|
|
472
|
+
const isShortcut = (event.ctrlKey || event.metaKey) &&
|
|
473
|
+
['a', 'c', 'v', 'x'].includes(event.key.toLowerCase())
|
|
474
|
+
if (isShortcut) return
|
|
475
|
+
|
|
476
|
+
// Allow only digits, spaces, dashes, parentheses, and plus
|
|
477
|
+
const allowedChars = /[0-9\s\-\(\)\+]/
|
|
478
|
+
if (!allowedChars.test(event.key)) {
|
|
479
|
+
event.preventDefault()
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Handle plus sign - only allow at beginning
|
|
484
|
+
if (event.key === '+') {
|
|
485
|
+
const input = event.target
|
|
486
|
+
const cursorPos = input.selectionStart
|
|
487
|
+
const hasExistingPlus = input.value.includes('+')
|
|
488
|
+
|
|
489
|
+
if (hasExistingPlus || cursorPos !== 0) {
|
|
490
|
+
event.preventDefault()
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
onPhoneFocus() {
|
|
496
|
+
this.isFocused = true
|
|
497
|
+
},
|
|
498
|
+
|
|
499
|
+
onPhoneBlur() {
|
|
500
|
+
this.isFocused = false
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
focusPhoneInput() {
|
|
504
|
+
if (this.$refs.phoneInput) {
|
|
505
|
+
this.$refs.phoneInput.focus()
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
// Utility methods
|
|
510
|
+
cleanPhoneInput(value) {
|
|
511
|
+
if (!value) return ''
|
|
512
|
+
|
|
513
|
+
// Remove all non-digit characters except spaces, dashes, parentheses, and plus
|
|
514
|
+
return value.replace(/[^0-9\s\-\(\)\+]/g, '')
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
isWithinMaxLength(value) {
|
|
518
|
+
// E.164 maximum is 15 digits total
|
|
519
|
+
const digitsOnly = value.replace(/\D/g, '')
|
|
520
|
+
return digitsOnly.length <= 15
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
getE164Number(phoneValue) {
|
|
524
|
+
if (!phoneValue) return ''
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
// If it already looks like an international number, try to parse it as-is
|
|
528
|
+
if (phoneValue.startsWith('+')) {
|
|
529
|
+
const phoneNumber = parsePhoneNumberFromString(phoneValue)
|
|
530
|
+
return phoneNumber ? phoneNumber.number : phoneValue
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Otherwise, try to parse it with the selected country
|
|
534
|
+
const phoneNumber = parsePhoneNumberFromString(phoneValue, this.selectedCountryCode)
|
|
535
|
+
return phoneNumber ? phoneNumber.number : phoneValue
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return phoneValue
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
|
|
541
|
+
closeDropdown() {
|
|
542
|
+
this.isDropdownOpen = false
|
|
543
|
+
this.searchTerm = ''
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
handleClickOutside(event) {
|
|
547
|
+
if (this.isDropdownOpen && !this.$refs.countrySelector.contains(event.target)) {
|
|
548
|
+
this.closeDropdown()
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
// Auto-detect country from IP geolocation
|
|
553
|
+
async detectCountryByLocation() {
|
|
554
|
+
if (this.isDetectingCountry) return
|
|
555
|
+
|
|
556
|
+
this.isDetectingCountry = true
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
// Try multiple geolocation services for better reliability
|
|
560
|
+
const countryCode = await this.getCountryFromIP()
|
|
561
|
+
if (countryCode && this.countries.find(c => c.code === countryCode)) {
|
|
562
|
+
this.selectedCountryCode = countryCode
|
|
563
|
+
}
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.warn('Failed to detect country by location:', error)
|
|
566
|
+
} finally {
|
|
567
|
+
this.isDetectingCountry = false
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
|
|
571
|
+
async getCountryFromIP() {
|
|
572
|
+
// Try ipapi.co first (free, reliable)
|
|
573
|
+
try {
|
|
574
|
+
const response = await fetch('https://ipapi.co/json/', {
|
|
575
|
+
timeout: 5000
|
|
576
|
+
})
|
|
577
|
+
const data = await response.json()
|
|
578
|
+
return data.country_code
|
|
579
|
+
} catch (error) {
|
|
580
|
+
console.warn('ipapi.co failed:', error)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Fallback to ip-api.com
|
|
584
|
+
try {
|
|
585
|
+
const response = await fetch('http://ip-api.com/json/', {
|
|
586
|
+
timeout: 5000
|
|
587
|
+
})
|
|
588
|
+
const data = await response.json()
|
|
589
|
+
return data.countryCode
|
|
590
|
+
} catch (error) {
|
|
591
|
+
console.warn('ip-api.com failed:', error)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Fallback to ipinfo.io
|
|
595
|
+
try {
|
|
596
|
+
const response = await fetch('https://ipinfo.io/json', {
|
|
597
|
+
timeout: 5000
|
|
598
|
+
})
|
|
599
|
+
const data = await response.json()
|
|
600
|
+
return data.country
|
|
601
|
+
} catch (error) {
|
|
602
|
+
console.warn('ipinfo.io failed:', error)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return null
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
// Auto-detect country from existing phone number
|
|
609
|
+
autoDetectCountryFromPhone(phoneValue) {;
|
|
610
|
+
try {
|
|
611
|
+
const phoneNumber = parsePhoneNumberFromString(phoneValue)
|
|
612
|
+
if (phoneNumber && phoneNumber.country) {
|
|
613
|
+
this.selectedCountryCode = phoneNumber.country
|
|
614
|
+
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.warn('Failed to auto-detect country from phone:', error)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return false;
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
// API methods
|
|
625
|
+
async loadCountriesData() {
|
|
626
|
+
try {
|
|
627
|
+
this.isLoadingCountries = true
|
|
628
|
+
|
|
629
|
+
// Fetch from REST Countries API
|
|
630
|
+
const response = await fetch('https://restcountries.com/v3.1/all?fields=name,cca2,idd,flags')
|
|
631
|
+
const countries = await response.json()
|
|
632
|
+
|
|
633
|
+
// Transform data to our format
|
|
634
|
+
this.countriesData = countries
|
|
635
|
+
.filter(country => country.idd?.root && country.idd?.suffixes?.length > 0)
|
|
636
|
+
.map(country => ({
|
|
637
|
+
code: country.cca2,
|
|
638
|
+
name: country.name.common,
|
|
639
|
+
dialCode: country.idd.root + country.idd.suffixes[0],
|
|
640
|
+
flag: country.flags?.png || null
|
|
641
|
+
}))
|
|
642
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
643
|
+
|
|
644
|
+
} catch (error) {
|
|
645
|
+
console.error('Failed to load countries data:', error)
|
|
646
|
+
// Fallback to libphonenumber-js data
|
|
647
|
+
this.countriesData = getCountries().map(code => ({
|
|
648
|
+
code,
|
|
649
|
+
name: this.getCountryName(code),
|
|
650
|
+
dialCode: getCountryCallingCode(code),
|
|
651
|
+
flag: null
|
|
652
|
+
}))
|
|
653
|
+
} finally {
|
|
654
|
+
this.isLoadingCountries = false
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
|
|
658
|
+
// Fallback country data methods
|
|
659
|
+
getCountryName(code) {
|
|
660
|
+
const countryNames = {
|
|
661
|
+
'US': 'United States', 'CA': 'Canada', 'GB': 'United Kingdom', 'FR': 'France',
|
|
662
|
+
'DE': 'Germany', 'IT': 'Italy', 'ES': 'Spain', 'AU': 'Australia',
|
|
663
|
+
'JP': 'Japan', 'CN': 'China', 'IN': 'India', 'BR': 'Brazil',
|
|
664
|
+
'MX': 'Mexico', 'AR': 'Argentina', 'RU': 'Russia', 'ZA': 'South Africa',
|
|
665
|
+
'EG': 'Egypt', 'NG': 'Nigeria', 'KE': 'Kenya', 'MA': 'Morocco',
|
|
666
|
+
'SA': 'Saudi Arabia', 'AE': 'UAE', 'IL': 'Israel', 'TR': 'Turkey',
|
|
667
|
+
'TH': 'Thailand', 'SG': 'Singapore', 'MY': 'Malaysia', 'ID': 'Indonesia',
|
|
668
|
+
'PH': 'Philippines', 'VN': 'Vietnam', 'KR': 'South Korea', 'TW': 'Taiwan',
|
|
669
|
+
'HK': 'Hong Kong', 'NZ': 'New Zealand', 'CH': 'Switzerland', 'AT': 'Austria',
|
|
670
|
+
'BE': 'Belgium', 'NL': 'Netherlands', 'DK': 'Denmark', 'SE': 'Sweden',
|
|
671
|
+
'NO': 'Norway', 'FI': 'Finland', 'PL': 'Poland', 'CZ': 'Czech Republic',
|
|
672
|
+
'HU': 'Hungary', 'RO': 'Romania', 'BG': 'Bulgaria', 'GR': 'Greece',
|
|
673
|
+
'PT': 'Portugal', 'IE': 'Ireland', 'IS': 'Iceland', 'LU': 'Luxembourg',
|
|
674
|
+
'MT': 'Malta', 'CY': 'Cyprus', 'EE': 'Estonia', 'LV': 'Latvia',
|
|
675
|
+
'LT': 'Lithuania', 'SK': 'Slovakia', 'SI': 'Slovenia', 'HR': 'Croatia',
|
|
676
|
+
'RS': 'Serbia', 'BA': 'Bosnia', 'ME': 'Montenegro', 'MK': 'North Macedonia',
|
|
677
|
+
'AL': 'Albania', 'XK': 'Kosovo', 'MD': 'Moldova', 'UA': 'Ukraine',
|
|
678
|
+
'BY': 'Belarus', 'GE': 'Georgia', 'AM': 'Armenia', 'AZ': 'Azerbaijan',
|
|
679
|
+
'KZ': 'Kazakhstan', 'UZ': 'Uzbekistan', 'KG': 'Kyrgyzstan', 'TJ': 'Tajikistan',
|
|
680
|
+
'TM': 'Turkmenistan', 'MN': 'Mongolia', 'AF': 'Afghanistan', 'PK': 'Pakistan',
|
|
681
|
+
'BD': 'Bangladesh', 'LK': 'Sri Lanka', 'MV': 'Maldives', 'BT': 'Bhutan',
|
|
682
|
+
'NP': 'Nepal', 'MM': 'Myanmar', 'LA': 'Laos', 'KH': 'Cambodia',
|
|
683
|
+
'BN': 'Brunei', 'TL': 'East Timor', 'FJ': 'Fiji', 'PG': 'Papua New Guinea',
|
|
684
|
+
'SB': 'Solomon Islands', 'VU': 'Vanuatu', 'NC': 'New Caledonia', 'PF': 'French Polynesia',
|
|
685
|
+
'WS': 'Samoa', 'TO': 'Tonga', 'KI': 'Kiribati', 'TV': 'Tuvalu',
|
|
686
|
+
'NR': 'Nauru', 'PW': 'Palau', 'FM': 'Micronesia', 'MH': 'Marshall Islands',
|
|
687
|
+
'CK': 'Cook Islands', 'NU': 'Niue', 'TK': 'Tokelau', 'WF': 'Wallis and Futuna',
|
|
688
|
+
'AS': 'American Samoa', 'GU': 'Guam', 'MP': 'Northern Mariana Islands', 'VI': 'US Virgin Islands',
|
|
689
|
+
'PR': 'Puerto Rico', 'DO': 'Dominican Republic', 'CU': 'Cuba', 'JM': 'Jamaica',
|
|
690
|
+
'HT': 'Haiti', 'TT': 'Trinidad and Tobago', 'BB': 'Barbados', 'LC': 'Saint Lucia',
|
|
691
|
+
'VC': 'Saint Vincent', 'GD': 'Grenada', 'AG': 'Antigua and Barbuda', 'KN': 'Saint Kitts and Nevis',
|
|
692
|
+
'DM': 'Dominica', 'BZ': 'Belize', 'GT': 'Guatemala', 'SV': 'El Salvador',
|
|
693
|
+
'HN': 'Honduras', 'NI': 'Nicaragua', 'CR': 'Costa Rica', 'PA': 'Panama',
|
|
694
|
+
'CO': 'Colombia', 'VE': 'Venezuela', 'GY': 'Guyana', 'SR': 'Suriname',
|
|
695
|
+
'GF': 'French Guiana', 'UY': 'Uruguay', 'PY': 'Paraguay', 'BO': 'Bolivia',
|
|
696
|
+
'PE': 'Peru', 'EC': 'Ecuador', 'CL': 'Chile', 'FK': 'Falkland Islands',
|
|
697
|
+
'GS': 'South Georgia', 'SH': 'Saint Helena', 'AC': 'Ascension Island', 'TA': 'Tristan da Cunha',
|
|
698
|
+
'CV': 'Cape Verde', 'ST': 'São Tomé and Príncipe', 'GQ': 'Equatorial Guinea', 'GA': 'Gabon',
|
|
699
|
+
'CG': 'Republic of the Congo', 'CD': 'Democratic Republic of the Congo', 'CF': 'Central African Republic',
|
|
700
|
+
'TD': 'Chad', 'CM': 'Cameroon', 'NE': 'Niger', 'BF': 'Burkina Faso',
|
|
701
|
+
'ML': 'Mali', 'SN': 'Senegal', 'GM': 'Gambia', 'GW': 'Guinea-Bissau',
|
|
702
|
+
'GN': 'Guinea', 'SL': 'Sierra Leone', 'LR': 'Liberia', 'CI': 'Ivory Coast',
|
|
703
|
+
'GH': 'Ghana', 'TG': 'Togo', 'BJ': 'Benin', 'DZ': 'Algeria',
|
|
704
|
+
'TN': 'Tunisia', 'LY': 'Libya', 'SD': 'Sudan', 'SS': 'South Sudan',
|
|
705
|
+
'ET': 'Ethiopia', 'ER': 'Eritrea', 'DJ': 'Djibouti', 'SO': 'Somalia',
|
|
706
|
+
'UG': 'Uganda', 'RW': 'Rwanda', 'BI': 'Burundi', 'TZ': 'Tanzania',
|
|
707
|
+
'MW': 'Malawi', 'ZM': 'Zambia', 'ZW': 'Zimbabwe', 'BW': 'Botswana',
|
|
708
|
+
'NA': 'Namibia', 'SZ': 'Eswatini', 'LS': 'Lesotho', 'MG': 'Madagascar',
|
|
709
|
+
'MU': 'Mauritius', 'SC': 'Seychelles', 'KM': 'Comoros', 'YT': 'Mayotte',
|
|
710
|
+
'RE': 'Réunion', 'MZ': 'Mozambique', 'AO': 'Angola'
|
|
711
|
+
}
|
|
712
|
+
return countryNames[code] || code
|
|
713
|
+
},
|
|
714
|
+
|
|
715
|
+
onTriggerKeydown(event) {
|
|
716
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
717
|
+
event.preventDefault()
|
|
718
|
+
this.toggleDropdown()
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
</script>
|
|
724
|
+
|
|
725
|
+
<style scoped>
|
|
726
|
+
.vlInputGroup {
|
|
727
|
+
display: flex;
|
|
728
|
+
align-items: stretch;
|
|
729
|
+
border-radius: 0.5rem;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.country-selector {
|
|
733
|
+
position: relative;
|
|
734
|
+
flex-shrink: 0;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.country-trigger {
|
|
738
|
+
display: flex;
|
|
739
|
+
align-items: center;
|
|
740
|
+
padding: 0.5rem 0.75rem;
|
|
741
|
+
border: 1px solid #d1d5db;
|
|
742
|
+
border-right: none;
|
|
743
|
+
border-radius: 0.5rem 0 0 0.5rem;
|
|
744
|
+
background: white;
|
|
745
|
+
cursor: pointer;
|
|
746
|
+
user-select: none;
|
|
747
|
+
min-width: 120px;
|
|
748
|
+
transition: border-color 0.15s ease-in-out;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.country-trigger:hover {
|
|
752
|
+
border-color: #9ca3af;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.country-trigger.is-open {
|
|
756
|
+
border-color: #3b82f6;
|
|
757
|
+
box-shadow: 0 0 0 1px #3b82f6;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.flag-img {
|
|
761
|
+
width: 20px;
|
|
762
|
+
height: 15px;
|
|
763
|
+
margin-right: 0.375rem;
|
|
764
|
+
border-radius: 2px;
|
|
765
|
+
object-fit: cover;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.flag-fallback {
|
|
769
|
+
margin-right: 0.375rem;
|
|
770
|
+
font-size: 1rem;
|
|
771
|
+
}
|
|
772
|
+
.flag-loading {
|
|
773
|
+
margin-right: 0.375rem;
|
|
774
|
+
font-size: 0.875rem;
|
|
775
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
776
|
+
}
|
|
777
|
+
@keyframes pulse {
|
|
778
|
+
0%, 100% { opacity: 1; }
|
|
779
|
+
50% { opacity: 0.5; }
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.country-trigger .code {
|
|
783
|
+
font-weight: 500;
|
|
784
|
+
color: #374151;
|
|
785
|
+
margin-right: auto;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.country-trigger .arrow {
|
|
789
|
+
font-size: 0.625rem;
|
|
790
|
+
color: #6b7280;
|
|
791
|
+
transition: transform 0.15s ease-in-out;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.country-trigger.is-open .arrow {
|
|
795
|
+
transform: rotate(180deg);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.country-dropdown {
|
|
799
|
+
position: absolute;
|
|
800
|
+
top: 100%;
|
|
801
|
+
left: 0;
|
|
802
|
+
z-index: 50;
|
|
803
|
+
background: white;
|
|
804
|
+
border: 1px solid #d1d5db;
|
|
805
|
+
border-radius: 0.5rem;
|
|
806
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
807
|
+
max-height: 400px;
|
|
808
|
+
min-width: 320px;
|
|
809
|
+
width: max-content;
|
|
810
|
+
overflow: hidden;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.search-container {
|
|
814
|
+
padding: 0.5rem;
|
|
815
|
+
border-bottom: 1px solid #e5e7eb;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.search-input {
|
|
819
|
+
width: 100%;
|
|
820
|
+
padding: 0.5rem 0.75rem;
|
|
821
|
+
border: 1px solid #d1d5db;
|
|
822
|
+
border-radius: 0.375rem;
|
|
823
|
+
outline: none;
|
|
824
|
+
font-size: 0.875rem;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.search-input:focus {
|
|
828
|
+
border-color: #3b82f6;
|
|
829
|
+
box-shadow: 0 0 0 1px #3b82f6;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.countries-list {
|
|
833
|
+
max-height: 340px;
|
|
834
|
+
overflow-y: auto;
|
|
835
|
+
overflow-x: hidden;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.country-option {
|
|
839
|
+
display: flex;
|
|
840
|
+
align-items: center;
|
|
841
|
+
width: 100%;
|
|
842
|
+
padding: 0.5rem 0.75rem;
|
|
843
|
+
border: none;
|
|
844
|
+
background: none;
|
|
845
|
+
text-align: left;
|
|
846
|
+
cursor: pointer;
|
|
847
|
+
transition: background-color 0.15s ease-in-out;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.country-option:hover {
|
|
851
|
+
background-color: #f9fafb;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.country-option.is-selected {
|
|
855
|
+
background-color: #eff6ff;
|
|
856
|
+
color: #1d4ed8;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
.country-option .flag-img {
|
|
860
|
+
width: 20px;
|
|
861
|
+
height: 15px;
|
|
862
|
+
margin-right: 0.5rem;
|
|
863
|
+
border-radius: 2px;
|
|
864
|
+
object-fit: cover;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.country-option .flag-fallback {
|
|
868
|
+
margin-right: 0.5rem;
|
|
869
|
+
font-size: 1rem;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.country-option .name {
|
|
873
|
+
flex: 1;
|
|
874
|
+
font-size: 0.875rem;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.country-option .dial-code {
|
|
878
|
+
font-size: 0.75rem;
|
|
879
|
+
color: #6b7280;
|
|
880
|
+
font-weight: 500;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.phone-input {
|
|
884
|
+
flex: 1;
|
|
885
|
+
padding: 0.5rem 0.75rem;
|
|
886
|
+
border: 1px solid #d1d5db;
|
|
887
|
+
border-radius: 0 0.5rem 0.5rem 0;
|
|
888
|
+
outline: none;
|
|
889
|
+
font-size: 1rem;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.phone-input:focus {
|
|
893
|
+
border-color: #3b82f6;
|
|
894
|
+
box-shadow: 0 0 0 1px #3b82f6;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
.phone-input:disabled {
|
|
898
|
+
background-color: #f9fafb;
|
|
899
|
+
color: #6b7280;
|
|
900
|
+
cursor: not-allowed;
|
|
901
|
+
}
|
|
902
|
+
</style>
|