-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
{!! $oldCommentBody !!}
+
@can('update', $ticket)
@@ -200,32 +209,35 @@
if (insertButton) {
const form = insertButton.closest('[data-helpdesk-comment-form]');
const select = form?.querySelector('[data-canned-response-select]');
+ const editor = form?.querySelector('[data-helpdesk-rich-editor]');
const textarea = form?.querySelector('textarea[name="commentBody"]');
const selected = select?.selectedOptions?.[0];
- const body = decodeHtmlEntities(selected?.dataset?.body || '');
+ const body = normaliseCannedHtml(selected?.dataset?.body || '');
- if (textarea && body.trim() !== '') {
- textarea.value = textarea.value.trim() === ''
+ if (editor && textarea && body.trim() !== '') {
+ editor.innerHTML = editor.innerHTML.trim() === ''
? body
- : textarea.value.replace(/\s+$/, '') + "\n\n" + body;
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
- textarea.focus();
+ : editor.innerHTML.replace(/\s+$/, '') + '
' + body;
+ syncRichEditor(editor);
+ editor.focus();
}
return;
}
- const toolbarButton = event.target.closest('[data-helpdesk-tag], [data-helpdesk-wrap]');
+ const toolbarButton = event.target.closest('[data-helpdesk-command], [data-helpdesk-block], [data-helpdesk-tag], [data-helpdesk-wrap]');
if (toolbarButton) {
const toolbar = toolbarButton.closest('[data-helpdesk-html-toolbar]');
- const textarea = document.getElementById(toolbar?.dataset?.helpdeskHtmlToolbar || '');
+ const editor = document.getElementById(toolbar?.dataset?.helpdeskHtmlToolbar || '');
- if (textarea) {
+ if (editor?.matches('[contenteditable="true"]')) {
+ applyRichEditorCommand(editor, toolbarButton);
+ } else if (editor) {
const tag = toolbarButton.dataset.helpdeskTag;
const before = toolbarButton.dataset.helpdeskWrap || (tag === 'ul' ? '
- ' : `<${tag}>`);
const after = toolbarButton.dataset.helpdeskWrapEnd || (tag === 'ul' ? '
' : `${tag}>`);
- wrapSelection(textarea, before, after);
+ wrapSelection(editor, before, after);
}
return;
@@ -270,7 +282,25 @@
}
});
+ document.addEventListener('submit', function (event) {
+ const form = event.target.closest('[data-helpdesk-comment-form]');
+
+ if (! form) {
+ return;
+ }
+
+ form.querySelectorAll('[data-helpdesk-rich-editor]').forEach((editor) => {
+ syncRichEditor(editor);
+ });
+ });
+
document.addEventListener('input', function (event) {
+ const richEditor = event.target.closest('[data-helpdesk-rich-editor]');
+
+ if (richEditor) {
+ syncRichEditor(richEditor);
+ }
+
const search = event.target.closest('[data-helpdesk-comment-search]');
if (search) {
@@ -319,6 +349,72 @@
return textarea.value;
}
+ function normaliseCannedHtml(value) {
+ let decoded = decodeHtmlEntities(value);
+
+ while (decoded.includes('<') || decoded.includes('>') || decoded.includes('<')) {
+ const next = decodeHtmlEntities(decoded);
+
+ if (next === decoded) {
+ break;
+ }
+
+ decoded = next;
+ }
+
+ return decoded;
+ }
+
+ function syncRichEditor(editor) {
+ const target = document.getElementById(editor.dataset.helpdeskSyncTarget || '');
+
+ if (! target) {
+ return;
+ }
+
+ target.value = editor.innerHTML.trim();
+ target.dispatchEvent(new Event('input', { bubbles: true }));
+ target.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+
+ function applyRichEditorCommand(editor, button) {
+ editor.focus();
+
+ const command = button.dataset.helpdeskCommand;
+
+ if (command) {
+ document.execCommand(command, false, null);
+ syncRichEditor(editor);
+ return;
+ }
+
+ const block = button.dataset.helpdeskBlock;
+
+ if (block === 'blockquote') {
+ document.execCommand('formatBlock', false, 'blockquote');
+ syncRichEditor(editor);
+ return;
+ }
+
+ if (block === 'code') {
+ const selection = window.getSelection();
+ const selected = selection && selection.toString() !== '' ? selection.toString() : 'code';
+ document.execCommand('insertHTML', false, '
' + escapeHtml(selected) + '');
+ syncRichEditor(editor);
+ }
+ }
+
+ function escapeHtml(value) {
+ const span = document.createElement('span');
+ span.textContent = value;
+ return span.innerHTML;
+ }
+
+ document.addEventListener('DOMContentLoaded', function () {
+ document.querySelectorAll('[data-helpdesk-rich-editor]').forEach((editor) => syncRichEditor(editor));
+ updateCommentToggleButtons();
+ });
+
function wrapSelection(textarea, before, after) {
const start = textarea.selectionStart || 0;
const end = textarea.selectionEnd || 0;
diff --git a/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php b/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php
index 094e6237..1968ac59 100644
--- a/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php
+++ b/Modules/Helpdesk/Resources/views/partials/comment-thread.blade.php
@@ -12,21 +12,31 @@
$isInternal = (bool) $comment->is_internal;
$borderClass = $isInternal ? 'border-amber-300 dark:border-amber-700' : 'border-red-200 dark:border-red-900/70';
$railClass = $isInternal ? 'bg-amber-500' : 'bg-red-500';
- $renderedBody = (string) $comment->body;
+ $renderedBody = html_entity_decode((string) $comment->body, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+
+ while (str_contains($renderedBody, '<') || str_contains($renderedBody, '>') || str_contains($renderedBody, '<')) {
+ $decodedBody = html_entity_decode($renderedBody, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+
+ if ($decodedBody === $renderedBody) {
+ break;
+ }
+
+ $renderedBody = $decodedBody;
+ }
if ($currentTicket !== null) {
$renderedBody = strtr($renderedBody, [
- '{{ ticket.reference }}' => e((string) ($currentTicket->reference ?? '')),
- '{{ ticket.subject }}' => e((string) ($currentTicket->subject ?? '')),
- '{{ ticket.status }}' => e((string) ($currentTicket->status?->name ?? '')),
- '{{ ticket.priority }}' => e((string) ($currentTicket->priority?->name ?? '')),
- '{{ ticket.category }}' => e((string) ($currentTicket->category?->name ?? '')),
- '{{ ticket.project }}' => e((string) ($currentTicket->project?->title ?? '')),
- '{{ ticket.team }}' => e((string) ($currentTicket->team?->name ?? '')),
- '{{ requester.name }}' => e((string) ($currentTicket->owner?->name ?? $currentTicket->user?->name ?? '')),
- '{{ agent.name }}' => e((string) ($currentTicket->agent?->name ?? '')),
- '{{ actor.name }}' => e((string) ($commentUser?->name ?? '')),
- '{{ ticket.url }}' => e(route('helpdesk.tickets.show', ['ticket' => $currentTicket->getKey()])),
+ '{{ ticket.reference }}' => (string) e((string) ($currentTicket->reference ?? '')),
+ '{{ ticket.subject }}' => (string) e((string) ($currentTicket->subject ?? '')),
+ '{{ ticket.status }}' => (string) e((string) ($currentTicket->status?->name ?? '')),
+ '{{ ticket.priority }}' => (string) e((string) ($currentTicket->priority?->name ?? '')),
+ '{{ ticket.category }}' => (string) e((string) ($currentTicket->category?->name ?? '')),
+ '{{ ticket.project }}' => (string) e((string) ($currentTicket->project?->title ?? '')),
+ '{{ ticket.team }}' => (string) e((string) ($currentTicket->team?->name ?? '')),
+ '{{ requester.name }}' => (string) e((string) ($currentTicket->owner?->name ?? $currentTicket->user?->name ?? '')),
+ '{{ agent.name }}' => (string) e((string) ($currentTicket->agent?->name ?? '')),
+ '{{ actor.name }}' => (string) e((string) ($commentUser?->name ?? '')),
+ '{{ ticket.url }}' => (string) e(route('helpdesk.tickets.show', ['ticket' => $currentTicket->getKey()])),
]);
}
diff --git a/Modules/Helpdesk/Tests/Browser/HelpdeskTicketBrowserTest.php b/Modules/Helpdesk/Tests/Browser/HelpdeskTicketBrowserTest.php
index 51677615..92ea184c 100644
--- a/Modules/Helpdesk/Tests/Browser/HelpdeskTicketBrowserTest.php
+++ b/Modules/Helpdesk/Tests/Browser/HelpdeskTicketBrowserTest.php
@@ -64,7 +64,7 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase
'name' => 'Browser canned response',
'slug' => 'browser-canned-response',
'visibility' => 'agents',
- 'body' => 'Inserted canned browser response',
+ 'body' => '
Inserted canned browser response
',
'is_active' => true,
'sort_order' => 1,
]);
@@ -76,8 +76,11 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase
->assertSee('Browser response ticket')
->select('canned_response_id', (string) $cannedResponse->id)
->press('@insert-canned-response')
- ->assertInputValue('commentBody', 'Inserted canned browser response')
- ->type('@ticket-comment-editor', 'First browser response')
+ ->assertInputValue('commentBody', '
Inserted canned browser response
');
+
+ $this->setHelpdeskEditor($browser, 'ticket-comment-editor', '
First browser response
');
+
+ $browser
->press('@post-comment')
->waitForText('First browser response', 10)
->assertSee('First browser response');
@@ -85,7 +88,7 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase
$comment = Comment::query()
->where('ticket_id', $ticket->id)
->whereNull('parent_id')
- ->where('body', 'First browser response')
+ ->where('body', '
First browser response
')
->firstOrFail();
$browser->press('@reply-to-comment-'.$comment->id)
@@ -99,13 +102,13 @@ final class HelpdeskTicketBrowserTest extends DuskTestCase
$this->assertDatabaseHas('helpdesk_comments', [
'ticket_id' => $ticket->id,
'parent_id' => null,
- 'body' => 'First browser response',
+ 'body' => '
First browser response
',
]);
$parentId = Comment::query()
->where('ticket_id', $ticket->id)
->whereNull('parent_id')
- ->where('body', 'First browser response')
+ ->where('body', '
First browser response
')
->value('id');
$this->assertDatabaseHas('helpdesk_comments', [