Added Internal Links #84
This commit is contained in:
@@ -4,26 +4,26 @@
|
||||
<div v-if="editor" class="border-b border-gray-100 dark:border-gray-800 px-4 py-2 flex flex-wrap gap-1 items-center sticky top-0 bg-white dark:bg-gray-900 z-20 shadow-sm select-none">
|
||||
|
||||
<div class="flex gap-0.5 border-r border-gray-200 dark:border-gray-700 pr-2 mr-2">
|
||||
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }" class="toolbar-btn font-bold">B</button>
|
||||
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }" class="toolbar-btn italic">I</button>
|
||||
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }" class="toolbar-btn line-through">S</button>
|
||||
<button @click="editor.chain().focus().toggleHighlight().run()" :class="{ 'is-active': editor.isActive('highlight') }" class="toolbar-btn text-yellow-500 font-bold">M</button>
|
||||
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }" class="toolbar-btn font-bold" title="Fett">B</button>
|
||||
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }" class="toolbar-btn italic" title="Kursiv">I</button>
|
||||
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }" class="toolbar-btn line-through" title="Durchgestrichen">S</button>
|
||||
<button @click="editor.chain().focus().toggleHighlight().run()" :class="{ 'is-active': editor.isActive('highlight') }" class="toolbar-btn text-yellow-500 font-bold" title="Markieren">M</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-0.5 border-r border-gray-200 dark:border-gray-700 pr-2 mr-2">
|
||||
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }" class="toolbar-btn">H1</button>
|
||||
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }" class="toolbar-btn">H2</button>
|
||||
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }" class="toolbar-btn font-mono text-xs"></></button>
|
||||
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }" class="toolbar-btn" title="Überschrift 1">H1</button>
|
||||
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }" class="toolbar-btn" title="Überschrift 2">H2</button>
|
||||
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }" class="toolbar-btn font-mono text-xs" title="Code Block"></></button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-0.5 border-r border-gray-200 dark:border-gray-700 pr-2 mr-2">
|
||||
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }" class="toolbar-btn">•</button>
|
||||
<button @click="editor.chain().focus().toggleTaskList().run()" :class="{ 'is-active': editor.isActive('taskList') }" class="toolbar-btn">☑</button>
|
||||
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }" class="toolbar-btn" title="Liste">•</button>
|
||||
<button @click="editor.chain().focus().toggleTaskList().run()" :class="{ 'is-active': editor.isActive('taskList') }" class="toolbar-btn" title="Aufgabenliste">☑</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-0.5 items-center">
|
||||
<button @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()" class="toolbar-btn">▦</button>
|
||||
<button @click="addVideo" class="toolbar-btn text-red-600">
|
||||
<button @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()" class="toolbar-btn" title="Tabelle">▦</button>
|
||||
<button @click="addVideo" class="toolbar-btn text-red-600" title="YouTube Video">
|
||||
<UIcon name="i-heroicons-video-camera" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -71,19 +71,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue'
|
||||
import { useRouter } from '#app'
|
||||
import { useEditor, EditorContent, VueNodeViewRenderer } from '@tiptap/vue-3'
|
||||
// @ts-ignore
|
||||
import { BubbleMenu, FloatingMenu } from '@tiptap/vue-3/menus'
|
||||
|
||||
// Tiptap Extensions
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import CodeBlock from '@tiptap/extension-code-block'
|
||||
import { Table } from '@tiptap/extension-table'
|
||||
import { TableRow } from '@tiptap/extension-table-row'
|
||||
import { TableCell } from '@tiptap/extension-table-cell'
|
||||
import { TableHeader } from '@tiptap/extension-table-header'
|
||||
import BubbleMenuExtension from '@tiptap/extension-bubble-menu'
|
||||
import FloatingMenuExtension from '@tiptap/extension-floating-menu'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import TaskList from '@tiptap/extension-task-list'
|
||||
import TaskItem from '@tiptap/extension-task-item'
|
||||
@@ -92,18 +87,39 @@ import Highlight from '@tiptap/extension-highlight'
|
||||
import Youtube from '@tiptap/extension-youtube'
|
||||
import Typography from '@tiptap/extension-typography'
|
||||
import CharacterCount from '@tiptap/extension-character-count'
|
||||
import CodeBlock from '@tiptap/extension-code-block'
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// WICHTIGE ÄNDERUNG: NUR Table, KEINE Row/Cell/Header Imports
|
||||
// -----------------------------------------------------------
|
||||
import { Table } from '@tiptap/extension-table'
|
||||
import { TableRow } from '@tiptap/extension-table-row'
|
||||
import { TableCell } from '@tiptap/extension-table-cell'
|
||||
import { TableHeader } from '@tiptap/extension-table-header'
|
||||
|
||||
import BubbleMenuExtension from '@tiptap/extension-bubble-menu'
|
||||
import FloatingMenuExtension from '@tiptap/extension-floating-menu'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
|
||||
// IMPORT DER NEUEN KOMPONENTE
|
||||
import TiptapTaskItem from './TiptapTaskItem.vue'
|
||||
import wikiSuggestion from '~/composables/useWikiSuggestion'
|
||||
|
||||
const props = defineProps<{ modelValue: any }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const router = useRouter()
|
||||
|
||||
// Damit wir TaskItem nicht ständig neu initialisieren
|
||||
const CustomTaskItem = TaskItem.extend({
|
||||
addNodeView() {
|
||||
return VueNodeViewRenderer(TiptapTaskItem)
|
||||
},
|
||||
})
|
||||
|
||||
const editor = useEditor({
|
||||
content: props.modelValue,
|
||||
extensions: [
|
||||
StarterKit.configure({ codeBlock: false }),
|
||||
Placeholder.configure({ placeholder: 'Tippe "/" oder schreibe los...' }),
|
||||
Placeholder.configure({ placeholder: 'Tippe "/" für Befehle oder "#" für Seiten...' }),
|
||||
|
||||
BubbleMenuExtension.configure({ shouldShow: ({ editor, from, to }) => !editor.state.selection.empty && (to - from > 0) }),
|
||||
FloatingMenuExtension,
|
||||
@@ -112,35 +128,63 @@ const editor = useEditor({
|
||||
Image, Highlight, Typography, CharacterCount, CodeBlock,
|
||||
Youtube.configure({ width: 640, height: 480 }),
|
||||
|
||||
Table.configure({ resizable: true }), TableRow, TableHeader, TableCell,
|
||||
// -----------------------------------------------------------
|
||||
// WICHTIGE ÄNDERUNG: Nur Table.configure()
|
||||
// TableRow, TableHeader, TableCell wurden HIER ENTFERNT
|
||||
// -----------------------------------------------------------
|
||||
Table.configure({ resizable: true }),
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
|
||||
// TASK LIST KONFIGURATION
|
||||
// TASK LIST
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
CustomTaskItem.configure({
|
||||
nested: true,
|
||||
}).extend({
|
||||
// HIER SAGEN WIR TIPTAP: BENUTZE UNSERE VUE KOMPONENTE
|
||||
addNodeView() {
|
||||
return VueNodeViewRenderer(TiptapTaskItem)
|
||||
}),
|
||||
|
||||
// INTERNAL LINKING
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'wiki-mention',
|
||||
},
|
||||
suggestion: {
|
||||
...wikiSuggestion,
|
||||
char: '#'
|
||||
},
|
||||
|
||||
}),
|
||||
],
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'min-h-[500px] pb-40',
|
||||
},
|
||||
handleClick: (view, pos, event) => {
|
||||
const target = event.target as HTMLElement
|
||||
if (target.closest('.wiki-mention')) {
|
||||
const id = target.getAttribute('data-id')
|
||||
if (id) {
|
||||
router.push(`/wiki/${id}`)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
emit('update:modelValue', editor.getJSON())
|
||||
},
|
||||
})
|
||||
|
||||
// Sync Model -> Editor
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (editor.value && JSON.stringify(val) !== JSON.stringify(editor.value.getJSON())) {
|
||||
//@ts-ignore
|
||||
editor.value.commands.setContent(val, false)
|
||||
}
|
||||
})
|
||||
|
||||
// ACTIONS
|
||||
const setLink = () => {
|
||||
if (!editor.value) return
|
||||
const previousUrl = editor.value.getAttributes('link').href
|
||||
@@ -174,22 +218,38 @@ const addVideo = () => {
|
||||
@apply bg-gray-200 dark:bg-gray-600 text-black dark:text-white;
|
||||
}
|
||||
|
||||
/* EDITOR STYLES */
|
||||
/* GLOBAL EDITOR STYLES */
|
||||
:deep(.prose) {
|
||||
/* Cursor Fix */
|
||||
.ProseMirror {
|
||||
outline: none;
|
||||
caret-color: currentColor;
|
||||
}
|
||||
|
||||
/* LIST RESET: Wir müssen nur die UL resetten, die LI wird von Vue gerendert */
|
||||
/* LIST RESET */
|
||||
ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Table Fixes */
|
||||
/* MENTION */
|
||||
.wiki-mention {
|
||||
/* Pill-Shape, grau/neutral statt knallig blau */
|
||||
@apply bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 px-1.5 py-0.5 rounded-md text-sm font-medium no-underline inline-block mx-0.5 align-middle border border-gray-200 dark:border-gray-700;
|
||||
|
||||
box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.wiki-mention::before {
|
||||
@apply text-gray-400 dark:text-gray-500 mr-0.5;
|
||||
}
|
||||
|
||||
.wiki-mention:hover {
|
||||
@apply bg-primary-50 dark:bg-primary-900/30 border-primary-200 dark:border-primary-800 text-primary-700 dark:text-primary-400;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* TABLE */
|
||||
table { width: 100% !important; border-collapse: collapse; table-layout: fixed; margin: 1rem 0; }
|
||||
th, td { border: 1px solid #d1d5db; padding: 0.5rem; vertical-align: top; box-sizing: border-box; min-width: 1em; }
|
||||
.dark th, .dark td { border-color: #374151; }
|
||||
@@ -197,16 +257,16 @@ const addVideo = () => {
|
||||
.dark th { background-color: #1f2937; }
|
||||
.column-resize-handle { background-color: #3b82f6; width: 4px; }
|
||||
|
||||
/* Code Block */
|
||||
/* CODE */
|
||||
pre { background: #0d1117; color: #c9d1d9; font-family: 'JetBrains Mono', monospace; padding: 0.75rem 1rem; border-radius: 0.5rem; margin: 1rem 0; overflow-x: auto; }
|
||||
code { color: inherit; padding: 0; background: none; font-size: 0.9rem; }
|
||||
|
||||
/* Images */
|
||||
/* IMG */
|
||||
img { max-width: 100%; height: auto; border-radius: 6px; display: block; margin: 1rem 0; }
|
||||
|
||||
/* Misc */
|
||||
/* MISC */
|
||||
p.is-editor-empty:first-child::before { color: #9ca3af; content: attr(data-placeholder); float: left; height: 0; pointer-events: none; }
|
||||
mark { background-color: #fef08a; padding: 0.1rem 0.2rem; border-radius: 0.2rem; }
|
||||
.ProseMirror-selectednode { outline: 2px solid #3b82f6; }
|
||||
p.is-editor-empty:first-child::before { color: #9ca3af; content: attr(data-placeholder); float: left; height: 0; pointer-events: none; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user