Rich Text Editor: Add emoji suggestion support (#30873)
* Add support for emoji suggestions To both the rich text/plain text modes of the RTE. * Add emoji completion test to WysiwygComposer * Fix code as per test case, do no-op for community case * bump wysiwyg to the version with suggestions supported. * Add more unit tests for processTextReplacement
This commit is contained in:
@@ -60,6 +60,7 @@ export function PlainTextComposer({
|
||||
handleCommand,
|
||||
handleMention,
|
||||
handleAtRoomMention,
|
||||
handleEmoji,
|
||||
} = usePlainTextListeners(initialContent, onChange, onSend, eventRelation, isAutoReplaceEmojiEnabled);
|
||||
const composerFunctions = useComposerFunctions(editorRef, setContent);
|
||||
usePlainTextInitialization(initialContent, editorRef);
|
||||
@@ -84,6 +85,7 @@ export function PlainTextComposer({
|
||||
handleMention={handleMention}
|
||||
handleCommand={handleCommand}
|
||||
handleAtRoomMention={handleAtRoomMention}
|
||||
handleEmoji={handleEmoji}
|
||||
/>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
|
||||
@@ -40,6 +40,12 @@ interface WysiwygAutocompleteProps {
|
||||
*/
|
||||
handleAtRoomMention: FormattingFunctions["mentionAtRoom"];
|
||||
|
||||
/**
|
||||
* This handler will be called with the emoji character on clicking
|
||||
* an emoji in the autocomplete list or pressing enter on a selected item
|
||||
*/
|
||||
handleEmoji: FormattingFunctions["emoji"];
|
||||
|
||||
ref?: Ref<Autocomplete>;
|
||||
}
|
||||
|
||||
@@ -55,6 +61,7 @@ const WysiwygAutocomplete = ({
|
||||
handleMention,
|
||||
handleCommand,
|
||||
handleAtRoomMention,
|
||||
handleEmoji,
|
||||
ref,
|
||||
}: WysiwygAutocompleteProps): JSX.Element | null => {
|
||||
const { room } = useScopedRoomContext("room");
|
||||
@@ -89,7 +96,14 @@ const WysiwygAutocomplete = ({
|
||||
return;
|
||||
}
|
||||
// TODO - handle "community" type
|
||||
case "community": {
|
||||
return; // no-op until we decide how to handle community in the wysiwyg composer
|
||||
}
|
||||
default:
|
||||
{
|
||||
// similar to the cider editor we handle emoji and other plain text replacement in the default case
|
||||
handleEmoji(completion.completion);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({
|
||||
handleMention={wysiwyg.mention}
|
||||
handleAtRoomMention={wysiwyg.mentionAtRoom}
|
||||
handleCommand={wysiwyg.command}
|
||||
handleEmoji={wysiwyg.emoji}
|
||||
/>
|
||||
<FormattingButtons composer={wysiwyg} actionStates={actionStates} disabled={disableFormatting} />
|
||||
<Editor
|
||||
|
||||
@@ -60,6 +60,7 @@ export function usePlainTextListeners(
|
||||
handleMention: (link: string, text: string, attributes: AllowedMentionAttributes) => void;
|
||||
handleAtRoomMention: (attributes: AllowedMentionAttributes) => void;
|
||||
handleCommand: (text: string) => void;
|
||||
handleEmoji: (emoji: string) => void;
|
||||
onSelect: (event: SyntheticEvent<HTMLDivElement>) => void;
|
||||
suggestion: MappedSuggestion | null;
|
||||
} {
|
||||
@@ -95,8 +96,15 @@ export function usePlainTextListeners(
|
||||
// For separation of concerns, the suggestion handling is kept in a separate hook but is
|
||||
// nested here because we do need to be able to update the `content` state in this hook
|
||||
// when a user selects a suggestion from the autocomplete menu
|
||||
const { suggestion, onSelect, handleCommand, handleMention, handleAtRoomMention, handleEmojiReplacement } =
|
||||
useSuggestion(ref, setText, isAutoReplaceEmojiEnabled);
|
||||
const {
|
||||
suggestion,
|
||||
onSelect,
|
||||
handleCommand,
|
||||
handleMention,
|
||||
handleAtRoomMention,
|
||||
handleEmojiSuggestion,
|
||||
handleEmojiReplacement,
|
||||
} = useSuggestion(ref, setText, isAutoReplaceEmojiEnabled);
|
||||
|
||||
const onInput = useCallback(
|
||||
(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
||||
@@ -178,5 +186,6 @@ export function usePlainTextListeners(
|
||||
handleCommand,
|
||||
handleMention,
|
||||
handleAtRoomMention,
|
||||
handleEmoji: handleEmojiSuggestion,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export function useSuggestion(
|
||||
handleMention: (href: string, displayName: string, attributes: AllowedMentionAttributes) => void;
|
||||
handleAtRoomMention: (attributes: AllowedMentionAttributes) => void;
|
||||
handleCommand: (text: string) => void;
|
||||
handleEmojiSuggestion: (text: string) => void;
|
||||
handleEmojiReplacement: () => void;
|
||||
onSelect: (event: SyntheticEvent<HTMLDivElement>) => void;
|
||||
suggestion: MappedSuggestion | null;
|
||||
@@ -86,11 +87,15 @@ export function useSuggestion(
|
||||
|
||||
const handleEmojiReplacement = (): void => processEmojiReplacement(suggestionData, setSuggestionData, setText);
|
||||
|
||||
const handleEmojiSuggestion = (emoji: string): void =>
|
||||
processTextReplacement(emoji, suggestionData, setSuggestionData, setText);
|
||||
|
||||
return {
|
||||
suggestion: suggestionData?.mappedSuggestion ?? null,
|
||||
handleCommand,
|
||||
handleMention,
|
||||
handleAtRoomMention,
|
||||
handleEmojiSuggestion,
|
||||
handleEmojiReplacement,
|
||||
onSelect,
|
||||
};
|
||||
@@ -260,10 +265,31 @@ export function processEmojiReplacement(
|
||||
setText: (text?: string) => void,
|
||||
): void {
|
||||
// if we do not have a suggestion of the correct type, return early
|
||||
if (suggestionData === null || suggestionData.mappedSuggestion.type !== `custom`) {
|
||||
if (suggestionData?.mappedSuggestion?.type !== `custom`) {
|
||||
return;
|
||||
}
|
||||
const { node, mappedSuggestion } = suggestionData;
|
||||
|
||||
processTextReplacement(suggestionData.mappedSuggestion.text, suggestionData, setSuggestionData, setText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the relevant part of the editor text, replacing the suggestionData selection with the replacement text.
|
||||
* @param replacementText - the text that we will insert into the DOM
|
||||
* @param suggestionData - representation of the part of the DOM that will be replaced
|
||||
* @param setSuggestionData - setter function to set the suggestion state
|
||||
* @param setText - setter function to set the content of the composer
|
||||
*/
|
||||
export function processTextReplacement(
|
||||
replacementText: string,
|
||||
suggestionData: SuggestionState,
|
||||
setSuggestionData: React.Dispatch<React.SetStateAction<SuggestionState>>,
|
||||
setText: (text?: string) => void,
|
||||
): void {
|
||||
// if we do not have suggestion data return early
|
||||
if (suggestionData === null) {
|
||||
return;
|
||||
}
|
||||
const { node } = suggestionData;
|
||||
const existingContent = node.textContent;
|
||||
|
||||
if (existingContent == null) {
|
||||
@@ -273,7 +299,7 @@ export function processEmojiReplacement(
|
||||
// replace the emoticon with the suggesed emoji
|
||||
const newContent =
|
||||
existingContent.slice(0, suggestionData.startOffset) +
|
||||
mappedSuggestion.text +
|
||||
replacementText +
|
||||
existingContent.slice(suggestionData.endOffset);
|
||||
|
||||
node.textContent = newContent;
|
||||
@@ -405,6 +431,8 @@ export function getMappedSuggestion(text: string, isAutoReplaceEmojiEnabled?: bo
|
||||
case "#":
|
||||
case "@":
|
||||
return { keyChar: firstChar, text: restOfString, type: "mention" };
|
||||
case ":":
|
||||
return { keyChar: firstChar, text: restOfString, type: "emoji" };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user