tag rendering pass one
This commit is contained in:
@@ -84,6 +84,7 @@ final class TicketCommentStoreController extends Controller
|
||||
*/
|
||||
private function normaliseHtmlBody(string $body): string
|
||||
{
|
||||
$body = html_entity_decode($body, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$body = preg_replace('/<\s*(script|style)[^>]*>.*?<\s*\/\s*\1\s*>/is', '', $body) ?: '';
|
||||
$body = preg_replace('/\s+on[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $body) ?: '';
|
||||
$body = preg_replace('/javascript\s*:/i', '', $body) ?: '';
|
||||
|
||||
@@ -153,17 +153,26 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$oldCommentBody = html_entity_decode((string) old('commentBody'), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$oldCommentBody = preg_replace('/<\s*(script|style)[^>]*>.*?<\s*\/\s*\1\s*>/is', '', $oldCommentBody) ?: '';
|
||||
$oldCommentBody = preg_replace('/\s+on[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $oldCommentBody) ?: '';
|
||||
$oldCommentBody = preg_replace('/javascript\s*:/i', '', $oldCommentBody) ?: '';
|
||||
$oldCommentBody = strip_tags($oldCommentBody, '<p><br><strong><b><em><i><u><ul><ol><li><blockquote><pre><code><a>');
|
||||
@endphp
|
||||
|
||||
<div>
|
||||
<label for="ticket-comment-body" class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-200">Comment</label>
|
||||
<div class="mb-2 flex flex-wrap gap-2" data-helpdesk-html-toolbar="ticket-comment-body">
|
||||
<button type="button" data-helpdesk-tag="strong" class="rounded-lg border px-2 py-1 text-xs font-black">B</button>
|
||||
<button type="button" data-helpdesk-tag="em" class="rounded-lg border px-2 py-1 text-xs italic">I</button>
|
||||
<button type="button" data-helpdesk-tag="u" class="rounded-lg border px-2 py-1 text-xs underline">U</button>
|
||||
<button type="button" data-helpdesk-tag="ul" class="rounded-lg border px-2 py-1 text-xs">List</button>
|
||||
<button type="button" data-helpdesk-wrap="<blockquote>" data-helpdesk-wrap-end="</blockquote>" class="rounded-lg border px-2 py-1 text-xs">Quote</button>
|
||||
<button type="button" data-helpdesk-wrap="<code>" data-helpdesk-wrap-end="</code>" class="rounded-lg border px-2 py-1 text-xs">Code</button>
|
||||
<label for="ticket-comment-body-editor" class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-200">Comment</label>
|
||||
<div class="mb-2 flex flex-wrap gap-2" data-helpdesk-html-toolbar="ticket-comment-body-editor">
|
||||
<button type="button" data-helpdesk-command="bold" class="rounded-lg border px-2 py-1 text-xs font-black">B</button>
|
||||
<button type="button" data-helpdesk-command="italic" class="rounded-lg border px-2 py-1 text-xs italic">I</button>
|
||||
<button type="button" data-helpdesk-command="underline" class="rounded-lg border px-2 py-1 text-xs underline">U</button>
|
||||
<button type="button" data-helpdesk-command="insertUnorderedList" class="rounded-lg border px-2 py-1 text-xs">List</button>
|
||||
<button type="button" data-helpdesk-block="blockquote" class="rounded-lg border px-2 py-1 text-xs">Quote</button>
|
||||
<button type="button" data-helpdesk-block="code" class="rounded-lg border px-2 py-1 text-xs">Code</button>
|
||||
</div>
|
||||
<textarea id="ticket-comment-body" dusk="ticket-comment-editor" name="commentBody" rows="6" placeholder="Write a professional HTML response..." required class="block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm text-zinc-900 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">{{ old('commentBody') }}</textarea>
|
||||
<div id="ticket-comment-body-editor" dusk="ticket-comment-editor" contenteditable="true" role="textbox" aria-multiline="true" data-helpdesk-rich-editor data-helpdesk-sync-target="ticket-comment-body" class="min-h-40 block w-full rounded-xl border border-zinc-300 bg-white p-3 text-sm text-zinc-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 prose prose-sm max-w-none dark:prose-invert">{!! $oldCommentBody !!}</div>
|
||||
<textarea id="ticket-comment-body" name="commentBody" required class="sr-only" tabindex="-1" aria-hidden="true">{{ $oldCommentBody }}</textarea>
|
||||
</div>
|
||||
|
||||
@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+$/, '') + '<br><br>' + 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' ? '<ul><li>' : `<${tag}>`);
|
||||
const after = toolbarButton.dataset.helpdeskWrapEnd || (tag === 'ul' ? '</li></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('&lt;')) {
|
||||
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, '<code>' + escapeHtml(selected) + '</code>');
|
||||
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;
|
||||
|
||||
@@ -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, '&lt;')) {
|
||||
$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()])),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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' => '<p>Inserted canned browser response</p>',
|
||||
'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', '<p>Inserted canned browser response</p>');
|
||||
|
||||
$this->setHelpdeskEditor($browser, 'ticket-comment-editor', '<p>First browser response</p>');
|
||||
|
||||
$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', '<p>First browser response</p>')
|
||||
->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' => '<p>First browser response</p>',
|
||||
]);
|
||||
|
||||
$parentId = Comment::query()
|
||||
->where('ticket_id', $ticket->id)
|
||||
->whereNull('parent_id')
|
||||
->where('body', 'First browser response')
|
||||
->where('body', '<p>First browser response</p>')
|
||||
->value('id');
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_comments', [
|
||||
|
||||
Reference in New Issue
Block a user