react-bootstrap-typeahead-tabindex
Version:
React-based typeahead using the Bootstrap theme
800 lines (658 loc) • 26.1 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _noop2 = require('lodash/noop');
var _noop3 = _interopRequireDefault(_noop2);
var _isFinite2 = require('lodash/isFinite');
var _isFinite3 = _interopRequireDefault(_isFinite2);
var _isEqual2 = require('lodash/isEqual');
var _isEqual3 = _interopRequireDefault(_isEqual2);
var _find2 = require('lodash/find');
var _find3 = _interopRequireDefault(_find2);
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 _reactOnclickoutside = require('react-onclickoutside');
var _reactOnclickoutside2 = _interopRequireDefault(_reactOnclickoutside);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _ClearButton = require('./ClearButton.react');
var _ClearButton2 = _interopRequireDefault(_ClearButton);
var _Loader = require('./Loader.react');
var _Loader2 = _interopRequireDefault(_Loader);
var _Overlay = require('./Overlay.react');
var _Overlay2 = _interopRequireDefault(_Overlay);
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 _addCustomOption = require('./utils/addCustomOption');
var _addCustomOption2 = _interopRequireDefault(_addCustomOption);
var _defaultFilterBy = require('./utils/defaultFilterBy');
var _defaultFilterBy2 = _interopRequireDefault(_defaultFilterBy);
var _getHintText = require('./utils/getHintText');
var _getHintText2 = _interopRequireDefault(_getHintText);
var _getInputText = require('./utils/getInputText');
var _getInputText2 = _interopRequireDefault(_getInputText);
var _getOptionLabel = require('./utils/getOptionLabel');
var _getOptionLabel2 = _interopRequireDefault(_getOptionLabel);
var _getTruncatedOptions = require('./utils/getTruncatedOptions');
var _getTruncatedOptions2 = _interopRequireDefault(_getTruncatedOptions);
var _warn = require('./utils/warn');
var _warn2 = _interopRequireDefault(_warn);
var _keyCode = require('./utils/keyCode');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
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; }
function getInitialState(props) {
var defaultSelected = props.defaultSelected,
maxResults = props.maxResults;
var selected = props.selected.slice();
if (defaultSelected && defaultSelected.length) {
selected = defaultSelected;
}
return {
activeIndex: -1,
activeItem: null,
initialItem: null,
selected: selected,
showMenu: false,
shownResults: maxResults,
text: ''
};
}
/**
* Typeahead
*/
var Typeahead = function (_React$Component) {
_inherits(Typeahead, _React$Component);
function Typeahead(props) {
_classCallCheck(this, Typeahead);
var _this = _possibleConstructorReturn(this, (Typeahead.__proto__ || Object.getPrototypeOf(Typeahead)).call(this, props));
_this._getFilteredResults = function () {
var _this$props = _this.props,
caseSensitive = _this$props.caseSensitive,
filterBy = _this$props.filterBy,
ignoreDiacritics = _this$props.ignoreDiacritics,
labelKey = _this$props.labelKey,
minLength = _this$props.minLength,
multiple = _this$props.multiple,
options = _this$props.options;
var _this$state = _this.state,
selected = _this$state.selected,
text = _this$state.text;
if (text.length < minLength) {
return [];
}
var callback = Array.isArray(filterBy) ? function (option) {
return (0, _defaultFilterBy2.default)(option, text, labelKey, multiple && !!(0, _find3.default)(selected, function (o) {
return (0, _isEqual3.default)(o, option);
}), { caseSensitive: caseSensitive, ignoreDiacritics: ignoreDiacritics, fields: filterBy });
} : function (option) {
return filterBy(option, text);
};
return options.filter(callback);
};
_this.blur = function () {
_this.refs.input.blur();
_this._hideDropdown();
};
_this.clear = function () {
_this.setState(getInitialState(_this.props));
_this._updateSelected([]);
_this._updateText('');
};
_this.focus = function () {
_this.refs.input.focus();
};
_this._renderInput = function (results) {
var _this$props2 = _this.props,
bsSize = _this$props2.bsSize,
disabled = _this$props2.disabled,
labelKey = _this$props2.labelKey,
minLength = _this$props2.minLength,
multiple = _this$props2.multiple,
name = _this$props2.name,
onComponentUpdate = _this$props2.onComponentUpdate,
placeholder = _this$props2.placeholder,
renderToken = _this$props2.renderToken,
styles = _this$props2.styles,
tabIndex = _this$props2.tabIndex;
var _this$state2 = _this.state,
activeIndex = _this$state2.activeIndex,
activeItem = _this$state2.activeItem,
initialItem = _this$state2.initialItem,
selected = _this$state2.selected,
text = _this$state2.text;
var Input = multiple ? _TokenizerInput2.default : _TypeaheadInput2.default;
var inputProps = {
bsSize: bsSize,
disabled: disabled,
name: name,
onComponentUpdate: onComponentUpdate,
placeholder: placeholder,
renderToken: renderToken,
styles: styles,
tabIndex: tabIndex
};
return _react2.default.createElement(Input, _extends({}, inputProps, {
activeIndex: activeIndex,
activeItem: activeItem,
hasAux: !!_this._renderAux(),
hintText: (0, _getHintText2.default)({
activeItem: activeItem,
initialItem: initialItem,
labelKey: labelKey,
minLength: minLength,
selected: selected,
text: text
}),
initialItem: initialItem,
labelKey: labelKey,
onAdd: _this._handleAddOption,
onBlur: _this._handleBlur,
onChange: _this._handleTextChange,
onFocus: _this._handleFocus,
onKeyDown: function onKeyDown(e) {
return _this._handleKeydown(results, e);
},
onRemove: _this._handleRemoveOption,
options: results,
ref: 'input',
selected: selected.slice(),
value: (0, _getInputText2.default)({ activeItem: activeItem, labelKey: labelKey, multiple: multiple, selected: selected, text: text })
}));
};
_this._renderMenu = function (results, shouldPaginate) {
var _this$props3 = _this.props,
align = _this$props3.align,
bodyContainer = _this$props3.bodyContainer,
dropup = _this$props3.dropup,
emptyLabel = _this$props3.emptyLabel,
labelKey = _this$props3.labelKey,
maxHeight = _this$props3.maxHeight,
minLength = _this$props3.minLength,
newSelectionPrefix = _this$props3.newSelectionPrefix,
paginationText = _this$props3.paginationText,
renderMenu = _this$props3.renderMenu,
renderMenuItemChildren = _this$props3.renderMenuItemChildren,
styles = _this$props3.styles;
var _this$state3 = _this.state,
showMenu = _this$state3.showMenu,
text = _this$state3.text;
var menuProps = {
align: align,
dropup: dropup,
emptyLabel: emptyLabel,
labelKey: labelKey,
maxHeight: maxHeight,
newSelectionPrefix: newSelectionPrefix,
paginationText: paginationText,
onPaginate: _this._handlePagination,
paginate: shouldPaginate,
styles: styles,
text: text
};
var menu = renderMenu ? renderMenu(results, menuProps) : _react2.default.createElement(_TypeaheadMenu2.default, _extends({}, menuProps, {
options: results,
renderMenuItemChildren: renderMenuItemChildren
}));
return _react2.default.createElement(
_Overlay2.default,
{
container: bodyContainer ? document.body : _this,
show: showMenu && text.length >= minLength,
target: function target() {
return _this.refs.input;
} },
menu
);
};
_this._renderAux = function () {
var _this$props4 = _this.props,
bsSize = _this$props4.bsSize,
clearButton = _this$props4.clearButton,
disabled = _this$props4.disabled,
isLoading = _this$props4.isLoading;
if (isLoading) {
return _react2.default.createElement(_Loader2.default, { bsSize: bsSize });
}
if (clearButton && !disabled && _this.state.selected.length) {
return _react2.default.createElement(_ClearButton2.default, {
bsSize: bsSize,
className: 'bootstrap-typeahead-clear-button',
onClick: _this.clear
});
}
};
_this._handleActiveItemChange = function (activeItem) {
_this.setState({ activeItem: activeItem });
};
_this._handleBlur = function (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);
};
_this._handleFocus = function (e) {
_this.props.onFocus(e);
_this.setState({ showMenu: true });
};
_this._handleInitialItemChange = function (initialItem) {
var currentItem = _this.state.initialItem;
if (!currentItem) {
_this.setState({ initialItem: initialItem });
return;
}
var labelKey = _this.props.labelKey;
// Don't update the initial item if it hasn't changed. For custom items,
// compare the `labelKey` values since a unique id is generated each time,
// causing the comparison to always return false otherwise.
if ((0, _isEqual3.default)(initialItem, currentItem) || initialItem.customOption && initialItem[labelKey] === currentItem[labelKey]) {
return;
}
_this.setState({ initialItem: initialItem });
};
_this._handleTextChange = function (text) {
var _getInitialState = getInitialState(_this.props),
activeIndex = _getInitialState.activeIndex,
activeItem = _getInitialState.activeItem;
_this.setState({
activeIndex: activeIndex,
activeItem: activeItem,
showMenu: true
});
_this._updateText(text);
};
_this._handleKeydown = function (options, e) {
var _this$state4 = _this.state,
activeItem = _this$state4.activeItem,
showMenu = _this$state4.showMenu;
switch (e.keyCode) {
case _keyCode.UP:
case _keyCode.DOWN:
// Don't cycle through the options if the menu is hidden.
if (!showMenu) {
return;
}
var activeIndex = _this.state.activeIndex;
// 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;
}
var newState = { activeIndex: activeIndex };
if (activeIndex === -1) {
// Reset the active item if there is no active index.
newState.activeItem = null;
}
_this.setState(newState);
break;
case _keyCode.ESC:
case _keyCode.TAB:
// Prevent closing dialogs.
e.keyCode === _keyCode.ESC && e.preventDefault();
_this._hideDropdown();
break;
case _keyCode.RETURN:
// if menu is shown and we have active item
// there is no any sense to submit form on <RETURN>
if (!_this.props.submitFormOnEnter || showMenu && activeItem) {
// Prevent submitting forms.
e.preventDefault();
}
if (showMenu && activeItem) {
_this._handleAddOption(activeItem);
}
break;
}
};
_this._handleAddOption = function (selectedOption) {
var _this$props5 = _this.props,
multiple = _this$props5.multiple,
labelKey = _this$props5.labelKey;
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 = (0, _getOptionLabel2.default)(selectedOption, labelKey);
}
_this._hideDropdown();
_this._updateSelected(selected);
_this._updateText(text);
_this.setState({ initialItem: selectedOption });
};
_this._handlePagination = function (e) {
var _this$props6 = _this.props,
maxResults = _this$props6.maxResults,
onPaginate = _this$props6.onPaginate;
onPaginate(e);
_this.setState({ shownResults: _this.state.shownResults + maxResults });
};
_this._handleRemoveOption = function (removedOption) {
var selected = _this.state.selected.filter(function (option) {
return !(0, _isEqual3.default)(option, removedOption);
});
// Make sure the input stays focused after the item is removed.
_this.focus();
_this._hideDropdown();
_this._updateSelected(selected);
};
_this.handleClickOutside = function (e) {
_this.state.showMenu && _this._hideDropdown();
};
_this._hideDropdown = function () {
var _getInitialState2 = getInitialState(_this.props),
activeIndex = _getInitialState2.activeIndex,
activeItem = _getInitialState2.activeItem,
showMenu = _getInitialState2.showMenu,
shownResults = _getInitialState2.shownResults;
_this.setState({
activeIndex: activeIndex,
activeItem: activeItem,
showMenu: showMenu,
shownResults: shownResults
});
};
_this._updateSelected = function (selected) {
_this.setState({ selected: selected });
_this.props.onChange(selected);
};
_this._updateText = function (text) {
_this.setState({ text: text });
_this.props.onInputChange(text);
};
_this.state = getInitialState(props);
return _this;
}
_createClass(Typeahead, [{
key: 'getChildContext',
value: function getChildContext() {
return {
activeIndex: this.state.activeIndex,
onActiveItemChange: this._handleActiveItemChange,
onInitialItemChange: this._handleInitialItemChange,
onMenuItemClick: this._handleAddOption
};
}
}, {
key: 'componentWillMount',
value: function componentWillMount() {
var _props = this.props,
allowNew = _props.allowNew,
caseSensitive = _props.caseSensitive,
filterBy = _props.filterBy,
ignoreDiacritics = _props.ignoreDiacritics,
labelKey = _props.labelKey;
(0, _warn2.default)(!(typeof filterBy === 'function' && (caseSensitive || !ignoreDiacritics)), 'Your `filterBy` function will override the `caseSensitive` and ' + '`ignoreDiacritics` props.');
(0, _warn2.default)(!(typeof labelKey === 'function' && allowNew), '`labelKey` must be a string if creating new options is allowed.');
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
this.props.autoFocus && this.focus();
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
var multiple = nextProps.multiple,
selected = nextProps.selected;
// If new selections are passed via props, treat as a controlled input.
if (!(0, _isEqual3.default)(selected, this.props.selected)) {
this.setState({ selected: selected });
}
// If component changes from multi-select to single-select, keep only the
// first selection, if any.
if (this.props.multiple && !multiple) {
this._updateSelected(this.state.selected.slice(0, 1));
}
if (multiple !== this.props.multiple) {
this.setState({ text: '' });
}
}
}, {
key: 'render',
value: function render() {
var _props2 = this.props,
allowNew = _props2.allowNew,
className = _props2.className,
dropup = _props2.dropup,
labelKey = _props2.labelKey,
paginate = _props2.paginate,
styles = _props2.styles;
var _state = this.state,
shownResults = _state.shownResults,
text = _state.text;
// First filter the results by the input string.
var results = this._getFilteredResults();
// This must come before we truncate.
var shouldPaginate = paginate && results.length > shownResults;
// Truncate if necessary.
if (shouldPaginate) {
results = (0, _getTruncatedOptions2.default)(results, shownResults);
}
// Add the custom option.
if (allowNew) {
results = (0, _addCustomOption2.default)(results, text, labelKey);
}
return _react2.default.createElement(
'div',
{
className: (0, _classnames2.default)('bootstrap-typeahead', 'clearfix', 'open', {
'dropup': dropup
}, className),
style: _extends({ position: 'relative' }, styles.wrapper) },
this._renderInput(results),
this._renderAux(),
this._renderMenu(results, shouldPaginate)
);
}
/**
* Public method to allow external clearing of the input. Clears both text
* and selection(s).
*/
/**
* From `onClickOutside` HOC.
*/
}]);
return Typeahead;
}(_react2.default.Component);
Typeahead.propTypes = {
/**
* 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: _propTypes2.default.bool,
/**
* Autofocus the input when the component initially mounts.
*/
autoFocus: _propTypes2.default.bool,
/**
* Whether to render the menu inline or attach to `document.body`.
*/
bodyContainer: _propTypes2.default.bool,
/**
* Whether or not filtering should be case-sensitive.
*/
caseSensitive: _propTypes2.default.bool,
/**
* Displays a button to clear the input when there are selections.
*/
clearButton: _propTypes2.default.bool,
/**
* Specify any pre-selected options. Use only if you want the component to
* be uncontrolled.
*/
defaultSelected: _propTypes2.default.array,
/**
* Specify whether the menu should appear above the input.
*/
dropup: _propTypes2.default.bool,
/**
* Either an array of fields in `option` to search, or a custom filtering
* callback.
*/
filterBy: _propTypes2.default.oneOfType([_propTypes2.default.arrayOf(_propTypes2.default.string.isRequired), _propTypes2.default.func]),
/**
* Whether the filter should ignore accents and other diacritical marks.
*/
ignoreDiacritics: _propTypes2.default.bool,
/**
* Indicate whether an asynchromous data fetch is happening.
*/
isLoading: _propTypes2.default.bool,
/**
* Specify the option key to use for display or a function returning the
* display string. By default, the selector will use the `label` key.
*/
labelKey: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.func]),
/**
* Maximum number of results to display by default. Mostly done for
* performance reasons so as not to render too many DOM nodes in the case of
* large data sets.
*/
maxResults: _propTypes2.default.number,
/**
* Number of input characters that must be entered before showing results.
*/
minLength: _propTypes2.default.number,
/**
* Whether or not multiple selections are allowed.
*/
multiple: _propTypes2.default.bool,
/**
* Invoked when the input is blurred. Receives an event.
*/
onBlur: _propTypes2.default.func,
/**
* Invoked whenever items are added or removed. Receives an array of the
* selected options.
*/
onChange: _propTypes2.default.func,
/**
* Fires a callback when the component is updated
*/
onComponentUpdate: _propTypes2.default.func,
/**
* Invoked when the input is focused. Receives an event.
*/
onFocus: _propTypes2.default.func,
/**
* Invoked when the input value changes. Receives the string value of the
* input.
*/
onInputChange: _propTypes2.default.func,
/**
* Invoked when the pagination menu item is clicked. Receives an event.
*/
onPaginate: _propTypes2.default.func,
/**
* Full set of options, including pre-selected options. Must either be an
* array of objects (recommended) or strings.
*/
options: _propTypes2.default.oneOfType([_propTypes2.default.arrayOf(_propTypes2.default.object.isRequired), _propTypes2.default.arrayOf(_propTypes2.default.string.isRequired)]).isRequired,
/**
* Give user the ability to display additional results if the number of
* results exceeds `maxResults`.
*/
paginate: _propTypes2.default.bool,
/**
* Callback for custom menu rendering.
*/
renderMenu: _propTypes2.default.func,
/**
* The selected option(s) displayed in the input. Use this prop if you want
* to control the component via its parent.
*/
selected: _propTypes2.default.array,
/**
* List of styles passed down to subsequent elements
*/
styles: _propTypes2.default.shape({
wrapper: _propTypes2.default.object,
tokenizer: _propTypes2.default.object,
token: _propTypes2.default.object,
inputWrapper: _propTypes2.default.object,
input: _propTypes2.default.object,
inputHint: _propTypes2.default.object,
resultMenu: _propTypes2.default.object,
resultItem: _propTypes2.default.object
}),
/**
* Propagate <RETURN> event to parent form.
*/
submitFormOnEnter: _propTypes2.default.bool,
/**
* Set a custom tabindex value.
*/
tabIndex: function tabIndex(props, propName, componentName) {
var prop = props[propName];
if (!(0, _isFinite3.default)(prop) || prop < -1) {
return new Error('\n Invalid prop `' + propName + '` supplied to `' + componentName + '`.\n Validation failed; ' + propName + ' must be a number greater than or equal\n to -1.\n ');
}
}
};
Typeahead.defaultProps = {
allowNew: false,
autoFocus: false,
bodyContainer: false,
caseSensitive: false,
clearButton: false,
defaultSelected: [],
dropup: false,
filterBy: [],
ignoreDiacritics: true,
isLoading: false,
labelKey: 'label',
maxResults: 100,
minLength: 0,
multiple: false,
onBlur: _noop3.default,
onChange: _noop3.default,
onComponentUpdate: _noop3.default,
onFocus: _noop3.default,
onInputChange: _noop3.default,
onPaginate: _noop3.default,
paginate: true,
selected: [],
styles: {
wrapper: {},
tokenizer: {},
token: {},
tokenClose: {},
inputWrapper: {},
input: {},
inputHint: {},
resultMenu: {},
resultItem: {},
resultItemLink: {}
},
submitFormOnEnter: false,
tabIndex: 0
};
Typeahead.childContextTypes = {
activeIndex: _propTypes2.default.number.isRequired,
onActiveItemChange: _propTypes2.default.func.isRequired,
onInitialItemChange: _propTypes2.default.func.isRequired,
onMenuItemClick: _propTypes2.default.func.isRequired
};
exports.default = (0, _reactOnclickoutside2.default)(Typeahead);