Merge pull request #261 from shariquerik/email-reply-collapser
feat: Email Reply Collapser
This commit is contained in:
commit
f1ede153b5
@ -9,6 +9,7 @@
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/extension-paragraph": "^2.4.0",
|
||||
"@twilio/voice-sdk": "^2.10.2",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"@vueuse/integrations": "^10.3.0",
|
||||
|
||||
@ -442,7 +442,7 @@
|
||||
'outgoing_call',
|
||||
].includes(activity.activity_type),
|
||||
'bg-white': ['added', 'removed', 'changed'].includes(
|
||||
activity.activity_type
|
||||
activity.activity_type,
|
||||
),
|
||||
}"
|
||||
>
|
||||
@ -528,7 +528,10 @@
|
||||
<span v-if="activity.data.bcc">{{ activity.data.bcc }}</span>
|
||||
</div>
|
||||
<EmailContent :content="activity.data.content" />
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-if="activity.data?.attachments?.length"
|
||||
class="flex flex-wrap gap-2"
|
||||
>
|
||||
<AttachmentItem
|
||||
v-for="a in activity.data.attachments"
|
||||
:key="a.file_url"
|
||||
@ -1102,7 +1105,7 @@ const defaultActions = computed(() => {
|
||||
},
|
||||
]
|
||||
return actions.filter((action) =>
|
||||
action.condition ? action.condition() : true
|
||||
action.condition ? action.condition() : true,
|
||||
)
|
||||
})
|
||||
|
||||
@ -1120,12 +1123,12 @@ const activities = computed(() => {
|
||||
} else if (props.title == 'Emails') {
|
||||
if (!all_activities.data?.versions) return []
|
||||
activities = all_activities.data.versions.filter(
|
||||
(activity) => activity.activity_type === 'communication'
|
||||
(activity) => activity.activity_type === 'communication',
|
||||
)
|
||||
} else if (props.title == 'Comments') {
|
||||
if (!all_activities.data?.versions) return []
|
||||
activities = all_activities.data.versions.filter(
|
||||
(activity) => activity.activity_type === 'comment'
|
||||
(activity) => activity.activity_type === 'comment',
|
||||
)
|
||||
} else if (props.title == 'Calls') {
|
||||
if (!all_activities.data?.calls) return []
|
||||
@ -1338,12 +1341,15 @@ function reply(email, reply_all = false) {
|
||||
editor.bccEmails = bcc
|
||||
}
|
||||
|
||||
let repliedMessage = `<blockquote>${message}</blockquote>`
|
||||
|
||||
editor.editor
|
||||
.chain()
|
||||
.clearContent()
|
||||
.insertContent(message)
|
||||
.insertContent('<p>.</p>')
|
||||
.updateAttributes('paragraph', {class:'reply-to-content'})
|
||||
.insertContent(repliedMessage)
|
||||
.focus('all')
|
||||
.setBlockquote()
|
||||
.insertContentAt(0, { type: 'paragraph' })
|
||||
.focus('start')
|
||||
.run()
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
style="
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
black calc(100% - 30px),
|
||||
black calc(100% - 20px),
|
||||
transparent 100%
|
||||
);
|
||||
"
|
||||
@ -27,6 +27,90 @@ const files = import.meta.globEager('/src/index.css', { query: '?inline' })
|
||||
const css = files['/src/index.css'].default
|
||||
|
||||
const iframeRef = ref(null)
|
||||
const _content = ref(props.content)
|
||||
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(_content.value, 'text/html')
|
||||
|
||||
const gmailReplyToContent = doc.querySelectorAll('div.gmail_quote')
|
||||
const outlookReplyToContent = doc.querySelectorAll('div#appendonsend')
|
||||
const replyToContent = doc.querySelectorAll('p.reply-to-content')
|
||||
|
||||
if (gmailReplyToContent.length) {
|
||||
_content.value = parseReplyToContent(doc, 'div.gmail_quote', true)
|
||||
} else if (outlookReplyToContent.length) {
|
||||
_content.value = parseReplyToContent(doc, 'div#appendonsend')
|
||||
} else if (replyToContent.length) {
|
||||
_content.value = parseReplyToContent(doc, 'p.reply-to-content')
|
||||
}
|
||||
|
||||
function parseReplyToContent(doc, selector, forGmail = false) {
|
||||
function handleAllInstances(doc) {
|
||||
const replyToContentElements = doc.querySelectorAll(selector)
|
||||
if (replyToContentElements.length === 0) return
|
||||
const replyToContentElement = replyToContentElements[0]
|
||||
replaceReplyToContent(replyToContentElement, forGmail)
|
||||
handleAllInstances(doc)
|
||||
}
|
||||
|
||||
handleAllInstances(doc)
|
||||
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
|
||||
function replaceReplyToContent(replyToContentElement, forGmail) {
|
||||
if (!replyToContentElement) return
|
||||
let randomId = Math.random().toString(36).substring(2, 7)
|
||||
const wrapper = doc.createElement('div')
|
||||
wrapper.classList.add('replied-content')
|
||||
|
||||
const collapseLabel = doc.createElement('label')
|
||||
collapseLabel.classList.add('collapse')
|
||||
collapseLabel.setAttribute('for', randomId)
|
||||
collapseLabel.innerHTML = '...'
|
||||
wrapper.appendChild(collapseLabel)
|
||||
|
||||
const collapseInput = doc.createElement('input')
|
||||
collapseInput.setAttribute('id', randomId)
|
||||
collapseInput.setAttribute('class', 'replyCollapser')
|
||||
collapseInput.setAttribute('type', 'checkbox')
|
||||
wrapper.appendChild(collapseInput)
|
||||
|
||||
if (forGmail) {
|
||||
const prevSibling = replyToContentElement.previousElementSibling
|
||||
if (prevSibling && prevSibling.tagName === 'BR') {
|
||||
prevSibling.remove()
|
||||
}
|
||||
let cloned = replyToContentElement.cloneNode(true)
|
||||
cloned.classList.remove('gmail_quote')
|
||||
wrapper.appendChild(cloned)
|
||||
} else {
|
||||
const allSiblings = Array.from(replyToContentElement.parentElement.children)
|
||||
const replyToContentIndex = allSiblings.indexOf(replyToContentElement)
|
||||
const followingSiblings = allSiblings.slice(replyToContentIndex + 1)
|
||||
|
||||
if (followingSiblings.length === 0) return
|
||||
|
||||
let clonedFollowingSiblings = followingSiblings.map((sibling) =>
|
||||
sibling.cloneNode(true),
|
||||
)
|
||||
|
||||
const div = doc.createElement('div')
|
||||
div.append(...clonedFollowingSiblings)
|
||||
|
||||
wrapper.append(div)
|
||||
|
||||
// Remove all siblings after the reply-to-content element
|
||||
for (let i = replyToContentIndex + 1; i < allSiblings.length; i++) {
|
||||
replyToContentElement.parentElement.removeChild(allSiblings[i])
|
||||
}
|
||||
}
|
||||
|
||||
replyToContentElement.parentElement.replaceChild(
|
||||
wrapper,
|
||||
replyToContentElement,
|
||||
)
|
||||
}
|
||||
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
@ -35,6 +119,35 @@ const htmlContent = `
|
||||
<style>
|
||||
${css}
|
||||
|
||||
.replied-content .collapse {
|
||||
margin: 10px 0 10px 0;
|
||||
visibility: visible;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: larger;
|
||||
font-weight: 700;
|
||||
height: 12px;
|
||||
line-height: 0.1;
|
||||
background: #e8eaed;
|
||||
width: 23px;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.replied-content .collapse:hover {
|
||||
background: #dadce0;
|
||||
}
|
||||
|
||||
.replied-content .collapse + input {
|
||||
display: none;
|
||||
}
|
||||
.replied-content .collapse + input + div {
|
||||
display: none;
|
||||
}
|
||||
.replied-content .collapse + input:checked + div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
word-break: break-word;
|
||||
}
|
||||
@ -110,7 +223,7 @@ const htmlContent = `
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div ref="emailContentRef" class="email-content prose-f">${props.content}</div>
|
||||
<div ref="emailContentRef" class="email-content prose-f">${_content.value}</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
@ -120,7 +233,18 @@ watch(iframeRef, (iframe) => {
|
||||
iframe.onload = () => {
|
||||
const emailContent =
|
||||
iframe.contentWindow.document.querySelector('.email-content')
|
||||
iframe.style.height = emailContent.offsetHeight + 25 + 'px'
|
||||
let parent = emailContent.closest('html')
|
||||
|
||||
iframe.style.height = parent.offsetHeight + 'px'
|
||||
|
||||
let replyCollapsers = emailContent.querySelectorAll('.replyCollapser')
|
||||
if (replyCollapsers.length) {
|
||||
replyCollapsers.forEach((replyCollapser) => {
|
||||
replyCollapser.addEventListener('change', () => {
|
||||
iframe.style.height = parent.offsetHeight + 'px'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
<template>
|
||||
<TextEditor
|
||||
ref="textEditor"
|
||||
:editor-class="['prose-sm max-w-none', editable && 'min-h-[7rem]']"
|
||||
:editor-class="[
|
||||
'prose-sm max-w-none',
|
||||
editable && 'min-h-[7rem]',
|
||||
'[&_p.reply-to-content]:hidden',
|
||||
]"
|
||||
:content="content"
|
||||
@change="editable ? (content = $event) : null"
|
||||
:starterkit-options="{ heading: { levels: [2, 3, 4, 5, 6] } }"
|
||||
:starterkit-options="{
|
||||
heading: { levels: [2, 3, 4, 5, 6] },
|
||||
paragraph: false,
|
||||
}"
|
||||
:placeholder="placeholder"
|
||||
:editable="editable"
|
||||
:extensions="[CustomParagraph]"
|
||||
>
|
||||
<template #top>
|
||||
<div class="flex flex-col gap-3">
|
||||
@ -25,13 +33,17 @@
|
||||
:label="__('CC')"
|
||||
variant="ghost"
|
||||
@click="toggleCC()"
|
||||
:class="[cc ? '!bg-gray-300 hover:bg-gray-200' : '!text-gray-500']"
|
||||
:class="[
|
||||
cc ? '!bg-gray-300 hover:bg-gray-200' : '!text-gray-500',
|
||||
]"
|
||||
/>
|
||||
<Button
|
||||
:label="__('BCC')"
|
||||
variant="ghost"
|
||||
@click="toggleBCC()"
|
||||
:class="[bcc ? '!bg-gray-300 hover:bg-gray-200' : '!text-gray-500']"
|
||||
:class="[
|
||||
bcc ? '!bg-gray-300 hover:bg-gray-200' : '!text-gray-500',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -164,6 +176,7 @@ import MultiselectInput from '@/components/Controls/MultiselectInput.vue'
|
||||
import EmailTemplateSelectorModal from '@/components/Modals/EmailTemplateSelectorModal.vue'
|
||||
import { TextEditorBubbleMenu, TextEditor, FileUploader, call } from 'frappe-ui'
|
||||
import { validateEmail } from '@/utils'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import { EditorContent } from '@tiptap/vue-3'
|
||||
import { ref, computed, defineModel, nextTick } from 'vue'
|
||||
|
||||
@ -198,6 +211,24 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const CustomParagraph = Paragraph.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
class: {
|
||||
default: null,
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.class) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
class: `${attributes.class}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const modelValue = defineModel()
|
||||
const attachments = defineModel('attachments')
|
||||
const content = defineModel('content')
|
||||
|
||||
@ -1808,6 +1808,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.2.6.tgz#0e66d2ce21116e43fd006961c42f187ee5e5beab"
|
||||
integrity sha512-M2rM3pfzziUb7xS9x2dANCokO89okbqg5IqU4VPkZhk0Mfq9czyCatt58TYkAsE3ccsGhdTYtFBTDeKBtsHUqg==
|
||||
|
||||
"@tiptap/extension-paragraph@^2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.4.0.tgz#5b9aea8775937b327bbe6754be12ae3144fb09ff"
|
||||
integrity sha512-+yse0Ow67IRwcACd9K/CzBcxlpr9OFnmf0x9uqpaWt1eHck1sJnti6jrw5DVVkyEBHDh/cnkkV49gvctT/NyCw==
|
||||
|
||||
"@tiptap/extension-placeholder@^2.0.3":
|
||||
version "2.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.2.6.tgz#7cb63e398a5301d1e132d4145daef3acb87e7dc5"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user