Corp/backend/static/js/components/postDetailModal.js
Игорь Надеин 3ec09a0fa7 fix(message-inputs): кнопки не переносятся на новую строку при масштабировании
КРИТИЧЕСКИЕ ИСПРАВЛЕНИЯ CSS:
- Добавлен flex-wrap: nowrap в .message-field (запрет переноса)
- Добавлен min-width: 0 в .message-input (позволяет сжиматься)
- Удален flex-wrap: wrap из mobile media query
- Улучшены mobile стили: меньшие отступы и размеры кнопок
- Поле ввода теперь корректно сжимается на мобильных
- Кнопки смайлика и отправки остаются на месте

ВЛИЯНИЕ:
- Модальное окно постов
- Чаты (messenger)
- Все формы комментариев
- Все места с .message-field компонентом

ОТКЛЮЧЕН DEBUG в postDetailModal.js
2025-11-29 16:31:06 +03:00

557 lines
18 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.

/**
* postDetailModal.js
* Модуль для открытия деталей поста в модальном окне
* Полностью переработанная версия
*/
(function() {
'use strict';
// Константы
const INITIAL_COMMENTS_COUNT = 10;
const DEBUG = false; // Отключаем - всё работает
// Состояние
let modal = null;
let modalElement = null;
let currentPost = null;
/**
* Инициализация
*/
function initPostDetailModal() {
modalElement = document.getElementById('postDetailModal');
if (!modalElement) {
console.warn('postDetailModal: элемент #postDetailModal не найден');
return;
}
modal = new bootstrap.Modal(modalElement);
modalElement.addEventListener('hidden.bs.modal', function() {
const contentDiv = document.getElementById('postDetailContent');
if (contentDiv) {
contentDiv.innerHTML = `
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
`;
}
currentPost = null;
});
attachPostLinkHandlers();
}
/**
* Утилиты для работы с cookies
*/
function getCsrfToken() {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'csrftoken') {
return decodeURIComponent(value);
}
}
return null;
}
function getAccessToken() {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'access_token') {
return decodeURIComponent(value);
}
}
return null;
}
/**
* Утилиты
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function getCurrentUserId() {
// Получаем ID текущего пользователя из meta-тега
const meta = document.querySelector('meta[name="user-id"]');
if (meta) {
const userId = meta.getAttribute('content');
return userId ? parseInt(userId) : null;
}
return null;
}
function getAuthorId(authorObj) {
if (!authorObj) return null;
// API возвращает author.id
return authorObj.id || null;
}
function formatDate(dateString) {
const date = new Date(dateString);
const now = new Date();
const diff = now - date;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) {
return 'Сегодня, ' + date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
} else if (days === 1) {
return 'Вчера, ' + date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
} else if (days < 7) {
return days + ' дн. назад';
} else {
return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
}
}
function formatBody(text) {
return text.replace(/\n/g, '<br>');
}
function pluralizeComments(count) {
const cases = [2, 0, 1, 1, 1, 2];
const titles = ['комментарий', 'комментария', 'комментариев'];
return titles[(count % 100 > 4 && count % 100 < 20) ? 2 : cases[(count % 10 < 5) ? count % 10 : 5]];
}
/**
* Загрузка поста
*/
async function loadPostDetail(postId) {
const accessToken = getAccessToken();
const headers = { 'Accept': 'application/json' };
if (accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`;
}
try {
const response = await fetch(`/api/v1/posts/${postId}/`, {
method: 'GET',
headers: headers
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const post = await response.json();
if (DEBUG) {
console.log('=== POST DEBUG ===');
console.log('🙋 Current User ID:', getCurrentUserId());
console.log('📝 Post:', post);
console.log('👤 Post author:', post.author);
console.log('🆔 Post author ID:', getAuthorId(post.author), typeof getAuthorId(post.author));
if (post.comments && post.comments.length > 0) {
console.log('\n=== ALL COMMENTS DEBUG ===');
console.table(post.comments.map(c => ({
id: c.id,
author_name: c.author?.full_name,
author_id: c.author?.id,
is_me: c.author?.id === getCurrentUserId() ? '✅ МОЙ' : '❌ чужой',
text: c.text.substring(0, 30)
})));
}
console.log('==================\n');
}
currentPost = post;
renderPostDetail(post);
} catch (error) {
console.error('Ошибка загрузки поста:', error);
showError('Не удалось загрузить пост');
}
}
/**
* Рендеринг
*/
function renderPostDetail(post) {
const contentDiv = document.getElementById('postDetailContent');
const postAuthorId = getAuthorId(post.author);
if (DEBUG) {
console.log('Rendering with postAuthorId:', postAuthorId);
}
let html = '<button type="button" class="btn-close position-absolute top-0 end-0 m-3" data-bs-dismiss="modal" aria-label="Закрыть" style="z-index: 1050;"></button>';
html += '<article class="card">';
html += renderPostHeader(post);
if (post.image) {
html += `<div class="feed-img"><img src="${post.image}" alt="${escapeHtml(post.title || 'Публикация')}"></div>`;
}
html += '<div class="card-body">';
html += `<h3 class="feed-title mb-3">${escapeHtml(post.title || 'Без названия')}</h3>`;
if (post.body) {
html += `<div class="feed-text mb-3">${formatBody(post.body)}</div>`;
}
html += '</div>';
html += renderPostFooter(post);
html += renderCommentsSection(post, postAuthorId);
html += '</article>';
contentDiv.innerHTML = html;
if (post.comments && post.comments.length > 0) {
contentDiv.dataset.allComments = JSON.stringify(post.comments);
contentDiv.dataset.postAuthorId = postAuthorId || '';
}
scrollToBottom();
attachLikeHandlers();
attachLoadMoreCommentsHandler();
attachCommentFormHandler();
}
function renderPostHeader(post) {
let html = '<header class="card-header">';
html += '<div class="card-icon">';
if (post.author && post.author.avatar) {
html += `<img src="${post.author.avatar}" alt="">`;
} else {
html += '<i class="bi-person"></i>';
}
html += '</div>';
html += '<div class="feed-info">';
html += `<div class="card-title">${escapeHtml(post.author?.full_name || 'Аноним')}</div>`;
html += `<time class="feed-date">${formatDate(post.created_at)}</time>`;
html += '</div>';
if (post.pinned) {
html += '<span class="feed-pin"><i class="bi-pin-angle"></i></span>';
}
html += '</header>';
return html;
}
function renderPostFooter(post) {
let html = '<footer class="feed-ft">';
html += '<div class="feed-stats">';
const likeIcon = post.user_has_liked ? 'bi-heart-fill text-danger' : 'bi-heart';
html += `<button class="btn btn-ghost btn-sm like-btn" data-post-id="${post.id}">`;
html += `<i class="${likeIcon}"></i> ${post.likes_count || 0}`;
html += '</button>';
html += '</div>';
html += '</footer>';
return html;
}
function renderCommentsSection(post, postAuthorId) {
let html = '<div class="comments-section-wrapper">';
if (post.comments && post.comments.length > 0) {
html += '<div class="px-3 pt-3">';
html += '<hr class="my-3">';
html += '<div class="comments-section-header">';
html += '<i class="bi-chat-dots comments-icon"></i>';
html += '<h5 class="comments-title">Комментарии</h5>';
html += `<span class="comments-count">(${post.comments.length})</span>`;
html += '</div>';
if (post.comments.length > INITIAL_COMMENTS_COUNT) {
const remainingCount = post.comments.length - INITIAL_COMMENTS_COUNT;
html += `<button class="btn btn-link btn-sm mb-2 load-more-comments">`;
html += `<i class="bi-chevron-up"></i> Показать ещё ${remainingCount} ${pluralizeComments(remainingCount)}`;
html += '</button>';
}
html += '<div class="comments-list comments-scrollable" id="commentsListContainer">';
const visibleComments = post.comments.slice(-INITIAL_COMMENTS_COUNT);
visibleComments.forEach(comment => {
html += renderComment(comment, postAuthorId);
});
html += '</div>';
html += '</div>';
}
html += renderCommentForm(post.id);
html += '</div>';
return html;
}
function renderComment(comment, postAuthorId) {
// ВАЖНО: Отображаем справа комментарии ТЕКУЩЕГО пользователя (как в мессенджере)
// НЕ автора поста, а автора комментария === текущий пользователь
const currentUserId = getCurrentUserId();
const commentAuthorId = getAuthorId(comment.author);
// Мой комментарий = справа, чужой = слева
const isMyComment = (currentUserId !== null && commentAuthorId !== null &&
currentUserId === commentAuthorId);
if (DEBUG) {
console.log(`🔍 Comment #${comment.id} by ${comment.author?.full_name}:`, {
'currentUserId (ME)': currentUserId,
'commentAuthorId': commentAuthorId,
'isMyComment': isMyComment,
'CSS class': isMyComment ? 'comment-item--author (RIGHT ➡️)' : 'comment-item (LEFT ⬅️)'
});
}
const commentClass = isMyComment ? 'comment-item comment-item--author' : 'comment-item';
let html = `<div class="${commentClass}"`;
// Добавляем data-атрибуты для отладки в инспекторе
html += ` data-comment-id="${comment.id}"`;
html += ` data-is-my-comment="${isMyComment}"`;
html += ` data-comment-author-id="${commentAuthorId}"`;
html += ` data-current-user-id="${currentUserId}">`;
html += '<div class="comment-header">';
const authorName = comment.author?.full_name || 'Аноним';
const authorId = getAuthorId(comment.author);
if (authorId) {
html += `<a href="/employees/employee/${authorId}/" class="comment-author">${escapeHtml(authorName)}</a>`;
} else {
html += `<span class="comment-author">${escapeHtml(authorName)}</span>`;
}
html += '<span class="comment-separator">·</span>';
html += `<time class="comment-date">${formatDate(comment.created_at)}</time>`;
html += '</div>';
html += `<div class="comment-body">${formatBody(comment.text)}</div>`;
html += '</div>';
return html;
}
function renderCommentForm(postId) {
let html = '<div class="comment-form-sticky">';
html += '<div class="px-3 py-2">';
html += `<form class="comment-form-modal" data-post-id="${postId}">`;
html += '<div class="message-field message-field--compact">';
html += '<div class="dropdown message-emoji">';
html += '<button type="button" class="btn btn-ghost btn-emoji message-icon-btn" ';
html += 'data-bs-toggle="dropdown" aria-expanded="false" title="Вставить смайлик">';
html += '<i class="bi-emoji-smile"></i>';
html += '</button>';
html += '<div class="dropdown-menu dropdown-menu-start message-emoji-menu">';
html += '<emoji-picker data-emoji-picker class="chat-emoji-picker"></emoji-picker>';
html += '</div>';
html += '</div>';
html += '<textarea name="text" class="form-control message-input" rows="1" placeholder="Оставить комментарий…" required></textarea>';
html += '<button class="btn btn-primary message-send" type="submit" title="Отправить"><i class="bi-send"></i></button>';
html += '</div>';
html += '</form>';
html += '</div>';
html += '</div>';
return html;
}
/**
* Обработчики
*/
function scrollToBottom() {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const container = document.getElementById('commentsListContainer');
if (container) {
container.scrollTop = container.scrollHeight;
}
});
});
}
function attachPostLinkHandlers() {
document.addEventListener('click', function(e) {
const link = e.target.closest('a[data-post-link]');
if (link) {
e.preventDefault();
const postId = link.dataset.postId;
if (postId) {
openPostModal(postId);
}
}
});
}
function attachLoadMoreCommentsHandler() {
const loadMoreBtn = document.querySelector('.load-more-comments');
if (!loadMoreBtn) return;
loadMoreBtn.addEventListener('click', function() {
const contentDiv = document.getElementById('postDetailContent');
const allComments = JSON.parse(contentDiv.dataset.allComments || '[]');
const postAuthorId = contentDiv.dataset.postAuthorId;
const container = document.getElementById('commentsListContainer');
if (!container || allComments.length === 0) return;
const scrollHeight = container.scrollHeight;
const scrollTop = container.scrollTop;
let html = '';
allComments.forEach(comment => {
html += renderComment(comment, postAuthorId);
});
container.innerHTML = html;
const newScrollHeight = container.scrollHeight;
const heightDiff = newScrollHeight - scrollHeight;
container.scrollTop = scrollTop + heightDiff;
this.remove();
});
}
function attachCommentFormHandler() {
const form = document.querySelector('.comment-form-modal');
if (!form) return;
form.addEventListener('submit', async function(e) {
e.preventDefault();
const textarea = this.querySelector('textarea[name="text"]');
const text = textarea.value.trim();
if (!text) return;
const postId = this.dataset.postId;
await submitComment(postId, text);
textarea.value = '';
});
}
async function submitComment(postId, text) {
const csrfToken = getCsrfToken();
if (!csrfToken) {
alert('Необходимо авторизоваться');
return;
}
try {
const response = await fetch(`/api/v1/comments/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
post: parseInt(postId),
text: text
})
});
if (response.ok) {
// Перезагружаем пост
await loadPostDetail(postId);
} else {
const errorData = await response.json().catch(() => ({}));
console.error('Error submitting comment:', errorData);
alert('Ошибка при отправке комментария');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка сети');
}
}
function attachLikeHandlers() {
const likeButtons = document.querySelectorAll('#postDetailContent .like-btn');
likeButtons.forEach(btn => {
btn.addEventListener('click', async function(e) {
e.preventDefault();
const postId = this.dataset.postId;
await toggleLike(postId, this);
});
});
}
async function toggleLike(postId, button) {
const csrfToken = getCsrfToken();
if (!csrfToken) {
alert('Необходимо авторизоваться');
return;
}
const icon = button.querySelector('i');
const isLiked = icon.classList.contains('bi-heart-fill');
const endpoint = isLiked ? 'unlike' : 'like';
try {
const response = await fetch(`/api/v1/posts/${postId}/${endpoint}/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const data = await response.json();
if (isLiked) {
icon.classList.remove('bi-heart-fill', 'text-danger');
icon.classList.add('bi-heart');
} else {
icon.classList.remove('bi-heart');
icon.classList.add('bi-heart-fill', 'text-danger');
}
const countText = button.childNodes[1];
if (countText) {
countText.textContent = ' ' + (data.likes_count || 0);
}
} else {
alert('Ошибка при обработке лайка');
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка сети');
}
}
function showError(message) {
const contentDiv = document.getElementById('postDetailContent');
contentDiv.innerHTML = `
<div class="alert alert-danger" role="alert">
<i class="bi-exclamation-triangle"></i> ${escapeHtml(message)}
</div>
`;
}
function openPostModal(postId) {
if (!modal) {
console.error('Modal не инициализирован');
return;
}
modal.show();
loadPostDetail(postId);
}
// Инициализация
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPostDetailModal);
} else {
initPostDetailModal();
}
window.openPostModal = openPostModal;
})();