@draft-js-plugins/mention
Version:
Mention Plugin for DraftJS
1,107 lines (1,061 loc) • 44.2 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var immutable = require('immutable');
var React = require('react');
var clsx = require('clsx');
var draftJs = require('draft-js');
var PropTypes = require('prop-types');
var escapeRegExp = require('lodash/escapeRegExp');
var once = require('lodash/once');
var reactPopper = require('react-popper');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
var clsx__default = /*#__PURE__*/_interopDefaultLegacy(clsx);
var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
var escapeRegExp__default = /*#__PURE__*/_interopDefaultLegacy(escapeRegExp);
var once__default = /*#__PURE__*/_interopDefaultLegacy(once);
function _extends() {
_extends = Object.assign ? Object.assign.bind() : function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
_setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _createForOfIteratorHelperLoose(o, allowArrayLike) {
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
if (it) return (it = it.call(o)).next.bind(it);
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
return function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function MentionLink(_ref) {
var mention = _ref.mention,
children = _ref.children,
className = _ref.className;
return /*#__PURE__*/React__default["default"].createElement("a", {
href: mention.link,
className: className,
spellCheck: false,
"data-testid": "mentionLink"
}, children);
}
function MentionText(_ref2) {
var children = _ref2.children,
className = _ref2.className;
return /*#__PURE__*/React__default["default"].createElement("span", {
className: className,
spellCheck: false,
"data-testid": "mentionText"
}, children);
}
function Mention(props) {
var entityKey = props.entityKey,
_props$theme = props.theme,
theme = _props$theme === void 0 ? {} : _props$theme,
mentionComponent = props.mentionComponent,
children = props.children,
decoratedText = props.decoratedText,
className = props.className,
contentState = props.contentState;
var combinedClassName = clsx__default["default"](theme.mention, className);
var mention = contentState.getEntity(entityKey).getData().mention;
var Component = mentionComponent || (mention.link ? MentionLink : MentionText);
return /*#__PURE__*/React__default["default"].createElement(Component, {
entityKey: entityKey,
mention: mention,
theme: theme,
className: combinedClassName,
decoratedText: decoratedText
}, children);
}
/**
* Return tail end of the string matching trigger upto the position.
*/
function getSearchTextAt(blockText, position, triggers) {
var str = blockText.substr(0, position);
var triggerPattern = triggers.map(function (trigger) {
return escapeRegExp__default["default"](trigger);
}).join('|');
var TRIGGER_REGEX = new RegExp("(\\s|^)(" + triggerPattern + ")", 'g');
var matches = str.matchAll(TRIGGER_REGEX);
var triggerStartIndex = 0;
var valueStartIndex = 0;
for (var _iterator = _createForOfIteratorHelperLoose(matches), _step; !(_step = _iterator()).done;) {
var match = _step.value;
var spaceLen = match[1].length;
var matchLen = match[2].length;
triggerStartIndex = (match.index || 0) + spaceLen;
valueStartIndex = triggerStartIndex + matchLen;
}
var matchingString = str.slice(valueStartIndex);
var end = str.length;
return {
begin: triggerStartIndex,
end: end,
matchingString: matchingString
};
}
var getSearchText = (function (editorState, selection, triggers) {
var anchorKey = selection.getAnchorKey();
var anchorOffset = selection.getAnchorOffset();
var currentContent = editorState.getCurrentContent();
var currentBlock = currentContent.getBlockForKey(anchorKey);
var blockText = currentBlock.getText();
return getSearchTextAt(blockText, anchorOffset, triggers);
});
function getTypeByTrigger(trigger) {
return trigger === '@' ? 'mention' : trigger + "mention";
}
function addMention(editorState, mention, mentionPrefix, mentionTrigger, entityMutability) {
var contentStateWithEntity = editorState.getCurrentContent().createEntity(getTypeByTrigger(mentionTrigger), entityMutability, {
mention: mention
});
var entityKey = contentStateWithEntity.getLastCreatedEntityKey();
var currentSelectionState = editorState.getSelection();
var _getSearchText = getSearchText(editorState, currentSelectionState, [mentionTrigger]),
begin = _getSearchText.begin,
end = _getSearchText.end;
// get selection of the @mention search text
var mentionTextSelection = currentSelectionState.merge({
anchorOffset: begin,
focusOffset: end
});
var mentionReplacedContent = draftJs.Modifier.replaceText(editorState.getCurrentContent(), mentionTextSelection, "" + (typeof mentionPrefix === 'string' ? mentionPrefix : mentionPrefix(mentionTrigger)) + mention.name, editorState.getCurrentInlineStyle(), entityKey);
// If the mention is inserted at the end, a space is appended right after for
// a smooth writing experience.
var blockKey = mentionTextSelection.getAnchorKey();
var blockSize = editorState.getCurrentContent().getBlockForKey(blockKey).getLength();
if (blockSize === end) {
mentionReplacedContent = draftJs.Modifier.insertText(mentionReplacedContent, mentionReplacedContent.getSelectionAfter(), ' ');
}
var newEditorState = draftJs.EditorState.push(editorState, mentionReplacedContent, 'insert-fragment');
return draftJs.EditorState.forceSelection(newEditorState, mentionReplacedContent.getSelectionAfter());
}
var decodeOffsetKey = function decodeOffsetKey(offsetKey) {
var _offsetKey$split = offsetKey.split('-'),
blockKey = _offsetKey$split[0],
decoratorKey = _offsetKey$split[1],
leafKey = _offsetKey$split[2];
return {
blockKey: blockKey,
decoratorKey: parseInt(decoratorKey, 10),
leafKey: parseInt(leafKey, 10)
};
};
var decodeOffsetKey$1 = decodeOffsetKey;
function filterUndefineds(value) {
return value !== undefined;
}
function getTriggerForMention(editorState, searches, mentionTriggers) {
// get the current selection
var selection = editorState.getSelection();
var anchorKey = selection.getAnchorKey();
var anchorOffset = selection.getAnchorOffset();
// the list should not be visible if a range is selected or the editor has no focus
if (!selection.isCollapsed() || !selection.getHasFocus()) {
return null;
}
// identify the start & end positon of each search-text
var offsetDetails = searches.map(function (offsetKey) {
return decodeOffsetKey$1(offsetKey);
});
// a leave can be empty when it is removed due event.g. using backspace
// do not check leaves, use full decorated portal text
var leaves = offsetDetails.filter(function (offsetDetail) {
return offsetDetail.blockKey === anchorKey;
}).map(function (offsetDetail) {
return editorState.getBlockTree(offsetDetail.blockKey).getIn([offsetDetail.decoratorKey]);
});
// if all leaves are undefined the popover should be removed
if (leaves.every(function (leave) {
return leave === undefined;
})) {
return null;
}
// Checks that the cursor is after the @ character but still somewhere in
// the word (search term). Setting it to allow the cursor to be left of
// the @ causes troubles due selection confusion.
var blockText = editorState.getCurrentContent().getBlockForKey(anchorKey).getText();
var triggerForSelectionInsideWord = leaves.filter(filterUndefineds).map(function (_ref) {
var start = _ref.start,
end = _ref.end;
return mentionTriggers.map(function (trigger) {
return (
// @ is the first character
start === 0 && anchorOffset >= start + trigger.length &&
//should not trigger if the cursor is before the trigger
blockText.substr(0, trigger.length) === trigger && anchorOffset <= end ||
// @ is in the text or at the end, multi triggers
mentionTriggers.length > 1 && anchorOffset >= start + trigger.length && (blockText.substr(start + 1, trigger.length) === trigger || blockText.substr(start, trigger.length) === trigger) && anchorOffset <= end ||
// @ is in the text or at the end, single trigger
mentionTriggers.length === 1 && anchorOffset >= start + trigger.length && anchorOffset <= end ? trigger : undefined
);
}).filter(filterUndefineds)[0];
}).filter(filterUndefineds);
if (triggerForSelectionInsideWord.isEmpty()) {
return null;
}
var _triggerForSelectionI = triggerForSelectionInsideWord.entrySeq().first(),
activeOffsetKey = _triggerForSelectionI[0],
activeTrigger = _triggerForSelectionI[1];
return {
activeOffsetKey: activeOffsetKey,
activeTrigger: activeTrigger
};
}
var getRelativeParent = function getRelativeParent(element) {
if (!element) {
return null;
}
var position = window.getComputedStyle(element).getPropertyValue('position');
if (position !== 'static') {
return element;
}
return getRelativeParent(element.parentElement);
};
function positionSuggestions(_ref) {
var decoratorRect = _ref.decoratorRect,
popover = _ref.popover,
props = _ref.props;
var relativeParent = getRelativeParent(popover.parentElement);
var relativeRect;
if (relativeParent) {
var relativeParentRect = relativeParent.getBoundingClientRect();
relativeRect = {
scrollLeft: relativeParent.scrollLeft,
scrollTop: relativeParent.scrollTop,
left: decoratorRect.left - relativeParentRect.left,
top: decoratorRect.bottom - relativeParentRect.top
};
} else {
relativeRect = {
scrollTop: window.pageYOffset || document.documentElement.scrollTop,
scrollLeft: window.pageXOffset || document.documentElement.scrollLeft,
top: decoratorRect.bottom,
left: decoratorRect.left
};
}
var left = relativeRect.left + relativeRect.scrollLeft;
var top = relativeRect.top + relativeRect.scrollTop;
var transform;
var transition;
if (props.open) {
if (props.suggestions.length > 0) {
transform = 'scale(1)';
transition = 'all 0.25s cubic-bezier(.3,1.2,.2,1)';
} else {
transform = 'scale(0)';
transition = 'all 0.35s cubic-bezier(.3,1,.2,1)';
}
}
return {
left: left + "px",
top: top + "px",
transform: transform,
transformOrigin: '1em 0%',
transition: transition
};
}
var warning = once__default["default"](function (text) {
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line no-console
console.warn(text);
}
});
function Avatar(_ref) {
var mention = _ref.mention,
_ref$theme = _ref.theme,
theme = _ref$theme === void 0 ? {} : _ref$theme;
if (mention.avatar) {
return /*#__PURE__*/React__default["default"].createElement("img", {
src: mention.avatar,
className: theme.mentionSuggestionsEntryAvatar,
role: "presentation"
});
}
return null;
}
var _excluded$1 = ["mention", "theme", "isFocused", "searchValue", "selectMention"];
function DefaultEntryComponent(props) {
var mention = props.mention,
theme = props.theme,
isFocused = props.isFocused;
props.searchValue;
props.selectMention;
var parentProps = _objectWithoutPropertiesLoose(props, _excluded$1);
return /*#__PURE__*/React__default["default"].createElement("div", _extends({}, parentProps, {
"aria-selected": isFocused
}), /*#__PURE__*/React__default["default"].createElement(Avatar, {
mention: mention,
theme: theme
}), /*#__PURE__*/React__default["default"].createElement("span", {
className: theme == null ? void 0 : theme.mentionSuggestionsEntryText
}, mention.name));
}
var Entry = function Entry(_ref) {
var onMentionSelect = _ref.onMentionSelect,
mention = _ref.mention,
theme = _ref.theme,
index = _ref.index,
onMentionFocus = _ref.onMentionFocus,
isFocused = _ref.isFocused,
id = _ref.id,
searchValue = _ref.searchValue,
EntryComponent = _ref.entryComponent;
var mouseDown = React.useRef(false);
var ref = React.useRef(null);
React.useEffect(function () {
if (isFocused) {
//delay the scrolling as the popup needs some time for positioning
requestAnimationFrame(function () {
var _ref$current;
return (_ref$current = ref.current) == null ? void 0 : _ref$current.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
});
}
}, [isFocused]);
React.useEffect(function () {
mouseDown.current = false;
});
var onMouseUp = function onMouseUp() {
if (mouseDown.current) {
onMentionSelect(mention);
mouseDown.current = false;
}
};
var onMouseDown = function onMouseDown(event) {
// Note: important to avoid a content edit change
event.preventDefault();
mouseDown.current = true;
};
var onMouseEnter = function onMouseEnter() {
onMentionFocus(index);
};
var className = isFocused ? theme.mentionSuggestionsEntryFocused : theme.mentionSuggestionsEntry;
return /*#__PURE__*/React__default["default"].createElement("div", {
ref: ref
}, /*#__PURE__*/React__default["default"].createElement(EntryComponent, {
className: className,
onMouseDown: onMouseDown,
onMouseUp: onMouseUp,
onMouseEnter: onMouseEnter,
role: "option",
id: id,
"aria-selected": isFocused ? 'true' : undefined,
theme: theme,
mention: mention,
isFocused: isFocused,
searchValue: searchValue,
selectMention: onMentionSelect
}));
};
Entry.propTypes = {
entryComponent: PropTypes__default["default"].any.isRequired,
searchValue: PropTypes__default["default"].string,
// eslint-disable-next-line react/no-unused-prop-types
onMentionSelect: PropTypes__default["default"].func
};
var Entry$1 = Entry;
function Popover(_ref) {
var store = _ref.store,
children = _ref.children,
theme = _ref.theme,
_ref$popperOptions = _ref.popperOptions,
popperOptions = _ref$popperOptions === void 0 ? {
placement: 'bottom-start'
} : _ref$popperOptions;
var _useState = React.useState(function () {
return clsx__default["default"](theme.mentionSuggestions, theme.mentionSuggestionsPopup);
}),
className = _useState[0],
setClassName = _useState[1];
var _useState2 = React.useState(null),
popperElement = _useState2[0],
setPopperElement = _useState2[1];
var _usePopper = reactPopper.usePopper(store.getReferenceElement(), popperElement, popperOptions),
styles = _usePopper.styles,
attributes = _usePopper.attributes;
React.useEffect(function () {
requestAnimationFrame(function () {
return setClassName(clsx__default["default"](theme.mentionSuggestions, theme.mentionSuggestionsPopup, theme.mentionSuggestionsPopupVisible));
});
}, [theme]);
return /*#__PURE__*/React__default["default"].createElement("div", _extends({
ref: setPopperElement,
style: styles.popper
}, attributes.popper, {
className: className,
role: "listbox"
}), children);
}
var _excluded = ["entryComponent", "popoverComponent", "popperOptions", "popoverContainer", "onOpenChange", "onAddMention", "onSearchChange", "suggestions", "ariaProps", "callbacks", "theme", "store", "entityMutability", "positionSuggestions", "mentionTriggers", "mentionPrefix"];
var MentionSuggestions = /*#__PURE__*/function (_Component) {
_inheritsLoose(MentionSuggestions, _Component);
function MentionSuggestions(props) {
var _this;
_this = _Component.call(this, props) || this;
_this.state = {
focusedOptionIndex: 0
};
_this.key = draftJs.genKey();
_this.lastActiveTrigger = '';
_this.onEditorStateChange = function (editorState) {
var searches = _this.props.store.getAllSearches();
// if no search portal is active there is no need to show the popover
if (searches.size === 0) {
return editorState;
}
var removeList = function removeList() {
_this.props.store.resetEscapedSearch();
_this.closeDropdown();
return editorState;
};
var triggerForMention = getTriggerForMention(editorState, searches, _this.props.mentionTriggers);
if (!triggerForMention) {
return removeList();
}
var lastActiveOffsetKey = _this.activeOffsetKey;
_this.activeOffsetKey = triggerForMention.activeOffsetKey;
_this.onSearchChange(editorState, editorState.getSelection(), _this.activeOffsetKey, lastActiveOffsetKey, triggerForMention.activeTrigger);
// make sure the escaped search is reseted in the cursor since the user
// already switched to another mention search
if (!_this.props.store.isEscaped(_this.activeOffsetKey || '')) {
_this.props.store.resetEscapedSearch();
}
// If none of the above triggered to close the window, it's safe to assume
// the dropdown should be open. This is useful when a user focuses on another
// input field and then comes back: the dropdown will show again.
if (!_this.props.open && !_this.props.store.isEscaped(_this.activeOffsetKey || '')) {
_this.openDropdown();
}
// makes sure the focused index is reseted every time a new selection opens
// or the selection was moved to another mention search
if (lastActiveOffsetKey !== _this.activeOffsetKey) {
_this.setState({
focusedOptionIndex: 0
});
}
return editorState;
};
_this.onSearchChange = function (editorState, selection, activeOffsetKey, lastActiveOffsetKey, trigger) {
var _getSearchText = getSearchText(editorState, selection, [trigger]),
searchValue = _getSearchText.matchingString;
if (_this.lastActiveTrigger !== trigger || _this.lastSearchValue !== searchValue || activeOffsetKey !== lastActiveOffsetKey) {
_this.lastActiveTrigger = trigger;
_this.lastSearchValue = searchValue;
_this.props.onSearchChange({
trigger: trigger,
value: searchValue
});
//reset focus item if search is cahnged
_this.setState({
focusedOptionIndex: 0
});
}
};
_this.onDownArrow = function (keyboardEvent) {
keyboardEvent.preventDefault();
var newIndex = _this.state.focusedOptionIndex + 1;
_this.onMentionFocus(newIndex >= _this.props.suggestions.length ? 0 : newIndex);
};
_this.onTab = function (keyboardEvent) {
keyboardEvent.preventDefault();
_this.commitSelection();
};
_this.onUpArrow = function (keyboardEvent) {
keyboardEvent.preventDefault();
if (_this.props.suggestions.length > 0) {
var newIndex = _this.state.focusedOptionIndex - 1;
_this.onMentionFocus(newIndex < 0 ? _this.props.suggestions.length - 1 : newIndex);
}
};
_this.onEscape = function (keyboardEvent) {
keyboardEvent.preventDefault();
_this.props.store.escapeSearch(_this.activeOffsetKey || '');
_this.closeDropdown();
// to force a re-render of the outer component to change the aria props
_this.props.store.setEditorState(_this.props.store.getEditorState());
};
_this.onMentionSelect = function (mention) {
// Note: This can happen in case a user typed @xxx (invalid mention) and
// then hit Enter. Then the mention will be undefined.
if (!mention) {
return;
}
if (_this.props.onAddMention) {
_this.props.onAddMention(mention);
}
_this.closeDropdown();
var newEditorState = addMention(_this.props.store.getEditorState(), mention, _this.props.mentionPrefix, _this.lastActiveTrigger || '', _this.props.entityMutability);
_this.props.store.setEditorState(newEditorState);
};
_this.onMentionFocus = function (index) {
var descendant = "mention-option-" + _this.key + "-" + index;
_this.props.ariaProps.ariaActiveDescendantID = descendant;
_this.setState({
focusedOptionIndex: index
});
// to force a re-render of the outer component to change the aria props
_this.props.store.setEditorState(_this.props.store.getEditorState());
};
_this.commitSelection = function () {
var mention = _this.props.suggestions[_this.state.focusedOptionIndex];
if (!_this.props.store.getIsOpened() || !mention) {
return 'not-handled';
}
_this.onMentionSelect(mention);
return 'handled';
};
_this.openDropdown = function () {
// This is a really nasty way of attaching & releasing the key related functions.
// It assumes that the keyFunctions object will not loose its reference and
// by this we can replace inner parameters spread over different modules.
// This better be some registering & unregistering logic. PRs are welcome :)
_this.props.callbacks.handleReturn = _this.commitSelection;
_this.props.callbacks.keyBindingFn = function (keyboardEvent) {
// arrow down
if (keyboardEvent.keyCode === 40) {
_this.onDownArrow(keyboardEvent);
}
// arrow up
if (keyboardEvent.keyCode === 38) {
_this.onUpArrow(keyboardEvent);
}
// escape
if (keyboardEvent.keyCode === 27) {
_this.onEscape(keyboardEvent);
}
// tab
if (keyboardEvent.keyCode === 9) {
_this.onTab(keyboardEvent);
}
return undefined;
};
var descendant = "mention-option-" + _this.key + "-" + _this.state.focusedOptionIndex;
_this.props.ariaProps.ariaActiveDescendantID = descendant;
_this.props.ariaProps.ariaOwneeID = "mentions-list-" + _this.key;
_this.props.ariaProps.ariaHasPopup = 'true';
_this.props.ariaProps.ariaExpanded = true;
_this.props.onOpenChange(true);
};
_this.closeDropdown = function () {
// make sure none of these callbacks are triggered
_this.props.callbacks.handleReturn = undefined;
_this.props.callbacks.keyBindingFn = undefined;
_this.props.ariaProps.ariaHasPopup = 'false';
_this.props.ariaProps.ariaExpanded = false;
_this.props.ariaProps.ariaActiveDescendantID = undefined;
_this.props.ariaProps.ariaOwneeID = undefined;
_this.props.onOpenChange(false);
};
_this.props.callbacks.onChange = _this.onEditorStateChange;
return _this;
}
var _proto = MentionSuggestions.prototype;
_proto.componentDidUpdate = function componentDidUpdate() {
if (this.popover) {
// In case the list shrinks there should be still an option focused.
// Note: this might run multiple times and deduct 1 until the condition is
// not fullfilled anymore.
var size = this.props.suggestions.length;
if (size > 0 && this.state.focusedOptionIndex >= size) {
this.setState({
focusedOptionIndex: size - 1
});
}
// Note: this is a simple protection for the error when componentDidUpdate
// try to get new getPortalClientRect, but the key already was deleted by
// previous action. (right now, it only can happened when set the mention
// trigger to be multi-characters which not supported anyway!)
if (!this.props.store.getAllSearches().has(this.activeOffsetKey)) {
return;
}
var decoratorRect = this.props.store.getPortalClientRect(this.activeOffsetKey);
var positionSuggestions$1 = this.props.positionSuggestions || positionSuggestions;
var newStyles = positionSuggestions$1({
decoratorRect: decoratorRect,
props: this.props,
popover: this.popover
});
for (var _i = 0, _Object$entries = Object.entries(newStyles); _i < _Object$entries.length; _i++) {
var _Object$entries$_i = _Object$entries[_i],
key = _Object$entries$_i[0],
value = _Object$entries$_i[1];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.popover.style[key] = value;
}
}
};
_proto.componentWillUnmount = function componentWillUnmount() {
this.props.callbacks.onChange = undefined;
};
_proto.render = function render() {
var _this2 = this;
if (!this.props.open) {
return null;
}
var _this$props = this.props,
entryComponent = _this$props.entryComponent,
popoverComponent = _this$props.popoverComponent,
popperOptions = _this$props.popperOptions,
_this$props$popoverCo = _this$props.popoverContainer,
PopoverContainer = _this$props$popoverCo === void 0 ? Popover : _this$props$popoverCo;
_this$props.onOpenChange;
_this$props.onAddMention;
_this$props.onSearchChange;
_this$props.suggestions;
_this$props.ariaProps;
_this$props.callbacks;
var _this$props$theme = _this$props.theme,
theme = _this$props$theme === void 0 ? {} : _this$props$theme;
_this$props.store;
_this$props.entityMutability;
var positionSuggestions = _this$props.positionSuggestions;
_this$props.mentionTriggers;
_this$props.mentionPrefix;
var elementProps = _objectWithoutPropertiesLoose(_this$props, _excluded);
if (popoverComponent || positionSuggestions) {
warning('The properties `popoverComponent` and `positionSuggestions` are deprecated and will be removed in @draft-js-plugins/mentions 6.0 . Use `popperOptions` instead');
return /*#__PURE__*/React__default["default"].cloneElement(popoverComponent || /*#__PURE__*/React__default["default"].createElement("div", null), _extends({}, elementProps, {
className: theme.mentionSuggestions,
role: 'listbox',
id: "mentions-list-" + this.key,
ref: function ref(element) {
_this2.popover = element;
}
}), this.props.suggestions.map(function (mention, index) {
return /*#__PURE__*/React__default["default"].createElement(Entry$1, {
key: mention.id != null ? mention.id : mention.name,
onMentionSelect: _this2.onMentionSelect,
onMentionFocus: _this2.onMentionFocus,
isFocused: _this2.state.focusedOptionIndex === index,
mention: mention,
index: index,
id: "mention-option-" + _this2.key + "-" + index,
theme: theme,
searchValue: _this2.lastSearchValue,
entryComponent: entryComponent || DefaultEntryComponent
});
}));
}
if (!this.props.renderEmptyPopup && this.props.suggestions.length === 0) {
return null;
}
return /*#__PURE__*/React__default["default"].createElement(PopoverContainer, {
store: this.props.store,
popperOptions: popperOptions,
theme: theme
}, this.props.suggestions.map(function (mention, index) {
return /*#__PURE__*/React__default["default"].createElement(Entry$1, {
key: mention.id != null ? mention.id : mention.name,
onMentionSelect: _this2.onMentionSelect,
onMentionFocus: _this2.onMentionFocus,
isFocused: _this2.state.focusedOptionIndex === index,
mention: mention,
index: index,
id: "mention-option-" + _this2.key + "-" + index,
theme: theme,
searchValue: _this2.lastSearchValue,
entryComponent: entryComponent || DefaultEntryComponent
});
}));
};
return MentionSuggestions;
}(React.Component);
MentionSuggestions.propTypes = {
open: PropTypes__default["default"].bool.isRequired,
onOpenChange: PropTypes__default["default"].func.isRequired,
entityMutability: PropTypes__default["default"].oneOf(['SEGMENTED', 'IMMUTABLE', 'MUTABLE']),
entryComponent: PropTypes__default["default"].func,
onAddMention: PropTypes__default["default"].func,
suggestions: PropTypes__default["default"].array.isRequired
};
var MentionSuggestions$1 = MentionSuggestions;
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
function MentionSuggestionsPortal(props) {
var searchPortal = React.useRef();
// Note: this is a workaround for an obscure issue: https://github.com/draft-js-plugins/draft-js-plugins/pull/667/files
// Ideally we can remove this in the future.
var searchPortalRef = function searchPortalRef(element) {
searchPortal.current = element;
props.store.setReferenceElement(element);
};
var updatePortalClientRect = function updatePortalClientRect(currentProps) {
currentProps.store.updatePortalClientRect(currentProps.offsetKey, function () {
return searchPortal.current.getBoundingClientRect();
});
};
// When inputting Japanese characters (or any complex alphabet which requires
// hitting enter to commit the characters), that action was causing a race
// condition when we used UNSAFE_componentWillMount. By using componentDidMount
// instead of UNSAFE_componentWillMount, the component will unmount unregister and
// then properly mount and register after. Prior to this change,
// UNSAFE_componentWillMount would not fire after componentWillUnmount even though it
// was still in the DOM, so it wasn't re-registering the offsetkey.
useIsomorphicLayoutEffect(function () {
props.store.register(props.offsetKey);
props.store.setIsOpened(true);
updatePortalClientRect(props);
// trigger a re-render so the MentionSuggestions becomes active
props.store.setEditorState(props.store.getEditorState());
return function () {
props.store.unregister(props.offsetKey);
props.store.setIsOpened(false);
props.store.setReferenceElement(null);
};
}, []);
React.useEffect(function () {
// Use hook to set reference element. CFR: https://github.com/draft-js-plugins/draft-js-plugins/issues/2966
if (searchPortal.current) {
props.store.setReferenceElement(searchPortal.current);
}
}, [searchPortal.current]);
React.useEffect(function () {
updatePortalClientRect(props);
});
return /*#__PURE__*/React__default["default"].createElement("span", {
ref: searchPortalRef
}, props.children);
}
var defaultRegExp = '[' + '\\w-' +
// Latin-1 Supplement (letters only) - https://en.wikipedia.org/wiki/List_of_Unicode_characters#Latin-1_Supplement
"\xC0-\xD6" + "\xD8-\xF6" + "\xF8-\xFF" +
// Latin Extended-A (without deprecated character) - https://en.wikipedia.org/wiki/List_of_Unicode_characters#Latin_Extended-A
"\u0100-\u0148" + "\u014A-\u017F" +
// Cyrillic symbols: \u0410-\u044F - https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
"\u0410-\u044F" +
// Symbols that are sometimes used in Japanese names: \u3005-\u3006
"\u3005-\u3006" +
// hiragana (japanese): \u3040-\u309F - https://gist.github.com/ryanmcgrath/982242#file-japaneseregex-js
"\u3040-\u309F" +
// katakana (japanese): \u30A0-\u30FF - https://gist.github.com/ryanmcgrath/982242#file-japaneseregex-js
"\u30A0-\u30FF" +
// For an advanced explaination about Hangul see https://github.com/draft-js-plugins/draft-js-plugins/pull/480#issuecomment-254055437
// Hangul Jamo (korean): \u3130-\u318F - https://en.wikipedia.org/wiki/Korean_language_and_computers#Hangul_in_Unicode
// Hangul Syllables (korean): \uAC00-\uD7A3 - https://en.wikipedia.org/wiki/Korean_language_and_computers#Hangul_in_Unicode
"\u3130-\u318F" + "\uAC00-\uD7A3" +
// common chinese symbols: \u4e00-\u9eff - http://stackoverflow.com/a/1366113/837709
// extended to \u9fa5 https://github.com/draft-js-plugins/draft-js-plugins/issues/1888
"\u4E00-\u9FA5" +
// Arabic https://en.wikipedia.org/wiki/Arabic_(Unicode_block)
"\u0600-\u06FF" +
// Vietnamese http://vietunicode.sourceforge.net/charset/
"\xC0-\u1EF9" + ']';
var defaultTheme = {
mention: "m6zwb4v",
// CSS class for suggestions component
mentionSuggestions: "mnw6qvm",
mentionSuggestionsPopup: "m1ymsnxd",
mentionSuggestionsPopupVisible: "m126ak5t",
// CSS classes for an entry in the suggestions component
mentionSuggestionsEntry: "mtiwdxc",
mentionSuggestionsEntryFocused: "myz2dw1",
mentionSuggestionsEntryText: "mpqdcgq",
mentionSuggestionsEntryAvatar: "m1mfvffo"
};
var findMentionEntities = function findMentionEntities(triggers) {
return function (contentBlock, callback, contentState) {
contentBlock.findEntityRanges(function (character) {
var entityKey = character.getEntity();
return entityKey !== null && triggers.some(function (trigger) {
return contentState.getEntity(entityKey).getType() === getTypeByTrigger(trigger);
});
}, callback);
};
};
var mentionStrategy = findMentionEntities;
var whitespaceRegEx = /\s/;
function checkForWhiteSpaceBeforeTrigger(text, index) {
if (index === 0) {
return true;
}
return whitespaceRegEx.test(text[index - 1]);
}
function findInContentBlock(regex, text, nonEntityStart, callback) {
var matchArr;
var start;
var prevLastIndex = regex.lastIndex;
// Go through all matches in the text and return the indices to the callback
// Break the loop if lastIndex is not changed
while ((matchArr = regex.exec(text)) !== null) {
if (regex.lastIndex === prevLastIndex) {
break;
}
prevLastIndex = regex.lastIndex;
start = nonEntityStart + matchArr.index;
var _end = start + matchArr[0].length;
if (whitespaceRegEx.test(text[start])) {
//trim the result so that we have no whitespaces
start += 1;
}
callback(start, _end);
}
}
function findInContentBlockWithWhitespace(regex, text, nonEntityStart, callback) {
var matchArr;
var start;
var prevLastIndex = regex.lastIndex;
// Go through all matches in the text and return the indices to the callback
// Break the loop if lastIndex is not changed
while ((matchArr = regex.exec(text)) !== null) {
if (regex.lastIndex === prevLastIndex) {
break;
}
prevLastIndex = regex.lastIndex;
start = nonEntityStart + matchArr.index;
var _end2 = start + matchArr[0].length;
//check if whitespace support is active that the char before the trigger is a white space #1844
if (checkForWhiteSpaceBeforeTrigger(text, matchArr.index)) {
callback(start, _end2);
}
}
}
var findWithRegex = function findWithRegex(regex, contentBlock, supportWhiteSpace, callback) {
var contentBlockText = contentBlock.getText();
// exclude entities, when matching
contentBlock.findEntityRanges(function (character) {
return !character.getEntity();
}, function (nonEntityStart, nonEntityEnd) {
var text = contentBlockText.slice(nonEntityStart, nonEntityEnd);
if (supportWhiteSpace) {
findInContentBlockWithWhitespace(regex, text, nonEntityStart, callback);
} else {
findInContentBlock(regex, text, nonEntityStart, callback);
}
});
};
var mentionSuggestionsStrategy = (function (triggers, supportWhiteSpace, regExp) {
var triggerPattern = "(" + triggers.map(function (trigger) {
return escapeRegExp__default["default"](trigger);
}).join('|') + ")";
var MENTION_REGEX = supportWhiteSpace ? new RegExp(triggerPattern + "(" + regExp + "|\\s)*", 'g') : new RegExp("(\\s|^)" + triggerPattern + regExp + "*", 'g');
return function (contentBlock, callback) {
findWithRegex(MENTION_REGEX, contentBlock, supportWhiteSpace, callback);
};
});
// Get the first 5 suggestions that match
var defaultSuggestionsFilter$1 = function defaultSuggestionsFilter(searchValue, suggestions, trigger) {
var value = searchValue.toLowerCase();
var triggerSuggestions = trigger && !Array.isArray(suggestions) ? suggestions[trigger] : suggestions;
var filteredSuggestions = triggerSuggestions.filter(function (suggestion) {
return !value || suggestion.name.toLowerCase().indexOf(value) > -1;
});
var length = filteredSuggestions.length < 5 ? filteredSuggestions.length : 5;
return filteredSuggestions.slice(0, length);
};
var suggestionsFilter = defaultSuggestionsFilter$1;
var index = (function (config) {
if (config === void 0) {
config = {};
}
var callbacks = {
keyBindingFn: undefined,
handleKeyCommand: undefined,
handleReturn: undefined,
onChange: undefined
};
var ariaProps = {
ariaHasPopup: 'false',
ariaExpanded: false,
ariaOwneeID: undefined,
ariaActiveDescendantID: undefined
};
var searches = immutable.Map();
var escapedSearch;
var clientRectFunctions = immutable.Map();
var isOpened = false;
var referenceElement;
var store = {
getEditorState: undefined,
setEditorState: undefined,
getPortalClientRect: function getPortalClientRect(offsetKey) {
return clientRectFunctions.get(offsetKey)();
},
getAllSearches: function getAllSearches() {
return searches;
},
isEscaped: function isEscaped(offsetKey) {
return escapedSearch === offsetKey;
},
escapeSearch: function escapeSearch(offsetKey) {
escapedSearch = offsetKey;
},
resetEscapedSearch: function resetEscapedSearch() {
escapedSearch = undefined;
},
register: function register(offsetKey) {
searches = searches.set(offsetKey, offsetKey);
},
updatePortalClientRect: function updatePortalClientRect(offsetKey, func) {
clientRectFunctions = clientRectFunctions.set(offsetKey, func);
},
unregister: function unregister(offsetKey) {
searches = searches["delete"](offsetKey);
clientRectFunctions = clientRectFunctions["delete"](offsetKey);
},
getIsOpened: function getIsOpened() {
return isOpened;
},
setIsOpened: function setIsOpened(nextIsOpened) {
isOpened = nextIsOpened;
},
getReferenceElement: function getReferenceElement() {
return referenceElement;
},
setReferenceElement: function setReferenceElement(element) {
referenceElement = element;
}
};
// Styles are overwritten instead of merged as merging causes a lot of confusion.
//
// Why? Because when merging a developer needs to know all of the underlying
// styles which needs a deep dive into the code. Merging also makes it prone to
// errors when upgrading as basically every styling change would become a major
// breaking change. 1px of an increased padding can break a whole layout.
var _config = config,
_config$mentionPrefix = _config.mentionPrefix,
mentionPrefix = _config$mentionPrefix === void 0 ? '' : _config$mentionPrefix,
_config$theme = _config.theme,
theme = _config$theme === void 0 ? defaultTheme : _config$theme,
positionSuggestions = _config.positionSuggestions,
mentionComponent = _config.mentionComponent,
_config$mentionSugges = _config.mentionSuggestionsComponent,
MentionSuggestionsComponent = _config$mentionSugges === void 0 ? MentionSuggestions$1 : _config$mentionSugges,
_config$entityMutabil = _config.entityMutability,
entityMutability = _config$entityMutabil === void 0 ? 'SEGMENTED' : _config$entityMutabil,
_config$mentionTrigge = _config.mentionTrigger,
mentionTrigger = _config$mentionTrigge === void 0 ? '@' : _config$mentionTrigge,
_config$mentionRegExp = _config.mentionRegExp,
mentionRegExp = _config$mentionRegExp === void 0 ? defaultRegExp : _config$mentionRegExp,
_config$supportWhites = _config.supportWhitespace,
supportWhitespace = _config$supportWhites === void 0 ? false : _config$supportWhites,
popperOptions = _config.popperOptions;
var mentionTriggers = typeof mentionTrigger === 'string' ? [mentionTrigger] : mentionTrigger;
var mentionSearchProps = {
ariaProps: ariaProps,
callbacks: callbacks,
theme: theme,
store: store,
entityMutability: entityMutability,
positionSuggestions: positionSuggestions,
mentionTriggers: mentionTriggers,
mentionPrefix: mentionPrefix,
popperOptions: popperOptions
};
var DecoratedMentionSuggestionsComponent = function DecoratedMentionSuggestionsComponent(props) {
return /*#__PURE__*/React__default["default"].createElement(MentionSuggestionsComponent, _extends({}, props, mentionSearchProps));
};
var DecoratedMention = function DecoratedMention(props) {
return /*#__PURE__*/React__default["default"].createElement(Mention, _extends({}, props, {
theme: theme,
mentionComponent: mentionComponent
}));
};
var DecoratedMentionSuggestionsPortal = function DecoratedMentionSuggestionsPortal(props) {
return /*#__PURE__*/React__default["default"].createElement(MentionSuggestionsPortal, _extends({}, props, {
store: store
}));
};
return {
MentionSuggestions: DecoratedMentionSuggestionsComponent,
decorators: [{
strategy: mentionStrategy(mentionTriggers),
component: DecoratedMention
}, {
strategy: mentionSuggestionsStrategy(mentionTriggers, supportWhitespace, mentionRegExp),
component: DecoratedMentionSuggestionsPortal
}],
getAccessibilityProps: function getAccessibilityProps() {
return {
role: 'combobox',
ariaAutoComplete: 'list',
ariaHasPopup: ariaProps.ariaHasPopup,
ariaExpanded: ariaProps.ariaExpanded,
ariaActiveDescendantID: ariaProps.ariaActiveDescendantID,
ariaOwneeID: ariaProps.ariaOwneeID
};
},
initialize: function initialize(_ref) {
var getEditorState = _ref.getEditorState,
setEditorState = _ref.setEditorState;
store.getEditorState = getEditorState;
store.setEditorState = setEditorState;
},
keyBindingFn: function keyBindingFn(keyboardEvent) {
return callbacks.keyBindingFn && callbacks.keyBindingFn(keyboardEvent);
},
handleReturn: function handleReturn(keyboardEvent) {
return callbacks.handleReturn && callbacks.handleReturn(keyboardEvent);
},
onChange: function onChange(editorState) {
if (callbacks.onChange) {
return callbacks.onChange(editorState);
}
return editorState;
}
};
});
var defaultSuggestionsFilter = suggestionsFilter;
exports.MentionSuggestions = MentionSuggestions$1;
exports.Popover = Popover;
exports.addMention = addMention;
exports["default"] = index;
exports.defaultSuggestionsFilter = defaultSuggestionsFilter;
exports.defaultTheme = defaultTheme;