Files
itflow/js/signature_pad.js

180 lines
6.7 KiB
JavaScript

// The signing pad (includes/contract_signature_pad.php) - on the guest signing
// page, in the client portal and in the agent's Countersign window. Draw
// with a mouse, finger or pen, or type the name instead; as the form is sent
// the drawing goes into it as a PNG. A form with nothing drawn isn't sent.
// The ink is dark on a white pad in either theme - it is going on paper.
// The server checks it all again.
//
// Wrapped in an IIFE because a modal can be opened, closed and opened again
// in one page load, which re-runs this file. No inline styles or scripts:
// the client portal's Content-Security-Policy allows neither.
(function () {
function wire(pad) {
if (pad.hasAttribute('data-wired')) {
return;
}
pad.setAttribute('data-wired', '1');
const form = pad.closest('form');
const canvas = pad.querySelector('[data-signature-canvas]');
const hint = pad.querySelector('[data-signature-hint]');
const mode = pad.querySelector('[data-signature-mode]');
const data = pad.querySelector('[data-signature-data]');
const name = pad.querySelector('[data-signature-name]');
const typed = pad.querySelector('[data-signature-typed]');
const context = canvas.getContext('2d');
const strokes = [];
let stroke = null;
// The canvas matches its box, sharp on high-density screens; the strokes are kept to draw again after a resize
function size() {
const ratio = window.devicePixelRatio || 1;
const box = canvas.getBoundingClientRect();
if (!box.width) {
return;
}
canvas.width = Math.round(box.width * ratio);
canvas.height = Math.round(box.height * ratio);
context.setTransform(ratio, 0, 0, ratio, 0, 0);
redraw();
}
function redraw() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.lineWidth = 2.4;
context.lineCap = 'round';
context.lineJoin = 'round';
context.strokeStyle = '#1d2433';
context.fillStyle = '#1d2433';
strokes.forEach(function (points) {
if (points.length === 1) {
context.beginPath();
context.arc(points[0][0], points[0][1], 1.3, 0, Math.PI * 2);
context.fill();
return;
}
context.beginPath();
context.moveTo(points[0][0], points[0][1]);
for (let i = 1; i < points.length; i++) {
context.lineTo(points[i][0], points[i][1]);
}
context.stroke();
});
}
function point(event) {
const box = canvas.getBoundingClientRect();
return [event.clientX - box.left, event.clientY - box.top];
}
function setHint(text, error) {
hint.textContent = text;
hint.classList.toggle('is-error', Boolean(error));
hint.hidden = !text;
}
canvas.addEventListener('pointerdown', function (event) {
event.preventDefault();
canvas.setPointerCapture(event.pointerId);
stroke = [point(event)];
strokes.push(stroke);
setHint('', false);
redraw();
});
canvas.addEventListener('pointermove', function (event) {
if (!stroke) {
return;
}
event.preventDefault();
stroke.push(point(event));
redraw();
});
['pointerup', 'pointercancel', 'pointerleave'].forEach(function (type) {
canvas.addEventListener(type, function () {
stroke = null;
});
});
pad.querySelector('[data-signature-clear]').addEventListener('click', function () {
strokes.length = 0;
redraw();
setHint('Sign here with your mouse, finger or pen', false);
});
function drawTyped() {
const value = name ? name.value.trim() : '';
typed.textContent = value || 'Write your full name above';
typed.classList.toggle('is-empty', !value);
}
if (name) {
name.addEventListener('input', drawTyped);
}
pad.querySelectorAll('[data-signature-tab]').forEach(function (tab) {
tab.addEventListener('click', function () {
const which = tab.getAttribute('data-signature-tab');
mode.value = which;
pad.querySelectorAll('[data-signature-tab]').forEach(function (other) {
const on = other === tab;
other.classList.toggle('active', on);
other.setAttribute('aria-pressed', on ? 'true' : 'false');
});
pad.querySelectorAll('[data-signature-panel]').forEach(function (panel) {
panel.hidden = panel.getAttribute('data-signature-panel') !== which;
});
if (which === 'draw') {
size();
} else {
drawTyped();
}
});
});
if (form) {
form.addEventListener('submit', function (event) {
if (mode.value !== 'draw') {
data.value = '';
return;
}
const drawn = strokes.reduce(function (total, points) {
return total + points.length;
}, 0);
if (drawn < 6) {
event.preventDefault();
event.stopImmediatePropagation();
setHint('Draw your signature here first - or choose Type', true);
canvas.scrollIntoView({ block: 'center', behavior: 'smooth' });
return;
}
data.value = canvas.toDataURL('image/png');
}, true);
}
// A pad in a modal or a hidden tab has no size until it is shown
size();
if (window.ResizeObserver) {
let width = canvas.getBoundingClientRect().width;
new ResizeObserver(function () {
const now = canvas.getBoundingClientRect().width;
if (now && now !== width) {
width = now;
size();
}
}).observe(canvas);
} else {
window.addEventListener('resize', size);
}
drawTyped();
}
function wireAll() {
document.querySelectorAll('[data-signature-pad]').forEach(wire);
}
wireAll();
// Countersign opens in a modal loaded after the page
document.addEventListener('shown.bs.modal', wireAll);
})();