This commit is contained in:
Tomas Dvorak
2025-10-23 22:26:50 +02:00
parent 63700eedb2
commit 70ea0c3c91
75 changed files with 3337 additions and 1160 deletions
@@ -36,6 +36,7 @@ import {
} from 'lucide-react';
import { Image as ChakraImage } from '@chakra-ui/react';
import { cropAndUpload, quickEditImage } from '../../services/imageProcessing';
import { assetUrl } from '../../utils/url';
interface ImageFilters {
brightness: number;
@@ -245,28 +246,36 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
return;
}
// Calculate crop data in pixels
// Calculate crop data in natural image pixels (backend expects absolute pixels)
const img = imgRef.current;
const percToPx = (val: number, size: number) => (crop.unit === '%' ? (val / 100) * size : val);
const displayW = img.width;
const displayH = img.height;
const naturalW = img.naturalWidth || displayW;
const naturalH = img.naturalHeight || displayH;
const scaleX = naturalW / Math.max(1, displayW);
const scaleY = naturalH / Math.max(1, displayH);
const toDisplayPx = (val: number, size: number) => (crop.unit === '%' ? (val / 100) * size : val);
let cropData = undefined;
if (crop.width && crop.height && crop.width > 0 && crop.height > 0) {
const cropPx = {
x: Math.round(Math.max(0, percToPx(crop.x || 0, img.width))),
y: Math.round(Math.max(0, percToPx(crop.y || 0, img.height))),
width: Math.round(Math.min(img.width, percToPx(crop.width || img.width, img.width))),
height: Math.round(Math.min(img.height, percToPx(crop.height || img.height, img.height))),
};
// Adjust crop to fit image bounds
if (cropPx.x + cropPx.width > img.width) {
cropPx.width = img.width - cropPx.x;
}
if (cropPx.y + cropPx.height > img.height) {
cropPx.height = img.height - cropPx.y;
}
cropData = cropPx;
// Convert selection from displayed coordinates to natural pixel coordinates
const dispX = Math.max(0, toDisplayPx(crop.x || 0, displayW));
const dispY = Math.max(0, toDisplayPx(crop.y || 0, displayH));
const dispW = Math.min(displayW, toDisplayPx(crop.width || displayW, displayW));
const dispH = Math.min(displayH, toDisplayPx(crop.height || displayH, displayH));
let natX = Math.round(dispX * scaleX);
let natY = Math.round(dispY * scaleY);
let natW = Math.round(dispW * scaleX);
let natH = Math.round(dispH * scaleY);
// Clamp within natural bounds
if (natX + natW > naturalW) natW = naturalW - natX;
if (natY + natH > naturalH) natH = naturalH - natY;
natW = Math.max(1, natW);
natH = Math.max(1, natH);
cropData = { x: natX, y: natY, width: natW, height: natH };
}
toast({ title: 'Zpracování obrázku...', status: 'info', duration: 2000 });
@@ -290,16 +299,25 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
const range = quill.getSelection();
const index = range ? range.index : quill.getLength();
// Insert the image
quill.insertEmbed(index, 'image', res.url, 'api');
// Move cursor after the image
quill.setSelection(index + 1, 0, 'api');
// Force content change to trigger re-render
onChangeRef.current(quill.root.innerHTML);
toast({ title: 'Obrázek vložen', status: 'success', duration: 2000 });
const absoluteUrl = assetUrl(res.url) || res.url;
const img = new Image();
img.onload = () => {
try {
quill.insertEmbed(index, 'image', absoluteUrl, 'api');
// Move cursor after the image
quill.setSelection(index + 1, 0, 'api');
// Force content change to trigger re-render
onChangeRef.current(quill.root.innerHTML);
toast({ title: 'Obrázek vložen', status: 'success', duration: 2000 });
} catch (e) {
console.error('Insert after preload error:', e);
toast({ title: 'Chyba při vkládání obrázku', description: String(e), status: 'error' });
}
};
img.onerror = () => {
toast({ title: 'Obrázek nelze načíst', description: absoluteUrl, status: 'error' });
};
img.src = absoluteUrl;
} catch (embedError) {
console.error('Error inserting image:', embedError);
toast({ title: 'Chyba při vkládání obrázku', description: String(embedError), status: 'error' });
@@ -716,6 +734,12 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
// Delete selected image on Delete key
const handleKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement | null;
const tag = target?.tagName;
// Do not act on Delete/Backspace if user is typing in an input, textarea, or contentEditable
if (tag === 'INPUT' || tag === 'TEXTAREA' || (target && (target as any).isContentEditable)) {
return;
}
if (selectedImage && (e.key === 'Delete' || e.key === 'Backspace')) {
e.preventDefault();
selectedImage.remove();
@@ -754,6 +778,9 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
editor.root.addEventListener('scroll', handleScroll);
editor.root.addEventListener('dragstart', handleDragStart);
document.addEventListener('keydown', handleKeyDown);
// Also reposition on window resize and any document scroll (capture phase)
window.addEventListener('resize', handleScroll);
document.addEventListener('scroll', handleScroll, true);
return () => {
editor.root.removeEventListener('click', handleImageClick);
@@ -761,10 +788,12 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
editor.root.removeEventListener('scroll', handleScroll);
editor.root.removeEventListener('dragstart', handleDragStart);
document.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('resize', handleScroll);
document.removeEventListener('scroll', handleScroll, true);
removeResizeHandle();
deselectImage();
};
}, [readOnly, toast]);
}, [readOnly, toast, isMounted]);
// Apply filters to selected image
const applyFiltersToImage = useCallback((img: HTMLImageElement, filters: ImageFilters) => {
@@ -850,7 +879,8 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
});
// Replace image src
selectedImageElement.src = res.url;
const absoluteUrl = assetUrl(res.url) || res.url;
selectedImageElement.src = absoluteUrl;
// Reset filters to default since they're now baked into the image
setImageFilters({
@@ -873,6 +903,8 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
const editor = quillRef.current?.getEditor();
if (editor) {
onChangeRef.current(editor.root.innerHTML);
// Force overlay reposition
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
}
toast({ title: 'Filtry aplikovány', status: 'success', duration: 2000 });
@@ -904,6 +936,8 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
const editor = quillRef.current?.getEditor();
if (editor) {
onChangeRef.current(editor.root.innerHTML);
// Force overlay reposition
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
}
toast({ title: `Obrázek zarovnán ${alignment === 'left' ? 'vlevo' : alignment === 'center' ? 'na střed' : 'vpravo'}`, status: 'success', duration: 1500 });
}
@@ -924,6 +958,8 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
setManualWidth(finalWidth.toString());
if (editor) {
onChangeRef.current(editor.root.innerHTML);
// Force overlay reposition
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
}
toast({ title: 'Šířka nastavena', description: `${finalWidth}px`, status: 'success', duration: 1500 });
} else {
@@ -1218,9 +1254,9 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
maxH="80vh"
overflowY="auto"
pointerEvents="auto"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
onMouseUp={(e) => { e.preventDefault(); e.stopPropagation(); }}
onClick={(e) => { e.stopPropagation(); }}
onMouseDown={(e) => { e.stopPropagation(); }}
onMouseUp={(e) => { e.stopPropagation(); }}
css={{
'&::-webkit-scrollbar': {
width: '6px',
@@ -1302,7 +1338,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
type="number"
value={manualWidth}
onChange={(e) => setManualWidth(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && applyManualWidth()}
onKeyDown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); applyManualWidth(); } }}
placeholder="Šířka v px"
min={50}
/>