Stat Tracker

Monday, December 16, 2013

Capturing Signatures with HTML5 Canvas in Salesforce 1 Mobile

Recently Salesforce.com released Salesforce 1, their latest mobile application. Salesforce 1 releases a number of new features that enable developers to create mobile applications. One person I spoke with recently at Dreamforce wanted to know how they could capture signatures in the mobile application. With HTML5 Canvas, Visualforce, and a little JavaScript, you can easily roll your own lightweight signature capture functionality in Salesforce 1.

Here is a brief demonstration video:


So I have included the entire source code below, but a few items about the tricky parts.

1. You will need to setup JavaScript event listeners on the canvas for touchstart, touchmove, and touchend. That is what the canvas will execute when you touch and drag your finger on it.

2. You will need to use JavaScript Remoting to ensure that you properly pass the Canvas content into your Apex Controller so that it can save it. The canvas can be converted into a Base64 String with the Canvase.toDataURI() method. That is how you get the bytes from the Canvas into an Attachment in Salesforce.com.

These are illustrated in the sample code.

And here is the source code for the VF and Apex. If you put these into a Visualforce Tab, and make it enabled for Salesforce 1 Mobile, then you easily reuse this sample code.

jQuery: http://code.jquery.com/jquery-2.1.1.min.js

jQuery Mobile Resources (Download Links)
Version 1.3.2: http://jquerymobile.com/resources/download/jquery.mobile-1.3.2.zip

Latest Version 1.4.4: http://jquerymobile.com/resources/download/jquery.mobile-1.4.4.zip

Source Code:

Visualforce Page Code:

<apex:page controller="AnyObjectSignatureController" showheader="false" sidebar="false" standardStylesheets="false">
<script>var $j = jQuery.noConflict();</script>
<apex:stylesheet value="{!URLFOR($Resource.jquerymobile,'/jquerymobile/jquery.mobile-1.3.2.min.css')}"/>
<apex:includeScript value="{!URLFOR($Resource.jquery)}"  />
<apex:includeScript value="{!URLFOR($Resource.jquerymobile,'/jquerymobile/jquery.mobile-1.3.2.min.js')}"/>

<div data-role="page" id="signatureCaptureHome"> 
<div data-role="content">
<input id="accountNameId" type="text" name="accountName"/>
<input type="button" name="findAccountBtn" onclick="findAccounts();" value="Find Accounts"/>
<h1 id="recordSigId">Record Signature:</h1>
<canvas id="signatureCanvas" height="200px" width="300px"/>
<input id="saveSigButton" type="button" name="SigCap" onclick="saveSignature();" value="Capture Signature"></input>
</div> 
</div> 
<div data-role="page" id="signatureCaptureHome"> 
<div data-role="content">
<input id="accountNameId" type="text" name="accountName"/>
<input type="button" name="findAccountBtn" onclick="findAccounts();" value="Find Accounts"/>
</div> 
</div> 

<script>

    var canvas;
    var context;
    var drawingUtil;
    var isDrawing = false;
    var accountId = '';

function DrawingUtil() 
{
    isDrawing = false;
    canvas.addEventListener("touchstart",start,false);
    canvas.addEventListener("touchmove",draw,false);
    canvas.addEventListener("touchend",stop,false);
    context.strokeStyle = "#FFF";  
}

//Start Event for Signature Captuare on HTML5 Canvas
function start(event) 
{
    isDrawing = true;
    canvas = document.getElementById("signatureCanvas");
    context = canvas.getContext("2d");    
    context.strokeStyle = "rgba(155,0,0,0.5)";      
    context.beginPath();
     context.moveTo(event.touches[0].pageX - canvas.getBoundingClientRect().left,event.touches[0].pageY - canvas.getBoundingClientRect().top);
}

//Event while someone is drawing to caputre the path while they draw....
function draw(event) {
    event.preventDefault();
    if(isDrawing) {     
        context.lineTo(event.touches[0].pageX - canvas.getBoundingClientRect().left,event.touches[0].pageY - canvas.getBoundingClientRect().top);
        context.stroke();
    }
}


//Event when someone stops drawing their signature line
function stop(event) {
    if(isDrawing) {
        context.stroke();
        context.closePath();
        isDrawing = false;
    }
}

canvas = document.getElementById("signatureCanvas");
context = canvas.getContext("2d");
drawingUtil = new DrawingUtil(canvas);

function saveSignature()
{
var strDataURI = canvas.toDataURL();
    // alert(strDataURI);
    strDataURI = strDataURI.replace(/^data:image\/(png|jpg);base64,/, "");
//alert(strDataURI);
AnyObjectSignatureController.saveSignature(strDataURI,accountId,processResult);
}

function processResult(result)
{
alert(JSON.stringify(result));
}

function findAccounts()
{
var nameValue = document.getElementById("accountNameId").value;
AnyObjectSignatureController.findAccounts(nameValue, processSearchResult);

function processSearchResult(result)
{
$j = jQuery.noConflict();
//$j("#accountList").html("");
$j.each(result, function(i, record) {accountId = record.Id; $j("#recordSigId").html("Record Signature: " + record.Name);});
$j("#recordSigId").trigger("update");
//$j("#accountList").trigger("update");
//alert(JSON.stringify(result));
}


</script>

</apex:page>

Apex Controller:
global with sharing class AnyObjectSignatureController 
{
public AnyObjectSignatureController()
{
}
@RemoteAction
global static List<Account> findAccounts(String name)
{
name = '%' + name + '%';
List<Account> accounts = [Select Id, Name from Account where Name like :name];
return accounts;
}
@RemoteAction
global static String saveSignature(String signatureBody, String parentId) 
{
try
{
system.debug('Record Id == ' + parentId);
system.debug(signatureBody);
Attachment a = new Attachment();
a.ParentId = parentId;
a.Body = EncodingUtil.base64Decode(signatureBody);
a.ContentType = 'image/png';
a.Name = 'Signature Capture.png';
insert a;
return '{success:true, attachId:' + a.Id + '}';
}catch(Exception e)
{
return JSON.serialize(e);
}
return null;
}

}