From bbccf871f274c28014a3c8d2fb3e9d18815872df Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Tue, 24 Sep 2024 16:08:02 +0200 Subject: [PATCH] signature component + improvements --- package.json | 4 +- .../rental-agreement.component.html | 19 +- .../rental-agreement.component.scss | 161 +++++++----- .../rental-agreement.component.ts | 177 ++++--------- src/app/signature/signature.component.html | 14 + src/app/signature/signature.component.scss | 62 +++++ src/app/signature/signature.component.ts | 241 ++++++++++++++++++ src/index.html | 28 +- 8 files changed, 494 insertions(+), 212 deletions(-) create mode 100644 src/app/signature/signature.component.html create mode 100644 src/app/signature/signature.component.scss create mode 100644 src/app/signature/signature.component.ts diff --git a/package.json b/package.json index e7f9104..5779759 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "scripts": { "ng": "ng", - "start": "ng serve", + "start": "ng serve --host 0.0.0.0", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" @@ -36,4 +36,4 @@ "tailwindcss": "^3.4.4", "typescript": "~5.4.2" } -} +} \ No newline at end of file diff --git a/src/app/rental-agreement/rental-agreement.component.html b/src/app/rental-agreement/rental-agreement.component.html index 770dabe..587bfcb 100644 --- a/src/app/rental-agreement/rental-agreement.component.html +++ b/src/app/rental-agreement/rental-agreement.component.html @@ -5,17 +5,13 @@ - -
-
- Datum: -
- -
- - -
-
+ + +

Wohnungs-Mietvertrag

Zwischen

@@ -359,6 +355,7 @@ Mietvertrages. Sinngemäß gilt dies auch für Einzelsatellitenempfangsanlagen.

+
Datum: {{ currentDate }}
; @ViewChild('measureContainer') measureContainer!: ElementRef; - @ViewChild('signatureCanvas') signatureCanvas!: ElementRef; pages: string[] = []; private currentPageHeight = 0; @@ -28,153 +28,83 @@ export class RentalAgreementComponent implements AfterViewInit { currentDate: string = new Date().toISOString().substr(0, 10); // YYYY-MM-DD signatureImage: string | null = null; - private ctx!: CanvasRenderingContext2D; - private isDrawing = false; - constructor(private renderer: Renderer2, private el: ElementRef) {} ngAfterViewInit() { - // Initialisieren des Canvas - const canvas = this.signatureCanvas.nativeElement; - 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'; + // Initiales Rendern der Inhalte + setTimeout(() => this.renderContent(), 0); } /** - * Setzt die Ereignislistener für das Canvas. + * Handler für die Speicherung der Unterschrift. + * @param signatureData Das Bild der Unterschrift als Data URL. */ - 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)); + onSignatureSaved(signatureData: string) { + this.signatureImage = signatureData; + this.renderContent(); } /** - * 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) { - event.preventDefault(); - this.isDrawing = true; - const { x, y } = this.getXY(event); - this.ctx.beginPath(); - this.ctx.moveTo(x, y); + onDateChanged(date: any) { + this.currentDate = date; + this.renderContent(); } /** - * Zeichnet auf dem Canvas. + * Rendert den Inhalt und teilt ihn in Seiten auf. */ - draw(event: MouseEvent | TouchEvent) { - if (!this.isDrawing) return; - event.preventDefault(); - const { x, y } = this.getXY(event); - this.ctx.lineTo(x, y); - this.ctx.stroke(); - } + renderContent() { + this.pages = []; + this.currentPageHeight = 0; - /** - * Beendet das Zeichnen auf dem Canvas. - */ - stopDrawing(event: MouseEvent | TouchEvent) { - if (!this.isDrawing) return; - event.preventDefault(); - this.isDrawing = false; - } + // Klonen des formContent-Elements + const currentContent = this.formContent.nativeElement.cloneNode( + true + ) as HTMLElement; - /** - * 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; + // Aktualisieren von Datum und Unterschrift im geklonten Inhalt + const signatureContainer = currentContent.querySelector( + '.signature-container' + ) as HTMLElement; + if (signatureContainer) { + signatureContainer.querySelector( + '.date' + )!.textContent = `Datum: ${this.currentDate}`; + const img = signatureContainer.querySelector( + '.signature-image' + ) as HTMLImageElement; + if (img && this.signatureImage) { + img.src = this.signatureImage; + } else if (img) { + img.remove(); // Entfernen des img-Tags, falls keine Unterschrift vorhanden ist + } } - 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.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 children = Array.from(currentContent.children); const totalElements = children.length; + let currentPage = ''; + for (let i = 0; i < totalElements; i++) { const element = children[i] as HTMLElement; - // Überprüfen, ob das aktuelle Element ein

ist + // Spezielle Behandlung für

-Elemente if (element.tagName.toLowerCase() === 'h2') { - // Clone das

Element + // Klonen und Messen des

-Elements const h2Clone = element.cloneNode(true) as HTMLElement; this.renderer.appendChild(this.measureContainer.nativeElement, h2Clone); - // Berechnung der Höhe inklusive Margins für

const h2Style = window.getComputedStyle(h2Clone); const h2MarginTop = parseFloat(h2Style.marginTop) || 0; const h2MarginBottom = parseFloat(h2Style.marginBottom) || 0; const h2Height = h2Clone.offsetHeight + h2MarginTop + h2MarginBottom; - // Entfernen des geklonten

Elements aus dem Messcontainer this.renderer.removeChild(this.measureContainer.nativeElement, h2Clone); - // Prüfen, ob es ein nachfolgendes

Element gibt + // Prüfen, ob ein nachfolgendes

-Element vorhanden ist let pHeight = 0; if (i + 1 < totalElements) { const nextElement = children[i + 1] as HTMLElement; @@ -185,13 +115,11 @@ export class RentalAgreementComponent implements AfterViewInit { pClone ); - // Berechnung der Höhe inklusive Margins für

const pStyle = window.getComputedStyle(pClone); const pMarginTop = parseFloat(pStyle.marginTop) || 0; const pMarginBottom = parseFloat(pStyle.marginBottom) || 0; pHeight = pClone.offsetHeight + pMarginTop + pMarginBottom; - // Entfernen des geklonten

Elements aus dem Messcontainer this.renderer.removeChild( this.measureContainer.nativeElement, pClone @@ -199,7 +127,6 @@ export class RentalAgreementComponent implements AfterViewInit { //} } - // Gesamthöhe von

und folgendem

(falls vorhanden) const totalH2PHeight = h2Height + pHeight; // Prüfen, ob

und

auf die aktuelle Seite passen @@ -216,10 +143,10 @@ export class RentalAgreementComponent implements AfterViewInit { this.currentPageHeight += h2Height; } - // Nun das nachfolgende

Element hinzufügen, falls vorhanden + //

-Element hinzufügen, falls vorhanden if (pHeight > 0) { if (this.currentPageHeight + pHeight > this.maxPageHeight) { - // Neue Seite beginnen für

+ // Neue Seite für

beginnen this.pages.push(currentPage); currentPage = children[i + 1].outerHTML; this.currentPageHeight = pHeight; @@ -228,21 +155,18 @@ export class RentalAgreementComponent implements AfterViewInit { currentPage += children[i + 1].outerHTML; this.currentPageHeight += pHeight; } - // Überspringen des nächsten Elements, da es bereits verarbeitet wurde - i += 1; + i += 1; // Überspringen des bereits verarbeiteten

-Elements } } else { - // Für alle anderen Elemente wie

,

usw. + // Behandlung aller anderen Elemente const clone = element.cloneNode(true) as HTMLElement; this.renderer.appendChild(this.measureContainer.nativeElement, clone); - // Berechnung der Höhe inklusive Margins const computedStyle = window.getComputedStyle(clone); const marginTop = parseFloat(computedStyle.marginTop) || 0; const marginBottom = parseFloat(computedStyle.marginBottom) || 0; const height = clone.offsetHeight + marginTop + marginBottom; - // Entfernen des geklonten Elements aus dem Messcontainer this.renderer.removeChild(this.measureContainer.nativeElement, clone); if (this.currentPageHeight + height > this.maxPageHeight) { @@ -264,6 +188,9 @@ export class RentalAgreementComponent implements AfterViewInit { } } + /** + * Druckt das Dokument. + */ print() { window.print(); } diff --git a/src/app/signature/signature.component.html b/src/app/signature/signature.component.html new file mode 100644 index 0000000..bf09004 --- /dev/null +++ b/src/app/signature/signature.component.html @@ -0,0 +1,14 @@ +

+ +
+ Datum: + +
+ +
+ + +
+
diff --git a/src/app/signature/signature.component.scss b/src/app/signature/signature.component.scss new file mode 100644 index 0000000..4863a5e --- /dev/null +++ b/src/app/signature/signature.component.scss @@ -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; + } +} diff --git a/src/app/signature/signature.component.ts b/src/app/signature/signature.component.ts new file mode 100644 index 0000000..343ec8f --- /dev/null +++ b/src/app/signature/signature.component.ts @@ -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; + @Output() signatureSaved = new EventEmitter(); + @Output() dateChanged = new EventEmitter(); + + 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; + } +} diff --git a/src/index.html b/src/index.html index 404320b..fca5f1e 100644 --- a/src/index.html +++ b/src/index.html @@ -1,13 +1,19 @@ - + - - - ToyotaFormApp - - - - - - - + + + PDF Signature App + + + + + + + + + +