added vehicles and vehicles extra, dynamic tables for entities, dynamic dropdowns with overlay for selecting and manage data
This commit is contained in:
@@ -0,0 +1,654 @@
|
||||
import React, { useRef, useEffect, useMemo } from 'react';
|
||||
import TextField from './TextField';
|
||||
import DBTextField from './DBTextField';
|
||||
import FrameElement from './FrameElement';
|
||||
import HorizontalLine from './HorizontalLine';
|
||||
import VerticalLine from './VerticalLine';
|
||||
import SymbolElement from './SymbolElement';
|
||||
import BandElement from './BandElement';
|
||||
import { useReportData } from './DataContext';
|
||||
import { BandProvider } from './BandContext';
|
||||
import { detectCrossroads } from './utils/crossroadDetector';
|
||||
import { elementIntersectsRect } from './utils/elementUtils';
|
||||
import { calculateBandInstances, calculateTotalContentHeight } from './utils/bandRenderer';
|
||||
import './EditorCanvas.css';
|
||||
|
||||
const CHAR_WIDTH = 18;
|
||||
const CHAR_HEIGHT = 32;
|
||||
const GRID_COLS = 80;
|
||||
const GRID_ROWS = 66;
|
||||
|
||||
function EditorCanvas({
|
||||
elements,
|
||||
selectedElementIds,
|
||||
onElementSelect,
|
||||
onSelectMultiple,
|
||||
onDeselectAll,
|
||||
onElementUpdate,
|
||||
onElementDelete,
|
||||
toolMode,
|
||||
borderStyle,
|
||||
onAddElement,
|
||||
previewMode = false,
|
||||
selectedChar = '─'
|
||||
}) {
|
||||
const canvasRef = useRef(null);
|
||||
const [frameStart, setFrameStart] = React.useState(null);
|
||||
const [framePreview, setFramePreview] = React.useState(null);
|
||||
const [lineStart, setLineStart] = React.useState(null);
|
||||
const [linePreview, setLinePreview] = React.useState(null);
|
||||
const [selectionRect, setSelectionRect] = React.useState(null);
|
||||
const [isSelecting, setIsSelecting] = React.useState(false);
|
||||
const [isPainting, setIsPainting] = React.useState(false);
|
||||
const [paintedPositions, setPaintedPositions] = React.useState(new Set());
|
||||
const [didDragSelect, setDidDragSelect] = React.useState(false);
|
||||
|
||||
// Get report data for preview mode
|
||||
const { reportData } = useReportData();
|
||||
|
||||
// Calculate total content height in preview mode
|
||||
const contentHeight = useMemo(() => {
|
||||
if (previewMode) {
|
||||
const bands = elements.filter(el => el.type === 'band');
|
||||
const totalHeight = calculateTotalContentHeight(bands, reportData, elements);
|
||||
return totalHeight * CHAR_HEIGHT;
|
||||
}
|
||||
return GRID_ROWS * CHAR_HEIGHT; // Default height in design mode
|
||||
}, [previewMode, elements, reportData]);
|
||||
|
||||
const snapToGrid = (pixelX, pixelY) => {
|
||||
const col = Math.max(0, Math.min(GRID_COLS - 1, Math.round(pixelX / CHAR_WIDTH)));
|
||||
const row = Math.max(0, Math.min(GRID_ROWS - 1, Math.round(pixelY / CHAR_HEIGHT)));
|
||||
return { col, row };
|
||||
};
|
||||
|
||||
const snapToGridAlt = (pixelX, pixelY) => {
|
||||
const col = Math.max(0, Math.min(GRID_COLS - 1, Math.floor(pixelX / CHAR_WIDTH)));
|
||||
const row = Math.max(0, Math.min(GRID_ROWS - 1, Math.floor(pixelY / CHAR_HEIGHT)));
|
||||
return { col, row };
|
||||
};
|
||||
|
||||
|
||||
const gridToPixels = (col, row) => ({
|
||||
x: col * CHAR_WIDTH,
|
||||
y: row * CHAR_HEIGHT
|
||||
});
|
||||
|
||||
const handleCanvasMouseDown = (e) => {
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const pixelX = e.clientX - rect.left;
|
||||
const pixelY = e.clientY - rect.top;
|
||||
const { col, row } = snapToGridAlt(pixelX, pixelY);
|
||||
|
||||
if (toolMode === 'select') {
|
||||
// Start selection rectangle
|
||||
setIsSelecting(true);
|
||||
setSelectionRect({ startX: col, startY: row, endX: col, endY: row });
|
||||
setDidDragSelect(false);
|
||||
return;
|
||||
} else if (toolMode === 'drawTable') {
|
||||
// Start painting symbols
|
||||
setIsPainting(true);
|
||||
paintSymbol(col, row);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCanvasClick = (e) => {
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const pixelX = e.clientX - rect.left;
|
||||
const pixelY = e.clientY - rect.top;
|
||||
const { col, row } = snapToGridAlt(pixelX, pixelY);
|
||||
|
||||
if (toolMode === 'addText') {
|
||||
const newElement = {
|
||||
id: `text-${Date.now()}`,
|
||||
type: 'text',
|
||||
x: col,
|
||||
y: row,
|
||||
width: 10,
|
||||
height: 1,
|
||||
content: 'Text'
|
||||
};
|
||||
onAddElement(newElement);
|
||||
} else if (toolMode === 'addDBText') {
|
||||
const newElement = {
|
||||
id: `dbtext-${Date.now()}`,
|
||||
type: 'dbtext',
|
||||
x: col,
|
||||
y: row,
|
||||
width: 10,
|
||||
height: 1,
|
||||
objectKey: '',
|
||||
fieldPath: ''
|
||||
};
|
||||
onAddElement(newElement);
|
||||
} else if (toolMode === 'addFrame') {
|
||||
if (!frameStart) {
|
||||
// First click - set start position
|
||||
setFrameStart({ col, row });
|
||||
} else {
|
||||
// Second click - create frame
|
||||
const x = Math.min(frameStart.col, col);
|
||||
const y = Math.min(frameStart.row, row);
|
||||
const width = Math.abs(col - frameStart.col) + 1;
|
||||
const height = Math.abs(row - frameStart.row) + 1;
|
||||
|
||||
if (width >= 3 && height >= 3) {
|
||||
const newElement = {
|
||||
id: `frame-${Date.now()}`,
|
||||
type: 'frame',
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
borderStyle
|
||||
};
|
||||
onAddElement(newElement);
|
||||
}
|
||||
setFrameStart(null);
|
||||
setFramePreview(null);
|
||||
}
|
||||
} else if (toolMode === 'addHLine') {
|
||||
if (!lineStart) {
|
||||
// First click - set start position
|
||||
setLineStart({ col, row, type: 'hline' });
|
||||
} else {
|
||||
// Second click - create horizontal line
|
||||
const x = Math.min(lineStart.col, col);
|
||||
const y = lineStart.row;
|
||||
const length = Math.abs(col - lineStart.col) + 1;
|
||||
|
||||
if (length >= 2) {
|
||||
const newElement = {
|
||||
id: `hline-${Date.now()}`,
|
||||
type: 'hline',
|
||||
x,
|
||||
y,
|
||||
length,
|
||||
lineStyle: borderStyle
|
||||
};
|
||||
onAddElement(newElement);
|
||||
}
|
||||
setLineStart(null);
|
||||
setLinePreview(null);
|
||||
}
|
||||
} else if (toolMode === 'addVLine') {
|
||||
if (!lineStart) {
|
||||
// First click - set start position
|
||||
setLineStart({ col, row, type: 'vline' });
|
||||
} else {
|
||||
// Second click - create vertical line
|
||||
const x = lineStart.col;
|
||||
const y = Math.min(lineStart.row, row);
|
||||
const length = Math.abs(row - lineStart.row) + 1;
|
||||
|
||||
if (length >= 2) {
|
||||
const newElement = {
|
||||
id: `vline-${Date.now()}`,
|
||||
type: 'vline',
|
||||
x,
|
||||
y,
|
||||
length,
|
||||
lineStyle: borderStyle
|
||||
};
|
||||
onAddElement(newElement);
|
||||
}
|
||||
setLineStart(null);
|
||||
setLinePreview(null);
|
||||
}
|
||||
} else if (toolMode === 'addBand') {
|
||||
// Add band (always full width, x=0)
|
||||
// Check for overlap with existing bands
|
||||
const newBand = {
|
||||
id: `band-${Date.now()}`,
|
||||
type: 'band',
|
||||
x: 0,
|
||||
y: row,
|
||||
width: 80,
|
||||
height: 3,
|
||||
bandType: 'detail',
|
||||
dataSource: '',
|
||||
caption: '',
|
||||
children: [],
|
||||
parentBandId: null
|
||||
};
|
||||
|
||||
// Check if this overlaps with any existing band
|
||||
const overlaps = elements.some(el => {
|
||||
if (el.type !== 'band') return false;
|
||||
const elMaxY = el.y + el.height - 1;
|
||||
const newMaxY = newBand.y + newBand.height - 1;
|
||||
return !(elMaxY < newBand.y || el.y > newMaxY);
|
||||
});
|
||||
|
||||
if (overlaps) {
|
||||
alert('Cannot place band here - it overlaps with another band');
|
||||
} else {
|
||||
onAddElement(newBand);
|
||||
}
|
||||
} else if (toolMode === 'select') {
|
||||
// Click on canvas background - deselect all (but not if we just did a drag selection)
|
||||
if (!didDragSelect) {
|
||||
onDeselectAll();
|
||||
}
|
||||
setDidDragSelect(false);
|
||||
}
|
||||
};
|
||||
|
||||
const paintSymbol = (col, row) => {
|
||||
const key = `${col},${row}`;
|
||||
if (paintedPositions.has(key)) return; // Already painted this cell
|
||||
|
||||
// Check if symbol already exists at this position
|
||||
const existingIndex = elements.findIndex(
|
||||
el => el.type === 'symbol' && el.x === col && el.y === row
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// Replace existing symbol
|
||||
onElementUpdate(elements[existingIndex].id, { char: selectedChar });
|
||||
} else {
|
||||
// Create new symbol
|
||||
const newElement = {
|
||||
id: `symbol-${Date.now()}-${Math.random()}`,
|
||||
type: 'symbol',
|
||||
x: col,
|
||||
y: row,
|
||||
char: selectedChar
|
||||
};
|
||||
onAddElement(newElement);
|
||||
}
|
||||
|
||||
setPaintedPositions(prev => new Set([...prev, key]));
|
||||
};
|
||||
|
||||
const findElementsInRect = (rect) => {
|
||||
const minX = Math.min(rect.startX, rect.endX);
|
||||
const maxX = Math.max(rect.startX, rect.endX);
|
||||
const minY = Math.min(rect.startY, rect.endY);
|
||||
const maxY = Math.max(rect.startY, rect.endY);
|
||||
|
||||
// Use element class method for intersection checking
|
||||
return elements
|
||||
.filter(el => elementIntersectsRect(el, { minX, maxX, minY, maxY }))
|
||||
.map(el => el.id);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const pixelX = e.clientX - rect.left;
|
||||
const pixelY = e.clientY - rect.top;
|
||||
const { col, row } = snapToGrid(pixelX, pixelY);
|
||||
const { col: altCol, row: altRow } = snapToGridAlt(pixelX, pixelY);
|
||||
|
||||
if (isSelecting && toolMode === 'select') {
|
||||
setSelectionRect(prev => {
|
||||
// Mark that we dragged if the rectangle has actually moved
|
||||
if (prev && (prev.startX !== altCol || prev.startY !== altRow)) {
|
||||
setDidDragSelect(true);
|
||||
}
|
||||
return { ...prev, endX: altCol, endY: altRow };
|
||||
});
|
||||
} else if (isPainting && toolMode === 'drawTable') {
|
||||
paintSymbol(altCol, altRow);
|
||||
} else if (toolMode === 'addFrame' && frameStart) {
|
||||
const x = Math.min(frameStart.col, col);
|
||||
const y = Math.min(frameStart.row, row);
|
||||
const width = Math.abs(col - frameStart.col) + 1;
|
||||
const height = Math.abs(row - frameStart.row) + 1;
|
||||
|
||||
setFramePreview({ x, y, width, height });
|
||||
} else if (toolMode === 'addHLine' && lineStart) {
|
||||
const x = Math.min(lineStart.col, col);
|
||||
const y = lineStart.row;
|
||||
const length = Math.abs(col - lineStart.col) + 1;
|
||||
|
||||
setLinePreview({ x, y, length, type: 'hline' });
|
||||
} else if (toolMode === 'addVLine' && lineStart) {
|
||||
const x = lineStart.col;
|
||||
const y = Math.min(lineStart.row, row);
|
||||
const length = Math.abs(row - lineStart.row) + 1;
|
||||
|
||||
setLinePreview({ x, y, length, type: 'vline' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleElementDragStart = (elementId, startX, startY) => {
|
||||
const element = elements.find(el => el.id === elementId);
|
||||
return {
|
||||
elementId,
|
||||
startX,
|
||||
startY,
|
||||
initialElementX: element.x,
|
||||
initialElementY: element.y
|
||||
};
|
||||
};
|
||||
|
||||
const handleElementDrag = (dragData, deltaX, deltaY) => {
|
||||
const element = elements.find(el => el.id === dragData.elementId);
|
||||
if (!element) return;
|
||||
|
||||
// Calculate new position based on initial position + delta
|
||||
const newPixelX = (dragData.initialElementX * CHAR_WIDTH) + deltaX;
|
||||
const newPixelY = (dragData.initialElementY * CHAR_HEIGHT) + deltaY;
|
||||
const { col, row } = snapToGrid(newPixelX, newPixelY);
|
||||
|
||||
// Only update if position actually changed
|
||||
if (col !== element.x || row !== element.y) {
|
||||
// If this element is selected with others, move all selected elements
|
||||
if (selectedElementIds.includes(dragData.elementId) && selectedElementIds.length > 1) {
|
||||
const deltaCol = col - element.x;
|
||||
const deltaRow = row - element.y;
|
||||
selectedElementIds.forEach(id => {
|
||||
const el = elements.find(e => e.id === id);
|
||||
if (el) {
|
||||
onElementUpdate(id, { x: el.x + deltaCol, y: el.y + deltaRow });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
onElementUpdate(dragData.elementId, { x: col, y: row });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add global mouseup listener to handle drag completion outside canvas
|
||||
useEffect(() => {
|
||||
const handleGlobalMouseUp = () => {
|
||||
if (isSelecting && toolMode === 'select' && selectionRect) {
|
||||
const selectedIds = findElementsInRect(selectionRect);
|
||||
onSelectMultiple(selectedIds);
|
||||
setIsSelecting(false);
|
||||
setSelectionRect(null);
|
||||
} else if (isPainting && toolMode === 'drawTable') {
|
||||
setIsPainting(false);
|
||||
setPaintedPositions(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mouseup', handleGlobalMouseUp);
|
||||
return () => document.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||
}, [isSelecting, isPainting, selectionRect, toolMode, elements, onSelectMultiple]);
|
||||
|
||||
// Render grid lines
|
||||
const renderGrid = () => {
|
||||
const gridLines = [];
|
||||
// Vertical lines
|
||||
for (let i = 0; i <= GRID_COLS; i++) {
|
||||
gridLines.push(
|
||||
<div
|
||||
key={`v-${i}`}
|
||||
className="grid-line vertical"
|
||||
style={{ left: `${i * (CHAR_WIDTH)}px` }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// Horizontal lines
|
||||
for (let i = 0; i <= GRID_ROWS; i++) {
|
||||
gridLines.push(
|
||||
<div
|
||||
key={`h-${i}`}
|
||||
className="grid-line horizontal"
|
||||
style={{ top: `${i * CHAR_HEIGHT}px` }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return gridLines;
|
||||
};
|
||||
|
||||
// Calculate crossroad map for intersection rendering
|
||||
const crossroadMap = useMemo(() => {
|
||||
return detectCrossroads(elements);
|
||||
}, [elements]);
|
||||
|
||||
const showCrosshair = toolMode === 'addFrame' || toolMode === 'addHLine' || toolMode === 'addVLine';
|
||||
|
||||
// Render page break indicators (every 66 rows in preview mode)
|
||||
const renderPageBreaks = () => {
|
||||
if (!previewMode) return null;
|
||||
|
||||
const breaks = [];
|
||||
const pageHeight = GRID_ROWS * CHAR_HEIGHT; // 66 rows * 32px = 2112px
|
||||
const numPages = Math.ceil(contentHeight / pageHeight);
|
||||
|
||||
for (let i = 1; i < numPages; i++) {
|
||||
breaks.push(
|
||||
<div
|
||||
key={`page-break-${i}`}
|
||||
className="page-break-indicator"
|
||||
style={{ top: `${i * pageHeight}px` }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return breaks;
|
||||
};
|
||||
|
||||
// Helper function to render an element (used for standalone and child elements)
|
||||
const renderElement = (element, parentBandId = null) => {
|
||||
const commonProps = {
|
||||
key: element.id,
|
||||
element,
|
||||
isSelected: selectedElementIds.includes(element.id),
|
||||
isSingleSelection: selectedElementIds.length === 1 && selectedElementIds[0] === element.id,
|
||||
onSelect: (e) => onElementSelect(element.id, e?.ctrlKey || e?.metaKey),
|
||||
onUpdate: (updates) => onElementUpdate(element.id, updates),
|
||||
onDelete: () => onElementDelete(element.id),
|
||||
onDragStart: handleElementDragStart,
|
||||
onDrag: handleElementDrag,
|
||||
charWidth: CHAR_WIDTH,
|
||||
charHeight: CHAR_HEIGHT,
|
||||
toolMode
|
||||
};
|
||||
|
||||
if (element.type === 'text') {
|
||||
return <TextField {...commonProps} />;
|
||||
} else if (element.type === 'dbtext') {
|
||||
return (
|
||||
<DBTextField
|
||||
{...commonProps}
|
||||
previewMode={previewMode}
|
||||
parentBandId={parentBandId}
|
||||
/>
|
||||
);
|
||||
} else if (element.type === 'frame') {
|
||||
return <FrameElement {...commonProps} />;
|
||||
} else if (element.type === 'hline') {
|
||||
return <HorizontalLine {...commonProps} crossroadMap={crossroadMap} />;
|
||||
} else if (element.type === 'vline') {
|
||||
return <VerticalLine {...commonProps} crossroadMap={crossroadMap} />;
|
||||
} else if (element.type === 'symbol') {
|
||||
return <SymbolElement {...commonProps} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="canvas-container">
|
||||
<div
|
||||
ref={canvasRef}
|
||||
className={`editor-canvas ${showCrosshair ? 'crosshair' : ''}`}
|
||||
onClick={handleCanvasClick}
|
||||
onMouseDown={handleCanvasMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
>
|
||||
<div
|
||||
className="editor-canvas-content"
|
||||
style={{
|
||||
minHeight: `${GRID_ROWS * CHAR_HEIGHT}px`,
|
||||
height: previewMode ? `${contentHeight}px` : `${GRID_ROWS * CHAR_HEIGHT}px`
|
||||
}}
|
||||
>
|
||||
{renderGrid()}
|
||||
|
||||
{/* Page break indicators */}
|
||||
{renderPageBreaks()}
|
||||
|
||||
{/* Render selection rectangle */}
|
||||
{selectionRect && isSelecting && (
|
||||
<div
|
||||
className="selection-rectangle"
|
||||
style={{
|
||||
left: `${Math.min(selectionRect.startX, selectionRect.endX) * CHAR_WIDTH}px`,
|
||||
top: `${Math.min(selectionRect.startY, selectionRect.endY) * CHAR_HEIGHT}px`,
|
||||
width: `${(Math.abs(selectionRect.endX - selectionRect.startX) + 1) * CHAR_WIDTH}px`,
|
||||
height: `${(Math.abs(selectionRect.endY - selectionRect.startY) + 1) * CHAR_HEIGHT}px`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Render frame preview */}
|
||||
{framePreview && (
|
||||
<div
|
||||
className="frame-preview"
|
||||
style={{
|
||||
left: `${framePreview.x * CHAR_WIDTH}px`,
|
||||
top: `${framePreview.y * CHAR_HEIGHT}px`,
|
||||
width: `${framePreview.width * CHAR_WIDTH}px`,
|
||||
height: `${framePreview.height * CHAR_HEIGHT}px`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Render line preview */}
|
||||
{linePreview && (
|
||||
<div
|
||||
className="line-preview"
|
||||
style={{
|
||||
left: `${linePreview.x * CHAR_WIDTH}px`,
|
||||
top: `${linePreview.y * CHAR_HEIGHT}px`,
|
||||
width: linePreview.type === 'hline' ? `${linePreview.length * CHAR_WIDTH}px` : `${CHAR_WIDTH}px`,
|
||||
height: linePreview.type === 'vline' ? `${linePreview.length * CHAR_HEIGHT}px` : `${CHAR_HEIGHT}px`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Render elements */}
|
||||
{elements.map(element => {
|
||||
// Check if element is a child of a band
|
||||
const isChildOfBand = elements.some(el => el.type === 'band' && el.children && el.children.includes(element.id));
|
||||
|
||||
// In design mode, skip child elements (they're rendered inside band containers)
|
||||
if (!previewMode) {
|
||||
if (isChildOfBand && element.type !== 'band') {
|
||||
return null; // Skip - will be rendered as child of band
|
||||
}
|
||||
} else {
|
||||
// In preview mode, skip ALL elements - they're rendered in dedicated sections below
|
||||
// (bands get repeated with data, children render inside bands, standalone elements get shifted)
|
||||
return null;
|
||||
}
|
||||
|
||||
// Render bands specially in design mode
|
||||
if (element.type === 'band') {
|
||||
if (!previewMode) {
|
||||
// Design mode: render band as container with children
|
||||
const childElements = elements.filter(el => element.children && element.children.includes(el.id));
|
||||
return (
|
||||
<BandElement
|
||||
key={element.id}
|
||||
element={element}
|
||||
isSelected={selectedElementIds.includes(element.id)}
|
||||
isSingleSelection={selectedElementIds.length === 1 && selectedElementIds[0] === element.id}
|
||||
onSelect={(e) => onElementSelect(element.id, e?.ctrlKey || e?.metaKey)}
|
||||
onUpdate={(updates) => onElementUpdate(element.id, updates)}
|
||||
onDelete={() => onElementDelete(element.id)}
|
||||
onDragStart={handleElementDragStart}
|
||||
onDrag={handleElementDrag}
|
||||
charWidth={CHAR_WIDTH}
|
||||
charHeight={CHAR_HEIGHT}
|
||||
toolMode={toolMode}
|
||||
>
|
||||
{/* Render child elements inside band */}
|
||||
{childElements.map(childEl => renderElement(childEl, element.id))}
|
||||
</BandElement>
|
||||
);
|
||||
}
|
||||
// Preview mode: skip (bands rendered separately below)
|
||||
return null;
|
||||
}
|
||||
|
||||
// Render other element types
|
||||
return renderElement(element);
|
||||
})}
|
||||
|
||||
{/* Preview mode: Render band instances with repeated data */}
|
||||
{previewMode && (() => {
|
||||
const bands = elements.filter(el => el.type === 'band');
|
||||
const bandInstances = calculateBandInstances(bands, reportData, elements);
|
||||
|
||||
return bandInstances.map(instance => {
|
||||
// Build band data map for this instance
|
||||
const bandDataMap = instance.data ? { [instance.bandId]: instance.data } : {};
|
||||
|
||||
return (
|
||||
<BandProvider key={`${instance.bandId}-instance-${instance.instanceIndex}`} bandDataMap={bandDataMap}>
|
||||
{/* Render child elements at adjusted positions */}
|
||||
{instance.childElements.map(childEl => {
|
||||
// Child element Y is relative to band, so add instance Y to get absolute position
|
||||
const adjustedElement = {
|
||||
...childEl,
|
||||
y: instance.y + childEl.y
|
||||
};
|
||||
|
||||
return renderElement(adjustedElement, instance.bandId);
|
||||
})}
|
||||
</BandProvider>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
|
||||
{/* Preview mode: Render standalone elements that aren't in bands */}
|
||||
{previewMode && (() => {
|
||||
const bands = elements.filter(el => el.type === 'band');
|
||||
const bandInstances = calculateBandInstances(bands, reportData, elements);
|
||||
|
||||
// Build a map of band Y ranges in design mode vs preview mode
|
||||
// IMPORTANT: Only consider top-level bands (no parent) because nested band expansion
|
||||
// is already included in their parent's expansion
|
||||
const bandShifts = new Map();
|
||||
bands.filter(band => !band.parentBandId).forEach(band => {
|
||||
const bandDesignMaxY = band.y + band.height - 1;
|
||||
|
||||
// Find all instances of this band
|
||||
const instances = bandInstances.filter(inst => inst.bandId === band.id);
|
||||
if (instances.length > 0) {
|
||||
const lastInstance = instances[instances.length - 1];
|
||||
const bandPreviewMaxY = lastInstance.y + lastInstance.height - 1;
|
||||
|
||||
// Calculate how much this band expanded
|
||||
const expansion = bandPreviewMaxY - bandDesignMaxY;
|
||||
bandShifts.set(band.id, { designMaxY: bandDesignMaxY, expansion });
|
||||
}
|
||||
});
|
||||
|
||||
return elements.filter(el => {
|
||||
if (el.type === 'band') return false; // Skip bands themselves
|
||||
// Check if this element is a child of any band
|
||||
const isChildOfBand = elements.some(band => band.type === 'band' && band.children && band.children.includes(el.id));
|
||||
return !isChildOfBand; // Only render if NOT a child of a band
|
||||
}).map(element => {
|
||||
// Calculate cumulative shift for elements below bands
|
||||
let totalShift = 0;
|
||||
bandShifts.forEach(({ designMaxY, expansion }) => {
|
||||
if (element.y > designMaxY) {
|
||||
totalShift += expansion;
|
||||
}
|
||||
});
|
||||
|
||||
// Render with adjusted Y position
|
||||
const adjustedElement = {
|
||||
...element,
|
||||
y: element.y + totalShift
|
||||
};
|
||||
|
||||
return renderElement(adjustedElement);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditorCanvas;
|
||||
Reference in New Issue
Block a user