feat: implement format toggle

Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
2026-08-28 22:29:22 -04:00
parent 45cb0bd817
commit 704e01fece
3 changed files with 155 additions and 58 deletions
+53
View File
@@ -12,6 +12,7 @@ import type { CollectionObject, MessageAddressObject } from '@MailManager/models
import { useMailOperationsStore } from '@/stores/mailOperationsStore' import { useMailOperationsStore } from '@/stores/mailOperationsStore'
import { useMailCompositionStore } from '@/stores/mailCompositionStore' import { useMailCompositionStore } from '@/stores/mailCompositionStore'
import { ComposerMode } from '@/types/composer' import { ComposerMode } from '@/types/composer'
import type { ComposerFormat } from '@/types/composer'
import { documentsSelectorComponent } from '@/services/documentsSelector' import { documentsSelectorComponent } from '@/services/documentsSelector'
import ComposerToolbar from '@/components/composer/ComposerToolbar.vue' import ComposerToolbar from '@/components/composer/ComposerToolbar.vue'
import ComposerSender from '@/components/composer/ComposerSender.vue' import ComposerSender from '@/components/composer/ComposerSender.vue'
@@ -48,6 +49,8 @@ const {
// State // State
const showCc = ref(false) const showCc = ref(false)
const showBcc = ref(false) const showBcc = ref(false)
const format = ref<ComposerFormat>('rich')
const textEdited = ref(false)
const applyingDraftToEditor = ref(false) const applyingDraftToEditor = ref(false)
const showDocumentSelector = ref(false) const showDocumentSelector = ref(false)
const showCloseDialog = ref(false) const showCloseDialog = ref(false)
@@ -85,6 +88,14 @@ const subject = computed({
const attachments = computed(() => activeDraft.value?.attachments ?? {}) const attachments = computed(() => activeDraft.value?.attachments ?? {})
const text = computed({
get: () => activeDraft.value?.message.body.text ?? '',
set: value => {
textEdited.value = true
compositionStore.updateBody({ text: value, html: textToHtml(value) })
},
})
// Initialize Tiptap editor // Initialize Tiptap editor
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
@@ -118,6 +129,8 @@ watch(
compositionStore.openDraft(props.mode, props.source) compositionStore.openDraft(props.mode, props.source)
showCc.value = (activeDraft.value?.message.cc.length ?? 0) > 0 showCc.value = (activeDraft.value?.message.cc.length ?? 0) > 0
showBcc.value = (activeDraft.value?.message.bcc.length ?? 0) > 0 showBcc.value = (activeDraft.value?.message.bcc.length ?? 0) > 0
format.value = 'rich'
textEdited.value = false
applyingDraftToEditor.value = true applyingDraftToEditor.value = true
currentEditor.commands.setContent(activeDraft.value?.message.body.html || '') currentEditor.commands.setContent(activeDraft.value?.message.body.html || '')
@@ -249,6 +262,42 @@ const toggleLink = () => {
const isActive = (name: string, attrs?: any) => { const isActive = (name: string, attrs?: any) => {
return editor.value?.isActive(name, attrs) || false return editor.value?.isActive(name, attrs) || false
} }
const toggleFormat = () => {
if (format.value === 'rich') {
if (editor.value) {
compositionStore.updateBody({
html: activeDraft.value?.message.body.html ?? '',
text: editor.value.getText(),
})
}
textEdited.value = false
format.value = 'plain'
return
}
format.value = 'rich'
if (textEdited.value && editor.value) {
applyingDraftToEditor.value = true
editor.value.commands.setContent(activeDraft.value?.message.body.html || '')
applyingDraftToEditor.value = false
}
}
function textToHtml(text: string): string {
if (!text) {
return ''
}
return text
.split(/\r?\n/)
.map(line => `<p>${escapeHtml(line) || '<br>'}</p>`)
.join('')
}
function escapeHtml(value: string): string {
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
</script> </script>
<template> <template>
@@ -297,6 +346,8 @@ const isActive = (name: string, attrs?: any) => {
<ComposerEditor <ComposerEditor
:editor="editor" :editor="editor"
:format="format"
:text="text"
:is-bold-active="isActive('bold')" :is-bold-active="isActive('bold')"
:is-italic-active="isActive('italic')" :is-italic-active="isActive('italic')"
:is-underline-active="isActive('underline')" :is-underline-active="isActive('underline')"
@@ -309,6 +360,8 @@ const isActive = (name: string, attrs?: any) => {
@bullet-list="toggleBulletList" @bullet-list="toggleBulletList"
@ordered-list="toggleOrderedList" @ordered-list="toggleOrderedList"
@link="toggleLink" @link="toggleLink"
@toggle-format="toggleFormat"
@update:text="text = $event"
@attach-from-device="handleAttachFromDevice" @attach-from-device="handleAttachFromDevice"
@attach-from-documents="handleAttachFromDocuments" @attach-from-documents="handleAttachFromDocuments"
/> />
+42 -1
View File
@@ -9,6 +9,14 @@ defineProps({
type: Object as PropType<Editor | null>, type: Object as PropType<Editor | null>,
default: null, default: null,
}, },
format: {
type: String as PropType<'rich' | 'plain'>,
required: true,
},
text: {
type: String,
default: '',
},
isBoldActive: { isBoldActive: {
type: Boolean, type: Boolean,
required: true, required: true,
@@ -42,6 +50,8 @@ const emit = defineEmits<{
bulletList: [] bulletList: []
orderedList: [] orderedList: []
link: [] link: []
toggleFormat: []
'update:text': [value: string]
attachFromDevice: [files: File[]] attachFromDevice: [files: File[]]
attachFromDocuments: [] attachFromDocuments: []
}>() }>()
@@ -66,6 +76,7 @@ function handleFileChange(event: Event) {
<template> <template>
<v-toolbar density="compact" elevation="0" class="editor-toolbar"> <v-toolbar density="compact" elevation="0" class="editor-toolbar">
<template v-if="format === 'rich'">
<v-btn <v-btn
icon icon
size="small" size="small"
@@ -130,6 +141,21 @@ function handleFileChange(event: Event) {
<v-tooltip activator="parent" location="bottom">Link</v-tooltip> <v-tooltip activator="parent" location="bottom">Link</v-tooltip>
</v-btn> </v-btn>
<v-divider vertical class="mx-2" />
</template>
<v-btn
icon
size="small"
:class="{ 'v-btn--active': format === 'plain' }"
@click="$emit('toggleFormat')"
>
<v-icon>{{ format === 'plain' ? 'mdi-format-text' : 'mdi-code-tags' }}</v-icon>
<v-tooltip activator="parent" location="bottom">
{{ format === 'plain' ? 'Rich Text' : 'Plain Text' }}
</v-tooltip>
</v-btn>
<v-spacer /> <v-spacer />
<input <input
@@ -154,7 +180,17 @@ function handleFileChange(event: Event) {
<v-divider /> <v-divider />
<div class="editor-container"> <div class="editor-container">
<EditorContent :editor="editor ?? undefined" /> <EditorContent v-if="format === 'rich'" :editor="editor ?? undefined" />
<v-textarea
v-else
:model-value="text"
variant="plain"
auto-grow
hide-details
placeholder="Write your message..."
class="text-editor pa-4"
@update:model-value="$emit('update:text', $event)"
/>
</div> </div>
</template> </template>
@@ -179,6 +215,11 @@ function handleFileChange(event: Event) {
color: rgb(var(--v-theme-primary)); color: rgb(var(--v-theme-primary));
} }
.text-editor :deep(textarea) {
min-height: 300px;
font-family: inherit;
}
:deep(.tiptap-editor) { :deep(.tiptap-editor) {
outline: none; outline: none;
min-height: 300px; min-height: 300px;
+3
View File
@@ -9,6 +9,9 @@ export enum ComposerMode {
Forward = 'forward', Forward = 'forward',
} }
/** Body editing format: rich (Tiptap/HTML) or plain text. */
export type ComposerFormat = 'rich' | 'plain'
export interface ComposerDraftAttachment { export interface ComposerDraftAttachment {
identifier: string identifier: string
composition: string composition: string