<!--
Source of the code: https://github.com/facebook/draft-js/blob/master/examples/draft-0-10-0/entity/
-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Draft • Entity Editor</title>
  </head>
  <body>
    <div id="target"></div>
  </body>
</html>
'use strict';

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

      const rawContent = {
        blocks: [
          {
            text: (
              'This is an "immutable" entity: Superman. Deleting any ' +
              'characters will delete the entire entity. Adding characters ' +
              'will remove the entity from the range.'
            ),
            type: 'unstyled',
            entityRanges: [{offset: 31, length: 8, key: 'first'}],
          },
          {
            text: '',
            type: 'unstyled',
          },
          {
            text: (
              'This is a "mutable" entity: Batman. Characters may be added ' +
              'and removed.'
            ),
            type: 'unstyled',
            entityRanges: [{offset: 28, length: 6, key: 'second'}],
          },
          {
            text: '',
            type: 'unstyled',
          },
          {
            text: (
              'This is a "segmented" entity: Green Lantern. Deleting any ' +
              'characters will delete the current "segment" from the range. ' +
              'Adding characters will remove the entire entity from the range.'
            ),
            type: 'unstyled',
            entityRanges: [{offset: 30, length: 13, key: 'third'}],
          },
        ],

        entityMap: {
          first: {
            type: 'TOKEN',
            mutability: 'IMMUTABLE',
          },
          second: {
            type: 'TOKEN',
            mutability: 'MUTABLE',
          },
          third: {
            type: 'TOKEN',
            mutability: 'SEGMENTED',
          },
        },
      };

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

          const decorator = new CompositeDecorator([
            {
              strategy: getEntityStrategy('IMMUTABLE'),
              component: TokenSpan,
            },
            {
              strategy: getEntityStrategy('MUTABLE'),
              component: TokenSpan,
            },
            {
              strategy: getEntityStrategy('SEGMENTED'),
              component: TokenSpan,
            },
          ]);

          const blocks = convertFromRaw(rawContent);

          this.state = {
            editorState: EditorState.createWithContent(blocks, decorator),
          };

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

        render() {
          return (
            <div style={styles.root}>
              <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 getEntityStrategy(mutability) {
        return function(contentBlock, callback, contentState) {
          contentBlock.findEntityRanges(
            (character) => {
              const entityKey = character.getEntity();
              if (entityKey === null) {
                return false;
              }
              return contentState.getEntity(entityKey).getMutability() === mutability;
            },
            callback
          );
        };
      }

      function getDecoratedStyle(mutability) {
        switch (mutability) {
          case 'IMMUTABLE': return styles.immutable;
          case 'MUTABLE': return styles.mutable;
          case 'SEGMENTED': return styles.segmented;
          default: return null;
        }
      }

      const TokenSpan = (props) => {
        const style = getDecoratedStyle(
          props.contentState.getEntity(props.entityKey).getMutability()
        );
        return (
          <span data-offset-key={props.offsetkey} style={style}>
            {props.children}
          </span>
        );
      };

      const styles = {
        root: {
          fontFamily: '\'Helvetica\', sans-serif',
          padding: 20,
          width: 600,
        },
        editor: {
          border: '1px solid #ccc',
          cursor: 'text',
          minHeight: 80,
          padding: 10,
        },
        button: {
          marginTop: 10,
          textAlign: 'center',
        },
        immutable: {
          backgroundColor: 'rgba(0, 0, 0, 0.2)',
          padding: '2px 0',
        },
        mutable: {
          backgroundColor: 'rgba(204, 204, 255, 1.0)',
          padding: '2px 0',
        },
        segmented: {
          backgroundColor: 'rgba(248, 222, 126, 1.0)',
          padding: '2px 0',
        },
      };

      ReactDOM.render(
        <EntityEditorExample />,
        document.getElementById('target')
      );
View Compiled

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/draft-js/0.10.0/Draft.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react-dom.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.js
  4. https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js
  5. https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.3/es6-shim.min.js
  6. https://cdnjs.cloudflare.com/ajax/libs/draft-js/0.10.0/Draft.js