Corp/backend/static/js/components/feedComments.js

405 lines
14 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.

/**
* feedComments.js
* Обработка быстрого добавления комментариев в карточках постов
*/
(function() {
'use strict';
/**
* Получение CSRF токена из cookie
*/
function getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [cookieName, value] = cookie.trim().split('=');
if (cookieName === name) {
return decodeURIComponent(value);
}
}
return null;
}
/**
* Форматирование даты в формат "ДД.ММ.ГГГГ ЧЧ:ММ"
*/
function formatDate(dateString) {
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}.${month}.${year} ${hours}:${minutes}`;
}
/**
* Обработка нажатия Enter в textarea
*/
function handleTextareaKeydown(e) {
// Enter без Shift - отправка формы
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const form = e.target.closest('form');
if (form) {
form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}
}
// Shift+Enter - новая строка (поведение по умолчанию)
}
/**
* Вставка emoji в textarea
*/
function insertEmoji(textarea, emoji) {
if (!textarea) return;
const start = textarea.selectionStart ?? textarea.value.length;
const end = textarea.selectionEnd ?? textarea.value.length;
const value = textarea.value || '';
const emojiText = typeof emoji === 'string' ? emoji : emoji.emoji || emoji.unicode || emoji;
textarea.value = `${value.slice(0, start)}${emojiText}${value.slice(end)}`;
const caret = start + emojiText.length;
textarea.focus();
textarea.setSelectionRange?.(caret, caret);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
/**
* Инициализация emoji picker для формы
*/
function initEmojiPicker(form) {
const picker = form.querySelector('[data-emoji-picker]');
if (!picker) return;
const textarea = form.querySelector('textarea[name="text"]');
if (!textarea) return;
// Обработчик выбора emoji
picker.addEventListener('emoji-click', (event) => {
event.preventDefault();
const emoji = event.detail?.unicode;
if (emoji) {
insertEmoji(textarea, emoji);
}
});
// Предотвращаем закрытие dropdown при клике внутри emoji-picker
const emojiDropdown = picker.closest('.dropdown-menu');
if (emojiDropdown) {
emojiDropdown.addEventListener('click', (event) => {
event.stopPropagation();
});
}
}
/**
* Добавление комментария в DOM
*/
function appendCommentToDOM(postId, commentData) {
// Находим контейнер с комментариями для этого поста
const collapsibleBlock = document.querySelector(`#ccoll-${postId}`);
if (!collapsibleBlock) return;
// Ищем существующий контейнер комментариев или создаём новый
let commentsContainer = collapsibleBlock.querySelector('.px-3.pt-2');
if (!commentsContainer) {
// Создаём контейнер если его нет
commentsContainer = document.createElement('div');
commentsContainer.className = 'px-3 pt-2';
// Вставляем перед формой
const form = collapsibleBlock.querySelector('.comment-form-quick');
if (form) {
collapsibleBlock.insertBefore(commentsContainer, form.parentElement);
} else {
collapsibleBlock.appendChild(commentsContainer);
}
}
// Создаём HTML нового комментария с использованием SCSS классов
const commentHTML = createCommentHTML(commentData);
// Добавляем новый комментарий
const tempDiv = document.createElement('div');
tempDiv.innerHTML = commentHTML;
commentsContainer.appendChild(tempDiv.firstElementChild);
// Обновляем счётчик комментариев
updateCommentCount(postId);
}
/**
* Экранирование HTML для безопасного вывода
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Обновление счётчика комментариев
*/
function updateCommentCount(postId) {
// Находим кнопку с комментариями
const commentBtn = document.querySelector(`[data-bs-target="#ccoll-${postId}"]`);
if (!commentBtn) return;
const countSpan = commentBtn.querySelector('span');
if (countSpan) {
const currentCount = parseInt(countSpan.textContent) || 0;
countSpan.textContent = currentCount + 1;
}
// Также обновляем ссылку "Показать все комментарии"
const collapsibleBlock = document.querySelector(`#ccoll-${postId}`);
if (collapsibleBlock) {
const showAllLink = collapsibleBlock.querySelector('.text-primary');
if (showAllLink) {
const linkText = showAllLink.textContent;
const match = linkText.match(/\((\d+)\)/);
if (match) {
const newCount = parseInt(match[1]) + 1;
showAllLink.textContent = linkText.replace(/\(\d+\)/, `(${newCount})`);
}
}
}
}
/**
* Добавление комментария
*/
async function addComment(form, postId) {
const csrfToken = getCookie('csrftoken');
if (!csrfToken) {
alert('Необходимо авторизоваться');
return;
}
const textarea = form.querySelector('textarea[name="text"]');
const text = textarea.value.trim();
if (!text) {
alert('Введите текст комментария');
return;
}
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.disabled = true;
const formData = new FormData();
formData.append('post', postId);
formData.append('text', text);
try {
const response = await fetch('/api/v1/comments/', {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});
if (response.ok) {
const commentData = await response.json();
// Очищаем форму
textarea.value = '';
// Добавляем комментарий в DOM
appendCommentToDOM(postId, commentData);
// Показываем сообщение об успехе (опционально)
console.log('Комментарий добавлен успешно');
} else {
const error = await response.json();
let errorMsg = 'Ошибка добавления комментария:\n';
for (const [field, errors] of Object.entries(error)) {
errorMsg += `${field}: ${Array.isArray(errors) ? errors.join(', ') : errors}\n`;
}
alert(errorMsg);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка сети при добавлении комментария');
} finally {
submitBtn.disabled = false;
}
}
/**
* Инициализация обработчиков форм комментариев
*/
function initFeedComments() {
// Обработка нажатия Enter в textarea
document.addEventListener('keydown', function(e) {
const commentTextarea = e.target.closest('textarea[name="text"]');
if (commentTextarea && (commentTextarea.closest('.comment-form-quick') || commentTextarea.closest('.comment-form-modal'))) {
handleTextareaKeydown(e);
}
});
// Используем делегирование событий для обеих форм
document.addEventListener('submit', function(e) {
// Быстрая форма в карточках
const quickForm = e.target.closest('.comment-form-quick');
if (quickForm) {
e.preventDefault();
const postId = quickForm.dataset.postId;
if (postId) {
addComment(quickForm, postId);
}
return;
}
// Форма в модальном окне
const modalForm = e.target.closest('.comment-form-modal');
if (modalForm) {
e.preventDefault();
const postId = modalForm.dataset.postId;
if (postId) {
addCommentToModal(modalForm, postId);
}
return;
}
});
// Инициализация emoji picker для уже существующих форм
document.querySelectorAll('.comment-form-quick, .comment-form-modal').forEach(form => {
initEmojiPicker(form);
});
// Наблюдатель за добавлением новых форм (для динамически загружаемых постов)
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Element node
// Проверяем, является ли добавленный узел формой комментария
if (node.classList && (node.classList.contains('comment-form-quick') || node.classList.contains('comment-form-modal'))) {
initEmojiPicker(node);
}
// Или ищем формы внутри добавленного узла
const forms = node.querySelectorAll ? node.querySelectorAll('.comment-form-quick, .comment-form-modal') : [];
forms.forEach(form => initEmojiPicker(form));
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
/**
* Добавление комментария в модальном окне
*/
async function addCommentToModal(form, postId) {
const csrfToken = getCookie('csrftoken');
if (!csrfToken) {
alert('Необходимо авторизоваться');
return;
}
const textarea = form.querySelector('textarea[name="text"]');
const text = textarea.value.trim();
if (!text) {
alert('Введите текст комментария');
return;
}
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.disabled = true;
const formData = new FormData();
formData.append('post', postId);
formData.append('text', text);
try {
const response = await fetch('/api/v1/comments/', {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});
if (response.ok) {
const commentData = await response.json();
// Очищаем форму
textarea.value = '';
// Добавляем комментарий в список модального окна
const commentsList = document.querySelector('.comments-scrollable');
if (commentsList) {
const commentHTML = createCommentHTML(commentData);
const tempDiv = document.createElement('div');
tempDiv.innerHTML = commentHTML;
commentsList.appendChild(tempDiv.firstElementChild);
// Прокручиваем к новому комментарию
commentsList.scrollTop = commentsList.scrollHeight;
}
// Обновляем счётчик
const countSpan = document.querySelector('.comments-count');
if (countSpan) {
const currentCount = parseInt(countSpan.textContent.replace(/[()]/g, '')) || 0;
countSpan.textContent = `(${currentCount + 1})`;
}
console.log('Комментарий добавлен в модальное окно');
} else {
const error = await response.json();
let errorMsg = 'Ошибка добавления комментария:\n';
for (const [field, errors] of Object.entries(error)) {
errorMsg += `${field}: ${Array.isArray(errors) ? errors.join(', ') : errors}\n`;
}
alert(errorMsg);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка сети при добавлении комментария');
} finally {
submitBtn.disabled = false;
}
}
/**
* Создание HTML комментария (вспомогательная функция)
*/
function createCommentHTML(commentData) {
const authorName = commentData.author?.full_name || 'Сотрудник';
const authorId = commentData.author?.id;
const authorLink = authorId
? `/employees/${authorId}/`
: '#';
const commentDate = formatDate(commentData.created_at);
return `
<div class="comment-item">
<div class="comment-header">
${authorId
? `<a href="${authorLink}" class="comment-author">${authorName}</a>`
: `<span class="comment-author">${authorName}</span>`
}
<span class="comment-separator">·</span>
<time class="comment-date">${commentDate}</time>
</div>
<div class="comment-body">${escapeHtml(commentData.text)}</div>
</div>
`;
}
// Инициализация при загрузке DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initFeedComments);
} else {
initFeedComments();
}
})();