UNPKG

@talend/react-components

Version:
474 lines (455 loc) 15.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; exports.escapeRegexCharacters = escapeRegexCharacters; var _react = require("react"); var _reactI18next = require("react-i18next"); var _classnames = _interopRequireDefault(require("classnames")); var _propTypes = _interopRequireDefault(require("prop-types")); var _constants = _interopRequireDefault(require("../constants")); var _FocusManager = _interopRequireDefault(require("../FocusManager")); var _Icon = _interopRequireDefault(require("../Icon")); var _Typeahead = _interopRequireDefault(require("../Typeahead")); var _DatalistModule = _interopRequireDefault(require("./Datalist.module.scss")); var _lodash = require("lodash"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /* eslint-disable react/jsx-no-bind */ function escapeRegexCharacters(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } const PROPS_TO_OMIT = ['restricted', 'titleMap', 'value']; const DISPLAY = { ALL: 'all', FILTER: 'filter', NONE: 'none' }; function isValuePresentInSuggestions(titleMap, filterValue, multiSection) { return multiSection ? titleMap.find(group => group.suggestions.find(item => filterValue.toLowerCase() === item.name.toLowerCase())) : titleMap.find(itemValue => filterValue.toLowerCase() === itemValue.name.toLowerCase()); } function buildSuggestions({ displayMode, titleMap, filterValue, multiSection, allowAddNewElements, allowAddNewElementsSuffix }) { if (displayMode === DISPLAY.NONE) { return undefined; } if (displayMode === DISPLAY.ALL && filterValue) { const result = [...titleMap]; if (allowAddNewElements && !isValuePresentInSuggestions(titleMap, filterValue, multiSection)) { result.unshift({ title: `${filterValue} ${allowAddNewElementsSuffix}`, name: filterValue, value: filterValue }); } return result; } if (displayMode === DISPLAY.ALL || !filterValue) { return titleMap; } // building multiSection items or single section items const escapedValue = escapeRegexCharacters(filterValue.trim()); const regex = new RegExp(escapedValue, 'i'); const result = multiSection ? titleMap.map(group => ({ ...group, suggestions: filterValue ? group.suggestions.filter(item => regex.test(item.name)) : group.suggestions })).filter(group => group.suggestions.length > 0) : titleMap.filter(itemValue => regex.test(itemValue.name)); if (allowAddNewElements && !isValuePresentInSuggestions(titleMap, filterValue, multiSection)) { result.unshift({ title: `${filterValue} ${allowAddNewElementsSuffix}`, name: filterValue, value: filterValue }); } return result; } function findEntry(titleMap, attributeName, attributeValue = '') { if (!titleMap) { return null; } for (let index = 0; index < titleMap.length; index += 1) { const entry = titleMap[index]; if (entry.suggestions) { // entry is a category, it has suggestions that are real entries {name, value} const categorySuggestion = entry.suggestions.find(item => !item.disabled && item[attributeName].toLowerCase() === attributeValue.toLowerCase()); if (categorySuggestion) { return categorySuggestion; } } else if (!entry.disabled && String(entry[attributeName]).toLowerCase() === String(attributeValue).toLowerCase()) { // entry is {name, value} return entry; } } return null; } function getEntryFromName(titleMap, name, restricted) { if (name === '') { return { name, value: '' }; } const entry = findEntry(titleMap, 'name', name); if (entry) { return entry; } return restricted ? undefined : { name, value: name }; } function getEntryFromValue(titleMap, value, restricted) { const entry = findEntry(titleMap, 'value', value); if (entry) { return entry; } return restricted ? undefined : { name: value, value }; } function getEntry(titleMap, nameOrValue, restricted) { const entry = findEntry(titleMap, 'name', nameOrValue) || findEntry(titleMap, 'value', nameOrValue); if (entry) { return entry; } return restricted ? undefined : { name: nameOrValue, value: nameOrValue }; } function Datalist(props) { var _props$allowAddNewEle; // Current persisted value // It's the object value key { name: "display value", value: "technical value" } const [{ name, value }, setEntry] = (0, _react.useState)({}); const { t } = (0, _reactI18next.useTranslation)(_constants.default); // suggestions: filter value, display flag, current hover selection const [filterValue, setFilterValue] = (0, _react.useState)(''); const [displaySuggestion, setDisplaySuggestion] = (0, _react.useState)(DISPLAY.NONE); const [selection, setSelection] = (0, _react.useState)({ focusedItemIndex: undefined, focusedSectionIndex: undefined }); // suggestions computation // we don't memo bacause it changes at EVERY modification const suggestions = buildSuggestions({ displayMode: props.readOnly || props.disabled ? DISPLAY.NONE : displaySuggestion, titleMap: props.titleMap, filterValue, multiSection: props.multiSection, allowAddNewElements: props.allowAddNewElements, allowAddNewElementsSuffix: (_props$allowAddNewEle = props.allowAddNewElementsSuffix) !== null && _props$allowAddNewEle !== void 0 ? _props$allowAddNewEle : t('NEW_WITH_PARENTHESIS', '(new)') }); // in uncontrolled mode, props.value acts as an initial value, then Datalist handles state, props.value never changes. // in controlled mode, props.value has to be reflected every time it changes (0, _react.useEffect)(() => { if (props.value === undefined || props.value === null) { return; } const entry = getEntryFromValue(props.titleMap, props.value); if (!entry) { return; } if (entry.value !== value || entry.name !== name) { setEntry(entry); } // Update the input value only if user did not change it if (!name && !filterValue || name === filterValue) { setFilterValue(entry.name); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.value, props.titleMap]); // Suggestion display syntaxic sugar const resetSelection = () => setSelection({ focusedItemIndex: undefined, focusedSectionIndex: undefined }); const showFilteredSuggestions = () => setDisplaySuggestion(DISPLAY.FILTER); const showAllSuggestions = () => setDisplaySuggestion(DISPLAY.ALL); const hideSuggestions = () => { setDisplaySuggestion(DISPLAY.NONE); resetSelection(); }; const resetFilter = () => { const entry = getEntryFromValue(props.titleMap, value); if (entry) { setFilterValue(entry.name); } }; /** * Set a value. * This new value can be persisted, or not. If not, it enables the ESC key to reset the value * @param event The change event * @param newValue The new value * @param persist Value will be persisted if true */ function updateValue(event, entry, persist) { // update the suggestion filter const newFilter = entry.name; setFilterValue(newFilter); if (persist) { setEntry(entry); props.onChange(event, { value: entry.value }); } else if (props.onLiveChange) { props.onLiveChange(event, entry.value); } } /** * Update the focused item and section given the current value * Managing the two cases (multi section and single section) */ function setSelectionToCurrentValue() { if (props.multiSection) { const groups = props.titleMap; for (let sectionIndex = 0; sectionIndex < groups.length; sectionIndex += 1) { const focusedIndex = groups[sectionIndex].suggestions.findIndex(item => item.name === value); if (focusedIndex > -1) { setSelection({ focusedItemIndex: focusedIndex, focusedSectionIndex: sectionIndex }); break; } } } else { const index = props.titleMap.findIndex(item => item.name === value); setSelection({ focusedItemIndex: index === -1 ? null : index }); } } /** * Persist the current filter value as value * @param {*} event */ function persistValue(event) { hideSuggestions(); const selectedEntry = getEntryFromValue(props.titleMap, value, props.restricted); // If the filterValue is different from the selected entry if (!selectedEntry || selectedEntry.name !== filterValue) { const entry = getEntryFromName(props.titleMap, filterValue, props.restricted); if (entry && entry.value !== value) { updateValue(event, entry, true); } else { resetFilter(); } } } /** * On blur: persist the value and remove the suggestions * @param event The blur event */ const onBlur = function onBlur(event) { if (props.onBlur) { props.onBlur(); } persistValue(event); }; /** * Update value (non persistent) on input value change and update the suggestions * @param event the change event * @param payload */ function onFilterChange(event, payload) { const entry = getEntry(props.titleMap, payload.value); updateValue(event, entry, false); showFilteredSuggestions(); // resetting selection here in order to reinit the section + item indexes resetSelection(); } /** * Display suggestions on focus * @param event the focus event */ function onFocus(event) { if (props.onFocus) { props.onFocus(event); } event.target.select(); showAllSuggestions(); setSelectionToCurrentValue(); } /** * Display suggestions on click * @param event the click event */ function onClick() { showAllSuggestions(); setSelectionToCurrentValue(); } /** * Select an item in suggestions list * @param event The select event * @param sectionIndex The section index in suggestions list * @param itemIndex The item index in suggestions list */ function onSelect(event, { sectionIndex, itemIndex }) { const newEntry = props.multiSection ? suggestions[sectionIndex].suggestions[itemIndex] : suggestions[itemIndex]; hideSuggestions(); if (newEntry.disabled || newEntry.value === value) { event.preventDefault(); return; } updateValue(event, newEntry, true); } /** * Hook on input keydown * ESC: reset the value to the previous persited one * ENTER: select a suggestion, persist the value, or submit the form * UP/DOWN: update the active suggestion index * @param event The keydown event * @param highlightedItemIndex The previous focused suggestion index * @param newHighlightedItemIndex The new focused suggestion index * @param highlightedSectionIndex The previous focused section index * @param newHighlightedSectionIndex The new focused section index */ function onKeyDown(event, params) { const { highlightedItemIndex, highlightedSectionIndex, newHighlightedItemIndex, newHighlightedSectionIndex } = params; switch (event.key) { case 'Esc': case 'Escape': event.preventDefault(); resetFilter(); hideSuggestions(); break; case 'Enter': if (!suggestions) { break; } event.preventDefault(); if (Number.isInteger(highlightedItemIndex)) { // suggestions are displayed and an item has the focus : we select it onSelect(event, { itemIndex: highlightedItemIndex, sectionIndex: highlightedSectionIndex }); } else { // there is no focused item and the current value is not persisted, we persist it persistValue(event); } break; case 'Down': case 'ArrowDown': case 'Up': case 'ArrowUp': event.preventDefault(); if (!suggestions) { // display all suggestions when they are not displayed showAllSuggestions(); setSelectionToCurrentValue(); break; } setSelection({ focusedItemIndex: newHighlightedItemIndex, focusedSectionIndex: newHighlightedSectionIndex }); break; default: break; } } /** * Returns the selected item's icon props if there's one or undefined. * @returns {Object|undefined} */ function getSelectedIcon() { if (props.titleMap) { if (props.multiSection) { const multiSection = props.titleMap.find(titleMap => titleMap.suggestions.find(suggestion => suggestion.name === value)); return (0, _lodash.get)(multiSection && multiSection.suggestions.find(suggestion => suggestion.name === value), 'icon'); } const item = props.titleMap.find(titleMap => titleMap.name === value) || props.titleMap.find(titleMap => titleMap.value === value); if (item) { return (0, _lodash.get)(item, 'icon'); } } return undefined; } const icon = getSelectedIcon(); return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_FocusManager.default, { onFocusOut: onBlur, className: _DatalistModule.default['tc-datalist-item'], children: [icon && /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, { className: _DatalistModule.default['tc-datalist-item-icon'], ...icon }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Typeahead.default, { ...(0, _lodash.omit)(props, PROPS_TO_OMIT), className: (0, _classnames.default)('tc-datalist', props.className), focusedItemIndex: selection.focusedItemIndex, focusedSectionIndex: selection.focusedSectionIndex, items: suggestions, onChange: onFilterChange, onFocus: onFocus, onClick: onClick, onKeyDown: onKeyDown, onSelect: onSelect, theme: { container: (0, _classnames.default)(_DatalistModule.default.container, 'tc-datalist-container'), itemsContainer: _DatalistModule.default['items-container'], itemsList: _DatalistModule.default.items }, value: filterValue, valueId: value, caret: true })] }, "focus-manager"); } Datalist.displayName = 'Datalist component'; Datalist.defaultProps = { value: '', restricted: false, multiSection: false, titleMap: [] }; Datalist.propTypes = { autoFocus: _propTypes.default.bool, allowAddNewElements: _propTypes.default.bool, allowAddNewElementsSuffix: _propTypes.default.string, isLoading: _propTypes.default.bool, className: _propTypes.default.string, onBlur: _propTypes.default.func, onChange: _propTypes.default.func.isRequired, onFocus: _propTypes.default.func, onClick: _propTypes.default.func, onLiveChange: _propTypes.default.func, disabled: _propTypes.default.bool, multiSection: _propTypes.default.bool, readOnly: _propTypes.default.bool, restricted: _propTypes.default.bool, titleMap: _propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.shape({ name: _propTypes.default.string.isRequired, value: _propTypes.default.string.isRequired }), _propTypes.default.shape({ title: _propTypes.default.string, suggestions: _propTypes.default.arrayOf(_propTypes.default.shape({ name: _propTypes.default.string, value: _propTypes.default.string })) })])), value: _propTypes.default.string }; var _default = exports.default = Datalist; //# sourceMappingURL=Datalist.component.js.map