Draft-js: How to remove an entity?

Created on 9 Mar 2016  路  16Comments  路  Source: facebook/draft-js

I'm not seeing an API to remove an entity. E.g. a "remove link" button that preserves the text but removes the complete entity independent of selection.

screen shot 2016-03-09 at 1 42 45 pm

Is there a way to do this with the current API?

question

Most helpful comment

this is way too complex for such a simple operation...

All 16 comments

Setting the entity to null for the range will remove it. http://facebook.github.io/draft-js/docs/api-reference-modifier.html#applyentity

Just trying to implement exactly that but I don't see the way to do it.

@caabernathy did it worked for you?
Did you used the CompositeDecorator strategy to render the link and the overlay inside a component?
I don't see the way to get the editor state inside that component and then remove the selection/entity.

Help would be appreciated.

For the tooltip I used a normal React component as a sibling of the Editor component. The pseudocode to remove the entity looks like:

var entityKey = getEntityKeyAtCurrentSelection(editorState)
var entitySelectionState = getEntitySelectionState(editorState, entityKey)
this.setState({
    editorState: RichUtils.toggleLink(editorState, entitySelectionState, null),
})

Cool! I guess it should work also for me.
Could you show me how your getEntitySelectionState method looks like?

Thanks!

@bondia You can try something like this (it works on one block only):

import { SelectionState } from 'draft-js';
import getRangesForDraftEntity from 'draft-js/lib/getRangesForDraftEntity';

const getEntitySelectionState = (contentState, selectionState, entityKey) => {
  const selectionKey = selectionState.getAnchorKey();
  const selectionOffset = selectionState.getAnchorOffset();
  const block = contentState.getBlockForKey(selectionKey);
  const blockKey = block.getKey();

  let entitySelection;
  getRangesForDraftEntity(block, entityKey).forEach((range) => {
    if (range.start <= selectionOffset && selectionOffset <= range.end) {
      entitySelection = new SelectionState({
        anchorOffset: range.start,
        anchorKey: blockKey,
        focusOffset: range.end,
        focusKey: blockKey,
        isBackward: false,
        hasFocus: selectionState.getHasFocus(),
      });
    }
  });
  return entitySelection;
};

@ghigt Aren't blockKey and selectionKey refer to the same thing in the code you pasted here? Just wondering.

@kevinguard Indeed, blockKey seems unnecessary.

this is way too complex for such a simple operation...

Just save the SelectionState on Entity data (when you create the Entity), it should work (I will try it and post the result here)

var currentContentState = currentEditorState.getCurrentContent();

var newContentState = Modifier.applyEntity(currentContentState, currentEditorState.getSelection(), null);
var newEditorState = EditorState.push(currentEditorState, newContentState, 'apply-entity');

This is the solution that worked wonders for me. (based on @gnoesiboe)

export default (atomicBlock, editorState) => {
    const contentState = editorState.getCurrentContent();
    const selection = editorState.getSelection();
    const selectionOfAtomicBlock = selection.merge({
        anchorKey: atomicBlock.getKey(),
        anchorOffset: 0,
        focusKey: atomicBlock.getKey(),
        focusOffset: atomicBlock.getLength(),
    });

    // override the entity data and wipe it from the block. This prevents the data from being tacked onto another block
    const contentStateWithoutEntity = Modifier.applyEntity(contentState, selectionOfAtomicBlock, null);
    const editorStateWithoutEntity = EditorState.push(editorState, contentStateWithoutEntity, 'apply-entity');

    // now delete the block. Purge it from existance so that it may never harm your beautiful editor again
    const contentStateWithoutBlock = Modifier.removeRange(contentStateWithoutEntity, selectionOfAtomicBlock, DIRECTION.BACKWARD);
    return EditorState.push(editorStateWithoutEntity, contentStateWithoutBlock, 'remove-range',);
}

The only downside I have is that the actual entity data is never deleted (still lives on the entityMap). That irks the perfectionist in me, but I can now not worry about rampant images popping up in my html output.

mar-02-2018 14-39-20

I have not tested this but I think it would work to remove an entity based on the cursor position of the Editor.

import {Editor,
        EditorState,
        ContentState,
        SelectionState,
        Modifier}
        from 'draft-js';

//Returns a new editor with the entity removed where the cursors was located.
export function removeEntity(currentEditorState) {

  //https://draftjs.org/docs/api-reference-selection-state.html#start-end-vs-anchor-focus
  //Fetching current position of the cursor.
  const currentSelectionState = currentEditorState.getSelection();
  const startKey = currentSelectionState.getStartKey();
  //Getting the content object
  const currentContent = currentEditorState.getCurrentContent();

  //Finding what block within the content we are dealing with based on cursor position
  //https://draftjs.org/docs/api-reference-content-state.html#getblockforkey
  const currentContentBlock = currentContent.getBlockForKey(startKey);

  //Finding what entity is at the cursor if any
  //https://draftjs.org/docs/api-reference-content-block.html#getentityat
  const entityWithCursor = currentContentBlock.getEntityAt(startKey);

  //Creating a new selection object to encompass the beginning and end of the text entity
  const newSelection = SelectionState.createEmpty(blockKey);
  let updatedSelection;

  //https://draftjs.org/docs/api-reference-content-block.html#findentityranges
  //Looking for the begining and end of the entity
  currentContentBlock.findEntityRanges((character) => {
    //https://draftjs.org/docs/api-reference-character-metadata.html#getentity
    return character.getEntity() == entityWithCursor;
  },
  (start,end) => {
    //updating selection with appropriate positions
    updatedSelection = newSelection.merge({
                                                    anchorOffset: start,
                                                    focusOffset: end
                                                });
  })
  //https://draftjs.org/docs/api-reference-modifier.html#applyentity
  //https://draftjs.org/docs/api-reference-editor-change-type.html#apply-entity
  //creating a modifier, with entityKey = null to remove entity altogether
  const removeEntity = Modifier.applyEntity(currentContent, updatedSelection, null);
  //https://draftjs.org/docs/api-reference-editor-state.html#push
  //modifying existing EditorState and defining the change as 'apply-entity' as to allow undo/redo
  const updatedEditor = EditorState.push(currentEditorState,removeEntity,'apply-entity');

  return updatedEditor;
}

Just in case anyone needs working version:

import {EditorState, Modifier} from 'draft-js';

export default function removeEntity(editorState) {
  const contentState = editorState.getCurrentContent();
  const selectionState = editorState.getSelection();
  const startKey = selectionState.getStartKey();
  const contentBlock = contentState.getBlockForKey(startKey);
  const startOffset = selectionState.getStartOffset();
  const entity = contentBlock.getEntityAt(startOffset);

  if (!entity) {
    return editorState;
  }

  let entitySelection = null;

  contentBlock.findEntityRanges(
    (character) => character.getEntity() === entity,
    (start, end) => {
      entitySelection = selectionState.merge({
        anchorOffset: start,
        focusOffset: end
      });
    }
  );

  const newContentState = Modifier.applyEntity(
    contentState,
    entitySelection,
    null
  );

  const newEditorState = EditorState.push(
    editorState,
    newContentState,
    'apply-entity'
  );

  return newEditorState;
}

Here is what worked for me:

I wanted to remove an atomic block that had entities attached to it, but couldn't get it to work with removeRange or applyEntity. So I just modified the blockMap myself manually:

import {EditorState} from 'draft-js'

function removeBlock(editorState, block) {
  const contentState = editorState.getCurrentContent()
  const newBlockMap = contentState.blockMap.delete(atomicBlock.getKey())  // this is the important one that actually deletes a block
  const newContentState = contentState.set('blockMap', newBlockMap)
  const newEditorState = EditorState.push(editorState, newContentState, 'remove-block')
  return newEditorState
}

I wasn't able to figure out how to remove the entity from the entity map, though, since it doesn't appear to be an immutable map

@hellendag what is selectionState there? It's so unclear

Just save the SelectionState on Entity data (when you create the Entity), it should work (I will try it and post the result here)

This helped me out and avoids a lot of the overhead.

//Create an entity with selection state
        const newContentState = currentContent.createEntity('LINKED-ENTITY', 'MUTABLE', { selectedText: selectedText, entityId: entityId, selectionState: selectionState });

//In the remove call
//Already created a single entity with id of "1"
        const entityKey = "1";
        const contentState = editorState.getCurrentContent();

        //Get Entity
        const entity = contentState.getEntity(entityKey)
        const data = entity.getData()
        console.log(Selection State: ${data.selectionState})

        //Unlink Entity
        const newContentState = Modifier.applyEntity(contentState, data.selectionState, null);
        const newEditorState = EditorState.push(editorState, newContentState, 'apply-entity')
        setEditorState(newEditorState)
Was this page helpful?
0 / 5 - 0 ratings