From c81cb71fb576761f553b292a896b0622433dbac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Mon, 8 Dec 2025 07:55:53 +0300 Subject: [PATCH] feat: add paste and drag-and-drop support for chat files - Added paste event handler to detect and handle files from clipboard - Support for pasting images and files with Ctrl+V / Cmd+V - Added drag-and-drop file upload functionality - Visual feedback with green highlight during drag-over - Supports multiple file paste/drop at once - Prevents text insertion when files are pasted - Uses clipboard items API for better file detection - Fallback to clipboard files for broader compatibility - Console logging for debugging paste/drop operations --- backend/static/css/components/chat-detail.css | 8 ++ backend/static/js/components/chatComposer.js | 111 ++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/backend/static/css/components/chat-detail.css b/backend/static/css/components/chat-detail.css index 491c64d0..7a171241 100644 --- a/backend/static/css/components/chat-detail.css +++ b/backend/static/css/components/chat-detail.css @@ -172,6 +172,14 @@ main.main { box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.15); } +/* Стиль при перетаскивании файлов */ +.composer-textarea.drag-over { + border-color: var(--bs-success); + background: rgba(var(--bs-success-rgb), 0.05); + box-shadow: 0 0 0 0.2rem rgba(var(--bs-success-rgb), 0.15); +} + + .btn-attachment { padding: 8px 12px; border-radius: 50%; diff --git a/backend/static/js/components/chatComposer.js b/backend/static/js/components/chatComposer.js index 1a477bdf..3d24296b 100644 --- a/backend/static/js/components/chatComposer.js +++ b/backend/static/js/components/chatComposer.js @@ -100,6 +100,31 @@ class ChatComposer { this.textarea.addEventListener('input', () => { this.handleTyping(); }); + + // Обработка вставки из буфера обмена + this.textarea.addEventListener('paste', (e) => { + this.handlePaste(e); + }); + + // Обработка drag-and-drop + this.textarea.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.textarea.classList.add('drag-over'); + }); + + this.textarea.addEventListener('dragleave', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.textarea.classList.remove('drag-over'); + }); + + this.textarea.addEventListener('drop', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.textarea.classList.remove('drag-over'); + this.handleDrop(e); + }); } window.addEventListener('chat:message-received', this.boundMessageHandler); @@ -155,6 +180,92 @@ class ChatComposer { this.renderPreview(); } + /** + * Обработка вставки из буфера обмена + * Поддерживает вставку изображений и файлов + */ + handlePaste(event) { + const clipboardData = event.clipboardData || window.clipboardData; + if (!clipboardData) return; + + const items = Array.from(clipboardData.items || []); + const files = Array.from(clipboardData.files || []); + + // Проверяем наличие файлов + let hasFiles = false; + const pastedFiles = []; + + // Сначала проверяем items (более надежный способ) + items.forEach((item) => { + if (item.kind === 'file') { + const file = item.getAsFile(); + if (file) { + pastedFiles.push(file); + hasFiles = true; + } + } + }); + + // Если не нашли через items, пробуем files + if (!hasFiles && files.length > 0) { + pastedFiles.push(...files); + hasFiles = true; + } + + // Если есть файлы - обрабатываем их + if (hasFiles && pastedFiles.length > 0) { + console.log('[ChatComposer] Pasted files:', pastedFiles); + + // Предотвращаем вставку текста/HTML если вставлены файлы + event.preventDefault(); + + // Добавляем файлы в selectedFiles + pastedFiles.forEach((file) => { + const uuidSupported = window.crypto && typeof window.crypto.randomUUID === 'function'; + const fileId = uuidSupported ? window.crypto.randomUUID() : `${Date.now()}-${Math.random()}`; + this.selectedFiles.push({ id: fileId, file }); + }); + + // Обновляем превью + this.renderPreview(); + + // Показываем уведомление пользователю + const fileCount = pastedFiles.length; + const fileWord = fileCount === 1 ? 'файл' : fileCount < 5 ? 'файла' : 'файлов'; + console.log(`[ChatComposer] Добавлено ${fileCount} ${fileWord} из буфера обмена`); + } + // Если файлов нет - оставляем стандартное поведение (вставка текста) + } + + /** + * Обработка перетаскивания файлов (drag-and-drop) + */ + handleDrop(event) { + const dataTransfer = event.dataTransfer; + if (!dataTransfer) return; + + const files = Array.from(dataTransfer.files || []); + + if (files.length > 0) { + console.log('[ChatComposer] Dropped files:', files); + + // Добавляем файлы в selectedFiles + files.forEach((file) => { + const uuidSupported = window.crypto && typeof window.crypto.randomUUID === 'function'; + const fileId = uuidSupported ? window.crypto.randomUUID() : `${Date.now()}-${Math.random()}`; + this.selectedFiles.push({ id: fileId, file }); + }); + + // Обновляем превью + this.renderPreview(); + + // Показываем уведомление пользователю + const fileCount = files.length; + const fileWord = fileCount === 1 ? 'файл' : fileCount < 5 ? 'файла' : 'файлов'; + console.log(`[ChatComposer] Добавлено ${fileCount} ${fileWord} перетаскиванием`); + } + } + handleTyping() { const now = Date.now(); if (now - this.lastTypingSent > this.typingThrottle) {