Erweiterungen
This commit is contained in:
721
src/main/frontend/invoice-generator/invoice-generator.js
Normal file
721
src/main/frontend/invoice-generator/invoice-generator.js
Normal file
@@ -0,0 +1,721 @@
|
||||
// Invoice Generator - Native HTML5 Canvas Implementation
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
class InvoiceGenerator {
|
||||
constructor() {
|
||||
this.canvas = null;
|
||||
this.ctx = null;
|
||||
this.elements = new Map();
|
||||
this.selectedElement = null;
|
||||
this.elementCounter = 0;
|
||||
this.isDragging = false;
|
||||
this.isResizing = false;
|
||||
this.dragStart = { x: 0, y: 0 };
|
||||
this.elementStart = { x: 0, y: 0, w: 0, h: 0 };
|
||||
this.activeHandle = null;
|
||||
this.container = null;
|
||||
|
||||
// Page dimensions (A4 at 96 DPI)
|
||||
this.pageX = 50;
|
||||
this.pageY = 30;
|
||||
this.pageWidth = 794;
|
||||
this.pageHeight = 1123;
|
||||
|
||||
// Handle configuration
|
||||
this.handleSize = 10;
|
||||
this.handlePadding = 3;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.container = document.getElementById('invoice-canvas-container');
|
||||
if (!this.container) {
|
||||
console.error('Canvas container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create canvas
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.style.display = 'block';
|
||||
this.updateCanvasSize();
|
||||
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.container.innerHTML = '';
|
||||
this.container.appendChild(this.canvas);
|
||||
|
||||
// Bind events
|
||||
this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
|
||||
this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||
this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||
this.canvas.addEventListener('mouseleave', this.onMouseUp.bind(this));
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.updateCanvasSize();
|
||||
this.draw();
|
||||
});
|
||||
|
||||
this.draw();
|
||||
console.log('Invoice generator initialized');
|
||||
}
|
||||
|
||||
updateCanvasSize() {
|
||||
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);
|
||||
}
|
||||
|
||||
getMousePos(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top
|
||||
};
|
||||
}
|
||||
|
||||
// Check if point is inside element
|
||||
hitTest(x, y, el) {
|
||||
const w = el.width || 150;
|
||||
const h = el.height || 30;
|
||||
return x >= el.x && x <= el.x + w && y >= el.y && y <= el.y + h;
|
||||
}
|
||||
|
||||
// Check if point is on a resize handle
|
||||
getHandleAt(x, y, el) {
|
||||
if (!el) return null;
|
||||
|
||||
const w = el.width || 150;
|
||||
const h = el.height || 30;
|
||||
const hs = this.handleSize + this.handlePadding;
|
||||
|
||||
// Define handle positions (center points)
|
||||
const handles = {
|
||||
'nw': { x: el.x, y: el.y },
|
||||
'n': { x: el.x + w / 2, y: el.y },
|
||||
'ne': { x: el.x + w, y: el.y },
|
||||
'w': { x: el.x, y: el.y + h / 2 },
|
||||
'e': { x: el.x + w, y: el.y + h / 2 },
|
||||
'sw': { x: el.x, y: el.y + h },
|
||||
's': { x: el.x + w / 2, y: el.y + h },
|
||||
'se': { x: el.x + w, y: el.y + h }
|
||||
};
|
||||
|
||||
for (const [name, pos] of Object.entries(handles)) {
|
||||
const dx = x - pos.x;
|
||||
const dy = y - pos.y;
|
||||
if (Math.abs(dx) <= hs && Math.abs(dy) <= hs) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onMouseDown(e) {
|
||||
const pos = this.getMousePos(e);
|
||||
|
||||
// First check if clicking on a handle of selected element
|
||||
if (this.selectedElement) {
|
||||
const handle = this.getHandleAt(pos.x, pos.y, this.selectedElement);
|
||||
if (handle) {
|
||||
console.log('Resize handle clicked:', handle);
|
||||
this.isResizing = true;
|
||||
this.activeHandle = handle;
|
||||
this.dragStart = { x: pos.x, y: pos.y };
|
||||
this.elementStart = {
|
||||
x: this.selectedElement.x,
|
||||
y: this.selectedElement.y,
|
||||
w: this.selectedElement.width || 100,
|
||||
h: this.selectedElement.height || 30
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if clicking on an element
|
||||
let clickedElement = null;
|
||||
for (const el of Array.from(this.elements.values()).reverse()) {
|
||||
if (this.hitTest(pos.x, pos.y, el)) {
|
||||
clickedElement = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (clickedElement) {
|
||||
this.selectElement(clickedElement.id);
|
||||
this.isDragging = true;
|
||||
this.dragStart = { x: pos.x, y: pos.y };
|
||||
this.elementStart = { x: clickedElement.x, y: clickedElement.y };
|
||||
this.canvas.style.cursor = 'move';
|
||||
} else {
|
||||
this.deselectAll();
|
||||
}
|
||||
|
||||
this.draw();
|
||||
}
|
||||
|
||||
onMouseMove(e) {
|
||||
const pos = this.getMousePos(e);
|
||||
|
||||
if (this.isResizing && this.selectedElement) {
|
||||
this.doResize(pos.x, pos.y);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isDragging && this.selectedElement) {
|
||||
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;
|
||||
|
||||
// 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)));
|
||||
|
||||
this.draw();
|
||||
this.notifyChange();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update cursor
|
||||
if (this.selectedElement) {
|
||||
const handle = this.getHandleAt(pos.x, pos.y, this.selectedElement);
|
||||
if (handle) {
|
||||
const cursors = {
|
||||
'nw': 'nw-resize', 'ne': 'ne-resize', 'sw': 'sw-resize', 'se': 'se-resize',
|
||||
'n': 'ns-resize', 's': 'ns-resize', 'e': 'ew-resize', 'w': 'ew-resize'
|
||||
};
|
||||
this.canvas.style.cursor = cursors[handle] || 'default';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check hover over elements
|
||||
let hovering = false;
|
||||
for (const el of Array.from(this.elements.values()).reverse()) {
|
||||
if (this.hitTest(pos.x, pos.y, el)) {
|
||||
hovering = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.canvas.style.cursor = hovering ? 'move' : 'default';
|
||||
}
|
||||
|
||||
onMouseUp(e) {
|
||||
this.isDragging = false;
|
||||
this.isResizing = false;
|
||||
this.activeHandle = null;
|
||||
this.canvas.style.cursor = 'default';
|
||||
}
|
||||
|
||||
doResize(mouseX, mouseY) {
|
||||
if (!this.selectedElement || !this.activeHandle) return;
|
||||
|
||||
const el = this.selectedElement;
|
||||
el.manuallyResized = true; // Mark as manually resized
|
||||
|
||||
const start = this.elementStart;
|
||||
const minSize = 20;
|
||||
|
||||
// Calculate new dimensions based on handle
|
||||
switch (this.activeHandle) {
|
||||
case 'se':
|
||||
el.width = Math.max(minSize, mouseX - start.x);
|
||||
el.height = Math.max(minSize, mouseY - start.y);
|
||||
break;
|
||||
|
||||
case 'sw':
|
||||
const newWsw = start.w + (start.x - mouseX);
|
||||
if (newWsw >= minSize) {
|
||||
el.x = mouseX;
|
||||
el.width = newWsw;
|
||||
}
|
||||
el.height = Math.max(minSize, mouseY - start.y);
|
||||
break;
|
||||
|
||||
case 'ne':
|
||||
el.width = Math.max(minSize, mouseX - start.x);
|
||||
const newHne = start.h + (start.y - mouseY);
|
||||
if (newHne >= minSize) {
|
||||
el.y = mouseY;
|
||||
el.height = newHne;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'nw':
|
||||
const newWnw = start.w + (start.x - mouseX);
|
||||
const newHnw = start.h + (start.y - mouseY);
|
||||
if (newWnw >= minSize) {
|
||||
el.x = mouseX;
|
||||
el.width = newWnw;
|
||||
}
|
||||
if (newHnw >= minSize) {
|
||||
el.y = mouseY;
|
||||
el.height = newHnw;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'e':
|
||||
el.width = Math.max(minSize, mouseX - start.x);
|
||||
break;
|
||||
|
||||
case 'w':
|
||||
const newWw = start.w + (start.x - mouseX);
|
||||
if (newWw >= minSize) {
|
||||
el.x = mouseX;
|
||||
el.width = newWw;
|
||||
}
|
||||
break;
|
||||
|
||||
case 's':
|
||||
el.height = Math.max(minSize, mouseY - start.y);
|
||||
break;
|
||||
|
||||
case 'n':
|
||||
const newHn = start.h + (start.y - mouseY);
|
||||
if (newHn >= minSize) {
|
||||
el.y = mouseY;
|
||||
el.height = newHn;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.draw();
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
notifyChange() {
|
||||
if (this.selectedElement && window.invoiceGeneratorView?.$server) {
|
||||
const el = this.selectedElement;
|
||||
window.invoiceGeneratorView.$server.updatePropertiesPanel(
|
||||
el.id, el.type, el.text || '', el.x, el.y, el.fontSize || 14, el.color || '#000000'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!this.ctx) return;
|
||||
|
||||
const ctx = this.ctx;
|
||||
|
||||
// Clear
|
||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = '#e8e8e8';
|
||||
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Page shadow
|
||||
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);
|
||||
|
||||
// Page
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight);
|
||||
ctx.strokeStyle = '#cccccc';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(this.pageX, this.pageY, this.pageWidth, this.pageHeight);
|
||||
|
||||
// Draw elements
|
||||
this.elements.forEach(el => this.drawElement(el));
|
||||
|
||||
// Draw selection
|
||||
if (this.selectedElement) {
|
||||
this.drawSelection(this.selectedElement);
|
||||
}
|
||||
}
|
||||
|
||||
drawImagePlaceholder(ctx, el) {
|
||||
ctx.fillStyle = '#f0f0f0';
|
||||
ctx.fillRect(el.x, el.y, el.width || 100, el.height || 100);
|
||||
ctx.strokeStyle = '#999999';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(el.x, el.y, el.width || 100, el.height || 100);
|
||||
ctx.fillStyle = '#666666';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('Bild', el.x + (el.width || 100) / 2, el.y + (el.height || 100) / 2);
|
||||
}
|
||||
|
||||
drawElement(el) {
|
||||
const ctx = this.ctx;
|
||||
ctx.save();
|
||||
|
||||
switch (el.type) {
|
||||
case 'line':
|
||||
ctx.strokeStyle = el.color || '#333333';
|
||||
ctx.lineWidth = el.strokeWidth || 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(el.x, el.y);
|
||||
ctx.lineTo(el.x + (el.width || 200), el.y);
|
||||
ctx.stroke();
|
||||
break;
|
||||
|
||||
case 'image':
|
||||
if (el.imageData) {
|
||||
// Draw actual image
|
||||
const img = el.imageObj;
|
||||
if (img && img.complete) {
|
||||
// Draw image scaled to fit element while maintaining aspect ratio
|
||||
const imgAspect = img.width / img.height;
|
||||
const elAspect = (el.width || 100) / (el.height || 100);
|
||||
|
||||
let drawWidth, drawHeight, drawX, drawY;
|
||||
|
||||
if (imgAspect > elAspect) {
|
||||
// Image is wider than element
|
||||
drawWidth = el.width || 100;
|
||||
drawHeight = drawWidth / imgAspect;
|
||||
drawX = el.x;
|
||||
drawY = el.y + ((el.height || 100) - drawHeight) / 2;
|
||||
} else {
|
||||
// Image is taller than element
|
||||
drawHeight = el.height || 100;
|
||||
drawWidth = drawHeight * imgAspect;
|
||||
drawX = el.x + ((el.width || 100) - drawWidth) / 2;
|
||||
drawY = el.y;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
|
||||
|
||||
// Draw border
|
||||
ctx.strokeStyle = '#cccccc';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(el.x, el.y, el.width || 100, el.height || 100);
|
||||
} else {
|
||||
// Image still loading or failed
|
||||
this.drawImagePlaceholder(ctx, el);
|
||||
}
|
||||
} else {
|
||||
// No image uploaded yet - draw placeholder
|
||||
this.drawImagePlaceholder(ctx, el);
|
||||
}
|
||||
break;
|
||||
|
||||
default: // text elements
|
||||
this.drawTextElement(el, ctx);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Wrap text to fit within maxWidth
|
||||
wrapText(ctx, text, maxWidth) {
|
||||
const words = text.split(' ');
|
||||
const lines = [];
|
||||
let currentLine = words[0];
|
||||
|
||||
for (let i = 1; i < words.length; i++) {
|
||||
const word = words[i];
|
||||
const width = ctx.measureText(currentLine + ' ' + word).width;
|
||||
if (width < maxWidth) {
|
||||
currentLine += ' ' + word;
|
||||
} else {
|
||||
lines.push(currentLine);
|
||||
currentLine = word;
|
||||
}
|
||||
}
|
||||
lines.push(currentLine);
|
||||
return lines;
|
||||
}
|
||||
|
||||
drawTextElement(el, ctx) {
|
||||
ctx.font = `${el.fontStyle || ''} ${el.fontSize || 14}px ${el.fontFamily || 'Arial'}`;
|
||||
ctx.fillStyle = el.color || '#333333';
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
const maxWidth = el.width || 150;
|
||||
const lineHeight = (el.fontSize || 14) * 1.2;
|
||||
const maxHeight = el.height || 1000;
|
||||
|
||||
// Calculate max lines that fit
|
||||
const maxLines = Math.floor(maxHeight / lineHeight);
|
||||
|
||||
// Split by explicit newlines first, then wrap each line
|
||||
const explicitLines = (el.text || '').split('\n');
|
||||
const allLines = [];
|
||||
|
||||
for (const line of explicitLines) {
|
||||
const metrics = ctx.measureText(line);
|
||||
if (metrics.width <= maxWidth) {
|
||||
allLines.push(line);
|
||||
} else {
|
||||
const wrapped = this.wrapText(ctx, line, maxWidth);
|
||||
allLines.push(...wrapped);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if we need ellipsis
|
||||
const needsEllipsis = allLines.length > maxLines;
|
||||
const linesToDraw = needsEllipsis ? maxLines : allLines.length;
|
||||
|
||||
// Draw lines
|
||||
let y = el.y;
|
||||
for (let i = 0; i < linesToDraw; i++) {
|
||||
const line = allLines[i];
|
||||
|
||||
// If this is the last line and we need ellipsis
|
||||
if (needsEllipsis && i === linesToDraw - 1) {
|
||||
const ellipsis = '...';
|
||||
// Check if the line with ellipsis would fit
|
||||
if (ctx.measureText(line + ellipsis).width <= maxWidth) {
|
||||
ctx.fillText(line + ellipsis, el.x, y);
|
||||
} else {
|
||||
// Need to trim the line to fit ellipsis
|
||||
let trimmed = line;
|
||||
while (trimmed.length > 0 && ctx.measureText(trimmed + ellipsis).width > maxWidth) {
|
||||
trimmed = trimmed.slice(0, -1);
|
||||
}
|
||||
ctx.fillText(trimmed + ellipsis, el.x, y);
|
||||
}
|
||||
} else {
|
||||
ctx.fillText(line, el.x, y);
|
||||
}
|
||||
y += lineHeight;
|
||||
}
|
||||
|
||||
// Update height based on actual content if not manually resized
|
||||
if (!el.manuallyResized) {
|
||||
el.height = Math.max(lineHeight, allLines.length * lineHeight);
|
||||
}
|
||||
}
|
||||
|
||||
drawSelection(el) {
|
||||
const ctx = this.ctx;
|
||||
const x = el.x;
|
||||
const y = el.y;
|
||||
const w = el.width || 150;
|
||||
const h = el.height || 30;
|
||||
const hs = this.handleSize;
|
||||
|
||||
// Selection border
|
||||
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;
|
||||
|
||||
const positions = [
|
||||
[x - hs/2, y - hs/2], // nw
|
||||
[x + w/2 - hs/2, y - hs/2], // n
|
||||
[x + w - hs/2, y - hs/2], // ne
|
||||
[x - hs/2, y + h/2 - hs/2], // w
|
||||
[x + w - hs/2, y + h/2 - hs/2], // e
|
||||
[x - hs/2, y + h - hs/2], // sw
|
||||
[x + w/2 - hs/2, y + h - hs/2], // s
|
||||
[x + w - hs/2, y + h - hs/2] // se
|
||||
];
|
||||
|
||||
positions.forEach(([hx, hy]) => {
|
||||
ctx.fillRect(hx, hy, hs, hs);
|
||||
ctx.strokeRect(hx, hy, hs, hs);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
let el = {
|
||||
id, type, x, y,
|
||||
width: 150,
|
||||
height: 30,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Arial',
|
||||
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':
|
||||
case 'company':
|
||||
el.text = type === 'customer' ? 'Kundenname\nStraße Nr.\nPLZ Ort' : '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;
|
||||
}
|
||||
|
||||
this.elements.set(id, el);
|
||||
this.selectElement(id);
|
||||
this.draw();
|
||||
}
|
||||
|
||||
selectElement(id) {
|
||||
this.selectedElement = this.elements.get(id) || null;
|
||||
this.draw();
|
||||
if (this.selectedElement) {
|
||||
this.notifyChange();
|
||||
}
|
||||
}
|
||||
|
||||
deselectAll() {
|
||||
this.selectedElement = null;
|
||||
this.draw();
|
||||
if (window.invoiceGeneratorView?.$server) {
|
||||
window.invoiceGeneratorView.$server.resetPropertiesPanel();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementText(id, text) {
|
||||
const el = this.elements.get(id);
|
||||
if (el) {
|
||||
el.text = text;
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementPosition(id, x, y) {
|
||||
const el = this.elements.get(id);
|
||||
if (el) {
|
||||
if (x !== null) el.x = x;
|
||||
if (y !== null) el.y = y;
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementFontSize(id, size) {
|
||||
const el = this.elements.get(id);
|
||||
if (el) {
|
||||
el.fontSize = size;
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
updateElementImage(id, imageDataUrl) {
|
||||
const el = this.elements.get(id);
|
||||
if (el && el.type === 'image') {
|
||||
el.imageData = imageDataUrl;
|
||||
|
||||
// Create image object
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
el.imageObj = img;
|
||||
this.draw();
|
||||
};
|
||||
img.onerror = () => {
|
||||
console.error('Failed to load image');
|
||||
el.imageData = null;
|
||||
el.imageObj = null;
|
||||
this.draw();
|
||||
};
|
||||
img.src = imageDataUrl;
|
||||
}
|
||||
}
|
||||
|
||||
deleteElement(id) {
|
||||
this.elements.delete(id);
|
||||
if (this.selectedElement?.id === id) {
|
||||
this.selectedElement = null;
|
||||
}
|
||||
this.draw();
|
||||
}
|
||||
|
||||
clearCanvas() {
|
||||
this.elements.clear();
|
||||
this.selectedElement = null;
|
||||
this.draw();
|
||||
}
|
||||
|
||||
getCanvasData() {
|
||||
return {
|
||||
elements: Array.from(this.elements.values())
|
||||
};
|
||||
}
|
||||
|
||||
exportTemplate() {
|
||||
const data = JSON.stringify(this.getCanvasData(), null, 2);
|
||||
const blob = new Blob([data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'template.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
generatePreview() {
|
||||
const temp = document.createElement('canvas');
|
||||
temp.width = this.pageWidth;
|
||||
temp.height = this.pageHeight;
|
||||
const tctx = temp.getContext('2d');
|
||||
|
||||
tctx.fillStyle = '#ffffff';
|
||||
tctx.fillRect(0, 0, temp.width, temp.height);
|
||||
|
||||
// Draw elements offset by page position
|
||||
const originalPageX = this.pageX;
|
||||
const originalPageY = this.pageY;
|
||||
this.pageX = 0;
|
||||
this.pageY = 0;
|
||||
|
||||
this.elements.forEach(el => {
|
||||
const ox = el.x, oy = el.y;
|
||||
el.x = ox - originalPageX;
|
||||
el.y = oy - originalPageY;
|
||||
this.drawElement.call({ ctx: tctx, elements: this.elements }, el);
|
||||
el.x = ox;
|
||||
el.y = oy;
|
||||
});
|
||||
|
||||
this.pageX = originalPageX;
|
||||
this.pageY = originalPageY;
|
||||
|
||||
const win = window.open();
|
||||
if (win) {
|
||||
win.document.write(`<img src="${temp.toDataURL()}" style="max-width:100%">`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.invoiceGenerator = new InvoiceGenerator();
|
||||
})();
|
||||
Reference in New Issue
Block a user