Fix kiosk signature pad: host locally, fix canvas resize timing

- Download signature_pad 4.1.7 to wwwroot/lib/signature-pad/ to eliminate
  CDN SRI hash failures and network dependencies on the tablet
- Wrap resizeCanvas in requestAnimationFrame so offsetWidth is non-zero
  when measured (browser layout pass must complete first)
- Add guard for SignaturePad not defined (shows user-visible error instead
  of silent JS crash)
- Add scrollIntoView on signature validation error for better tablet UX

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 21:14:49 -04:00
parent 0c8723ef84
commit 4e9c9d321a
3 changed files with 56 additions and 39 deletions
@@ -90,9 +90,7 @@
@if (isInPerson) @if (isInPerson)
{ {
@section Scripts { @section Scripts {
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js" <script src="~/lib/signature-pad/signature_pad.umd.min.js"></script>
integrity="sha384-bQMMRVcRi5vEIBLKnB4FY7tBOA9k/Qvd/9zSWMNO4h0zfB2qLj4DV2R/JyPAbF3"
crossorigin="anonymous"></script>
<script src="~/js/kiosk-terms.js"></script> <script src="~/js/kiosk-terms.js"></script>
} }
} }
@@ -3,21 +3,34 @@
(function () { (function () {
// ── Signature pad (InPerson sessions only) ───────────────────────────────── // ── Signature pad (InPerson sessions only) ─────────────────────────────────
const canvas = document.getElementById("signatureCanvas"); const canvas = document.getElementById("signatureCanvas");
if (canvas) { if (!canvas) return;
if (typeof SignaturePad === "undefined") {
console.error("signature_pad failed to load — signature capture unavailable");
canvas.parentElement.insertAdjacentHTML("beforeend",
'<p class="text-danger small mt-1">Signature pad failed to load. Please refresh the page.</p>');
return;
}
const pad = new SignaturePad(canvas, { penColor: "#1e293b" }); const pad = new SignaturePad(canvas, { penColor: "#1e293b" });
// Scale canvas to device pixel ratio for crisp rendering on high-DPI tablets // Scale canvas to device pixel ratio — must run after layout so offsetWidth is non-zero.
// requestAnimationFrame ensures the browser has finished its first layout pass.
function resizeCanvas() { function resizeCanvas() {
const ratio = Math.max(window.devicePixelRatio || 1, 1); const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio; const w = canvas.offsetWidth;
canvas.height = canvas.offsetHeight * ratio; const h = canvas.offsetHeight;
if (w === 0 || h === 0) return; // layout not ready yet; resize event will retry
canvas.width = w * ratio;
canvas.height = h * ratio;
canvas.getContext("2d").scale(ratio, ratio); canvas.getContext("2d").scale(ratio, ratio);
pad.clear(); pad.clear();
} }
resizeCanvas();
requestAnimationFrame(resizeCanvas);
window.addEventListener("resize", resizeCanvas); window.addEventListener("resize", resizeCanvas);
// Show visual feedback when the canvas has been signed // Visual feedback when the canvas has been signed
pad.addEventListener("endStroke", function () { pad.addEventListener("endStroke", function () {
canvas.classList.add("signed"); canvas.classList.add("signed");
}); });
@@ -38,11 +51,11 @@
const msg = document.getElementById("signatureError"); const msg = document.getElementById("signatureError");
if (msg) msg.classList.remove("d-none"); if (msg) msg.classList.remove("d-none");
canvas.classList.add("is-invalid"); canvas.classList.add("is-invalid");
canvas.scrollIntoView({ behavior: "smooth", block: "center" });
return; return;
} }
hiddenInput.value = pad.toDataURL("image/png"); hiddenInput.value = pad.toDataURL("image/png");
} }
}); });
} }
}
})(); })();
File diff suppressed because one or more lines are too long