svelte-tel-input 0.14.2 → 1.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # svelte-tel-input
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - feat: prevent typing non-tel input characters into the input field. ([#108](https://github.com/gyurielf/svelte-tel-input/pull/108))
8
+
9
+ - fix: now you can use value prop as a single entry ([#108](https://github.com/gyurielf/svelte-tel-input/pull/108))
10
+
11
+ - feat: support monorepos ([#108](https://github.com/gyurielf/svelte-tel-input/pull/108))
12
+
13
+ - feat: sanitize pasted E164 number ([#108](https://github.com/gyurielf/svelte-tel-input/pull/108))
14
+
15
+ - feat: switch country automatically if the pasted/entered phone number contains a valid country calling code ([#108](https://github.com/gyurielf/svelte-tel-input/pull/108))
16
+
17
+ - fix: clear input on manual country change ([#108](https://github.com/gyurielf/svelte-tel-input/pull/108))
18
+
19
+ ### Minor Changes
20
+
21
+ - chore: tweak types ([#108](https://github.com/gyurielf/svelte-tel-input/pull/108))
22
+
3
23
  ## 0.14.2
4
24
 
5
25
  ### Patch Changes
package/README.md CHANGED
@@ -6,7 +6,14 @@
6
6
 
7
7
  > Lightweight svelte tel/phone input standardizer.
8
8
 
9
- The package is in `BETA` stage, expect bugs.
9
+ The package is recently bumped to `1.0`. If you experience any problems, please open an issue, to be able to me to fix it.
10
+
11
+ ## Goals
12
+
13
+ - Solve the problem that a users can enter the same phone number in different formats.
14
+ - Storing a phone number in a standard format, that can be indexable and searchable in any database.
15
+ - Should be accessible for the the browser. Eg. for a `<a href="tel+36201234567 />`.
16
+ - The stored phone number format can be useable for any SMS gateway(e.g for 2FA) and if somebody can call the number from anywhere, it should work.
10
17
 
11
18
  ## Installation
12
19
 
@@ -16,6 +23,14 @@ Svelte Tel Input is distributed via [npm](https://www.npmjs.com/package/svelte-t
16
23
  npm install --save svelte-tel-input
17
24
  ```
18
25
 
26
+ ## Features
27
+
28
+ - Parse and validate phone number.You can store one exact format (`E164`), no matter how users type their phone numbers.
29
+ - Format (specified to its country), to make it more readable.
30
+ - Optionally it can set the user's current country automatically, via IP lookup.
31
+ - Prevent non-digits typing into the input, except the `+` sign (and `space` optionally).
32
+ - Handle copy-pasted phone numbers, it's sanitize non-digit characters except the `+` sign (and `space` optionally).
33
+
19
34
  ## Usage
20
35
 
21
36
  ### Basic
@@ -25,30 +40,42 @@ npm install --save svelte-tel-input
25
40
  ```html
26
41
  <script lang="ts">
27
42
  import TelInput from 'svelte-tel-input';
28
- import type { NormalizedTelNumber, CountryCode } from 'svelte-tel-input/types';
43
+ import type { NormalizedTelNumber, CountryCode, E164Number } from 'svelte-tel-input/types';
29
44
 
30
45
  // Any Country Code Alpha-2 (ISO 3166)
31
- let country: CountryCode = 'US';
46
+ let country: CountryCode | null = 'HU';
32
47
 
48
+ // You must use E164 number format. It's guarantee the parsing and storing consistency.
49
+ let value: E164Number | null = '+36301234567';
50
+
51
+ // Optional - Extended information about the parsed phone number
33
52
  let parsedTelInput: NormalizedTelNumber | null = null;
34
53
  </script>
35
54
 
36
- <TelInput {country} bind:parsedTelInput class="any class passed down" />
55
+ <TelInput bind:country bind:parsedTelInput bind:value class="any class passed down" />
37
56
  ```
38
57
 
39
58
  <p align="right">(<a href="#readme-top">back to top</a>)</p>
40
59
 
41
- ## Features
60
+ ## API
42
61
 
43
- - Parse and validate phone number.
44
- - Standardize parsed phone numbers. You can store one exact format, no matter how users type their phone numbers.
45
- - Mask typed inputs (country specificly), to make it more readable.
46
- - Automatically set the user's current country using an IP lookup.
62
+ The default export of the library is the main TelInput component. It has the following props:
63
+
64
+ | Props | Type | Default Value | Usage |
65
+ | -------------- | --------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
66
+ | country | `CountryCode \| null` | `null` | It's accept any Country Code Alpha-2 (ISO 3166). You can set manually (e.g: by the user via a select). The parser will inspect the entered phone number and if it detect a valid country calling code, then it's automatically set the country to according to the detected country calling code. E.g: `+36` -> `HU` |
67
+ | disabled | `boolean` | `false` | It's block the parser and prevent entering input. You must handle its styling on your own. |
68
+ | valid | `boolean` | `true` | Indicates whether the entered tel number validity. |
69
+ | value | `E164Number \| null` | `null` | [E164](https://en.wikipedia.org/wiki/E.164) is the international format of phone.numbers. This is the main entry point to store and/or load an existent phone number. |
70
+ | parsedTelInput | `NormalizedTelInput \|null` | `null` | All of the formatted results of the tel input. |
71
+ | class | `string` | `` | You can pass down any classname to the component |
47
72
 
48
73
  <p align="right">(<a href="#readme-top">back to top</a>)</p>
49
74
 
50
75
  ## Dependencies
51
76
 
77
+ [svelte](https://svelte.dev/)
78
+
52
79
  [libphonenumber-js](https://gitlab.com/catamphetamine/libphonenumber-js)
53
80
 
54
81
  <p align="right">(<a href="#readme-top">back to top</a>)</p>
@@ -70,8 +97,6 @@ npm install --save svelte-tel-input
70
97
  - [x] Add basics docs and examples
71
98
  - [ ] Add advanced examples
72
99
  - [ ] Improve A11Y
73
- - [ ] Default country sould be optional. ( ip | browserLang |off )
74
- - [ ] Simlify code and types
75
100
 
76
101
  See the [open issues](https://github.com/gyurielf/svelte-tel-input/issues) for a list of proposed features (and known issues).
77
102
 
@@ -270,6 +270,7 @@ const allCountries = [
270
270
  ['Tokelau', 'tk', '690'],
271
271
  ['Tonga', 'to', '676'],
272
272
  ['Trinidad and Tobago', 'tt', '1', 22, ['868']],
273
+ ['Tristan da Cunha', 'ta', '290'],
273
274
  ['Tunisia (‫تونس‬‎)', 'tn', '216'],
274
275
  ['Turkey (Türkiye)', 'tr', '90'],
275
276
  ['Turkmenistan', 'tm', '993'],
@@ -4,31 +4,14 @@ import { clickOutsideAction } from '../../utils/directives/clickOutsideAction';
4
4
  import TelInput from './TelInput.svelte';
5
5
  import { isSelected } from '../../utils/helpers';
6
6
  export let searchText = '';
7
- export let selected = {
8
- id: 'HU',
9
- label: 'Hungary (Magyarország) +36',
10
- name: 'Hungary (Magyarország)',
11
- iso2: 'HU',
12
- dialCode: '36',
13
- priority: 0,
14
- areaCodes: null
15
- };
7
+ let selected;
16
8
  export let clickOutside = true;
17
9
  export let closeOnClick = true;
18
10
  export let disabled = false;
19
- export let parsedTelInput = {
20
- countryCode: 'HU',
21
- isValid: true,
22
- phoneNumber: '+36301234567',
23
- countryCallingCode: '36',
24
- formattedNumber: '+36 30 123 4567',
25
- nationalNumber: '301234567',
26
- formatInternational: '+36 30 123 4567',
27
- formatOriginal: '30 123 4567',
28
- formatNational: '06 30 123 4567',
29
- uri: 'tel:+36301234567',
30
- e164: '+36301234567'
31
- };
11
+ export let parsedTelInput = null;
12
+ export let value;
13
+ $: selectedCountryDialCode =
14
+ normalizedCountries.find((el) => el.iso2 === selected)?.dialCode || null;
32
15
  let isOpen = false;
33
16
  $: isValid = parsedTelInput?.isValid ?? false;
34
17
  const toggleDropDown = (e) => {
@@ -59,18 +42,7 @@ const handleSelect = (val, e) => {
59
42
  if (disabled)
60
43
  return;
61
44
  e?.preventDefault();
62
- if (typeof selected === 'object' && typeof val === 'object' && selected?.id && val?.id) {
63
- if (typeof selected === 'object' && typeof val === 'object' && selected.id !== val.id) {
64
- selected = val;
65
- onChange(val);
66
- selectClick();
67
- }
68
- else {
69
- dispatch('same', { option: val });
70
- selectClick();
71
- }
72
- }
73
- else if (((selected === undefined || selected === null) && typeof val === 'object') ||
45
+ if (((selected === undefined || selected === null) && typeof val === 'object') ||
74
46
  (typeof selected === typeof val && selected !== val)) {
75
47
  selected = val;
76
48
  onChange(val);
@@ -102,8 +74,8 @@ const onChange = (selectedCountry) => {
102
74
  >
103
75
  {#if selected && selected !== null}
104
76
  <div class="inline-flex items-center text-left">
105
- <span class="flag flag-{selected.iso2.toLowerCase()} flex-shrink-0 mr-3" />
106
- <span class=" text-gray-500">+{selected.dialCode}</span>
77
+ <span class="flag flag-{selected.toLowerCase()} flex-shrink-0 mr-3" />
78
+ <span class=" text-gray-500">+{selectedCountryDialCode}</span>
107
79
  </div>
108
80
  {:else}
109
81
  Please select
@@ -147,7 +119,7 @@ const onChange = (selectedCountry) => {
147
119
  />
148
120
  {/if}
149
121
  {#each filteredItems as country (country.id)}
150
- {@const isActive = isSelected(country, selected)}
122
+ {@const isActive = isSelected(country.iso2, selected)}
151
123
  <li role="option" aria-selected={isActive}>
152
124
  <button
153
125
  type="button"
@@ -157,7 +129,7 @@ const onChange = (selectedCountry) => {
157
129
  ? 'bg-gray-600 dark:text-white'
158
130
  : 'dark:hover:text-white dark:text-gray-400'}"
159
131
  on:click={(e) => {
160
- handleSelect(country, e);
132
+ handleSelect(country.iso2, e);
161
133
  }}
162
134
  >
163
135
  <div class="inline-flex items-center text-left">
@@ -176,8 +148,9 @@ const onChange = (selectedCountry) => {
176
148
 
177
149
  <TelInput
178
150
  id="tel-input"
179
- country={selected?.iso2}
151
+ bind:country={selected}
180
152
  bind:parsedTelInput
153
+ bind:value
181
154
  class="border border-gray-300 border-l-gray-100 dark:border-l-gray-700 dark:border-gray-600 {isValid
182
155
  ? `bg-gray-50 dark:bg-gray-700
183
156
  dark:placeholder-gray-400 dark:text-white text-gray-900`
@@ -1,13 +1,13 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
- import type { NormalizedTelNumber, Country } from '../../types';
2
+ import type { NormalizedTelNumber, E164Number } from '../../types';
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  searchText?: string | undefined;
6
- selected?: Country | null | undefined;
7
6
  clickOutside?: boolean | undefined;
8
7
  closeOnClick?: boolean | undefined;
9
8
  disabled?: boolean | undefined;
10
9
  parsedTelInput?: NormalizedTelNumber | null | undefined;
10
+ value: E164Number | null;
11
11
  };
12
12
  events: {
13
13
  change: CustomEvent<any>;
@@ -16,9 +16,9 @@ declare const __propDef: {
16
16
  };
17
17
  slots: {};
18
18
  };
19
- export declare type AdvancedTelInputProps = typeof __propDef.props;
20
- export declare type AdvancedTelInputEvents = typeof __propDef.events;
21
- export declare type AdvancedTelInputSlots = typeof __propDef.slots;
19
+ export type AdvancedTelInputProps = typeof __propDef.props;
20
+ export type AdvancedTelInputEvents = typeof __propDef.events;
21
+ export type AdvancedTelInputSlots = typeof __propDef.slots;
22
22
  export default class AdvancedTelInput extends SvelteComponentTyped<AdvancedTelInputProps, AdvancedTelInputEvents, AdvancedTelInputSlots> {
23
23
  }
24
24
  export {};
@@ -1,42 +1,96 @@
1
- <script>import { watcher } from '../../stores';
2
- import { normalizeTelInput } from '../../utils/helpers';
1
+ <script>import { createEventDispatcher, onMount } from 'svelte';
2
+ import metadata from 'libphonenumber-js/metadata.min.json';
3
3
  import { parsePhoneNumberWithError, ParseError } from 'libphonenumber-js';
4
- import { onMount } from 'svelte';
5
- export let country = null;
6
- export let rawTelInput = null;
7
- export let parsedTelInput = null;
4
+ import { telInputAction } from '../../utils/directives/telInputAction';
5
+ import { normalizeTelInput, getCountryForPartialE164Number } from '../../utils/helpers';
6
+ import { watcher } from '../../stores';
7
+ const dispatch = createEventDispatcher();
8
+ export let country;
9
+ export let value;
10
+ export let parsedTelInput;
11
+ export let valid = true;
8
12
  export let disabled = false;
9
- onMount(() => {
10
- if (parsedTelInput !== null) {
11
- rawTelInput = parsedTelInput.nationalNumber;
12
- handleParsePhoneNumber(country, parsedTelInput.phoneNumber);
13
+ let inputValue = value;
14
+ let prevCountry = country;
15
+ const handleInputAction = (value) => {
16
+ handleParsePhoneNumber(value, country);
17
+ };
18
+ const updateCountry = (countryCode) => {
19
+ country = countryCode;
20
+ prevCountry = countryCode;
21
+ return country;
22
+ };
23
+ const handleParsePhoneNumber = (input, currCountry = null) => {
24
+ if (input) {
25
+ const numberHasCountry = getCountryForPartialE164Number(input, { metadata });
26
+ if (numberHasCountry && numberHasCountry !== prevCountry) {
27
+ updateCountry(numberHasCountry);
28
+ }
29
+ try {
30
+ parsedTelInput = normalizeTelInput(parsePhoneNumberWithError(input, currCountry ?? numberHasCountry));
31
+ }
32
+ catch (err) {
33
+ if (err instanceof ParseError) {
34
+ // Not a phone number, non-existent country, etc.
35
+ parsedTelInput = {
36
+ isValid: false,
37
+ error: err.message
38
+ };
39
+ dispatch('parseError', err.message);
40
+ }
41
+ else {
42
+ throw err;
43
+ }
44
+ }
45
+ // It's keep the html input value on the first parsed format, or the user's format.
46
+ if (parsedTelInput?.isValid && parsedTelInput?.formatOriginal) {
47
+ // It's need for refreshing html input value, if it is the same as the previouly parsed.
48
+ if (inputValue === parsedTelInput?.formatOriginal) {
49
+ inputValue = null;
50
+ }
51
+ inputValue = parsedTelInput?.formatOriginal;
52
+ }
53
+ value = parsedTelInput?.e164 ?? null;
54
+ valid = parsedTelInput?.isValid ?? false;
55
+ dispatch('valid', valid);
56
+ dispatch('parseInput', parsedTelInput);
57
+ }
58
+ else {
59
+ if (currCountry !== prevCountry) {
60
+ value = null;
61
+ inputValue = '';
62
+ valid = true;
63
+ }
64
+ prevCountry = currCountry;
13
65
  }
14
- });
15
- const handleInput = (event) => {
16
- const inputVal = event.target.value.replace(/\s/g, '');
17
- rawTelInput = inputVal;
18
- handleParsePhoneNumber(country, inputVal);
19
66
  };
20
- const handleParsePhoneNumber = (country, input) => {
21
- try {
22
- parsedTelInput = normalizeTelInput(parsePhoneNumberWithError(input, country || undefined));
67
+ const initialize = () => {
68
+ if (value && country) {
69
+ handleParsePhoneNumber(value, country);
23
70
  }
24
- catch (err) {
25
- if (err instanceof ParseError) {
26
- // Not a phone number, non-existent country, etc.
27
- parsedTelInput = {
28
- isValid: false,
29
- error: err.message
30
- };
71
+ else if (value) {
72
+ const numberHasCountry = getCountryForPartialE164Number(value, { metadata });
73
+ if (numberHasCountry) {
74
+ updateCountry(numberHasCountry);
75
+ handleParsePhoneNumber(value, country);
31
76
  }
32
77
  else {
33
- throw err;
78
+ handleParsePhoneNumber(value);
34
79
  }
35
80
  }
81
+ else if (parsedTelInput && parsedTelInput.phoneNumber) {
82
+ handleParsePhoneNumber(parsedTelInput.phoneNumber, country);
83
+ }
36
84
  };
85
+ onMount(() => {
86
+ initialize();
87
+ });
88
+ let initRun = true;
37
89
  const watchFunction = () => {
38
- if (rawTelInput !== null)
39
- handleParsePhoneNumber(country, rawTelInput);
90
+ if (!initRun) {
91
+ handleParsePhoneNumber(null, country);
92
+ }
93
+ initRun = false;
40
94
  };
41
95
  const countryChangeWatch = watcher(null, watchFunction);
42
96
  $: $countryChangeWatch = country;
@@ -44,9 +98,18 @@ $: $countryChangeWatch = country;
44
98
 
45
99
  <input
46
100
  {disabled}
47
- value={parsedTelInput?.formatOriginal ?? rawTelInput ?? ''}
48
101
  type="tel"
49
- class={$$props.class}
102
+ value={inputValue}
50
103
  {...$$restProps}
51
- on:input={handleInput}
104
+ class={$$props.class}
105
+ on:beforeinput
106
+ on:blur
107
+ on:change
108
+ on:focus
109
+ on:input
110
+ on:keydown
111
+ on:keypress
112
+ on:keyup
113
+ on:paste
114
+ use:telInputAction={handleInputAction}
52
115
  />
@@ -1,21 +1,35 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
- import type { NormalizedTelNumber, CountryCode } from '../../types';
2
+ import type { NormalizedTelNumber, CountryCode, E164Number } from '../../types';
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  [x: string]: any;
6
- country?: CountryCode | null | undefined;
7
- rawTelInput?: string | null | undefined;
8
- parsedTelInput?: Partial<NormalizedTelNumber> | null | undefined;
6
+ country: CountryCode | null;
7
+ value: E164Number | null;
8
+ parsedTelInput: Partial<NormalizedTelNumber> | null;
9
+ valid?: boolean | undefined;
9
10
  disabled?: boolean | undefined;
10
11
  };
11
12
  events: {
13
+ beforeinput: InputEvent;
14
+ blur: FocusEvent;
15
+ change: Event;
16
+ focus: FocusEvent;
17
+ input: Event;
18
+ keydown: KeyboardEvent;
19
+ keypress: KeyboardEvent;
20
+ keyup: KeyboardEvent;
21
+ paste: ClipboardEvent;
22
+ parseError: CustomEvent<any>;
23
+ valid: CustomEvent<any>;
24
+ parseInput: CustomEvent<any>;
25
+ } & {
12
26
  [evt: string]: CustomEvent<any>;
13
27
  };
14
28
  slots: {};
15
29
  };
16
- export declare type TelInputProps = typeof __propDef.props;
17
- export declare type TelInputEvents = typeof __propDef.events;
18
- export declare type TelInputSlots = typeof __propDef.slots;
30
+ export type TelInputProps = typeof __propDef.props;
31
+ export type TelInputEvents = typeof __propDef.events;
32
+ export type TelInputSlots = typeof __propDef.slots;
19
33
  export default class TelInput extends SvelteComponentTyped<TelInputProps, TelInputEvents, TelInputSlots> {
20
34
  }
21
35
  export {};
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { default } from './components/Input/TelInput.svelte';
2
- export { getCurrentCountry, isSelected } from './utils/helpers';
2
+ export { getCurrentCountry, isSelected, inputParser, inspectAllowedChars } from './utils/helpers';
3
3
  export { clickOutsideAction } from './utils/directives/clickOutsideAction';
4
4
  export { normalizedCountries } from './assets';
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { default } from './components/Input/TelInput.svelte';
2
- export { getCurrentCountry, isSelected } from './utils/helpers';
2
+ export { getCurrentCountry, isSelected, inputParser, inspectAllowedChars } from './utils/helpers';
3
3
  export { clickOutsideAction } from './utils/directives/clickOutsideAction';
4
4
  export { normalizedCountries } from './assets';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "svelte-tel-input",
3
3
  "description": "svelte-tel-input",
4
- "version": "0.14.2",
4
+ "version": "1.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/gyurielf/svelte-tel-input.git"
@@ -21,126 +21,62 @@
21
21
  "node": ">= 16"
22
22
  },
23
23
  "dependencies": {
24
- "libphonenumber-js": "^1.10.14"
24
+ "libphonenumber-js": "^1.10.16",
25
+ "svelte": "^3.55.0"
25
26
  },
26
27
  "devDependencies": {
27
- "@babel/core": "^7.19.6",
28
- "@babel/preset-env": "^7.19.4",
29
- "@changesets/cli": "^2.25.0",
30
- "@changesets/get-github-info": "^0.5.1",
31
- "@changesets/types": "^5.2.0",
28
+ "@babel/core": "^7.20.12",
29
+ "@babel/preset-env": "^7.20.2",
30
+ "@changesets/cli": "^2.26.0",
31
+ "@changesets/get-github-info": "^0.5.2",
32
+ "@changesets/types": "^5.2.1",
32
33
  "@macfja/svelte-persistent-store": "^2.2.1",
33
- "@sveltejs/adapter-static": "^1.0.0-next.45",
34
- "@sveltejs/kit": "^1.0.0-next.520",
35
- "@sveltejs/package": "^1.0.0-next.5",
34
+ "@sveltejs/adapter-static": "^1.0.1",
35
+ "@sveltejs/kit": "^1.0.7",
36
+ "@sveltejs/package": "^1.0.2",
36
37
  "@testing-library/jest-dom": "^5.16.5",
37
38
  "@testing-library/svelte": "^3.2.2",
38
- "@types/jest": "^29.2.0",
39
- "@typescript-eslint/eslint-plugin": "^5.40.1",
40
- "@typescript-eslint/parser": "^5.40.1",
41
- "autoprefixer": "^10.4.12",
42
- "babel-jest": "^29.2.1",
43
- "babel-loader": "^8.2.5",
44
- "cssnano": "^5.1.13",
39
+ "@types/jest": "^29.2.5",
40
+ "@typescript-eslint/eslint-plugin": "^5.48.0",
41
+ "@typescript-eslint/parser": "^5.48.0",
42
+ "autoprefixer": "^10.4.13",
43
+ "babel-jest": "^29.3.1",
44
+ "babel-loader": "^9.1.2",
45
+ "cssnano": "^5.1.14",
45
46
  "dotenv": "^16.0.3",
46
- "eslint": "^8.25.0",
47
- "eslint-config-prettier": "^8.5.0",
48
- "eslint-plugin-jest": "^27.1.3",
47
+ "edit-package-json": "^0.8.7",
48
+ "eslint": "^8.31.0",
49
+ "eslint-config-prettier": "^8.6.0",
50
+ "eslint-plugin-jest": "^27.2.1",
49
51
  "eslint-plugin-svelte3": "^4.0.0",
50
- "husky": "^8.0.1",
51
- "jest": "^29.2.1",
52
+ "husky": "^8.0.3",
53
+ "jest": "^29.3.1",
52
54
  "jest-matchmedia-mock": "^1.1.0",
53
55
  "micromatch": "^4.0.5",
54
- "postcss": "^8.4.18",
56
+ "postcss": "^8.4.21",
55
57
  "postcss-load-config": "^4.0.1",
56
- "prettier": "^2.7.1",
57
- "prettier-plugin-svelte": "^2.8.0",
58
- "svelte": "^3.52.0",
59
- "svelte-check": "^2.9.2",
60
- "svelte-inview": "^3.0.1",
58
+ "prettier": "^2.8.1",
59
+ "prettier-plugin-svelte": "^2.9.0",
60
+ "svelte-check": "^3.0.1",
61
+ "svelte-inview": "^3.0.2",
61
62
  "svelte-jester": "^2.3.2",
62
- "svelte-loader": "^3.1.3",
63
- "svelte-preprocess": "^4.10.7",
64
- "svelte2tsx": "^0.5.20",
65
- "tailwindcss": "^3.2.0",
63
+ "svelte-loader": "^3.1.4",
64
+ "svelte-preprocess": "^5.0.0",
65
+ "svelte2tsx": "^0.6.0",
66
+ "tailwindcss": "^3.2.4",
66
67
  "ts-jest": "^29.0.3",
67
68
  "tsconfig-paths-webpack-plugin": "^4.0.0",
68
- "tslib": "^2.4.0",
69
- "typescript": "^4.8.4",
70
- "vite": "^3.1.8"
69
+ "tslib": "^2.4.1",
70
+ "typescript": "^4.9.4",
71
+ "vite": "^4.0.4"
71
72
  },
72
- "standard-version": {
73
- "skip": {
74
- "tag": true
75
- },
76
- "types": [
77
- {
78
- "type": "chore",
79
- "section": "Others (chore)",
80
- "hidden": false
81
- },
82
- {
83
- "type": "revert",
84
- "section": "Reverts",
85
- "hidden": false
86
- },
87
- {
88
- "type": "feat",
89
- "section": "Features",
90
- "hidden": false
91
- },
92
- {
93
- "type": "fix",
94
- "section": "Bug Fixes",
95
- "hidden": false
96
- },
97
- {
98
- "type": "improvement",
99
- "section": "Feature Improvements",
100
- "hidden": false
101
- },
102
- {
103
- "type": "docs",
104
- "section": "Docs",
105
- "hidden": false
106
- },
107
- {
108
- "type": "style",
109
- "section": "Styling",
110
- "hidden": false
111
- },
112
- {
113
- "type": "refactor",
114
- "section": "Code Refactoring",
115
- "hidden": false
116
- },
117
- {
118
- "type": "perf",
119
- "section": "Performance Improvements",
120
- "hidden": false
121
- },
122
- {
123
- "type": "test",
124
- "section": "Tests",
125
- "hidden": false
126
- },
127
- {
128
- "type": "build",
129
- "section": "Build System",
130
- "hidden": false
131
- },
132
- {
133
- "type": "ci",
134
- "section": "CI",
135
- "hidden": false
136
- }
137
- ]
138
- },
139
- "svelte": ".",
140
73
  "type": "module",
141
74
  "license": "MIT",
75
+ "types": "./types/index.d.ts",
142
76
  "exports": {
143
77
  "./package.json": "./package.json",
144
- ".": "./index.js"
145
- }
78
+ ".": "./index.js",
79
+ "./styles/flags.css": "./styles/flags.css"
80
+ },
81
+ "svelte": "./index.js"
146
82
  }
File without changes
package/types/index.d.ts CHANGED
@@ -3,8 +3,10 @@ import type {
3
3
  CountryCode,
4
4
  E164Number,
5
5
  NationalNumber,
6
+ MetadataJson,
6
7
  PhoneNumber
7
8
  } from 'libphonenumber-js';
9
+ import type { Countries } from 'libphonenumber-js/types';
8
10
 
9
11
  export interface Country {
10
12
  id: string;
@@ -50,4 +52,25 @@ export interface NormalizedTelNumber {
50
52
  export type PhoneNumberParseError = 'NOT_A_NUMBER' | 'INVALID_COUNTRY' | 'TOO_SHORT' | 'TOO_LONG';
51
53
  export type PhoneType = 'FIXED_LINE' | 'MOBILE';
52
54
 
53
- export type { CountryCallingCode, CountryCode, E164Number, NationalNumber, PhoneNumber };
55
+ export type {
56
+ CountryCallingCode,
57
+ CountryCode,
58
+ E164Number,
59
+ NationalNumber,
60
+ PhoneNumber,
61
+ Countries,
62
+ MetadataJson
63
+ };
64
+
65
+ export interface TelInputValidity {
66
+ value: boolean | null;
67
+ errorMessage?: string;
68
+ }
69
+
70
+ export type TelInputDispatchEvents = {
71
+ country: CountryCode | null;
72
+ parseError: string;
73
+ parseInput: Partial<NormalizedTelNumber> | null;
74
+ valid: boolean;
75
+ value: E164Number | null;
76
+ };
@@ -0,0 +1,3 @@
1
+ export declare const telInputAction: (node: HTMLInputElement, handler: (val: string) => void) => {
2
+ destroy(): void;
3
+ };
@@ -0,0 +1,20 @@
1
+ import { inspectAllowedChars, inputParser } from '../..';
2
+ export const telInputAction = (node, handler) => {
3
+ const onInput = (event) => {
4
+ if (node && node.contains(event.target)) {
5
+ const value = event.target.value;
6
+ const formattedInput = inputParser(value, {
7
+ parseCharacter: inspectAllowedChars,
8
+ allowSpaces: true
9
+ });
10
+ node.value = formattedInput;
11
+ handler(formattedInput);
12
+ }
13
+ };
14
+ node.addEventListener('input', onInput, true);
15
+ return {
16
+ destroy() {
17
+ node.removeEventListener('input', onInput, true);
18
+ }
19
+ };
20
+ };
@@ -1,10 +1,77 @@
1
- import type { PhoneNumber } from '../types';
1
+ import type { PhoneNumber, MetadataJson, Countries, E164Number, CountryCode } from '../types';
2
2
  export declare const capitalize: (str: string) => string;
3
3
  export declare const getCurrentCountry: () => Promise<string | undefined>;
4
4
  export declare const isNumber: (value: number) => boolean;
5
- export declare const normalizeTelInput: (input: PhoneNumber) => {
6
- [k: string]: string | boolean | import("libphonenumber-js/types").E164Number | import("libphonenumber-js/types").CountryCallingCode | import("libphonenumber-js/types").NationalNumber | null | undefined;
5
+ export declare const normalizeTelInput: (input?: PhoneNumber) => {
6
+ [k: string]: string | boolean | E164Number | import("libphonenumber-js/types").CountryCallingCode | import("libphonenumber-js/types").NationalNumber | null | undefined;
7
7
  };
8
8
  export declare const isSelected: <T extends {
9
9
  id: string;
10
- }>(itemToSelect: T, selectedItem: T | null | undefined) => boolean;
10
+ }>(itemToSelect: string | T, selectedItem: string | T | null | undefined) => boolean;
11
+ export declare const getInternationalPhoneNumberPrefix: (country: CountryCode, metadata: MetadataJson) => string;
12
+ /**
13
+ * Trims phone number digits if they exceed the maximum possible length
14
+ * for a national (significant) number for the country.
15
+ * @param {string} number - A possibly incomplete phone number digits string. Can be a possibly incomplete E.164 phone number.
16
+ * @param {string} country
17
+ * @param {object} metadata - `libphonenumber-js` metadata.
18
+ * @return {string} Can be empty.
19
+ */
20
+ export declare const trimNumber: (number: E164Number, country: CountryCode, metadata: MetadataJson) => string;
21
+ export declare const getMaxNumberLength: (country: CountryCode, metadata: MetadataJson) => number;
22
+ /**
23
+ * If the phone number being input is an international one
24
+ * then tries to derive the country from the phone number.
25
+ * (regardless of whether there's any country currently selected)
26
+ * @param {string} partialE164Number - A possibly incomplete E.164 phone number.
27
+ * @param {string?} country - Currently selected country.
28
+ * @param {string[]?} countries - A list of available countries. If not passed then "all countries" are assumed.
29
+ * @param {object} metadata - `libphonenumber-js` metadata.
30
+ * @return {string?}
31
+ */
32
+ export declare const getCountryForPartialE164Number: (partialE164Number: E164Number, { country, countries, required, metadata }: {
33
+ country?: CountryCode | undefined;
34
+ countries?: Countries[] | undefined;
35
+ required?: boolean | undefined;
36
+ metadata: MetadataJson;
37
+ }) => CountryCode | undefined;
38
+ /**
39
+ * Determines the country for a given (possibly incomplete) E.164 phone number.
40
+ * @param {string} number - A possibly incomplete E.164 phone number.
41
+ * @param {object} metadata - `libphonenumber-js` metadata.
42
+ * @return {string?}
43
+ */
44
+ export declare const getCountryFromPossiblyIncompleteInternationalPhoneNumber: (number: E164Number, metadata: MetadataJson) => CountryCode | undefined;
45
+ /**
46
+ * Parses a partially entered national phone number digits
47
+ * (or a partially entered E.164 international phone number)
48
+ * and returns the national significant number part.
49
+ * National significant number returned doesn't come with a national prefix.
50
+ * @param {string} number - National number digits. Or possibly incomplete E.164 phone number.
51
+ * @param {string?} country
52
+ * @param {object} metadata - `libphonenumber-js` metadata.
53
+ * @return {string} [result]
54
+ */
55
+ export declare const getNationalSignificantNumberDigits: (number: E164Number, country: CountryCode, metadata: MetadataJson) => import("libphonenumber-js/types").NationalNumber | undefined;
56
+ /**
57
+ * Checks if a partially entered E.164 phone number could belong to a country.
58
+ * @param {string} number
59
+ * @param {CountryCode} country
60
+ * @return {boolean}
61
+ */
62
+ export declare const couldNumberBelongToCountry: (number: E164Number, country: CountryCode, metadata: MetadataJson) => boolean;
63
+ export declare const isSupportedCountry: (country: CountryCode, metadata: MetadataJson) => boolean;
64
+ /**
65
+ * These mappings map a character (key) to a specific digit that should
66
+ * replace it for normalization purposes.
67
+ * @param {string} character
68
+ * @returns {string}
69
+ */
70
+ export declare const allowedCharacters: (character: string, { spaces }?: {
71
+ spaces?: boolean | undefined;
72
+ }) => string;
73
+ export declare const inputParser: (text: string, { allowSpaces, parseCharacter }: {
74
+ allowSpaces: boolean;
75
+ parseCharacter: (characted: string, val: string, allowSpaces?: boolean) => string;
76
+ }) => string;
77
+ export declare const inspectAllowedChars: (character: string, value: string, allowSpaces?: boolean) => string;
package/utils/helpers.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { AsYouType, Metadata, getCountryCallingCode } from 'libphonenumber-js/core';
1
2
  export const capitalize = (str) => {
2
3
  return (str && str[0].toUpperCase() + str.slice(1).toLowerCase()) || '';
3
4
  };
5
+ // Use carefully, it can be rate limited.
4
6
  export const getCurrentCountry = async () => {
5
7
  try {
6
8
  const response = await (await fetch('https://ip2c.org/s')).text();
@@ -23,6 +25,7 @@ export const normalizeTelInput = (input) => {
23
25
  const filteredResult = Object.fromEntries(Object.entries({
24
26
  countryCode: input ? input.country : null,
25
27
  isValid: input ? input.isValid() : false,
28
+ isPossible: input ? input.isPossible() : false,
26
29
  phoneNumber: input ? input.number : null,
27
30
  countryCallingCode: input ? input.countryCallingCode : null,
28
31
  formattedNumber: input ? input.formatInternational() : null,
@@ -57,7 +60,224 @@ export const isSelected = (itemToSelect, selectedItem) => {
57
60
  return false;
58
61
  }
59
62
  }
63
+ else if (itemToSelect === selectedItem) {
64
+ return true;
65
+ }
60
66
  else {
61
67
  return false;
62
68
  }
63
69
  };
70
+ export const getInternationalPhoneNumberPrefix = (country, metadata) => {
71
+ const ONLY_DIGITS_REGEXP = /^\d+$/;
72
+ // Standard international phone number prefix: "+" and "country calling code".
73
+ let prefix = '+' + getCountryCallingCode(country, metadata);
74
+ // Get "leading digits" for a phone number of the country.
75
+ // If there're "leading digits" then they can be part of the prefix too.
76
+ const newMetadata = new Metadata(metadata);
77
+ const leadingDigits = newMetadata.numberingPlan?.leadingDigits();
78
+ if (leadingDigits && ONLY_DIGITS_REGEXP.test(leadingDigits)) {
79
+ prefix += leadingDigits;
80
+ }
81
+ return prefix;
82
+ };
83
+ /**
84
+ * Trims phone number digits if they exceed the maximum possible length
85
+ * for a national (significant) number for the country.
86
+ * @param {string} number - A possibly incomplete phone number digits string. Can be a possibly incomplete E.164 phone number.
87
+ * @param {string} country
88
+ * @param {object} metadata - `libphonenumber-js` metadata.
89
+ * @return {string} Can be empty.
90
+ */
91
+ export const trimNumber = (number, country, metadata) => {
92
+ const nationalSignificantNumberPart = getNationalSignificantNumberDigits(number, country, metadata);
93
+ if (nationalSignificantNumberPart) {
94
+ const overflowDigitsCount = nationalSignificantNumberPart.length - getMaxNumberLength(country, metadata);
95
+ if (overflowDigitsCount > 0) {
96
+ return number.slice(0, number.length - overflowDigitsCount);
97
+ }
98
+ }
99
+ return number;
100
+ };
101
+ export const getMaxNumberLength = (country, metadata) => {
102
+ // Get "possible lengths" for a phone number of the country.
103
+ const newMetadata = new Metadata(metadata);
104
+ newMetadata.selectNumberingPlan(country);
105
+ // Return the last "possible length".
106
+ if (newMetadata.numberingPlan) {
107
+ return newMetadata.numberingPlan.possibleLengths()[newMetadata.numberingPlan.possibleLengths().length - 1];
108
+ }
109
+ else {
110
+ throw new Error('There is no metadata object.');
111
+ }
112
+ };
113
+ /**
114
+ * If the phone number being input is an international one
115
+ * then tries to derive the country from the phone number.
116
+ * (regardless of whether there's any country currently selected)
117
+ * @param {string} partialE164Number - A possibly incomplete E.164 phone number.
118
+ * @param {string?} country - Currently selected country.
119
+ * @param {string[]?} countries - A list of available countries. If not passed then "all countries" are assumed.
120
+ * @param {object} metadata - `libphonenumber-js` metadata.
121
+ * @return {string?}
122
+ */
123
+ export const getCountryForPartialE164Number = (partialE164Number, { country, countries, required, metadata }) => {
124
+ if (partialE164Number === '+') {
125
+ // Don't change the currently selected country yet.
126
+ return country;
127
+ }
128
+ const derived_country = getCountryFromPossiblyIncompleteInternationalPhoneNumber(partialE164Number, metadata);
129
+ // If a phone number is being input in international form
130
+ // and the country can already be derived from it,
131
+ // then select that country.
132
+ if (derived_country && (!countries || countries.indexOf(derived_country) >= 0)) {
133
+ return derived_country;
134
+ }
135
+ // If "International" country option has not been disabled
136
+ // and the international phone number entered doesn't correspond
137
+ // to the currently selected country then reset the currently selected country.
138
+ else if (country &&
139
+ !required &&
140
+ !couldNumberBelongToCountry(partialE164Number, country, metadata)) {
141
+ return undefined;
142
+ }
143
+ // Don't change the currently selected country.
144
+ return country;
145
+ };
146
+ /**
147
+ * Determines the country for a given (possibly incomplete) E.164 phone number.
148
+ * @param {string} number - A possibly incomplete E.164 phone number.
149
+ * @param {object} metadata - `libphonenumber-js` metadata.
150
+ * @return {string?}
151
+ */
152
+ export const getCountryFromPossiblyIncompleteInternationalPhoneNumber = (number, metadata) => {
153
+ const formatter = new AsYouType(undefined, metadata);
154
+ formatter.input(number);
155
+ // // `001` is a special "non-geograpical entity" code
156
+ // // in Google's `libphonenumber` library.
157
+ // if (formatter.getCountry() === '001') {
158
+ // return
159
+ // }
160
+ return formatter.getCountry();
161
+ };
162
+ /**
163
+ * Parses a partially entered national phone number digits
164
+ * (or a partially entered E.164 international phone number)
165
+ * and returns the national significant number part.
166
+ * National significant number returned doesn't come with a national prefix.
167
+ * @param {string} number - National number digits. Or possibly incomplete E.164 phone number.
168
+ * @param {string?} country
169
+ * @param {object} metadata - `libphonenumber-js` metadata.
170
+ * @return {string} [result]
171
+ */
172
+ export const getNationalSignificantNumberDigits = (number, country, metadata) => {
173
+ // Create "as you type" formatter.
174
+ const formatter = new AsYouType(country, metadata);
175
+ // Input partial national phone number.
176
+ formatter.input(number);
177
+ // Return the parsed partial national phone number.
178
+ const phoneNumber = formatter.getNumber();
179
+ return phoneNumber && phoneNumber.nationalNumber;
180
+ };
181
+ /**
182
+ * Checks if a partially entered E.164 phone number could belong to a country.
183
+ * @param {string} number
184
+ * @param {CountryCode} country
185
+ * @return {boolean}
186
+ */
187
+ export const couldNumberBelongToCountry = (number, country, metadata) => {
188
+ const intlPhoneNumberPrefix = getInternationalPhoneNumberPrefix(country, metadata);
189
+ let i = 0;
190
+ while (i < number.length && i < intlPhoneNumberPrefix.length) {
191
+ if (number[i] !== intlPhoneNumberPrefix[i]) {
192
+ return false;
193
+ }
194
+ i++;
195
+ }
196
+ return true;
197
+ };
198
+ export const isSupportedCountry = (country, metadata) => {
199
+ return metadata.countries[country] !== undefined;
200
+ };
201
+ /**
202
+ * These mappings map a character (key) to a specific digit that should
203
+ * replace it for normalization purposes.
204
+ * @param {string} character
205
+ * @returns {string}
206
+ */
207
+ export const allowedCharacters = (character, { spaces } = {
208
+ spaces: false
209
+ }) => {
210
+ const DIGITS = {
211
+ '0': '0',
212
+ '1': '1',
213
+ '2': '2',
214
+ '3': '3',
215
+ '4': '4',
216
+ '5': '5',
217
+ '6': '6',
218
+ '7': '7',
219
+ '8': '8',
220
+ '9': '9',
221
+ '\uFF10': '0',
222
+ '\uFF11': '1',
223
+ '\uFF12': '2',
224
+ '\uFF13': '3',
225
+ '\uFF14': '4',
226
+ '\uFF15': '5',
227
+ '\uFF16': '6',
228
+ '\uFF17': '7',
229
+ '\uFF18': '8',
230
+ '\uFF19': '9',
231
+ '\u0660': '0',
232
+ '\u0661': '1',
233
+ '\u0662': '2',
234
+ '\u0663': '3',
235
+ '\u0664': '4',
236
+ '\u0665': '5',
237
+ '\u0666': '6',
238
+ '\u0667': '7',
239
+ '\u0668': '8',
240
+ '\u0669': '9',
241
+ '\u06F0': '0',
242
+ '\u06F1': '1',
243
+ '\u06F2': '2',
244
+ '\u06F3': '3',
245
+ '\u06F4': '4',
246
+ '\u06F5': '5',
247
+ '\u06F6': '6',
248
+ '\u06F7': '7',
249
+ '\u06F8': '8',
250
+ '\u06F9': '9' // Eastern-Arabic digit 9,
251
+ };
252
+ // Allow spaces
253
+ if (spaces) {
254
+ const regex = new RegExp('[\\t\\n\\v\\f\\r \\u00a0\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u200b\\u2028\\u2029\\u3000]', 'g');
255
+ if (regex.test(character)) {
256
+ return character;
257
+ }
258
+ }
259
+ // Allow digits
260
+ return DIGITS[character];
261
+ };
262
+ export const inputParser = (text, { allowSpaces, parseCharacter }) => {
263
+ let value = '';
264
+ let index = 0;
265
+ while (index < text.length) {
266
+ const character = parseCharacter(text[index], value, allowSpaces);
267
+ if (character !== undefined) {
268
+ value += character;
269
+ }
270
+ index++;
271
+ }
272
+ return value;
273
+ };
274
+ export const inspectAllowedChars = (character, value, allowSpaces) => {
275
+ // Leading plus is allowed
276
+ if (character === '+') {
277
+ if (!value) {
278
+ return character;
279
+ }
280
+ }
281
+ // Allowed characters
282
+ return allowedCharacters(character, { spaces: allowSpaces });
283
+ };