fds-vue-core 8.2.2 → 8.2.4
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/FdsSearchSelect/FdsSearchSelect.vue.d.ts +3 -3
- package/dist/components/FdsSearchSelectPro/FdsSearchSelectPro.vue.d.ts +3 -3
- package/dist/components/Form/FdsInput/FdsInput.vue.d.ts +2 -2
- package/dist/components/Form/FdsPhonenumber/FdsPhonenumber.vue.d.ts +0 -3
- package/dist/components/Form/FdsPhonenumber/normalizePhoneInput.d.ts +7 -0
- package/dist/components/Form/FdsPhonenumber/phoneMask.d.ts +2 -0
- package/dist/components/Form/FdsPhonenumber/types.d.ts +1 -1
- package/dist/fds-vue-core.cjs.js +166 -37
- package/dist/fds-vue-core.cjs.js.map +1 -1
- package/dist/fds-vue-core.css +1 -1
- package/dist/fds-vue-core.es.js +166 -37
- package/dist/fds-vue-core.es.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/components/Form/FdsInput/FdsInput.stories.ts +21 -1
- package/src/components/Form/FdsInput/FdsInput.vue +46 -28
- package/src/components/Form/FdsPhonenumber/FdsPhonenumber.vue +31 -10
- package/src/components/Form/FdsPhonenumber/FdsPhonenumberCountryPicker.vue +1 -1
- package/src/components/Form/FdsPhonenumber/normalizePhoneInput.ts +88 -0
- package/src/components/Form/FdsPhonenumber/phoneMask.ts +28 -0
- package/src/components/Form/FdsPhonenumber/types.ts +0 -3
package/package.json
CHANGED
|
@@ -63,7 +63,7 @@ const meta: Meta<typeof FdsInput> = {
|
|
|
63
63
|
},
|
|
64
64
|
maskOptions: {
|
|
65
65
|
control: { type: 'object' },
|
|
66
|
-
description: 'Additional options
|
|
66
|
+
description: 'Additional IMask options (lazy defaults to true).',
|
|
67
67
|
},
|
|
68
68
|
maxlength: {
|
|
69
69
|
control: { type: 'number' },
|
|
@@ -302,6 +302,26 @@ export const WithMaskPhone: Story = {
|
|
|
302
302
|
}),
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
export const WithMaskPostalCode: Story = {
|
|
306
|
+
render: () => ({
|
|
307
|
+
components: { FdsInput },
|
|
308
|
+
setup() {
|
|
309
|
+
const postalCode = ref('')
|
|
310
|
+
return { postalCode }
|
|
311
|
+
},
|
|
312
|
+
template: `
|
|
313
|
+
<div>
|
|
314
|
+
<FdsInput
|
|
315
|
+
v-model="postalCode"
|
|
316
|
+
label="Postnummer"
|
|
317
|
+
mask="000 00"
|
|
318
|
+
/>
|
|
319
|
+
<p class="mt-4">Värde: {{ postalCode }}</p>
|
|
320
|
+
</div>
|
|
321
|
+
`,
|
|
322
|
+
}),
|
|
323
|
+
}
|
|
324
|
+
|
|
305
325
|
export const AllStates: Story = {
|
|
306
326
|
render: () => ({
|
|
307
327
|
components: { FdsInput },
|
|
@@ -38,7 +38,7 @@ const props = withDefaults(defineProps<FdsInputProps>(), {
|
|
|
38
38
|
|
|
39
39
|
const emit = defineEmits<{
|
|
40
40
|
(e: 'input', ev: Event): void
|
|
41
|
-
(e: '
|
|
41
|
+
(e: 'autocomplete', ev: Event): void
|
|
42
42
|
(e: 'clearInput'): void
|
|
43
43
|
(e: 'update:value', value: string): void
|
|
44
44
|
(e: 'keyup', ev: KeyboardEvent): void
|
|
@@ -49,8 +49,12 @@ const emit = defineEmits<{
|
|
|
49
49
|
const { id, type, maxlength, attrs } = useAttrsWithDefaults(props)
|
|
50
50
|
const { t } = useFdsI18n()
|
|
51
51
|
|
|
52
|
-
// Get type from attrs first (user-provided), then props, with 'text' as fallback
|
|
53
|
-
|
|
52
|
+
// Get type from attrs first (user-provided), then props, with 'text' as fallback.
|
|
53
|
+
// IMask only supports type="text".
|
|
54
|
+
const inputType = computed(() => {
|
|
55
|
+
if (props.mask) return 'text'
|
|
56
|
+
return type.value ?? props.type ?? 'text'
|
|
57
|
+
})
|
|
54
58
|
|
|
55
59
|
const autoId = `fds-input-${Math.random().toString(36).slice(2, 9)}`
|
|
56
60
|
const inputId = computed(() => id.value ?? autoId)
|
|
@@ -138,28 +142,29 @@ const inputPaddingRight = computed(() => {
|
|
|
138
142
|
|
|
139
143
|
const externalClass = computed(() => (props.class ?? attrs.class) as HTMLAttributes['class'] | undefined)
|
|
140
144
|
|
|
145
|
+
const inputPaddingClasses = 'px-3 py-[calc(0.75rem-1px)]'
|
|
146
|
+
|
|
141
147
|
const inputClasses = computed(() => [
|
|
142
|
-
'block rounded-md border border-gray-500
|
|
148
|
+
'block rounded-md border border-gray-500',
|
|
143
149
|
maxlength.value ? '' : 'w-full',
|
|
150
|
+
inputPaddingClasses,
|
|
144
151
|
inputPaddingRight.value,
|
|
145
152
|
'focus:outline-2 focus:outline-blue-500 -outline-offset-2 focus:border-transparent',
|
|
146
153
|
showDateIcon.value &&
|
|
147
154
|
'[&::-webkit-calendar-picker-indicator]:opacity-0 [&::-webkit-calendar-picker-indicator]:pointer-events-none',
|
|
148
155
|
props.disabled
|
|
149
156
|
? 'text-gray-800 outline-dashed outline-2 outline-gray-400 cursor-not-allowed border-transparent'
|
|
150
|
-
: 'bg-white',
|
|
157
|
+
: 'text-gray-900 bg-white',
|
|
151
158
|
isInvalid.value && 'outline-2 outline-red-600',
|
|
152
159
|
props.inputClass,
|
|
153
160
|
])
|
|
154
161
|
|
|
155
162
|
const inputStyle = computed(() => {
|
|
156
|
-
if (maxlength.value)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
163
|
+
if (!maxlength.value) return undefined
|
|
164
|
+
return {
|
|
165
|
+
width: `calc(${maxlength.value}ch + 1.5rem + 0.25rem)`,
|
|
166
|
+
maxWidth: '100%',
|
|
161
167
|
}
|
|
162
|
-
return {}
|
|
163
168
|
})
|
|
164
169
|
|
|
165
170
|
const rightIconsContainerClasses = computed(() => [
|
|
@@ -167,6 +172,11 @@ const rightIconsContainerClasses = computed(() => [
|
|
|
167
172
|
showClearButton.value ? 'right-2' : 'right-3',
|
|
168
173
|
])
|
|
169
174
|
|
|
175
|
+
const inputBindings = computed(() => ({
|
|
176
|
+
...inputAttrs.value,
|
|
177
|
+
...(props.mask ? {} : { value: internalValue.value }),
|
|
178
|
+
}))
|
|
179
|
+
|
|
170
180
|
// Use modelValue if bound via v-model, otherwise use value prop
|
|
171
181
|
const internalValue = computed({
|
|
172
182
|
get: () => (modelValue.value !== undefined ? modelValue.value : (props.value ?? '')),
|
|
@@ -180,6 +190,9 @@ const internalValue = computed({
|
|
|
180
190
|
|
|
181
191
|
function onClear() {
|
|
182
192
|
internalValue.value = ''
|
|
193
|
+
if (maskInstance) {
|
|
194
|
+
maskInstance.value = ''
|
|
195
|
+
}
|
|
183
196
|
emit('clearInput')
|
|
184
197
|
}
|
|
185
198
|
|
|
@@ -221,27 +234,22 @@ const inputRef = ref<HTMLInputElement | null>(null)
|
|
|
221
234
|
let maskInstance: any = null
|
|
222
235
|
|
|
223
236
|
const createMask = () => {
|
|
224
|
-
// Destroy existing mask if any
|
|
225
237
|
if (maskInstance) {
|
|
226
238
|
maskInstance.destroy()
|
|
227
239
|
maskInstance = null
|
|
228
240
|
}
|
|
229
241
|
|
|
230
|
-
// Create new mask if mask prop is provided and input exists
|
|
231
242
|
if (props.mask && inputRef.value) {
|
|
232
|
-
|
|
243
|
+
maskInstance = IMask(inputRef.value, {
|
|
233
244
|
mask: props.mask,
|
|
234
245
|
...props.maskOptions,
|
|
235
|
-
|
|
246
|
+
lazy: props.maskOptions?.lazy ?? true,
|
|
247
|
+
} as any)
|
|
236
248
|
|
|
237
|
-
maskInstance = IMask(inputRef.value, maskConfig as any)
|
|
238
|
-
|
|
239
|
-
// Sync mask value with internalValue
|
|
240
249
|
if (internalValue.value) {
|
|
241
250
|
maskInstance.value = internalValue.value
|
|
242
251
|
}
|
|
243
252
|
|
|
244
|
-
// Update internalValue when mask value changes
|
|
245
253
|
maskInstance.on('accept', () => {
|
|
246
254
|
if (maskInstance) {
|
|
247
255
|
internalValue.value = maskInstance.value
|
|
@@ -279,24 +287,35 @@ const ariaDescribedBy = computed(() => {
|
|
|
279
287
|
return props.meta ? metaId.value : undefined
|
|
280
288
|
})
|
|
281
289
|
|
|
282
|
-
// Watch for mask prop changes and recreate mask
|
|
290
|
+
// Watch for mask-related prop changes and recreate mask
|
|
283
291
|
watch(
|
|
284
|
-
() => props.mask,
|
|
292
|
+
() => [props.mask, props.maskOptions] as const,
|
|
285
293
|
() => {
|
|
286
294
|
nextTick(() => {
|
|
287
295
|
createMask()
|
|
288
296
|
})
|
|
289
297
|
},
|
|
298
|
+
{ deep: true },
|
|
290
299
|
)
|
|
291
300
|
|
|
301
|
+
function shouldSyncMaskFromModel(newValue: string | undefined, currentMaskedValue: string): boolean {
|
|
302
|
+
if (newValue === undefined) {
|
|
303
|
+
return false
|
|
304
|
+
}
|
|
305
|
+
if (document.activeElement !== inputRef.value) {
|
|
306
|
+
return true
|
|
307
|
+
}
|
|
308
|
+
const newDigits = String(newValue).replace(/\D/g, '')
|
|
309
|
+
const currentDigits = String(currentMaskedValue ?? '').replace(/\D/g, '')
|
|
310
|
+
return newDigits !== currentDigits
|
|
311
|
+
}
|
|
312
|
+
|
|
292
313
|
// Watch for external value changes and update mask
|
|
293
|
-
// Only update if the value actually changed and user is not actively editing
|
|
294
314
|
watch(
|
|
295
315
|
() => props.value,
|
|
296
316
|
(newValue) => {
|
|
297
317
|
if (maskInstance && newValue !== undefined && newValue !== maskInstance.value) {
|
|
298
|
-
|
|
299
|
-
if (document.activeElement !== inputRef.value) {
|
|
318
|
+
if (shouldSyncMaskFromModel(newValue, maskInstance.value)) {
|
|
300
319
|
maskInstance.value = newValue
|
|
301
320
|
}
|
|
302
321
|
}
|
|
@@ -307,8 +326,7 @@ watch(
|
|
|
307
326
|
() => modelValue.value,
|
|
308
327
|
(newValue) => {
|
|
309
328
|
if (maskInstance && newValue !== undefined && newValue !== maskInstance.value) {
|
|
310
|
-
|
|
311
|
-
if (document.activeElement !== inputRef.value) {
|
|
329
|
+
if (shouldSyncMaskFromModel(newValue, maskInstance.value)) {
|
|
312
330
|
maskInstance.value = newValue
|
|
313
331
|
}
|
|
314
332
|
}
|
|
@@ -340,7 +358,6 @@ watch(
|
|
|
340
358
|
ref="inputRef"
|
|
341
359
|
:type="inputType === 'password' ? (showPassword ? 'text' : 'password') : inputType"
|
|
342
360
|
:required="props.required"
|
|
343
|
-
:value="internalValue"
|
|
344
361
|
:disabled="props.disabled"
|
|
345
362
|
:tabindex="props.disabled ? -1 : undefined"
|
|
346
363
|
:aria-invalid="props.valid === false ? 'true' : undefined"
|
|
@@ -353,9 +370,10 @@ watch(
|
|
|
353
370
|
:placeholder="props.placeholder"
|
|
354
371
|
:pattern="props.pattern"
|
|
355
372
|
:searchIcon="props.searchIcon"
|
|
356
|
-
v-bind="
|
|
373
|
+
v-bind="inputBindings"
|
|
357
374
|
@input="handleInputChange"
|
|
358
375
|
@change="handleInputChange"
|
|
376
|
+
@autocomplete="$emit('autocomplete', $event)"
|
|
359
377
|
@blur="$emit('blur', $event)"
|
|
360
378
|
@keyup="
|
|
361
379
|
(ev) => {
|
|
@@ -3,6 +3,8 @@ import { computed, ref, useAttrs, watch } from 'vue'
|
|
|
3
3
|
import { useFdsI18n } from '../../../plugin/useFdsI18n'
|
|
4
4
|
import FdsInput from '../FdsInput/FdsInput.vue'
|
|
5
5
|
import { buildCountryOptions, sortCountryOptionsByName } from './countries'
|
|
6
|
+
import { getPhoneMaskForCountry } from './phoneMask'
|
|
7
|
+
import { normalizeNationalPhoneInput, shouldNormalizePhoneInput } from './normalizePhoneInput'
|
|
6
8
|
import FdsPhonenumberCountryPicker from './FdsPhonenumberCountryPicker.vue'
|
|
7
9
|
import type { FdsPhonenumberEmits, FdsPhonenumberProps } from './types'
|
|
8
10
|
import { getPhoneValidationState, validatePhoneNumber } from './validatePhone'
|
|
@@ -11,7 +13,7 @@ defineOptions({
|
|
|
11
13
|
inheritAttrs: false,
|
|
12
14
|
})
|
|
13
15
|
|
|
14
|
-
const [nationalNumber
|
|
16
|
+
const [nationalNumber] = defineModel<string>({ default: '' })
|
|
15
17
|
const country = defineModel<string>('country', { default: 'SE' })
|
|
16
18
|
const attrs = useAttrs()
|
|
17
19
|
|
|
@@ -25,12 +27,9 @@ const props = withDefaults(defineProps<FdsPhonenumberProps>(), {
|
|
|
25
27
|
autocomplete: 'tel',
|
|
26
28
|
required: false,
|
|
27
29
|
placeholder: undefined,
|
|
28
|
-
maxlength: undefined,
|
|
29
|
-
minlength: undefined,
|
|
30
30
|
name: undefined,
|
|
31
31
|
autofocus: false,
|
|
32
32
|
readonly: false,
|
|
33
|
-
pattern: undefined,
|
|
34
33
|
defaultCountry: 'SE',
|
|
35
34
|
countries: undefined,
|
|
36
35
|
locale: undefined,
|
|
@@ -57,12 +56,10 @@ const forwardedInputProps = computed(() => ({
|
|
|
57
56
|
autocomplete: props.autocomplete,
|
|
58
57
|
required: props.required,
|
|
59
58
|
placeholder: props.placeholder,
|
|
60
|
-
maxlength: props.maxlength,
|
|
61
|
-
minlength: props.minlength,
|
|
62
59
|
name: props.name,
|
|
63
60
|
autofocus: props.autofocus,
|
|
64
61
|
readonly: props.readonly,
|
|
65
|
-
|
|
62
|
+
inputmode: 'tel',
|
|
66
63
|
...inputAttrs.value,
|
|
67
64
|
}))
|
|
68
65
|
|
|
@@ -77,6 +74,12 @@ const countryItems = computed(() => {
|
|
|
77
74
|
)
|
|
78
75
|
})
|
|
79
76
|
|
|
77
|
+
const phoneMask = computed(() => getPhoneMaskForCountry(country.value ?? props.defaultCountry))
|
|
78
|
+
|
|
79
|
+
const phoneMaskOptions = computed(() => ({
|
|
80
|
+
prepare: (value: string) => applyPhoneNormalization(value, country.value ?? props.defaultCountry),
|
|
81
|
+
}))
|
|
82
|
+
|
|
80
83
|
/** Set after the phone number field has blurred; validation does not run on input. */
|
|
81
84
|
const committedValid = ref<boolean | null>(null)
|
|
82
85
|
|
|
@@ -109,6 +112,20 @@ function runValidation() {
|
|
|
109
112
|
emit('update:e164', result.isValid && result.phoneNumber ? result.phoneNumber : '')
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
function applyPhoneNormalization(value: string, countryCode = country.value ?? props.defaultCountry): string {
|
|
116
|
+
if (!shouldNormalizePhoneInput(value, countryCode)) {
|
|
117
|
+
return value
|
|
118
|
+
}
|
|
119
|
+
return normalizeNationalPhoneInput(value, countryCode)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
watch(nationalNumber, (value) => {
|
|
123
|
+
const normalized = applyPhoneNormalization(value ?? '')
|
|
124
|
+
if (normalized !== (value ?? '')) {
|
|
125
|
+
nationalNumber.value = normalized
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
112
129
|
watch(country, () => {
|
|
113
130
|
if (committedValid.value === null) {
|
|
114
131
|
return
|
|
@@ -117,6 +134,10 @@ watch(country, () => {
|
|
|
117
134
|
})
|
|
118
135
|
|
|
119
136
|
function handleBlur(ev: FocusEvent) {
|
|
137
|
+
const normalized = applyPhoneNormalization(nationalNumber.value ?? '')
|
|
138
|
+
if (normalized !== (nationalNumber.value ?? '')) {
|
|
139
|
+
nationalNumber.value = normalized
|
|
140
|
+
}
|
|
120
141
|
runValidation()
|
|
121
142
|
emit('blur', ev)
|
|
122
143
|
}
|
|
@@ -139,7 +160,7 @@ function onNoCountryResults(value: boolean) {
|
|
|
139
160
|
<div v-if="meta" class="font-thin mb-1">
|
|
140
161
|
{{ meta }}
|
|
141
162
|
</div>
|
|
142
|
-
<div class="flex flex-wrap items-start gap-x-
|
|
163
|
+
<div class="flex flex-wrap items-start gap-x-2">
|
|
143
164
|
<FdsPhonenumberCountryPicker
|
|
144
165
|
v-model="country"
|
|
145
166
|
:items="countryItems"
|
|
@@ -152,8 +173,8 @@ function onNoCountryResults(value: boolean) {
|
|
|
152
173
|
/>
|
|
153
174
|
<FdsInput
|
|
154
175
|
v-model="nationalNumber"
|
|
155
|
-
:
|
|
156
|
-
|
|
176
|
+
:mask="phoneMask"
|
|
177
|
+
:mask-options="phoneMaskOptions"
|
|
157
178
|
:valid="displayValid"
|
|
158
179
|
:disabled="disabled"
|
|
159
180
|
:optional="optional"
|
|
@@ -377,7 +377,7 @@ onBeforeUnmount(() => {
|
|
|
377
377
|
v-if="dropdownOpen && !disabled && filteredCountries.length > 0"
|
|
378
378
|
role="listbox"
|
|
379
379
|
:id="listboxId"
|
|
380
|
-
class="absolute left-0 z-20 mt-
|
|
380
|
+
class="absolute left-0 z-20 mt-2 min-w-[20rem] w-max max-w-[calc(100vw-2rem)] max-h-[280px] overflow-y-auto rounded-md border border-gray-300 bg-white"
|
|
381
381
|
v-bind="listDataAttrs"
|
|
382
382
|
@mouseleave="clearHoverPreview"
|
|
383
383
|
>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import parsePhoneNumberFromString, { getCountryCallingCode, type CountryCode } from 'libphonenumber-js/mobile/es6'
|
|
2
|
+
import { getPhoneMaskForCountry } from './phoneMask'
|
|
3
|
+
|
|
4
|
+
const nationalDigitsForMask = (parsed: NonNullable<ReturnType<typeof parsePhoneNumberFromString>>): string =>
|
|
5
|
+
parsed.formatNational().replace(/\D/g, '')
|
|
6
|
+
|
|
7
|
+
const tryParseForCountry = (candidate: string, country: CountryCode): ReturnType<typeof parsePhoneNumberFromString> => {
|
|
8
|
+
const parsed = parsePhoneNumberFromString(candidate, country) ?? parsePhoneNumberFromString(candidate)
|
|
9
|
+
if (!parsed?.isValid() || parsed.country !== country) {
|
|
10
|
+
return undefined
|
|
11
|
+
}
|
|
12
|
+
return parsed
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Whether the value likely contains a country calling code or international formatting. */
|
|
16
|
+
export const shouldNormalizePhoneInput = (value: string, countryIso2: string): boolean => {
|
|
17
|
+
const trimmed = value.trim()
|
|
18
|
+
if (!trimmed) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
if (trimmed.includes('+') || trimmed.startsWith('00')) {
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const digits = trimmed.replace(/\D/g, '')
|
|
26
|
+
const mask = getPhoneMaskForCountry(countryIso2)
|
|
27
|
+
if (!mask) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const maxDigits = (mask.match(/0/g) ?? []).length
|
|
32
|
+
if (digits.length > maxDigits) {
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const callingCode = getCountryCallingCode(countryIso2 as CountryCode)
|
|
38
|
+
if (digits.startsWith(callingCode) && digits.length > maxDigits - 1) {
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Converts international/autocomplete values to national digits for the IMask field.
|
|
50
|
+
* IMask reapplies spacing from the country mask.
|
|
51
|
+
*/
|
|
52
|
+
export const normalizeNationalPhoneInput = (value: string, countryIso2: string): string => {
|
|
53
|
+
const trimmed = value.trim()
|
|
54
|
+
if (!trimmed) {
|
|
55
|
+
return ''
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const country = countryIso2.trim().toUpperCase() as CountryCode
|
|
59
|
+
const digits = trimmed.replace(/\D/g, '')
|
|
60
|
+
|
|
61
|
+
const candidates = new Set<string>([trimmed])
|
|
62
|
+
if (digits) {
|
|
63
|
+
candidates.add(digits)
|
|
64
|
+
candidates.add(`+${digits}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
const parsed = tryParseForCountry(candidate, country)
|
|
69
|
+
if (parsed) {
|
|
70
|
+
return nationalDigitsForMask(parsed)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const callingCode = getCountryCallingCode(country)
|
|
76
|
+
if (digits.startsWith(callingCode) && digits.length > callingCode.length) {
|
|
77
|
+
const withoutCode = digits.slice(callingCode.length)
|
|
78
|
+
const parsed = tryParseForCountry(withoutCode, country) ?? tryParseForCountry(`0${withoutCode}`, country)
|
|
79
|
+
if (parsed) {
|
|
80
|
+
return nationalDigitsForMask(parsed)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
return trimmed
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return trimmed
|
|
88
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getExampleNumber, type CountryCode } from 'libphonenumber-js/mobile/es6'
|
|
2
|
+
import examples from 'libphonenumber-js/mobile/examples'
|
|
3
|
+
|
|
4
|
+
const maskCache = new Map<string, string>()
|
|
5
|
+
|
|
6
|
+
/** IMask pattern with space grouping derived from libphonenumber mobile example numbers. */
|
|
7
|
+
export const getPhoneMaskForCountry = (countryIso2: string): string | undefined => {
|
|
8
|
+
const country = countryIso2.trim().toUpperCase() as CountryCode
|
|
9
|
+
const cached = maskCache.get(country)
|
|
10
|
+
if (cached) {
|
|
11
|
+
return cached
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const example = getExampleNumber(country, examples)
|
|
15
|
+
if (!example) {
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const groups = example
|
|
20
|
+
.formatNational()
|
|
21
|
+
.split(/\D+/)
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.map((part) => part.length)
|
|
24
|
+
|
|
25
|
+
const mask = groups.map((size) => '0'.repeat(size)).join(' ')
|
|
26
|
+
maskCache.set(country, mask)
|
|
27
|
+
return mask
|
|
28
|
+
}
|
|
@@ -7,12 +7,9 @@ type FdsPhonenumberInputPassthroughProps = Pick<
|
|
|
7
7
|
| 'autocomplete'
|
|
8
8
|
| 'required'
|
|
9
9
|
| 'placeholder'
|
|
10
|
-
| 'maxlength'
|
|
11
|
-
| 'minlength'
|
|
12
10
|
| 'name'
|
|
13
11
|
| 'autofocus'
|
|
14
12
|
| 'readonly'
|
|
15
|
-
| 'pattern'
|
|
16
13
|
>
|
|
17
14
|
|
|
18
15
|
export interface FdsPhonenumberProps extends FdsPhonenumberInputPassthroughProps {
|