This commit is contained in:
Tomas Dvorak
2025-10-24 14:52:46 +02:00
parent 70ea0c3c91
commit 8a7c292e54
41 changed files with 912 additions and 404 deletions
@@ -126,6 +126,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [imageWidth, setImageWidth] = useState<number>(0);
const [manualWidth, setManualWidth] = useState<string>('');
const [widthPercent, setWidthPercent] = useState<number>(0);
// Define toolbar configurations
const toolbarConfigs = {
@@ -499,12 +500,17 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
img.style.width = `${newWidth}px`;
img.style.maxWidth = '100%';
img.style.height = 'auto';
try { img.setAttribute('width', String(Math.round(newWidth))); } catch {}
setImageWidth(newWidth);
setManualWidth(newWidth.toString());
try {
const editorWidth = editor.root.clientWidth || newWidth || 1;
setWidthPercent(Math.max(1, Math.min(100, Math.round((newWidth / editorWidth) * 100))));
} catch {}
updateHandlePositions();
};
const onMouseUp = () => {
const onMouseUp: (ev: MouseEvent) => void = () => {
isResizing = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
@@ -557,6 +563,10 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
const currentWidth = img.offsetWidth || img.width;
setImageWidth(currentWidth);
setManualWidth(currentWidth.toString());
try {
const editorWidth = editor.root.clientWidth || currentWidth || 1;
setWidthPercent(Math.max(1, Math.min(100, Math.round((currentWidth / editorWidth) * 100))));
} catch {}
// Load saved filters
const filtersData = img.getAttribute('data-filters');
@@ -665,10 +675,59 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
const handleMouseDown = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (target.tagName === 'IMG' && selectedImage === target) {
// Only enable dragging if clicking directly on the image (not on resize handle)
// Allow edge-drag fallback resize if overlay handle doesn't catch it
const rect = target.getBoundingClientRect();
const isNearEdge = (e.clientX > rect.right - 20 || e.clientY > rect.bottom - 20);
if (isNearEdge) return; // Let resize handle take over
const nearLeft = e.clientX < rect.left + 16;
const nearRight = e.clientX > rect.right - 16;
const nearTop = e.clientY < rect.top + 16;
const nearBottom = e.clientY > rect.bottom - 16;
if (nearLeft || nearRight || nearTop || nearBottom) {
e.preventDefault();
e.stopPropagation();
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = (target as HTMLImageElement).offsetWidth;
const startHeight = (target as HTMLImageElement).offsetHeight;
const aspectRatio = startWidth / Math.max(1, startHeight);
const edge = nearRight ? 'right' : nearLeft ? 'left' : nearBottom ? 'bottom' : 'top';
const onMouseMove: (ev: MouseEvent) => void = (ev: MouseEvent) => {
if (!isResizing) return;
const deltaX = ev.clientX - startX;
const deltaY = ev.clientY - startY;
let newWidth = startWidth;
if (edge === 'right') newWidth = startWidth + deltaX;
else if (edge === 'left') newWidth = startWidth - deltaX;
else if (edge === 'bottom') newWidth = startWidth + (deltaY * aspectRatio);
else if (edge === 'top') newWidth = startWidth - (deltaY * aspectRatio);
const maxW = editor.root.clientWidth - 40;
newWidth = Math.max(50, Math.min(newWidth, maxW));
const imgEl = target as HTMLImageElement;
imgEl.style.width = `${newWidth}px`;
imgEl.style.maxWidth = '100%';
imgEl.style.height = 'auto';
try { imgEl.setAttribute('width', String(Math.round(newWidth))); } catch {}
setImageWidth(newWidth);
setManualWidth(String(Math.round(newWidth)));
try {
const editorWidth = editor.root.clientWidth || newWidth || 1;
setWidthPercent(Math.max(1, Math.min(100, Math.round((newWidth / editorWidth) * 100))));
} catch {}
handleScroll();
};
const onMouseUp = () => {
isResizing = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
onChangeRef.current(editor.root.innerHTML);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
return;
}
e.preventDefault();
e.stopPropagation();
@@ -690,7 +749,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
// Already set in selectImage, but ensure it's off
target.setAttribute('draggable', 'false');
const onMouseMove = (e: MouseEvent) => {
const onMouseMove: (e: MouseEvent) => void = (e: MouseEvent) => {
if (!isDragging || !selectedImage) return;
const deltaX = e.clientX - startX;
@@ -718,7 +777,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
}
};
const onMouseUp = () => {
const onMouseUp: (e: MouseEvent) => void = () => {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
@@ -943,30 +1002,74 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
}
}, [selectedImageElement, toast]);
const applyWidthPx = useCallback((px: number, opts?: { silent?: boolean }) => {
if (!selectedImageElement) return;
const editor = quillRef.current?.getEditor();
const maxWidth = editor ? editor.root.clientWidth - 40 : 1200;
const finalWidth = Math.min(Math.max(50, Math.round(px)), maxWidth);
selectedImageElement.style.width = `${finalWidth}px`;
selectedImageElement.style.height = 'auto';
selectedImageElement.style.maxWidth = '100%';
selectedImageElement.setAttribute('width', String(finalWidth));
setImageWidth(finalWidth);
setManualWidth(finalWidth.toString());
if (editor) {
onChangeRef.current(editor.root.innerHTML);
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
}
if (!opts?.silent) {
toast({ title: 'Šířka nastavena', description: `${finalWidth}px`, status: 'success', duration: 1500 });
}
}, [selectedImageElement, toast]);
const resetWidth = useCallback(() => {
if (!selectedImageElement) return;
const editor = quillRef.current?.getEditor();
selectedImageElement.style.width = '';
selectedImageElement.style.height = '';
selectedImageElement.style.maxWidth = '100%';
selectedImageElement.removeAttribute('width');
const currentWidth = selectedImageElement.offsetWidth || selectedImageElement.width || 0;
setImageWidth(currentWidth);
setManualWidth('');
if (editor) {
onChangeRef.current(editor.root.innerHTML);
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
}
toast({ title: 'Šířka resetována', status: 'info', duration: 1200 });
}, [selectedImageElement, toast]);
const applyPercent = useCallback((percent: number, opts?: { silent?: boolean }) => {
const clamped = Math.max(5, Math.min(100, Math.round(percent)));
setWidthPercent(clamped);
const editor = quillRef.current?.getEditor();
if (editor && selectedImageElement) {
const px = (editor.root.clientWidth * clamped) / 100;
applyWidthPx(px, opts);
}
}, [applyWidthPx, selectedImageElement]);
// Set manual width
const applyManualWidth = useCallback(() => {
if (selectedImageElement && manualWidth) {
const width = parseInt(manualWidth);
if (!isNaN(width) && width > 0) {
const raw = manualWidth.trim();
if (raw.endsWith('%')) {
const percent = parseFloat(raw.slice(0, -1));
const editor = quillRef.current?.getEditor();
const maxWidth = editor ? editor.root.clientWidth - 40 : 1200;
const finalWidth = Math.min(Math.max(50, width), maxWidth);
selectedImageElement.style.width = `${finalWidth}px`;
selectedImageElement.style.height = 'auto';
selectedImageElement.style.maxWidth = '100%';
setImageWidth(finalWidth);
setManualWidth(finalWidth.toString());
if (editor) {
onChangeRef.current(editor.root.innerHTML);
// Force overlay reposition
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
if (editor && !isNaN(percent) && percent > 0) {
const px = (editor.root.clientWidth * percent) / 100;
applyWidthPx(px);
return;
}
toast({ title: 'Šířka nastavena', description: `${finalWidth}px`, status: 'success', duration: 1500 });
}
const width = parseInt(raw, 10);
if (!isNaN(width) && width > 0) {
applyWidthPx(width);
} else {
toast({ title: 'Neplatná šířka', description: 'Zadejte kladné číslo', status: 'warning', duration: 1500 });
toast({ title: 'Neplatná šířka', description: 'Zadejte kladné číslo nebo procenta (např. 50%)', status: 'warning', duration: 1500 });
}
}
}, [selectedImageElement, manualWidth, toast]);
}, [selectedImageElement, manualWidth, toast, applyWidthPx]);
// Delete selected image
const deleteSelectedImage = useCallback(() => {
@@ -1329,7 +1432,8 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
</HStack>
</VStack>
{/* Width Control */}
{/* Width Control */
}
<VStack align="stretch" spacing={2}>
<Text fontSize="xs" fontWeight="semibold" color="gray.600">Šířka obrázku</Text>
<HStack spacing={2}>
@@ -1351,7 +1455,28 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
Nastavit
</Button>
</HStack>
<Text fontSize="xs" color="gray.500">Aktuální: {imageWidth}px</Text>
<Text fontSize="xs" color="gray.500">Aktuální: {imageWidth}px ({widthPercent || 0}%)</Text>
<HStack spacing={2}>
<Button size="xs" variant="outline" onClick={() => applyPercent(25, { silent: true })}>25%</Button>
<Button size="xs" variant="outline" onClick={() => applyPercent(50, { silent: true })}>50%</Button>
<Button size="xs" variant="outline" onClick={() => applyPercent(75, { silent: true })}>75%</Button>
<Button size="xs" variant="outline" onClick={() => applyPercent(100, { silent: true })}>100%</Button>
<Button size="xs" colorScheme="gray" variant="ghost" onClick={resetWidth}>Reset</Button>
</HStack>
<FormControl>
<HStack justify="space-between">
<FormLabel fontSize="xs" mb={0}>Šířka (%)</FormLabel>
<Text fontSize="xs" color="gray.500">{widthPercent || 0}%</Text>
</HStack>
<input
type="range"
min="5"
max="100"
value={widthPercent || 0}
onChange={(e) => applyPercent(Number(e.target.value), { silent: true })}
style={{ width: '100%' }}
/>
</FormControl>
</VStack>
{/* Transform Buttons */}