wip: tiptap editor

This commit is contained in:
Dominik Pschenitschni 2022-08-04 22:05:41 +02:00 committed by kolaente
parent 01c2acdf34
commit 24b8915983
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
6 changed files with 1036 additions and 2 deletions

View File

@ -50,6 +50,32 @@
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/vue-fontawesome": "3.0.3",
"@github/hotkey": "2.1.1",
"@infectoone/vue-ganttastic": "2.1.2",
"@kyvg/vue3-notification": "2.6.1",
"@sentry/tracing": "7.19.0",
"@sentry/vue": "7.19.0",
"@tiptap/extension-character-count": "2.0.0-beta.202",
"@tiptap/extension-code-block-lowlight": "2.0.0-beta.202",
"@tiptap/extension-document": "2.0.0-beta.202",
"@tiptap/extension-highlight": "2.0.0-beta.202",
"@tiptap/extension-image": "2.0.0-beta.202",
"@tiptap/extension-link": "2.0.0-beta.202",
"@tiptap/extension-table": "2.0.0-beta.202",
"@tiptap/extension-table-cell": "2.0.0-beta.202",
"@tiptap/extension-table-header": "2.0.0-beta.202",
"@tiptap/extension-table-row": "2.0.0-beta.202",
"@tiptap/extension-task-item": "2.0.0-beta.202",
"@tiptap/extension-task-list": "2.0.0-beta.202",
"@tiptap/extension-typography": "2.0.0-beta.202",
"@tiptap/starter-kit": "2.0.0-beta.202",
"@tiptap/vue-3": "2.0.0-beta.202",
"@types/is-touch-device": "1.0.0",
"@types/lodash.clonedeep": "4.5.7",
"@types/sortablejs": "1.15.0",
"@vueuse/core": "9.5.0",
"@vueuse/router": "9.5.0",
"axios": "0.27.2",
"blurhash": "2.0.4",
"@infectoone/vue-ganttastic": "2.2.0",
"@intlify/unplugin-vue-i18n": "1.4.0",
"@kyvg/vue3-notification": "3.0.2",
@ -74,6 +100,7 @@
"is-touch-device": "1.0.1",
"klona": "2.0.6",
"lodash.debounce": "4.0.8",
"lowlight": "2.7.0",
"marked": "9.1.2",
"pinia": "2.1.7",
"register-service-worker": "1.7.2",

View File

@ -0,0 +1,46 @@
<template>
<node-view-wrapper class="code-block">
<select contenteditable="false" v-model="selectedLanguage">
<option :value="null">
auto
</option>
<option disabled>
</option>
<option v-for="(language, index) in languages" :value="language" :key="index">
{{ language }}
</option>
</select>
<pre><code><node-view-content /></code></pre>
</node-view-wrapper>
</template>
<script setup lang="ts">
import {ref, computed} from 'vue'
import {NodeViewContent, nodeViewProps, NodeViewWrapper} from '@tiptap/vue-3'
const props = defineProps(nodeViewProps)
const languages = ref(props.extension.options.lowlight.listLanguages())
const selectedLanguage = computed({
get() {
return props.node.attrs.language
},
set(language) {
props.updateAttributes({ language })
},
})
</script>
<style lang="scss">
.code-block {
position: relative;
select {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
}
</style>

View File

@ -0,0 +1,495 @@
<template>
<div class="editor-toolbar">
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
title="h1"
>
<span class="icon"> <icon :icon="['fa', 'fa-header']" /> </span>1
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
title="h2"
>
<span class="icon"> <icon :icon="['fa', 'fa-header']" /> </span>2
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
title="h3"
>
<span class="icon"> <icon :icon="['fa', 'fa-header']" /> </span>3
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"
title="h4"
>
<span class="icon"> <icon :icon="['fa', 'fa-header']" /> </span>4
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 5 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"
title="h5"
>
<span class="icon"> <icon :icon="['fa', 'fa-header']" /> </span>5
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeading({ level: 6 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"
title="h6"
>
<span class="icon"> <icon :icon="['fa', 'fa-header']" /> </span>6
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
title="bold"
>
<span class="icon">
<icon :icon="['fa', 'fa-bold']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
title="italic"
>
<span class="icon">
<icon :icon="['fa', 'fa-italic']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
title="strike"
>
<span class="icon">
<icon :icon="['fa', 'fa-strikethrough']" />
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleCode().run()"
:class="{ 'is-active': editor.isActive('code') }"
title="code"
>
<span class="icon">
<icon :icon="['fa', 'fa-code']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleCodeBlock().run()"
:class="{ 'is-active': editor.isActive('codeBlock') }"
title="code block"
>
<span class="icon">
<icon :icon="['fa', 'fa-code']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleBlockquote().run()"
:class="{ 'is-active': editor.isActive('blockquote') }"
title="quote"
>
<span class="icon">
<icon :icon="['fa', 'fa-quote-right']" />
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
title="bullet list"
>
<span class="icon">
<icon :icon="['fa', 'fa-list-ol']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleOrderedList().run()"
:class="{ 'is-active': editor.isActive('orderedList') }"
title="ordered list"
>
<span class="icon">
<icon :icon="['fa', 'fa-list-ul']" />
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().unsetAllMarks().run()"
>
clear marks
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().clearNodes().run()"
>
clear nodes
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="setLink"
:class="{ 'is-active': editor.isActive('link') }"
title="set link"
>
<span class="icon">
<icon :icon="['fa', 'fa-link']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().unsetLink().run()"
:disabled="!editor.isActive('link')"
title="unset link"
>
<span class="icon">
<icon :icon="['fa', 'fa-unlink']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleTaskList().run()"
:class="{ 'is-active': editor.isActive('taskList') }"
title="task list"
>
<span class="icon">
<icon :icon="['fa', ' fa-list-check']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().setParagraph().run()"
:class="{ 'is-active': editor.isActive('paragraph') }"
title="paragraph"
>
<span class="icon">
<icon :icon="['fa', 'fa-paragraph']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().setHorizontalRule().run()"
>
horizontal rule
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().setHardBreak().run()"
>
hard break
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().undo().run()"
title="undo"
>
<span class="icon">
<icon :icon="['fa', 'fa-undo']" />
</span>
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().redo().run()"
title="redo"
>
<span class="icon">
<icon :icon="['fa', 'fa-redo']" />
</span>
</BaseButton>
</div>
<div class="editor-toolbar__segment">
<!-- table -->
<BaseButton
class="editor-toolbar__button"
@click="toggleTableMode"
:class="{ 'is-active': editor.isActive('table') }"
title="table"
>
<span class="icon">
<icon :icon="['fa', 'fa-table']" />
</span>
</BaseButton>
<div v-if="tableMode">
<BaseButton
class="editor-toolbar__button"
@click="
editor
.chain()
.focus()
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run()
"
>
insertTable
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addColumnBefore().run()"
:disabled="!editor.can().addColumnBefore()"
>
addColumnBefore
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addColumnAfter().run()"
:disabled="!editor.can().addColumnAfter()"
>
addColumnAfter
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().deleteColumn().run()"
:disabled="!editor.can().deleteColumn()"
>
deleteColumn
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addRowBefore().run()"
:disabled="!editor.can().addRowBefore()"
>
addRowBefore
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().addRowAfter().run()"
:disabled="!editor.can().addRowAfter()"
>
addRowAfter
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().deleteRow().run()"
:disabled="!editor.can().deleteRow()"
>
deleteRow
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().deleteTable().run()"
:disabled="!editor.can().deleteTable()"
>
deleteTable
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().mergeCells().run()"
:disabled="!editor.can().mergeCells()"
>
mergeCells
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().splitCell().run()"
:disabled="!editor.can().splitCell()"
>
splitCell
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeaderColumn().run()"
:disabled="!editor.can().toggleHeaderColumn()"
>
toggleHeaderColumn
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeaderRow().run()"
:disabled="!editor.can().toggleHeaderRow()"
>
toggleHeaderRow
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().toggleHeaderCell().run()"
:disabled="!editor.can().toggleHeaderCell()"
>
toggleHeaderCell
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().mergeOrSplit().run()"
:disabled="!editor.can().mergeOrSplit()"
>
mergeOrSplit
</BaseButton>
<!-- <BaseButton
class="editor-toolbar__button"
@click="
editor
.chain()
.focus()
.setCellAttribute('backgroundColor', '#FAF594')
.run()
"
:disabled="
!editor.can().setCellAttribute('backgroundColor', '#FAF594')
"
>
setCellAttribute
</BaseButton> -->
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().fixTables().run()"
:disabled="!editor.can().fixTables()"
>
fixTables
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().goToNextCell().run()"
:disabled="!editor.can().goToNextCell()"
>
goToNextCell
</BaseButton>
<BaseButton
class="editor-toolbar__button"
@click="editor.chain().focus().goToPreviousCell().run()"
:disabled="!editor.can().goToPreviousCell()"
>
goToPreviousCell
</BaseButton>
</div>
</div>
<div class="editor-toolbar__segment">
<BaseButton class="editor-toolbar__button" @click="addImage" title="Add image from URL">
<span class="icon">
<icon :icon="['fa', 'fa-file-image']" />
</span>
</BaseButton>
</div>
</div>
</template>
<script setup lang="ts">
import {ref, type PropType} from 'vue'
import {Editor} from '@tiptap/vue-3'
import BaseButton from '@/components/base/BaseButton.vue'
const props = defineProps({
editor: {
default: null,
type: Editor as PropType<Editor>,
},
})
const tableMode = ref(false)
function toggleTableMode() {
tableMode.value = !tableMode.value
}
function addImage() {
const url = window.prompt('URL')
if (url) {
props.editor?.chain().focus().setImage({ src: url }).run()
}
}
function setLink() {
const previousUrl = props.editor.getAttributes('link').href
const url = window.prompt('URL', previousUrl)
// cancelled
if (url === null) {
return
}
// empty
if (url === '') {
props.editor.chain().focus().extendMarkRange('link').unsetLink().run()
return
}
// update link
props.editor
.chain()
.focus()
.extendMarkRange('link')
.setLink({ href: url, target: '_blank' })
.run()
}
</script>
<style lang="scss" scoped>
.editor-toolbar {
background: var(--grey-50);
border: 1px solid var(--grey-200);
border-bottom: none;
// position: relative;
user-select: none;
padding: 9px 10px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
display: flex;
flex-wrap: wrap;
> * + * {
// .editor-toolbar i.separator {
border--left-color: var(--grey-200) !important;
// }
// .editor-toolbar i.separator {
// display: inline-block;
// width: 0;
border-left: 1px solid var(--grey-200);
// border-right: 1px solid #fff;
// color: transparent;
// text-indent: -10px;
margin-left: 6px;
padding-left: 6px;
// }
}
}
.editor-toolbar__button {
color: var(--grey-700);
// width: 30px;
height: 30px;
border-radius: 3px;
border: 1px solid transparent;
&:hover {
background: var(--grey-200);
border-color: var(--grey-300);
}
}
</style>

View File

@ -0,0 +1,430 @@
<template>
<div class="tiptap">
<EditorToolbar v-if="editor" :editor="editor" />
<editor-content class="tiptap__editor" :editor="editor" />
</div>
</template>
<script lang="ts">
export const TIPTAP_TEXT_VALUE_PREFIX = '<!-- VIKUNJA TIPTAP -->\n'
const tiptapRegex = new RegExp(`${TIPTAP_TEXT_VALUE_PREFIX}`, 's')
</script>
<script setup lang="ts">
import {ref, watch, computed, onBeforeUnmount, type PropType} from 'vue'
import {marked} from 'marked'
import {refDebounced} from '@vueuse/core'
import EditorToolbar from './EditorToolbar.vue'
import Link from '@tiptap/extension-link'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
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 Highlight from '@tiptap/extension-highlight'
import Typography from '@tiptap/extension-typography'
import Document from '@tiptap/extension-document'
import Image from '@tiptap/extension-image'
// import Text from '@tiptap/extension-text'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import CharacterCount from '@tiptap/extension-character-count'
import StarterKit from '@tiptap/starter-kit'
import {EditorContent, useEditor, VueNodeViewRenderer} from '@tiptap/vue-3'
// load all highlight.js languages
import {lowlight} from 'lowlight'
import CodeBlock from './CodeBlock.vue'
// const CustomDocument = Document.extend({
// content: 'taskList',
// })
const CustomTaskItem = TaskItem.extend({
content: 'inline*',
})
const CustomTableCell = TableCell.extend({
addAttributes() {
return {
// extend the existing attributes
...this.parent?.(),
// and add a new one
backgroundColor: {
default: null,
parseHTML: (element: HTMLElement) => element.getAttribute('data-background-color'),
renderHTML: (attributes) => {
return {
'data-background-color': attributes.backgroundColor,
style: `background-color: ${attributes.backgroundColor}`,
}
},
},
}
},
})
const props = defineProps({
modelValue: {
type: String as PropType<string>,
default: '',
},
})
const emit = defineEmits(['update:modelValue', 'change'])
const inputHTML = ref('')
watch(
() => props.modelValue,
() => {
if (!props.modelValue.startsWith(TIPTAP_TEXT_VALUE_PREFIX)) {
// convert Markdown to HTML
return TIPTAP_TEXT_VALUE_PREFIX + marked.parse(props.modelValue)
}
return props.modelValue.replace(tiptapRegex, '')
},
{ immediate: true },
)
const debouncedInputHTML = refDebounced(inputHTML, 1000)
watch(debouncedInputHTML, (value) => {
emit('update:modelValue', TIPTAP_TEXT_VALUE_PREFIX + value)
emit('change', TIPTAP_TEXT_VALUE_PREFIX + value) // FIXME: remove this
})
const editor = useEditor({
content: inputHTML.value,
extensions: [
StarterKit,
Highlight,
Typography,
Link.configure({
openOnClick: false,
validate: (href: string) => /^https?:\/\//.test(href),
}),
// Table.configure({
// resizable: true,
// }),
// TableRow,
// TableHeader,
// // Default TableCell
// // TableCell,
// // Custom TableCell with backgroundColor attribute
// CustomTableCell,
// // start
// Document,
// // Text,
// Image,
// // Tasks
// CustomDocument,
TaskList,
CustomTaskItem,
// // character count
// CharacterCount,
// CodeBlockLowlight.extend({
// addNodeView() {
// return VueNodeViewRenderer(CodeBlock)
// },
// }).configure({ lowlight }),
],
onUpdate: () => {
// HTML
inputHTML.value = editor.value!.getHTML()
// JSON
// this.$emit('update:modelValue', this.editor.getJSON())
},
})
watch(inputHTML, (value) => {
if (!editor.value) return
// HTML
const isSame = editor.value.getHTML() === value
// JSON
// const isSame = JSON.stringify(editor.value.getJSON()) === JSON.stringify(value)
if (isSame) {
return
}
editor.value.commands.setContent(value, false)
})
onBeforeUnmount(() => editor.value?.destroy())
</script>
<style lang="scss">
.tiptap__editor {
// box-sizing: border-box;
// height: auto;
min-height: 150px;
border: 1px solid #ddd;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding: 10px;
// font: inherit;
// z-index: 0;
// word-wrap: break-word;
border: 1px solid var(--grey-200) !important;
background: var(--white);
}
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
a {
color: #68cef8;
}
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;
}
}
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;
}
.hljs-comment,
.hljs-quote {
color: #616161;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f98181;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #fbbc88;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #b9f18d;
}
.hljs-title,
.hljs-section {
color: #faf594;
}
.hljs-keyword,
.hljs-selector-tag {
color: #70cff8;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}
img {
max-width: 100%;
height: auto;
&.ProseMirror-selectednode {
outline: 3px solid #68cef8;
}
}
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;
}
}
/* Table-specific styling */
.ProseMirror {
table {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0;
overflow: hidden;
td,
th {
min-width: 1em;
border: 2px solid #ced4da;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box;
position: relative;
> * {
margin-bottom: 0;
}
}
th {
font-weight: bold;
text-align: left;
background-color: #f1f3f5;
}
.selectedCell:after {
z-index: 2;
position: absolute;
content: "";
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(200, 200, 255, 0.4);
pointer-events: none;
}
.column-resize-handle {
position: absolute;
right: -2px;
top: 0;
bottom: -2px;
width: 4px;
background-color: #adf;
pointer-events: none;
}
p {
margin: 0;
}
}
}
.tableWrapper {
overflow-x: auto;
}
.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
// tasklist
ul[data-type="taskList"] {
list-style: none;
padding: 0;
li {
display: flex;
align-items: center;
> label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
}
> div {
flex: 1 1 auto;
}
}
input[type="checkbox"] {
cursor: pointer;
}
}
// character count
.character-count {
margin-top: 1rem;
display: flex;
align-items: center;
color: #68cef8;
&--warning {
color: #fb5151;
}
&__graph {
margin-right: 0.5rem;
}
&__text {
color: #868e96;
}
}
</style>

View File

@ -1,3 +1,6 @@
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
export default createAsyncComponent(() => import('@/components/input/editor.vue'))
// const Editor = createAsyncComponent(() => import('@/components/input/editor.vue'))
const TipTap = createAsyncComponent(() => import('@/components/base/TipTap.vue'))
export default TipTap

View File

@ -6,6 +6,10 @@ import {
faArchive,
faArrowLeft,
faArrowUpFromBracket,
faBold,
faItalic,
faStrikethrough,
faCode,
faBars,
faBell,
faBolt,
@ -29,6 +33,7 @@ import {
faFilter,
faForward,
faGripLines,
faHeader,
faHistory,
faImage,
faKeyboard,
@ -59,14 +64,26 @@ import {
faTimes,
faTrashAlt,
faUser,
faUsers, faX,
faUsers,
faQuoteRight,
faListUl,
faLink,
faUndo,
faRedo,
faUnlink,
faParagraph,
faTable,
faX,
} from '@fortawesome/free-solid-svg-icons'
import {
faBellSlash,
faCalendarAlt,
faCheckSquare,
faClock,
faComments,
faFileImage,
faSave,
faSquareCheck,
faStar,
faSun,
faTimesCircle,
@ -76,6 +93,21 @@ import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import type {FontAwesomeIcon as FontAwesomeIconFixedTypes} from '@/types/vue-fontawesome'
library.add(faBold)
library.add(faUndo)
library.add(faRedo)
library.add(faItalic)
library.add(faLink)
library.add(faUnlink)
library.add(faParagraph)
library.add(faSquareCheck)
library.add(faTable)
library.add(faFileImage)
library.add(faCheckSquare)
library.add(faStrikethrough)
library.add(faCode)
library.add(faQuoteRight)
library.add(faListUl)
library.add(faAlignLeft)
library.add(faAngleRight)
library.add(faArchive)
@ -107,6 +139,7 @@ library.add(faFillDrip)
library.add(faFilter)
library.add(faForward)
library.add(faGripLines)
library.add(faHeader)
library.add(faHistory)
library.add(faImage)
library.add(faKeyboard)