3

I am developing a rich text editor with Draft.js (which is great!). The following code, which allows the user to edit a link, works logically fine, but I am not happy with the user experience.

If the user selects a portion of a link and run this code, this code divides that link into multiple links, which is NOT what the user wants.

For example, if a phase "buy this book" is linked with URL-A, and the user selects "buy this ", and changes it to URL-B, that portion will be linked with URL-B, but "book" is still linked with URL-A.

Ideally, when the user selects a portion of a linked text, I'd like to automatically expand the selection to the entire link, then execute this code.

I, however, am not able to figure out how to do it (expand the selection to the entire link).

editLink = () => {
    const { editorState } = this.state;
    const selection = editorState.getSelection();
    if (selection.isCollapsed()) {
      return;
    }

    let url = ''; // default
    const content = editorState.getCurrentContent();
    const startKey = selection.getStartKey();
    const startOffset = selection.getStartOffset();
    const blockWithLinkAtBeginning = content.getBlockForKey(startKey);
    const linkKey = blockWithLinkAtBeginning.getEntityAt(startOffset);
    if (linkKey) {
      const linkInstance = content.getEntity(linkKey);
      url = linkInstance.getData().url;
    }

    let link = window.prompt("Paste the link", url);
    if (!link) {
      console.log("removing link");
      const newEditorState = RichUtils.toggleLink(editorState, selection, null);
      this.setState({ editorState: newEditorState });
      return;
    }
    console.log("adding a link", link);
    const contentWithEntity = content.createEntity('LINK', 'MUTABLE', { url: link });
    const entityKey = contentWithEntity.getLastCreatedEntityKey();
    const newEditorState = EditorState.set(editorState, { currentContent: contentWithEntity });
    const yetNewEditorState = RichUtils.toggleLink(newEditorState, newEditorState.getSelection(), entityKey);

    this.setState({ editorState: yetNewEditorState} );
  }

I'd really appreciate any help or suggestions.

Satoshi Nakajima
  • 1,863
  • 18
  • 29

1 Answers1

4

There are two ways to do this. The first is probably what you're attempting-- applying a new link on top of the current link, thus overriding it. This isn't the best way to do it, but it can be done.

The second is simpler. In a ContentState object, there is a method replaceEntityData(). So you could implement it like this:

editLink = () => {
    const { editorState } = this.state;
    const selection = editorState.getSelection();
    if (selection.isCollapsed()) {
      return;
    }

    let url = ''; // default
    const content = editorState.getCurrentContent();
    const startKey = selection.getStartKey();
    const startOffset = selection.getStartOffset();
    const block = content.getBlockForKey(startKey);
    const linkKey = block.getEntityAt(startOffset);

    let link = window.prompt("Paste the link", url);
    if (!link) { //REMOVING LINK
        var contentWithRemovedLink = content;
        block.findEntityRanges(charData => { //You need to use block.findEntityRanges() API to get the whole range of link

            const entityKey = charData.getEntity();
            if (!entityKey) return false;
            return entityKey === linkKey //Need to return TRUE only for your specific link. 
        }, (start, end) => {
                const entitySelection = new SelectionState({
                    anchorKey: block.getKey(),  //You already have the block key
                    focusKey: block.getKey(),
                    anchorOffset: start,   //Just use the start/end provided by the API
                    focusOffset: end })
                contentWithRemovedLink = Modifier.applyEntity(content, entitySelection, null)

            return;
        })

        const newEditorState = EditorState.set(
            editorState, { currentContent: contentWithRemovedLink });
        return;
    }
    console.log("adding a link", link);

    //CHANGING LINK
    const contentWithUpdatedLink = content.replaceEntityData(linkKey, { url: link });
    const newEditorState = EditorState.set(editorState, { currentContent: contentWithUpdatedLink });
    //Now do as you please.
  }

TO REMOVE LINK:

On the ContentBlock api, there is a method called findEntityRanges(). That function takes two parameters:

  1. (char: CharacterMetadata) => boolean: a filter function for a characterMetadata object (each continuous ENTITY + INLINE_STYLE combo has a unique CharacterMetatdata object. You can get to the entity via characterMetadata.getEntity() from there.). If this function executes as TRUE, (2) is executed.
  2. (start: number, end: number) => void. This gives you access to the start and end offsets for each specific character range that executed TRUE. Now you can do whatever you like with the start and end.

After that, you can apply a NULL entity with a new SelectionState that encompasses the whole link. That removes the link entity.

TO CHANGE LINK:

You already have the linkKey. Just call content.replaceEntityData(linkKey, {url: "MY NEW URL"}) to generate a new ContentState with the new URL. API defined here: https://draftjs.org/docs/api-reference-content-state#replaceentitydata

jf1234
  • 215
  • 1
  • 11