| 1 |
function typoCssVarsForEl(typo, prefix, el) { |
| 2 |
if (!typo || typeof typo !== 'object') return; |
| 3 |
var map = { |
| 4 |
family: 'font-family', weight: 'font-weight', style: 'font-style', |
| 5 |
transform: 'text-transform', decoration: 'text-decoration', |
| 6 |
sizeDesktop: 'font-size-d', sizeTablet: 'font-size-t', sizeMobile: 'font-size-m', |
| 7 |
sizeUnit: null, |
| 8 |
lineHeightDesktop: 'line-height-d', lineHeightTablet: 'line-height-t', lineHeightMobile: 'line-height-m', |
| 9 |
lineHeightUnit: null, |
| 10 |
letterSpacingDesktop: 'letter-spacing-d', letterSpacingTablet: 'letter-spacing-t', letterSpacingMobile: 'letter-spacing-m', |
| 11 |
letterSpacingUnit: null, |
| 12 |
wordSpacingDesktop: 'word-spacing-d', wordSpacingTablet: 'word-spacing-t', wordSpacingMobile: 'word-spacing-m', |
| 13 |
wordSpacingUnit: null |
| 14 |
}; |
| 15 |
var sizeU = typo.sizeUnit || 'px'; |
| 16 |
var lhU = typo.lineHeightUnit || ''; |
| 17 |
var lsU = typo.letterSpacingUnit || 'px'; |
| 18 |
var wsU = typo.wordSpacingUnit || 'px'; |
| 19 |
Object.keys(map).forEach(function (k) { |
| 20 |
var css = map[k]; if (!css) return; |
| 21 |
var v = typo[k]; if (v === undefined || v === '' || v === null) return; |
| 22 |
if (/size|spacing/i.test(k)) { |
| 23 |
var u = /letterSpacing/.test(k) ? lsU : /wordSpacing/.test(k) ? wsU : /lineHeight/.test(k) ? lhU : sizeU; |
| 24 |
v = v + u; |
| 25 |
} |
| 26 |
el.style.setProperty(prefix + css, v); |
| 27 |
}); |
| 28 |
} |
| 29 |
|
| 30 |
(function () { |
| 31 |
'use strict'; |
| 32 |
|
| 33 |
var PRESETS = [ |
| 34 |
{ label: 'Every minute', expr: '* * * * *' }, |
| 35 |
{ label: 'Every hour', expr: '0 * * * *' }, |
| 36 |
{ label: 'Daily 9 AM', expr: '0 9 * * *' }, |
| 37 |
{ label: 'Weekdays 9 AM', expr: '0 9 * * 1-5' }, |
| 38 |
{ label: 'Weekly Monday', expr: '0 9 * * 1' }, |
| 39 |
{ label: 'Monthly 1st', expr: '0 9 1 * *' }, |
| 40 |
{ label: 'Yearly Jan 1st', expr: '0 0 1 1 *' } |
| 41 |
]; |
| 42 |
|
| 43 |
var MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; |
| 44 |
var WEEKDAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; |
| 45 |
|
| 46 |
/* ---- Describe a cron expression in English ---- */ |
| 47 |
function describe(expr) { |
| 48 |
var parts = (expr || '').trim().split(/\s+/); |
| 49 |
if (parts.length !== 5) return 'Invalid expression'; |
| 50 |
|
| 51 |
var min = parts[0], hr = parts[1], dom = parts[2], mon = parts[3], dow = parts[4]; |
| 52 |
|
| 53 |
function fmtTime(h, m) { |
| 54 |
var H = parseInt(h, 10), M = parseInt(m, 10); |
| 55 |
if (isNaN(H) || isNaN(M)) return 'at ' + m + ':' + h; |
| 56 |
var ampm = H >= 12 ? 'PM' : 'AM'; |
| 57 |
var h12 = H % 12 || 12; |
| 58 |
return 'at ' + h12 + ':' + String(M).padStart(2, '0') + ' ' + ampm; |
| 59 |
} |
| 60 |
|
| 61 |
var timeStr = ''; |
| 62 |
if (min === '*' && hr === '*') { |
| 63 |
timeStr = 'every minute'; |
| 64 |
} else if (min === '*') { |
| 65 |
timeStr = 'every minute of hour ' + hr; |
| 66 |
} else if (hr === '*') { |
| 67 |
timeStr = 'at minute ' + min + ' of every hour'; |
| 68 |
} else { |
| 69 |
timeStr = fmtTime(hr, min); |
| 70 |
} |
| 71 |
|
| 72 |
var domStr = ''; |
| 73 |
if (dom !== '*') { |
| 74 |
if (/^\d+$/.test(dom)) domStr = 'on day ' + dom + ' of the month'; |
| 75 |
else domStr = 'on day ' + dom + ' of the month'; |
| 76 |
} |
| 77 |
|
| 78 |
var monStr = ''; |
| 79 |
if (mon !== '*') { |
| 80 |
if (/^\d+$/.test(mon) && parseInt(mon) >= 1 && parseInt(mon) <= 12) monStr = 'in ' + MONTHS[parseInt(mon) - 1]; |
| 81 |
else monStr = 'in month ' + mon; |
| 82 |
} |
| 83 |
|
| 84 |
var dowStr = ''; |
| 85 |
if (dow !== '*') { |
| 86 |
var days = dow.split(',').map(function (d) { |
| 87 |
if (/^\d+$/.test(d)) return WEEKDAYS[parseInt(d) % 7] || d; |
| 88 |
if (/^\d+-\d+$/.test(d)) { |
| 89 |
var range = d.split('-'); |
| 90 |
return (WEEKDAYS[parseInt(range[0])] || range[0]) + '–' + (WEEKDAYS[parseInt(range[1])] || range[1]); |
| 91 |
} |
| 92 |
return d; |
| 93 |
}); |
| 94 |
dowStr = 'on ' + days.join(', '); |
| 95 |
} |
| 96 |
|
| 97 |
var parts2 = [timeStr]; |
| 98 |
if (domStr) parts2.push(domStr); |
| 99 |
if (monStr) parts2.push(monStr); |
| 100 |
if (dowStr) parts2.push(dowStr); |
| 101 |
if (dom === '*' && mon === '*' && dow === '*' && timeStr !== 'every minute') parts2.push('every day'); |
| 102 |
|
| 103 |
return parts2.join(', '); |
| 104 |
} |
| 105 |
|
| 106 |
/* ---- Validate a cron field ---- */ |
| 107 |
function validateField(val, min, max) { |
| 108 |
if (val === '*') return true; |
| 109 |
if (/^\*\/\d+$/.test(val)) return true; |
| 110 |
if (/^\d+$/.test(val)) { var n = parseInt(val); return n >= min && n <= max; } |
| 111 |
if (/^\d+-\d+$/.test(val)) { |
| 112 |
var parts = val.split('-'); |
| 113 |
return parseInt(parts[0]) >= min && parseInt(parts[1]) <= max && parseInt(parts[0]) <= parseInt(parts[1]); |
| 114 |
} |
| 115 |
if (/^\d+(,\d+)+$/.test(val)) { |
| 116 |
return val.split(',').every(function (n) { var x = parseInt(n); return x >= min && x <= max; }); |
| 117 |
} |
| 118 |
return false; |
| 119 |
} |
| 120 |
|
| 121 |
/* ---- Compute next N run times ---- */ |
| 122 |
function nextRuns(expr, count) { |
| 123 |
var parts = (expr || '').trim().split(/\s+/); |
| 124 |
if (parts.length !== 5) return []; |
| 125 |
|
| 126 |
function matchField(val, n, min, max) { |
| 127 |
if (val === '*') return true; |
| 128 |
if (/^\*\/(\d+)$/.test(val)) { var step = parseInt(val.slice(2)); return n % step === 0; } |
| 129 |
if (/^\d+$/.test(val)) return parseInt(val) === n; |
| 130 |
if (/^\d+-\d+$/.test(val)) { var r = val.split('-'); return n >= parseInt(r[0]) && n <= parseInt(r[1]); } |
| 131 |
if (/^\d+(,\d+)+$/.test(val)) return val.split(',').map(Number).indexOf(n) !== -1; |
| 132 |
return false; |
| 133 |
} |
| 134 |
|
| 135 |
var results = []; |
| 136 |
var d = new Date(); |
| 137 |
d.setSeconds(0, 0); |
| 138 |
d.setMinutes(d.getMinutes() + 1); |
| 139 |
|
| 140 |
var limit = 0; |
| 141 |
while (results.length < count && limit < 525600) { |
| 142 |
limit++; |
| 143 |
if ( |
| 144 |
matchField(parts[3], d.getMonth() + 1, 1, 12) && |
| 145 |
matchField(parts[2], d.getDate(), 1, 31) && |
| 146 |
matchField(parts[4], d.getDay(), 0, 7) && |
| 147 |
matchField(parts[1], d.getHours(), 0, 23) && |
| 148 |
matchField(parts[0], d.getMinutes(), 0, 59) |
| 149 |
) { |
| 150 |
results.push(new Date(d)); |
| 151 |
} |
| 152 |
d.setMinutes(d.getMinutes() + 1); |
| 153 |
} |
| 154 |
return results; |
| 155 |
} |
| 156 |
|
| 157 |
function relativeTime(date) { |
| 158 |
var diff = date - Date.now(); |
| 159 |
var mins = Math.round(diff / 60000); |
| 160 |
if (mins < 60) return 'in ' + mins + ' min'; |
| 161 |
var hrs = Math.round(mins / 60); |
| 162 |
if (hrs < 24) return 'in ' + hrs + ' hr'; |
| 163 |
return 'in ' + Math.round(hrs / 24) + ' day' + (Math.round(hrs/24) > 1 ? 's' : ''); |
| 164 |
} |
| 165 |
|
| 166 |
function initBlock(root) { |
| 167 |
var opts; |
| 168 |
try { opts = JSON.parse(root.getAttribute('data-opts')); } catch (e) { return; } |
| 169 |
|
| 170 |
var a = opts; |
| 171 |
var expr = a.defaultExpression || '0 9 * * *'; |
| 172 |
var exprParts = expr.split(/\s+/); |
| 173 |
while (exprParts.length < 5) exprParts.push('*'); |
| 174 |
|
| 175 |
root.innerHTML = ''; |
| 176 |
|
| 177 |
var wrap = document.createElement('div'); |
| 178 |
wrap.className = 'bkbg-cb-wrap'; |
| 179 |
wrap.style.cssText = 'background:' + a.sectionBg + ';max-width:' + a.contentMaxWidth + 'px;margin:0 auto;'; |
| 180 |
root.appendChild(wrap); |
| 181 |
|
| 182 |
typoCssVarsForEl(a.typoTitle, '--bkbg-cb-ttl-', wrap); |
| 183 |
typoCssVarsForEl(a.typoSubtitle, '--bkbg-cb-sub-', wrap); |
| 184 |
|
| 185 |
if (a.showTitle) { |
| 186 |
var h = document.createElement('div'); |
| 187 |
h.className = 'bkbg-cb-title'; |
| 188 |
h.style.color = a.titleColor; |
| 189 |
h.textContent = a.title; |
| 190 |
wrap.appendChild(h); |
| 191 |
} |
| 192 |
|
| 193 |
if (a.showSubtitle) { |
| 194 |
var s = document.createElement('div'); |
| 195 |
s.className = 'bkbg-cb-subtitle'; |
| 196 |
s.style.color = a.subtitleColor; |
| 197 |
s.textContent = a.subtitle; |
| 198 |
wrap.appendChild(s); |
| 199 |
} |
| 200 |
|
| 201 |
// Presets |
| 202 |
if (a.showPresets) { |
| 203 |
var presetsEl = document.createElement('div'); |
| 204 |
presetsEl.className = 'bkbg-cb-presets'; |
| 205 |
presetsEl.style.background = a.cardBg; |
| 206 |
PRESETS.forEach(function (p) { |
| 207 |
var btn = document.createElement('button'); |
| 208 |
btn.className = 'bkbg-cb-preset-btn'; |
| 209 |
btn.textContent = p.label; |
| 210 |
btn.title = p.expr; |
| 211 |
updatePresetStyle(btn, p.expr === expr); |
| 212 |
btn.addEventListener('click', function () { |
| 213 |
expr = p.expr; |
| 214 |
exprParts = expr.split(/\s+/); |
| 215 |
updateInputs(); |
| 216 |
renderAll(); |
| 217 |
presetsEl.querySelectorAll('.bkbg-cb-preset-btn').forEach(function (b) { |
| 218 |
updatePresetStyle(b, b.title === expr); |
| 219 |
}); |
| 220 |
}); |
| 221 |
presetsEl.appendChild(btn); |
| 222 |
}); |
| 223 |
wrap.appendChild(presetsEl); |
| 224 |
} |
| 225 |
|
| 226 |
function updatePresetStyle(btn, active) { |
| 227 |
btn.style.cssText = 'background:' + (active ? a.accentColor : '#e5e7eb') + ';color:' + (active ? '#fff' : '#374151') + ';'; |
| 228 |
} |
| 229 |
|
| 230 |
// Fields card |
| 231 |
var fieldsCard = document.createElement('div'); |
| 232 |
fieldsCard.className = 'bkbg-cb-card'; |
| 233 |
fieldsCard.style.background = a.cardBg; |
| 234 |
wrap.appendChild(fieldsCard); |
| 235 |
|
| 236 |
var FIELD_DEFS = [ |
| 237 |
{ label: 'Minute', min: 0, max: 59 }, |
| 238 |
{ label: 'Hour', min: 0, max: 23 }, |
| 239 |
{ label: 'Day', min: 1, max: 31 }, |
| 240 |
{ label: 'Month', min: 1, max: 12 }, |
| 241 |
{ label: 'Weekday', min: 0, max: 7 } |
| 242 |
]; |
| 243 |
|
| 244 |
var fieldsRow = document.createElement('div'); |
| 245 |
fieldsRow.className = 'bkbg-cb-fields'; |
| 246 |
fieldsCard.appendChild(fieldsRow); |
| 247 |
|
| 248 |
var inputs = []; |
| 249 |
|
| 250 |
FIELD_DEFS.forEach(function (def, idx) { |
| 251 |
var col = document.createElement('div'); |
| 252 |
col.className = 'bkbg-cb-field-col'; |
| 253 |
|
| 254 |
var inp = document.createElement('input'); |
| 255 |
inp.type = 'text'; |
| 256 |
inp.className = 'bkbg-cb-field-input'; |
| 257 |
inp.value = exprParts[idx] || '*'; |
| 258 |
inp.style.cssText = 'background:' + a.fieldBg + ';color:' + a.accentColor + ';'; |
| 259 |
inp.setAttribute('aria-label', def.label); |
| 260 |
inp.title = def.label + ' (' + def.min + '-' + def.max + ')'; |
| 261 |
|
| 262 |
inp.addEventListener('input', function () { |
| 263 |
exprParts[idx] = inp.value || '*'; |
| 264 |
expr = exprParts.join(' '); |
| 265 |
renderAll(); |
| 266 |
}); |
| 267 |
|
| 268 |
inputs.push(inp); |
| 269 |
|
| 270 |
var lbl = document.createElement('div'); |
| 271 |
lbl.className = 'bkbg-cb-field-label'; |
| 272 |
lbl.style.color = a.subtitleColor; |
| 273 |
lbl.textContent = def.label; |
| 274 |
|
| 275 |
col.appendChild(inp); |
| 276 |
col.appendChild(lbl); |
| 277 |
fieldsRow.appendChild(col); |
| 278 |
}); |
| 279 |
|
| 280 |
function updateInputs() { |
| 281 |
inputs.forEach(function (inp, idx) { |
| 282 |
inp.value = exprParts[idx] || '*'; |
| 283 |
}); |
| 284 |
} |
| 285 |
|
| 286 |
// Expression display |
| 287 |
var exprDisplay = document.createElement('div'); |
| 288 |
exprDisplay.className = 'bkbg-cb-expr-display'; |
| 289 |
exprDisplay.style.cssText = 'background:' + a.fieldBg + ';color:' + a.accentColor + ';'; |
| 290 |
fieldsCard.appendChild(exprDisplay); |
| 291 |
|
| 292 |
// Error display |
| 293 |
var errorEl = document.createElement('div'); |
| 294 |
errorEl.className = 'bkbg-cb-error'; |
| 295 |
errorEl.style.display = 'none'; |
| 296 |
fieldsCard.appendChild(errorEl); |
| 297 |
|
| 298 |
// Actions |
| 299 |
var actionsRow = document.createElement('div'); |
| 300 |
actionsRow.className = 'bkbg-cb-actions'; |
| 301 |
var copyBtn = document.createElement('button'); |
| 302 |
copyBtn.className = 'bkbg-cb-btn'; |
| 303 |
copyBtn.style.background = a.accentColor; |
| 304 |
copyBtn.textContent = 'Copy Expression'; |
| 305 |
var copyMsg = document.createElement('div'); |
| 306 |
copyMsg.className = 'bkbg-cb-copy-msg'; |
| 307 |
copyMsg.style.color = a.accentColor; |
| 308 |
copyMsg.textContent = '✓ Copied to clipboard!'; |
| 309 |
|
| 310 |
copyBtn.addEventListener('click', function () { |
| 311 |
navigator.clipboard && navigator.clipboard.writeText(expr).catch(function () { |
| 312 |
var t = document.createElement('textarea'); |
| 313 |
t.value = expr; document.body.appendChild(t); t.select(); |
| 314 |
document.execCommand('copy'); document.body.removeChild(t); |
| 315 |
}); |
| 316 |
copyMsg.classList.add('bkbg-cb-show'); |
| 317 |
setTimeout(function () { copyMsg.classList.remove('bkbg-cb-show'); }, 2000); |
| 318 |
}); |
| 319 |
|
| 320 |
actionsRow.appendChild(copyBtn); |
| 321 |
fieldsCard.appendChild(actionsRow); |
| 322 |
fieldsCard.appendChild(copyMsg); |
| 323 |
|
| 324 |
// Description card |
| 325 |
var descCard = null, descText = null; |
| 326 |
if (a.showDescription) { |
| 327 |
descCard = document.createElement('div'); |
| 328 |
descCard.className = 'bkbg-cb-desc-card'; |
| 329 |
descCard.style.cssText = 'background:' + a.cardBg + ';border-left-color:' + a.accentColor + ';'; |
| 330 |
|
| 331 |
var descLabel = document.createElement('div'); |
| 332 |
descLabel.className = 'bkbg-cb-desc-label'; |
| 333 |
descLabel.style.color = a.subtitleColor; |
| 334 |
descLabel.textContent = 'DESCRIPTION'; |
| 335 |
descCard.appendChild(descLabel); |
| 336 |
|
| 337 |
descText = document.createElement('div'); |
| 338 |
descText.className = 'bkbg-cb-desc-text'; |
| 339 |
descText.style.color = a.labelColor; |
| 340 |
descCard.appendChild(descText); |
| 341 |
wrap.appendChild(descCard); |
| 342 |
} |
| 343 |
|
| 344 |
// Next runs card |
| 345 |
var nextCard = null, nextList = null; |
| 346 |
if (a.showNextRuns) { |
| 347 |
nextCard = document.createElement('div'); |
| 348 |
nextCard.className = 'bkbg-cb-card'; |
| 349 |
nextCard.style.background = a.cardBg; |
| 350 |
|
| 351 |
var nextTitle = document.createElement('div'); |
| 352 |
nextTitle.className = 'bkbg-cb-next-title'; |
| 353 |
nextTitle.style.color = a.labelColor; |
| 354 |
nextTitle.textContent = 'Next Run Times'; |
| 355 |
nextCard.appendChild(nextTitle); |
| 356 |
|
| 357 |
nextList = document.createElement('div'); |
| 358 |
nextCard.appendChild(nextList); |
| 359 |
wrap.appendChild(nextCard); |
| 360 |
} |
| 361 |
|
| 362 |
function renderAll() { |
| 363 |
// Update expression display |
| 364 |
exprDisplay.textContent = exprParts.join(' '); |
| 365 |
|
| 366 |
// Validate |
| 367 |
var valid = [ |
| 368 |
validateField(exprParts[0], 0, 59), |
| 369 |
validateField(exprParts[1], 0, 23), |
| 370 |
validateField(exprParts[2], 1, 31), |
| 371 |
validateField(exprParts[3], 1, 12), |
| 372 |
validateField(exprParts[4], 0, 7) |
| 373 |
]; |
| 374 |
|
| 375 |
var allValid = valid.every(Boolean); |
| 376 |
inputs.forEach(function (inp, idx) { |
| 377 |
inp.style.borderColor = valid[idx] ? 'transparent' : '#ef4444'; |
| 378 |
}); |
| 379 |
|
| 380 |
if (!allValid) { |
| 381 |
errorEl.style.display = 'block'; |
| 382 |
errorEl.textContent = 'Invalid expression — check highlighted fields'; |
| 383 |
} else { |
| 384 |
errorEl.style.display = 'none'; |
| 385 |
} |
| 386 |
|
| 387 |
// Description |
| 388 |
if (descText) { |
| 389 |
descText.textContent = describe(exprParts.join(' ')); |
| 390 |
} |
| 391 |
|
| 392 |
// Next runs |
| 393 |
if (nextList) { |
| 394 |
nextList.innerHTML = ''; |
| 395 |
if (allValid) { |
| 396 |
var runs = nextRuns(exprParts.join(' '), parseInt(a.nextRunsCount) || 5); |
| 397 |
runs.forEach(function (d) { |
| 398 |
var item = document.createElement('div'); |
| 399 |
item.className = 'bkbg-cb-next-item'; |
| 400 |
item.style.color = a.labelColor; |
| 401 |
|
| 402 |
var arrow = document.createElement('span'); |
| 403 |
arrow.className = 'bkbg-cb-next-arrow'; |
| 404 |
arrow.style.color = a.accentColor; |
| 405 |
arrow.textContent = '→'; |
| 406 |
|
| 407 |
var dateText = document.createElement('span'); |
| 408 |
dateText.textContent = d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }); |
| 409 |
|
| 410 |
var rel = document.createElement('span'); |
| 411 |
rel.className = 'bkbg-cb-next-relative'; |
| 412 |
rel.textContent = relativeTime(d); |
| 413 |
|
| 414 |
[arrow, dateText, rel].forEach(function (el) { item.appendChild(el); }); |
| 415 |
nextList.appendChild(item); |
| 416 |
}); |
| 417 |
if (runs.length === 0) { |
| 418 |
var noRun = document.createElement('div'); |
| 419 |
noRun.style.cssText = 'color:' + a.subtitleColor + ';font-size:13px;padding:8px 0;'; |
| 420 |
noRun.textContent = 'No upcoming runs found in the next year.'; |
| 421 |
nextList.appendChild(noRun); |
| 422 |
} |
| 423 |
} else { |
| 424 |
var errNote = document.createElement('div'); |
| 425 |
errNote.style.cssText = 'color:#ef4444;font-size:13px;padding:8px 0;'; |
| 426 |
errNote.textContent = 'Fix the expression to see next run times.'; |
| 427 |
nextList.appendChild(errNote); |
| 428 |
} |
| 429 |
} |
| 430 |
} |
| 431 |
|
| 432 |
renderAll(); |
| 433 |
} |
| 434 |
|
| 435 |
function init() { |
| 436 |
document.querySelectorAll('.bkbg-cb-app').forEach(initBlock); |
| 437 |
} |
| 438 |
|
| 439 |
if (document.readyState === 'loading') { |
| 440 |
document.addEventListener('DOMContentLoaded', init); |
| 441 |
} else { |
| 442 |
init(); |
| 443 |
} |
| 444 |
})(); |
| 445 |
|