3 * @link http://www.open-emr.org
4 * @author Jerry Padgett <sjpadgett@gmail.com>
5 * @copyright Copyright (c) 2016-2022 Jerry Padgett <sjpadgett@gmail.com>
6 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
9 const signhere = "data:image/svg+xml,%3C%3Fxml version='1.0' standalone='no'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 20010904//EN' 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd'%3E%3Csvg version='1.0' xmlns='http://www.w3.org/2000/svg' width='852.000000pt' height='265.000000pt' viewBox='0 0 852.000000 265.000000' preserveAspectRatio='xMidYMid meet'%3E%3Cg transform='translate(0.000000,265.000000) scale(0.100000,-0.100000)'%0Afill='%23000000' stroke='none'%3E%3Cpath d='M390 1534 c-19 -14 -67 -57 -107 -96 l-71 -71 -31 34 c-16 19 -38 49%0A-48 67 -18 32 -53 44 -53 18 0 -17 47 -111 72 -144 17 -23 17 -25 -21 -75 -46%0A-62 -85 -143 -77 -163 3 -8 14 -14 25 -14 15 0 27 15 45 53 23 52 71 117 85%0A117 4 0 15 -15 25 -32 11 -18 34 -50 52 -70 18 -20 40 -51 49 -67 9 -19 23%0A-31 35 -31 38 0 18 71 -34 119 -33 31 -89 117 -84 130 1 6 49 57 106 115 56%0A57 102 111 102 120 0 23 -29 18 -70 -10z'/%3E%3C/g%3E%3C/svg%3E";
15 if (typeof isPortal === 'undefined') {
19 if (typeof ptName === 'undefined') {
22 if (typeof webRoot === 'undefined' && typeof top.webroot_url !== 'undefined') {
23 var webRoot = top.webroot_url;
25 if (typeof isModule === 'undefined') {
29 if (typeof cpid === 'undefined') {
33 if (typeof cuser === 'undefined') {
37 function signerAlertMsg(message, timer = 5000, type = 'danger', size = '') {
38 $('#signerAlertBox').remove();
39 size = (size == 'lg') ? 'left:25%;width:50%;' : 'left:35%;width:30%;';
40 let style = "position:fixed;top:25%;" + size + " bottom:0;z-index:1020;z-index:5000";
41 $("body").prepend("<div class='container text-center' id='signerAlertBox' style='" + style + "'></div>");
42 let mHtml = '<div id="alertMessage" class="alert alert-' + type + ' alert-dismissable">' +
43 '<button type="button" class="close btn btn-link btn-cancel" data-dismiss="alert" aria-hidden="true">×</button>' +
44 '<h5 class="alert-heading text-center">Alert!</h5><hr>' +
45 '<p>' + message + '</p>' +
47 $('#signerAlertBox').append(mHtml);
48 $('#alertMessage').on('closed.bs.alert', function () {
49 clearTimeout(AlertMsg);
50 $('#signerAlertBox').remove();
52 let AlertMsg = setTimeout(function () {
53 $('#alertMessage').fadeOut(800, function () {
54 $('#signerAlertBox').remove();
59 function getSignature(othis, isInit = false, returnSignature = false) {
60 return new Promise(resolve => {
61 let signer, signerType = "";
64 if ($(othis).attr('src') != signhere && !isInit) {
65 $(othis).attr('src', signhere);
68 if (typeof webRoot !== 'undefined' && webRoot !== null) {
69 libUrl = webRoot + '/portal/';
71 libUrl = top.webroot_url ? (top.webroot_url + '/portal/') : "./";
74 if (typeof cpid === 'undefined' && typeof cuser === 'undefined') {
75 cpid = $(othis).data('pid');
76 cuser = $(othis).data('user');
78 let otype = $(othis).attr('data-type');
80 if (typeof otype === 'undefined' || otype === null) {
81 otype = $(othis).data('type');
83 if (otype == 'admin-signature') {
84 signer = adminName ? adminName : cuser;
85 signerType = "admin-signature";
86 $("#isAdmin").prop('checked', true);
88 } else if (otype == 'witness-signature') {
90 signerType = "witness-signature";
91 $("#isAdmin").prop('checked', false);
96 signerType = "patient-signature";
97 $("#isAdmin").prop('checked', false);
108 let url = libUrl + "sign/lib/show-signature.php";
110 credentials: 'include',
112 body: JSON.stringify(params),
114 'Accept': 'application/json, text/plain, */*',
115 'Content-Type': 'application/json'
117 }).then(signature => signature.json()).then(signature => {
118 if (returnSignature === true) {
121 placeSignature(signature, othis).then(function (r) {
125 }).catch(error => signerAlertMsg(error));
130 function placeSignature(responseData, el) {
131 return new Promise(resolve => {
132 if (responseData.message === "error") {
133 $(el).attr('src', "");
134 $(el).attr('alt', "No Signature on File");
135 signerAlertMsg('Error Patient and or User Id missing');
137 } else if (responseData.message === "insert error") {
138 $(el).attr('src', "");
139 $(el).attr('alt', "No Signature on File");
140 signerAlertMsg('Error adding signature');
142 } else if (responseData.message === "waiting" && $(el).attr('data-type') === 'patient-signature') {
143 $(el).attr('src', "");
144 $(el).attr('alt', "No Signature on File");
145 $("#isAdmin").attr('checked', false);
147 } else if (responseData.message === "waiting" && $(el).attr('data-type') === 'admin-signature') {
148 $(el).attr('src', "");
149 $(el).attr('alt', "No Signature on File");
150 $("#isAdmin").attr('checked', true);
152 } else if (responseData.message === "waiting") {
153 $(el).attr('src', "");
154 $(el).attr('alt', "No Signature on File");
158 i.onload = function () {
159 $(el).attr('src', i.src)
160 resolve('done'); // display image
162 if (!isDataURL(responseData)) {
163 alert("Invalid Signature.");
167 i.src = responseData; // load image
171 function archiveSignature(signImage = '', edata = '') {
172 let libUrl, signer, signerType = "";
176 if (typeof webRoot !== 'undefined' && webRoot !== null) {
177 libUrl = webRoot + '/portal/';
184 pid: edata.data.cpid,
185 user: edata.data.cuser,
187 signer: edata.data.signer,
188 type: edata.data.type,
192 if ($("#isAdmin").is(':checked') === false) {
194 signer = ptName ? ptName : cuser;
195 signerType = "patient-signature";
198 signer = adminName ? adminName : cuser;
199 signerType = "admin-signature";
210 let url = libUrl + "sign/lib/save-signature.php";
212 credentials: 'include',
214 body: JSON.stringify(data),
216 'Content-Type': 'application/json',
217 'Connection': 'close'
219 }).then(response => response.json()).then(function (response) {
220 $("#openSignModal").modal("hide");
222 ).catch(error => signerAlertMsg(error));
227 function isDataURL(dataUrl = '') {
228 return !!dataUrl.match(isDataURL.regex);
231 isDataURL.regex = /^\s*data:([a-z]+\/[a-z]+(;[a-z\-]+=[a-z\-]+)?)?(;base64)?,[a-z0-9!$&',()*+;=\-._~:@\/?%\s]*\s*$/i;
233 // call if need to bind pen clicks after a dynamic template load. ie templates.
236 // fetch modal template and append to body.
239 let url = top.webroot_url ? top.webroot_url : webRoot;
240 url += "/portal/sign/assets/signer_modal.php?isPortal=" + encodeURIComponent(isPortal);
242 credentials: 'include'
243 }).then(jsonTemplate => jsonTemplate.json()).then(jsonTemplate => {
244 $("body").append(jsonTemplate);
245 }).then(function () {
247 }).catch((error) => alert("Modal Template Fetch:" + error));
250 function initSignerApi() {
251 // ya think there'd be more!
252 function callModal(e) {
254 cuser = e.data.cuser;
255 let type = e.data.type;
256 let signerName = e.data.signerName || '';
258 $('#openSignModal #name').val(signerName);
259 $('#openSignModal #labelName').html(" " + msgSignator + ": <b>" + signerName + "</b>");
260 $('#openSignModal #pid').val(cpid);
261 $('#openSignModal #user').val(cuser);
262 $('#openSignModal #signatureModal').data('type', type);
263 if (type === 'admin-signature' && isPortal) {
264 signerAlertMsg(xl('Signer Pad not available for this signature type!', 2000));
267 if (type === 'admin-signature') {
268 adminName = signerName;
269 $("#isAdmin").prop('checked', true);
273 $("#isAdmin").prop('checked', false);
276 $("#openSignModal").modal('show');
279 function doConfirm(result) {
280 placeSignature(result.data.signature, $lastEl);
283 // global binding for signer form/template tag clicks.
284 // gets called after a new/edited template gets loaded.
285 // mostly used with Patient Documents list however,
286 // existing portal workflow is legacy and will deprecate here soon.
287 bindFetch = function () {
288 $("img[data-action=fetch_signature]").on("click", function (e) {
292 let pid = $lastEl.data('pid');
293 let user = $lastEl.data('user');
294 let type = $lastEl.data('type');
295 $('#openSignModal #signatureModal').data('type', type);
297 let url = webRoot + "/portal/sign/lib/show-signature.php";
304 if (type === "admin-signature" && isPortal) {
305 // don't allow patient to change user signature.
307 } else if (isPortal && type !== "witness-signature") {
311 $lastEl.attr('src', signhere);
314 credentials: 'include',
316 body: JSON.stringify({
324 'Accept': 'application/json, text/plain, */*',
325 'Content-Type': 'application/json'
327 }).then(response => response.json()).then(function (response) {
329 if (type === "admin-signature") {
330 signerName = response.userName;
331 adminName = signerName;
333 signerName = response.ptName;
335 } // response.signature is available if needed in future.
341 signerName: signerName
344 }).catch(error => signerAlertMsg(error));
348 // default all signatures to icon regardless of new or edit..
349 $(".signature").each(function (index, value) {
350 if (!$(this).attr('src'))
351 $(this).attr('src', signhere);
356 // initial binds for form pen clicks
359 //-------------------- Continue loading seq with init of modal buttons and various bindings --------------------//
360 $(function (global) {
361 var wrapper = document.getElementById("openSignModal");
362 var canvasOptions = {
365 penColor: 'rgb(0, 0, 255)',
367 var openPatientButton = document.querySelector("[data-type=patient-signature]");
368 var openAdminButton = document.querySelector("[data-type=admin-signature]");
369 var placeSignatureButton = wrapper.querySelector("[data-action=place]");
370 var showSignature = wrapper.querySelector("[data-action=show]");
371 var saveSignature = wrapper.querySelector("[data-action=save_signature]");
372 var clearButton = wrapper.querySelector("[data-action=clear]");
373 var canvas = wrapper.querySelector("canvas");
376 // this offsets signature image to center on element somewhat
377 // on any form (css) box height:70px length:auto center at 20px.
379 let els = this.querySelectorAll("img[data-action=fetch_signature]");
381 for (i = 0; i < els.length; i++) {
382 els[i].style.top = (els[i].offsetTop - 20) + 'px';
383 els[i].setAttribute("data-offset", true);
387 $("#openSignModal .close").on("click", function (e) {
388 signaturePad.clear();
391 if (typeof placeSignatureButton === 'undefined' || !placeSignatureButton) {
392 placeSignatureButton = wrapper.querySelector("[data-action=place]");
394 $("#openSignModal").on('show.bs.modal', function (e) {
395 let type = $('#openSignModal #signatureModal').data('type');
396 if (type === 'admin-signature' && isPortal) {
397 signerAlertMsg('Signer Pad not available for this signature type!', 2000);
400 if (type === 'witness-signature') {
401 placeSignatureButton.classList.add('d-none');
403 placeSignatureButton.classList.remove('d-none');
406 // for our dynamically added modal
407 $("#openSignModal").on('shown.bs.modal', function (e) {
408 let type = $('#openSignModal #signatureModal').data('type');
410 if (type === "admin-signature") {
411 $("#isAdmin").prop('checked', true);
412 placeSignatureButton.setAttribute("data-type", type);
415 $("#isAdmin").prop('checked', false);
416 placeSignatureButton.setAttribute("data-type", type);
419 $('#signatureModal').data('type', type);
421 let showElement = document.getElementById('signatureModal');
422 $('#signatureModal').attr('src', signhere);
423 $("#openSignModal").modal({backdrop: false});
428 'padding-right': '0px'
430 $('body').bind('selectstart', function () {
433 $(this).modal('handleUpdate');
434 }).on('shown.bs.modal', function (e) { // yes two shown events
435 signaturePad = new SignaturePad(canvas, canvasOptions);
437 }).on('hide.bs.modal', function () {
438 if ((typeof $lastEl !== 'undefined' || !$lastEl) && typeof event === "undefined") {
439 if (!signaturePad.isEmpty()) {
440 let dataURL = signaturePad.toDataURL();
441 placeSignature(dataURL, $lastEl);
444 }).on('hidden.bs.modal', function () {
446 'overflow': 'inherit'
448 $('body').unbind('selectstart');
451 clearButton.addEventListener("click", function (event) {
452 signaturePad.clear();
455 saveSignature.addEventListener("click", function (event) {
456 if (signaturePad.isEmpty()) {
457 signerAlertMsg(msgNeedSign, 3000);
460 let signerName, type = '';
461 type = $('#signatureModal').data('type');
462 if ($("#isAdmin").is(':checked') === true || type === 'admin-signature') {
465 if (type === 'witness-signature') {
466 let dataURL = signaturePad.toDataURL();
472 signer: 'Witness Signature'
474 archiveSignature(encodeURIComponent(dataURL), e);
477 webRoot = webRoot ? webRoot : top.webroot_url;
478 let url = webRoot + "/portal/sign/lib/show-signature.php";
480 credentials: 'include',
482 body: JSON.stringify({
490 'Accept': 'application/json, text/plain, */*',
491 'Content-Type': 'application/json'
493 }).then(response => response.json()).then(function (response) {
494 signerName = ptName = response.ptName;
496 signerName = adminName = response.userName;
498 let dataURL = signaturePad.toDataURL();
506 archiveSignature(encodeURIComponent(dataURL), e);
510 placeSignatureButton.addEventListener("click", function (event) {
511 let thisElement = $(this);
512 getSignature(thisElement, true).then(r => {
513 let imgurl = thisElement.attr('src');
514 signaturePad.fromDataURL(imgurl).then(r => {
520 if (showSignature !== null)
521 showSignature.addEventListener("click", function (event) { // for modal view
522 let showElement = document.getElementById('signatureModal');
523 getSignature(showElement);
526 function resizeCanvas() {
527 let ratio = Math.max(window.devicePixelRatio || 1, 1);
528 canvas.width = canvas.offsetWidth * ratio;
529 canvas.height = canvas.offsetHeight * ratio;
530 canvas.getContext("2d").scale(ratio, ratio);
533 // this is nifty JS download. leaving for future.
534 // plus someone may come across and find useful.
535 function download(dataURL, filename) {
536 let blob = dataURLToBlob(dataURL);
537 let url = window.URL.createObjectURL(blob);
538 let a = document.createElement("a");
539 a.style = "display: none";
541 a.download = filename;
542 document.body.appendChild(a);
544 window.URL.revokeObjectURL(url);
547 function dataURLToBlob(dataURL) {
548 // Code taken from https://github.com/ebidel/filer.js
549 let parts = dataURL.split(';base64,');
550 let contentType = parts[0].split(":")[1];
551 let raw = window.atob(parts[1]);
552 let rawLength = raw.length;
553 let uInt8Array = new Uint8Array(rawLength);
555 for (var i = 0; i < rawLength; ++i) {
556 uInt8Array[i] = raw.charCodeAt(i);
559 return new Blob([uInt8Array], {type: contentType});
562 // TODO create method to remove line on canvas save
563 function drawSignatureLine() {
564 let context = canvas.getContext('2d');
565 context.lineWidth = .4;
566 context.strokeStyle = '#333';
568 context.moveTo(0, 200);
569 context.lineTo(900, 200);
573 // resize event and initial resize
574 window.onresize = function () {