UNPKG

19.2 kBJavaScriptView Raw
1import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/objectWithoutPropertiesLoose";
2import _assertThisInitialized from "@babel/runtime/helpers/assertThisInitialized";
3import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose";
4import _defineProperty from "@babel/runtime/helpers/defineProperty";
5import _extends from "@babel/runtime/helpers/extends";
6import isEqual from 'fast-deep-equal';
7import PropTypes from 'prop-types';
8import React from 'react';
9import TypeaheadManager from './TypeaheadManager';
10import { caseSensitiveType, checkPropType, defaultInputValueType, defaultSelectedType, deprecated, highlightOnlyResultType, ignoreDiacriticsType, isRequiredForA11y, labelKeyType, optionType, selectedType } from '../propTypes';
11import { addCustomOption, defaultFilterBy, getOptionLabel, getStringLabelKey, getUpdatedActiveIndex, getTruncatedOptions, head, isShown, isString, noop, uniqueId, validateSelectedPropChange } from '../utils';
12import { DEFAULT_LABELKEY, DOWN, ESC, RETURN, TAB, UP } from '../constants';
13var propTypes = {
14 /**
15 * Allows the creation of new selections on the fly. Note that any new items
16 * will be added to the list of selections, but not the list of original
17 * options unless handled as such by `Typeahead`'s parent.
18 *
19 * If a function is specified, it will be used to determine whether a custom
20 * option should be included. The return value should be true or false.
21 */
22 allowNew: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
23
24 /**
25 * Autofocus the input when the component initially mounts.
26 */
27 autoFocus: PropTypes.bool,
28
29 /**
30 * Whether or not filtering should be case-sensitive.
31 */
32 caseSensitive: checkPropType(PropTypes.bool, caseSensitiveType),
33
34 /**
35 * The initial value displayed in the text input.
36 */
37 defaultInputValue: checkPropType(PropTypes.string, defaultInputValueType),
38
39 /**
40 * Whether or not the menu is displayed upon initial render.
41 */
42 defaultOpen: PropTypes.bool,
43
44 /**
45 * Specify any pre-selected options. Use only if you want the component to
46 * be uncontrolled.
47 */
48 defaultSelected: checkPropType(PropTypes.arrayOf(optionType), defaultSelectedType),
49
50 /**
51 * Either an array of fields in `option` to search, or a custom filtering
52 * callback.
53 */
54 filterBy: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string.isRequired), PropTypes.func]),
55
56 /**
57 * Highlights the menu item if there is only one result and allows selecting
58 * that item by hitting enter. Does not work with `allowNew`.
59 */
60 highlightOnlyResult: checkPropType(PropTypes.bool, highlightOnlyResultType),
61
62 /**
63 * An html id attribute, required for assistive technologies such as screen
64 * readers.
65 */
66 id: checkPropType(PropTypes.oneOfType([PropTypes.number, PropTypes.string]), isRequiredForA11y),
67
68 /**
69 * Whether the filter should ignore accents and other diacritical marks.
70 */
71 ignoreDiacritics: checkPropType(PropTypes.bool, ignoreDiacriticsType),
72
73 /**
74 * Specify the option key to use for display or a function returning the
75 * display string. By default, the selector will use the `label` key.
76 */
77 labelKey: checkPropType(PropTypes.oneOfType([PropTypes.string, PropTypes.func]), labelKeyType),
78
79 /**
80 * Maximum number of results to display by default. Mostly done for
81 * performance reasons so as not to render too many DOM nodes in the case of
82 * large data sets.
83 */
84 maxResults: PropTypes.number,
85
86 /**
87 * Number of input characters that must be entered before showing results.
88 */
89 minLength: PropTypes.number,
90
91 /**
92 * Whether or not multiple selections are allowed.
93 */
94 multiple: PropTypes.bool,
95
96 /**
97 * Invoked when the input is blurred. Receives an event.
98 */
99 onBlur: PropTypes.func,
100
101 /**
102 * Invoked whenever items are added or removed. Receives an array of the
103 * selected options.
104 */
105 onChange: PropTypes.func,
106
107 /**
108 * Invoked when the input is focused. Receives an event.
109 */
110 onFocus: PropTypes.func,
111
112 /**
113 * Invoked when the input value changes. Receives the string value of the
114 * input.
115 */
116 onInputChange: PropTypes.func,
117
118 /**
119 * Invoked when a key is pressed. Receives an event.
120 */
121 onKeyDown: PropTypes.func,
122
123 /**
124 * Invoked when menu visibility changes.
125 */
126 onMenuToggle: PropTypes.func,
127
128 /**
129 * Invoked when the pagination menu item is clicked. Receives an event.
130 */
131 onPaginate: PropTypes.func,
132
133 /**
134 * Whether or not the menu should be displayed. `undefined` allows the
135 * component to control visibility, while `true` and `false` show and hide
136 * the menu, respectively.
137 */
138 open: PropTypes.bool,
139
140 /**
141 * Full set of options, including pre-selected options. Must either be an
142 * array of objects (recommended) or strings.
143 */
144 options: PropTypes.arrayOf(optionType).isRequired,
145
146 /**
147 * Give user the ability to display additional results if the number of
148 * results exceeds `maxResults`.
149 */
150 paginate: PropTypes.bool,
151
152 /**
153 * The selected option(s) displayed in the input. Use this prop if you want
154 * to control the component via its parent.
155 */
156 selected: checkPropType(PropTypes.arrayOf(optionType), selectedType),
157
158 /**
159 * Allows selecting the hinted result by pressing enter.
160 */
161 selectHintOnEnter: deprecated(PropTypes.bool, 'Use the `shouldSelect` prop on the `Hint` component to define which ' + 'keystrokes can select the hint.')
162};
163var defaultProps = {
164 allowNew: false,
165 autoFocus: false,
166 caseSensitive: false,
167 defaultInputValue: '',
168 defaultOpen: false,
169 defaultSelected: [],
170 filterBy: [],
171 highlightOnlyResult: false,
172 ignoreDiacritics: true,
173 labelKey: DEFAULT_LABELKEY,
174 maxResults: 100,
175 minLength: 0,
176 multiple: false,
177 onBlur: noop,
178 onFocus: noop,
179 onInputChange: noop,
180 onKeyDown: noop,
181 onMenuToggle: noop,
182 onPaginate: noop,
183 paginate: true
184};
185export function getInitialState(props) {
186 var defaultInputValue = props.defaultInputValue,
187 defaultOpen = props.defaultOpen,
188 defaultSelected = props.defaultSelected,
189 maxResults = props.maxResults,
190 multiple = props.multiple;
191 var selected = props.selected ? props.selected.slice() : defaultSelected.slice();
192 var text = defaultInputValue;
193
194 if (!multiple && selected.length) {
195 // Set the text if an initial selection is passed in.
196 text = getOptionLabel(head(selected), props.labelKey);
197
198 if (selected.length > 1) {
199 // Limit to 1 selection in single-select mode.
200 selected = selected.slice(0, 1);
201 }
202 }
203
204 return {
205 activeIndex: -1,
206 activeItem: null,
207 initialItem: null,
208 isFocused: false,
209 selected: selected,
210 showMenu: defaultOpen,
211 shownResults: maxResults,
212 text: text
213 };
214}
215export function clearTypeahead(state, props) {
216 return _extends({}, getInitialState(props), {
217 isFocused: state.isFocused,
218 selected: [],
219 text: ''
220 });
221}
222export function hideMenu(state, props) {
223 var _getInitialState = getInitialState(props),
224 activeIndex = _getInitialState.activeIndex,
225 activeItem = _getInitialState.activeItem,
226 initialItem = _getInitialState.initialItem,
227 shownResults = _getInitialState.shownResults;
228
229 return {
230 activeIndex: activeIndex,
231 activeItem: activeItem,
232 initialItem: initialItem,
233 showMenu: false,
234 shownResults: shownResults
235 };
236}
237export function toggleMenu(state, props) {
238 return state.showMenu ? hideMenu(state, props) : {
239 showMenu: true
240 };
241}
242
243var Typeahead = /*#__PURE__*/function (_React$Component) {
244 _inheritsLoose(Typeahead, _React$Component);
245
246 function Typeahead() {
247 var _this;
248
249 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
250 args[_key] = arguments[_key];
251 }
252
253 _this = _React$Component.call.apply(_React$Component, [this].concat(args)) || this;
254
255 _defineProperty(_assertThisInitialized(_this), "state", getInitialState(_this.props));
256
257 _defineProperty(_assertThisInitialized(_this), "inputNode", void 0);
258
259 _defineProperty(_assertThisInitialized(_this), "isMenuShown", false);
260
261 _defineProperty(_assertThisInitialized(_this), "items", []);
262
263 _defineProperty(_assertThisInitialized(_this), "blur", function () {
264 _this.inputNode && _this.inputNode.blur();
265
266 _this.hideMenu();
267 });
268
269 _defineProperty(_assertThisInitialized(_this), "clear", function () {
270 _this.setState(clearTypeahead);
271 });
272
273 _defineProperty(_assertThisInitialized(_this), "focus", function () {
274 _this.inputNode && _this.inputNode.focus();
275 });
276
277 _defineProperty(_assertThisInitialized(_this), "getInput", function () {
278 return _this.inputNode;
279 });
280
281 _defineProperty(_assertThisInitialized(_this), "inputRef", function (inputNode) {
282 _this.inputNode = inputNode;
283 });
284
285 _defineProperty(_assertThisInitialized(_this), "setItem", function (item, position) {
286 _this.items[position] = item;
287 });
288
289 _defineProperty(_assertThisInitialized(_this), "hideMenu", function () {
290 _this.setState(hideMenu);
291 });
292
293 _defineProperty(_assertThisInitialized(_this), "toggleMenu", function () {
294 _this.setState(toggleMenu);
295 });
296
297 _defineProperty(_assertThisInitialized(_this), "_handleActiveIndexChange", function (activeIndex) {
298 _this.setState(function (state) {
299 return {
300 activeIndex: activeIndex,
301 activeItem: activeIndex === -1 ? null : state.activeItem
302 };
303 });
304 });
305
306 _defineProperty(_assertThisInitialized(_this), "_handleActiveItemChange", function (activeItem) {
307 // Don't update the active item if it hasn't changed.
308 if (!isEqual(activeItem, _this.state.activeItem)) {
309 _this.setState({
310 activeItem: activeItem
311 });
312 }
313 });
314
315 _defineProperty(_assertThisInitialized(_this), "_handleBlur", function (e) {
316 e.persist();
317
318 _this.setState({
319 isFocused: false
320 }, function () {
321 return _this.props.onBlur(e);
322 });
323 });
324
325 _defineProperty(_assertThisInitialized(_this), "_handleChange", function (selected) {
326 _this.props.onChange && _this.props.onChange(selected);
327 });
328
329 _defineProperty(_assertThisInitialized(_this), "_handleClear", function () {
330 _this.setState(clearTypeahead, function () {
331 return _this._handleChange([]);
332 });
333 });
334
335 _defineProperty(_assertThisInitialized(_this), "_handleFocus", function (e) {
336 e.persist();
337
338 _this.setState({
339 isFocused: true,
340 showMenu: true
341 }, function () {
342 return _this.props.onFocus(e);
343 });
344 });
345
346 _defineProperty(_assertThisInitialized(_this), "_handleInitialItemChange", function (initialItem) {
347 // Don't update the initial item if it hasn't changed.
348 if (!isEqual(initialItem, _this.state.initialItem)) {
349 _this.setState({
350 initialItem: initialItem
351 });
352 }
353 });
354
355 _defineProperty(_assertThisInitialized(_this), "_handleInputChange", function (e) {
356 e.persist();
357 var text = e.currentTarget.value;
358 var _this$props = _this.props,
359 multiple = _this$props.multiple,
360 onInputChange = _this$props.onInputChange; // Clear selections when the input value changes in single-select mode.
361
362 var shouldClearSelections = _this.state.selected.length && !multiple;
363
364 _this.setState(function (state, props) {
365 var _getInitialState2 = getInitialState(props),
366 activeIndex = _getInitialState2.activeIndex,
367 activeItem = _getInitialState2.activeItem,
368 shownResults = _getInitialState2.shownResults;
369
370 return {
371 activeIndex: activeIndex,
372 activeItem: activeItem,
373 selected: shouldClearSelections ? [] : state.selected,
374 showMenu: true,
375 shownResults: shownResults,
376 text: text
377 };
378 }, function () {
379 onInputChange(text, e);
380 shouldClearSelections && _this._handleChange([]);
381 });
382 });
383
384 _defineProperty(_assertThisInitialized(_this), "_handleKeyDown", function (e) {
385 var activeItem = _this.state.activeItem; // Skip most actions when the menu is hidden.
386
387 if (!_this.isMenuShown) {
388 if (e.keyCode === UP || e.keyCode === DOWN) {
389 _this.setState({
390 showMenu: true
391 });
392 }
393
394 _this.props.onKeyDown(e);
395
396 return;
397 }
398
399 switch (e.keyCode) {
400 case UP:
401 case DOWN:
402 // Prevent input cursor from going to the beginning when pressing up.
403 e.preventDefault();
404
405 _this._handleActiveIndexChange(getUpdatedActiveIndex(_this.state.activeIndex, e.keyCode, _this.items));
406
407 break;
408
409 case RETURN:
410 // Prevent form submission while menu is open.
411 e.preventDefault();
412 activeItem && _this._handleMenuItemSelect(activeItem, e);
413 break;
414
415 case ESC:
416 case TAB:
417 // ESC simply hides the menu. TAB will blur the input and move focus to
418 // the next item; hide the menu so it doesn't gain focus.
419 _this.hideMenu();
420
421 break;
422
423 default:
424 break;
425 }
426
427 _this.props.onKeyDown(e);
428 });
429
430 _defineProperty(_assertThisInitialized(_this), "_handleMenuItemSelect", function (option, e) {
431 if (option.paginationOption) {
432 _this._handlePaginate(e);
433 } else {
434 _this._handleSelectionAdd(option);
435 }
436 });
437
438 _defineProperty(_assertThisInitialized(_this), "_handlePaginate", function (e) {
439 e.persist();
440
441 _this.setState(function (state, props) {
442 return {
443 shownResults: state.shownResults + props.maxResults
444 };
445 }, function () {
446 return _this.props.onPaginate(e, _this.state.shownResults);
447 });
448 });
449
450 _defineProperty(_assertThisInitialized(_this), "_handleSelectionAdd", function (option) {
451 var _this$props2 = _this.props,
452 multiple = _this$props2.multiple,
453 labelKey = _this$props2.labelKey;
454 var selected;
455 var selection = option;
456 var text; // Add a unique id to the custom selection. Avoid doing this in `render` so
457 // the id doesn't increment every time.
458
459 if (!isString(selection) && selection.customOption) {
460 selection = _extends({}, selection, {
461 id: uniqueId('new-id-')
462 });
463 }
464
465 if (multiple) {
466 // If multiple selections are allowed, add the new selection to the
467 // existing selections.
468 selected = _this.state.selected.concat(selection);
469 text = '';
470 } else {
471 // If only a single selection is allowed, replace the existing selection
472 // with the new one.
473 selected = [selection];
474 text = getOptionLabel(selection, labelKey);
475 }
476
477 _this.setState(function (state, props) {
478 return _extends({}, hideMenu(state, props), {
479 initialItem: selection,
480 selected: selected,
481 text: text
482 });
483 }, function () {
484 return _this._handleChange(selected);
485 });
486 });
487
488 _defineProperty(_assertThisInitialized(_this), "_handleSelectionRemove", function (selection) {
489 var selected = _this.state.selected.filter(function (option) {
490 return !isEqual(option, selection);
491 }); // Make sure the input stays focused after the item is removed.
492
493
494 _this.focus();
495
496 _this.setState(function (state, props) {
497 return _extends({}, hideMenu(state, props), {
498 selected: selected
499 });
500 }, function () {
501 return _this._handleChange(selected);
502 });
503 });
504
505 return _this;
506 }
507
508 var _proto = Typeahead.prototype;
509
510 _proto.componentDidMount = function componentDidMount() {
511 this.props.autoFocus && this.focus();
512 };
513
514 _proto.componentDidUpdate = function componentDidUpdate(prevProps, prevState) {
515 var _this$props3 = this.props,
516 labelKey = _this$props3.labelKey,
517 multiple = _this$props3.multiple,
518 selected = _this$props3.selected;
519 validateSelectedPropChange(selected, prevProps.selected); // Sync selections in state with those in props.
520
521 if (selected && !isEqual(selected, prevState.selected)) {
522 this.setState({
523 selected: selected
524 });
525
526 if (!multiple) {
527 this.setState({
528 text: selected.length ? getOptionLabel(head(selected), labelKey) : ''
529 });
530 }
531 }
532 };
533
534 _proto.render = function render() {
535 // Omit `onChange` so Flow doesn't complain.
536 var _this$props4 = this.props,
537 onChange = _this$props4.onChange,
538 otherProps = _objectWithoutPropertiesLoose(_this$props4, ["onChange"]);
539
540 var mergedPropsAndState = _extends({}, otherProps, this.state);
541
542 var filterBy = mergedPropsAndState.filterBy,
543 labelKey = mergedPropsAndState.labelKey,
544 options = mergedPropsAndState.options,
545 paginate = mergedPropsAndState.paginate,
546 shownResults = mergedPropsAndState.shownResults,
547 text = mergedPropsAndState.text;
548 this.isMenuShown = isShown(mergedPropsAndState);
549 this.items = []; // Reset items on re-render.
550
551 var results = [];
552
553 if (this.isMenuShown) {
554 var cb = typeof filterBy === 'function' ? filterBy : defaultFilterBy;
555 results = options.filter(function (option) {
556 return cb(option, mergedPropsAndState);
557 }); // This must come before results are truncated.
558
559 var shouldPaginate = paginate && results.length > shownResults; // Truncate results if necessary.
560
561 results = getTruncatedOptions(results, shownResults); // Add the custom option if necessary.
562
563 if (addCustomOption(results, mergedPropsAndState)) {
564 var _results$push;
565
566 results.push((_results$push = {
567 customOption: true
568 }, _results$push[getStringLabelKey(labelKey)] = text, _results$push));
569 } // Add the pagination item if necessary.
570
571
572 if (shouldPaginate) {
573 var _results$push2;
574
575 results.push((_results$push2 = {}, _results$push2[getStringLabelKey(labelKey)] = '', _results$push2.paginationOption = true, _results$push2));
576 }
577 }
578
579 return /*#__PURE__*/React.createElement(TypeaheadManager, _extends({}, mergedPropsAndState, {
580 hideMenu: this.hideMenu,
581 inputNode: this.inputNode,
582 inputRef: this.inputRef,
583 isMenuShown: this.isMenuShown,
584 onActiveItemChange: this._handleActiveItemChange,
585 onAdd: this._handleSelectionAdd,
586 onBlur: this._handleBlur,
587 onChange: this._handleInputChange,
588 onClear: this._handleClear,
589 onFocus: this._handleFocus,
590 onHide: this.hideMenu,
591 onInitialItemChange: this._handleInitialItemChange,
592 onKeyDown: this._handleKeyDown,
593 onMenuItemClick: this._handleMenuItemSelect,
594 onRemove: this._handleSelectionRemove,
595 results: results,
596 setItem: this.setItem,
597 toggleMenu: this.toggleMenu
598 }));
599 };
600
601 return Typeahead;
602}(React.Component);
603
604_defineProperty(Typeahead, "propTypes", propTypes);
605
606_defineProperty(Typeahead, "defaultProps", defaultProps);
607
608export default Typeahead;
\No newline at end of file