mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
123 lines
3.3 KiB
Vue
123 lines
3.3 KiB
Vue
<template>
|
|
<BubbleMenu :editor="editor" />
|
|
<EditorContent :editor="editor"
|
|
v-bind="$attrs"
|
|
:class="[$style.content, 'prose prose-sm dark:prose-invert break-words prose-a:no-underline prose-a:hover:underline prose-p:first-of-type:mt-0']" />
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import Emoji, { emojis } from "@tiptap/extension-emoji";
|
|
import Highlight from "@tiptap/extension-highlight";
|
|
import Link from "@tiptap/extension-link";
|
|
import Mention from "@tiptap/extension-mention";
|
|
import Placeholder from "@tiptap/extension-placeholder";
|
|
import Subscript from "@tiptap/extension-subscript";
|
|
import Superscript from "@tiptap/extension-superscript";
|
|
import Underline from "@tiptap/extension-underline";
|
|
import StarterKit from "@tiptap/starter-kit";
|
|
import { Editor, EditorContent } from "@tiptap/vue-3";
|
|
import BubbleMenu from "./bubble-menu.vue";
|
|
import suggestion from "./suggestion.ts";
|
|
|
|
const content = defineModel<string>("content");
|
|
const rawContent = defineModel<string>("rawContent");
|
|
const {
|
|
placeholder,
|
|
disabled,
|
|
mode = "rich",
|
|
} = defineProps<{
|
|
placeholder?: string;
|
|
mode?: "rich" | "plain";
|
|
disabled?: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
pasteFiles: [files: File[]];
|
|
}>();
|
|
|
|
const editor = new Editor({
|
|
extensions: [
|
|
StarterKit,
|
|
Placeholder.configure({
|
|
placeholder,
|
|
}),
|
|
Highlight,
|
|
Link,
|
|
Subscript,
|
|
Superscript,
|
|
Underline,
|
|
Mention.configure({
|
|
HTMLAttributes: {
|
|
class: "mention",
|
|
},
|
|
suggestion,
|
|
}),
|
|
Emoji.configure({
|
|
emojis: emojis.concat(
|
|
identity.value?.emojis.map((emoji) => ({
|
|
name: emoji.shortcode,
|
|
shortcodes: [emoji.shortcode],
|
|
group: emoji.category ?? undefined,
|
|
tags: [],
|
|
fallbackImage: emoji.url,
|
|
})) || [],
|
|
),
|
|
HTMLAttributes: {
|
|
class: "emoji not-prose",
|
|
},
|
|
}),
|
|
],
|
|
content: content.value,
|
|
onUpdate: ({ editor }) => {
|
|
content.value = mode === "rich" ? editor.getHTML() : editor.getText();
|
|
rawContent.value = editor.getText();
|
|
},
|
|
onPaste: (event) => {
|
|
// If pasting files, prevent the default behavior
|
|
if (event.clipboardData && event.clipboardData.files.length > 0) {
|
|
event.preventDefault();
|
|
const files = Array.from(event.clipboardData.files);
|
|
emit("pasteFiles", files);
|
|
}
|
|
},
|
|
autofocus: true,
|
|
editable: !disabled,
|
|
});
|
|
|
|
watchEffect(() => {
|
|
if (disabled) {
|
|
editor.setEditable(false);
|
|
} else {
|
|
editor.setEditable(true);
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
editor.destroy();
|
|
});
|
|
</script>
|
|
|
|
<style module>
|
|
@import url("~/styles/content.css");
|
|
</style>
|
|
|
|
<style>
|
|
@reference "../../styles/index.css";
|
|
|
|
.tiptap p.is-editor-empty:first-child::before {
|
|
color: var(--muted-foreground);
|
|
content: attr(data-placeholder);
|
|
float: left;
|
|
height: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.tiptap .mention {
|
|
@apply font-bold rounded-sm text-primary-foreground bg-primary px-1 py-0.5;
|
|
}
|
|
|
|
.tiptap .emoji>img {
|
|
@apply h-[1lh] align-middle inline hover:scale-110 transition-transform duration-75 ease-in-out;
|
|
}
|
|
</style>
|