Add a pop-up next to the Form view modals for Connect Cards, with zoom + print support.
// ==UserScript==
// @name Connect Card Upload Viewer
// @namespace https://github.com/nate-kean/
// @version 2026.04.05
// @description Add a pop-up next to the Form view modals for Connect Cards, with zoom + print support.
// @author Nate Kean
// @match https://jamesriver.fellowshiponego.com/forms
// @grant none
// @license MIT
// @require https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.7/viewer.min.js
// ==/UserScript==
(async function() {
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForElement(selector, pollingRateMs=100, parent=document) {
let el;
while (true) {
el = parent.querySelector(selector);
if (el) return el;
await delay(pollingRateMs);
}
}
async function elementGone(selector, pollingRateMs=100, parent=document) {
let el;
while (true) {
el = parent.querySelector(selector);
if (!el) return;
await delay(pollingRateMs);
}
}
document.head.insertAdjacentHTML("beforeend", `
<style id="nates-connect-card-viewer-css">
#nates-connect-card-viewer {
position: absolute;
top: 6rem;
right: 1rem;
width: 23vw;
height: 80vh;
background-color: #fff;
padding: 1.5rem;
box-shadow: 0 20px 50px 0 rgba(52,52,52,.13) !important;
overflow-y: auto;
}
#nates-connect-card-viewer img {
max-width: 100%;
cursor: zoom-in;
}
#nates-connect-card-viewer button {
float: right;
margin-left: 0.5rem;
}
</style>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.7/viewer.min.css"
/>
`);
while (true) {
const modal = await waitForElement(".modal-dialog");
const detail = modal.querySelector("h3.modal-title > span > span.info");
while (detail.textContent === "") {
await delay(10);
}
let popup = null;
if (detail.textContent === "Connect Card Upload" || detail.textContent === "Baptism Card Upload") {
const firstName = modal.querySelector("input[data-qa='field-person-name-input-first-name']").value;
const lastName = modal.querySelector("input[data-qa='field-person-name-input-last-name']").value;
const fileURLs = modal.querySelectorAll("a.file-upload-link");
const memos = modal.querySelectorAll("textarea[data-qa='field-memo-textarea']");
const campus = modal.querySelector("input[data-qa='field-radio-radio-value']:checked")?.value;
popup = document.createElement("div");
popup.id = "nates-connect-card-viewer";
// Close button
const closeBtn = document.createElement("button");
closeBtn.textContent = "X";
closeBtn.addEventListener("click", () => popup.remove());
popup.appendChild(closeBtn);
// Print button
const printBtn = document.createElement("button");
printBtn.textContent = "Print";
printBtn.addEventListener("click", () => {
const printWindow = window.open("", "_blank");
const blocksHTML = [];
for (let i = 0; i < fileURLs.length; i++) {
const text = `${firstName} ${lastName} - ${campus} - ${memos[i].textContent || "(no desc)"}`;
// Adjust image size dynamically based on text length
const imgMaxHeight = text.length > 120 ? "65vh" : "75vh";
blocksHTML.push(`
<div class="print-block">
<img src="${fileURLs[i].href}" style="max-height:${imgMaxHeight}" />
<p>${text}</p>
</div>
`);
}
printWindow.document.write(`
<html>
<head>
<title>Print</title>
<style>
@media print {
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
.print-block {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
page-break-after: always;
break-after: page;
break-inside: avoid;
page-break-inside: avoid;
padding: 20px;
box-sizing: border-box;
}
img {
max-width: 100%;
object-fit: contain;
}
p {
margin-top: 10px;
font-size: 16px;
text-align: center;
}
}
button {
display: none;
}
</style>
</head>
<body>
${blocksHTML.join("")}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
printWindow.print();
// intentionally NOT closing window
});
popup.appendChild(printBtn);
// Viewer content
for (let i = 0; i < fileURLs.length; i++) {
const wrapper = document.createElement("div");
const img = document.createElement("img");
img.src = fileURLs[i].href;
new Viewer(img);
const desc = document.createElement("p");
desc.textContent = `${firstName} ${lastName} - ${campus} - ${memos[i].textContent || "(no desc)"}`;
wrapper.appendChild(img);
wrapper.appendChild(desc);
popup.appendChild(wrapper);
}
document.body.appendChild(popup);
}
await elementGone(".modal-dialog");
popup?.remove();
}
})();