feat: implement format toggle
Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
}
|
||||||
</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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,68 +76,84 @@ function handleFileChange(event: Event) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-toolbar density="compact" elevation="0" class="editor-toolbar">
|
<v-toolbar density="compact" elevation="0" class="editor-toolbar">
|
||||||
<v-btn
|
<template v-if="format === 'rich'">
|
||||||
icon
|
<v-btn
|
||||||
size="small"
|
icon
|
||||||
:class="{ 'v-btn--active': isBoldActive }"
|
size="small"
|
||||||
@click="$emit('bold')"
|
:class="{ 'v-btn--active': isBoldActive }"
|
||||||
>
|
@click="$emit('bold')"
|
||||||
<v-icon>mdi-format-bold</v-icon>
|
>
|
||||||
<v-tooltip activator="parent" location="bottom">Bold</v-tooltip>
|
<v-icon>mdi-format-bold</v-icon>
|
||||||
</v-btn>
|
<v-tooltip activator="parent" location="bottom">Bold</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
:class="{ 'v-btn--active': isItalicActive }"
|
||||||
|
@click="$emit('italic')"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-format-italic</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="bottom">Italic</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
:class="{ 'v-btn--active': isUnderlineActive }"
|
||||||
|
@click="$emit('underline')"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-format-underline</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="bottom">Underline</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-divider vertical class="mx-2" />
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
:class="{ 'v-btn--active': isBulletListActive }"
|
||||||
|
@click="$emit('bulletList')"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-format-list-bulleted</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="bottom">Bullet List</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
:class="{ 'v-btn--active': isOrderedListActive }"
|
||||||
|
@click="$emit('orderedList')"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-format-list-numbered</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="bottom">Numbered List</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-divider vertical class="mx-2" />
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
:class="{ 'v-btn--active': isLinkActive }"
|
||||||
|
@click="$emit('link')"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-link</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="bottom">Link</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-divider vertical class="mx-2" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
icon
|
||||||
size="small"
|
size="small"
|
||||||
:class="{ 'v-btn--active': isItalicActive }"
|
:class="{ 'v-btn--active': format === 'plain' }"
|
||||||
@click="$emit('italic')"
|
@click="$emit('toggleFormat')"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-format-italic</v-icon>
|
<v-icon>{{ format === 'plain' ? 'mdi-format-text' : 'mdi-code-tags' }}</v-icon>
|
||||||
<v-tooltip activator="parent" location="bottom">Italic</v-tooltip>
|
<v-tooltip activator="parent" location="bottom">
|
||||||
</v-btn>
|
{{ format === 'plain' ? 'Rich Text' : 'Plain Text' }}
|
||||||
|
</v-tooltip>
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
size="small"
|
|
||||||
:class="{ 'v-btn--active': isUnderlineActive }"
|
|
||||||
@click="$emit('underline')"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-format-underline</v-icon>
|
|
||||||
<v-tooltip activator="parent" location="bottom">Underline</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-divider vertical class="mx-2" />
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
size="small"
|
|
||||||
:class="{ 'v-btn--active': isBulletListActive }"
|
|
||||||
@click="$emit('bulletList')"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-format-list-bulleted</v-icon>
|
|
||||||
<v-tooltip activator="parent" location="bottom">Bullet List</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
size="small"
|
|
||||||
:class="{ 'v-btn--active': isOrderedListActive }"
|
|
||||||
@click="$emit('orderedList')"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-format-list-numbered</v-icon>
|
|
||||||
<v-tooltip activator="parent" location="bottom">Numbered List</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-divider vertical class="mx-2" />
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
size="small"
|
|
||||||
:class="{ 'v-btn--active': isLinkActive }"
|
|
||||||
@click="$emit('link')"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-link</v-icon>
|
|
||||||
<v-tooltip activator="parent" location="bottom">Link</v-tooltip>
|
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user