Base: React, Redux
리엑트에서 많이 사용하는 텍스트에디터 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;
비동기처리 이벤트 없이 처음 실행되는 방법 - React.js (0) | 2022.12.21 |
---|---|
회원가입 관련 Front-end와 Back-end 정리 (0) | 2022.06.01 |
boiler-plate (0) | 2020.08.25 |
노마드코더님 _ React #3 (0) | 2020.06.16 |
노마드코더강의_React#1 (0) | 2020.06.12 |
댓글 영역