30
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

React.jsAdvent Calendar 2018

Day 4

Slate.js入門!リッチテキストエディタを作る

Last updated at Posted at 2018-12-03

最初に

テキストエディタが作成できるライブラリはたくさんあります。例えば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 />をただレンダリングするだけにしておきます。

App.js
import React from "react";
import TextEditor from "./components/TextEditor";

class App extends React.Component {
  render() {
    return <TextEditor />; /* TextEditor.jsをレンダリングする */
  }
}

export default App;

ちなみにindex.cssはこんな感じにしてあります。好みですが、等幅フォントに設定してあります。

index.css
body {
  margin: 0;
  padding: 0;
  font-family: Consolas, monospace;
  background-color: rgb(240, 240, 240);
}

エディタをつくる!

ここからSlateを使ってエディタを作成していきます。いったん下記の状態を作ります。

動かしてみる

まずはとりあえずエディタが動くようにしましょう。ちょっと長くなりますが、以下のようになります。とりあえずこれを貼り付けてyarn startすれば、エディタが動くようになっているはずです!

TextEditor.js
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にセットしています。

Editorコンポーネント
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関数を渡す
TextEditor.jsを修正
// 修正する
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のアイコンを使えば、よりいっそうテキストエディタっぽくなります。

完成したソースコードはこちらに置いておきますので、よかったら参考にしてください。

修正内容をざっくりと紹介

TextEditor.jsの修正内容
// アイコンがクリックされたら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

30
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?