signature component + improvements
This commit is contained in:
parent
b4ef9acb43
commit
bbccf871f2
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve --host 0.0.0.0",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test"
|
"test": "ng test"
|
||||||
|
|
@ -36,4 +36,4 @@
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"typescript": "~5.4.2"
|
"typescript": "~5.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,17 +5,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Sichtbares Unterschriftsfeld für den Benutzer -->
|
<!-- Integrierte SignatureComponent -->
|
||||||
<div class="signature-input">
|
<app-signature
|
||||||
<div class="date-input">
|
class="signature-component print:hidden"
|
||||||
Datum: <input type="date" [(ngModel)]="currentDate" />
|
(signatureSaved)="onSignatureSaved($event)"
|
||||||
</div>
|
(dateChanged)="onDateChanged($event)"
|
||||||
<canvas #signatureCanvas class="signature-canvas"></canvas>
|
>
|
||||||
<div class="buttons">
|
</app-signature>
|
||||||
<button (click)="clearSignature()">Unterschrift löschen</button>
|
|
||||||
<button (click)="saveSignature()">Unterschrift speichern</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div #formContent style="display: none">
|
<div #formContent style="display: none">
|
||||||
<h1 class="text-3xl font-bold mb-6">Wohnungs-Mietvertrag</h1>
|
<h1 class="text-3xl font-bold mb-6">Wohnungs-Mietvertrag</h1>
|
||||||
<p class="mb-4">Zwischen</p>
|
<p class="mb-4">Zwischen</p>
|
||||||
|
|
@ -359,6 +355,7 @@
|
||||||
Mietvertrages. Sinngemäß gilt dies auch für Einzelsatellitenempfangsanlagen.
|
Mietvertrages. Sinngemäß gilt dies auch für Einzelsatellitenempfangsanlagen.
|
||||||
</p>
|
</p>
|
||||||
<!-- Unterschriftsfeld im Dokument -->
|
<!-- Unterschriftsfeld im Dokument -->
|
||||||
|
<!-- Unterschriftsfeld im Dokument -->
|
||||||
<div class="signature-container">
|
<div class="signature-container">
|
||||||
<div class="date">Datum: {{ currentDate }}</div>
|
<div class="date">Datum: {{ currentDate }}</div>
|
||||||
<img
|
<img
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,103 @@
|
||||||
.document {
|
.document {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
}
|
}
|
||||||
.page {
|
.page {
|
||||||
width: 210mm;
|
width: 210mm;
|
||||||
height: 297mm;
|
height: 297mm;
|
||||||
padding: 20mm;
|
padding: 20mm;
|
||||||
margin: 10mm auto;
|
margin: 10mm auto;
|
||||||
background: white;
|
background: white;
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.document {
|
.document {
|
||||||
/* Ihre Dokumentstile */
|
/* Ihre Dokumentstile */
|
||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
/* Seitenstile, z.B. Seitenränder, Breaks etc. */
|
/* Seitenstile, z.B. Seitenränder, Breaks etc. */
|
||||||
page-break-after: always;
|
page-break-after: always;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-content {
|
.page-content {
|
||||||
/* Inhaltsstile */
|
/* Inhaltsstile */
|
||||||
}
|
}
|
||||||
|
|
||||||
.signature-input {
|
// .signature-input {
|
||||||
margin-top: 20px;
|
// margin-top: 20px;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.date-input {
|
// .date-input {
|
||||||
margin-bottom: 10px;
|
// margin-bottom: 10px;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.signature-canvas {
|
// .signature-canvas {
|
||||||
border: 1px dashed #000;
|
// border: 1px dashed #000;
|
||||||
width: 300px;
|
// width: 300px;
|
||||||
height: 100px;
|
// height: 100px;
|
||||||
touch-action: none; /* Verbessert die Touch-Interaktion */
|
// touch-action: none; /* Verbessert die Touch-Interaktion */
|
||||||
}
|
// }
|
||||||
|
|
||||||
.buttons {
|
// .buttons {
|
||||||
margin-top: 10px;
|
// margin-top: 10px;
|
||||||
}
|
// }
|
||||||
|
/* Styling für die Signaturkomponente */
|
||||||
.signature-container {
|
.signature-component {
|
||||||
|
position: fixed; /* Für Desktop-Anpinnen */
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 100; /* Sicherstellen, dass es über anderen Elementen liegt */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) and (orientation: landscape) {
|
||||||
|
/* Mobile Landscape: Vollbildanzeige */
|
||||||
|
.signature-component.fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-container .date {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-image {
|
|
||||||
border: 1px dashed #000;
|
|
||||||
width: 300px;
|
|
||||||
height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.page {
|
|
||||||
margin: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Optional: Schließen-Button für Vollbild */
|
||||||
|
.signature-component.fullscreen .close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.signature-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-container .date {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-image {
|
||||||
|
border: 1px dashed #000;
|
||||||
|
width: 300px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.page {
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,18 @@ import {
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { SignatureComponent } from '../signature/signature.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-rental-agreement',
|
selector: 'app-rental-agreement',
|
||||||
|
imports: [CommonModule, FormsModule, SignatureComponent],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
|
||||||
templateUrl: './rental-agreement.component.html',
|
templateUrl: './rental-agreement.component.html',
|
||||||
styleUrl: './rental-agreement.component.scss',
|
styleUrls: ['./rental-agreement.component.scss'],
|
||||||
})
|
})
|
||||||
export class RentalAgreementComponent implements AfterViewInit {
|
export class RentalAgreementComponent implements AfterViewInit {
|
||||||
@ViewChild('formContent') formContent!: ElementRef<HTMLDivElement>;
|
@ViewChild('formContent') formContent!: ElementRef<HTMLDivElement>;
|
||||||
@ViewChild('measureContainer') measureContainer!: ElementRef<HTMLDivElement>;
|
@ViewChild('measureContainer') measureContainer!: ElementRef<HTMLDivElement>;
|
||||||
@ViewChild('signatureCanvas') signatureCanvas!: ElementRef<HTMLCanvasElement>;
|
|
||||||
|
|
||||||
pages: string[] = [];
|
pages: string[] = [];
|
||||||
private currentPageHeight = 0;
|
private currentPageHeight = 0;
|
||||||
|
|
@ -28,153 +28,83 @@ export class RentalAgreementComponent implements AfterViewInit {
|
||||||
currentDate: string = new Date().toISOString().substr(0, 10); // YYYY-MM-DD
|
currentDate: string = new Date().toISOString().substr(0, 10); // YYYY-MM-DD
|
||||||
signatureImage: string | null = null;
|
signatureImage: string | null = null;
|
||||||
|
|
||||||
private ctx!: CanvasRenderingContext2D;
|
|
||||||
private isDrawing = false;
|
|
||||||
|
|
||||||
constructor(private renderer: Renderer2, private el: ElementRef) {}
|
constructor(private renderer: Renderer2, private el: ElementRef) {}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
// Initialisieren des Canvas
|
// Initiales Rendern der Inhalte
|
||||||
const canvas = this.signatureCanvas.nativeElement;
|
setTimeout(() => this.renderContent(), 0);
|
||||||
this.ctx = canvas.getContext('2d')!;
|
|
||||||
this.resizeCanvas();
|
|
||||||
this.setupCanvasEvents();
|
|
||||||
//setTimeout(() => this.renderContent(), 0);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Passt die Canvas-Größe an.
|
|
||||||
*/
|
|
||||||
resizeCanvas() {
|
|
||||||
const canvas = this.signatureCanvas.nativeElement;
|
|
||||||
canvas.width = 300; // Anpassen nach Bedarf
|
|
||||||
canvas.height = 100; // Anpassen nach Bedarf
|
|
||||||
this.ctx.lineWidth = 2;
|
|
||||||
this.ctx.strokeStyle = '#000';
|
|
||||||
this.ctx.lineCap = 'round';
|
|
||||||
this.ctx.lineJoin = 'round';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setzt die Ereignislistener für das Canvas.
|
* Handler für die Speicherung der Unterschrift.
|
||||||
|
* @param signatureData Das Bild der Unterschrift als Data URL.
|
||||||
*/
|
*/
|
||||||
setupCanvasEvents() {
|
onSignatureSaved(signatureData: string) {
|
||||||
const canvas = this.signatureCanvas.nativeElement;
|
this.signatureImage = signatureData;
|
||||||
|
this.renderContent();
|
||||||
// Maus-Ereignisse
|
|
||||||
canvas.addEventListener('mousedown', this.startDrawing.bind(this));
|
|
||||||
canvas.addEventListener('mousemove', this.draw.bind(this));
|
|
||||||
canvas.addEventListener('mouseup', this.stopDrawing.bind(this));
|
|
||||||
canvas.addEventListener('mouseleave', this.stopDrawing.bind(this));
|
|
||||||
|
|
||||||
// Touch-Ereignisse
|
|
||||||
canvas.addEventListener('touchstart', this.startDrawing.bind(this));
|
|
||||||
canvas.addEventListener('touchmove', this.draw.bind(this));
|
|
||||||
canvas.addEventListener('touchend', this.stopDrawing.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Startet das Zeichnen auf dem Canvas.
|
* Handler für die Änderung des Datums.
|
||||||
|
* @param date Das neue Datum im Format YYYY-MM-DD.
|
||||||
*/
|
*/
|
||||||
startDrawing(event: MouseEvent | TouchEvent) {
|
onDateChanged(date: any) {
|
||||||
event.preventDefault();
|
this.currentDate = date;
|
||||||
this.isDrawing = true;
|
this.renderContent();
|
||||||
const { x, y } = this.getXY(event);
|
|
||||||
this.ctx.beginPath();
|
|
||||||
this.ctx.moveTo(x, y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeichnet auf dem Canvas.
|
* Rendert den Inhalt und teilt ihn in Seiten auf.
|
||||||
*/
|
*/
|
||||||
draw(event: MouseEvent | TouchEvent) {
|
renderContent() {
|
||||||
if (!this.isDrawing) return;
|
this.pages = [];
|
||||||
event.preventDefault();
|
this.currentPageHeight = 0;
|
||||||
const { x, y } = this.getXY(event);
|
|
||||||
this.ctx.lineTo(x, y);
|
|
||||||
this.ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Klonen des formContent-Elements
|
||||||
* Beendet das Zeichnen auf dem Canvas.
|
const currentContent = this.formContent.nativeElement.cloneNode(
|
||||||
*/
|
true
|
||||||
stopDrawing(event: MouseEvent | TouchEvent) {
|
) as HTMLElement;
|
||||||
if (!this.isDrawing) return;
|
|
||||||
event.preventDefault();
|
|
||||||
this.isDrawing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Aktualisieren von Datum und Unterschrift im geklonten Inhalt
|
||||||
* Ermittelt die X- und Y-Koordinaten auf dem Canvas.
|
const signatureContainer = currentContent.querySelector(
|
||||||
*/
|
'.signature-container'
|
||||||
getXY(event: MouseEvent | TouchEvent): { x: number; y: number } {
|
) as HTMLElement;
|
||||||
const canvas = this.signatureCanvas.nativeElement;
|
if (signatureContainer) {
|
||||||
let clientX: number;
|
signatureContainer.querySelector(
|
||||||
let clientY: number;
|
'.date'
|
||||||
|
)!.textContent = `Datum: ${this.currentDate}`;
|
||||||
if (event instanceof MouseEvent) {
|
const img = signatureContainer.querySelector(
|
||||||
clientX = event.clientX;
|
'.signature-image'
|
||||||
clientY = event.clientY;
|
) as HTMLImageElement;
|
||||||
} else {
|
if (img && this.signatureImage) {
|
||||||
clientX = event.touches[0].clientX;
|
img.src = this.signatureImage;
|
||||||
clientY = event.touches[0].clientY;
|
} else if (img) {
|
||||||
|
img.remove(); // Entfernen des img-Tags, falls keine Unterschrift vorhanden ist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
const children = Array.from(currentContent.children);
|
||||||
const x = clientX - rect.left;
|
|
||||||
const y = clientY - rect.top;
|
|
||||||
return { x, y };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Löscht die Unterschrift vom Canvas.
|
|
||||||
*/
|
|
||||||
clearSignature() {
|
|
||||||
this.ctx.clearRect(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
this.signatureCanvas.nativeElement.width,
|
|
||||||
this.signatureCanvas.nativeElement.height
|
|
||||||
);
|
|
||||||
this.signatureImage = null;
|
|
||||||
this.renderContent(); // Neu rendern, um die Unterschrift aus den Seiten zu entfernen
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Speichert die Unterschrift als Bild.
|
|
||||||
*/
|
|
||||||
saveSignature() {
|
|
||||||
const canvas = this.signatureCanvas.nativeElement;
|
|
||||||
this.signatureImage = canvas.toDataURL('image/png');
|
|
||||||
this.pages = [];
|
|
||||||
setTimeout(() => {
|
|
||||||
this.renderContent(); // Neu rendern, um die Unterschrift in den Seiten einzufügen
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderContent() {
|
|
||||||
let currentPage = '';
|
|
||||||
const children = Array.from(this.formContent.nativeElement.children);
|
|
||||||
const totalElements = children.length;
|
const totalElements = children.length;
|
||||||
|
|
||||||
|
let currentPage = '';
|
||||||
|
|
||||||
for (let i = 0; i < totalElements; i++) {
|
for (let i = 0; i < totalElements; i++) {
|
||||||
const element = children[i] as HTMLElement;
|
const element = children[i] as HTMLElement;
|
||||||
|
|
||||||
// Überprüfen, ob das aktuelle Element ein <h2> ist
|
// Spezielle Behandlung für <h2>-Elemente
|
||||||
if (element.tagName.toLowerCase() === 'h2') {
|
if (element.tagName.toLowerCase() === 'h2') {
|
||||||
// Clone das <h2> Element
|
// Klonen und Messen des <h2>-Elements
|
||||||
const h2Clone = element.cloneNode(true) as HTMLElement;
|
const h2Clone = element.cloneNode(true) as HTMLElement;
|
||||||
this.renderer.appendChild(this.measureContainer.nativeElement, h2Clone);
|
this.renderer.appendChild(this.measureContainer.nativeElement, h2Clone);
|
||||||
|
|
||||||
// Berechnung der Höhe inklusive Margins für <h2>
|
|
||||||
const h2Style = window.getComputedStyle(h2Clone);
|
const h2Style = window.getComputedStyle(h2Clone);
|
||||||
const h2MarginTop = parseFloat(h2Style.marginTop) || 0;
|
const h2MarginTop = parseFloat(h2Style.marginTop) || 0;
|
||||||
const h2MarginBottom = parseFloat(h2Style.marginBottom) || 0;
|
const h2MarginBottom = parseFloat(h2Style.marginBottom) || 0;
|
||||||
const h2Height = h2Clone.offsetHeight + h2MarginTop + h2MarginBottom;
|
const h2Height = h2Clone.offsetHeight + h2MarginTop + h2MarginBottom;
|
||||||
|
|
||||||
// Entfernen des geklonten <h2> Elements aus dem Messcontainer
|
|
||||||
this.renderer.removeChild(this.measureContainer.nativeElement, h2Clone);
|
this.renderer.removeChild(this.measureContainer.nativeElement, h2Clone);
|
||||||
|
|
||||||
// Prüfen, ob es ein nachfolgendes <p> Element gibt
|
// Prüfen, ob ein nachfolgendes <p>-Element vorhanden ist
|
||||||
let pHeight = 0;
|
let pHeight = 0;
|
||||||
if (i + 1 < totalElements) {
|
if (i + 1 < totalElements) {
|
||||||
const nextElement = children[i + 1] as HTMLElement;
|
const nextElement = children[i + 1] as HTMLElement;
|
||||||
|
|
@ -185,13 +115,11 @@ export class RentalAgreementComponent implements AfterViewInit {
|
||||||
pClone
|
pClone
|
||||||
);
|
);
|
||||||
|
|
||||||
// Berechnung der Höhe inklusive Margins für <p>
|
|
||||||
const pStyle = window.getComputedStyle(pClone);
|
const pStyle = window.getComputedStyle(pClone);
|
||||||
const pMarginTop = parseFloat(pStyle.marginTop) || 0;
|
const pMarginTop = parseFloat(pStyle.marginTop) || 0;
|
||||||
const pMarginBottom = parseFloat(pStyle.marginBottom) || 0;
|
const pMarginBottom = parseFloat(pStyle.marginBottom) || 0;
|
||||||
pHeight = pClone.offsetHeight + pMarginTop + pMarginBottom;
|
pHeight = pClone.offsetHeight + pMarginTop + pMarginBottom;
|
||||||
|
|
||||||
// Entfernen des geklonten <p> Elements aus dem Messcontainer
|
|
||||||
this.renderer.removeChild(
|
this.renderer.removeChild(
|
||||||
this.measureContainer.nativeElement,
|
this.measureContainer.nativeElement,
|
||||||
pClone
|
pClone
|
||||||
|
|
@ -199,7 +127,6 @@ export class RentalAgreementComponent implements AfterViewInit {
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gesamthöhe von <h2> und folgendem <p> (falls vorhanden)
|
|
||||||
const totalH2PHeight = h2Height + pHeight;
|
const totalH2PHeight = h2Height + pHeight;
|
||||||
|
|
||||||
// Prüfen, ob <h2> und <p> auf die aktuelle Seite passen
|
// Prüfen, ob <h2> und <p> auf die aktuelle Seite passen
|
||||||
|
|
@ -216,10 +143,10 @@ export class RentalAgreementComponent implements AfterViewInit {
|
||||||
this.currentPageHeight += h2Height;
|
this.currentPageHeight += h2Height;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nun das nachfolgende <p> Element hinzufügen, falls vorhanden
|
// <p>-Element hinzufügen, falls vorhanden
|
||||||
if (pHeight > 0) {
|
if (pHeight > 0) {
|
||||||
if (this.currentPageHeight + pHeight > this.maxPageHeight) {
|
if (this.currentPageHeight + pHeight > this.maxPageHeight) {
|
||||||
// Neue Seite beginnen für <p>
|
// Neue Seite für <p> beginnen
|
||||||
this.pages.push(currentPage);
|
this.pages.push(currentPage);
|
||||||
currentPage = children[i + 1].outerHTML;
|
currentPage = children[i + 1].outerHTML;
|
||||||
this.currentPageHeight = pHeight;
|
this.currentPageHeight = pHeight;
|
||||||
|
|
@ -228,21 +155,18 @@ export class RentalAgreementComponent implements AfterViewInit {
|
||||||
currentPage += children[i + 1].outerHTML;
|
currentPage += children[i + 1].outerHTML;
|
||||||
this.currentPageHeight += pHeight;
|
this.currentPageHeight += pHeight;
|
||||||
}
|
}
|
||||||
// Überspringen des nächsten Elements, da es bereits verarbeitet wurde
|
i += 1; // Überspringen des bereits verarbeiteten <p>-Elements
|
||||||
i += 1;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Für alle anderen Elemente wie <h1>, <p> usw.
|
// Behandlung aller anderen Elemente
|
||||||
const clone = element.cloneNode(true) as HTMLElement;
|
const clone = element.cloneNode(true) as HTMLElement;
|
||||||
this.renderer.appendChild(this.measureContainer.nativeElement, clone);
|
this.renderer.appendChild(this.measureContainer.nativeElement, clone);
|
||||||
|
|
||||||
// Berechnung der Höhe inklusive Margins
|
|
||||||
const computedStyle = window.getComputedStyle(clone);
|
const computedStyle = window.getComputedStyle(clone);
|
||||||
const marginTop = parseFloat(computedStyle.marginTop) || 0;
|
const marginTop = parseFloat(computedStyle.marginTop) || 0;
|
||||||
const marginBottom = parseFloat(computedStyle.marginBottom) || 0;
|
const marginBottom = parseFloat(computedStyle.marginBottom) || 0;
|
||||||
const height = clone.offsetHeight + marginTop + marginBottom;
|
const height = clone.offsetHeight + marginTop + marginBottom;
|
||||||
|
|
||||||
// Entfernen des geklonten Elements aus dem Messcontainer
|
|
||||||
this.renderer.removeChild(this.measureContainer.nativeElement, clone);
|
this.renderer.removeChild(this.measureContainer.nativeElement, clone);
|
||||||
|
|
||||||
if (this.currentPageHeight + height > this.maxPageHeight) {
|
if (this.currentPageHeight + height > this.maxPageHeight) {
|
||||||
|
|
@ -264,6 +188,9 @@ export class RentalAgreementComponent implements AfterViewInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Druckt das Dokument.
|
||||||
|
*/
|
||||||
print() {
|
print() {
|
||||||
window.print();
|
window.print();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<div class="signature-container" [class.fullscreen]="isFullscreen">
|
||||||
|
<button class="close-button" *ngIf="isFullscreen" (click)="exitFullscreen()">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<div class="date-input">
|
||||||
|
Datum:
|
||||||
|
<input type="date" [(ngModel)]="currentDate" (change)="onDateChange()" />
|
||||||
|
</div>
|
||||||
|
<canvas #signatureCanvas class="signature-canvas" tabindex="0"></canvas>
|
||||||
|
<div class="buttons">
|
||||||
|
<button (click)="clearSignature()">Unterschrift löschen</button>
|
||||||
|
<button (click)="saveSignature()">Unterschrift speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
.signature-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-canvas {
|
||||||
|
border: 1px dashed #000;
|
||||||
|
width: 300px;
|
||||||
|
height: 100px;
|
||||||
|
touch-action: none; /* Verbessert die Touch-Interaktion */
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
/* Vollbild-Styling wird über die Klasse 'fullscreen' hinzugefügt */
|
||||||
|
.signature-container.fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-container.fullscreen .signature-canvas {
|
||||||
|
width: 90%;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-container.fullscreen .buttons {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-container.fullscreen .close-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { fromEvent, Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-signature',
|
||||||
|
imports: [CommonModule, FormsModule],
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './signature.component.html',
|
||||||
|
styleUrls: ['./signature.component.scss'],
|
||||||
|
})
|
||||||
|
export class SignatureComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
@ViewChild('signatureCanvas') signatureCanvas!: ElementRef<HTMLCanvasElement>;
|
||||||
|
@Output() signatureSaved = new EventEmitter<string>();
|
||||||
|
@Output() dateChanged = new EventEmitter<string>();
|
||||||
|
|
||||||
|
currentDate: string = new Date().toISOString().substr(0, 10); // YYYY-MM-DD
|
||||||
|
signatureImage: string | null = null;
|
||||||
|
|
||||||
|
isFullscreen: boolean = false; // Zustand für Vollbild
|
||||||
|
|
||||||
|
private ctx!: CanvasRenderingContext2D;
|
||||||
|
private isDrawing = false;
|
||||||
|
|
||||||
|
currentOrientation: string = 'unbekannt';
|
||||||
|
private orientationSubscription: Subscription;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.orientationSubscription = fromEvent(
|
||||||
|
window,
|
||||||
|
'orientationchange'
|
||||||
|
).subscribe(() => {
|
||||||
|
this.checkOrientation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.initializeCanvas();
|
||||||
|
this.setupCanvasEvents();
|
||||||
|
//this.checkOrientation(); // Initiale Überprüfung der Orientierung
|
||||||
|
//window.addEventListener('resize', this.checkOrientation.bind(this)); // Listener für Änderungen
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
// Abmelden des Subscriptions beim Zerstören der Komponente
|
||||||
|
if (this.orientationSubscription) {
|
||||||
|
this.orientationSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert das Canvas.
|
||||||
|
*/
|
||||||
|
initializeCanvas() {
|
||||||
|
const canvas = this.signatureCanvas.nativeElement;
|
||||||
|
this.ctx = canvas.getContext('2d')!;
|
||||||
|
this.resizeCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passt die Canvas-Größe an und behält den Inhalt (Unterschrift) bei.
|
||||||
|
*/
|
||||||
|
resizeCanvas() {
|
||||||
|
const canvas = this.signatureCanvas.nativeElement;
|
||||||
|
const previousDataUrl = this.signatureImage; // Vorhandene Unterschrift speichern
|
||||||
|
|
||||||
|
if (this.isFullscreen) {
|
||||||
|
// Fullscreen-Modus: Größere Canvas-Größe
|
||||||
|
canvas.width = window.innerWidth * 0.9; // 90% der Fensterbreite
|
||||||
|
canvas.height = window.innerHeight * 0.3; // 30% der Fensterhöhe
|
||||||
|
} else {
|
||||||
|
// Normaler Modus: Standardgröße
|
||||||
|
canvas.width = 300; // Anpassen nach Bedarf
|
||||||
|
canvas.height = 100; // Anpassen nach Bedarf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeichenkontext neu konfigurieren
|
||||||
|
this.ctx.lineWidth = 2;
|
||||||
|
this.ctx.strokeStyle = '#000';
|
||||||
|
this.ctx.lineCap = 'round';
|
||||||
|
this.ctx.lineJoin = 'round';
|
||||||
|
|
||||||
|
if (previousDataUrl) {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = previousDataUrl;
|
||||||
|
img.onload = () => {
|
||||||
|
// Berechne Skalierungsfaktoren
|
||||||
|
const scaleX = canvas.width / img.width;
|
||||||
|
const scaleY = canvas.height / img.height;
|
||||||
|
const scale = Math.min(scaleX, scaleY); // Proportional skalieren
|
||||||
|
|
||||||
|
const x = canvas.width / 2 - (img.width * scale) / 2;
|
||||||
|
const y = canvas.height / 2 - (img.height * scale) / 2;
|
||||||
|
|
||||||
|
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
this.ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Falls keine Unterschrift vorhanden ist, Canvas leeren
|
||||||
|
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt die Ereignislistener für das Canvas.
|
||||||
|
*/
|
||||||
|
setupCanvasEvents() {
|
||||||
|
const canvas = this.signatureCanvas.nativeElement;
|
||||||
|
|
||||||
|
// Maus-Ereignisse
|
||||||
|
canvas.addEventListener('mousedown', this.startDrawing.bind(this));
|
||||||
|
canvas.addEventListener('mousemove', this.draw.bind(this));
|
||||||
|
canvas.addEventListener('mouseup', this.stopDrawing.bind(this));
|
||||||
|
canvas.addEventListener('mouseleave', this.stopDrawing.bind(this));
|
||||||
|
|
||||||
|
// Touch-Ereignisse
|
||||||
|
canvas.addEventListener('touchstart', this.startDrawing.bind(this));
|
||||||
|
canvas.addEventListener('touchmove', this.draw.bind(this));
|
||||||
|
canvas.addEventListener('touchend', this.stopDrawing.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet das Zeichnen auf dem Canvas.
|
||||||
|
*/
|
||||||
|
startDrawing(event: MouseEvent | TouchEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.isDrawing = true;
|
||||||
|
const { x, y } = this.getXY(event);
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeichnet auf dem Canvas.
|
||||||
|
*/
|
||||||
|
draw(event: MouseEvent | TouchEvent) {
|
||||||
|
if (!this.isDrawing) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const { x, y } = this.getXY(event);
|
||||||
|
this.ctx.lineTo(x, y);
|
||||||
|
this.ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beendet das Zeichnen auf dem Canvas.
|
||||||
|
*/
|
||||||
|
stopDrawing(event: MouseEvent | TouchEvent) {
|
||||||
|
if (!this.isDrawing) return;
|
||||||
|
event.preventDefault();
|
||||||
|
this.isDrawing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt die X- und Y-Koordinaten auf dem Canvas.
|
||||||
|
*/
|
||||||
|
getXY(event: MouseEvent | TouchEvent): { x: number; y: number } {
|
||||||
|
const canvas = this.signatureCanvas.nativeElement;
|
||||||
|
let clientX: number;
|
||||||
|
let clientY: number;
|
||||||
|
|
||||||
|
if (event instanceof MouseEvent) {
|
||||||
|
clientX = event.clientX;
|
||||||
|
clientY = event.clientY;
|
||||||
|
} else {
|
||||||
|
clientX = event.touches[0].clientX;
|
||||||
|
clientY = event.touches[0].clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = clientX - rect.left;
|
||||||
|
const y = clientY - rect.top;
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löscht die Unterschrift vom Canvas.
|
||||||
|
*/
|
||||||
|
clearSignature() {
|
||||||
|
this.ctx.clearRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
this.signatureCanvas.nativeElement.width,
|
||||||
|
this.signatureCanvas.nativeElement.height
|
||||||
|
);
|
||||||
|
this.signatureImage = null;
|
||||||
|
this.signatureSaved.emit(null as any); // Emit null, um die Unterschrift zu löschen
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speichert die Unterschrift als Bild.
|
||||||
|
*/
|
||||||
|
saveSignature() {
|
||||||
|
const canvas = this.signatureCanvas.nativeElement;
|
||||||
|
this.signatureImage = canvas.toDataURL('image/png');
|
||||||
|
this.signatureSaved.emit(this.signatureImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handhabung der Datumseingabeänderung.
|
||||||
|
*/
|
||||||
|
onDateChange() {
|
||||||
|
this.dateChanged.emit(this.currentDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Überprüft die Bildschirmorientierung und setzt den Vollbildmodus entsprechend.
|
||||||
|
*/
|
||||||
|
checkOrientation() {
|
||||||
|
const wasFullscreen = this.isFullscreen;
|
||||||
|
if (window.innerWidth > window.innerHeight) {
|
||||||
|
// Landscape
|
||||||
|
this.isFullscreen = true;
|
||||||
|
} else {
|
||||||
|
// Portrait
|
||||||
|
this.isFullscreen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasFullscreen !== this.isFullscreen) {
|
||||||
|
this.resizeCanvas(); // Canvas-Größe bei Änderung anpassen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deaktiviert den Vollbildmodus.
|
||||||
|
*/
|
||||||
|
exitFullscreen() {
|
||||||
|
this.isFullscreen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<title>ToyotaFormApp</title>
|
<title>PDF Signature App</title>
|
||||||
<base href="/">
|
<meta
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
http-equiv="Cache-Control"
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
content="no-cache, no-store, must-revalidate"
|
||||||
</head>
|
/>
|
||||||
<body class="bg-gray-100">
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
<app-root></app-root>
|
<meta http-equiv="Expires" content="0" />
|
||||||
</body>
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100">
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue