기능구현중
This commit is contained in:
220
frontend/components/common/TiptapEditor.vue
Normal file
220
frontend/components/common/TiptapEditor.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div class="tiptap-editor">
|
||||
<!-- 툴바 -->
|
||||
<div v-if="showToolbar && editor" class="tiptap-toolbar border-bottom bg-light px-2 py-1">
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('bold') }"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
title="굵게 (Ctrl+B)">
|
||||
<i class="bi bi-type-bold"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('italic') }"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
title="기울임 (Ctrl+I)">
|
||||
<i class="bi bi-type-italic"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('strike') }"
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
title="취소선">
|
||||
<i class="bi bi-type-strikethrough"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('heading', { level: 2 }) }"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
title="제목">
|
||||
<i class="bi bi-type-h2"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('heading', { level: 3 }) }"
|
||||
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
title="소제목">
|
||||
<i class="bi bi-type-h3"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('bulletList') }"
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
title="글머리 기호">
|
||||
<i class="bi bi-list-ul"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('orderedList') }"
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
title="번호 목록">
|
||||
<i class="bi bi-list-ol"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
:class="{ active: editor.isActive('blockquote') }"
|
||||
@click="editor.chain().focus().toggleBlockquote().run()"
|
||||
title="인용">
|
||||
<i class="bi bi-quote"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
@click="editor.chain().focus().setHorizontalRule().run()"
|
||||
title="구분선">
|
||||
<i class="bi bi-hr"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
@click="editor.chain().focus().undo().run()"
|
||||
:disabled="!editor.can().undo()"
|
||||
title="실행 취소 (Ctrl+Z)">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
@click="editor.chain().focus().redo().run()"
|
||||
:disabled="!editor.can().redo()"
|
||||
title="다시 실행 (Ctrl+Y)">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 에디터 본문 -->
|
||||
<EditorContent :editor="editor" class="tiptap-content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import Link from '@tiptap/extension-link'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
showToolbar?: boolean
|
||||
minHeight?: string
|
||||
readonly?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const editor = useEditor({
|
||||
content: props.modelValue || '',
|
||||
editable: !props.readonly,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: {
|
||||
levels: [2, 3]
|
||||
}
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: props.placeholder || '내용을 입력하세요...'
|
||||
}),
|
||||
Link.configure({
|
||||
openOnClick: false
|
||||
})
|
||||
],
|
||||
onUpdate: ({ editor }) => {
|
||||
emit('update:modelValue', editor.getHTML())
|
||||
}
|
||||
})
|
||||
|
||||
// props 변경 감지
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (editor.value && newValue !== editor.value.getHTML()) {
|
||||
editor.value.commands.setContent(newValue || '')
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.readonly, (newValue) => {
|
||||
if (editor.value) {
|
||||
editor.value.setEditable(!newValue)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor.value?.destroy()
|
||||
})
|
||||
|
||||
// 외부에서 접근 가능한 메서드
|
||||
defineExpose({
|
||||
getHTML: () => editor.value?.getHTML() || '',
|
||||
getText: () => editor.value?.getText() || '',
|
||||
focus: () => editor.value?.commands.focus(),
|
||||
clear: () => editor.value?.commands.clearContent()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tiptap-editor {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tiptap-toolbar .btn.active {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tiptap-content {
|
||||
min-height: v-bind('minHeight || "300px"');
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap) {
|
||||
outline: none;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap p.is-editor-empty:first-child::before) {
|
||||
color: #adb5bd;
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap h2) {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap h3) {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap ul),
|
||||
.tiptap-content :deep(.tiptap ol) {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap blockquote) {
|
||||
border-left: 3px solid #dee2e6;
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap hr) {
|
||||
border: none;
|
||||
border-top: 1px solid #dee2e6;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.tiptap-content :deep(.tiptap p) {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user