report editor with text field, lines, frames, and table draw. multiselect etc.

This commit is contained in:
2026-01-25 09:58:55 +02:00
parent 7f04566242
commit fb1d4ae7ad
26 changed files with 3162 additions and 3 deletions
@@ -0,0 +1,88 @@
/**
* Parse text content and replace {variable.field} expressions with actual data values
* @param {string} text - Text containing binding expressions like "Owner: {owner.name}"
* @param {object} data - Data object containing the values to bind
* @returns {string} - Text with bindings resolved
*/
export function parseBindings(text, data) {
if (!text || typeof text !== 'string') {
return text || '';
}
if (!data || typeof data !== 'object') {
return text;
}
// Regex to match {variable.field.nested} patterns
const bindingRegex = /\{([a-zA-Z0-9_.]+)\}/g;
return text.replace(bindingRegex, (match, path) => {
const value = resolveNestedPath(data, path);
// If value is found, return it; otherwise keep the original binding
if (value !== undefined && value !== null) {
return String(value);
}
return match; // Keep {variable} if not found
});
}
/**
* Resolve a nested path in an object (e.g., "owner.contact.name")
* @param {object} obj - The object to traverse
* @param {string} path - Dot-separated path (e.g., "owner.name")
* @returns {*} - The resolved value or undefined
*/
function resolveNestedPath(obj, path) {
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current === null || current === undefined) {
return undefined;
}
if (!current.hasOwnProperty(key)) {
return undefined;
}
current = current[key];
}
return current;
}
/**
* Check if text contains any binding expressions
* @param {string} text - Text to check
* @returns {boolean} - True if text contains bindings
*/
export function hasBindings(text) {
if (!text || typeof text !== 'string') {
return false;
}
return /\{[a-zA-Z0-9_.]+\}/.test(text);
}
/**
* Extract all binding paths from text
* @param {string} text - Text to analyze
* @returns {string[]} - Array of binding paths (e.g., ["owner.name", "vessel.id"])
*/
export function extractBindingPaths(text) {
if (!text || typeof text !== 'string') {
return [];
}
const bindingRegex = /\{([a-zA-Z0-9_.]+)\}/g;
const paths = [];
let match;
while ((match = bindingRegex.exec(text)) !== null) {
paths.push(match[1]);
}
return paths;
}
@@ -0,0 +1,186 @@
/**
* Crossroad characters for different line intersections
*/
const CROSSROAD_CHARS = {
single: {
cross: '┼', // All 4 directions
tTop: '┴', // Left, Right, Up (T pointing up)
tBottom: '┬', // Left, Right, Down (T pointing down)
tLeft: '┤', // Up, Down, Left (T pointing left)
tRight: '├', // Up, Down, Right (T pointing right)
horizontal: '─', // Left, Right only
vertical: '│', // Up, Down only
cornerTL: '┌', // Top-left corner (Down, Right)
cornerTR: '┐', // Top-right corner (Down, Left)
cornerBL: '└', // Bottom-left corner (Up, Right)
cornerBR: '┘' // Bottom-right corner (Up, Left)
},
double: {
cross: '╬',
tTop: '╩',
tBottom: '╦',
tLeft: '╣',
tRight: '╠',
horizontal: '═',
vertical: '║',
cornerTL: '╔',
cornerTR: '╗',
cornerBL: '╚',
cornerBR: '╝'
}
};
/**
* Detect crossroads and return a map of positions to junction characters
* @param {Array} elements - Array of report elements (text, frame, hline, vline)
* @returns {Object} - Map of 'x,y' positions to junction characters
*/
export function detectCrossroads(elements) {
// Build occupancy map: { 'x,y': { up, down, left, right, style } }
const occupancyMap = {};
// Process each element and mark occupied positions
elements.forEach(element => {
if (element.type === 'hline') {
// Horizontal line occupies positions from x to x+length
for (let i = 0; i < element.length; i++) {
const x = element.x + i;
const y = element.y;
const key = `${x},${y}`;
if (!occupancyMap[key]) {
occupancyMap[key] = { up: false, down: false, left: false, right: false, styles: [] };
}
// Horizontal line means left and right connections
if (i > 0) occupancyMap[key].left = true;
if (i < element.length - 1) occupancyMap[key].right = true;
occupancyMap[key].styles.push(element.lineStyle || 'single');
}
} else if (element.type === 'vline') {
// Vertical line occupies positions from y to y+length
for (let i = 0; i < element.length; i++) {
const x = element.x;
const y = element.y + i;
const key = `${x},${y}`;
if (!occupancyMap[key]) {
occupancyMap[key] = { up: false, down: false, left: false, right: false, styles: [] };
}
// Vertical line means up and down connections
if (i > 0) occupancyMap[key].up = true;
if (i < element.length - 1) occupancyMap[key].down = true;
occupancyMap[key].styles.push(element.lineStyle || 'single');
}
} else if (element.type === 'frame') {
// Frame has borders on all edges
const { x, y, width, height, borderStyle } = element;
const style = borderStyle || 'single';
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
const posX = x + col;
const posY = y + row;
const key = `${posX},${posY}`;
// Only process border positions (edges of frame)
const isTopEdge = row === 0;
const isBottomEdge = row === height - 1;
const isLeftEdge = col === 0;
const isRightEdge = col === width - 1;
if (isTopEdge || isBottomEdge || isLeftEdge || isRightEdge) {
if (!occupancyMap[key]) {
occupancyMap[key] = { up: false, down: false, left: false, right: false, styles: [] };
}
// Determine connections based on position in frame
if (isTopEdge && col > 0 && col < width - 1) {
occupancyMap[key].left = true;
occupancyMap[key].right = true;
}
if (isBottomEdge && col > 0 && col < width - 1) {
occupancyMap[key].left = true;
occupancyMap[key].right = true;
}
if (isLeftEdge && row > 0 && row < height - 1) {
occupancyMap[key].up = true;
occupancyMap[key].down = true;
}
if (isRightEdge && row > 0 && row < height - 1) {
occupancyMap[key].up = true;
occupancyMap[key].down = true;
}
// Corners
if (isTopEdge && isLeftEdge) {
occupancyMap[key].right = true;
occupancyMap[key].down = true;
}
if (isTopEdge && isRightEdge) {
occupancyMap[key].left = true;
occupancyMap[key].down = true;
}
if (isBottomEdge && isLeftEdge) {
occupancyMap[key].right = true;
occupancyMap[key].up = true;
}
if (isBottomEdge && isRightEdge) {
occupancyMap[key].left = true;
occupancyMap[key].up = true;
}
occupancyMap[key].styles.push(style);
}
}
}
}
});
// Build crossroad map by analyzing each occupied position
const crossroadMap = {};
Object.keys(occupancyMap).forEach(key => {
const { up, down, left, right, styles } = occupancyMap[key];
// Determine the predominant style (prefer double if any double exists)
const style = styles.includes('double') ? 'double' : 'single';
const chars = CROSSROAD_CHARS[style];
// Count connections
const connectionCount = (up ? 1 : 0) + (down ? 1 : 0) + (left ? 1 : 0) + (right ? 1 : 0);
// Only create crossroad if there's an intersection (more than 2 connections)
if (connectionCount >= 3) {
// Determine the appropriate junction character
if (up && down && left && right) {
crossroadMap[key] = chars.cross;
} else if (up && left && right) {
crossroadMap[key] = chars.tTop;
} else if (down && left && right) {
crossroadMap[key] = chars.tBottom;
} else if (up && down && left) {
crossroadMap[key] = chars.tLeft;
} else if (up && down && right) {
crossroadMap[key] = chars.tRight;
}
}
});
return crossroadMap;
}
/**
* Check if a position has a crossroad
* @param {Object} crossroadMap - Map of crossroads
* @param {number} x - Grid x position
* @param {number} y - Grid y position
* @returns {string|null} - Crossroad character or null
*/
export function getCrossroadChar(crossroadMap, x, y) {
const key = `${x},${y}`;
return crossroadMap[key] || null;
}