report editor has db text and bands (detail, subdetails, summary, header, footer) detail/subdetail work ok.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Context for tracking band iteration state
|
||||
* Used to provide current data item to elements inside bands
|
||||
*/
|
||||
const BandContext = createContext();
|
||||
|
||||
export function BandProvider({ children, bandDataMap = {} }) {
|
||||
// Map of bandId -> current data item
|
||||
// Can be provided externally or managed internally
|
||||
const [internalBandData, setInternalBandData] = useState({});
|
||||
|
||||
// Use external bandDataMap if provided, otherwise use internal state
|
||||
const currentBandData = Object.keys(bandDataMap).length > 0 ? bandDataMap : internalBandData;
|
||||
|
||||
const setBandData = (bandId, data) => {
|
||||
setInternalBandData(prev => ({
|
||||
...prev,
|
||||
[bandId]: data
|
||||
}));
|
||||
};
|
||||
|
||||
const clearBandData = () => {
|
||||
setInternalBandData({});
|
||||
};
|
||||
|
||||
const removeBandData = (bandId) => {
|
||||
setInternalBandData(prev => {
|
||||
const newData = { ...prev };
|
||||
delete newData[bandId];
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<BandContext.Provider value={{ currentBandData, setBandData, clearBandData, removeBandData }}>
|
||||
{children}
|
||||
</BandContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBandContext() {
|
||||
const context = useContext(BandContext);
|
||||
if (!context) {
|
||||
throw new Error('useBandContext must be used within a BandProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { useElementDrag } from './hooks/useElementDrag';
|
||||
import { useElementSelection } from './hooks/useElementSelection';
|
||||
import { getElementStyle } from './utils/elementUtils';
|
||||
import './elements.css';
|
||||
|
||||
/**
|
||||
* Band element component
|
||||
* Represents a repeating section that iterates over data arrays
|
||||
*/
|
||||
function BandElement({
|
||||
element,
|
||||
isSelected,
|
||||
isSingleSelection,
|
||||
onSelect,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
charWidth,
|
||||
charHeight,
|
||||
toolMode,
|
||||
children // Child elements to render inside band (in design mode)
|
||||
}) {
|
||||
// Use custom hooks for drag and selection
|
||||
const { isDragging, handleMouseDown: handleDragMouseDown } = useElementDrag({
|
||||
isSelected,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
toolMode,
|
||||
constrainX: true // Only vertical dragging
|
||||
});
|
||||
|
||||
const { handleClick } = useElementSelection({
|
||||
isSelected,
|
||||
onSelect,
|
||||
onDelete,
|
||||
toolMode
|
||||
});
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
e.stopPropagation();
|
||||
if (!isSelected) {
|
||||
onSelect();
|
||||
}
|
||||
handleDragMouseDown(e, element.id);
|
||||
};
|
||||
|
||||
// Get caption text
|
||||
const captionText = element.caption ||
|
||||
`${element.bandType}: ${element.dataSource || 'no data'}`;
|
||||
|
||||
const style = {
|
||||
...getElementStyle(element, charWidth, charHeight),
|
||||
borderTop: '2px dashed #667eea',
|
||||
borderBottom: '2px dashed #667eea',
|
||||
borderLeft: 'none',
|
||||
borderRight: 'none',
|
||||
backgroundColor: isSelected
|
||||
? 'rgba(102, 126, 234, 0.08)'
|
||||
: 'rgba(200, 200, 200, 0.03)',
|
||||
boxSizing: 'border-box'
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`band-element ${isSelected ? 'selected' : ''} ${isDragging ? 'dragging' : ''}`}
|
||||
style={style}
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div className="band-caption">
|
||||
{captionText}
|
||||
</div>
|
||||
{/* Render child elements inside band container in design mode */}
|
||||
<div className="band-children">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BandElement;
|
||||
@@ -86,18 +86,152 @@ function ConfigPanel({ apiEndpoint, onApiEndpointChange, isOpen, onClose }) {
|
||||
if (!showManualInput && !manualDataInput) {
|
||||
// Pre-fill with example data when opening for the first time
|
||||
setManualDataInput(JSON.stringify({
|
||||
"reportTitle": "Weight Measurements Report",
|
||||
"reportDate": "2026-01-26",
|
||||
"owner": {
|
||||
"name": "John Doe",
|
||||
"contact": {
|
||||
"phone": "555-1234"
|
||||
"phone": "555-1234",
|
||||
"email": "john.doe@example.com"
|
||||
}
|
||||
},
|
||||
"vessel": {
|
||||
"id": "ABC123",
|
||||
"type": "Truck"
|
||||
"type": "Truck",
|
||||
"capacity": 5000
|
||||
},
|
||||
"measurements": [
|
||||
{
|
||||
"id": 1,
|
||||
"weight": 1500,
|
||||
"timestamp": "2026-01-23T10:00:00"
|
||||
"timestamp": "2026-01-26T08:00:00",
|
||||
"operator": "Alice",
|
||||
"items": [
|
||||
{ "name": "Box A", "quantity": 10, "unitWeight": 50 },
|
||||
{ "name": "Box B", "quantity": 20, "unitWeight": 25 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"weight": 1650,
|
||||
"timestamp": "2026-01-26T09:30:00",
|
||||
"operator": "Bob",
|
||||
"items": [
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Pallet D", "quantity": 15, "unitWeight": 30 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"weight": 1820,
|
||||
"timestamp": "2026-01-26T11:15:00",
|
||||
"operator": "Charlie",
|
||||
"items": [
|
||||
{ "name": "Container E", "quantity": 8, "unitWeight": 75 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"weight": 41820,
|
||||
"timestamp": "2026-01-26T11:15:00",
|
||||
"operator": "4Charlie",
|
||||
"items": [
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Crate C", "quantity": 5, "unitWeight": 100 },
|
||||
{ "name": "Container E", "quantity": 8, "unitWeight": 75 }
|
||||
]
|
||||
}
|
||||
|
||||
],
|
||||
"summary": {
|
||||
"totalMeasurements": 3,
|
||||
"averageWeight": 1656.67,
|
||||
"maxWeight": 1820,
|
||||
"minWeight": 1500
|
||||
}
|
||||
}, null, 2));
|
||||
}
|
||||
setShowManualInput(!showManualInput);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { useReportData } from './DataContext';
|
||||
import { useBandContext } from './BandContext';
|
||||
import { resolveDBTextValue } from './utils/dataResolver';
|
||||
import ResizeHandles from './ResizeHandles';
|
||||
import { useElementDrag } from './hooks/useElementDrag';
|
||||
import { useElementSelection } from './hooks/useElementSelection';
|
||||
import { getElementStyle, isTextMultiLine, getTextWhiteSpace } from './utils/elementUtils';
|
||||
import './elements.css';
|
||||
|
||||
function DBTextField({
|
||||
element,
|
||||
isSelected,
|
||||
isSingleSelection,
|
||||
onSelect,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
charWidth,
|
||||
charHeight,
|
||||
previewMode = false,
|
||||
toolMode,
|
||||
parentBandId = null // ID of parent band (for automatic data binding)
|
||||
}) {
|
||||
const { reportData } = useReportData();
|
||||
const { currentBandData } = useBandContext();
|
||||
|
||||
// Use custom hooks for drag and selection
|
||||
const { isDragging, handleMouseDown: handleDragMouseDown } = useElementDrag({
|
||||
isSelected,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
toolMode
|
||||
});
|
||||
|
||||
const { handleClick } = useElementSelection({
|
||||
isSelected,
|
||||
onSelect,
|
||||
onDelete,
|
||||
toolMode
|
||||
});
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
onSelect(); // Select element before dragging
|
||||
handleDragMouseDown(e, element.id);
|
||||
};
|
||||
|
||||
const handleResize = (updates) => {
|
||||
onUpdate(updates);
|
||||
};
|
||||
|
||||
// Resolve data value
|
||||
const displayContent = previewMode
|
||||
? resolveDBTextValue(reportData, element.objectKey, element.fieldPath, parentBandId, { currentBandData })
|
||||
: parentBandId
|
||||
? `{${element.fieldPath}}` // Inside band: show field only
|
||||
: `{${element.objectKey}.${element.fieldPath}}`; // Outside band: show object.field
|
||||
|
||||
// Check if data is resolved
|
||||
// If inside a band, only fieldPath is required; otherwise both objectKey and fieldPath are required
|
||||
const isUnresolved = parentBandId
|
||||
? (!element.fieldPath || (previewMode && !displayContent))
|
||||
: (!element.objectKey || !element.fieldPath || (previewMode && !displayContent));
|
||||
|
||||
// Use element class methods
|
||||
const multiLine = isTextMultiLine(element);
|
||||
const whiteSpace = getTextWhiteSpace(element);
|
||||
|
||||
const style = {
|
||||
...getElementStyle(element, charWidth, charHeight),
|
||||
minWidth: `${charWidth}px`,
|
||||
minHeight: `${charHeight}px`,
|
||||
whiteSpace
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`db-text-field ${isSelected ? 'selected' : ''} ${isDragging ? 'dragging' : ''} ${isUnresolved ? 'unresolved' : ''}`}
|
||||
style={style}
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<span className="db-text-field-content" style={{ whiteSpace }}>
|
||||
{displayContent || '(No Data)'}
|
||||
</span>
|
||||
{isSingleSelection && (
|
||||
<ResizeHandles
|
||||
element={element}
|
||||
onResize={handleResize}
|
||||
charWidth={charWidth}
|
||||
charHeight={charHeight}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DBTextField;
|
||||
@@ -25,6 +25,13 @@
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.editor-canvas-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editor-canvas.crosshair {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
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;
|
||||
@@ -37,6 +43,19 @@ function EditorCanvas({
|
||||
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)));
|
||||
@@ -92,6 +111,18 @@ function EditorCanvas({
|
||||
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
|
||||
@@ -166,6 +197,36 @@ function EditorCanvas({
|
||||
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) {
|
||||
@@ -208,15 +269,10 @@ function EditorCanvas({
|
||||
const minY = Math.min(rect.startY, rect.endY);
|
||||
const maxY = Math.max(rect.startY, rect.endY);
|
||||
|
||||
return elements.filter(el => {
|
||||
const elMinX = el.x;
|
||||
const elMinY = el.y;
|
||||
const elMaxX = el.x + (el.width || el.length || 1) - 1;
|
||||
const elMaxY = el.y + (el.height || (el.type === 'vline' ? el.length : 1)) - 1;
|
||||
|
||||
// Check if element intersects with selection rectangle
|
||||
return !(elMaxX < minX || elMinX > maxX || elMaxY < minY || elMinY > maxY);
|
||||
}).map(el => el.id);
|
||||
// Use element class method for intersection checking
|
||||
return elements
|
||||
.filter(el => elementIntersectsRect(el, { minX, maxX, minY, maxY }))
|
||||
.map(el => el.id);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
@@ -347,6 +403,66 @@ function EditorCanvas({
|
||||
|
||||
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
|
||||
@@ -355,9 +471,19 @@ function EditorCanvas({
|
||||
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
|
||||
@@ -399,9 +525,27 @@ function EditorCanvas({
|
||||
|
||||
{/* Render elements */}
|
||||
{elements.map(element => {
|
||||
if (element.type === 'text') {
|
||||
// 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 (
|
||||
<TextField
|
||||
<BandElement
|
||||
key={element.id}
|
||||
element={element}
|
||||
isSelected={selectedElementIds.includes(element.id)}
|
||||
@@ -413,81 +557,95 @@ function EditorCanvas({
|
||||
onDrag={handleElementDrag}
|
||||
charWidth={CHAR_WIDTH}
|
||||
charHeight={CHAR_HEIGHT}
|
||||
previewMode={previewMode}
|
||||
toolMode={toolMode}
|
||||
/>
|
||||
);
|
||||
} else if (element.type === 'frame') {
|
||||
return (
|
||||
<FrameElement
|
||||
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)}
|
||||
onDelete={() => onElementDelete(element.id)}
|
||||
onUpdate={(updates) => onElementUpdate(element.id, updates)}
|
||||
onDragStart={handleElementDragStart}
|
||||
onDrag={handleElementDrag}
|
||||
charWidth={CHAR_WIDTH}
|
||||
charHeight={CHAR_HEIGHT}
|
||||
toolMode={toolMode}
|
||||
/>
|
||||
);
|
||||
} else if (element.type === 'hline') {
|
||||
return (
|
||||
<HorizontalLine
|
||||
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)}
|
||||
onDelete={() => onElementDelete(element.id)}
|
||||
onUpdate={(updates) => onElementUpdate(element.id, updates)}
|
||||
onDragStart={handleElementDragStart}
|
||||
onDrag={handleElementDrag}
|
||||
charWidth={CHAR_WIDTH}
|
||||
charHeight={CHAR_HEIGHT}
|
||||
crossroadMap={crossroadMap}
|
||||
toolMode={toolMode}
|
||||
/>
|
||||
);
|
||||
} else if (element.type === 'vline') {
|
||||
return (
|
||||
<VerticalLine
|
||||
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)}
|
||||
onDelete={() => onElementDelete(element.id)}
|
||||
onUpdate={(updates) => onElementUpdate(element.id, updates)}
|
||||
onDragStart={handleElementDragStart}
|
||||
onDrag={handleElementDrag}
|
||||
charWidth={CHAR_WIDTH}
|
||||
charHeight={CHAR_HEIGHT}
|
||||
crossroadMap={crossroadMap}
|
||||
toolMode={toolMode}
|
||||
/>
|
||||
);
|
||||
} else if (element.type === 'symbol') {
|
||||
return (
|
||||
<SymbolElement
|
||||
key={element.id}
|
||||
element={element}
|
||||
isSelected={selectedElementIds.includes(element.id)}
|
||||
onSelect={(e) => onElementSelect(element.id, e?.ctrlKey || e?.metaKey)}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import ResizeHandles from './ResizeHandles';
|
||||
import { useElementDrag } from './hooks/useElementDrag';
|
||||
import { useElementSelection } from './hooks/useElementSelection';
|
||||
import { getElementStyle, isPositionOnFrameBorder } from './utils/elementUtils';
|
||||
import './elements.css';
|
||||
|
||||
const BORDER_CHARS = {
|
||||
@@ -23,20 +26,22 @@ const BORDER_CHARS = {
|
||||
|
||||
function FrameElement({ element, isSelected, isSingleSelection, onSelect, onDelete, onUpdate, onDragStart, onDrag, charWidth, charHeight, toolMode }) {
|
||||
const chars = BORDER_CHARS[element.borderStyle || 'single'];
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragData, setDragData] = useState(null);
|
||||
const [isHoveringBorder, setIsHoveringBorder] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e) => {
|
||||
if (isSelected && e.key === 'Delete') {
|
||||
onDelete();
|
||||
}
|
||||
};
|
||||
// Use custom hooks for drag and selection
|
||||
const { isDragging, handleMouseDown: handleDragMouseDown } = useElementDrag({
|
||||
isSelected,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
toolMode
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
return () => document.removeEventListener('keydown', handleKeyPress);
|
||||
}, [isSelected, onDelete]);
|
||||
useElementSelection({
|
||||
isSelected,
|
||||
onSelect,
|
||||
onDelete,
|
||||
toolMode
|
||||
});
|
||||
|
||||
const renderFrame = () => {
|
||||
const lines = [];
|
||||
@@ -70,11 +75,10 @@ function FrameElement({ element, isSelected, isSingleSelection, onSelect, onDele
|
||||
return lines;
|
||||
};
|
||||
|
||||
// Use element class method for style calculation
|
||||
const baseStyle = getElementStyle(element, charWidth, charHeight);
|
||||
const style = {
|
||||
left: `${element.x * charWidth}px`,
|
||||
top: `${element.y * charHeight}px`,
|
||||
width: `${element.width * charWidth}px`,
|
||||
height: `${element.height * charHeight}px`,
|
||||
...baseStyle,
|
||||
cursor: (toolMode === 'select' && isHoveringBorder) ? 'pointer' : 'default'
|
||||
};
|
||||
|
||||
@@ -88,9 +92,8 @@ function FrameElement({ element, isSelected, isSingleSelection, onSelect, onDele
|
||||
const col = Math.floor(relativeX / charWidth);
|
||||
const row = Math.floor(relativeY / charHeight);
|
||||
|
||||
// Check if on border (first/last row or first/last column)
|
||||
const onBorder = row === 0 || row === element.height - 1 || col === 0 || col === element.width - 1;
|
||||
return onBorder;
|
||||
// Use element class method to check if position is on border
|
||||
return isPositionOnFrameBorder(element, col, row);
|
||||
};
|
||||
|
||||
const handleClick = (e) => {
|
||||
@@ -113,48 +116,14 @@ function FrameElement({ element, isSelected, isSingleSelection, onSelect, onDele
|
||||
// Only handle dragging in select mode and on border clicks
|
||||
if (toolMode !== 'select' || !isClickOnBorder(e)) return;
|
||||
|
||||
e.stopPropagation();
|
||||
onSelect(e);
|
||||
|
||||
setIsDragging(true);
|
||||
const data = onDragStart(element.id, e.clientX, e.clientY);
|
||||
setDragData({ ...data, startMouseX: e.clientX, startMouseY: e.clientY });
|
||||
onSelect(e); // Select element before dragging
|
||||
handleDragMouseDown(e, element.id);
|
||||
};
|
||||
|
||||
const handleResize = (updates) => {
|
||||
onUpdate(updates);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
|
||||
// Add class to body to disable transitions globally during drag
|
||||
document.body.classList.add('dragging-active');
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragData) {
|
||||
const deltaX = e.clientX - dragData.startMouseX;
|
||||
const deltaY = e.clientY - dragData.startMouseY;
|
||||
onDrag(dragData, deltaX, deltaY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
setDragData(null);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
}, [isDragging, dragData, onDrag]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`frame-element ${isSelected ? 'selected' : ''} ${isDragging ? 'dragging' : ''}`}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import ResizeHandles from './ResizeHandles';
|
||||
import { useElementDrag } from './hooks/useElementDrag';
|
||||
import { useElementSelection } from './hooks/useElementSelection';
|
||||
import { getElementStyle } from './utils/elementUtils';
|
||||
import './elements.css';
|
||||
|
||||
const LINE_CHARS = {
|
||||
@@ -21,76 +24,32 @@ function HorizontalLine({
|
||||
crossroadMap = {},
|
||||
toolMode
|
||||
}) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragData, setDragData] = useState(null);
|
||||
|
||||
const lineChar = LINE_CHARS[element.lineStyle || 'single'];
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e) => {
|
||||
if (isSelected && e.key === 'Delete') {
|
||||
onDelete();
|
||||
}
|
||||
};
|
||||
// Use custom hooks for drag and selection
|
||||
const { isDragging, handleMouseDown: handleDragMouseDown } = useElementDrag({
|
||||
isSelected,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
toolMode
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
return () => document.removeEventListener('keydown', handleKeyPress);
|
||||
}, [isSelected, onDelete]);
|
||||
|
||||
const handleClick = (e) => {
|
||||
// Only handle selection in select mode, let other modes bubble to canvas
|
||||
if (toolMode === 'select') {
|
||||
e.stopPropagation();
|
||||
onSelect(e);
|
||||
}
|
||||
};
|
||||
const { handleClick } = useElementSelection({
|
||||
isSelected,
|
||||
onSelect,
|
||||
onDelete,
|
||||
toolMode
|
||||
});
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
// Only handle dragging in select mode
|
||||
if (toolMode !== 'select') return;
|
||||
|
||||
e.stopPropagation();
|
||||
onSelect(e);
|
||||
|
||||
setIsDragging(true);
|
||||
const data = onDragStart(element.id, e.clientX, e.clientY);
|
||||
setDragData({ ...data, startMouseX: e.clientX, startMouseY: e.clientY });
|
||||
onSelect(e); // Select element before dragging
|
||||
handleDragMouseDown(e, element.id);
|
||||
};
|
||||
|
||||
const handleResize = (updates) => {
|
||||
onUpdate(updates);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
|
||||
// Add class to body to disable transitions globally during drag
|
||||
document.body.classList.add('dragging-active');
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragData) {
|
||||
const deltaX = e.clientX - dragData.startMouseX;
|
||||
const deltaY = e.clientY - dragData.startMouseY;
|
||||
onDrag(dragData, deltaX, deltaY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
setDragData(null);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
}, [isDragging, dragData, onDrag]);
|
||||
|
||||
// Render line with possible crossroad overrides
|
||||
const renderLine = () => {
|
||||
let line = '';
|
||||
@@ -109,12 +68,8 @@ function HorizontalLine({
|
||||
return line;
|
||||
};
|
||||
|
||||
const style = {
|
||||
left: `${element.x * charWidth}px`,
|
||||
top: `${element.y * charHeight}px`,
|
||||
width: `${element.length * charWidth}px`,
|
||||
height: `${charHeight}px`
|
||||
};
|
||||
// Use element class method for style calculation
|
||||
const style = getElementStyle(element, charWidth, charHeight);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
.object-inspector {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
border-right: 1px solid #ddd;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
overflow-y: auto;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.inspector-header {
|
||||
background: #e0e0e0;
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.inspector-empty {
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inspector-section {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.property-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.property-row label {
|
||||
flex: 0 0 70px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.property-row input,
|
||||
.property-row select {
|
||||
flex: 1;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.property-row input:disabled {
|
||||
background: #e0e0e0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.property-row select:disabled {
|
||||
background: #e0e0e0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.property-row input:focus,
|
||||
.property-row select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
import React from 'react';
|
||||
import { useReportData } from './DataContext';
|
||||
import { getAvailableObjects, getAvailableFields } from './utils/dataResolver';
|
||||
import { getAvailableArrays } from './utils/bandRenderer';
|
||||
import './ObjectInspector.css';
|
||||
|
||||
function ObjectInspector({ element, onUpdate, allElements = [] }) {
|
||||
const { reportData } = useReportData();
|
||||
|
||||
if (!element) {
|
||||
return (
|
||||
<div className="object-inspector">
|
||||
<div className="inspector-header">Object Inspector</div>
|
||||
<div className="inspector-empty">No element selected</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleChange = (property, value) => {
|
||||
onUpdate({ [property]: value });
|
||||
};
|
||||
|
||||
const handleNumberChange = (property, value) => {
|
||||
const numValue = parseInt(value, 10);
|
||||
if (!isNaN(numValue)) {
|
||||
onUpdate({ [property]: numValue });
|
||||
}
|
||||
};
|
||||
|
||||
// Get available objects and fields for DBTextField
|
||||
// If DBTextField is inside a band, find the band and get fields from its data structure
|
||||
let availableObjects = [];
|
||||
let availableFields = [];
|
||||
let parentBand = null;
|
||||
|
||||
if (element.type === 'dbtext') {
|
||||
// Check if this element is a child of a band
|
||||
parentBand = allElements.find(el => el.type === 'band' && el.children && el.children.includes(element.id));
|
||||
|
||||
if (parentBand && parentBand.dataSource) {
|
||||
// Inside a band - get fields from band's data array structure
|
||||
let bandDataArray;
|
||||
|
||||
if (parentBand.parentBandId) {
|
||||
// Nested band - resolve through parent chain
|
||||
const grandparentBand = allElements.find(el => el.id === parentBand.parentBandId);
|
||||
if (grandparentBand && grandparentBand.dataSource) {
|
||||
const grandparentData = reportData?.[grandparentBand.dataSource];
|
||||
if (Array.isArray(grandparentData) && grandparentData.length > 0) {
|
||||
// Navigate to nested array using parentBand.dataSource
|
||||
const nestedData = grandparentData[0][parentBand.dataSource];
|
||||
if (Array.isArray(nestedData) && nestedData.length > 0) {
|
||||
bandDataArray = nestedData;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Top-level band - get data directly
|
||||
bandDataArray = reportData?.[parentBand.dataSource];
|
||||
}
|
||||
|
||||
if (Array.isArray(bandDataArray) && bandDataArray.length > 0) {
|
||||
// Get fields from first item in array
|
||||
availableFields = getAvailableFields(bandDataArray[0]);
|
||||
}
|
||||
// Don't show object dropdown when inside band
|
||||
availableObjects = [];
|
||||
} else {
|
||||
// Not inside a band - use normal object/field selection
|
||||
availableObjects = getAvailableObjects(reportData);
|
||||
availableFields = element.objectKey ? getAvailableFields(reportData[element.objectKey]) : [];
|
||||
}
|
||||
}
|
||||
|
||||
// Get available arrays for Band data source
|
||||
// If this band has a parent, get arrays from parent's data structure
|
||||
let availableArrays = [];
|
||||
let currentBandParent = null;
|
||||
if (element.type === 'band') {
|
||||
if (element.parentBandId) {
|
||||
currentBandParent = allElements.find(el => el.id === element.parentBandId);
|
||||
}
|
||||
availableArrays = getAvailableArrays(reportData, currentBandParent);
|
||||
}
|
||||
|
||||
// Get all bands for parent band dropdown (excluding current element)
|
||||
const availableBands = element.type === 'band'
|
||||
? allElements.filter(el => el.type === 'band' && el.id !== element.id)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="object-inspector">
|
||||
<div className="inspector-header">Object Inspector</div>
|
||||
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">General</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Type:</label>
|
||||
<input type="text" value={element.type} disabled />
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>X:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={element.x}
|
||||
onChange={(e) => handleNumberChange('x', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Y:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={element.y}
|
||||
onChange={(e) => handleNumberChange('y', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(element.width !== undefined) && (
|
||||
<div className="property-row">
|
||||
<label>Width:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={element.width}
|
||||
onChange={(e) => handleNumberChange('width', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(element.height !== undefined) && (
|
||||
<div className="property-row">
|
||||
<label>Height:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={element.height}
|
||||
onChange={(e) => handleNumberChange('height', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{element.length !== undefined && (
|
||||
<div className="property-row">
|
||||
<label>Length:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={element.length}
|
||||
onChange={(e) => handleNumberChange('length', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* TextField specific */}
|
||||
{element.type === 'text' && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">Text</div>
|
||||
<div className="property-row">
|
||||
<label>Content:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={element.content || ''}
|
||||
onChange={(e) => handleChange('content', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* DBTextField specific */}
|
||||
{element.type === 'dbtext' && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">Data Binding</div>
|
||||
|
||||
{parentBand ? (
|
||||
<>
|
||||
<div className="property-info" style={{
|
||||
fontSize: '11px',
|
||||
color: '#667eea',
|
||||
marginBottom: '8px',
|
||||
padding: '6px',
|
||||
background: 'rgba(102, 126, 234, 0.1)',
|
||||
borderRadius: '3px'
|
||||
}}>
|
||||
{parentBand.parentBandId ? (
|
||||
<>
|
||||
Inside nested band: {parentBand.dataSource}
|
||||
<br />
|
||||
(within {allElements.find(el => el.id === parentBand.parentBandId)?.dataSource})
|
||||
</>
|
||||
) : (
|
||||
<>Inside band: {parentBand.dataSource}</>
|
||||
)}
|
||||
<br />
|
||||
Fields auto-bind to current row
|
||||
</div>
|
||||
<div className="property-row">
|
||||
<label>Field:</label>
|
||||
<select
|
||||
value={element.fieldPath || ''}
|
||||
onChange={(e) => handleChange('fieldPath', e.target.value)}
|
||||
>
|
||||
<option value="">-- Select Field --</option>
|
||||
{availableFields.map(path => (
|
||||
<option key={path} value={path}>{path}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="property-row">
|
||||
<label>Object:</label>
|
||||
<select
|
||||
value={element.objectKey || ''}
|
||||
onChange={(e) => handleChange('objectKey', e.target.value)}
|
||||
>
|
||||
<option value="">-- Select Object --</option>
|
||||
{availableObjects.map(key => (
|
||||
<option key={key} value={key}>{key}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Field:</label>
|
||||
<select
|
||||
value={element.fieldPath || ''}
|
||||
onChange={(e) => handleChange('fieldPath', e.target.value)}
|
||||
disabled={!element.objectKey}
|
||||
>
|
||||
<option value="">-- Select Field --</option>
|
||||
{availableFields.map(path => (
|
||||
<option key={path} value={path}>{path}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Frame/Line specific */}
|
||||
{(element.type === 'frame' || element.type === 'hline' || element.type === 'vline') && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">Style</div>
|
||||
<div className="property-row">
|
||||
<label>Style:</label>
|
||||
<select
|
||||
value={element.borderStyle || element.lineStyle || 'single'}
|
||||
onChange={(e) => {
|
||||
const prop = element.type === 'frame' ? 'borderStyle' : 'lineStyle';
|
||||
handleChange(prop, e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="single">Single</option>
|
||||
<option value="double">Double</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Symbol specific */}
|
||||
{element.type === 'symbol' && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">Symbol</div>
|
||||
<div className="property-row">
|
||||
<label>Character:</label>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={1}
|
||||
value={element.char || ''}
|
||||
onChange={(e) => handleChange('char', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Band specific */}
|
||||
{element.type === 'band' && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">Band</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Type:</label>
|
||||
<select
|
||||
value={element.bandType || 'detail'}
|
||||
onChange={(e) => handleChange('bandType', e.target.value)}
|
||||
>
|
||||
<option value="header">Header</option>
|
||||
<option value="detail">Detail</option>
|
||||
<option value="subdetail">Sub-Detail</option>
|
||||
<option value="footer">Footer</option>
|
||||
<option value="summary">Summary</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Data Source:</label>
|
||||
<select
|
||||
value={element.dataSource || ''}
|
||||
onChange={(e) => handleChange('dataSource', e.target.value)}
|
||||
>
|
||||
<option value="">-- No Data --</option>
|
||||
{availableArrays.map(arr => (
|
||||
<option key={arr.path} value={arr.path}>{arr.path}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Caption:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={element.caption || ''}
|
||||
onChange={(e) => handleChange('caption', e.target.value)}
|
||||
placeholder="Auto-generated"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>Parent Band:</label>
|
||||
<select
|
||||
value={element.parentBandId || ''}
|
||||
onChange={(e) => handleChange('parentBandId', e.target.value || null)}
|
||||
>
|
||||
<option value="">-- None (Root) --</option>
|
||||
{availableBands.map(band => (
|
||||
<option key={band.id} value={band.id}>
|
||||
{band.caption || `${band.bandType}: ${band.dataSource}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ObjectInspector;
|
||||
@@ -4,3 +4,9 @@
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { DataProvider } from './DataContext';
|
||||
import { BandProvider } from './BandContext';
|
||||
import Toolbar from './Toolbar';
|
||||
import EditorCanvas from './EditorCanvas';
|
||||
import ConfigPanel from './ConfigPanel';
|
||||
import CharacterPalette from './CharacterPalette';
|
||||
import ObjectInspector from './ObjectInspector';
|
||||
import './ReportEditor.css';
|
||||
|
||||
function ReportEditorContent() {
|
||||
@@ -24,10 +26,44 @@ function ReportEditorContent() {
|
||||
const [showCharacterPalette, setShowCharacterPalette] = useState(false);
|
||||
|
||||
const handleAddElement = (element) => {
|
||||
setReport(prev => ({
|
||||
setReport(prev => {
|
||||
// Check if the new element is positioned inside a band
|
||||
const containingBand = prev.elements.find(el => {
|
||||
if (el.type !== 'band') return false;
|
||||
// Check if element is within band's Y range
|
||||
const bandMinY = el.y;
|
||||
const bandMaxY = el.y + el.height - 1;
|
||||
const elementY = element.y;
|
||||
return elementY >= bandMinY && elementY <= bandMaxY;
|
||||
});
|
||||
|
||||
// Adjust element position to be relative to band if inside one
|
||||
let adjustedElement = { ...element };
|
||||
if (containingBand) {
|
||||
adjustedElement.y = element.y - containingBand.y;
|
||||
}
|
||||
|
||||
let updatedElements = [...prev.elements, adjustedElement];
|
||||
|
||||
// If inside a band, add element ID to band's children array
|
||||
if (containingBand) {
|
||||
updatedElements = updatedElements.map(el => {
|
||||
if (el.id === containingBand.id) {
|
||||
return {
|
||||
...el,
|
||||
children: [...(el.children || []), adjustedElement.id]
|
||||
};
|
||||
}
|
||||
return el;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
elements: [...prev.elements, element]
|
||||
}));
|
||||
elements: updatedElements
|
||||
};
|
||||
});
|
||||
|
||||
setSelectedElementIds([element.id]);
|
||||
// After adding an element, switch back to select mode (unless in drawTable mode)
|
||||
if (toolMode !== 'drawTable') {
|
||||
@@ -36,12 +72,40 @@ function ReportEditorContent() {
|
||||
};
|
||||
|
||||
const handleElementUpdate = (elementId, updates) => {
|
||||
setReport(prev => ({
|
||||
setReport(prev => {
|
||||
const element = prev.elements.find(el => el.id === elementId);
|
||||
|
||||
// Check if this is a band height change
|
||||
if (element && element.type === 'band' && updates.height !== undefined && updates.height !== element.height) {
|
||||
const heightDelta = updates.height - element.height;
|
||||
const bandMaxY = element.y + element.height - 1;
|
||||
|
||||
// Shift all elements (including other bands) below this band
|
||||
return {
|
||||
...prev,
|
||||
elements: prev.elements.map(el => {
|
||||
if (el.id === elementId) {
|
||||
// Apply updates to the band itself
|
||||
return { ...el, ...updates };
|
||||
}
|
||||
// Shift elements that are below the band (not children of the band)
|
||||
const isChildOfThisBand = element.children && element.children.includes(el.id);
|
||||
if (!isChildOfThisBand && el.y > bandMaxY) {
|
||||
return { ...el, y: el.y + heightDelta };
|
||||
}
|
||||
return el;
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// Normal update (not a band height change)
|
||||
return {
|
||||
...prev,
|
||||
elements: prev.elements.map(el =>
|
||||
el.id === elementId ? { ...el, ...updates } : el
|
||||
)
|
||||
}));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleElementDelete = (elementId) => {
|
||||
@@ -138,6 +202,11 @@ function ReportEditorContent() {
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [showConfigPanel, selectedElementIds, toolMode, report.elements]);
|
||||
|
||||
// Get the selected element for the Object Inspector
|
||||
const selectedElement = selectedElementIds.length === 1
|
||||
? report.elements.find(el => el.id === selectedElementIds[0])
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="report-editor">
|
||||
<Toolbar
|
||||
@@ -149,6 +218,7 @@ function ReportEditorContent() {
|
||||
onTogglePreview={handleTogglePreview}
|
||||
onConfigureAPI={() => setShowConfigPanel(true)}
|
||||
/>
|
||||
<div className="editor-layout">
|
||||
<EditorCanvas
|
||||
elements={report.elements}
|
||||
selectedElementIds={selectedElementIds}
|
||||
@@ -163,6 +233,14 @@ function ReportEditorContent() {
|
||||
previewMode={previewMode}
|
||||
selectedChar={selectedChar}
|
||||
/>
|
||||
</div>
|
||||
{selectedElementIds.length === 1 && (
|
||||
<ObjectInspector
|
||||
element={selectedElement}
|
||||
allElements={report.elements}
|
||||
onUpdate={(updates) => handleElementUpdate(selectedElementIds[0], updates)}
|
||||
/>
|
||||
)}
|
||||
<ConfigPanel
|
||||
apiEndpoint={report.apiEndpoint}
|
||||
onApiEndpointChange={handleApiEndpointChange}
|
||||
@@ -186,7 +264,9 @@ function ReportEditorContent() {
|
||||
function ReportEditor() {
|
||||
return (
|
||||
<DataProvider>
|
||||
<BandProvider>
|
||||
<ReportEditorContent />
|
||||
</BandProvider>
|
||||
</DataProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ function ResizeHandles({ element, onResize, charWidth, charHeight }) {
|
||||
case 'vline':
|
||||
return ['start', 'end'];
|
||||
case 'text':
|
||||
case 'dbtext':
|
||||
return ['nw', 'ne', 'sw', 'se'];
|
||||
case 'band':
|
||||
return ['s']; // Only bottom handle for height adjustment
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -42,6 +45,14 @@ function ResizeHandles({ element, onResize, charWidth, charHeight }) {
|
||||
const contentLength = element.content ? element.content.length : 1;
|
||||
width = (element.width || Math.max(contentLength, 5)) * charWidth;
|
||||
height = (element.height || 1) * charHeight;
|
||||
} else if (element.type === 'dbtext') {
|
||||
// For DB text fields, use stored dimensions
|
||||
width = (element.width || 10) * charWidth;
|
||||
height = (element.height || 1) * charHeight;
|
||||
} else if (element.type === 'band') {
|
||||
// For bands, always full width
|
||||
width = 80 * charWidth;
|
||||
height = (element.height || 3) * charHeight;
|
||||
} else {
|
||||
// For frames and other elements
|
||||
width = (element.width || 1) * charWidth;
|
||||
@@ -179,7 +190,7 @@ function ResizeHandles({ element, onResize, charWidth, charHeight }) {
|
||||
// Moving end point up/down
|
||||
updates.length = Math.max(MIN_LINE_LENGTH, initial.length + deltaRow);
|
||||
}
|
||||
} else if (element.type === 'text') {
|
||||
} else if (element.type === 'text' || element.type === 'dbtext') {
|
||||
const initial = dragData.initialElement;
|
||||
|
||||
switch (dragData.handle) {
|
||||
@@ -208,6 +219,14 @@ function ResizeHandles({ element, onResize, charWidth, charHeight }) {
|
||||
updates.height = Math.max(MIN_TEXT_SIZE.height, initial.height + deltaRow);
|
||||
break;
|
||||
}
|
||||
} else if (element.type === 'band') {
|
||||
const initial = dragData.initialElement;
|
||||
const MIN_BAND_HEIGHT = 1;
|
||||
|
||||
// Bands only resize vertically (height)
|
||||
if (dragData.handle === 's') {
|
||||
updates.height = Math.max(MIN_BAND_HEIGHT, initial.height + deltaRow);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { useElementDrag } from './hooks/useElementDrag';
|
||||
import { useElementSelection } from './hooks/useElementSelection';
|
||||
import { getElementStyle } from './utils/elementUtils';
|
||||
import './elements.css';
|
||||
|
||||
function SymbolElement({
|
||||
@@ -12,76 +15,28 @@ function SymbolElement({
|
||||
charHeight,
|
||||
toolMode
|
||||
}) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragData, setDragData] = useState(null);
|
||||
// Use custom hooks for drag and selection
|
||||
const { isDragging, handleMouseDown: handleDragMouseDown } = useElementDrag({
|
||||
isSelected,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
toolMode
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e) => {
|
||||
if (isSelected && e.key === 'Delete') {
|
||||
onDelete();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
return () => document.removeEventListener('keydown', handleKeyPress);
|
||||
}, [isSelected, onDelete]);
|
||||
|
||||
const handleClick = (e) => {
|
||||
// Only handle selection in select mode, let other modes bubble to canvas
|
||||
if (toolMode === 'select') {
|
||||
e.stopPropagation();
|
||||
onSelect(e);
|
||||
}
|
||||
};
|
||||
const { handleClick } = useElementSelection({
|
||||
isSelected,
|
||||
onSelect,
|
||||
onDelete,
|
||||
toolMode
|
||||
});
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
// Only handle dragging in select mode
|
||||
if (toolMode !== 'select') return;
|
||||
|
||||
e.stopPropagation();
|
||||
onSelect(e);
|
||||
|
||||
setIsDragging(true);
|
||||
const data = onDragStart(element.id, e.clientX, e.clientY);
|
||||
setDragData({ ...data, startMouseX: e.clientX, startMouseY: e.clientY });
|
||||
onSelect(e); // Select element before dragging
|
||||
handleDragMouseDown(e, element.id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
|
||||
// Add class to body to disable transitions globally during drag
|
||||
document.body.classList.add('dragging-active');
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragData) {
|
||||
const deltaX = e.clientX - dragData.startMouseX;
|
||||
const deltaY = e.clientY - dragData.startMouseY;
|
||||
onDrag(dragData, deltaX, deltaY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
setDragData(null);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
}, [isDragging, dragData, onDrag]);
|
||||
|
||||
const style = {
|
||||
left: `${element.x * charWidth}px`,
|
||||
top: `${element.y * charHeight}px`,
|
||||
width: `${charWidth}px`,
|
||||
height: `${charHeight}px`
|
||||
};
|
||||
// Use element class method for style calculation
|
||||
const style = getElementStyle(element, charWidth, charHeight);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useReportData } from './DataContext';
|
||||
import { parseBindings, hasBindings } from './utils/bindingParser';
|
||||
import ResizeHandles from './ResizeHandles';
|
||||
import { useElementDrag } from './hooks/useElementDrag';
|
||||
import { useElementSelection } from './hooks/useElementSelection';
|
||||
import { getElementStyle, isTextMultiLine, getTextWhiteSpace } from './utils/elementUtils';
|
||||
import './elements.css';
|
||||
|
||||
function TextField({
|
||||
@@ -15,16 +16,29 @@ function TextField({
|
||||
onDrag,
|
||||
charWidth,
|
||||
charHeight,
|
||||
previewMode = false,
|
||||
toolMode
|
||||
}) {
|
||||
const { reportData } = useReportData();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editContent, setEditContent] = useState(element.content);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragData, setDragData] = useState(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
// Use custom hooks for drag and selection
|
||||
const { isDragging, handleMouseDown: handleDragMouseDown } = useElementDrag({
|
||||
isSelected,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
toolMode
|
||||
});
|
||||
|
||||
const { handleClick } = useElementSelection({
|
||||
isSelected,
|
||||
onSelect,
|
||||
onDelete: () => {
|
||||
if (!isEditing) onDelete();
|
||||
},
|
||||
toolMode
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
@@ -32,14 +46,6 @@ function TextField({
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const handleClick = (e) => {
|
||||
// Only handle selection in select mode, let other modes bubble to canvas
|
||||
if (toolMode === 'select') {
|
||||
e.stopPropagation();
|
||||
onSelect(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoubleClick = (e) => {
|
||||
// Only allow editing in select mode
|
||||
if (toolMode === 'select') {
|
||||
@@ -72,92 +78,37 @@ function TextField({
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
if (isEditing) return;
|
||||
|
||||
// Only handle dragging in select mode
|
||||
if (toolMode !== 'select') return;
|
||||
|
||||
e.stopPropagation();
|
||||
onSelect();
|
||||
|
||||
setIsDragging(true);
|
||||
const data = onDragStart(element.id, e.clientX, e.clientY);
|
||||
setDragData({ ...data, startMouseX: e.clientX, startMouseY: e.clientY });
|
||||
onSelect(); // Select element before dragging
|
||||
handleDragMouseDown(e, element.id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
|
||||
// Add class to body to disable transitions globally during drag
|
||||
document.body.classList.add('dragging-active');
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragData) {
|
||||
const deltaX = e.clientX - dragData.startMouseX;
|
||||
const deltaY = e.clientY - dragData.startMouseY;
|
||||
onDrag(dragData, deltaX, deltaY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
setDragData(null);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
}, [isDragging, dragData, onDrag]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSelected) {
|
||||
setIsEditing(false);
|
||||
}
|
||||
}, [isSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e) => {
|
||||
if (isSelected && !isEditing && e.key === 'Delete') {
|
||||
onDelete();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
return () => document.removeEventListener('keydown', handleKeyPress);
|
||||
}, [isSelected, isEditing, onDelete]);
|
||||
|
||||
const handleResize = (updates) => {
|
||||
onUpdate(updates);
|
||||
};
|
||||
|
||||
// Resolve bindings if in preview mode
|
||||
const displayContent = previewMode
|
||||
? parseBindings(element.content, reportData)
|
||||
: element.content;
|
||||
// Display content directly (no binding resolution)
|
||||
const displayContent = element.content;
|
||||
|
||||
// Check if field has bindings for styling
|
||||
const fieldHasBindings = hasBindings(element.content);
|
||||
|
||||
const isMultiLine = element.height && element.height > 1;
|
||||
// Use element class methods
|
||||
const multiLine = isTextMultiLine(element);
|
||||
const whiteSpace = getTextWhiteSpace(element);
|
||||
|
||||
const style = {
|
||||
left: `${element.x * charWidth}px`,
|
||||
top: `${element.y * charHeight}px`,
|
||||
width: element.width ? `${element.width * charWidth}px` : 'auto',
|
||||
height: element.height ? `${element.height * charHeight}px` : 'auto',
|
||||
...getElementStyle(element, charWidth, charHeight),
|
||||
minWidth: `${charWidth}px`,
|
||||
minHeight: `${charHeight}px`,
|
||||
whiteSpace: isMultiLine ? 'pre-wrap' : 'nowrap'
|
||||
whiteSpace
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`text-field ${isSelected ? 'selected' : ''} ${isDragging ? 'dragging' : ''} ${fieldHasBindings && !previewMode ? 'has-bindings' : ''}`}
|
||||
className={`text-field ${isSelected ? 'selected' : ''} ${isDragging ? 'dragging' : ''}`}
|
||||
style={style}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
@@ -177,7 +128,7 @@ function TextField({
|
||||
) : (
|
||||
<span
|
||||
className="text-field-content"
|
||||
style={{ whiteSpace: isMultiLine ? 'pre-wrap' : 'nowrap' }}
|
||||
style={{ whiteSpace }}
|
||||
>
|
||||
{displayContent}
|
||||
</span>
|
||||
|
||||
@@ -27,6 +27,13 @@ function Toolbar({
|
||||
>
|
||||
T Add Text
|
||||
</button>
|
||||
<button
|
||||
className={`toolbar-button ${toolMode === 'addDBText' ? 'active' : ''}`}
|
||||
onClick={() => onToolChange('addDBText')}
|
||||
title="Add Database Text Field"
|
||||
>
|
||||
DB DB Text
|
||||
</button>
|
||||
<button
|
||||
className={`toolbar-button ${toolMode === 'addFrame' ? 'active' : ''}`}
|
||||
onClick={() => onToolChange('addFrame')}
|
||||
@@ -55,6 +62,13 @@ function Toolbar({
|
||||
>
|
||||
⊞ Draw
|
||||
</button>
|
||||
<button
|
||||
className={`toolbar-button ${toolMode === 'addBand' ? 'active' : ''}`}
|
||||
onClick={() => onToolChange('addBand')}
|
||||
title="Add Band (B)"
|
||||
>
|
||||
▦ Band
|
||||
</button>
|
||||
</div>
|
||||
<div className="toolbar-separator"></div>
|
||||
<div className="toolbar-group">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import ResizeHandles from './ResizeHandles';
|
||||
import { useElementDrag } from './hooks/useElementDrag';
|
||||
import { useElementSelection } from './hooks/useElementSelection';
|
||||
import { getElementStyle } from './utils/elementUtils';
|
||||
import './elements.css';
|
||||
|
||||
const LINE_CHARS = {
|
||||
@@ -21,76 +24,32 @@ function VerticalLine({
|
||||
crossroadMap = {},
|
||||
toolMode
|
||||
}) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragData, setDragData] = useState(null);
|
||||
|
||||
const lineChar = LINE_CHARS[element.lineStyle || 'single'];
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e) => {
|
||||
if (isSelected && e.key === 'Delete') {
|
||||
onDelete();
|
||||
}
|
||||
};
|
||||
// Use custom hooks for drag and selection
|
||||
const { isDragging, handleMouseDown: handleDragMouseDown } = useElementDrag({
|
||||
isSelected,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
toolMode
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
return () => document.removeEventListener('keydown', handleKeyPress);
|
||||
}, [isSelected, onDelete]);
|
||||
|
||||
const handleClick = (e) => {
|
||||
// Only handle selection in select mode, let other modes bubble to canvas
|
||||
if (toolMode === 'select') {
|
||||
e.stopPropagation();
|
||||
onSelect(e);
|
||||
}
|
||||
};
|
||||
const { handleClick } = useElementSelection({
|
||||
isSelected,
|
||||
onSelect,
|
||||
onDelete,
|
||||
toolMode
|
||||
});
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
// Only handle dragging in select mode
|
||||
if (toolMode !== 'select') return;
|
||||
|
||||
e.stopPropagation();
|
||||
onSelect(e);
|
||||
|
||||
setIsDragging(true);
|
||||
const data = onDragStart(element.id, e.clientX, e.clientY);
|
||||
setDragData({ ...data, startMouseX: e.clientX, startMouseY: e.clientY });
|
||||
onSelect(e); // Select element before dragging
|
||||
handleDragMouseDown(e, element.id);
|
||||
};
|
||||
|
||||
const handleResize = (updates) => {
|
||||
onUpdate(updates);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
|
||||
// Add class to body to disable transitions globally during drag
|
||||
document.body.classList.add('dragging-active');
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragData) {
|
||||
const deltaX = e.clientX - dragData.startMouseX;
|
||||
const deltaY = e.clientY - dragData.startMouseY;
|
||||
onDrag(dragData, deltaX, deltaY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
setDragData(null);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
}, [isDragging, dragData, onDrag]);
|
||||
|
||||
// Render line vertically with possible crossroad overrides
|
||||
const renderLine = () => {
|
||||
const lines = [];
|
||||
@@ -111,12 +70,8 @@ function VerticalLine({
|
||||
return lines;
|
||||
};
|
||||
|
||||
const style = {
|
||||
left: `${element.x * charWidth}px`,
|
||||
top: `${element.y * charHeight}px`,
|
||||
width: `${charWidth}px`,
|
||||
height: `${element.length * charHeight}px`
|
||||
};
|
||||
// Use element class method for style calculation
|
||||
const style = getElementStyle(element, charWidth, charHeight);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Common wrapper component for all element types
|
||||
* Handles common styling, click handlers, and resize handles
|
||||
*/
|
||||
function ElementWrapper({
|
||||
className,
|
||||
style,
|
||||
isSelected,
|
||||
isDragging,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
onMouseDown,
|
||||
children,
|
||||
showResizeHandles,
|
||||
ResizeHandles
|
||||
}) {
|
||||
const classes = [
|
||||
className,
|
||||
isSelected ? 'selected' : '',
|
||||
isDragging ? 'dragging' : ''
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
{children}
|
||||
{showResizeHandles && ResizeHandles}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ElementWrapper;
|
||||
@@ -42,15 +42,6 @@ body.dragging-active .text-field {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.text-field.has-bindings {
|
||||
background: rgba(102, 182, 234, 0.1);
|
||||
border-color: rgba(102, 182, 234, 0.4);
|
||||
}
|
||||
|
||||
.text-field.has-bindings:hover {
|
||||
border-color: rgba(102, 182, 234, 0.6);
|
||||
}
|
||||
|
||||
.text-field-content {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
@@ -226,3 +217,123 @@ body.dragging-active .symbol-element {
|
||||
background: #ff6600;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* DB Text Field elements */
|
||||
.db-text-field {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
border: 2px solid transparent;
|
||||
box-sizing: content-box;
|
||||
font-family: 'Courier New', 'Consolas', monospace;
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
white-space: nowrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
overflow: hidden;
|
||||
letter-spacing: -1.2px;
|
||||
font-variant-ligatures: none;
|
||||
background: rgba(76, 175, 80, 0.05);
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
will-change: auto;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
/* Disable transitions when any element is being dragged */
|
||||
body.dragging-active .db-text-field {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.db-text-field:hover {
|
||||
border-color: rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.db-text-field.selected {
|
||||
border-color: #4caf50;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
.db-text-field.dragging {
|
||||
opacity: 0.7;
|
||||
cursor: grabbing;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.db-text-field.unresolved {
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
border-color: rgba(255, 152, 0, 0.3);
|
||||
}
|
||||
|
||||
.db-text-field-content {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Band elements */
|
||||
.band-element {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
/* Disable transitions when any element is being dragged */
|
||||
body.dragging-active .band-element {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.band-element.selected {
|
||||
background: rgba(102, 126, 234, 0.1) !important;
|
||||
}
|
||||
|
||||
.band-element.dragging {
|
||||
opacity: 0.7;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.band-caption {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 4px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 10px;
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.band-children {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.band-children > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Page break indicator for preview mode */
|
||||
.page-break-indicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border-top: 2px dashed #999;
|
||||
pointer-events: none;
|
||||
z-index: 500;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Custom hook for handling element dragging with grid snapping
|
||||
* @param {Object} params
|
||||
* @param {boolean} params.isSelected - Whether element is selected
|
||||
* @param {Function} params.onDragStart - Callback when drag starts
|
||||
* @param {Function} params.onDrag - Callback during drag
|
||||
* @param {string} params.toolMode - Current tool mode
|
||||
* @returns {Object} Drag state and handlers
|
||||
*/
|
||||
export function useElementDrag({ isSelected, onDragStart, onDrag, toolMode }) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragData, setDragData] = useState(null);
|
||||
|
||||
const handleMouseDown = (e, elementId) => {
|
||||
// Only handle dragging in select mode
|
||||
if (toolMode !== 'select') return;
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
setIsDragging(true);
|
||||
const data = onDragStart(elementId, e.clientX, e.clientY);
|
||||
setDragData({ ...data, startMouseX: e.clientX, startMouseY: e.clientY });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
|
||||
// Add class to body to disable transitions globally during drag
|
||||
document.body.classList.add('dragging-active');
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragData) {
|
||||
const deltaX = e.clientX - dragData.startMouseX;
|
||||
const deltaY = e.clientY - dragData.startMouseY;
|
||||
onDrag(dragData, deltaX, deltaY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
setDragData(null);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.classList.remove('dragging-active');
|
||||
};
|
||||
}, [isDragging, dragData, onDrag]);
|
||||
|
||||
return {
|
||||
isDragging,
|
||||
handleMouseDown
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Custom hook for handling element selection and deletion
|
||||
* @param {Object} params
|
||||
* @param {boolean} params.isSelected - Whether element is selected
|
||||
* @param {Function} params.onSelect - Callback when element is selected
|
||||
* @param {Function} params.onDelete - Callback when element is deleted
|
||||
* @param {string} params.toolMode - Current tool mode
|
||||
* @returns {Object} Selection handlers
|
||||
*/
|
||||
export function useElementSelection({ isSelected, onSelect, onDelete, toolMode }) {
|
||||
// Handle click for selection
|
||||
const handleClick = (e) => {
|
||||
// Only handle selection in select mode, let other modes bubble to canvas
|
||||
if (toolMode === 'select') {
|
||||
e.stopPropagation();
|
||||
onSelect(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle delete key press
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (e) => {
|
||||
if (isSelected && e.key === 'Delete') {
|
||||
onDelete();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
return () => document.removeEventListener('keydown', handleKeyPress);
|
||||
}, [isSelected, onDelete]);
|
||||
|
||||
return {
|
||||
handleClick
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* Base Element class for all report elements
|
||||
* Provides common properties and methods
|
||||
*/
|
||||
export class Element {
|
||||
constructor({ id, type, x, y }) {
|
||||
this.id = id || `${type}-${Date.now()}`;
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate pixel position from grid coordinates
|
||||
*/
|
||||
getPixelPosition(charWidth, charHeight) {
|
||||
return {
|
||||
left: this.x * charWidth,
|
||||
top: this.y * charHeight
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update element properties
|
||||
*/
|
||||
update(updates) {
|
||||
Object.assign(this, updates);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if element intersects with a rectangle
|
||||
*/
|
||||
intersectsRect(rect) {
|
||||
const bounds = this.getBounds();
|
||||
return !(
|
||||
bounds.maxX < rect.minX ||
|
||||
bounds.minX > rect.maxX ||
|
||||
bounds.maxY < rect.minY ||
|
||||
bounds.minY > rect.maxY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element bounds (must be overridden by subclasses)
|
||||
*/
|
||||
getBounds() {
|
||||
return {
|
||||
minX: this.x,
|
||||
minY: this.y,
|
||||
maxX: this.x,
|
||||
maxY: this.y
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element dimensions in grid units (must be overridden by subclasses)
|
||||
*/
|
||||
getDimensions() {
|
||||
return { width: 1, height: 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get style object for rendering
|
||||
*/
|
||||
getStyle(charWidth, charHeight) {
|
||||
const pos = this.getPixelPosition(charWidth, charHeight);
|
||||
const dims = this.getDimensions();
|
||||
return {
|
||||
left: `${pos.left}px`,
|
||||
top: `${pos.top}px`,
|
||||
width: `${dims.width * charWidth}px`,
|
||||
height: `${dims.height * charHeight}px`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize to JSON
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
x: this.x,
|
||||
y: this.y
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create element from JSON
|
||||
*/
|
||||
static fromJSON(json) {
|
||||
const elementClasses = {
|
||||
text: TextElement,
|
||||
dbtext: DBTextElement,
|
||||
frame: FrameElement,
|
||||
hline: HorizontalLineElement,
|
||||
vline: VerticalLineElement,
|
||||
symbol: SymbolElement,
|
||||
band: BandElement
|
||||
};
|
||||
|
||||
const ElementClass = elementClasses[json.type];
|
||||
if (!ElementClass) {
|
||||
throw new Error(`Unknown element type: ${json.type}`);
|
||||
}
|
||||
|
||||
return new ElementClass(json);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Database-bound text field element
|
||||
*/
|
||||
export class DBTextElement extends Element {
|
||||
constructor({ id, x, y, width = 10, height = 1, objectKey = '', fieldPath = '' }) {
|
||||
super({ id, type: 'dbtext', x, y });
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.objectKey = objectKey;
|
||||
this.fieldPath = fieldPath;
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return {
|
||||
minX: this.x,
|
||||
minY: this.y,
|
||||
maxX: this.x + this.width - 1,
|
||||
maxY: this.y + this.height - 1
|
||||
};
|
||||
}
|
||||
|
||||
getDimensions() {
|
||||
return { width: this.width, height: this.height };
|
||||
}
|
||||
|
||||
isMultiLine() {
|
||||
return this.height > 1;
|
||||
}
|
||||
|
||||
getWhiteSpace() {
|
||||
return this.isMultiLine() ? 'pre-wrap' : 'nowrap';
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
objectKey: this.objectKey,
|
||||
fieldPath: this.fieldPath
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text field element
|
||||
*/
|
||||
export class TextElement extends Element {
|
||||
constructor({ id, x, y, content = '', width = 10, height = 1 }) {
|
||||
super({ id, type: 'text', x, y });
|
||||
this.content = content;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return {
|
||||
minX: this.x,
|
||||
minY: this.y,
|
||||
maxX: this.x + this.width - 1,
|
||||
maxY: this.y + this.height - 1
|
||||
};
|
||||
}
|
||||
|
||||
getDimensions() {
|
||||
return { width: this.width, height: this.height };
|
||||
}
|
||||
|
||||
isMultiLine() {
|
||||
return this.height > 1;
|
||||
}
|
||||
|
||||
getWhiteSpace() {
|
||||
return this.isMultiLine() ? 'pre-wrap' : 'nowrap';
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
content: this.content,
|
||||
width: this.width,
|
||||
height: this.height
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Frame element
|
||||
*/
|
||||
export class FrameElement extends Element {
|
||||
constructor({ id, x, y, width = 10, height = 5, borderStyle = 'single' }) {
|
||||
super({ id, type: 'frame', x, y });
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.borderStyle = borderStyle;
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return {
|
||||
minX: this.x,
|
||||
minY: this.y,
|
||||
maxX: this.x + this.width - 1,
|
||||
maxY: this.y + this.height - 1
|
||||
};
|
||||
}
|
||||
|
||||
getDimensions() {
|
||||
return { width: this.width, height: this.height };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a position (relative to element) is on the border
|
||||
*/
|
||||
isPositionOnBorder(relativeCol, relativeRow) {
|
||||
return (
|
||||
relativeRow === 0 ||
|
||||
relativeRow === this.height - 1 ||
|
||||
relativeCol === 0 ||
|
||||
relativeCol === this.width - 1
|
||||
);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
borderStyle: this.borderStyle
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontal line element
|
||||
*/
|
||||
export class HorizontalLineElement extends Element {
|
||||
constructor({ id, x, y, length = 5, lineStyle = 'single' }) {
|
||||
super({ id, type: 'hline', x, y });
|
||||
this.length = length;
|
||||
this.lineStyle = lineStyle;
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return {
|
||||
minX: this.x,
|
||||
minY: this.y,
|
||||
maxX: this.x + this.length - 1,
|
||||
maxY: this.y
|
||||
};
|
||||
}
|
||||
|
||||
getDimensions() {
|
||||
return { width: this.length, height: 1 };
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
length: this.length,
|
||||
lineStyle: this.lineStyle
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vertical line element
|
||||
*/
|
||||
export class VerticalLineElement extends Element {
|
||||
constructor({ id, x, y, length = 5, lineStyle = 'single' }) {
|
||||
super({ id, type: 'vline', x, y });
|
||||
this.length = length;
|
||||
this.lineStyle = lineStyle;
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return {
|
||||
minX: this.x,
|
||||
minY: this.y,
|
||||
maxX: this.x,
|
||||
maxY: this.y + this.length - 1
|
||||
};
|
||||
}
|
||||
|
||||
getDimensions() {
|
||||
return { width: 1, height: this.length };
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
length: this.length,
|
||||
lineStyle: this.lineStyle
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Symbol element (single character)
|
||||
*/
|
||||
export class SymbolElement extends Element {
|
||||
constructor({ id, x, y, char = '─' }) {
|
||||
super({ id, type: 'symbol', x, y });
|
||||
this.char = char;
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return {
|
||||
minX: this.x,
|
||||
minY: this.y,
|
||||
maxX: this.x,
|
||||
maxY: this.y
|
||||
};
|
||||
}
|
||||
|
||||
getDimensions() {
|
||||
return { width: 1, height: 1 };
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
char: this.char
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Band element (repeating section for data arrays)
|
||||
*/
|
||||
export class BandElement extends Element {
|
||||
constructor({
|
||||
id,
|
||||
y,
|
||||
height = 3,
|
||||
bandType = 'detail',
|
||||
dataSource = '',
|
||||
caption = '',
|
||||
children = [],
|
||||
parentBandId = null
|
||||
}) {
|
||||
super({ id, type: 'band', x: 0, y });
|
||||
this.width = 80; // Always full width
|
||||
this.height = height;
|
||||
this.bandType = bandType;
|
||||
this.dataSource = dataSource;
|
||||
this.caption = caption;
|
||||
this.children = children; // Array of child element IDs
|
||||
this.parentBandId = parentBandId; // For nested bands
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return {
|
||||
minX: 0,
|
||||
minY: this.y,
|
||||
maxX: 79,
|
||||
maxY: this.y + this.height - 1
|
||||
};
|
||||
}
|
||||
|
||||
getDimensions() {
|
||||
return { width: 80, height: this.height };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this band overlaps with another band
|
||||
*/
|
||||
overlaps(otherBand) {
|
||||
if (!otherBand || otherBand.type !== 'band') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const thisBounds = this.getBounds();
|
||||
const otherBounds = otherBand.getBounds();
|
||||
|
||||
return !(
|
||||
thisBounds.maxY < otherBounds.minY ||
|
||||
thisBounds.minY > otherBounds.maxY
|
||||
);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
bandType: this.bandType,
|
||||
dataSource: this.dataSource,
|
||||
caption: this.caption,
|
||||
children: this.children,
|
||||
parentBandId: this.parentBandId
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Utilities for rendering bands in preview mode
|
||||
* Handles band iteration, nesting, and layout calculation
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolve nested path in object (e.g., 'contact.phone')
|
||||
*/
|
||||
function resolveNestedPath(obj, path) {
|
||||
if (!path || !obj) return undefined;
|
||||
|
||||
const parts = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current && typeof current === 'object' && part in current) {
|
||||
current = current[part];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data array for a band
|
||||
* @param {Object} reportData - The full report data
|
||||
* @param {string} dataSource - Path to array in reportData (e.g., 'measurements', 'items')
|
||||
* @param {Object} parentBandData - Data from parent band (for nested bands)
|
||||
* @returns {Array} - Array of data items for band iteration
|
||||
*/
|
||||
export function getBandDataArray(reportData, dataSource, parentBandData = null) {
|
||||
if (!dataSource) return [];
|
||||
|
||||
// If inside parent band, use parent's current data
|
||||
if (parentBandData) {
|
||||
const data = resolveNestedPath(parentBandData, dataSource);
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
// Otherwise, use reportData
|
||||
if (!reportData) return [];
|
||||
const data = reportData[dataSource];
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate band instances for preview mode
|
||||
* Returns array of band instances with positions and data
|
||||
* @param {Array} bands - Array of band elements
|
||||
* @param {Object} reportData - The full report data
|
||||
* @param {Array} allElements - All elements in the report
|
||||
* @param {Object} parentBandData - Data from parent band (for nested bands)
|
||||
* @param {number} yOffset - Y offset for nested bands (parent instance's Y position)
|
||||
* @param {number} parentDesignY - Design Y position of parent band (for relative positioning)
|
||||
* @returns {Array} - Array of band instances
|
||||
*/
|
||||
export function calculateBandInstances(bands, reportData, allElements, parentBandData = null, yOffset = null, parentDesignY = null) {
|
||||
const instances = [];
|
||||
|
||||
// Get bands at current nesting level
|
||||
const currentLevelBands = bands.filter(band =>
|
||||
parentBandData ? band.parentBandId === parentBandData.parentBandId : !band.parentBandId
|
||||
);
|
||||
|
||||
// Sort bands by Y position
|
||||
const sortedBands = [...currentLevelBands].sort((a, b) => a.y - b.y);
|
||||
|
||||
// Track where previous band ended in both design and preview mode
|
||||
let previousBandDesignEnd = null;
|
||||
let previousBandPreviewEnd = null;
|
||||
let currentY = 0;
|
||||
|
||||
for (const band of sortedBands) {
|
||||
// Calculate starting Y for this band
|
||||
if (previousBandDesignEnd === null) {
|
||||
// First band at this nesting level
|
||||
if (yOffset !== null && parentDesignY !== null) {
|
||||
// Nested band: position relative to parent
|
||||
const relativeY = band.y - parentDesignY;
|
||||
currentY = yOffset + relativeY;
|
||||
} else {
|
||||
// Top-level band: use design Y
|
||||
currentY = band.y;
|
||||
}
|
||||
} else {
|
||||
// Subsequent bands: maintain the gap between bands from design mode
|
||||
const designGap = band.y - previousBandDesignEnd - 1;
|
||||
// Start this band after the previous band's preview end + the design gap
|
||||
currentY = previousBandPreviewEnd + 1 + designGap;
|
||||
}
|
||||
|
||||
const bandStartY = currentY;
|
||||
const dataArray = getBandDataArray(reportData, band.dataSource, parentBandData);
|
||||
|
||||
// Get child elements for this band
|
||||
const childElements = allElements.filter(el => band.children && band.children.includes(el.id));
|
||||
|
||||
// Get nested bands (bands that have this band as parent)
|
||||
const nestedBands = bands.filter(b => b.parentBandId === band.id);
|
||||
|
||||
if (dataArray.length === 0) {
|
||||
// No data - render once with null data (for static bands like headers/footers)
|
||||
const bandInstance = {
|
||||
bandId: band.id,
|
||||
instanceIndex: 0,
|
||||
y: currentY,
|
||||
height: band.height,
|
||||
data: null,
|
||||
childElements: childElements,
|
||||
originalBandY: band.y,
|
||||
bandType: band.bandType
|
||||
};
|
||||
|
||||
instances.push(bandInstance);
|
||||
|
||||
// Handle nested bands with no data
|
||||
if (nestedBands.length > 0) {
|
||||
const nestedInstances = calculateBandInstances(
|
||||
nestedBands,
|
||||
reportData,
|
||||
allElements,
|
||||
null, // No parent data
|
||||
currentY, // Parent instance Y
|
||||
band.y // Parent design Y
|
||||
);
|
||||
instances.push(...nestedInstances);
|
||||
currentY = Math.max(currentY + band.height, ...nestedInstances.map(ni => ni.y + ni.height));
|
||||
} else {
|
||||
currentY += band.height;
|
||||
}
|
||||
} else {
|
||||
// Render once per data item
|
||||
dataArray.forEach((item, index) => {
|
||||
const bandInstance = {
|
||||
bandId: band.id,
|
||||
instanceIndex: index,
|
||||
y: currentY,
|
||||
height: band.height,
|
||||
data: item,
|
||||
childElements: childElements,
|
||||
originalBandY: band.y,
|
||||
bandType: band.bandType
|
||||
};
|
||||
|
||||
instances.push(bandInstance);
|
||||
|
||||
// Handle nested bands
|
||||
if (nestedBands.length > 0) {
|
||||
const nestedInstances = calculateBandInstances(
|
||||
nestedBands,
|
||||
reportData,
|
||||
allElements,
|
||||
{ ...item, parentBandId: band.id }, // Pass current item as parent data
|
||||
currentY, // Parent instance Y
|
||||
band.y // Parent design Y
|
||||
);
|
||||
instances.push(...nestedInstances);
|
||||
// Update currentY to account for nested band height
|
||||
const nestedHeight = nestedInstances.reduce((max, ni) => Math.max(max, ni.y + ni.height), currentY + band.height);
|
||||
currentY = nestedHeight;
|
||||
} else {
|
||||
currentY += band.height;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Track where this band ended in both design and preview mode
|
||||
previousBandDesignEnd = band.y + band.height - 1;
|
||||
previousBandPreviewEnd = currentY - 1;
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total content height for preview mode
|
||||
* @param {Array} bands - Array of band elements
|
||||
* @param {Object} reportData - The full report data
|
||||
* @param {Array} allElements - All elements in the report
|
||||
* @param {number} minHeight - Minimum height (default canvas height)
|
||||
* @returns {number} - Total height in grid rows
|
||||
*/
|
||||
export function calculateTotalContentHeight(bands, reportData, allElements, minHeight = 66) {
|
||||
if (bands.length === 0) {
|
||||
return minHeight;
|
||||
}
|
||||
|
||||
const instances = calculateBandInstances(bands, reportData, allElements);
|
||||
|
||||
if (instances.length === 0) {
|
||||
return minHeight;
|
||||
}
|
||||
|
||||
// Find the maximum Y + height from all instances
|
||||
const maxY = instances.reduce((max, instance) => {
|
||||
return Math.max(max, instance.y + instance.height);
|
||||
}, 0);
|
||||
|
||||
// Also consider non-band elements
|
||||
const nonBandElements = allElements.filter(el => el.type !== 'band' && !bands.some(b => b.children.includes(el.id)));
|
||||
const maxElementY = nonBandElements.reduce((max, el) => {
|
||||
const bounds = el.getBounds ? el.getBounds() : { maxY: el.y };
|
||||
return Math.max(max, bounds.maxY + 1);
|
||||
}, 0);
|
||||
|
||||
return Math.max(minHeight, maxY, maxElementY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available arrays from reportData (for ObjectInspector dropdown)
|
||||
* @param {Object} reportData - The full report data
|
||||
* @param {Object} parentBand - Parent band (for nested bands)
|
||||
* @returns {Array} - Array of { key, path } objects
|
||||
*/
|
||||
export function getAvailableArrays(reportData, parentBand = null) {
|
||||
const arrays = [];
|
||||
|
||||
// If there's a parent band, get arrays from the parent's data structure
|
||||
if (parentBand && parentBand.dataSource) {
|
||||
const parentDataArray = reportData?.[parentBand.dataSource];
|
||||
if (Array.isArray(parentDataArray) && parentDataArray.length > 0) {
|
||||
// Look at first item in parent's array to find nested arrays
|
||||
const sampleItem = parentDataArray[0];
|
||||
findArraysInObject(sampleItem, arrays);
|
||||
return arrays;
|
||||
}
|
||||
return []; // Parent has no data
|
||||
}
|
||||
|
||||
// No parent band - get top-level arrays only
|
||||
function findTopLevelArrays(obj) {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
const value = obj[key];
|
||||
if (Array.isArray(value)) {
|
||||
arrays.push({ key, path: key });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findTopLevelArrays(reportData);
|
||||
return arrays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to find arrays within an object (for nested band data sources)
|
||||
* @param {Object} obj - Object to search
|
||||
* @param {Array} arrays - Array to populate with results
|
||||
* @param {string} prefix - Path prefix for nested objects
|
||||
*/
|
||||
function findArraysInObject(obj, arrays, prefix = '') {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
const value = obj[key];
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
arrays.push({ key, path });
|
||||
// Don't recurse into arrays - we only want immediate arrays
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// Recurse into nested objects
|
||||
findArraysInObject(value, arrays, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Resolve nested path in object (e.g., 'contact.phone')
|
||||
*/
|
||||
function resolveNestedPath(obj, path) {
|
||||
if (!path || !obj) return undefined;
|
||||
|
||||
const parts = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current && typeof current === 'object' && part in current) {
|
||||
current = current[part];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve DBTextField value from reportData
|
||||
* @param {Object} reportData - The full report data
|
||||
* @param {string} objectKey - Top-level object key (e.g., 'owner', 'vessel')
|
||||
* @param {string} fieldPath - Dot-notation path to field (e.g., 'name', 'contact.phone')
|
||||
* @param {string} parentBandId - ID of parent band (if element is inside a band)
|
||||
* @param {Object} bandContext - Band context with currentBandData map
|
||||
* @returns {string} - Resolved value as string
|
||||
*/
|
||||
export function resolveDBTextValue(reportData, objectKey, fieldPath, parentBandId = null, bandContext = null) {
|
||||
// If inside band, use band's current data for automatic binding
|
||||
if (parentBandId && bandContext && bandContext.currentBandData && bandContext.currentBandData[parentBandId]) {
|
||||
const bandData = bandContext.currentBandData[parentBandId];
|
||||
|
||||
// If objectKey is empty, resolve directly from band data
|
||||
if (!objectKey) {
|
||||
if (!fieldPath) return '';
|
||||
const value = resolveNestedPath(bandData, fieldPath);
|
||||
return value !== undefined ? String(value) : '';
|
||||
}
|
||||
|
||||
// If objectKey is provided, resolve from objectKey in band data
|
||||
const objectData = bandData[objectKey];
|
||||
if (!objectData) return '';
|
||||
|
||||
if (!fieldPath) {
|
||||
return typeof objectData === 'object' ? '' : String(objectData);
|
||||
}
|
||||
|
||||
const value = resolveNestedPath(objectData, fieldPath);
|
||||
return value !== undefined ? String(value) : '';
|
||||
}
|
||||
|
||||
// Normal resolution (not in band or no band context)
|
||||
if (!reportData || !objectKey) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const objectData = reportData[objectKey];
|
||||
if (!objectData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!fieldPath) {
|
||||
// If no fieldPath, try to display the object itself (if primitive)
|
||||
return typeof objectData === 'object' ? '' : String(objectData);
|
||||
}
|
||||
|
||||
const value = resolveNestedPath(objectData, fieldPath);
|
||||
return value !== undefined ? String(value) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available object keys from reportData
|
||||
*/
|
||||
export function getAvailableObjects(reportData) {
|
||||
if (!reportData || typeof reportData !== 'object') {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(reportData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available field paths from an object (recursive)
|
||||
*/
|
||||
export function getAvailableFields(obj, prefix = '') {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fields = [];
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
const fullPath = prefix ? `${prefix}.${key}` : key;
|
||||
const value = obj[key];
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
// Nested object - add this key and recurse
|
||||
fields.push(fullPath);
|
||||
fields.push(...getAvailableFields(value, fullPath));
|
||||
} else {
|
||||
// Primitive or array - add this key
|
||||
fields.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Element } from '../models/Element';
|
||||
|
||||
/**
|
||||
* Create an element instance from plain object data
|
||||
* This allows us to use class methods on plain state objects
|
||||
*/
|
||||
export function createElementInstance(elementData) {
|
||||
return Element.fromJSON(elementData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element style using class method
|
||||
*/
|
||||
export function getElementStyle(element, charWidth, charHeight) {
|
||||
const instance = createElementInstance(element);
|
||||
return instance.getStyle(charWidth, charHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element bounds using class method
|
||||
*/
|
||||
export function getElementBounds(element) {
|
||||
const instance = createElementInstance(element);
|
||||
return instance.getBounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element dimensions using class method
|
||||
*/
|
||||
export function getElementDimensions(element) {
|
||||
const instance = createElementInstance(element);
|
||||
return instance.getDimensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if element intersects with rectangle
|
||||
*/
|
||||
export function elementIntersectsRect(element, rect) {
|
||||
const instance = createElementInstance(element);
|
||||
return instance.intersectsRect(rect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if text element is multi-line
|
||||
*/
|
||||
export function isTextMultiLine(element) {
|
||||
if (element.type !== 'text') return false;
|
||||
const instance = createElementInstance(element);
|
||||
return instance.isMultiLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get white-space style for text element
|
||||
*/
|
||||
export function getTextWhiteSpace(element) {
|
||||
if (element.type !== 'text') return 'normal';
|
||||
const instance = createElementInstance(element);
|
||||
return instance.getWhiteSpace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if position is on frame border
|
||||
*/
|
||||
export function isPositionOnFrameBorder(element, relativeCol, relativeRow) {
|
||||
if (element.type !== 'frame') return false;
|
||||
const instance = createElementInstance(element);
|
||||
return instance.isPositionOnBorder(relativeCol, relativeRow);
|
||||
}
|
||||
Reference in New Issue
Block a user