svelte-tel-input 4.2.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 +96 -71
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types/index.d.ts +17 -0
- package/dist/utils/countryHelpers.d.ts +6 -0
- package/dist/utils/countryHelpers.js +14 -0
- package/dist/utils/cursorPosition.d.ts +0 -13
- package/dist/utils/cursorPosition.js +0 -28
- package/dist/utils/directives/telInputAction.d.ts +2 -2
- package/dist/utils/directives/telInputAction.js +7 -23
- package/dist/utils/helpers.d.ts +2 -16
- package/dist/utils/helpers.js +26 -108
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +19 -19
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount, untrack } from 'svelte';
|
|
3
|
-
import { ParseError } from 'libphonenumber-js/max';
|
|
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
|
-
import {
|
|
8
|
-
import type {
|
|
7
|
+
import { getCountryByIso2, guessCountryByPartialNumber } from '../../utils/countryHelpers.js';
|
|
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,
|
|
@@ -38,46 +44,59 @@
|
|
|
38
44
|
validationError = $bindable<Readonly<ValidationError>>(null),
|
|
39
45
|
options = defaultOptions,
|
|
40
46
|
el = $bindable(undefined),
|
|
47
|
+
validateProps = false,
|
|
41
48
|
'aria-invalid': ariaInvalidProp = undefined,
|
|
42
49
|
...rest
|
|
43
50
|
}: Props = $props();
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
badProp('
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
52
|
+
// Runtime prop type-checking, opt-in via `validateProps`. Off by default so the
|
|
53
|
+
// checks add nothing to production bundles; enable it (e.g. in development) to
|
|
54
|
+
// get descriptive errors on malformed props.
|
|
55
|
+
// Intentionally reads the initial value only — this is a one-time init guard.
|
|
56
|
+
// svelte-ignore state_referenced_locally
|
|
57
|
+
if (validateProps)
|
|
58
|
+
untrack(() => {
|
|
59
|
+
const badProp = (prop: string, expected: string, got: unknown): never => {
|
|
60
|
+
const gotDesc =
|
|
61
|
+
got !== null && typeof got === 'object'
|
|
62
|
+
? Array.isArray(got)
|
|
63
|
+
? 'array'
|
|
64
|
+
: `object { ${Object.keys(got as object)
|
|
65
|
+
.slice(0, 4)
|
|
66
|
+
.join(
|
|
67
|
+
', '
|
|
68
|
+
)}${Object.keys(got as object).length > 4 ? ', …' : ''} }`
|
|
69
|
+
: typeof got;
|
|
70
|
+
throw new TypeError(
|
|
71
|
+
`<TelInput> invalid prop "${prop}": expected ${expected}, but received ${gotDesc}.`
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (typeof value !== 'string') badProp('value', 'string', value);
|
|
76
|
+
if (country !== null && country !== undefined && typeof country !== 'string')
|
|
77
|
+
badProp('country', 'CountryCode | null | undefined', country);
|
|
78
|
+
if (name !== null && name !== undefined && typeof name !== 'string')
|
|
79
|
+
badProp('name', 'string | null', name);
|
|
80
|
+
if (
|
|
81
|
+
placeholder !== null &&
|
|
82
|
+
placeholder !== undefined &&
|
|
83
|
+
typeof placeholder !== 'string'
|
|
84
|
+
)
|
|
85
|
+
badProp('placeholder', 'string | null', placeholder);
|
|
86
|
+
if (disabled !== undefined && disabled !== null && typeof disabled !== 'boolean')
|
|
87
|
+
badProp('disabled', 'boolean', disabled);
|
|
88
|
+
if (readonly !== null && readonly !== undefined && typeof readonly !== 'boolean')
|
|
89
|
+
badProp('readonly', 'boolean | null', readonly);
|
|
90
|
+
if (required !== null && required !== undefined && typeof required !== 'boolean')
|
|
91
|
+
badProp('required', 'boolean | null', required);
|
|
92
|
+
if (size !== null && size !== undefined && typeof size !== 'number')
|
|
93
|
+
badProp('size', 'number | null', size);
|
|
94
|
+
if (
|
|
95
|
+
options !== undefined &&
|
|
96
|
+
(typeof options !== 'object' || options === null || Array.isArray(options))
|
|
97
|
+
)
|
|
98
|
+
badProp('options', 'TelInputOptions object', options);
|
|
99
|
+
});
|
|
81
100
|
|
|
82
101
|
// Fix: initialize to null so server and client start with an identical render.
|
|
83
102
|
// The ID is generated only in onMount (client-only), avoiding UUID hydration mismatches.
|
|
@@ -124,7 +143,7 @@
|
|
|
124
143
|
if (fullDialCodeMatch) effectiveCountryIso2 = detected?.iso2 ?? null;
|
|
125
144
|
}
|
|
126
145
|
const countryObj = effectiveCountryIso2
|
|
127
|
-
?
|
|
146
|
+
? getCountryByIso2(effectiveCountryIso2)
|
|
128
147
|
: undefined;
|
|
129
148
|
try {
|
|
130
149
|
const details = parsePhoneInput(value, countryObj);
|
|
@@ -132,13 +151,14 @@
|
|
|
132
151
|
if (
|
|
133
152
|
initialFormat === 'national' &&
|
|
134
153
|
details.formattedNumber &&
|
|
135
|
-
details.countryCallingCode
|
|
154
|
+
details.countryCallingCode &&
|
|
155
|
+
details.formattedNumber.startsWith(`+${details.countryCallingCode} `)
|
|
136
156
|
) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
157
|
+
return toNationalFormat(
|
|
158
|
+
details.formattedNumber,
|
|
159
|
+
details.countryCallingCode,
|
|
160
|
+
spaces
|
|
161
|
+
);
|
|
142
162
|
}
|
|
143
163
|
return spaces ? (details.formattedNumber ?? value) : value;
|
|
144
164
|
} catch {
|
|
@@ -160,7 +180,6 @@
|
|
|
160
180
|
//Initialized to the incoming prop values so the first render never false-fires.
|
|
161
181
|
let _lastWrittenValue: string = value;
|
|
162
182
|
let _lastWrittenCountry: CountryCode | null | undefined = untrack(() => country);
|
|
163
|
-
// let isInitialized = $state(false);
|
|
164
183
|
|
|
165
184
|
// When true, `handleParsePhoneNumber` and the spaces-effect will display the
|
|
166
185
|
// national (dial-code-stripped) format. Starts as `true` when
|
|
@@ -173,11 +192,11 @@
|
|
|
173
192
|
...options
|
|
174
193
|
});
|
|
175
194
|
|
|
176
|
-
const handleInputAction = (value: string) => {
|
|
195
|
+
const handleInputAction = (value: string, preParsed?: PreParsed) => {
|
|
177
196
|
if (disabled || readonly) return;
|
|
178
197
|
// First user keystroke exits the "initial national format" display mode.
|
|
179
198
|
_showNationalFormat = false;
|
|
180
|
-
handleParsePhoneNumber(value, country, combinedOptions.validateOn !== 'blur');
|
|
199
|
+
handleParsePhoneNumber(value, country, combinedOptions.validateOn !== 'blur', preParsed);
|
|
181
200
|
};
|
|
182
201
|
|
|
183
202
|
const getValidationError = (
|
|
@@ -250,12 +269,13 @@
|
|
|
250
269
|
};
|
|
251
270
|
|
|
252
271
|
const getCountryObj = (iso2: CountryCode | null | undefined) =>
|
|
253
|
-
iso2 ?
|
|
272
|
+
iso2 ? getCountryByIso2(iso2) : undefined;
|
|
254
273
|
|
|
255
274
|
const handleParsePhoneNumber = (
|
|
256
275
|
rawInput: string | null,
|
|
257
276
|
currCountry: CountryCode | null = null,
|
|
258
|
-
shouldValidate = true
|
|
277
|
+
shouldValidate = true,
|
|
278
|
+
preParsed?: PreParsed
|
|
259
279
|
) => {
|
|
260
280
|
// Country-only change: reset state unless option says to keep valid.
|
|
261
281
|
if (rawInput === null && currCountry !== null) {
|
|
@@ -342,8 +362,14 @@
|
|
|
342
362
|
? numberHasCountry
|
|
343
363
|
: selectedCountry;
|
|
344
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;
|
|
345
369
|
try {
|
|
346
|
-
detailedValue =
|
|
370
|
+
detailedValue = canReuse
|
|
371
|
+
? preParsed.detail
|
|
372
|
+
: parsePhoneInput(rawInput, normalizerCountry);
|
|
347
373
|
} catch (err) {
|
|
348
374
|
if (err instanceof ParseError) {
|
|
349
375
|
detailedValue = {
|
|
@@ -382,17 +408,14 @@
|
|
|
382
408
|
if (
|
|
383
409
|
_showNationalFormat &&
|
|
384
410
|
detailedValue?.formattedNumber &&
|
|
385
|
-
detailedValue?.countryCallingCode
|
|
411
|
+
detailedValue?.countryCallingCode &&
|
|
412
|
+
detailedValue.formattedNumber.startsWith(`+${detailedValue.countryCallingCode} `)
|
|
386
413
|
) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
inputValue = combinedOptions.spaces
|
|
393
|
-
? (detailedValue.formattedNumber ?? rawInput)
|
|
394
|
-
: rawInput;
|
|
395
|
-
}
|
|
414
|
+
inputValue = toNationalFormat(
|
|
415
|
+
detailedValue.formattedNumber,
|
|
416
|
+
detailedValue.countryCallingCode,
|
|
417
|
+
combinedOptions.spaces
|
|
418
|
+
);
|
|
396
419
|
} else {
|
|
397
420
|
inputValue = combinedOptions.spaces
|
|
398
421
|
? (detailedValue?.formattedNumber ?? rawInput)
|
|
@@ -435,14 +458,17 @@
|
|
|
435
458
|
if (
|
|
436
459
|
_showNationalFormat &&
|
|
437
460
|
detailedValue.formattedNumber &&
|
|
438
|
-
detailedValue.countryCallingCode
|
|
461
|
+
detailedValue.countryCallingCode &&
|
|
462
|
+
detailedValue.formattedNumber.startsWith(
|
|
463
|
+
`+${detailedValue.countryCallingCode} `
|
|
464
|
+
)
|
|
439
465
|
) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
466
|
+
inputValue = toNationalFormat(
|
|
467
|
+
detailedValue.formattedNumber,
|
|
468
|
+
detailedValue.countryCallingCode,
|
|
469
|
+
spaces
|
|
470
|
+
);
|
|
471
|
+
return;
|
|
446
472
|
}
|
|
447
473
|
inputValue = spaces
|
|
448
474
|
? (detailedValue.formattedNumber ?? inputValue)
|
|
@@ -495,7 +521,6 @@
|
|
|
495
521
|
// so national format is applied automatically when initialFormat='national'. Later maybe this could be optional.
|
|
496
522
|
handleParsePhoneNumber(value, currentCountry);
|
|
497
523
|
}
|
|
498
|
-
// isInitialized = true;
|
|
499
524
|
onLoad?.();
|
|
500
525
|
});
|
|
501
526
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { default as TelInput } from './components/input/TelInput.svelte';
|
|
2
|
-
export { parse, normalizeToE164, pickCountries } from './utils/index.js';
|
|
2
|
+
export { parse, normalizeToE164, pickCountries, getCountryByIso2 } from './utils/index.js';
|
|
3
3
|
export { countries } from './assets/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { default as TelInput } from './components/input/TelInput.svelte';
|
|
2
|
-
export { parse, normalizeToE164, pickCountries } from './utils/index.js';
|
|
2
|
+
export { parse, normalizeToE164, pickCountries, getCountryByIso2 } from './utils/index.js';
|
|
3
3
|
export { countries } from './assets/index.js';
|
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`
|
|
@@ -138,6 +148,13 @@ export interface Props extends HTMLInputAttributes {
|
|
|
138
148
|
options?: TelInputOptions;
|
|
139
149
|
/** Binding to the underlying `<input>` element */
|
|
140
150
|
el?: HTMLInputElement | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* Run runtime type-checking on incoming props, throwing a descriptive
|
|
153
|
+
* `TypeError` on a mismatch. Off by default so it adds nothing to production
|
|
154
|
+
* bundles; enable it (e.g. in development) to catch malformed props early.
|
|
155
|
+
* @default false
|
|
156
|
+
*/
|
|
157
|
+
validateProps?: boolean;
|
|
141
158
|
/**
|
|
142
159
|
* Callback fired when the country changes (auto-detected or user-selected).
|
|
143
160
|
* @param newCountry The new country code or null.
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { CountryCode, Country } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Look up a country by its ISO 3166-1 alpha-2 code (e.g. `'US'`, `'HU'`).
|
|
4
|
+
*
|
|
5
|
+
* O(1) against the bundled country list. Returns `undefined` if not found.
|
|
6
|
+
*/
|
|
7
|
+
export declare const getCountryByIso2: (iso2: CountryCode) => Country | undefined;
|
|
2
8
|
export declare const getCountry: ({ field, value, countries }: {
|
|
3
9
|
/**
|
|
4
10
|
* Field to search by
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { countries as normalizedCountries } from '../assets/index.js';
|
|
2
|
+
// O(1) lookup index for the default country list, keyed by ISO 3166-1 alpha-2.
|
|
3
|
+
// Built once at module load. iso2 lookups run on every keystroke, so they go
|
|
4
|
+
// through this Map instead of a linear scan over ~249 countries.
|
|
5
|
+
const iso2Index = new Map(normalizedCountries.map((c) => [c.iso2, c]));
|
|
6
|
+
/**
|
|
7
|
+
* Look up a country by its ISO 3166-1 alpha-2 code (e.g. `'US'`, `'HU'`).
|
|
8
|
+
*
|
|
9
|
+
* O(1) against the bundled country list. Returns `undefined` if not found.
|
|
10
|
+
*/
|
|
11
|
+
export const getCountryByIso2 = (iso2) => iso2Index.get(iso2);
|
|
2
12
|
export const getCountry = ({ field, value, countries = normalizedCountries }) => {
|
|
3
13
|
if (['priority'].includes(field)) {
|
|
4
14
|
throw new Error(`Field "${field}" is not supported`);
|
|
5
15
|
}
|
|
16
|
+
// Fast path: iso2 lookups against the default list hit the prebuilt index.
|
|
17
|
+
if (field === 'iso2' && countries === normalizedCountries) {
|
|
18
|
+
return iso2Index.get(value);
|
|
19
|
+
}
|
|
6
20
|
return countries.find((currentCountry) => {
|
|
7
21
|
return value === currentCountry[field];
|
|
8
22
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
interface CalculateCursorPositionProps {
|
|
2
2
|
beforeValue: string;
|
|
3
3
|
beforeCursor: number;
|
|
4
|
-
beforeSelection: number;
|
|
5
4
|
afterInputValue?: string;
|
|
6
5
|
afterInputCursor?: number;
|
|
7
6
|
afterValue: string;
|
|
@@ -18,16 +17,4 @@ export declare const isDigit: (char?: string) => boolean;
|
|
|
18
17
|
* Calculate the correct cursor position after input formatting
|
|
19
18
|
*/
|
|
20
19
|
export declare const calculateCursorPosition: ({ beforeValue, beforeCursor, afterInputValue, afterInputCursor, afterValue, currentCursor, isDeletion, deletionDirection, hasSelection }: CalculateCursorPositionProps) => number;
|
|
21
|
-
interface GetCursorPositionProps {
|
|
22
|
-
phoneBeforeInput: string;
|
|
23
|
-
phoneAfterInput: string;
|
|
24
|
-
phoneAfterFormatted: string;
|
|
25
|
-
cursorPositionAfterInput: number;
|
|
26
|
-
leftOffset?: number;
|
|
27
|
-
deletion?: 'forward' | 'backward' | undefined;
|
|
28
|
-
isReplacement?: boolean;
|
|
29
|
-
}
|
|
30
|
-
export declare const isNumeric: (char?: string) => boolean;
|
|
31
|
-
export declare const getCursorPosition: (props: GetCursorPositionProps) => number;
|
|
32
|
-
export declare const setCursorPosition: (node: HTMLInputElement, cursorPosition: number) => void;
|
|
33
20
|
export {};
|
|
@@ -104,31 +104,3 @@ export const calculateCursorPosition = ({ beforeValue, beforeCursor, afterInputV
|
|
|
104
104
|
const newPos = findNthRelevantPosition(afterValue, digitsBefore, true);
|
|
105
105
|
return Math.min(skipFormattingChars(afterValue, newPos), afterValue.length);
|
|
106
106
|
};
|
|
107
|
-
export const isNumeric = isDigit;
|
|
108
|
-
export const getCursorPosition = (props) => {
|
|
109
|
-
return calculateCursorPosition({
|
|
110
|
-
beforeValue: props.phoneBeforeInput,
|
|
111
|
-
beforeCursor: props.cursorPositionAfterInput,
|
|
112
|
-
beforeSelection: 0,
|
|
113
|
-
afterInputValue: props.phoneAfterInput,
|
|
114
|
-
afterInputCursor: props.cursorPositionAfterInput,
|
|
115
|
-
afterValue: props.phoneAfterFormatted,
|
|
116
|
-
isDeletion: !!props.deletion,
|
|
117
|
-
deletionDirection: props.deletion || null,
|
|
118
|
-
hasSelection: false
|
|
119
|
-
});
|
|
120
|
-
};
|
|
121
|
-
export const setCursorPosition = (node, cursorPosition) => {
|
|
122
|
-
/**
|
|
123
|
-
* HACK: should set cursor on the next tick to make sure that the phone value is updated
|
|
124
|
-
* useTimeout with 0ms provides issues when two keys are pressed same time
|
|
125
|
-
*/
|
|
126
|
-
Promise.resolve().then(() => {
|
|
127
|
-
// workaround for safari autofocus bug:
|
|
128
|
-
// Check if the input is focused before setting the cursor, otherwise safari sometimes autofocuses on setSelectionRange
|
|
129
|
-
if (typeof window === 'undefined' || node !== document?.activeElement) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
node?.setSelectionRange(cursorPosition, cursorPosition);
|
|
133
|
-
});
|
|
134
|
-
};
|
|
@@ -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
|
-
import {
|
|
2
|
-
import { parsePhoneInput } from '../helpers.js';
|
|
1
|
+
import { getCountryByIso2 } from '../countryHelpers.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,10 +94,8 @@ const onInput = (event, params, node) => {
|
|
|
108
94
|
};
|
|
109
95
|
const userInput = node.value;
|
|
110
96
|
const currentCursor = node.selectionStart ?? 0;
|
|
111
|
-
const normalized =
|
|
112
|
-
const countryObj = params.country
|
|
113
|
-
? getCountry({ field: 'iso2', value: params.country })
|
|
114
|
-
: undefined;
|
|
97
|
+
const normalized = normalizeForLibphonenumber(userInput);
|
|
98
|
+
const countryObj = params.country ? getCountryByIso2(params.country) : undefined;
|
|
115
99
|
const details = parsePhoneInput(normalized, countryObj);
|
|
116
100
|
// Display formatting:
|
|
117
101
|
// - When `spaces` is true, show formatted-as-you-type output (national or intl)
|
|
@@ -123,7 +107,6 @@ const onInput = (event, params, node) => {
|
|
|
123
107
|
const newPosition = calculateCursorPosition({
|
|
124
108
|
beforeValue: state.beforeValue,
|
|
125
109
|
beforeCursor: state.beforeCursor,
|
|
126
|
-
beforeSelection: state.beforeSelection,
|
|
127
110
|
afterInputValue: userInput,
|
|
128
111
|
afterInputCursor: currentCursor,
|
|
129
112
|
afterValue: formattedInput,
|
|
@@ -134,8 +117,9 @@ const onInput = (event, params, node) => {
|
|
|
134
117
|
// Update value and cursor
|
|
135
118
|
node.value = formattedInput;
|
|
136
119
|
node.setSelectionRange(newPosition, newPosition);
|
|
137
|
-
// Notify parent component
|
|
138
|
-
|
|
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 });
|
|
139
123
|
// Clear state
|
|
140
124
|
inputState = null;
|
|
141
125
|
};
|
package/dist/utils/helpers.d.ts
CHANGED
|
@@ -4,22 +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
|
|
8
|
-
|
|
9
|
-
}>(itemToSelect: T | string, selectedItem: (T | undefined | null) | string) => boolean;
|
|
10
|
-
/**
|
|
11
|
-
* These mappings map a character (key) to a specific digit that should
|
|
12
|
-
* replace it for normalization purposes.
|
|
13
|
-
* @param {string} character
|
|
14
|
-
* @returns {string}
|
|
15
|
-
*/
|
|
16
|
-
export declare const allowedCharacters: (character: string, { spaces, plusSign }?: {
|
|
17
|
-
spaces?: boolean;
|
|
18
|
-
plusSign?: boolean;
|
|
19
|
-
}) => string | undefined;
|
|
20
|
-
export declare const inputParser: (input: string, { allowSpaces }: {
|
|
21
|
-
allowSpaces: boolean;
|
|
22
|
-
}) => string;
|
|
7
|
+
export declare const normalizeForLibphonenumber: (input: string) => string;
|
|
8
|
+
export declare const toNationalFormat: (internationalFormat: string, countryCallingCode: string, spaces: boolean) => string;
|
|
23
9
|
export declare const parsePhoneInput: (input: string, country: Country | undefined) => DetailedValue;
|
|
24
10
|
/**
|
|
25
11
|
* Parse a raw phone number string into a `DetailedValue`.
|
package/dist/utils/helpers.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { AsYouType, getExampleNumber, formatIncompletePhoneNumber, validatePhoneNumberLength } from 'libphonenumber-js/max';
|
|
1
|
+
import { AsYouType, getExampleNumber, formatIncompletePhoneNumber, validatePhoneNumberLength } from 'libphonenumber-js/max/es6';
|
|
2
2
|
import { examplePhoneNumbers, countries } from '../assets/index.js';
|
|
3
|
-
import {
|
|
4
|
-
const whiteSpaceRegex = new RegExp('[\\t\\n\\v\\f\\r \\u00a0\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u200b\\u2028\\u2029\\u3000]', 'g');
|
|
5
|
-
const plusSignRegex = new RegExp('\\+', 'g');
|
|
3
|
+
import { getCountryByIso2 } from './countryHelpers.js';
|
|
6
4
|
export const generatePlaceholder = (country, { spaces, format } = {
|
|
7
5
|
spaces: true,
|
|
8
6
|
format: 'international'
|
|
@@ -10,12 +8,7 @@ export const generatePlaceholder = (country, { spaces, format } = {
|
|
|
10
8
|
const examplePhoneNumber = getExampleNumber(country, examplePhoneNumbers);
|
|
11
9
|
if (examplePhoneNumber) {
|
|
12
10
|
if (format === 'national') {
|
|
13
|
-
|
|
14
|
-
const prefix = `+${examplePhoneNumber.countryCallingCode} `;
|
|
15
|
-
const national = international.startsWith(prefix)
|
|
16
|
-
? international.slice(prefix.length)
|
|
17
|
-
: international;
|
|
18
|
-
return spaces ? national : national.replace(/\s/g, '');
|
|
11
|
+
return toNationalFormat(examplePhoneNumber.formatInternational().trim(), examplePhoneNumber.countryCallingCode, spaces);
|
|
19
12
|
}
|
|
20
13
|
return spaces ? examplePhoneNumber.formatInternational().trim() : examplePhoneNumber.number;
|
|
21
14
|
}
|
|
@@ -24,102 +17,10 @@ export const generatePlaceholder = (country, { spaces, format } = {
|
|
|
24
17
|
return '';
|
|
25
18
|
}
|
|
26
19
|
};
|
|
27
|
-
export const isSelected = (itemToSelect, selectedItem) => {
|
|
28
|
-
if (!selectedItem) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
if (typeof selectedItem === 'object' && typeof itemToSelect === 'object') {
|
|
32
|
-
return selectedItem.id === itemToSelect.id;
|
|
33
|
-
}
|
|
34
|
-
return itemToSelect === selectedItem;
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* These mappings map a character (key) to a specific digit that should
|
|
38
|
-
* replace it for normalization purposes.
|
|
39
|
-
* @param {string} character
|
|
40
|
-
* @returns {string}
|
|
41
|
-
*/
|
|
42
|
-
export const allowedCharacters = (character, { spaces, plusSign } = {
|
|
43
|
-
spaces: true,
|
|
44
|
-
plusSign: true
|
|
45
|
-
}) => {
|
|
46
|
-
const DIGITS = {
|
|
47
|
-
'0': '0',
|
|
48
|
-
'1': '1',
|
|
49
|
-
'2': '2',
|
|
50
|
-
'3': '3',
|
|
51
|
-
'4': '4',
|
|
52
|
-
'5': '5',
|
|
53
|
-
'6': '6',
|
|
54
|
-
'7': '7',
|
|
55
|
-
'8': '8',
|
|
56
|
-
'9': '9',
|
|
57
|
-
'\uFF10': '0', // Fullwidth digit 0
|
|
58
|
-
'\uFF11': '1', // Fullwidth digit 1
|
|
59
|
-
'\uFF12': '2', // Fullwidth digit 2
|
|
60
|
-
'\uFF13': '3', // Fullwidth digit 3
|
|
61
|
-
'\uFF14': '4', // Fullwidth digit 4
|
|
62
|
-
'\uFF15': '5', // Fullwidth digit 5
|
|
63
|
-
'\uFF16': '6', // Fullwidth digit 6
|
|
64
|
-
'\uFF17': '7', // Fullwidth digit 7
|
|
65
|
-
'\uFF18': '8', // Fullwidth digit 8
|
|
66
|
-
'\uFF19': '9', // Fullwidth digit 9
|
|
67
|
-
'\u0660': '0', // Arabic-indic digit 0
|
|
68
|
-
'\u0661': '1', // Arabic-indic digit 1
|
|
69
|
-
'\u0662': '2', // Arabic-indic digit 2
|
|
70
|
-
'\u0663': '3', // Arabic-indic digit 3
|
|
71
|
-
'\u0664': '4', // Arabic-indic digit 4
|
|
72
|
-
'\u0665': '5', // Arabic-indic digit 5
|
|
73
|
-
'\u0666': '6', // Arabic-indic digit 6
|
|
74
|
-
'\u0667': '7', // Arabic-indic digit 7
|
|
75
|
-
'\u0668': '8', // Arabic-indic digit 8
|
|
76
|
-
'\u0669': '9', // Arabic-indic digit 9
|
|
77
|
-
'\u06F0': '0', // Eastern-Arabic digit 0
|
|
78
|
-
'\u06F1': '1', // Eastern-Arabic digit 1
|
|
79
|
-
'\u06F2': '2', // Eastern-Arabic digit 2
|
|
80
|
-
'\u06F3': '3', // Eastern-Arabic digit 3
|
|
81
|
-
'\u06F4': '4', // Eastern-Arabic digit 4
|
|
82
|
-
'\u06F5': '5', // Eastern-Arabic digit 5
|
|
83
|
-
'\u06F6': '6', // Eastern-Arabic digit 6
|
|
84
|
-
'\u06F7': '7', // Eastern-Arabic digit 7
|
|
85
|
-
'\u06F8': '8', // Eastern-Arabic digit 8
|
|
86
|
-
'\u06F9': '9' // Eastern-Arabic digit 9,
|
|
87
|
-
};
|
|
88
|
-
// Type Guard for index signature.
|
|
89
|
-
const isValidKey = (key) => {
|
|
90
|
-
return key in DIGITS;
|
|
91
|
-
};
|
|
92
|
-
// Allow spaces && plus sign character
|
|
93
|
-
if ((spaces && whiteSpaceRegex.test(character)) ||
|
|
94
|
-
(plusSign && plusSignRegex.test(character))) {
|
|
95
|
-
return character;
|
|
96
|
-
}
|
|
97
|
-
if (isValidKey(character))
|
|
98
|
-
return DIGITS[character];
|
|
99
|
-
};
|
|
100
|
-
export const inputParser = (input, { allowSpaces }) => {
|
|
101
|
-
let value = '';
|
|
102
|
-
for (let index = 0; index < input.length; index++) {
|
|
103
|
-
if (input[index] === '+' && !value) {
|
|
104
|
-
value += input[index];
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
const character = allowedCharacters(input[index], { spaces: allowSpaces });
|
|
108
|
-
if (character !== undefined) {
|
|
109
|
-
value += character;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// We force the + sign as a prefix for the number.
|
|
114
|
-
if (value.length > 0 && !value.startsWith('+')) {
|
|
115
|
-
value = '+' + value;
|
|
116
|
-
}
|
|
117
|
-
return value;
|
|
118
|
-
};
|
|
119
20
|
// ---------------------------------------------------------------------------
|
|
120
21
|
// Phone-number normalizer
|
|
121
22
|
// ---------------------------------------------------------------------------
|
|
122
|
-
const normalizeForLibphonenumber = (input) => {
|
|
23
|
+
export const normalizeForLibphonenumber = (input) => {
|
|
123
24
|
let value = '';
|
|
124
25
|
for (let i = 0; i < input.length; i++) {
|
|
125
26
|
const ch = input[i];
|
|
@@ -176,6 +77,14 @@ const stripSpecialChars = (s) => s
|
|
|
176
77
|
.replace(/[()-]/g, ' ')
|
|
177
78
|
.replace(/\s{2,}/g, ' ')
|
|
178
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
|
+
};
|
|
179
88
|
export const parsePhoneInput = (input, country) => {
|
|
180
89
|
const normalized = normalizeForLibphonenumber(input);
|
|
181
90
|
const defaultCountryIso2 = country?.iso2;
|
|
@@ -203,13 +112,22 @@ export const parsePhoneInput = (input, country) => {
|
|
|
203
112
|
else if (defaultCountryIso2) {
|
|
204
113
|
const formatted = formatIncompletePhoneNumber(capped, defaultCountryIso2);
|
|
205
114
|
if (formatted === capped && country?.dialCode) {
|
|
206
|
-
|
|
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) {
|
|
207
125
|
// Trunk-prefixed complete number (e.g. GB "07947…" → nationalNumber
|
|
208
126
|
// "7947…"). Synthesis would be invalid; use the actual international
|
|
209
127
|
// format which correctly omits the trunk prefix.
|
|
210
128
|
formattedNumber =
|
|
211
129
|
formatInternational && countryCallingCode
|
|
212
|
-
? formatInternational
|
|
130
|
+
? stripCallingCode(formatInternational, countryCallingCode)
|
|
213
131
|
: stripSpecialChars(capped);
|
|
214
132
|
}
|
|
215
133
|
else {
|
|
@@ -217,7 +135,7 @@ export const parsePhoneInput = (input, country) => {
|
|
|
217
135
|
// Synthesize an international string to get consistent spacing from
|
|
218
136
|
// the first digit, then strip "+{dialCode} ".
|
|
219
137
|
const intl = formatIncompletePhoneNumber(`+${country.dialCode}${capped}`);
|
|
220
|
-
formattedNumber = stripSpecialChars(intl
|
|
138
|
+
formattedNumber = stripSpecialChars(stripCallingCode(intl, country.dialCode));
|
|
221
139
|
}
|
|
222
140
|
}
|
|
223
141
|
else {
|
|
@@ -245,7 +163,7 @@ export const parsePhoneInput = (input, country) => {
|
|
|
245
163
|
formatInternational,
|
|
246
164
|
formatNational,
|
|
247
165
|
formatOriginal: formatInternational && countryCallingCode
|
|
248
|
-
? formatInternational
|
|
166
|
+
? stripCallingCode(formatInternational, countryCallingCode)
|
|
249
167
|
: null,
|
|
250
168
|
formattedNumber,
|
|
251
169
|
isPossible: asYouType.isPossible(),
|
|
@@ -265,7 +183,7 @@ export const parsePhoneInput = (input, country) => {
|
|
|
265
183
|
* default country when the number has no leading `+`.
|
|
266
184
|
*/
|
|
267
185
|
export const parse = (raw, country) => {
|
|
268
|
-
const countryObj = country ?
|
|
186
|
+
const countryObj = country ? getCountryByIso2(country) : undefined;
|
|
269
187
|
return parsePhoneInput(raw, countryObj);
|
|
270
188
|
};
|
|
271
189
|
/**
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
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.
|
|
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,34 +30,34 @@
|
|
|
30
30
|
"svelte": "^5.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"libphonenumber-js": "1.
|
|
33
|
+
"libphonenumber-js": "1.13.7"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@sveltejs/adapter-auto": "7.0.1",
|
|
37
|
-
"@sveltejs/kit": "^2.
|
|
38
|
-
"@sveltejs/package": "^2.5.
|
|
39
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
40
|
-
"@tailwindcss/vite": "^4.
|
|
37
|
+
"@sveltejs/kit": "^2.67.0",
|
|
38
|
+
"@sveltejs/package": "^2.5.8",
|
|
39
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
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
|
-
"jsdom": "^29.
|
|
47
|
+
"jsdom": "^29.1.1",
|
|
48
48
|
"micromatch": "^4.0.8",
|
|
49
|
-
"postcss": "^8.5.
|
|
50
|
-
"publint": "^0.3.
|
|
51
|
-
"svelte": "^5.
|
|
52
|
-
"svelte-check": "^4.
|
|
53
|
-
"svelte2tsx": "^0.7.
|
|
54
|
-
"tailwindcss": "^4.
|
|
49
|
+
"postcss": "^8.5.15",
|
|
50
|
+
"publint": "^0.3.21",
|
|
51
|
+
"svelte": "^5.56.4",
|
|
52
|
+
"svelte-check": "^4.7.1",
|
|
53
|
+
"svelte2tsx": "^0.7.57",
|
|
54
|
+
"tailwindcss": "^4.3.1",
|
|
55
55
|
"tslib": "^2.8.1",
|
|
56
|
-
"typescript": "^
|
|
57
|
-
"valibot": "^1.
|
|
58
|
-
"vite": "^
|
|
59
|
-
"vitest": "^4.1.
|
|
60
|
-
"zod": "^4.3
|
|
56
|
+
"typescript": "^6.0.3",
|
|
57
|
+
"valibot": "^1.4.1",
|
|
58
|
+
"vite": "^8.1.0",
|
|
59
|
+
"vitest": "^4.1.9",
|
|
60
|
+
"zod": "^4.4.3"
|
|
61
61
|
},
|
|
62
62
|
"type": "module",
|
|
63
63
|
"license": "MIT",
|