@mtg-rio/mui-mentions
Version:
@mention people in a MUI TextField
120 lines • 7.04 kB
JavaScript
import { __rest } from "tslib";
import { TextField } from '@mui/material';
import React, { useEffect, useRef, useState } from 'react';
import Highlighter from './Highlighter';
import SuggestionsOverlay from './SuggestionsOverlay';
import { DefaultDisplayTransform, DefaultMarkupTemplate, } from './types';
import { applyChangeToValue, findStartOfMentionInPlainText, getMentions, getPlainText, isNumber, makeMentionsMarkup, mapPlainTextIndex, spliceString, } from './utils/utils';
function MentionsTextField(props) {
const [stateValue, setStateValue] = useState(props.defaultValue || '');
const [inputRef, setInputRef] = useState(null);
const highlighterRef = useRef(null);
const cursorRef = useRef(null);
const suggestionsMouseDown = useRef(false);
const [selectionStart, setSelectionStart] = useState(null);
const [selectionEnd, setSelectionEnd] = useState(null);
useEffect(() => {
const input = inputRef;
const onScroll = () => {
if (!highlighterRef.current || !input) {
return;
}
highlighterRef.current.scrollLeft = input.scrollLeft;
highlighterRef.current.scrollTop = input.scrollTop;
};
input === null || input === void 0 ? void 0 : input.addEventListener('scroll', onScroll);
return () => input === null || input === void 0 ? void 0 : input.removeEventListener('scroll', onScroll);
}, [inputRef, highlighterRef]);
useEffect(() => {
const input = inputRef;
if (!input || (input.selectionStart === selectionStart && input.selectionEnd === selectionEnd)) {
return;
}
input.setSelectionRange(selectionStart, selectionEnd);
}, [selectionStart, selectionEnd, inputRef]);
const { value, defaultValue: _defaultValue, dataSources, highlightColor } = props, others = __rest(props, ["value", "defaultValue", "dataSources", "highlightColor"]);
const finalValue = value !== undefined ? value : stateValue;
const handleBlur = () => {
if (!suggestionsMouseDown.current) {
setSelectionStart(null);
setSelectionEnd(null);
}
suggestionsMouseDown.current = false;
};
const handleSuggestionsMouseDown = () => {
suggestionsMouseDown.current = true;
};
const addMention = (suggestion, { childIndex, querySequenceStart, querySequenceEnd, plainTextValue }) => {
const dataSource = dataSources[childIndex];
const { markup, displayTransform, appendSpaceOnAdd, onAdd } = dataSource;
const start = mapPlainTextIndex(finalValue, dataSources, querySequenceStart, 'START');
if (!isNumber(start)) {
return;
}
const end = start + querySequenceEnd - querySequenceStart;
let insert = makeMentionsMarkup(markup || DefaultMarkupTemplate, suggestion.id, suggestion.display);
let displayValue = (displayTransform || DefaultDisplayTransform)(suggestion.id, suggestion.display);
if (appendSpaceOnAdd) {
insert += ' ';
displayValue += ' ';
}
const newCaretPosition = querySequenceStart + displayValue.length;
setSelectionStart(newCaretPosition);
setSelectionEnd(newCaretPosition);
// Propagate change
const newValue = spliceString(finalValue, start, end, insert);
const mentions = getMentions(newValue, dataSources);
const newPlainTextValue = spliceString(plainTextValue, querySequenceStart, querySequenceEnd, displayValue);
const onChange = props.onChange || setStateValue;
onChange(newValue, newPlainTextValue, mentions);
onAdd === null || onAdd === void 0 ? void 0 : onAdd(suggestion, start, end);
};
const handleChange = (ev) => {
let newPlainTextValue = ev.target.value;
let selectionStartBefore = selectionStart;
if (!isNumber(selectionStartBefore)) {
selectionStartBefore = ev.target.selectionStart;
}
let selectionEndBefore = selectionEnd;
if (!isNumber(selectionEndBefore)) {
selectionEndBefore = ev.target.selectionEnd;
}
// Derive the new value to set by applying the local change in the textarea's plain text
const newValue = applyChangeToValue(finalValue, newPlainTextValue, selectionStartBefore, selectionEndBefore, ev.target.selectionEnd || 0, dataSources, props.multiline);
// In case a mention is deleted, also adjust the new plain text value
newPlainTextValue = getPlainText(newValue, dataSources);
// Save current selection after change to be able to restore caret position after rerendering
let selectionStartAfter = ev.target.selectionStart;
let selectionEndAfter = ev.target.selectionEnd;
// Adjust selection range in case a mention will be deleted by the characters outside of the
// selection range that are automatically deleted
const startOfMention = findStartOfMentionInPlainText(finalValue, dataSources, ev.target.selectionStart || 0);
if (startOfMention !== undefined && selectionEndAfter !== null && selectionEndAfter > startOfMention) {
// only if a deletion has taken place
const data = ev.nativeEvent.data;
selectionStartAfter = startOfMention + (data ? data.length : 0);
selectionEndAfter = selectionStartAfter;
}
setSelectionStart(selectionStartAfter);
setSelectionEnd(selectionEndAfter);
const mentions = getMentions(newValue, dataSources);
// Propagate change
const onChange = props.onChange || setStateValue;
onChange(newValue, newPlainTextValue, mentions);
};
const handleSelect = (ev) => {
var _a;
setSelectionStart(ev.target.selectionStart);
setSelectionEnd(ev.target.selectionEnd);
(_a = props.onSelect) === null || _a === void 0 ? void 0 : _a.call(props, ev);
};
const inputProps = Object.assign(Object.assign({}, others), { value: getPlainText(finalValue, dataSources, props.multiline), onChange: handleChange, onSelect: handleSelect, onBlur: handleBlur, inputProps: {
sx: { overscrollBehavior: 'none' },
} });
return (React.createElement(React.Fragment, null,
React.createElement(Highlighter, { highlighterRef: highlighterRef, cursorRef: cursorRef, selectionStart: selectionStart, selectionEnd: selectionEnd, value: finalValue, dataSources: dataSources, inputRef: inputRef, multiline: inputProps.multiline, color: highlightColor || props.color }),
React.createElement(TextField, Object.assign({ inputRef: (ref) => setInputRef(ref) }, inputProps)),
React.createElement(SuggestionsOverlay, { value: finalValue, dataSources: dataSources, selectionStart: selectionStart, selectionEnd: selectionEnd, cursorRef: cursorRef, loading: false, onSelect: addMention, onMouseDown: handleSuggestionsMouseDown })));
}
export default MentionsTextField;
//# sourceMappingURL=MentionsTextField.js.map