Draft-js: Current selection DOM element

Created on 23 Feb 2016  路  22Comments  路  Source: facebook/draft-js

I am trying to get the current selection element so that I can calculate the offset from the top of the document. I would like to position buttons on the left of the cursor like used in Facebook Note editor.

screenshot 2016-02-23 09 18 26

Is there an easy way to get the selected dom element?

question

Most helpful comment

Here's my function for getting the selected block element

getSelectedBlockElement = () => {
  var selection = window.getSelection()
  if (selection.rangeCount == 0) return null
  var node = selection.getRangeAt(0).startContainer
  do {
    if (node.getAttribute && node.getAttribute('data-block') == 'true')
      return node
    node = node.parentNode
  } while (node != null)
  return null
};

All 22 comments

Yes, I've wondered the same. Is there some kind of event on editor that gets fired if the selection changed with infos about position etc.?

Great question.

For the Notes editor, I reach directly into the DOM Selection object, identify the block that contains the cursor, and calculate the position of the element.

This isn't exposed in the current API, though I think it might be useful to have a utility function available so that this doesn't need to be reimplemented.

One solution would be to add ref={blockKey} to DraftEditorBlock or its parent div so we could use findDOMNode to get the element.

Example:
ReactDOM.findDOMNode(editorState.getSelection().getStartKey())

Each top-level block element has data-block={true}, which you can use to find it if looking upward from the node with the current DOM Selection.

Thank you that helps a lot!

What would be most helpful here? A utility to provide the DOM element for the selected ContentBlock? I don't want people to have to go digging into the Selection API docs at MDN. :) So I can put something together if it would be useful.

A utility that provides a reference to the current block with cursor focus would be great.

How about the boundingClientRect of the current selection (to ease in positioning popups, widgets, etc)?

Great library! I was toying around with the Medium style formatting bar shown in the video (centering controls above selection), and, from quickly digging around MDN got something working in Chrome with just window.getSelection().getRangeAt(0).getBoundingClientRect(). But I have no clue if this will break in IE, etc.

It'd be amazing if this library itself could handle any cross-browser and edge cases involved in positioning relative to a selection!

I've had some success with https://github.com/bkniffler/draft-wysiwyg/blob/master/src/draft.js#L67 and https://github.com/bkniffler/draft-wysiwyg/blob/master/src/draft.js#L269

Its a mouseUp handler on the draft-js wrapping div.

mouseUp(e) {
      function getSelected() {
         var t = '';
         if (window.getSelection) {
            t = window.getSelection();
         } else if (document.getSelection) {
            t = document.getSelection();
         } else if (document.selection) {
            t = document.selection.createRange().text;
         }
         return t;
      }

      setTimeout(()=> {
         var selection = this.state.value.getSelection();
         if (selection.isCollapsed()) {
            return this.setState({toolbox: null});
         }
         else {
            var selected = getSelected();
            var rect = selected.getRangeAt(0).getBoundingClientRect();
            this.setState({toolbox: {left: rect.left, top: rect.top, width: rect.width}});
         }
      }, 1)
   }

The resulting position data can easily be used with something like react-portal to show a tooltip like here https://github.com/bkniffler/draft-wysiwyg/blob/master/src/components/tooltip.js

The live example: http://draft-wysiwyg.herokuapp.com/
I've only tested in Safari/Chrome though.

@bkniffler Nice! Thanks for sharing.

Here's my function for getting the selected block element

getSelectedBlockElement = () => {
  var selection = window.getSelection()
  if (selection.rangeCount == 0) return null
  var node = selection.getRangeAt(0).startContainer
  do {
    if (node.getAttribute && node.getAttribute('data-block') == 'true')
      return node
    node = node.parentNode
  } while (node != null)
  return null
};

I managed to get this working. To position the buttons on the left, I first retrieved the selected block element using something akin to the function above. Then I used the offsetTop property as it's parent node is the editor container, so it's safe to use this. This gave me the vertical offset for the buttons.

To then display the popover container which adds all the inline styles, like bold, italic, link, etc. I grabbed the position of the editor and the position of the current selection using the getBoundingClientRect method and computed the difference between the two to figure out where to place the inline buttons relative to the editor.

See it in action here

@AlastairTaft Getting the current block is straightforward using Draft:

const currentContent = editorState.getCurrentContent()
const selection = editorState.getSelection()
const currentBlock = currentContent.getBlockForKey(selection.getStartKey())

I prefer this technique over using the window object and digging through DOM nodes.

For retrieving information about the ContentBlock that contains selection, definitely rely on the model, not on the DOM.

For retrieving information about the actual node in order to obtain DOM position information, @AlastairTaft's solution is pretty close to what we use internally. :)

@gscottolson That's much nicer thanks

EDIT: Just realised that method pulls back the object representation of the block not the DOM element.

Hopefully this will be useful as well: https://github.com/facebook/draft-js/blob/master/src/component/selection/getVisibleSelectionRect.js

Going to go ahead and close this issue since we've got a useful thread here.

I'm trying to display a suggestions container and I'm trying to use the "new" getVisibleSelectionRect, but it returns null when the editor is empty. And the editor is empty when I'm typing the first character, because I'm using the function inside the onChange event.
So I tried to use @AlastairTaft's code to get the block and calculate the position but I'm having some difficulties to properly get the exact position of the cursor/selection, because the block is quite large. I cannot display the container at the beginning/end of the block. @hellendag do you mind sharing a bit how you do this at facebook?

Reading through this thread several months after it was closed, and still am confused. I too would like to display a popover block with autocomplete suggestions positioned close to the caret. Is there a good technique for finding the position of the caret (its absolute top and left offsets or the offsets relative to the Editor element)?

@azangru This is working for me:

// From MobX observables

  @observable isSelectionActive = false;
  @observable selectionCoords = {};

  @action modifySelection(value) {
    this.isSelectionActive = value;
  }

  @action setCoords({ editorBound, cursorBound }) {
    console.log('editorBound, cursorBound', editorBound, cursorBound);
    this.selectionCoords = {
      top: cursorBound.top - editorBound.top + 20,
      left: cursorBound.left - editorBound.left,
    };
  }

// The events of the class

  _handleBeforeInput(char) {

    switch(char) {

      case '@': {

        // The rectangle that bounds the editor
        const
          editorNode = document.getElementById(this.props.id),
          editorBound = editorNode.getBoundingClientRect();

        // The rectangle that bounds the cursor
        const
          s = window.getSelection(),
          oRange = s.getRangeAt(0),
          cursorBound = oRange.getBoundingClientRect();

        this.setCoords({ editorBound, cursorBound });
        this.modifySelection(true);
        return true;
      }

      case '#': {
        console.log('handled #!');
        break;
      }

    }

    return false;

  }

What is still needed is to control when the position of the caret is close to the edge to prevent the autosuggest div to overflow the parent div. In that case the property should be top & right instead of top & left.

Hope this helps!

@jmaguirrei Wow, thank you!

I eventually adopted the approach taken by the Draft-js-plugins team, where they use a draft-js decorator to wrap the word for which autosuggestion is offered in a span, and save the coordinates of that span. Once those coordinates are available, it is trivial to position the autocompletion box near the word that the cursor is in.

Thank you for offering another solution to this problem!

@azangru You are welcome, glad to help! I havent finished my editor component yet, so maybe I will be trying another solutions, like yours, in the future. Keep in touch. Regards

Using window.getSelection().getRangeAt(0).getBoundingClientRect() or document.getSelection().getRangeAt(0).getBoundingClientRect() to get the "cursor's" position does not work with Safari 10.0.3. It works with Chrome, Firefox and Edge, though.

Any idea how to get the position in another way?

Was this page helpful?
0 / 5 - 0 ratings