mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 10:42:57 +00:00
dev day #69
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user