| 1 |
/* Category Grid — frontend */ |
| 2 |
(function () { |
| 3 |
'use strict'; |
| 4 |
|
| 5 |
function typoCssVarsForEl(typo, prefix, el) { |
| 6 |
if (!typo) return; |
| 7 |
if (typo.family) el.style.setProperty(prefix + 'font-family', "'" + typo.family + "', sans-serif"); |
| 8 |
if (typo.weight) el.style.setProperty(prefix + 'font-weight', typo.weight); |
| 9 |
if (typo.transform) el.style.setProperty(prefix + 'text-transform', typo.transform); |
| 10 |
if (typo.style) el.style.setProperty(prefix + 'font-style', typo.style); |
| 11 |
if (typo.decoration) el.style.setProperty(prefix + 'text-decoration', typo.decoration); |
| 12 |
var su = typo.sizeUnit || 'px'; |
| 13 |
if (typo.sizeDesktop !== '' && typo.sizeDesktop != null) el.style.setProperty(prefix + 'font-size-d', typo.sizeDesktop + su); |
| 14 |
if (typo.sizeTablet !== '' && typo.sizeTablet != null) el.style.setProperty(prefix + 'font-size-t', typo.sizeTablet + su); |
| 15 |
if (typo.sizeMobile !== '' && typo.sizeMobile != null) el.style.setProperty(prefix + 'font-size-m', typo.sizeMobile + su); |
| 16 |
var lhu = typo.lineHeightUnit || ''; |
| 17 |
if (typo.lineHeightDesktop !== '' && typo.lineHeightDesktop != null) el.style.setProperty(prefix + 'line-height-d', typo.lineHeightDesktop + lhu); |
| 18 |
if (typo.lineHeightTablet !== '' && typo.lineHeightTablet != null) el.style.setProperty(prefix + 'line-height-t', typo.lineHeightTablet + lhu); |
| 19 |
if (typo.lineHeightMobile !== '' && typo.lineHeightMobile != null) el.style.setProperty(prefix + 'line-height-m', typo.lineHeightMobile + lhu); |
| 20 |
var lsu = typo.letterSpacingUnit || 'px'; |
| 21 |
if (typo.letterSpacingDesktop !== '' && typo.letterSpacingDesktop != null) el.style.setProperty(prefix + 'letter-spacing-d', typo.letterSpacingDesktop + lsu); |
| 22 |
if (typo.letterSpacingTablet !== '' && typo.letterSpacingTablet != null) el.style.setProperty(prefix + 'letter-spacing-t', typo.letterSpacingTablet + lsu); |
| 23 |
if (typo.letterSpacingMobile !== '' && typo.letterSpacingMobile != null) el.style.setProperty(prefix + 'letter-spacing-m', typo.letterSpacingMobile + lsu); |
| 24 |
var wsu = typo.wordSpacingUnit || 'px'; |
| 25 |
if (typo.wordSpacingDesktop !== '' && typo.wordSpacingDesktop != null) el.style.setProperty(prefix + 'word-spacing-d', typo.wordSpacingDesktop + wsu); |
| 26 |
if (typo.wordSpacingTablet !== '' && typo.wordSpacingTablet != null) el.style.setProperty(prefix + 'word-spacing-t', typo.wordSpacingTablet + wsu); |
| 27 |
if (typo.wordSpacingMobile !== '' && typo.wordSpacingMobile != null) el.style.setProperty(prefix + 'word-spacing-m', typo.wordSpacingMobile + wsu); |
| 28 |
} |
| 29 |
|
| 30 |
/* ── emoji for categories (hash-based stable assignment) ─────────────────── */ |
| 31 |
var DEFAULT_EMOJIS = ['📁', '📂', '🗂️', '📌', '🔖', '🏷️', '💡', '✏️', '📝', '📊', '🎯', '🔍']; |
| 32 |
|
| 33 |
function emojiForName(name) { |
| 34 |
var hash = 0; |
| 35 |
for (var i = 0; i < name.length; i++) { |
| 36 |
hash = (hash * 31 + name.charCodeAt(i)) | 0; |
| 37 |
} |
| 38 |
return DEFAULT_EMOJIS[Math.abs(hash) % DEFAULT_EMOJIS.length]; |
| 39 |
} |
| 40 |
|
| 41 |
function getCatColor(index, colorMode, multiColors, accent) { |
| 42 |
if (colorMode === 'multi') { |
| 43 |
var cols = (multiColors || '').split(',').map(function (c) { return c.trim(); }).filter(Boolean); |
| 44 |
if (cols.length) { return cols[index % cols.length]; } |
| 45 |
} |
| 46 |
return accent || '#6c3fb5'; |
| 47 |
} |
| 48 |
|
| 49 |
/* ── build a single card ──────────────────────────────────────────────────── */ |
| 50 |
function buildCard(cat, index, cfg) { |
| 51 |
var accent = getCatColor(index, cfg.colorMode, cfg.multiColors, cfg.accent); |
| 52 |
var emoji = emojiForName(cat.name); |
| 53 |
var showBar = cfg.cardStyle === 'card-accent'; |
| 54 |
var isCircle = cfg.cardStyle === 'icon-circle'; |
| 55 |
var isLeft = cfg.iconPos === 'left'; |
| 56 |
|
| 57 |
var a = document.createElement('a'); |
| 58 |
a.className = 'bkcg-card'; |
| 59 |
a.href = cat.link || '#'; |
| 60 |
a.target = cfg.linkTarget || '_self'; |
| 61 |
if (cfg.linkTarget === '_blank') { a.rel = 'noopener noreferrer'; } |
| 62 |
|
| 63 |
Object.assign(a.style, { |
| 64 |
background: cfg.cardBg, |
| 65 |
borderRadius: cfg.cardRadius + 'px', |
| 66 |
padding: cfg.cardPadding + 'px', |
| 67 |
boxShadow: cfg.cardShadow ? '0 2px 16px rgba(0,0,0,0.08)' : 'none', |
| 68 |
border: cfg.cardBorder ? '1px solid ' + cfg.borderColor : 'none', |
| 69 |
textAlign: cfg.textAlign, |
| 70 |
display: 'flex', |
| 71 |
flexDirection: isLeft ? 'row' : 'column', |
| 72 |
alignItems: isLeft ? 'flex-start' : (cfg.textAlign === 'center' ? 'center' : 'flex-start'), |
| 73 |
gap: isLeft ? '12px' : '8px', |
| 74 |
textDecoration: 'none', |
| 75 |
color: 'inherit', |
| 76 |
position: 'relative', |
| 77 |
overflow: 'hidden', |
| 78 |
}); |
| 79 |
|
| 80 |
a.setAttribute('data-bg-hover', cfg.cardBgHover); |
| 81 |
a.setAttribute('data-bg', cfg.cardBg); |
| 82 |
|
| 83 |
/* accent top bar */ |
| 84 |
if (showBar) { |
| 85 |
var bar = document.createElement('div'); |
| 86 |
bar.style.cssText = 'position:absolute;top:0;left:0;right:0;height:3px;background:' + accent; |
| 87 |
a.appendChild(bar); |
| 88 |
} |
| 89 |
|
| 90 |
/* icon */ |
| 91 |
if (cfg.showIcon) { |
| 92 |
var iconWrap = document.createElement('div'); |
| 93 |
iconWrap.className = 'bkcg-icon'; |
| 94 |
Object.assign(iconWrap.style, { |
| 95 |
fontSize: cfg.iconSize + 'px', |
| 96 |
lineHeight: '1', |
| 97 |
flexShrink: '0', |
| 98 |
background: isCircle ? accent + '18' : 'transparent', |
| 99 |
width: isCircle ? (cfg.iconSize * 1.8) + 'px' : undefined, |
| 100 |
height: isCircle ? (cfg.iconSize * 1.8) + 'px' : undefined, |
| 101 |
borderRadius: isCircle ? '50%' : undefined, |
| 102 |
display: isCircle ? 'flex' : 'block', |
| 103 |
alignItems: 'center', |
| 104 |
justifyContent: 'center', |
| 105 |
}); |
| 106 |
var useChar = !cfg.iconType || cfg.iconType === 'custom-char'; |
| 107 |
if (useChar) { |
| 108 |
iconWrap.textContent = emoji; |
| 109 |
} else { |
| 110 |
var IP = window.bkbgIconPicker; |
| 111 |
var iconEl = IP && IP.buildFrontendIcon(cfg.iconType, null, cfg.iconDashicon, cfg.iconImageUrl, cfg.iconDashiconColor); |
| 112 |
if (iconEl) iconWrap.appendChild(iconEl); |
| 113 |
else iconWrap.textContent = emoji; |
| 114 |
} |
| 115 |
a.appendChild(iconWrap); |
| 116 |
} |
| 117 |
|
| 118 |
/* body */ |
| 119 |
var body = document.createElement('div'); |
| 120 |
body.className = 'bkcg-body'; |
| 121 |
body.style.flex = '1'; |
| 122 |
body.style.minWidth = '0'; |
| 123 |
|
| 124 |
var nameEl = document.createElement('p'); |
| 125 |
nameEl.className = 'bkcg-name'; |
| 126 |
nameEl.textContent = cat.name; |
| 127 |
Object.assign(nameEl.style, { |
| 128 |
margin: '0 0 4px', color: cfg.nameColor |
| 129 |
}); |
| 130 |
body.appendChild(nameEl); |
| 131 |
|
| 132 |
if (cfg.showCount) { |
| 133 |
var countEl = document.createElement('span'); |
| 134 |
countEl.className = 'bkcg-count'; |
| 135 |
countEl.textContent = cat.count + ' ' + (cfg.countLabel || 'posts'); |
| 136 |
Object.assign(countEl.style, { |
| 137 |
display: 'inline-block', background: cfg.countBg, color: cfg.countColor, |
| 138 |
borderRadius: '999px', padding: '1px 8px', fontSize: '11px', fontWeight: '600', marginBottom: '6px' |
| 139 |
}); |
| 140 |
body.appendChild(countEl); |
| 141 |
} |
| 142 |
|
| 143 |
if (cfg.showDesc && cat.description) { |
| 144 |
var descEl = document.createElement('p'); |
| 145 |
descEl.className = 'bkcg-desc'; |
| 146 |
var desc = cat.description.replace(/<[^>]+>/g, '').slice(0, cfg.descLen || 80); |
| 147 |
descEl.textContent = desc; |
| 148 |
Object.assign(descEl.style, { |
| 149 |
margin: '4px 0 0', color: cfg.descColor |
| 150 |
}); |
| 151 |
body.appendChild(descEl); |
| 152 |
} |
| 153 |
|
| 154 |
if (cfg.showArrow) { |
| 155 |
var arrow = document.createElement('span'); |
| 156 |
arrow.className = 'bkcg-arrow'; |
| 157 |
arrow.textContent = '→'; |
| 158 |
Object.assign(arrow.style, { display: 'block', marginTop: '8px', color: accent, fontSize: '18px' }); |
| 159 |
body.appendChild(arrow); |
| 160 |
} |
| 161 |
|
| 162 |
a.appendChild(body); |
| 163 |
return a; |
| 164 |
} |
| 165 |
|
| 166 |
/* ── hover effect ────────────────────────────────────────────────────────── */ |
| 167 |
function attachHover(grid, cfg) { |
| 168 |
if (grid.getAttribute('data-animate') !== '1') { return; } |
| 169 |
|
| 170 |
grid.addEventListener('mouseover', function (e) { |
| 171 |
var card = e.target.closest('.bkcg-card'); |
| 172 |
if (!card) { return; } |
| 173 |
card.style.background = cfg.cardBgHover; |
| 174 |
}); |
| 175 |
|
| 176 |
grid.addEventListener('mouseout', function (e) { |
| 177 |
var card = e.target.closest('.bkcg-card'); |
| 178 |
if (!card) { return; } |
| 179 |
card.style.background = cfg.cardBg; |
| 180 |
}); |
| 181 |
} |
| 182 |
|
| 183 |
/* ── read all config from data attributes ─────────────────────────────────── */ |
| 184 |
function parseConfig(grid) { |
| 185 |
return { |
| 186 |
max: parseInt(grid.getAttribute('data-max'), 10) || 9, |
| 187 |
orderBy: grid.getAttribute('data-order-by') || 'count', |
| 188 |
order: grid.getAttribute('data-order') || 'desc', |
| 189 |
hideEmpty: grid.getAttribute('data-hide-empty') !== '0', |
| 190 |
exclude: grid.getAttribute('data-exclude') || '', |
| 191 |
include: grid.getAttribute('data-include') || '', |
| 192 |
columns: parseInt(grid.getAttribute('data-columns'), 10) || 3, |
| 193 |
gap: parseInt(grid.getAttribute('data-gap'), 10) || 16, |
| 194 |
cardStyle: grid.getAttribute('data-card-style') || 'card', |
| 195 |
iconPos: grid.getAttribute('data-icon-pos') || 'top', |
| 196 |
showIcon: grid.getAttribute('data-show-icon') !== '0', |
| 197 |
iconSize: parseInt(grid.getAttribute('data-icon-size'), 10) || 36, |
| 198 |
iconType: grid.getAttribute('data-icon-type') || 'custom-char', |
| 199 |
iconDashicon: grid.getAttribute('data-icon-dashicon') || '', |
| 200 |
iconImageUrl: grid.getAttribute('data-icon-image-url') || '', |
| 201 |
showCount: grid.getAttribute('data-show-count') !== '0', |
| 202 |
countLabel: grid.getAttribute('data-count-label') || 'posts', |
| 203 |
showDesc: grid.getAttribute('data-show-desc') === '1', |
| 204 |
descLen: parseInt(grid.getAttribute('data-desc-len'), 10) || 80, |
| 205 |
showArrow: grid.getAttribute('data-show-arrow') !== '0', |
| 206 |
linkTarget: grid.getAttribute('data-link-target') || '_self', |
| 207 |
textAlign: grid.getAttribute('data-text-align') || 'center', |
| 208 |
colorMode: grid.getAttribute('data-color-mode') || 'single', |
| 209 |
multiColors: grid.getAttribute('data-multi-colors') || '', |
| 210 |
accent: grid.getAttribute('data-accent') || '#6c3fb5', |
| 211 |
nameColor: grid.getAttribute('data-name-color') || '#1e1e1e', |
| 212 |
nameSize: parseInt(grid.getAttribute('data-name-size'), 10) || 16, |
| 213 |
countColor: grid.getAttribute('data-count-color') || '#6c3fb5', |
| 214 |
countBg: grid.getAttribute('data-count-bg') || '#ede9fe', |
| 215 |
descColor: grid.getAttribute('data-desc-color') || '#6b7280', |
| 216 |
descSize: parseInt(grid.getAttribute('data-desc-size'), 10) || 13, |
| 217 |
cardBg: grid.getAttribute('data-card-bg') || '#ffffff', |
| 218 |
cardBgHover: grid.getAttribute('data-card-bg-hover') || '#f5f3ff', |
| 219 |
cardRadius: parseInt(grid.getAttribute('data-card-radius'), 10) || 12, |
| 220 |
cardPadding: parseInt(grid.getAttribute('data-card-padding'), 10) || 24, |
| 221 |
cardShadow: grid.getAttribute('data-card-shadow') !== '0', |
| 222 |
cardBorder: grid.getAttribute('data-card-border') === '1', |
| 223 |
borderColor: grid.getAttribute('data-border-color') || '#e5e7eb', |
| 224 |
}; |
| 225 |
} |
| 226 |
|
| 227 |
/* ── main init ───────────────────────────────────────────────────────────── */ |
| 228 |
function init() { |
| 229 |
document.querySelectorAll('.bkcg-grid[data-max]').forEach(function (grid) { |
| 230 |
if (grid._bkcgInit) { return; } |
| 231 |
grid._bkcgInit = true; |
| 232 |
|
| 233 |
var cfg = parseConfig(grid); |
| 234 |
|
| 235 |
/* apply grid CSS */ |
| 236 |
grid.style.gridTemplateColumns = 'repeat(' + cfg.columns + ', 1fr)'; |
| 237 |
grid.style.gap = cfg.gap + 'px'; |
| 238 |
|
| 239 |
/* typography CSS vars */ |
| 240 |
var typoName = {}; |
| 241 |
var typoDesc = {}; |
| 242 |
try { typoName = JSON.parse(grid.getAttribute('data-typo-name') || '{}'); } catch (e) {} |
| 243 |
try { typoDesc = JSON.parse(grid.getAttribute('data-typo-desc') || '{}'); } catch (e) {} |
| 244 |
typoCssVarsForEl(typoName, '--bkcg-n-', grid); |
| 245 |
typoCssVarsForEl(typoDesc, '--bkcg-d-', grid); |
| 246 |
|
| 247 |
/* build API URL */ |
| 248 |
var apiBase = (window.wpApiSettings && window.wpApiSettings.root) |
| 249 |
? window.wpApiSettings.root.replace(/\/$/, '') |
| 250 |
: '/wp-json'; |
| 251 |
|
| 252 |
var params = [ |
| 253 |
'per_page=' + cfg.max, |
| 254 |
'orderby=' + cfg.orderBy, |
| 255 |
'order=' + cfg.order, |
| 256 |
cfg.hideEmpty ? 'hide_empty=true' : '', |
| 257 |
cfg.exclude ? 'exclude=' + cfg.exclude : '', |
| 258 |
cfg.include ? 'include=' + cfg.include : '', |
| 259 |
'_fields=id,name,count,description,link', |
| 260 |
].filter(Boolean).join('&'); |
| 261 |
|
| 262 |
var url = apiBase + '/wp/v2/categories?' + params; |
| 263 |
|
| 264 |
fetch(url) |
| 265 |
.then(function (r) { return r.json(); }) |
| 266 |
.then(function (cats) { |
| 267 |
grid.innerHTML = ''; |
| 268 |
cats.forEach(function (cat, idx) { |
| 269 |
var card = buildCard({ |
| 270 |
name: cat.name, |
| 271 |
count: cat.count, |
| 272 |
description: cat.description ? cat.description.replace(/<[^>]+>/g, '') : '', |
| 273 |
link: cat.link || '#', |
| 274 |
}, idx, cfg); |
| 275 |
grid.appendChild(card); |
| 276 |
}); |
| 277 |
attachHover(grid, cfg); |
| 278 |
}) |
| 279 |
.catch(function () { |
| 280 |
grid.innerHTML = '<p style="color:#888;text-align:center;padding:24px;">Could not load categories.</p>'; |
| 281 |
}); |
| 282 |
}); |
| 283 |
} |
| 284 |
|
| 285 |
if (document.readyState === 'loading') { |
| 286 |
document.addEventListener('DOMContentLoaded', init); |
| 287 |
} else { |
| 288 |
init(); |
| 289 |
} |
| 290 |
}()); |
| 291 |
|