Corp/backend/static/js/components/chatComposer.js

231 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ChatComposer.js
* Отвечает за логику ввода текста, выбора файлов и отправки сообщений.
* Полностью заменяет стандартную отправку формы.
*/
class ChatComposer {
constructor(config) {
this.chatId = config.chatId;
this.apiEndpoint = config.apiEndpoint || '/api/v1/communications/upload-message/';
this.csrfToken = this.getCookie('csrftoken');
// DOM Elements
this.form = document.getElementById('chatForm');
this.textarea = document.getElementById('id_content');
this.previewContainer = document.getElementById('attachmentPreview');
// File Inputs
this.inputs = {
document: document.getElementById('documentInput'),
image: document.getElementById('imageInput'),
camera: document.getElementById('cameraInput'),
audio: document.getElementById('audioInput')
};
// Buttons
this.buttons = {
document: document.getElementById('attachDocument'),
image: document.getElementById('attachImage'),
camera: document.getElementById('attachCamera'),
audio: document.getElementById('attachAudio'),
send: this.form?.querySelector('.btn-send')
};
// State
this.selectedFiles = [];
this.isSubmitting = false;
this.init();
}
init() {
if (!this.form) {
console.error('ChatComposer: Form not found');
return;
}
this.bindEvents();
console.log('ChatComposer: Initialized');
}
bindEvents() {
// 1. Блокировка стандартной отправки
this.form.addEventListener('submit', (e) => {
e.preventDefault();
e.stopPropagation();
this.handleSubmit();
});
// 2. Обработка кнопок вложений
if (this.buttons.document) this.buttons.document.addEventListener('click', (e) => { e.preventDefault(); this.inputs.document.click(); });
if (this.buttons.image) this.buttons.image.addEventListener('click', (e) => { e.preventDefault(); this.inputs.image.click(); });
if (this.buttons.camera) this.buttons.camera.addEventListener('click', (e) => { e.preventDefault(); this.inputs.camera.click(); });
if (this.buttons.audio) this.buttons.audio.addEventListener('click', (e) => { e.preventDefault(); this.inputs.audio.click(); });
// 3. Обработка выбора файлов (change event)
Object.values(this.inputs).forEach(input => {
if (input) {
input.addEventListener('change', (e) => this.handleFileSelect(e.target.files));
}
});
// 4. Enter для отправки (если не Shift+Enter)
this.textarea.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.form.dispatchEvent(new Event('submit'));
}
});
}
handleFileSelect(files) {
if (!files || files.length === 0) return;
// Добавляем новые файлы к существующим
this.selectedFiles = [...this.selectedFiles, ...Array.from(files)];
// Очищаем input, чтобы можно было выбрать тот же файл снова
Object.values(this.inputs).forEach(input => { if(input) input.value = ''; });
this.updatePreview();
}
removeFile(index) {
this.selectedFiles.splice(index, 1);
this.updatePreview();
}
updatePreview() {
if (!this.previewContainer) return;
if (this.selectedFiles.length === 0) {
this.previewContainer.classList.add('d-none');
this.previewContainer.innerHTML = '';
return;
}
this.previewContainer.classList.remove('d-none');
this.previewContainer.innerHTML = '';
this.selectedFiles.forEach((file, index) => {
const item = document.createElement('div');
item.className = 'attachment-item d-flex align-items-center gap-2 p-2 border rounded mb-2 bg-white';
let icon = 'bi-file-earmark';
if (file.type.startsWith('image/')) icon = 'bi-image';
else if (file.type.startsWith('video/')) icon = 'bi-camera-video';
else if (file.type.startsWith('audio/')) icon = 'bi-music-note';
item.innerHTML = `
<i class="${icon} fs-4 text-primary"></i>
<div class="flex-grow-1 text-truncate">
<div class="fw-semibold text-truncate">${file.name}</div>
<div class="small text-secondary">${this.formatFileSize(file.size)}</div>
</div>
<button type="button" class="btn btn-sm btn-ghost text-danger remove-file-btn">
<i class="bi-x-lg"></i>
</button>
`;
item.querySelector('.remove-file-btn').addEventListener('click', () => this.removeFile(index));
this.previewContainer.appendChild(item);
});
}
async handleSubmit() {
if (this.isSubmitting) return;
const content = this.textarea.value.trim();
// Валидация
if (!content && this.selectedFiles.length === 0) {
// Можно добавить визуальный эффект ошибки
this.textarea.focus();
return;
}
this.isSubmitting = true;
this.toggleLoading(true);
try {
const formData = new FormData();
formData.append('chat_id', this.chatId);
formData.append('content', content); // Отправляем даже пустой текст
// Добавляем файлы
this.selectedFiles.forEach((file, index) => {
formData.append(`file_${index}`, file);
});
console.log(`ChatComposer: Sending message. Content len: ${content.length}, Files: ${this.selectedFiles.length}`);
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers: {
'X-CSRFToken': this.csrfToken
},
body: formData
});
const result = await response.json();
if (response.ok && result.ok) {
this.resetForm();
} else {
console.error('ChatComposer: Server error', result);
alert('Ошибка отправки: ' + (result.error || 'Неизвестная ошибка'));
}
} catch (error) {
console.error('ChatComposer: Network error', error);
alert('Ошибка сети. Проверьте подключение.');
} finally {
this.isSubmitting = false;
this.toggleLoading(false);
this.textarea.focus();
}
}
resetForm() {
this.textarea.value = '';
this.selectedFiles = [];
this.updatePreview();
// Сброс высоты textarea (если используется авто-высота)
this.textarea.style.height = 'auto';
}
toggleLoading(isLoading) {
if (this.buttons.send) {
this.buttons.send.disabled = isLoading;
this.buttons.send.innerHTML = isLoading
? '<span class="spinner-border spinner-border-sm"></span>'
: '<i class="bi-send-fill"></i>';
}
}
formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
}
// Экспорт для использования
window.ChatComposer = ChatComposer;