UNPKG

@kineticdata/react

Version:
425 lines (417 loc) 20 kB
"use strict"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard")["default"]; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault")["default"]; Object.defineProperty(exports, "__esModule", { value: true }); exports.Typeahead = void 0; var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/toConsumableArray")); var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/objectSpread2")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/createClass")); var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/assertThisInitialized")); var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/inherits")); var _createSuper2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/createSuper")); var _react = _interopRequireWildcard(require("react")); var _reactAutosuggest = _interopRequireDefault(require("react-autosuggest")); var _immutable = require("immutable"); var _lodashEs = require("lodash-es"); var initialState = { editing: false, searchField: null, searchValue: '', refocus: false, result: null }; var Typeahead = exports.Typeahead = /*#__PURE__*/function (_React$Component) { (0, _inherits2["default"])(Typeahead, _React$Component); var _super = (0, _createSuper2["default"])(Typeahead); function Typeahead(props) { var _this; (0, _classCallCheck2["default"])(this, Typeahead); _this = _super.call(this, props); _this.edit = function (event) { if (event.type === 'click' || event.type === 'keydown' && (event.keyCode === 13 || event.keyCode === 32)) { event.preventDefault(); event.stopPropagation(); if (!_this.props.disabled) { _this.setState({ editing: true }); } } }; _this.remove = function (i) { return function (event) { if (event) { event.preventDefault(); event.stopPropagation(); } if (!_this.props.disabled) { _this.props.onChange(_this.props.multiple ? _this.props.value["delete"](i) : null); } }; }; _this.onHighlight = function (_ref) { var suggestion = _ref.suggestion; if (typeof _this.props.onHighlight === 'function') { _this.props.onHighlight(suggestion); } }; _this.onSuggestionsResponse = function (searchedValue) { return function (_ref2) { var suggestions = _ref2.suggestions, error = _ref2.error, nextPageToken = _ref2.nextPageToken; if (searchedValue === _this.state.searchValue) { var _this$props$action; // when multiple mode is enabled we don't want to allow the same // suggestion to be selected twice, we compare existing selections to // suggestions using the getSuggestionValue function that returns a string // this is necessary because one of the objects may have additional fields // but should be treated as equal to an object without those fields. var mappedValues = _this.props.multiple && _this.props.value.map(_this.props.getSuggestionValue); var filtered = suggestions.map(function (suggestion) { return (0, _immutable.fromJS)(suggestion); }).filter(function (suggestion) { return !_this.props.multiple || !mappedValues.includes(_this.props.getSuggestionValue(suggestion)); }); var customSuggestion = _this.props.custom && // if the current searchValue matches an existing suggestion we do not // include it as a custom option filtered.filter(function (suggestion) { return _this.props.getSuggestionValue(suggestion) === _this.state.searchValue; }).length === 0 && (0, _immutable.fromJS)(_this.props.custom(_this.state.searchValue)); // If an action object was provided, create a suggestion that will be // used to trigger this action var actionSuggestion = typeof ((_this$props$action = _this.props.action) === null || _this$props$action === void 0 ? void 0 : _this$props$action.fn) === 'function' && (0, _immutable.fromJS)((0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, _this.props.action), {}, { __isAction: true })); _this.setState({ result: { error: error, nextPageToken: nextPageToken, suggestions: [].concat((0, _toConsumableArray2["default"])(filtered), [customSuggestion, actionSuggestion]).filter(Boolean), customSuggestion: customSuggestion } }); } }; }; // Called by Autosuggest when a fetch is requested. With the prop // alwaysRenderSuggestions this will be called onFocus, onChange, and even // when a suggestion is selected. Because of the latter, we check to see if we // should ignore the operation. Otherwise we update the searchValue in the // state and componentDidUpdate is responsible for calling search. We also // check to see if escape was pressed while the searchValue is empty, if so we // close the Autosuggest by setting state to initialState. _this.onSuggestionsFetchRequested = function (_ref3) { var searchValue = _ref3.value, reason = _ref3.reason; if (reason === 'escape-pressed' && _this.state.searchValue === '') { _this.setState(initialState); } else if (reason !== 'suggestion-selected') { _this.setState({ editing: true, searchValue: searchValue }); } else if (!_this.props.multiple && !!searchValue) { _this.setState({ refocus: true }); } }; // This implementation assumes that this is only called on blur of the input // because we are using the `alwaysRenderSuggestions` prop. _this.onSuggestionsClearRequested = function () { _this.setState(initialState); }; _this.setSearchField = function (searchField) { return function () { _this.setState({ searchField: searchField }); }; }; // Called when a suggestion is clicked or enter is pressed. For multiple mode // we also reset the searchValue to an empty string and the Autosuggest will // remain open. For single mode we close the Autosuggest entirely by setting // state to initialState. Finally we call the onChange event to update the // parent field. _this.onSuggestionSelected = function (event, _ref4) { var method = _ref4.method, suggestion = _ref4.suggestion; // Prevent form submission if enter key is used to select suggestion. if (method === 'enter') { event.preventDefault(); } // Update state if single search or query is multiple and should be cleared if (!_this.props.multiple || !event.ctrlKey && !event.metaKey && !event.shiftKey) { _this.setState(_this.props.multiple ? { searchValue: '' } : initialState); } // If this is an action suggestion, trigger the action and skip triggering // the on change event if (suggestion.get('__isAction')) { suggestion.get('fn')(); } else { _this.props.onChange(_this.props.multiple ? _this.props.value.push(suggestion) : suggestion); } }; _this.state = initialState; _this.autosuggest = /*#__PURE__*/(0, _react.createRef)(); _this.focusRef = /*#__PURE__*/(0, _react.createRef)(); _this.search = (0, _lodashEs.debounce)(function () { var _this$props; (_this$props = _this.props).search.apply(_this$props, arguments); }, 150); _this.renderInputComponent = renderInputComponent.bind((0, _assertThisInitialized2["default"])(_this)); _this.renderSuggestion = renderSuggestion.bind((0, _assertThisInitialized2["default"])(_this)); _this.renderSuggestionsContainer = renderSuggestionsContainer.bind((0, _assertThisInitialized2["default"])(_this)); _this.renderSelections = renderSelections.bind((0, _assertThisInitialized2["default"])(_this)); return _this; } (0, _createClass2["default"])(Typeahead, [{ key: "componentDidUpdate", value: function componentDidUpdate(prevProps, prevState, snapshot) { var searchLongEnough = !this.props.minSearchLength || this.state.searchValue.length >= this.props.minSearchLength; var searchChanged = this.state.searchField !== prevState.searchField || this.state.searchValue !== prevState.searchValue; var valueChanged = !(0, _immutable.is)(this.props.value, prevProps.value); if (this.state.editing) { if (searchChanged || valueChanged || !prevState.editing) { // Always clear the result even if the search value is not long enough, // in that case a message should be displayed in place of results. this.setState({ result: null }); if (searchLongEnough) { this.search(this.state.searchField, this.state.searchValue, this.onSuggestionsResponse(this.state.searchValue)); } } // If the previous state was not editing then we make sure the Autosuggest // input element is focused because it may not be visible before this. if (!prevState.editing) { this.autosuggest.current.input.focus(); } } if (this.state.refocus && !prevState.refocus) { if (this.focusRef.current) { this.focusRef.current.focus(); } this.setState({ refocus: false }); } } }, { key: "render", value: function render() { var _this$props$component = this.props.components.SelectionsContainer, SelectionsContainer = _this$props$component === void 0 ? SelectionsContainerDefault : _this$props$component; return /*#__PURE__*/_react["default"].createElement(SelectionsContainer, { disabled: this.props.disabled, multiple: this.props.multiple, value: this.props.value, selections: this.props.multiple || !this.state.editing ? this.renderSelections() : null, input: (this.props.multiple || this.state.editing) && /*#__PURE__*/_react["default"].createElement(_reactAutosuggest["default"], { alwaysRenderSuggestions: true, getSuggestionValue: this.props.getSuggestionValue, highlightFirstSuggestion: !this.props.noAutoHighlight, inputProps: { value: this.state.searchValue, onBlur: this.props.onBlur, onChange: onChangeNOOP, onFocus: this.props.onFocus, selection: this.props.value, placeholder: this.props.placeholder, id: this.props.id, form: this.props.form }, onSuggestionHighlighted: this.onHighlight, onSuggestionSelected: this.onSuggestionSelected, onSuggestionsClearRequested: this.onSuggestionsClearRequested, onSuggestionsFetchRequested: this.onSuggestionsFetchRequested, ref: this.autosuggest, renderInputComponent: this.renderInputComponent, renderSuggestion: this.renderSuggestion, renderSuggestionsContainer: this.renderSuggestionsContainer, suggestions: this.state.result ? this.state.result.suggestions : [] }) }); } }]); return Typeahead; }(_react["default"].Component); // RENDER HELPERS below need to bind to the Typeahead instance because they use // methods / props / state. They could be defined in the class instead but since // they do not alter state at all they were moved here to make that class // hopefully easier to comprehend. They are intended to be passed to the // corresponding (by name) props of the Autosuggest component. // https://github.com/moroshko/react-autosuggest#render-suggestions-container-prop function renderSuggestionsContainer(_ref5) { var containerProps = _ref5.containerProps, children = _ref5.children; var _this$props2 = this.props, action = _this$props2.action, _this$props2$componen = _this$props2.components, _this$props2$componen2 = _this$props2$componen.Status, Status = _this$props2$componen2 === void 0 ? StatusDefault : _this$props2$componen2, _this$props2$componen3 = _this$props2$componen.SuggestionsContainer, SuggestionsContainer = _this$props2$componen3 === void 0 ? SuggestionsContainerDefault : _this$props2$componen3, custom = _this$props2.custom, getStatusProps = _this$props2.getStatusProps, minSearchLength = _this$props2.minSearchLength, setSearchField = this.setSearchField, state = this.state; return /*#__PURE__*/_react["default"].createElement(SuggestionsContainer, { containerProps: containerProps, open: state.editing }, /*#__PURE__*/_react["default"].createElement(Status, getStatusProps({ searchField: state.searchField, setSearchField: setSearchField, error: state.result && state.result.error, value: state.searchValue, empty: state.result && !state.result.suggestions.some(function (suggestion) { return !suggestion.get('__isAction'); }), more: state.result && !!state.result.nextPageToken, "short": minSearchLength && state.searchValue.length < minSearchLength, pending: !state.result, custom: !!custom, action: !!action })), children); } // https://github.com/moroshko/react-autosuggest#render-suggestion-prop function renderSuggestion(suggestion, _ref6) { var isHighlighted = _ref6.isHighlighted; var _this$props3 = this.props, _this$props3$componen = _this$props3.components, _this$props3$componen2 = _this$props3$componen.Suggestion, Suggestion = _this$props3$componen2 === void 0 ? SuggestionDefault : _this$props3$componen2, _this$props3$componen3 = _this$props3$componen.SuggestionAction, SuggestionAction = _this$props3$componen3 === void 0 ? SuggestionDefault : _this$props3$componen3, getSuggestionValue = _this$props3.getSuggestionValue; var custom = this.state.result && this.state.result.customSuggestion === suggestion; var action = !!suggestion.get('__isAction'); return !action ? /*#__PURE__*/_react["default"].createElement(Suggestion, { active: isHighlighted, custom: custom, suggestion: suggestion, suggestionValue: getSuggestionValue(suggestion) }) : /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement("hr", null), /*#__PURE__*/_react["default"].createElement(SuggestionAction, { active: isHighlighted, custom: custom, suggestion: suggestion, suggestionValue: suggestion.get('label') })); } // https://github.com/moroshko/react-autosuggest#renderinputcomponent-optional function renderInputComponent(inputProps) { var _this$props4 = this.props, _this$props4$componen = _this$props4.components.Input, Input = _this$props4$componen === void 0 ? TypeaheadInputDefault : _this$props4$componen, invalid = _this$props4.invalid, minSearchLength = _this$props4.minSearchLength; return /*#__PURE__*/_react["default"].createElement(Input, { inputProps: inputProps, invalid: invalid, minSearchLength: minSearchLength }); } // Another render helper like the ones above but not actually for Autosuggest, // just meant to cleanup the render function of Typeahead. function renderSelections() { var edit = this.edit, focusRef = this.focusRef, _this$props5 = this.props, _this$props5$componen = _this$props5.components.Selection, Selection = _this$props5$componen === void 0 ? SelectionDefault : _this$props5$componen, disabled = _this$props5.disabled, getSuggestionValue = _this$props5.getSuggestionValue, multiple = _this$props5.multiple, value = _this$props5.value, placeholder = _this$props5.placeholder, id = _this$props5.id, form = _this$props5.form, invalid = _this$props5.invalid, minSearchLength = _this$props5.minSearchLength, remove = this.remove; return (multiple ? value : _immutable.List.of(value)).map(function (selection, i) { var suggestionValue = getSuggestionValue(selection); return /*#__PURE__*/_react["default"].createElement(Selection, { key: suggestionValue, disabled: disabled, edit: !multiple ? edit : null, focusRef: !multiple ? focusRef : null, remove: multiple ? remove(i) : remove(), selection: selection, suggestionValue: suggestionValue, placeholder: !multiple ? placeholder : null, id: !multiple ? id : null, invalid: invalid, form: form, minSearchLength: minSearchLength }); }); } // DEFAULT COMPONENTS below render minimally useful content. They can and should // be overridden by the components prop passed to Typeahead. If they need props // or state from the Typeahead instance there will be a render* helper above // that will bind to it. var StatusDefault = function StatusDefault(props) { return /*#__PURE__*/_react["default"].createElement("div", null, props.info && /*#__PURE__*/_react["default"].createElement("div", null, props.info, /*#__PURE__*/_react["default"].createElement("button", { onClick: props.clearFilterField }, "\xD7")), props.warning && /*#__PURE__*/_react["default"].createElement("div", null, props.warning), props.filterFieldOptions && props.filterFieldOptions.map(function (_ref7, i) { var label = _ref7.label, onClick = _ref7.onClick; return /*#__PURE__*/_react["default"].createElement("button", { onClick: onClick, key: i }, label); })); }; var SuggestionsContainerDefault = function SuggestionsContainerDefault(_ref8) { var children = _ref8.children, containerProps = _ref8.containerProps, open = _ref8.open; return /*#__PURE__*/_react["default"].createElement("div", Object.assign({}, containerProps, { style: open ? {} : { display: 'none' } }), children); }; var SelectionsContainerDefault = function SelectionsContainerDefault(_ref9) { var selections = _ref9.selections, input = _ref9.input; return /*#__PURE__*/_react["default"].createElement(_react.Fragment, null, selections, input); }; var SelectionDefault = function SelectionDefault(_ref10) { var selection = _ref10.selection, remove = _ref10.remove, edit = _ref10.edit, suggestionValue = _ref10.suggestionValue; return /*#__PURE__*/_react["default"].createElement("div", null, /*#__PURE__*/_react["default"].createElement("span", null, suggestionValue || /*#__PURE__*/_react["default"].createElement("em", null, "None")), edit && /*#__PURE__*/_react["default"].createElement("button", { type: "button", onClick: edit }, suggestionValue ? 'edit' : 'add'), selection && /*#__PURE__*/_react["default"].createElement("button", { type: "button", onClick: remove }, "remove")); }; var SuggestionDefault = function SuggestionDefault(_ref11) { var active = _ref11.active, suggestionValue = _ref11.suggestionValue; return /*#__PURE__*/_react["default"].createElement("div", null, active ? /*#__PURE__*/_react["default"].createElement("strong", null, suggestionValue) : suggestionValue); }; var TypeaheadInputDefault = function TypeaheadInputDefault(_ref12) { var inputProps = _ref12.inputProps; return /*#__PURE__*/_react["default"].createElement("input", inputProps); }; // This is passed to the Autosuggest input but we do not want to update that // value when pressing up/down and we do not want to set that input when a // suggestion is clicked so we are making this a noop. Instead we update the // searchValue state when `onSuggestionsFetchRequested` is called. var onChangeNOOP = function onChangeNOOP(event, _ref13) { var newValue = _ref13.newValue, method = _ref13.method; };