Allgemein ·Erweiterung ·Fiori

Digitale Unterschrift als Control in SAPUI5

Ausgangssituation

In Zeiten der Digitalisierung und dem papierlosen Büro gehören digitale Unterschriften heutzutage zum modernen Unternehmensalltag. Verträge und Dokumente werden bereits an vielen Stellen nicht mehr auf Papier, sondern auf Tablets oder Handys unterzeichnet und direkt digital abgespeichert. So umgeht man unnötig analoge Papierarbeiten und man kann sich um die wichtigen Dinge im Alltag kümmern.

Wieso also nicht Verträge in modernen Fiori-Oberflächen unterzeichnen? SAP bietet standardmäßig diverse, schick aussehende UI5 Controls an, mit denen man Tabellen, Graphen oder Diagramme darstellen kann, also auch ein Control für digitale Unterschriften?
Derzeit leider nein. Also warum nicht selber ran und mit modernen HTML5 und JavaScript-Optionen ein eigenes Control bauen, das wir jedes mal einsetzen können, wenn wir eine digitale Signatur in unserer App benötigen.

(Wer sich das komplette Coding als Control kopieren möchte, kann auch gerne einfach an das Ende des Blogartikels springen und den Code kopieren.)

Das Ziel

Das Endergbnis soll in etwa wie im Folgenden aussehen. Wir möchten ein eigenständiges Control, mit folgenden Eigenschaften:

  • Attribute/Properties, die man in der XML setzen und verändern kann
  • Responsible, sowohl auf Desktop, als auch auf mobilen Endgeräten anwendbar
  • Signatur als Bild speichern können
  • Signaturbereich wieder löschen können

Eigene Control erstellen – der Grundbaustein, das Fundament

Wie man bestimmte Controls erweitert und auf bestehenden Controls aufbaut, wurde bereits in einem vorherigem Blog beschrieben.

In diesem Blog erstellen wir unser eigenes Control. Zunächst das Grundgerüst. Wir habe uns ein neues Projekt mittels “SAP UI5 Application”-Template erstellt, sodass das Grundgerüst vorhanden ist und wir eine leere App besitzen, in der wir die Digitale Signatur einfügen. In dieses Grundgerüst erweitern wir die Ordnerstruktur unter “webapp” um den Ordner “custom” und den Unterordner “controls”. Hier erstellen wir uns eine neue JavaScript-Datei, die den Namen unseres Controls hat, in meinem Fall “DigitalSignature.js”.

 

In dieses neue Control arbeiten wir nun das Coding ein, das letztlich unsere Signatur zeichnet und die Funktionalitäten bereitstellt.

Das Grundgerüst unseres individuellen Controls sieht wie folgt aus und ist letztendlich “einfach nur” eine Erweiterung eines allgemeinem SAPUI5 Controls. Wir erweitern das Control um unsere eigene Anpassungen, die in dem Ordner “custom/controls/” in der Datei “DigitalSignature” wiederfinden zu sind.

sap.ui.define(
	["sap/ui/core/Control"],
	function (Control) {
		return Control.extend("custom.controls.DigitalSignature", {
			metadata: {
				properties: {},
				aggregations: {}
			},
			renderer: function (oRm, oControl) {
				},
			onAfterRendering: function (oEvent) {
				//if I need to do any post render actions, it will happen here
				if (sap.ui.core.Control.prototype.onAfterRendering) {
					sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments); //run the super class's method first

				}
			}
		});
	});

Nun können wir unsere digitale Signatur bereits mit nur kleinen Anpassungen in unsere View einpflegen:

<mvc:View controllerName="de.acando.blog.DigitalSignatureBlog.controller.MainView" xmlns:html="http://www.w3.org/1999/xhtml"
	xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m" xmlns:customControl="de.acando.blog.DigitalSignatureBlog.custom.controls">
	<App id="idAppControl">
		<pages>
			<Page title="{i18n>title}">
				<content>
					<customControl:DigitalSignature id="digitalSignatureId"/>
				</content>
			</Page>
		</pages>
	</App>
</mvc:View>

 

Wenn man die App nun startet erkennt man jedoch relativ schnell, dass der Inhalt noch sehr leer ist. Dies liegt natürlich daran, dass wir in der Methode “renderer” unseres eigenen Controls noch keine Inhalte zeichnen.

Aus dem Nichts entsteht eine Box – Die Wände hochziehen

Die digitale Signatur erstellen wir mit dem HTML5 Element Canvas. Mit Canvas lassen sich Boxen, Linien, Kreise und Rechtecke in den verschiedensten Formen und Farben zeichnen und ist daher sehr gut geeignet für unsere Anwendung.

Zudem müssen wir uns Gedanken darüber machen, welche Eigenschaften des Controls der User letztlich verändern können soll. Wie wäre es mit den folgenden Eigenschaften:

  • Höhe des Bereichs für die Unterschrift – width
  • Breite des Bereichs für die Unterschrift – height
  • Füllfarbe des Bereichs für die Unterschrift – fillColor
  • Farbe der Unterschrift – signatureColor
  • Breite der Linie der Unterschrift – lineWidth
  • Form der Linie der Unterschrift – lineCap
  • Farbe des Rahmen – borderColor
  • Breite des Rahmen – borderSize
  • Stil/Form des Rahmen – borderStyle

Diese Eigenschaften können wir zu den Properties der Metadaten hinzufügen. Das Hinzufügen der Attribute zu den Properties hat in erster Linie zwei positive Nebeneffekte.

  1. Durch das Hinzufügen dieser Attribute zu den Properties werden automatisch die get- und set-Methoden der jeweiligen Attribute erstellt, ohne dass wir die Methoden codieren müssen.
  2. In der XML-View können wir die Attribute direkt dem Control zuweisen, ohne dass wir Attribute im Controller setzen müssen
metadata: {
	properties: {
		width: {
			type: "sap.ui.core.CSSSize",
			defaultValue: "auto"
		},
		height: {
			type: "sap.ui.core.CSSSize",
			defaultValue: "auto"
		},
		borderColor: {
			type: "sap.ui.core.CSSColor",
			defaultValue: "#000000"
		},
		borderSize: {
			type: "sap.ui.core.CSSSize",
			defaultValue: "1px"
		},
		borderStyle: {
			type: "string",
			defaultValue: "none" //none, hidden, dotted, dashed, solid, double, groove, ridge, inset, outset, initial, inherit
		},
		fillColor: {
			type: "sap.ui.core.CSSColor",
			defaultValue: "#FFFFFF"
		},
		signatureColor: {
			type: "sap.ui.core.CSSColor",
			defaultValue: "#000000"
		},
		lineWidth: {
			type: "float",
			defaultValue: 1.5
		},
		lineCap: {
			type: "string",
			defaultValue: "round" //round, butt, square
		}
	},
	aggregations: {}
},

Nun zur renderer-Funktion. Hier “zeichnen” wir nun das Canvas-Element, also die Fläche, in der wir letztlich unsere Unterschrift zeichnen möchten.
In der dritten Zeile fügen wir die Attribute ein, die unser Control durch die xml-View mitbekommt, bspw. die ID des Controls.

renderer: function (oRm, oControl) {
	oRm.write("<canvas class='signature-pad' ");
	oRm.writeControlData(oControl);
	oRm.addStyle("width", oControl.getWidth());
	oRm.addStyle("height", oControl.getHeight());
	oRm.addStyle("border", oControl.getBorderSize() + " " + oControl.getBorderStyle() + " " + oControl.getBorderColor());
	oRm.writeStyles();
	oRm.write("/>");
},

Ein erstes Ergebnis? Wenn wir die App starten sehen wir leider noch nichts. Das bedeutet jedoch nicht, dass wir einen Fehler verursacht haben. Ein Blick mit den Browser Entwicklungstools zeigt, dass das Feld vorhanden ist.

Im Endeffekt haben wir unserem Bereich lediglich noch keine Farben gegeben. Dies machen wir nun in unserer onAfterRendering-Methode, da wir auf unser HTML5-Element zugreifen wollen, dass hierfür schon “gerendert” sein muss. Wir lagern die Methode aus, um diese wieder benutzbar machen zu können und um unseren Code sauber zu halten.

onAfterRendering: function (oEvent) {
	//if I need to do any post render actions, it will happen here
	if (sap.ui.core.Control.prototype.onAfterRendering) {
		sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments); //run the super class's method first
		this._drawSignatureArea(this);
	}
},

_drawSignatureArea: function (oControl) {
	var canvas = $("#" + oControl.getId())[0]; //This get´s our canvas-element by jQuery
	var context = canvas.getContext("2d"); //Getitng the context of our canvas-area
        canvas.width = canvas.clientWidth;
        canvas.height = canvas.clientHeight;
	context.fillStyle = oControl.getFillColor(); //Setting the FillColor/FillStyle
	context.strokeStyle = oControl.getSignatureColor(); //Setting the SignaturColor/StrokeStyle
	context.lineWidth = oControl.getLineWidth(); //Setting the SignatureLineWidth
	context.lineCap = oControl.getLineCap(); //Setting the LineCap of the Signature
	context.fillRect(0, 0, canvas.width, canvas.height);
},

 

Siehe da, wir haben ein erstes Ergebnis. Wir sehen nun unsere Fläche und können diese mit verschiedensten Attributen in der XML-View Einstellungen ändern. Im Folgenden zwei Beispiele:

  • Standard
    <mvc:View controllerName="de.acando.blog.DigitalSignatureBlog.controller.MainView" xmlns:html="http://www.w3.org/1999/xhtml"
    	xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m" xmlns:customControl="de.acando.blog.DigitalSignatureBlog.custom.controls">
    	<App id="idAppControl">
    		<pages>
    			<Page title="{i18n>title}">
    				<content>
    					<customControl:DigitalSignature id="digitalSignatureId"/>
    				</content>
    			</Page>
    		</pages>
    	</App>
    </mvc:View>
    

  • Orange Hintergrundfläche, dicker lilaner gepunkteter Rahmen und Weite auf 7 0%
    <mvc:View controllerName="de.acando.blog.DigitalSignatureBlog.controller.MainView" xmlns:html="http://www.w3.org/1999/xhtml"
    	xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m" xmlns:customControl="de.acando.blog.DigitalSignatureBlog.custom.controls">
    	<App id="idAppControl">
    		<pages>
    			<Page title="{i18n>title}">
    				<content>
    					<customControl:DigitalSignature id="digitalSignatureId" 
    					width="70%" borderColor="#eb1af2" borderSize="4px" borderStyle="dotted"
    					fillColor="#ffb630"/>
    				</content>
    			</Page>
    		</pages>
    	</App>
    </mvc:View>
    

 

In die Box die Signatur – Das Dach setzen

Wir haben die Fläche und das Grundgerüst, jetzt fehlt noch das Eigentliche, die Signatur.
Hierfür implementieren wir klassische HTML5-EventListener, mit denen wir darauf “warten”, dass entweder die Maus oder ein Finger unsere Signaturfläche berührt und darauf zeichnen möchte.

Hierfür implementieren wir uns eine Methode, die das Zeichnen der Signatur möglich macht.

_makeAreaDrawable: function (oControl) {
	var canvas = $("#" + oControl.getId())[0]; //This get´s our canvas-element by jQuery
	var context = canvas.getContext("2d"); //Getitng the context of our canvas-area
	var pixels = [];
	var xyLast = {};
	var xyAddLast = {};
	var calculate = false;

	function getCoords(oEvent) {
		var x, y;
		if (oEvent.changedTouches && oEvent.changedTouches[0]) {
			var offsety = canvas.offsetTop || 0;
			var offsetx = canvas.offsetLeft || 0;
			x = oEvent.changedTouches[0].pageX - offsetx;
			y = oEvent.changedTouches[0].pageY - offsety;
		} else if (oEvent.layerX || oEvent.layerX === 0) {
			x = oEvent.layerX;
			y = oEvent.layerY;
		} else if (oEvent.offsetX || oEvent.offsetX === 0) {
			x = oEvent.offsetX;
			y = oEvent.offsetY;
		}
		return {
			x: x,
			y: y
		};
	}
	/**
	 * Eventhandler for when the mouse moves on the specific area without pushing
	 * */
	function onMouseMove(oEvent) {
		oEvent.preventDefault();
		oEvent.stopPropagation();
		var xy = getCoords(oEvent);
		var xyAdd = {
			x: (xyLast.x + xy.x) / 2,
			y: (xyLast.y + xy.y) / 2
		};
		if (calculate) {
			var xLast = (xyAddLast.x + xyLast.x + xyAdd.x) / 3;
			var yLast = (xyAddLast.y + xyLast.y + xyAdd.y) / 3;
			pixels.push(xLast, yLast);
		} else {
			calculate = true;
		}
		context.quadraticCurveTo(xyLast.x, xyLast.y, xyAdd.x, xyAdd.y);
		pixels.push(xyAdd.x, xyAdd.y);
		context.stroke();
		context.beginPath();
		context.moveTo(xyAdd.x, xyAdd.y);
		xyAddLast = xyAdd;
		xyLast = xy;
	}

	/**
	 * Eventhandler for when the mouse moves is pressed on the specific area
	 * */
	function onMouseDown(oEvent) {
		oEvent.preventDefault();
		oEvent.stopPropagation();
		canvas.addEventListener("mouseup", onMouseUp, false);
		canvas.addEventListener("mousemove", onMouseMove, false);
		canvas.addEventListener("touchend", onMouseUp, false);
		canvas.addEventListener("touchmove", onMouseMove, false);
		$("body").on("mouseup", onMouseUp, false);
		$("body").on("touchend", onMouseUp, false);
		var xy = getCoords(oEvent);
		context.beginPath();
		pixels.push("moveStart");
		context.moveTo(xy.x, xy.y);
		pixels.push(xy.x, xy.y);
		xyLast = xy;
	}

	/**
	 * removes the eventhandlers
	 * */
	function removeEventListeners() {
		canvas.removeEventListener("mousemove", onMouseMove, false);
		canvas.removeEventListener("mouseup", onMouseUp, false);
		canvas.removeEventListener("touchmove", onMouseMove, false);
		canvas.removeEventListener("touchend", onMouseUp, false);
		$("body").off("mouseup", onMouseUp, false);
		$("body").off("touchend", onMouseUp, false);
	}

	/**
	 * Eventhandler for when the mouse stops pushing on the specific area
	 * */
	function onMouseUp(oEvent) {
		removeEventListeners();
		context.stroke();
		pixels.push("e");
		calculate = false;
	}
	canvas.addEventListener("touchstart", onMouseDown, false);
	canvas.addEventListener("mousedown", onMouseDown, false);
}

Den Aufruf dieser Methode ergänzen wir in unserem onAfterRendering Eventhandler:

onAfterRendering: function (oEvent) {
		if (sap.ui.core.Control.prototype.onAfterRendering) {
			sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments); //run the super class's method first
			this._drawSignatureArea(this);
			this._makeAreaDrawable(this);
		}
	},

Das Ergebnis sieht wie folgt aus, wenn wir die App neu starten und in unseren Bereich mit der Maus zeichnen:

Voilà, unser Control lässt uns zeichnen.

Wer beim Control mit Prozentzahlen gearbeitet hat, um die Weite und die Höhe zu setzen, der hat sicherlich bemerkt, dass sich die Signatur seltsam beim Zeichnen verhält. Entweder ist die Linie zu dick oder zu dünn oder die Signatur ist versetzt. Dies liegt daran, dass das canvas-Element “zu früh” rendert und nicht nachrendert, nachdem die komplette View gerendert wurde. In unserer onAfterRendering-Methode müssen wir das rerendern noch dem

onAfterRendering: function (oEvent) {
if (sap.ui.core.Control.prototype.onAfterRendering) {
		sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments); //run the super class's method first
		this._drawSignatureArea(this);
		this._makeAreaDrawable(this);
		var that = this; //make the control resizable and redraw when something changed
		sap.ui.core.ResizeHandler.register(this, function () {
			that._drawSignatureArea(that);
		});
	}
},
Die Signatur löschen oder als Bild bekommen/speichern – Das Haus wohnlich machen

Nun haben wir unsere Signatur. Wie schön wäre es, wenn unser Control standardmäßig eine Methode implementiert, mit der man die Signatur löschen, oder als Bild bekommen kann.
Hierfür stellen wir nun öffentliche Methoden im Control bereit, die die Funktionalitäten bieten:

Zum Löschen:

clearArea: function () {
	this._drawSignatureArea(this);
},

Um die Signatur als Bild zu bekommen (einmal als JPEG und einmal als PNG):

getSignatureAsJpeg: function () {
	return this._getCanvasAsPicture("image/jpeg");
},

getSignatureAsPng: function () {
	return this._getCanvasAsPicture("image/png");
},

_getCanvasAsPicture: function (sMimetype) {
	var canvas = $("#" + this.getId())[0];
	var image = canvas.toDataURL(sMimetype);
	return image;
},
Das komplette Coding – Einzugsbereit
sap.ui.define(
	["sap/ui/core/Control"],
	function (Control) {
		return Control.extend("custom.controls.DigitalSignature", {
			metadata: {
				properties: {
					width: {
						type: "sap.ui.core.CSSSize",
						defaultValue: "auto"
					},
					height: {
						type: "sap.ui.core.CSSSize",
						defaultValue: "auto"
					},
					borderColor: {
						type: "sap.ui.core.CSSColor",
						defaultValue: "#000000"
					},
					borderSize: {
						type: "sap.ui.core.CSSSize",
						defaultValue: "1px"
					},
					borderStyle: {
						type: "string",
						defaultValue: "none" //none, hidden, dotted, dashed, solid, double, groove, ridge, inset, outset, initial, inherit
					},
					fillColor: {
						type: "sap.ui.core.CSSColor",
						defaultValue: "#FFFFFF"
					},
					signatureColor: {
						type: "sap.ui.core.CSSColor",
						defaultValue: "#000000"
					},
					lineWidth: {
						type: "float",
						defaultValue: 1.5
					},
					lineCap: {
						type: "string",
						defaultValue: "round" //round, butt, square
					}
				},
				aggregations: {}
			},
                        renderer: function (oRm, oControl) {
	                  oRm.write("<canvas class='signature-pad' ");
	                  oRm.writeControlData(oControl);
	                  oRm.addStyle("width", oControl.getWidth());
	                  oRm.addStyle("height", oControl.getHeight());
	                  oRm.addStyle("border", oControl.getBorderSize() + " " + oControl.getBorderStyle() + " " + oControl.getBorderColor());
	                  oRm.writeStyles();
	                  oRm.write("/>");
                        },

			onAfterRendering: function (oEvent) {
				//if I need to do any post render actions, it will happen here
				if (sap.ui.core.Control.prototype.onAfterRendering) {
					sap.ui.core.Control.prototype.onAfterRendering.apply(this, arguments); //run the super class's method first
					this._drawSignatureArea(this);
					this._makeAreaDrawable(this);
					var that = this;										//make the control resizable and redraw when something changed
					sap.ui.core.ResizeHandler.register(this, function () {
						that._drawSignatureArea(that);
					});
				}
			},
			clearArea: function () {
				this._drawSignatureArea(this);
			},

			getSignatureAsJpeg: function () {
				return this._getCanvasAsPicture("image/jpeg");
			},

			getSignatureAsPng: function () {
				return this._getCanvasAsPicture("image/png");
			},

			_getCanvasAsPicture: function (sMimetype) {
				var canvas = $("#" + this.getId())[0];
				var image = canvas.toDataURL(sMimetype);
				return image;
			},

			_drawSignatureArea: function (oControl) {
				var canvas = $("#" + oControl.getId())[0]; //This get´s our canvas-element by jQuery
				var context = canvas.getContext("2d"); //Getitng the context of our canvas-area
				canvas.width = canvas.clientWidth;
				canvas.height = canvas.clientHeight;
				context.fillStyle = oControl.getFillColor(); //Setting the FillColor/FillStyle
				context.strokeStyle = oControl.getSignatureColor(); //Setting the SignaturColor/StrokeStyle
				context.lineWidth = oControl.getLineWidth(); //Setting the SignatureLineWidth
				context.lineCap = oControl.getLineCap(); //Setting the LineCap of the Signature
				context.fillRect(0, 0, canvas.width, canvas.height);
			},

			_makeAreaDrawable: function (oControl) {
				var canvas = $("#" + oControl.getId())[0]; //This get´s our canvas-element by jQuery
				var context = canvas.getContext("2d"); //Getitng the context of our canvas-area
				var pixels = [];
				var xyLast = {};
				var xyAddLast = {};
				var calculate = false;

				function getCoords(oEvent) {
					var x, y;
					if (oEvent.changedTouches && oEvent.changedTouches[0]) {
						var offsety = canvas.offsetTop || 0;
						var offsetx = canvas.offsetLeft || 0;
						x = oEvent.changedTouches[0].pageX - offsetx;
						y = oEvent.changedTouches[0].pageY - offsety;
					} else if (oEvent.layerX || oEvent.layerX === 0) {
						x = oEvent.layerX;
						y = oEvent.layerY;
					} else if (oEvent.offsetX || oEvent.offsetX === 0) {
						x = oEvent.offsetX;
						y = oEvent.offsetY;
					}
					return {
						x: x,
						y: y
					};
				}
				/**
				 * Eventhandler for when the mouse moves on the specific area without pushing
				 * */
				function onMouseMove(oEvent) {
					oEvent.preventDefault();
					oEvent.stopPropagation();
					var xy = getCoords(oEvent);
					xyAdd = xy;
					var xyAdd = {
						x: (xyLast.x + xy.x) / 2,
						y: (xyLast.y + xy.y) / 2
					};
					if (calculate) {
						var xLast = (xyAddLast.x + xyLast.x + xyAdd.x) / 3;
						var yLast = (xyAddLast.y + xyLast.y + xyAdd.y) / 3;
						pixels.push(xLast, yLast);
					} else {
						calculate = true;
					}
					context.quadraticCurveTo(xyLast.x, xyLast.y, xyAdd.x, xyAdd.y);
					pixels.push(xyAdd.x, xyAdd.y);
					context.stroke();
					context.beginPath();
					context.moveTo(xyAdd.x, xyAdd.y);
					xyAddLast = xyAdd;
					xyLast = xy;
				}

				/**
				 * Eventhandler for when the mouse moves is pressed on the specific area
				 * */
				function onMouseDown(oEvent) {
					oEvent.preventDefault();
					oEvent.stopPropagation();
					canvas.addEventListener("mouseup", onMouseUp, false);
					canvas.addEventListener("mousemove", onMouseMove, false);
					canvas.addEventListener("touchend", onMouseUp, false);
					canvas.addEventListener("touchmove", onMouseMove, false);
					$("body").on("mouseup", onMouseUp, false);
					$("body").on("touchend", onMouseUp, false);
					var xy = getCoords(oEvent);
					context.beginPath();
					pixels.push("moveStart");
					context.moveTo(xy.x, xy.y);
					pixels.push(xy.x, xy.y);
					xyLast = xy;
				}

				/**
				 * removes the eventhandlers
				 * */
				function removeEventListeners() {
					canvas.removeEventListener("mousemove", onMouseMove, false);
					canvas.removeEventListener("mouseup", onMouseUp, false);
					canvas.removeEventListener("touchmove", onMouseMove, false);
					canvas.removeEventListener("touchend", onMouseUp, false);
					$("body").off("mouseup", onMouseUp, false);
					$("body").off("touchend", onMouseUp, false);
				}

				/**
				 * Eventhandler for when the mouse stops pushing on the specific area
				 * */
				function onMouseUp(oEvent) {
					removeEventListeners();
					context.stroke();
					pixels.push("e");
					calculate = false;
				}
				canvas.addEventListener("touchstart", onMouseDown, false);
				canvas.addEventListener("mousedown", onMouseDown, false);
			}
		});
	});
Zusammenfassung – Was haben wir gelernt?

Wir haben unser eigenes Control erstellt, das wir nun jederzeit in andere Views einsetzen können und das auch direkt Eigenschaften und Methoden bereitstellt. Die Eigenschaften und Methoden lassen sich nach belieben erweitern und an spezifische Bedürfnisse anpassen.

Bis zum nächsten Blog! Möchtet ihr noch mehr über eigene Controls erfahren? Dieser Blog könnte hilfreich sein!

Schon gesehen?
Arbeiten als SAPUI5-Entwickler bei Acando!

Schreibe einen Kommentar