UNPKG

@uimkit/uikit-react

Version:

<img style="width:64px" src="https://mgmt.uimkit.chat/media/img/avatar.png"/>

587 lines (584 loc) 29.5 kB
import { __extends, __awaiter, __generator, __assign } from 'tslib'; import React__default from 'react'; import '../../node_modules/.pnpm/prop-types@15.8.1/node_modules/prop-types/index.js'; import getCaretCoordinates from '../../node_modules/.pnpm/textarea-caret@3.1.0/node_modules/textarea-caret/index.js'; import '../../node_modules/react-is/index.js'; import { clsx } from '../../node_modules/.pnpm/clsx@1.2.1/node_modules/clsx/dist/clsx.m.js'; import { List } from './List.js'; import { DEFAULT_CARET_POSITION, errorMessage, defaultScrollToItem, triggerPropsCheck } from './utils.js'; import { UICommandItem } from '../UICommandItem/UICommandItem.js'; import { UIUserItem } from '../UIUserItem/UIUserItem.js'; import { r as reactIs } from '../../_virtual/index3.js'; import { p as propTypes } from '../../_virtual/index4.js'; var ReactTextareaAutocomplete = /** @class */ (function (_super) { __extends(ReactTextareaAutocomplete, _super); function ReactTextareaAutocomplete(props) { var _this = _super.call(this, props) || this; _this.getSelectionPosition = function () { if (!_this.textareaRef) return null; return { selectionEnd: _this.textareaRef.selectionEnd, selectionStart: _this.textareaRef.selectionStart, }; }; _this.getSelectedText = function () { if (!_this.textareaRef) return null; var _a = _this.textareaRef, selectionEnd = _a.selectionEnd, selectionStart = _a.selectionStart; if (selectionStart === selectionEnd) return null; return _this.state.value.substr(selectionStart, selectionEnd - selectionStart); }; _this.setCaretPosition = function (position) { if (position === void 0) { position = 0; } if (!_this.textareaRef) return; _this.textareaRef.focus(); _this.textareaRef.setSelectionRange(position, position); }; _this.getCaretPosition = function () { if (!_this.textareaRef) return 0; return _this.textareaRef.selectionEnd; }; /** * isComposing prevents double submissions in Korean and other languages. * starting point for a read: * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/isComposing * In the long term, the fix should happen by handling keypress, but changing this has unknown implications. * @param event React.KeyboardEvent */ _this._defaultShouldSubmit = function (event) { return event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing; }; _this._handleKeyDown = function (event) { var _a = _this.props.shouldSubmit, shouldSubmit = _a === void 0 ? _this._defaultShouldSubmit : _a; // prevent default behaviour when the selection list is rendered if ((event.key === 'ArrowUp' || event.key === 'ArrowDown') && _this.dropdownRef) event.preventDefault(); if (shouldSubmit === null || shouldSubmit === void 0 ? void 0 : shouldSubmit(event)) return _this._onEnter(event); if (event.key === ' ') return _this._onSpace(event); if (event.key === 'Escape') return _this._closeAutocomplete(); }; _this._onEnter = function (event) { if (!_this.textareaRef) return; var trigger = _this.state.currentTrigger; if (!trigger || !_this.state.data) { // trigger a submit _this._replaceWord(); if (_this.textareaRef) { _this.textareaRef.selectionEnd = 0; } _this.props.handleSubmit(event); _this._closeAutocomplete(); } }; _this._onSpace = function (event) { if (!_this.props.replaceWord || !_this.textareaRef) return; // don't change characters if the element doesn't have focus var hasFocus = _this.textareaRef.matches(':focus'); if (!hasFocus) return; _this._replaceWord(); }; _this._replaceWord = function () { return __awaiter(_this, void 0, void 0, function () { var value, lastWordRegex, match, lastWord, spaces, newWord, textBeforeWord, textAfterCaret, newText; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: value = this.state.value; lastWordRegex = /([^\s]+)(\s*)$/; match = lastWordRegex.exec(value.slice(0, this.getCaretPosition())); lastWord = match && match[1]; if (!lastWord) return [2 /*return*/]; spaces = match[2]; return [4 /*yield*/, this.props.replaceWord(lastWord)]; case 1: newWord = _a.sent(); if (newWord == null) return [2 /*return*/]; textBeforeWord = value.slice(0, this.getCaretPosition() - match[0].length); textAfterCaret = value.slice(this.getCaretPosition(), -1); newText = textBeforeWord + newWord + spaces + textAfterCaret; this.setState({ value: newText, }, function () { // fire onChange event after successful selection var e = new CustomEvent('change', { bubbles: true }); _this.textareaRef.dispatchEvent(e); if (_this.props.onChange) _this.props.onChange(e); }); return [2 /*return*/]; } }); }); }; _this._onSelect = function (newToken) { var _a = _this.props, closeCommandsList = _a.closeCommandsList, closeMentionsList = _a.closeMentionsList, onChange = _a.onChange, showCommandsList = _a.showCommandsList, showMentionsList = _a.showMentionsList; var _b = _this.state, stateTrigger = _b.currentTrigger, selectionEnd = _b.selectionEnd, textareaValue = _b.value; var currentTrigger = showCommandsList ? '/' : showMentionsList ? '@' : stateTrigger; if (!currentTrigger) return; var computeCaretPosition = function (position, token, startToken) { switch (position) { case 'start': return startToken; case 'next': case 'end': return startToken + token.length; default: if (!Number.isInteger(position)) { throw new Error('RTA: caretPosition should be "start", "next", "end" or number.'); } return position; } }; var textToModify = showCommandsList ? '/' : showMentionsList ? '@' : textareaValue.slice(0, selectionEnd); var startOfTokenPosition = textToModify.lastIndexOf(currentTrigger); // we add space after emoji is selected if a caret position is next var newTokenString = newToken.caretPosition === 'next' ? "".concat(newToken.text, " ") : newToken.text; var newCaretPosition = computeCaretPosition(newToken.caretPosition, newTokenString, startOfTokenPosition); var modifiedText = textToModify.substring(0, startOfTokenPosition) + newTokenString; var valueToReplace = textareaValue.replace(textToModify, modifiedText); // set the new textarea value and after that set the caret back to its position _this.setState({ dataLoading: false, value: valueToReplace, }, function () { console.log('文字变更了'); // fire onChange event after successful selection var e = new CustomEvent('change', { bubbles: true }); _this.textareaRef.dispatchEvent(e); if (onChange) onChange(e); _this.setCaretPosition(newCaretPosition); }); _this._closeAutocomplete(); if (showCommandsList) closeCommandsList(); if (showMentionsList) closeMentionsList(); }; _this._getItemOnSelect = function (paramTrigger) { var stateTrigger = _this.state.currentTrigger; var triggerSettings = _this._getCurrentTriggerSettings(paramTrigger); var currentTrigger = paramTrigger || stateTrigger; if (!currentTrigger || !triggerSettings) return null; var callback = triggerSettings.callback; if (!callback) return null; return function (item) { if (typeof callback !== 'function') { throw new Error('Output functor is not defined! You have to define "output" function. https://github.com/webscopeio/react-textarea-autocomplete#trigger-type'); } if (callback) { return callback(item, currentTrigger); } return null; }; }; _this._getTextToReplace = function (paramTrigger) { var _a = _this.state, actualToken = _a.actualToken, stateTrigger = _a.currentTrigger; var triggerSettings = _this._getCurrentTriggerSettings(paramTrigger); var currentTrigger = paramTrigger || stateTrigger; if (!currentTrigger || !triggerSettings) return null; var output = triggerSettings.output; return function (item) { if (typeof item === 'object' && (!output || typeof output !== 'function')) { throw new Error('Output functor is not defined! If you are using items as object you have to define "output" function. https://github.com/webscopeio/react-textarea-autocomplete#trigger-type'); } if (output) { var textToReplace = output(item, currentTrigger); if (!textToReplace || typeof textToReplace === 'number') { throw new Error("Output functor should return string or object in shape {text: string, caretPosition: string | number}.\nGot \"".concat(String(textToReplace), "\". Check the implementation for trigger \"").concat(currentTrigger, "\" and its token \"").concat(actualToken, "\"\n\nSee https://github.com/webscopeio/react-textarea-autocomplete#trigger-type for more informations.\n")); } if (typeof textToReplace === 'string') { return { caretPosition: DEFAULT_CARET_POSITION, text: textToReplace, }; } if (!textToReplace.text && currentTrigger !== ':') { throw new Error("Output \"text\" is not defined! Object should has shape {text: string, caretPosition: string | number}. Check the implementation for trigger \"".concat(currentTrigger, "\" and its token \"").concat(actualToken, "\"\n")); } if (!textToReplace.caretPosition) { throw new Error("Output \"caretPosition\" is not defined! Object should has shape {text: string, caretPosition: string | number}. Check the implementation for trigger \"".concat(currentTrigger, "\" and its token \"").concat(actualToken, "\"\n")); } return textToReplace; } if (typeof item !== 'string') { throw new Error('Output item should be string\n'); } return { caretPosition: DEFAULT_CARET_POSITION, text: "".concat(currentTrigger).concat(item).concat(currentTrigger), }; }; }; _this._getCurrentTriggerSettings = function (paramTrigger) { var stateTrigger = _this.state.currentTrigger; var currentTrigger = paramTrigger || stateTrigger; if (!currentTrigger) return null; return _this.props.trigger[currentTrigger]; }; _this._getValuesFromProvider = function () { var _a = _this.state, actualToken = _a.actualToken, currentTrigger = _a.currentTrigger; var triggerSettings = _this._getCurrentTriggerSettings(); if (!currentTrigger || !triggerSettings) return; var component = triggerSettings.component, dataProvider = triggerSettings.dataProvider; if (typeof dataProvider !== 'function') { throw new Error('Trigger provider has to be a function!'); } _this.setState({ dataLoading: true }); // Modified: send the full text to support / style commands dataProvider(actualToken, _this.state.value, function (data, token) { // Make sure that the result is still relevant for current query if (token !== _this.state.actualToken) return; if (!Array.isArray(data)) { throw new Error('Trigger provider has to provide an array!'); } if (!reactIs.exports.isValidElementType(component)) { throw new Error('Component should be defined!'); } // throw away if we resolved old trigger if (currentTrigger !== _this.state.currentTrigger) return; // if we haven't resolved any data let's close the autocomplete if (!data.length) { _this._closeAutocomplete(); return; } _this.setState({ component: component, data: data, dataLoading: false, }); }); }; _this._getSuggestions = function (paramTrigger) { var _a = _this.state, stateTrigger = _a.currentTrigger, data = _a.data; var currentTrigger = paramTrigger || stateTrigger; if (!currentTrigger || !data || (data && !data.length)) return null; return data; }; /** * Close autocomplete, also clean up trigger (to avoid slow promises) */ _this._closeAutocomplete = function () { _this.setState({ currentTrigger: null, data: null, dataLoading: false, left: null, top: null, }); }; _this._cleanUpProps = function () { var props = __assign({}, _this.props); var notSafe = [ 'additionalTextareaProps', 'className', 'closeCommandsList', 'closeMentionsList', 'closeOnClickOutside', 'containerClassName', 'containerStyle', 'disableMentions', 'dropdownClassName', 'dropdownStyle', 'grow', 'handleSubmit', 'innerRef', 'itemClassName', 'itemStyle', 'listClassName', 'listStyle', 'loaderClassName', 'loaderStyle', 'loadingComponent', 'minChar', 'movePopupAsYouType', 'onCaretPositionChange', 'onChange', 'ref', 'replaceWord', 'scrollToItem', 'shouldSubmit', 'showCommandsList', 'showMentionsList', 'SuggestionItem', 'SuggestionList', 'trigger', 'value', ]; // eslint-disable-next-line for (var prop in props) { if (notSafe.includes(prop)) delete props[prop]; } return props; }; _this._isCommand = function (text) { if (text[0] !== '/') return false; var tokens = text.split(' '); return tokens.length <= 1; }; _this._changeHandler = function (e) { var _a = _this.props, minChar = _a.minChar, movePopupAsYouType = _a.movePopupAsYouType, onCaretPositionChange = _a.onCaretPositionChange, onChange = _a.onChange, trigger = _a.trigger; var _b = _this.state, left = _b.left, top = _b.top; var textarea = e.target; var selectionEnd = textarea.selectionEnd, selectionStart = textarea.selectionStart, value = textarea.value; if (onChange) { e.persist(); onChange(e); } if (onCaretPositionChange) onCaretPositionChange(_this.getCaretPosition()); _this.setState({ value: value }); var currentTrigger; var lastToken; if (_this._isCommand(value)) { currentTrigger = '/'; lastToken = value; } else { var triggerTokens = Object.keys(trigger).join().replace('/', ''); var triggerNorWhitespace = "[^\\s".concat(triggerTokens, "]*"); var regex = new RegExp("(?!^|\\W)?[".concat(triggerTokens, "]").concat(triggerNorWhitespace, "\\s?").concat(triggerNorWhitespace, "$"), 'g'); var tokenMatch = value.slice(0, selectionEnd).match(regex); lastToken = tokenMatch && tokenMatch[tokenMatch.length - 1].trim(); // 根据 关键字符 设置当前 trigger currentTrigger = (lastToken && Object.keys(trigger).find(function (a) { return a === lastToken[0]; })) || null; } /* if we lost the trigger token or there is no following character we want to close the autocomplete */ if (!lastToken || lastToken.length <= minChar) { _this._closeAutocomplete(); return; } var actualToken = lastToken.slice(1); // if trigger is not configured step out from the function, otherwise proceed if (!currentTrigger) return; if (movePopupAsYouType || (top === null && left === null) || // if we have single char - trigger it means we want to re-position the autocomplete lastToken.length === 1) { var _c = getCaretCoordinates(textarea, selectionEnd), newLeft = _c.left, newTop = _c.top; _this.setState({ // make position relative to textarea left: newLeft, top: newTop - _this.textareaRef.scrollTop || 0, }); } _this.setState({ actualToken: actualToken, currentTrigger: currentTrigger, selectionEnd: selectionEnd, selectionStart: selectionStart, }, function () { try { _this._getValuesFromProvider(); } catch (err) { errorMessage(err.message); } }); }; _this._selectHandler = function (e) { var _a = _this.props, onCaretPositionChange = _a.onCaretPositionChange, onSelect = _a.onSelect; if (onCaretPositionChange) onCaretPositionChange(_this.getCaretPosition()); if (onSelect) { e.persist(); onSelect(e); } }; // The textarea itself is outside the auto-select dropdown. _this._onClickAndBlurHandler = function (e) { var _a = _this.props, closeOnClickOutside = _a.closeOnClickOutside, onBlur = _a.onBlur; // If this is a click: e.target is the textarea, and e.relatedTarget is the thing // that was actually clicked. If we clicked inside the auto-select dropdown, then // that's not a blur, from the auto-select point of view, so then do nothing. var el = e.relatedTarget; if (_this.dropdownRef && el instanceof Node && _this.dropdownRef.contains(el)) { return; } if (closeOnClickOutside) _this._closeAutocomplete(); if (onBlur) { e.persist(); onBlur(e); } }; _this._onScrollHandler = function () { return _this._closeAutocomplete(); }; _this._dropdownScroll = function (item) { var scrollToItem = _this.props.scrollToItem; if (!scrollToItem) return; if (scrollToItem === true) { defaultScrollToItem(_this.dropdownRef, item); return; } if (typeof scrollToItem !== 'function' || scrollToItem.length !== 2) { throw new Error('`scrollToItem` has to be boolean (true for default implementation) or function with two parameters: container, item.'); } scrollToItem(_this.dropdownRef, item); }; _this.getTriggerProps = function () { var _a = _this.props, showCommandsList = _a.showCommandsList, showMentionsList = _a.showMentionsList, trigger = _a.trigger; var _b = _this.state, component = _b.component, currentTrigger = _b.currentTrigger, selectionEnd = _b.selectionEnd, value = _b.value; var selectedItem = _this._getItemOnSelect(); var suggestionData = _this._getSuggestions(); var textToReplace = _this._getTextToReplace(); var triggerProps = { component: component, currentTrigger: currentTrigger, getSelectedItem: selectedItem, getTextToReplace: textToReplace, selectionEnd: selectionEnd, value: value, values: suggestionData, }; if ((showCommandsList && trigger['/']) || (showMentionsList && trigger['@'])) { var currentCommands_1; var getCommands = trigger[showCommandsList ? '/' : '@'].dataProvider; getCommands === null || getCommands === void 0 ? void 0 : getCommands('', showCommandsList ? '/' : '@', function (data) { currentCommands_1 = data; }); triggerProps.component = showCommandsList ? UICommandItem : UIUserItem; triggerProps.currentTrigger = showCommandsList ? '/' : '@'; triggerProps.getTextToReplace = _this._getTextToReplace(showCommandsList ? '/' : '@'); triggerProps.getSelectedItem = _this._getItemOnSelect(showCommandsList ? '/' : '@'); triggerProps.selectionEnd = 1; triggerProps.value = showCommandsList ? '/' : '@'; triggerProps.values = currentCommands_1; } return triggerProps; }; _this.setDropdownRef = function (element) { _this.dropdownRef = element; }; var _a = _this.props, loadingComponent = _a.loadingComponent, trigger = _a.trigger, value = _a.value; // TODO: it would be better to have the parent control state... // if (value) this.state.value = value; if (!loadingComponent) { throw new Error('RTA: loadingComponent is not defined'); } if (!trigger) { throw new Error('RTA: trigger is not defined'); } _this.state = { actualToken: '', component: null, currentTrigger: null, data: null, dataLoading: false, left: null, selectionEnd: 0, selectionStart: 0, top: null, value: value || '', }; return _this; } /** * setup to emulate the UNSAFE_componentWillReceiveProps */ ReactTextareaAutocomplete.getDerivedStateFromProps = function (props, state) { if (props.value !== state.propsValue || !state.value) { return { propsValue: props.value, value: props.value }; } else { return null; } }; ReactTextareaAutocomplete.prototype.renderSuggestionListContainer = function () { var _a = this.props, disableMentions = _a.disableMentions, dropdownClassName = _a.dropdownClassName, dropdownStyle = _a.dropdownStyle, itemClassName = _a.itemClassName, itemStyle = _a.itemStyle, listClassName = _a.listClassName, SuggestionItem = _a.SuggestionItem, _b = _a.SuggestionList, SuggestionList = _b === void 0 ? List : _b; var triggerProps = this.getTriggerProps(); if (triggerProps.values && triggerProps.currentTrigger && !(disableMentions && triggerProps.currentTrigger === '@')) { return (React__default.createElement("div", { className: clsx('rta__autocomplete', 'str-chat__suggestion-list-container', dropdownClassName), ref: this.setDropdownRef, style: dropdownStyle }, React__default.createElement(SuggestionList, __assign({ className: clsx('str-chat__suggestion-list', listClassName), dropdownScroll: this._dropdownScroll, itemClassName: clsx('str-chat__suggestion-list-item', itemClassName), itemStyle: itemStyle, onSelect: this._onSelect, SuggestionItem: SuggestionItem }, triggerProps)))); } return null; }; ReactTextareaAutocomplete.prototype.render = function () { var _this = this; var _a = this.props, className = _a.className, containerClassName = _a.containerClassName, containerStyle = _a.containerStyle, style = _a.style; var maxRows = this.props.maxRows; var _b = this.state, dataLoading = _b.dataLoading, value = _b.value; if (!this.props.grow) maxRows = 1; // By setting defaultValue to undefined, avoid error: // ForwardRef(TextareaAutosize) contains a textarea with both value and defaultValue props. // Textarea elements must be either controlled or uncontrolled return (React__default.createElement("div", { className: clsx('rta', containerClassName, { 'rta--loading': dataLoading, }), style: containerStyle }, this.renderSuggestionListContainer(), React__default.createElement("textarea", __assign({ "data-testid": 'message-input' }, this._cleanUpProps(), { className: clsx('rta__textarea', className), maxRows: maxRows, onBlur: this._onClickAndBlurHandler, onChange: this._changeHandler, onClick: this._onClickAndBlurHandler, onFocus: this.props.onFocus, onKeyDown: this._handleKeyDown, onScroll: this._onScrollHandler, onSelect: this._selectHandler, ref: function (ref) { var _a; (_a = _this.props) === null || _a === void 0 ? void 0 : _a.innerRef(ref); _this.textareaRef = ref; }, style: style, value: value }, this.props.additionalTextareaProps, { defaultValue: undefined })))); }; ReactTextareaAutocomplete.defaultProps = { closeOnClickOutside: true, maxRows: 10, minChar: 1, movePopupAsYouType: false, scrollToItem: true, value: '', }; return ReactTextareaAutocomplete; }(React__default.Component)); ReactTextareaAutocomplete.propTypes = { className: propTypes.exports.string, closeOnClickOutside: propTypes.exports.bool, containerClassName: propTypes.exports.string, containerStyle: propTypes.exports.object, disableMentions: propTypes.exports.bool, dropdownClassName: propTypes.exports.string, dropdownStyle: propTypes.exports.object, itemClassName: propTypes.exports.string, itemStyle: propTypes.exports.object, listClassName: propTypes.exports.string, listStyle: propTypes.exports.object, loaderClassName: propTypes.exports.string, loaderStyle: propTypes.exports.object, loadingComponent: propTypes.exports.elementType, minChar: propTypes.exports.number, onBlur: propTypes.exports.func, onCaretPositionChange: propTypes.exports.func, onChange: propTypes.exports.func, onSelect: propTypes.exports.func, shouldSubmit: propTypes.exports.func, style: propTypes.exports.object, SuggestionList: propTypes.exports.elementType, trigger: triggerPropsCheck, value: propTypes.exports.string, }; export { ReactTextareaAutocomplete }; //# sourceMappingURL=Textarea.js.map