mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
180 lines
6.7 KiB
JavaScript
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);
|
|
})();
|