Autoría | Ultima modificación | Ver Log |
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 testsdocumentReady: () => document.readyState === 'complete',};if (typeof window === 'object') window.intlTelInputGlobals = intlTelInputGlobals;// these vars persist through all instances of the pluginlet id = 0;const defaults = {// whether or not to allow the dropdownallowDropdown: true,// if there is just a dial code in the input: remove it on blurautoHideDialCode: true,// add a placeholder in the input with an example number for the selected countryautoPlaceholder: 'polite',// modify the parentClasscustomContainer: '',// modify the auto placeholdercustomPlaceholder: null,// append menu to specified elementdropdownContainer: null,// don't display these countriesexcludeCountries: [],// format the input value during initialisation and on setNumberformatOnDisplay: true,// geoIp lookup functiongeoIpLookup: null,// inject a hidden input with this name, and on submit, populate it with the result of getNumberhiddenInput: '',// initial countryinitialCountry: '',// localized country names e.g. { 'de': 'Deutschland' }localizedCountries: null,// don't insert international dial codesnationalMode: true,// display only these countriesonlyCountries: [],// number type to use for placeholdersplaceholderNumberType: 'MOBILE',// the countries at the top of the list. defaults to united states and united kingdompreferredCountries: ['us', 'gb'],// display the country dial code next to the selected flag so it's not part of the typed numberseparateDialCode: false,// specify the path to the libphonenumber script to enable validation/formattingutilsScript: '',};// https://en.wikipedia.org/wiki/List_of_North_American_Numbering_Plan_area_codes#Non-geographic_area_codesconst 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 IE11const 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 pluginconst 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-varsclass Iti {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 IE11const 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 codesif (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 cssdocument.body.classList.add('iti-mobile');// on mobile, we want a full screen dropdown, so we must append it to the bodyif (!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// completeif (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 existthis.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 existsthis.selectedCountryData = {};// process all the data: onlyCountries, excludeCountries, preferredCountries etcthis._processCountryData();// generate the markupthis._generateMarkup();// set the initial state of the input value and the selected flagthis._setInitialState();// start all of the event listeners: autoHideDialCode, input keydown, selectedFlag clickthis._initListeners();// utils script, and auto countrythis._initRequests();}/********************* PRIVATE METHODS********************/// prepare all of the country data, including onlyCountries, excludeCountries and// preferredCountries options_processCountryData() {// process onlyCountries or excludeCountries array if presentthis._processAllCountries();// process the countryCodes mapthis._processCountryCodes();// process the preferredCountriesthis._processPreferredCountries();// translate countries according to localizedCountries optionif (this.options.localizedCountries) this._translateCountriesByLocale();// sort countries by nameif (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 countryCodefor (let i = 0; i < this.countryCodes[countryCode].length; i++) {if (this.countryCodes[countryCode][i] === iso2) return;}// check for undefined as 0 is falsyconst 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 codesthis.dialCodes = {};// here we store "country codes" (both dial codes and their area codes)this.countryCodes = {};// first: add dial codesfor (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 matchesfor (let i = 0; i < this.countries.length; i++) {const c = this.countries[i];// area codesif (c.areaCodes) {const rootCountryCode = this.countryCodes[c.dialCode][0];// for each area codefor (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 wellfor (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 codethis._addCountryCode(rootCountryCode, partialDialCode);this._addCountryCode(c.iso2, partialDialCode);}// add the full area codethis._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 navigablethis.selectedFlag.setAttribute('tabindex', '0');this.dropdownArrow = this._createEl('div', { class: 'iti__arrow' }, this.selectedFlag);// country dropdown: preferred countries, then divider, then all countriesthis.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 markupif (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 nameif (i !== -1) hiddenInputName = `${name.substr(0, i)}[${hiddenInputName}]`;}this.hiddenInput = this._createEl('input', {type: 'hidden',name: hiddenInputName,});wrapper.appendChild(this.hiddenInput);}}// add a country <li> to the countryList <ul> container_appendListItems(countries, className, preferred) {// we create so many DOM elements, it is faster to build a temp string// and then add everything to the DOM in one go at the endlet tmp = '';// for each countryfor (let i = 0; i < countries.length; i++) {const c = countries[i];const idSuffix = preferred ? '-preferred' : '';// open the list itemtmp += `<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'>`;// add the flagtmp += `<div class='iti__flag-box'><div class='iti__flag iti__${c.iso2}'></div></div>`;// and the country name and dial codetmp += `<span class='iti__country-name'>${c.name}</span>`;tmp += `<span class='iti__dial-code'>+${c.dialCode}</span>`;// close the list itemtmp += '</li>';}this.countryList.insertAdjacentHTML('beforeend', tmp);}// set the initial state of the input value and the selected flag by:// 1. extracting a dial code from the given number// 2. using explicit initialCountry// 3. picking the first preferred country// 4. picking the first country_setInitialState() {const val = this.telInput.value;const dialCode = this._getDialCode(val);const isRegionlessNanp = this._isRegionlessNanp(val);const {initialCountry,nationalMode,autoHideDialCode,separateDialCode,} = this.options;// if we already have a dial code, and it's not a regionlessNanp, we can go ahead and set the// flag, else fall back to the default countryif (dialCode && !isRegionlessNanp) {this._updateFlagFromNumber(val);} else if (initialCountry !== 'auto') {// see if we should select a flagif (initialCountry) {this._setFlag(initialCountry.toLowerCase());} else {if (dialCode && isRegionlessNanp) {// has intl dial code, is regionless nanp, and no initialCountry, so default to USthis._setFlag('us');} else {// no dial code and no initialCountry, so default to first in listthis.defaultCountry = (this.preferredCountries.length) ? this.preferredCountries[0].iso2: this.countries[0].iso2;if (!val) {this._setFlag(this.defaultCountry);}}}// if empty and no nationalMode and no autoHideDialCode then insert the default dial codeif (!val && !nationalMode && !autoHideDialCode && !separateDialCode) {this.telInput.value = `+${this.selectedCountryData.dialCode}`;}}// NOTE: if initialCountry is set to auto, that will be handled separately// format - note this wont be run after _updateDialCode as that's only called if no valif (val) this._updateValFromNumber(val);}// initialise the main event listeners: input keyup, and click selected flag_initListeners() {this._initKeyListeners();if (this.options.autoHideDialCode) this._initBlurListeners();if (this.options.allowDropdown) this._initDropdownListeners();if (this.hiddenInput) this._initHiddenInputListener();}// update hidden input on form submit_initHiddenInputListener() {this._handleHiddenInputSubmit = () => {this.hiddenInput.value = this.getNumber();};if (this.telInput.form) this.telInput.form.addEventListener('submit', this._handleHiddenInputSubmit);}// iterate through parent nodes to find the closest label ancestor, if it exists_getClosestLabel() {let el = this.telInput;while (el && el.tagName !== 'LABEL') el = el.parentNode;return el;}// initialise the dropdown listeners_initDropdownListeners() {// hack for input nested inside label (which is valid markup): clicking the selected-flag to// open the dropdown would then automatically trigger a 2nd click on the input which would// close it againthis._handleLabelClick = (e) => {// if the dropdown is closed, then focus the input, else ignore the clickif (this.countryList.classList.contains('iti__hide')) this.telInput.focus();else e.preventDefault();};const label = this._getClosestLabel();if (label) label.addEventListener('click', this._handleLabelClick);// toggle country dropdown on clickthis._handleClickSelectedFlag = () => {// only intercept this event if we're opening the dropdown// else let it bubble up to the top ("click-off-to-close" listener)// we cannot just stopPropagation as it may be needed to close another instanceif (this.countryList.classList.contains('iti__hide') && !this.telInput.disabled && !this.telInput.readOnly) {this._showDropdown();}};this.selectedFlag.addEventListener('click', this._handleClickSelectedFlag);// open dropdown list if currently focusedthis._handleFlagsContainerKeydown = (e) => {const isDropdownHidden = this.countryList.classList.contains('iti__hide');if (isDropdownHidden && ['ArrowUp', 'Up', 'ArrowDown', 'Down', ' ', 'Enter'].indexOf(e.key) !== -1) {// prevent form from being submitted if "ENTER" was pressede.preventDefault();// prevent event from being handled again by documente.stopPropagation();this._showDropdown();}// allow navigation from dropdown to input on TABif (e.key === 'Tab') this._closeDropdown();};this.flagsContainer.addEventListener('keydown', this._handleFlagsContainerKeydown);}// init many requests: utils script / geo ip lookup_initRequests() {// if the user has specified the path to the utils script, fetch it on window.load, else resolveif (this.options.utilsScript && !window.intlTelInputUtils) {// if the plugin is being initialised after the window.load event has already been firedif (window.intlTelInputGlobals.documentReady()) {window.intlTelInputGlobals.loadUtils(this.options.utilsScript);} else {// wait until the load event so we don't block any other requests e.g. the flags imagewindow.addEventListener('load', () => {window.intlTelInputGlobals.loadUtils(this.options.utilsScript);});}} else this.resolveUtilsScriptPromise();if (this.options.initialCountry === 'auto') this._loadAutoCountry();else this.resolveAutoCountryPromise();}// perform the geo ip lookup_loadAutoCountry() {// 3 options:// 1) already loaded (we're done)// 2) not already started loading (start)// 3) already started loading (do nothing - just wait for loading callback to fire)if (window.intlTelInputGlobals.autoCountry) {this.handleAutoCountry();} else if (!window.intlTelInputGlobals.startedLoadingAutoCountry) {// don't do this twice!window.intlTelInputGlobals.startedLoadingAutoCountry = true;if (typeof this.options.geoIpLookup === 'function') {this.options.geoIpLookup((countryCode) => {window.intlTelInputGlobals.autoCountry = countryCode.toLowerCase();// tell all instances the auto country is ready// TODO: this should just be the current instances// UPDATE: use setTimeout in case their geoIpLookup function calls this callback straight// away (e.g. if they have already done the geo ip lookup somewhere else). Using// setTimeout means that the current thread of execution will finish before executing// this, which allows the plugin to finish initialising.setTimeout(() => forEachInstance('handleAutoCountry'));}, () => forEachInstance('rejectAutoCountryPromise'));}}}// initialize any key listeners_initKeyListeners() {// update flag on keyupthis._handleKeyupEvent = () => {if (this._updateFlagFromNumber(this.telInput.value)) {this._triggerCountryChange();}};this.telInput.addEventListener('keyup', this._handleKeyupEvent);// update flag on cut/paste events (now supported in all major browsers)this._handleClipboardEvent = () => {// hack because "paste" event is fired before input is updatedsetTimeout(this._handleKeyupEvent);};this.telInput.addEventListener('cut', this._handleClipboardEvent);this.telInput.addEventListener('paste', this._handleClipboardEvent);}// adhere to the input's maxlength attr_cap(number) {const max = this.telInput.getAttribute('maxlength');return (max && number.length > max) ? number.substr(0, max) : number;}// listen for blur/submit (for autoHideDialCode feature)_initBlurListeners() {// on blur or form submit: if just a dial code then remove itthis._handleSubmitOrBlurEvent = () => {this._removeEmptyDialCode();};if (this.telInput.form) this.telInput.form.addEventListener('submit', this._handleSubmitOrBlurEvent);this.telInput.addEventListener('blur', this._handleSubmitOrBlurEvent);// made the decision not to trigger blur() now, because would only do anything in the case// where they manually set the initial value to just a dial code, in which case they probably// want it to be displayed.}// clear the input if it just contains a dial code_removeEmptyDialCode() {if (this.telInput.value.charAt(0) === '+') {const numeric = this._getNumeric(this.telInput.value);// if just a plus, or if just a dial codeif (!numeric || this.selectedCountryData.dialCode === numeric) {this.telInput.value = '';}}}// extract the numeric digits from the given string_getNumeric(s) {return s.replace(/\D/g, '');}// trigger a custom event on the input_trigger(name) {// have to use old school document.createEvent as IE11 doesn't support `new Event()` syntaxconst e = document.createEvent('Event');e.initEvent(name, true, true); // can bubble, and is cancellablethis.telInput.dispatchEvent(e);}// show the dropdown_showDropdown() {this.countryList.classList.remove('iti__hide');this.selectedFlag.setAttribute('aria-expanded', 'true');this._setDropdownPosition();// update highlighting and scroll to active list itemif (this.activeItem) {this._highlightListItem(this.activeItem, false);this._scrollTo(this.activeItem, true);}// bind all the dropdown-related listeners: mouseover, click, click-off, keydownthis._bindDropdownListeners();// update the arrowthis.dropdownArrow.classList.add('iti__arrow--up');this._trigger('open:countrydropdown');}// make sure the el has the className or not, depending on the value of shouldHaveClass_toggleClass(el, className, shouldHaveClass) {if (shouldHaveClass && !el.classList.contains(className)) el.classList.add(className);else if (!shouldHaveClass && el.classList.contains(className)) el.classList.remove(className);}// decide where to position dropdown (depends on position within viewport, and scroll)_setDropdownPosition() {if (this.options.dropdownContainer) {this.options.dropdownContainer.appendChild(this.dropdown);}if (!this.isMobile) {const pos = this.telInput.getBoundingClientRect();// windowTop from https://stackoverflow.com/a/14384091/217866const windowTop = window.pageYOffset || document.documentElement.scrollTop;const inputTop = pos.top + windowTop;const dropdownHeight = this.countryList.offsetHeight;// dropdownFitsBelow = (dropdownBottom < windowBottom)const dropdownFitsBelow = (inputTop + this.telInput.offsetHeight + dropdownHeight< (windowTop + window.innerHeight));const dropdownFitsAbove = (inputTop - dropdownHeight > windowTop);// by default, the dropdown will be below the input. If we want to position it above the// input, we add the dropup class.this._toggleClass(this.countryList, 'iti__country-list--dropup', (!dropdownFitsBelow && dropdownFitsAbove));// if dropdownContainer is enabled, calculate postionif (this.options.dropdownContainer) {// by default the dropdown will be directly over the input because it's not in the flow.// If we want to position it below, we need to add some extra top value.const extraTop = (!dropdownFitsBelow && dropdownFitsAbove) ? 0 : this.telInput.offsetHeight;// calculate placementthis.dropdown.style.top = `${inputTop + extraTop}px`;this.dropdown.style.left = `${pos.left + document.body.scrollLeft}px`;// close menu on window scrollthis._handleWindowScroll = () => this._closeDropdown();window.addEventListener('scroll', this._handleWindowScroll);}}}// iterate through parent nodes to find the closest list item_getClosestListItem(target) {let el = target;while (el && el !== this.countryList && !el.classList.contains('iti__country')) el = el.parentNode;// if we reached the countryList element, then return nullreturn (el === this.countryList) ? null : el;}// we only bind dropdown listeners when the dropdown is open_bindDropdownListeners() {// when mouse over a list item, just highlight that one// we add the class "highlight", so if they hit "enter" we know which one to selectthis._handleMouseoverCountryList = (e) => {// handle event delegation, as we're listening for this event on the countryListconst listItem = this._getClosestListItem(e.target);if (listItem) this._highlightListItem(listItem, false);};this.countryList.addEventListener('mouseover', this._handleMouseoverCountryList);// listen for country selectionthis._handleClickCountryList = (e) => {const listItem = this._getClosestListItem(e.target);if (listItem) this._selectListItem(listItem);};this.countryList.addEventListener('click', this._handleClickCountryList);// click off to close// (except when this initial opening click is bubbling up)// we cannot just stopPropagation as it may be needed to close another instancelet isOpening = true;this._handleClickOffToClose = () => {if (!isOpening) this._closeDropdown();isOpening = false;};document.documentElement.addEventListener('click', this._handleClickOffToClose);// listen for up/down scrolling, enter to select, or letters to jump to country name.// use keydown as keypress doesn't fire for non-char keys and we want to catch if they// just hit down and hold it to scroll down (no keyup event).// listen on the document because that's where key events are triggered if no input has focuslet query = '';let queryTimer = null;this._handleKeydownOnDropdown = (e) => {// prevent down key from scrolling the whole page,// and enter key from submitting a form etce.preventDefault();// up and down to navigateif (e.key === 'ArrowUp' || e.key === 'Up' || e.key === 'ArrowDown' || e.key === 'Down') this._handleUpDownKey(e.key);// enter to selectelse if (e.key === 'Enter') this._handleEnterKey();// esc to closeelse if (e.key === 'Escape') this._closeDropdown();// alpha chars to perform search// regex allows one latin alpha char or space, based on https://stackoverflow.com/a/26900132/217866)else if (/^[a-zA-ZÀ-ÿа-яА-Я ]$/.test(e.key)) {// jump to countries that start with the query stringif (queryTimer) clearTimeout(queryTimer);query += e.key.toLowerCase();this._searchForCountry(query);// if the timer hits 1 second, reset the queryqueryTimer = setTimeout(() => {query = '';}, 1000);}};document.addEventListener('keydown', this._handleKeydownOnDropdown);}// highlight the next/prev item in the list (and ensure it is visible)_handleUpDownKey(key) {let next = (key === 'ArrowUp' || key === 'Up') ? this.highlightedItem.previousElementSibling : this.highlightedItem.nextElementSibling;if (next) {// skip the dividerif (next.classList.contains('iti__divider')) {next = (key === 'ArrowUp' || key === 'Up') ? next.previousElementSibling : next.nextElementSibling;}this._highlightListItem(next, true);}}// select the currently highlighted item_handleEnterKey() {if (this.highlightedItem) this._selectListItem(this.highlightedItem);}// find the first list item whose name starts with the query string_searchForCountry(query) {for (let i = 0; i < this.countries.length; i++) {if (this._startsWith(this.countries[i].name, query)) {const listItem = this.countryList.querySelector(`#iti-${this.id}__item-${this.countries[i].iso2}`);// update highlighting and scrollthis._highlightListItem(listItem, false);this._scrollTo(listItem, true);break;}}}// check if string a starts with string b_startsWith(a, b) {return (a.substr(0, b.length).toLowerCase() === b);}// update the input's value to the given val (format first if possible)// NOTE: this is called from _setInitialState, handleUtils and setNumber_updateValFromNumber(originalNumber) {let number = originalNumber;if (this.options.formatOnDisplay && window.intlTelInputUtils && this.selectedCountryData) {const useNational = (!this.options.separateDialCode&& (this.options.nationalMode || number.charAt(0) !== '+'));const { NATIONAL, INTERNATIONAL } = intlTelInputUtils.numberFormat;const format = useNational ? NATIONAL : INTERNATIONAL;number = intlTelInputUtils.formatNumber(number, this.selectedCountryData.iso2, format);}number = this._beforeSetNumber(number);this.telInput.value = number;}// check if need to select a new flag based on the given number// Note: called from _setInitialState, keyup handler, setNumber_updateFlagFromNumber(originalNumber) {// if we're in nationalMode and we already have US/Canada selected, make sure the number starts// with a +1 so _getDialCode will be able to extract the area code// update: if we dont yet have selectedCountryData, but we're here (trying to update the flag// from the number), that means we're initialising the plugin with a number that already has a// dial code, so fine to ignore this bitlet number = originalNumber;const selectedDialCode = this.selectedCountryData.dialCode;const isNanp = selectedDialCode === '1';if (number && this.options.nationalMode && isNanp && number.charAt(0) !== '+') {if (number.charAt(0) !== '1') number = `1${number}`;number = `+${number}`;}// update flag if user types area code for another countryif (this.options.separateDialCode && selectedDialCode && number.charAt(0) !== '+') {number = `+${selectedDialCode}${number}`;}// try and extract valid dial code from inputconst dialCode = this._getDialCode(number, true);const numeric = this._getNumeric(number);let countryCode = null;if (dialCode) {const countryCodes = this.countryCodes[this._getNumeric(dialCode)];// check if the right country is already selected. this should be false if the number is// longer than the matched dial code because in this case we need to make sure that if// there are multiple country matches, that the first one is selected (note: we could// just check that here, but it requires the same loop that we already have later)const alreadySelected = (countryCodes.indexOf(this.selectedCountryData.iso2) !== -1)&& (numeric.length <= dialCode.length - 1);const isRegionlessNanpNumber = (selectedDialCode === '1' && this._isRegionlessNanp(numeric));// only update the flag if:// A) NOT (we currently have a NANP flag selected, and the number is a regionlessNanp)// AND// B) the right country is not already selectedif (!isRegionlessNanpNumber && !alreadySelected) {// if using onlyCountries option, countryCodes[0] may be empty, so we must find the first// non-empty indexfor (let j = 0; j < countryCodes.length; j++) {if (countryCodes[j]) {countryCode = countryCodes[j];break;}}}} else if (number.charAt(0) === '+' && numeric.length) {// invalid dial code, so empty// Note: use getNumeric here because the number has not been formatted yet, so could contain// bad charscountryCode = '';} else if (!number || number === '+') {// empty, or just a plus, so defaultcountryCode = this.defaultCountry;}if (countryCode !== null) {return this._setFlag(countryCode);}return false;}// check if the given number is a regionless NANP number (expects the number to contain an// international dial code)_isRegionlessNanp(number) {const numeric = this._getNumeric(number);if (numeric.charAt(0) === '1') {const areaCode = numeric.substr(1, 3);return (regionlessNanpNumbers.indexOf(areaCode) !== -1);}return false;}// remove highlighting from other list items and highlight the given item_highlightListItem(listItem, shouldFocus) {const prevItem = this.highlightedItem;if (prevItem) prevItem.classList.remove('iti__highlight');this.highlightedItem = listItem;this.highlightedItem.classList.add('iti__highlight');if (shouldFocus) this.highlightedItem.focus();}// find the country data for the given country code// the ignoreOnlyCountriesOption is only used during init() while parsing the onlyCountries array_getCountryData(countryCode, ignoreOnlyCountriesOption, allowFail) {const countryList = (ignoreOnlyCountriesOption) ? allCountries : this.countries;for (let i = 0; i < countryList.length; i++) {if (countryList[i].iso2 === countryCode) {return countryList[i];}}if (allowFail) {return null;}throw new Error(`No country data for '${countryCode}'`);}// select the given flag, update the placeholder and the active list item// Note: called from _setInitialState, _updateFlagFromNumber, _selectListItem, setCountry_setFlag(countryCode) {const prevCountry = (this.selectedCountryData.iso2) ? this.selectedCountryData : {};// do this first as it will throw an error and stop if countryCode is invalidthis.selectedCountryData = (countryCode) ? this._getCountryData(countryCode, false, false) : {};// update the defaultCountry - we only need the iso2 from now on, so just store thatif (this.selectedCountryData.iso2) {this.defaultCountry = this.selectedCountryData.iso2;}this.selectedFlagInner.setAttribute('class', `iti__flag iti__${countryCode}`);// update the selected country's title attributeconst title = (countryCode) ? `${this.selectedCountryData.name}: +${this.selectedCountryData.dialCode}` : 'Unknown';this.selectedFlag.setAttribute('title', title);if (this.options.separateDialCode) {const dialCode = (this.selectedCountryData.dialCode) ? `+${this.selectedCountryData.dialCode}` : '';this.selectedDialCode.innerHTML = dialCode;// offsetWidth is zero if input is in a hidden container during initialisationconst selectedFlagWidth = this.selectedFlag.offsetWidth || this._getHiddenSelectedFlagWidth();// add 6px of padding after the grey selected-dial-code box, as this is what we use in the cssthis.telInput.style.paddingLeft = `${selectedFlagWidth + 6}px`;}// and the input's placeholderthis._updatePlaceholder();// update the active list itemif (this.options.allowDropdown) {const prevItem = this.activeItem;if (prevItem) {prevItem.classList.remove('iti__active');prevItem.setAttribute('aria-selected', 'false');}if (countryCode) {// check if there is a preferred item first, else fall back to standardconst nextItem = this.countryList.querySelector(`#iti-${this.id}__item-${countryCode}-preferred`) || this.countryList.querySelector(`#iti-${this.id}__item-${countryCode}`);nextItem.setAttribute('aria-selected', 'true');nextItem.classList.add('iti__active');this.activeItem = nextItem;this.selectedFlag.setAttribute('aria-activedescendant', nextItem.getAttribute('id'));}}// return if the flag has changed or notreturn (prevCountry.iso2 !== countryCode);}// when the input is in a hidden container during initialisation, we must inject some markup// into the end of the DOM to calculate the correct offsetWidth_getHiddenSelectedFlagWidth() {// to get the right styling to apply, all we need is a shallow clone of the container,// and then to inject a deep clone of the selectedFlag elementconst containerClone = this.telInput.parentNode.cloneNode();containerClone.style.visibility = 'hidden';document.body.appendChild(containerClone);const flagsContainerClone = this.flagsContainer.cloneNode();containerClone.appendChild(flagsContainerClone);const selectedFlagClone = this.selectedFlag.cloneNode(true);flagsContainerClone.appendChild(selectedFlagClone);const width = selectedFlagClone.offsetWidth;containerClone.parentNode.removeChild(containerClone);return width;}// update the input placeholder to an example number from the currently selected country_updatePlaceholder() {const shouldSetPlaceholder = (this.options.autoPlaceholder === 'aggressive') || (!this.hadInitialPlaceholder && this.options.autoPlaceholder === 'polite');if (window.intlTelInputUtils && shouldSetPlaceholder) {const numberType = intlTelInputUtils.numberType[this.options.placeholderNumberType];let placeholder = (this.selectedCountryData.iso2) ? intlTelInputUtils.getExampleNumber(this.selectedCountryData.iso2, this.options.nationalMode, numberType) : '';placeholder = this._beforeSetNumber(placeholder);if (typeof this.options.customPlaceholder === 'function') {placeholder = this.options.customPlaceholder(placeholder, this.selectedCountryData);}this.telInput.setAttribute('placeholder', placeholder);}}// called when the user selects a list item from the dropdown_selectListItem(listItem) {// update selected flag and active list itemconst flagChanged = this._setFlag(listItem.getAttribute('data-country-code'));this._closeDropdown();this._updateDialCode(listItem.getAttribute('data-dial-code'), true);// focus the inputthis.telInput.focus();// put cursor at end - this fix is required for FF and IE11 (with nationalMode=false i.e. auto// inserting dial code), who try to put the cursor at the beginning the first timeconst len = this.telInput.value.length;this.telInput.setSelectionRange(len, len);if (flagChanged) {this._triggerCountryChange();}}// close the dropdown and unbind any listeners_closeDropdown() {this.countryList.classList.add('iti__hide');this.selectedFlag.setAttribute('aria-expanded', 'false');// update the arrowthis.dropdownArrow.classList.remove('iti__arrow--up');// unbind key eventsdocument.removeEventListener('keydown', this._handleKeydownOnDropdown);document.documentElement.removeEventListener('click', this._handleClickOffToClose);this.countryList.removeEventListener('mouseover', this._handleMouseoverCountryList);this.countryList.removeEventListener('click', this._handleClickCountryList);// remove menu from containerif (this.options.dropdownContainer) {if (!this.isMobile) window.removeEventListener('scroll', this._handleWindowScroll);if (this.dropdown.parentNode) this.dropdown.parentNode.removeChild(this.dropdown);}this._trigger('close:countrydropdown');}// check if an element is visible within it's container, else scroll until it is_scrollTo(element, middle) {const container = this.countryList;// windowTop from https://stackoverflow.com/a/14384091/217866const windowTop = window.pageYOffset || document.documentElement.scrollTop;const containerHeight = container.offsetHeight;const containerTop = container.getBoundingClientRect().top + windowTop;const containerBottom = containerTop + containerHeight;const elementHeight = element.offsetHeight;const elementTop = element.getBoundingClientRect().top + windowTop;const elementBottom = elementTop + elementHeight;let newScrollTop = elementTop - containerTop + container.scrollTop;const middleOffset = (containerHeight / 2) - (elementHeight / 2);if (elementTop < containerTop) {// scroll upif (middle) newScrollTop -= middleOffset;container.scrollTop = newScrollTop;} else if (elementBottom > containerBottom) {// scroll downif (middle) newScrollTop += middleOffset;const heightDifference = containerHeight - elementHeight;container.scrollTop = newScrollTop - heightDifference;}}// replace any existing dial code with the new one// Note: called from _selectListItem and setCountry_updateDialCode(newDialCodeBare, hasSelectedListItem) {const inputVal = this.telInput.value;// save having to pass this every timeconst newDialCode = `+${newDialCodeBare}`;let newNumber;if (inputVal.charAt(0) === '+') {// there's a plus so we're dealing with a replacement (doesn't matter if nationalMode or not)const prevDialCode = this._getDialCode(inputVal);if (prevDialCode) {// current number contains a valid dial code, so replace itnewNumber = inputVal.replace(prevDialCode, newDialCode);} else {// current number contains an invalid dial code, so ditch it// (no way to determine where the invalid dial code ends and the rest of the number begins)newNumber = newDialCode;}} else if (this.options.nationalMode || this.options.separateDialCode) {// don't do anythingreturn;} else {// nationalMode is disabledif (inputVal) {// there is an existing value with no dial code: prefix the new dial codenewNumber = newDialCode + inputVal;} else if (hasSelectedListItem || !this.options.autoHideDialCode) {// no existing value and either they've just selected a list item, or autoHideDialCode is// disabled: insert new dial codenewNumber = newDialCode;} else {return;}}this.telInput.value = newNumber;}// try and extract a valid international dial code from a full telephone number// Note: returns the raw string inc plus character and any whitespace/dots etc_getDialCode(number, includeAreaCode) {let dialCode = '';// only interested in international numbers (starting with a plus)if (number.charAt(0) === '+') {let numericChars = '';// iterate over charsfor (let i = 0; i < number.length; i++) {const c = number.charAt(i);// if char is number (https://stackoverflow.com/a/8935649/217866)if (!isNaN(parseInt(c, 10))) {numericChars += c;// if current numericChars make a valid dial codeif (includeAreaCode) {if (this.countryCodes[numericChars]) {// store the actual raw string (useful for matching later)dialCode = number.substr(0, i + 1);}} else {if (this.dialCodes[numericChars]) {dialCode = number.substr(0, i + 1);// if we're just looking for a dial code, we can break as soon as we find onebreak;}}// stop searching as soon as we can - in this case when we hit max lenif (numericChars.length === this.countryCodeMaxLen) {break;}}}}return dialCode;}// get the input val, adding the dial code if separateDialCode is enabled_getFullNumber() {const val = this.telInput.value.trim();const { dialCode } = this.selectedCountryData;let prefix;const numericVal = this._getNumeric(val);if (this.options.separateDialCode && val.charAt(0) !== '+' && dialCode && numericVal) {// when using separateDialCode, it is visible so is effectively part of the typed numberprefix = `+${dialCode}`;} else {prefix = '';}return prefix + val;}// remove the dial code if separateDialCode is enabled// also cap the length if the input has a maxlength attribute_beforeSetNumber(originalNumber) {let number = originalNumber;if (this.options.separateDialCode) {let dialCode = this._getDialCode(number);// if there is a valid dial codeif (dialCode) {// in case _getDialCode returned an area code as welldialCode = `+${this.selectedCountryData.dialCode}`;// a lot of numbers will have a space separating the dial code and the main number, and// some NANP numbers will have a hyphen e.g. +1 684-733-1234 - in both cases we want to get// rid of it// NOTE: don't just trim all non-numerics as may want to preserve an open parenthesis etcconst start = (number[dialCode.length] === ' ' || number[dialCode.length] === '-') ? dialCode.length + 1 : dialCode.length;number = number.substr(start);}}return this._cap(number);}// trigger the 'countrychange' event_triggerCountryChange() {this._trigger('countrychange');}/*************************** SECRET PUBLIC METHODS**************************/// this is called when the geoip call returnshandleAutoCountry() {if (this.options.initialCountry === 'auto') {// we must set this even if there is an initial val in the input: in case the initial val is// invalid and they delete it - they should see their auto countrythis.defaultCountry = window.intlTelInputGlobals.autoCountry;// if there's no initial value in the input, then update the flagif (!this.telInput.value) {this.setCountry(this.defaultCountry);}this.resolveAutoCountryPromise();}}// this is called when the utils request completeshandleUtils() {// if the request was successfulif (window.intlTelInputUtils) {// if there's an initial value in the input, then format itif (this.telInput.value) {this._updateValFromNumber(this.telInput.value);}this._updatePlaceholder();}this.resolveUtilsScriptPromise();}/********************* PUBLIC METHODS********************/// remove plugindestroy() {const { form } = this.telInput;if (this.options.allowDropdown) {// make sure the dropdown is closed (and unbind listeners)this._closeDropdown();this.selectedFlag.removeEventListener('click', this._handleClickSelectedFlag);this.flagsContainer.removeEventListener('keydown', this._handleFlagsContainerKeydown);// label click hackconst label = this._getClosestLabel();if (label) label.removeEventListener('click', this._handleLabelClick);}// unbind hiddenInput listenersif (this.hiddenInput && form) form.removeEventListener('submit', this._handleHiddenInputSubmit);// unbind autoHideDialCode listenersif (this.options.autoHideDialCode) {if (form) form.removeEventListener('submit', this._handleSubmitOrBlurEvent);this.telInput.removeEventListener('blur', this._handleSubmitOrBlurEvent);}// unbind key events, and cut/paste eventsthis.telInput.removeEventListener('keyup', this._handleKeyupEvent);this.telInput.removeEventListener('cut', this._handleClipboardEvent);this.telInput.removeEventListener('paste', this._handleClipboardEvent);// remove attribute of id instance: data-intl-tel-input-idthis.telInput.removeAttribute('data-intl-tel-input-id');// remove markup (but leave the original input)const wrapper = this.telInput.parentNode;wrapper.parentNode.insertBefore(this.telInput, wrapper);wrapper.parentNode.removeChild(wrapper);delete window.intlTelInputGlobals.instances[this.id];}// get the extension from the current numbergetExtension() {if (window.intlTelInputUtils) {return intlTelInputUtils.getExtension(this._getFullNumber(), this.selectedCountryData.iso2);}return '';}// format the number to the given formatgetNumber(format) {if (window.intlTelInputUtils) {const { iso2 } = this.selectedCountryData;return intlTelInputUtils.formatNumber(this._getFullNumber(), iso2, format);}return '';}// get the type of the entered number e.g. landline/mobilegetNumberType() {if (window.intlTelInputUtils) {return intlTelInputUtils.getNumberType(this._getFullNumber(), this.selectedCountryData.iso2);}return -99;}// get the country data for the currently selected flaggetSelectedCountryData() {return this.selectedCountryData;}// get the validation errorgetValidationError() {if (window.intlTelInputUtils) {const { iso2 } = this.selectedCountryData;return intlTelInputUtils.getValidationError(this._getFullNumber(), iso2);}return -99;}// validate the input val - assumes the global function isValidNumber (from utilsScript)isValidNumber() {const val = this._getFullNumber().trim();const countryCode = (this.options.nationalMode) ? this.selectedCountryData.iso2 : '';return (window.intlTelInputUtils ? intlTelInputUtils.isValidNumber(val, countryCode) : null);}// update the selected flag, and update the input val accordinglysetCountry(originalCountryCode) {const countryCode = originalCountryCode.toLowerCase();// check if already selectedif (!this.selectedFlagInner.classList.contains(`iti__${countryCode}`)) {this._setFlag(countryCode);this._updateDialCode(this.selectedCountryData.dialCode, false);this._triggerCountryChange();}}// set the input value and update the flagsetNumber(number) {// we must update the flag first, which updates this.selectedCountryData, which is used for// formatting the number before displaying itconst flagChanged = this._updateFlagFromNumber(number);this._updateValFromNumber(number);if (flagChanged) {this._triggerCountryChange();}}// set the placeholder number typsetPlaceholderNumberType(type) {this.options.placeholderNumberType = type;this._updatePlaceholder();}}/********************* STATIC METHODS********************/// get the country data objectintlTelInputGlobals.getCountryData = () => allCountries;// inject a <script> element to load utils.jsconst injectScript = (path, handleSuccess, handleFailure) => {// inject a new script element into the pageconst script = document.createElement('script');script.onload = () => {forEachInstance('handleUtils');if (handleSuccess) handleSuccess();};script.onerror = () => {forEachInstance('rejectUtilsScriptPromise');if (handleFailure) handleFailure();};script.className = 'iti-load-utils';script.async = true;script.src = path;document.body.appendChild(script);};// load the utils scriptintlTelInputGlobals.loadUtils = (path) => {// 2 options:// 1) not already started loading (start)// 2) already started loading (do nothing - just wait for the onload callback to fire, which will// trigger handleUtils on all instances, invoking their resolveUtilsScriptPromise functions)if (!window.intlTelInputUtils && !window.intlTelInputGlobals.startedLoadingUtilsScript) {// only do this oncewindow.intlTelInputGlobals.startedLoadingUtilsScript = true;// if we have promises, then return a promiseif (typeof Promise !== 'undefined') {return new Promise((resolve, reject) => injectScript(path, resolve, reject));}injectScript(path);}return null;};// default optionsintlTelInputGlobals.defaults = defaults;// versionintlTelInputGlobals.version = '<%= version %>';