| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
function toArray(nl) { |
| 5 |
return Array.prototype.slice.call(nl || []); |
| 6 |
} |
| 7 |
|
| 8 |
function initDataTable(wrap) { |
| 9 |
var searchInput = wrap.querySelector('[data-search]'); |
| 10 |
var table = wrap.querySelector('.bkbg-dt-table'); |
| 11 |
var thead = table ? table.querySelector('.bkbg-dt-thead') : null; |
| 12 |
var tbody = table ? table.querySelector('.bkbg-dt-tbody') : null; |
| 13 |
var headers = thead ? toArray(thead.querySelectorAll('.bkbg-dt-th')) : []; |
| 14 |
var allRows = tbody ? toArray(tbody.querySelectorAll('.bkbg-dt-tr')) : []; |
| 15 |
|
| 16 |
var sortable = wrap.getAttribute('data-sortable') === '1'; |
| 17 |
var paginate = wrap.getAttribute('data-paginate') === '1'; |
| 18 |
var perPage = parseInt(wrap.getAttribute('data-per-page'), 10) || 10; |
| 19 |
|
| 20 |
var currentSort = { col: -1, dir: 'asc' }; |
| 21 |
var currentPage = 1; |
| 22 |
var filteredRows = allRows.slice(); |
| 23 |
|
| 24 |
// Search functionality |
| 25 |
function filterRows(query) { |
| 26 |
query = (query || '').toLowerCase().trim(); |
| 27 |
if (!query) { |
| 28 |
filteredRows = allRows.slice(); |
| 29 |
} else { |
| 30 |
filteredRows = allRows.filter(function (row) { |
| 31 |
var text = row.textContent.toLowerCase(); |
| 32 |
return text.indexOf(query) !== -1; |
| 33 |
}); |
| 34 |
} |
| 35 |
currentPage = 1; |
| 36 |
render(); |
| 37 |
} |
| 38 |
|
| 39 |
// Sort functionality |
| 40 |
function sortRows(colIndex) { |
| 41 |
if (currentSort.col === colIndex) { |
| 42 |
currentSort.dir = currentSort.dir === 'asc' ? 'desc' : 'asc'; |
| 43 |
} else { |
| 44 |
currentSort.col = colIndex; |
| 45 |
currentSort.dir = 'asc'; |
| 46 |
} |
| 47 |
|
| 48 |
// Update header UI |
| 49 |
headers.forEach(function (th, i) { |
| 50 |
th.removeAttribute('data-sort'); |
| 51 |
if (i === colIndex) { |
| 52 |
th.setAttribute('data-sort', currentSort.dir); |
| 53 |
} |
| 54 |
}); |
| 55 |
|
| 56 |
filteredRows.sort(function (a, b) { |
| 57 |
var cellA = a.children[colIndex]; |
| 58 |
var cellB = b.children[colIndex]; |
| 59 |
if (!cellA || !cellB) return 0; |
| 60 |
|
| 61 |
var valA = cellA.textContent.trim(); |
| 62 |
var valB = cellB.textContent.trim(); |
| 63 |
|
| 64 |
// Try numeric sort |
| 65 |
var numA = parseFloat(valA.replace(/[^0-9.-]/g, '')); |
| 66 |
var numB = parseFloat(valB.replace(/[^0-9.-]/g, '')); |
| 67 |
|
| 68 |
if (!isNaN(numA) && !isNaN(numB)) { |
| 69 |
return currentSort.dir === 'asc' ? numA - numB : numB - numA; |
| 70 |
} |
| 71 |
|
| 72 |
// String sort |
| 73 |
var cmp = valA.localeCompare(valB); |
| 74 |
return currentSort.dir === 'asc' ? cmp : -cmp; |
| 75 |
}); |
| 76 |
|
| 77 |
currentPage = 1; |
| 78 |
render(); |
| 79 |
} |
| 80 |
|
| 81 |
// Pagination |
| 82 |
function renderPagination() { |
| 83 |
var existing = wrap.querySelector('.bkbg-dt-pagination'); |
| 84 |
if (existing) existing.remove(); |
| 85 |
|
| 86 |
if (!paginate || filteredRows.length <= perPage) return; |
| 87 |
|
| 88 |
var totalPages = Math.ceil(filteredRows.length / perPage); |
| 89 |
var pagination = document.createElement('div'); |
| 90 |
pagination.className = 'bkbg-dt-pagination'; |
| 91 |
|
| 92 |
// Previous button |
| 93 |
var prevBtn = document.createElement('button'); |
| 94 |
prevBtn.className = 'bkbg-dt-page-btn'; |
| 95 |
prevBtn.textContent = '←'; |
| 96 |
prevBtn.disabled = currentPage === 1; |
| 97 |
prevBtn.addEventListener('click', function () { |
| 98 |
if (currentPage > 1) { |
| 99 |
currentPage--; |
| 100 |
render(); |
| 101 |
} |
| 102 |
}); |
| 103 |
pagination.appendChild(prevBtn); |
| 104 |
|
| 105 |
// Page buttons |
| 106 |
var startPage = Math.max(1, currentPage - 2); |
| 107 |
var endPage = Math.min(totalPages, startPage + 4); |
| 108 |
if (endPage - startPage < 4) { |
| 109 |
startPage = Math.max(1, endPage - 4); |
| 110 |
} |
| 111 |
|
| 112 |
for (var i = startPage; i <= endPage; i++) { |
| 113 |
(function (page) { |
| 114 |
var btn = document.createElement('button'); |
| 115 |
btn.className = 'bkbg-dt-page-btn' + (page === currentPage ? ' is-active' : ''); |
| 116 |
btn.textContent = page; |
| 117 |
btn.addEventListener('click', function () { |
| 118 |
currentPage = page; |
| 119 |
render(); |
| 120 |
}); |
| 121 |
pagination.appendChild(btn); |
| 122 |
})(i); |
| 123 |
} |
| 124 |
|
| 125 |
// Next button |
| 126 |
var nextBtn = document.createElement('button'); |
| 127 |
nextBtn.className = 'bkbg-dt-page-btn'; |
| 128 |
nextBtn.textContent = '→'; |
| 129 |
nextBtn.disabled = currentPage === totalPages; |
| 130 |
nextBtn.addEventListener('click', function () { |
| 131 |
if (currentPage < totalPages) { |
| 132 |
currentPage++; |
| 133 |
render(); |
| 134 |
} |
| 135 |
}); |
| 136 |
pagination.appendChild(nextBtn); |
| 137 |
|
| 138 |
// Info |
| 139 |
var info = document.createElement('span'); |
| 140 |
info.className = 'bkbg-dt-page-info'; |
| 141 |
var start = (currentPage - 1) * perPage + 1; |
| 142 |
var end = Math.min(currentPage * perPage, filteredRows.length); |
| 143 |
info.textContent = start + '-' + end + ' of ' + filteredRows.length; |
| 144 |
pagination.appendChild(info); |
| 145 |
|
| 146 |
wrap.appendChild(pagination); |
| 147 |
} |
| 148 |
|
| 149 |
// Render visible rows |
| 150 |
function render() { |
| 151 |
if (!tbody) return; |
| 152 |
|
| 153 |
// Reorder DOM elements to match sorted order |
| 154 |
filteredRows.forEach(function (row) { |
| 155 |
tbody.appendChild(row); |
| 156 |
}); |
| 157 |
|
| 158 |
// Hide all rows first |
| 159 |
allRows.forEach(function (row) { |
| 160 |
row.style.display = 'none'; |
| 161 |
}); |
| 162 |
|
| 163 |
// Show filtered & paginated |
| 164 |
var start = paginate ? (currentPage - 1) * perPage : 0; |
| 165 |
var end = paginate ? start + perPage : filteredRows.length; |
| 166 |
|
| 167 |
for (var i = start; i < end && i < filteredRows.length; i++) { |
| 168 |
filteredRows[i].style.display = ''; |
| 169 |
} |
| 170 |
|
| 171 |
// Empty state |
| 172 |
var emptyEl = wrap.querySelector('.bkbg-dt-empty'); |
| 173 |
if (filteredRows.length === 0) { |
| 174 |
if (!emptyEl) { |
| 175 |
emptyEl = document.createElement('div'); |
| 176 |
emptyEl.className = 'bkbg-dt-empty'; |
| 177 |
emptyEl.textContent = 'No results found'; |
| 178 |
wrap.querySelector('.bkbg-dt-container').appendChild(emptyEl); |
| 179 |
} |
| 180 |
emptyEl.style.display = ''; |
| 181 |
} else if (emptyEl) { |
| 182 |
emptyEl.style.display = 'none'; |
| 183 |
} |
| 184 |
|
| 185 |
renderPagination(); |
| 186 |
} |
| 187 |
|
| 188 |
// Export to CSV |
| 189 |
function exportCsv() { |
| 190 |
function csvCell(text) { |
| 191 |
var s = String(text == null ? '' : text); |
| 192 |
// Normalize whitespace/newlines for CSV rows. |
| 193 |
s = s.replace(/\r?\n/g, ' ').trim(); |
| 194 |
// Prevent Excel/Sheets formula injection when opening CSV. |
| 195 |
// See: values starting with = + - @ can be treated as formulas. |
| 196 |
if (/^[=+\-@]/.test(s)) { |
| 197 |
s = "'" + s; |
| 198 |
} |
| 199 |
s = s.replace(/"/g, '""'); |
| 200 |
return '"' + s + '"'; |
| 201 |
} |
| 202 |
|
| 203 |
var headerRow = headers.map(function (th) { |
| 204 |
return csvCell(th.textContent); |
| 205 |
}).join(','); |
| 206 |
|
| 207 |
var dataRows = filteredRows.map(function (row) { |
| 208 |
return toArray(row.children).map(function (td) { |
| 209 |
return csvCell(td.textContent); |
| 210 |
}).join(','); |
| 211 |
}); |
| 212 |
|
| 213 |
var csv = [headerRow].concat(dataRows).join('\n'); |
| 214 |
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); |
| 215 |
var url = URL.createObjectURL(blob); |
| 216 |
var link = document.createElement('a'); |
| 217 |
link.href = url; |
| 218 |
link.download = 'table-data.csv'; |
| 219 |
link.click(); |
| 220 |
URL.revokeObjectURL(url); |
| 221 |
} |
| 222 |
|
| 223 |
// Event listeners |
| 224 |
if (searchInput) { |
| 225 |
var debounceTimer; |
| 226 |
searchInput.addEventListener('input', function () { |
| 227 |
clearTimeout(debounceTimer); |
| 228 |
debounceTimer = setTimeout(function () { |
| 229 |
filterRows(searchInput.value); |
| 230 |
}, 200); |
| 231 |
}); |
| 232 |
} |
| 233 |
|
| 234 |
if (sortable) { |
| 235 |
headers.forEach(function (th, index) { |
| 236 |
th.addEventListener('click', function () { |
| 237 |
sortRows(index); |
| 238 |
}); |
| 239 |
th.style.cursor = 'pointer'; |
| 240 |
}); |
| 241 |
} |
| 242 |
|
| 243 |
var exportBtn = wrap.querySelector('[data-export="csv"]'); |
| 244 |
if (exportBtn) { |
| 245 |
exportBtn.addEventListener('click', function (e) { |
| 246 |
e.preventDefault(); |
| 247 |
exportCsv(); |
| 248 |
}); |
| 249 |
} |
| 250 |
|
| 251 |
// Initial render |
| 252 |
render(); |
| 253 |
} |
| 254 |
|
| 255 |
function init() { |
| 256 |
var tables = document.querySelectorAll('.bkbg-dt-wrap:not([data-initialized])'); |
| 257 |
for (var i = 0; i < tables.length; i++) { |
| 258 |
tables[i].setAttribute('data-initialized', '1'); |
| 259 |
initDataTable(tables[i]); |
| 260 |
} |
| 261 |
} |
| 262 |
|
| 263 |
// Initialize on DOMContentLoaded |
| 264 |
if (document.readyState === 'loading') { |
| 265 |
document.addEventListener('DOMContentLoaded', init); |
| 266 |
} else { |
| 267 |
init(); |
| 268 |
} |
| 269 |
|
| 270 |
// Also try on window load as fallback |
| 271 |
window.addEventListener('load', init); |
| 272 |
})(); |
| 273 |
|