PageSourceSearch

https://qr-scanner.app/tools/qr-tools.js

js qr-scanner.app collected 2026-09-24 13:54:30 UTC 12,165 bytes, 318 lines download raw bytes

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, '&amp;').replace(/</g, '&lt;')
41			.replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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.