svelte-tel-input 4.2.0 → 4.3.0

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,10 +1,10 @@
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
5
  import { parsePhoneInput } from '../../utils/helpers.js';
6
6
  import { telInputAction } from '../../utils/directives/telInputAction.js';
7
- import { getCountry, guessCountryByPartialNumber } from '../../utils/countryHelpers.js';
7
+ import { getCountryByIso2, guessCountryByPartialNumber } from '../../utils/countryHelpers.js';
8
8
  import type { CountryCode, TelInputOptions, Props, ValidationError } from '../../types';
9
9
 
10
10
  const defaultOptions = {
@@ -38,46 +38,59 @@
38
38
  validationError = $bindable<Readonly<ValidationError>>(null),
39
39
  options = defaultOptions,
40
40
  el = $bindable(undefined),
41
+ validateProps = false,
41
42
  'aria-invalid': ariaInvalidProp = undefined,
42
43
  ...rest
43
44
  }: Props = $props();
44
45
 
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
- });
46
+ // Runtime prop type-checking, opt-in via `validateProps`. Off by default so the
47
+ // checks add nothing to production bundles; enable it (e.g. in development) to
48
+ // get descriptive errors on malformed props.
49
+ // Intentionally reads the initial value only — this is a one-time init guard.
50
+ // svelte-ignore state_referenced_locally
51
+ if (validateProps)
52
+ untrack(() => {
53
+ const badProp = (prop: string, expected: string, got: unknown): never => {
54
+ const gotDesc =
55
+ got !== null && typeof got === 'object'
56
+ ? Array.isArray(got)
57
+ ? 'array'
58
+ : `object { ${Object.keys(got as object)
59
+ .slice(0, 4)
60
+ .join(
61
+ ', '
62
+ )}${Object.keys(got as object).length > 4 ? ', …' : ''} }`
63
+ : typeof got;
64
+ throw new TypeError(
65
+ `<TelInput> invalid prop "${prop}": expected ${expected}, but received ${gotDesc}.`
66
+ );
67
+ };
68
+
69
+ if (typeof value !== 'string') badProp('value', 'string', value);
70
+ if (country !== null && country !== undefined && typeof country !== 'string')
71
+ badProp('country', 'CountryCode | null | undefined', country);
72
+ if (name !== null && name !== undefined && typeof name !== 'string')
73
+ badProp('name', 'string | null', name);
74
+ if (
75
+ placeholder !== null &&
76
+ placeholder !== undefined &&
77
+ typeof placeholder !== 'string'
78
+ )
79
+ badProp('placeholder', 'string | null', placeholder);
80
+ if (disabled !== undefined && disabled !== null && typeof disabled !== 'boolean')
81
+ badProp('disabled', 'boolean', disabled);
82
+ if (readonly !== null && readonly !== undefined && typeof readonly !== 'boolean')
83
+ badProp('readonly', 'boolean | null', readonly);
84
+ if (required !== null && required !== undefined && typeof required !== 'boolean')
85
+ badProp('required', 'boolean | null', required);
86
+ if (size !== null && size !== undefined && typeof size !== 'number')
87
+ badProp('size', 'number | null', size);
88
+ if (
89
+ options !== undefined &&
90
+ (typeof options !== 'object' || options === null || Array.isArray(options))
91
+ )
92
+ badProp('options', 'TelInputOptions object', options);
93
+ });
81
94
 
82
95
  // Fix: initialize to null so server and client start with an identical render.
83
96
  // The ID is generated only in onMount (client-only), avoiding UUID hydration mismatches.
@@ -124,7 +137,7 @@
124
137
  if (fullDialCodeMatch) effectiveCountryIso2 = detected?.iso2 ?? null;
125
138
  }
126
139
  const countryObj = effectiveCountryIso2
127
- ? getCountry({ field: 'iso2', value: effectiveCountryIso2 })
140
+ ? getCountryByIso2(effectiveCountryIso2)
128
141
  : undefined;
129
142
  try {
130
143
  const details = parsePhoneInput(value, countryObj);
@@ -160,7 +173,6 @@
160
173
  //Initialized to the incoming prop values so the first render never false-fires.
161
174
  let _lastWrittenValue: string = value;
162
175
  let _lastWrittenCountry: CountryCode | null | undefined = untrack(() => country);
163
- // let isInitialized = $state(false);
164
176
 
165
177
  // When true, `handleParsePhoneNumber` and the spaces-effect will display the
166
178
  // national (dial-code-stripped) format. Starts as `true` when
@@ -250,7 +262,7 @@
250
262
  };
251
263
 
252
264
  const getCountryObj = (iso2: CountryCode | null | undefined) =>
253
- iso2 ? getCountry({ field: 'iso2', value: iso2 }) : undefined;
265
+ iso2 ? getCountryByIso2(iso2) : undefined;
254
266
 
255
267
  const handleParsePhoneNumber = (
256
268
  rawInput: string | null,
@@ -495,7 +507,6 @@
495
507
  // so national format is applied automatically when initialFormat='national'. Later maybe this could be optional.
496
508
  handleParsePhoneNumber(value, currentCountry);
497
509
  }
498
- // isInitialized = true;
499
510
  onLoad?.();
500
511
  });
501
512
 
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';
@@ -138,6 +138,13 @@ export interface Props extends HTMLInputAttributes {
138
138
  options?: TelInputOptions;
139
139
  /** Binding to the underlying `<input>` element */
140
140
  el?: HTMLInputElement | undefined;
141
+ /**
142
+ * Run runtime type-checking on incoming props, throwing a descriptive
143
+ * `TypeError` on a mismatch. Off by default so it adds nothing to production
144
+ * bundles; enable it (e.g. in development) to catch malformed props early.
145
+ * @default false
146
+ */
147
+ validateProps?: boolean;
141
148
  /**
142
149
  * Callback fired when the country changes (auto-detected or user-selected).
143
150
  * @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,4 +1,4 @@
1
- import { getCountry } from '../countryHelpers.js';
1
+ import { getCountryByIso2 } from '../countryHelpers.js';
2
2
  import { parsePhoneInput } from '../helpers.js';
3
3
  import { calculateCursorPosition } from '../cursorPosition.js';
4
4
  let inputState = null;
@@ -109,9 +109,7 @@ const onInput = (event, params, node) => {
109
109
  const userInput = node.value;
110
110
  const currentCursor = node.selectionStart ?? 0;
111
111
  const normalized = normalizeUserInput(userInput);
112
- const countryObj = params.country
113
- ? getCountry({ field: 'iso2', value: params.country })
114
- : undefined;
112
+ const countryObj = params.country ? getCountryByIso2(params.country) : undefined;
115
113
  const details = parsePhoneInput(normalized, countryObj);
116
114
  // Display formatting:
117
115
  // - When `spaces` is true, show formatted-as-you-type output (national or intl)
@@ -123,7 +121,6 @@ const onInput = (event, params, node) => {
123
121
  const newPosition = calculateCursorPosition({
124
122
  beforeValue: state.beforeValue,
125
123
  beforeCursor: state.beforeCursor,
126
- beforeSelection: state.beforeSelection,
127
124
  afterInputValue: userInput,
128
125
  afterInputCursor: currentCursor,
129
126
  afterValue: formattedInput,
@@ -4,22 +4,6 @@ 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;
23
7
  export declare const parsePhoneInput: (input: string, country: Country | undefined) => DetailedValue;
24
8
  /**
25
9
  * 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'
@@ -24,98 +22,6 @@ export const generatePlaceholder = (country, { spaces, format } = {
24
22
  return '';
25
23
  }
26
24
  };
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
25
  // ---------------------------------------------------------------------------
120
26
  // Phone-number normalizer
121
27
  // ---------------------------------------------------------------------------
@@ -265,7 +171,7 @@ export const parsePhoneInput = (input, country) => {
265
171
  * default country when the number has no leading `+`.
266
172
  */
267
173
  export const parse = (raw, country) => {
268
- const countryObj = country ? getCountry({ field: 'iso2', value: country }) : undefined;
174
+ const countryObj = country ? getCountryByIso2(country) : undefined;
269
175
  return parsePhoneInput(raw, countryObj);
270
176
  };
271
177
  /**
@@ -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.0",
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.6"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@sveltejs/adapter-auto": "7.0.1",
37
37
  "@sveltejs/kit": "^2.57.1",
38
- "@sveltejs/package": "^2.5.7",
38
+ "@sveltejs/package": "^2.5.8",
39
39
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
40
- "@tailwindcss/vite": "^4.2.4",
40
+ "@tailwindcss/vite": "^4.3.1",
41
41
  "@testing-library/jest-dom": "6.9.1",
42
42
  "@testing-library/svelte": "^5.3.1",
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
49
  "postcss": "^8.5.10",
50
- "publint": "^0.3.18",
50
+ "publint": "^0.3.21",
51
51
  "svelte": "^5.55.4",
52
- "svelte-check": "^4.4.6",
53
- "svelte2tsx": "^0.7.53",
54
- "tailwindcss": "^4.2.4",
52
+ "svelte-check": "^4.6.0",
53
+ "svelte2tsx": "^0.7.56",
54
+ "tailwindcss": "^4.3.1",
55
55
  "tslib": "^2.8.1",
56
56
  "typescript": "^5.9.3",
57
- "valibot": "^1.3.1",
57
+ "valibot": "^1.4.1",
58
58
  "vite": "^7.3.2",
59
59
  "vitest": "^4.1.5",
60
- "zod": "^4.3.6"
60
+ "zod": "^4.4.3"
61
61
  },
62
62
  "type": "module",
63
63
  "license": "MIT",