|
|
|
@ -117,9 +117,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="w-full flex flex-col gap-3">
|
|
|
|
|
<div class="w-full rounded-md text-black" x-data="app()" x-init="init($refs.wysiwyg)">
|
|
|
|
|
<div class="border border-gray-200 overflow-hidden rounded-md">
|
|
|
|
|
<div class="w-full border-b border-gray-200 flex flex-wrap text-xl text-gray-600">
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
|
|
|
|
|
@click="format('bold')">
|
|
|
|
|
<i class="mdi mdi-format-bold"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
|
|
|
|
|
@click="format('italic')">
|
|
|
|
|
<i class="mdi mdi-format-italic"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
|
|
|
|
|
@click="format('underline')">
|
|
|
|
|
<i class="mdi mdi-format-underline"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
|
|
|
|
|
@click="format('formatBlock','P')">
|
|
|
|
|
<i class="mdi mdi-format-paragraph"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
|
|
|
|
|
@click="format('formatBlock','H1')">
|
|
|
|
|
<i class="mdi mdi-format-header-1"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
|
|
|
|
|
@click="format('formatBlock','H2')">
|
|
|
|
|
<i class="mdi mdi-format-header-2"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
|
|
|
|
|
@click="format('formatBlock','H3')">
|
|
|
|
|
<i class="mdi mdi-format-header-3"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
|
|
|
|
|
@click="format('insertUnorderedList')">
|
|
|
|
|
<i class="mdi mdi-format-list-bulleted"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
|
|
|
|
|
@click="format('insertOrderedList')">
|
|
|
|
|
<i class="mdi mdi-format-list-numbered"></i>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
|
|
|
|
|
id="rightToLeft" @click="setDirection('rtl')">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
|
|
|
|
class="w-[18px] h-[18px]" fill="none">
|
|
|
|
|
<path d="M15 3.5H21" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
stroke-linecap="round" stroke-linejoin="round" />
|
|
|
|
|
<path d="M15 9.5H21" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
stroke-linecap="round" stroke-linejoin="round" />
|
|
|
|
|
<path d="M3 15.5H21" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
stroke-linecap="round" stroke-linejoin="round" />
|
|
|
|
|
<path d="M3 21.5H21" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
stroke-linecap="round" stroke-linejoin="round" />
|
|
|
|
|
<path
|
|
|
|
|
d="M3.58579 9.91421C4.17157 10.5 5.11438 10.5 7 10.5C8.88562 10.5 9.82843 10.5 10.4142 9.91421C11 9.32843 11 8.38562 11 6.5C11 4.61438 11 3.67157 10.4142 3.08579C9.82843 2.5 8.88562 2.5 7 2.5C5.11438 2.5 4.17157 2.5 3.58579 3.08579C3 3.67157 3 4.61438 3 6.5C3 8.38562 3 9.32843 3.58579 9.91421Z"
|
|
|
|
|
stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
|
|
|
|
stroke-linejoin="round" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button type="button"
|
|
|
|
|
class="w-[40px] h-[40px] border-r border-gray-200 flex justify-center items-center text-gray-400 duration-300 cursor-pointer hover:text-customGold"
|
|
|
|
|
id="leftToLeft" @click="setDirection('ltr')">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
|
|
|
|
class="w-[18px] h-[18px]" fill="none">
|
|
|
|
|
<path d="M3 3.5H9" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
stroke-linecap="round" stroke-linejoin="round" />
|
|
|
|
|
<path d="M3 9.5H9" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
stroke-linecap="round" stroke-linejoin="round" />
|
|
|
|
|
<path d="M3 15.5H21" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
stroke-linecap="round" stroke-linejoin="round" />
|
|
|
|
|
<path d="M3 21.5H21" stroke="currentColor" stroke-width="1.5"
|
|
|
|
|
stroke-linecap="round" stroke-linejoin="round" />
|
|
|
|
|
<path
|
|
|
|
|
d="M13.5858 9.91421C14.1716 10.5 15.1144 10.5 17 10.5C18.8856 10.5 19.8284 10.5 20.4142 9.91421C21 9.32843 21 8.38562 21 6.5C21 4.61438 21 3.67157 20.4142 3.08579C19.8284 2.5 18.8856 2.5 17 2.5C15.1144 2.5 14.1716 2.5 13.5858 3.08579C13 3.67157 13 4.61438 13 6.5C13 8.38562 13 9.32843 13.5858 9.91421Z"
|
|
|
|
|
stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
|
|
|
|
stroke-linejoin="round" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="w-full">
|
|
|
|
|
<iframe x-ref="wysiwyg" class="w-full h-[300px] overflow-y-auto"></iframe>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<textarea name="description" rows="8" id="textEditor"
|
|
|
|
|
class="w-full bg-white px-3 py-3 border border-gray-200 rounded-b-md outline-none text-gray-500 resize-none"
|
|
|
|
|
class="w-full bg-white px-3 py-3 border border-gray-200 rounded-b-md outline-none text-gray-500 resize-none hidden"
|
|
|
|
|
placeholder="Add Comment..." required></textarea>
|
|
|
|
|
|
|
|
|
|
<div class="w-full flex flex-col items-end gap-3">
|
|
|
|
@ -153,114 +247,170 @@
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
<script>
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
const ticketId = '{{ticket.id}}';
|
|
|
|
|
const wsUrl = `ws://${window.location.host}/ws/ticketroom/${ticketId}/`;
|
|
|
|
|
const socket = new WebSocket(wsUrl);
|
|
|
|
|
let typingTimeout = null;
|
|
|
|
|
|
|
|
|
|
socket.onopen = () => {
|
|
|
|
|
console.log('WebSocket connection established');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.onmessage = (event) => {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
if (data.event_type === 'typing') {
|
|
|
|
|
const typingDiv = document.getElementById('typing-notification');
|
|
|
|
|
typingDiv.innerHTML = data.html;
|
|
|
|
|
} else if (data.event_type === 'stop_typing') {
|
|
|
|
|
const typingDiv = document.getElementById('typing-notification');
|
|
|
|
|
typingDiv.innerHTML = '';
|
|
|
|
|
} else if (data.event_type === 'reaction') {
|
|
|
|
|
console.log(data.reaction);
|
|
|
|
|
const updateElement = document.getElementById(`update-${data.update_id}`);
|
|
|
|
|
updateElement.querySelectorAll('.reaction-button').forEach(button => {
|
|
|
|
|
button.classList.remove('border-2', 'border-secondosiblue');
|
|
|
|
|
if (button.dataset.reaction === data.reaction) {
|
|
|
|
|
button.classList.add('border-2', 'border-secondosiblue');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const submittedReactions = updateElement.querySelector('#submitted-reactions');
|
|
|
|
|
submittedReactions.classList.remove('w-full', 'border-t', 'border-gray-200', 'pt-5', 'flex', 'justify-start', 'items-center', 'gap-3', 'p-5');
|
|
|
|
|
updateElement.querySelectorAll('.submittedreaction-button').forEach(button => {
|
|
|
|
|
button.classList.add('hidden');
|
|
|
|
|
if (button.dataset.reaction === data.reaction) {
|
|
|
|
|
submittedReactions.classList.add('w-full', 'border-t', 'border-gray-200', 'pt-5', 'flex', 'justify-start', 'items-center', 'gap-3', 'p-5');
|
|
|
|
|
button.classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
const typingDiv = document.getElementById('typing-notification');
|
|
|
|
|
const messagesDiv = document.getElementById('messages');
|
|
|
|
|
messagesDiv.insertAdjacentHTML('beforeend', data.html);
|
|
|
|
|
typingDiv.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
function app(socket) {
|
|
|
|
|
return {
|
|
|
|
|
wysiwyg: null,
|
|
|
|
|
socket: socket,
|
|
|
|
|
typingTimeout: null,
|
|
|
|
|
init: function (el) {
|
|
|
|
|
this.wysiwyg = el;
|
|
|
|
|
this.setupWysiwyg();
|
|
|
|
|
},
|
|
|
|
|
setupWysiwyg: function() {
|
|
|
|
|
this.updateTextarea();
|
|
|
|
|
this.wysiwyg.contentDocument.body.addEventListener('input', this.onInput.bind(this));
|
|
|
|
|
this.wysiwyg.contentDocument.querySelector('head').innerHTML += `<style>
|
|
|
|
|
*, ::after, ::before {box-sizing: border-box;}
|
|
|
|
|
:root {tab-size: 4;}
|
|
|
|
|
html {line-height: 1.15;text-size-adjust: 100%;}
|
|
|
|
|
body {margin: 0px; padding: 1rem 0.5rem;}
|
|
|
|
|
body {font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";}
|
|
|
|
|
</style>`;
|
|
|
|
|
this.wysiwyg.contentDocument.body.innerHTML = '';
|
|
|
|
|
this.wysiwyg.contentDocument.designMode = "on";
|
|
|
|
|
},
|
|
|
|
|
onInput: function() {
|
|
|
|
|
this.updateTextarea();
|
|
|
|
|
this.sendTypingEvent();
|
|
|
|
|
},
|
|
|
|
|
updateTextarea: function() {
|
|
|
|
|
var wrapperDiv = document.createElement('div');
|
|
|
|
|
wrapperDiv.className = this.wysiwyg.classList.contains('rtl') ? 'rtl' : 'ltr';
|
|
|
|
|
wrapperDiv.innerHTML = this.wysiwyg.contentDocument.body.innerHTML;
|
|
|
|
|
document.getElementById('textEditor').value = wrapperDiv.outerHTML;
|
|
|
|
|
},
|
|
|
|
|
sendTypingEvent: function() {
|
|
|
|
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
|
|
|
clearTimeout(this.typingTimeout);
|
|
|
|
|
const data = { event_type: 'typing' };
|
|
|
|
|
this.socket.send(JSON.stringify(data));
|
|
|
|
|
|
|
|
|
|
this.typingTimeout = setTimeout(() => {
|
|
|
|
|
const stopTypingData = { event_type: 'stop_typing' };
|
|
|
|
|
this.socket.send(JSON.stringify(stopTypingData));
|
|
|
|
|
}, 1500);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
format: function (cmd, param) {
|
|
|
|
|
this.wysiwyg.contentDocument.execCommand(cmd, false, param || null);
|
|
|
|
|
this.updateTextarea();
|
|
|
|
|
},
|
|
|
|
|
setDirection: function(direction) {
|
|
|
|
|
this.wysiwyg.classList.remove('rtl', 'ltr');
|
|
|
|
|
this.wysiwyg.contentDocument.body.classList.remove('rtl', 'ltr');
|
|
|
|
|
document.getElementById('textEditor').classList.remove('rtl', 'ltr');
|
|
|
|
|
|
|
|
|
|
this.wysiwyg.classList.add(direction);
|
|
|
|
|
this.wysiwyg.contentDocument.body.classList.add(direction);
|
|
|
|
|
document.getElementById('textEditor').classList.add(direction);
|
|
|
|
|
|
|
|
|
|
this.wysiwyg.contentDocument.body.style.direction = direction;
|
|
|
|
|
this.updateTextarea();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initializeWebSocket() {
|
|
|
|
|
const ticketId = '{{ticket.id}}';
|
|
|
|
|
const wsUrl = `ws://${window.location.host}/ws/ticketroom/${ticketId}/`;
|
|
|
|
|
const socket = new WebSocket(wsUrl);
|
|
|
|
|
|
|
|
|
|
socket.onopen = () => {
|
|
|
|
|
console.log('WebSocket connection established');
|
|
|
|
|
const appInstance = app(socket);
|
|
|
|
|
const wysiwygElement = document.querySelector('iframe'); // Replace with your specific iframe selector
|
|
|
|
|
appInstance.init(wysiwygElement);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.onmessage = (event) => {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
if (data.event_type === 'typing') {
|
|
|
|
|
const typingDiv = document.getElementById('typing-notification');
|
|
|
|
|
typingDiv.innerHTML = data.html;
|
|
|
|
|
} else if (data.event_type === 'stop_typing') {
|
|
|
|
|
const typingDiv = document.getElementById('typing-notification');
|
|
|
|
|
typingDiv.innerHTML = '';
|
|
|
|
|
} else if (data.event_type === 'reaction') {
|
|
|
|
|
const updateElement = document.getElementById(`update-${data.update_id}`);
|
|
|
|
|
updateElement.querySelectorAll('.reaction-button').forEach(button => {
|
|
|
|
|
button.classList.remove('border-2', 'border-secondosiblue');
|
|
|
|
|
if (button.dataset.reaction === data.reaction) {
|
|
|
|
|
button.classList.add('border-2', 'border-secondosiblue');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const submittedReactions = updateElement.querySelector('#submitted-reactions');
|
|
|
|
|
submittedReactions.classList.remove('w-full', 'border-t', 'border-gray-200', 'pt-5', 'flex', 'justify-start', 'items-center', 'gap-3', 'p-5');
|
|
|
|
|
updateElement.querySelectorAll('.submittedreaction-button').forEach(button => {
|
|
|
|
|
button.classList.add('hidden');
|
|
|
|
|
if (button.dataset.reaction === data.reaction) {
|
|
|
|
|
submittedReactions.classList.add('w-full', 'border-t', 'border-gray-200', 'pt-5', 'flex', 'justify-start', 'items-center', 'gap-3', 'p-5');
|
|
|
|
|
button.classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
const typingDiv = document.getElementById('typing-notification');
|
|
|
|
|
const messagesDiv = document.getElementById('messages');
|
|
|
|
|
messagesDiv.insertAdjacentHTML('beforeend', data.html);
|
|
|
|
|
typingDiv.innerHTML = '';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.onclose = () => {
|
|
|
|
|
console.log('WebSocket connection closed');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.onerror = (error) => {
|
|
|
|
|
console.log('WebSocket error:', error);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const form = document.getElementById('ticketForm');
|
|
|
|
|
const textEditor = document.getElementById('textEditor');
|
|
|
|
|
const wysiwygElement = document.querySelector('iframe'); // Replace with your specific iframe selector
|
|
|
|
|
|
|
|
|
|
form.addEventListener('submit', (event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
const formData = new FormData(form);
|
|
|
|
|
const description = formData.get('description');
|
|
|
|
|
const filePath = formData.getAll('filePath');
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
event_type: 'update',
|
|
|
|
|
description: description,
|
|
|
|
|
filePath: filePath
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.onclose = () => {
|
|
|
|
|
console.log('WebSocket connection closed');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.onerror = (error) => {
|
|
|
|
|
console.log('WebSocket error:', error);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const form = document.getElementById('ticketForm');
|
|
|
|
|
const textEditor = document.getElementById('textEditor');
|
|
|
|
|
|
|
|
|
|
textEditor.addEventListener('input', () => {
|
|
|
|
|
clearTimeout(typingTimeout);
|
|
|
|
|
const data = {
|
|
|
|
|
event_type: 'typing'
|
|
|
|
|
};
|
|
|
|
|
socket.send(JSON.stringify(data));
|
|
|
|
|
|
|
|
|
|
typingTimeout = setTimeout(() => {
|
|
|
|
|
const stopTypingData = {
|
|
|
|
|
event_type: 'stop_typing'
|
|
|
|
|
};
|
|
|
|
|
socket.send(JSON.stringify(stopTypingData));
|
|
|
|
|
}, 1500);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
form.addEventListener('submit', (event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
socket.send(JSON.stringify(data));
|
|
|
|
|
form.reset();
|
|
|
|
|
textEditor.value = '';
|
|
|
|
|
wysiwygElement.contentDocument.body.innerHTML = ''; // Clear the WYSIWYG content
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const formData = new FormData(form);
|
|
|
|
|
const description = formData.get('description');
|
|
|
|
|
const filePath = formData.getAll('filePath');
|
|
|
|
|
document.addEventListener('click', (event) => {
|
|
|
|
|
if (event.target.classList.contains('reaction-button')) {
|
|
|
|
|
const updateId = event.target.dataset.updateId;
|
|
|
|
|
const reaction = event.target.dataset.reaction;
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
event_type: 'update',
|
|
|
|
|
description: description,
|
|
|
|
|
filePath: filePath
|
|
|
|
|
event_type: 'update_reaction',
|
|
|
|
|
update_id: updateId,
|
|
|
|
|
reaction: reaction
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.send(JSON.stringify(data));
|
|
|
|
|
form.reset();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click', (event) => {
|
|
|
|
|
if (event.target.classList.contains('reaction-button')) {
|
|
|
|
|
const updateId = event.target.dataset.updateId;
|
|
|
|
|
const reaction = event.target.dataset.reaction;
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
event_type: 'update_reaction',
|
|
|
|
|
update_id: updateId,
|
|
|
|
|
reaction: reaction
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
socket.send(JSON.stringify(data));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
initializeWebSocket();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -389,7 +539,7 @@
|
|
|
|
|
<!---------------------- JS SCRIPTS -------------------->
|
|
|
|
|
|
|
|
|
|
<script type="text/javascript" src="{% static 'js/tickets/ticket-details.js' %}"></script>
|
|
|
|
|
<script type="text/javascript" src="{% static 'js/inputs/text-editor.js' %}"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script type="text/javascript" src='{% static "js/inputs/file-uploader.js" %}'></script>
|
|
|
|
|
|
|
|
|
|