signavio / react-mentions

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

How can I programatically insert a mention #193

Open saidymadi opened 7 years ago

saidymadi commented 7 years ago

Hello ,

could you provide an example of how I can programatically insert a mention into the react mention text box ?

for example . I have a button

I want my users to be able to click this button to immediately insert the userA into the current react-mentions component

brigand commented 7 years ago

@saidymadi given <MentionsInput markup="{{__type__:__id__}}" I can insert '{{mention:0067EA03CF283F76}}' into the string that becomes the value prop of MentionsInput.

saidymadi commented 7 years ago

yes that is the easy part, however, how would i insert it at the current cursor location ?

in my case, i have bunch of buttons each is for a suggested mention

i want the user to be able to click on any of them to insert that into the current cursor location

jfschwarz commented 7 years ago

First, you can pass your own onSelect handler to MentionsInput and keep track of the cursor position:

<MentionInput onSelect={(event) => console.log(event.target.selectionStart)}>

Then, you have to map the cursor position in the plain text, to a position in the value with markup. For this purpose we internally use the function mapPlainTextIndex. While I cannot recommend using private API, I think this would be your only option at the moment.

If you manage to build a working example, I would be glad to take a look at it to see how we could best support this use case with public APIs.

saidymadi commented 7 years ago

I have managed to achieve this as follow :

you need to update the value but you also need to update the underlying text area in order for it to calculate and adjust the cursor correctly in my application mentionsInputRef is the reference to the <MentionInput and mentionsInputRef.wrappedInstance.refs.input refers to the <Mention textarea or Input control in my text Application I added an insert user handler

this.handleInsertMention = function (userObject) {
          // a little uncomfortable must rework to expose add user in a different way
          if (this.socialMarkup
            && this.socialMarkup.mentionsInputRef
            && this.socialMarkup.mentionsInputRef.wrappedInstance
            && this.socialMarkup.mentionsInputRef.wrappedInstance.refs.input) {
            const inputElement = this.socialMarkup.mentionsInputRef.wrappedInstance.refs.input
            const newVal = `${this.socialMarkup.state.value } @[${userObject.display}](user:${userObject.id})`
            const updateUnderlyingTextInputArea = function () {
              this.socialMarkup.mentionsInputRef.wrappedInstance.handleInsertMention( { target: inputElement })
            }.bind(this)
            this.socialMarkup.setState({ ...this.socialMarkup.state, value: newVal }, updateUnderlyingTextInputArea)
          }
        }.bind(this)
gbsandeep commented 7 years ago

@saidymadi , I have a similar requirement as yours. Could you please clarify couple of things...?

  1. I see that function "handleInsertMention" is called inside updateUnderlyingTextInputArea. Can you please explain this part?
  2. I see that const newVal contains userObject at the end of the current input value and not based on the cursor position. Is that true?

If possible could you share the actual code (may be obfuscating any business logic specific to your project)?

Thanks in advance.

saidymadi commented 7 years ago

Hey I will have some free time towards the end of my working day . In 5-6 hours and I will create a branch with the solution to auto insert based off the main master without any of my other modifications .

gbsandeep commented 7 years ago

Hi, Thanks for responding quickly.

I was able to get add mentions working this way... This code always inserts mention at the beginning of the input and not at the current cursor position. Perhaps, I can take a look at your changes and see if I can get that part working.

Included ref in JSX:

<MentionsInput ... 
ref={ e => this.mentionInput = e}.... >
  <Mention...>
</MentionsInput>

Add button (part of my wrapper): <button onClick={this.addMention}> Add Mention </button>

addMention() {
   this.mentionInput.wrappedInstance.addMention(
      {
        id: "Id1",
        display: "Dynamic Mention"
      }, {
        mentionDescriptor: this.mentionInput.props.children[0],
        querySequenceStart: 0,  
        querySequenceEnd: 0,
        plainTextValue: ""
      }
    )
}

querySequenceStart, querySequenceEnd and plainTextValue - these values need to be correctly computed so that insert occurs at the current cursor position.

jfschwarz commented 6 years ago

I advise against a solution relying on .wrappedInstance as posted above. Be aware that this is private API we might change in the future. This means your app may break even when just upgrading to a new patch release of react-mentions.

The recommended way for programmatically inserting mentions is by updating the value directly. As outlined above the mapPlainTextIndex function might be useful for mapping a cursor position to a position in the value containing the markup. While mapPlainTextIndex currently is not part of the official API yet, this is the much more futureproof way and we will most probably add this function to the exports of this module soon.

kasperpihl commented 6 years ago

@jfschwarz trying to get around doing this, but I'm having an issue how to tell MentionsInput, to update the selected index.

My current plan of action

  1. Track e.target.selectionStart from MentionsInput onSelect
  2. Update value (with markup) to include the new insertion markup. (<!id|display>)
  3. Tell ReactMentions that new selection index is right after the inserted markup.

But how do I do this? After I update my value manually, cursor keeps jumping to the end when typing in the middle fx.

Thanks a lot :)

bdjnk commented 6 years ago

If you use autofocus, or disable mention inserting until the input fires onFocus, you can get the selection using onSelect, and the inputElement using onFocus or onBlur.

Once you have all that you can handle mention insertion as follows:

  handleInsert(insert) {
    const { value, selection, inputElement, markup } = this.state;

    const start = utils.mapPlainTextIndex(value, markup, selection.start);
    const end = utils.mapPlainTextIndex(value, markup, selection.end);

    const pos = selection.start + insert.length;

    this.setState({
        value: value.substr(0, start) + insert + value.substr(end)
    }, () => {
      inputElement.selectionStart = pos;
      inputElement.selectionEnd = pos;
      inputElement.focus();
    });
  }

And here, for your edification, is a live working example.

I don't yet know the correct way to make this process accessible and generic and officially blessed, but it is possible currently with mapPlainTextIndex, as you can see.

kalemi19 commented 5 years ago

If you're wondering how to use utils.mapPlainTextIndex in the new version 3.0.2 this is how I got it to work.

Basically instead of passing markup, you need to pass an array of objects containing the regex, markup and displayTransform function. If you have only one Mention child, this will be an array of size 1.

export const MENTION_MARKUP = "@mention[__display__](__id__)";
export const MENTION_REGEX = /@mention\[(.+?)]\((.+?)\)/;

const start = utils.mapPlainTextIndex(
  value,
  [
    {
      regex: MENTION_REGEX,
      markup: MENTION_MARKUP,
      displayTransform: function(id, display) {
        return display;
      },
    },
  ],
  selection.start
);

const end = utils.mapPlainTextIndex(
  value,
  [
    {
      regex: MENTION_REGEX,
      markup: MENTION_MARKUP,
      displayTransform: function(id, display) {
        return display;
      },
    },
  ],
  selection.end
);

If you use autofocus, or disable mention inserting until the input fires onFocus, you can get the selection using onSelect, and the inputElement using onFocus or onBlur.

Once you have all that you can handle mention insertion as follows:

  handleInsert(insert) {
    const { value, selection, inputElement, markup } = this.state;

    const start = utils.mapPlainTextIndex(value, markup, selection.start);
    const end = utils.mapPlainTextIndex(value, markup, selection.end);

    const pos = selection.start + insert.length;

    this.setState({
        value: value.substr(0, start) + insert + value.substr(end)
    }, () => {
      inputElement.selectionStart = pos;
      inputElement.selectionEnd = pos;
      inputElement.focus();
    });
  }

And here, for your edification, is a live working example.

I don't yet know the correct way to make this process accessible and generic and officially blessed, but it is possible currently with mapPlainTextIndex, as you can see.

filippofilip95 commented 5 years ago

@kalemi19 utils and its functions are no more accessible in 3.1.0. after the installation of a package. How did you import it? Thanks.

Edit: I got it to work by downgrading to3.0.2. and following your workaround. This version has utils accessible. But version 3.1.0. has not. Any ideas on how to access utils at this version?

Edit2: To got it to work with 3.1.0. I Edited exports in node_modules/react-mentions/dist/react-mentions.esm.js. It's just a quick workaround instead of a long term solution. It could be great if utils were exported in future versions.

kalemi19 commented 5 years ago

@filippofilip95 my snippet / workaround is for 3.0.2. I haven't upgraded to 3.1.0 yet.

P.S. Apologies for the delay.

jsheffers commented 4 years ago

Since this is a controlled input, when I insert the mention into the textarea the onChange() handler is never fired. Meaning, if I click to add two mentions, my value string shows the two mentions, but I don't have access to the mentions provided by the onChange handler. Any thoughts on how to keep track of this?

raDiesle commented 4 years ago

Any simple solution exists as of 4.0.1 ?

muyiwaoyeniyi commented 4 years ago

@raDiesle Did you find a solution to this?

@jfschwarz Any help with this please? There really is no easy way to insert at a cursor position since the utils functions are no longer public.

Thanks

minnyww commented 2 years ago

Answer 2021 i can insert key at cursor by click the button and replace key if cursor position at previous key

here code sandbox https://codesandbox.io/s/react-mentions-forked-p7rdt?file=/src/index.js

michaelbrant commented 2 years ago

any simple solution to this? no offense, but that solution^ is like 80+ lines of code

denismosolov commented 2 years ago

If you're wondering how to use utils.mapPlainTextIndex in the new version 3.0.2 this is how I got it to work.

Basically instead of passing markup, you need to pass an array of objects containing the regex, markup and displayTransform function. If you have only one Mention child, this will be an array of size 1.

export const MENTION_MARKUP = "@mention[__display__](__id__)";
export const MENTION_REGEX = /@mention\[(.+?)]\((.+?)\)/;

const start = utils.mapPlainTextIndex(
  value,
  [
    {
      regex: MENTION_REGEX,
      markup: MENTION_MARKUP,
      displayTransform: function(id, display) {
        return display;
      },
    },
  ],
  selection.start
);

const end = utils.mapPlainTextIndex(
  value,
  [
    {
      regex: MENTION_REGEX,
      markup: MENTION_MARKUP,
      displayTransform: function(id, display) {
        return display;
      },
    },
  ],
  selection.end
);

If you use autofocus, or disable mention inserting until the input fires onFocus, you can get the selection using onSelect, and the inputElement using onFocus or onBlur. Once you have all that you can handle mention insertion as follows:

  handleInsert(insert) {
    const { value, selection, inputElement, markup } = this.state;

    const start = utils.mapPlainTextIndex(value, markup, selection.start);
    const end = utils.mapPlainTextIndex(value, markup, selection.end);

    const pos = selection.start + insert.length;

    this.setState({
        value: value.substr(0, start) + insert + value.substr(end)
    }, () => {
      inputElement.selectionStart = pos;
      inputElement.selectionEnd = pos;
      inputElement.focus();
    });
  }

And here, for your edification, is a live working example. I don't yet know the correct way to make this process accessible and generic and officially blessed, but it is possible currently with mapPlainTextIndex, as you can see.

a live working example upgraded to 4.4.7

kresimir-coko commented 1 year ago

I'd like this functionality, too

andymerskin commented 6 months ago

The provided examples above were really helpful. I found it easiest to export the necessary utilities from react-mention's ESM module, use patch-package to capture the change for an NPM or Yarn postinstall process, and use these utils alongside the Mentions config to handle text insertion cleanly.

It would be a very welcome change to see this as a part of the core library.

Proposed solution

Something like this, where the method is attached to the inputRef that can be called imperatively:

const inputRef = useRef(null);
...
<MentionsInput inputRef={inputRef}>
...
</MentionsInput>
<button onClick={() => inputRef.current.addMention(...)}>Add a mention</button>