js-kompo-utils 1.0.34 → 1.0.36
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.
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
<input
|
|
57
57
|
ref="phoneInput"
|
|
58
58
|
class="phone-input"
|
|
59
|
+
:class="{ 'has-extension': showExtension }"
|
|
59
60
|
:value="formattedValue"
|
|
60
61
|
v-bind="$_attributes"
|
|
61
62
|
v-on="$_events"
|
|
@@ -64,6 +65,44 @@
|
|
|
64
65
|
@focus="onPhoneFocus"
|
|
65
66
|
@blur="onPhoneBlur"
|
|
66
67
|
/>
|
|
68
|
+
|
|
69
|
+
<!-- Extension ("poste"), opt-in via ->withExtension(). Collapsed until asked for,
|
|
70
|
+
so the common case stays a single field. -->
|
|
71
|
+
<button
|
|
72
|
+
v-if="showExtension && !extensionOpen"
|
|
73
|
+
type="button"
|
|
74
|
+
class="phone-extension-toggle"
|
|
75
|
+
:title="extensionLabel"
|
|
76
|
+
:disabled="$_attributes.disabled"
|
|
77
|
+
@click="openExtension"
|
|
78
|
+
>
|
|
79
|
+
<span class="phone-extension-plus">+</span>
|
|
80
|
+
<span class="phone-extension-toggle-label">{{ extensionLabel }}</span>
|
|
81
|
+
</button>
|
|
82
|
+
|
|
83
|
+
<div v-else-if="showExtension" class="phone-extension-wrap">
|
|
84
|
+
<span class="phone-extension-prefix">{{ extensionLabel }}</span>
|
|
85
|
+
<input
|
|
86
|
+
ref="extensionInput"
|
|
87
|
+
class="phone-extension-input"
|
|
88
|
+
type="text"
|
|
89
|
+
inputmode="numeric"
|
|
90
|
+
autocomplete="off"
|
|
91
|
+
:value="extensionValue"
|
|
92
|
+
:placeholder="extensionPlaceholder"
|
|
93
|
+
:disabled="$_attributes.disabled"
|
|
94
|
+
:readonly="$_attributes.readonly"
|
|
95
|
+
@input="onExtensionInput"
|
|
96
|
+
@keydown.esc="closeExtension"
|
|
97
|
+
/>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class="phone-extension-clear"
|
|
101
|
+
:title="extensionClearLabel"
|
|
102
|
+
:disabled="$_attributes.disabled"
|
|
103
|
+
@click="closeExtension"
|
|
104
|
+
>×</button>
|
|
105
|
+
</div>
|
|
67
106
|
</div>
|
|
68
107
|
</vl-form-field>
|
|
69
108
|
</template>
|
|
@@ -96,6 +135,11 @@ export default {
|
|
|
96
135
|
|
|
97
136
|
// Geolocation state
|
|
98
137
|
isDetectingCountry: false,
|
|
138
|
+
|
|
139
|
+
// Optional extension ("poste"), submitted as `<name>_ext`. Starts open only
|
|
140
|
+
// when there is one to show, so an empty field stays a single box.
|
|
141
|
+
extensionValue: this.normalizeExtension(this.$_config('extensionValue')),
|
|
142
|
+
extensionOpen: this.normalizeExtension(this.$_config('extensionValue')) !== '',
|
|
99
143
|
}
|
|
100
144
|
},
|
|
101
145
|
|
|
@@ -114,6 +158,22 @@ export default {
|
|
|
114
158
|
placeholder() {
|
|
115
159
|
return this.$_config('placeholder') || 'Phone number'
|
|
116
160
|
},
|
|
161
|
+
|
|
162
|
+
showExtension() {
|
|
163
|
+
return !!this.$_config('withExtension')
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
extensionLabel() {
|
|
167
|
+
return this.$_config('extensionLabel') || 'ext'
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
extensionPlaceholder() {
|
|
171
|
+
return this.$_config('extensionPlaceholder') || '000'
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
extensionClearLabel() {
|
|
175
|
+
return this.$_config('extensionClearLabel') || 'Remove'
|
|
176
|
+
},
|
|
117
177
|
|
|
118
178
|
inputGroupClass() {
|
|
119
179
|
const baseClass = 'vlInputGroup rounded-lg flex items-stretch'
|
|
@@ -121,6 +181,14 @@ export default {
|
|
|
121
181
|
return `${baseClass} ${errorClass}`
|
|
122
182
|
},
|
|
123
183
|
|
|
184
|
+
// An explicit country() is a hard setting. Otherwise the IP lookup runs when it is turned
|
|
185
|
+
// on — its result wins over defaultCountry — or when there is no default to start from.
|
|
186
|
+
shouldDetectByIp() {
|
|
187
|
+
if (this.$_config('country')) return false
|
|
188
|
+
|
|
189
|
+
return !!this.$_config('detectCountryByIp') || !this.selectedCountryCode
|
|
190
|
+
},
|
|
191
|
+
|
|
124
192
|
// Current country info
|
|
125
193
|
currentCountry() {
|
|
126
194
|
const country = this.countries.find(c => c.code === this.selectedCountryCode)
|
|
@@ -212,6 +280,13 @@ export default {
|
|
|
212
280
|
}
|
|
213
281
|
},
|
|
214
282
|
|
|
283
|
+
// Kompo swaps `component` in place on a refresh without re-running data(), which is why
|
|
284
|
+
// the number re-syncs above. The extension has to follow or it keeps the stale prefill.
|
|
285
|
+
'component.config.extensionValue'(newValue) {
|
|
286
|
+
this.extensionValue = this.normalizeExtension(newValue)
|
|
287
|
+
this.extensionOpen = this.extensionOpen || this.extensionValue !== ''
|
|
288
|
+
},
|
|
289
|
+
|
|
215
290
|
selectedCountryCode(newCode, oldCode) {
|
|
216
291
|
if (newCode !== oldCode && this.phoneValue) {
|
|
217
292
|
this.reformatWithCountry(newCode)
|
|
@@ -226,10 +301,10 @@ export default {
|
|
|
226
301
|
// Set initial value
|
|
227
302
|
// this.component.value = this.phoneValue
|
|
228
303
|
|
|
229
|
-
//
|
|
304
|
+
// A stored number always decides its own country; an empty field may ask the IP.
|
|
230
305
|
if (this.phoneValue) {
|
|
231
306
|
this.autoDetectCountryFromPhone(this.phoneValue)
|
|
232
|
-
} else if (
|
|
307
|
+
} else if (this.shouldDetectByIp) {
|
|
233
308
|
this.detectCountryByLocation()
|
|
234
309
|
}
|
|
235
310
|
},
|
|
@@ -239,6 +314,42 @@ export default {
|
|
|
239
314
|
},
|
|
240
315
|
|
|
241
316
|
methods: {
|
|
317
|
+
// The extension is not part of the number, so it rides alongside it as its own
|
|
318
|
+
// key instead of being folded into the value the way E.164 cannot carry it.
|
|
319
|
+
$_fill(jsonFormData) {
|
|
320
|
+
jsonFormData[this.$_name] = (this.$_value === null || this.$_value === undefined) ? '' : this.$_value
|
|
321
|
+
|
|
322
|
+
if (this.showExtension) {
|
|
323
|
+
jsonFormData[this.$_name + '_ext'] = this.extensionValue || ''
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
// `|| ''` would swallow a "0" extension, and the column is a string.
|
|
328
|
+
normalizeExtension(value) {
|
|
329
|
+
return (value === null || value === undefined) ? '' : String(value)
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
onExtensionInput(event) {
|
|
333
|
+
this.extensionValue = event.target.value.replace(/[^0-9#*]/g, '')
|
|
334
|
+
|
|
335
|
+
if (event.target.value !== this.extensionValue) {
|
|
336
|
+
event.target.value = this.extensionValue
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
openExtension() {
|
|
341
|
+
this.extensionOpen = true
|
|
342
|
+
|
|
343
|
+
this.$nextTick(() => this.$refs.extensionInput?.focus())
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
// Collapsing clears the value: the empty companion still posts, so the stored
|
|
347
|
+
// extension is removed rather than silently kept.
|
|
348
|
+
closeExtension() {
|
|
349
|
+
this.extensionValue = ''
|
|
350
|
+
this.extensionOpen = false
|
|
351
|
+
},
|
|
352
|
+
|
|
242
353
|
// Country selection methods
|
|
243
354
|
toggleDropdown() {
|
|
244
355
|
this.isDropdownOpen = !this.isDropdownOpen
|
|
@@ -569,40 +680,25 @@ export default {
|
|
|
569
680
|
}
|
|
570
681
|
},
|
|
571
682
|
|
|
683
|
+
// Free, keyless, https + CORS. http:// providers are mixed content on an https page and
|
|
684
|
+
// never resolve, and a provider answering without a code (ipapi.co rate-limits often) has
|
|
685
|
+
// to fall through to the next one instead of ending the chain.
|
|
572
686
|
async getCountryFromIP() {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const response = await fetch('http://ip-api.com/json/', {
|
|
587
|
-
timeout: 5000
|
|
588
|
-
})
|
|
589
|
-
const data = await response.json()
|
|
590
|
-
return data.countryCode
|
|
591
|
-
} catch (error) {
|
|
592
|
-
console.warn('ip-api.com failed:', error)
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Fallback to ipinfo.io
|
|
596
|
-
try {
|
|
597
|
-
const response = await fetch('https://ipinfo.io/json', {
|
|
598
|
-
timeout: 5000
|
|
599
|
-
})
|
|
600
|
-
const data = await response.json()
|
|
601
|
-
return data.country
|
|
602
|
-
} catch (error) {
|
|
603
|
-
console.warn('ipinfo.io failed:', error)
|
|
687
|
+
const providers = [
|
|
688
|
+
['https://get.geojs.io/v1/ip/country.json', data => data.country],
|
|
689
|
+
['https://ipwho.is/', data => data.success && data.country_code],
|
|
690
|
+
['https://ipapi.co/json/', data => data.country_code],
|
|
691
|
+
]
|
|
692
|
+
|
|
693
|
+
for (const [url, readCountry] of providers) {
|
|
694
|
+
try {
|
|
695
|
+
const code = readCountry(await (await fetch(url)).json())
|
|
696
|
+
if (code) return code
|
|
697
|
+
} catch (error) {
|
|
698
|
+
console.warn(`${url} failed:`, error)
|
|
699
|
+
}
|
|
604
700
|
}
|
|
605
|
-
|
|
701
|
+
|
|
606
702
|
return null
|
|
607
703
|
},
|
|
608
704
|
|
|
@@ -922,4 +1018,100 @@ export default {
|
|
|
922
1018
|
color: #6b7280;
|
|
923
1019
|
cursor: not-allowed;
|
|
924
1020
|
}
|
|
1021
|
+
|
|
1022
|
+
.phone-input.has-extension {
|
|
1023
|
+
border-radius: 0;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/* Collapsed state: a quiet pill closing the group's right edge. */
|
|
1027
|
+
.phone-extension-toggle {
|
|
1028
|
+
display: flex;
|
|
1029
|
+
align-items: center;
|
|
1030
|
+
gap: 0.2rem;
|
|
1031
|
+
flex: none;
|
|
1032
|
+
padding: 0 0.7rem;
|
|
1033
|
+
margin-left: -1px;
|
|
1034
|
+
border: 1px solid #d1d5db;
|
|
1035
|
+
border-radius: 0 0.5rem 0.5rem 0;
|
|
1036
|
+
background-color: transparent;
|
|
1037
|
+
color: #6b7280;
|
|
1038
|
+
font-size: 0.8125rem;
|
|
1039
|
+
line-height: 1;
|
|
1040
|
+
white-space: nowrap;
|
|
1041
|
+
cursor: pointer;
|
|
1042
|
+
transition: color 0.15s ease, background-color 0.15s ease;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.phone-extension-toggle:hover:not(:disabled) {
|
|
1046
|
+
color: #3b82f6;
|
|
1047
|
+
background-color: #f3f4f6;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.phone-extension-toggle:disabled {
|
|
1051
|
+
cursor: not-allowed;
|
|
1052
|
+
opacity: 0.6;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.phone-extension-plus {
|
|
1056
|
+
font-size: 0.95rem;
|
|
1057
|
+
line-height: 1;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/* Expanded state: label + field + dismiss, sharing one bordered cell. */
|
|
1061
|
+
.phone-extension-wrap {
|
|
1062
|
+
display: flex;
|
|
1063
|
+
align-items: center;
|
|
1064
|
+
flex: none;
|
|
1065
|
+
margin-left: -1px;
|
|
1066
|
+
padding-left: 0.6rem;
|
|
1067
|
+
border: 1px solid #d1d5db;
|
|
1068
|
+
border-radius: 0 0.5rem 0.5rem 0;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.phone-extension-wrap:focus-within {
|
|
1072
|
+
border-color: #3b82f6;
|
|
1073
|
+
box-shadow: 0 0 0 1px #3b82f6;
|
|
1074
|
+
z-index: 1;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
.phone-extension-prefix {
|
|
1078
|
+
color: #9ca3af;
|
|
1079
|
+
font-size: 0.8125rem;
|
|
1080
|
+
white-space: nowrap;
|
|
1081
|
+
pointer-events: none;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.phone-extension-input {
|
|
1085
|
+
width: 3.75rem;
|
|
1086
|
+
min-width: 0;
|
|
1087
|
+
padding: 0.5rem 0.25rem;
|
|
1088
|
+
border: none;
|
|
1089
|
+
outline: none;
|
|
1090
|
+
background-color: transparent;
|
|
1091
|
+
font-size: 1rem;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
.phone-extension-input:disabled {
|
|
1095
|
+
color: #6b7280;
|
|
1096
|
+
cursor: not-allowed;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
.phone-extension-clear {
|
|
1100
|
+
padding: 0 0.55rem 0 0.15rem;
|
|
1101
|
+
border: none;
|
|
1102
|
+
background-color: transparent;
|
|
1103
|
+
color: #9ca3af;
|
|
1104
|
+
font-size: 1.05rem;
|
|
1105
|
+
line-height: 1;
|
|
1106
|
+
cursor: pointer;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.phone-extension-clear:hover:not(:disabled) {
|
|
1110
|
+
color: #ef4444;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.phone-extension-clear:disabled {
|
|
1114
|
+
cursor: not-allowed;
|
|
1115
|
+
opacity: 0.6;
|
|
1116
|
+
}
|
|
925
1117
|
</style>
|