UNPKG

@mtg-rio/mui-mentions

Version:

@mention people in a MUI TextField

179 lines 8.72 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const material_1 = require("@mui/material"); const react_1 = tslib_1.__importStar(require("react")); const Suggestion_1 = tslib_1.__importDefault(require("./Suggestion")); const types_1 = require("./types"); const utils_1 = require("./utils/utils"); function SuggestionsOverlay(props) { const { value, dataSources, selectionStart, selectionEnd, cursorRef, onSelect, onMouseDown } = props; const ulElement = (0, react_1.useRef)(null); const [suggestions, setSuggestions] = (0, react_1.useState)({}); const [focusIndex, setFocusIndex] = (0, react_1.useState)(0); const [scrollFocusedIntoView, setScrollFocusedIntoView] = (0, react_1.useState)(false); const [loading, setLoading] = (0, react_1.useState)(false); (0, react_1.useEffect)(() => { const current = ulElement.current; if (!scrollFocusedIntoView || !current) { return; } const scrollTop = current.scrollTop; let { top, bottom } = current.children[focusIndex].getBoundingClientRect(); const { top: topContainer } = current.getBoundingClientRect(); top = top - topContainer + scrollTop; bottom = bottom - topContainer + scrollTop; if (top < scrollTop) { // Handles scrolling up as focusIndex decreases current.scrollTop = top; } else if (bottom > current.offsetHeight + scrollTop) { // Handles scrolling down as focusIndex increases current.scrollTop = bottom - current.offsetHeight; } setScrollFocusedIntoView(false); }, [scrollFocusedIntoView, ulElement, focusIndex, setScrollFocusedIntoView]); const queryDataSource = (0, react_1.useCallback)((source, query, sourceIndex, querySequenceStart, querySequenceEnd, fullText) => tslib_1.__awaiter(this, void 0, void 0, function* () { try { const dataProvider = (0, utils_1.getDataProvider)(source.data, source.ignoreAccents, source.filterSuggestions); setLoading(true); const results = yield dataProvider(query); setSuggestions((s) => { return Object.assign(Object.assign({}, s), { [sourceIndex]: { queryInfo: { childIndex: sourceIndex, query, querySequenceStart, querySequenceEnd, plainTextValue: fullText, }, results, } }); }); } catch (err) { console.error(err); } finally { setLoading(false); } }), [setSuggestions]); (0, react_1.useEffect)(() => { setSuggestions({}); if (!selectionStart || selectionStart !== selectionEnd) { return; } const plainText = (0, utils_1.getPlainText)(value, dataSources); const positionInValue = (0, utils_1.mapPlainTextIndex)(plainText, dataSources, selectionStart, 'NULL'); if (!positionInValue) { return; } const substringStartIndex = (0, utils_1.getEndOfLastMention)(plainText.substring(0, positionInValue), dataSources); const substring = plainText.substring(substringStartIndex, selectionStart); // Check if suggestions have to be shown: // Match the trigger patterns of all Mention children on the extracted substring dataSources.forEach((source, sourceIndex) => { if (!source) { return; } const regex = (0, utils_1.makeTriggerRegex)(source.trigger || types_1.DefaultTrigger, source.allowSpaceInQuery); const match = substring.match(regex); if (match) { const querySequenceStart = substringStartIndex + substring.indexOf(match[1], match.index); queryDataSource(source, match[2], sourceIndex, querySequenceStart, querySequenceStart + match[1].length, plainText); } }); }, [setSuggestions, selectionStart, selectionEnd, dataSources, value, queryDataSource]); const clearSuggestions = (0, react_1.useCallback)(() => { setSuggestions({}); setFocusIndex(0); }, [setSuggestions, setFocusIndex]); const handleSelect = (0, react_1.useCallback)((result, queryInfo) => { onSelect(result, queryInfo); clearSuggestions(); }, [onSelect, clearSuggestions]); const handleMouseEnter = (0, react_1.useCallback)((focusIndex) => { setFocusIndex(focusIndex); }, [setFocusIndex]); const renderedSuggestions = (0, react_1.useMemo)(() => { return Object.values(suggestions).reduce((accResults, { results, queryInfo }) => [ ...accResults, ...results.map((result, index) => (react_1.default.createElement(Suggestion_1.default, { key: result.id, id: result.id, query: queryInfo.query, index: index, suggestion: result, focused: index === focusIndex, onClick: () => handleSelect(result, queryInfo), onMouseEnter: () => handleMouseEnter(index) }))), ], []); }, [suggestions, handleSelect, handleMouseEnter, focusIndex]); if (selectionStart === null || selectionStart !== selectionEnd) { // The user either is not typing or has highlighted text, // so we shouldn't show the suggestions return null; } if (!loading && renderedSuggestions.length === 0) { return null; } if (loading) { return null; } return (react_1.default.createElement(react_1.default.Fragment, null, react_1.default.createElement(KeyboardListener, { suggestions: suggestions, clearSuggestions: clearSuggestions, onSelect: handleSelect, focusIndex: focusIndex, setFocusIndex: setFocusIndex, setScrollFocusedIntoView: setScrollFocusedIntoView }), react_1.default.createElement(material_1.Popper, { open: true, anchorEl: cursorRef.current, placement: 'bottom-start', sx: { zIndex: 2 } }, react_1.default.createElement(material_1.Paper, { elevation: 8, onMouseDown: onMouseDown }, react_1.default.createElement(material_1.List, { ref: ulElement, sx: { width: '300px', maxHeight: '40vh', overflow: 'auto' } }, renderedSuggestions.length > 0 ? renderedSuggestions : loading && (react_1.default.createElement(material_1.Stack, { justifyContent: 'center', alignItems: 'center', height: '40vh' }, react_1.default.createElement(material_1.CircularProgress, null)))))))); } exports.default = SuggestionsOverlay; var Key; (function (Key) { Key["Tab"] = "Tab"; Key["Return"] = "Enter"; Key["Escape"] = "Escape"; Key["Up"] = "ArrowUp"; Key["Down"] = "ArrowDown"; })(Key || (Key = {})); function KeyboardListener(props) { const { suggestions, clearSuggestions, focusIndex, setFocusIndex, setScrollFocusedIntoView, onSelect } = props; (0, react_1.useEffect)(() => { const shiftFocus = (delta) => { const suggestionsCount = (0, utils_1.countSuggestions)(suggestions); setFocusIndex((suggestionsCount + focusIndex + delta) % suggestionsCount); setScrollFocusedIntoView(true); }; const selectFocused = () => { const { result, queryInfo } = Object.values(suggestions).reduce((acc, { results, queryInfo }) => [ ...acc, ...results.map((result) => ({ result, queryInfo })), ], [])[focusIndex]; onSelect(result, queryInfo); }; const handleKeyDown = (ev) => { switch (ev.key) { case Key.Escape: { clearSuggestions(); break; } case Key.Down: { shiftFocus(+1); break; } case Key.Up: { shiftFocus(-1); break; } case Key.Return: case Key.Tab: { selectFocused(); break; } default: { return; } } ev.preventDefault(); ev.stopPropagation(); }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [suggestions, clearSuggestions, focusIndex, setFocusIndex, onSelect, setScrollFocusedIntoView]); return null; } //# sourceMappingURL=SuggestionsOverlay.js.map