Move it near the text box. Change handling of updates to be more robust and remove the possibility of a particular bug that was occuring. Set the display threshold higher, now 50%.
291 lines
10 KiB
JavaScript
291 lines
10 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
// --- 1. DOM REFERENCES ---
|
|
const qrContainer = document.getElementById('qr-container');
|
|
const downloadBtn = document.getElementById('btn-download');
|
|
const stickyWrapper = document.querySelector('.sticky-wrapper');
|
|
|
|
// Gauge Elements
|
|
const capMeter = document.getElementById('capacity-meter');
|
|
const capBar = document.getElementById('capacity-bar');
|
|
const capText = document.getElementById('capacity-text');
|
|
const capLabel = document.getElementById('capacity-label');
|
|
|
|
// Inputs
|
|
const modeSelector = document.getElementById('mode-selector');
|
|
const optEcc = document.getElementById('opt-ecc');
|
|
const optSize = document.getElementById('opt-size');
|
|
const optFg = document.getElementById('opt-fg');
|
|
const optBg = document.getElementById('opt-bg');
|
|
const hexFg = document.getElementById('hex-fg');
|
|
const hexBg = document.getElementById('hex-bg');
|
|
|
|
const fmtRadios = document.getElementsByName('opt-fmt');
|
|
|
|
let debounceTimer;
|
|
|
|
// --- 2. STATE GETTERS (Pure Functions) ---
|
|
|
|
function getFormat() {
|
|
for (const r of fmtRadios) {
|
|
if (r.checked) return r.value;
|
|
}
|
|
return 'png';
|
|
}
|
|
|
|
function getDataString() {
|
|
const mode = modeSelector.value;
|
|
if (mode === 'text') {
|
|
return document.getElementById('inp-text').value;
|
|
} else if (mode === 'wifi') {
|
|
const ssid = document.getElementById('inp-wifi-ssid').value;
|
|
const pass = document.getElementById('inp-wifi-pass').value;
|
|
const type = document.getElementById('inp-wifi-type').value;
|
|
// Note: Even if empty, we return the structure so the logic can decide
|
|
if (!ssid) return '';
|
|
const cleanSSID = ssid.replace(/([\\;,:])/g, '\\$1');
|
|
const cleanPass = pass.replace(/([\\;,:])/g, '\\$1');
|
|
return `WIFI:S:${cleanSSID};T:${type};P:${cleanPass};;`;
|
|
} else if (mode === 'email') {
|
|
const to = document.getElementById('inp-email-to').value;
|
|
const sub = document.getElementById('inp-email-sub').value;
|
|
const body = document.getElementById('inp-email-body').value;
|
|
if (!to) return '';
|
|
return `mailto:${to}?subject=${encodeURIComponent(sub)}&body=${encodeURIComponent(body)}`;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
// --- 3. RENDER HELPERS ---
|
|
|
|
function generateSVGString(modules, size, fg, bg) {
|
|
const count = modules.length;
|
|
const modSize = size / count;
|
|
|
|
let pathData = '';
|
|
for (let r = 0; r < count; r++) {
|
|
for (let c = 0; c < count; c++) {
|
|
if (modules[r][c]) {
|
|
const x = c * modSize;
|
|
const y = r * modSize;
|
|
pathData += `M${x},${y}h${modSize}v${modSize}h-${modSize}z`;
|
|
}
|
|
}
|
|
}
|
|
|
|
return `
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">
|
|
<rect width="100%" height="100%" fill="${bg}"/>
|
|
<path d="${pathData}" fill="${fg}" shape-rendering="crispEdges"/>
|
|
</svg>
|
|
`;
|
|
}
|
|
|
|
function renderError() {
|
|
qrContainer.innerHTML = `
|
|
<div class="qr-error-msg">
|
|
ERROR: CAPACITY EXCEEDED<br>
|
|
<span style="font-size:0.7em; opacity:0.8; display:block; margin-top:0.5rem;">
|
|
REDUCE TEXT OR<br>LOWER ECC LEVEL
|
|
</span>
|
|
</div>
|
|
`;
|
|
downloadBtn.disabled = true;
|
|
downloadBtn.textContent = "GENERATION FAILED";
|
|
stickyWrapper.classList.add('has-error');
|
|
}
|
|
|
|
// --- 4. CORE LOGIC ---
|
|
|
|
// A. Capacity Gauge (Fast / Instant)
|
|
function updateCapacityUI(textData) {
|
|
if (!textData) {
|
|
capMeter.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const blob = new Blob([textData]);
|
|
const bytes = blob.size;
|
|
|
|
// Limits based on Version 40 (Byte Mode)
|
|
const maxCapacityMap = { 'L': 2950, 'M': 2328, 'Q': 1660, 'H': 1270 };
|
|
const maxBytes = maxCapacityMap[optEcc.value] || 1270;
|
|
|
|
const raw_usage = Math.round((bytes / maxBytes) * 100);
|
|
const usage = Math.min(100, raw_usage);
|
|
|
|
if (usage > 50) {
|
|
capMeter.style.display = 'block';
|
|
capBar.style.width = `${usage}%`;
|
|
|
|
capText.textContent = `${usage}% (${bytes} / ${maxBytes} B)`;
|
|
|
|
if (raw_usage <= 100) {
|
|
capLabel.textContent = 'CAPACITY';
|
|
} else {
|
|
capLabel.textContent = 'OVER CAPACITY';
|
|
}
|
|
|
|
if (usage > 90) {
|
|
capBar.style.backgroundColor = 'red';
|
|
capText.style.color = 'red';
|
|
} else {
|
|
capBar.style.backgroundColor = '#000';
|
|
capText.style.color = '#000';
|
|
}
|
|
} else {
|
|
capMeter.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// B. The Main Renderer (Stateless: Flush and Rebuild)
|
|
function renderQR() {
|
|
// 1. Gather State
|
|
const textData = getDataString();
|
|
const format = getFormat();
|
|
const ecc = QRCode.CorrectLevel[optEcc.value];
|
|
const colorDark = optFg.value;
|
|
const colorLight = optBg.value;
|
|
|
|
// Update Hex Labels
|
|
hexFg.textContent = colorDark;
|
|
hexBg.textContent = colorLight;
|
|
|
|
// 2. Reset DOM
|
|
qrContainer.innerHTML = '';
|
|
stickyWrapper.classList.remove('has-error');
|
|
|
|
// 3. Handle Empty State
|
|
if (!textData || textData.trim() === '') {
|
|
downloadBtn.disabled = true;
|
|
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
|
|
return; // Stop here. Container is clean.
|
|
}
|
|
|
|
downloadBtn.textContent = `DOWNLOAD ${format.toUpperCase()}`;
|
|
|
|
// 4. Validate Size (Prevent Crashes)
|
|
let size = parseInt(optSize.value) || 256;
|
|
if (size < 64) size = 64;
|
|
if (size > 4000) size = 4000;
|
|
|
|
// 5. Generate
|
|
try {
|
|
// We create a local instance. It populates qrContainer with a Canvas/Img.
|
|
// We do not save this instance globally. It is single-use.
|
|
const instance = new QRCode(qrContainer, {
|
|
text: textData, // Passing text here triggers makeCode immediately
|
|
width: size,
|
|
height: size,
|
|
colorDark : colorDark,
|
|
colorLight : colorLight,
|
|
correctLevel : ecc
|
|
});
|
|
|
|
// 6. Post-Process (Swap to SVG if needed)
|
|
if (format === 'svg') {
|
|
// The library just painted a canvas. We hide it and inject SVG.
|
|
// We use the internal data of our local 'instance'
|
|
const modules = instance._oQRCode.modules;
|
|
|
|
// Hide library output
|
|
const nodes = qrContainer.childNodes;
|
|
for(let i=0; i<nodes.length; i++) {
|
|
if(nodes[i].style) nodes[i].style.display = 'none';
|
|
}
|
|
|
|
// Inject SVG
|
|
const svgString = generateSVGString(modules, size, colorDark, colorLight);
|
|
qrContainer.insertAdjacentHTML('beforeend', svgString);
|
|
}
|
|
else {
|
|
// PNG Mode: The library already appended an <img>. We are done.
|
|
// Just ensure it's visible (library defaults to display:none sometimes for the img)
|
|
const img = qrContainer.querySelector('img');
|
|
if(img) img.style.display = 'block';
|
|
}
|
|
|
|
downloadBtn.disabled = false;
|
|
|
|
} catch (e) {
|
|
console.log(e)
|
|
// 7. Handle Overflow/Errors
|
|
// Since we flushed the DOM at the start, we can just render the error now.
|
|
renderError();
|
|
}
|
|
}
|
|
|
|
// --- 5. EVENT ORCHESTRATION ---
|
|
|
|
function handleUpdate(immediate = false) {
|
|
const text = getDataString();
|
|
|
|
// Always update gauge immediately
|
|
updateCapacityUI(text);
|
|
|
|
// Debounce the heavy rendering
|
|
clearTimeout(debounceTimer);
|
|
if (immediate) {
|
|
renderQR();
|
|
} else {
|
|
debounceTimer = setTimeout(renderQR, 300);
|
|
}
|
|
}
|
|
|
|
// --- 6. LISTENERS ---
|
|
|
|
// Configuration Inputs (Colors, ECC, Size) -> Trigger Rebuild
|
|
[optEcc, optSize, optFg, optBg].forEach(el => {
|
|
el.addEventListener('input', () => handleUpdate(false));
|
|
});
|
|
|
|
// Format Change -> Trigger Rebuild (Instant)
|
|
fmtRadios.forEach(r => r.addEventListener('change', () => handleUpdate(true)));
|
|
|
|
// Mode Switch -> Change Form & Rebuild
|
|
modeSelector.addEventListener('change', (e) => {
|
|
const newMode = e.target.value;
|
|
document.querySelectorAll('.input-form').forEach(f => f.classList.remove('active'));
|
|
document.getElementById(`form-${newMode}`).classList.add('active');
|
|
handleUpdate(true);
|
|
});
|
|
|
|
// Data Entry -> Debounced Rebuild
|
|
document.querySelectorAll('.input-form').forEach(form => {
|
|
form.addEventListener('input', () => handleUpdate(false));
|
|
});
|
|
|
|
// Download Handler
|
|
downloadBtn.addEventListener('click', () => {
|
|
const format = getFormat();
|
|
|
|
if (format === 'png') {
|
|
const img = qrContainer.querySelector('img');
|
|
if (img && img.src) {
|
|
const link = document.createElement('a');
|
|
link.href = img.src;
|
|
link.download = `qr-manifesto-${Date.now()}.png`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
}
|
|
} else {
|
|
const svgEl = qrContainer.querySelector('svg');
|
|
if (svgEl) {
|
|
const serializer = new XMLSerializer();
|
|
const svgString = serializer.serializeToString(svgEl);
|
|
const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = `qr-manifesto-${Date.now()}.svg`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Initial Render
|
|
handleUpdate(true);
|
|
});
|