| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
function reducedMotion() { |
| 5 |
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; |
| 6 |
} |
| 7 |
|
| 8 |
function initToggle(wrap) { |
| 9 |
var cfg = { |
| 10 |
toggleStyle: wrap.dataset.toggleStyle || 'switch', |
| 11 |
position: wrap.dataset.position || 'inline', |
| 12 |
floatCorner: wrap.dataset.floatCorner || 'bottom-right', |
| 13 |
floatX: parseInt(wrap.dataset.floatX, 10) || 24, |
| 14 |
floatY: parseInt(wrap.dataset.floatY, 10) || 24, |
| 15 |
storageKey: wrap.dataset.storageKey || 'bkdm-theme', |
| 16 |
attr: wrap.dataset.attr || 'data-theme', |
| 17 |
darkVal: wrap.dataset.darkVal || 'dark', |
| 18 |
lightVal: wrap.dataset.lightVal || 'light', |
| 19 |
defaultTheme: wrap.dataset.default || 'light', |
| 20 |
respectSystem: wrap.dataset.respectSystem !== 'false', |
| 21 |
buttonSize: parseInt(wrap.dataset.buttonSize, 10) || 44, |
| 22 |
borderRadius: parseInt(wrap.dataset.borderRadius, 10) || 50, |
| 23 |
duration: reducedMotion() ? 0 : (parseInt(wrap.dataset.duration, 10) || 300), |
| 24 |
showIcon: wrap.dataset.showIcon !== 'false', |
| 25 |
showLabel: wrap.dataset.showLabel === 'true', |
| 26 |
lightIcon: wrap.dataset.lightIcon || '☀️', |
| 27 |
darkIcon: wrap.dataset.darkIcon || '🌙', |
| 28 |
lightLabel: wrap.dataset.lightLabel || 'Light', |
| 29 |
darkLabel: wrap.dataset.darkLabel || 'Dark', |
| 30 |
hoverScale: wrap.dataset.hoverScale !== 'false', |
| 31 |
shadow: wrap.dataset.shadow !== 'false', |
| 32 |
lightBg: wrap.dataset.lightBg || '#f3f4f6', |
| 33 |
lightColor: wrap.dataset.lightColor || '#111827', |
| 34 |
darkBg: wrap.dataset.darkBg || '#1f2937', |
| 35 |
darkColor: wrap.dataset.darkColor || '#f9fafb', |
| 36 |
trackLight: wrap.dataset.trackLight || '#d1d5db', |
| 37 |
trackDark: wrap.dataset.trackDark || '#4f46e5', |
| 38 |
thumb: wrap.dataset.thumb || '#ffffff', |
| 39 |
}; |
| 40 |
|
| 41 |
// Set CSS vars for floating offsets |
| 42 |
wrap.style.setProperty('--bkdm-offset-x', cfg.floatX + 'px'); |
| 43 |
wrap.style.setProperty('--bkdm-offset-y', cfg.floatY + 'px'); |
| 44 |
|
| 45 |
// Determine initial theme |
| 46 |
var saved = null; |
| 47 |
try { saved = localStorage.getItem(cfg.storageKey); } catch (e) {} |
| 48 |
|
| 49 |
var sysDark = cfg.respectSystem |
| 50 |
? window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches |
| 51 |
: false; |
| 52 |
|
| 53 |
var current = saved || (sysDark ? cfg.darkVal : cfg.lightVal); |
| 54 |
if (!saved && !sysDark) current = cfg.defaultTheme === 'dark' ? cfg.darkVal : cfg.lightVal; |
| 55 |
|
| 56 |
// Build button |
| 57 |
var btn = wrap.querySelector('.bkdm-btn'); |
| 58 |
if (!btn) return; |
| 59 |
|
| 60 |
var thumbSize = Math.round(cfg.buttonSize * 0.5); |
| 61 |
|
| 62 |
btn.style.minWidth = cfg.buttonSize + 'px'; |
| 63 |
btn.style.height = cfg.buttonSize + 'px'; |
| 64 |
btn.style.padding = '0 12px'; |
| 65 |
btn.style.borderRadius = cfg.borderRadius + 'px'; |
| 66 |
btn.style.fontSize = Math.round(cfg.buttonSize * 0.42) + 'px'; |
| 67 |
btn.style.transition = 'background ' + cfg.duration + 'ms ease, color ' + cfg.duration + 'ms ease, transform 0.15s ease'; |
| 68 |
if (cfg.shadow) btn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.18)'; |
| 69 |
|
| 70 |
if (cfg.hoverScale) { |
| 71 |
btn.addEventListener('mouseenter', function () { btn.style.transform = 'scale(1.08)'; }); |
| 72 |
btn.addEventListener('mouseleave', function () { btn.style.transform = 'scale(1)'; }); |
| 73 |
} |
| 74 |
|
| 75 |
// Build inner HTML for switch style |
| 76 |
var iconEl, trackEl, thumbEl, labelEl; |
| 77 |
|
| 78 |
if (cfg.toggleStyle === 'switch') { |
| 79 |
btn.style.background = 'none'; |
| 80 |
btn.style.boxShadow = 'none'; |
| 81 |
btn.style.padding = '0'; |
| 82 |
btn.style.minWidth = 'unset'; |
| 83 |
btn.style.height = 'unset'; |
| 84 |
|
| 85 |
if (cfg.showIcon) { |
| 86 |
iconEl = document.createElement('span'); |
| 87 |
iconEl.className = 'bkdm-icon'; |
| 88 |
iconEl.style.fontSize = Math.round(cfg.buttonSize * 0.42) + 'px'; |
| 89 |
iconEl.style.transition = 'opacity ' + cfg.duration + 'ms ease'; |
| 90 |
btn.appendChild(iconEl); |
| 91 |
} |
| 92 |
|
| 93 |
trackEl = document.createElement('div'); |
| 94 |
trackEl.className = 'bkdm-track'; |
| 95 |
trackEl.style.width = (cfg.buttonSize + 4) + 'px'; |
| 96 |
trackEl.style.height = Math.round(cfg.buttonSize * 0.6) + 'px'; |
| 97 |
trackEl.style.borderRadius = cfg.borderRadius + 'px'; |
| 98 |
trackEl.style.transition = 'background ' + cfg.duration + 'ms ease'; |
| 99 |
|
| 100 |
thumbEl = document.createElement('div'); |
| 101 |
thumbEl.className = 'bkdm-thumb'; |
| 102 |
thumbSize = Math.round(cfg.buttonSize * 0.6) - 4; |
| 103 |
thumbEl.style.width = thumbSize + 'px'; |
| 104 |
thumbEl.style.height = thumbSize + 'px'; |
| 105 |
thumbEl.style.background = cfg.thumb; |
| 106 |
thumbEl.style.transition = 'transform ' + cfg.duration + 'ms ease'; |
| 107 |
|
| 108 |
trackEl.appendChild(thumbEl); |
| 109 |
btn.appendChild(trackEl); |
| 110 |
|
| 111 |
if (cfg.showLabel) { |
| 112 |
labelEl = document.createElement('span'); |
| 113 |
labelEl.className = 'bkdm-label'; |
| 114 |
btn.appendChild(labelEl); |
| 115 |
} |
| 116 |
} else { |
| 117 |
// icon/button style |
| 118 |
if (cfg.showIcon) { |
| 119 |
iconEl = document.createElement('span'); |
| 120 |
iconEl.className = 'bkdm-icon'; |
| 121 |
iconEl.style.transition = 'opacity ' + cfg.duration + 'ms ease'; |
| 122 |
btn.appendChild(iconEl); |
| 123 |
} |
| 124 |
if (cfg.showLabel) { |
| 125 |
labelEl = document.createElement('span'); |
| 126 |
labelEl.className = 'bkdm-label'; |
| 127 |
btn.appendChild(labelEl); |
| 128 |
} |
| 129 |
} |
| 130 |
|
| 131 |
function apply(theme) { |
| 132 |
var isDark = (theme === cfg.darkVal); |
| 133 |
current = theme; |
| 134 |
|
| 135 |
// Apply attribute to html element |
| 136 |
document.documentElement.setAttribute(cfg.attr.replace(/^data-/, ''), isDark ? cfg.darkVal : cfg.lightVal); |
| 137 |
// Also set the full data attribute |
| 138 |
document.documentElement.setAttribute(cfg.attr, isDark ? cfg.darkVal : cfg.lightVal); |
| 139 |
|
| 140 |
try { localStorage.setItem(cfg.storageKey, theme); } catch (e) {} |
| 141 |
|
| 142 |
btn.setAttribute('aria-pressed', String(isDark)); |
| 143 |
|
| 144 |
if (iconEl) iconEl.textContent = isDark ? cfg.darkIcon : cfg.lightIcon; |
| 145 |
if (labelEl) labelEl.textContent = isDark ? cfg.darkLabel : cfg.lightLabel; |
| 146 |
|
| 147 |
if (cfg.toggleStyle === 'switch') { |
| 148 |
trackEl.style.background = isDark ? cfg.trackDark : cfg.trackLight; |
| 149 |
var travelX = trackEl.offsetWidth - thumbSize - 4; |
| 150 |
thumbEl.style.transform = isDark ? 'translateX(' + travelX + 'px)' : 'translateX(0)'; |
| 151 |
} else { |
| 152 |
btn.style.background = isDark ? cfg.darkBg : cfg.lightBg; |
| 153 |
btn.style.color = isDark ? cfg.darkColor : cfg.lightColor; |
| 154 |
} |
| 155 |
} |
| 156 |
|
| 157 |
btn.addEventListener('click', function () { |
| 158 |
var next = (current === cfg.darkVal) ? cfg.lightVal : cfg.darkVal; |
| 159 |
apply(next); |
| 160 |
}); |
| 161 |
|
| 162 |
// Initial paint (defer one tick so track has layout) |
| 163 |
requestAnimationFrame(function () { apply(current); }); |
| 164 |
|
| 165 |
// Listen to system pref changes |
| 166 |
if (cfg.respectSystem && window.matchMedia) { |
| 167 |
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) { |
| 168 |
try { if (localStorage.getItem(cfg.storageKey)) return; } catch (ex) {} |
| 169 |
apply(e.matches ? cfg.darkVal : cfg.lightVal); |
| 170 |
}); |
| 171 |
} |
| 172 |
} |
| 173 |
|
| 174 |
function init() { |
| 175 |
var wraps = document.querySelectorAll('.bkdm-wrap[data-toggle-style]'); |
| 176 |
wraps.forEach(function (wrap) { initToggle(wrap); }); |
| 177 |
} |
| 178 |
|
| 179 |
if (document.readyState === 'loading') { |
| 180 |
document.addEventListener('DOMContentLoaded', init); |
| 181 |
} else { |
| 182 |
init(); |
| 183 |
} |
| 184 |
|
| 185 |
}()); |
| 186 |
|