Skip to content

Instantly share code, notes, and snippets.

@gokhantaskan
Last active August 30, 2023 07:37
Show Gist options
  • Save gokhantaskan/333b16e89f4fec3ec894b291a2ccfb6b to your computer and use it in GitHub Desktop.
Save gokhantaskan/333b16e89f4fec3ec894b291a2ccfb6b to your computer and use it in GitHub Desktop.
TipTap WYSIWYG editor | Vue 3 + VeeValidate
<script setup lang="ts">
import CharacterCount from "@tiptap/extension-character-count";
import Link from "@tiptap/extension-link";
import Subscript from "@tiptap/extension-subscript";
import Superscript from "@tiptap/extension-superscript";
import StarterKit from "@tiptap/starter-kit";
import { Editor, EditorContent } from "@tiptap/vue-3";
const {
name,
rules,
label,
limit = 300,
} = defineProps<{
name: string;
rules?: any;
label?: string;
limit?: number;
}>();
const compRef = ref<HTMLDivElement>();
const editorRef = ref<Editor>();
const { value: html, errorMessage } = useField<string>(name, rules, {
label: label,
initialValue: "",
});
onMounted(async () => {
editorRef.value = new Editor({
extensions: [
StarterKit,
Subscript,
Superscript,
Link.configure({
HTMLAttributes: {
rel: "noopener noreferrer",
target: "_blank",
},
validate: href => /^https?:\/\//.test(href),
}),
CharacterCount.configure({
limit,
}),
],
content: unref(html),
onUpdate: () => {
if (editorRef.value) html.value = editorRef.value?.getHTML();
},
});
await nextTick();
const editorElement: HTMLDivElement | null | undefined =
compRef.value?.querySelector('[contenteditable="true"]');
if (editorElement) {
const editorStyles = getComputedStyle(editorElement);
const desiredLineHeight = parseFloat(editorStyles.lineHeight) * 3;
const paddingY =
parseFloat(editorStyles.paddingTop) +
parseFloat(editorStyles.paddingBottom);
editorElement.style.minHeight = `${desiredLineHeight + paddingY}px`;
} else {
console.warn("Failed to retrieve the editor element");
}
});
onBeforeUnmount(() => {
editorRef.value?.destroy();
});
</script>
<template>
<div class="space-y-1">
<FormInputLabel v-if="label">
{{ label }}
</FormInputLabel>
<div
ref="compRef"
class="c-editor"
:class="{
'has-error': errorMessage,
}"
>
<div
v-if="editorRef"
:class="[
'c-editor-header',
'p-3 border border-b-0 rounded-t-lg bg-gray-50',
'flex items-center justify-start gap-2',
]"
>
<button
:disabled="!editorRef.can().chain().focus().toggleBold().run()"
:class="{ 'is-active': editorRef.isActive('bold') }"
type="button"
@click="editorRef.chain().focus().toggleBold().run()"
>
<FaIcon :icon="['far', 'bold']" />
</button>
<button
:disabled="!editorRef.can().chain().focus().toggleItalic().run()"
:class="{ 'is-active': editorRef.isActive('italic') }"
type="button"
@click="editorRef.chain().focus().toggleItalic().run()"
>
<FaIcon :icon="['far', 'italic']" />
</button>
<button
:disabled="!editorRef.can().chain().focus().toggleStrike().run()"
:class="{ 'is-active': editorRef.isActive('strike') }"
type="button"
@click="editorRef.chain().focus().toggleStrike().run()"
>
<FaIcon :icon="['far', 'strikethrough']" />
</button>
<span></span>
<button
:class="{ 'is-active': editorRef.isActive('subscript') }"
:disabled="!editorRef.can().chain().focus().toggleSubscript().run()"
type="button"
@click="editorRef.chain().focus().toggleSubscript().run()"
>
<FaIcon :icon="['far', 'subscript']" />
</button>
<button
:class="{ 'is-active': editorRef.isActive('superscript') }"
:disabled="!editorRef.can().chain().focus().toggleSuperscript().run()"
type="button"
@click="editorRef.chain().focus().toggleSuperscript().run()"
>
<FaIcon :icon="['far', 'superscript']" />
</button>
<span></span>
<button
:class="{ 'is-active': editorRef.isActive('bulletList') }"
type="button"
@click="editorRef.chain().focus().toggleBulletList().run()"
>
<FaIcon :icon="['far', 'list-ul']" />
</button>
<button
:class="{ 'is-active': editorRef.isActive('orderedList') }"
type="button"
@click="editorRef.chain().focus().toggleOrderedList().run()"
>
<FaIcon :icon="['far', 'list-ol']" />
</button>
</div>
<div :class="['c-editor-wrapper', 'p-3 border border-t-0 rounded-b-lg']">
<editor-content :editor="editorRef" />
<div
v-if="editorRef"
class="cursor-default text-right text-xs text-gray-600"
>
{{ editorRef.storage.characterCount.characters() }}/{{ limit }}
</div>
</div>
</div>
<FormInputHelper :error-message="errorMessage" />
</div>
</template>
<style lang="scss" scoped>
.c-editor {
&:focus-within :is(.c-editor-header, .c-editor-wrapper) {
@apply border-primary-500;
}
&.has-error :is(.c-editor-header, .c-editor-wrapper) {
@apply border-danger-500;
}
:deep() {
button[type="button"] {
@apply w-6 h-6 text-sm rounded;
@apply inline-flex items-center justify-center;
&:hover {
@apply text-gray-500;
}
&.is-active {
@apply text-primary-500;
}
}
span {
@apply bg-gray-300 w-px self-stretch;
}
[contenteditable="true"] {
outline: none;
}
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment