feat(fullstack): Replace TITLE_BODY element with rich text editor

This commit is contained in:
2025-12-21 17:59:53 +01:00
parent 54570fae4f
commit 374c8d8905
11 changed files with 1838 additions and 1054 deletions

View File

@@ -4,7 +4,7 @@
:key="getElementKey(formElementItem.formElement, formElementItem.indexInSubsection)"
>
<div class="group flex py-3 lg:py-4">
<div class="flex-auto">
<div class="flex-auto min-w-0">
<p v-if="formElementItem.formElement.title" class="font-semibold">{{ formElementItem.formElement.title }}</p>
<p v-if="formElementItem.formElement.description" class="text-dimmed pb-3">
{{ formElementItem.formElement.description }}
@@ -52,7 +52,7 @@
"
:content="{ align: 'end' }"
@update:open="
(isOpen) =>
(isOpen: boolean) =>
handleDropdownToggle(
getElementKey(formElementItem.formElement, formElementItem.indexInSubsection),
isOpen
@@ -134,8 +134,8 @@ function getResolvedComponent(formElement: FormElementDto) {
return resolveComponent('TheInput')
case 'TEXTAREA':
return resolveComponent('TheTextarea')
case 'TITLE_BODY_TEXTFIELDS':
return resolveComponent('TheTitleBodyInput')
case 'RICH_TEXT':
return resolveComponent('TheEditor')
case 'DATE':
return resolveComponent('TheDate')
default:

View File

@@ -0,0 +1,106 @@
<template>
<div class="bg-white dark:bg-white rounded-md border border-gray-200 dark:border-gray-200 overflow-hidden">
<UEditor
v-slot="{ editor }"
v-model="htmlContent"
content-type="html"
:editable="!props.disabled"
:placeholder="t('applicationForms.formElements.richTextPlaceholder')"
:ui="{
content: 'bg-white dark:bg-white',
base: 'min-h-[200px] p-3 bg-white dark:bg-white'
}"
class="w-full"
>
<UEditorToolbar
:editor="editor"
:items="toolbarItems"
class="border-b border-muted sticky top-0 inset-x-0 px-3 py-2 z-50 bg-default overflow-x-auto"
/>
<UEditorSuggestionMenu :editor="editor" :items="suggestionItems" />
<UEditorMentionMenu :editor="editor" :items="mentionItems" />
<UEditorDragHandle :editor="editor" />
</UEditor>
</div>
</template>
<script setup lang="ts">
import { EmployeeDataCategory, ProcessingPurpose, type FormOptionDto } from '~~/.api-client'
const { t } = useI18n()
const props = defineProps<{
formOptions: FormOptionDto[]
disabled?: boolean
}>()
const emit = defineEmits<{
(e: 'update:formOptions', value: FormOptionDto[]): void
}>()
const htmlContent = computed({
get: () => props.formOptions[0]?.value ?? '',
set: (newValue: string) => {
const updatedOptions: FormOptionDto[] = [...props.formOptions]
if (updatedOptions.length === 0) {
const createdOption: FormOptionDto = {
value: newValue,
label: '',
processingPurpose: ProcessingPurpose.None,
employeeDataCategory: EmployeeDataCategory.None
}
updatedOptions.push(createdOption)
} else {
const firstOption = updatedOptions[0]!
updatedOptions[0] = { ...firstOption, value: newValue }
}
emit('update:formOptions', updatedOptions)
}
})
const toolbarItems = [
[
{ kind: 'undo', icon: 'i-lucide-undo' },
{ kind: 'redo', icon: 'i-lucide-redo' }
],
[
{ kind: 'heading', level: 1, icon: 'i-lucide-heading-1', label: 'H1' },
{ kind: 'heading', level: 2, icon: 'i-lucide-heading-2', label: 'H2' },
{ kind: 'heading', level: 3, icon: 'i-lucide-heading-3', label: 'H3' }
],
[
{ kind: 'mark', mark: 'bold', icon: 'i-lucide-bold' },
{ kind: 'mark', mark: 'italic', icon: 'i-lucide-italic' },
{ kind: 'mark', mark: 'underline', icon: 'i-lucide-underline' },
{ kind: 'mark', mark: 'strike', icon: 'i-lucide-strikethrough' }
],
[
{ kind: 'bulletList', icon: 'i-lucide-list' },
{ kind: 'orderedList', icon: 'i-lucide-list-ordered' }
],
[
{ kind: 'blockquote', icon: 'i-lucide-quote' },
{ kind: 'codeBlock', icon: 'i-lucide-code' }
],
[{ kind: 'link', icon: 'i-lucide-link' }]
]
const suggestionItems = [
[
{ kind: 'heading', level: 1, label: 'Überschrift 1', icon: 'i-lucide-heading-1' },
{ kind: 'heading', level: 2, label: 'Überschrift 2', icon: 'i-lucide-heading-2' },
{ kind: 'heading', level: 3, label: 'Überschrift 3', icon: 'i-lucide-heading-3' }
],
[
{ kind: 'bulletList', label: 'Aufzählung', icon: 'i-lucide-list' },
{ kind: 'orderedList', label: 'Nummerierung', icon: 'i-lucide-list-ordered' }
],
[
{ kind: 'blockquote', label: 'Zitat', icon: 'i-lucide-quote' },
{ kind: 'codeBlock', label: 'Code Block', icon: 'i-lucide-code' },
{ kind: 'horizontalRule', label: 'Trennlinie', icon: 'i-lucide-separator-horizontal' }
]
]
const mentionItems: Array<{ label: string; avatar?: { src: string } }> = []
</script>

View File

@@ -1,72 +0,0 @@
<template>
<UFormField :label="$t('applicationForms.formElements.title')">
<UInput v-model="title" class="w-full" :disabled="props.disabled" />
</UFormField>
<UFormField :label="$t('applicationForms.formElements.text')">
<UTextarea v-model="body" class="w-full" autoresize :disabled="props.disabled" />
</UFormField>
</template>
<script setup lang="ts">
import type { FormOptionDto } from '~~/.api-client'
const props = defineProps<{
formOptions: FormOptionDto[]
disabled?: boolean
}>()
const emit = defineEmits<{
(e: 'update:formOptions', value: FormOptionDto[]): void
}>()
const SEPARATOR = '|||'
const title = computed({
get: () => {
const currentValue = props.formOptions[0]?.value ?? ''
return splitValue(currentValue).title
},
set: (newTitle: string) => {
const firstOption = props.formOptions[0]
if (firstOption) {
const currentValue = firstOption.value ?? ''
const { body: currentBody } = splitValue(currentValue)
const combinedValue = joinValue(newTitle, currentBody)
const updatedModelValue = [...props.formOptions]
updatedModelValue[0] = { ...firstOption, value: combinedValue }
emit('update:formOptions', updatedModelValue)
}
}
})
const body = computed({
get: () => {
const currentValue = props.formOptions[0]?.value ?? ''
return splitValue(currentValue).body
},
set: (newBody: string) => {
const firstOption = props.formOptions[0]
if (firstOption) {
const currentValue = firstOption.value ?? ''
const { title: currentTitle } = splitValue(currentValue)
const combinedValue = joinValue(currentTitle, newBody)
const updatedModelValue = [...props.formOptions]
updatedModelValue[0] = { ...firstOption, value: combinedValue }
emit('update:formOptions', updatedModelValue)
}
}
})
function splitValue(value: string): { title: string; body: string } {
const parts = value.split(SEPARATOR)
return {
title: parts[0] || '',
body: parts[1] || ''
}
}
function joinValue(title: string, body: string): string {
return `${title}${SEPARATOR}${body}`
}
</script>

View File

@@ -14,13 +14,13 @@ export function useFormElementManagement() {
description: 'Bitte fügen Sie hier Ihre Ergänzungen ein.',
options: [
{
value: '|||',
value: '',
label: '',
processingPurpose: 'NONE',
employeeDataCategory: 'NONE'
}
],
type: 'TITLE_BODY_TEXTFIELDS'
type: 'RICH_TEXT'
}
}