-
This commit is contained in:
35
CLAUDE.md
35
CLAUDE.md
@@ -46,12 +46,25 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
|
|||||||
|
|
||||||
## Quick Start - Session Recovery
|
## Quick Start - Session Recovery
|
||||||
|
|
||||||
**Ultima sessione:** 28 Novembre 2025 (notte)
|
**Ultima sessione:** 29 Novembre 2025
|
||||||
|
|
||||||
**Stato progetto:** Migrazione Oracle APEX → .NET + React TypeScript in corso
|
**Stato progetto:** Migrazione Oracle APEX → .NET + React TypeScript in corso
|
||||||
|
|
||||||
**Lavoro completato nell'ultima sessione:**
|
**Lavoro completato nell'ultima sessione:**
|
||||||
|
|
||||||
|
- **NUOVA FEATURE: Selezione Multipla nel Report Editor** - COMPLETATO
|
||||||
|
- Implementato sistema di selezione multipla personalizzato (senza usare ActiveSelection di Fabric.js che causava riposizionamento oggetti)
|
||||||
|
- **Selezione con rettangolo di trascinamento**: trascinando sul canvas vuoto appare rettangolo blu tratteggiato, al rilascio seleziona tutti gli oggetti che intersecano
|
||||||
|
- **Shift+click**: aggiunge/rimuove oggetti dalla selezione
|
||||||
|
- **Spostamento multiplo**: quando più oggetti sono selezionati, trascinandone uno si spostano tutti insieme
|
||||||
|
- **Feedback visivo**: oggetti selezionati mostrano bordo blu (#1976d2) e ombra
|
||||||
|
- **Gestione corretta degli eventi**: i ref (`selectedElementIdsRef`, `onSelectElementRef`, etc.) evitano stale closures negli event handler
|
||||||
|
- **File modificati:**
|
||||||
|
- `EditorCanvas.tsx` - Nuovi handler `handleMouseDown`, `handleMouseUp`, logica selezione multipla, refs per valori correnti
|
||||||
|
- `ReportEditorPage.tsx` - Cambiato `selectedElementId: string | null` → `selectedElementIds: string[]`, aggiornati tutti i riferimenti
|
||||||
|
|
||||||
|
**Lavoro completato nelle sessioni precedenti (28 Novembre 2025 notte):**
|
||||||
|
|
||||||
- **NUOVA FEATURE: Sincronizzazione Real-Time Efficiente** - COMPLETATO
|
- **NUOVA FEATURE: Sincronizzazione Real-Time Efficiente** - COMPLETATO
|
||||||
- **Prima:** Al salvataggio veniva inviata solo una notifica `DataSaved`, l'altra sessione ricaricava il template dal server (lento)
|
- **Prima:** Al salvataggio veniva inviata solo una notifica `DataSaved`, l'altra sessione ricaricava il template dal server (lento)
|
||||||
- **Dopo:** Al salvataggio viene inviato l'intero template via SignalR (`BroadcastTemplateSync`), l'altra sessione lo applica direttamente (istantaneo)
|
- **Dopo:** Al salvataggio viene inviato l'intero template via SignalR (`BroadcastTemplateSync`), l'altra sessione lo applica direttamente (istantaneo)
|
||||||
@@ -1141,6 +1154,26 @@ frontend/src/
|
|||||||
- Check `isPending` spostato dentro il callback del setTimeout
|
- Check `isPending` spostato dentro il callback del setTimeout
|
||||||
- **File:** `ReportEditorPage.tsx`
|
- **File:** `ReportEditorPage.tsx`
|
||||||
|
|
||||||
|
26. **Selezione Multipla Fabric.js - Riposizionamento Oggetti (FIX 29/11/2025):**
|
||||||
|
- **Problema:** Usando `ActiveSelection` di Fabric.js per la selezione multipla, gli oggetti venivano riposizionati/spostati quando selezionati
|
||||||
|
- **Causa:** `ActiveSelection` raggruppa gli oggetti e le loro coordinate diventano relative al centro del gruppo. Inoltre, ricreando l'`ActiveSelection` nell'effect quando cambiava `selectedElementIds`, gli oggetti venivano spostati
|
||||||
|
- **Soluzione:** Sistema di selezione multipla completamente personalizzato:
|
||||||
|
- Disabilitata selezione nativa di Fabric.js (`selection: false` nel canvas)
|
||||||
|
- Implementato `handleMouseDown` per:
|
||||||
|
- Click su oggetto → selezione singola
|
||||||
|
- Shift+click → aggiunge/rimuove dalla selezione
|
||||||
|
- Click su canvas vuoto → inizio rettangolo di selezione
|
||||||
|
- Click su oggetto già selezionato (multi) → inizio drag multiplo
|
||||||
|
- Implementato `handleMouseMove` per:
|
||||||
|
- Disegno rettangolo di selezione (Rect blu tratteggiato)
|
||||||
|
- Spostamento multiplo oggetti (aggiorna `left`/`top` di ogni oggetto)
|
||||||
|
- Implementato `handleMouseUp` per:
|
||||||
|
- Fine rettangolo → calcola intersezione e seleziona oggetti
|
||||||
|
- Fine drag multiplo → aggiorna template con nuove posizioni
|
||||||
|
- Feedback visivo: bordo blu e ombra sugli oggetti selezionati (invece di ActiveSelection)
|
||||||
|
- Usati refs (`selectedElementIdsRef`, `onSelectElementRef`, etc.) per evitare stale closures negli event handler registrati una sola volta
|
||||||
|
- **File:** `EditorCanvas.tsx`, `ReportEditorPage.tsx`
|
||||||
|
|
||||||
### Schema Database Report System
|
### Schema Database Report System
|
||||||
|
|
||||||
Le tabelle sono già nel DbContext (`AppollinareDbContext.cs`):
|
Le tabelle sono già nel DbContext (`AppollinareDbContext.cs`):
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ export interface RemoteCursor {
|
|||||||
|
|
||||||
interface EditorCanvasProps {
|
interface EditorCanvasProps {
|
||||||
template: AprtTemplate;
|
template: AprtTemplate;
|
||||||
selectedElementId: string | null;
|
selectedElementIds: string[];
|
||||||
onSelectElement: (id: string | null) => void;
|
onSelectElement: (ids: string[]) => void;
|
||||||
onUpdateElement: (id: string, updates: Partial<AprtElement>) => void;
|
onUpdateElement: (id: string, updates: Partial<AprtElement>) => void;
|
||||||
/** Called when a drag/resize operation completes - use to commit to history */
|
/** Called when a drag/resize operation completes - use to commit to history */
|
||||||
onUpdateElementComplete?: () => void;
|
onUpdateElementComplete?: () => void;
|
||||||
@@ -78,7 +78,7 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
|
|||||||
(
|
(
|
||||||
{
|
{
|
||||||
template,
|
template,
|
||||||
selectedElementId,
|
selectedElementIds,
|
||||||
onSelectElement,
|
onSelectElement,
|
||||||
onUpdateElement,
|
onUpdateElement,
|
||||||
onUpdateElementComplete,
|
onUpdateElementComplete,
|
||||||
@@ -106,6 +106,28 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
|
|||||||
const loadingImagesRef = useRef<Set<string>>(new Set()); // elementIds currently being loaded
|
const loadingImagesRef = useRef<Set<string>>(new Set()); // elementIds currently being loaded
|
||||||
const templateElementsRef = useRef(template.elements); // Ref for accessing current elements in callbacks
|
const templateElementsRef = useRef(template.elements); // Ref for accessing current elements in callbacks
|
||||||
|
|
||||||
|
// Custom multi-selection state
|
||||||
|
const isDrawingSelectionRef = useRef(false);
|
||||||
|
const selectionStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
|
const selectionRectRef = useRef<fabric.Rect | null>(null);
|
||||||
|
|
||||||
|
// Track selected objects for multi-move
|
||||||
|
const multiSelectedObjectsRef = useRef<FabricObjectWithData[]>([]);
|
||||||
|
const isDraggingMultiRef = useRef(false);
|
||||||
|
const dragStartPosRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
|
|
||||||
|
// Refs to access current values in event handlers (avoids stale closures)
|
||||||
|
const selectedElementIdsRef = useRef(selectedElementIds);
|
||||||
|
selectedElementIdsRef.current = selectedElementIds;
|
||||||
|
const onSelectElementRef = useRef(onSelectElement);
|
||||||
|
onSelectElementRef.current = onSelectElement;
|
||||||
|
const onUpdateElementRef = useRef(onUpdateElement);
|
||||||
|
onUpdateElementRef.current = onUpdateElement;
|
||||||
|
const onUpdateElementCompleteRef = useRef(onUpdateElementComplete);
|
||||||
|
onUpdateElementCompleteRef.current = onUpdateElementComplete;
|
||||||
|
const zoomRef = useRef(zoom);
|
||||||
|
zoomRef.current = zoom;
|
||||||
|
|
||||||
// Keep templateElementsRef in sync
|
// Keep templateElementsRef in sync
|
||||||
templateElementsRef.current = template.elements;
|
templateElementsRef.current = template.elements;
|
||||||
|
|
||||||
@@ -167,18 +189,49 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
|
|||||||
// Event handlers
|
// Event handlers
|
||||||
const handleSelectionCreated = useCallback(
|
const handleSelectionCreated = useCallback(
|
||||||
(e: { selected: fabric.FabricObject[] }) => {
|
(e: { selected: fabric.FabricObject[] }) => {
|
||||||
const selected = e.selected?.[0] as FabricObjectWithData | undefined;
|
// Ignore if we're handling multi-selection manually
|
||||||
if (selected?.data?.id) {
|
if (isDraggingMultiRef.current || isDrawingSelectionRef.current) {
|
||||||
onSelectElement(selected.data.id as string);
|
return;
|
||||||
|
}
|
||||||
|
// Ignore if we already have multi-selection (don't let Fabric override it)
|
||||||
|
if (selectedElementIdsRef.current.length > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIds: string[] = [];
|
||||||
|
if (e.selected) {
|
||||||
|
for (const obj of e.selected) {
|
||||||
|
const objData = (obj as FabricObjectWithData).data;
|
||||||
|
if (
|
||||||
|
objData?.id &&
|
||||||
|
!objData.isGrid &&
|
||||||
|
!objData.isMargin &&
|
||||||
|
!objData.isGuide
|
||||||
|
) {
|
||||||
|
selectedIds.push(objData.id as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectedIds.length > 0) {
|
||||||
|
onSelectElementRef.current(selectedIds);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onSelectElement],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectionCleared = useCallback(() => {
|
const handleSelectionCleared = useCallback(() => {
|
||||||
onSelectElement(null);
|
// Ignore if we're handling multi-selection manually
|
||||||
|
if (isDraggingMultiRef.current || isDrawingSelectionRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ignore if we have multi-selection (don't let Fabric clear it)
|
||||||
|
if (selectedElementIdsRef.current.length > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectElementRef.current([]);
|
||||||
clearGuideLines();
|
clearGuideLines();
|
||||||
}, [onSelectElement, clearGuideLines]);
|
}, [clearGuideLines]);
|
||||||
|
|
||||||
const handleObjectMoving = useCallback(
|
const handleObjectMoving = useCallback(
|
||||||
(e: fabric.TEvent<MouseEvent> & { target?: fabric.FabricObject }) => {
|
(e: fabric.TEvent<MouseEvent> & { target?: fabric.FabricObject }) => {
|
||||||
@@ -661,10 +714,101 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Custom multi-selection: mouse down handler
|
||||||
|
const handleMouseDown = useCallback(
|
||||||
|
(e: { e: MouseEvent; target?: fabric.FabricObject }) => {
|
||||||
|
if (!fabricRef.current) return;
|
||||||
|
const canvas = fabricRef.current;
|
||||||
|
const pointer = canvas.getScenePoint(e.e);
|
||||||
|
|
||||||
|
// If clicking on an object, handle normal selection or multi-drag
|
||||||
|
if (e.target) {
|
||||||
|
const objData = (e.target as FabricObjectWithData).data;
|
||||||
|
if (
|
||||||
|
objData?.id &&
|
||||||
|
!objData.isGrid &&
|
||||||
|
!objData.isMargin &&
|
||||||
|
!objData.isGuide
|
||||||
|
) {
|
||||||
|
const currentSelectedIds = selectedElementIdsRef.current;
|
||||||
|
|
||||||
|
// Check if this object is already in multi-selection
|
||||||
|
if (
|
||||||
|
currentSelectedIds.length > 1 &&
|
||||||
|
currentSelectedIds.includes(objData.id as string)
|
||||||
|
) {
|
||||||
|
// Start dragging all selected objects
|
||||||
|
isDraggingMultiRef.current = true;
|
||||||
|
dragStartPosRef.current = { x: pointer.x, y: pointer.y };
|
||||||
|
|
||||||
|
// Store references to all selected objects
|
||||||
|
multiSelectedObjectsRef.current = [];
|
||||||
|
for (const id of currentSelectedIds) {
|
||||||
|
const obj = elementsMapRef.current.get(id);
|
||||||
|
if (obj) {
|
||||||
|
multiSelectedObjectsRef.current.push(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift+click to add/remove from selection
|
||||||
|
if (e.e.shiftKey) {
|
||||||
|
if (currentSelectedIds.includes(objData.id as string)) {
|
||||||
|
// Remove from selection
|
||||||
|
onSelectElementRef.current(
|
||||||
|
currentSelectedIds.filter((id) => id !== objData.id),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Add to selection
|
||||||
|
onSelectElementRef.current([
|
||||||
|
...currentSelectedIds,
|
||||||
|
objData.id as string,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal click on object - select just this one
|
||||||
|
onSelectElementRef.current([objData.id as string]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking on empty canvas - clear selection and start drawing selection rectangle
|
||||||
|
onSelectElementRef.current([]);
|
||||||
|
isDrawingSelectionRef.current = true;
|
||||||
|
selectionStartRef.current = { x: pointer.x, y: pointer.y };
|
||||||
|
|
||||||
|
// Create selection rectangle
|
||||||
|
const selectionRect = new fabric.Rect({
|
||||||
|
left: pointer.x,
|
||||||
|
top: pointer.y,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
fill: "rgba(25, 118, 210, 0.1)",
|
||||||
|
stroke: "#1976d2",
|
||||||
|
strokeWidth: 1,
|
||||||
|
strokeDashArray: [4, 4],
|
||||||
|
selectable: false,
|
||||||
|
evented: false,
|
||||||
|
excludeFromExport: true,
|
||||||
|
});
|
||||||
|
(selectionRect as FabricObjectWithData).data = {
|
||||||
|
isSelectionRect: true,
|
||||||
|
};
|
||||||
|
selectionRectRef.current = selectionRect;
|
||||||
|
canvas.add(selectionRect);
|
||||||
|
canvas.renderAll();
|
||||||
|
},
|
||||||
|
[], // No dependencies - uses refs
|
||||||
|
);
|
||||||
|
|
||||||
const handleMouseMove = useCallback(
|
const handleMouseMove = useCallback(
|
||||||
(e: { e: MouseEvent }) => {
|
(e: { e: MouseEvent }) => {
|
||||||
if (!fabricRef.current) return;
|
if (!fabricRef.current) return;
|
||||||
const pointer = fabricRef.current.getScenePoint(e.e);
|
const canvas = fabricRef.current;
|
||||||
|
const pointer = canvas.getScenePoint(e.e);
|
||||||
const xMm = Math.round((pxToMm(pointer.x) / zoom) * 10) / 10;
|
const xMm = Math.round((pxToMm(pointer.x) / zoom) * 10) / 10;
|
||||||
const yMm = Math.round((pxToMm(pointer.y) / zoom) * 10) / 10;
|
const yMm = Math.round((pxToMm(pointer.y) / zoom) * 10) / 10;
|
||||||
setCursorPosition({ x: xMm, y: yMm });
|
setCursorPosition({ x: xMm, y: yMm });
|
||||||
@@ -673,10 +817,139 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
|
|||||||
if (onCursorMove) {
|
if (onCursorMove) {
|
||||||
onCursorMove(xMm, yMm);
|
onCursorMove(xMm, yMm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle multi-drag
|
||||||
|
if (isDraggingMultiRef.current && dragStartPosRef.current) {
|
||||||
|
const dx = pointer.x - dragStartPosRef.current.x;
|
||||||
|
const dy = pointer.y - dragStartPosRef.current.y;
|
||||||
|
|
||||||
|
for (const obj of multiSelectedObjectsRef.current) {
|
||||||
|
obj.set({
|
||||||
|
left: (obj.left || 0) + dx,
|
||||||
|
top: (obj.top || 0) + dy,
|
||||||
|
});
|
||||||
|
obj.setCoords();
|
||||||
|
}
|
||||||
|
|
||||||
|
dragStartPosRef.current = { x: pointer.x, y: pointer.y };
|
||||||
|
canvas.renderAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle selection rectangle drawing
|
||||||
|
if (
|
||||||
|
isDrawingSelectionRef.current &&
|
||||||
|
selectionStartRef.current &&
|
||||||
|
selectionRectRef.current
|
||||||
|
) {
|
||||||
|
const startX = selectionStartRef.current.x;
|
||||||
|
const startY = selectionStartRef.current.y;
|
||||||
|
|
||||||
|
const left = Math.min(startX, pointer.x);
|
||||||
|
const top = Math.min(startY, pointer.y);
|
||||||
|
const width = Math.abs(pointer.x - startX);
|
||||||
|
const height = Math.abs(pointer.y - startY);
|
||||||
|
|
||||||
|
selectionRectRef.current.set({
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[zoom, onCursorMove],
|
[zoom, onCursorMove],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Custom multi-selection: mouse up handler
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
if (!fabricRef.current) return;
|
||||||
|
const canvas = fabricRef.current;
|
||||||
|
|
||||||
|
// Handle end of multi-drag
|
||||||
|
if (isDraggingMultiRef.current) {
|
||||||
|
isDraggingMultiRef.current = false;
|
||||||
|
dragStartPosRef.current = null;
|
||||||
|
|
||||||
|
// Update all moved objects in the template
|
||||||
|
for (const obj of multiSelectedObjectsRef.current) {
|
||||||
|
const objData = obj.data;
|
||||||
|
if (objData?.id) {
|
||||||
|
const scaleX = obj.scaleX || 1;
|
||||||
|
const scaleY = obj.scaleY || 1;
|
||||||
|
const finalWidth = (obj.width || 0) * scaleX;
|
||||||
|
const finalHeight = (obj.height || 0) * scaleY;
|
||||||
|
|
||||||
|
onUpdateElementRef.current(objData.id as string, {
|
||||||
|
position: {
|
||||||
|
x: pxToMm(obj.left || 0) / zoomRef.current,
|
||||||
|
y: pxToMm(obj.top || 0) / zoomRef.current,
|
||||||
|
width: pxToMm(finalWidth) / zoomRef.current,
|
||||||
|
height: pxToMm(finalHeight) / zoomRef.current,
|
||||||
|
rotation: obj.angle || 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit to history
|
||||||
|
onUpdateElementCompleteRef.current?.();
|
||||||
|
multiSelectedObjectsRef.current = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle end of selection rectangle
|
||||||
|
if (isDrawingSelectionRef.current && selectionRectRef.current) {
|
||||||
|
const selRect = selectionRectRef.current;
|
||||||
|
const rectLeft = selRect.left || 0;
|
||||||
|
const rectTop = selRect.top || 0;
|
||||||
|
const rectRight = rectLeft + (selRect.width || 0);
|
||||||
|
const rectBottom = rectTop + (selRect.height || 0);
|
||||||
|
|
||||||
|
// Find all objects within the selection rectangle
|
||||||
|
const selectedIds: string[] = [];
|
||||||
|
const objects = canvas.getObjects();
|
||||||
|
|
||||||
|
for (const obj of objects) {
|
||||||
|
const objData = (obj as FabricObjectWithData).data;
|
||||||
|
if (
|
||||||
|
objData?.id &&
|
||||||
|
!objData.isGrid &&
|
||||||
|
!objData.isMargin &&
|
||||||
|
!objData.isGuide &&
|
||||||
|
!objData.isSelectionRect
|
||||||
|
) {
|
||||||
|
const objLeft = obj.left || 0;
|
||||||
|
const objTop = obj.top || 0;
|
||||||
|
const objRight = objLeft + (obj.width || 0) * (obj.scaleX || 1);
|
||||||
|
const objBottom = objTop + (obj.height || 0) * (obj.scaleY || 1);
|
||||||
|
|
||||||
|
// Check if object intersects with selection rectangle
|
||||||
|
if (
|
||||||
|
objLeft < rectRight &&
|
||||||
|
objRight > rectLeft &&
|
||||||
|
objTop < rectBottom &&
|
||||||
|
objBottom > rectTop
|
||||||
|
) {
|
||||||
|
selectedIds.push(objData.id as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove selection rectangle
|
||||||
|
canvas.remove(selRect);
|
||||||
|
selectionRectRef.current = null;
|
||||||
|
isDrawingSelectionRef.current = false;
|
||||||
|
selectionStartRef.current = null;
|
||||||
|
|
||||||
|
// Update selection
|
||||||
|
onSelectElementRef.current(selectedIds);
|
||||||
|
|
||||||
|
canvas.renderAll();
|
||||||
|
}
|
||||||
|
}, []); // No dependencies - uses refs
|
||||||
|
|
||||||
// Keyboard navigation
|
// Keyboard navigation
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
@@ -741,7 +1014,7 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
|
|||||||
width: canvasWidth * zoom,
|
width: canvasWidth * zoom,
|
||||||
height: canvasHeight * zoom,
|
height: canvasHeight * zoom,
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: "#ffffff",
|
||||||
selection: true,
|
selection: false, // Disable native multi-selection (causes object repositioning issues)
|
||||||
preserveObjectStacking: true,
|
preserveObjectStacking: true,
|
||||||
uniformScaling: false, // Allow free resize from corners (not locked to aspect ratio)
|
uniformScaling: false, // Allow free resize from corners (not locked to aspect ratio)
|
||||||
});
|
});
|
||||||
@@ -770,6 +1043,8 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
|
|||||||
canvas.on("mouse:over", handleMouseOver as any);
|
canvas.on("mouse:over", handleMouseOver as any);
|
||||||
canvas.on("mouse:out", handleMouseOut as any);
|
canvas.on("mouse:out", handleMouseOut as any);
|
||||||
canvas.on("mouse:move", handleMouseMove as any);
|
canvas.on("mouse:move", handleMouseMove as any);
|
||||||
|
canvas.on("mouse:down", handleMouseDown as any);
|
||||||
|
canvas.on("mouse:up", handleMouseUp as any);
|
||||||
canvas.on("text:changed", handleTextChanged as any);
|
canvas.on("text:changed", handleTextChanged as any);
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
@@ -784,6 +1059,8 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
|
|||||||
canvas.off("mouse:over");
|
canvas.off("mouse:over");
|
||||||
canvas.off("mouse:out");
|
canvas.off("mouse:out");
|
||||||
canvas.off("mouse:move");
|
canvas.off("mouse:move");
|
||||||
|
canvas.off("mouse:down");
|
||||||
|
canvas.off("mouse:up");
|
||||||
canvas.off("text:changed");
|
canvas.off("text:changed");
|
||||||
window.removeEventListener("keydown", handleKeyDown);
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
canvas.dispose();
|
canvas.dispose();
|
||||||
@@ -1158,21 +1435,79 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
|
|||||||
});
|
});
|
||||||
}, [template.elements, zoom]);
|
}, [template.elements, zoom]);
|
||||||
|
|
||||||
// Update selection when selectedElementId changes externally
|
// Update visual selection feedback when selectedElementIds changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fabricRef.current) return;
|
if (!fabricRef.current) return;
|
||||||
|
|
||||||
if (selectedElementId) {
|
const canvas = fabricRef.current;
|
||||||
const obj = elementsMapRef.current.get(selectedElementId);
|
|
||||||
if (obj && fabricRef.current.getActiveObject() !== obj) {
|
// Clear all selection styling first
|
||||||
fabricRef.current.setActiveObject(obj);
|
const allObjects = canvas.getObjects();
|
||||||
fabricRef.current.renderAll();
|
for (const obj of allObjects) {
|
||||||
|
const objData = (obj as FabricObjectWithData).data;
|
||||||
|
if (
|
||||||
|
objData?.id &&
|
||||||
|
!objData.isGrid &&
|
||||||
|
!objData.isMargin &&
|
||||||
|
!objData.isGuide &&
|
||||||
|
!objData.isSelectionRect
|
||||||
|
) {
|
||||||
|
// Reset to default styling
|
||||||
|
obj.set({
|
||||||
|
strokeWidth:
|
||||||
|
obj.type === "line"
|
||||||
|
? obj.strokeWidth || 1
|
||||||
|
: (objData.originalStrokeWidth ?? 0),
|
||||||
|
stroke:
|
||||||
|
objData.originalStroke ?? (obj.type === "line" ? obj.stroke : ""),
|
||||||
|
shadow: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedElementIds.length === 0) {
|
||||||
|
canvas.discardActiveObject();
|
||||||
|
canvas.renderAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedElementIds.length === 1) {
|
||||||
|
// Single selection - use Fabric's native selection (with controls)
|
||||||
|
const obj = elementsMapRef.current.get(selectedElementIds[0]);
|
||||||
|
if (obj && canvas.getActiveObject() !== obj) {
|
||||||
|
canvas.setActiveObject(obj);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fabricRef.current.discardActiveObject();
|
// Multi-selection - show visual feedback on each selected object
|
||||||
fabricRef.current.renderAll();
|
canvas.discardActiveObject(); // Clear native selection
|
||||||
|
|
||||||
|
for (const id of selectedElementIds) {
|
||||||
|
const obj = elementsMapRef.current.get(id);
|
||||||
|
if (obj) {
|
||||||
|
const objData = obj.data;
|
||||||
|
// Store original values if not already stored
|
||||||
|
if (objData && objData.originalStrokeWidth === undefined) {
|
||||||
|
objData.originalStrokeWidth = obj.strokeWidth || 0;
|
||||||
|
objData.originalStroke = obj.stroke || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply selection styling - blue border and shadow
|
||||||
|
obj.set({
|
||||||
|
stroke: "#1976d2",
|
||||||
|
strokeWidth: 2,
|
||||||
|
shadow: new fabric.Shadow({
|
||||||
|
color: "rgba(25, 118, 210, 0.4)",
|
||||||
|
blur: 10,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [selectedElementId]);
|
|
||||||
|
canvas.renderAll();
|
||||||
|
}, [selectedElementIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -127,10 +127,8 @@ export default function ReportEditorPage() {
|
|||||||
// Page state - track which page is currently being edited
|
// Page state - track which page is currently being edited
|
||||||
const [currentPageId, setCurrentPageId] = useState<string>(defaultPage.id);
|
const [currentPageId, setCurrentPageId] = useState<string>(defaultPage.id);
|
||||||
|
|
||||||
// Editor state
|
// Editor state - support multiple selection
|
||||||
const [selectedElementId, setSelectedElementId] = useState<string | null>(
|
const [selectedElementIds, setSelectedElementIds] = useState<string[]>([]);
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [zoom, setZoom] = useState(isMobile ? 0.5 : 1);
|
const [zoom, setZoom] = useState(isMobile ? 0.5 : 1);
|
||||||
const [showGrid, setShowGrid] = useState(true);
|
const [showGrid, setShowGrid] = useState(true);
|
||||||
const [snapOptions, setSnapOptions] = useState<SnapOptions>({
|
const [snapOptions, setSnapOptions] = useState<SnapOptions>({
|
||||||
@@ -256,8 +254,10 @@ export default function ReportEditorPage() {
|
|||||||
elements: prev.elements.filter((el) => el.id !== message.itemId),
|
elements: prev.elements.filter((el) => el.id !== message.itemId),
|
||||||
}));
|
}));
|
||||||
// Clear selection if deleted element was selected
|
// Clear selection if deleted element was selected
|
||||||
if (selectedElementId === message.itemId) {
|
if (selectedElementIds.includes(message.itemId)) {
|
||||||
setSelectedElementId(null);
|
setSelectedElementIds((prev) =>
|
||||||
|
prev.filter((id) => id !== message.itemId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isApplyingRemoteChange.current = false;
|
isApplyingRemoteChange.current = false;
|
||||||
@@ -396,18 +396,18 @@ export default function ReportEditorPage() {
|
|||||||
}, [
|
}, [
|
||||||
collaboration,
|
collaboration,
|
||||||
historyActions,
|
historyActions,
|
||||||
selectedElementId,
|
selectedElementIds,
|
||||||
template,
|
template,
|
||||||
queryClient,
|
queryClient,
|
||||||
id,
|
id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Send selection changes to collaborators
|
// Send selection changes to collaborators (send first selected element for compatibility)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
||||||
collaboration.sendSelectionChanged(selectedElementId);
|
collaboration.sendSelectionChanged(selectedElementIds[0] || null);
|
||||||
}
|
}
|
||||||
}, [collaboration, selectedElementId]);
|
}, [collaboration, selectedElementIds]);
|
||||||
|
|
||||||
// Send view/page navigation to collaborators
|
// Send view/page navigation to collaborators
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -629,10 +629,14 @@ export default function ReportEditorPage() {
|
|||||||
historyActions.redo();
|
historyActions.redo();
|
||||||
}, [historyActions]);
|
}, [historyActions]);
|
||||||
|
|
||||||
// Get selected element
|
// Get selected element(s) - for single selection compatibility, use first selected
|
||||||
|
const selectedElementId = selectedElementIds[0] || null;
|
||||||
const selectedElement = selectedElementId
|
const selectedElement = selectedElementId
|
||||||
? template.elements.find((e) => e.id === selectedElementId)
|
? template.elements.find((e) => e.id === selectedElementId)
|
||||||
: null;
|
: null;
|
||||||
|
const selectedElements = selectedElementIds
|
||||||
|
.map((id) => template.elements.find((e) => e.id === id))
|
||||||
|
.filter((e): e is AprtElement => e !== undefined);
|
||||||
|
|
||||||
// Dataset management
|
// Dataset management
|
||||||
const handleAddDataset = useCallback((dataset: DatasetTypeDto) => {
|
const handleAddDataset = useCallback((dataset: DatasetTypeDto) => {
|
||||||
@@ -682,7 +686,7 @@ export default function ReportEditorPage() {
|
|||||||
|
|
||||||
// Switch to the new page
|
// Switch to the new page
|
||||||
setCurrentPageId(newPageId);
|
setCurrentPageId(newPageId);
|
||||||
setSelectedElementId(null);
|
setSelectedElementIds([]);
|
||||||
}, [template.pages.length, historyActions, collaboration]);
|
}, [template.pages.length, historyActions, collaboration]);
|
||||||
|
|
||||||
// Duplicate page with all its elements
|
// Duplicate page with all its elements
|
||||||
@@ -717,7 +721,7 @@ export default function ReportEditorPage() {
|
|||||||
|
|
||||||
// Switch to the new page
|
// Switch to the new page
|
||||||
setCurrentPageId(newPageId);
|
setCurrentPageId(newPageId);
|
||||||
setSelectedElementId(null);
|
setSelectedElementIds([]);
|
||||||
},
|
},
|
||||||
[template.pages, template.elements, historyActions],
|
[template.pages, template.elements, historyActions],
|
||||||
);
|
);
|
||||||
@@ -749,7 +753,7 @@ export default function ReportEditorPage() {
|
|||||||
if (newCurrentPage) {
|
if (newCurrentPage) {
|
||||||
setCurrentPageId(newCurrentPage.id);
|
setCurrentPageId(newCurrentPage.id);
|
||||||
}
|
}
|
||||||
setSelectedElementId(null);
|
setSelectedElementIds([]);
|
||||||
},
|
},
|
||||||
[template.pages, historyActions, collaboration],
|
[template.pages, historyActions, collaboration],
|
||||||
);
|
);
|
||||||
@@ -796,7 +800,7 @@ export default function ReportEditorPage() {
|
|||||||
// Select page
|
// Select page
|
||||||
const handleSelectPage = useCallback((pageId: string) => {
|
const handleSelectPage = useCallback((pageId: string) => {
|
||||||
setCurrentPageId(pageId);
|
setCurrentPageId(pageId);
|
||||||
setSelectedElementId(null); // Clear selection when switching pages
|
setSelectedElementIds([]); // Clear selection when switching pages
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ============ END PAGE MANAGEMENT HANDLERS ============
|
// ============ END PAGE MANAGEMENT HANDLERS ============
|
||||||
@@ -851,7 +855,7 @@ export default function ReportEditorPage() {
|
|||||||
...prev,
|
...prev,
|
||||||
elements: [...prev.elements, newElement],
|
elements: [...prev.elements, newElement],
|
||||||
}));
|
}));
|
||||||
setSelectedElementId(newElement.id);
|
setSelectedElementIds([newElement.id]);
|
||||||
|
|
||||||
// Send to collaborators
|
// Send to collaborators
|
||||||
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
||||||
@@ -958,7 +962,7 @@ export default function ReportEditorPage() {
|
|||||||
...prev,
|
...prev,
|
||||||
elements: [...prev.elements, newElement],
|
elements: [...prev.elements, newElement],
|
||||||
}));
|
}));
|
||||||
setSelectedElementId(newElement.id);
|
setSelectedElementIds([newElement.id]);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
selectedElementId,
|
selectedElementId,
|
||||||
@@ -991,7 +995,7 @@ export default function ReportEditorPage() {
|
|||||||
...prev,
|
...prev,
|
||||||
elements: prev.elements.filter((el) => el.id !== selectedElementId),
|
elements: prev.elements.filter((el) => el.id !== selectedElementId),
|
||||||
}));
|
}));
|
||||||
setSelectedElementId(null);
|
setSelectedElementIds([]);
|
||||||
}, [selectedElementId, historyActions, collaboration]);
|
}, [selectedElementId, historyActions, collaboration]);
|
||||||
|
|
||||||
// Copy element
|
// Copy element
|
||||||
@@ -1011,7 +1015,7 @@ export default function ReportEditorPage() {
|
|||||||
...prev,
|
...prev,
|
||||||
elements: [...prev.elements, copy],
|
elements: [...prev.elements, copy],
|
||||||
}));
|
}));
|
||||||
setSelectedElementId(copy.id);
|
setSelectedElementIds([copy.id]);
|
||||||
}, [selectedElement, historyActions]);
|
}, [selectedElement, historyActions]);
|
||||||
|
|
||||||
// Toggle lock
|
// Toggle lock
|
||||||
@@ -1066,7 +1070,7 @@ export default function ReportEditorPage() {
|
|||||||
// Handle context menu event from canvas
|
// Handle context menu event from canvas
|
||||||
const handleContextMenu = useCallback((event: ContextMenuEvent) => {
|
const handleContextMenu = useCallback((event: ContextMenuEvent) => {
|
||||||
if (event.elementId) {
|
if (event.elementId) {
|
||||||
setSelectedElementId(event.elementId);
|
setSelectedElementIds(event.elementId ? [event.elementId] : []);
|
||||||
}
|
}
|
||||||
setContextMenu({
|
setContextMenu({
|
||||||
open: true,
|
open: true,
|
||||||
@@ -1113,7 +1117,7 @@ export default function ReportEditorPage() {
|
|||||||
...prev,
|
...prev,
|
||||||
elements: [...prev.elements, pastedElement],
|
elements: [...prev.elements, pastedElement],
|
||||||
}));
|
}));
|
||||||
setSelectedElementId(pastedElement.id);
|
setSelectedElementIds([pastedElement.id]);
|
||||||
}, [clipboard, historyActions]);
|
}, [clipboard, historyActions]);
|
||||||
|
|
||||||
// Duplicate element
|
// Duplicate element
|
||||||
@@ -1366,19 +1370,15 @@ export default function ReportEditorPage() {
|
|||||||
|
|
||||||
// Selection operations
|
// Selection operations
|
||||||
const handleSelectAll = useCallback(() => {
|
const handleSelectAll = useCallback(() => {
|
||||||
// For now, just show a message - multi-selection requires more work
|
// Select all elements on the current page
|
||||||
if (template.elements.length > 0) {
|
const pageElementIds = currentPageElements.map((e) => e.id);
|
||||||
setSelectedElementId(template.elements[template.elements.length - 1].id);
|
if (pageElementIds.length > 0) {
|
||||||
setSnackbar({
|
setSelectedElementIds(pageElementIds);
|
||||||
open: true,
|
|
||||||
message: "Selezione multipla non ancora implementata",
|
|
||||||
severity: "error",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [template.elements]);
|
}, [currentPageElements]);
|
||||||
|
|
||||||
const handleDeselectAll = useCallback(() => {
|
const handleDeselectAll = useCallback(() => {
|
||||||
setSelectedElementId(null);
|
setSelectedElementIds([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Toggle visibility
|
// Toggle visibility
|
||||||
@@ -1704,13 +1704,13 @@ export default function ReportEditorPage() {
|
|||||||
onPrevPage={() => {
|
onPrevPage={() => {
|
||||||
if (currentPageIndex > 0) {
|
if (currentPageIndex > 0) {
|
||||||
setCurrentPageId(template.pages[currentPageIndex - 1].id);
|
setCurrentPageId(template.pages[currentPageIndex - 1].id);
|
||||||
setSelectedElementId(null);
|
setSelectedElementIds([]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onNextPage={() => {
|
onNextPage={() => {
|
||||||
if (currentPageIndex < template.pages.length - 1) {
|
if (currentPageIndex < template.pages.length - 1) {
|
||||||
setCurrentPageId(template.pages[currentPageIndex + 1].id);
|
setCurrentPageId(template.pages[currentPageIndex + 1].id);
|
||||||
setSelectedElementId(null);
|
setSelectedElementIds([]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
// New props for enhanced toolbar
|
// New props for enhanced toolbar
|
||||||
@@ -1764,11 +1764,11 @@ export default function ReportEditorPage() {
|
|||||||
margins: currentPage?.margins || template.meta.margins,
|
margins: currentPage?.margins || template.meta.margins,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
selectedElementId={selectedElementId}
|
selectedElementIds={selectedElementIds}
|
||||||
onSelectElement={(id) => {
|
onSelectElement={(ids) => {
|
||||||
setSelectedElementId(id);
|
setSelectedElementIds(ids);
|
||||||
// On mobile, auto-open properties when selecting element
|
// On mobile, auto-open properties when selecting element
|
||||||
if (isMobile && id) {
|
if (isMobile && ids.length > 0) {
|
||||||
setMobilePanel("properties");
|
setMobilePanel("properties");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -1973,7 +1973,7 @@ export default function ReportEditorPage() {
|
|||||||
open={contextMenu.open}
|
open={contextMenu.open}
|
||||||
position={contextMenu.position}
|
position={contextMenu.position}
|
||||||
selectedElement={selectedElement || null}
|
selectedElement={selectedElement || null}
|
||||||
selectedElements={selectedElement ? [selectedElement] : []}
|
selectedElements={selectedElements}
|
||||||
hasClipboard={clipboard !== null}
|
hasClipboard={clipboard !== null}
|
||||||
onClose={closeContextMenu}
|
onClose={closeContextMenu}
|
||||||
// Clipboard actions
|
// Clipboard actions
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user