react-bootstrap-autosuggest
Version:
Autosuggest component for react-bootstrap
1,348 lines (1,212 loc) • 58.1 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ObjectListAdapter = exports.MapListAdapter = exports.ArrayListAdapter = exports.EmptyListAdapter = exports.ListAdapter = exports.ItemAdapter = undefined;
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 _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _shallowEqual = require('fbjs/lib/shallowEqual');
var _shallowEqual2 = _interopRequireDefault(_shallowEqual);
var _keycode = require('keycode');
var _keycode2 = _interopRequireDefault(_keycode);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactBootstrap = require('react-bootstrap');
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _warning = require('warning');
var _warning2 = _interopRequireDefault(_warning);
var _Choices = require('./Choices');
var _Choices2 = _interopRequireDefault(_Choices);
var _Suggestions = require('./Suggestions');
var _Suggestions2 = _interopRequireDefault(_Suggestions);
var _ItemAdapter = require('./ItemAdapter');
var _ItemAdapter2 = _interopRequireDefault(_ItemAdapter);
var _ListAdapter = require('./ListAdapter');
var _ListAdapter2 = _interopRequireDefault(_ListAdapter);
var _EmptyListAdapter = require('./EmptyListAdapter');
var _EmptyListAdapter2 = _interopRequireDefault(_EmptyListAdapter);
var _ArrayListAdapter = require('./ArrayListAdapter');
var _ArrayListAdapter2 = _interopRequireDefault(_ArrayListAdapter);
var _MapListAdapter = require('./MapListAdapter');
var _MapListAdapter2 = _interopRequireDefault(_MapListAdapter);
var _ObjectListAdapter = require('./ObjectListAdapter');
var _ObjectListAdapter2 = _interopRequireDefault(_ObjectListAdapter);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
exports.ItemAdapter = _ItemAdapter2['default'];
exports.ListAdapter = _ListAdapter2['default'];
exports.EmptyListAdapter = _EmptyListAdapter2['default'];
exports.ArrayListAdapter = _ArrayListAdapter2['default'];
exports.MapListAdapter = _MapListAdapter2['default'];
exports.ObjectListAdapter = _ObjectListAdapter2['default'];
/**
* Combo-box input component that combines a drop-down list and a single-line
* editable text box. The set of options for the drop-down list can be
* controlled dynamically. Selection of multiple items is supported using a
* tag/pill-style user interface within a simulated text box.
*/
var Autosuggest = function (_React$Component) {
_inherits(Autosuggest, _React$Component);
_createClass(Autosuggest, null, [{
key: 'defaultInputSelect',
value: function defaultInputSelect(input, value, completion) {
// https://html.spec.whatwg.org/multipage/forms.html#do-not-apply
switch (input.type) {
case 'text':
case 'search':
case 'url':
case 'tel':
case 'password':
// istanbul ignore else
if (input.setSelectionRange) {
input.setSelectionRange(value.length, completion.length);
} else if (input.createTextRange) {
// old IE
var range = input.createTextRange();
range.moveEnd('character', completion.length);
range.moveStart('character', value.length);
range.select();
}
}
}
}]);
function Autosuggest(props) {
var _ref;
_classCallCheck(this, Autosuggest);
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
/* istanbul ignore next: https://github.com/gotwarlost/istanbul/issues/690#issuecomment-265718617 */
var _this = _possibleConstructorReturn(this, (_ref = Autosuggest.__proto__ || Object.getPrototypeOf(Autosuggest)).call.apply(_ref, [this, props].concat(_toConsumableArray(args))));
_this._itemAdapter = props.itemAdapter || new _ItemAdapter2['default']();
_this._itemAdapter.receiveProps(props);
_this._listAdapter = props.datalistAdapter || _this._getListAdapter(props.datalist);
_this._listAdapter.receiveProps(props, _this._itemAdapter);
var _this$_getValueFromPr = _this._getValueFromProps(props),
inputValue = _this$_getValueFromPr.inputValue,
inputItem = _this$_getValueFromPr.inputItem,
inputItemEphemeral = _this$_getValueFromPr.inputItemEphemeral,
selectedItems = _this$_getValueFromPr.selectedItems;
_this._setValueMeta(inputItem, inputItemEphemeral, true, true);
_this._lastValidItem = inputItem;
_this._lastValidValue = inputValue;
_this._keyPressCount = 0;
_this.state = {
open: false,
disableFilter: false,
inputValue: inputValue,
inputValueKeyPress: 0,
inputFocused: false,
selectedItems: selectedItems,
searchValue: null
};
_this._lastOnChangeValue = _this._getCurrentValue();
_this._lastOnSelectValue = inputItem;
var self = _this; // https://github.com/facebook/flow/issues/1517
self._renderSelected = _this._renderSelected.bind(_this);
self._getItemKey = _this._getItemKey.bind(_this);
self._isSelectedItem = _this._isSelectedItem.bind(_this);
self._renderSuggested = _this._renderSuggested.bind(_this);
self._handleToggleClick = _this._handleToggleClick.bind(_this);
self._handleInputChange = _this._handleInputChange.bind(_this);
self._handleItemSelect = _this._handleItemSelect.bind(_this);
self._removeItem = _this._removeItem.bind(_this);
self._handleShowAll = _this._handleShowAll.bind(_this);
self._handleKeyDown = _this._handleKeyDown.bind(_this);
self._handleKeyPress = _this._handleKeyPress.bind(_this);
self._handleMenuClose = _this._handleMenuClose.bind(_this);
self._handleInputFocus = _this._handleInputFocus.bind(_this);
self._handleInputBlur = _this._handleInputBlur.bind(_this);
self._handleFocus = _this._handleFocus.bind(_this);
self._handleBlur = _this._handleBlur.bind(_this);
return _this;
}
_createClass(Autosuggest, [{
key: '_getListAdapter',
value: function _getListAdapter(list) {
if (list == null) {
return new _EmptyListAdapter2['default']();
} else if (Array.isArray(list)) {
return new _ArrayListAdapter2['default']();
} else if (list instanceof Map) {
return new _MapListAdapter2['default']();
} else if (typeof list === 'object') {
return new _ObjectListAdapter2['default']();
} else {
throw Error('Unexpected datalist type: datalistAdapter required');
}
}
}, {
key: '_getValueFromProps',
value: function _getValueFromProps(props) {
var inputValue = '';
var inputItem = null;
var inputItemEphemeral = false;
var selectedItems = [];
var value = props.value || props.defaultValue;
if (value != null) {
if (props.multiple) {
if (Array.isArray(value)) {
selectedItems = this._filterItems(value, props);
} else {
(0, _warning2['default'])(!value, 'Array expected for value property');
}
} else if (props.valueIsItem) {
var itemValue = this._itemAdapter.getInputValue(value);
if (props.datalist != null) {
inputItem = this._listAdapter.findMatching(props.datalist, itemValue);
if (inputItem != null) {
inputValue = inputItem === value ? itemValue : this._itemAdapter.getInputValue(inputItem);
} else if (props.datalistOnly && !props.datalistPartial) {
this._warnInvalidValue(value);
} else {
inputValue = itemValue;
inputItem = value;
}
} else {
inputValue = itemValue;
inputItem = value;
}
} else if (value) {
if (props.datalist != null) {
inputItem = this._listAdapter.findMatching(props.datalist, value);
if (inputItem != null) {
inputValue = this._itemAdapter.getInputValue(inputItem);
} else if (props.datalistOnly && !props.datalistPartial) {
this._warnInvalidValue(value);
} else {
inputValue = value.toString();
inputItem = this._itemAdapter.newFromValue(value);
inputItemEphemeral = true;
}
} else {
inputValue = value.toString();
inputItem = this._itemAdapter.newFromValue(value);
inputItemEphemeral = true;
}
}
}
return { inputValue: inputValue, inputItem: inputItem, inputItemEphemeral: inputItemEphemeral, selectedItems: selectedItems };
}
}, {
key: '_filterItems',
value: function _filterItems(items, props) {
if (props.datalist != null || !props.allowDuplicates) {
var result = [];
var valueSet = {};
var different = false;
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = items[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var _item = _step.value;
var _value = this._itemAdapter.getInputValue(_item);
if (!props.allowDuplicates && valueSet[_value]) {
different = true;
continue;
}
var listItem = this._listAdapter.findMatching(props.datalist, _value);
if (listItem != null) {
result.push(listItem);
valueSet[_value] = true;
different = true;
} else if (props.datalistOnly && !props.datalistPartial) {
this._warnInvalidValue(_value);
different = true;
} else {
result.push(_item);
valueSet[_value] = true;
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator['return']) {
_iterator['return']();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
if (different) {
return result;
}
}
return items;
}
}, {
key: '_warnInvalidValue',
value: function _warnInvalidValue(value) {
(0, _warning2['default'])(false, 'Value "%s" does not match any datalist value', value);
}
}, {
key: '_setInputValue',
value: function _setInputValue(value, callback) {
// track keypress count in state so re-render is forced even if value is
// unchanged; this is necessary when typing over the autocompleted range
// with matching characters to properly maintain the input selection range
this.setState({
inputValue: value,
inputValueKeyPress: this._keyPressCount
}, callback);
}
}, {
key: '_setValueMeta',
value: function _setValueMeta(inputItem) {
var inputItemEphemeral = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var isValid = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : inputItem != null;
var validated = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : isValid;
this._inputItem = inputItem;
this._inputItemEphemeral = inputItemEphemeral;
this._valueIsValid = isValid;
this._valueWasValidated = validated;
}
}, {
key: '_clearInput',
value: function _clearInput() {
this._setValueMeta(null, false, true, true);
this._setInputValue('');
}
}, {
key: '_getValueUsing',
value: function _getValueUsing(props, inputValue, inputItem, selectedItems) {
return props.multiple ? selectedItems : props.valueIsItem ? inputItem : inputValue;
}
}, {
key: '_getCurrentValue',
value: function _getCurrentValue() {
return this._getValueUsing(this.props, this.state.inputValue, this._inputItem, this.state.selectedItems);
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
// IE8 can jump cursor position if not immediately updated to typed value;
// for other browsers, we can avoid re-rendering for the auto-complete
this._autoCompleteAfterRender = !this.refs.input.setSelectionRange;
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
if (nextProps.itemAdapter != this.props.itemAdapter) {
this._itemAdapter = nextProps.itemAdapter || new _ItemAdapter2['default']();
}
this._itemAdapter.receiveProps(nextProps);
if (nextProps.datalist != this.props.datalist || nextProps.datalistAdapter != this.props.datalistAdapter) {
if (nextProps.datalistAdapter) {
this._listAdapter = nextProps.datalistAdapter;
} else {
var listAdapter = this._getListAdapter(nextProps.datalist);
if (listAdapter.constructor != this._listAdapter.constructor) {
this._listAdapter = listAdapter;
}
}
}
this._listAdapter.receiveProps(nextProps, this._itemAdapter);
// if props.value changes (to a value other than the current state), or
// validation changes to make state invalid, propagate props.value to state
var nextValue = nextProps.value;
var inputValue = this.state.inputValue;
var valueChanged = nextValue !== this.props.value && nextValue !== this._getValueUsing(nextProps, inputValue, this._inputItem, this.state.selectedItems);
var inputItem = void 0,
inputValueInvalid = void 0,
propsValueInvalid = void 0,
validateSelected = void 0;
if (!valueChanged) {
if (nextProps.datalistOnly) {
var canValidate = !nextProps.datalistPartial && nextProps.datalist != null;
var validationChanged = !this.props.datalistOnly || !nextProps.datalistPartial && this.props.datalistPartial || nextProps.datalist != this.props.datalist;
if (inputValue) {
inputItem = this._listAdapter.findMatching(nextProps.datalist, inputValue);
if (inputItem == null) {
if (!canValidate && !this._inputItemEphemeral) {
inputItem = this._inputItem;
} else if (this._inputItemEphemeral && nextValue === inputValue) {
propsValueInvalid = true;
}
}
inputValueInvalid = inputItem == null && validationChanged;
// update metadata but don't reset input value if invalid but focused
if (inputValueInvalid && this._focused) {
this._setValueMeta(null, false, false, true);
if (validationChanged && canValidate && this._lastValidItem != null) {
// revalidate last valid item, which will be restored on blur
this._lastValidItem = this._listAdapter.findMatching(nextProps.datalist, this._lastValidValue);
if (this._lastValidItem == null) {
this._lastValidValue = '';
}
}
inputValueInvalid = false;
}
} else {
inputItem = null;
inputValueInvalid = false;
}
validateSelected = nextProps.multiple && canValidate && validationChanged;
}
if (nextProps.multiple && !nextProps.allowDuplicates && this.props.allowDuplicates) {
validateSelected = true;
}
}
// inputValueInvalid implies !multiple, since inputValue of multiple should
// be blank when not focused
if (valueChanged || inputValueInvalid) {
var inputItemEphemeral = void 0,
_selectedItems = void 0;
if (propsValueInvalid) {
inputValue = '';
inputItemEphemeral = false;
_selectedItems = [];
} else {
var _getValueFromProps2 = this._getValueFromProps(nextProps);
inputValue = _getValueFromProps2.inputValue;
inputItem = _getValueFromProps2.inputItem;
inputItemEphemeral = _getValueFromProps2.inputItemEphemeral;
_selectedItems = _getValueFromProps2.selectedItems;
}
// if props.value change resolved to current state item, don't reset input
if (inputItem !== this._inputItem || !this._focused) {
this._setValueMeta(inputItem, inputItemEphemeral, true, true);
this._setInputValue(inputValue);
this.setState({ selectedItems: _selectedItems });
validateSelected = false;
this._lastValidItem = inputItem;
this._lastValidValue = inputValue;
// suppress onChange (but not onSelect) if value came from props
if (valueChanged) {
this._lastOnChangeValue = this._getValueUsing(nextProps, inputValue, inputItem, _selectedItems);
}
} else if (valueChanged && nextProps.multiple) {
this.setState({ selectedItems: _selectedItems });
}
} else if (inputValue && nextProps.datalist != this.props.datalist && this._focused) {
// if datalist changed but value didn't, attempt to autocomplete
this._checkAutoComplete(inputValue, nextProps);
}
if (validateSelected) {
var _selectedItems2 = this._filterItems(this.state.selectedItems, nextProps);
this.setState({ selectedItems: _selectedItems2 });
}
// open dropdown if datalist message is set while focused
if (nextProps.datalistMessage && nextProps.datalistMessage != this.props.datalistMessage && this._focused) {
this._open('message', nextProps);
}
}
}, {
key: 'shouldComponentUpdate',
value: function shouldComponentUpdate(nextProps, nextState) {
return !(0, _shallowEqual2['default'])(this.props, nextProps) || !(0, _shallowEqual2['default'])(this.state, nextState);
}
}, {
key: 'componentWillUpdate',
value: function componentWillUpdate(nextProps, nextState) {
var suggestions = this.refs.suggestions;
this._menuFocusedBeforeUpdate = suggestions && suggestions.isFocused();
var nextInputValue = nextState.inputValue;
if (nextInputValue != this.state.inputValue) {
var inputItem = void 0,
inputItemEphemeral = void 0,
isValid = void 0;
if (!this._valueWasValidated) {
if (nextInputValue) {
inputItem = this._listAdapter.findMatching(nextProps.datalist, nextInputValue);
if (inputItem == null && !nextProps.datalistOnly) {
inputItem = this._itemAdapter.newFromValue(nextInputValue);
inputItemEphemeral = true;
isValid = true;
} else {
inputItemEphemeral = false;
isValid = inputItem != null;
}
} else {
inputItem = null;
inputItemEphemeral = false;
isValid = !nextProps.required;
}
this._setValueMeta(inputItem, inputItemEphemeral, isValid);
} else {
inputItem = this._inputItem;
isValid = this._valueIsValid;
}
if (isValid) {
this._lastValidItem = inputItem;
this._lastValidValue = inputItem && !inputItemEphemeral ? this._itemAdapter.getInputValue(inputItem) : nextInputValue;
}
if (isValid) {
var _multiple = nextProps.multiple,
_onChange = nextProps.onChange;
if (!_multiple && _onChange) {
var _value2 = this._getValueUsing(nextProps, nextInputValue, inputItem, nextState.selectedItems);
if (_value2 !== this._lastOnChangeValue) {
this._lastOnChangeValue = _value2;
_onChange(_value2);
}
}
var _onSelect = nextProps.onSelect;
if (_onSelect && inputItem !== this._lastOnSelectValue) {
this._lastOnSelectValue = inputItem;
_onSelect(inputItem);
}
}
}
var onToggle = nextProps.onToggle;
if (onToggle && nextState.open != this.state.open) {
onToggle(nextState.open);
}
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(prevProps, prevState) {
if (this.state.open && !prevState.open && this._lastOpenEventType === 'keydown' || this.state.disableFilter && !prevState.disableFilter && this._menuFocusedBeforeUpdate) {
this.refs.suggestions.focusFirst();
} else if (!this.state.open && prevState.open) {
// closed
if (this._menuFocusedBeforeUpdate) {
this._menuFocusedBeforeUpdate = false;
this._focusInput();
}
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
clearTimeout(this._focusTimeoutId);
this._focusTimeoutId = null;
clearTimeout(this._searchTimeoutId);
this._searchTimeoutId = null;
}
}, {
key: '_focusInput',
value: function _focusInput() {
var input = _reactDom2['default'].findDOMNode(this.refs.input);
// istanbul ignore else
if (input instanceof HTMLElement) {
input.focus();
}
}
}, {
key: '_open',
value: function _open(eventType, props) {
this._lastOpenEventType = eventType;
var disableFilter = eventType !== 'autocomplete' && this._hasNoOrExactMatch(props);
this.setState({ open: true, disableFilter: disableFilter });
var onSearch = props.onSearch;
var _state = this.state,
inputValue = _state.inputValue,
searchValue = _state.searchValue;
if (onSearch && searchValue !== inputValue) {
this.setState({ searchValue: inputValue });
onSearch(inputValue);
}
}
}, {
key: '_close',
value: function _close() {
this.setState({ open: false });
}
}, {
key: '_toggleOpen',
value: function _toggleOpen(eventType, props) {
if (this.state.open) {
this._close();
} else {
this._open(eventType, props);
}
}
}, {
key: '_canOpen',
value: function _canOpen() {
var datalist = this.props.datalist;
return datalist == null && this.props.onSearch || !this._listAdapter.isEmpty(datalist) || !!this.props.datalistMessage;
}
}, {
key: '_hasNoOrExactMatch',
value: function _hasNoOrExactMatch(props) {
var _this2 = this;
if (this._inputItem != null && !this._inputItemEphemeral) {
return true; // exact match
}
var foldedValue = this._itemAdapter.foldValue(this.state.inputValue);
return this._listAdapter.find(props.datalist, function (item) {
return _this2._itemAdapter.itemIncludedByInput(item, foldedValue);
}) == null;
}
}, {
key: 'render',
value: function render() {
var showToggle = this.props.showToggle;
var toggleCanOpen = this._canOpen();
var toggleVisible = showToggle === 'auto' ? toggleCanOpen : showToggle;
var classes = {
autosuggest: true,
open: this.state.open,
disabled: this.props.disabled,
dropdown: toggleVisible && !this.props.dropup,
dropup: toggleVisible && this.props.dropup
};
return _react2['default'].createElement(
'div',
{
key: 'dropdown',
className: (0, _classnames2['default'])(classes),
onFocus: this._handleFocus,
onBlur: this._handleBlur },
this._renderInputGroup(toggleVisible, toggleCanOpen),
this._renderMenu()
);
}
}, {
key: '_renderInputGroup',
value: function _renderInputGroup(toggleVisible, toggleCanOpen) {
var addonBefore = this.props.addonBefore ? _react2['default'].createElement(
'span',
{ className: 'input-group-addon', key: 'addonBefore' },
this.props.addonBefore
) : null;
var addonAfter = this.props.addonAfter ? _react2['default'].createElement(
'span',
{ className: 'input-group-addon', key: 'addonAfter' },
this.props.addonAfter
) : null;
var buttonBefore = this.props.buttonBefore ? _react2['default'].createElement(
'span',
{ className: 'input-group-btn' },
this.props.buttonBefore
) : null;
// Bootstrap expects the dropdown toggle to be last,
// as it does not reset the right border radius for toggles:
// .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle)
// { @include border-right-radius(0); }
var toggle = toggleVisible && this._renderToggle(toggleCanOpen);
var buttonAfter = toggle || this.props.buttonAfter ? _react2['default'].createElement(
'span',
{ className: 'input-group-btn' },
this.props.buttonAfter,
toggle
) : null;
var classes = (0, _classnames2['default'])({
'input-group': addonBefore || addonAfter || buttonBefore || buttonAfter,
'input-group-sm': this.props.bsSize === 'small',
'input-group-lg': this.props.bsSize === 'large',
'input-group-toggle': !!toggle
});
return classes ? _react2['default'].createElement(
'div',
{ className: classes, key: 'input-group' },
addonBefore,
buttonBefore,
this._renderChoices(),
addonAfter,
buttonAfter
) : this._renderChoices();
}
}, {
key: '_renderToggle',
value: function _renderToggle(canOpen) {
return _react2['default'].createElement(_reactBootstrap.Dropdown.Toggle, {
ref: 'toggle',
key: 'toggle',
id: this.props.toggleId,
bsSize: this.props.bsSize,
disabled: this.props.disabled || !canOpen,
open: this.state.open,
onClick: this._handleToggleClick,
onKeyDown: this._handleKeyDown });
}
}, {
key: '_renderChoices',
value: function _renderChoices() {
if (this.props.multiple) {
var _props$choicesClass = this.props.choicesClass,
ChoicesClass = _props$choicesClass === undefined ? _Choices2['default'] : _props$choicesClass;
return _react2['default'].createElement(
ChoicesClass,
{ ref: 'choices',
autoHeight: !this.props.showToggle && !this.props.addonAfter && !this.props.addonBefore && !this.props.buttonAfter && !this.props.buttonBefore,
disabled: this.props.disabled,
focused: this.state.inputFocused,
inputValue: this.state.inputValue,
items: this.state.selectedItems,
onKeyPress: this._handleKeyPress,
onRemove: this._removeItem,
renderItem: this._renderSelected },
this._renderInput()
);
}
return this._renderInput();
}
// autobind
}, {
key: '_renderSelected',
value: function _renderSelected(item) {
return this._itemAdapter.renderSelected(item);
}
}, {
key: '_renderInput',
value: function _renderInput() {
var formGroup = this.context.$bs_formGroup;
var controlId = formGroup && formGroup.controlId;
var extraProps = {};
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = Object.keys(this.props)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var key = _step2.value;
if (!Autosuggest.propTypes[key]) {
extraProps[key] = this.props[key];
}
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2['return']) {
_iterator2['return']();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
var noneSelected = !this.props.multiple || !this.state.selectedItems.length;
// set autoComplete off to avoid a redundant browser drop-down menu,
// but allow it to be overridden by extra props for auto-fill purposes
return _react2['default'].createElement('input', _extends({
autoComplete: 'off'
}, extraProps, {
className: (0, _classnames2['default'])(this.props.className, { 'form-control': !this.props.multiple }),
ref: 'input',
key: 'input',
id: controlId,
disabled: this.props.disabled,
required: this.props.required && noneSelected,
placeholder: noneSelected ? this.props.placeholder : undefined,
type: this.props.type,
value: this.state.inputValue,
onChange: this._handleInputChange,
onKeyDown: this._handleKeyDown,
onKeyPress: this._handleKeyPress,
onFocus: this._handleInputFocus,
onBlur: this._handleInputBlur }));
}
}, {
key: '_renderMenu',
value: function _renderMenu() {
var _this3 = this;
this._pseudofocusedItem = null;
var open = this.state.open;
if (!open) {
return null;
}
var datalist = this.props.datalist;
var foldedValue = this._itemAdapter.foldValue(this.state.inputValue);
this._foldedInputValue = foldedValue;
var items = void 0;
if (this.state.disableFilter) {
items = this._listAdapter.toArray(datalist);
} else {
items = this._listAdapter.filter(datalist, function (item) {
return _this3._itemAdapter.itemIncludedByInput(item, foldedValue) && _this3._allowItem(item);
});
}
items = this._itemAdapter.sortItems(items, foldedValue);
var filtered = items.length < this._listAdapter.getLength(datalist);
// visually indicate that first item will be selected if Enter is pressed
// while the input element is focused (unless multiple and not datalist-only)
var focusedIndex = void 0;
if (items.length > 0 && this.state.inputFocused && (!this.props.multiple || this.props.datalistOnly)) {
this._pseudofocusedItem = items[focusedIndex = 0];
}
var _props = this.props,
_props$suggestionsCla = _props.suggestionsClass,
SuggestionsClass = _props$suggestionsCla === undefined ? _Suggestions2['default'] : _props$suggestionsCla,
datalistMessage = _props.datalistMessage,
onDatalistMessageSelect = _props.onDatalistMessageSelect,
toggleId = _props.toggleId;
return _react2['default'].createElement(SuggestionsClass, { ref: 'suggestions',
datalistMessage: datalistMessage,
filtered: filtered,
focusedIndex: focusedIndex,
getItemKey: this._getItemKey,
isSelectedItem: this._isSelectedItem,
items: items,
labelledBy: toggleId,
onClose: this._handleMenuClose,
onDatalistMessageSelect: onDatalistMessageSelect,
onDisableFilter: this._handleShowAll,
onSelect: this._handleItemSelect,
open: open,
renderItem: this._renderSuggested });
}
}, {
key: '_allowItem',
value: function _allowItem(item) {
var _this4 = this;
if (this.props.allowDuplicates) {
return true;
}
var value = this._itemAdapter.getInputValue(item);
return !this.state.selectedItems.find(function (i) {
return _this4._itemAdapter.getInputValue(i) === value;
});
}
// autobind
}, {
key: '_getItemKey',
value: function _getItemKey(item) {
return this._itemAdapter.getReactKey(item);
}
// autobind
}, {
key: '_isSelectedItem',
value: function _isSelectedItem(item) {
return this._itemAdapter.itemMatchesInput(item, this._foldedInputValue);
}
// autobind
}, {
key: '_renderSuggested',
value: function _renderSuggested(item) {
return this._itemAdapter.renderSuggested(item);
}
// autobind
}, {
key: '_handleToggleClick',
value: function _handleToggleClick() {
this._toggleOpen('click', this.props);
}
// autobind
}, {
key: '_handleInputChange',
value: function _handleInputChange(event) {
var _this5 = this;
var _ref2 = event.target,
value = _ref2.value;
// prevent auto-complete on backspace/delete/copy/paste/etc.
var allowAutoComplete = this._keyPressCount > this.state.inputValueKeyPress;
if (allowAutoComplete && value) {
if (this._autoCompleteAfterRender) {
this._setValueMeta();
this._setInputValue(value, function () {
_this5._checkAutoComplete(value, _this5.props);
});
} else if (!this._checkAutoComplete(value, this.props)) {
this._setValueMeta();
this._setInputValue(value);
}
} else {
this._setValueMeta();
this._setInputValue(value);
}
// suppress onSearch if can't auto-complete and not open
if (allowAutoComplete || this.state.open) {
var _onSearch = this.props.onSearch;
if (_onSearch) {
clearTimeout(this._searchTimeoutId);
this._searchTimeoutId = setTimeout(function () {
_this5._searchTimeoutId = null;
if (value != _this5.state.searchValue) {
_this5.setState({ searchValue: value });
_onSearch(value);
}
}, this.props.searchDebounce);
}
}
}
}, {
key: '_checkAutoComplete',
value: function _checkAutoComplete(value, props) {
var _this6 = this;
// open dropdown if any items would be included
var valueUpdated = false;
var datalist = props.datalist;
var foldedValue = this._itemAdapter.foldValue(value);
var includedItems = this._listAdapter.filter(datalist, function (i) {
return _this6._itemAdapter.itemIncludedByInput(i, foldedValue) && _this6._allowItem(i);
});
if (includedItems.length > 0) {
// if only one item is included and the value must come from the list,
// autocomplete using that item
var _datalistOnly = props.datalistOnly,
_datalistPartial = props.datalistPartial;
if (includedItems.length === 1 && _datalistOnly && !_datalistPartial) {
var found = includedItems[0];
var foundValue = this._itemAdapter.getInputValue(found);
var callback = void 0;
var _inputSelect = props.inputSelect;
if (value != foundValue && _inputSelect && this._itemAdapter.foldValue(foundValue).startsWith(foldedValue)) {
var _input = this.refs.input;
callback = function callback() {
_inputSelect(_input, value, foundValue);
};
}
this._setValueMeta(found);
this._setInputValue(foundValue, callback);
valueUpdated = true;
if (this.state.open ? props.closeOnCompletion : value != foundValue && !props.closeOnCompletion) {
this._toggleOpen('autocomplete', props);
}
} else {
// otherwise, just check if any values match, and select the first one
// (without modifying the input value)
var _found = includedItems.find(function (i) {
return _this6._itemAdapter.itemMatchesInput(i, foldedValue);
});
if (_found) {
this._setValueMeta(_found);
this._setInputValue(value);
valueUpdated = true;
}
// open dropdown unless exactly one matching value was found
if (!this.state.open && (!_found || includedItems.length > 1)) {
this._open('autocomplete', props);
}
}
}
return valueUpdated;
}
// autobind
}, {
key: '_handleItemSelect',
value: function _handleItemSelect(item) {
if (this.props.multiple) {
this._addItem(item);
} else {
var itemValue = this._itemAdapter.getInputValue(item);
this._setValueMeta(item);
this._setInputValue(itemValue);
}
this._close();
}
}, {
key: '_addItem',
value: function _addItem(item) {
if (this._allowItem(item)) {
var _selectedItems3 = [].concat(_toConsumableArray(this.state.selectedItems), [item]);
this.setState({ selectedItems: _selectedItems3 });
var _props2 = this.props,
_onAdd = _props2.onAdd,
_onChange2 = _props2.onChange;
if (_onAdd) {
_onAdd(item);
}
if (_onChange2) {
_onChange2(_selectedItems3);
}
}
this._clearInput();
if (this.state.open) {
this._close();
}
}
// autobind
}, {
key: '_removeItem',
value: function _removeItem(index) {
var previousItems = this.state.selectedItems;
var selectedItems = previousItems.slice(0, index).concat(previousItems.slice(index + 1));
this.setState({ selectedItems: selectedItems });
var _props3 = this.props,
onRemove = _props3.onRemove,
onChange = _props3.onChange;
if (onRemove) {
onRemove(index);
}
if (onChange) {
onChange(selectedItems);
}
}
}, {
key: '_addInputValue',
value: function _addInputValue() {
if (this._inputItem) {
this._addItem(this._inputItem);
return true;
}
return false;
}
// autobind
}, {
key: '_handleShowAll',
value: function _handleShowAll() {
this.setState({ disableFilter: true });
}
// autobind
}, {
key: '_handleKeyDown',
value: function _handleKeyDown(event) {
if (this.props.disabled) return;
switch (event.keyCode || event.which) {
case _keycode2['default'].codes.down:
case _keycode2['default'].codes['page down']:
if (this.state.open) {
this.refs.suggestions.focusFirst();
} else if (this._canOpen()) {
this._open('keydown', this.props);
}
event.preventDefault();
break;
case _keycode2['default'].codes.left:
case _keycode2['default'].codes.backspace:
if (this.refs.choices && this.refs.input && this._getCursorPosition(this.refs.input) === 0) {
this.refs.choices.focusLast();
event.preventDefault();
}
break;
case _keycode2['default'].codes.right:
if (this.refs.choices && this.refs.input && this._getCursorPosition(this.refs.input) === this.state.inputValue.length) {
this.refs.choices.focusFirst();
event.preventDefault();
}
break;
case _keycode2['default'].codes.enter:
if (this.props.multiple && this.state.inputValue) {
event.preventDefault();
if (this._addInputValue()) {
break;
}
}
if (this.state.open && this.state.inputFocused) {
event.preventDefault();
if (this._pseudofocusedItem) {
this._handleItemSelect(this._pseudofocusedItem);
} else {
this._close();
}
}
break;
case _keycode2['default'].codes.esc:
case _keycode2['default'].codes.tab:
this._handleMenuClose(event);
break;
}
}
}, {
key: '_getCursorPosition',
value: function _getCursorPosition(input) {
var inputNode = _reactDom2['default'].findDOMNode(input);
// istanbul ignore else
if (inputNode instanceof HTMLInputElement) {
return inputNode.selectionStart;
}
}
// autobind
}, {
key: '_handleKeyPress',
value: function _handleKeyPress() {
++this._keyPressCount;
}
// autobind
}, {
key: '_handleMenuClose',
value: function _handleMenuClose() {
if (this.state.open) {
this._close();
}
}
// autobind
}, {
key: '_handleInputFocus',
value: function _handleInputFocus() {
this.setState({ inputFocused: true });
}
// autobind
}, {
key: '_handleInputBlur',
value: function _handleInputBlur() {
this.setState({ inputFocused: false });
}
// autobind
}, {
key: '_handleFocus',
value: function _handleFocus() {
if (this._focusTimeoutId) {
clearTimeout(this._focusTimeoutId);
this._focusTimeoutId = null;
} else {
this._focused = true;
var _onFocus = this.props.onFocus;
if (_onFocus) {
var _value3 = this._getCurrentValue();
_onFocus(_value3);
}
}
}
// autobind
}, {
key: '_handleBlur',
value: function _handleBlur() {
var _this7 = this;
this._focusTimeoutId = setTimeout(function () {
_this7._focusTimeoutId = null;
_this7._focused = false;
var inputValue = _this7.state.inputValue;
var onBlur = _this7.props.onBlur;
if (_this7.props.multiple) {
if (inputValue && !_this7._addInputValue()) {
_this7._clearInput();
}
} else if (inputValue != _this7._lastValidValue) {
// invoke onBlur after state change, rather than immediately
var callback = void 0;
if (onBlur) {
callback = function callback() {
var value = _this7._getCurrentValue();
onBlur(value);
};
}
// restore last valid value/item
_this7._setValueMeta(_this7._lastValidItem, false, true, true);
_this7._setInputValue(_this7._lastValidValue, callback);
return;
}
if (onBlur) {
var _value4 = _this7._getCurrentValue();
onBlur(_value4);
}
}, 1);
}
}]);
return Autosuggest;
}(_react2['default'].Component);
Autosuggest.propTypes = {
/**
* Text or component appearing in the input group after the input element
* (and before any button specified in `buttonAfter`).
*/
addonAfter: _propTypes2['default'].node,
/**
* Text or component appearing in the input group before the input element
* (and before any button specified in `buttonBefore`).
*/
addonBefore: _propTypes2['default'].node,
/**
* Indicates whether duplicate values are allowed in `multiple` mode.
*/
allowDuplicates: _propTypes2['default'].bool,
/**
* Specifies the size of the form group and its contained components.
* Leave undefined for normal/medium size.
*/
bsSize: _propTypes2['default'].oneOf(['small', 'large']),
/**
* Button component appearing in the input group after the input element
* (and after any add-on specified in `addonAfter`).
*/
buttonAfter: _propTypes2['default'].node,
/**
* Button component appearing in the input group before the input element
* (and after any add-on specified in `addonBefore`).
*/
buttonBefore: _propTypes2['default'].node,
/**
* React component class used to render the selected items in multiple mode.
*/
choicesClass: _propTypes2['default'].oneOfType([_propTypes2['default'].func, _propTypes2['default'].string]),
/**
* Indicates whether the drop-down menu should be closed automatically when
* auto-completion occurs. By default, the menu will remain open, so the
* user can see any additional information about the selected item (such as
* a shorthand code that caused it to be selected).
*/
closeOnCompletion: _propTypes2['default'].bool,
/**
* A collection of items (such as an array, object, or Map) used as
* auto-complete suggestions. Each item may have any type supported by the
* `itemAdapter`. The default item adapter has basic support for any
* non-null type: it will initially try to access item properties using the
* configured property names (`itemReactKeyPropName`, `itemSortKeyPropName`,
* and `itemValuePropName`), but will fall back to using the `toString`
* method to obtain these properties to support primitives and other object
* types.
*
* If `datalist` is undefined or null and `onSearch` is not, the datalist
* is assumed to be dynamically populated, and the drop-down toggle will be
* enabled and will trigger `onSearch` the first time it is clicked.
* Conversely, an empty `datalist` or undefined/null `onSearch` indicates
* that there are no auto-complete options.
*/
datalist: _propTypes2['default'].any,
/**
* An instance of the ListAdapter class that provides datalist access
* methods required by this component.
*/
datalistAdapter: _propTypes2['default'].object,
/**
* Message to be displayed at the end of the datalist. It can be used to
* indicate that data is being fetched asynchronously, that an error
* occurred fetching data, or that additional options can be requested.
* It behaves similarly to a menu item, except that it is not filtered or
* sorted and cannot be selected (except to invoke `onDatalistMessageSelect`).
* Changing this property to a different non-null value while the component
* is focused causes the drop-down menu to be opened, which is useful for
* reporting status, such as that options are being fetched or failed to be
* fetched.
*/
datalistMessage: _propTypes2['default'].node,
/**
* Indicates that only values matching an item from the `datalist` property
* are considered valid. For search purposes, intermediate values of the
* underlying `input` element may not match while the component is focused,
* but any non-matching value will be replaced with the previous matching
* value when the component loses focus.
*
* Note that there are two cases where the current (valid) value may not
* correspond to an item in the datalist:
*
* - If the value was provided by the `value` or `defaultValue` property
* and either `datalist` is undefined/null (as opposed to empty) or
* `datalistPartial` is true, the value is assumed to be valid.
* - If `datalist` changes and `datalistPartial` is true, any previously
* valid value is assumed to remain valid. (Conversely, if `datalist`
* changes and `datalistPartial` is false, a previously valid value will
* be invalidated if not in the new `datalist`.)
*/
datalistOnly: _propTypes2['default'].bool,
/**
* Indicates that the `datalist` property should be considered incomplete
* for validation purposes. Specifically, if both `datalistPartial` and
* `datalistOnly` are true, changes to the `datalist` will not render
* invalid a value that was previously valid. This is useful in cases where
* a partial datalist is obtained dynamically in response to the `onSearch`
* callback.
*/
datalistPartial: _propTypes2['default'].bool,
/**
* Initial value to be rendered when used as an
* [uncontrolled component](https://facebook.github.io/react/docs/forms.html#uncontrolled-components)
* (i.e. no `value` property is supplied).
*/
defaultValue: _propTypes2['default'].any,
/**
* Indicates whether the form group is disabled, which causes all of its
* contained elements to ignore input and focus events and to be displayed
* grayed out.
*/
disabled: _propTypes2['default'].bool,
/**
* Indicates whether the suggestion list should drop up instead of down.
*
* Note that currently a drop-up list extending past the top of the page is
* clipped, rendering the clipped items inaccessible, whereas a drop-down
* list will extend the page and allow scrolling as necessary.
*/
dropup: _propTypes2['default'].bool,
/**
* Custom class name applied to the input group.
*/
groupClassName: _propTypes2['default'].st