feat: article editor core
This commit is contained in:
1 parent
d493a181e1
commit
a4734b2c91
55 files changed
+4186
-496
No files matched your search
@@ -0,0 +1,255 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { EditorToolbarItem } from '@nuxt/ui'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import { ArticleIndent } from '@/lib/articleIndent'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
articleId: number
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'request-image': []
|
||||
}>()
|
||||
|
||||
const editorInstance = ref<Editor | null>(null)
|
||||
const extensions = [ArticleIndent]
|
||||
|
||||
function captureEditor(editor: Editor) {
|
||||
editorInstance.value = editor
|
||||
return ''
|
||||
}
|
||||
|
||||
function onUpdate(value: string) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
function currentBlockLabel(editor: Editor) {
|
||||
for (let level = 1; level <= 4; level += 1) {
|
||||
if (editor.isActive('heading', { level })) {
|
||||
return `Heading ${level}`
|
||||
}
|
||||
}
|
||||
return 'Paragraph'
|
||||
}
|
||||
|
||||
function isDefaultAlignment(editor: Editor) {
|
||||
return !['center', 'right', 'justify'].some((align) =>
|
||||
editor.isActive({ textAlign: align }),
|
||||
)
|
||||
}
|
||||
|
||||
function buildItems(editor: Editor): EditorToolbarItem[][] {
|
||||
return [
|
||||
[
|
||||
{
|
||||
kind: 'undo',
|
||||
icon: 'i-lucide-undo-2',
|
||||
tooltip: { text: 'Undo' },
|
||||
},
|
||||
{
|
||||
kind: 'redo',
|
||||
icon: 'i-lucide-redo-2',
|
||||
tooltip: { text: 'Redo' },
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
icon: 'i-lucide-pilcrow',
|
||||
label: currentBlockLabel(editor),
|
||||
trailingIcon: 'i-lucide-chevron-down',
|
||||
ui: { trailingIcon: 'size-3' },
|
||||
tooltip: { text: 'Paragraph style' },
|
||||
items: [
|
||||
{
|
||||
icon: 'i-lucide-pilcrow',
|
||||
label: 'Paragraph',
|
||||
onSelect: () =>
|
||||
editor.chain().focus().setParagraph().run(),
|
||||
},
|
||||
{
|
||||
kind: 'heading',
|
||||
level: 1,
|
||||
icon: 'i-lucide-heading-1',
|
||||
label: 'Heading 1',
|
||||
tooltip: { text: 'Heading 1' },
|
||||
},
|
||||
{
|
||||
kind: 'heading',
|
||||
level: 2,
|
||||
icon: 'i-lucide-heading-2',
|
||||
label: 'Heading 2',
|
||||
tooltip: { text: 'Heading 2' },
|
||||
},
|
||||
{
|
||||
kind: 'heading',
|
||||
level: 3,
|
||||
icon: 'i-lucide-heading-3',
|
||||
label: 'Heading 3',
|
||||
tooltip: { text: 'Heading 3' },
|
||||
},
|
||||
{
|
||||
kind: 'heading',
|
||||
level: 4,
|
||||
icon: 'i-lucide-heading-4',
|
||||
label: 'Heading 4',
|
||||
tooltip: { text: 'Heading 4' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
kind: 'mark',
|
||||
mark: 'bold',
|
||||
icon: 'i-lucide-bold',
|
||||
tooltip: { text: 'Bold' },
|
||||
},
|
||||
{
|
||||
kind: 'mark',
|
||||
mark: 'italic',
|
||||
icon: 'i-lucide-italic',
|
||||
tooltip: { text: 'Italic' },
|
||||
},
|
||||
{
|
||||
kind: 'mark',
|
||||
mark: 'underline',
|
||||
icon: 'i-lucide-underline',
|
||||
tooltip: { text: 'Underline' },
|
||||
},
|
||||
{
|
||||
kind: 'mark',
|
||||
mark: 'strike',
|
||||
icon: 'i-lucide-strikethrough',
|
||||
tooltip: { text: 'Strikethrough' },
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
kind: 'textAlign',
|
||||
align: 'left',
|
||||
active: isDefaultAlignment(editor),
|
||||
icon: 'i-lucide-align-left',
|
||||
tooltip: { text: 'Align left' },
|
||||
},
|
||||
{
|
||||
kind: 'textAlign',
|
||||
align: 'center',
|
||||
icon: 'i-lucide-align-center',
|
||||
tooltip: { text: 'Align center' },
|
||||
},
|
||||
{
|
||||
kind: 'textAlign',
|
||||
align: 'right',
|
||||
icon: 'i-lucide-align-right',
|
||||
tooltip: { text: 'Align right' },
|
||||
},
|
||||
{
|
||||
kind: 'textAlign',
|
||||
align: 'justify',
|
||||
icon: 'i-lucide-align-justify',
|
||||
tooltip: { text: 'Justify' },
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
kind: 'bulletList',
|
||||
icon: 'i-lucide-list',
|
||||
tooltip: { text: 'Bullet list' },
|
||||
},
|
||||
{
|
||||
kind: 'orderedList',
|
||||
icon: 'i-lucide-list-ordered',
|
||||
tooltip: { text: 'Numbered list' },
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-indent-decrease',
|
||||
tooltip: { text: 'Outdent' },
|
||||
onClick: () => editor.chain().focus().outdent().run(),
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-indent-increase',
|
||||
tooltip: { text: 'Indent' },
|
||||
onClick: () => editor.chain().focus().indent().run(),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
kind: 'clearFormatting',
|
||||
icon: 'i-lucide-remove-formatting',
|
||||
tooltip: { text: 'Clear formatting' },
|
||||
},
|
||||
{
|
||||
icon: 'i-lucide-image-plus',
|
||||
label: 'Image',
|
||||
tooltip: { text: 'Add an image' },
|
||||
onClick: () => emit('request-image'),
|
||||
},
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focus() {
|
||||
editorInstance.value?.commands.focus()
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UEditor
|
||||
v-slot="{ editor }"
|
||||
:model-value="props.modelValue"
|
||||
:extensions="extensions"
|
||||
content-type="html"
|
||||
:editable="!props.disabled"
|
||||
:ui="{ base: 'article-content' }"
|
||||
class="article-editor grid h-full min-h-0 grid-cols-1 grid-rows-[auto_minmax(0,1fr)_auto] lg:grid-cols-[minmax(0,1fr)_17.5rem]"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<span class="hidden">{{ captureEditor(editor) }}</span>
|
||||
<UEditorToolbar
|
||||
:editor="editor"
|
||||
:items="buildItems(editor)"
|
||||
layout="fixed"
|
||||
class="col-span-full row-start-1 border-b border-default bg-default px-6 py-1.5"
|
||||
/>
|
||||
<div class="col-start-1 row-start-3">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
<div
|
||||
class="col-start-2 row-span-2 row-start-2 hidden min-h-0 lg:block"
|
||||
>
|
||||
<slot name="aside" />
|
||||
</div>
|
||||
</UEditor>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.article-editor :deep([data-slot='content']) {
|
||||
grid-column-start: 1;
|
||||
grid-row-start: 2;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 2.5rem 3rem;
|
||||
background: var(--ui-bg);
|
||||
}
|
||||
.article-editor :deep(.ProseMirror) {
|
||||
max-width: 42rem;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
@media (max-width: 47.999rem) {
|
||||
.article-editor :deep([data-slot='content']) {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user