Proyectos de Subversion LeadersLinked - Antes de SPA

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 www 1
const intlTelInputGlobals = {
2
  getInstance: (input) => {
3
    const id = input.getAttribute('data-intl-tel-input-id');
4
    return window.intlTelInputGlobals.instances[id];
5
  },
6
  instances: {},
7
  // using a global like this allows us to mock it in the tests
8
  documentReady: () => document.readyState === 'complete',
9
};
10
 
11
if (typeof window === 'object') window.intlTelInputGlobals = intlTelInputGlobals;
12
 
13
// these vars persist through all instances of the plugin
14
let id = 0;
15
const defaults = {
16
  // whether or not to allow the dropdown
17
  allowDropdown: true,
18
  // if there is just a dial code in the input: remove it on blur
19
  autoHideDialCode: true,
20
  // add a placeholder in the input with an example number for the selected country
21
  autoPlaceholder: 'polite',
22
  // modify the parentClass
23
  customContainer: '',
24
  // modify the auto placeholder
25
  customPlaceholder: null,
26
  // append menu to specified element
27
  dropdownContainer: null,
28
  // don't display these countries
29
  excludeCountries: [],
30
  // format the input value during initialisation and on setNumber
31
  formatOnDisplay: true,
32
  // geoIp lookup function
33
  geoIpLookup: null,
34
  // inject a hidden input with this name, and on submit, populate it with the result of getNumber
35
  hiddenInput: '',
36
  // initial country
37
  initialCountry: '',
38
  // localized country names e.g. { 'de': 'Deutschland' }
39
  localizedCountries: null,
40
  // don't insert international dial codes
41
  nationalMode: true,
42
  // display only these countries
43
  onlyCountries: [],
44
  // number type to use for placeholders
45
  placeholderNumberType: 'MOBILE',
46
  // the countries at the top of the list. defaults to united states and united kingdom
47
  preferredCountries: ['us', 'gb'],
48
  // display the country dial code next to the selected flag so it's not part of the typed number
49
  separateDialCode: false,
50
  // specify the path to the libphonenumber script to enable validation/formatting
51
  utilsScript: '',
52
};
53
// https://en.wikipedia.org/wiki/List_of_North_American_Numbering_Plan_area_codes#Non-geographic_area_codes
54
const regionlessNanpNumbers = ['800', '822', '833', '844', '855', '866', '877', '880', '881', '882', '883', '884', '885', '886', '887', '888', '889'];
55
 
56
 
57
// utility function to iterate over an object. can't use Object.entries or native forEach because
58
// of IE11
59
const forEachProp = (obj, callback) => {
60
  const keys = Object.keys(obj);
61
  for (let i = 0; i < keys.length; i++) {
62
    callback(keys[i], obj[keys[i]]);
63
  }
64
};
65
 
66
 
67
// run a method on each instance of the plugin
68
const forEachInstance = (method) => {
69
  forEachProp(window.intlTelInputGlobals.instances, (key) => {
70
    window.intlTelInputGlobals.instances[key][method]();
71
  });
72
};
73
 
74
 
75
// this is our plugin class that we will create an instance of
76
// eslint-disable-next-line no-unused-vars
77
class Iti {
78
  constructor(input, options) {
79
    this.id = id++;
80
    this.telInput = input;
81
 
82
    this.activeItem = null;
83
    this.highlightedItem = null;
84
 
85
    // process specified options / defaults
86
    // alternative to Object.assign, which isn't supported by IE11
87
    const customOptions = options || {};
88
    this.options = {};
89
    forEachProp(defaults, (key, value) => {
90
      this.options[key] = (customOptions.hasOwnProperty(key)) ? customOptions[key] : value;
91
    });
92
 
93
    this.hadInitialPlaceholder = Boolean(input.getAttribute('placeholder'));
94
  }
95
 
96
  _init() {
97
    // if in nationalMode, disable options relating to dial codes
98
    if (this.options.nationalMode) this.options.autoHideDialCode = false;
99
 
100
    // if separateDialCode then doesn't make sense to A) insert dial code into input
101
    // (autoHideDialCode), and B) display national numbers (because we're displaying the country
102
    // dial code next to them)
103
    if (this.options.separateDialCode) {
104
      this.options.autoHideDialCode = this.options.nationalMode = false;
105
    }
106
 
107
    // we cannot just test screen size as some smartphones/website meta tags will report desktop
108
    // resolutions
109
    // Note: for some reason jasmine breaks if you put this in the main Plugin function with the
110
    // rest of these declarations
111
    // Note: to target Android Mobiles (and not Tablets), we must find 'Android' and 'Mobile'
112
    this.isMobile = /Android.+Mobile|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
113
 
114
    if (this.isMobile) {
115
      // trigger the mobile dropdown css
116
      document.body.classList.add('iti-mobile');
117
 
118
      // on mobile, we want a full screen dropdown, so we must append it to the body
119
      if (!this.options.dropdownContainer) this.options.dropdownContainer = document.body;
120
    }
121
 
122
    // these promises get resolved when their individual requests complete
123
    // this way the dev can do something like iti.promise.then(...) to know when all requests are
124
    // complete
125
    if (typeof Promise !== 'undefined') {
126
      const autoCountryPromise = new Promise((resolve, reject) => {
127
        this.resolveAutoCountryPromise = resolve;
128
        this.rejectAutoCountryPromise = reject;
129
      });
130
      const utilsScriptPromise = new Promise((resolve, reject) => {
131
        this.resolveUtilsScriptPromise = resolve;
132
        this.rejectUtilsScriptPromise = reject;
133
      });
134
      this.promise = Promise.all([autoCountryPromise, utilsScriptPromise]);
135
    } else {
136
      // prevent errors when Promise doesn't exist
137
      this.resolveAutoCountryPromise = this.rejectAutoCountryPromise = () => {};
138
      this.resolveUtilsScriptPromise = this.rejectUtilsScriptPromise = () => {};
139
    }
140
 
141
    // in various situations there could be no country selected initially, but we need to be able
142
    // to assume this variable exists
143
    this.selectedCountryData = {};
144
 
145
    // process all the data: onlyCountries, excludeCountries, preferredCountries etc
146
    this._processCountryData();
147
 
148
    // generate the markup
149
    this._generateMarkup();
150
 
151
    // set the initial state of the input value and the selected flag
152
    this._setInitialState();
153
 
154
    // start all of the event listeners: autoHideDialCode, input keydown, selectedFlag click
155
    this._initListeners();
156
 
157
    // utils script, and auto country
158
    this._initRequests();
159
  }
160
 
161
 
162
  /********************
163
   *  PRIVATE METHODS
164
   ********************/
165
 
166
 
167
  // prepare all of the country data, including onlyCountries, excludeCountries and
168
  // preferredCountries options
169
  _processCountryData() {
170
    // process onlyCountries or excludeCountries array if present
171
    this._processAllCountries();
172
 
173
    // process the countryCodes map
174
    this._processCountryCodes();
175
 
176
    // process the preferredCountries
177
    this._processPreferredCountries();
178
 
179
    // translate countries according to localizedCountries option
180
    if (this.options.localizedCountries) this._translateCountriesByLocale();
181
 
182
    // sort countries by name
183
    if (this.options.onlyCountries.length || this.options.localizedCountries) {
184
      this.countries.sort(this._countryNameSort);
185
    }
186
  }
187
 
188
 
189
  // add a country code to this.countryCodes
190
  _addCountryCode(iso2, countryCode, priority) {
191
    if (countryCode.length > this.countryCodeMaxLen) {
192
      this.countryCodeMaxLen = countryCode.length;
193
    }
194
    if (!this.countryCodes.hasOwnProperty(countryCode)) {
195
      this.countryCodes[countryCode] = [];
196
    }
197
    // bail if we already have this country for this countryCode
198
    for (let i = 0; i < this.countryCodes[countryCode].length; i++) {
199
      if (this.countryCodes[countryCode][i] === iso2) return;
200
    }
201
    // check for undefined as 0 is falsy
202
    const index = (priority !== undefined) ? priority : this.countryCodes[countryCode].length;
203
    this.countryCodes[countryCode][index] = iso2;
204
  }
205
 
206
 
207
  // process onlyCountries or excludeCountries array if present
208
  _processAllCountries() {
209
    if (this.options.onlyCountries.length) {
210
      const lowerCaseOnlyCountries = this.options.onlyCountries.map(
211
        country => country.toLowerCase()
212
      );
213
      this.countries = allCountries.filter(
214
        country => lowerCaseOnlyCountries.indexOf(country.iso2) > -1
215
      );
216
    } else if (this.options.excludeCountries.length) {
217
      const lowerCaseExcludeCountries = this.options.excludeCountries.map(
218
        country => country.toLowerCase()
219
      );
220
      this.countries = allCountries.filter(
221
        country => lowerCaseExcludeCountries.indexOf(country.iso2) === -1
222
      );
223
    } else {
224
      this.countries = allCountries;
225
    }
226
  }
227
 
228
  // Translate Countries by object literal provided on config
229
  _translateCountriesByLocale() {
230
    for (let i = 0; i < this.countries.length; i++) {
231
      const iso = this.countries[i].iso2.toLowerCase();
232
      if (this.options.localizedCountries.hasOwnProperty(iso)) {
233
        this.countries[i].name = this.options.localizedCountries[iso];
234
      }
235
    }
236
  }
237
 
238
  // sort by country name
239
  _countryNameSort(a, b) {
240
    return a.name.localeCompare(b.name);
241
  }
242
 
243
 
244
  // process the countryCodes map
245
  _processCountryCodes() {
246
    this.countryCodeMaxLen = 0;
247
    // here we store just dial codes
248
    this.dialCodes = {};
249
    // here we store "country codes" (both dial codes and their area codes)
250
    this.countryCodes = {};
251
 
252
    // first: add dial codes
253
    for (let i = 0; i < this.countries.length; i++) {
254
      const c = this.countries[i];
255
      if (!this.dialCodes[c.dialCode]) this.dialCodes[c.dialCode] = true;
256
      this._addCountryCode(c.iso2, c.dialCode, c.priority);
257
    }
258
 
259
    // next: add area codes
260
    // this is a second loop over countries, to make sure we have all of the "root" countries
261
    // already in the map, so that we can access them, as each time we add an area code substring
262
    // to the map, we also need to include the "root" country's code, as that also matches
263
    for (let i = 0; i < this.countries.length; i++) {
264
      const c = this.countries[i];
265
      // area codes
266
      if (c.areaCodes) {
267
        const rootCountryCode = this.countryCodes[c.dialCode][0];
268
        // for each area code
269
        for (let j = 0; j < c.areaCodes.length; j++) {
270
          const areaCode = c.areaCodes[j];
271
          // for each digit in the area code to add all partial matches as well
272
          for (let k = 1; k < areaCode.length; k++) {
273
            const partialDialCode = c.dialCode + areaCode.substr(0, k);
274
            // start with the root country, as that also matches this dial code
275
            this._addCountryCode(rootCountryCode, partialDialCode);
276
            this._addCountryCode(c.iso2, partialDialCode);
277
          }
278
          // add the full area code
279
          this._addCountryCode(c.iso2, c.dialCode + areaCode);
280
        }
281
      }
282
    }
283
  }
284
 
285
 
286
  // process preferred countries - iterate through the preferences, fetching the country data for
287
  // each one
288
  _processPreferredCountries() {
289
    this.preferredCountries = [];
290
    for (let i = 0; i < this.options.preferredCountries.length; i++) {
291
      const countryCode = this.options.preferredCountries[i].toLowerCase();
292
      const countryData = this._getCountryData(countryCode, false, true);
293
      if (countryData) this.preferredCountries.push(countryData);
294
    }
295
  }
296
 
297
 
298
  // create a DOM element
299
  _createEl(name, attrs, container) {
300
    const el = document.createElement(name);
301
    if (attrs) forEachProp(attrs, (key, value) => el.setAttribute(key, value));
302
    if (container) container.appendChild(el);
303
    return el;
304
  }
305
 
306
 
307
  // generate all of the markup for the plugin: the selected flag overlay, and the dropdown
308
  _generateMarkup() {
309
    // if autocomplete does not exist on the element and its form, then
310
    // prevent autocomplete as there's no safe, cross-browser event we can react to, so it can
311
    // easily put the plugin in an inconsistent state e.g. the wrong flag selected for the
312
    // autocompleted number, which on submit could mean wrong number is saved (esp in nationalMode)
313
    if (!this.telInput.hasAttribute('autocomplete') && !(this.telInput.form && this.telInput.form.hasAttribute('autocomplete'))) {
314
      this.telInput.setAttribute('autocomplete', 'off');
315
    }
316
 
317
    // containers (mostly for positioning)
318
    let parentClass = 'iti';
319
    if (this.options.allowDropdown) parentClass += ' iti--allow-dropdown';
320
    if (this.options.separateDialCode) parentClass += ' iti--separate-dial-code';
321
    if (this.options.customContainer) {
322
      parentClass += ' ';
323
      parentClass += this.options.customContainer;
324
    }
325
 
326
    const wrapper = this._createEl('div', { class: parentClass });
327
    this.telInput.parentNode.insertBefore(wrapper, this.telInput);
328
    this.flagsContainer = this._createEl('div', { class: 'iti__flag-container' }, wrapper);
329
    wrapper.appendChild(this.telInput);
330
 
331
    // selected flag (displayed to left of input)
332
    this.selectedFlag = this._createEl('div', {
333
      class: 'iti__selected-flag',
334
      role: 'combobox',
335
      'aria-controls': `iti-${this.id}__country-listbox`,
336
      'aria-owns': `iti-${this.id}__country-listbox`,
337
      'aria-expanded': 'false',
338
    }, this.flagsContainer);
339
    this.selectedFlagInner = this._createEl('div', { class: 'iti__flag' }, this.selectedFlag);
340
 
341
    if (this.options.separateDialCode) {
342
      this.selectedDialCode = this._createEl('div', { class: 'iti__selected-dial-code' }, this.selectedFlag);
343
    }
344
 
345
    if (this.options.allowDropdown) {
346
      // make element focusable and tab navigable
347
      this.selectedFlag.setAttribute('tabindex', '0');
348
      this.dropdownArrow = this._createEl('div', { class: 'iti__arrow' }, this.selectedFlag);
349
 
350
      // country dropdown: preferred countries, then divider, then all countries
351
      this.countryList = this._createEl('ul', {
352
        class: 'iti__country-list iti__hide',
353
        id: `iti-${this.id}__country-listbox`,
354
        role: 'listbox',
355
        'aria-label': 'List of countries',
356
      });
357
      if (this.preferredCountries.length) {
358
        this._appendListItems(this.preferredCountries, 'iti__preferred', true);
359
        this._createEl('li', {
360
          class: 'iti__divider',
361
          role: 'separator',
362
          'aria-disabled': 'true',
363
        }, this.countryList);
364
      }
365
      this._appendListItems(this.countries, 'iti__standard');
366
 
367
      // create dropdownContainer markup
368
      if (this.options.dropdownContainer) {
369
        this.dropdown = this._createEl('div', { class: 'iti iti--container' });
370
        this.dropdown.appendChild(this.countryList);
371
      } else {
372
        this.flagsContainer.appendChild(this.countryList);
373
      }
374
    }
375
 
376
    if (this.options.hiddenInput) {
377
      let hiddenInputName = this.options.hiddenInput;
378
      const name = this.telInput.getAttribute('name');
379
      if (name) {
380
        const i = name.lastIndexOf('[');
381
        // if input name contains square brackets, then give the hidden input the same name,
382
        // replacing the contents of the last set of brackets with the given hiddenInput name
383
        if (i !== -1) hiddenInputName = `${name.substr(0, i)}[${hiddenInputName}]`;
384
      }
385
      this.hiddenInput = this._createEl('input', {
386
        type: 'hidden',
387
        name: hiddenInputName,
388
      });
389
      wrapper.appendChild(this.hiddenInput);
390
    }
391
  }
392
 
393
 
394
  // add a country <li> to the countryList <ul> container
395
  _appendListItems(countries, className, preferred) {
396
    // we create so many DOM elements, it is faster to build a temp string
397
    // and then add everything to the DOM in one go at the end
398
    let tmp = '';
399
    // for each country
400
    for (let i = 0; i < countries.length; i++) {
401
      const c = countries[i];
402
      const idSuffix = preferred ? '-preferred' : '';
403
      // open the list item
404
      tmp += `<li class='iti__country ${className}' tabIndex='-1' id='iti-${this.id}__item-${c.iso2}${idSuffix}' role='option' data-dial-code='${c.dialCode}' data-country-code='${c.iso2}' aria-selected='false'>`;
405
      // add the flag
406
      tmp += `<div class='iti__flag-box'><div class='iti__flag iti__${c.iso2}'></div></div>`;
407
      // and the country name and dial code
408
      tmp += `<span class='iti__country-name'>${c.name}</span>`;
409
      tmp += `<span class='iti__dial-code'>+${c.dialCode}</span>`;
410
      // close the list item
411
      tmp += '</li>';
412
    }
413
    this.countryList.insertAdjacentHTML('beforeend', tmp);
414
  }
415
 
416
 
417
  // set the initial state of the input value and the selected flag by:
418
  // 1. extracting a dial code from the given number
419
  // 2. using explicit initialCountry
420
  // 3. picking the first preferred country
421
  // 4. picking the first country
422
  _setInitialState() {
423
    const val = this.telInput.value;
424
    const dialCode = this._getDialCode(val);
425
    const isRegionlessNanp = this._isRegionlessNanp(val);
426
    const {
427
      initialCountry,
428
      nationalMode,
429
      autoHideDialCode,
430
      separateDialCode,
431
    } = this.options;
432
 
433
    // if we already have a dial code, and it's not a regionlessNanp, we can go ahead and set the
434
    // flag, else fall back to the default country
435
    if (dialCode && !isRegionlessNanp) {
436
      this._updateFlagFromNumber(val);
437
    } else if (initialCountry !== 'auto') {
438
      // see if we should select a flag
439
      if (initialCountry) {
440
        this._setFlag(initialCountry.toLowerCase());
441
      } else {
442
        if (dialCode && isRegionlessNanp) {
443
          // has intl dial code, is regionless nanp, and no initialCountry, so default to US
444
          this._setFlag('us');
445
        } else {
446
          // no dial code and no initialCountry, so default to first in list
447
          this.defaultCountry = (this.preferredCountries.length) ? this.preferredCountries[0].iso2
448
            : this.countries[0].iso2;
449
          if (!val) {
450
            this._setFlag(this.defaultCountry);
451
          }
452
        }
453
      }
454
 
455
      // if empty and no nationalMode and no autoHideDialCode then insert the default dial code
456
      if (!val && !nationalMode && !autoHideDialCode && !separateDialCode) {
457
        this.telInput.value = `+${this.selectedCountryData.dialCode}`;
458
      }
459
    }
460
    // NOTE: if initialCountry is set to auto, that will be handled separately
461
 
462
    // format - note this wont be run after _updateDialCode as that's only called if no val
463
    if (val) this._updateValFromNumber(val);
464
  }
465
 
466
 
467
  // initialise the main event listeners: input keyup, and click selected flag
468
  _initListeners() {
469
    this._initKeyListeners();
470
    if (this.options.autoHideDialCode) this._initBlurListeners();
471
    if (this.options.allowDropdown) this._initDropdownListeners();
472
    if (this.hiddenInput) this._initHiddenInputListener();
473
  }
474
 
475
 
476
  // update hidden input on form submit
477
  _initHiddenInputListener() {
478
    this._handleHiddenInputSubmit = () => {
479
      this.hiddenInput.value = this.getNumber();
480
    };
481
    if (this.telInput.form) this.telInput.form.addEventListener('submit', this._handleHiddenInputSubmit);
482
  }
483
 
484
 
485
  // iterate through parent nodes to find the closest label ancestor, if it exists
486
  _getClosestLabel() {
487
    let el = this.telInput;
488
    while (el && el.tagName !== 'LABEL') el = el.parentNode;
489
    return el;
490
  }
491
 
492
 
493
  // initialise the dropdown listeners
494
  _initDropdownListeners() {
495
    // hack for input nested inside label (which is valid markup): clicking the selected-flag to
496
    // open the dropdown would then automatically trigger a 2nd click on the input which would
497
    // close it again
498
    this._handleLabelClick = (e) => {
499
      // if the dropdown is closed, then focus the input, else ignore the click
500
      if (this.countryList.classList.contains('iti__hide')) this.telInput.focus();
501
      else e.preventDefault();
502
    };
503
    const label = this._getClosestLabel();
504
    if (label) label.addEventListener('click', this._handleLabelClick);
505
 
506
    // toggle country dropdown on click
507
    this._handleClickSelectedFlag = () => {
508
      // only intercept this event if we're opening the dropdown
509
      // else let it bubble up to the top ("click-off-to-close" listener)
510
      // we cannot just stopPropagation as it may be needed to close another instance
511
      if (this.countryList.classList.contains('iti__hide') && !this.telInput.disabled && !this.telInput.readOnly) {
512
        this._showDropdown();
513
      }
514
    };
515
    this.selectedFlag.addEventListener('click', this._handleClickSelectedFlag);
516
 
517
    // open dropdown list if currently focused
518
    this._handleFlagsContainerKeydown = (e) => {
519
      const isDropdownHidden = this.countryList.classList.contains('iti__hide');
520
 
521
      if (isDropdownHidden && ['ArrowUp', 'Up', 'ArrowDown', 'Down', ' ', 'Enter'].indexOf(e.key) !== -1) {
522
        // prevent form from being submitted if "ENTER" was pressed
523
        e.preventDefault();
524
        // prevent event from being handled again by document
525
        e.stopPropagation();
526
        this._showDropdown();
527
      }
528
 
529
      // allow navigation from dropdown to input on TAB
530
      if (e.key === 'Tab') this._closeDropdown();
531
    };
532
    this.flagsContainer.addEventListener('keydown', this._handleFlagsContainerKeydown);
533
  }
534
 
535
 
536
  // init many requests: utils script / geo ip lookup
537
  _initRequests() {
538
    // if the user has specified the path to the utils script, fetch it on window.load, else resolve
539
    if (this.options.utilsScript && !window.intlTelInputUtils) {
540
      // if the plugin is being initialised after the window.load event has already been fired
541
      if (window.intlTelInputGlobals.documentReady()) {
542
        window.intlTelInputGlobals.loadUtils(this.options.utilsScript);
543
      } else {
544
        // wait until the load event so we don't block any other requests e.g. the flags image
545
        window.addEventListener('load', () => {
546
          window.intlTelInputGlobals.loadUtils(this.options.utilsScript);
547
        });
548
      }
549
    } else this.resolveUtilsScriptPromise();
550
 
551
    if (this.options.initialCountry === 'auto') this._loadAutoCountry();
552
    else this.resolveAutoCountryPromise();
553
  }
554
 
555
 
556
  // perform the geo ip lookup
557
  _loadAutoCountry() {
558
    // 3 options:
559
    // 1) already loaded (we're done)
560
    // 2) not already started loading (start)
561
    // 3) already started loading (do nothing - just wait for loading callback to fire)
562
    if (window.intlTelInputGlobals.autoCountry) {
563
      this.handleAutoCountry();
564
    } else if (!window.intlTelInputGlobals.startedLoadingAutoCountry) {
565
      // don't do this twice!
566
      window.intlTelInputGlobals.startedLoadingAutoCountry = true;
567
 
568
      if (typeof this.options.geoIpLookup === 'function') {
569
        this.options.geoIpLookup((countryCode) => {
570
          window.intlTelInputGlobals.autoCountry = countryCode.toLowerCase();
571
          // tell all instances the auto country is ready
572
          // TODO: this should just be the current instances
573
          // UPDATE: use setTimeout in case their geoIpLookup function calls this callback straight
574
          // away (e.g. if they have already done the geo ip lookup somewhere else). Using
575
          // setTimeout means that the current thread of execution will finish before executing
576
          // this, which allows the plugin to finish initialising.
577
          setTimeout(() => forEachInstance('handleAutoCountry'));
578
        }, () => forEachInstance('rejectAutoCountryPromise'));
579
      }
580
    }
581
  }
582
 
583
 
584
  // initialize any key listeners
585
  _initKeyListeners() {
586
    // update flag on keyup
587
    this._handleKeyupEvent = () => {
588
      if (this._updateFlagFromNumber(this.telInput.value)) {
589
        this._triggerCountryChange();
590
      }
591
    };
592
    this.telInput.addEventListener('keyup', this._handleKeyupEvent);
593
 
594
    // update flag on cut/paste events (now supported in all major browsers)
595
    this._handleClipboardEvent = () => {
596
      // hack because "paste" event is fired before input is updated
597
      setTimeout(this._handleKeyupEvent);
598
    };
599
    this.telInput.addEventListener('cut', this._handleClipboardEvent);
600
    this.telInput.addEventListener('paste', this._handleClipboardEvent);
601
  }
602
 
603
 
604
  // adhere to the input's maxlength attr
605
  _cap(number) {
606
    const max = this.telInput.getAttribute('maxlength');
607
    return (max && number.length > max) ? number.substr(0, max) : number;
608
  }
609
 
610
 
611
  // listen for blur/submit (for autoHideDialCode feature)
612
  _initBlurListeners() {
613
    // on blur or form submit: if just a dial code then remove it
614
    this._handleSubmitOrBlurEvent = () => {
615
      this._removeEmptyDialCode();
616
    };
617
    if (this.telInput.form) this.telInput.form.addEventListener('submit', this._handleSubmitOrBlurEvent);
618
    this.telInput.addEventListener('blur', this._handleSubmitOrBlurEvent);
619
 
620
    // made the decision not to trigger blur() now, because would only do anything in the case
621
    // where they manually set the initial value to just a dial code, in which case they probably
622
    // want it to be displayed.
623
  }
624
 
625
 
626
  // clear the input if it just contains a dial code
627
  _removeEmptyDialCode() {
628
    if (this.telInput.value.charAt(0) === '+') {
629
      const numeric = this._getNumeric(this.telInput.value);
630
      // if just a plus, or if just a dial code
631
      if (!numeric || this.selectedCountryData.dialCode === numeric) {
632
        this.telInput.value = '';
633
      }
634
    }
635
  }
636
 
637
 
638
  // extract the numeric digits from the given string
639
  _getNumeric(s) {
640
    return s.replace(/\D/g, '');
641
  }
642
 
643
 
644
  // trigger a custom event on the input
645
  _trigger(name) {
646
    // have to use old school document.createEvent as IE11 doesn't support `new Event()` syntax
647
    const e = document.createEvent('Event');
648
    e.initEvent(name, true, true); // can bubble, and is cancellable
649
    this.telInput.dispatchEvent(e);
650
  }
651
 
652
 
653
  // show the dropdown
654
  _showDropdown() {
655
    this.countryList.classList.remove('iti__hide');
656
    this.selectedFlag.setAttribute('aria-expanded', 'true');
657
 
658
    this._setDropdownPosition();
659
 
660
    // update highlighting and scroll to active list item
661
    if (this.activeItem) {
662
      this._highlightListItem(this.activeItem, false);
663
      this._scrollTo(this.activeItem, true);
664
    }
665
 
666
    // bind all the dropdown-related listeners: mouseover, click, click-off, keydown
667
    this._bindDropdownListeners();
668
 
669
    // update the arrow
670
    this.dropdownArrow.classList.add('iti__arrow--up');
671
 
672
    this._trigger('open:countrydropdown');
673
  }
674
 
675
 
676
  // make sure the el has the className or not, depending on the value of shouldHaveClass
677
  _toggleClass(el, className, shouldHaveClass) {
678
    if (shouldHaveClass && !el.classList.contains(className)) el.classList.add(className);
679
    else if (!shouldHaveClass && el.classList.contains(className)) el.classList.remove(className);
680
  }
681
 
682
 
683
  // decide where to position dropdown (depends on position within viewport, and scroll)
684
  _setDropdownPosition() {
685
    if (this.options.dropdownContainer) {
686
      this.options.dropdownContainer.appendChild(this.dropdown);
687
    }
688
 
689
    if (!this.isMobile) {
690
      const pos = this.telInput.getBoundingClientRect();
691
      // windowTop from https://stackoverflow.com/a/14384091/217866
692
      const windowTop = window.pageYOffset || document.documentElement.scrollTop;
693
      const inputTop = pos.top + windowTop;
694
      const dropdownHeight = this.countryList.offsetHeight;
695
      // dropdownFitsBelow = (dropdownBottom < windowBottom)
696
      const dropdownFitsBelow = (inputTop + this.telInput.offsetHeight + dropdownHeight
697
        < (windowTop + window.innerHeight));
698
      const dropdownFitsAbove = (inputTop - dropdownHeight > windowTop);
699
 
700
      // by default, the dropdown will be below the input. If we want to position it above the
701
      // input, we add the dropup class.
702
      this._toggleClass(this.countryList, 'iti__country-list--dropup', (!dropdownFitsBelow && dropdownFitsAbove));
703
 
704
      // if dropdownContainer is enabled, calculate postion
705
      if (this.options.dropdownContainer) {
706
        // by default the dropdown will be directly over the input because it's not in the flow.
707
        // If we want to position it below, we need to add some extra top value.
708
        const extraTop = (!dropdownFitsBelow && dropdownFitsAbove) ? 0 : this.telInput.offsetHeight;
709
 
710
        // calculate placement
711
        this.dropdown.style.top = `${inputTop + extraTop}px`;
712
        this.dropdown.style.left = `${pos.left + document.body.scrollLeft}px`;
713
 
714
        // close menu on window scroll
715
        this._handleWindowScroll = () => this._closeDropdown();
716
        window.addEventListener('scroll', this._handleWindowScroll);
717
      }
718
    }
719
  }
720
 
721
 
722
  // iterate through parent nodes to find the closest list item
723
  _getClosestListItem(target) {
724
    let el = target;
725
    while (el && el !== this.countryList && !el.classList.contains('iti__country')) el = el.parentNode;
726
    // if we reached the countryList element, then return null
727
    return (el === this.countryList) ? null : el;
728
  }
729
 
730
 
731
  // we only bind dropdown listeners when the dropdown is open
732
  _bindDropdownListeners() {
733
    // when mouse over a list item, just highlight that one
734
    // we add the class "highlight", so if they hit "enter" we know which one to select
735
    this._handleMouseoverCountryList = (e) => {
736
      // handle event delegation, as we're listening for this event on the countryList
737
      const listItem = this._getClosestListItem(e.target);
738
      if (listItem) this._highlightListItem(listItem, false);
739
    };
740
    this.countryList.addEventListener('mouseover', this._handleMouseoverCountryList);
741
 
742
    // listen for country selection
743
    this._handleClickCountryList = (e) => {
744
      const listItem = this._getClosestListItem(e.target);
745
      if (listItem) this._selectListItem(listItem);
746
    };
747
    this.countryList.addEventListener('click', this._handleClickCountryList);
748
 
749
    // click off to close
750
    // (except when this initial opening click is bubbling up)
751
    // we cannot just stopPropagation as it may be needed to close another instance
752
    let isOpening = true;
753
    this._handleClickOffToClose = () => {
754
      if (!isOpening) this._closeDropdown();
755
      isOpening = false;
756
    };
757
    document.documentElement.addEventListener('click', this._handleClickOffToClose);
758
 
759
    // listen for up/down scrolling, enter to select, or letters to jump to country name.
760
    // use keydown as keypress doesn't fire for non-char keys and we want to catch if they
761
    // just hit down and hold it to scroll down (no keyup event).
762
    // listen on the document because that's where key events are triggered if no input has focus
763
    let query = '';
764
    let queryTimer = null;
765
    this._handleKeydownOnDropdown = (e) => {
766
      // prevent down key from scrolling the whole page,
767
      // and enter key from submitting a form etc
768
      e.preventDefault();
769
 
770
      // up and down to navigate
771
      if (e.key === 'ArrowUp' || e.key === 'Up' || e.key === 'ArrowDown' || e.key === 'Down') this._handleUpDownKey(e.key);
772
      // enter to select
773
      else if (e.key === 'Enter') this._handleEnterKey();
774
      // esc to close
775
      else if (e.key === 'Escape') this._closeDropdown();
776
      // alpha chars to perform search
777
      // regex allows one latin alpha char or space, based on https://stackoverflow.com/a/26900132/217866)
778
      else if (/^[a-zA-ZÀ-ÿа-яА-Я ]$/.test(e.key)) {
779
        // jump to countries that start with the query string
780
        if (queryTimer) clearTimeout(queryTimer);
781
        query += e.key.toLowerCase();
782
        this._searchForCountry(query);
783
        // if the timer hits 1 second, reset the query
784
        queryTimer = setTimeout(() => {
785
          query = '';
786
        }, 1000);
787
      }
788
    };
789
    document.addEventListener('keydown', this._handleKeydownOnDropdown);
790
  }
791
 
792
 
793
  // highlight the next/prev item in the list (and ensure it is visible)
794
  _handleUpDownKey(key) {
795
    let next = (key === 'ArrowUp' || key === 'Up') ? this.highlightedItem.previousElementSibling : this.highlightedItem.nextElementSibling;
796
    if (next) {
797
      // skip the divider
798
      if (next.classList.contains('iti__divider')) {
799
        next = (key === 'ArrowUp' || key === 'Up') ? next.previousElementSibling : next.nextElementSibling;
800
      }
801
      this._highlightListItem(next, true);
802
    }
803
  }
804
 
805
 
806
  // select the currently highlighted item
807
  _handleEnterKey() {
808
    if (this.highlightedItem) this._selectListItem(this.highlightedItem);
809
  }
810
 
811
 
812
  // find the first list item whose name starts with the query string
813
  _searchForCountry(query) {
814
    for (let i = 0; i < this.countries.length; i++) {
815
      if (this._startsWith(this.countries[i].name, query)) {
816
        const listItem = this.countryList.querySelector(`#iti-${this.id}__item-${this.countries[i].iso2}`);
817
        // update highlighting and scroll
818
        this._highlightListItem(listItem, false);
819
        this._scrollTo(listItem, true);
820
        break;
821
      }
822
    }
823
  }
824
 
825
 
826
  // check if string a starts with string b
827
  _startsWith(a, b) {
828
    return (a.substr(0, b.length).toLowerCase() === b);
829
  }
830
 
831
 
832
  // update the input's value to the given val (format first if possible)
833
  // NOTE: this is called from _setInitialState, handleUtils and setNumber
834
  _updateValFromNumber(originalNumber) {
835
    let number = originalNumber;
836
    if (this.options.formatOnDisplay && window.intlTelInputUtils && this.selectedCountryData) {
837
      const useNational = (!this.options.separateDialCode
838
        && (this.options.nationalMode || number.charAt(0) !== '+'));
839
      const { NATIONAL, INTERNATIONAL } = intlTelInputUtils.numberFormat;
840
      const format = useNational ? NATIONAL : INTERNATIONAL;
841
      number = intlTelInputUtils.formatNumber(number, this.selectedCountryData.iso2, format);
842
    }
843
 
844
    number = this._beforeSetNumber(number);
845
    this.telInput.value = number;
846
  }
847
 
848
 
849
  // check if need to select a new flag based on the given number
850
  // Note: called from _setInitialState, keyup handler, setNumber
851
  _updateFlagFromNumber(originalNumber) {
852
    // if we're in nationalMode and we already have US/Canada selected, make sure the number starts
853
    // with a +1 so _getDialCode will be able to extract the area code
854
    // update: if we dont yet have selectedCountryData, but we're here (trying to update the flag
855
    // from the number), that means we're initialising the plugin with a number that already has a
856
    // dial code, so fine to ignore this bit
857
    let number = originalNumber;
858
    const selectedDialCode = this.selectedCountryData.dialCode;
859
    const isNanp = selectedDialCode === '1';
860
    if (number && this.options.nationalMode && isNanp && number.charAt(0) !== '+') {
861
      if (number.charAt(0) !== '1') number = `1${number}`;
862
      number = `+${number}`;
863
    }
864
 
865
    // update flag if user types area code for another country
866
    if (this.options.separateDialCode && selectedDialCode && number.charAt(0) !== '+') {
867
      number = `+${selectedDialCode}${number}`;
868
    }
869
 
870
    // try and extract valid dial code from input
871
    const dialCode = this._getDialCode(number, true);
872
    const numeric = this._getNumeric(number);
873
    let countryCode = null;
874
    if (dialCode) {
875
      const countryCodes = this.countryCodes[this._getNumeric(dialCode)];
876
      // check if the right country is already selected. this should be false if the number is
877
      // longer than the matched dial code because in this case we need to make sure that if
878
      // there are multiple country matches, that the first one is selected (note: we could
879
      // just check that here, but it requires the same loop that we already have later)
880
      const alreadySelected = (countryCodes.indexOf(this.selectedCountryData.iso2) !== -1)
881
        && (numeric.length <= dialCode.length - 1);
882
      const isRegionlessNanpNumber = (selectedDialCode === '1' && this._isRegionlessNanp(numeric));
883
 
884
      // only update the flag if:
885
      // A) NOT (we currently have a NANP flag selected, and the number is a regionlessNanp)
886
      // AND
887
      // B) the right country is not already selected
888
      if (!isRegionlessNanpNumber && !alreadySelected) {
889
        // if using onlyCountries option, countryCodes[0] may be empty, so we must find the first
890
        // non-empty index
891
        for (let j = 0; j < countryCodes.length; j++) {
892
          if (countryCodes[j]) {
893
            countryCode = countryCodes[j];
894
            break;
895
          }
896
        }
897
      }
898
    } else if (number.charAt(0) === '+' && numeric.length) {
899
      // invalid dial code, so empty
900
      // Note: use getNumeric here because the number has not been formatted yet, so could contain
901
      // bad chars
902
      countryCode = '';
903
    } else if (!number || number === '+') {
904
      // empty, or just a plus, so default
905
      countryCode = this.defaultCountry;
906
    }
907
 
908
    if (countryCode !== null) {
909
      return this._setFlag(countryCode);
910
    }
911
    return false;
912
  }
913
 
914
 
915
  // check if the given number is a regionless NANP number (expects the number to contain an
916
  // international dial code)
917
  _isRegionlessNanp(number) {
918
    const numeric = this._getNumeric(number);
919
    if (numeric.charAt(0) === '1') {
920
      const areaCode = numeric.substr(1, 3);
921
      return (regionlessNanpNumbers.indexOf(areaCode) !== -1);
922
    }
923
    return false;
924
  }
925
 
926
 
927
  // remove highlighting from other list items and highlight the given item
928
  _highlightListItem(listItem, shouldFocus) {
929
    const prevItem = this.highlightedItem;
930
    if (prevItem) prevItem.classList.remove('iti__highlight');
931
    this.highlightedItem = listItem;
932
    this.highlightedItem.classList.add('iti__highlight');
933
 
934
    if (shouldFocus) this.highlightedItem.focus();
935
  }
936
 
937
 
938
  // find the country data for the given country code
939
  // the ignoreOnlyCountriesOption is only used during init() while parsing the onlyCountries array
940
  _getCountryData(countryCode, ignoreOnlyCountriesOption, allowFail) {
941
    const countryList = (ignoreOnlyCountriesOption) ? allCountries : this.countries;
942
    for (let i = 0; i < countryList.length; i++) {
943
      if (countryList[i].iso2 === countryCode) {
944
        return countryList[i];
945
      }
946
    }
947
    if (allowFail) {
948
      return null;
949
    }
950
    throw new Error(`No country data for '${countryCode}'`);
951
  }
952
 
953
 
954
  // select the given flag, update the placeholder and the active list item
955
  // Note: called from _setInitialState, _updateFlagFromNumber, _selectListItem, setCountry
956
  _setFlag(countryCode) {
957
    const prevCountry = (this.selectedCountryData.iso2) ? this.selectedCountryData : {};
958
 
959
    // do this first as it will throw an error and stop if countryCode is invalid
960
    this.selectedCountryData = (countryCode) ? this._getCountryData(countryCode, false, false) : {};
961
    // update the defaultCountry - we only need the iso2 from now on, so just store that
962
    if (this.selectedCountryData.iso2) {
963
      this.defaultCountry = this.selectedCountryData.iso2;
964
    }
965
 
966
    this.selectedFlagInner.setAttribute('class', `iti__flag iti__${countryCode}`);
967
    // update the selected country's title attribute
968
    const title = (countryCode) ? `${this.selectedCountryData.name}: +${this.selectedCountryData.dialCode}` : 'Unknown';
969
    this.selectedFlag.setAttribute('title', title);
970
 
971
    if (this.options.separateDialCode) {
972
      const dialCode = (this.selectedCountryData.dialCode) ? `+${this.selectedCountryData.dialCode}` : '';
973
      this.selectedDialCode.innerHTML = dialCode;
974
      // offsetWidth is zero if input is in a hidden container during initialisation
975
      const selectedFlagWidth = this.selectedFlag.offsetWidth || this._getHiddenSelectedFlagWidth();
976
 
977
      // add 6px of padding after the grey selected-dial-code box, as this is what we use in the css
978
      this.telInput.style.paddingLeft = `${selectedFlagWidth + 6}px`;
979
    }
980
 
981
    // and the input's placeholder
982
    this._updatePlaceholder();
983
 
984
    // update the active list item
985
    if (this.options.allowDropdown) {
986
      const prevItem = this.activeItem;
987
      if (prevItem) {
988
        prevItem.classList.remove('iti__active');
989
        prevItem.setAttribute('aria-selected', 'false');
990
      }
991
      if (countryCode) {
992
        // check if there is a preferred item first, else fall back to standard
993
        const nextItem = this.countryList.querySelector(`#iti-${this.id}__item-${countryCode}-preferred`) || this.countryList.querySelector(`#iti-${this.id}__item-${countryCode}`);
994
        nextItem.setAttribute('aria-selected', 'true');
995
        nextItem.classList.add('iti__active');
996
        this.activeItem = nextItem;
997
        this.selectedFlag.setAttribute('aria-activedescendant', nextItem.getAttribute('id'));
998
      }
999
    }
1000
 
1001
    // return if the flag has changed or not
1002
    return (prevCountry.iso2 !== countryCode);
1003
  }
1004
 
1005
 
1006
  // when the input is in a hidden container during initialisation, we must inject some markup
1007
  // into the end of the DOM to calculate the correct offsetWidth
1008
  _getHiddenSelectedFlagWidth() {
1009
    // to get the right styling to apply, all we need is a shallow clone of the container,
1010
    // and then to inject a deep clone of the selectedFlag element
1011
    const containerClone = this.telInput.parentNode.cloneNode();
1012
    containerClone.style.visibility = 'hidden';
1013
    document.body.appendChild(containerClone);
1014
 
1015
    const flagsContainerClone = this.flagsContainer.cloneNode();
1016
    containerClone.appendChild(flagsContainerClone);
1017
 
1018
    const selectedFlagClone = this.selectedFlag.cloneNode(true);
1019
    flagsContainerClone.appendChild(selectedFlagClone);
1020
 
1021
    const width = selectedFlagClone.offsetWidth;
1022
    containerClone.parentNode.removeChild(containerClone);
1023
    return width;
1024
  }
1025
 
1026
 
1027
  // update the input placeholder to an example number from the currently selected country
1028
  _updatePlaceholder() {
1029
    const shouldSetPlaceholder = (this.options.autoPlaceholder === 'aggressive') || (!this.hadInitialPlaceholder && this.options.autoPlaceholder === 'polite');
1030
    if (window.intlTelInputUtils && shouldSetPlaceholder) {
1031
      const numberType = intlTelInputUtils.numberType[this.options.placeholderNumberType];
1032
      let placeholder = (this.selectedCountryData.iso2) ? intlTelInputUtils.getExampleNumber(this.selectedCountryData.iso2, this.options.nationalMode, numberType) : '';
1033
 
1034
      placeholder = this._beforeSetNumber(placeholder);
1035
      if (typeof this.options.customPlaceholder === 'function') {
1036
        placeholder = this.options.customPlaceholder(placeholder, this.selectedCountryData);
1037
      }
1038
      this.telInput.setAttribute('placeholder', placeholder);
1039
    }
1040
  }
1041
 
1042
 
1043
  // called when the user selects a list item from the dropdown
1044
  _selectListItem(listItem) {
1045
    // update selected flag and active list item
1046
    const flagChanged = this._setFlag(listItem.getAttribute('data-country-code'));
1047
    this._closeDropdown();
1048
 
1049
    this._updateDialCode(listItem.getAttribute('data-dial-code'), true);
1050
 
1051
    // focus the input
1052
    this.telInput.focus();
1053
    // put cursor at end - this fix is required for FF and IE11 (with nationalMode=false i.e. auto
1054
    // inserting dial code), who try to put the cursor at the beginning the first time
1055
    const len = this.telInput.value.length;
1056
    this.telInput.setSelectionRange(len, len);
1057
 
1058
    if (flagChanged) {
1059
      this._triggerCountryChange();
1060
    }
1061
  }
1062
 
1063
 
1064
  // close the dropdown and unbind any listeners
1065
  _closeDropdown() {
1066
    this.countryList.classList.add('iti__hide');
1067
    this.selectedFlag.setAttribute('aria-expanded', 'false');
1068
    // update the arrow
1069
    this.dropdownArrow.classList.remove('iti__arrow--up');
1070
 
1071
    // unbind key events
1072
    document.removeEventListener('keydown', this._handleKeydownOnDropdown);
1073
    document.documentElement.removeEventListener('click', this._handleClickOffToClose);
1074
    this.countryList.removeEventListener('mouseover', this._handleMouseoverCountryList);
1075
    this.countryList.removeEventListener('click', this._handleClickCountryList);
1076
 
1077
    // remove menu from container
1078
    if (this.options.dropdownContainer) {
1079
      if (!this.isMobile) window.removeEventListener('scroll', this._handleWindowScroll);
1080
      if (this.dropdown.parentNode) this.dropdown.parentNode.removeChild(this.dropdown);
1081
    }
1082
 
1083
    this._trigger('close:countrydropdown');
1084
  }
1085
 
1086
 
1087
  // check if an element is visible within it's container, else scroll until it is
1088
  _scrollTo(element, middle) {
1089
    const container = this.countryList;
1090
    // windowTop from https://stackoverflow.com/a/14384091/217866
1091
    const windowTop = window.pageYOffset || document.documentElement.scrollTop;
1092
    const containerHeight = container.offsetHeight;
1093
    const containerTop = container.getBoundingClientRect().top + windowTop;
1094
    const containerBottom = containerTop + containerHeight;
1095
    const elementHeight = element.offsetHeight;
1096
    const elementTop = element.getBoundingClientRect().top + windowTop;
1097
    const elementBottom = elementTop + elementHeight;
1098
    let newScrollTop = elementTop - containerTop + container.scrollTop;
1099
    const middleOffset = (containerHeight / 2) - (elementHeight / 2);
1100
 
1101
    if (elementTop < containerTop) {
1102
      // scroll up
1103
      if (middle) newScrollTop -= middleOffset;
1104
      container.scrollTop = newScrollTop;
1105
    } else if (elementBottom > containerBottom) {
1106
      // scroll down
1107
      if (middle) newScrollTop += middleOffset;
1108
      const heightDifference = containerHeight - elementHeight;
1109
      container.scrollTop = newScrollTop - heightDifference;
1110
    }
1111
  }
1112
 
1113
 
1114
  // replace any existing dial code with the new one
1115
  // Note: called from _selectListItem and setCountry
1116
  _updateDialCode(newDialCodeBare, hasSelectedListItem) {
1117
    const inputVal = this.telInput.value;
1118
    // save having to pass this every time
1119
    const newDialCode = `+${newDialCodeBare}`;
1120
 
1121
    let newNumber;
1122
    if (inputVal.charAt(0) === '+') {
1123
      // there's a plus so we're dealing with a replacement (doesn't matter if nationalMode or not)
1124
      const prevDialCode = this._getDialCode(inputVal);
1125
      if (prevDialCode) {
1126
        // current number contains a valid dial code, so replace it
1127
        newNumber = inputVal.replace(prevDialCode, newDialCode);
1128
      } else {
1129
        // current number contains an invalid dial code, so ditch it
1130
        // (no way to determine where the invalid dial code ends and the rest of the number begins)
1131
        newNumber = newDialCode;
1132
      }
1133
    } else if (this.options.nationalMode || this.options.separateDialCode) {
1134
      // don't do anything
1135
      return;
1136
    } else {
1137
      // nationalMode is disabled
1138
      if (inputVal) {
1139
        // there is an existing value with no dial code: prefix the new dial code
1140
        newNumber = newDialCode + inputVal;
1141
      } else if (hasSelectedListItem || !this.options.autoHideDialCode) {
1142
        // no existing value and either they've just selected a list item, or autoHideDialCode is
1143
        // disabled: insert new dial code
1144
        newNumber = newDialCode;
1145
      } else {
1146
        return;
1147
      }
1148
    }
1149
 
1150
    this.telInput.value = newNumber;
1151
  }
1152
 
1153
 
1154
  // try and extract a valid international dial code from a full telephone number
1155
  // Note: returns the raw string inc plus character and any whitespace/dots etc
1156
  _getDialCode(number, includeAreaCode) {
1157
    let dialCode = '';
1158
    // only interested in international numbers (starting with a plus)
1159
    if (number.charAt(0) === '+') {
1160
      let numericChars = '';
1161
      // iterate over chars
1162
      for (let i = 0; i < number.length; i++) {
1163
        const c = number.charAt(i);
1164
        // if char is number (https://stackoverflow.com/a/8935649/217866)
1165
        if (!isNaN(parseInt(c, 10))) {
1166
          numericChars += c;
1167
          // if current numericChars make a valid dial code
1168
          if (includeAreaCode) {
1169
            if (this.countryCodes[numericChars]) {
1170
              // store the actual raw string (useful for matching later)
1171
              dialCode = number.substr(0, i + 1);
1172
            }
1173
          } else {
1174
            if (this.dialCodes[numericChars]) {
1175
              dialCode = number.substr(0, i + 1);
1176
              // if we're just looking for a dial code, we can break as soon as we find one
1177
              break;
1178
            }
1179
          }
1180
          // stop searching as soon as we can - in this case when we hit max len
1181
          if (numericChars.length === this.countryCodeMaxLen) {
1182
            break;
1183
          }
1184
        }
1185
      }
1186
    }
1187
    return dialCode;
1188
  }
1189
 
1190
 
1191
  // get the input val, adding the dial code if separateDialCode is enabled
1192
  _getFullNumber() {
1193
    const val = this.telInput.value.trim();
1194
    const { dialCode } = this.selectedCountryData;
1195
    let prefix;
1196
    const numericVal = this._getNumeric(val);
1197
 
1198
    if (this.options.separateDialCode && val.charAt(0) !== '+' && dialCode && numericVal) {
1199
      // when using separateDialCode, it is visible so is effectively part of the typed number
1200
      prefix = `+${dialCode}`;
1201
    } else {
1202
      prefix = '';
1203
    }
1204
    return prefix + val;
1205
  }
1206
 
1207
 
1208
  // remove the dial code if separateDialCode is enabled
1209
  // also cap the length if the input has a maxlength attribute
1210
  _beforeSetNumber(originalNumber) {
1211
    let number = originalNumber;
1212
    if (this.options.separateDialCode) {
1213
      let dialCode = this._getDialCode(number);
1214
      // if there is a valid dial code
1215
      if (dialCode) {
1216
        // in case _getDialCode returned an area code as well
1217
        dialCode = `+${this.selectedCountryData.dialCode}`;
1218
        // a lot of numbers will have a space separating the dial code and the main number, and
1219
        // some NANP numbers will have a hyphen e.g. +1 684-733-1234 - in both cases we want to get
1220
        // rid of it
1221
        // NOTE: don't just trim all non-numerics as may want to preserve an open parenthesis etc
1222
        const start = (number[dialCode.length] === ' ' || number[dialCode.length] === '-') ? dialCode.length + 1 : dialCode.length;
1223
        number = number.substr(start);
1224
      }
1225
    }
1226
 
1227
    return this._cap(number);
1228
  }
1229
 
1230
 
1231
  // trigger the 'countrychange' event
1232
  _triggerCountryChange() {
1233
    this._trigger('countrychange');
1234
  }
1235
 
1236
 
1237
  /**************************
1238
   *  SECRET PUBLIC METHODS
1239
   **************************/
1240
 
1241
 
1242
  // this is called when the geoip call returns
1243
  handleAutoCountry() {
1244
    if (this.options.initialCountry === 'auto') {
1245
      // we must set this even if there is an initial val in the input: in case the initial val is
1246
      // invalid and they delete it - they should see their auto country
1247
      this.defaultCountry = window.intlTelInputGlobals.autoCountry;
1248
      // if there's no initial value in the input, then update the flag
1249
      if (!this.telInput.value) {
1250
        this.setCountry(this.defaultCountry);
1251
      }
1252
      this.resolveAutoCountryPromise();
1253
    }
1254
  }
1255
 
1256
 
1257
  // this is called when the utils request completes
1258
  handleUtils() {
1259
    // if the request was successful
1260
    if (window.intlTelInputUtils) {
1261
      // if there's an initial value in the input, then format it
1262
      if (this.telInput.value) {
1263
        this._updateValFromNumber(this.telInput.value);
1264
      }
1265
      this._updatePlaceholder();
1266
    }
1267
    this.resolveUtilsScriptPromise();
1268
  }
1269
 
1270
 
1271
  /********************
1272
   *  PUBLIC METHODS
1273
   ********************/
1274
 
1275
 
1276
  // remove plugin
1277
  destroy() {
1278
    const { form } = this.telInput;
1279
 
1280
    if (this.options.allowDropdown) {
1281
      // make sure the dropdown is closed (and unbind listeners)
1282
      this._closeDropdown();
1283
      this.selectedFlag.removeEventListener('click', this._handleClickSelectedFlag);
1284
      this.flagsContainer.removeEventListener('keydown', this._handleFlagsContainerKeydown);
1285
      // label click hack
1286
      const label = this._getClosestLabel();
1287
      if (label) label.removeEventListener('click', this._handleLabelClick);
1288
    }
1289
 
1290
    // unbind hiddenInput listeners
1291
    if (this.hiddenInput && form) form.removeEventListener('submit', this._handleHiddenInputSubmit);
1292
 
1293
    // unbind autoHideDialCode listeners
1294
    if (this.options.autoHideDialCode) {
1295
      if (form) form.removeEventListener('submit', this._handleSubmitOrBlurEvent);
1296
      this.telInput.removeEventListener('blur', this._handleSubmitOrBlurEvent);
1297
    }
1298
 
1299
    // unbind key events, and cut/paste events
1300
    this.telInput.removeEventListener('keyup', this._handleKeyupEvent);
1301
    this.telInput.removeEventListener('cut', this._handleClipboardEvent);
1302
    this.telInput.removeEventListener('paste', this._handleClipboardEvent);
1303
 
1304
    // remove attribute of id instance: data-intl-tel-input-id
1305
    this.telInput.removeAttribute('data-intl-tel-input-id');
1306
 
1307
    // remove markup (but leave the original input)
1308
    const wrapper = this.telInput.parentNode;
1309
    wrapper.parentNode.insertBefore(this.telInput, wrapper);
1310
    wrapper.parentNode.removeChild(wrapper);
1311
 
1312
    delete window.intlTelInputGlobals.instances[this.id];
1313
  }
1314
 
1315
 
1316
  // get the extension from the current number
1317
  getExtension() {
1318
    if (window.intlTelInputUtils) {
1319
      return intlTelInputUtils.getExtension(this._getFullNumber(), this.selectedCountryData.iso2);
1320
    }
1321
    return '';
1322
  }
1323
 
1324
 
1325
  // format the number to the given format
1326
  getNumber(format) {
1327
    if (window.intlTelInputUtils) {
1328
      const { iso2 } = this.selectedCountryData;
1329
      return intlTelInputUtils.formatNumber(this._getFullNumber(), iso2, format);
1330
    }
1331
    return '';
1332
  }
1333
 
1334
 
1335
  // get the type of the entered number e.g. landline/mobile
1336
  getNumberType() {
1337
    if (window.intlTelInputUtils) {
1338
      return intlTelInputUtils.getNumberType(this._getFullNumber(), this.selectedCountryData.iso2);
1339
    }
1340
    return -99;
1341
  }
1342
 
1343
 
1344
  // get the country data for the currently selected flag
1345
  getSelectedCountryData() {
1346
    return this.selectedCountryData;
1347
  }
1348
 
1349
 
1350
  // get the validation error
1351
  getValidationError() {
1352
    if (window.intlTelInputUtils) {
1353
      const { iso2 } = this.selectedCountryData;
1354
      return intlTelInputUtils.getValidationError(this._getFullNumber(), iso2);
1355
    }
1356
    return -99;
1357
  }
1358
 
1359
 
1360
  // validate the input val - assumes the global function isValidNumber (from utilsScript)
1361
  isValidNumber() {
1362
    const val = this._getFullNumber().trim();
1363
    const countryCode = (this.options.nationalMode) ? this.selectedCountryData.iso2 : '';
1364
    return (window.intlTelInputUtils ? intlTelInputUtils.isValidNumber(val, countryCode) : null);
1365
  }
1366
 
1367
 
1368
  // update the selected flag, and update the input val accordingly
1369
  setCountry(originalCountryCode) {
1370
    const countryCode = originalCountryCode.toLowerCase();
1371
    // check if already selected
1372
    if (!this.selectedFlagInner.classList.contains(`iti__${countryCode}`)) {
1373
      this._setFlag(countryCode);
1374
      this._updateDialCode(this.selectedCountryData.dialCode, false);
1375
      this._triggerCountryChange();
1376
    }
1377
  }
1378
 
1379
 
1380
  // set the input value and update the flag
1381
  setNumber(number) {
1382
    // we must update the flag first, which updates this.selectedCountryData, which is used for
1383
    // formatting the number before displaying it
1384
    const flagChanged = this._updateFlagFromNumber(number);
1385
    this._updateValFromNumber(number);
1386
    if (flagChanged) {
1387
      this._triggerCountryChange();
1388
    }
1389
  }
1390
 
1391
  // set the placeholder number typ
1392
  setPlaceholderNumberType(type) {
1393
    this.options.placeholderNumberType = type;
1394
    this._updatePlaceholder();
1395
  }
1396
}
1397
 
1398
 
1399
/********************
1400
 *  STATIC METHODS
1401
 ********************/
1402
 
1403
 
1404
// get the country data object
1405
intlTelInputGlobals.getCountryData = () => allCountries;
1406
 
1407
 
1408
// inject a <script> element to load utils.js
1409
const injectScript = (path, handleSuccess, handleFailure) => {
1410
  // inject a new script element into the page
1411
  const script = document.createElement('script');
1412
  script.onload = () => {
1413
    forEachInstance('handleUtils');
1414
    if (handleSuccess) handleSuccess();
1415
  };
1416
  script.onerror = () => {
1417
    forEachInstance('rejectUtilsScriptPromise');
1418
    if (handleFailure) handleFailure();
1419
  };
1420
  script.className = 'iti-load-utils';
1421
  script.async = true;
1422
  script.src = path;
1423
  document.body.appendChild(script);
1424
};
1425
 
1426
 
1427
// load the utils script
1428
intlTelInputGlobals.loadUtils = (path) => {
1429
  // 2 options:
1430
  // 1) not already started loading (start)
1431
  // 2) already started loading (do nothing - just wait for the onload callback to fire, which will
1432
  // trigger handleUtils on all instances, invoking their resolveUtilsScriptPromise functions)
1433
  if (!window.intlTelInputUtils && !window.intlTelInputGlobals.startedLoadingUtilsScript) {
1434
    // only do this once
1435
    window.intlTelInputGlobals.startedLoadingUtilsScript = true;
1436
 
1437
    // if we have promises, then return a promise
1438
    if (typeof Promise !== 'undefined') {
1439
      return new Promise((resolve, reject) => injectScript(path, resolve, reject));
1440
    }
1441
    injectScript(path);
1442
  }
1443
  return null;
1444
};
1445
 
1446
 
1447
// default options
1448
intlTelInputGlobals.defaults = defaults;
1449
 
1450
// version
1451
intlTelInputGlobals.version = '<%= version %>';