Draft.js使っていますか?
こんにちは! samayottaです。
Draft.jsは、WYSIWYG リッチエディタを作成するためのフレームワークです。
Facebook謹製で、Immutable.jsに基づく設計(関数型言語の思想に基づく設計)になっているのが特長です。有名どころでは、WantedlyのエディタページはDraft.jsによって書かれています。
Facebook謹製フレームワークDraft.js + React.jsでつくるリッチテキストエディタ
もしあなたがReactを書いていて、エディタ コンポーネントを求めているなら、必ず候補にあがる選択肢の一つでしょう。
しかしながら、多くのjsライブラリでそうであるように、Draft.jsについて書かれた日本語資料は多くなく、また自由度の高さゆえに抽象的でとっつきにくいところがあります。
そこでこの記事では、Draft.jsの公式デモ・プログラムの導入を行いその読解を手がかりとして取り扱いをお伝えできればと思います。特に取り扱いについては、「Draft.jsのミソ」であるeditorStateとcontentStateオブジェクトの利用を中心に説明します。
この記事を読めば、Reactアプリケーションの中にDraft.jsコンポーネントを設置し、さらにはAPIを引きながらいろいろな操作を実装できるようになるはずです。
Draft.jsの高機能エディタデモを動かす
まずは公式ページのexampleプログラムを実行してみましょう。
$ git clone https://github.com/facebook/draft-js
リポジトリをcloneし、
examples/draft0-10-0/rich/rich.html
をブラウザで開くと下のようにエディタが出てきました。
実際、このプログラムはとても良く動作します。まさにこのように動くエディタを実装したいですね。
デモ・プログラムの読解
では、このエディタはどのようにして動いているのでしょうか?
ソースコードを追っていくと、次のような内容に目が行きます:
- 44行目 this.onChange
this.onChange = (editorState) => this.setState({editorState});
まず一番基本的なところから見ていきます。
RichEditorExample
クラスはこのonChangeメソッドによってレンダリングが更新されます。このonChangeはReactユーザーにはお馴染みの記法で、いつものstrの代わりにeditorState
なるオブジェクトをセットしていることが読み取れますね。更新するときはこのオブジェクトをsetStateすれば良いようです。
- 113行目 InlineStyleControls コンポーネント
<InlineStyleControls
editorState={editorState}
onToggle={this.toggleInlineStyle}
/>
このコンポーネントがエディタ上部の[Bold, Italic, Underline, Monospace]ボタンをレンダリングしています。クリックするとonToggleが実行されるのだろうと当たりをつけて読むと(実際そうなのですが)、this._toggleInlineStyle
なる関数が中身だということが読み取れます。
_toggleInlineStyle(inlineStyle) {
this.onChange(
RichUtils.toggleInlineStyle(
this.props.editorState,
inlineStyle
)
);
}
この関数で注意したいことは2点です。
-
関数
RichUtiles.toggleInlineStyle
RichUtilsモジュールというものがDraft.jsにあり、それを利用して、文字をボールドにしたりイタリックにしたりできるということがわかります。そしてthis.onChange()にその返値が渡されているということは、この関数の返値の型はeditorState
なのでしょう。 -
引数
inlineStyle
ちょっとこのブログを読むのを止めて、この引数inlineStyle
に何が渡ってくるかを調べてみてください。
ほんの少し入り組んでいますが、最終的には157行目this.props.onToggle(this.props.style);
に辿り着き、そしてthis.props.styleは変数INLINE_STYLES
のstyle
であることが読み取れるでしょう。つまり"BOLD"とか"ITALIC"とかいったstrが、このinlineStyle
の中身になります。
ここまでの読解の結果、editorState
なるオブジェクトが重要そうだということがわかってきました。
また、関数RichUtiles.toggleInlineStyle
はそのeditorState
とstrを引数に取り、何かの変化を加え、editorState
を返す関数であることが読み取れます。
答え合わせ
さて、上の読解により何となく事態はつかめてきたように思います。それでは、実際のAPIを参照しましょう。
Draft.js RichUtils toggleInlineStyle
toggleInlineStyle(
editorState: EditorState,
inlineStyle: string
): EditorState
RichUtilsの中には、deleteやコードブロック、タブといった操作のためのユーティリティ関数が取りそろえられています。あなたがそのような操作を実行したかったら、これらの関数に現在のeditorState
を渡し、そして変更が加えられたeditorState
を受け取って、エディタにsetStateしてやればよいというわけです。
これであなたは基本的なDraft.jsによるエディタ作成の方法を身につけたことになります。お疲れ様でした!
*このようにeditorState
の参照透過性を維持しながらプログラミングを進められるのがDraft.jsの良さであり、冒頭で述べたImmutable.jsに基づく設計(関数型言語の思想に基づく設計)という言葉の意味かなあというのが僕の理解です。
実践編
それでは、満を持してDraft.jsプログラミングの実践に移りましょう。
いくつかのポイントについても追加的に解説します。
空のeditorStateを生成する
import EditorState from 'draft-js';
const editorState = EditorState.createEmpty();
strからeditorStateを生成する
const editorState = EditorState.createWithContent(
ContentState.createFromText("Hello Draft.js!")
);
ここで一つ新しい登場人物contentState
が出てきました。contentState
はその名の通りエディタの内容の本質的に重要なコンテンツ、例えば本文や装飾、その範囲などについて記載したオブジェクトです。editorState
はそれよりもメタな情報、たとえばUndo/Redoスタックなどを貯蔵しています。
editorStateにstringを足し込む
// origin
const editorState = EditorState.createWithContent(
ContentState.createFromText("Hello Draft.js!")
);
// add
const newText = editorState.getCurrentContent().getPlainText() + "Good night.";
const newEditorState = EditorState.createWithContent(
ContentState.createFromText(newText)
);
Draft.jsでは、このeditorState
とcontentState
の行き来が重要になります。contentStateをeditorStateから抜き出すためのメソッドは.getCurrentContentです。
この両者の関係は次のQiitaエントリに詳しいです。
苦しんで覚えるDraft.js -リッチテキストエディタをシュッと作る-
セーブロード実装
Firebaseにエディタの状態をセーブしたり、ロードしたりしましょう。
import {EditorState, ContentState} from 'draft-js';
import {convertToRaw, convertFromRaw} from 'draft-js';
import {stateToHTML} from "draft-js-export-html";
セーブメソッド
saveText(title){
const contentState = this.state.editorState.getCurrentContent();
const content = convertToRaw(contentState);
const html = stateToHTML(contentState);
// 以下FireBaseの処理
const textsRef = this.db.ref(this.savePointPath + "savedText/");
const newTextRef = textsRef.push();
return newTextRef.update(
{title:title, time:Date.now(), content:JSON.stringify(content), html:html}
);
}
draft-js-export-htmlを使ってエディタの内容をHTML変換し、本体と一緒にpushしています。こういうものをpushしておくと、previewなどが簡単に実装できて便利です。parseするときはhtml-react-parser などが便利です。
ロードメソッド
//引数contentはstringifyしたJSON文字列
setEditorState(e, content){
const contentState = convertFromRaw(JSON.parse(content));
const editorState = EditorState.createWithContent(contentState);
this.setState({editorState});
}
StringifyしたcontentStateオブジェクトを、再びparseしてeditorStateにはめこみます。
あとはsetStateするだけでOKです。
Draft.jsのひろがり
以上でこの記事の主要な内容は終わりです。お疲れ様でした!
今後Draft.jsの日本語記事が増え、より多くの知見が得られるのを私も楽しみにしています。
最後に、Draft.jsのプラグイン追加について触れておきます。
DraftJS Plugins
Facebook非公式ながら、DraftJS Pluginsのページにはハッシュタグ、絵文字、動かせる画像、ドラッグ&ドロップなど、ブログ・ミニブログ制作に欠かせない機能がずらりと並んでいます。よりモダンな機能を求めるなら、このサイトも要チェックです。
footnote
この記事はミクシィ2019新卒 advent calender2018のために書かれたものです。
この前の記事はPhalanxさんのKaggle奮闘記~塩コンペ編~です。並み居る猛者を抑えての一位ということで、すごすぎですね……。自分もより精進したいと思います。
私の本業はいちおう機械学習erで、就職面接中もずっとそのような説明をしてきたのですが、ここにきてフロントエンド JavaScriptが生活の中心を占めるようになりました。Urtica プロジェクトにかかりきりになるようになったからです。機械学習とはまったく違う世界のプログラミングで、始めた当初は困ることも多かったですが、何とかこなせるようになると、人間が触る部分の実装も興味深いなあという気持ちになりました。
今後も積極的に二足の草鞋を履いていく所存です。