Writing a Last Draft plugin with Draft.js

Last Draft Editor in action with the ld-gif plugin

Last Draft is a Draft.js editor based on Megadraft with added support for conversion to and from html, and a focus on some easy to use plugins.

The plugin model uses the same one built by marcelometal vierno and ricobl in MegaDraft, and I found it much simpler to use and extend, compared to the Draft.js plugins project built by https://www.draft-js-plugins.com nikgraf.

Using MegaDraft’s plugin model means that plugins built for Last Draft can easily be ported over to MegaDraft and vice versa. It would be nice to have something more official than this, but at this stage the 2 Editors are different in spite of using the 1 model for extending them.

So on to Last Draft and its plugins.

At the moment there are 9 Last Draft plugins available.

ld-image (built in)- Adds drag and drop image functionality
ld-video - Adds embed video functionality from youtube and vimeo
ld-audio - Adds an audio player with soundcloud support
ld-color-picker - Adds Color picker functionality
ld-emoji - Adds emoji functionality
ld-sticker - Adds sticker functionality
ld-gif - Adds gif functionality
ld-mention - Adds mention functionality
ld-html - Ads Edit html functionality
ld-todo - Adds todo functionality

So now to look at how we build a Last Draft plugin. The best approach is to clone a similar plugin and start playing around with it. However in this example we will build a gif plugin from scratch, which allows us to select gifs from giphy into our editor.

Here is how a plugin export looks in Last Draft/Megadraft

import GifButton from './GifButton'
import GifBlock from './GifBlock'
import GifModal from './GifModal'
export default {
type: 'gif',
button: GifButton,
block: GifBlock,
modal: GifModal
}

As we can see we have a modal, block, button and type. A plugin can define all of these, however only the type is required.

First lets look at the button component. It is a simple svg icon, nothing too fancy here. I am removing the svg and class names here for simplicity, I will link to the source at the end of the article.

import React, {Component} from 'react'
export default class extends Component {
render () {
return (
<svg fill='currentColor' height='24'>
</svg>
)
}
}

Now on to the Gif block. This is the block which will be added to the editorState, usually in the button click or from a picker in the modal, which is the approach we will take. As we can see the block has the image element with the data, in this case just the gif url src We also add a simple button to remove the block from the editor. In Last Draft and ld plugins the common approach is to use styled-components which is a nice way of styling in css within a react component. We also add class names to elements for users to override if they desire.

import React, {Component} from 'react'
import styled from 'styled-components'
export default class extends Component {
itemAction () {
this.props.container.remove()
}
  render () {
return (
<GifBlockWrapper>
<GifBlock>
<Image
src={this.props.data.src} />
</GifBlock>
<BlockAction onClick={::this.itemAction} key='deleteGif'>
<svg width='22' height='22'>
</svg>
</BlockAction>
</GifBlockWrapper>
)
}
}
const GifBlockWrapper = styled.div`
position: relative;
`
const GifBlock = styled.div`
display: flex;
`
const Image = styled.img`
display: block;
`
const BlockAction = styled.div`
cursor: pointer;
color: #ddd;
width: 1rem;
&:hover {
color: #333;
}
`

Okay now we have a block which can be added to the Editor, we need a way for users to add it. Modals are a common way of adding more complex functionality in Last Draft. If we simply wanted to add a static block, like a Todo, we would probably just add it in the onClick of the button, but in this more advanced example lets add the Modal.

import React, {Component} from 'react'
import styled from 'styled-components'
import Picker from './Picker'
export default class extends Component {
addGif (gif) {
let gifUrl = gif.fixed_width.url
const { editorState, onChange, insertDataBlock } = this.props
const data = {src: gifUrl, type: 'gif'}
onChange(insertDataBlock(editorState, data))
this.props.closeModal()
}
  render () {
return (
<Wrapper className='ld-gif-modal-wrapper'>
<Picker
onSelected={::this.addGif}
closeModal={this.props.closeModal} />
</Wrapper>
)
}
}
const Wrapper = styled.div`
position: relative;
background-color: inherit;
width: 360px;
`

Okay so that is fairly simple, and we can see it has a Picker component which picks our gif from Giphy and on select we add a new block with Last Draft’s insertDataBlock method. Which updates the state via this.props.onChange passing in the data for the block, which is just the url for the gif. The onChange event is the normal way of updating state in Draft, so check out the docs for more info on it.

The picker component which searches Giphy for a gif and returns a list to select from is quite simple, and I actually have this as a separate react component, so it has nothing specific to Draft. https://github.com/StevenIseki/react-giphy-picker Here is a simplified version of the picker code without the full styles.

import React, { Component } from 'react'
import styled from 'styled-components'
import 'whatwg-fetch'
export default class extends Component {
searchGifs () {
const {giphySearchUrl, searchValue} = this.state
if (searchValue.length < 1) { return }
let url = giphySearchUrl + '&q=' + searchValue.replace(' ', '+')
this.setState({gifs: []})
fetch(url, {
method: 'get'
}).then((response) => {
return response.json()
}).then((response) => {
let gifs = response.data.map((g, i) => {return g.images})
this.setState({gifs})
})
}
  onGiphySelect (gif) {
this.props.onSelected(gif)
}
  onSearchChange (e) {
this.setState(
{searchValue: e.target.value},
() => this.searchGifs()
)
}
  onKeyDown (e) {
if (e.key === 'Escape') { this.reset() }
}
  reset () {
this.setState({searchValue: ''})
}
render() {
const {gifs} = this.state
return (
<GiphyPickerWrapper>
<Input
name='giphy-search'
type="text"
onChange={::this.onSearchChange}
value={this.state.searchValue}
onKeyDown={::this.onKeyDown} />
<GiphyWrapper>
{
gifs.map((g, i) => {
return (
<Giphy key={i} src={g.fixed_width.url}
onClick={() => {this.onGiphySelect(g)}} />
)
})
}
</GiphyWrapper>
</GiphyPickerWrapper>
)
}
}
const GiphyPickerWrapper = styled.div`
position: absolute;
background-color: inherit;
`
const GiphyPicker = styled.div`
cursor: pointer;
`
const GiphyWrapper = styled.div`
display: flex;
flex-direction: column;
`
const Giphy = styled.img`
cursor: pointer;
`
const Input = styled.input`
background-color: white;
`
const CloseWrapper = styled.div`
position: absolute;
background: transparent;
color: #ccc;
`

It is fairly easy to follow. The searchGif function searches for gifs from the Giphy search endpoint and returns the gifs in a list. The onSelected prop is used to trigger the adding of the Gif, as shown earlier in the Modal component. onSelected={::this.addGif}

That is it, our first Last Draft plugin. See I knew we could do it…

The code for ld-gif is here See it in use in the Last Draft Editor http://lastdraft.xyz