File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import {EditorContent, useEditor} from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Placeholder from '@tiptap/extension-placeholder';
import Image from '@tiptap/extension-image';
import {Table} from '@tiptap/extension-table';
import {TableCell} from '@tiptap/extension-table-cell';
import {TableHeader} from '@tiptap/extension-table-header';
import {TableRow} from '@tiptap/extension-table-row';
import {useEffect} from 'react';
type RichTextEditorProps = {
value: string;
onChange: (value: string) => void;
placeholder?: string;
disabled?: boolean;
};
type ToolbarButtonProps = {
label: string;
title: string;
active?: boolean;
disabled?: boolean;
onClick: () => void;
};
const ToolbarButton = ({label, title, active = false, disabled = false, onClick}: ToolbarButtonProps) => (
<button
type="button"
className={active ? 'active' : ''}
title={title}
aria-label={title}
disabled={disabled}
onClick={onClick}
>
{label}
</button>
);
export const RichTextEditor = ({
value,
onChange,
placeholder = '내용을 입력하세요.',
disabled = false,
}: RichTextEditorProps) => {
const editor = useEditor({
editable: !disabled,
extensions: [
StarterKit,
Underline,
Link.configure({
openOnClick: false,
autolink: true,
defaultProtocol: 'https',
}),
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
Placeholder.configure({
placeholder,
}),
Image.configure({
allowBase64: true,
}),
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
TableCell,
],
content: value || '',
editorProps: {
attributes: {
class: 'rich_text_editor_body',
},
},
onUpdate: ({editor}) => {
onChange(editor.getHTML());
},
});
useEffect(() => {
if (!editor) {
return;
}
editor.setEditable(!disabled);
}, [disabled, editor]);
useEffect(() => {
if (!editor || value === editor.getHTML()) {
return;
}
editor.commands.setContent(value || '', {emitUpdate: false});
}, [editor, value]);
if (!editor) {
return null;
}
const setLink = () => {
const previousUrl = editor.getAttributes('link').href as string | undefined;
const url = window.prompt('링크 URL을 입력하세요.', previousUrl ?? '');
if (url === null) {
return;
}
if (!url.trim()) {
editor.chain().focus().extendMarkRange('link').unsetLink().run();
return;
}
editor.chain().focus().extendMarkRange('link').setLink({href: url.trim()}).run();
};
const addImage = () => {
const url = window.prompt('이미지 URL을 입력하세요.');
if (!url?.trim()) {
return;
}
editor.chain().focus().setImage({src: url.trim()}).run();
};
return (
<div className="rich_text_editor">
<div className="rich_text_editor_toolbar">
<ToolbarButton
label="H2"
title="제목"
active={editor.isActive('heading', {level: 2})}
disabled={disabled}
onClick={() => editor.chain().focus().toggleHeading({level: 2}).run()}
/>
<ToolbarButton
label="B"
title="굵게"
active={editor.isActive('bold')}
disabled={disabled}
onClick={() => editor.chain().focus().toggleBold().run()}
/>
<ToolbarButton
label="I"
title="기울임"
active={editor.isActive('italic')}
disabled={disabled}
onClick={() => editor.chain().focus().toggleItalic().run()}
/>
<ToolbarButton
label="U"
title="밑줄"
active={editor.isActive('underline')}
disabled={disabled}
onClick={() => editor.chain().focus().toggleUnderline().run()}
/>
<ToolbarButton
label="S"
title="취소선"
active={editor.isActive('strike')}
disabled={disabled}
onClick={() => editor.chain().focus().toggleStrike().run()}
/>
<ToolbarButton
label="•"
title="글머리 기호"
active={editor.isActive('bulletList')}
disabled={disabled}
onClick={() => editor.chain().focus().toggleBulletList().run()}
/>
<ToolbarButton
label="1."
title="번호 목록"
active={editor.isActive('orderedList')}
disabled={disabled}
onClick={() => editor.chain().focus().toggleOrderedList().run()}
/>
<ToolbarButton
label="L"
title="왼쪽 정렬"
active={editor.isActive({textAlign: 'left'})}
disabled={disabled}
onClick={() => editor.chain().focus().setTextAlign('left').run()}
/>
<ToolbarButton
label="C"
title="가운데 정렬"
active={editor.isActive({textAlign: 'center'})}
disabled={disabled}
onClick={() => editor.chain().focus().setTextAlign('center').run()}
/>
<ToolbarButton
label="R"
title="오른쪽 정렬"
active={editor.isActive({textAlign: 'right'})}
disabled={disabled}
onClick={() => editor.chain().focus().setTextAlign('right').run()}
/>
<ToolbarButton
label="Link"
title="링크"
active={editor.isActive('link')}
disabled={disabled}
onClick={setLink}
/>
<ToolbarButton
label="Img"
title="이미지"
disabled={disabled}
onClick={addImage}
/>
<ToolbarButton
label="Tbl"
title="표 삽입"
disabled={disabled}
onClick={() => editor.chain().focus().insertTable({rows: 3, cols: 3, withHeaderRow: true}).run()}
/>
<ToolbarButton
label="Undo"
title="실행 취소"
disabled={disabled || !editor.can().undo()}
onClick={() => editor.chain().focus().undo().run()}
/>
<ToolbarButton
label="Redo"
title="다시 실행"
disabled={disabled || !editor.can().redo()}
onClick={() => editor.chain().focus().redo().run()}
/>
</div>
<EditorContent editor={editor}/>
</div>
);
};