react-phone-number-input
Version:
Telephone input for React
938 lines (744 loc) • 28.4 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _slicedToArray2 = require('babel-runtime/helpers/slicedToArray');
var _slicedToArray3 = _interopRequireDefault(_slicedToArray2);
var _keys = require('babel-runtime/core-js/object/keys');
var _keys2 = _interopRequireDefault(_keys);
var _extends2 = require('babel-runtime/helpers/extends');
var _extends3 = _interopRequireDefault(_extends2);
var _objectWithoutProperties2 = require('babel-runtime/helpers/objectWithoutProperties');
var _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2);
var _getIterator2 = require('babel-runtime/core-js/get-iterator');
var _getIterator3 = _interopRequireDefault(_getIterator2);
var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');
var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);
var _inherits2 = require('babel-runtime/helpers/inherits');
var _inherits3 = _interopRequireDefault(_inherits2);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _libphonenumberJs = require('libphonenumber-js');
var _inputFormat = require('input-format');
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _reactResponsiveUi = require('./react-responsive-ui');
var _countryNames = require('./country names.json');
var _countryNames2 = _interopRequireDefault(_countryNames);
var _internationalIcon = require('./international icon');
var _internationalIcon2 = _interopRequireDefault(_internationalIcon);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// A list of all country codes
var all_countries = [];
// Country code to country name map
// Not importing here directly from `react-responsive-ui` npm package
// just to reduce the overall bundle size.
var default_dictionary = {
International: 'International'
};
// Populate `all_countries` and `default_dictionary`
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = (0, _getIterator3.default)(_countryNames2.default), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var item = _step.value;
var _item = (0, _slicedToArray3.default)(item, 2);
var code = _item[0];
var name = _item[1];
all_countries.push(code.toUpperCase());
default_dictionary[code.toUpperCase()] = name;
}
// Allows passing custom `libphonenumber-js` metadata
// to reduce the overall bundle size.
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
var Input = function (_Component) {
(0, _inherits3.default)(Input, _Component);
function Input(props) {
(0, _classCallCheck3.default)(this, Input);
var _this = (0, _possibleConstructorReturn3.default)(this, (Input.__proto__ || (0, _getPrototypeOf2.default)(Input)).call(this, props));
_this.state = {};
var countries = props.countries;
var value = props.value;
var dictionary = props.dictionary;
var international = props.international;
var internationalIcon = props.internationalIcon;
var flags = props.flags;
var country = props.country;
// Autodetect country if value is set
// and is international (which it should be)
if (!country && value && value[0] === '+') {
// Will be left `undefined` in case of non-detection
country = (0, _libphonenumberJs.parse)(value).country;
}
// If there will be no "International" option
// then a `country` must be selected.
if (!should_add_international_option(props) && !country) {
country = countries[0];
}
// Set the currently selected country
_this.state.country_code = country;
// If a phone number `value` is passed then format it
if (value) {
// Set the currently entered `value`
_this.state.value = _this.correct_initial_value_if_neccessary(value, country);
}
// `<Select/>` options
_this.select_options = [];
// Add the "International" option to the country list (if suitable)
if (should_add_international_option(props)) {
_this.select_options.push({
label: from_dictionary('International', props),
icon: flags === false ? undefined : internationalIcon
});
}
// Add a `<Select/>` option for each country
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = (0, _getIterator3.default)(countries), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var country_code = _step2.value;
_this.select_options.push({
value: country_code,
label: from_dictionary(country_code, props),
icon: get_country_option_icon(country_code, props)
});
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
_this.focus = _this.focus.bind(_this);
_this.on_key_down = _this.on_key_down.bind(_this);
_this.on_change = _this.on_change.bind(_this);
_this.set_country = _this.set_country.bind(_this);
_this.parse = _this.parse.bind(_this);
_this.format = _this.format.bind(_this);
_this.country_select_toggled = _this.country_select_toggled.bind(_this);
_this.on_country_select_tab_out = _this.on_country_select_tab_out.bind(_this);
return _this;
}
// If the country code is specified
// If the value has a leading plus sign
// If it converts into a valid national number for this country
// Then the value is set to be that national number
// Else
// The leading + sign is trimmed
// Else
// The value stays as it is
// Else
// If the value has a leading + sign
// The value stays as it is
// Else
// The + sign is prepended
//
(0, _createClass3.default)(Input, [{
key: 'correct_initial_value_if_neccessary',
value: function correct_initial_value_if_neccessary(value, country_code) {
var _props = this.props;
var metadata = _props.metadata;
var convertToNational = _props.convertToNational;
if (!value) {
return;
}
// If the country code is specified
if (country_code) {
// If the value has a leading plus sign
if (value[0] === '+' && convertToNational) {
// If it's a fully-entered phone number
// that converts into a valid national number for this country
// then the value is set to be that national number.
var parsed = (0, _libphonenumberJs.parse)(value, metadata);
if (parsed.country === country_code) {
return this.format(parsed.phone, country_code).text;
}
// Else the leading + sign is trimmed.
return value.slice(1);
}
// Else the value stays as it is
return value;
}
// The country is not set.
// Assuming that's an international phone number.
// If the value has a leading + sign
if (value[0] === '+') {
// The value is correct
return value;
}
// The + sign is prepended
return '+' + value;
}
}, {
key: 'set_country_code_value',
value: function set_country_code_value(country_code) {
var onCountryChange = this.props.onCountryChange;
if (onCountryChange) {
onCountryChange(country_code);
}
this.setState({ country_code: country_code });
}
// `<select/>` `onChange` handler
}, {
key: 'set_country',
value: function set_country(country_code, focus) {
var metadata = this.props.metadata;
// Previously selected country
var previous_country_code = this.state.country_code;
this.set_country_code_value(country_code);
// Adjust the phone number (`value`)
// according to the selected `country_code`
var value = this.state.value;
// If switching to a country from International
// If the international number belongs to this country
// Convert it to a national number
// Else
// Trim the leading + sign
//
// If switching to a country from a country
// If the value has a leading + sign
// If the international number belongs to this country
// Convert it to a national number
// Else
// Trim the leading + sign
// Else
// The value stays as it is
//
// If switching to International from a country
// If the value has a leading + sign
// The value stays as it is
// Else
// Take the international plaintext value
if (value) {
// If switching to a country from International
if (!previous_country_code && country_code) {
// The value is international plaintext
var parsed = (0, _libphonenumberJs.parse)(value, metadata);
// If it's for this country,
// then convert it to a national number
if (parsed.country === country_code) {
value = this.format(parsed.phone, country_code).text;
}
// Else just trim the + sign
else {
value = value.slice(1);
}
}
if (previous_country_code && country_code) {
if (value[0] === '+') {
var _parsed = (0, _libphonenumberJs.parse)(value, metadata);
if (_parsed.country === country_code) {
value = this.format(_parsed.phone, country_code).text;
} else {
value = value.slice(1);
}
}
}
// If switching to International from a country
if (previous_country_code && !country_code) {
// If no leading + sign
if (value[0] !== '+') {
// Take the international plaintext value
var national_number = parse_partial_number(value, previous_country_code, metadata).national_number;
value = (0, _libphonenumberJs.format)(national_number, previous_country_code, 'International_plaintext', metadata);
}
}
// Update the adjusted `value`
// and update `this.props.value` (in e.164 phone number format)
// according to the new `this.state.value`.
// (keep them in sync)
this.on_change(value, country_code);
}
// Focus the phone number input upon country selection
// (do it in a timeout because the `<input/>`
// is hidden while selecting a country)
if (focus !== false) {
setTimeout(this.focus, 0);
}
}
// `input-format` `parse` character function
// https://github.com/halt-hammerzeit/input-format
}, {
key: 'parse',
value: function parse(character, value) {
var countries = this.props.countries;
if (character === '+') {
// Only allow a leading `+`
if (!value) {
// If the "International" option is available
// then allow the leading `+` because it's meant to be this way.
//
// Otherwise, the leading `+` will either erase all subsequent digits
// (if they're not appropriate for the selected country)
// or the subsequent digits (if any) will join the `+`
// forming an international phone number. Because a user
// might be comfortable with entering an international phone number
// (i.e. with country code) rather than the local one.
// Therefore such possibility is given.
//
return character;
}
}
// For digits
else if (character >= '0' && character <= '9') {
var metadata = this.props.metadata;
var country_code = this.state.country_code;
// If the "International" option is not available
// and if the value has a leading `+`
// then it means that the phone number being entered
// is an international one, so only allow the country phone code
// for the selected country to be entered.
if (!should_add_international_option(this.props) && value && value[0] === '+') {
if (!could_phone_number_belong_to_country(value + character, country_code, metadata)) {
return;
}
return character;
}
return character;
}
}
// `input-format` `format` function
// https://github.com/halt-hammerzeit/input-format
}, {
key: 'format',
value: function format(value) {
var country_code = arguments.length <= 1 || arguments[1] === undefined ? this.state.country_code : arguments[1];
var metadata = this.props.metadata;
// `value` is already parsed input, i.e.
// either International plaintext phone number
// or just local phone number digits.
// "As you type" formatter
var formatter = new _libphonenumberJs.as_you_type(country_code, metadata);
// Is used to check if a country code can already be derived
this.formatter = formatter;
// Format phone number
var text = formatter.input(value);
return { text: text, template: formatter.template };
}
// Can be called externally
}, {
key: 'focus',
value: function focus() {
_reactDom2.default.findDOMNode(this.input).focus();
}
// `<input/>` `onKeyDown` handler
}, {
key: 'on_key_down',
value: function on_key_down(event) {
var onKeyDown = this.props.onKeyDown;
// Expand country `<select/>`` on "Down arrow" key press
if (event.keyCode === 40) {
this.select.toggle();
}
if (onKeyDown) {
onKeyDown(event);
}
}
// `<input/>` `onChange` handler.
// Updates `this.props.value` (in e.164 phone number format)
// according to the new `this.state.value`.
// (keeps them in sync)
}, {
key: 'on_change',
value: function on_change(value) {
var country_code = arguments.length <= 1 || arguments[1] === undefined ? this.state.country_code : arguments[1];
var _props2 = this.props;
var metadata = _props2.metadata;
var onChange = _props2.onChange;
// If the `<input/>` is empty then just exit
if (!value) {
this.setState({ value: value });
return onChange(value);
}
// For international phone number
if (value[0] === '+') {
// If an international phone number is being erased up to the first `+` sign
// or if an international phone number is just starting (with a `+` sign)
// then unset the current country because it's clear that a user intends to change it.
if (value.length === 1) {
country_code = undefined;
this.set_country_code_value(country_code);
}
// If a phone number is being input as an international one
// and the country code can already be derived,
// then switch the country.
// (`001` is a special "non-geograpical entity" code in `libphonenumber` library)
else if (this.formatter.country && this.formatter.country !== '001') {
country_code = this.formatter.country;
this.set_country_code_value(country_code);
}
}
// If "International" mode is selected
// and the `value` doesn't start with a + sign,
// then prepend it to the `value`.
else if (!country_code) {
value = '+' + value;
}
// Convert `value` to E.164 phone number format
// and write it to `this.props.value`.
onChange(e164(value, country_code, metadata));
// Update the `value`
this.setState({ value: value });
}
// When country `<select/>` is toggled
}, {
key: 'country_select_toggled',
value: function country_select_toggled(is_shown) {
this.setState({ country_select_is_shown: is_shown });
}
// Focuses the `<input/>` field
// on tab out of the country `<select/>`
}, {
key: 'on_country_select_tab_out',
value: function on_country_select_tab_out(event) {
event.preventDefault();
// Focus the phone number input upon country selection
// (do it in a timeout because the `<input/>`
// is hidden while selecting a country)
setTimeout(this.focus, 0);
}
// Can a user change the default country or not.
}, {
key: 'can_change_country',
value: function can_change_country() {
var countries = this.props.countries;
// If `countries` is empty,
// then only "International" option is available,
// so can't switch it.
//
// If `countries` is a single allowed country,
// then cant's switch it.
//
return countries.length > 1;
}
// Listen for default country property:
// if it is set after the page loads
// and the user hasn't selected a country yet
// then select the default country.
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(new_props) {
var _props3 = this.props;
var countries = _props3.countries;
var country = _props3.country;
var value = _props3.value;
// If the default country changed
// (e.g. in case of IP detection)
if (new_props.country !== country) {
// If the phone number input field is currently empty
// (e.g. not touched yet) then change the selected `country`
// to the newly passed one (e.g. as a result of a GeoIP query)
if (!value) {
// If the passed `country` allowed then update it
if (countries.indexOf(new_props.country) !== -1) {
// Set the new `country`
this.set_country(new_props.country, false);
}
}
}
}
}, {
key: 'render',
value: function render() {
var _this2 = this;
var _props4 = this.props;
var dictionary = _props4.dictionary;
var saveOnIcons = _props4.saveOnIcons;
var showCountrySelect = _props4.showCountrySelect;
var international = _props4.international;
var internationalIcon = _props4.internationalIcon;
var country = _props4.country;
var countries = _props4.countries;
var onCountryChange = _props4.onCountryChange;
var flags = _props4.flags;
var flagsPath = _props4.flagsPath;
var convertToNational = _props4.convertToNational;
var disabled = _props4.disabled;
var style = _props4.style;
var className = _props4.className;
var metadata = _props4.metadata;
var input_props = (0, _objectWithoutProperties3.default)(_props4, ['dictionary', 'saveOnIcons', 'showCountrySelect', 'international', 'internationalIcon', 'country', 'countries', 'onCountryChange', 'flags', 'flagsPath', 'convertToNational', 'disabled', 'style', 'className', 'metadata']);
var country_select_is_shown = this.state.country_select_is_shown;
var markup = _react2.default.createElement(
'div',
{ style: style, className: (0, _classnames2.default)('react-phone-number-input', className) },
showCountrySelect && this.can_change_country() && _react2.default.createElement(_reactResponsiveUi.Select, {
ref: function ref(_ref) {
return _this2.select = _ref;
},
value: this.state.country_code,
options: this.select_options,
onChange: this.set_country,
disabled: disabled,
onToggle: this.country_select_toggled,
onTabOut: this.on_country_select_tab_out,
autocomplete: true,
autocompleteShowAll: true,
concise: true,
focusUponSelection: false,
saveOnIcons: saveOnIcons,
name: input_props.name ? input_props.name + '__country' : undefined,
className: 'react-phone-number-input__country',
style: select_style }),
!country_select_is_shown && _react2.default.createElement(_inputFormat.ReactInput, (0, _extends3.default)({}, input_props, {
ref: function ref(_ref2) {
return _this2.input = _ref2;
},
value: this.state.value,
onChange: this.on_change,
disabled: disabled,
type: 'tel',
parse: this.parse,
format: this.format,
onKeyDown: this.on_key_down,
className: (0, _classnames2.default)('rrui__input', 'react-phone-number-input__phone'),
style: input_style }))
);
return markup;
}
}]);
return Input;
}(_react.Component);
// Parses a partially entered phone number
// and returns the national number so far.
// Not using `libphonenumber-js`'s `parse`
// function here because `parse` only works
// when the number is fully entered,
// and this one is for partially entered number.
Input.propTypes = {
// Phone number `value`.
// Is a plaintext international phone number
// (e.g. "+12223333333" for USA)
value: _react.PropTypes.string,
// This handler is called each time
// the phone number <input/> changes its textual value.
onChange: _react.PropTypes.func.isRequired,
// This `onBlur` interceptor is a workaround for `redux-form`,
// so that it gets a parsed `value` in its `onBlur` handler,
// not the formatted one.
// (`redux-form` passed `onBlur` to this component
// and this component intercepts that `onBlur`
// to make sure it works correctly with `redux-form`)
onBlur: _react.PropTypes.func,
// Set `onKeyDown` handler.
// Can be used in special cases to handle e.g. enter pressed
onKeyDown: _react.PropTypes.func,
// Disables both the <input/> and the <select/>
// (is `false` by default)
disabled: _react.PropTypes.bool.isRequired,
// Two-letter country code
// to be used as the default country
// for local (non-international) phone numbers.
country: _react.PropTypes.string,
// Is called when the selected country changes
// (either by a user manually, or by autoparsing
// an international phone number being input).
// This handler does not need to update the `country` property.
// It's simply a listener for those who might need that for whatever purpose.
onCountryChange: _react.PropTypes.func,
// Localization dictionary:
// `{ International: 'Международный', RU: 'Россия', US: 'США', ... }`
dictionary: _react.PropTypes.objectOf(_react.PropTypes.string),
// An optional list of allowed countries
countries: _react.PropTypes.arrayOf(_react.PropTypes.string).isRequired,
// Custom national flag icons
flags: _react.PropTypes.oneOfType([_react.PropTypes.objectOf(_react2.default.PropTypes.element), _react.PropTypes.bool]),
// A base URL path for national flag SVG icons.
// By default it uses the ones from `flag-icon-css` github repo.
flagsPath: _react.PropTypes.string.isRequired,
// If set to `false`, then country flags will be shown
// for all countries in the options list
// (not just for selected country).
saveOnIcons: _react.PropTypes.bool.isRequired,
// Whether to show country `<Select/>`
// (is `true` by default)
showCountrySelect: _react.PropTypes.bool.isRequired,
// Whether to add the "International" option
// to the list of countries.
international: _react.PropTypes.bool,
// Custom "International" phone number type icon.
internationalIcon: _react.PropTypes.element.isRequired,
// Should the initially passed phone number `value`
// be converted to a national phone number for its country.
// (is `true` by default)
convertToNational: _react.PropTypes.bool.isRequired,
// CSS style object
style: _react.PropTypes.object,
// CSS class
className: _react.PropTypes.string,
// `libphonenumber-js` metadata
metadata: _react.PropTypes.shape({
countries: _react.PropTypes.object.isRequired
}).isRequired
};
Input.defaultProps = {
// Is enabled
disabled: false,
// Include all countries by default
countries: all_countries,
// By default use the ones from `flag-icon-css` github repo.
flagsPath: 'https://lipis.github.io/flag-icon-css/flags/4x3/',
// Default international icon (globe)
internationalIcon: _react2.default.createElement(
'div',
{ className: 'react-phone-number-input__icon react-phone-number-input__icon--international' },
_react2.default.createElement(_internationalIcon2.default, null)
),
// Custom country names
dictionary: {},
// Don't show flags for all countries in the options list
// (show it just for selected country).
// (to save user's traffic because all flags are about 3 MegaBytes)
saveOnIcons: true,
// Show country `<Select/>` by default
showCountrySelect: true,
// Convert the initially passed phone number `value`
// to a national phone number for its country.
convertToNational: true
};
exports.default = Input;
function parse_partial_number(value, country_code, metadata) {
// "As you type" formatter
var formatter = new _libphonenumberJs.as_you_type(country_code, metadata);
// Input partially entered phone number
formatter.input(value);
// Return the parsed partial phone number
// (has `.national_number`, `.country`, etc)
return formatter;
}
// Converts `value` to E.164 phone number format
function e164(value, country_code, metadata) {
// If the phone number is being input in a country-specific format
// If the value has a leading + sign
// The value stays as it is
// Else
// The value is converted to international plaintext
// Else, the phone number is being input in an international format
// If the value has a leading + sign
// The value stays as it is
// Else
// The value is prepended with a + sign
if (country_code) {
if (value[0] === '+') {
return value;
}
var partial_national_number = parse_partial_number(value, country_code).national_number;
return (0, _libphonenumberJs.format)(partial_national_number, country_code, 'International_plaintext', metadata);
}
if (value[0] === '+') {
return value;
}
return '+' + value;
}
var select_style = {
display: 'inline-block',
verticalAlign: 'bottom'
};
var input_style = select_style;
// Gets country flag element by country code
function get_country_option_icon(country_code, _ref3) {
var flags = _ref3.flags;
var flagsPath = _ref3.flagsPath;
if (flags === false) {
return undefined;
}
if (flags && flags[country_code]) {
return flags[country_code];
}
return _react2.default.createElement('img', {
className: 'react-phone-number-input__icon',
src: '' + flagsPath + country_code.toLowerCase() + '.svg' });
}
// Whether to add the "International" option to the list of countries
function should_add_international_option(properties) {
var countries = properties.countries;
var international = properties.international;
// If this behaviour is explicitly set, then do as it says.
if (international !== undefined) {
return international;
}
// If `countries` is empty,
// then only "International" option is available, so add it.
if (countries.length === 0) {
return true;
}
// If `countries` is a single allowed country,
// then don't add the "International" option
// because it would make no sense.
if (countries.length === 1) {
return false;
}
// Show the "International" option by default
return true;
}
// Gets a text from dictionary
function from_dictionary(key, properties) {
var dictionary = properties.dictionary;
return dictionary[key] || default_dictionary[key];
}
// Is it possible that the partially entered phone number belongs to the given country
function could_phone_number_belong_to_country(phone_number, country_code, metadata) {
// Strip the leading `+`
var phone_number_digits = phone_number.slice(1);
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = (0, _getIterator3.default)((0, _keys2.default)(metadata.country_phone_code_to_countries)), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var country_phone_code = _step3.value;
var possible_country_phone_code = phone_number_digits.substring(0, country_phone_code.length);
if (country_phone_code.indexOf(possible_country_phone_code) === 0) {
// This country phone code is possible.
// Does the given country correspond to this country phone code.
if (metadata.country_phone_code_to_countries[country_phone_code].indexOf(country_code) >= 0) {
return true;
}
}
}
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
}
//# sourceMappingURL=input.js.map