UNPKG

@mtg-rio/mui-mentions

Version:

@mention people in a MUI TextField

120 lines 7.04 kB
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