상세 컨텐츠

본문 제목

react-quill 사용(이미지 포함)방법

CODING/React.js

by 뚜뚜 DDUDDU 2022. 5. 29. 22:04

본문

Base: React, Redux 

리엑트에서 많이 사용하는 텍스트에디터 react-quill 라이브러리를 활용해보자.

react-quill

 

설치

node설치 후,

yarn add react-quill

또는

npm install react-quill

 

 

활용

첫번째 시도

import React, { useCallback, useRef, useState, useEffect } from 'react';
import dynamic from 'next/dynamic'
const QuillNoSSRWrapper = dynamic(import('react-quill'), {
  ssr: false,
  loading: () => <p>Loading ...</p>,
})


import "react-quill/dist/quill.snow.css";

import { backUrl } from 'config/config';
import { useSelector, useDispatch } from "react-redux";


const Editor = () => {

  const [editorHtml, setEditorHtml] = useState('');
  const handleChange = (html) => {
    setEditorHtml(html);
    // let editor = null;
    // setTimeout(() => {
    //   editor.clipboard.dangerouslyPasteHTML(modHtml);
    //   let selection = editor.getSelection();
    //   selection.index = modHtml.length;
    //   editor.setSelection(selection);
    // }, 0);
  }


  const apiPostNewsImage = () => {
    dispatch({
      type: UPLOAD_MICRO_IMAGES_REQUEST,
      data: imageFormData,
    })
    const { imagePaths } = useSelector(state => state.micro);
    return imagePaths[0]

    // API post, returns image location as string e.g. 'http://www.example.com/images/foo.png'
  }


  const imageHandler = () => {
    const input = document.createElement('input');
    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');
    input.click();
    input.onchange = async () => {
      const file = input.files[0];
      const imageformData = new FormData();
      imageformData.append('image', file);
      console.log("imageformData:", imageformData);

      
      const quill = useRef();

      // Insert temporary loading placeholder image
      quill.insertEmbed(range.index, 'image', `${backUrl}/${me.avatar}`);

      // API post, returns image location as string e.g. 'http://www.example.com/images/foo.png'
      const res = await apiPostNewsImage(imageformData);

      // Save current cursor state
      const range = quill.getSelection(true);
      // Move cursor to right side of image (easier to continue typing)
      quill.setSelection(range.index + 1);
      // Remove placeholder image
      quill.deleteText(range.index, 1);

      // Insert uploaded image
      // quill.insertEmbed(range.index, 'image', res.body.image);
      quill.insertEmbed(range.index, 'image', res);
    };
  }

  return (
    <div className="text-editor">
      {JSON.stringify(editorHtml)}
      <hr />
      <QuillNoSSRWrapper

        ref={quill}
        onChange={handleChange}
        placeholder="자세한 내용을 입력해주세요."
        modules={{
          toolbar: {
            container: [
              [{ header: '1' }, { header: '2' }, { header: [3, 4, 5, 6] }, { font: [] }],
              [{ size: [] }],
              ['bold', 'italic', 'underline', 'strike', 'blockquote'],
              [{ list: 'ordered' }, { list: 'bullet' }],
              ['link', 'video'],
              ['link', 'image', 'video'],
              ['clean'],
              ['code-block']
            ],
            handlers: {
              image: imageHandler
            }
          }
        }}
      />
    </div>
  );
}

export default Editor;



// import React, { useState, useEffect, useCallback } from "react";
// import dynamic from 'next/dynamic';
// const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
// import "react-quill/dist/quill.snow.css";

// const EmptyComponent = () => false;

// const modules = {
//   toolbar: [
//     [{ header: [1, 2, 3, 4, false] }],
//     ["bold", "italic", "underline", "strike", "blockquote"],
//     [{ color: [] }, { background: [] }],
//     [
//       { align: [] },
//       { list: "ordered" },
//       { list: "bullet" },
//       { indent: "-1" },
//       { indent: "+1" }
//     ],
//     ["link", "image", "video", "code", "code-block"],
//     ["clean"],
//     [{
//       handlers: { image: imageHandler }
//     }]
//   ]
// };

// const formats = [
//   "header", "bold", "italic", "underline", "strike", "blockquote",
//   "list", "bullet", "indent", "link", "image", "color", "background",
//   "align", "code", "code-block"
// ];

// const NextQuill = (props) => {
//   const [quill, setQuill] = useState(<EmptyComponent />);

//   useEffect(() => {
//     // console.log(window);
//     const ReactQuill =
//       typeof window === "object" ? require("react-quill") : <EmptyComponent />;
//     setQuill(<ReactQuill {...props} />);
//   }, []);
//   return quill;
// };

// export default function IndexPage() {

//   const [html, setHtml] = useState("");
//   const onChange = useCallback((value) => {
//     setHtml(value);
//     console.log("html:", html);
//     console.log("value:", value);
//   }, [])

//   return (
//     <div className="text-editor">
//       {JSON.stringify(html)}
//       <NextQuill
//         style={{ height: "200px" }}
//         modules={modules}
//         formats={formats}
//         placeholder="자세한 내용을 입력해주세요."
//         // value={html}
//         onChange={onChange}
//       />
//     </div>
//   );
// }


/micro/1/course



실패 Class형
import React, { Component, useMemo, useRef, useEffect } from 'react';
import dynamic from 'next/dynamic'
import 'react-quill/dist/quill.snow.css';
import PropTypes from 'prop-types';


const QuillNoSSRWrapper = dynamic(import('react-quill'), {
  ssr: false,
  loading: () => <p>Loading ...</p>,
})
// const { Quill } = dynamic(import('react-quill'))

import { useSelector, useDispatch } from "react-redux";
import { UPLOAD_MICRO_IMAGES_REQUEST } from "reducers/micro";
import { backUrl } from 'config/config';

const propTypes = {
  value: PropTypes.string,
  onChange: PropTypes.func,
  // getEditor: PropTypes.func,
};

class index extends Component {
  constructor(props) {
    super(props);
    this.state = { editorHtml: '', mountedEditor: false }
    this.quillRef = null;
    this.reactQuillRef = null;
    this.handleClick = this.handleClick.bind(this)
    this.handleChange = this.handleChange.bind(this)
    this.attachQuillRefs = this.attachQuillRefs.bind(this);
    this.imageHandler = this.imageHandler.bind(this);
  }

  componentDidMount() {
    this.attachQuillRefs()
  }

  componentDidUpdate() {
    this.attachQuillRefs()
  }

  attachQuillRefs() {
    // Ensure React-Quill reference is available:
    if (typeof this.reactQuillRef.getEditor !== 'function') return;
    // Skip if Quill reference is defined:
    if (this.quillRef != null) return;

    const quillRef = this.reactQuillRef.getEditor();
    if (quillRef != null) this.quillRef = quillRef;
  }

  handleClick() {
    var range = this.quillRef.getSelection();
    let position = range ? range.index : 0;
    this.quillRef.insertText(position, 'Hello, World! ')
    this.quillRef.insertEmbed(position, 'audio', 'https://www.sample-videos.com/audio/mp3/crowd-cheering.mp3', 'user');
  }

  handleChange(value) {
    this.setState({ text: value })
  }


  imageHandler() {
    const input = document.createElement('input');
    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');
    input.click();
    input.onchange = async (e) => {
      // const file = input.files[0];
      // const files = e.target.files;
      const imageFormData = new FormData();
      // imageformData.append('image', files[0]);
      [].forEach.call(e.target.files, (f) => {
        imageFormData.append('image', f)
      });
      console.log("imageformData:", imageFormData);
      const dispatch = useDispatch();
      const { imagePaths } = useSelector(state => state.micro);
      dispatch({
        type: UPLOAD_MICRO_IMAGES_REQUEST,
        data: imageFormData,
      })
      // // Insert temporary loading placeholder image
      // quill.insertEmbed(range.index, 'image', `${backUrl}/`);

      // API post, returns image location as string e.g. 'http://www.example.com/images/foo.png'
      const url = `${backUrl}/${imagePaths}`
      // const res = await apiPostNewsImage(url);
      console.log("quillRef:", this.quillRef)
      // Save current cursor state
      const range = this.quillRef.getSelection(true);

      // // Remove placeholder image
      // quill.deleteText(range.index, 1);

      // Insert uploaded image
      // quill.insertEmbed(range.index, 'image', res.body.image);
      this.quillRef.insertEmbed(range.index, 'image', url);
      // Move cursor to right side of image (easier to continue typing)
      this.quillRef.setSelection(range.index + 1);
    };
  }



  render() {

    // const { value, onChange } = this.props;
    const modules = {
      toolbar: {
        container: [
          //[{ 'font': [] }],
          [{ "header": 1 }, { "header": 2 }, { "header": [3, 4, 5, 6] }],
          [{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown
          ['bold', 'italic', 'underline', 'strike', 'blockquote'],
          [{ "list": 'ordered' }, { "list": 'bullet' }, { "indent": '-1' }, { "indent": '+1' }],
          ['link', 'image'],
          [{ 'align': [] }, { 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
          ['clean'],
          ['code-block'],
        ],
        handlers: {
          image: this.imageHandler
        }
      }
    }

    const formats = [
      //'font',
      'header',
      'bold', 'italic', 'underline', 'strike', 'blockquote',
      'list', 'bullet', 'indent',
      'link', 'image', 'size',
      'align', 'color', 'background', 'code-block', 'code',
    ]

    return (
      <div style={{ height: "350px" }}>
        <QuillNoSSRWrapper
          id="editor"
          ref={(el) => { this.reactQuillRef = el }}
          style={{ height: "300px" }}
          theme="snow"
          modules={modules}
          formats={formats}
          value={this.state.text}
          onChange={this.handleChange}>
          <div id="react-quill" style={{ overflowY: "scroll" }} />
        </QuillNoSSRWrapper>
      </div>
    )
  }
}

index.propTypes = propTypes;

export default index;

 

다시 시도

 

import React, { Component, useMemo, useRef, useEffect } from 'react';
import dynamic from 'next/dynamic'
import 'react-quill/dist/quill.snow.css';
import PropTypes from 'prop-types';
const QuillNoSSRWrapper = dynamic(import('react-quill'), {
  ssr: false,
  loading: () => <p>Loading ...</p>,
})
// const { Quill } = dynamic(import('react-quill'))

import { useSelector, useDispatch } from "react-redux";
import { UPLOAD_MICRO_IMAGES_REQUEST } from "reducers/micro";
import { backUrl } from 'config/config';

const propTypes = {
  value: PropTypes.string,
  onChange: PropTypes.func,
  // getEditor: PropTypes.func,
};


const index = ({ value, onChange }) => {
  const quillRef = useRef(null);
  const reactQuillRef = useRef(null);

  const dispatch = useDispatch();
  const { imagePaths } = useSelector(state => state.micro);

  const attachQuillRefs = () => {
    if (typeof reactQuillRef.getEditor !== 'function') return;
    quillRef = reactQuillRef.getEditor();
  }

  // const quillRef = null;
  useEffect(() => {
    attachQuillRefs()
  }, [])

  const imageHandler = () => {
    const input = document.createElement('input');
    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');
    input.click();
    input.onchange = async (e) => {
      // const file = input.files[0];
      // const files = e.target.files;
      const imageFormData = new FormData();
      // imageformData.append('image', files[0]);
      [].forEach.call(e.target.files, (f) => {
        imageFormData.append('image', f)
      });
      console.log("imageformData:", imageFormData);

      dispatch({
        type: UPLOAD_MICRO_IMAGES_REQUEST,
        data: imageFormData,
      })


      // // Insert temporary loading placeholder image
      // quill.insertEmbed(range.index, 'image', `${backUrl}/`);

      // API post, returns image location as string e.g. 'http://www.example.com/images/foo.png'
      const url = `${backUrl}/${imagePaths}`
      // const res = await apiPostNewsImage(url);
      console.log("quillRef:", quillRef)
      // Save current cursor state
      const range = quillRef.getSelection(true);

      // // Remove placeholder image
      // quill.deleteText(range.index, 1);

      // Insert uploaded image
      // quill.insertEmbed(range.index, 'image', res.body.image);
      quillRef.insertEmbed(range.index, 'image', url);
      // Move cursor to right side of image (easier to continue typing)
      quillRef.setSelection(range.index + 1);
    };
  }


  const modules = useMemo(() => ({
    toolbar: {
      container: [
        //[{ 'font': [] }],
        [{ "header": 1 }, { "header": 2 }, { "header": [3, 4, 5, 6] }],
        [{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown
        ['bold', 'italic', 'underline', 'strike', 'blockquote'],
        [{ "list": 'ordered' }, { "list": 'bullet' }, { "indent": '-1' }, { "indent": '+1' }],
        ['link', 'image'],
        [{ 'align': [] }, { 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
        ['clean'],
        ['code-block'],
      ],
      handlers: {
        image: imageHandler
      }
    }
  }), [])

  const formats = [
    //'font',
    'header',
    'bold', 'italic', 'underline', 'strike', 'blockquote',
    'list', 'bullet', 'indent',
    'link', 'image', 'size',
    'align', 'color', 'background', 'code-block', 'code',
  ]


  return (
    <div style={{ height: "350px" }}>
      <QuillNoSSRWrapper
        id="editor"
        ref={reactQuillRef}
        style={{ height: "300px" }}
        theme="snow"
        modules={modules}
        formats={formats}
        value={value || ''}
        onChange={(content, delta, source, editor) => onChange(editor.getHTML())}>
        <div id="react-quill" style={{ overflowY: "scroll" }} />
      </QuillNoSSRWrapper>
    </div>
  )
}


index.defaultProps = {
  // quillRef: null,
  // reactQuillRef: null,
  value: undefined,
  onChange: () => { },
  // getEditor: () => { },
};

index.propTypes = propTypes;

export default index;

 

관련글 더보기

댓글 영역