22 <TextEditor
33 ref =" editorRef"
44 :editor-class =" [
5- 'prose-sm max-w-full mx-6 md:mx-10 max-h-[50vh] py-3',
6- 'min-h-[7rem]',
5+ 'prose-sm max-w-full mx-6 md:mx-10 py-3',
76 getFontFamily(newEmail),
8- editable && '!max-h-[35vh] overflow-y-auto',
97 '[& _p.reply-to-content]:hidden',
108 ]"
119 :content =" newEmail"
1513 @change =" editable ? (newEmail = $event) : null"
1614 :extensions =" [ComponentUtils, HandleExcelPaste]"
1715 :uploadFunction =" (file:any)=>uploadFunction(file, doctype, ticketId)"
16+ @keydown.capture.stop =" handleKeydown"
1817 >
1918 <template #top >
2019 <div class =" mx-6 md:mx-10 flex items-center gap-2 border-y py-2.5" >
6564 />
6665 </div >
6766 </template >
68- <!-- <template v-slot:editor="{ _editor }">
69- <EditorContent
70- :class="[editable && 'max-h-[35vh] overflow-y-auto']"
71- :editor="_editor"
72- />
73- </template> -->
67+
68+ <template #editor >
69+ <div class =" overflow-y-auto min-h-[7rem] max-h-[30vh]" >
70+ <EditorContent :editor =" editor" />
71+ <div
72+ v-if =" quotedContent"
73+ ref =" quotedContentRef"
74+ contenteditable =" true"
75+ class =" prose !max-w-full mx-6 md:mx-10 my-2 border-l-4 border-gray-300 pl-4 text-sm focus:outline-none"
76+ @input =" onQuotedInput"
77+ />
78+ </div >
79+ </template >
7480 <template #bottom >
7581 <!-- Attachments -->
7682 <div class =" flex flex-wrap gap-2 px-10" >
165171<script setup lang="ts">
166172import {
167173 AttachmentItem ,
168- SavedRepliesSelectorModal ,
169174 MultiSelectInput ,
175+ SavedRepliesSelectorModal ,
170176} from " @/components" ;
177+ import { EditorContent } from " @tiptap/vue-3" ;
171178import { AttachmentIcon } from " @/components/icons" ;
172179import { useTyping } from " @/composables/realtime" ;
173180import { useAuthStore } from " @/stores/auth" ;
@@ -180,7 +187,6 @@ import {
180187 uploadFunction ,
181188 validateEmailWithZod ,
182189} from " @/utils" ;
183- // import { EditorContent } from "@tiptap/vue-3";
184190import { useStorage } from " @vueuse/core" ;
185191import {
186192 FileUploader ,
@@ -190,11 +196,19 @@ import {
190196 toast ,
191197} from " frappe-ui" ;
192198import { useOnboarding } from " frappe-ui/frappe" ;
193- import { computed , nextTick , onBeforeUnmount , ref , watch } from " vue" ;
199+ import {
200+ computed ,
201+ nextTick ,
202+ onBeforeUnmount ,
203+ onMounted ,
204+ ref ,
205+ watch ,
206+ } from " vue" ;
194207import SavedReplyIcon from " ./icons/SavedReplyIcon.vue" ;
195208
196209const editorRef = ref (null );
197210const showSavedRepliesSelectorModal = ref (false );
211+ const quotedContentRef = ref <HTMLElement | null >(null );
198212
199213const props = defineProps ({
200214 ticketId: {
@@ -241,6 +255,12 @@ const newEmail = useStorage<null | string>(
241255 " emailBoxContent" + props .ticketId ,
242256 null
243257);
258+
259+ const quotedContent = useStorage <null | string >(
260+ " quotedEmailBoxContent" + props .ticketId ,
261+ null
262+ );
263+
244264const { updateOnboardingStep } = useOnboarding (" helpdesk" );
245265const { isManager } = useAuthStore ();
246266
@@ -249,10 +269,11 @@ const { onUserType, cleanup } = useTyping(props.ticketId);
249269
250270const attachments = ref ([]);
251271const isUploading = ref (false );
252-
253272const isDisabled = computed (() => {
254273 return (
255- isContentEmpty (newEmail .value ) || sendMail .loading || isUploading .value
274+ (isContentEmpty (newEmail .value ) && isContentEmpty (quotedContent .value )) ||
275+ sendMail .loading ||
276+ isUploading .value
256277 );
257278});
258279
@@ -295,7 +316,11 @@ const sendMail = createResource({
295316 to: toEmailsClone .value .join (" ," ),
296317 cc: ccEmailsClone .value ?.join (" ," ),
297318 bcc: bccEmailsClone .value ?.join (" ," ),
298- message: newEmail .value ,
319+ message:
320+ newEmail .value +
321+ (quotedContentRef .value
322+ ? ` <p class="reply-to-content"><p><blockquote>${quotedContentRef .value .innerHTML }</blockquote> `
323+ : " " ),
299324 },
300325 }),
301326 onSuccess : () => {
@@ -310,7 +335,7 @@ const sendMail = createResource({
310335});
311336
312337function submitMail() {
313- if (isContentEmpty (newEmail .value )) {
338+ if (isContentEmpty (newEmail .value ) && isContentEmpty ( quotedContent . value ) ) {
314339 return false ;
315340 }
316341 if (! toEmailsClone .value .length ) {
@@ -323,6 +348,21 @@ function submitMail() {
323348 sendMail .submit ();
324349}
325350
351+ watch (quotedContent , (newVal , oldVal ) => {
352+ if (! oldVal && newVal ) {
353+ nextTick (() => {
354+ if (quotedContentRef .value ) {
355+ quotedContentRef .value .innerHTML = newVal ;
356+ }
357+ });
358+ }
359+ });
360+ function onQuotedInput() {
361+ const el = quotedContentRef .value ;
362+ if (! el ) return ;
363+ quotedContent .value = el .innerHTML || null ;
364+ }
365+
326366function toggleCC() {
327367 showCC .value = ! showCC .value ;
328368
@@ -354,15 +394,16 @@ function addToReply(
354394 toEmailsClone .value = toEmails ;
355395 ccEmailsClone .value = ccEmails ;
356396 bccEmailsClone .value = bccEmails ;
357- const repliedMessage = ` <p class="reply-to-content"><p><blockquote>${body }</blockquote> ` ;
358- editorRef .value .editor
359- .chain ()
360- .clearContent ()
361- .insertContent (repliedMessage )
362- .focus (" all" )
363- .insertContentAt (0 , { type: " paragraph" })
364- .focus (" start" )
365- .run ();
397+
398+ if (body !== quotedContent .value ) {
399+ // trigger change for watch when replied to body data is different from current quoted content
400+ quotedContent .value = null ;
401+ nextTick (() => {
402+ quotedContent .value = body ;
403+ });
404+ }
405+
406+ editorRef .value .editor .chain ().clearContent ().focus (" start" ).run ();
366407 nextTick (() => {
367408 newEmail .value = editorRef .value .editor .getHTML ();
368409 });
@@ -371,21 +412,94 @@ function addToReply(
371412function resetState() {
372413 newEmail .value = null ;
373414 attachments .value = [];
415+ quotedContent .value = null ;
374416}
375417
376418function handleDiscard() {
377419 attachments .value = [];
378420 newEmail .value = null ;
379-
421+ quotedContent . value = null ;
380422 ccEmailsClone .value = [];
381423 bccEmailsClone .value = [];
382- ccEmailsClone .value = [];
383424 showCC .value = false ;
384425 showBCC .value = false ;
385426
386427 emit (" discard" );
387428}
388429
430+ // on load set quoted content from storage
431+ onMounted (() => {
432+ if (quotedContent .value ) {
433+ nextTick (() => {
434+ if (quotedContentRef .value ) {
435+ quotedContentRef .value .innerHTML = quotedContent .value ;
436+ }
437+ });
438+ }
439+ });
440+
441+ function handleSelectAll(e : KeyboardEvent ) {
442+ const active = document .activeElement ;
443+ const editorDom = editorRef .value ?.editor ?.view ?.dom as
444+ | HTMLElement
445+ | undefined ;
446+ const quotedEl = quotedContentRef .value ;
447+ const sel = window .getSelection ();
448+ if (! sel || ! editorDom ) return ;
449+ if (! editorDom .contains (active ) && ! (quotedEl && quotedEl .contains (active ))) {
450+ return ;
451+ }
452+ e .preventDefault ();
453+ sel .removeAllRanges ();
454+ const range = document .createRange ();
455+
456+ if (quotedEl ) {
457+ range .setStartBefore (editorDom );
458+ range .setEndAfter (quotedEl );
459+ } else {
460+ range .selectNodeContents (editorDom );
461+ }
462+ sel .addRange (range );
463+ }
464+
465+ function handleDelete(e : KeyboardEvent ) {
466+ const sel = window .getSelection ();
467+ const quotedEl = quotedContentRef .value ;
468+ const editorDom = editorRef .value ?.editor ?.view ?.dom as
469+ | HTMLElement
470+ | undefined ;
471+
472+ if (! sel || sel .isCollapsed || ! quotedEl || ! editorDom ) return ;
473+
474+ const isSelectingEntireEditor = sel .containsNode (editorDom , true );
475+
476+ const isSelectingEntireQuote = sel .containsNode (quotedEl , true );
477+
478+ if (isSelectingEntireEditor && isSelectingEntireQuote ) {
479+ e .preventDefault ();
480+
481+ editorRef .value ?.editor ?.commands ?.clearContent ();
482+ newEmail .value = null ;
483+ quotedContent .value = null ;
484+
485+ sel .removeAllRanges ();
486+ }
487+ }
488+
489+ function handleKeydown(e : KeyboardEvent ) {
490+ const key = e .key .toLowerCase ();
491+
492+ if ((e .metaKey || e .ctrlKey ) && key === " a" ) {
493+ handleSelectAll (e );
494+ return ;
495+ }
496+
497+ if (key === " backspace" || key === " delete" ) {
498+ handleDelete (e );
499+ return ;
500+ }
501+ }
502+
389503const editor = computed (() => {
390504 return editorRef .value .editor ;
391505});
0 commit comments