react-phone-number-input
Version:
Telephone input for React
1,316 lines (1,068 loc) • 38.9 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _taggedTemplateLiteral2 = require('babel-runtime/helpers/taggedTemplateLiteral');
var _taggedTemplateLiteral3 = _interopRequireDefault(_taggedTemplateLiteral2);
var _extends2 = require('babel-runtime/helpers/extends');
var _extends3 = _interopRequireDefault(_extends2);
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 _templateObject = (0, _taggedTemplateLiteral3.default)(['\n\twrapper\n\t\t// Sometimes (e.g. when using mobile dropdown menus)\n\t\t// "position: relative" could be overridden to "static"\n\t\t// to allow for the menu stretching to full screen width.\n\t\t// Therefore it was moved to CSS from inline styles.\n\n\t\t-webkit-user-select : none\n\t\t-moz-user-select : none\n\t\t-ms-user-select : none\n\t\tuser-select : none\n\n\tlist\n\t\tlist-style-type : none\n\t\toverflow-x : hidden\n\n\tselected\n\t\tbox-sizing : border-box\n\n\tselected_flex_wrapper\n\t\tdisplay : flex\n\t\talign-items : center\n\n\tselected_label\n\t\tflex : 1\n\t\toverflow : hidden\n\t\ttext-overflow : ellipsis\n\n\tarrow\n\n\tseparator\n\t\tpadding : 0\n\t\tline-height : 0\n\t\tfont-size : 0\n\n\tlabel\n\t\tposition : absolute\n\t\twhite-space : nowrap\n\n\t\t-webkit-user-select : none\n\t\t-moz-user-select : none\n\t\t-ms-user-select : none\n\t\tuser-select : none\n\n\t\t// Vertically align\n\t\tdisplay : flex\n\t\talign-items : center\n\t\theight : 100%\n'], ['\n\twrapper\n\t\t// Sometimes (e.g. when using mobile dropdown menus)\n\t\t// "position: relative" could be overridden to "static"\n\t\t// to allow for the menu stretching to full screen width.\n\t\t// Therefore it was moved to CSS from inline styles.\n\n\t\t-webkit-user-select : none\n\t\t-moz-user-select : none\n\t\t-ms-user-select : none\n\t\tuser-select : none\n\n\tlist\n\t\tlist-style-type : none\n\t\toverflow-x : hidden\n\n\tselected\n\t\tbox-sizing : border-box\n\n\tselected_flex_wrapper\n\t\tdisplay : flex\n\t\talign-items : center\n\n\tselected_label\n\t\tflex : 1\n\t\toverflow : hidden\n\t\ttext-overflow : ellipsis\n\n\tarrow\n\n\tseparator\n\t\tpadding : 0\n\t\tline-height : 0\n\t\tfont-size : 0\n\n\tlabel\n\t\tposition : absolute\n\t\twhite-space : nowrap\n\n\t\t-webkit-user-select : none\n\t\t-moz-user-select : none\n\t\t-ms-user-select : none\n\t\tuser-select : none\n\n\t\t// Vertically align\n\t\tdisplay : flex\n\t\talign-items : center\n\t\theight : 100%\n']); // https://github.com/halt-hammerzeit/react-responsive-ui/blob/master/source/select.js
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _reactStyling = require('react-styling');
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _dom = require('./misc/dom');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// Possible enhancements:
//
// * If the menu is close to a screen edge,
// automatically reposition it so that it fits on the screen
// * Maybe show menu immediately above the toggler
// (like in Material design), not below it.
//
// https://material.google.com/components/menus.html
var value_prop_type = _react2.default.PropTypes.oneOfType([_react2.default.PropTypes.string, _react2.default.PropTypes.number, _react2.default.PropTypes.bool]);
var Select = function (_PureComponent) {
(0, _inherits3.default)(Select, _PureComponent);
function Select(props) {
(0, _classCallCheck3.default)(this, Select);
// Shouldn't memory leak because
// the set of options is assumed to be constant.
var _this = (0, _possibleConstructorReturn3.default)(this, (Select.__proto__ || (0, _getPrototypeOf2.default)(Select)).call(this, props));
_this.state = {};
_this.options = {};
_this.toggle = _this.toggle.bind(_this);
_this.document_clicked = _this.document_clicked.bind(_this);
_this.on_key_down = _this.on_key_down.bind(_this);
_this.on_autocomplete_input_change = _this.on_autocomplete_input_change.bind(_this);
_this.on_key_down_in_container = _this.on_key_down_in_container.bind(_this);
var value = props.value;
var autocomplete = props.autocomplete;
var options = props.options;
var children = props.children;
var menu = props.menu;
var toggler = props.toggler;
var onChange = props.onChange;
if (autocomplete) {
if (!options) {
throw new Error('"options" property is required for an "autocomplete" select');
}
_this.state.matching_options = _this.get_matching_options(options, value);
}
if (children && !menu) {
_react2.default.Children.forEach(children, function (element) {
if (!element.props.value) {
throw new Error('You must specify "value" prop on each child of <Select/>');
}
if (!element.props.label) {
throw new Error('You must specify "label" prop on each child of <Select/>');
}
});
}
if (menu && !toggler) {
throw new Error('Supply a "toggler" component when enabling "menu" in <Select/>');
}
if (!menu && !onChange) {
throw new Error('"onChange" property must be specified for <Select/>');
}
return _this;
}
// Client side rendering, javascript is enabled
(0, _createClass3.default)(Select, [{
key: 'componentDidMount',
value: function componentDidMount() {
document.addEventListener('click', this.document_clicked);
var fallback = this.props.fallback;
if (fallback) {
this.setState({ javascript: true });
}
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(previous_props, previous_state) {
var _state = this.state;
var expanded = _state.expanded;
var height = _state.height;
if (expanded !== previous_state.expanded) {
if (expanded && this.should_animate()) {
if (height === undefined) {
this.calculate_height();
}
}
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
document.removeEventListener('click', this.document_clicked);
}
}, {
key: 'render',
value: function render() {
var _this2 = this;
var _props = this.props;
var id = _props.id;
var upward = _props.upward;
var scroll = _props.scroll;
var children = _props.children;
var menu = _props.menu;
var toggler = _props.toggler;
var alignment = _props.alignment;
var autocomplete = _props.autocomplete;
var saveOnIcons = _props.saveOnIcons;
var fallback = _props.fallback;
var disabled = _props.disabled;
var placeholder = _props.placeholder;
var label = _props.label;
var error = _props.error;
var indicateInvalid = _props.indicateInvalid;
var style = _props.style;
var className = _props.className;
var _state2 = this.state;
var expanded = _state2.expanded;
var list_height = _state2.list_height;
var options = this.get_options();
var list_style = styles.list;
// Makes the options list scrollable (only when not in `autocomplete` mode).
if (this.is_scrollable() && this.state.list_height !== undefined) {
list_style = (0, _extends3.default)({}, list_style, { maxHeight: list_height + 'px' });
}
var overflow = scroll && options && this.overflown();
var list_items = void 0;
// If a list of options is supplied as an array of `{ value, label }`,
// then transform those elements to <buttons/>
if (options) {
list_items = options.map(function (_ref, index) {
var value = _ref.value;
var label = _ref.label;
var icon = _ref.icon;
return _this2.render_list_item({ index: index, value: value, label: label, icon: !saveOnIcons && icon, overflow: overflow });
});
}
// Else, if a list of options is supplied as a set of child React elements,
// then render those elements.
else {
list_items = _react2.default.Children.map(children, function (element, index) {
if (!element) {
return;
}
return _this2.render_list_item({ index: index, element: element });
});
}
var wrapper_style = (0, _extends3.default)({}, styles.wrapper, { textAlign: alignment });
var selected = this.get_selected_option();
var markup = _react2.default.createElement(
'div',
{
ref: function ref(_ref4) {
return _this2.select = _ref4;
},
onKeyDown: this.on_key_down_in_container,
style: style ? (0, _extends3.default)({}, wrapper_style, style) : wrapper_style,
className: (0, _classnames2.default)('rrui__select', {
'rrui__rich': fallback,
'rrui__select--upward': upward,
'rrui__select--expanded': expanded,
'rrui__select--collapsed': !expanded,
'rrui__select--disabled': disabled
}, className) },
!menu && this.render_selected_item(),
label && (this.get_selected_option() || placeholder) && _react2.default.createElement(
'label',
{
htmlFor: id,
className: (0, _classnames2.default)('rrui__input-label', {
'rrui__input-label--invalid': error && indicateInvalid
}),
style: styles.label },
label
),
menu && _react2.default.createElement(
'div',
{
ref: function ref(_ref2) {
return _this2.menu_toggler;
},
className: 'rrui__select__toggler' },
_react2.default.cloneElement(toggler, { onClick: this.toggle })
),
_react2.default.createElement(
'ul',
{
ref: function ref(_ref3) {
return _this2.list = _ref3;
},
style: list_style,
className: (0, _classnames2.default)('rrui__expandable', 'rrui__expandable--overlay', 'rrui__select__options', 'rrui__shadow', {
'rrui__expandable--expanded': expanded,
'rrui__select__options--expanded': expanded,
'rrui__select__options--left-aligned': alignment === 'left',
'rrui__select__options--right-aligned': alignment === 'right',
'rrui__select__options--simple-left-aligned': !children && alignment === 'left',
'rrui__select__options--simple-right-aligned': !children && alignment === 'right',
// CSS selector performance optimization
'rrui__select__options--upward': upward,
'rrui__select__options--downward': !upward
}) },
list_items
),
fallback && !this.state.javascript && this.render_static(),
error && indicateInvalid && _react2.default.createElement(
'div',
{ className: 'rrui__input-error' },
error
)
);
return markup;
}
}, {
key: 'render_list_item',
value: function render_list_item(_ref5) // , first, last
{
var _this3 = this;
var index = _ref5.index;
var element = _ref5.element;
var value = _ref5.value;
var label = _ref5.label;
var icon = _ref5.icon;
var overflow = _ref5.overflow;
var _props2 = this.props;
var disabled = _props2.disabled;
var menu = _props2.menu;
var scrollbarPadding = _props2.scrollbarPadding;
var _state3 = this.state;
var focused_option_value = _state3.focused_option_value;
var expanded = _state3.expanded;
// If a list of options is supplied as a set of child React elements,
// then extract values from their props.
if (element) {
value = element.props.value;
}
var is_focused = !menu && value === focused_option_value;
var item_style = void 0;
// on overflow the vertical scrollbar will take up space
// reducing padding-right and the only way to fix that
// is to add additional padding-right
//
// a hack to restore padding-right taken up by a vertical scrollbar
if (overflow && scrollbarPadding) {
item_style = { marginRight: (0, _dom.get_scrollbar_width)() + 'px' };
}
var button = void 0;
// If a list of options is supplied as a set of child React elements,
// then enhance those elements with extra props.
if (element) {
(function () {
var extra_props = {
style: item_style ? (0, _extends3.default)({}, item_style, element.props.style) : element.props.style,
className: (0, _classnames2.default)('rrui__select__option', {
'rrui__select__option--focused': is_focused
}, element.props.className)
};
var onClick = element.props.onClick;
extra_props.onClick = function (event) {
if (menu) {
_this3.toggle();
} else {
_this3.item_clicked(value, event);
}
if (onClick) {
onClick(event);
}
};
button = _react2.default.cloneElement(element, extra_props);
})();
}
// Else, if a list of options is supplied as an array of `{ value, label }`,
// then transform those options to <buttons/>
else {
button = _react2.default.createElement(
'button',
{
type: 'button',
onClick: function onClick(event) {
return _this3.item_clicked(value, event);
},
disabled: disabled,
tabIndex: '-1',
className: (0, _classnames2.default)('rrui__select__option', {
'rrui__select__option--focused': is_focused,
// CSS selector performance optimization
'rrui__select__option--disabled': disabled
}),
style: item_style },
icon && _react2.default.cloneElement(icon, { className: (0, _classnames2.default)(icon.props.className, 'rrui__select__option-icon') }),
label
);
}
var markup = _react2.default.createElement(
'li',
{
key: get_option_key(value),
ref: function ref(_ref6) {
return _this3.options[get_option_key(value)] = _ref6;
},
className: (0, _classnames2.default)('rrui__expandable__content', 'rrui__select__options-list-item', {
'rrui__select__separator-option': element && element.type === Select.Separator,
'rrui__expandable__content--expanded': expanded,
// CSS selector performance optimization
'rrui__select__options-list-item--expanded': expanded
}) },
button
);
return markup;
}
}, {
key: 'render_selected_item',
value: function render_selected_item() {
var _this4 = this;
var _props3 = this.props;
var children = _props3.children;
var value = _props3.value;
var placeholder = _props3.placeholder;
var label = _props3.label;
var disabled = _props3.disabled;
var autocomplete = _props3.autocomplete;
var concise = _props3.concise;
var _state4 = this.state;
var expanded = _state4.expanded;
var autocomplete_width = _state4.autocomplete_width;
var autocomplete_input_value = _state4.autocomplete_input_value;
var selected = this.get_selected_option();
var selected_label = this.get_selected_option_label();
var selected_text = selected ? selected_label : placeholder || label;
var style = styles.selected;
if (autocomplete && expanded) {
// style = { ...style, width: autocomplete_width + 'px' }
var _markup = _react2.default.createElement('input', {
type: 'text',
ref: function ref(_ref7) {
return _this4.autocomplete = _ref7;
},
placeholder: selected_text,
value: autocomplete_input_value,
onChange: this.on_autocomplete_input_change,
onKeyDown: this.on_key_down,
style: style,
className: (0, _classnames2.default)('rrui__input', 'rrui__select__selected', 'rrui__select__selected--autocomplete', {
'rrui__select__selected--nothing': !selected_label,
// CSS selector performance optimization
'rrui__select__selected--expanded': expanded,
'rrui__select__selected--disabled': disabled
}) });
return _markup;
}
var markup = _react2.default.createElement(
'button',
{
ref: function ref(_ref8) {
return _this4.selected = _ref8;
},
type: 'button',
disabled: disabled,
onClick: this.toggle,
onKeyDown: this.on_key_down,
style: style,
className: (0, _classnames2.default)('rrui__input', 'rrui__select__selected', {
'rrui__select__selected--nothing': !selected_label
}) },
_react2.default.createElement(
'div',
{
style: styles.selected_flex_wrapper,
className: 'rrui__select__selected-content' },
_react2.default.createElement(
'div',
{
style: styles.selected_label,
className: 'rrui__select__selected-label' },
concise && selected && selected.icon ? _react2.default.cloneElement(selected.icon, { title: selected_label }) : selected_text
),
_react2.default.createElement('div', {
className: (0, _classnames2.default)('rrui__select__arrow', {
// CSS selector performance optimization
'rrui__select__arrow--expanded': expanded
}),
style: styles.arrow })
)
);
return markup;
}
// supports disabled javascript
}, {
key: 'render_static',
value: function render_static() {
var _props4 = this.props;
var id = _props4.id;
var name = _props4.name;
var value = _props4.value;
var label = _props4.label;
var disabled = _props4.disabled;
var options = _props4.options;
var menu = _props4.menu;
var toggler = _props4.toggler;
var style = _props4.style;
var className = _props4.className;
var children = _props4.children;
if (menu) {
return _react2.default.createElement(
'div',
{ className: 'rrui__rich__fallback' },
toggler
);
}
var markup = _react2.default.createElement(
'div',
{ className: 'rrui__rich__fallback' },
_react2.default.createElement(
'select',
{
id: id,
name: name,
value: value === null ? undefined : value,
disabled: disabled,
onChange: function onChange(event) {},
style: style ? (0, _extends3.default)({}, style, { width: 'auto' }) : { width: 'auto' },
className: className },
options ? options.map(function (item, i) {
return _react2.default.createElement(
'option',
{
className: 'rrui__select__option',
key: i + ' ' + item.value,
value: item.value },
item.label
);
}) : _react2.default.Children.map(children, function (child) {
if (!child) {
return;
}
return _react2.default.createElement(
'option',
{
className: 'rrui__select__option',
key: child.props.value,
value: child.props.value },
child.props.label
);
})
)
);
return markup;
}
}, {
key: 'get_selected_option',
value: function get_selected_option() {
var value = this.props.value;
return this.get_option(value);
}
}, {
key: 'get_option',
value: function get_option(value) {
var _props5 = this.props;
var options = _props5.options;
var children = _props5.children;
if (options) {
return options.filter(function (x) {
return x.value === value;
})[0];
}
var option = void 0;
_react2.default.Children.forEach(children, function (child) {
if (child.props.value === value) {
option = child;
}
});
return option;
}
}, {
key: 'get_option_index',
value: function get_option_index(option) {
var _props6 = this.props;
var options = _props6.options;
var children = _props6.children;
if (options) {
return options.indexOf(option);
}
var option_index = void 0;
_react2.default.Children.forEach(children, function (child, index) {
if (child.props.value === option.value) {
option_index = index;
}
});
return option_index;
}
}, {
key: 'get_selected_option_label',
value: function get_selected_option_label() {
var options = this.props.options;
var selected = this.get_selected_option();
if (!selected) {
return;
}
if (options) {
return selected.label;
}
return selected.props.label;
}
}, {
key: 'overflown',
value: function overflown() {
var _props7 = this.props;
var options = _props7.options;
var maxItems = _props7.maxItems;
return options.length > maxItems;
}
}, {
key: 'scrollable_list_height',
value: function scrollable_list_height() {
var state = arguments.length <= 0 || arguments[0] === undefined ? this.state : arguments[0];
var maxItems = this.props.maxItems;
// (Adding vertical padding so that it shows these `maxItems` options fully)
return (state.height - 2 * state.vertical_padding) * (maxItems / this.get_options().length) + state.vertical_padding;
}
}, {
key: 'should_animate',
value: function should_animate() {
return true;
// return this.props.options.length >= this.props.transition_item_count_min
}
}, {
key: 'toggle',
value: function toggle(event) {
var _this5 = this;
var toggle_options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
if (event) {
// Don't navigate away when clicking links
event.preventDefault();
// Not discarding the click event because
// other expanded selects may be listening to it.
// // Discard the click event so that it won't reach `document` click listener
// event.stopPropagation() // doesn't work
// event.nativeEvent.stopImmediatePropagation()
}
var _props8 = this.props;
var disabled = _props8.disabled;
var autocomplete = _props8.autocomplete;
var options = _props8.options;
var value = _props8.value;
var focusUponSelection = _props8.focusUponSelection;
var onToggle = _props8.onToggle;
if (disabled) {
return;
}
var expanded = this.state.expanded;
if (!expanded && autocomplete) {
var autocomplete_value = this.get_selected_option_label() || '';
this.setState({
autocomplete_input_value: autocomplete_value,
matching_options: this.get_matching_options(options, autocomplete_value)
});
// if (!this.state.autocomplete_width)
// {
// this.setState({ autocomplete_width: this.get_widest_label_width() })
// }
}
// Deferring expanding the select upon click
// because document.onClick should finish first,
// otherwise `event.target` may be detached from the DOM
// and it would immediately toggle back to collapsed state.
setTimeout(function () {
_this5.setState({
expanded: !expanded
});
if (!expanded && options) {
// Focus either the selected option
// or the first option in the list.
var focused_option_value = value || options[0].value;
_this5.setState({ focused_option_value: focused_option_value });
// Scroll down to the focused option
_this5.scroll_to(focused_option_value);
}
// If it's autocomplete, then focus <input/> field
// upon toggling the select component.
if (autocomplete && !toggle_options.dont_focus_after_toggle) {
if (!expanded || expanded && focusUponSelection) {
setTimeout(function () {
// Focus the toggler
if (expanded) {
_this5.selected.focus();
} else {
_this5.autocomplete.focus();
}
}, 0);
}
}
if (onToggle) {
onToggle(!expanded);
}
if (toggle_options.callback) {
toggle_options.callback();
}
}, 0);
}
}, {
key: 'item_clicked',
value: function item_clicked(value, event) {
if (event) {
event.preventDefault();
}
var _props9 = this.props;
var disabled = _props9.disabled;
var onChange = _props9.onChange;
var autocomplete = _props9.autocomplete;
var focusUponSelection = _props9.focusUponSelection;
if (disabled) {
return;
}
// Focus the toggler
if (focusUponSelection) {
if (autocomplete) {
this.autocomplete.focus();
} else {
this.selected.focus();
}
}
this.toggle(undefined, { callback: function callback() {
return onChange(value);
} });
}
}, {
key: 'document_clicked',
value: function document_clicked(event) {
var autocomplete = _reactDom2.default.findDOMNode(this.autocomplete);
var selected_option = _reactDom2.default.findDOMNode(this.selected);
var options_list = _reactDom2.default.findDOMNode(this.list);
// Don't close the select if its expander button has been clicked,
// or if autocomplete has been clicked,
// or if an option was selected from the list.
if (options_list.contains(event.target) || autocomplete && autocomplete.contains(event.target) || selected_option && selected_option.contains(event.target)) {
return;
}
this.setState({ expanded: false });
var onToggle = this.props.onToggle;
if (onToggle) {
onToggle(false);
}
}
// Would have used `onBlur()` handler here
// with `container.contains(event.relatedTarget)`,
// but it has an IE bug in React.
// https://github.com/facebook/react/issues/3751
//
// Therefore, using the hacky `document.onClick` handlers
// and this `onKeyDown` Tab handler
// until `event.relatedTarget` support is consistent in React.
//
}, {
key: 'on_key_down_in_container',
value: function on_key_down_in_container(event) {
if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) {
return;
}
var expanded = this.state.expanded;
switch (event.keyCode) {
// Toggle on Tab out
case 9:
if (expanded) {
this.toggle(undefined, { dont_focus_after_toggle: true });
var onTabOut = this.props.onTabOut;
if (onTabOut) {
onTabOut(event);
}
}
return;
}
}
}, {
key: 'on_key_down',
value: function on_key_down(event) {
var _this6 = this;
if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) {
return;
}
var _props10 = this.props;
var options = _props10.options;
var value = _props10.value;
var autocomplete = _props10.autocomplete;
var _state5 = this.state;
var expanded = _state5.expanded;
var focused_option_value = _state5.focused_option_value;
// Maybe add support for `children` arrow navigation in future
if (options) {
switch (event.keyCode) {
// Select the previous option (if present) on up arrow
case 38:
event.preventDefault();
var previous = this.previous_focusable_option();
if (previous) {
this.show_option(previous.value, 'top');
return this.setState({ focused_option_value: previous.value });
}
return;
// Select the next option (if present) on down arrow
case 40:
event.preventDefault();
var next = this.next_focusable_option();
if (next) {
this.show_option(next.value, 'bottom');
return this.setState({ focused_option_value: next.value });
}
return;
// Collapse on Escape
//
// Maybe add this kind of support for "Escape" key in some future:
// hiding the item list, cancelling current item selection process
// and restoring the selection present before the item list was toggled.
//
case 27:
// Collapse the list if it's expanded
if (this.state.expanded) {
this.toggle();
// Restore focus when the list is collapsed
setTimeout(function () {
_this6.selected.focus();
}, 0);
}
return;
// on Enter
case 13:
// Choose the focused item on Enter
if (expanded) {
event.preventDefault();
// If an item is focused
// (which may not be a case
// when autocomplete is matching no items)
// (still for non-autocomplete select
// it is valid to have a default option)
if (this.get_options() && this.get_options().length > 0) {
// Choose the focused item
this.item_clicked(focused_option_value);
// And collapse the select
this.toggle();
}
}
// Else it should have just submitted the form on Enter,
// but it wouldn't because the select element activator is a <button/>
// therefore hitting Enter while being focused on it just pushes that button.
// So submit the enclosing form manually.
else {
if ((0, _dom.submit_parent_form)(_reactDom2.default.findDOMNode(this.select))) {
event.preventDefault();
}
}
return;
// on Spacebar
case 32:
// Choose the focused item on Enter
if (expanded) {
// only if it it's an `options` select
// and also if it's not an autocomplete
if (this.get_options() && !autocomplete) {
event.preventDefault();
// `focused_option_value` could be non-existent
// in case of `autocomplete`, but since
// we're explicitly not handling autocomplete here
// it is valid to select any options including the default ones.
this.item_clicked(focused_option_value);
this.toggle();
}
}
// Expand the select otherwise
else {
event.preventDefault();
this.toggle();
}
return;
}
}
}
}, {
key: 'get_options',
value: function get_options() {
var _props11 = this.props;
var autocomplete = _props11.autocomplete;
var autocompleteShowAll = _props11.autocompleteShowAll;
var maxItems = _props11.maxItems;
var options = _props11.options;
var matching_options = this.state.matching_options;
if (!autocomplete) {
return options;
}
if (autocompleteShowAll) {
return matching_options;
}
return matching_options.slice(0, maxItems);
}
// Get the previous option (relative to the currently focused option)
}, {
key: 'previous_focusable_option',
value: function previous_focusable_option() {
var options = this.get_options();
var focused_option_value = this.state.focused_option_value;
var i = 0;
while (i < options.length) {
if (options[i].value === focused_option_value) {
if (i - 1 >= 0) {
return options[i - 1];
}
}
i++;
}
}
// Get the next option (relative to the currently focused option)
}, {
key: 'next_focusable_option',
value: function next_focusable_option() {
var options = this.get_options();
var focused_option_value = this.state.focused_option_value;
var i = 0;
while (i < options.length) {
if (options[i].value === focused_option_value) {
if (i + 1 < options.length) {
return options[i + 1];
}
}
i++;
}
}
// Scrolls to an option having the value
}, {
key: 'scroll_to',
value: function scroll_to(value) {
var option_element = _reactDom2.default.findDOMNode(this.options[get_option_key(value)]);
// If this option isn't even shown
// (e.g. autocomplete)
// then don't scroll to it because there's nothing to scroll to.
if (!option_element) {
return;
}
_reactDom2.default.findDOMNode(this.list).scrollTop = option_element.offsetTop;
}
// Fully shows an option having the `value` (scrolls to it if neccessary)
}, {
key: 'show_option',
value: function show_option(value, gravity) {
var option_element = _reactDom2.default.findDOMNode(this.options[get_option_key(value)]);
var list = _reactDom2.default.findDOMNode(this.list);
switch (gravity) {
case 'top':
if (option_element.offsetTop < list.scrollTop) {
list.scrollTop = option_element.offsetTop;
}
return;
case 'bottom':
if (option_element.offsetTop + option_element.offsetHeight > list.scrollTop + list.offsetHeight) {
list.scrollTop = option_element.offsetTop + option_element.offsetHeight - list.offsetHeight;
}
return;
}
}
// Calculates height of the expanded item list
}, {
key: 'calculate_height',
value: function calculate_height() {
var options = this.props.options;
var list_dom_node = _reactDom2.default.findDOMNode(this.list);
var border = parseInt(window.getComputedStyle(list_dom_node).borderTopWidth);
var height = list_dom_node.scrollHeight; // + 2 * border // inner height + 2 * border
var vertical_padding = parseInt(window.getComputedStyle(list_dom_node.firstChild).paddingTop);
// For things like "accordeon".
//
// const images = list_dom_node.querySelectorAll('img')
//
// if (images.length > 0)
// {
// return this.preload_images(list_dom_node, images)
// }
var state = { height: height, vertical_padding: vertical_padding, border: border };
if (this.is_scrollable() && options && this.overflown()) {
state.list_height = this.scrollable_list_height(state);
}
this.setState(state);
}
}, {
key: 'is_scrollable',
value: function is_scrollable() {
var _props12 = this.props;
var menu = _props12.menu;
var autocomplete = _props12.autocomplete;
var autocompleteShowAll = _props12.autocompleteShowAll;
var scroll = _props12.scroll;
return !menu && (autocomplete && autocompleteShowAll || !autocomplete) && scroll;
}
// This turned out not to work for `autocomplete`
// because not all options are ever shown.
// get_widest_label_width()
// {
// // <ul/> -> <li/> -> <button/>
// const label = ReactDOM.findDOMNode(this.list).firstChild.firstChild
//
// const style = getComputedStyle(label)
//
// const width = parseFloat(style.width)
// const side_padding = parseFloat(style.paddingLeft)
//
// return width - 2 * side_padding
// }
}, {
key: 'get_matching_options',
value: function get_matching_options(options, value) {
// If the autocomplete value is `undefined` or empty
if (!value) {
return options;
}
value = value.toLowerCase();
return options.filter(function (_ref9) {
var label = _ref9.label;
var verbose = _ref9.verbose;
return (verbose || label).toLowerCase().indexOf(value) >= 0;
});
}
}, {
key: 'on_autocomplete_input_change',
value: function on_autocomplete_input_change(event) {
var options = this.props.options;
var input = event.target.value;
var matching_options = this.get_matching_options(options, input);
this.setState({
autocomplete_input_value: input,
matching_options: matching_options,
focused_option_value: matching_options.length > 0 ? matching_options[0].value : undefined
});
}
// // https://github.com/daviferreira/react-sanfona/blob/master/src/AccordionItem/index.jsx#L54
// // Wait for images to load before calculating maxHeight
// preload_images(node, images)
// {
// let images_loaded = 0
//
// const image_loaded = () =>
// {
// images_loaded++
//
// if (images_loaded === images.length)
// {
// this.setState
// ({
// height: this.props.expanded ? node.scrollHeight : 0
// })
// }
// }
//
// for (let i = 0; i < images.length; i += 1)
// {
// const image = new Image()
// image.src = images[i].src
// image.onload = image.onerror = image_loaded
// }
// }
}]);
return Select;
}(_react.PureComponent);
Select.propTypes = {
// A list of selectable options
options: _react.PropTypes.arrayOf(_react.PropTypes.shape({
// Option value (may be `undefined`)
value: value_prop_type,
// Option label (may be `undefined`)
label: _react2.default.PropTypes.string,
// Option icon
icon: _react2.default.PropTypes.node
})),
// HTML form input `name` attribute
name: _react.PropTypes.string,
// Label which is placed above the select
label: _react.PropTypes.string,
// Placeholder (like "Choose")
placeholder: _react.PropTypes.string,
// Show icon only for selected item,
// and only if `concise` is `true`.
saveOnIcons: _react.PropTypes.bool,
// Disables this control
disabled: _react.PropTypes.bool,
// Selected option value
value: value_prop_type,
// Is called when an option is selected
onChange: _react.PropTypes.func,
// (exotic use case)
// Falls back to a plain HTML input
// when javascript is disabled (e.g. Tor)
fallback: _react.PropTypes.bool.isRequired,
// CSS class
className: _react.PropTypes.string,
// CSS style object
style: _react.PropTypes.object,
// If this flag is set to `true`,
// and `icon` is specified for a selected option,
// then the selected option will be displayed
// as icon only, without the label.
concise: _react.PropTypes.bool,
// If set to `true`, autocompletion is available
// upon expanding the options list.
autocomplete: _react.PropTypes.bool,
// If set to `true`, autocomple will show all
// matching options instead of just `maxItems`.
autocompleteShowAll: _react.PropTypes.bool,
// Options list alignment ("left", "right")
alignment: _react.PropTypes.oneOf(['left', 'right']),
// If `menu` flag is set to `true`
// then it's gonna be a dropdown menu
// with `children` elements inside.
menu: _react.PropTypes.bool,
// If `menu` flag is set to `true`
// then `toggler` is the dropdown menu button.
toggler: _react.PropTypes.element,
// If `scroll` is `false`, then options list
// is not limited in height.
// Is `true` by default (scrollable).
scroll: _react.PropTypes.bool.isRequired,
// If this flag is set to `true`,
// then the dropdown expands itself upward.
// (as opposed to the default downward)
upward: _react.PropTypes.bool,
// Maximum items fitting the options list height (scrollable).
// In case of `autocomplete` that's the maximum number of matched items shown.
// Is `6` by default.
maxItems: _react.PropTypes.number.isRequired,
// Is `true` by default (only when the list of options is scrollable)
scrollbarPadding: _react.PropTypes.bool,
focusUponSelection: _react.PropTypes.bool.isRequired,
onTabOut: _react.PropTypes.func,
onToggle: _react.PropTypes.func
// transition_item_count_min : PropTypes.number,
// transition_duration_min : PropTypes.number,
// transition_duration_max : PropTypes.number
};
Select.defaultProps = {
alignment: 'left',
scroll: true,
maxItems: 6,
scrollbarPadding: true,
focusUponSelection: true,
fallback: false
};
exports.default = Select;
Select.Separator = function (props) {
return _react2.default.createElement('div', { className: 'rrui__select__separator', style: styles.separator });
};
var styles = (0, _reactStyling.flat)(_templateObject);
// There can be an `undefined` value,
// so just `{ value }` won't do here.
function get_option_key(value) {
return value === undefined ? '@@rrui/select/undefined' : value;
}
//# sourceMappingURL=select.js.map