tiptap3을 이용해 text editor로 작성후 저장, 수정을 구현해보았다!
파일업로드도 구현 예정이다.
npm install @tiptap/vue-3
npm install @tiptap/starter-kit
TextEditor.vue
<template>
<div v-if="editor">
<!-- 볼드, 이태릭, 취소선등 에디터 옵션. 아이콘은 Material icon에서 원하는 아이콘을 가져왔다. -->
<button
@click="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
<v-icon icon="mdi-format-bold"></v-icon>
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:disabled="!editor.can().chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
>
<v-icon icon="mdi-format-italic"></v-icon>
</button>
<button
@click="editor.chain().focus().toggleStrike().run()"
:disabled="!editor.can().chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
>
<v-icon icon="mdi-format-strikethrough"></v-icon>
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
>
<v-icon icon="mdi-format-header-1"></v-icon>
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
>
<v-icon icon="mdi-format-header-2"></v-icon>
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
>
<v-icon icon="mdi-format-header-3"></v-icon>
</button>
<button
@click="editor.chain().focus().undo().run()"
:disabled="!editor.can().chain().focus().undo().run()"
>
<v-icon icon="mdi-undo"></v-icon>
</button>
<button
@click="editor.chain().focus().redo().run()"
:disabled="!editor.can().chain().focus().redo().run()"
>
<v-icon icon="mdi-redo"></v-icon>
</button>
</div>
<!-- 에디터 영역 -->
<editor-content :editor="editor" style="height: 90%" />
</template>
<script>
import StarterKit from "@tiptap/starter-kit"; // starterKit를 사용하면 에디터 extensions를 하나씩 import하지 않고 starterKit에 내장되어 있는 extensions을 사용할 수 있다.
import { Editor, EditorContent } from "@tiptap/vue-3";
export default {
components: {
EditorContent,
},
props: {
modelValue: {
type: String,
default: "",
},
},
emits: ["update:modelValue"],
data() {
return {
editor: null,
};
},
watch: {
modelValue(value) {
// HTML 그대로 서버에 저장하려는 방식을 선택
const isSame = this.editor.getHTML() === value;
// JSON으로 변환시 사용
// const isSame = JSON.stringify(this.editor.getJSON()) === JSON.stringify(value)
if (isSame) {
return;
}
this.editor.commands.setContent(value, false);
},
},
mounted() {
this.editor = new Editor({
extensions: [StarterKit, Image],
content: this.modelValue,
onUpdate: () => {
this.$emit("update:modelValue", this.editor.getHTML());
// JSON일 경우
// this.$emit("update:modelValue", this.editor.getJSON());
},
});
},
beforeUnmount() {
this.editor.destroy();
},
};
</script>
<style lang="scss">
.ProseMirror {
height: 100%;
border: 1px solid black;
border-radius: 10px;
padding: 6px;
overflow: scroll;
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0d0d0d;
color: #fff;
font-family: "JetBrainsMono", monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
img {
max-width: 100%;
height: auto;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0d0d0d, 0.1);
}
hr {
border: none;
border-top: 2px solid rgba(#0d0d0d, 0.1);
margin: 2rem 0;
}
}
</style>
TextEditor를 사용할 컴포넌트
<template>
<div>
<text-editor v-model="content" />
<button @click="saveHandler">저장하기</button>
</div>
</template>
<script>
import TextEditor from "@/components/common/TextEditor.vue";
export default {
name: "NoticePost",
components: { TextEditor },
data() {
return {
content: "",
};
},
methods: {
saveHandler() {
// 서버 저장 요청 로직
}
},
created() {
// 수정일 경우 content 값을 넣어주기 위한 로직
if (수정일경우) {
this.$api.GET_CONTENT_DATA(this.$route.params.id).then((res) => {
this.content = res.data.content;
});
}
},
};
</script>
저장된 HTML을 보여주는 컴포넌트
<template>
<div v-html="content"/>
<template>
<script>
export default {
name: "NoticeDetail",
created() {
// 서버에 저장된 데이터 요청
this.$api.GET_CONTENT_DATA(this.$route.params.id).then((res) => {
this.content = res.data.content;
});
},
data() {
return {
content: "",
};
},
</script>
공감하며 읽었습니다. 좋은 글 감사드립니다.