signavio / react-mentions

@mention people in a textarea
https://react-mentions.vercel.app
Other
2.4k stars 562 forks source link

Emoji is inserted at wrong position, instead of current cursor position #424

Open karanparmar1 opened 3 years ago

karanparmar1 commented 3 years ago

Steps to reproduce:

  1. import mr-emoji , but it doesn't matter can be any emoji pkg
  2. pick any Emoji from EmojiPicker
  3. Try to insert it Emoji at middle of the text that has mentions
  4. I wrote a code to insert the emoji at current cursor position with :emoji_code:
  5. Emoji ends up being inserted at position of cursor for real msg[that has mention id||displayValue] too

Expected behaviour: Hi @karan parmar @karan parmar 👍 @karan parmar Observed behaviour: Hi @<!12345678912345||karan p 👍 armar> @karan parmar @karan parmar

Workaround: haven't found any

This article has demo of inserting emoji but inserts at end of the message https://medium.com/@aly.essam.seoudi/simple-react-guide-to-implement-chat-comment-input-component-react-mentions-emoji-mart-a48a4d8c4fad

plxmargaux commented 3 years ago

I've run into a similar issue and had to find a workaround, basically I just loop through every mention (if there are any) and count the number of characters that differ from the original textarea value. In the end, you get a cursor position different from the selectionStart value of the input. Works like a charm but difficult to apprehend 👍

karanparmar1 commented 3 years ago

Works like a charm but difficult to apprehend 👍

Thank you , I used the same approach , though it was tedious to find the mention in final text had to do few comparisons, but at the end returned expected result as we required.

abishekjayanth commented 3 years ago

I've run into a similar issue and had to find a workaround, basically I just loop through every mention (if there are any) and count the number of characters that differ from the original textarea value. In the end, you get a cursor position different from the selectionStart value of the input. Works like a charm but difficult to apprehend 👍

Can you please share the code sample?, I'm also facing the same issue, while appending emoji from the custom picker component.

plxmargaux commented 3 years ago

I've run into a similar issue and had to find a workaround, basically I just loop through every mention (if there are any) and count the number of characters that differ from the original textarea value. In the end, you get a cursor position different from the selectionStart value of the input. Works like a charm but difficult to apprehend 👍

Can you please share the code sample?, I'm also facing the same issue, while appending emoji from the custom picker component.

Sure, I created a function in our react project, I basically call it with an onChange event on my input, and after initialisation.

I believe there are some useless lines in the function but maybe you can find what you're looking for 🤔

/**
     * Check cursorPosition with Facebook mentions
     * @param mentions
     * @param e
     */
    const checkCursorPosition = (mentions = null, e = null) => {
        let finalMentions = mentions || mentionsTable;

        if (finalMentions.length > 0) {
            let tempCursorPosition = inputRef.current.selectionStart;
            let charactersDifference = 0;
            let i = 0;
            let isFinished = false;

            while (i < finalMentions.length && !isFinished) {
                if (finalMentions[i] === undefined) {
                    isFinished = true;
                }

                let plainTextIndex = parseInt(finalMentions[i].plainTextIndex);
                let mentionDisplay = finalMentions[i].display;
                let mentionId = `@[${finalMentions[i].id}]`;
                let mentionEnd = plainTextIndex + mentionDisplay.length;

                if (tempCursorPosition < plainTextIndex) {
                    isFinished = true;
                } else if (plainTextIndex < tempCursorPosition && tempCursorPosition < mentionEnd) {
                    if (e && e.keyCode === 37) {
                        setCursorPosition(charactersDifference + plainTextIndex);
                        inputRef.current.selectionEnd = plainTextIndex;
                    } else {
                        setCursorPosition(
                            charactersDifference + plainTextIndex + mentionDisplay.length + mentionId.length + 2
                        );
                        inputRef.current.selectionStart = plainTextIndex + mentionDisplay.length;
                    }

                    return;
                } else if (tempCursorPosition >= mentionEnd) {
                    charactersDifference += mentionId.length + 2;
                }

                i++;
            }

            setCursorPosition(inputRef.current.selectionStart + charactersDifference);
        } else {
            setCursorPosition(inputRef.current.selectionStart);
        }
    };
FrankDupree commented 2 years ago

@plxmargaux thanks for your snippet! i'm having some weird side effects. i don't know why the authors ain't attending to issues marked with getting cursor positions. setCursorPosition(charactersDifference + plainTextIndex); im guessing the method above uses setState or useState. setting states while in a loop could have negative side effects.

when a user types, i store the last known cursor postion

const commentHandleChange = (e, newValue, newPlainTextValue, mentionz) => {
    let finalMentions = mentionz;
    let cuPos;

        if (finalMentions.length > 0) {
                    ........
                if (tempCursorPosition < plainTextIndex) {
                    isFinished = true;
                } else if (plainTextIndex < tempCursorPosition && tempCursorPosition < mentionEnd) {
                    if (e && e.keyCode === 37) {
                        cuPos = charactersDifference + plainTextIndex;
                        inputRef.current.selectionEnd = plainTextIndex;
                    } else {
                                                cuPos= charactersDifference + plainTextIndex + mentionDisplay.length + 
                                                 mentionId.length + 2 inputRef.current.selectionStart = plainTextIndex + 
                                                 mentionDisplay.length;
                    }

                    return;
                } else if (tempCursorPosition >= mentionEnd) {
                    charactersDifference += mentionId.length + 2;
                }

                i++;
            }

            cuPos= inputRef.current.selectionStart + charactersDifference;
        } else {
            cuPos= inputRef.current.selectionStart;
        }

    setData((data) => ({
      ...data,
      comment: newValue,
      cursorPos:cuPos, //my emoji picker needs this to insert the smileys at the right position
      mentions:[...mentionz]
    }));

  };

i have got a button wired to an onClick event, which pops up a list of emojis. on selecting any emoji, emoji-mart gives me a object and i simply get the .native property off it: {"id":"kissing","name":"Kissing Face","short_names":["kissing"],"colons":":kissing:","emoticons":[],"unified":"1f617","skin":null,"native":"😗"}

there is a possibility the user might want to insert more than one smiley at a time. so I increment the current cursor position by two on every insert.

const setEmojiAtPosition=(insert)=>{
      let textBeforeCursorPosition = comment.substring(0, cursorPos);
      let textAfterCursorPosition = comment.substring(cursorPos, comment.length);
      setData((data) => ({
        ...data,
        comment: textBeforeCursorPosition + insert + textAfterCursorPosition,
        cursorPos: cursorPos + 2
      }));
  }

remember when the emoji list pops up, the input looses focus. i attach a focus event listener to the inputRef

useEffect(()=>{
    inputRef.current.addEventListener("focus", (e)=>{
      console.log("i was focused");
      calibratePos(e);
    });

  }, []);

here, calibratePos(e) is simply the same function as commentHandleChange or @plxmargaux snippet that calculates the current cursor position but only sets the current cursor position for the emoji picker to use.

whenever the input box looses focus thanks to the floating emoji picker, should the user click any area in the input field, the onFocus event fires only once and stores the cursor position for the emoji picker to use.

if the user keeps on typing, then the onChange event keeps firing, calls commentHandleChange and calculates the cursor position. i'm still having some side effects though. incase anyone has a better approach, please share.

michaelbrant commented 2 years ago

What is this plainTextIndex variable I keep seeing? My mentions look like this: [ {id:"123", display: "my var"}, {id:"124", display: "my var 2"} ]

The code above references a plainTextIndex key of the mention object but I have no clue what that value is supposed to be

ilyahorski commented 2 weeks ago

Hi, it's my solution, for this issue

const [showEmojiPicker, setShowEmojiPicker] = useState(false);
  const [cursorPosition, setCursorPosition] = useState(0);

  const {
    register,
    reset,
    handleSubmit,
    setFocus,
    setValue,
    watch,
    formState: { errors },
  } = useForm();

  const message = watch(type, "");

  const handleChange = (event) => {
    setCursorPosition(event.target.selectionEnd);
  };

  const addEmoji = (emoji) => {
    const text =
      message.slice(0, cursorPosition) +
      emoji.native +
      message.slice(cursorPosition);
    setValue(type, text);
    setFocus(type);
    setTimeout(() => {
      if (messageRef.current) {
        messageRef.current.selectionStart = cursorPosition + emoji.native.length;
        messageRef.current.selectionEnd = cursorPosition + emoji.native.length;
      }
    }, 0);
  };

and for textarea add onChange and onClick with the same function

<textarea
            title={`Write your ${type} here`}
            ref={messageRef}
            id={type}
            className="w-full min-h-[48px] h-[48px] pl-1 max-h-32 bg-white dark:bg-gray-600/10 outline-none rounded-lg focus:border-[1px] focus:border-primary-50"
            placeholder={placeholder}
            onKeyDown={handleKeyDown}
            onChange={handleChange}
            onClick={handleChange}
            rows={4}
            cols={50}
            maxLength={maxLength}
            {...register(type, {
              required: true,
              maxLength: maxLength,
            })}
          />