Skip to content

Commit 6ca9884

Browse files
authored
Merge pull request #240 from gyurielf/fix-strict-country-option
Option to make the selected country permanent and prevent country update from input.
2 parents fbdbbc6 + a3dc07d commit 6ca9884

File tree

11 files changed

+118
-42
lines changed

11 files changed

+118
-42
lines changed

.changeset/stupid-cooks-sneeze.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte-tel-input': minor
3+
---
4+
5+
feat: option to disallow detecting and changing country from e164 number

apps/site/src/lib/views/OptionsPanel.svelte

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,27 @@
7979
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">Spaces</span
8080
>
8181
</label>
82+
<label
83+
for="strictCountry-{markupId}"
84+
class="relative inline-flex items-center align-middle cursor-pointer"
85+
>
86+
<input
87+
id="strictCountry-{markupId}"
88+
type="checkbox"
89+
value={options.strictCountry}
90+
class="sr-only peer"
91+
checked={options.strictCountry}
92+
on:change={() => {
93+
options.strictCountry = !options.strictCountry;
94+
}}
95+
/>
96+
<div
97+
class="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"
98+
/>
99+
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"
100+
>Strict Country</span
101+
>
102+
</label>
82103
<label
83104
for="autoPlaceholder-{markupId}"
84105
class="relative inline-flex items-center align-middle cursor-pointer"

apps/site/src/lib/views/Usage.svelte

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
autoPlaceholder: true,
3030
spaces: true,
3131
invalidateOnCountryChange: true,
32-
format: 'national'
32+
format: 'national',
33+
strictCountry: false
3334
} satisfies TelInputOptions;
3435
3536
/* basicWithE164 example */
@@ -43,7 +44,8 @@
4344
autoPlaceholder: true,
4445
spaces: true,
4546
invalidateOnCountryChange: false,
46-
format: 'national'
47+
format: 'national',
48+
strictCountry: false
4749
} satisfies TelInputOptions;
4850
4951
/* basicWithNull example */
@@ -57,7 +59,8 @@
5759
autoPlaceholder: true,
5860
spaces: true,
5961
invalidateOnCountryChange: false,
60-
format: 'national'
62+
format: 'national',
63+
strictCountry: false
6164
} satisfies TelInputOptions;
6265
6366
/* eventDriven example */
@@ -71,7 +74,8 @@
7174
autoPlaceholder: true,
7275
spaces: true,
7376
invalidateOnCountryChange: false,
74-
format: 'national'
77+
format: 'national',
78+
strictCountry: false
7579
} satisfies TelInputOptions;
7680
</script>
7781

packages/svelte-tel-input/.eslintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ static
2020

2121
# Utility and CI tools
2222
zarf
23-
scripts
23+
scripts
24+
**/reash/*

packages/svelte-tel-input/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ node_modules
1010
.vercel
1111
.output
1212
coverage
13-
.vscode
13+
.vscode
14+
reash/*

packages/svelte-tel-input/.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ yarn.lock
2121
# Utility and CI tools
2222
zarf
2323
scripts
24-
examplePhoneNumbers.ts
24+
examplePhoneNumbers.ts
25+
**/reash/*

packages/svelte-tel-input/src/lib/components/input/TelInput.svelte

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
getCountryForPartialE164Number,
77
generatePlaceholder,
88
telInputAction,
9-
109
allowedCharacters
11-
1210
} from '$lib/utils/index.js';
1311
import type { DetailedValue, CountryCode, E164Number, TelInputOptions } from '$lib/types';
1412
@@ -24,7 +22,8 @@
2422
autoPlaceholder: true,
2523
spaces: true,
2624
invalidateOnCountryChange: false,
27-
format: 'national'
25+
format: 'national',
26+
strictCountry: false
2827
} satisfies TelInputOptions;
2928
3029
export let autocomplete: string | null = null;
@@ -79,6 +78,7 @@
7978
country = countryCode;
8079
prevCountry = country;
8180
dispatch('updateCountry', country);
81+
dispatch('updateDetailedValue', detailedValue);
8282
}
8383
return country;
8484
};
@@ -89,39 +89,44 @@
8989
initialCursorPosition: number
9090
) => {
9191
let fvIndex = 0;
92-
for(let nvIndex = 0; nvIndex < initialCursorPosition; nvIndex++) {
93-
92+
for (let nvIndex = 0; nvIndex < initialCursorPosition; nvIndex++) {
9493
// Since newValue has not been normalized yet, we need to map any non standard digits.
95-
const nvChar = allowedCharacters(newValue[nvIndex], {spaces: false});
94+
const nvChar = allowedCharacters(newValue[nvIndex], { spaces: false });
9695
9796
// For each non-formatting character encountered in the value entered by the user,
9897
// find the corresponding digit in the formatted value.
99-
if(nvChar >= '0' && nvChar <= '9') {
100-
while(!(formattedValue[fvIndex] >= '0' && formattedValue[fvIndex] <= '9') && fvIndex < formattedValue.length) {
98+
if (nvChar >= '0' && nvChar <= '9') {
99+
while (
100+
!(formattedValue[fvIndex] >= '0' && formattedValue[fvIndex] <= '9') &&
101+
fvIndex < formattedValue.length
102+
) {
101103
fvIndex++;
102104
}
103105
fvIndex++;
104106
}
105107
}
106108
107109
return fvIndex;
108-
}
110+
};
109111
110112
const handleParsePhoneNumber = async (
111113
rawInput: string | null,
112114
currCountry: CountryCode | null = null
113115
) => {
114116
const input = rawInput as E164Number;
115117
if (input !== null) {
116-
const numberHasCountry = getCountryForPartialE164Number(input);
118+
const detectedCountry = getCountryForPartialE164Number(input);
119+
const useCountry = options?.strictCountry
120+
? currCountry
121+
: detectedCountry ?? currCountry;
117122
118-
if (numberHasCountry && numberHasCountry !== prevCountry) {
119-
updateCountry(numberHasCountry);
123+
if (!options?.strictCountry && detectedCountry && detectedCountry !== prevCountry) {
124+
updateCountry(detectedCountry);
120125
}
121126
122127
try {
123128
detailedValue = normalizeTelInput(
124-
parsePhoneNumberWithError(input, numberHasCountry ?? currCountry ?? undefined)
129+
parsePhoneNumberWithError(input, useCountry ?? undefined)
125130
);
126131
} catch (err) {
127132
if (err instanceof ParseError) {
@@ -144,8 +149,12 @@
144149
145150
// Need to wait for input element to update before cursor position can be restored
146151
await tick();
147-
if(el) {
148-
const newCursorPosition = findNewCursorPosition(input, inputValue, initialCursorPosition)
152+
if (el) {
153+
const newCursorPosition = findNewCursorPosition(
154+
input,
155+
inputValue,
156+
initialCursorPosition
157+
);
149158
el.selectionStart = newCursorPosition;
150159
el.selectionEnd = newCursorPosition;
151160
}
@@ -154,8 +163,12 @@
154163
155164
// Need to wait for input element to update before cursor position can be restored
156165
await tick();
157-
if(el) {
158-
const newCursorPosition = findNewCursorPosition(input, inputValue, initialCursorPosition)
166+
if (el) {
167+
const newCursorPosition = findNewCursorPosition(
168+
input,
169+
inputValue,
170+
initialCursorPosition
171+
);
159172
el.selectionStart = newCursorPosition;
160173
el.selectionEnd = newCursorPosition;
161174
}
@@ -226,14 +239,19 @@
226239
if (castedValue) {
227240
handleParsePhoneNumber(
228241
castedValue,
229-
getCountryForPartialE164Number(castedValue) || newCountry
242+
options?.strictCountry
243+
? country
244+
: getCountryForPartialE164Number(castedValue) || newCountry
230245
);
231246
}
232247
};
233248
234249
onMount(() => {
235250
if (value) {
236-
handleParsePhoneNumber(value, getCountryForPartialE164Number(value) || country);
251+
handleParsePhoneNumber(
252+
value,
253+
options?.strictCountry ? country : getCountryForPartialE164Number(value) || country
254+
);
237255
}
238256
});
239257
</script>
@@ -265,6 +283,7 @@
265283
use:telInputAction={{
266284
handler: handleInputAction,
267285
spaces: combinedOptions.spaces,
268-
value
286+
value,
287+
strictCountryCode: combinedOptions.strictCountry
269288
}}
270289
/>

packages/svelte-tel-input/src/lib/types/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ export interface TelInputOptions {
7575
* @default false
7676
*/
7777
invalidateOnCountryChange?: boolean;
78+
/**
79+
* Prevent automatic country parsing from the input value. It validates via the passed `country` property value.
80+
* @default false
81+
*/
82+
strictCountry?: boolean;
7883
/**
7984
* "international": `+36 20 123 4567`,
8085
* "default": `20 123 4567`

packages/svelte-tel-input/src/lib/utils/directives/telInputAction.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@ export const telInputAction = (
44
node: HTMLInputElement,
55
{
66
handler,
7-
spaces
7+
spaces,
8+
strictCountryCode
89
}: {
910
handler: (val: string) => void;
1011
spaces: boolean;
1112
value: E164Number | null;
13+
strictCountryCode: boolean;
1214
}
1315
) => {
1416
const onInput = (event: Event) => {
1517
if (node && node.contains(event.target as HTMLInputElement)) {
1618
const currentValue = (event.target as HTMLInputElement).value;
1719
const formattedInput = inputParser(currentValue, {
1820
parseCharacter: inspectAllowedChars,
19-
allowSpaces: spaces
21+
allowSpaces: spaces,
22+
disallowPlusSign: strictCountryCode
2023
});
2124
node.value = formattedInput;
2225
handler(formattedInput);

packages/svelte-tel-input/src/lib/utils/helpers.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,16 @@ export const normalizeTelInput = (input?: PhoneNumber) => {
4848
isPossible: input ? input.isPossible() : false,
4949
phoneNumber: input ? input.number : null,
5050
countryCallingCode: input ? input.countryCallingCode : null,
51-
formattedNumber: input ? (new AsYouType()).input(input.number) : null,
51+
formattedNumber: input ? new AsYouType().input(input.number) : null,
5252
nationalNumber: input ? input.nationalNumber : null,
53-
formatInternational: input ? (new AsYouType()).input(input.number) : null,
53+
formatInternational: input ? new AsYouType().input(input.number) : null,
5454
formatOriginal: input
55-
? (new AsYouType())
55+
? new AsYouType()
5656
.input(input.number)
5757
.slice(input.countryCallingCode.length + 1)
5858
.trim()
5959
: null,
60-
formatNational: input ? (new AsYouType(input.country)).input(input.number) : null,
60+
formatNational: input ? new AsYouType(input.country).input(input.number) : null,
6161
uri: input ? input.getURI() : null,
6262
e164: input ? input.number : null
6363
}).filter(([, value]) => value !== null)
@@ -336,16 +336,23 @@ export const inputParser = (
336336
text: string,
337337
{
338338
allowSpaces,
339-
parseCharacter
339+
parseCharacter,
340+
disallowPlusSign
340341
}: {
341342
allowSpaces: boolean;
342-
parseCharacter: (char: string, val: string, allowSpaces?: boolean) => string | undefined;
343+
disallowPlusSign: boolean;
344+
parseCharacter: (
345+
char: string,
346+
val: string,
347+
allowSpaces: boolean,
348+
disallowPlusSign: boolean
349+
) => string | undefined;
343350
}
344351
) => {
345352
let value = '';
346353

347354
for (let index = 0; index < text.length; index++) {
348-
const character = parseCharacter(text[index], value, allowSpaces);
355+
const character = parseCharacter(text[index], value, allowSpaces, disallowPlusSign);
349356
if (character !== undefined) {
350357
value += character;
351358
}
@@ -354,9 +361,14 @@ export const inputParser = (
354361
return value;
355362
};
356363

357-
export const inspectAllowedChars = (character: string, value: string, allowSpaces?: boolean) => {
364+
export const inspectAllowedChars = (
365+
character: string,
366+
value: string,
367+
allowSpaces: boolean,
368+
disallowPlusSign: boolean
369+
) => {
358370
// Leading plus is allowed
359-
if (character === '+') {
371+
if (!disallowPlusSign && character === '+') {
360372
if (!value) {
361373
return character;
362374
}

0 commit comments

Comments
 (0)