fix(messages): override Quill clipboard.onPaste for single image insert + resize CSS
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Root cause of double paste: Quill's internal clipboard handler AND my DOM
handler both processed the image. Fix: override clipboard.onPaste directly
to intercept images before Quill processes them.

- uploadAndInsertImage() extracted as reusable method
- CSS: img.selected gets resize:both for native browser resize handle
- Removed complex DOM resize hack (didn't work with Quill contenteditable)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-30 16:06:50 +02:00
parent 2145a73bf3
commit f793522ab4
2 changed files with 56 additions and 86 deletions

View File

@ -1268,97 +1268,34 @@
}, },
}); });
// Image resize: click on image in editor to show resize handles // Override Quill's image handler — upload pasted/dropped images to server
state.quill.root.addEventListener('click', function(e) { // This replaces Quill's default clipboard image handling completely
if (e.target.tagName === 'IMG') { state.quill.getModule('toolbar').addHandler('image', function() {
var img = e.target; // Manual image button (if ever added to toolbar)
// Remove any existing resize wrapper var input = document.createElement('input');
document.querySelectorAll('.img-resize-wrapper').forEach(function(w) { input.type = 'file';
var inner = w.querySelector('img'); input.accept = 'image/*';
if (inner) w.parentNode.replaceChild(inner, w); input.onchange = function() { if (input.files[0]) Composer.uploadAndInsertImage(input.files[0]); };
}); input.click();
// Wrap image in resize container
var wrapper = document.createElement('div');
wrapper.className = 'img-resize-wrapper';
wrapper.contentEditable = 'false';
wrapper.style.cssText = 'display:inline-block; position:relative; border:2px solid #3b82f6; padding:2px;';
img.parentNode.insertBefore(wrapper, img);
wrapper.appendChild(img);
// Add resize handle
var handle = document.createElement('div');
handle.style.cssText = 'position:absolute; right:-4px; bottom:-4px; width:12px; height:12px; background:#3b82f6; border-radius:2px; cursor:nwse-resize;';
wrapper.appendChild(handle);
// Drag to resize
handle.addEventListener('mousedown', function(me) {
me.preventDefault();
var startX = me.clientX;
var startW = img.offsetWidth;
function onMove(mm) {
var newW = Math.max(50, startW + (mm.clientX - startX));
img.style.width = newW + 'px';
img.style.height = 'auto';
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
}); });
// Click outside image removes resize wrapper // Intercept paste at Quill's clipboard module level
document.addEventListener('click', function(e) { var originalPaste = state.quill.clipboard.onPaste;
if (!e.target.closest('.img-resize-wrapper') && !e.target.closest('.ql-editor img')) { state.quill.clipboard.onPaste = function(e) {
document.querySelectorAll('.img-resize-wrapper').forEach(function(w) {
var inner = w.querySelector('img');
if (inner) w.parentNode.replaceChild(inner, w);
});
}
});
// Block Quill's default image paste (prevents double insert)
var Delta = Quill.import('delta');
state.quill.clipboard.addMatcher('IMG', function(node, delta) {
return new Delta(); // Remove pasted images from Quill's clipboard processing
});
// Handle pasted images ourselves — upload to server
state.quill.root.addEventListener('paste', function(e) {
var clipboardData = e.clipboardData || window.clipboardData; var clipboardData = e.clipboardData || window.clipboardData;
if (!clipboardData || !clipboardData.items) return; if (clipboardData && clipboardData.items) {
for (var i = 0; i < clipboardData.items.length; i++) { for (var i = 0; i < clipboardData.items.length; i++) {
var item = clipboardData.items[i]; if (clipboardData.items[i].type.indexOf('image') !== -1) {
if (item.type.indexOf('image') !== -1) { e.preventDefault();
e.preventDefault(); var file = clipboardData.items[i].getAsFile();
e.stopImmediatePropagation(); if (file) Composer.uploadAndInsertImage(file);
var file = item.getAsFile(); return; // Don't call original paste
if (file) {
var fd = new FormData();
fd.append('image', file, 'pasted-image.png');
api('/api/messages/upload-image', 'POST', fd)
.then(function(result) {
if (result && result.url) {
var range = state.quill.getSelection(true);
state.quill.insertEmbed(range.index, 'image', result.url);
state.quill.setSelection(range.index + 1);
}
})
.catch(function(err) {
var reader = new FileReader();
reader.onload = function(ev) {
var range = state.quill.getSelection(true);
state.quill.insertEmbed(range.index, 'image', ev.target.result);
state.quill.setSelection(range.index + 1);
};
reader.readAsDataURL(file);
});
} }
return;
} }
} }
}, true); // No image — let Quill handle text paste normally
if (originalPaste) originalPaste.call(this, e);
};
// Typing indicator // Typing indicator
state.quill.on('text-change', function () { state.quill.on('text-change', function () {
@ -1401,6 +1338,29 @@
_sendQueue: [], _sendQueue: [],
_queueProcessing: false, _queueProcessing: false,
// Upload image and insert into Quill editor
uploadAndInsertImage: function(file) {
var fd = new FormData();
fd.append('image', file, file.name || 'pasted-image.png');
api('/api/messages/upload-image', 'POST', fd)
.then(function(result) {
if (result && result.url) {
var range = state.quill.getSelection(true) || { index: state.quill.getLength() };
state.quill.insertEmbed(range.index, 'image', result.url);
state.quill.setSelection(range.index + 1);
}
})
.catch(function() {
var reader = new FileReader();
reader.onload = function(ev) {
var range = state.quill.getSelection(true) || { index: state.quill.getLength() };
state.quill.insertEmbed(range.index, 'image', ev.target.result);
state.quill.setSelection(range.index + 1);
};
reader.readAsDataURL(file);
});
},
sendContent: function (html, text) { sendContent: function (html, text) {
if (!state.currentConversationId) return; if (!state.currentConversationId) return;
// Add to queue with current state snapshot // Add to queue with current state snapshot

View File

@ -19,13 +19,23 @@
object-fit: contain; object-fit: contain;
} }
.message-content img:hover { opacity: 0.9; } .message-content img:hover { opacity: 0.9; }
/* Images in Quill editor — resizable */ /* Images in Quill editor — click to select, drag corner to resize */
.ql-editor img { .ql-editor img {
max-width: 100%; max-width: 100%;
max-height: 200px; max-height: 200px;
border-radius: 4px; border-radius: 4px;
cursor: pointer;
}
.ql-editor img:hover {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.ql-editor img.selected {
outline: 2px solid #3b82f6;
outline-offset: 2px;
resize: both; resize: both;
overflow: hidden; overflow: hidden;
display: inline-block;
} }
/* Hide floating buttons that overlap with chat input area */ /* Hide floating buttons that overlap with chat input area */
.pwa-smart-banner, .staging-panel-toggle { display: none !important; } .pwa-smart-banner, .staging-panel-toggle { display: none !important; }