Implement Autocomplete in Draft JS but without a "trigger" like "@"
Asked Answered
M

1

16

I want to implement something like a tag editor. However, it's meant just for those tags so I want the user to see the autocomplete suggestions popup without having to type something like @ or #, just the text itself.

I have something that kinda works, but the popup displays in strange positions on the screen:

  • when I first type something and the popup appears, it appears somewhere near the top-left corner of the screen
  • after the first entity is created, when press SPACE and start typing again, the popup appears a couple of pixels to the right from it's intuitive position (i.e. under the first letter of the word)

Here is an example of a well-known editor of this kind (although not implemented with Draft), so you can get a better understanding of what I want to implement.

Gmail email composer

First things first, here is the function that triggers the suggestions popup:

private onChange(editorState: EditorState) {
  const content = editorState.getCurrentContent();
  const selection = editorState.getSelection();
  const currentBlock = content.getBlockForKey(selection.getAnchorKey());

  if (selection.isCollapsed()) {
    const blockText = currentBlock.getText();
    const wordMeta = getWordAt(blockText, selection.getAnchorOffset());
    const categoryRegex = /([\w]*)/;
    const matches = wordMeta.word.match(categoryRegex);
    const existingEntity = currentBlock.getEntityAt(wordMeta.begin);

    if (!existingEntity && matches) {
      const categorySearch = matches[1];
      const selection = window.getSelection();
      if (this.state.autoComplete.search !== categorySearch && selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);
        const boundingRect = getRangeBoundingClientRect(range);

        this.setState((prevState: StateType) => {
          let state = {
            autoComplete: {
              active: true,
              search: categorySearch,
              searchMeta: {
                begin: wordMeta.begin,
                end: wordMeta.end,
              },
            },
            selectionRect: prevState.selectionRect,
          };

          if (prevState.autoComplete.active === false) {
            state.selectionRect = boundingRect;
          }

          return state;
        });
      }
    }
  }

  this.props.onChange(editorState);
}

Here is the getWordAt function:

function getWordAt(text: string, pos: number): WordMeta
{
  const left = text.slice(0, pos + 1).search(/\S+$/);
  const right = text.slice(pos).search(/\s/);

  if (right < 0) {
    return {
      word: text.slice(left),
      begin: left,
      end: text.length,
    };
  }

  return {
    word: text.slice(left, right + pos),
    begin: left,
    end: right + pos,
  };
}

What would be a better way of handling the position of the popup and maybe even the strategy for autocompletion of this kind as well? Thank you!

Milomilon answered 4/1, 2018 at 10:41 Comment(13)
If it's simply a positioning issue (and not otherwise timing or behavior based), and it's consistent across browsers, might investigate just adding some CSS to coerce the popup to the desirable positionCentuplicate
@GregRozmarynowycz It's not just the positioning. The approach Facebook offers for entity creation is based on @-handles, but my case seems not to be correctly achievable using that approach. I am looking for some kind of approach that is not hacky in any way and will produce correct results!Milomilon
@Milomilon you are done with showing autosuggest without typing @? And now you are facing issue with positioning ? Or you need solution for autosuggest and position both?Mausoleum
Both! The thing you see in my code snippet is very error-prone and is very hard to extend later... that's why I though a Regex solution is not ok so I asked here! But yes! I need help with both positioning and autosuggestMilomilon
As I understand, your problem is that you don't know how many pixels you need to push the suggestions box to the right relative to the input field, so that it appears right below the user's cursor? Have you seen this: https://mcmap.net/q/411854/-get-cursor-or-text-position-in-pixels-for-input-element?Ruy
Well I am already using getRangeBoundingClientRect but that is not the only problem. the problem is that I somehow need to trigger the autocompletion for the text that is not included in any entity so I can get its bounding rectangle and display the autocompletionMilomilon
How on earth did this get 11 upvotes?! Something smells fishy here...Slickenside
@Liam, what's the problem? You can't just say that without an explanation.Milomilon
This question is far too broad/vague. IMO it should be closed. That's why 1 month and two bounties later you still don't have an answer.Slickenside
Too vague? I have provided screenshots, explanations and code snippets along with what I want to achieve. Should I write a thesis on this subject so it's not too vague for you?Milomilon
Quite the opposite, the question should be answerable. The reason your not getting any answers is because it's not. You've asked for too much and no amount of bounties is going to fix that. It's also lacking a Minimal, Complete, and Verifiable example of your problem.Slickenside
@Milomilon Can you post a fiddle or pen to show the problem?Plenipotentiary
@Plenipotentiary I will try to put it up as simple as possibleMilomilon
L
1

Instead Of draft-js-typeahead - TypeaheadEditor is a react component that wraps draft's Editor. You can use React-Autosuggest component that meets the requirements. It has custom rendering that works natively with React elements. It's fast and pretty easily customizable. Have full control over suggestions rendering.

We can make it handle JS objects instead of plain strings.

  1. the onSuggestionSelected props is a callback to get the selected suggestion
  2. suggestionRenderer method takes suggestion and returns React markup

Check out React-Autosuggest.

You can use above component by using a custom block renderer, it is possible to introduce complex rich interactions within the frame of your editor.

You have to break your head to achieve what you want to, its not straight forward. This was my suggestion through which you might achieve it but its not that easy.

Lyle answered 19/2, 2018 at 11:19 Comment(4)
react-autosuggest seems to only support one selected item out of the box which is not hard to get even without draft-js. My problem comes from needing more items that can be selected.Milomilon
Then behaviors such as navigating between the selected elements with left/right keys or deleting them with <kbd>Delete</kbd> or <kbd>Backspace</kbd> or right-click and copy would be extremely hard to achieve. This is why I want to achieve this with DraftJS, the Facebook team has already implemented these behaviors in a pretty successful wayMilomilon
You could, however, implement something like this: - User selects an item from the autosuggest. - On selection, add the selected item to a list that is maintained outside of the autosuggest. - After item is selected, clear the input, so that user could add another item to the list. OR There is a way to achieve it with react-autosuggest using renderInputComponent function - which is used only if you need to customize the rendering of the input. I think you should give it a shot.Lyle
Yes, you are right and you should use DraftJS but with that you can use this component only for that element to achieve exactly what you want.Lyle

© 2022 - 2024 — McMap. All rights reserved.