UNPKG

@mtg-rio/mui-mentions

Version:

@mention people in a MUI TextField

177 lines 8.36 kB
import { __awaiter } from "tslib"; import { CircularProgress, List, Paper, Popper, Stack } from '@mui/material'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Suggestion from './Suggestion'; import { DefaultTrigger, } from './types'; import { countSuggestions, getDataProvider, getEndOfLastMention, getPlainText, makeTriggerRegex, mapPlainTextIndex, } from './utils/utils'; function SuggestionsOverlay(props) { const { value, dataSources, selectionStart, selectionEnd, cursorRef, onSelect, onMouseDown } = props; const ulElement = useRef(null); const [suggestions, setSuggestions] = useState({}); const [focusIndex, setFocusIndex] = useState(0); const [scrollFocusedIntoView, setScrollFocusedIntoView] = useState(false); const [loading, setLoading] = useState(false); 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 = useCallback((source, query, sourceIndex, querySequenceStart, querySequenceEnd, fullText) => __awaiter(this, void 0, void 0, function* () { try { const dataProvider = 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]); useEffect(() => { setSuggestions({}); if (!selectionStart || selectionStart !== selectionEnd) { return; } const plainText = getPlainText(value, dataSources); const positionInValue = mapPlainTextIndex(plainText, dataSources, selectionStart, 'NULL'); if (!positionInValue) { return; } const substringStartIndex = 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 = makeTriggerRegex(source.trigger || 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 = useCallback(() => { setSuggestions({}); setFocusIndex(0); }, [setSuggestions, setFocusIndex]); const handleSelect = useCallback((result, queryInfo) => { onSelect(result, queryInfo); clearSuggestions(); }, [onSelect, clearSuggestions]); const handleMouseEnter = useCallback((focusIndex) => { setFocusIndex(focusIndex); }, [setFocusIndex]); const renderedSuggestions = useMemo(() => { return Object.values(suggestions).reduce((accResults, { results, queryInfo }) => [ ...accResults, ...results.map((result, index) => (React.createElement(Suggestion, { 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.createElement(React.Fragment, null, React.createElement(KeyboardListener, { suggestions: suggestions, clearSuggestions: clearSuggestions, onSelect: handleSelect, focusIndex: focusIndex, setFocusIndex: setFocusIndex, setScrollFocusedIntoView: setScrollFocusedIntoView }), React.createElement(Popper, { open: true, anchorEl: cursorRef.current, placement: 'bottom-start', sx: { zIndex: 2 } }, React.createElement(Paper, { elevation: 8, onMouseDown: onMouseDown }, React.createElement(List, { ref: ulElement, sx: { width: '300px', maxHeight: '40vh', overflow: 'auto' } }, renderedSuggestions.length > 0 ? renderedSuggestions : loading && (React.createElement(Stack, { justifyContent: 'center', alignItems: 'center', height: '40vh' }, React.createElement(CircularProgress, null)))))))); } export 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; useEffect(() => { const shiftFocus = (delta) => { const suggestionsCount = 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