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