最初に
テキストエディタが作成できるライブラリはたくさんあります。例えばfacebookが公開したDraft.jsはReactで使いやすいため有名だと思います。最近ではMicrosoftが公開したrooster.jsなどもあります。今回はたくさんのテキストエディタフレームワークの中から、Slateというものを選択してみました。Slateに関する日本語記事をほとんど見かけなかったので、今回はこちらをネタに書いてみます。(Draft.jsはぼちぼち見かけました)
Slate.jsとは
SlateはReactとImmutableの上に構築されたフレームワークです。Reactを使っている開発者であれば、かなり直感的にテキストエディタを構築できます。まだBeta版なので突然APIが消えたりする事もありますが…。
GitHub
https://github.com/ianstormtaylor/slate
ドキュメント
https://docs.slatejs.org
できあがりのイメージ
こんな感じのエディタが簡単に仕上がります。
エディタをつくる!..ための準備をする
プロジェクト作成
さっそくエディタをつくっていきましょう。とはいえ、Reactが動く環境をまず整えないといけません。create-react-appを使ってプロジェクトを生成します。
$ create-react-app 201812-react-slate-app
$ cd 201812-react-slate-app
Slateをインストールします。Slateはimmutableにも依存しているので、それもインストールします。
$ yarn add slate slate-react immutable
App.js,index.cssを編集する
初期のApp.jsはReactのロゴを描画していますが、まるっと削除してしまいます。この後、src/components/TextEditor.js
を作成して、そこでSlateの設定を行います。なのでApp.jsは<TextEditor />
をただレンダリングするだけにしておきます。
import React from "react";
import TextEditor from "./components/TextEditor";
class App extends React.Component {
render() {
return <TextEditor />; /* TextEditor.jsをレンダリングする */
}
}
export default App;
ちなみにindex.cssはこんな感じにしてあります。好みですが、等幅フォントに設定してあります。
body {
margin: 0;
padding: 0;
font-family: Consolas, monospace;
background-color: rgb(240, 240, 240);
}
エディタをつくる!
ここからSlateを使ってエディタを作成していきます。いったん下記の状態を作ります。
動かしてみる
まずはとりあえずエディタが動くようにしましょう。ちょっと長くなりますが、以下のようになります。とりあえずこれを貼り付けてyarn start
すれば、エディタが動くようになっているはずです!
import React from "react";
import { Editor } from "slate-react";
import { Value } from "slate";
const initialValue = Value.fromJSON({
document: {
nodes: [
{
object: "block",
type: "paragraph",
nodes: [
{
object: "text",
leaves: [{ text: "Hello Slate.js!!!" }]
}
]
}
]
}
});
class TextEditor extends React.Component {
state = {
value: initialValue
};
onChange = ({ value }) => {
this.setState({ value });
};
onKeyDown = (e, editor, next) => {
return next();
};
render() {
return (
<Editor
value={this.state.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
renderMark={this.renderMark}
/>
);
}
}
export default TextEditor;
解説
TextEditor.jsから重要な部分だけピックアップして解説を記載します。
読んでいただければわかると思うのですが、render()
で描画しているのは<Editor />
コンポーネントのみです。これがレンダリングされる部分がエディタとなります。プロパティもたった3つだけです。
先頭に設定したvalue={this.state.value}
はstateから取得した値をセットしています。state.value
にはエディタの状態(テキストの文字列や修飾された内容)を保存してあります。これの型はSlateのValueオブジェクトとなっています。このサンプルではinitialValue
というValueオブジェクトの初期値を定義してからReactのstateにセットしています。
state = {
value: initialValue /* 初期値はSlateのオブジェクトを渡す */
};
render() {
return (
<Editor
value={this.state.value} /* エディタの値。Reactのstateに保持させている */
onChange={this.onChange} /* エディタの中身が更新されたとき */
onKeyDown={this.onKeyDown} /* キーが押されたとき */
/>
);
}
以下の2つのアロー関数はEditorコンポーネントに渡す必要があるものです。onChange
はEditorに設定したvalue(SlateのValueオブジェクト)が更新されたときに呼ばれます。ここでstateに渡して保持させています。
onKeyDown
はキー入力されたときです。ここではreturn next()
としています。これがないと改行やバックスペースを受け付けてくれません。
/* Editorの中身が更新される(テキスト入力がなされる)ときに呼ばれる */
onChange = ({ value }) => {
this.setState({ value });
};
/* キー入力の受付。使っていない引数は、あとで使う予定 */
onKeyDown = (e, editor, next) => {
return next();
};
ただし、まだこの状態だとtextareaタグと同等くらいにしかなりません。ここから、リッチテキストエディタらしい振る舞いを実装していきます。
マウスで選択したエリアを太文字にする
テキストの一部を選択し、Cmd + bと入力することで太文字にしてみます。TextEditorを以下のように修正してみます。
- onKeyDownを修正
- renderMark関数を追加
- EditorコンポーネントにrenderMark関数を渡す
// 修正する
onKeyDown = (e, editor, next) => {
if (!e.metaKey) {
// cmdキーが入力されていなければ、エディタとしてそのままの動作
// (キー入力がそのままエディタに反映される)
return next();
}
// ブラウザの処理を制止(よく使います)
e.preventDefault();
switch (e.key) {
case "b": { // cmd + bのときに太文字
editor.toggleMark("bold");
return;
}
default: {
return;
}
}
};
// 新しく定義
renderMark = (props, next) => {
switch (props.mark.type) {
case "bold": { // 太文字にするため<strong>で囲う
return <strong {...props.attributes}>{props.children}</strong>;
}
default: {
return;
}
}
};
render() {
return (
<Editor
value={this.state.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
renderMark={this.renderMark} // テキストを修飾するときに呼ばれる
/>
);
}
解説
新しくrenderMark
関数が追加になりました。これがリッチテキストエディタを作るに欠かせない重要な関数で、テキストを修飾する役割を持ちます。ここでは、引数に"bold"
が渡ってきたときに<strong>...</strong>
として修飾を行います。
onKeyDown = (e, editor, next) => {
// 省略
editor.toggleMark("bold"); // キー入力条件でrenderMarkを呼び出す
};
renderMark = (props, next) => {
switch (props.mark.type) {
case "bold": { // 引数を判定し、マッチしたら<strong>で囲う
return <strong {...props.attributes}>{props.children}</strong>;
}
}
};
つまり、renderMark
にパターンをどんどん追加していけば、いろいろな修飾を実装することが可能になります。
renderMark = (props, next) => {
switch (props.mark.type) {
case "bold": {
// 太文字にする
return <strong {...props.attributes}>{props.children}</strong>;
}
case "italic": {
// イタリック体にする
return <em {...props.attributes} property="italic">{props.children}</em>;
}
case "underlined": {
// 下線を引く
return <u {...props.attributes}>{props.children}</u>;
}
}
};
冒頭のエディタを実装する
この記事の一番上でお見せしたgifのエディタを作成します。Slateの振る舞いに関してはだいたい説明が終わったので、後は使える修飾子を増やしたり、アイコンのクリックを追加したり、デザインを整えればOKです。Material UIのアイコンを使えば、よりいっそうテキストエディタっぽくなります。
完成したソースコードはこちらに置いておきますので、よかったら参考にしてください。
修正内容をざっくりと紹介
// アイコンがクリックされたらtoggleMark
onIconClick = (e, name) => {
e.preventDefault();
this.editor.toggleMark(name);
};
// エディタの内容を保持するための関数
ref = editor => {
this.editor = editor;
};
render() {
const { classes } = this.props;
return (
<div className={classes.root}>
{/* Material UIのPaperを使って影をつける */}
<Paper className={classes.iconsPaper} elevation={6}>
<FormatBoldIcon
className={classes.icon}
onClick={e => this.onIconClick(e, "bold")} // onClickで修飾する
/>
<FormatItalicIcon
className={classes.icon}
onClick={e => this.onIconClick(e, "italic")} // onClickで修飾する
/>
{/* 省略 */}
</Paper>
<Paper className={classes.editorPaper} elevation={6}>
<Editor
className={classes.editor}
value={this.state.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
renderMark={this.renderMark}
ref={this.ref}
/>
</Paper>
</div>
);
}
参考
Medium - Let’s build a fast, slick and customizable rich text editor with Slate.js and React