Erweiterungen

This commit is contained in:
2026-02-13 11:27:06 +01:00
parent 120a8e0571
commit 2da3d13431
6 changed files with 1491 additions and 141 deletions

View File

@@ -4,7 +4,7 @@
'use strict';
class InvoiceGenerator {
constructor() {
constructor(containerId) {
this.canvas = null;
this.ctx = null;
this.elements = new Map();
@@ -16,22 +16,33 @@
this.elementStart = { x: 0, y: 0, w: 0, h: 0 };
this.activeHandle = null;
this.container = null;
this.containerId = containerId || 'invoice-canvas-container';
// Page dimensions (A4 at 96 DPI)
this.pageX = 50;
this.pageY = 30;
this.pageWidth = 794;
this.pageHeight = 1123;
this.scale = 1;
// Handle configuration
this.handleSize = 10;
this.handlePadding = 3;
// Grid configuration (Snap to Grid)
this.gridSize = 5;
this.showGrid = true;
}
// Snap value to grid
snapToGrid(value) {
return Math.round(value / this.gridSize) * this.gridSize;
}
init() {
this.container = document.getElementById('invoice-canvas-container');
this.container = document.getElementById(this.containerId);
if (!this.container) {
console.error('Canvas container not found');
console.error('Canvas container not found: ' + this.containerId);
return;
}
@@ -50,6 +61,10 @@
this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
this.canvas.addEventListener('mouseleave', this.onMouseUp.bind(this));
// Keyboard navigation for selected element
this.canvas.setAttribute('tabindex', '0');
this.canvas.addEventListener('keydown', this.onKeyDown.bind(this));
window.addEventListener('resize', () => {
this.updateCanvasSize();
this.draw();
@@ -63,14 +78,32 @@
const rect = this.container.getBoundingClientRect();
this.canvas.width = rect.width;
this.canvas.height = rect.height;
this.pageX = Math.max(20, (this.canvas.width - this.pageWidth) / 2);
// Calculate scale to maximize page size while maintaining aspect ratio
// Use full available space with minimal padding
const padding = 10;
const availableWidth = this.canvas.width - padding * 2;
const availableHeight = this.canvas.height - padding * 2;
// Scale to fit the available space (can be larger than 1 if container is bigger than A4)
this.scale = Math.min(availableWidth / this.pageWidth, availableHeight / this.pageHeight);
// Center the scaled page
const scaledPageWidth = this.pageWidth * this.scale;
const scaledPageHeight = this.pageHeight * this.scale;
this.pageX = (this.canvas.width - scaledPageWidth) / 2;
this.pageY = (this.canvas.height - scaledPageHeight) / 2;
}
getMousePos(e) {
const rect = this.canvas.getBoundingClientRect();
// Convert screen coordinates to canvas coordinates
const canvasX = e.clientX - rect.left;
const canvasY = e.clientY - rect.top;
// Convert to page coordinates (accounting for scale and offset)
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
x: (canvasX - this.pageX) / this.scale,
y: (canvasY - this.pageY) / this.scale
};
}
@@ -166,14 +199,18 @@
const dx = pos.x - this.dragStart.x;
const dy = pos.y - this.dragStart.y;
this.selectedElement.x = this.elementStart.x + dx;
this.selectedElement.y = this.elementStart.y + dy;
let newX = this.elementStart.x + dx;
let newY = this.elementStart.y + dy;
// Constrain to page
this.selectedElement.x = Math.max(this.pageX, Math.min(this.selectedElement.x,
this.pageX + this.pageWidth - (this.selectedElement.width || 100)));
this.selectedElement.y = Math.max(this.pageY, Math.min(this.selectedElement.y,
this.pageY + this.pageHeight - (this.selectedElement.height || 30)));
// Constrain to page (in page coordinates, page starts at 0,0)
newX = Math.max(0, Math.min(newX,
this.pageWidth - (this.selectedElement.width || 100)));
newY = Math.max(0, Math.min(newY,
this.pageHeight - (this.selectedElement.height || 30)));
// Snap to grid
this.selectedElement.x = this.snapToGrid(newX);
this.selectedElement.y = this.snapToGrid(newY);
this.draw();
this.notifyChange();
@@ -211,6 +248,47 @@
this.canvas.style.cursor = 'default';
}
onKeyDown(e) {
if (!this.selectedElement) return;
const step = this.gridSize; // Move by grid size (5px)
let moved = false;
switch(e.key) {
case 'ArrowUp':
this.selectedElement.y = Math.max(0, this.selectedElement.y - step);
moved = true;
e.preventDefault();
break;
case 'ArrowDown':
this.selectedElement.y = Math.min(
this.pageHeight - (this.selectedElement.height || 30),
this.selectedElement.y + step
);
moved = true;
e.preventDefault();
break;
case 'ArrowLeft':
this.selectedElement.x = Math.max(0, this.selectedElement.x - step);
moved = true;
e.preventDefault();
break;
case 'ArrowRight':
this.selectedElement.x = Math.min(
this.pageWidth - (this.selectedElement.width || 100),
this.selectedElement.x + step
);
moved = true;
e.preventDefault();
break;
}
if (moved) {
this.draw();
this.notifyChange();
}
}
doResize(mouseX, mouseY) {
if (!this.selectedElement || !this.activeHandle) return;
@@ -308,17 +386,31 @@
ctx.fillStyle = '#e8e8e8';
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Page shadow
ctx.save();
// Apply scale and offset
ctx.translate(this.pageX, this.pageY);
ctx.scale(this.scale, this.scale);
// Page shadow (drawn before scaling offset)
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(this.pageX + 4, this.pageY + this.pageHeight, this.pageWidth, 4);
ctx.fillRect(this.pageX + this.pageWidth, this.pageY + 4, 4, this.pageHeight);
ctx.fillRect(this.pageX + 4 * this.scale, this.pageY + this.pageHeight * this.scale, this.pageWidth * this.scale, 4);
ctx.fillRect(this.pageX + this.pageWidth * this.scale, this.pageY + 4 * this.scale, 4, this.pageHeight * this.scale);
ctx.restore();
// Page
ctx.fillStyle = '#ffffff';
ctx.fillRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight);
ctx.fillRect(0, 0, this.pageWidth, this.pageHeight);
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 1;
ctx.strokeRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight);
ctx.lineWidth = 1 / this.scale;
ctx.strokeRect(0, 0, this.pageWidth, this.pageHeight);
// Draw grid if enabled
if (this.showGrid) {
this.drawGrid(ctx);
}
// Draw elements
this.elements.forEach(el => this.drawElement(el));
@@ -327,6 +419,8 @@
if (this.selectedElement) {
this.drawSelection(this.selectedElement);
}
ctx.restore();
}
drawImagePlaceholder(ctx, el) {
@@ -491,7 +585,7 @@
const y = el.y;
const w = el.width || 150;
const h = el.height || 30;
const hs = this.handleSize;
const hs = this.handleSize / this.scale;
// Selection border
ctx.strokeStyle = '#1976d2';
@@ -522,13 +616,43 @@
});
}
drawGrid(ctx) {
ctx.save();
ctx.strokeStyle = 'rgba(200, 200, 200, 0.3)';
ctx.lineWidth = 0.5 / this.scale;
// Vertical lines (draw in page coordinates)
for (let x = 0; x <= this.pageWidth; x += this.gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, this.pageHeight);
ctx.stroke();
}
// Horizontal lines
for (let y = 0; y <= this.pageHeight; y += this.gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(this.pageWidth, y);
ctx.stroke();
}
ctx.restore();
}
addElement(type, label, dropX, dropY) {
this.elementCounter++;
const id = `element-${this.elementCounter}`;
const rect = this.container.getBoundingClientRect();
const x = Math.max(this.pageX + 10, dropX - rect.left - 50);
const y = Math.max(this.pageY + 10, dropY - rect.top - 15);
// dropX and dropY are in container coordinates, convert to page coordinates
const pageCoordX = (dropX - this.pageX) / this.scale;
const pageCoordY = (dropY - this.pageY) / this.scale;
// Snap initial position to grid (relative to page origin)
const rawX = Math.max(10, pageCoordX - 50);
const rawY = Math.max(10, pageCoordY - 15);
const x = this.snapToGrid(rawX);
const y = this.snapToGrid(rawY);
let el = {
id, type, x, y,
@@ -595,6 +719,10 @@
this.draw();
if (this.selectedElement) {
this.notifyChange();
// Focus canvas for keyboard navigation
if (this.canvas) {
this.canvas.focus();
}
}
}
@@ -617,8 +745,8 @@
updateElementPosition(id, x, y) {
const el = this.elements.get(id);
if (el) {
if (x !== null) el.x = x;
if (y !== null) el.y = y;
if (x !== null) el.x = this.snapToGrid(x);
if (y !== null) el.y = this.snapToGrid(y);
this.draw();
}
}
@@ -631,6 +759,14 @@
}
}
updateElementColor(id, color) {
const el = this.elements.get(id);
if (el) {
el.color = color;
this.draw();
}
}
updateElementImage(id, imageDataUrl) {
const el = this.elements.get(id);
if (el && el.type === 'image') {
@@ -683,6 +819,12 @@
URL.revokeObjectURL(url);
}
exportTemplateJson() {
// Return template data as JSON string for Java processing
const data = this.getCanvasData();
return JSON.stringify(data);
}
generatePreview() {
const temp = document.createElement('canvas');
temp.width = this.pageWidth;
@@ -718,4 +860,5 @@
}
window.invoiceGenerator = new InvoiceGenerator();
window.InvoiceGenerator = InvoiceGenerator;
})();

View File

@@ -0,0 +1,534 @@
// Profile Invoice Generator - Initializes canvas on the edit-profile page
window.initProfileInvoiceGenerator = function() {
var containerId = 'invoice-canvas-container-profile';
var container = document.getElementById(containerId);
if (!container) {
console.error('Canvas container not found:', containerId);
return;
}
// Check if canvas already exists
var existingCanvas = container.querySelector('canvas');
if (existingCanvas) {
console.log('Canvas already exists');
return;
}
// Get container dimensions
var rect = container.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.error('Container has no size');
return;
}
// Create canvas element
var canvas = document.createElement('canvas');
canvas.style.display = 'block';
canvas.width = rect.width;
canvas.height = rect.height;
container.appendChild(canvas);
var ctx = canvas.getContext('2d');
// State
var elements = [];
var selectedElement = null;
var isDragging = false;
var dragStart = { x: 0, y: 0 };
var elementStart = { x: 0, y: 0 };
var elementCounter = 0;
var gridSize = 5;
// Page dimensions
var padding = 10;
var pageX, pageY, pageWidth, pageHeight;
function updatePageDimensions() {
var w = canvas.width;
var h = canvas.height;
var availableWidth = w - padding * 2;
var availableHeight = h - padding * 2;
pageWidth = Math.min(availableWidth, availableHeight * 0.707);
pageHeight = pageWidth / 0.707;
pageX = (w - pageWidth) / 2;
pageY = (h - pageHeight) / 2;
}
function snapToGrid(value) {
return Math.round(value / gridSize) * gridSize;
}
// Notify Java about element selection
function notifyElementSelected(el) {
if (window.invoiceGeneratorViewProfile && window.invoiceGeneratorViewProfile.$server) {
window.invoiceGeneratorViewProfile.$server.updatePropertiesPanel(
el.id,
el.type,
el.text || '',
el.x,
el.y,
el.fontSize || 14,
el.color || '#333333',
el.width || 100,
el.height || 30
);
}
}
function notifyElementDeselected() {
if (window.invoiceGeneratorViewProfile && window.invoiceGeneratorViewProfile.$server) {
window.invoiceGeneratorViewProfile.$server.resetPropertiesPanel();
}
}
// Draw function
function draw() {
var w = canvas.width;
var h = canvas.height;
// Clear background
ctx.fillStyle = '#e8e8e8';
ctx.fillRect(0, 0, w, h);
updatePageDimensions();
// Page shadow
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(pageX + 4, pageY + pageHeight, pageWidth, 4);
ctx.fillRect(pageX + pageWidth, pageY + 4, 4, pageHeight);
// White page
ctx.fillStyle = '#ffffff';
ctx.fillRect(pageX, pageY, pageWidth, pageHeight);
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 1;
ctx.strokeRect(pageX, pageY, pageWidth, pageHeight);
// Grid
ctx.strokeStyle = 'rgba(200, 200, 200, 0.3)';
ctx.lineWidth = 0.5;
for (var x = pageX; x <= pageX + pageWidth; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, pageY);
ctx.lineTo(x, pageY + pageHeight);
ctx.stroke();
}
for (var y = pageY; y <= pageY + pageHeight; y += gridSize) {
ctx.beginPath();
ctx.moveTo(pageX, y);
ctx.lineTo(pageX + pageWidth, y);
ctx.stroke();
}
// Draw elements
elements.forEach(function(el) {
drawElement(el);
});
// Draw selection
if (selectedElement) {
drawSelection(selectedElement);
}
}
function drawElement(el) {
ctx.save();
if (el.type === 'line') {
ctx.strokeStyle = el.color || '#333333';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pageX + el.x, pageY + el.y);
ctx.lineTo(pageX + el.x + el.width, pageY + el.y);
ctx.stroke();
} else if (el.type === 'image') {
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(pageX + el.x, pageY + el.y, el.width, el.height);
ctx.strokeStyle = '#999999';
ctx.strokeRect(pageX + el.x, pageY + el.y, el.width, el.height);
ctx.fillStyle = '#666666';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Bild', pageX + el.x + el.width / 2, pageY + el.y + el.height / 2);
} else {
// Text elements
ctx.font = (el.fontStyle || '') + ' ' + (el.fontSize || 14) + 'px Arial';
ctx.fillStyle = el.color || '#333333';
ctx.textBaseline = 'top';
var lines = (el.text || '').split('\n');
var lineHeight = (el.fontSize || 14) * 1.2;
var y = pageY + el.y;
lines.forEach(function(line) {
ctx.fillText(line, pageX + el.x, y);
y += lineHeight;
});
}
ctx.restore();
}
function drawSelection(el) {
var x = pageX + el.x;
var y = pageY + el.y;
var w = el.width || 100;
var h = el.height || 30;
var hs = 8; // handle size
ctx.strokeStyle = '#1976d2';
ctx.lineWidth = 2;
ctx.setLineDash([5, 3]);
ctx.strokeRect(x, y, w, h);
ctx.setLineDash([]);
// Handles
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#1976d2';
ctx.lineWidth = 2;
var positions = [
[x - hs/2, y - hs/2],
[x + w/2 - hs/2, y - hs/2],
[x + w - hs/2, y - hs/2],
[x - hs/2, y + h/2 - hs/2],
[x + w - hs/2, y + h/2 - hs/2],
[x - hs/2, y + h - hs/2],
[x + w/2 - hs/2, y + h - hs/2],
[x + w - hs/2, y + h - hs/2]
];
positions.forEach(function(pos) {
ctx.fillRect(pos[0], pos[1], hs, hs);
ctx.strokeRect(pos[0], pos[1], hs, hs);
});
}
function hitTest(x, y, el) {
var ex = pageX + el.x;
var ey = pageY + el.y;
var ew = el.width || 100;
var eh = el.height || 30;
return x >= ex && x <= ex + ew && y >= ey && y <= ey + eh;
}
// Mouse events
canvas.addEventListener('mousedown', function(e) {
var rect = canvas.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
// Check if clicking on an element
var clickedElement = null;
for (var i = elements.length - 1; i >= 0; i--) {
if (hitTest(x, y, elements[i])) {
clickedElement = elements[i];
break;
}
}
if (clickedElement) {
selectedElement = clickedElement;
isDragging = true;
dragStart = { x: x, y: y };
elementStart = { x: clickedElement.x, y: clickedElement.y };
canvas.style.cursor = 'move';
notifyElementSelected(selectedElement);
} else {
selectedElement = null;
notifyElementDeselected();
}
draw();
});
canvas.addEventListener('mousemove', function(e) {
var rect = canvas.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
if (isDragging && selectedElement) {
var dx = x - dragStart.x;
var dy = y - dragStart.y;
var newX = elementStart.x + dx;
var newY = elementStart.y + dy;
// Constrain to page
newX = Math.max(0, Math.min(newX, pageWidth - (selectedElement.width || 100)));
newY = Math.max(0, Math.min(newY, pageHeight - (selectedElement.height || 30)));
// Snap to grid
selectedElement.x = snapToGrid(newX);
selectedElement.y = snapToGrid(newY);
draw();
notifyElementSelected(selectedElement);
} else {
// Update cursor
var hovering = false;
for (var i = elements.length - 1; i >= 0; i--) {
if (hitTest(x, y, elements[i])) {
hovering = true;
break;
}
}
canvas.style.cursor = hovering ? 'move' : 'default';
}
});
canvas.addEventListener('mouseup', function() {
isDragging = false;
canvas.style.cursor = 'default';
});
canvas.addEventListener('mouseleave', function() {
isDragging = false;
canvas.style.cursor = 'default';
});
// Keyboard navigation
canvas.setAttribute('tabindex', '0');
canvas.addEventListener('keydown', function(e) {
if (!selectedElement) return;
var step = gridSize;
var moved = false;
switch(e.key) {
case 'ArrowUp':
selectedElement.y = Math.max(0, selectedElement.y - step);
moved = true;
e.preventDefault();
break;
case 'ArrowDown':
selectedElement.y = Math.min(pageHeight - (selectedElement.height || 30), selectedElement.y + step);
moved = true;
e.preventDefault();
break;
case 'ArrowLeft':
selectedElement.x = Math.max(0, selectedElement.x - step);
moved = true;
e.preventDefault();
break;
case 'ArrowRight':
selectedElement.x = Math.min(pageWidth - (selectedElement.width || 100), selectedElement.x + step);
moved = true;
e.preventDefault();
break;
case 'Delete':
var index = elements.indexOf(selectedElement);
if (index > -1) {
elements.splice(index, 1);
selectedElement = null;
notifyElementDeselected();
draw();
}
e.preventDefault();
break;
}
if (moved) {
draw();
notifyElementSelected(selectedElement);
}
});
// Add element function
window.addProfileElement = function(type, label, dropX, dropY) {
elementCounter++;
var id = 'element-' + elementCounter;
// Convert to page coordinates
var x = dropX - pageX - 50;
var y = dropY - pageY - 15;
x = Math.max(10, x);
y = Math.max(10, y);
x = snapToGrid(x);
y = snapToGrid(y);
var el = {
id: id,
type: type,
x: x,
y: y,
width: 150,
height: 30,
fontSize: 14,
color: '#333333'
};
switch (type) {
case 'text':
el.text = 'Text eingeben...';
el.height = 20;
break;
case 'header':
el.text = 'Überschrift';
el.fontSize = 24;
el.fontStyle = 'bold';
el.color = '#000000';
el.width = 200;
el.height = 30;
break;
case 'date':
el.text = 'Datum: ' + new Date().toLocaleDateString('de-DE');
el.fontSize = 12;
el.color = '#666666';
el.height = 16;
break;
case 'customer':
el.text = 'Kundenname\nStraße Nr.\nPLZ Ort';
el.height = 50;
el.fontSize = 12;
break;
case 'company':
el.text = 'Ihr Unternehmen\nIhre Straße\nIhre PLZ Ort';
el.height = 50;
el.fontSize = 12;
break;
case 'amount':
el.text = 'Gesamtbetrag: 0,00 €';
el.fontStyle = 'bold';
el.width = 180;
el.height = 20;
break;
case 'line':
el.text = '';
el.width = 200;
el.height = 2;
break;
case 'image':
el.text = 'Bild';
el.width = 100;
el.height = 100;
break;
default:
el.text = label || 'Neues Element';
el.height = 20;
}
elements.push(el);
selectedElement = el;
draw();
notifyElementSelected(el);
// Focus canvas for keyboard navigation
canvas.focus();
};
// Update element functions
window.updateProfileElementText = function(id, text) {
var el = elements.find(function(e) { return e.id === id; });
if (el) {
el.text = text;
draw();
}
};
window.updateProfileElementPosition = function(id, x, y) {
var el = elements.find(function(e) { return e.id === id; });
if (el) {
if (x !== null) el.x = snapToGrid(x);
if (y !== null) el.y = snapToGrid(y);
draw();
}
};
window.updateProfileElementFontSize = function(id, size) {
var el = elements.find(function(e) { return e.id === id; });
if (el) {
el.fontSize = size;
draw();
}
};
window.updateProfileElementColor = function(id, color) {
var el = elements.find(function(e) { return e.id === id; });
if (el) {
el.color = color;
draw();
}
};
window.updateProfileElementSize = function(id, width, height) {
var el = elements.find(function(e) { return e.id === id; });
if (el) {
if (width !== null) el.width = width;
if (height !== null) el.height = height;
draw();
}
};
window.deleteProfileElement = function(id) {
var index = elements.findIndex(function(e) { return e.id === id; });
if (index > -1) {
elements.splice(index, 1);
if (selectedElement && selectedElement.id === id) {
selectedElement = null;
notifyElementDeselected();
}
draw();
}
};
// Clear canvas function
window.clearProfileCanvas = function() {
elements = [];
selectedElement = null;
notifyElementDeselected();
draw();
};
// Get canvas data
window.getProfileCanvasData = function() {
return {
elements: elements
};
};
draw();
console.log('Profile canvas initialized');
// Handle window resize
window.addEventListener('resize', function() {
var newRect = container.getBoundingClientRect();
if (newRect.width > 0 && newRect.height > 0) {
canvas.width = newRect.width;
canvas.height = newRect.height;
draw();
}
});
// Setup drop zone
if (!container._dropSetup) {
container._dropSetup = true;
container.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
container.style.borderColor = 'var(--lumo-primary-color)';
});
container.addEventListener('dragleave', function(e) {
container.style.borderColor = 'var(--lumo-contrast-20pct)';
});
container.addEventListener('drop', function(e) {
e.preventDefault();
container.style.borderColor = 'var(--lumo-contrast-20pct)';
var templateType = e.dataTransfer.getData('template-type');
var templateLabel = e.dataTransfer.getData('template-label');
if (templateType && window.addProfileElement) {
var rect = container.getBoundingClientRect();
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
window.addProfileElement(templateType, templateLabel, x, y);
}
});
}
};