mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-05 03:02:56 +00:00
de day #74
This commit is contained in:
@@ -76,6 +76,8 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
const quillRef = useRef<ReactQuill | null>(null);
|
||||
const toolbarRef = useRef<HTMLDivElement | null>(null);
|
||||
const onChangeRef = useRef(onChange);
|
||||
const selectedImageIdRef = useRef<string | null>(null);
|
||||
const selectImageByIdRef = useRef<(id: string) => void>(() => {});
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
// Ensure component is mounted before rendering Quill
|
||||
@@ -192,6 +194,54 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
},
|
||||
}), [toolbarConfig, onImageUpload, handleImageUpload]);
|
||||
|
||||
// Localize Quill toolbar tooltips/labels to Czech
|
||||
useEffect(() => {
|
||||
if (!isMounted) return;
|
||||
const editor = quillRef.current?.getEditor();
|
||||
if (!editor) return;
|
||||
const container = editor.root?.parentElement; // .ql-container
|
||||
const toolbarEl = container?.previousElementSibling as HTMLElement | null; // .ql-toolbar
|
||||
if (!toolbarEl) return;
|
||||
|
||||
const setTitle = (selector: string, title: string) => {
|
||||
toolbarEl.querySelectorAll(selector).forEach((el) => {
|
||||
(el as HTMLElement).setAttribute('title', title);
|
||||
(el as HTMLElement).setAttribute('aria-label', title);
|
||||
});
|
||||
};
|
||||
|
||||
// Basic formatting
|
||||
setTitle('button.ql-bold', 'Tučné');
|
||||
setTitle('button.ql-italic', 'Kurzíva');
|
||||
setTitle('button.ql-underline', 'Podtržení');
|
||||
setTitle('button.ql-strike', 'Přeškrtnutí');
|
||||
setTitle('button.ql-link', 'Vložit odkaz');
|
||||
setTitle('button.ql-image', 'Vložit obrázek');
|
||||
setTitle('button.ql-blockquote', 'Citace');
|
||||
setTitle('button.ql-clean', 'Vyčistit formátování');
|
||||
|
||||
// Lists
|
||||
setTitle('button.ql-list[value="ordered"]', 'Číslovaný seznam');
|
||||
setTitle('button.ql-list[value="bullet"]', 'Odrážkový seznam');
|
||||
|
||||
// Alignment
|
||||
setTitle('button.ql-align', 'Zarovnání');
|
||||
setTitle('button.ql-align[value=""]', 'Zarovnat vlevo');
|
||||
setTitle('button.ql-align[value="center"]', 'Zarovnat na střed');
|
||||
setTitle('button.ql-align[value="right"]', 'Zarovnat vpravo');
|
||||
setTitle('button.ql-align[value="justify"]', 'Do bloku');
|
||||
|
||||
// Colors and background
|
||||
setTitle('.ql-color .ql-picker-label', 'Barva textu');
|
||||
setTitle('.ql-background .ql-picker-label', 'Barva pozadí');
|
||||
|
||||
// Headers
|
||||
setTitle('.ql-header .ql-picker-label', 'Nadpis');
|
||||
setTitle('.ql-header .ql-picker-item[data-value="1"]', 'Nadpis 1');
|
||||
setTitle('.ql-header .ql-picker-item[data-value="2"]', 'Nadpis 2');
|
||||
setTitle('.ql-header .ql-picker-item[data-value="3"]', 'Nadpis 3');
|
||||
}, [isMounted, toolbar]);
|
||||
|
||||
// Get cropped blob
|
||||
const getCroppedBlob = (image: HTMLImageElement, cropPixels: { x: number; y: number; width: number; height: number }): Promise<Blob> => {
|
||||
const canvas = document.createElement('canvas');
|
||||
@@ -368,18 +418,40 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
const editorRect = editor.root.getBoundingClientRect();
|
||||
const scrollTop = editor.root.scrollTop;
|
||||
const scrollLeft = editor.root.scrollLeft;
|
||||
const sizeLabel = document.createElement('div');
|
||||
sizeLabel.style.cssText = `
|
||||
position: absolute;
|
||||
top: -26px;
|
||||
right: 0;
|
||||
background: rgba(26,32,44,0.9);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
||||
`;
|
||||
const updateSizeLabel = (w: number) => {
|
||||
try {
|
||||
const edW = editor.root.clientWidth || w || 1;
|
||||
const pct = Math.max(1, Math.min(100, Math.round((w / edW) * 100)));
|
||||
sizeLabel.textContent = `${Math.round(w)} px (${pct}%)`;
|
||||
} catch {
|
||||
sizeLabel.textContent = `${Math.round(w)} px`;
|
||||
}
|
||||
};
|
||||
|
||||
// Create edge handles (right, bottom, left, top)
|
||||
const handles = [
|
||||
{ position: 'right', cursor: 'ew-resize', width: '8px', height: '60%' },
|
||||
{ position: 'bottom', cursor: 'ns-resize', width: '60%', height: '8px' },
|
||||
{ position: 'left', cursor: 'ew-resize', width: '8px', height: '60%' },
|
||||
{ position: 'top', cursor: 'ns-resize', width: '60%', height: '8px' },
|
||||
// Corner handles
|
||||
{ position: 'bottom-right', cursor: 'nwse-resize', width: '16px', height: '16px', isCorner: true },
|
||||
{ position: 'bottom-left', cursor: 'nesw-resize', width: '16px', height: '16px', isCorner: true },
|
||||
{ position: 'top-right', cursor: 'nesw-resize', width: '16px', height: '16px', isCorner: true },
|
||||
{ position: 'top-left', cursor: 'nwse-resize', width: '16px', height: '16px', isCorner: true },
|
||||
{ position: 'right', cursor: 'ew-resize', width: '12px', height: '60%' },
|
||||
{ position: 'bottom', cursor: 'ns-resize', width: '60%', height: '12px' },
|
||||
{ position: 'left', cursor: 'ew-resize', width: '12px', height: '60%' },
|
||||
{ position: 'top', cursor: 'ns-resize', width: '60%', height: '12px' },
|
||||
{ position: 'bottom-right', cursor: 'nwse-resize', width: '20px', height: '20px', isCorner: true },
|
||||
{ position: 'bottom-left', cursor: 'nesw-resize', width: '20px', height: '20px', isCorner: true },
|
||||
{ position: 'top-right', cursor: 'nesw-resize', width: '20px', height: '20px', isCorner: true },
|
||||
{ position: 'top-left', cursor: 'nwse-resize', width: '20px', height: '20px', isCorner: true },
|
||||
];
|
||||
|
||||
const updateHandlePositions = () => {
|
||||
@@ -467,7 +539,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
handle.addEventListener('mousedown', (e) => {
|
||||
handle.addEventListener('pointerdown', (e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
@@ -477,26 +549,19 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
startWidth = img.offsetWidth;
|
||||
const startHeight = img.offsetHeight;
|
||||
const aspectRatio = startWidth / startHeight;
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const onPointerMove = (ev: PointerEvent) => {
|
||||
if (!isResizing) return;
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
const deltaX = ev.clientX - startX;
|
||||
const deltaY = ev.clientY - startY;
|
||||
let newWidth = startWidth;
|
||||
|
||||
// Calculate new width based on handle position
|
||||
if (position.includes('right')) {
|
||||
newWidth = startWidth + deltaX;
|
||||
} else if (position.includes('left')) {
|
||||
newWidth = startWidth - deltaX;
|
||||
} else if (position.includes('bottom') || position.includes('top')) {
|
||||
// For vertical handles, maintain aspect ratio
|
||||
newWidth = startWidth + (deltaY * aspectRatio);
|
||||
}
|
||||
|
||||
// Constrain width
|
||||
newWidth = Math.max(50, Math.min(newWidth, editor.root.clientWidth - 40));
|
||||
|
||||
img.style.width = `${newWidth}px`;
|
||||
img.style.maxWidth = '100%';
|
||||
img.style.height = 'auto';
|
||||
@@ -508,25 +573,28 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
setWidthPercent(Math.max(1, Math.min(100, Math.round((newWidth / editorWidth) * 100))));
|
||||
} catch {}
|
||||
updateHandlePositions();
|
||||
updateSizeLabel(newWidth);
|
||||
};
|
||||
|
||||
const onMouseUp: (ev: MouseEvent) => void = () => {
|
||||
const onPointerUp = () => {
|
||||
isResizing = false;
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
document.removeEventListener('pointermove', onPointerMove);
|
||||
document.removeEventListener('pointerup', onPointerUp);
|
||||
onChangeRef.current(editor.root.innerHTML);
|
||||
const id = selectedImageIdRef.current;
|
||||
setTimeout(() => { if (id) { try { selectImageByIdRef.current?.(id); } catch {} } }, 30);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
document.addEventListener('pointermove', onPointerMove);
|
||||
document.addEventListener('pointerup', onPointerUp);
|
||||
});
|
||||
|
||||
container.appendChild(handle);
|
||||
});
|
||||
|
||||
updateHandlePositions();
|
||||
updateSizeLabel(img.offsetWidth || img.width || 0);
|
||||
editor.root.style.position = 'relative';
|
||||
editor.root.appendChild(container);
|
||||
container.appendChild(sizeLabel);
|
||||
resizeHandle = container;
|
||||
|
||||
return container;
|
||||
@@ -547,6 +615,13 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
}
|
||||
|
||||
selectedImage = img;
|
||||
// Ensure image has a persistent ID for reselection after content updates
|
||||
let id = img.getAttribute('data-img-id') || '';
|
||||
if (!id) {
|
||||
id = 'img-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
||||
try { img.setAttribute('data-img-id', id); } catch {}
|
||||
}
|
||||
selectedImageIdRef.current = id;
|
||||
img.style.outline = '3px solid #3182ce';
|
||||
img.style.cursor = 'move';
|
||||
img.style.boxShadow = '0 4px 12px rgba(49, 130, 206, 0.3)';
|
||||
@@ -622,6 +697,16 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
setShowImageToolbar(true);
|
||||
};
|
||||
|
||||
// Expose reselection helper bound to current effect scope
|
||||
selectImageByIdRef.current = (id: string) => {
|
||||
const ed = quillRef.current?.getEditor();
|
||||
if (!ed) return;
|
||||
const node = ed.root.querySelector(`img[data-img-id="${id}"]`) as HTMLImageElement | null;
|
||||
if (node) {
|
||||
selectImage(node);
|
||||
}
|
||||
};
|
||||
|
||||
const deselectImage = () => {
|
||||
if (selectedImage) {
|
||||
selectedImage.style.outline = '';
|
||||
@@ -965,6 +1050,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
// Force overlay reposition
|
||||
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
|
||||
}
|
||||
reselectAfterContentUpdate();
|
||||
|
||||
toast({ title: 'Filtry aplikovány', status: 'success', duration: 2000 });
|
||||
} catch (e: any) {
|
||||
@@ -998,10 +1084,20 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
// Force overlay reposition
|
||||
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
|
||||
}
|
||||
reselectAfterContentUpdate();
|
||||
toast({ title: `Obrázek zarovnán ${alignment === 'left' ? 'vlevo' : alignment === 'center' ? 'na střed' : 'vpravo'}`, status: 'success', duration: 1500 });
|
||||
}
|
||||
}, [selectedImageElement, toast]);
|
||||
|
||||
// Reselect helper after content updates (e.g., when value change triggers rerender)
|
||||
const reselectAfterContentUpdate = useCallback(() => {
|
||||
const id = selectedImageIdRef.current;
|
||||
if (!id) return;
|
||||
setTimeout(() => {
|
||||
try { selectImageByIdRef.current?.(id); } catch {}
|
||||
}, 30);
|
||||
}, []);
|
||||
|
||||
const applyWidthPx = useCallback((px: number, opts?: { silent?: boolean }) => {
|
||||
if (!selectedImageElement) return;
|
||||
const editor = quillRef.current?.getEditor();
|
||||
@@ -1017,6 +1113,8 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
onChangeRef.current(editor.root.innerHTML);
|
||||
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
|
||||
}
|
||||
// Keep selection active for subsequent operations (e.g., 50% → 75%)
|
||||
reselectAfterContentUpdate();
|
||||
if (!opts?.silent) {
|
||||
toast({ title: 'Šířka nastavena', description: `${finalWidth}px`, status: 'success', duration: 1500 });
|
||||
}
|
||||
@@ -1036,6 +1134,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
onChangeRef.current(editor.root.innerHTML);
|
||||
try { editor.root.dispatchEvent(new Event('scroll')); } catch {}
|
||||
}
|
||||
reselectAfterContentUpdate();
|
||||
toast({ title: 'Šířka resetována', status: 'info', duration: 1200 });
|
||||
}, [selectedImageElement, toast]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user