react-bootstrap-4-typeahead
Version:
React-based typeahead using the Bootstrap theme
432 lines (361 loc) • 13.1 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _pick2 = require('lodash/pick');
var _pick3 = _interopRequireDefault(_pick2);
var _noop2 = require('lodash/noop');
var _noop3 = _interopRequireDefault(_noop2);
var _isEqual2 = require('lodash/isEqual');
var _isEqual3 = _interopRequireDefault(_isEqual2);
var _isEmpty2 = require('lodash/isEmpty');
var _isEmpty3 = _interopRequireDefault(_isEmpty2);
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _TokenizerInput = require('./TokenizerInput.react');
var _TokenizerInput2 = _interopRequireDefault(_TokenizerInput);
var _TypeaheadInput = require('./TypeaheadInput.react');
var _TypeaheadInput2 = _interopRequireDefault(_TypeaheadInput);
var _TypeaheadMenu = require('./TypeaheadMenu.react');
var _TypeaheadMenu2 = _interopRequireDefault(_TypeaheadMenu);
var _getFilteredOptions = require('./getFilteredOptions');
var _getFilteredOptions2 = _interopRequireDefault(_getFilteredOptions);
var _reactOnclickoutside = require('react-onclickoutside');
var _reactOnclickoutside2 = _interopRequireDefault(_reactOnclickoutside);
var _keyCode = require('./keyCode');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
/**
* Typeahead
*/
var Typeahead = _react2.default.createClass({
displayName: 'Typeahead',
propTypes: {
/**
* Specify menu alignment. The default value is `justify`, which makes the
* menu as wide as the input and truncates long values. Specifying `left`
* or `right` will align the menu to that side and the width will be
* determined by the length of menu item values.
*/
align: _react.PropTypes.oneOf(['justify', 'left', 'right']),
/**
* Allows the creation of new selections on the fly. Note that any new items
* will be added to the list of selections, but not the list of original
* options unless handled as such by `Typeahead`'s parent.
*/
allowNew: _react.PropTypes.bool,
/**
* Specify any pre-selected options. Use only if you want the component to
* be uncontrolled.
*/
defaultSelected: _react.PropTypes.array,
/**
* Whether to disable the input. Will also disable selections when
* `multiple={true}`.
*/
disabled: _react.PropTypes.bool,
/**
* Message to display in the menu if there are no valid results.
*/
emptyLabel: _react.PropTypes.string,
/**
* Specify which option key to use for display. By default, the selector
* will use the `label` key.
*/
labelKey: _react.PropTypes.string,
/**
* Maximum height of the dropdown menu, in px.
*/
maxHeight: _react.PropTypes.number,
/**
* Number of input characters that must be entered before showing results.
*/
minLength: _react.PropTypes.number,
/**
* Whether or not multiple selections are allowed.
*/
multiple: _react.PropTypes.bool,
/**
* Provides the ability to specify a prefix before the user-entered text to
* indicate that the selection will be new. No-op unless `allowNew={true}`.
*/
newSelectionPrefix: _react.PropTypes.string,
/**
* Callback fired when the input is blurred. Receives an event.
*/
onBlur: _react.PropTypes.func,
/**
* Callback fired whenever items are added or removed. Receives an array of
* the selected options.
*/
onChange: _react.PropTypes.func,
/**
* Callback for handling changes to the user-input text.
*/
onInputChange: _react.PropTypes.func,
/**
* Full set of options, including pre-selected options.
*/
options: _react.PropTypes.array.isRequired,
/**
* For large option sets, initially display a subset of results for improved
* performance. If users scroll to the end, the last item will be a link to
* display the next set of results. Value represents the number of results
* to display. `0` will display all results.
*/
paginateResults: _react.PropTypes.number,
/**
* Prompt displayed when large data sets are paginated.
*/
paginationText: _react.PropTypes.string,
/**
* Placeholder text for the input.
*/
placeholder: _react.PropTypes.string,
/**
* Provides a hook for customized rendering of menu item contents.
*/
renderMenuItemChildren: _react.PropTypes.func,
/**
* The selected option(s) displayed in the input. Use this prop if you want
* to control the component via its parent.
*/
selected: _react.PropTypes.array
},
getDefaultProps: function getDefaultProps() {
return {
allowNew: false,
defaultSelected: [],
labelKey: 'label',
onBlur: _noop3.default,
onChange: _noop3.default,
onInputChange: _noop3.default,
minLength: 0,
multiple: false,
selected: []
};
},
getInitialState: function getInitialState() {
var defaultSelected = this.props.defaultSelected;
var selected = this.props.selected.slice();
if (!(0, _isEmpty3.default)(defaultSelected)) {
selected = defaultSelected;
}
return {
activeIndex: -1,
selected: selected,
showMenu: false,
text: ''
};
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
var multiple = nextProps.multiple,
selected = nextProps.selected;
if (!(0, _isEqual3.default)(selected, this.props.selected)) {
// If new selections are passed in via props, treat the component as a
// controlled input.
this.setState({ selected: selected });
}
if (multiple !== this.props.multiple) {
this.setState({ text: '' });
}
},
render: function render() {
var _props = this.props,
options = _props.options,
props = _objectWithoutProperties(_props, ['options']);
var _state = this.state,
selected = _state.selected,
text = _state.text;
var filteredOptions = (0, _getFilteredOptions2.default)(options, text, selected, props);
return _react2.default.createElement(
'div',
{
className: 'bootstrap-typeahead show',
style: { position: 'relative' } },
this._renderInput(filteredOptions),
this._renderMenu(filteredOptions)
);
},
blur: function blur() {
this.refs.input.blur();
},
/**
* Public method to allow external clearing of the input. Clears both text
* and selection(s).
*/
clear: function clear() {
var _getInitialState = this.getInitialState(),
activeIndex = _getInitialState.activeIndex,
showMenu = _getInitialState.showMenu;
var selected = [];
var text = '';
this.setState({
activeIndex: activeIndex,
selected: selected,
showMenu: showMenu,
text: text
});
this.props.onChange(selected);
this.props.onInputChange(text);
},
focus: function focus() {
this.refs.input.focus();
},
_renderInput: function _renderInput(filteredOptions) {
var _this = this;
var _props2 = this.props,
labelKey = _props2.labelKey,
multiple = _props2.multiple;
var _state2 = this.state,
activeIndex = _state2.activeIndex,
selected = _state2.selected,
text = _state2.text;
var Input = multiple ? _TokenizerInput2.default : _TypeaheadInput2.default;
var inputProps = (0, _pick3.default)(this.props, ['disabled', 'placeholder']);
return _react2.default.createElement(Input, _extends({}, inputProps, {
activeIndex: activeIndex,
labelKey: labelKey,
onAdd: this._handleAddOption,
onBlur: this._handleBlur,
onChange: this._handleTextChange,
onFocus: this._handleFocus,
onKeyDown: function onKeyDown(e) {
return _this._handleKeydown(filteredOptions, e);
},
onRemove: this._handleRemoveOption,
options: filteredOptions,
ref: 'input',
selected: selected.slice(),
text: text
}));
},
_renderMenu: function _renderMenu(filteredOptions) {
var _props3 = this.props,
labelKey = _props3.labelKey,
minLength = _props3.minLength;
var _state3 = this.state,
activeIndex = _state3.activeIndex,
showMenu = _state3.showMenu,
text = _state3.text;
if (!(showMenu && text.length >= minLength)) {
return null;
}
var menuProps = (0, _pick3.default)(this.props, ['align', 'emptyLabel', 'maxHeight', 'newSelectionPrefix', 'paginationText', 'renderMenuItemChildren']);
return _react2.default.createElement(_TypeaheadMenu2.default, _extends({}, menuProps, {
activeIndex: activeIndex,
initialResultCount: this.props.paginateResults,
labelKey: labelKey,
onClick: this._handleAddOption,
options: filteredOptions,
text: text
}));
},
_handleBlur: function _handleBlur(e) {
// Note: Don't hide the menu here, since that interferes with other actions
// like making a selection by clicking on a menu item.
this.props.onBlur(e);
},
_handleFocus: function _handleFocus() {
this.setState({ showMenu: true });
},
_handleTextChange: function _handleTextChange(text) {
var _getInitialState2 = this.getInitialState(),
activeIndex = _getInitialState2.activeIndex;
this.setState({
activeIndex: activeIndex,
showMenu: true,
text: text
});
this.props.onInputChange(text);
},
_handleKeydown: function _handleKeydown(options, e) {
var activeIndex = this.state.activeIndex;
switch (e.keyCode) {
case _keyCode.UP:
case _keyCode.DOWN:
// Don't cycle through the options if the menu is hidden.
if (!this.state.showMenu) {
return;
}
// Prevents input cursor from going to the beginning when pressing up.
e.preventDefault();
// Increment or decrement index based on user keystroke.
activeIndex += e.keyCode === _keyCode.UP ? -1 : 1;
// If we've reached the end, go back to the beginning or vice-versa.
if (activeIndex === options.length) {
activeIndex = -1;
} else if (activeIndex === -2) {
activeIndex = options.length - 1;
}
this.setState({ activeIndex: activeIndex });
break;
case _keyCode.ESC:
case _keyCode.TAB:
// Prevent closing dialogs.
e.keyCode === _keyCode.ESC && e.preventDefault();
this._hideDropdown();
break;
case _keyCode.RETURN:
// Prevent submitting forms.
e.preventDefault();
if (this.state.showMenu) {
var selected = options[activeIndex];
selected && this._handleAddOption(selected);
}
break;
}
},
_handleAddOption: function _handleAddOption(selectedOption) {
var _props4 = this.props,
multiple = _props4.multiple,
labelKey = _props4.labelKey,
onChange = _props4.onChange,
onInputChange = _props4.onInputChange;
var selected = void 0;
var text = void 0;
if (multiple) {
// If multiple selections are allowed, add the new selection to the
// existing selections.
selected = this.state.selected.concat(selectedOption);
text = '';
} else {
// If only a single selection is allowed, replace the existing selection
// with the new one.
selected = [selectedOption];
text = selectedOption[labelKey];
}
this.setState({ selected: selected, text: text });
this._hideDropdown();
onChange(selected);
onInputChange(text);
},
_handleRemoveOption: function _handleRemoveOption(removedOption) {
var selected = this.state.selected.slice();
selected = selected.filter(function (option) {
return !(0, _isEqual3.default)(option, removedOption);
});
// Make sure the input stays focused after the item is removed.
this.focus();
this.setState({ selected: selected });
this._hideDropdown();
this.props.onChange(selected);
},
/**
* From `listensToClickOutside` HOC.
*/
handleClickOutside: function handleClickOutside(e) {
this._hideDropdown();
},
_hideDropdown: function _hideDropdown() {
var _getInitialState3 = this.getInitialState(),
activeIndex = _getInitialState3.activeIndex,
showMenu = _getInitialState3.showMenu;
this.setState({
activeIndex: activeIndex,
showMenu: showMenu
});
}
});
exports.default = (0, _reactOnclickoutside2.default)(Typeahead);