1/* 2 * Shared QR tool engine for the qr-scanner.app /tools pages. 3 * 4 * Each tool page sets a few globals before including this file: 5 * window.QR_TYPE - one of: url, text, wifi, email, sms, phone, location, event, vcard 6 * window.QR_PRINT_TITLE - heading used on the printable card and PDF (optional) 7 * window.QR_FILE_BASE - download filename base, e.g. "wifi-qr-code" (optional) 8 * 9 * It expects these element ids to exist on the page: 10 * #qrcode-output - empty container the QR is rendered into 11 * #tool-actions - the row of Copy/PNG/PDF/Print buttons (hidden until first generate) 12 * #generator-alert - error box, with a #generator-alert-text span inside 13 * plus the input fields for the active QR_TYPE (same ids as the homepage generator). 14 * 15 * Requires /qrcode.js (davidshimjs) and pdf-lib to be loaded first. 16 */ 17(function () { 18 'use strict'; 19 20 function byId(id) { return document.getElementById(id); } 21 function val(id) { var el = byId(id); return el ? el.value : ''; } 22 function trimVal(id) { return val(id).trim(); } 23 function isChecked(id) { var el = byId(id); return el ? !!el.checked : false; } 24 25 var liveSilent = false; // suppress validation alerts during live (as-you-type) rendering 26 var liveTimer = null; 27 28 function showAlert(msg) { 29 if (liveSilent) { return; } 30 var box = byId('generator-alert'), txt = byId('generator-alert-text'); 31 if (txt) { txt.innerText = msg; } 32 if (box) { box.style.display = 'block'; } 33 } 34 function hideAlert() { 35 var box = byId('generator-alert'); 36 if (box) { box.style.display = 'none'; } 37 } 38 39 function escapeHtml(str) { 40 return String(str).replace(/&/g, '&').replace(/</g, '<') 41 .replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); 42 } 43 44 // Escape \ ; , : " as required by the WIFI: payload spec. 45 function wifiEscape(str) { return String(str).replace(/([\\;,:"])/g, '\\$1'); } 46 47 // Escape \ ; , and newlines as required by vCard / iCalendar values. 48 function icalEscape(str) { 49 return String(str).replace(/\\/g, '\\\\').replace(/;/g, '\\;') 50 .replace(/,/g, '\\,').replace(/\n/g, '\\n'); 51 } 52 53 // "YYYY-MM-DD" + "HH:MM" -> "YYYYMMDDTHHMMSS" 54 function icalDateTime(dateStr, timeStr) { 55 if (!dateStr) { return ''; } 56 var d = dateStr.replace(/-/g, ''); 57 var t = (timeStr || '00:00').replace(/:/g, ''); 58 if (t.length === 4) { t += '00'; } 59 return d + 'T' + t; 60 } 61 62 var builders = { 63 url: function () { 64 var u = trimVal('urlInput'); 65 if (!u) { showAlert('Please enter a URL.'); return null; } 66 // If the user typed no scheme (e.g. "example.com"), assume https so the code is tappable. 67 if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(u) && !/^(mailto:|tel:|sms:)/i.test(u)) { 68 u = 'https://' + u; 69 } 70 return u; 71 }, 72 text: function () { 73 var t = val('textInput'); 74 if (!t.trim()) { showAlert('Please enter some text.'); return null; } 75 return t; 76 }, 77 wifi: function () { 78 var ssid = trimVal('wifiSSID'); 79 var pass = val('wifiPassword'); 80 var sec = val('wifiSecurity') || 'WPA'; 81 var hidden = isChecked('wifiHidden'); 82 if (!ssid) { showAlert('Please enter your network name (SSID).'); return null; } 83 if (sec !== 'nopass' && !pass) { showAlert('Please enter the WiFi password, or choose "No password".'); return null; } 84 var p = 'WIFI:T:' + (sec === 'nopass' ? 'nopass' : sec) + ';S:' + wifiEscape(ssid) + ';'; 85 if (sec !== 'nopass') { p += 'P:' + wifiEscape(pass) + ';'; } 86 if (hidden) { p += 'H:true;'; } 87 return p + ';'; 88 }, 89 email: function () { 90 var to = trimVal('emailTo'); 91 if (!to) { showAlert('Please enter the recipient email address.'); return null; } 92 var subject = trimVal('emailSubject'); 93 var body = val('emailBody'); 94 var q = []; 95 if (subject) { q.push('subject=' + encodeURIComponent(subject)); } 96 if (body) { q.push('body=' + encodeURIComponent(body)); } 97 return 'mailto:' + to + (q.length ? '?' + q.join('&') : ''); 98 }, 99 sms: function () { 100 var num = trimVal('smsNumber'); 101 if (!num) { showAlert('Please enter a phone number.'); return null; } 102 var msg = val('smsMessage'); 103 return 'SMSTO:' + num + (msg ? ':' + msg : ''); 104 }, 105 phone: function () { 106 var num = trimVal('phoneNumber'); 107 if (!num) { showAlert('Please enter a phone number.'); return null; } 108 return 'tel:' + num; 109 }, 110 location: function () { 111 var lat = trimVal('latitude'), lng = trimVal('longitude'); 112 if (!lat || !lng) { showAlert('Please enter both latitude and longitude.'); return null; } 113 return 'geo:' + lat + ',' + lng; 114 }, 115 event: function () { 116 var title = trimVal('eventTitle'); 117 if (!title) { showAlert('Please enter an event title.'); return null; } 118 var start = icalDateTime(trimVal('eventStartDate'), trimVal('eventStartTime')); 119 if (!start) { showAlert('Please choose a start date.'); return null; } 120 var end = icalDateTime(trimVal('eventEndDate'), trimVal('eventEndTime')) || start; 121 var loc = trimVal('eventLocation'); 122 var desc = val('eventDescription'); 123 var lines = ['BEGIN:VEVENT', 'SUMMARY:' + icalEscape(title), 'DTSTART:' + start, 'DTEND:' + end]; 124 if (loc) { lines.push('LOCATION:' + icalEscape(loc)); } 125 if (desc) { lines.push('DESCRIPTION:' + icalEscape(desc)); } 126 lines.push('END:VEVENT'); 127 return 'BEGIN:VCALENDAR\nVERSION:2.0\n' + lines.join('\n') + '\nEND:VCALENDAR'; 128 }, 129 vcard: function () { 130 var first = trimVal('vcardFirstName'), last = trimVal('vcardLastName'); 131 if (!first && !last) { showAlert('Please enter a first or last name.'); return null; } 132 var phone = trimVal('vcardPhone'), email = trimVal('vcardEmail'), company = trimVal('vcardCompany'); 133 var lines = ['BEGIN:VCARD', 'VERSION:3.0', 134 'N:' + icalEscape(last) + ';' + icalEscape(first) + ';;;', 135 'FN:' + icalEscape((first + ' ' + last).trim())]; 136 if (company) { lines.push('ORG:' + icalEscape(company)); } 137 if (phone) { lines.push('TEL;TYPE=CELL:' + phone); } 138 if (email) { lines.push('EMAIL:' + email); } 139 lines.push('END:VCARD'); 140 return lines.join('\n'); 141 } 142 }; 143 144 function generate() { 145 var type = window.QR_TYPE; 146 var build = builders[type]; 147 if (!build) { showAlert('This tool is misconfigured (unknown QR type).'); return; } 148 var payload = build(); 149 if (payload === null || payload === undefined) { 150 // Invalid or empty input: clear the live preview. 151 if (liveSilent) { var oc = byId('qrcode-output'); if (oc) { oc.innerHTML = ''; } var ta = byId('tool-actions'); if (ta) { ta.style.display = 'none'; } } 152 return; 153 } 154 hideAlert(); 155 var out = byId('qrcode-output'); 156 out.innerHTML = ''; 157 // Render at high resolution for crisp downloads; CSS scales it down on screen. 158 new QRCode(out, { 159 text: payload, 160 width: 1024, 161 height: 1024, 162 colorDark: '#000000', 163 colorLight: '#ffffff', 164 correctLevel: QRCode.CorrectLevel.M 165 }); 166 var actions = byId('tool-actions'); 167 if (actions) { actions.style.display = 'flex'; } 168 } 169 170 function getQRDataURL() { 171 var out = byId('qrcode-output'); 172 if (!out) { return null; } 173 var canvas = out.querySelector('canvas'); 174 if (canvas) { return canvas.toDataURL('image/png'); } 175 var img = out.querySelector('img'); 176 return (img && img.src) ? img.src : null; 177 } 178 179 function triggerDownload(url, filename) { 180 var a = document.createElement('a'); 181 a.href = url; 182 a.download = filename; 183 document.body.appendChild(a); 184 a.click(); 185 document.body.removeChild(a); 186 } 187 188 function fileBase() { return window.QR_FILE_BASE || 'qr-code'; } 189 190 function downloadPNG() { 191 var u = getQRDataURL(); 192 if (!u) { showAlert('Please generate a QR code first.'); return; } 193 triggerDownload(u, fileBase() + '.png'); 194 } 195 196 async function downloadPDF() { 197 var u = getQRDataURL(); 198 if (!u) { showAlert('Please generate a QR code first.'); return; }
199 if (typeof PDFLib === 'undefined') { showAlert('PDF export is unavailable right now. Please use PNG.'); return; } 200 try { 201 var bytes = await fetch(u).then(function (r) { return r.arrayBuffer(); }); 202 var pdf = await PDFLib.PDFDocument.create(); 203 var page = pdf.addPage([595.28, 841.89]); // A4 portrait, points 204 var png = await pdf.embedPng(bytes); 205 var size = 320; 206 var x = (595.28 - size) / 2; 207 var y = (841.89 - size) / 2; 208 page.drawImage(png, { x: x, y: y, width: size, height: size }); 209 var title = window.QR_PRINT_TITLE || 'Scan this QR code'; 210 var font = await pdf.embedFont(PDFLib.StandardFonts.HelveticaBold); 211 var tw = font.widthOfTextAtSize(title, 20); 212 page.drawText(title, { x: (595.28 - tw) / 2, y: y + size + 30, size: 20, font: font }); 213 var outBytes = await pdf.save(); 214 triggerDownload(URL.createObjectURL(new Blob([outBytes], { type: 'application/pdf' })), fileBase() + '.pdf'); 215 } catch (e) { 216 showAlert('Could not create the PDF. Please try Save as PNG.'); 217 } 218 } 219 220 function copyQR() { 221 var out = byId('qrcode-output'); 222 var canvas = out ? out.querySelector('canvas') : null; 223 if (!canvas || !canvas.toBlob || typeof ClipboardItem === 'undefined' || !navigator.clipboard) { 224 showAlert('Copy is not supported in this browser. Please use PNG instead.'); 225 return; 226 } 227 canvas.toBlob(function (blob) { 228 navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]).catch(function () { 229 showAlert('Copy is not supported in this browser. Please use PNG instead.'); 230 }); 231 }); 232 } 233 234 function printCard() { 235 var u = getQRDataURL(); 236 if (!u) { showAlert('Please generate a QR code first.'); return; } 237 var title = escapeHtml(window.QR_PRINT_TITLE || 'Scan this QR code'); 238 var w = window.open('', '_blank'); 239 if (!w) { showAlert('Please allow pop-ups to print the card.'); return; } 240 w.document.write( 241 '<!DOCTYPE html><html><head><title>QR Code</title><style>' + 242 'body{font-family:Arial,Helvetica,sans-serif;text-align:center;margin:0;padding:40px;}' + 243 '.card{border:2px solid #a382d3;border-radius:16px;max-width:360px;margin:0 auto;padding:30px;}' + 244 'h1{font-size:24px;margin:0 0 6px 0;color:#333;}' + 245 'p{font-size:16px;color:#666;margin:6px 0;}' + 246 'img{width:260px;height:260px;margin:18px 0;}' + 247 '</style></head><body><div class="card">' + 248 '<h1>' + title + '</h1>' + 249 '<img src="' + u + '" alt="QR Code">' + 250 '<p>Scan with your phone camera</p>' + 251 '</div></body></html>' 252 ); 253 w.document.close(); 254 w.focus(); 255 setTimeout(function () { w.print(); }, 350); 256 } 257 258 // Show/hide a password field (used by the WiFi tool). 259 function togglePwd(id, linkEl) { 260 var input = byId(id); 261 if (!input) { return; } 262 if (input.type === 'password') { 263 input.type = 'text'; 264 if (linkEl) { linkEl.innerHTML = '<i class="fa fa-eye-slash"></i> Hide password'; } 265 } else { 266 input.type = 'password'; 267 if (linkEl) { linkEl.innerHTML = '<i class="fa fa-eye"></i> Show password'; } 268 } 269 } 270 271 // Disable the password field for an open (no-password) WiFi network. 272 function onWifiSecurityChange() { 273 var pw = byId('wifiPassword'); 274 if (!pw) { return; } 275 var nopass = val('wifiSecurity') === 'nopass'; 276 pw.disabled = nopass; 277 if (nopass) { pw.value = ''; } 278 } 279 280 // Live (as-you-type) rendering: debounce-regenerate on any field change. 281 function scheduleGenerate() { 282 clearTimeout(liveTimer); 283 liveTimer = setTimeout(liveGenerate, 130); 284 } 285 function liveGenerate() { 286 liveSilent = true; 287 try { generate(); } finally { liveSilent = false; } 288 } 289 function wireLiveInputs() { 290 var out = byId('qrcode-output'); 291 var card = out ? out.closest('.card') : null; 292 if (!card) { card = document.querySelector('.tool-card'); } 293 if (!card) { return; } 294 var els = card.querySelectorAll('input, select, textarea'); 295 for (var i = 0; i < els.length; i++) { 296 var el = els[i]; 297 if (el.type === 'file' || el.type === 'button' || el.type === 'submit') { continue; } 298 el.addEventListener('input', scheduleGenerate); 299 el.addEventListener('change', scheduleGenerate); 300 } 301 } 302 if (document.readyState === 'loading') { 303 document.addEventListener('DOMContentLoaded', wireLiveInputs); 304 } else { 305 wireLiveInputs(); 306 } 307 308 window.QRT = { 309 generate: generate, 310 downloadPNG: downloadPNG, 311 downloadPDF: downloadPDF, 312 copyQR: copyQR, 313 printCard: printCard, 314 hideAlert: hideAlert, 315 togglePwd: togglePwd, 316 onWifiSecurityChange: onWifiSecurityChange 317 }; 318})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.