Home

Awesome

Latest development is going on in this branch.

medium-draft - demo

npm version Build Status

A medium like rich text editor built upon draft-js with an emphasis on eliminating mouse usage by adding relevant keyboard shortcuts.

Documentation in progress.

Install the beta version using

npm install medium-draft@beta

Features

Following are the keyboard shortcuts to toggle block types (<kbd>Alt and CTRL</kbd> for Windows/Linux and <kbd>Option and Command</kbd> for OSX)
Other Shortcuts
Editor level commands

These commands are not a part of the core editor but have been implemented in the example code that uses the medium-draft editor.

Special characters while typing: While typing in an empty block, if the content matches one of the following, that particular block's type and look will be changed to the corresponding block specified below

Installation

Usage

medium-draft sits on top of draft-js with some built in functionalities and blocks. Its API is almost the same as that of draft-js. You can take a look at the demo editor's code to see the implementation.

CSS

Include the css that comes with the library in your HTML -

<link rel="stylesheet" type="text/css" href="https://unpkg.com/medium-draft/dist/medium-draft.css">

If you are using webpack for bundling, you can import the CSS like this in your JS code

import 'medium-draft/lib/index.css';

If you are using sideButtons, you will also need to include the css for font-awesome -

<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css">

or something equivalent.

JS (ES6)

At the minimum, you need to provide editorState and onChange props, the same as draft-js.


import React from 'react';
import ReactDOM from 'react-dom';

// if using webpack
// import 'medium-draft/lib/index.css';

import {
  Editor,
  createEditorState,
} from 'medium-draft';

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

    this.state = {
      editorState: createEditorState(), // for empty content
    };

    /*
    this.state = {
      editorState: createEditorState(data), // with content
    };
    */

    this.onChange = (editorState) => {
      this.setState({ editorState });
    };

    this.refsEditor = React.createRef();

  }

  componentDidMount() {
    this.refsEditor.current.focus();
  }

  render() {
    const { editorState } = this.state;
    return (
      <Editor
        ref={this.refsEditor}
        editorState={editorState}
        onChange={this.onChange} />
    );
  }
};

ReactDOM.render(
  <App />,
  document.getElementById('app')
);

Customizing side buttons

medium-draft's Editor accepts a prop called sideButtons. By default, there is only one (image) button, but you can add more. The sideButtons prop must be an array of objects with each object having the following signature:

{
  "title": "unique-button-name",
  "component": ButtonComponent
}

For ex:

{
  "title": "Image",
  "component": ImageSideButton
}

Example code:

Right now, the image button simply adds an image inside the editor using URL.createObjectURL. But if you would like to first upload the image to your server and then add that image to the editor, you can follow one of the 2 methods:

  1. Either extend the default ImageSideButton component that comes with medium-draft.

  2. Or create your own component with the complete functionality yourself.

For simplicity, we will follow the first method. If you study the implementation of ImageSideButton, you will see an onChange method that receives the file chooser event where the seleced files are available as event.target.files. We will simply override this method as we don't want to customize anything else. Also note that each side button component receives getEditorState function (returns the draft editorState), setEditorState(newEditorState) function (sets the new editorState) and close function which you need to call manually to close the side buttons list:

import React from 'react';
import {
  ImageSideButton,
  Block,
  addNewBlock,
  createEditorState,
  Editor,
} from 'medium-draft';
import 'isomorphic-fetch';

class CustomImageSideButton extends ImageSideButton {

  /*
  We will only check for first file and also whether
  it is an image or not.
  */
  onChange(e) {
    const file = e.target.files[0];
    if (file.type.indexOf('image/') === 0) {
      // This is a post request to server endpoint with image as `image`
      const formData = new FormData();
      formData.append('image', file);
      fetch('/your-server-endpoint', {
        method: 'POST',
        body: formData,
      }).then((response) => {
        if (response.status === 200) {
          // Assuming server responds with
          // `{ "url": "http://example-cdn.com/image.jpg"}`
          return response.json().then(data => {
            if (data.url) {
              this.props.setEditorState(addNewBlock(
                this.props.getEditorState(),
                Block.IMAGE, {
                  src: data.url,
                }
              ));
            }
          });
        }
      });
    }
    this.props.close();
  }

}

// Now pass this component instead of default prop to Editor example above.
class App extends React.Component {
  constructor(props) {
    super(props);

    this.sideButtons = [{
      title: 'Image',
      component: CustomImageSideButton,
    }];

    this.state = {
      editorState: createEditorState(), // for empty content
    };

    /*
    this.state = {
      editorState: createEditorState(data), // with content
    };
    */

    this.onChange = (editorState) => {
      this.setState({ editorState });
    };

    this.refsEditor = React.createRef()

  }

  componentDidMount() {
    this.refsEditor.current.focus();
  }

  render() {
    const { editorState } = this.state;
    return (
      <Editor
        ref={this.refsEditor}
        editorState={editorState}
        onChange={this.onChange}
        sideButtons={this.sideButtons}
      />
    );
  }
};

Removing side buttons

To remove the side buttons entirely, so that the circular add button never appears, just pass an empty array:

sideButtons={[]}

Customizing toolbar

There are three props you can use to customize the buttons in the toolbar that appears whenever you select text within the editor:

The default block-level editor buttons are ['header-three', 'unordered-list-item', 'ordered-list-item', 'blockquote', 'todo'], and the default inline editor buttons ['BOLD', 'ITALIC', 'UNDERLINE', 'HIGHLIGHT', 'hyperlink'].

For example, if you want to keep the default block buttons and add a few more, you can do something like the following:

import { BLOCK_BUTTONS } from 'medium-draft';

const blockButtons = [{
    label: 'H1',
    style: 'header-one',
    icon: 'header',
    description: 'Heading 1',
  },
  {
    label: 'H2',
    style: 'header-two',
    icon: 'header',
    description: 'Heading 2',
}].concat(BLOCK_BUTTONS);

// in your component
<Editor blockButtons={blockButtons} ... />

If you want to remove some buttons or reorder them, you could use functions like array.slice on the default BLOCK_BUTTONS and INLINE_BUTTONS, but this is probably more trouble than it's worth.

For this purpose it's better to use the toolbarConfig prop:

// custom ordering for block and inline buttons, and removes some buttons
const toolbarConfig = {
  block: ['unordered-list-item', 'header-one', 'header-three'],
  inline: ['BOLD', 'UNDERLINE', 'hyperlink'],
}

<Editor toolbarConfig={toolbarConfig} ... />

The strings inside the block and inline arrays must match the style attribute inside blockButtons and inlineButtons arrays.

To summarize: if you need add, remove, and reorder buttons, it's probably easiest to use blockButtons, inlineButtons, and toolbarConfig together.

Supply Your Own Toolbar

If the toolbar customization props aren't sufficient to get the behavior you want, you can inject your own toolbar with the ToolbarComponent prop.

This pattern is called component injection. Your ToolbarComponent receives the same props as the default toolbar.

If you want to write your own toolbar component, a good place to start is with the default component.

Render data to HTML

The feature to export HTML is available from version 0.4.1 onwards.

medium-draft uses draft-convert (which in turn uses react-dom-server) to render draft-js's editorState to HTML.

The exporter is not a part of the core library. If you want to use medium-draft-exporter, follow these steps -

Browserify/webpack

draft-convert is part of peerDependencies of medium-draft.

Code
  import mediumDraftExporter from 'medium-draft/lib/exporter';
  const editorState = /* your draft editorState */;
  const renderedHTML = mediumDraftExporter(editorState.getCurrentContent());
  /* Use renderedHTML */

Browser

<script src="https://unpkg.com/react-dom@15.2.1/dist/react-dom-server.min.js"></script>
<script src="https://unpkg.com/draft-convert@1.3.3/dist/draft-convert.min.js"></script>
<script src="https://unpkg.com/medium-draft/dist/medium-draft-exporter.js"></script>

The exporter is available as MediumDraftExporter global;

var mediumDraftExporter = MediumDraftExporter.default;
const editorState = /* your draft editorState */;
const renderedHTML = mediumDraftExporter(editorState.getCurrentContent());
/* Use renderedHTML */

The medium-draft-exporter also comes with a preset CSS if you want to apply some basic styles to the rendered HTML.

Load HTML exported using medium-draft-exporter to editorState

The feature to export HTML is available from version 0.5.3 onwards.

medium-draft uses draft-convert (which in turn uses react-dom-server) to render draft-js's editorState to HTML.

The importer is not a part of the core library. If you want to use medium-draft-importer, follow these steps -

Browserify/webpack

draft-convert is part of peerDependencies of medium-draft.

Code
  import { convertToRaw } from 'draft-js';
  import { createEditorState } from 'medium-draft';
  import mediumDraftImporter from 'medium-draft/lib/importer';

  const html = /* your previously exported html */;
  const editorState = createEditorState(convertToRaw(mediumDraftImporter(html)));
  // Use this editorState

Browser

<script src="https://unpkg.com/react-dom@15.2.1/dist/react-dom-server.min.js"></script>
<script src="https://unpkg.com/draft-convert@1.3.3/dist/draft-convert.min.js"></script>
<script src="https://unpkg.com/medium-draft/dist/medium-draft-importer.js"></script>

The importer is available as MediumDraftImporter global;

  const { convertToRaw } = Draft;
  const { createEditorState } = MediumDraft;
  const mediumDraftImporter = MediumDraftImporter.default;
  const html = /* your previously exported html */;
  const editorState = createEditorState(convertToRaw(mediumDraftImporter(html)));
  // Use this editorState

Issues

Developer

LICENSE

MIT