react-bootstrap-typeahead
Version:
React typeahead with Bootstrap styling
513 lines (502 loc) • 23.1 kB
JavaScript
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _fastDeepEqual = _interopRequireDefault(require("fast-deep-equal"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _react = _interopRequireDefault(require("react"));
var _TypeaheadManager = _interopRequireDefault(require("./TypeaheadManager"));
var _TypeaheadState = require("./TypeaheadState");
var _propTypes2 = require("../propTypes");
var _utils = require("../utils");
var _constants = require("../constants");
var _excluded = ["onChange"];
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2["default"])(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = (0, _getPrototypeOf2["default"])(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2["default"])(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2["default"])(this, result); }; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
var 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.
*
* If a function is specified, it will be used to determine whether a custom
* option should be included. The return value should be true or false.
*/
allowNew: _propTypes["default"].oneOfType([_propTypes["default"].bool, _propTypes["default"].func]),
/**
* Autofocus the input when the component initially mounts.
*/
autoFocus: _propTypes["default"].bool,
/**
* Whether or not filtering should be case-sensitive.
*/
caseSensitive: (0, _propTypes2.checkPropType)(_propTypes["default"].bool, _propTypes2.caseSensitiveType),
/**
* The initial value displayed in the text input.
*/
defaultInputValue: (0, _propTypes2.checkPropType)(_propTypes["default"].string, _propTypes2.defaultInputValueType),
/**
* Whether or not the menu is displayed upon initial render.
*/
defaultOpen: _propTypes["default"].bool,
/**
* Specify any pre-selected options. Use only if you want the component to
* be uncontrolled.
*/
defaultSelected: (0, _propTypes2.checkPropType)(_propTypes["default"].arrayOf(_propTypes2.optionType), _propTypes2.defaultSelectedType),
/**
* Either an array of fields in `option` to search, or a custom filtering
* callback.
*/
filterBy: _propTypes["default"].oneOfType([_propTypes["default"].arrayOf(_propTypes["default"].string.isRequired), _propTypes["default"].func]),
/**
* Highlights the menu item if there is only one result and allows selecting
* that item by hitting enter. Does not work with `allowNew`.
*/
highlightOnlyResult: (0, _propTypes2.checkPropType)(_propTypes["default"].bool, _propTypes2.highlightOnlyResultType),
/**
* An html id attribute, required for assistive technologies such as screen
* readers.
*/
id: (0, _propTypes2.checkPropType)(_propTypes["default"].oneOfType([_propTypes["default"].number, _propTypes["default"].string]), _propTypes2.isRequiredForA11y),
/**
* Whether the filter should ignore accents and other diacritical marks.
*/
ignoreDiacritics: (0, _propTypes2.checkPropType)(_propTypes["default"].bool, _propTypes2.ignoreDiacriticsType),
/**
* 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: (0, _propTypes2.checkPropType)(_propTypes["default"].oneOfType([_propTypes["default"].string, _propTypes["default"].func]), _propTypes2.labelKeyType),
/**
* 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: _propTypes["default"].number,
/**
* Number of input characters that must be entered before showing results.
*/
minLength: _propTypes["default"].number,
/**
* Whether or not multiple selections are allowed.
*/
multiple: _propTypes["default"].bool,
/**
* Invoked when the input is blurred. Receives an event.
*/
onBlur: _propTypes["default"].func,
/**
* Invoked whenever items are added or removed. Receives an array of the
* selected options.
*/
onChange: _propTypes["default"].func,
/**
* Invoked when the input is focused. Receives an event.
*/
onFocus: _propTypes["default"].func,
/**
* Invoked when the input value changes. Receives the string value of the
* input.
*/
onInputChange: _propTypes["default"].func,
/**
* Invoked when a key is pressed. Receives an event.
*/
onKeyDown: _propTypes["default"].func,
/**
* Invoked when menu visibility changes.
*/
onMenuToggle: _propTypes["default"].func,
/**
* Invoked when the pagination menu item is clicked. Receives an event.
*/
onPaginate: _propTypes["default"].func,
/**
* Whether or not the menu should be displayed. `undefined` allows the
* component to control visibility, while `true` and `false` show and hide
* the menu, respectively.
*/
open: _propTypes["default"].bool,
/**
* Full set of options, including pre-selected options. Must either be an
* array of objects (recommended) or strings.
*/
options: _propTypes["default"].arrayOf(_propTypes2.optionType).isRequired,
/**
* Give user the ability to display additional results if the number of
* results exceeds `maxResults`.
*/
paginate: _propTypes["default"].bool,
/**
* The selected option(s) displayed in the input. Use this prop if you want
* to control the component via its parent.
*/
selected: (0, _propTypes2.checkPropType)(_propTypes["default"].arrayOf(_propTypes2.optionType), _propTypes2.selectedType)
};
var defaultProps = {
allowNew: false,
autoFocus: false,
caseSensitive: false,
defaultInputValue: '',
defaultOpen: false,
defaultSelected: [],
filterBy: [],
highlightOnlyResult: false,
ignoreDiacritics: true,
labelKey: _constants.DEFAULT_LABELKEY,
maxResults: 100,
minLength: 0,
multiple: false,
onBlur: _utils.noop,
onFocus: _utils.noop,
onInputChange: _utils.noop,
onKeyDown: _utils.noop,
onMenuToggle: _utils.noop,
onPaginate: _utils.noop,
paginate: true
};
/**
* Manually trigger the input's change event.
* https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js/46012210#46012210
*/
function triggerInputChange(input, value) {
var inputValue = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value');
inputValue && inputValue.set && inputValue.set.call(input, value);
var e = new Event('input', {
bubbles: true
});
input.dispatchEvent(e);
}
var Typeahead = /*#__PURE__*/function (_React$Component) {
(0, _inherits2["default"])(Typeahead, _React$Component);
var _super = _createSuper(Typeahead);
function Typeahead() {
var _this;
(0, _classCallCheck2["default"])(this, Typeahead);
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = _super.call.apply(_super, [this].concat(args));
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "state", (0, _TypeaheadState.getInitialState)(_this.props));
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "inputNode", null);
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "isMenuShown", false);
// Keeps track of actual items displayed in the menu, after sorting,
// truncating, grouping, etc.
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "items", []);
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "blur", function () {
_this.inputNode && _this.inputNode.blur();
_this.hideMenu();
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "clear", function () {
_this.setState(_TypeaheadState.clearTypeahead);
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "focus", function () {
_this.inputNode && _this.inputNode.focus();
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "getInput", function () {
return _this.inputNode;
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "inputRef", function (inputNode) {
_this.inputNode = inputNode;
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "setItem", function (item, position) {
_this.items[position] = item;
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "hideMenu", function () {
_this.setState(_TypeaheadState.hideMenu);
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "toggleMenu", function () {
_this.setState(_TypeaheadState.toggleMenu);
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleActiveIndexChange", function (activeIndex) {
_this.setState(function (state) {
return {
activeIndex: activeIndex,
activeItem: activeIndex >= 0 ? state.activeItem : undefined
};
});
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleActiveItemChange", function (activeItem) {
// Don't update the active item if it hasn't changed.
if (!(0, _fastDeepEqual["default"])(activeItem, _this.state.activeItem)) {
_this.setState({
activeItem: activeItem
});
}
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleBlur", function (e) {
e.persist();
_this.setState({
isFocused: false
}, function () {
return _this.props.onBlur(e);
});
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleChange", function (selected) {
_this.props.onChange && _this.props.onChange(selected);
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleClear", function () {
_this.inputNode && triggerInputChange(_this.inputNode, '');
_this.setState(_TypeaheadState.clearTypeahead, function () {
// Change handler is automatically triggered for single selections but
// not multi-selections.
if (_this.props.multiple) {
_this._handleChange([]);
}
});
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleClick", function (e) {
var _this$props$inputProp;
e.persist();
var onClick = (_this$props$inputProp = _this.props.inputProps) === null || _this$props$inputProp === void 0 ? void 0 : _this$props$inputProp.onClick;
_this.setState(_TypeaheadState.clickOrFocusInput, function () {
return (0, _utils.isFunction)(onClick) && onClick(e);
});
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleFocus", function (e) {
e.persist();
_this.setState(_TypeaheadState.clickOrFocusInput, function () {
return _this.props.onFocus(e);
});
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleInitialItemChange", function (initialItem) {
// Don't update the initial item if it hasn't changed.
if (!(0, _fastDeepEqual["default"])(initialItem, _this.state.initialItem)) {
_this.setState({
initialItem: initialItem
});
}
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleInputChange", function (e) {
e.persist();
var text = e.currentTarget.value;
var _this$props = _this.props,
multiple = _this$props.multiple,
onInputChange = _this$props.onInputChange;
// Clear selections when the input value changes in single-select mode.
var shouldClearSelections = _this.state.selected.length && !multiple;
_this.setState(function (state, props) {
var _getInitialState = (0, _TypeaheadState.getInitialState)(props),
activeIndex = _getInitialState.activeIndex,
activeItem = _getInitialState.activeItem,
shownResults = _getInitialState.shownResults;
return {
activeIndex: activeIndex,
activeItem: activeItem,
selected: shouldClearSelections ? [] : state.selected,
showMenu: true,
shownResults: shownResults,
text: text
};
}, function () {
onInputChange(text, e);
shouldClearSelections && _this._handleChange([]);
});
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleKeyDown", function (e) {
var activeItem = _this.state.activeItem;
// Skip most actions when the menu is hidden.
if (!_this.isMenuShown) {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
_this.setState({
showMenu: true
});
}
_this.props.onKeyDown(e);
return;
}
switch (e.key) {
case 'ArrowUp':
case 'ArrowDown':
// Prevent input cursor from going to the beginning when pressing up.
e.preventDefault();
_this._handleActiveIndexChange((0, _utils.getUpdatedActiveIndex)(_this.state.activeIndex, e.key, _this.items));
break;
case 'Enter':
// Prevent form submission while menu is open.
e.preventDefault();
activeItem && _this._handleMenuItemSelect(activeItem, e);
break;
case 'Escape':
case 'Tab':
// ESC simply hides the menu. TAB will blur the input and move focus to
// the next item; hide the menu so it doesn't gain focus.
_this.hideMenu();
break;
default:
break;
}
_this.props.onKeyDown(e);
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleMenuItemSelect", function (option, e) {
if ((0, _utils.getOptionProperty)(option, 'paginationOption')) {
_this._handlePaginate(e);
} else {
_this._handleSelectionAdd(option);
}
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handlePaginate", function (e) {
e.persist();
_this.setState(function (state, props) {
return {
shownResults: state.shownResults + props.maxResults
};
}, function () {
return _this.props.onPaginate(e, _this.state.shownResults);
});
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleSelectionAdd", function (option) {
var _this$props2 = _this.props,
multiple = _this$props2.multiple,
labelKey = _this$props2.labelKey;
var selected;
var selection = option;
var text;
// Add a unique id to the custom selection. Avoid doing this in `render` so
// the id doesn't increment every time.
if (!(0, _utils.isString)(selection) && selection.customOption) {
selection = _objectSpread(_objectSpread({}, selection), {}, {
id: (0, _utils.uniqueId)('new-id-')
});
}
if (multiple) {
// If multiple selections are allowed, add the new selection to the
// existing selections.
selected = _this.state.selected.concat(selection);
text = '';
} else {
// If only a single selection is allowed, replace the existing selection
// with the new one.
selected = [selection];
text = (0, _utils.getOptionLabel)(selection, labelKey);
}
_this.setState(function (state, props) {
return _objectSpread(_objectSpread({}, (0, _TypeaheadState.hideMenu)(state, props)), {}, {
initialItem: selection,
selected: selected,
text: text
});
}, function () {
return _this._handleChange(selected);
});
});
(0, _defineProperty2["default"])((0, _assertThisInitialized2["default"])(_this), "_handleSelectionRemove", function (selection) {
var selected = _this.state.selected.filter(function (option) {
return !(0, _fastDeepEqual["default"])(option, selection);
});
// Make sure the input stays focused after the item is removed.
_this.focus();
_this.setState(function (state, props) {
return _objectSpread(_objectSpread({}, (0, _TypeaheadState.hideMenu)(state, props)), {}, {
selected: selected
});
}, function () {
return _this._handleChange(selected);
});
});
return _this;
}
(0, _createClass2["default"])(Typeahead, [{
key: "componentDidMount",
value: function componentDidMount() {
this.props.autoFocus && this.focus();
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps, prevState) {
var _this$props3 = this.props,
labelKey = _this$props3.labelKey,
multiple = _this$props3.multiple,
selected = _this$props3.selected;
(0, _utils.validateSelectedPropChange)(selected, prevProps.selected);
// Sync selections in state with those in props.
if (selected && !(0, _fastDeepEqual["default"])(selected, prevState.selected)) {
this.setState({
selected: selected
});
if (!multiple) {
this.setState({
text: selected.length ? (0, _utils.getOptionLabel)(selected[0], labelKey) : ''
});
}
}
}
}, {
key: "render",
value: function render() {
var _this$props4 = this.props,
onChange = _this$props4.onChange,
props = (0, _objectWithoutProperties2["default"])(_this$props4, _excluded);
var mergedPropsAndState = _objectSpread(_objectSpread({}, props), this.state);
var filterBy = mergedPropsAndState.filterBy,
labelKey = mergedPropsAndState.labelKey,
options = mergedPropsAndState.options,
paginate = mergedPropsAndState.paginate,
shownResults = mergedPropsAndState.shownResults,
text = mergedPropsAndState.text;
this.isMenuShown = (0, _utils.isShown)(mergedPropsAndState);
this.items = []; // Reset items on re-render.
var results = [];
if (this.isMenuShown) {
var cb = (0, _utils.isFunction)(filterBy) ? filterBy : _utils.defaultFilterBy;
results = options.filter(function (option) {
return cb(option, mergedPropsAndState);
});
// This must come before results are truncated.
var shouldPaginate = paginate && results.length > shownResults;
// Truncate results if necessary.
results = (0, _utils.getTruncatedOptions)(results, shownResults);
// Add the custom option if necessary.
if ((0, _utils.addCustomOption)(results, mergedPropsAndState)) {
results.push((0, _defineProperty2["default"])({
customOption: true
}, (0, _utils.getStringLabelKey)(labelKey), text));
}
// Add the pagination item if necessary.
if (shouldPaginate) {
var _results$push2;
results.push((_results$push2 = {}, (0, _defineProperty2["default"])(_results$push2, (0, _utils.getStringLabelKey)(labelKey), ''), (0, _defineProperty2["default"])(_results$push2, "paginationOption", true), _results$push2));
}
}
return /*#__PURE__*/_react["default"].createElement(_TypeaheadManager["default"], (0, _extends2["default"])({}, mergedPropsAndState, {
hideMenu: this.hideMenu,
inputNode: this.inputNode,
inputRef: this.inputRef,
isMenuShown: this.isMenuShown,
onActiveItemChange: this._handleActiveItemChange,
onAdd: this._handleSelectionAdd,
onBlur: this._handleBlur,
onChange: this._handleInputChange,
onClear: this._handleClear,
onClick: this._handleClick,
onFocus: this._handleFocus,
onHide: this.hideMenu,
onInitialItemChange: this._handleInitialItemChange,
onKeyDown: this._handleKeyDown,
onMenuItemClick: this._handleMenuItemSelect,
onRemove: this._handleSelectionRemove,
results: results,
setItem: this.setItem,
toggleMenu: this.toggleMenu
}));
}
}]);
return Typeahead;
}(_react["default"].Component);
(0, _defineProperty2["default"])(Typeahead, "propTypes", propTypes);
(0, _defineProperty2["default"])(Typeahead, "defaultProps", defaultProps);
var _default = exports["default"] = Typeahead;
;