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.
@@ -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 { getCountry, guessCountryByPartialNumber } from '../../utils/countryHelpers.js';
8
- import type { CountryCode, TelInputOptions, Props, ValidationError } from '../../types';
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
- untrack(() => {
46
- const badProp = (prop: string, expected: string, got: unknown): never => {
47
- const gotDesc =
48
- got !== null && typeof got === 'object'
49
- ? Array.isArray(got)
50
- ? 'array'
51
- : `object { ${Object.keys(got as object)
52
- .slice(0, 4)
53
- .join(', ')}${Object.keys(got as object).length > 4 ? ', …' : ''} }`
54
- : typeof got;
55
- throw new TypeError(
56
- `<TelInput> invalid prop "${prop}": expected ${expected}, but received ${gotDesc}.`
57
- );
58
- };
59
-
60
- if (typeof value !== 'string') badProp('value', 'string', value);
61
- if (country !== null && country !== undefined && typeof country !== 'string')
62
- badProp('country', 'CountryCode | null | undefined', country);
63
- if (name !== null && name !== undefined && typeof name !== 'string')
64
- badProp('name', 'string | null', name);
65
- if (placeholder !== null && placeholder !== undefined && typeof placeholder !== 'string')
66
- badProp('placeholder', 'string | null', placeholder);
67
- if (disabled !== undefined && disabled !== null && typeof disabled !== 'boolean')
68
- badProp('disabled', 'boolean', disabled);
69
- if (readonly !== null && readonly !== undefined && typeof readonly !== 'boolean')
70
- badProp('readonly', 'boolean | null', readonly);
71
- if (required !== null && required !== undefined && typeof required !== 'boolean')
72
- badProp('required', 'boolean | null', required);
73
- if (size !== null && size !== undefined && typeof size !== 'number')
74
- badProp('size', 'number | null', size);
75
- if (
76
- options !== undefined &&
77
- (typeof options !== 'object' || options === null || Array.isArray(options))
78
- )
79
- badProp('options', 'TelInputOptions object', options);
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
- ? getCountry({ field: 'iso2', value: effectiveCountryIso2 })
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
- const prefix = `+${details.countryCallingCode} `;
138
- if (details.formattedNumber.startsWith(prefix)) {
139
- const national = details.formattedNumber.slice(prefix.length);
140
- return spaces ? national : national.replace(/\s/g, '');
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 ? getCountry({ field: 'iso2', value: iso2 }) : undefined;
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 = parsePhoneInput(rawInput, normalizerCountry);
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
- const _prefix = `+${detailedValue.countryCallingCode} `;
388
- if (detailedValue.formattedNumber.startsWith(_prefix)) {
389
- const _national = detailedValue.formattedNumber.slice(_prefix.length);
390
- inputValue = combinedOptions.spaces ? _national : _national.replace(/\s/g, '');
391
- } else {
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
- const prefix = `+${detailedValue.countryCallingCode} `;
441
- if (detailedValue.formattedNumber.startsWith(prefix)) {
442
- const national = detailedValue.formattedNumber.slice(prefix.length);
443
- inputValue = spaces ? national : national.replace(/\s/g, '');
444
- return;
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';
@@ -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 { getCountry } from '../countryHelpers.js';
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 = normalizeUserInput(userInput);
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
- params.handler(formattedInput);
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
  };
@@ -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 isSelected: <T extends {
8
- id: string;
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`.
@@ -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 { getCountry } from './countryHelpers.js';
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
- const international = examplePhoneNumber.formatInternational().trim();
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
- if (phone && phone.nationalNumber !== capped) {
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.slice(countryCallingCode.length + 1).trim()
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.slice(country.dialCode.length + 1).trim());
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.slice(countryCallingCode.length + 1).trim()
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 ? getCountry({ field: 'iso2', value: country }) : undefined;
186
+ const countryObj = country ? getCountryByIso2(country) : undefined;
269
187
  return parsePhoneInput(raw, countryObj);
270
188
  };
271
189
  /**
@@ -1 +1,2 @@
1
1
  export { generatePlaceholder, parse, normalizeToE164, pickCountries } from './helpers.js';
2
+ export { getCountryByIso2 } from './countryHelpers.js';
@@ -1 +1,2 @@
1
1
  export { generatePlaceholder, parse, normalizeToE164, pickCountries } from './helpers.js';
2
+ export { getCountryByIso2 } from './countryHelpers.js';
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.2.0",
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.12.42"
33
+ "libphonenumber-js": "1.13.7"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@sveltejs/adapter-auto": "7.0.1",
37
- "@sveltejs/kit": "^2.57.1",
38
- "@sveltejs/package": "^2.5.7",
39
- "@sveltejs/vite-plugin-svelte": "^6.2.4",
40
- "@tailwindcss/vite": "^4.2.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.3.1",
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.0.2",
47
+ "jsdom": "^29.1.1",
48
48
  "micromatch": "^4.0.8",
49
- "postcss": "^8.5.10",
50
- "publint": "^0.3.18",
51
- "svelte": "^5.55.4",
52
- "svelte-check": "^4.4.6",
53
- "svelte2tsx": "^0.7.53",
54
- "tailwindcss": "^4.2.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": "^5.9.3",
57
- "valibot": "^1.3.1",
58
- "vite": "^7.3.2",
59
- "vitest": "^4.1.5",
60
- "zod": "^4.3.6"
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",