@mtg-rio/mui-mentions
Version:
@mention people in a MUI TextField
177 lines • 8.36 kB
JavaScript
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