react-telephone-input
Version:
React component for entering and validating international telephone numbers
694 lines (580 loc) • 23.6 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var R = _interopDefault(require('cramda'));
var VirtualList = _interopDefault(require('react-tiny-virtual-list'));
var debounce = _interopDefault(require('debounce'));
var memoize = _interopDefault(require('lodash.memoize'));
var React = require('react');
var classNames = _interopDefault(require('classnames'));
var enhanceWithClickOutside = _interopDefault(require('react-click-outside'));
var countryData = _interopDefault(require('country-telephone-data'));
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
subClass.__proto__ = superClass;
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
var first = R.first,
tail = R.tail;
function formatNumber(text, pattern, autoFormat) {
if (autoFormat === void 0) {
autoFormat = false;
}
if (!text || text.length === 0) {
return '+';
} // for all strings with length less than 3, just return it (1, 2 etc.)
// also return the same text if the selected country has no fixed format
if (text && text.length < 2 || !pattern || !autoFormat) {
return "+" + text;
}
var formattedObject = pattern.split('').reduce(function (acc, character) {
if (acc.remainingText.length === 0) {
return acc;
}
if (character !== '.') {
return {
formattedText: acc.formattedText + character,
remainingText: acc.remainingText
};
}
return {
formattedText: acc.formattedText + first(acc.remainingText),
remainingText: tail(acc.remainingText)
};
}, {
formattedText: '',
remainingText: text.split('')
});
return formattedObject.formattedText + formattedObject.remainingText.join('');
}
function replaceCountryCode(currentSelectedCountry, nextSelectedCountry, number) {
var dialCodeRegex = RegExp("^(" + currentSelectedCountry.dialCode + ")");
var codeToBeReplaced = number.match(dialCodeRegex);
var newNumber = number.replace(dialCodeRegex, nextSelectedCountry.dialCode);
if (codeToBeReplaced === null && newNumber === number) {
return nextSelectedCountry.dialCode + number;
}
return newNumber;
}
function isNumberValid(inputNumber) {
var countries = countryData.allCountries;
return R.any(function (country) {
return R.startsWith(country.dialCode, inputNumber) || R.startsWith(inputNumber, country.dialCode);
}, countries);
}
// memoize results based on the first 5/6 characters. That is all that matters
var find = R.find,
propEq = R.propEq,
startsWith = R.startsWith;
var allCountries = countryData.allCountries,
allCountryCodes = countryData.allCountryCodes;
function guessSelectedCountry(inputNumber, props) {
var defaultCountry = props.defaultCountry;
var onlyCountries = props.onlyCountries;
var secondBestGuess = find(propEq('iso2', defaultCountry), allCountries) || onlyCountries[0];
var inputNumberForCountries = inputNumber.substr(0, 4);
var bestGuess;
if (inputNumber.trim() !== '') {
bestGuess = onlyCountries.reduce(function (selectedCountry, country) {
// if the country dialCode exists WITH area code
if (allCountryCodes[inputNumberForCountries] && allCountryCodes[inputNumberForCountries][0] === country.iso2) {
return country; // if the selected country dialCode is there with the area code
} else if (allCountryCodes[inputNumberForCountries] && allCountryCodes[inputNumberForCountries][0] === selectedCountry.iso2) {
return selectedCountry; // else do the original if statement
}
if (startsWith(country.dialCode, inputNumber)) {
if (country.dialCode.length > selectedCountry.dialCode.length) {
return country;
}
if (country.dialCode.length === selectedCountry.dialCode.length && country.priority < selectedCountry.priority) {
return country;
}
}
return selectedCountry;
}, {
dialCode: '',
priority: 10001
});
} else {
return secondBestGuess;
}
if (!bestGuess || !bestGuess.name) {
return secondBestGuess;
}
return bestGuess;
}
var find$1 = R.find,
propEq$1 = R.propEq,
equals = R.equals,
findIndex = R.findIndex,
startsWith$1 = R.startsWith;
var allCountries$1 = countryData.allCountries,
iso2Lookup = countryData.iso2Lookup;
var isModernBrowser = true;
if (typeof document !== 'undefined') {
isModernBrowser = /*#__PURE__*/Boolean( /*#__PURE__*/document.createElement('input').setSelectionRange);
} else {
isModernBrowser = true;
}
var keys = {
UP: 38,
DOWN: 40,
RIGHT: 39,
LEFT: 37,
ENTER: 13,
ESC: 27,
PLUS: 43,
A: 65,
Z: 90,
SPACE: 32
};
function getDropdownListWidth() {
var defaultWidth = 400;
var horizontalMargin = 20;
if (window.innerWidth - horizontalMargin < defaultWidth) {
return window.innerWidth - horizontalMargin;
} else {
return defaultWidth;
}
}
var ReactTelephoneInput = /*#__PURE__*/function (_Component) {
_inheritsLoose(ReactTelephoneInput, _Component);
function ReactTelephoneInput(props) {
var _this;
_this = _Component.call(this, props) || this;
_this.numberInputRef = null; // put the cursor to the end of the input (usually after a focus event)
_this._cursorToEnd = function (skipFocus) {
if (skipFocus === void 0) {
skipFocus = false;
}
var input = _this.numberInputRef;
if (skipFocus) {
_this._fillDialCode();
} else {
if (input) {
input.focus();
}
if (isModernBrowser && input) {
var len = input.value.length;
input.setSelectionRange(len, len);
}
}
};
_this.handleFlagDropdownClick = function (e) {
if (_this.props.disabled) {
return;
}
e.preventDefault();
var preferredCountries = _this.state.preferredCountries;
var selectedCountry = _this.state.selectedCountry;
var onlyCountries = _this.props.onlyCountries;
var highlightCountryIndex = findIndex(propEq$1('iso2', selectedCountry.iso2), preferredCountries.concat(onlyCountries)); // need to put the highlight on the current selected country if the dropdown is going to open up
_this.setState({
showDropDown: !_this.state.showDropDown,
highlightCountryIndex: highlightCountryIndex
});
};
_this.handleInput = function (event) {
var formattedNumber = '+';
var newSelectedCountry = _this.state.selectedCountry;
var freezeSelection = _this.state.freezeSelection; // if the input is the same as before, must be some special key like enter, alt, command etc.
if (event.target.value === _this.state.formattedNumber) {
return;
}
if (event.preventDefault) {
event.preventDefault();
event.nativeEvent.preventDefault();
}
if (event.target.value && event.target.value.length > 0) {
// before entering the number in new format,
// lets check if the dial code now matches some other country
// replace all non-numeric characters from the input string
var inputNumber = event.target.value.replace(/\D/g, ''); // we don't need to send the whole number to guess the country...
// only the first 6 characters are enough
// the guess country function can then use memoization much more effectively
// since the set of input it gets has drastically reduced
if (!_this.state.freezeSelection || newSelectedCountry.dialCode.length > inputNumber.length) {
newSelectedCountry = guessSelectedCountry(inputNumber.substring(0, 6), _this.props);
freezeSelection = false;
}
formattedNumber = formatNumber(inputNumber, newSelectedCountry && newSelectedCountry.format ? newSelectedCountry.format : null, _this.props.autoFormat);
}
var caretPosition = event.target.selectionStart || 0;
var oldFormattedText = _this.state.formattedNumber;
var diff = formattedNumber.length - oldFormattedText.length;
var selectedCountry = newSelectedCountry.dialCode.length > 0 ? newSelectedCountry : _this.state.selectedCountry;
_this.setState({
formattedNumber: formattedNumber,
freezeSelection: freezeSelection,
selectedCountry: selectedCountry
}, function () {
if (isModernBrowser) {
if (caretPosition === 1 && formattedNumber.length === 2) {
caretPosition += 1;
}
if (diff > 0) {
caretPosition -= diff;
}
if (caretPosition > 0 && oldFormattedText.length >= formattedNumber.length) {
if (_this.numberInputRef) {
_this.numberInputRef.setSelectionRange(caretPosition, caretPosition);
}
}
}
if (_this.props.onChange) {
_this.props.onChange(formattedNumber, selectedCountry);
}
});
};
_this.handleInputClick = function () {
_this.setState({
showDropDown: false
});
};
_this.handleFlagItemClick = function (country) {
var onlyCountries = _this.props.onlyCountries;
var currentSelectedCountry = _this.state.selectedCountry;
var nextSelectedCountry = find$1(function (c) {
return c.iso2 === country.iso2;
}, onlyCountries); // tiny optimization
if (nextSelectedCountry && currentSelectedCountry.iso2 !== nextSelectedCountry.iso2) {
var newNumber = replaceCountryCode(currentSelectedCountry, nextSelectedCountry, _this.state.formattedNumber.replace(/\D/g, ''));
var formattedNumber = formatNumber(newNumber, nextSelectedCountry.format, _this.props.autoFormat);
_this.setState({
showDropDown: false,
selectedCountry: nextSelectedCountry,
freezeSelection: true,
formattedNumber: formattedNumber
}, function () {
_this._cursorToEnd();
if (_this.props.onChange) {
_this.props.onChange(formattedNumber, nextSelectedCountry);
}
});
} else {
_this.setState({
showDropDown: false
});
}
};
_this.handleInputFocus = function () {
// trigger parent component's onFocus handler
if (typeof _this.props.onFocus === 'function') {
_this.props.onFocus(_this.state.formattedNumber, _this.state.selectedCountry);
}
_this._fillDialCode();
};
_this._fillDialCode = function () {
var selectedCountry = _this.state.selectedCountry; // if the input is blank, insert dial code of the selected country
if (_this.numberInputRef && _this.numberInputRef.value === '+') {
_this.setState({
formattedNumber: "+" + selectedCountry.dialCode
});
}
};
_this._getHighlightCountryIndex = function (direction) {
var onlyCountries = _this.props.onlyCountries;
var _this$state = _this.state,
highlightCountryIndex = _this$state.highlightCountryIndex,
preferredCountries = _this$state.preferredCountries; // had to write own function because underscore does not have findIndex. lodash has it
var newHighlightCountryIndex = highlightCountryIndex + direction;
if (newHighlightCountryIndex < 0 || newHighlightCountryIndex >= onlyCountries.length + preferredCountries.length) {
return newHighlightCountryIndex - direction;
}
return newHighlightCountryIndex;
}; // memoize search results... caching all the way
_this._searchCountry = memoize(function (queryString) {
var onlyCountries = _this.props.onlyCountries;
if (!queryString || queryString.length === 0) {
return null;
} // don't include the preferred countries in search
var probableCountries = onlyCountries.filter(function (country) {
return country.name ? startsWith$1(queryString.toLowerCase(), country.name.toLowerCase()) : false;
}, _assertThisInitialized(_this));
return probableCountries[0];
});
_this.searchCountry = function () {
var onlyCountries = _this.props.onlyCountries;
var probableCandidate = _this._searchCountry(_this.state.queryString) || onlyCountries[0];
var probableCandidateIndex = findIndex(propEq$1('iso2', probableCandidate.iso2), _this.props.onlyCountries) + _this.state.preferredCountries.length;
_this.setState({
queryString: '',
highlightCountryIndex: probableCandidateIndex
});
};
_this.handleKeydown = function (event) {
var onlyCountries = _this.props.onlyCountries;
if (!_this.state.showDropDown || event.metaKey || event.altKey) {
return;
} // ie hack
if (event.preventDefault) {
event.preventDefault();
event.nativeEvent.preventDefault();
}
var _moveHighlight = function _moveHighlight(direction) {
var highlightCountryIndex = _this._getHighlightCountryIndex(direction);
_this.setState({
highlightCountryIndex: highlightCountryIndex
});
};
switch (event.which) {
case keys.DOWN:
_moveHighlight(1);
break;
case keys.UP:
_moveHighlight(-1);
break;
case keys.ENTER:
_this.handleFlagItemClick(_this.state.preferredCountries.concat(onlyCountries)[_this.state.highlightCountryIndex]);
break;
case keys.ESC:
_this.setState({
showDropDown: false
}, _this._cursorToEnd);
break;
default:
if (event.which >= keys.A && event.which <= keys.Z || event.which === keys.SPACE) {
_this.setState({
queryString: _this.state.queryString + String.fromCharCode(event.which)
}, _this.state.debouncedQueryStingSearcher);
}
}
};
_this.handleInputKeyDown = function (event) {
if (event.which === keys.ENTER && typeof _this.props.onEnterKeyPress === 'function') {
_this.props.onEnterKeyPress(event);
}
};
_this.handleClickOutside = function () {
if (_this.state.showDropDown) {
_this.setState({
showDropDown: false
});
}
};
_this.getCountryDropDownList = function () {
var onlyCountries = _this.props.onlyCountries;
var _this$state2 = _this.state,
highlightCountryIndex = _this$state2.highlightCountryIndex,
preferredCountries = _this$state2.preferredCountries;
var data = preferredCountries.concat(onlyCountries);
return React.createElement(VirtualList, {
width: getDropdownListWidth(),
height: 300,
itemCount: data.length,
itemSize: 40,
style: _this.props.listStyle,
className: "country-list",
scrollToIndex: highlightCountryIndex,
scrollToAlignment: 'center',
renderItem: function renderItem(_ref) {
var index = _ref.index,
style = _ref.style;
var country = data[index];
var itemClasses = classNames(_this.props.listItemClassName, {
preferred: findIndex(propEq$1('iso2', country.iso2), _this.state.preferredCountries) >= 0,
highlight: _this.state.highlightCountryIndex === index
});
var inputFlagClasses = "flag " + country.iso2;
return React.createElement("div", {
key: "flag_no_" + index,
"data-flag-key": "flag_no_" + index,
className: itemClasses,
"data-dial-code": country.dialCode,
"data-country-code": country.iso2,
onClick: _this.handleFlagItemClick.bind(_assertThisInitialized(_this), country),
style: style,
title: country.name + " - " + country.dialCode,
"data-test-id": "src_reacttelephoneinput_test_id_0"
}, React.createElement("div", {
className: inputFlagClasses,
style: _this.getFlagStyle(),
"data-test-id": "src_reacttelephoneinput_test_id_1"
}), React.createElement("span", {
className: "country-name",
"data-test-id": "src_reacttelephoneinput_test_id_2"
}, country.name), React.createElement("span", {
className: "dial-code",
"data-test-id": "src_reacttelephoneinput_test_id_3"
}, "+" + country.dialCode));
}
});
};
_this.getFlagStyle = function () {
if (_this.props.flagsImagePath) {
return {
backgroundImage: "url(" + _this.props.flagsImagePath + ")"
};
}
return {};
};
_this.handleInputBlur = function () {
var selectedCountry = _this.state.selectedCountry;
if (typeof _this.props.onBlur === 'function') {
_this.props.onBlur(_this.state.formattedNumber, selectedCountry);
}
};
_this.handleFlagKeyDown = function (event) {
// only trigger dropdown click if the dropdown is not already open.
// it will otherwise interfere with key up/down of list
if (event.which === keys.DOWN && _this.state.showDropDown === false) {
_this.handleFlagDropdownClick(event);
}
}; // eslint-disable-next-line
var preferredCountriesFromProps = props.preferredCountries;
var preferredCountries = preferredCountriesFromProps.map(function (iso2) {
return Object.prototype.hasOwnProperty.call(iso2Lookup, iso2) ? allCountries$1[iso2Lookup[iso2]] : null;
}).filter(function (val) {
return val !== null;
});
_this.state = {
firstCall: true,
preferredCountries: preferredCountries,
showDropDown: false,
queryString: '',
freezeSelection: false,
debouncedQueryStingSearcher: debounce(_this.searchCountry, 600),
formattedNumber: '',
highlightCountryIndex: 0
};
return _this;
}
var _proto = ReactTelephoneInput.prototype;
_proto.componentDidMount = function componentDidMount() {
this._cursorToEnd(true);
};
_proto.shouldComponentUpdate = function shouldComponentUpdate(nextProps, nextState) {
return !equals(nextProps, this.props) || !equals(nextState, this.state);
};
ReactTelephoneInput.getDerivedStateFromProps = function getDerivedStateFromProps(props, state) {
var inputNumber;
var onlyCountries = props.onlyCountries;
var showDropDown = state.showDropDown,
preferredCountries = state.preferredCountries,
selectedCountry = state.selectedCountry; // don't calculate new state if the dropdown is open. We might be changing
// the highlightCountryIndex using our keys
if (showDropDown) {
return state;
}
if (props.value) {
inputNumber = props.value;
} else if (props.initialValue && state.firstCall) {
inputNumber = props.initialValue;
} else if (props.value === null) {
// just clear the value
inputNumber = '';
} else if (state && state.formattedNumber && state.formattedNumber.length > 0) {
inputNumber = state.formattedNumber;
} else {
inputNumber = '';
}
var selectedCountryGuess = guessSelectedCountry(inputNumber.replace(/\D/g, ''), props); // if the guessed country has the same dialCode as the selected country in
// our state, we give preference to the already selected country
if (selectedCountry && selectedCountryGuess.dialCode === selectedCountry.dialCode) {
selectedCountryGuess = selectedCountry;
}
var selectedCountryGuessIndex = findIndex(propEq$1('iso2', selectedCountryGuess.iso2), preferredCountries.concat(onlyCountries));
var formattedNumber = formatNumber(inputNumber.replace(/\D/g, ''), selectedCountryGuess && selectedCountryGuess.format ? selectedCountryGuess.format : null, props.autoFormat);
return {
firstCall: false,
selectedCountry: selectedCountryGuess,
highlightCountryIndex: selectedCountryGuessIndex,
formattedNumber: formattedNumber
};
};
_proto.render = function render() {
var _this2 = this;
var isValid = this.props.isValid;
var selectedCountry = this.state.selectedCountry;
var arrowClasses = classNames({
arrow: true,
up: this.state.showDropDown
});
var inputClasses = classNames({
'form-control': true,
'invalid-number': !isValid(this.state.formattedNumber.replace(/\D/g, ''))
});
var flagViewClasses = classNames({
'flag-dropdown': true,
'open-dropdown': this.state.showDropDown
});
var inputFlagClasses = "flag " + selectedCountry.iso2;
var buttonProps = this.props.buttonProps;
var otherProps = this.props.inputProps;
if (otherProps && this.props.inputId) {
otherProps.id = this.props.inputId;
}
return React.createElement("div", {
className: classNames('react-tel-input', this.props.classNames, this.props.className),
"data-test-id": "src_reacttelephoneinput_test_id_4"
}, React.createElement("div", {
className: flagViewClasses,
onKeyDown: this.handleKeydown,
"data-test-id": "src_reacttelephoneinput_test_id_6"
}, React.createElement("button", Object.assign({
onClick: this.handleFlagDropdownClick,
className: "selected-flag",
title: selectedCountry.name + ": + " + selectedCountry.dialCode,
"data-test-id": "src_reacttelephoneinput_test_id_7",
onKeyDown: this.handleFlagKeyDown,
type: 'button'
}, buttonProps), React.createElement("div", {
className: inputFlagClasses,
style: this.getFlagStyle(),
"data-test-id": "src_reacttelephoneinput_test_id_8"
}, React.createElement("div", {
className: arrowClasses,
"data-test-id": "src_reacttelephoneinput_test_id_9"
}))), this.state.showDropDown ? this.getCountryDropDownList() : ''), React.createElement("input", Object.assign({
onChange: this.handleInput,
onClick: this.handleInputClick,
onFocus: this.handleInputFocus,
onBlur: this.handleInputBlur,
onKeyDown: this.handleInputKeyDown,
value: this.state.formattedNumber,
ref: function ref(node) {
_this2.numberInputRef = node;
},
type: "tel",
className: inputClasses,
autoComplete: this.props.autoComplete,
pattern: this.props.pattern,
required: this.props.required,
placeholder: this.props.placeholder,
disabled: this.props.disabled
}, otherProps, {
"data-test-id": "src_reacttelephoneinput_test_id_5"
})));
};
return ReactTelephoneInput;
}(React.Component);
ReactTelephoneInput.defaultProps = {
autoFormat: true,
onlyCountries: allCountries$1,
defaultCountry: allCountries$1[0].iso2,
isValid: isNumberValid,
flagsImagePath: 'flags.png',
onEnterKeyPress: function onEnterKeyPress() {},
preferredCountries: [],
disabled: false,
placeholder: '+1 (702) 123-4567',
autoComplete: 'tel',
required: false,
inputProps: {},
buttonProps: {},
listItemClassName: 'country',
listStyle: {
zIndex: 20,
backgroundColor: 'white'
}
};
var ReactTelephoneInput$1 = /*#__PURE__*/enhanceWithClickOutside(ReactTelephoneInput);
exports.ReactTelephoneInput = ReactTelephoneInput;
exports.default = ReactTelephoneInput$1;
//# sourceMappingURL=react-telephone-input.cjs.development.js.map