| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
function typoCssVarsForEl(typo, prefix, el) { |
| 5 |
if (!typo) return; |
| 6 |
if (typo.family) el.style.setProperty(prefix + 'font-family', "'" + typo.family + "', sans-serif"); |
| 7 |
if (typo.weight) el.style.setProperty(prefix + 'font-weight', typo.weight); |
| 8 |
if (typo.transform) el.style.setProperty(prefix + 'text-transform', typo.transform); |
| 9 |
if (typo.style) el.style.setProperty(prefix + 'font-style', typo.style); |
| 10 |
if (typo.decoration) el.style.setProperty(prefix + 'text-decoration', typo.decoration); |
| 11 |
var su = typo.sizeUnit || 'px'; |
| 12 |
if (typo.sizeDesktop !== '' && typo.sizeDesktop != null) el.style.setProperty(prefix + 'font-size-d', typo.sizeDesktop + su); |
| 13 |
if (typo.sizeTablet !== '' && typo.sizeTablet != null) el.style.setProperty(prefix + 'font-size-t', typo.sizeTablet + su); |
| 14 |
if (typo.sizeMobile !== '' && typo.sizeMobile != null) el.style.setProperty(prefix + 'font-size-m', typo.sizeMobile + su); |
| 15 |
var lhu = typo.lineHeightUnit || 'px'; |
| 16 |
if (typo.lineHeightDesktop !== '' && typo.lineHeightDesktop != null) el.style.setProperty(prefix + 'line-height-d', typo.lineHeightDesktop + lhu); |
| 17 |
if (typo.lineHeightTablet !== '' && typo.lineHeightTablet != null) el.style.setProperty(prefix + 'line-height-t', typo.lineHeightTablet + lhu); |
| 18 |
if (typo.lineHeightMobile !== '' && typo.lineHeightMobile != null) el.style.setProperty(prefix + 'line-height-m', typo.lineHeightMobile + lhu); |
| 19 |
var lsu = typo.letterSpacingUnit || 'px'; |
| 20 |
if (typo.letterSpacingDesktop !== '' && typo.letterSpacingDesktop != null) { el.style.setProperty(prefix + 'letter-spacing-d', typo.letterSpacingDesktop + lsu); el.style.setProperty(prefix + 'letter-spacing', typo.letterSpacingDesktop + lsu); } |
| 21 |
if (typo.letterSpacingTablet !== '' && typo.letterSpacingTablet != null) el.style.setProperty(prefix + 'letter-spacing-t', typo.letterSpacingTablet + lsu); |
| 22 |
if (typo.letterSpacingMobile !== '' && typo.letterSpacingMobile != null) el.style.setProperty(prefix + 'letter-spacing-m', typo.letterSpacingMobile + lsu); |
| 23 |
var wsu = typo.wordSpacingUnit || 'px'; |
| 24 |
if (typo.wordSpacingDesktop !== '' && typo.wordSpacingDesktop != null) { el.style.setProperty(prefix + 'word-spacing-d', typo.wordSpacingDesktop + wsu); el.style.setProperty(prefix + 'word-spacing', typo.wordSpacingDesktop + wsu); } |
| 25 |
if (typo.wordSpacingTablet !== '' && typo.wordSpacingTablet != null) el.style.setProperty(prefix + 'word-spacing-t', typo.wordSpacingTablet + wsu); |
| 26 |
if (typo.wordSpacingMobile !== '' && typo.wordSpacingMobile != null) el.style.setProperty(prefix + 'word-spacing-m', typo.wordSpacingMobile + wsu); |
| 27 |
} |
| 28 |
|
| 29 |
// ── Base64 helpers with Unicode support ────────────────────────────── |
| 30 |
function toBase64(str, urlSafe, lineBreaks) { |
| 31 |
var b64; |
| 32 |
try { |
| 33 |
// TextEncoder → Uint8Array → binary string → btoa handles Unicode cleanly |
| 34 |
var bytes = new TextEncoder().encode(str); |
| 35 |
var bin = ''; |
| 36 |
bytes.forEach(function(b) { bin += String.fromCharCode(b); }); |
| 37 |
b64 = btoa(bin); |
| 38 |
} catch(e) { |
| 39 |
return { error: 'Encoding error: ' + e.message }; |
| 40 |
} |
| 41 |
if (urlSafe) { b64 = b64.replace(/\+/g,'-').replace(/\//g,'_'); } |
| 42 |
if (lineBreaks) { |
| 43 |
var lines = []; |
| 44 |
for (var i = 0; i < b64.length; i += 76) { lines.push(b64.slice(i, i+76)); } |
| 45 |
b64 = lines.join('\n'); |
| 46 |
} |
| 47 |
return { result: b64 }; |
| 48 |
} |
| 49 |
|
| 50 |
function fromBase64(str, urlSafe) { |
| 51 |
try { |
| 52 |
var s = str.trim(); |
| 53 |
if (urlSafe) { s = s.replace(/-/g,'+').replace(/_/g,'/'); } |
| 54 |
s = s.replace(/\s/g, ''); |
| 55 |
var bin = atob(s); |
| 56 |
var bytes = new Uint8Array(bin.length); |
| 57 |
for (var i=0; i<bin.length; i++) { bytes[i] = bin.charCodeAt(i); } |
| 58 |
return { result: new TextDecoder().decode(bytes) }; |
| 59 |
} catch(e) { |
| 60 |
return { error: 'Invalid Base64 string. Please check the input.' }; |
| 61 |
} |
| 62 |
} |
| 63 |
|
| 64 |
// ── Bootstrap ──────────────────────────────────────────────────────── |
| 65 |
document.querySelectorAll('.bkbg-b64-app').forEach(function(root) { |
| 66 |
var opts; |
| 67 |
try { opts = JSON.parse(root.getAttribute('data-opts') || '{}'); } |
| 68 |
catch(e) { return; } |
| 69 |
|
| 70 |
var o = { |
| 71 |
title: opts.title || 'Base64 Encoder / Decoder', |
| 72 |
subtitle: opts.subtitle || '', |
| 73 |
defaultMode: opts.defaultMode || 'encode', |
| 74 |
defaultInput: opts.defaultInput || '', |
| 75 |
showUrlSafe: opts.showUrlSafe !== false, |
| 76 |
showLineBreaks: opts.showLineBreaks !== false, |
| 77 |
showStats: opts.showStats !== false, |
| 78 |
accentColor: opts.accentColor || '#0ea5e9', |
| 79 |
sectionBg: opts.sectionBg || '#f0f9ff', |
| 80 |
cardBg: opts.cardBg || '#ffffff', |
| 81 |
inputBg: opts.inputBg || '#f8fafc', |
| 82 |
outputBg: opts.outputBg || '#0f172a', |
| 83 |
outputColor: opts.outputColor || '#7dd3fc', |
| 84 |
titleColor: opts.titleColor || '#0c4a6e', |
| 85 |
subtitleColor: opts.subtitleColor || '#6b7280', |
| 86 |
labelColor: opts.labelColor || '#374151', |
| 87 |
titleFontSize: parseInt(opts.titleFontSize) || 26, |
| 88 |
contentMaxWidth: parseInt(opts.contentMaxWidth) || 720 |
| 89 |
}; |
| 90 |
|
| 91 |
var mode = o.defaultMode; |
| 92 |
|
| 93 |
// ── Build HTML ───────────────────────────────────────────────── |
| 94 |
root.innerHTML = |
| 95 |
'<div class="bkbg-b64-wrap" style="background:' + o.sectionBg + ';max-width:' + o.contentMaxWidth + 'px;--b64-accent:' + o.accentColor + '">' + |
| 96 |
(o.title ? '<h3 class="bkbg-b64-title" style="color:' + o.titleColor + '">' + o.title + '</h3>' : '') + |
| 97 |
(o.subtitle ? '<p class="bkbg-b64-subtitle" style="color:' + o.subtitleColor + '">' + o.subtitle + '</p>' : '') + |
| 98 |
|
| 99 |
'<div class="bkbg-b64-tabs">' + |
| 100 |
'<button class="bkbg-b64-tab" data-mode="encode">⬆ Encode</button>' + |
| 101 |
'<button class="bkbg-b64-tab" data-mode="decode">⬇ Decode</button>' + |
| 102 |
'</div>' + |
| 103 |
|
| 104 |
'<label class="bkbg-b64-field-label" id="b64-in-label" style="color:' + o.labelColor + '">Plain Text</label>' + |
| 105 |
'<textarea id="b64-input" class="bkbg-b64-textarea" style="background:' + o.inputBg + ';color:' + o.labelColor + '" placeholder="Enter text to encode…"></textarea>' + |
| 106 |
|
| 107 |
'<div class="bkbg-b64-options">' + |
| 108 |
(o.showUrlSafe ? '<label class="bkbg-b64-option-label" style="color:' + o.labelColor + '"><input type="checkbox" id="b64-urlsafe" style="accent-color:' + o.accentColor + '"> URL-safe Base64</label>' : '') + |
| 109 |
(o.showLineBreaks ? '<label class="bkbg-b64-option-label" style="color:' + o.labelColor + '"><input type="checkbox" id="b64-linebreaks" style="accent-color:' + o.accentColor + '"> Line breaks (76 chars)</label>' : '') + |
| 110 |
'</div>' + |
| 111 |
|
| 112 |
'<div class="bkbg-b64-output-header">' + |
| 113 |
'<span class="bkbg-b64-output-label" id="b64-out-label" style="color:' + o.labelColor + '">Base64 Output</span>' + |
| 114 |
'<button class="bkbg-b64-copy-btn" id="b64-copy">⧉ Copy</button>' + |
| 115 |
'</div>' + |
| 116 |
'<div class="bkbg-b64-output-box" id="b64-output" style="background:' + o.outputBg + ';color:' + o.outputColor + '">' + |
| 117 |
'<span class="bkbg-b64-output-placeholder">Output will appear here…</span>' + |
| 118 |
'<span class="bkbg-b64-toast" id="b64-toast">Copied!</span>' + |
| 119 |
'</div>' + |
| 120 |
|
| 121 |
(o.showStats ? |
| 122 |
'<div class="bkbg-b64-stats" id="b64-stats">' + |
| 123 |
'<span class="bkbg-b64-stat" id="b64-stat-in" style="color:' + o.labelColor + '">Input: 0 chars</span>' + |
| 124 |
'<span class="bkbg-b64-stat" id="b64-stat-out" style="color:' + o.labelColor + '">Output: 0 chars</span>' + |
| 125 |
'<span class="bkbg-b64-stat" id="b64-stat-ratio" style="color:' + o.labelColor + '">Ratio: —</span>' + |
| 126 |
'</div>' |
| 127 |
: '') + |
| 128 |
'</div>'; |
| 129 |
|
| 130 |
var wrapEl = root.querySelector('.bkbg-b64-wrap'); |
| 131 |
if (wrapEl) { |
| 132 |
typoCssVarsForEl(opts.titleTypo, '--bkbg-b64-title-', wrapEl); |
| 133 |
typoCssVarsForEl(opts.subtitleTypo, '--bkbg-b64-sub-', wrapEl); |
| 134 |
} |
| 135 |
|
| 136 |
var inEl = root.querySelector('#b64-input'); |
| 137 |
var inLabel = root.querySelector('#b64-in-label'); |
| 138 |
var outLabel = root.querySelector('#b64-out-label'); |
| 139 |
var outEl = root.querySelector('#b64-output'); |
| 140 |
var copyBtn = root.querySelector('#b64-copy'); |
| 141 |
var toast = root.querySelector('#b64-toast'); |
| 142 |
var urlsafeEl = root.querySelector('#b64-urlsafe'); |
| 143 |
var lbEl = root.querySelector('#b64-linebreaks'); |
| 144 |
var statIn = root.querySelector('#b64-stat-in'); |
| 145 |
var statOut = root.querySelector('#b64-stat-out'); |
| 146 |
var statRatio = root.querySelector('#b64-stat-ratio'); |
| 147 |
var tabs = root.querySelectorAll('.bkbg-b64-tab'); |
| 148 |
|
| 149 |
var currentOutput = ''; |
| 150 |
|
| 151 |
function setMode(m) { |
| 152 |
mode = m; |
| 153 |
tabs.forEach(function(t) { |
| 154 |
var act = t.getAttribute('data-mode') === mode; |
| 155 |
t.classList.toggle('active', act); |
| 156 |
t.style.background = act ? o.accentColor : ''; |
| 157 |
t.style.borderColor = act ? o.accentColor : ''; |
| 158 |
t.style.color = act ? '#fff' : o.labelColor; |
| 159 |
}); |
| 160 |
inLabel.textContent = mode === 'encode' ? 'Plain Text' : 'Base64 String'; |
| 161 |
outLabel.textContent = mode === 'encode' ? 'Base64 Output' : 'Decoded Text'; |
| 162 |
inEl.placeholder = mode === 'encode' ? 'Enter text to encode…' : 'Enter Base64 to decode…'; |
| 163 |
inEl.value = ''; |
| 164 |
outEl.innerHTML = '<span class="bkbg-b64-output-placeholder">Output will appear here…</span>'; |
| 165 |
currentOutput = ''; |
| 166 |
if (statIn) statIn.textContent = 'Input: 0 chars'; |
| 167 |
if (statOut) statOut.textContent = 'Output: 0 chars'; |
| 168 |
if (statRatio) statRatio.textContent = 'Ratio: —'; |
| 169 |
} |
| 170 |
|
| 171 |
function convert() { |
| 172 |
var text = inEl.value; |
| 173 |
var urlSafe = urlsafeEl ? urlsafeEl.checked : false; |
| 174 |
var lb = lbEl ? lbEl.checked : false; |
| 175 |
var res; |
| 176 |
|
| 177 |
if (!text) { |
| 178 |
outEl.innerHTML = '<span class="bkbg-b64-output-placeholder">Output will appear here…</span><span class="bkbg-b64-toast" id="b64-toast">Copied!</span>'; |
| 179 |
currentOutput = ''; |
| 180 |
return; |
| 181 |
} |
| 182 |
|
| 183 |
if (mode === 'encode') { |
| 184 |
res = toBase64(text, urlSafe, lb); |
| 185 |
} else { |
| 186 |
res = fromBase64(text, urlSafe); |
| 187 |
} |
| 188 |
|
| 189 |
if (res.error) { |
| 190 |
outEl.innerHTML = '<span class="bkbg-b64-toast" id="b64-toast">Copied!</span>' + |
| 191 |
'<span style="color:#f87171;font-style:italic">' + res.error + '</span>'; |
| 192 |
currentOutput = ''; |
| 193 |
} else { |
| 194 |
var esc = res.result.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); |
| 195 |
outEl.innerHTML = '<span class="bkbg-b64-toast" id="b64-toast">Copied!</span>' + esc; |
| 196 |
currentOutput = res.result; |
| 197 |
toast = root.querySelector('#b64-toast'); |
| 198 |
|
| 199 |
if (o.showStats) { |
| 200 |
var inLen = text.length; |
| 201 |
var outLen = res.result.replace(/\n/g,'').length; |
| 202 |
var ratio = inLen > 0 ? (outLen / inLen).toFixed(2) + 'x' : '—'; |
| 203 |
if (statIn) statIn.textContent = 'Input: ' + inLen + ' chars'; |
| 204 |
if (statOut) statOut.textContent = 'Output: ' + outLen + ' chars'; |
| 205 |
if (statRatio) statRatio.textContent = 'Ratio: ' + ratio; |
| 206 |
} |
| 207 |
} |
| 208 |
} |
| 209 |
|
| 210 |
// bind tab clicks |
| 211 |
tabs.forEach(function(t) { t.addEventListener('click', function(){ setMode(t.getAttribute('data-mode')); }); }); |
| 212 |
|
| 213 |
// live conversion on input |
| 214 |
inEl.addEventListener('input', convert); |
| 215 |
if (urlsafeEl) urlsafeEl.addEventListener('change', convert); |
| 216 |
if (lbEl) lbEl.addEventListener('change', convert); |
| 217 |
|
| 218 |
// copy button |
| 219 |
copyBtn.addEventListener('click', function() { |
| 220 |
if (!currentOutput) return; |
| 221 |
navigator.clipboard.writeText(currentOutput).then(function() { |
| 222 |
var t2 = root.querySelector('#b64-toast'); |
| 223 |
if (t2) { t2.classList.add('show'); setTimeout(function(){ t2.classList.remove('show'); }, 1800); } |
| 224 |
}).catch(function(){}); |
| 225 |
}); |
| 226 |
|
| 227 |
// initial |
| 228 |
setMode(mode); |
| 229 |
if (o.defaultInput) { inEl.value = o.defaultInput; convert(); } |
| 230 |
}); |
| 231 |
})(); |
| 232 |
|