4

I am trying to add target="_blank" to all the links in Draft.js content. I am new to this library, so my initial attempt is to simply iterate through all the entities and identify the LINK entities. However the entity map is coming up empty even though the content has a link in it. Here's my code:

getHtml = () => {
    const contentState = this.state.editorState.getCurrentContent();

    // entityMap shows as empty
    const entityMap = contentState.getEntityMap();
    console.log('entityMap', JSON.stringify(entityMap, null, 4));

    // stateToHTML() exports the anchor tag with href, but not target="_blank"
    return stateToHTML(contentState);
};

How do I iterate through all the entities and how do I insert target="_blank" when I find a LINK entity?

P.S. I am using version 0.10.5 of Draft.js.

Naresh
  • 23,937
  • 33
  • 132
  • 204

3 Answers3

7

draft-js-export-html stateToHTML() allows you to pass an options argument to change the shape of your entity object. If you are ok with adding target='_blank' to all of your anchor tags you could do this:

...
let options = {
  entityStyleFn: (entity) => {
    const entityType = entity.get('type').toLowerCase();
    if (entityType === 'link') {
      const data = entity.getData();
      return {
        element: 'a',
        attributes: {
          href: data.url,
          target:'_blank'
        },
        style: {
          // Put styles here...
        },
      };
    } 
  }
};
return stateToHTML(contentState, options);
MadEste
  • 110
  • 6
1

Link entities in Draft.js are implemented by Draft.js decorators.

For example, check the code of link-editor example from the official repository:

const decorator = new CompositeDecorator([
  {
    strategy: findLinkEntities,
    component: Link, // <== !!!
  },
]);

this.state = {
  editorState: EditorState.createEmpty(decorator),
  showURLInput: false,
  urlValue: '',
};

Here we define the decorator for matching link entities and pass Link component to appropriate property.

Here - the code of this component:

const Link = (props) => {
  const {url} = props.contentState.getEntity(props.entityKey).getData();
  return (
    <a href={url} style={styles.link}>
      {props.children}
    </a>
  );
};

So you just need to add target="_blank" for a tag. All link entities will render with this attribute in this case.

Check working demo:

'use strict';

const {
  convertToRaw,
  CompositeDecorator,
  ContentState,
  Editor,
  EditorState,
  RichUtils,
} = Draft;

class LinkEditorExample extends React.Component {
  constructor(props) {
    super(props);

    const decorator = new CompositeDecorator([
      {
        strategy: findLinkEntities,
        component: Link,
      },
    ]);

    this.state = {
      editorState: EditorState.createEmpty(decorator),
      showURLInput: false,
      urlValue: '',
    };

    this.focus = () => this.refs.editor.focus();
    this.onChange = (editorState) => this.setState({editorState});
    this.logState = () => {
      const content = this.state.editorState.getCurrentContent();
      console.log(convertToRaw(content));
    };

    this.promptForLink = this._promptForLink.bind(this);
    this.onURLChange = (e) => this.setState({urlValue: e.target.value});
    this.confirmLink = this._confirmLink.bind(this);
    this.onLinkInputKeyDown = this._onLinkInputKeyDown.bind(this);
    this.removeLink = this._removeLink.bind(this);
  }

  _promptForLink(e) {
    e.preventDefault();
    const {editorState} = this.state;
    const selection = editorState.getSelection();
    if (!selection.isCollapsed()) {
      const contentState = editorState.getCurrentContent();
      const startKey = editorState.getSelection().getStartKey();
      const startOffset = editorState.getSelection().getStartOffset();
      const blockWithLinkAtBeginning = contentState.getBlockForKey(startKey);
      const linkKey = blockWithLinkAtBeginning.getEntityAt(startOffset);

      let url = '';
      if (linkKey) {
        const linkInstance = contentState.getEntity(linkKey);
        url = linkInstance.getData().url;
      }

      this.setState({
        showURLInput: true,
        urlValue: url,
      }, () => {
        setTimeout(() => this.refs.url.focus(), 0);
      });
    }
  }

  _confirmLink(e) {
    e.preventDefault();
    const {editorState, urlValue} = this.state;
    const contentState = editorState.getCurrentContent();
    const contentStateWithEntity = contentState.createEntity(
      'LINK',
      'MUTABLE',
      {url: urlValue}
    );
    const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
    const newEditorState = EditorState.set(editorState, { currentContent: contentStateWithEntity });
    this.setState({
      editorState: RichUtils.toggleLink(
        newEditorState,
        newEditorState.getSelection(),
        entityKey
      ),
      showURLInput: false,
      urlValue: '',
    }, () => {
      setTimeout(() => this.refs.editor.focus(), 0);
    });
  }

  _onLinkInputKeyDown(e) {
    if (e.which === 13) {
      this._confirmLink(e);
    }
  }

  _removeLink(e) {
    e.preventDefault();
    const {editorState} = this.state;
    const selection = editorState.getSelection();
    if (!selection.isCollapsed()) {
      this.setState({
        editorState: RichUtils.toggleLink(editorState, selection, null),
      });
    }
  }

  render() {
    let urlInput;
    if (this.state.showURLInput) {
      urlInput =
        <div style={styles.urlInputContainer}>
          <input
            onChange={this.onURLChange}
            ref="url"
            style={styles.urlInput}
            type="text"
            value={this.state.urlValue}
            onKeyDown={this.onLinkInputKeyDown}
            />
          <button onMouseDown={this.confirmLink}>
            Confirm
          </button>
        </div>;
    }

    return (
      <div style={styles.root}>
        <div style={{marginBottom: 10}}>
          Select some text, then use the buttons to add or remove links
          on the selected text.
        </div>
        <div style={styles.buttons}>
          <button
            onMouseDown={this.promptForLink}
            style={{marginRight: 10}}>
            Add Link
          </button>
          <button onMouseDown={this.removeLink}>
            Remove Link
          </button>
        </div>
        {urlInput}
        <div style={styles.editor} onClick={this.focus}>
          <Editor
            editorState={this.state.editorState}
            onChange={this.onChange}
            placeholder="Enter some text..."
            ref="editor"
            />
        </div>
        <input
          onClick={this.logState}
          style={styles.button}
          type="button"
          value="Log State"
          />
      </div>
    );
  }
}

function findLinkEntities(contentBlock, callback, contentState) {
  contentBlock.findEntityRanges(
    (character) => {
      const entityKey = character.getEntity();
      return (
        entityKey !== null &&
        contentState.getEntity(entityKey).getType() === 'LINK'
      );
    },
    callback
  );
}

const Link = (props) => {
  const {url} = props.contentState.getEntity(props.entityKey).getData();
  return (
    <a href={url} target="_blank" style={styles.link}>
      {props.children}
    </a>
  );
};

const styles = {
  root: {
    fontFamily: '\'Georgia\', serif',
    padding: 20,
    width: 600,
  },
  buttons: {
    marginBottom: 10,
  },
  urlInputContainer: {
    marginBottom: 10,
  },
  urlInput: {
    fontFamily: '\'Georgia\', serif',
    marginRight: 10,
    padding: 3,
  },
  editor: {
    border: '1px solid #ccc',
    cursor: 'text',
    minHeight: 80,
    padding: 10,
  },
  button: {
    marginTop: 10,
    textAlign: 'center',
  },
  link: {
    color: '#3b5998',
    textDecoration: 'underline',
  },
};

ReactDOM.render(
  <LinkEditorExample />,
  document.getElementById('react-root')
);
body {
  font-family: Helvetica, sans-serif;
}

.public-DraftEditor-content {
  border: 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/draft-js/0.7.0/Draft.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/draft-js/0.10.0/Draft.js"></script>
<div id="react-root"></div>
Mikhail Shabrikov
  • 8,453
  • 1
  • 28
  • 35
  • 1
    Thanks for the quick response, Mikhail. Actually, I tried the decorator approach first, with draft-js-anchor-plugin. In that plugin, setting `linkTarget="_blank"` correctly sets `target="_blank"` on the anchor tag. However when I extract the content and convert to HTML using draft-js-export-html, `target="_blank"` is lost. See my issue logged on Github: https://github.com/sstur/draft-js-utils/issues/128. Any thoughts on where the problem might be? – Naresh Feb 20 '18 at 06:15
  • @Naresh can you show your code when you convert an editor state to html string (with `stateToHTML` method from `draft-js-export-html`)? – Mikhail Shabrikov Feb 20 '18 at 08:04
  • I updated the code to show the call to `draft-js-export-html` – Naresh Feb 20 '18 at 13:31
0

if you use draft-js-anchor-plugin, you can only put linkTarget attribute when you show content. code is below.

const linkPlugin = createLinkPlugin({linkTarget: '_blank'});
AhuraMazda
  • 460
  • 4
  • 22