| 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 || ''; |
| 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); |
| 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); |
| 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 |
function diffDates(from, to) { |
| 30 |
var d1 = new Date(from), d2 = new Date(to); |
| 31 |
if (isNaN(d1) || isNaN(d2)) return null; |
| 32 |
var swapped = false; |
| 33 |
if (d1 > d2) { var t = d1; d1 = d2; d2 = t; swapped = true; } |
| 34 |
var totalMs = d2 - d1; |
| 35 |
var totalDays = Math.floor(totalMs / 86400000); |
| 36 |
var totalWeeks = Math.floor(totalDays / 7); |
| 37 |
var totalHours = Math.floor(totalMs / 3600000); |
| 38 |
var totalMins = Math.floor(totalMs / 60000); |
| 39 |
|
| 40 |
/* Calendar years/months/days */ |
| 41 |
var y = d2.getFullYear() - d1.getFullYear(); |
| 42 |
var m = d2.getMonth() - d1.getMonth(); |
| 43 |
var d = d2.getDate() - d1.getDate(); |
| 44 |
if (d < 0) { m--; var prev = new Date(d2.getFullYear(), d2.getMonth(), 0); d += prev.getDate(); } |
| 45 |
if (m < 0) { y--; m += 12; } |
| 46 |
|
| 47 |
/* Working days */ |
| 48 |
var wdays = 0, cur = new Date(d1); |
| 49 |
while (cur < d2) { var dow = cur.getDay(); if (dow !== 0 && dow !== 6) wdays++; cur.setDate(cur.getDate()+1); } |
| 50 |
|
| 51 |
return { years:y, months:m, days:d, totalDays:totalDays, totalWeeks:totalWeeks, totalHours:totalHours, totalMins:totalMins, workingDays:wdays, swapped:swapped }; |
| 52 |
} |
| 53 |
|
| 54 |
function todayStr() { |
| 55 |
var d = new Date(), month = d.getMonth()+1, day = d.getDate(); |
| 56 |
return d.getFullYear() + '-' + (month<10?'0'+month:month) + '-' + (day<10?'0'+day:day); |
| 57 |
} |
| 58 |
|
| 59 |
function buildApp(app) { |
| 60 |
var opts = {}; |
| 61 |
try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch (e) {} |
| 62 |
|
| 63 |
var accent = opts.accentColor || '#6c3fb5'; |
| 64 |
var cardBg = opts.cardBg || '#ffffff'; |
| 65 |
var resultBg = opts.resultBg || '#f5f3ff'; |
| 66 |
var resultBdr= opts.resultBorder || '#ede9fe'; |
| 67 |
var cRadius = (opts.cardRadius || 16) + 'px'; |
| 68 |
var iRadius = (opts.inputRadius || 8) + 'px'; |
| 69 |
var maxW = (opts.maxWidth || 620) + 'px'; |
| 70 |
var ptop = (opts.paddingTop || 60) + 'px'; |
| 71 |
var pbot = (opts.paddingBottom|| 60) + 'px'; |
| 72 |
var numSz = (opts.numSize || 40) + 'px'; |
| 73 |
var titleSz = (opts.titleSize || 26) + 'px'; |
| 74 |
var lclr = opts.labelColor || '#374151'; |
| 75 |
|
| 76 |
var state = { from: todayStr(), to: todayStr() }; |
| 77 |
|
| 78 |
/* Set typography CSS vars on wrapper */ |
| 79 |
app.style.setProperty('--bkbg-dd-ttl-fs', titleSz); |
| 80 |
app.style.setProperty('--bkbg-dd-sub-fs', '14px'); |
| 81 |
app.style.setProperty('--bkbg-dd-num-fs', numSz); |
| 82 |
typoCssVarsForEl(opts.typoTitle, '--bkbg-dd-ttl-', app); |
| 83 |
typoCssVarsForEl(opts.typoSubtitle, '--bkbg-dd-sub-', app); |
| 84 |
typoCssVarsForEl(opts.typoNumber, '--bkbg-dd-num-', app); |
| 85 |
|
| 86 |
/* Card */ |
| 87 |
var card = document.createElement('div'); |
| 88 |
card.className = 'bkbg-dd-card'; |
| 89 |
card.style.cssText = 'background:' + cardBg + ';border-radius:' + cRadius + ';max-width:' + maxW + ';margin:0 auto;padding:' + ptop + ' 32px ' + pbot; |
| 90 |
app.innerHTML = ''; |
| 91 |
app.appendChild(card); |
| 92 |
|
| 93 |
/* Title */ |
| 94 |
if (opts.showTitle !== false && opts.title) { |
| 95 |
var h2 = document.createElement('h2'); |
| 96 |
h2.className = 'bkbg-dd-title'; |
| 97 |
h2.style.cssText = 'color:' + (opts.titleColor||'#1e1b4b'); |
| 98 |
h2.textContent = opts.title; |
| 99 |
card.appendChild(h2); |
| 100 |
} |
| 101 |
if (opts.showSubtitle !== false && opts.subtitle) { |
| 102 |
var sub = document.createElement('p'); |
| 103 |
sub.className = 'bkbg-dd-subtitle'; |
| 104 |
sub.style.cssText = 'color:' + (opts.subtitleColor||'#6b7280'); |
| 105 |
sub.textContent = opts.subtitle; |
| 106 |
card.appendChild(sub); |
| 107 |
} |
| 108 |
|
| 109 |
var inpCSS = 'width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:' + iRadius + ';font-size:15px;box-sizing:border-box;outline:none;background:#fff;color:' + lclr; |
| 110 |
|
| 111 |
/* Date pickers row */ |
| 112 |
var pickers = document.createElement('div'); |
| 113 |
pickers.className = 'bkbg-dd-pickers'; |
| 114 |
pickers.style.cssText = 'display:flex;gap:12px;align-items:flex-end;margin-bottom:20px;flex-wrap:wrap'; |
| 115 |
|
| 116 |
function mkPicker(labelTxt, val, onCh) { |
| 117 |
var col = document.createElement('div'); |
| 118 |
col.style.cssText = 'flex:1;min-width:120px'; |
| 119 |
var lbl = document.createElement('label'); |
| 120 |
lbl.className = 'bkbg-dd-label'; |
| 121 |
lbl.style.cssText = 'font-size:13px;font-weight:600;color:' + lclr + ';display:block;margin-bottom:4px'; |
| 122 |
lbl.textContent = labelTxt; |
| 123 |
var inp = document.createElement('input'); |
| 124 |
inp.type = 'date'; inp.value = val; inp.className = 'bkbg-dd-input'; inp.style.cssText = inpCSS; |
| 125 |
inp.addEventListener('change', function () { onCh(inp.value); }); |
| 126 |
col.appendChild(lbl); col.appendChild(inp); |
| 127 |
return { col: col, inp: inp }; |
| 128 |
} |
| 129 |
|
| 130 |
var fromPicker = mkPicker(opts.labelFrom || 'From Date', state.from, function (v) { state.from = v; update(); }); |
| 131 |
var toPicker = mkPicker(opts.labelTo || 'To Date', state.to, function (v) { state.to = v; update(); }); |
| 132 |
|
| 133 |
pickers.appendChild(fromPicker.col); |
| 134 |
|
| 135 |
if (opts.showSwapBtn !== false) { |
| 136 |
var swapBtn = document.createElement('button'); |
| 137 |
swapBtn.className = 'bkbg-dd-swap'; |
| 138 |
swapBtn.style.cssText = 'padding:10px 14px;border:1.5px solid ' + accent + ';border-radius:' + iRadius + ';background:#fff;color:' + accent + ';cursor:pointer;font-weight:600;font-size:18px;flex-shrink:0'; |
| 139 |
swapBtn.textContent = '⇄'; |
| 140 |
swapBtn.title = 'Swap dates'; |
| 141 |
swapBtn.addEventListener('click', function () { |
| 142 |
var tmp = state.from; |
| 143 |
state.from = state.to; |
| 144 |
state.to = tmp; |
| 145 |
fromPicker.inp.value = state.from; |
| 146 |
toPicker.inp.value = state.to; |
| 147 |
update(); |
| 148 |
}); |
| 149 |
pickers.appendChild(swapBtn); |
| 150 |
} |
| 151 |
|
| 152 |
pickers.appendChild(toPicker.col); |
| 153 |
card.appendChild(pickers); |
| 154 |
|
| 155 |
/* Results area */ |
| 156 |
var resultsArea = document.createElement('div'); |
| 157 |
card.appendChild(resultsArea); |
| 158 |
|
| 159 |
/* Stat configs */ |
| 160 |
function getStats(diff) { |
| 161 |
var list = []; |
| 162 |
if (opts.showTotalDays !== false) list.push({ label:'Total Days', val:diff.totalDays.toLocaleString(), unit:'days', color:opts.daysColor ||'#3b82f6' }); |
| 163 |
if (opts.showWeeks !== false) list.push({ label:'Total Weeks', val:diff.totalWeeks.toLocaleString(), unit:'weeks', color:opts.weeksColor ||'#10b981' }); |
| 164 |
if (opts.showHours !== false) list.push({ label:'Total Hours', val:diff.totalHours.toLocaleString(), unit:'hours', color:opts.hoursColor ||'#f59e0b' }); |
| 165 |
if (opts.showMinutes !== false) list.push({ label:'Total Minutes', val:diff.totalMins.toLocaleString(), unit:'minutes', color:'#8b5cf6' }); |
| 166 |
if (opts.showWorkingDays !== false) list.push({ label:'Working Days', val:diff.workingDays.toLocaleString(),unit:'Mon–Fri', color:opts.wdaysColor ||'#ef4444' }); |
| 167 |
return list; |
| 168 |
} |
| 169 |
|
| 170 |
function update() { |
| 171 |
resultsArea.innerHTML = ''; |
| 172 |
var diff = diffDates(state.from, state.to); |
| 173 |
var sameDay = state.from === state.to; |
| 174 |
|
| 175 |
if (!diff || sameDay) { |
| 176 |
var empty = document.createElement('div'); |
| 177 |
empty.className = 'bkbg-dd-empty'; |
| 178 |
empty.style.cssText = 'text-align:center;padding:32px;color:#9ca3af;background:' + resultBg + ';border-radius:' + cRadius + ';border:1.5px solid ' + resultBdr; |
| 179 |
empty.textContent = 'Select two different dates to see the difference.'; |
| 180 |
resultsArea.appendChild(empty); |
| 181 |
return; |
| 182 |
} |
| 183 |
|
| 184 |
/* YMD summary */ |
| 185 |
if (opts.showYMD !== false) { |
| 186 |
var box = document.createElement('div'); |
| 187 |
box.className = 'bkbg-dd-result-box'; |
| 188 |
box.style.cssText = 'background:' + resultBg + ';border:1.5px solid ' + resultBdr + ';border-radius:' + cRadius + ';padding:20px;text-align:center;margin-bottom:16px'; |
| 189 |
|
| 190 |
var rlbl = document.createElement('div'); |
| 191 |
rlbl.className = 'bkbg-dd-result-label'; |
| 192 |
rlbl.style.cssText = 'font-size:13px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.06em;margin-bottom:8px'; |
| 193 |
rlbl.textContent = 'Duration'; |
| 194 |
box.appendChild(rlbl); |
| 195 |
|
| 196 |
var ymdParts = []; |
| 197 |
if (diff.years > 0) ymdParts.push(diff.years + ' yr'); |
| 198 |
if (diff.months > 0) ymdParts.push(diff.months + ' mo'); |
| 199 |
if (diff.days > 0) ymdParts.push(diff.days + ' day'); |
| 200 |
if (ymdParts.length === 0) ymdParts.push('0 days'); |
| 201 |
|
| 202 |
var ymdEl = document.createElement('div'); |
| 203 |
ymdEl.className = 'bkbg-dd-ymd'; |
| 204 |
ymdEl.style.cssText = 'font-size:22px;font-weight:800;color:' + (opts.ymdColor||accent) + ';line-height:1.3'; |
| 205 |
ymdEl.textContent = ymdParts.join(' '); |
| 206 |
box.appendChild(ymdEl); |
| 207 |
|
| 208 |
if (diff.swapped) { |
| 209 |
var note = document.createElement('div'); |
| 210 |
note.className = 'bkbg-dd-swapped-note'; |
| 211 |
note.style.cssText = 'font-size:12px;color:#9ca3af;margin-top:8px'; |
| 212 |
note.textContent = '(dates swapped — showing absolute difference)'; |
| 213 |
box.appendChild(note); |
| 214 |
} |
| 215 |
|
| 216 |
resultsArea.appendChild(box); |
| 217 |
} |
| 218 |
|
| 219 |
/* Stat cards */ |
| 220 |
var stats = getStats(diff); |
| 221 |
if (stats.length > 0) { |
| 222 |
var cols = stats.length <= 2 ? '1fr '.repeat(stats.length).trim() |
| 223 |
: stats.length <= 4 ? '1fr 1fr' |
| 224 |
: '1fr 1fr 1fr'; |
| 225 |
|
| 226 |
var grid = document.createElement('div'); |
| 227 |
grid.className = 'bkbg-dd-stats'; |
| 228 |
grid.style.cssText = 'display:grid;grid-template-columns:' + cols + ';gap:10px'; |
| 229 |
|
| 230 |
stats.forEach(function (s) { |
| 231 |
var sc = document.createElement('div'); |
| 232 |
sc.className = 'bkbg-dd-stat'; |
| 233 |
sc.style.cssText = 'text-align:center;background:#fff;border-radius:10px;padding:16px 8px;border:1.5px solid #e5e7eb'; |
| 234 |
sc.innerHTML = '<div class="bkbg-dd-stat-label" style="font-size:' + (opts.statLabelFontSize || 11) + 'px;text-transform:uppercase;letter-spacing:0.05em;color:#9ca3af;margin-bottom:4px">' + s.label + '</div>' + |
| 235 |
'<div class="bkbg-dd-stat-num" style="color:' + s.color + '">' + s.val + '</div>' + |
| 236 |
'<div class="bkbg-dd-stat-unit" style="font-size:' + (opts.statUnitFontSize || 12) + 'px;color:#9ca3af">' + s.unit + '</div>'; |
| 237 |
grid.appendChild(sc); |
| 238 |
}); |
| 239 |
|
| 240 |
resultsArea.appendChild(grid); |
| 241 |
} |
| 242 |
} |
| 243 |
|
| 244 |
update(); |
| 245 |
} |
| 246 |
|
| 247 |
document.querySelectorAll('.bkbg-dd-app').forEach(buildApp); |
| 248 |
})(); |
| 249 |
|