vue-tel-input 6.0.0 → 6.0.3

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,623 +0,0 @@
1
- <template>
2
- <div :class="['vue-tel-input', styleClasses, { disabled: disabled }]">
3
- <div
4
- v-click-outside="clickedOutside"
5
- aria-label="Country Code Selector"
6
- aria-haspopup="listbox"
7
- :aria-expanded="open"
8
- role="button"
9
- :class="['vti__dropdown', { open: open, disabled: dropdownOptions.disabled }]"
10
- :tabindex="dropdownOptions.tabindex"
11
- @keydown="keyboardNav"
12
- @click="toggleDropdown"
13
- @keydown.space="toggleDropdown"
14
- @keydown.esc="reset"
15
- @keydown.tab="reset"
16
- >
17
- <span class="vti__selection">
18
- <span
19
- v-if="dropdownOptions.showFlags"
20
- :class="['vti__flag', activeCountryCode.toLowerCase()]"
21
- ></span>
22
- <span v-if="dropdownOptions.showDialCodeInSelection" class="vti__country-code">
23
- +{{ activeCountry && activeCountry.dialCode }}
24
- </span>
25
- <slot name="arrow-icon" :open="open">
26
- <span class="vti__dropdown-arrow">{{ open ? "▲" : "▼" }}</span>
27
- </slot>
28
- </span>
29
- <ul
30
- v-if="open"
31
- ref="list"
32
- class="vti__dropdown-list"
33
- :class="dropdownOpenDirection"
34
- role="listbox"
35
- >
36
- <li
37
- v-for="(pb, index) in sortedCountries"
38
- role="option"
39
- :class="['vti__dropdown-item', getItemClass(index, pb.iso2)]"
40
- :key="pb.iso2 + (pb.preferred ? '-preferred' : '')"
41
- tabindex="-1"
42
- @click="choose(pb)"
43
- @mousemove="selectedIndex = index"
44
- :aria-selected="activeCountryCode === pb.iso2 && !pb.preferred"
45
- >
46
- <span
47
- v-if="dropdownOptions.showFlags"
48
- :class="['vti__flag', pb.iso2.toLowerCase()]"
49
- ></span>
50
- <strong>{{ pb.name }}</strong>
51
- <span v-if="dropdownOptions.showDialCodeInList"> +{{ pb.dialCode }} </span>
52
- </li>
53
- </ul>
54
- </div>
55
- <input
56
- v-model="phone"
57
- ref="input"
58
- :type="inputOptions.type"
59
- :autocomplete="inputOptions.autocomplete"
60
- :autofocus="inputOptions.autofocus"
61
- :class="['vti__input', inputOptions.styleClasses]"
62
- :disabled="disabled"
63
- :id="inputOptions.id"
64
- :maxlength="inputOptions.maxlength"
65
- :name="inputOptions.name"
66
- :placeholder="parsedPlaceholder"
67
- :readonly="inputOptions.readonly"
68
- :required="inputOptions.required"
69
- :tabindex="inputOptions.tabindex"
70
- :value="modelValue"
71
- :aria-describedby="inputOptions['aria-describedby']"
72
- @blur="onBlur"
73
- @focus="onFocus"
74
- @input="onInput"
75
- @keyup.enter="onEnter"
76
- @keyup.space="onSpace"
77
- />
78
- <slot name="icon-right" />
79
- </div>
80
- </template>
81
-
82
- <script>
83
- import { parsePhoneNumberFromString } from 'libphonenumber-js';
84
- import utils, { getCountry, setCaretPosition } from '../utils';
85
- import clickOutside from '../directives/click-outside';
86
-
87
- function getDefault(key) {
88
- const value = utils.options[key];
89
- if (typeof value === 'undefined') {
90
- return utils.options[key];
91
- }
92
- return value;
93
- }
94
-
95
- // let examples = null;
96
- // const getExamples = () => new Promise(
97
- // (resolve) => (
98
- // examples
99
- // ? resolve(examples)
100
- // : import('libphonenumber-js/examples.mobile.json')
101
- // .then((results) => {
102
- // examples = results;
103
- // resolve(results);
104
- // })
105
- // ),
106
- // );
107
-
108
- export default {
109
- name: 'VueTelInput',
110
- directives: {
111
- clickOutside,
112
- },
113
- props: {
114
- modelValue: {
115
- type: String,
116
- default: '',
117
- },
118
- allCountries: {
119
- type: Array,
120
- default: () => getDefault('allCountries'),
121
- },
122
- autoFormat: {
123
- type: Boolean,
124
- default: () => getDefault('autoFormat'),
125
- },
126
- customValidate: {
127
- type: [Boolean, RegExp],
128
- default: () => getDefault('customValidate'),
129
- },
130
- defaultCountry: {
131
- // Default country code, ie: 'AU'
132
- // Will override the current country of user
133
- type: [String, Number],
134
- default: () => getDefault('defaultCountry'),
135
- },
136
- disabled: {
137
- type: Boolean,
138
- default: () => getDefault('disabled'),
139
- },
140
- autoDefaultCountry: {
141
- type: Boolean,
142
- default: () => getDefault('autoDefaultCountry'),
143
- },
144
- dropdownOptions: {
145
- type: Object,
146
- default: () => getDefault('dropdownOptions'),
147
- },
148
- ignoredCountries: {
149
- type: Array,
150
- default: () => getDefault('ignoredCountries'),
151
- },
152
- inputOptions: {
153
- type: Object,
154
- default: () => getDefault('inputOptions'),
155
- },
156
- invalidMsg: {
157
- type: String,
158
- default: () => getDefault('invalidMsg'),
159
- },
160
- mode: {
161
- type: String,
162
- default: () => getDefault('mode'),
163
- },
164
- onlyCountries: {
165
- type: Array,
166
- default: () => getDefault('onlyCountries'),
167
- },
168
- preferredCountries: {
169
- type: Array,
170
- default: () => getDefault('preferredCountries'),
171
- },
172
- validCharactersOnly: {
173
- type: Boolean,
174
- default: () => getDefault('validCharactersOnly'),
175
- },
176
- styleClasses: {
177
- type: [String, Array, Object],
178
- default: () => getDefault('styleClasses'),
179
- },
180
- },
181
- data() {
182
- return {
183
- phone: '',
184
- activeCountryCode: '',
185
- open: false,
186
- finishMounted: false,
187
- selectedIndex: null,
188
- typeToFindInput: '',
189
- typeToFindTimer: null,
190
- dropdownOpenDirection: 'below',
191
- parsedPlaceholder: this.inputOptions.placeholder,
192
- };
193
- },
194
- computed: {
195
- activeCountry() {
196
- return this.findCountry(this.activeCountryCode);
197
- },
198
- parsedMode() {
199
- if (this.mode === 'auto') {
200
- if (!this.phone || this.phone[0] !== '+') {
201
- return 'national';
202
- }
203
- return 'international';
204
- }
205
- if (!['international', 'national'].includes(this.mode)) {
206
- console.error('Invalid value of prop "mode"');
207
- return 'international';
208
- }
209
- return this.mode;
210
- },
211
- filteredCountries() {
212
- // List countries after filtered
213
- if (this.onlyCountries.length) {
214
- return this.allCountries
215
- .filter(({ iso2 }) => this.onlyCountries.some((c) => c.toUpperCase() === iso2));
216
- }
217
-
218
- if (this.ignoredCountries.length) {
219
- return this.allCountries.filter(
220
- ({ iso2 }) => !this.ignoredCountries.includes(iso2.toUpperCase())
221
- && !this.ignoredCountries.includes(iso2.toLowerCase()),
222
- );
223
- }
224
-
225
- return this.allCountries;
226
- },
227
- sortedCountries() {
228
- // Sort the list countries: from preferred countries to all countries
229
- const preferredCountries = this.getCountries(this.preferredCountries)
230
- .map((country) => ({ ...country, preferred: true }));
231
-
232
- return [...preferredCountries, ...this.filteredCountries];
233
- },
234
- phoneObject() {
235
- let result;
236
- if (this.phone?.[0] === '+') {
237
- result = parsePhoneNumberFromString(this.phone) || {};
238
- } else {
239
- result = parsePhoneNumberFromString(this.phone, this.activeCountryCode) || {};
240
- }
241
-
242
- const {
243
- metadata,
244
- ...phoneObject
245
- } = result;
246
-
247
- let valid = result.isValid?.();
248
- let formatted = this.phone;
249
-
250
- if (valid) {
251
- formatted = result.format?.(this.parsedMode.toUpperCase());
252
- }
253
-
254
- if (result.country && (this.ignoredCountries.length || this.onlyCountries.length)) {
255
- if (!this.findCountry(result.country)) {
256
- valid = false;
257
- Object.assign(result, { country: null });
258
- }
259
- }
260
-
261
- Object.assign(phoneObject, {
262
- countryCode: result.country,
263
- valid,
264
- country: this.activeCountry,
265
- formatted,
266
- });
267
-
268
- return phoneObject;
269
- },
270
- },
271
- watch: {
272
- activeCountry(value, oldValue) {
273
- if (!value && oldValue?.iso2) {
274
- this.activeCountryCode = oldValue.iso2;
275
- return;
276
- }
277
- if (value?.iso2) {
278
- this.$emit('country-changed', value);
279
- // this.resetPlaceholder();
280
- }
281
- },
282
- 'phoneObject.countryCode': function (value) {
283
- this.activeCountryCode = value || '';
284
- },
285
- 'phoneObject.valid': function () {
286
- this.$emit('validate', this.phoneObject);
287
- },
288
- 'phoneObject.formatted': function (value) {
289
- if (!this.autoFormat || this.customValidate) {
290
- return;
291
- }
292
- this.emitInput(value);
293
-
294
- this.$nextTick(() => {
295
- // In case `v-model` is not set, we need to update the `phone` to be new formatted value
296
- if (value && !this.modelValue) {
297
- this.phone = value;
298
- }
299
- });
300
- },
301
- // finishMounted() {
302
- // this.resetPlaceholder();
303
- // },
304
- 'inputOptions.placeholder': function () {
305
- this.resetPlaceholder();
306
- },
307
- modelValue(value, oldValue) {
308
- if (!this.testCharacters()) {
309
- this.$nextTick(() => {
310
- this.phone = oldValue;
311
- this.onInput();
312
- });
313
- } else {
314
- this.phone = value;
315
- }
316
- },
317
- open(isDropdownOpened) {
318
- // Emit open and close events
319
- if (isDropdownOpened) {
320
- this.setDropdownPosition();
321
- this.$emit('open');
322
- } else {
323
- this.$emit('close');
324
- }
325
- },
326
- },
327
- mounted() {
328
- if (this.modelValue) {
329
- this.phone = this.modelValue.trim();
330
- }
331
-
332
- this.cleanInvalidCharacters();
333
-
334
- this.initializeCountry()
335
- .then(() => {
336
- if (!this.phone
337
- && this.inputOptions?.showDialCode
338
- && this.activeCountryCode) {
339
- this.phone = `+${this.activeCountryCode}`;
340
- }
341
- this.$emit('validate', this.phoneObject);
342
- })
343
- .catch(console.error)
344
- .then(() => {
345
- this.finishMounted = true;
346
- });
347
- },
348
- methods: {
349
- resetPlaceholder() {
350
- this.parsedPlaceholder = this.inputOptions.placeholder;
351
- // TODO: Fix dynamicPlaceholder
352
- // if (!this.inputOptions.dynamicPlaceholder) {
353
- // return result;
354
- // }
355
- // getExamples()
356
- // .then((results) => {
357
- // examples = results;
358
- // const mode = (!this.mode || this.mode === 'auto') ? 'international' : this.mode;
359
- // const number = getExampleNumber(this.activeCountryCode.toUpperCase(), results);
360
- // this.parsedPlaceholder = number?.format(mode.toUpperCase()) || this.placeholder;
361
- // })
362
- // .catch(console.error);
363
- },
364
- initializeCountry() {
365
- return new Promise((resolve) => {
366
- /**
367
- * 1. If the phone included prefix (i.e. +12), try to get the country and set it
368
- */
369
- if (this.phone?.[0] === '+') {
370
- resolve();
371
- return;
372
- }
373
- /**
374
- * 2. Use default country if passed from parent
375
- */
376
- if (this.defaultCountry) {
377
- if (typeof this.defaultCountry === 'string') {
378
- this.choose(this.defaultCountry);
379
- resolve();
380
- return;
381
- }
382
- if (typeof this.defaultCountry === 'number') {
383
- const country = this.findCountryByDialCode(this.defaultCountry);
384
- if (country) {
385
- this.choose(country.iso2);
386
- resolve();
387
- return;
388
- }
389
- }
390
- }
391
-
392
- const fallbackCountry = this.preferredCountries[0] || this.filteredCountries[0];
393
- /**
394
- * 3. Check if fetching country based on user's IP is allowed, set it as the default country
395
- */
396
- if (this.autoDefaultCountry) {
397
- getCountry()
398
- .then((res) => {
399
- this.choose(res || this.activeCountryCode);
400
- })
401
- .catch((error) => {
402
- console.warn(error);
403
- /**
404
- * 4. Use the first country from preferred list (if available) or all countries list
405
- */
406
- this.choose(fallbackCountry);
407
- })
408
- .then(() => {
409
- resolve();
410
- });
411
- } else {
412
- /**
413
- * 4. Use the first country from preferred list (if available) or all countries list
414
- */
415
- this.choose(fallbackCountry);
416
- resolve();
417
- }
418
- });
419
- },
420
- /**
421
- * Get the list of countries from the list of iso2 code
422
- */
423
- getCountries(list = []) {
424
- return list
425
- .map((countryCode) => this.findCountry(countryCode))
426
- .filter(Boolean);
427
- },
428
- findCountry(iso = '') {
429
- return this.filteredCountries.find((country) => country.iso2 === iso.toUpperCase());
430
- },
431
- findCountryByDialCode(dialCode) {
432
- return this.filteredCountries.find((country) => Number(country.dialCode) === dialCode);
433
- },
434
- getItemClass(index, iso2) {
435
- const highlighted = this.selectedIndex === index;
436
- const lastPreferred = index === this.preferredCountries.length - 1;
437
- const preferred = this.preferredCountries.some((c) => c.toUpperCase() === iso2);
438
- return {
439
- highlighted,
440
- 'last-preferred': lastPreferred,
441
- preferred,
442
- };
443
- },
444
- choose(country) {
445
- let parsedCountry = country;
446
- if (typeof parsedCountry === 'string') {
447
- parsedCountry = this.findCountry(parsedCountry);
448
- }
449
-
450
- if (!parsedCountry) {
451
- return;
452
- }
453
- if (this.phone?.[0] === '+'
454
- && parsedCountry.iso2
455
- && this.phoneObject.nationalNumber) {
456
- this.activeCountryCode = parsedCountry.iso2;
457
- // Attach the current phone number with the newly selected country
458
- this.phone = parsePhoneNumberFromString(
459
- this.phoneObject.nationalNumber,
460
- parsedCountry.iso2,
461
- )
462
- .formatInternational();
463
- return;
464
- }
465
-
466
- if (this.inputOptions?.showDialCode && parsedCountry) {
467
- // Reset phone if the showDialCode is set
468
- this.phone = `+${parsedCountry.dialCode}`;
469
- this.activeCountryCode = parsedCountry.iso2 || '';
470
- return;
471
- }
472
-
473
- // update value, even if international mode is NOT used
474
- this.activeCountryCode = parsedCountry.iso2 || '';
475
- this.emitInput(this.phone);
476
- },
477
- cleanInvalidCharacters() {
478
- const currentPhone = this.phone;
479
- if (this.validCharactersOnly) {
480
- const results = this.phone.match(/[()\-+0-9\s]*/g);
481
- this.phone = results.join('');
482
- }
483
-
484
- if (this.customValidate && this.customValidate instanceof RegExp) {
485
- const results = this.phone.match(this.customValidate);
486
- this.phone = results.join('');
487
- }
488
-
489
- if (currentPhone !== this.phone) {
490
- this.emitInput(this.phone);
491
- }
492
- },
493
- testCharacters() {
494
- if (this.validCharactersOnly) {
495
- const result = /^[()\-+0-9\s]*$/.test(this.phone);
496
- if (!result) {
497
- return false;
498
- }
499
- }
500
- if (this.customValidate) {
501
- return this.testCustomValidate();
502
- }
503
- return true;
504
- },
505
- testCustomValidate() {
506
- return this.customValidate instanceof RegExp ? this.customValidate.test(this.phone) : false;
507
- },
508
- onInput() {
509
- this.$refs.input.setCustomValidity(this.phoneObject.valid ? '' : this.invalidMsg);
510
- // Returns response.number to assign it to v-model (if being used)
511
- // Returns full response for cases @input is used
512
- // and parent wants to return the whole response.
513
- this.emitInput(this.phone);
514
- },
515
- emitInput(value) {
516
- this.$emit('update:modelValue', value);
517
- this.$emit('on-input', value, this.phoneObject, this.$refs.input);
518
- },
519
- onBlur() {
520
- this.$emit('blur');
521
- },
522
- onFocus() {
523
- setCaretPosition(this.$refs.input, this.phone.length);
524
- this.$emit('focus');
525
- },
526
- onEnter() {
527
- this.$emit('enter');
528
- },
529
- onSpace() {
530
- this.$emit('space');
531
- },
532
- focus() {
533
- this.$refs.input.focus();
534
- },
535
- toggleDropdown() {
536
- if (this.disabled || this.dropdownOptions.disabled) {
537
- return;
538
- }
539
- this.open = !this.open;
540
- },
541
- clickedOutside() {
542
- this.open = false;
543
- },
544
- keyboardNav(e) {
545
- if (e.keyCode === 40) {
546
- // down arrow
547
- e.preventDefault();
548
- this.open = true;
549
- if (this.selectedIndex === null) {
550
- this.selectedIndex = 0;
551
- } else {
552
- this.selectedIndex = Math.min(this.sortedCountries.length - 1, this.selectedIndex + 1);
553
- }
554
- const selEle = this.$refs.list.children[this.selectedIndex];
555
- selEle.focus();
556
- if (selEle.offsetTop + selEle.clientHeight
557
- > this.$refs.list.scrollTop + this.$refs.list.clientHeight) {
558
- this.$refs.list.scrollTop = selEle.offsetTop
559
- - this.$refs.list.clientHeight
560
- + selEle.clientHeight;
561
- }
562
- } else if (e.keyCode === 38) {
563
- // up arrow
564
- e.preventDefault();
565
- this.open = true;
566
- if (this.selectedIndex === null) {
567
- this.selectedIndex = this.sortedCountries.length - 1;
568
- } else {
569
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
570
- }
571
- const selEle = this.$refs.list.children[this.selectedIndex];
572
- selEle.focus();
573
- if (selEle.offsetTop < this.$refs.list.scrollTop) {
574
- this.$refs.list.scrollTop = selEle.offsetTop;
575
- }
576
- } else if (e.keyCode === 13) {
577
- // enter key
578
- if (this.selectedIndex !== null) {
579
- this.choose(this.sortedCountries[this.selectedIndex]);
580
- }
581
- this.open = !this.open;
582
- } else {
583
- // typing a country's name
584
- this.typeToFindInput += e.key;
585
- clearTimeout(this.typeToFindTimer);
586
- this.typeToFindTimer = setTimeout(() => {
587
- this.typeToFindInput = '';
588
- }, 700);
589
- // don't include preferred countries so we jump to the right place in the alphabet
590
- const typedCountryI = this.sortedCountries
591
- .slice(this.preferredCountries.length)
592
- .findIndex((c) => c.name.toLowerCase().startsWith(this.typeToFindInput));
593
- if (typedCountryI >= 0) {
594
- this.selectedIndex = this.preferredCountries.length + typedCountryI;
595
- const selEle = this.$refs.list.children[this.selectedIndex];
596
- const needToScrollTop = selEle.offsetTop < this.$refs.list.scrollTop;
597
- const needToScrollBottom = selEle.offsetTop + selEle.clientHeight
598
- > this.$refs.list.scrollTop + this.$refs.list.clientHeight;
599
- if (needToScrollTop || needToScrollBottom) {
600
- this.$refs.list.scrollTop = selEle.offsetTop - this.$refs.list.clientHeight / 2;
601
- }
602
- }
603
- }
604
- },
605
- reset() {
606
- this.selectedIndex = this.sortedCountries.map((c) => c.iso2).indexOf(this.activeCountryCode);
607
- this.open = false;
608
- },
609
- setDropdownPosition() {
610
- const spaceBelow = window.innerHeight - this.$el.getBoundingClientRect().bottom;
611
- const hasEnoughSpaceBelow = spaceBelow > 200;
612
- if (hasEnoughSpaceBelow) {
613
- this.dropdownOpenDirection = 'below';
614
- } else {
615
- this.dropdownOpenDirection = 'above';
616
- }
617
- },
618
- },
619
- };
620
- </script>
621
-
622
- <style src="../assets/sprite.css"></style>
623
- <style src="../assets/component.css"></style>
@@ -1,24 +0,0 @@
1
- // Click-outside by BosNaufal: https://github.com/BosNaufal/vue-click-outside
2
- export default {
3
- beforeMount(el, binding, vNode) {
4
- // Provided expression must evaluate to a function.
5
- if (typeof binding.value !== 'function') {
6
- const compName = vNode.context.name;
7
- let warn = `[Vue-click-outside:] provided expression ${binding.expression} is not a function, but has to be`;
8
- if (compName) {
9
- warn += `Found in component ${compName}`;
10
- }
11
- console.warn(warn);
12
- }
13
- el.clickOutsideEvent = function (event) {
14
- if (!(el === event.target || el.contains(event.target))) {
15
- binding.value(event, el);
16
- }
17
- };
18
- // add Event Listeners
19
- document.body.addEventListener('click', el.clickOutsideEvent);
20
- },
21
- unmounted(el) {
22
- document.body.removeEventListener('click', el.clickOutsideEvent);
23
- },
24
- };
package/src/index.js DELETED
@@ -1,34 +0,0 @@
1
- import utils, { defaultOptions } from './utils';
2
- import VueTelInput from './components/vue-tel-input.vue';
3
-
4
- export { VueTelInput };
5
-
6
- export default {
7
- install(app, customOptions = {}) {
8
- const {
9
- dropdownOptions: customDropdownOptions,
10
- inputOptions: customInputOptions,
11
- ...otherCustomOptions
12
- } = customOptions;
13
- const {
14
- dropdownOptions: defaultDropdownOptions,
15
- inputOptions: defaultInputOptions,
16
- ...otherDefaultOptions
17
- } = defaultOptions;
18
-
19
- utils.options = {
20
- inputOptions: {
21
- ...defaultInputOptions,
22
- ...customInputOptions,
23
- },
24
- dropdownOptions: {
25
- ...defaultDropdownOptions,
26
- ...customDropdownOptions,
27
- },
28
- ...otherDefaultOptions,
29
- ...otherCustomOptions,
30
- };
31
-
32
- app.component('vue-tel-input', VueTelInput);
33
- },
34
- }