🐡

WYSIWYGエディタライブラリをMantineのRichTextEditorに変更した話 #3:Tiptapで機能拡張実装

2023/06/09に公開

エディタライブラリの変更について

こんにちは!
テイラーワークス プロダクトチームの谷本です。

前回に引き続き、自社プロダクトで使用しているWYSIWYGエディタのライブラリを変更した際の話をまとめていきます。
本記事は連載構成になっております。

本連載の構成

#1:概要  
#2:エディタライブラリ比較、選定
#3:エディタコンポーネント実装(UIカスタマイズと機能拡張) ←本記事です📝
#4:旧エディタとの互換性対応、まとめ

前回記事で、エディタライブラリはMantineの提供するRichTextEditorを使用することに決定しました。
https://mantine.dev/others/tiptap/

今回は、MantineのRichTextEditorをベースに、UIカスタマイズや機能拡張についてまとめます。

UIカスタマイズ

UIカスタマイズ①:アイコン差し替え

アイコンは、デザイナーに協力してもらい、独自のものを使用しました。
Tiptapで提供されているStarterKitのTooltipカスタマイズは、各アイコンのコンポーネントを作成し、Toolbarの各機能にPropsとして渡すだけでOKです👍

<RichTextEditor.Bold icon={BoldIcon} /> 

後述のOGPリンク機能のように独自実装した機能の場合は、下記のようにアイコン指定できます。

import { RichTextEditor } from '@mantine/tiptap'

export const InsertOGPLinkControl = () => {
...
 return (
  <RichTextEditor.Control
    onClick={addOGPLink}
    aria-label="プレビュー付きリンク"
    title="プレビュー付きリンク"
  >
    <DatasetLinkedOutlined fontSize="small" />
  </RichTextEditor.Control>
 )
}

UIカスタマイズ②:Tooltipの文言変更

ツールバーの各機能アイコンホバー時のTooltipは日本語に変更し、かつ自由なテキストにできるので、わかりやすくカスタマイズしました。

Tiptapで提供されているStarterKitのTooltipカスタマイズは、RichTextEditorにlabelsを渡すだけでカスタマイズできます。

 <RichTextEditor
   editor={editor}
   labels={{
   boldControlLabel: '太字',
   underlineControlLabel: '下線',
   linkControlLabel: 'テキストリンク',
   blockquoteControlLabel: '引用',
   bulletListControlLabel: 'リスト',
   orderedListControlLabel: '番号付きリスト',
   alignLeftControlLabel: '左寄せ',
   alignCenterControlLabel: '中央寄せ',
   alignRightControlLabel: '右寄せ',
  }}>

UIカスタマイズ③:RichTextEditorにスタイル適用

表示側に適用したいスタイルはRichTextEditorと表示側のBox要素両方に共通のCSSを適用しました。

const editorStyle = css`
  .mantine-RichTextEditor-toolbar {
    button {
      height: 32px;
      width: 32px;
    }
    top: 4.8rem;
  }
  .ProseMirror {
    max-height: 580px;
    min-height: 200px;
    overflow: scroll;
  ...

各機能ごとにクラス定義してスタイルをあてる方法もありますが、今回はRichTextEditorに直接スタイルをあてました。

独自機能実装

Tiptapの機能拡張方法

Tiptapは、公式で拡張機能が用意されています。
https://tiptap.dev/api/extensions

前回の連載でまとめたとおり、エディタコンポーネントの導入は簡単にできるので、上記の公式提供機能を追加して必要な機能をカスタマイズしていくことができます。(エディタ標準の機能は、StarterKitにまとめられているので、インポートすればエディタ準備完了)

拡張機能は、機能をインポートし、useEditorextensionsに記載するだけで使用することができます!

const editor = useEditor({
  extensions: [
    StarterKit,
    Underline,
    Link,
  ]
})

また、既存機能の拡張をして公式提供の機能に少し手を加えたり、1から独自機能を実装することもできます。

既存機能の拡張

Tiptapでの機能拡張の方法として、まず既存の機能を拡張する方法があります。
https://tiptap.dev/guide/custom-extensions#extend-existing-extensions

たとえば、段落の背景色をピンクにしたい場合、下記のように既存機能のParagraphをインポートし、extendで拡張します。

// 1. 拡張したい元機能をインポート
import Paragraph from '@tiptap/extension-paragraph'

// 2. Paragraphを拡張し、カスタムParagraphを定義
const CustomParagraph = Paragraph.extend({
  addAttributes() {
    return {
      bgcolor: {
      // デフォルトのHTML属性を記載
        default: 'pink',
      },
    },
  },
})

addAttributesでは、エディタ側に属性を伝え、コンテンツレンダリング時にデフォルトでHTML属性としてセットすることができます。
上記の例では、

<p bgcolor="pink">テキスト</p>

とレンダリングされます。

実際に背景色をつけるためにはstyleをつける必要があるため、renderHTMLを使用し、設定を行います。

// 1. 拡張したい元機能をインポート
import Paragraph from '@tiptap/extension-paragraph'

// 2. Paragraphを拡張し、カスタムParagraphを定義
export const CustomParagraph = Paragraph.extend({
  addAttributes() {
    return {
      bgcolor: {
        default: null,
        // HTML出力時のカスタマイズ
        renderHTML: () => {
          return {
            style: 'background: pink',
          }
        },
      },
    }
  },
})

これで、下記のようにレンダリングされます。

<p style="background: pink">テキスト</p>

また。renderHTMLでは、デフォルトセットのほか、既存のHTML属性やaddAttributesで追加した属性から値をセットすることもできます。

attributes: {
  textAlign: {
    default: 'left',
    // 属性から取得してstyle設定に反映
    renderHTML: attributes => ({
      style: `text-align: ${attributes.textAlign}`,
      }),
    // HTMLに設定した値をパース
    parseHTML: element => element.style.textAlign || 'left',
    },
  },

上記の例では、エディタツールバーのコマンド実行時にtext-alignの値となる属性を渡し、styleにセットすることができます。

renderHTMLparseHTMLを組み合わせて、エディタに自由な名前と型(HTMLで解析できる範囲)で属性を設定することができます。

諸々の拡張設定が終わったら、実装した拡張機能をextensions内に追加するだけです。

// 3. エディタにCustomParagraph追加
const editor = useEditor({
  extensions: [
    CustomParagraph,
    // …
  ],
})

以上で、既存の段落機能に背景色をつけることができます!

独自機能の実装

既存機能の拡張ではなく、新しく機能を追加したい場合は独自で実装することもできます。

https://tiptap.dev/guide/custom-extensions#create-new-extensions

独自実装時の構文は、拡張機能設定と同じです。

今回行った変更対応ではOGPリンク機能と画像アップロード機能が必要であり、それらは公式提供のものがなく既存機能拡張でも要件を満たすことが難しいため、独自機能として実装を行いました。

独自機能実装①:OGPリンク機能

OGPリンク機能は、公式のYouTube機能を参考に新しくノードを追加する方法で実装しました。

https://github.com/ueberdosis/tiptap/tree/develop/packages/extension-youtube

ノードの設定は基本的に既存機能の拡張と同じで、レンダリング時の設定を定義し、それに加えてコマンドの追加と、Mantineツールバーに渡すためのコントロール定義を行いました。

簡略化したノード実装ファイルの構成は下記のとおりです。

// 1. Nodeをインポート
import { Node } from '@tiptap/core'

export const OGPLink = Node.create({
  name: 'ogp',
  group: 'block',
  selectable: true,
  // OGPリンクに必要な属性を追加
  addAttributes() {
    return {
      href: {
        default: null,
      },
      title: {
        default: null,
      },
      description: {
        default: null,
      },
      image: {
        default: null,
      },
      target: {
        default: `_blank`,
      },
    }
  },
  renderHTML(){
  ...
  },
  parseHTML() {
  ...
  },
  // ツールバーでOGPリンク追加のメニューをクリックした際に実行するコマンド設定
  addCommands() {
    return {
      setOGPLink:
      // addAttributesで追加した属性をコマンド実行時に渡す
        (options: { href: string; title: string; description: string; image: string }) =>
        ({ commands }) => {
          return commands.insertContent({
            type: this.name,
            attrs: options,
          })
        },
    }
  },

ツールバー用のコントロール定義は下記構成です。

import { RichTextEditor, useRichTextEditorContext } from '@mantine/tiptap'
import { useCallback } from 'react'

export const InsertOGPLinkControl = () => {
  const { editor } = useRichTextEditorContext()
    
  const addOGPLink = useCallback(async () => {
      // ユーザー入力されたURLを取得
    const url = ...
    
    if (url) {
      try {
          // APIでURLをもとにOGPリンク表示に必要なデータ取得
        const data = await fetchOgp(url)
                // データ取得できた場合、ノード実装で追加したコマンドを実行(ここで必要な属性を渡す)
        data && data.url
          ? editor
              .chain()
              .focus()
              .setOGPLink({
                href: data.url,
                title: data.title,
                description: data.description,
                image: data.image,
              })
              .run()
          : ...
      } catch (e) {
        // エラー処理
      }
    }
  }, [editor, fetchOgp])

  return (
    <RichTextEditor.Control
      onClick={addOGPLink}
      aria-label="プレビュー付きリンク"
      title="プレビュー付きリンク"
    >
      {/* アイコン */}
    </RichTextEditor.Control>
  )
}

あとは、useEditorのextensionにOGPリンク機能を追加し、

const editor = useEditor({
    extensions: [
      OGPLink,
     ...

ツールバーにもコントロールを追加すればOKです!

<RichTextEditor editor={editor}>
 <RichTextEditor.Toolbar sticky stickyOffset={60}>
  <RichTextEditor.ControlsGroup>
  {/* ツールバー内の表示したいControlsGroup内に追加 */}
   <InsertOGPLinkControl />
  </RichTextEditor.ControlsGroup>
 </RichTextEditor.Toolbar>
</RichTextEditor>

独自機能実装②:画像アップロード機能

Tiptap提供のImage機能は、ImageのURLを入力し画像が挿入される仕組みになっています。
https://tiptap.dev/api/nodes/image

そのため、独自で画像アップロード機能(コントロール定義追加)を実装し、APIで返した画像URLをTiptapに渡して画像を挿入しました。

import { RichTextEditor, useRichTextEditorContext } from '@mantine/tiptap'
import { useRef } from 'react'

export const InsertImageControl = () => {
  const { editor } = useRichTextEditorContext()

  const inputRef = useRef(null)

  const uploadImage = () => inputRef?.current?.click()

  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const files = ...
    file &&
      uploadEditorImage(file) // APIでAWS S3にアップロード
        .then((url: string) => {
          editor.chain().focus().setImage({ src: url }).run()
        })
        .catch((err) => {
         // エラー処理
        })
  }

  return (
    <>
      <RichTextEditor.Control onClick={uploadImage} aria-label="画像" title="画像">
        {/* アイコン */}
      </RichTextEditor.Control>

      <input
        type="file"
        accept="image/*"
        aria-label="画像アップロード"
        value=""
        ref={inputRef}
        style={{ display: `none` }}
        onChange={handleFileChange}
      />
    </>
  )
}

こちらも、OGPリンク機能と同様、エディタコンポーネントファイルでインポートしてツールバーに追加したらOKです!

まとめ

今回は、UIカスタマイズと独自機能実装についてまとめました。
Tiptapは公式の提供する拡張機能も充実していますが、独自で機能実装できるのは嬉しいですね。

次回は、本連載最後の記事として、旧エディタとの互換性対応をまとめていきます!

また、テイラーワークスでは、エンジニア採用強化中です!
フロントエンドに限らず、バックエンド・インフラ・デザイナーなど全領域で絶賛募集中です。

▼ 少しでも興味をお持ちいただけましたら、採用ページもチェックしてみてください ▼
https://tailorworks.co.jp/careers

Tailor Worksテックブログ

Discussion