qin-team-recipe / 02-frontend

チーム02のフロントエンド用リポジトリです
3 stars 0 forks source link

リッチテキストエディタを選定する #40

Open DaisukeTujita opened 1 year ago

DaisukeTujita commented 1 year ago

リッチテキストエディタ選定

目的

下書きページでレシピ工程を作成する際に使用するリッチテキストエディタライブラリを選定する   スクリーンショット 2023-08-20 171147

前提

要件

候補の選び方と選定方法

DaisukeTujita commented 1 year ago

候補

インターネット上の情報等によりメジャーなライブラリを特定。いずれも要件を満たしている。

名称 概要 Githubスター数
Draft.js Meta製。2020年にアーカイブ化され開発されていない。 22.5k
lexical Meta製。Draftの後継。フレームワークなので作りこみが必要 15.2k
tiptap ヘッドレスUI。マンタインのベース。 20.7k
quill slackのWeb版で使われている 36.7k

npmトレンド

npmトレンド

参考サイト

ほぼ同様の選定を行っているサイトの情報。 2023年2月時点で9つのライブラリを比較しており、結果的にMantineを選定している。

WYSIWYGエディタライブラリをMantineのRichTextEditorに変更した話

DaisukeTujita commented 1 year ago

Mantineお試し

画像アップロードメニューを追加

mantineお試し

コード構成

src
├─app
|  └─test_mantine
|     └─page.tsx
├─components
|  ├─InsertImageControl.tsx
|  ├─MantineRichTextEditor.tsx
|  └─MantineSaveButton.tsx

page.tsx

import { MantineRichTextEditor } from '@/components/MantineRichTextEditor';
import { MantineSaveButton } from '@/components/MantineSaveButton';

export default function Mantine() {
  const content ='' //ここはサーバサイトでfetch

  return (
    <>
      <MantineSaveButton />
      <MantineRichTextEditor initailContent={content}/>  
    </>
  )
}

MantineRichTextEditor.tsx

"use client"
import { RichTextEditor, Link } from '@mantine/tiptap';
import { useEditor } from '@tiptap/react';
import Highlight from '@tiptap/extension-highlight';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Superscript from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
import { InsertImageControl } from './InsertImageControl';
import { useState } from 'react';
import Image from '@tiptap/extension-image'

type MantineRichTextEditorProp = {
  initailContent : string
}
export function MantineRichTextEditor(props : MantineRichTextEditorProp) {
  const {initailContent} = props
  const [content, setContent] = useState(initailContent);
  const editor = useEditor({
    extensions: [
      StarterKit,
      Underline,
      Link,
      Superscript,
      SubScript,
      Highlight,
      TextAlign.configure({ types: ['heading', 'paragraph'] }),
      Image,
    ],
    content: content ?? '',
    onUpdate: ({ editor }) => {
      localStorage.setItem("content", editor.getHTML())
    },
  });

  return (
    <RichTextEditor editor={editor}>
      <RichTextEditor.Toolbar sticky stickyOffset={60}>
        <RichTextEditor.ControlsGroup>
          <RichTextEditor.Bold />
          <RichTextEditor.Italic />
          <RichTextEditor.Underline />
        </RichTextEditor.ControlsGroup>

        <RichTextEditor.ControlsGroup>
          <RichTextEditor.H1 />
          <RichTextEditor.H2 />
          <RichTextEditor.H3 />
          <RichTextEditor.H4 />
        </RichTextEditor.ControlsGroup>

        <RichTextEditor.ControlsGroup>
          <RichTextEditor.Hr />
          <RichTextEditor.BulletList />
          <RichTextEditor.OrderedList />
        </RichTextEditor.ControlsGroup>

        <RichTextEditor.ControlsGroup>
          <RichTextEditor.Link />
          <RichTextEditor.Unlink />
        </RichTextEditor.ControlsGroup>

        <RichTextEditor.ControlsGroup>
          <RichTextEditor.AlignLeft />
          <RichTextEditor.AlignCenter />
          <RichTextEditor.AlignJustify />
          <RichTextEditor.AlignRight />
        </RichTextEditor.ControlsGroup>

        {/* 画像アップロード用メニューを追加 */}
        <RichTextEditor.ControlsGroup>
          <InsertImageControl />
        </RichTextEditor.ControlsGroup>
      </RichTextEditor.Toolbar>

      <RichTextEditor.Content />
    </RichTextEditor>
  );
}
DaisukeTujita commented 1 year ago

InsertImageControl.tsx

import { RichTextEditor, useRichTextEditorContext } from '@mantine/tiptap'
import { useRef } from 'react'
import {IoImageOutline} from 'react-icons/io5'

export const InsertImageControl = () => {
  const { editor } = useRichTextEditorContext()
  const inputRef = useRef<HTMLInputElement>(null)
  const uploadImage = () => inputRef?.current?.click()
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {

    // filesアップロード処理
    const file = e.target.files && e.target.files[0]
    if (!file) return
    try{
      let reader = new FileReader()
      reader.onloadend = () => {
        const imagePreviewUrl = reader.result?.toString()
        // 本来はここでストレージアップロード
        if (imagePreviewUrl) {
          editor
            .chain()
            .focus()
            .setImage({src: imagePreviewUrl}) 
            .run()
          localStorage.setItem("content", editor.getHTML())
        }
      }   
      reader.readAsDataURL(file)
    }catch(err){
      console.error(err)
    }
  }

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

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

MantineSaveButton.tsx

"use client"

export function MantineSaveButton() {

    const handleSave = () => {
        const content = localStorage.getItem("content")
        console.log(content) // 本来はここでpostしてDBに登録
    }
  return (
    <button onClick={handleSave}>Save</button>
  );
}
DaisukeTujita commented 1 year ago

シーケンス シーケンス