에디터를 결정하기에 앞서 여러가지를 테스트해봤다.
- slate: 데이터가 json 형태라 기존 데이터와 호환성 문제로 fail
- react-draft: 데이터가 객체형태라 viewer로 따로 써야하고 어쩌고 하는데 무엇보다 반응속도라 느려서 fail
- quill: toolbar를 세부항목까지 자유롭게 커스텀이 가능한 점은 좋았으나 표나 이미지 업로드 기능같은걸 내가 구현해야한다는 점에서 내가 게을러서 fail
- jodit: 무료는 로고 존재감도 쎄고 문서가 불친절해서 보류했다가 tiny mce로 결정해서 fail
- tiny mce: 문서가 매우 잘 되어있고 커뮤니티 버전으로도 어지간한 기능은 다 있어서 최종 결정하게 되었다.
import dynamic from 'next/dynamic'
import React, { useState, useRef, useMemo, useEffect, useCallback } from 'react'
const Editor = dynamic(() => import('@tinymce/tinymce-react').then((mode) => mode.Editor), {
ssr: false,
})
// plugin 기능 import 역할
const tinymcePlugins = [
'link',
'lists', // 기본 ol, ul
'image',
'autoresize',
'autolink',
'advlist', // ol, ul 불릿이 좀 더 여러가지가 생김
'table',
'preview',
'searchreplace', // 치환기능
'visualblocks', // 태그를 실루엣으로 보여주어 태그가 꼬일경우 편집하는데 도움됨
'code', // 코드삽입기능ㄴㄴ html 태그로 보여주는 기능임
'fullscreen',
'media', // 동영상
'accordion', // 티스토리 기준 접은글 기능
'autosave', // localstrage에 저장함
]
// toolbar에 해당 기능을 나열함. plugin이 필요한 기능의 경우 위에도 아래에도 둘 다 들어가야함
const tinymceToolbar =
'undo redo | blocks fontsize |' +
'bold italic underline strikethrough forecolor backcolor |' +
'bullist numlist blockquote|' +
'alignleft aligncenter alignright alignjustify | outdent indent |' +
'link image table accordion media | removeformat visualblocks searchreplace restoredraft | fullscreen preview code'
// 온갖 설정은 여기에 다 넣는다고 보면 됨
const editorInitOption = {
language: 'ko_KR', // 한국어팩
plugins: tinymcePlugins,
toolbar: tinymceToolbar,
color_default_background: '#E67E23', // fontBackColor 기본 선택은 되지만 "@tinymce/tinymce-react": "^4.3.0" 기준 pallete에서 선택은 안되어 있다.
color_default_foreground: window.matchMedia('(prefers-color-scheme: dark)').matches ? '#fff' : '#000', // fontColor 기본 선택
// dark skin 적용
// toolbar css 변경 방법은 찾고 있다.
skin: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'oxide-dark' : 'oxide',
// dark 대신 css 파일 경로를 넣어줄수도 있다.
// dark 적용후 import 되는 css 파일을 받아서 dark 기본색인 #222f3를 수정후 사용하면 좋다.
content_css: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default',
autosave_interval: '20s',
autosave_restore_when_empty: true, // 빈 편집기에 마지막 저장데이터 복원여부 확인
autosave_retention: '30m', // 30분 넘은 저장데이터는 삭제
autosave_prefix: 'tinymce-autosave-{path}{query}-{id}-',
// min_height: 250,
// max_height: document.querySelector('#__next').offsetHeight - 250,
menubar: false,
branding: false,
statusbar: false,
// automatic_uploads: false, // true가 기본값
// 이미지를 업로드할 서버 api 주소.
// 다음과 같은 형태로 반환해야 정상적으로 동작한다.
// { "location": "folder/sub-folder/new-location.png" }
images_upload_url: '/api/image-uploads',
images_upload_base_path: '/api/files', // 업로드한 파일의 중복경로
images_upload_credentials: true, // 인증정보가 cookie면 true
file_picker_callback: (callback, value, meta) => {
const input = document.createElement('input')
input.setAttribute('accept', 'image/*')
input.setAttribute('type', 'file')
input.onchange = function () {
const file = this.files[0]
const reader = new FileReader()
reader.onload = function () {
const id = 'blobid' + new Date().getTime()
const blobCache = tinymce.activeEditor.editorUpload.blobCache
const base64 = reader.result.split(',')[1]
const blobInfo = blobCache.create(id, file, base64)
blobCache.add(blobInfo)
callback(blobInfo.blobUri(), { alt: file.name })
}
reader.readAsDataURL(file)
}
input.click()
},
// 이미지 업로드 중 커스텀 기능을 넣어야한다면 이걸로 처리해야 함
// 위에서 설정한 항목중 images_upload_url, images_upload_base_path, images_upload_credentials를 쓰지 않고 내부 코드을 사용함
// convert_urls: false, // 아래에서 resolve가 절대경로를 반환하는 거라면 false 기본값은 true임
// images_upload_handler: (blobInfo, progress) =>
//
// new Promise((resolve, reject) => {
// const images_upload_url = '/api/image-uploads' // 이미지 업로드 api 주소
// const xhr = new XMLHttpRequest()
// xhr.withCredentials = true // 인증정보가 cookie면 true
// xhr.open('POST', images_upload_url)
// xhr.upload.onprogress = (e) => {
// progress((e.loaded / e.total) * 100)
// }
// xhr.onload = () => {
// if (xhr.status === 403) {
// reject({ message: 'HTTP Error: ' + xhr.status, remove: true })
// return
// }
// if (xhr.status < 200 || xhr.status >= 300) {
// reject('HTTP Error: ' + xhr.status)
// return
// }
// const json = JSON.parse(xhr.responseText)
// if (!json || typeof json.location != 'string') {
// reject('Invalid JSON: ' + xhr.responseText)
// return
// }
// /* 커스텀 설정 넣을 곳 */
// resolve(json.location) // 경로
// }
// xhr.onerror = () => {
// reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status)
// }
// const formData = new FormData()
// formData.append('file', blobInfo.blob(), blobInfo.filename())
// xhr.send(formData)
// }),
}
export default function Editor() {
const editorRef = useRef(null)
// editorRef.current.getContent()
return <Editor
onInit={(e, editor) => (editorRef.current = editor)}
apiKey="API키"
initialValue={에디터에 출력할 기본값이 있다면 여기}
init={editorInitOption}
/>
}
toolbar css는 일단.... _doucument 파일에다 <NextScript /> 아래에 <link rel="stylesheet" href="" /> 태그를 삽입하는 방식으로 일단 처리하려고 한다. 대신에 @media (prefers-color-scheme: dark) { } 로 감싸주었다.
'Javascript > React.js - Next.js' 카테고리의 다른 글
Next.js 다국어 라이브러리 next-i18next vs next-translate (2) | 2023.06.02 |
---|