@kineticdata/react
Version:
A React library for the Kinetic Platform
425 lines (417 loc) • 20 kB
JavaScript
"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;
};