import { getAllCountries } from './data' const allCountries = getAllCountries(); const intlTelInputGlobals = { getInstance: (input) => { const id = input.getAttribute('data-intl-tel-input-id'); return window.intlTelInputGlobals.instances[id]; }, instances: {}, // using a global like this allows us to mock it in the tests documentReady: () => document.readyState === 'complete', }; if (typeof window === 'object') window.intlTelInputGlobals = intlTelInputGlobals; // these vars persist through all instances of the plugin let id = 0; const defaults = { // whether or not to allow the dropdown allowDropdown: true, // if there is just a dial code in the input: remove it on blur autoHideDialCode: true, // add a placeholder in the input with an example number for the selected country autoPlaceholder: 'polite', // modify the parentClass customContainer: '', // modify the auto placeholder customPlaceholder: null, // append menu to specified element dropdownContainer: null, // don't display these countries excludeCountries: [], // format the input value during initialisation and on setNumber formatOnDisplay: true, // geoIp lookup function geoIpLookup: null, // inject a hidden input with this name, and on submit, populate it with the result of getNumber hiddenInput: '', // initial country initialCountry: '', // localized country names e.g. { 'de': 'Deutschland' } localizedCountries: null, // don't insert international dial codes nationalMode: true, // display only these countries onlyCountries: [], // number type to use for placeholders placeholderNumberType: 'MOBILE', // the countries at the top of the list. defaults to united states and united kingdom preferredCountries: ['us', 'gb'], // display the country dial code next to the selected flag so it's not part of the typed number separateDialCode: false, // specify the path to the libphonenumber script to enable validation/formatting utilsScript: '', }; // https://en.wikipedia.org/wiki/List_of_North_American_Numbering_Plan_area_codes#Non-geographic_area_codes const regionlessNanpNumbers = ['800', '822', '833', '844', '855', '866', '877', '880', '881', '882', '883', '884', '885', '886', '887', '888', '889']; // utility function to iterate over an object. can't use Object.entries or native forEach because // of IE11 const forEachProp = (obj, callback) => { const keys = Object.keys(obj); for (let i = 0; i < keys.length; i++) { callback(keys[i], obj[keys[i]]); } }; // run a method on each instance of the plugin const forEachInstance = (method) => { forEachProp(window.intlTelInputGlobals.instances, (key) => { window.intlTelInputGlobals.instances[key][method](); }); }; // this is our plugin class that we will create an instance of // eslint-disable-next-line no-unused-vars export default class IntlTel { constructor(input, options) { this.id = id++; this.telInput = input; this.activeItem = null; this.highlightedItem = null; // process specified options / defaults // alternative to Object.assign, which isn't supported by IE11 const customOptions = options || {}; this.options = {}; forEachProp(defaults, (key, value) => { this.options[key] = (customOptions.hasOwnProperty(key)) ? customOptions[key] : value; }); this.hadInitialPlaceholder = Boolean(input.getAttribute('placeholder')); } _init() { // if in nationalMode, disable options relating to dial codes if (this.options.nationalMode) this.options.autoHideDialCode = false; // if separateDialCode then doesn't make sense to A) insert dial code into input // (autoHideDialCode), and B) display national numbers (because we're displaying the country // dial code next to them) if (this.options.separateDialCode) { this.options.autoHideDialCode = this.options.nationalMode = false; } // we cannot just test screen size as some smartphones/website meta tags will report desktop // resolutions // Note: for some reason jasmine breaks if you put this in the main Plugin function with the // rest of these declarations // Note: to target Android Mobiles (and not Tablets), we must find 'Android' and 'Mobile' this.isMobile = /Android.+Mobile|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); if (this.isMobile) { // trigger the mobile dropdown css document.body.classList.add('iti-mobile'); // on mobile, we want a full screen dropdown, so we must append it to the body if (!this.options.dropdownContainer) this.options.dropdownContainer = document.body; } // these promises get resolved when their individual requests complete // this way the dev can do something like iti.promise.then(...) to know when all requests are // complete if (typeof Promise !== 'undefined') { const autoCountryPromise = new Promise((resolve, reject) => { this.resolveAutoCountryPromise = resolve; this.rejectAutoCountryPromise = reject; }); const utilsScriptPromise = new Promise((resolve, reject) => { this.resolveUtilsScriptPromise = resolve; this.rejectUtilsScriptPromise = reject; }); this.promise = Promise.all([autoCountryPromise, utilsScriptPromise]); } else { // prevent errors when Promise doesn't exist this.resolveAutoCountryPromise = this.rejectAutoCountryPromise = () => { }; this.resolveUtilsScriptPromise = this.rejectUtilsScriptPromise = () => { }; } // in various situations there could be no country selected initially, but we need to be able // to assume this variable exists this.selectedCountryData = {}; // process all the data: onlyCountries, excludeCountries, preferredCountries etc this._processCountryData(); // generate the markup this._generateMarkup(); // set the initial state of the input value and the selected flag this._setInitialState(); // start all of the event listeners: autoHideDialCode, input keydown, selectedFlag click this._initListeners(); // utils script, and auto country this._initRequests(); } /******************** * PRIVATE METHODS ********************/ // prepare all of the country data, including onlyCountries, excludeCountries and // preferredCountries options _processCountryData() { // process onlyCountries or excludeCountries array if present this._processAllCountries(); // process the countryCodes map this._processCountryCodes(); // process the preferredCountries this._processPreferredCountries(); // translate countries according to localizedCountries option if (this.options.localizedCountries) this._translateCountriesByLocale(); // sort countries by name if (this.options.onlyCountries.length || this.options.localizedCountries) { this.countries.sort(this._countryNameSort); } } // add a country code to this.countryCodes _addCountryCode(iso2, countryCode, priority) { if (countryCode.length > this.countryCodeMaxLen) { this.countryCodeMaxLen = countryCode.length; } if (!this.countryCodes.hasOwnProperty(countryCode)) { this.countryCodes[countryCode] = []; } // bail if we already have this country for this countryCode for (let i = 0; i < this.countryCodes[countryCode].length; i++) { if (this.countryCodes[countryCode][i] === iso2) return; } // check for undefined as 0 is falsy const index = (priority !== undefined) ? priority : this.countryCodes[countryCode].length; this.countryCodes[countryCode][index] = iso2; } // process onlyCountries or excludeCountries array if present _processAllCountries() { if (this.options.onlyCountries.length) { const lowerCaseOnlyCountries = this.options.onlyCountries.map( country => country.toLowerCase() ); this.countries = allCountries.filter( country => lowerCaseOnlyCountries.indexOf(country.iso2) > -1 ); } else if (this.options.excludeCountries.length) { const lowerCaseExcludeCountries = this.options.excludeCountries.map( country => country.toLowerCase() ); this.countries = allCountries.filter( country => lowerCaseExcludeCountries.indexOf(country.iso2) === -1 ); } else { this.countries = allCountries; } } // Translate Countries by object literal provided on config _translateCountriesByLocale() { for (let i = 0; i < this.countries.length; i++) { const iso = this.countries[i].iso2.toLowerCase(); if (this.options.localizedCountries.hasOwnProperty(iso)) { this.countries[i].name = this.options.localizedCountries[iso]; } } } // sort by country name _countryNameSort(a, b) { return a.name.localeCompare(b.name); } // process the countryCodes map _processCountryCodes() { this.countryCodeMaxLen = 0; // here we store just dial codes this.dialCodes = {}; // here we store "country codes" (both dial codes and their area codes) this.countryCodes = {}; // first: add dial codes for (let i = 0; i < this.countries.length; i++) { const c = this.countries[i]; if (!this.dialCodes[c.dialCode]) this.dialCodes[c.dialCode] = true; this._addCountryCode(c.iso2, c.dialCode, c.priority); } // next: add area codes // this is a second loop over countries, to make sure we have all of the "root" countries // already in the map, so that we can access them, as each time we add an area code substring // to the map, we also need to include the "root" country's code, as that also matches for (let i = 0; i < this.countries.length; i++) { const c = this.countries[i]; // area codes if (c.areaCodes) { const rootCountryCode = this.countryCodes[c.dialCode][0]; // for each area code for (let j = 0; j < c.areaCodes.length; j++) { const areaCode = c.areaCodes[j]; // for each digit in the area code to add all partial matches as well for (let k = 1; k < areaCode.length; k++) { const partialDialCode = c.dialCode + areaCode.substr(0, k); // start with the root country, as that also matches this dial code this._addCountryCode(rootCountryCode, partialDialCode); this._addCountryCode(c.iso2, partialDialCode); } // add the full area code this._addCountryCode(c.iso2, c.dialCode + areaCode); } } } } // process preferred countries - iterate through the preferences, fetching the country data for // each one _processPreferredCountries() { this.preferredCountries = []; for (let i = 0; i < this.options.preferredCountries.length; i++) { const countryCode = this.options.preferredCountries[i].toLowerCase(); const countryData = this._getCountryData(countryCode, false, true); if (countryData) this.preferredCountries.push(countryData); } } // create a DOM element _createEl(name, attrs, container) { const el = document.createElement(name); if (attrs) forEachProp(attrs, (key, value) => el.setAttribute(key, value)); if (container) container.appendChild(el); return el; } // generate all of the markup for the plugin: the selected flag overlay, and the dropdown _generateMarkup() { // if autocomplete does not exist on the element and its form, then // prevent autocomplete as there's no safe, cross-browser event we can react to, so it can // easily put the plugin in an inconsistent state e.g. the wrong flag selected for the // autocompleted number, which on submit could mean wrong number is saved (esp in nationalMode) if (!this.telInput.hasAttribute('autocomplete') && !(this.telInput.form && this.telInput.form.hasAttribute('autocomplete'))) { this.telInput.setAttribute('autocomplete', 'off'); } // containers (mostly for positioning) let parentClass = 'iti'; if (this.options.allowDropdown) { parentClass += ' iti--allow-dropdown'; } if (this.options.separateDialCode) { parentClass += ' iti--separate-dial-code'; } if (this.options.customContainer) { parentClass += ' '; parentClass += this.options.customContainer; } const wrapper = this._createEl('div', { class: parentClass }); this.telInput.parentNode.insertBefore(wrapper, this.telInput); this.flagsContainer = this._createEl('div', { class: 'iti__flag-container' }, wrapper); wrapper.appendChild(this.telInput); // selected flag (displayed to left of input) this.selectedFlag = this._createEl('div', { class: 'iti__selected-flag', role: 'combobox', 'aria-controls': `iti-${this.id}__country-listbox`, 'aria-owns': `iti-${this.id}__country-listbox`, 'aria-expanded': 'false', }, this.flagsContainer); this.selectedFlagInner = this._createEl('div', { class: 'iti__flag' }, this.selectedFlag); if (this.options.separateDialCode) { this.selectedDialCode = this._createEl('div', { class: 'iti__selected-dial-code' }, this.selectedFlag); } if (this.options.allowDropdown) { // make element focusable and tab navigable this.selectedFlag.setAttribute('tabindex', '0'); this.dropdownArrow = this._createEl('div', { class: 'iti__arrow' }, this.selectedFlag); // country dropdown: preferred countries, then divider, then all countries this.countryList = this._createEl('ul', { class: 'iti__country-list iti__hide', id: `iti-${this.id}__country-listbox`, role: 'listbox', 'aria-label': 'List of countries', }); if (this.preferredCountries.length) { this._appendListItems(this.preferredCountries, 'iti__preferred', true); this._createEl('li', { class: 'iti__divider', role: 'separator', 'aria-disabled': 'true', }, this.countryList); } this._appendListItems(this.countries, 'iti__standard'); // create dropdownContainer markup if (this.options.dropdownContainer) { this.dropdown = this._createEl('div', { class: 'iti iti--container' }); this.dropdown.appendChild(this.countryList); } else { this.flagsContainer.appendChild(this.countryList); } } if (this.options.hiddenInput) { let hiddenInputName = this.options.hiddenInput; const name = this.telInput.getAttribute('name'); if (name) { const i = name.lastIndexOf('['); // if input name contains square brackets, then give the hidden input the same name, // replacing the contents of the last set of brackets with the given hiddenInput name if (i !== -1) hiddenInputName = `${name.substr(0, i)}[${hiddenInputName}]`; } this.hiddenInput = this._createEl('input', { type: 'hidden', name: hiddenInputName, }); wrapper.appendChild(this.hiddenInput); } } // add a country
  • to the countryList