UNPKG

react-intl-tel-input

Version:

Telephone input component. Rewrite intl-tel-input in React.js.

1,123 lines (982 loc) 37.8 kB
import React, { Component, PropTypes } from 'react'; import { findDOMNode } from 'react-dom'; import classNames from 'classnames'; import AllCountries from '../components/AllCountries'; import FlagDropDown from '../components/FlagDropDown'; import TelInput from '../components/TelInput'; import utils from '../components/utils'; import _ from 'underscore.deferred'; import '../styles/intlTelInput.scss'; export default class IntlTelInputApp extends Component { static defaultProps = { css: ['intl-tel-input', ''], fieldName: '', fieldId: '', value: '', // define the countries that'll be present in the dropdown // defaults to the data defined in `AllCountries` countriesData: null, // whether or not to allow the dropdown allowDropdown: true, // if there is just a dial code in the input: remove it on blur, and re-add it on focus autoHideDialCode: true, // add or remove input placeholder with an example number for the selected country autoPlaceholder: true, // modify the auto placeholder customPlaceholder: null, // don't display these countries excludeCountries: [], // format the input value during initialisation formatOnInit: true, // display the country dial code next to the selected flag so it's not part of the typed number separateDialCode: false, // default country defaultCountry: '', // geoIp lookup function geoIpLookup: null, // don't insert international dial codes nationalMode: true, // number type to use for placeholders numberType: 'MOBILE', // function which can catch the "no this default country" exception noCountryDataHandler: null, // display only these countries onlyCountries: [], // the countries at the top of the list. defaults to united states and united kingdom preferredCountries: ['us', 'gb'], // specify the path to the libphonenumber script to enable validation/formatting utilsScript: '', onPhoneNumberChange: null, onPhoneNumberBlur: null, onSelectFlag: null, disabled: false, autoFocus: false, }; static propTypes = { css: PropTypes.arrayOf(PropTypes.string), fieldName: PropTypes.string, fieldId: PropTypes.string, value: PropTypes.string, countriesData: PropTypes.arrayOf(PropTypes.array), allowDropdown: PropTypes.bool, autoHideDialCode: PropTypes.bool, autoPlaceholder: PropTypes.bool, customPlaceholder: PropTypes.func, excludeCountries: PropTypes.arrayOf(PropTypes.string), formatOnInit: PropTypes.bool, separateDialCode: PropTypes.bool, defaultCountry: PropTypes.string, geoIpLookup: PropTypes.func, nationalMode: PropTypes.bool, numberType: PropTypes.string, noCountryDataHandler: PropTypes.func, onlyCountries: PropTypes.arrayOf(PropTypes.string), preferredCountries: PropTypes.arrayOf(PropTypes.string), utilsScript: PropTypes.string, onPhoneNumberChange: PropTypes.func, onPhoneNumberBlur: PropTypes.func, onSelectFlag: PropTypes.func, disabled: PropTypes.bool, placeholder: PropTypes.string, autoFocus: PropTypes.bool, }; constructor(props) { super(props); this.wrapperClass = {}; this.autoCountry = ''; this.tempCountry = ''; this.startedLoadingAutoCountry = false; this.deferreds = []; this.autoCountryDeferred = new _.Deferred(); this.utilsScriptDeferred = new _.Deferred(); this.isOpening = false; this.isMobile = /Android.+Mobile|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent); this.preferredCountries = []; this.countries = []; this.countryCodes = {}; this.windowLoaded = false; this.keys = { UP: 38, DOWN: 40, ENTER: 13, ESC: 27, PLUS: 43, A: 65, Z: 90, SPACE: 32, TAB: 9, }; this.query = ''; this.state = { showDropdown: false, highlightedCountry: 0, value: '', disabled: props.disabled, readonly: false, offsetTop: 0, outerHeight: 0, placeholder: '', title: '', countryCode: 'us', dialCode: '', }; this.selectedCountryData = {}; this.addCountryCode = this.addCountryCode.bind(this); this.autoCountryLoaded = this.autoCountryLoaded.bind(this); this.getDialCode = this.getDialCode.bind(this); this.handleOnBlur = this.handleOnBlur.bind(this); this.handleSelectedFlagKeydown = this.handleSelectedFlagKeydown.bind(this); this.setInitialState = this.setInitialState.bind(this); this.setNumber = this.setNumber.bind(this); this.scrollTo = this.scrollTo.bind(this); this.notifyPhoneNumberChange = this.notifyPhoneNumberChange.bind(this); this.isValidNumber = this.isValidNumber.bind(this); this.isValidNumberForRegion = this.isValidNumberForRegion.bind(this); this.isUnknownNanp = this.isUnknownNanp.bind(this); this.initRequests = this.initRequests.bind(this); this.updateFlagFromNumber = this.updateFlagFromNumber.bind(this); this.updatePlaceholder = this.updatePlaceholder.bind(this); this.loadAutoCountry = this.loadAutoCountry.bind(this); this.loadUtils = this.loadUtils.bind(this); this.processCountryData = this.processCountryData.bind(this); this.getNumber = this.getNumber.bind(this); // wrapping actions this.setFlag = this.setFlag.bind(this); this.clickSelectedFlag = this.clickSelectedFlag.bind(this); this.updateValFromNumber = this.updateValFromNumber.bind(this); this.handleWindowScroll = this.handleWindowScroll.bind(this); this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this); this.handleDocumentClick = this.handleDocumentClick.bind(this); this.bindDocumentClick = this.bindDocumentClick.bind(this); this.unbindDocumentClick = this.unbindDocumentClick.bind(this); this.searchForCountry = this.searchForCountry.bind(this); this.handleEnterKey = this.handleEnterKey.bind(this); this.toggleDropdown = this.toggleDropdown.bind(this); this.handleUpDownKey = this.handleUpDownKey.bind(this); this.handleInputChange = this.handleInputChange.bind(this); this.changeHighlightCountry = this.changeHighlightCountry.bind(this); } componentDidMount() { this.initialPlaceholder = this.props.placeholder; this.autoHideDialCode = this.props.autoHideDialCode; this.allowDropdown = this.props.allowDropdown; this.nationalMode = this.props.nationalMode; this.dropdownContainer = ''; // if in nationalMode, disable options relating to dial codes if (this.nationalMode) { this.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.props.separateDialCode) { this.autoHideDialCode = false; this.nationalMode = false; // let's force this for now for simplicity - we can support this later if need be this.allowDropdown = true; } this.processCountryData.call(this); this.tempCountry = this.getTempCountry(this.props.defaultCountry); if (document.readyState === 'complete') { this.windowLoaded = true; } else { window.addEventListener('load', () => { this.windowLoaded = true; }); } // generate the markup this.generateMarkup(); // set the initial state of the input value and the selected flag this.setInitialState(); // utils script, and auto country this.initRequests(); this.deferreds.push(this.autoCountryDeferred.promise()); this.deferreds.push(this.utilsScriptDeferred.promise()); _.when(this.deferreds).done(() => { this.setInitialState(); }); document.addEventListener('keydown', this.handleDocumentKeyDown); } componentWillUpdate(nextProps, nextState) { if (nextState.showDropdown) { document.addEventListener('keydown', this.handleDocumentKeyDown); this.bindDocumentClick(); } else { document.removeEventListener('keydown', this.handleDocumentKeyDown); this.unbindDocumentClick(); } } componentDidUpdate(prevProps, prevState) { if (prevState.value !== this.state.value) { this.notifyPhoneNumberChange(this.state.value); } } componentWillReceiveProps(nextProps) { if (this.props.value !== nextProps.value) { this.setState({ value: nextProps.value, }); } } componentWillUnmount() { document.removeEventListener('keydown', this.handleDocumentKeyDown); this.unbindDocumentClick(); } getTempCountry(countryCode) { if (countryCode === 'auto') { return 'auto'; } let countryData = utils.getCountryData(this.countries, countryCode); // check if country is available in the list if (!countryData.iso2) { if (this.props.preferredCountries.length > 0) { countryData = utils.getCountryData(this.countries, this.props.preferredCountries[0]); } else { countryData = AllCountries.getCountries()[0]; } } return countryData.iso2; } // set the input value and update the flag // NOTE: preventFormat arg is for public method setNumber(number, preventFormat) { // we must update the flag first, which updates this.selectedCountryData, // which is used for formatting the number before displaying it this.updateFlagFromNumber(number); this.updateValFromNumber(number, !preventFormat); } // get the extension from the current number getExtension() { if (window.intlTelInputUtils) { return window.intlTelInputUtils.getExtension( this.getFullNumber(), this.selectedCountryData.iso2); } return ''; } // format the number to the given format getNumber(format) { if (window.intlTelInputUtils) { return window.intlTelInputUtils.formatNumber( this.getFullNumber(), this.selectedCountryData.iso2, format); } return ''; } generateMarkup() { this.wrapperClass['allow-dropdown'] = this.allowDropdown; this.wrapperClass['separate-dial-code'] = this.props.separateDialCode; if (this.isMobile) { utils.addClass(document.querySelector('body'), 'iti-mobile'); // on mobile, we want a full screen dropdown, so we must append it to the body this.dropdownContainer = 'body'; window.addEventListener('scroll', this.handleWindowScroll); } } // this is called when the geoip call returns autoCountryLoaded() { if (this.tempCountry === 'auto') { this.tempCountry = this.autoCountry; this.autoCountryDeferred.resolve(); } } loadUtils() { if (window.intlTelInputUtils) { this.utilsScriptDeferred.resolve(); return; } const request = new XMLHttpRequest(); request.open('GET', this.props.utilsScript, true); request.onload = () => { if (request.status >= 200 && request.status < 400) { const data = request.responseText; if (data && !document.getElementById('intlTelInputUtils')) { const oBody = document.getElementsByTagName('body')[0]; const oScript = document.createElement('script'); oScript.id = 'intlTelInputUtils'; oScript.text = data; oBody.appendChild(oScript); } this.utilsScriptDeferred.resolve(); } }; request.send(); } handleSelectedFlagKeydown(e) { if (!this.state.showDropdown && (e.which === this.keys.UP || e.which === this.keys.DOWN || e.which === this.keys.SPACE || e.which === this.keys.ENTER) ) { // prevent form from being submitted if "ENTER" was pressed e.preventDefault(); // prevent event from being handled again by document e.stopPropagation(); this.toggleDropdown(true); } // allow navigation from dropdown to input on TAB if (e.which === this.keys.TAB) { this.toggleDropdown(false); } } // prepare all of the country data, including onlyCountries and preferredCountries options processCountryData() { // format countries data to what is necessary for component function // defaults to data defined in `AllCountries` AllCountries.initialize(this.props.countriesData); // process onlyCountries or excludeCountries array if present this.processAllCountries.call(this); // process the countryCodes map this.processCountryCodes.call(this); // set the preferredCountries property this.processPreferredCountries.call(this); } // add a country code to countryCodes addCountryCode(countryCodes, iso2, dialCode, priority) { if (!(dialCode in countryCodes)) { countryCodes[dialCode] = []; } const index = priority || 0; countryCodes[dialCode][index] = iso2; return countryCodes; } // filter the given countries using the process function filterCountries(countryArray, processFunc) { let i; // standardise case for (i = 0; i < countryArray.length; i++) { countryArray[i] = countryArray[i].toLowerCase(); } // build instance country array this.countries = []; for (i = 0; i < AllCountries.getCountries().length; i++) { if (processFunc(countryArray.indexOf(AllCountries.getCountries()[i].iso2))) { this.countries.push(AllCountries.getCountries()[i]); } } } processAllCountries() { if (this.props.onlyCountries.length) { // process onlyCountries option this.filterCountries(this.props.onlyCountries, (inArray) => // if country is in array inArray !== -1); } else if (this.props.excludeCountries.length) { // process excludeCountries option this.filterCountries(this.props.excludeCountries, (inArray) => // if country is not in array inArray === -1); } else { this.countries = AllCountries.getCountries(); } } // process the countryCodes map processCountryCodes() { this.countryCodes = {}; for (let i = 0; i < this.countries.length; i++) { const c = this.countries[i]; this.addCountryCode(this.countryCodes, c.iso2, c.dialCode, c.priority); // area codes if (c.areaCodes) { for (let j = 0; j < c.areaCodes.length; j++) { // full dial code is country code + dial code this.addCountryCode(this.countryCodes, c.iso2, c.dialCode + c.areaCodes[j]); } } } } // process preferred countries - iterate through the preferences, // fetching the country data for each one processPreferredCountries() { this.preferredCountries = []; for (let i = 0, max = this.props.preferredCountries.length; i < max; i++) { const countryCode = this.props.preferredCountries[i].toLowerCase(); const countryData = utils.getCountryData(this.countries, countryCode, true); if (countryData) { this.preferredCountries.push(countryData); } } } // set the initial state of the input value and the selected flag setInitialState() { const val = this.props.value || ''; // if we already have a dial code we can go ahead and set the flag, else fall back to default if (this.getDialCode(val)) { this.updateFlagFromNumber(val, true); } else if (this.tempCountry !== 'auto') { // see if we should select a flag if (this.tempCountry) { this.setFlag(this.tempCountry, true); } else { // no dial code and no tempCountry, so default to first in list this.defaultCountry = this.preferredCountries.length ? this.preferredCountries[0].iso2 : this.countries[0].iso2; if (!val) { this.setFlag(this.defaultCountry, true); } } // if empty and no nationalMode and no autoHideDialCode then insert the default dial code if (!val && !this.nationalMode && !this.autoHideDialCode && !this.props.separateDialCode) { this.setState({ value: `+${this.selectedCountryData.dialCode}`, }); } } // NOTE: if tempCountry is set to auto, that will be handled separately // format if (val) { this.updateValFromNumber(val, this.props.formatOnInit); } } initRequests() { // if the user has specified the path to the utils script, fetch it on window.load if (this.props.utilsScript) { // if the plugin is being initialised after the window.load event has already been fired if (this.windowLoaded) { this.loadUtils(); } else { // wait until the load event so we don't block any other requests e.g. the flags image window.addEventListener('load', () => { this.loadUtils(); }); } } else { this.utilsScriptDeferred.resolve(); } if (this.tempCountry === 'auto') { this.loadAutoCountry(); } else { this.autoCountryDeferred.resolve(); } } loadAutoCountry() { // check for localStorage const lsAutoCountry = (window.localStorage !== undefined) ? window.localStorage.getItem('itiAutoCountry') : ''; if (lsAutoCountry) { this.autoCountry = lsAutoCountry; } // 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 (this.autoCountry) { this.autoCountryLoaded(); } else if (!this.startedLoadingAutoCountry) { // don't do this twice! this.startedLoadingAutoCountry = true; if (typeof this.props.geoIpLookup === 'function') { this.props.geoIpLookup((countryCode) => { this.autoCountry = countryCode.toLowerCase(); if (window.localStorage !== undefined) { window.localStorage.setItem('itiAutoCountry', this.autoCountry); } // 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. this.autoCountryLoaded(); }); } } } cap(number) { const max = findDOMNode(this.refs.telInput).getAttribute('maxlength'); return max && number.length > max ? number.substr(0, max) : number; } removeEmptyDialCode() { const value = this.state.value; const startsPlus = value.charAt(0) === '+'; if (startsPlus) { const numeric = utils.getNumeric(value); // if just a plus, or if just a dial code if (!numeric || this.selectedCountryData.dialCode === numeric) { this.setState({ value: '', }); } } } // highlight the next/prev item in the list (and ensure it is visible) handleUpDownKey(key) { const current = findDOMNode(this.refs.flagDropDown).querySelectorAll('.highlight')[0]; const prevElement = (current) ? current.previousElementSibling : undefined; const nextElement = (current) ? current.nextElementSibling : undefined; let next = (key === this.keys.UP) ? prevElement : nextElement; if (next) { // skip the divider if (next.getAttribute('class').indexOf('divider') > -1) { next = (key === this.keys.UP) ? next.previousElementSibling : next.nextElementSibling; } this.scrollTo(next); const selectedIndex = utils.retrieveLiIndex(next); this.setState({ showDropdown: true, highlightedCountry: selectedIndex, }); } } // select the currently highlighted item handleEnterKey() { const current = findDOMNode(this.refs.flagDropDown).querySelectorAll('.highlight')[0]; if (current) { const selectedIndex = utils.retrieveLiIndex(current); const countryCode = current.getAttribute('data-country-code'); this.setState({ showDropdown: false, highlightedCountry: selectedIndex, countryCode, }, () => { this.setFlag(this.state.countryCode); this.unbindDocumentClick(); }); } } // find the first list item whose name starts with the query string searchForCountry(query) { for (let i = 0, max = this.countries.length; i < max; i++) { if (utils.startsWith(this.countries[i].name, query)) { const listItem = findDOMNode(this.refs.flagDropDown).querySelector( `.country-list [data-country-code="${this.countries[i].iso2}"]:not(.preferred)`); const selectedIndex = utils.retrieveLiIndex(listItem); // update highlighting and scroll this.setState({ showDropdown: true, highlightedCountry: selectedIndex, }); this.scrollTo(listItem, true); break; } } } // update the input's value to the given val (format first if possible) // NOTE: this is called from _setInitialState, handleUtils and setNumber updateValFromNumber(number, doFormat) { if (doFormat && window.intlTelInputUtils && this.selectedCountryData) { const format = !this.props.separateDialCode && (this.nationalMode || number.charAt(0) !== '+') ? window.intlTelInputUtils.numberFormat.NATIONAL : window.intlTelInputUtils.numberFormat.INTERNATIONAL; number = window.intlTelInputUtils.formatNumber(number, this.selectedCountryData.iso2, format); } number = this.beforeSetNumber(number); this.setState({ showDropdown: false, value: number, }, () => { this.unbindDocumentClick(); }); } // check if need to select a new flag based on the given number // Note: called from _setInitialState, keyup handler, setNumber updateFlagFromNumber(number, isInit) { // 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 bit if (number && this.nationalMode && this.selectedCountryData && this.selectedCountryData.dialCode === '1' && number.charAt(0) !== '+') { if (number.charAt(0) !== '1') { number = `1${number}`; } number = `+${number}`; } // try and extract valid dial code from input const dialCode = this.getDialCode(number); let countryCode = null; if (dialCode) { // check if one of the matching countries is already selected const countryCodes = this.countryCodes[utils.getNumeric(dialCode)]; const alreadySelected = this.selectedCountryData && countryCodes.indexOf(this.selectedCountryData.iso2) !== -1; // if a matching country is not already selected // (or this is an unknown NANP area code): choose the first in the list if (!alreadySelected || this.isUnknownNanp(number, dialCode)) { // if using onlyCountries option, countryCodes[0] may be empty, // so we must find the first non-empty index for (let j = 0; j < countryCodes.length; j++) { if (countryCodes[j]) { countryCode = countryCodes[j]; break; } } } } else if (number.charAt(0) === '+' && utils.getNumeric(number).length) { // invalid dial code, so empty // Note: use getNumeric here because the number has not been // formatted yet, so could contain bad chars countryCode = ''; } else if (!number || number === '+') { // empty, or just a plus, so default countryCode = this.defaultCountry; } if (countryCode !== null) { this.setFlag(countryCode, isInit); } } // check if the given number contains an unknown area code from // the North American Numbering Plan i.e. the only dialCode that // could be extracted was +1 but the actual number's length is >=4 isUnknownNanp(number, dialCode) { return (dialCode === '+1' && utils.getNumeric(number).length >= 4); } // select the given flag, update the placeholder and the active list item // Note: called from setInitialState, updateFlagFromNumber, selectListItem, setCountry setFlag(countryCode, isInit) { const prevCountry = this.selectedCountryData && this.selectedCountryData.iso2 ? this.selectedCountryData : {}; // do this first as it will throw an error and stop if countryCode is invalid this.selectedCountryData = countryCode ? utils.getCountryData(this.countries, countryCode, false, false, this.props.noCountryDataHandler) : {}; // update the defaultCountry - we only need the iso2 from now on, so just store that if (this.selectedCountryData.iso2) { this.defaultCountry = this.selectedCountryData.iso2; } // update the selected country's title attribute const title = countryCode ? `${this.selectedCountryData.name}: +${this.selectedCountryData.dialCode}` : 'Unknown'; let dialCode = this.state.dialCode; if (this.props.separateDialCode) { dialCode = this.selectedCountryData.dialCode ? `+${this.selectedCountryData.dialCode}` : ''; if (prevCountry.dialCode) { delete this.wrapperClass[`iti-sdc-${(prevCountry.dialCode.length + 1)}`]; } if (dialCode) { this.wrapperClass[`iti-sdc-${dialCode.length}`] = true; } } let selectedIndex = 0; if (countryCode && countryCode !== 'auto') { for (let i = 0, max = this.countries.length; i < max; i++) { if (this.countries[i].iso2 === countryCode) { selectedIndex = i; } } selectedIndex += this.preferredCountries.length; } if (this.state.showDropdown) { findDOMNode(this.refs.telInput).focus(); } this.setState({ showDropdown: false, highlightedCountry: selectedIndex, countryCode, title, dialCode, }, () => { // and the input's placeholder this.updatePlaceholder(); // update the active list item this.wrapperClass.active = false; // on change flag, trigger a custom event // Allow Main app to do things when a country is selected if (!isInit && prevCountry.iso2 !== countryCode && typeof this.props.onSelectFlag === 'function') { const currentNumber = this.state.value; const regionCode = this.selectedCountryData.iso2; const status = this.isValidNumberForRegion(currentNumber, regionCode); this.props.onSelectFlag(status, currentNumber, this.selectedCountryData); } }); } handleOnBlur() { this.removeEmptyDialCode(); if (typeof this.props.onPhoneNumberBlur === 'function') { const value = this.state.value; const isValid = this.isValidNumber(value); const fullNumber = this.formatFullNumber(value); this.props.onPhoneNumberBlur( isValid, value, this.selectedCountryData, fullNumber, this.getExtension()); } } bindDocumentClick() { this.isOpening = true; document.querySelector('html').addEventListener('click', this.handleDocumentClick); } unbindDocumentClick() { document.querySelector('html').removeEventListener('click', this.handleDocumentClick); } clickSelectedFlag() { if (!this.state.showDropdown && !this.state.disabled && !this.state.readonly) { this.setState({ showDropdown: true, offsetTop: utils.offset(findDOMNode(this.refs.telInput)).top, outerHeight: utils.getOuterHeight(findDOMNode(this.refs.telInput)), }, () => { const highlightItem = findDOMNode(this.refs.flagDropDown).querySelector('.highlight'); if (highlightItem) { this.scrollTo(highlightItem, true); } }); } } // update the input placeholder to an // example number from the currently selected country updatePlaceholder() { if (this.initialPlaceholder) { this.setState({ placeholder: this.initialPlaceholder, }); } else if (window.intlTelInputUtils && this.props.autoPlaceholder && this.selectedCountryData) { const numberType = window.intlTelInputUtils.numberType[this.props.numberType]; let placeholder = this.selectedCountryData.iso2 ? window.intlTelInputUtils.getExampleNumber(this.selectedCountryData.iso2, this.nationalMode, numberType) : ''; placeholder = this.beforeSetNumber(placeholder); if (typeof this.props.customPlaceholder === 'function') { placeholder = this.props.customPlaceholder(placeholder, this.selectedCountryData); } this.setState({ placeholder, }); } } toggleDropdown(status) { this.setState({ showDropdown: !!status, }, () => { if (!this.state.showDropdown) { this.unbindDocumentClick(); } }); } // check if an element is visible within it's container, else scroll until it is scrollTo(element, middle) { try { const container = findDOMNode(this.refs.flagDropDown).querySelector('.country-list'); const containerHeight = parseFloat( window.getComputedStyle(container).getPropertyValue('height'), 10); const containerTop = utils.offset(container).top; const containerBottom = containerTop + containerHeight; const elementHeight = utils.getOuterHeight(element); const elementTop = utils.offset(element).top; const elementBottom = elementTop + elementHeight; const middleOffset = (containerHeight / 2) - (elementHeight / 2); let newScrollTop = elementTop - containerTop + container.scrollTop; if (elementTop < containerTop) { // scroll up if (middle) { newScrollTop -= middleOffset; } container.scrollTop = newScrollTop; } else if (elementBottom > containerBottom) { // scroll down if (middle) { newScrollTop += middleOffset; } const heightDifference = containerHeight - elementHeight; container.scrollTop = newScrollTop - heightDifference; } } catch (err) { // do nothing } } // 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) { let dialCode = ''; // only interested in international numbers (starting with a plus) if (number.charAt(0) === '+') { let numericChars = ''; // iterate over chars for (let i = 0, max = number.length; i < max; i++) { const c = number.charAt(i); // if char is number if (utils.isNumeric(c)) { numericChars += c; // if current numericChars make a valid dial code if (this.countryCodes[numericChars]) { // store the actual raw string (useful for matching later) dialCode = number.substr(0, i + 1); } // longest dial code is 4 chars if (numericChars.length === 4) { break; } } } } return dialCode; } // get the input val, adding the dial code if separateDialCode is enabled getFullNumber() { const prefix = this.props.separateDialCode ? `+${this.selectedCountryData.dialCode}` : ''; return prefix + this.state.value; } // validate the input val - assumes the global function isValidNumber (from utilsScript) isValidNumber(number) { const val = utils.trim(number); const countryCode = (this.nationalMode) ? this.selectedCountryData.iso2 : ''; if (window.intlTelInputUtils) { return window.intlTelInputUtils.isValidNumber(val, countryCode); } return false; } isValidNumberForRegion(number, regionCode) { const val = utils.trim(number); if (window.intlTelInputUtils) { return window.intlTelInputUtils.isValidNumberForRegion(val, regionCode); } return false; } formatFullNumber(number) { return window.intlTelInputUtils ? this.getNumber(window.intlTelInputUtils.numberFormat.INTERNATIONAL) : number; } notifyPhoneNumberChange(newNumber) { if (typeof this.props.onPhoneNumberChange === 'function') { const isValid = this.isValidNumber(newNumber); const fullNumber = this.formatFullNumber(newNumber); this.props.onPhoneNumberChange( isValid, newNumber, this.selectedCountryData, fullNumber, this.getExtension()); } } // remove the dial code if separateDialCode is enabled beforeSetNumber(number) { if (this.props.separateDialCode) { let dialCode = this.getDialCode(number); if (dialCode) { // US dialCode is "+1", which is what we want // CA dialCode is "+1 123", which is wrong - should be "+1" // (as it has multiple area codes) // AS dialCode is "+1 684", which is what we want // Solution: if the country has area codes, then revert to just the dial code if (this.selectedCountryData.areaCodes !== null) { dialCode = `+${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 etc const start = number[dialCode.length] === ' ' || number[dialCode.length] === '-' ? dialCode.length + 1 : dialCode.length; number = number.substr(start); } } return this.cap(number); } handleWindowScroll() { this.setState({ showDropdown: false, }, () => { window.removeEventListener('scroll', this.handleDocumentScroll); }); } handleDocumentKeyDown(e) { let queryTimer; // prevent down key from scrolling the whole page, // and enter key from submitting a form etc e.preventDefault(); if (e.which === this.keys.UP || e.which === this.keys.DOWN) { // up and down to navigate this.handleUpDownKey(e.which); } else if (e.which === this.keys.ENTER) { // enter to select this.handleEnterKey(); } else if (e.which === this.keys.ESC) { // esc to close this.setState({ showDropdown: false, }); } else if ((e.which >= this.keys.A && e.which <= this.keys.Z) || e.which === this.keys.SPACE) { // upper case letters (note: keyup/keydown only return upper case letters) // jump to countries that start with the query string if (queryTimer) { clearTimeout(queryTimer); } if (!this.query) { this.query = ''; } this.query += String.fromCharCode(e.which); this.searchForCountry(this.query); // if the timer hits 1 second, reset the query queryTimer = setTimeout(() => { this.query = ''; }, 1000); } } handleDocumentClick(e) { // Click at the outside of country list if (e.target.getAttribute('class') === null || (e.target.getAttribute('class') && e.target.getAttribute('class').indexOf('country') === -1)) { this.isOpening = false; } if (!this.isOpening) { this.toggleDropdown(false); } this.isOpening = false; } handleInputChange(e) { this.setState({ value: e.target.value, }, () => { this.updateFlagFromNumber(this.state.value); }); } changeHighlightCountry(showDropdown, selectedIndex) { this.setState({ showDropdown, highlightedCountry: selectedIndex, }); } render() { this.wrapperClass[this.props.css[0]] = true; const inputClass = this.props.css[1]; if (this.state.showDropdown) { this.wrapperClass.expanded = true; } let wrapperClass = classNames(this.wrapperClass); const titleTip = (this.selectedCountryData) ? `${this.selectedCountryData.name}: +${this.selectedCountryData.dialCode}` : 'Unknown'; return ( <div className={wrapperClass}> <FlagDropDown ref="flagDropDown" allowDropdown={this.allowDropdown} dropdownContainer={this.dropdownContainer} separateDialCode={this.props.separateDialCode} dialCode={this.state.dialCode} clickSelectedFlag={this.clickSelectedFlag} setFlag={this.setFlag} countryCode={this.state.countryCode} isMobile={this.isMobile} handleSelectedFlagKeydown={this.handleSelectedFlagKeydown} changeHighlightCountry={this.changeHighlightCountry} countries={this.countries} showDropdown={this.state.showDropdown} inputTop={this.state.offsetTop} inputOuterHeight={this.state.outerHeight} preferredCountries={this.preferredCountries} highlightedCountry={this.state.highlightedCountry} titleTip={titleTip} /> <TelInput ref="telInput" handleInputChange={this.handleInputChange} handleOnBlur={this.handleOnBlur} className={inputClass} disabled={this.state.disabled} readonly={this.state.readonly} fieldName={this.props.fieldName} fieldId={this.props.fieldId} value={this.state.value} placeholder={this.state.placeholder} autoFocus={this.props.autoFocus} /> </div> ); } }