svelte-tel-input 4.3.0 → 4.3.1
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/input/TelInput.svelte +43 -29
- package/dist/types/index.d.ts +10 -0
- package/dist/utils/directives/telInputAction.d.ts +2 -2
- package/dist/utils/directives/telInputAction.js +5 -18
- package/dist/utils/helpers.d.ts +2 -0
- package/dist/utils/helpers.js +23 -11
- package/package.json +12 -12
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
import { onMount, untrack } from 'svelte';
|
|
3
3
|
import { ParseError } from 'libphonenumber-js/max/es6';
|
|
4
4
|
import { generatePlaceholder } from '../../utils/index.js';
|
|
5
|
-
import { parsePhoneInput } from '../../utils/helpers.js';
|
|
5
|
+
import { parsePhoneInput, toNationalFormat } from '../../utils/helpers.js';
|
|
6
6
|
import { telInputAction } from '../../utils/directives/telInputAction.js';
|
|
7
7
|
import { getCountryByIso2, guessCountryByPartialNumber } from '../../utils/countryHelpers.js';
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
CountryCode,
|
|
10
|
+
TelInputOptions,
|
|
11
|
+
Props,
|
|
12
|
+
ValidationError,
|
|
13
|
+
PreParsed
|
|
14
|
+
} from '../../types';
|
|
9
15
|
|
|
10
16
|
const defaultOptions = {
|
|
11
17
|
autoPlaceholder: true,
|
|
@@ -145,13 +151,14 @@
|
|
|
145
151
|
if (
|
|
146
152
|
initialFormat === 'national' &&
|
|
147
153
|
details.formattedNumber &&
|
|
148
|
-
details.countryCallingCode
|
|
154
|
+
details.countryCallingCode &&
|
|
155
|
+
details.formattedNumber.startsWith(`+${details.countryCallingCode} `)
|
|
149
156
|
) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
return toNationalFormat(
|
|
158
|
+
details.formattedNumber,
|
|
159
|
+
details.countryCallingCode,
|
|
160
|
+
spaces
|
|
161
|
+
);
|
|
155
162
|
}
|
|
156
163
|
return spaces ? (details.formattedNumber ?? value) : value;
|
|
157
164
|
} catch {
|
|
@@ -185,11 +192,11 @@
|
|
|
185
192
|
...options
|
|
186
193
|
});
|
|
187
194
|
|
|
188
|
-
const handleInputAction = (value: string) => {
|
|
195
|
+
const handleInputAction = (value: string, preParsed?: PreParsed) => {
|
|
189
196
|
if (disabled || readonly) return;
|
|
190
197
|
// First user keystroke exits the "initial national format" display mode.
|
|
191
198
|
_showNationalFormat = false;
|
|
192
|
-
handleParsePhoneNumber(value, country, combinedOptions.validateOn !== 'blur');
|
|
199
|
+
handleParsePhoneNumber(value, country, combinedOptions.validateOn !== 'blur', preParsed);
|
|
193
200
|
};
|
|
194
201
|
|
|
195
202
|
const getValidationError = (
|
|
@@ -267,7 +274,8 @@
|
|
|
267
274
|
const handleParsePhoneNumber = (
|
|
268
275
|
rawInput: string | null,
|
|
269
276
|
currCountry: CountryCode | null = null,
|
|
270
|
-
shouldValidate = true
|
|
277
|
+
shouldValidate = true,
|
|
278
|
+
preParsed?: PreParsed
|
|
271
279
|
) => {
|
|
272
280
|
// Country-only change: reset state unless option says to keep valid.
|
|
273
281
|
if (rawInput === null && currCountry !== null) {
|
|
@@ -354,8 +362,14 @@
|
|
|
354
362
|
? numberHasCountry
|
|
355
363
|
: selectedCountry;
|
|
356
364
|
|
|
365
|
+
// Reuse the action's parse when it used the same country we resolved to;
|
|
366
|
+
// only the dial-code country-switch path needs a re-parse.
|
|
367
|
+
const canReuse =
|
|
368
|
+
preParsed !== undefined && (normalizerCountry?.iso2 ?? null) === preParsed.countryIso2;
|
|
357
369
|
try {
|
|
358
|
-
detailedValue =
|
|
370
|
+
detailedValue = canReuse
|
|
371
|
+
? preParsed.detail
|
|
372
|
+
: parsePhoneInput(rawInput, normalizerCountry);
|
|
359
373
|
} catch (err) {
|
|
360
374
|
if (err instanceof ParseError) {
|
|
361
375
|
detailedValue = {
|
|
@@ -394,17 +408,14 @@
|
|
|
394
408
|
if (
|
|
395
409
|
_showNationalFormat &&
|
|
396
410
|
detailedValue?.formattedNumber &&
|
|
397
|
-
detailedValue?.countryCallingCode
|
|
411
|
+
detailedValue?.countryCallingCode &&
|
|
412
|
+
detailedValue.formattedNumber.startsWith(`+${detailedValue.countryCallingCode} `)
|
|
398
413
|
) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
inputValue = combinedOptions.spaces
|
|
405
|
-
? (detailedValue.formattedNumber ?? rawInput)
|
|
406
|
-
: rawInput;
|
|
407
|
-
}
|
|
414
|
+
inputValue = toNationalFormat(
|
|
415
|
+
detailedValue.formattedNumber,
|
|
416
|
+
detailedValue.countryCallingCode,
|
|
417
|
+
combinedOptions.spaces
|
|
418
|
+
);
|
|
408
419
|
} else {
|
|
409
420
|
inputValue = combinedOptions.spaces
|
|
410
421
|
? (detailedValue?.formattedNumber ?? rawInput)
|
|
@@ -447,14 +458,17 @@
|
|
|
447
458
|
if (
|
|
448
459
|
_showNationalFormat &&
|
|
449
460
|
detailedValue.formattedNumber &&
|
|
450
|
-
detailedValue.countryCallingCode
|
|
461
|
+
detailedValue.countryCallingCode &&
|
|
462
|
+
detailedValue.formattedNumber.startsWith(
|
|
463
|
+
`+${detailedValue.countryCallingCode} `
|
|
464
|
+
)
|
|
451
465
|
) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
466
|
+
inputValue = toNationalFormat(
|
|
467
|
+
detailedValue.formattedNumber,
|
|
468
|
+
detailedValue.countryCallingCode,
|
|
469
|
+
spaces
|
|
470
|
+
);
|
|
471
|
+
return;
|
|
458
472
|
}
|
|
459
473
|
inputValue = spaces
|
|
460
474
|
? (detailedValue.formattedNumber ?? inputValue)
|
package/dist/types/index.d.ts
CHANGED
|
@@ -41,6 +41,16 @@ export interface DetailedValue {
|
|
|
41
41
|
validationError: ValidationError;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* A parse result carried from the input action to the component, alongside the
|
|
46
|
+
* country it was parsed with, so the component can reuse it instead of parsing
|
|
47
|
+
* the same input a second time.
|
|
48
|
+
*/
|
|
49
|
+
export interface PreParsed {
|
|
50
|
+
detail: DetailedValue;
|
|
51
|
+
countryIso2: CountryCode | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
/**
|
|
45
55
|
* The reason the current phone number input is invalid.
|
|
46
56
|
* - `'REQUIRED'` — field is empty and `required` is `true`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { CountryCode } from '../../types/index.js';
|
|
1
|
+
import type { CountryCode, PreParsed } from '../../types/index.js';
|
|
2
2
|
interface TelInputActionParams {
|
|
3
|
-
handler: (val: string) => void;
|
|
3
|
+
handler: (val: string, preParsed?: PreParsed) => void;
|
|
4
4
|
spaces: boolean;
|
|
5
5
|
country: CountryCode | null | undefined;
|
|
6
6
|
value: string;
|
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
import { getCountryByIso2 } from '../countryHelpers.js';
|
|
2
|
-
import { parsePhoneInput } from '../helpers.js';
|
|
2
|
+
import { parsePhoneInput, normalizeForLibphonenumber } from '../helpers.js';
|
|
3
3
|
import { calculateCursorPosition } from '../cursorPosition.js';
|
|
4
4
|
let inputState = null;
|
|
5
|
-
const normalizeUserInput = (input) => {
|
|
6
|
-
let value = '';
|
|
7
|
-
for (let i = 0; i < input.length; i++) {
|
|
8
|
-
const ch = input[i];
|
|
9
|
-
if (ch >= '0' && ch <= '9') {
|
|
10
|
-
value += ch;
|
|
11
|
-
continue;
|
|
12
|
-
}
|
|
13
|
-
if (ch === '+' && value.length === 0) {
|
|
14
|
-
value += ch;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return value;
|
|
18
|
-
};
|
|
19
5
|
const isFormattingChar = (ch) => {
|
|
20
6
|
if (!ch)
|
|
21
7
|
return false;
|
|
@@ -108,7 +94,7 @@ const onInput = (event, params, node) => {
|
|
|
108
94
|
};
|
|
109
95
|
const userInput = node.value;
|
|
110
96
|
const currentCursor = node.selectionStart ?? 0;
|
|
111
|
-
const normalized =
|
|
97
|
+
const normalized = normalizeForLibphonenumber(userInput);
|
|
112
98
|
const countryObj = params.country ? getCountryByIso2(params.country) : undefined;
|
|
113
99
|
const details = parsePhoneInput(normalized, countryObj);
|
|
114
100
|
// Display formatting:
|
|
@@ -131,8 +117,9 @@ const onInput = (event, params, node) => {
|
|
|
131
117
|
// Update value and cursor
|
|
132
118
|
node.value = formattedInput;
|
|
133
119
|
node.setSelectionRange(newPosition, newPosition);
|
|
134
|
-
// Notify parent component
|
|
135
|
-
|
|
120
|
+
// Notify parent component, passing the already-computed parse so the
|
|
121
|
+
// component can reuse it instead of re-parsing the same input.
|
|
122
|
+
params.handler(formattedInput, { detail: details, countryIso2: params.country ?? null });
|
|
136
123
|
// Clear state
|
|
137
124
|
inputState = null;
|
|
138
125
|
};
|
package/dist/utils/helpers.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare const generatePlaceholder: (country: CountryCode, { spaces, forma
|
|
|
4
4
|
spaces: boolean;
|
|
5
5
|
format?: "international" | "national";
|
|
6
6
|
}) => string;
|
|
7
|
+
export declare const normalizeForLibphonenumber: (input: string) => string;
|
|
8
|
+
export declare const toNationalFormat: (internationalFormat: string, countryCallingCode: string, spaces: boolean) => string;
|
|
7
9
|
export declare const parsePhoneInput: (input: string, country: Country | undefined) => DetailedValue;
|
|
8
10
|
/**
|
|
9
11
|
* Parse a raw phone number string into a `DetailedValue`.
|
package/dist/utils/helpers.js
CHANGED
|
@@ -8,12 +8,7 @@ export const generatePlaceholder = (country, { spaces, format } = {
|
|
|
8
8
|
const examplePhoneNumber = getExampleNumber(country, examplePhoneNumbers);
|
|
9
9
|
if (examplePhoneNumber) {
|
|
10
10
|
if (format === 'national') {
|
|
11
|
-
|
|
12
|
-
const prefix = `+${examplePhoneNumber.countryCallingCode} `;
|
|
13
|
-
const national = international.startsWith(prefix)
|
|
14
|
-
? international.slice(prefix.length)
|
|
15
|
-
: international;
|
|
16
|
-
return spaces ? national : national.replace(/\s/g, '');
|
|
11
|
+
return toNationalFormat(examplePhoneNumber.formatInternational().trim(), examplePhoneNumber.countryCallingCode, spaces);
|
|
17
12
|
}
|
|
18
13
|
return spaces ? examplePhoneNumber.formatInternational().trim() : examplePhoneNumber.number;
|
|
19
14
|
}
|
|
@@ -25,7 +20,7 @@ export const generatePlaceholder = (country, { spaces, format } = {
|
|
|
25
20
|
// ---------------------------------------------------------------------------
|
|
26
21
|
// Phone-number normalizer
|
|
27
22
|
// ---------------------------------------------------------------------------
|
|
28
|
-
const normalizeForLibphonenumber = (input) => {
|
|
23
|
+
export const normalizeForLibphonenumber = (input) => {
|
|
29
24
|
let value = '';
|
|
30
25
|
for (let i = 0; i < input.length; i++) {
|
|
31
26
|
const ch = input[i];
|
|
@@ -82,6 +77,14 @@ const stripSpecialChars = (s) => s
|
|
|
82
77
|
.replace(/[()-]/g, ' ')
|
|
83
78
|
.replace(/\s{2,}/g, ' ')
|
|
84
79
|
.trim();
|
|
80
|
+
const stripCallingCode = (internationalFormat, countryCallingCode) => internationalFormat.slice(countryCallingCode.length + 1).trim();
|
|
81
|
+
export const toNationalFormat = (internationalFormat, countryCallingCode, spaces) => {
|
|
82
|
+
const prefix = `+${countryCallingCode} `;
|
|
83
|
+
const national = internationalFormat.startsWith(prefix)
|
|
84
|
+
? internationalFormat.slice(prefix.length)
|
|
85
|
+
: internationalFormat;
|
|
86
|
+
return spaces ? national : national.replace(/\s/g, '');
|
|
87
|
+
};
|
|
85
88
|
export const parsePhoneInput = (input, country) => {
|
|
86
89
|
const normalized = normalizeForLibphonenumber(input);
|
|
87
90
|
const defaultCountryIso2 = country?.iso2;
|
|
@@ -109,13 +112,22 @@ export const parsePhoneInput = (input, country) => {
|
|
|
109
112
|
else if (defaultCountryIso2) {
|
|
110
113
|
const formatted = formatIncompletePhoneNumber(capped, defaultCountryIso2);
|
|
111
114
|
if (formatted === capped && country?.dialCode) {
|
|
112
|
-
|
|
115
|
+
// Calling-code leak: libphonenumber consumed a leading digit that
|
|
116
|
+
// equals the country calling code as the code itself, so
|
|
117
|
+
// nationalNumber is shorter than what the user typed (e.g. US "11"
|
|
118
|
+
// → nationalNumber "1"). Those digits are national input, not a
|
|
119
|
+
// trunk prefix — fall through to the synthesis branch below so the
|
|
120
|
+
// typed digit is preserved instead of being sliced off.
|
|
121
|
+
const isCallingCodeLeak = !!countryCallingCode &&
|
|
122
|
+
capped.startsWith(countryCallingCode) &&
|
|
123
|
+
phone?.nationalNumber === capped.slice(countryCallingCode.length);
|
|
124
|
+
if (phone && phone.nationalNumber !== capped && !isCallingCodeLeak) {
|
|
113
125
|
// Trunk-prefixed complete number (e.g. GB "07947…" → nationalNumber
|
|
114
126
|
// "7947…"). Synthesis would be invalid; use the actual international
|
|
115
127
|
// format which correctly omits the trunk prefix.
|
|
116
128
|
formattedNumber =
|
|
117
129
|
formatInternational && countryCallingCode
|
|
118
|
-
? formatInternational
|
|
130
|
+
? stripCallingCode(formatInternational, countryCallingCode)
|
|
119
131
|
: stripSpecialChars(capped);
|
|
120
132
|
}
|
|
121
133
|
else {
|
|
@@ -123,7 +135,7 @@ export const parsePhoneInput = (input, country) => {
|
|
|
123
135
|
// Synthesize an international string to get consistent spacing from
|
|
124
136
|
// the first digit, then strip "+{dialCode} ".
|
|
125
137
|
const intl = formatIncompletePhoneNumber(`+${country.dialCode}${capped}`);
|
|
126
|
-
formattedNumber = stripSpecialChars(intl
|
|
138
|
+
formattedNumber = stripSpecialChars(stripCallingCode(intl, country.dialCode));
|
|
127
139
|
}
|
|
128
140
|
}
|
|
129
141
|
else {
|
|
@@ -151,7 +163,7 @@ export const parsePhoneInput = (input, country) => {
|
|
|
151
163
|
formatInternational,
|
|
152
164
|
formatNational,
|
|
153
165
|
formatOriginal: formatInternational && countryCallingCode
|
|
154
|
-
? formatInternational
|
|
166
|
+
? stripCallingCode(formatInternational, countryCallingCode)
|
|
155
167
|
: null,
|
|
156
168
|
formattedNumber,
|
|
157
169
|
isPossible: asYouType.isPossible(),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-tel-input",
|
|
3
3
|
"description": "svelte-tel-input",
|
|
4
|
-
"version": "4.3.
|
|
4
|
+
"version": "4.3.1",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/gyurielf/svelte-tel-input.git"
|
|
@@ -30,33 +30,33 @@
|
|
|
30
30
|
"svelte": "^5.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"libphonenumber-js": "1.13.
|
|
33
|
+
"libphonenumber-js": "1.13.7"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@sveltejs/adapter-auto": "7.0.1",
|
|
37
|
-
"@sveltejs/kit": "^2.
|
|
37
|
+
"@sveltejs/kit": "^2.67.0",
|
|
38
38
|
"@sveltejs/package": "^2.5.8",
|
|
39
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
39
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
40
40
|
"@tailwindcss/vite": "^4.3.1",
|
|
41
41
|
"@testing-library/jest-dom": "6.9.1",
|
|
42
|
-
"@testing-library/svelte": "^5.
|
|
42
|
+
"@testing-library/svelte": "^5.4.2",
|
|
43
43
|
"@testing-library/user-event": "^14.6.1",
|
|
44
44
|
"@types/micromatch": "^4.0.10",
|
|
45
45
|
"@types/node": "^22.17.0",
|
|
46
46
|
"dotenv": "^17.4.2",
|
|
47
47
|
"jsdom": "^29.1.1",
|
|
48
48
|
"micromatch": "^4.0.8",
|
|
49
|
-
"postcss": "^8.5.
|
|
49
|
+
"postcss": "^8.5.15",
|
|
50
50
|
"publint": "^0.3.21",
|
|
51
|
-
"svelte": "^5.
|
|
52
|
-
"svelte-check": "^4.
|
|
53
|
-
"svelte2tsx": "^0.7.
|
|
51
|
+
"svelte": "^5.56.4",
|
|
52
|
+
"svelte-check": "^4.7.1",
|
|
53
|
+
"svelte2tsx": "^0.7.57",
|
|
54
54
|
"tailwindcss": "^4.3.1",
|
|
55
55
|
"tslib": "^2.8.1",
|
|
56
|
-
"typescript": "^
|
|
56
|
+
"typescript": "^6.0.3",
|
|
57
57
|
"valibot": "^1.4.1",
|
|
58
|
-
"vite": "^
|
|
59
|
-
"vitest": "^4.1.
|
|
58
|
+
"vite": "^8.1.0",
|
|
59
|
+
"vitest": "^4.1.9",
|
|
60
60
|
"zod": "^4.4.3"
|
|
61
61
|
},
|
|
62
62
|
"type": "module",
|