| @@ -1,0 +1,386 @@ | ||
| 1 | +"use strict"; | |
| 2 | +(function ($) { | |
| 3 | + $(window).on('elementor/frontend/init', function () { | |
| 4 | + elementorFrontend.hooks.addAction('frontend/element_ready/king-addons-data-table.default', function ($scope) { | |
| 5 | + // Add elementor handler | |
| 6 | + elementorFrontend.elementsHandler.addHandler( | |
| 7 | + elementorModules.frontend.handlers.Base.extend({ | |
| 8 | + onInit: function onInit() { | |
| 9 | + const $scope = this.$element; | |
| 10 | + const $tableContainer = $scope.find('.king-addons-table-inner-container'); | |
| 11 | + const $tableBody = $tableContainer.find('tbody'); | |
| 12 | + const initialRows = $tableBody.find('tr'); | |
| 13 | + const paginationListItems = $scope.find('.king-addons-table-custom-pagination-list-item'); | |
| 14 | + let beforeFilter = $tableBody.find('.king-addons-table-row'); | |
| 15 | + let value = ""; | |
| 16 | + | |
| 17 | + // Initialize Perfect Scrollbar if needed | |
| 18 | + new PerfectScrollbar($tableContainer[0], {}); | |
| 19 | + | |
| 20 | + // Helper: get number of items per page (as integer) | |
| 21 | + const getItemsPerPage = () => +$tableContainer.attr('data-rows-per-page') || 0; | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * Updates the table with the filtered set of rows for a given page index (1-based). | |
| 25 | + * Also optionally checks the live-search value for resetting the table if empty. | |
| 26 | + */ | |
| 27 | + const displayRowsForPage = (pageIndex, checkSearch) => { | |
| 28 | + // Hide table while updating | |
| 29 | + $tableBody.hide(); | |
| 30 | + | |
| 31 | + const itemsPerPage = getItemsPerPage(); | |
| 32 | + // Filter rows that should appear on the given page | |
| 33 | + const newRows = initialRows.filter((i) => { | |
| 34 | + // Original code uses index++ inside filter; equivalently use (i+1) here | |
| 35 | + const rowNumber = i + 1; | |
| 36 | + return ( | |
| 37 | + rowNumber > itemsPerPage * (pageIndex - 1) && | |
| 38 | + rowNumber <= itemsPerPage * pageIndex | |
| 39 | + ); | |
| 40 | + }); | |
| 41 | + | |
| 42 | + // Update the table with these new rows | |
| 43 | + $tableBody.html(newRows); | |
| 44 | + | |
| 45 | + // If checkSearch is true and the current search is empty, revert back to beforeFilter | |
| 46 | + if (checkSearch && value === "") { | |
| 47 | + $tableBody.html(beforeFilter); | |
| 48 | + } | |
| 49 | + | |
| 50 | + $tableBody.show(); | |
| 51 | + // Refresh the 'beforeFilter' cache | |
| 52 | + beforeFilter = $tableBody.find('.king-addons-table-row'); | |
| 53 | + // Remove highlight classes if any | |
| 54 | + beforeFilter | |
| 55 | + .find('.king-addons-table-tr-before-remove') | |
| 56 | + .removeClass('king-addons-table-tr-before-remove'); | |
| 57 | + | |
| 58 | + updateEntryInfo(); | |
| 59 | + }; | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * Removes 'active' class from all pagination items, | |
| 63 | + * sets the correct item as active, and adjusts their visibility. | |
| 64 | + */ | |
| 65 | + const setActivePaginationItem = (pageIndex) => { | |
| 66 | + paginationListItems.removeClass('king-addons-active-pagination-item'); | |
| 67 | + paginationListItems.each((i, el) => { | |
| 68 | + // Page indices in UI are basically i+1 if user numbered them from 1..n | |
| 69 | + if (i + 1 === pageIndex) { | |
| 70 | + $(el).addClass('king-addons-active-pagination-item'); | |
| 71 | + } | |
| 72 | + }); | |
| 73 | + adjustPaginationList(); | |
| 74 | + }; | |
| 75 | + | |
| 76 | + /** | |
| 77 | + * Called by previous/next clicks to move forward/back by `delta` pages. | |
| 78 | + * If the new page is out of range, does nothing. | |
| 79 | + */ | |
| 80 | + const changePage = (delta) => { | |
| 81 | + const currentPage = +$scope.find('.king-addons-active-pagination-item').text() || 1; | |
| 82 | + const totalPages = paginationListItems.length; | |
| 83 | + const newPage = currentPage + delta; | |
| 84 | + if (newPage < 1 || newPage > totalPages) return; | |
| 85 | + | |
| 86 | + setActivePaginationItem(newPage); | |
| 87 | + // For Prev/Next we do want to check if search is empty | |
| 88 | + displayRowsForPage(newPage, true); | |
| 89 | + }; | |
| 90 | + | |
| 91 | + /** | |
| 92 | + * Adjusts pagination list items to show/hide neighbors correctly. | |
| 93 | + */ | |
| 94 | + const adjustPaginationList = () => { | |
| 95 | + const paginationIndex = $scope.find('.king-addons-active-pagination-item').index(); | |
| 96 | + paginationListItems.each((i, el) => { | |
| 97 | + // Always display the first/last, and the 2 neighbors around current item | |
| 98 | + if ( | |
| 99 | + i === 0 || | |
| 100 | + i === paginationListItems.length - 1 || | |
| 101 | + (i <= paginationIndex && i >= paginationIndex - 2) | |
| 102 | + ) { | |
| 103 | + $(el).css('display', 'flex'); | |
| 104 | + } else { | |
| 105 | + $(el).css('display', 'none'); | |
| 106 | + } | |
| 107 | + }); | |
| 108 | + }; | |
| 109 | + | |
| 110 | + /** | |
| 111 | + * Updates the "Showing X to Y of Z Entries" text (if enabled). | |
| 112 | + */ | |
| 113 | + const updateEntryInfo = () => { | |
| 114 | + if ($tableContainer.attr('data-entry-info') !== 'yes') return; | |
| 115 | + | |
| 116 | + const itemsPerPage = getItemsPerPage(); | |
| 117 | + const entryPage = +$scope.find('.king-addons-active-pagination-item').text() || 1; | |
| 118 | + const rowsOnPage = $tableBody.find('tr').length; | |
| 119 | + | |
| 120 | + const lastEntry = itemsPerPage * entryPage - (itemsPerPage - rowsOnPage); | |
| 121 | + const firstEntry = lastEntry - rowsOnPage + 1; | |
| 122 | + | |
| 123 | + const info = `Showing ${firstEntry} to ${lastEntry} of ${initialRows.length} Entries.`; | |
| 124 | + $scope.find('.king-addons-entry-info').html(info); | |
| 125 | + }; | |
| 126 | + | |
| 127 | + /** | |
| 128 | + * Live search logic: filters rows by matching text in any table cell. | |
| 129 | + */ | |
| 130 | + const initLiveSearch = () => { | |
| 131 | + $scope.find(".king-addons-table-live-search").on("keyup", function () { | |
| 132 | + value = this.value.toLowerCase().trim(); | |
| 133 | + | |
| 134 | + if (value !== "") { | |
| 135 | + $scope.find('.king-addons-table-pagination-cont') | |
| 136 | + .addClass('king-addons-hide-pagination-on-search'); | |
| 137 | + } else { | |
| 138 | + $scope.find('.king-addons-table-pagination-cont') | |
| 139 | + .removeClass('king-addons-hide-pagination-on-search'); | |
| 140 | + } | |
| 141 | + | |
| 142 | + const filteredRows = []; | |
| 143 | + initialRows.each((i, row) => { | |
| 144 | + const $row = $(row); | |
| 145 | + $row.find("td").each((_, cell) => { | |
| 146 | + const cellText = $(cell).text().toLowerCase().trim(); | |
| 147 | + // If found a match, add row to `filteredRows` once | |
| 148 | + if (cellText.indexOf(value) !== -1) { | |
| 149 | + filteredRows.push($row); | |
| 150 | + return false; // break out of .each loop for this row | |
| 151 | + } | |
| 152 | + }); | |
| 153 | + }); | |
| 154 | + | |
| 155 | + // If user cleared search, revert to all filtered items | |
| 156 | + $tableBody.html(value === "" ? beforeFilter : filteredRows); | |
| 157 | + updateEntryInfo(); | |
| 158 | + }); | |
| 159 | + }; | |
| 160 | + | |
| 161 | + /** | |
| 162 | + * Table sorting logic by clicking on <th>. | |
| 163 | + */ | |
| 164 | + const initTableSorting = () => { | |
| 165 | + // Only if data-table-sorting is 'yes' | |
| 166 | + if ($tableContainer.attr('data-table-sorting') !== 'yes') return; | |
| 167 | + | |
| 168 | + // Remove highlight class if user clicks outside any TH or highlight cell | |
| 169 | + $(window).on('click', (e) => { | |
| 170 | + const isTh = $(e.target).hasClass('king-addons-table-th'); | |
| 171 | + const inTh = $(e.target).closest('.king-addons-table-th').length > 0; | |
| 172 | + const isActiveTd = $(e.target).hasClass('king-addons-active-td-bg-color'); | |
| 173 | + const inActiveTd = $(e.target).closest('.king-addons-active-td-bg-color').length > 0; | |
| 174 | + | |
| 175 | + if (!isTh && !inTh && !isActiveTd && !inActiveTd) { | |
| 176 | + $scope.find('td.king-addons-active-td-bg-color').removeClass('king-addons-active-td-bg-color'); | |
| 177 | + } | |
| 178 | + }); | |
| 179 | + | |
| 180 | + const getCellValue = (row, idx) => | |
| 181 | + $(row).children('td').eq(idx).text(); | |
| 182 | + | |
| 183 | + const comparer = (idx) => (a, b) => { | |
| 184 | + const valA = getCellValue(a, idx); | |
| 185 | + const valB = getCellValue(b, idx); | |
| 186 | + // Numeric vs. text comparison | |
| 187 | + if ($.isNumeric(valA) && $.isNumeric(valB)) { | |
| 188 | + return valA - valB; | |
| 189 | + } | |
| 190 | + return valA.toString().localeCompare(valB); | |
| 191 | + }; | |
| 192 | + | |
| 193 | + // Handle clicks on <th> for sorting | |
| 194 | + $scope.find('th').on('click', function () { | |
| 195 | + const $this = $(this); | |
| 196 | + const indexOfTr = $this.index(); | |
| 197 | + | |
| 198 | + // Highlight the column's cells | |
| 199 | + $scope.find('td').each(function () { | |
| 200 | + $(this).toggleClass( | |
| 201 | + 'king-addons-active-td-bg-color', | |
| 202 | + $(this).index() === indexOfTr | |
| 203 | + ); | |
| 204 | + }); | |
| 205 | + | |
| 206 | + // Reset the sorting icon on every TH | |
| 207 | + $scope.find('th .king-addons-sorting-icon').html('<i class="fas fa-sort" aria-hidden="true"></i>'); | |
| 208 | + | |
| 209 | + // Sort the rows | |
| 210 | + const table = $this.closest('table'); | |
| 211 | + let rows = table.find('tr:gt(0)').toArray().sort(comparer(indexOfTr)); | |
| 212 | + | |
| 213 | + // Toggle ascending/descending | |
| 214 | + this.asc = !this.asc; | |
| 215 | + const isCustom = $scope.hasClass('king-addons-data-table-type-custom'); | |
| 216 | + | |
| 217 | + if ((isCustom && !this.asc) || (!isCustom && this.asc)) { | |
| 218 | + // Sort reversed | |
| 219 | + rows = rows.reverse(); | |
| 220 | + $this.find('.king-addons-sorting-icon').html( | |
| 221 | + isCustom | |
| 222 | + ? '<i class="fas fa-sort-down" aria-hidden="true"></i>' | |
| 223 | + : '<i class="fas fa-sort-up" aria-hidden="true"></i>' | |
| 224 | + ); | |
| 225 | + } else { | |
| 226 | + // Normal ascending | |
| 227 | + $this.find('.king-addons-sorting-icon').html( | |
| 228 | + isCustom | |
| 229 | + ? '<i class="fas fa-sort-up" aria-hidden="true"></i>' | |
| 230 | + : '<i class="fas fa-sort-down" aria-hidden="true"></i>' | |
| 231 | + ); | |
| 232 | + } | |
| 233 | + | |
| 234 | + // Re-append sorted rows | |
| 235 | + for (let i = 0; i < rows.length; i++) { | |
| 236 | + table.append(rows[i]); | |
| 237 | + } | |
| 238 | + | |
| 239 | + // Remove leftover classes from "expanded" or appended rows | |
| 240 | + beforeFilter.find('.king-addons-table-tr-before-remove').each(function () { | |
| 241 | + $(this) | |
| 242 | + .closest('.king-addons-table-row') | |
| 243 | + .next('.king-addons-table-appended-tr') | |
| 244 | + .remove(); | |
| 245 | + $(this).removeClass('king-addons-table-tr-before-remove'); | |
| 246 | + }); | |
| 247 | + }); | |
| 248 | + }; | |
| 249 | + | |
| 250 | + /** | |
| 251 | + * If data-row-pagination="yes", prepend an index column (#). | |
| 252 | + */ | |
| 253 | + const initRowPagination = () => { | |
| 254 | + if ($tableContainer.attr('data-row-pagination') !== 'yes') return; | |
| 255 | + | |
| 256 | + $scope.find('.king-addons-table-head-row').prepend( | |
| 257 | + '<th class="king-addons-table-th-pag" style="vertical-align: middle;">#</th>' | |
| 258 | + ); | |
| 259 | + initialRows.each(function (index) { | |
| 260 | + $(this).prepend( | |
| 261 | + `<td class="king-addons-table-td-pag" style="vertical-align: middle;"> | |
| 262 | + <span style="vertical-align: middle;">${index + 1}</span> | |
| 263 | + </td>` | |
| 264 | + ); | |
| 265 | + }); | |
| 266 | + }; | |
| 267 | + | |
| 268 | + /** | |
| 269 | + * Export buttons logic (CSV/XLS). | |
| 270 | + */ | |
| 271 | + const initTableExport = () => { | |
| 272 | + if (!$scope.find('.king-addons-table-export-button-cont').length) return; | |
| 273 | + | |
| 274 | + const exportBtn = $scope.find('.king-addons-table-export-button-cont .king-addons-button'); | |
| 275 | + | |
| 276 | + const csvEscape = (value) => { | |
| 277 | + const text = String(value == null ? '' : value).replace(/\r?\n/g, ' ').trim(); | |
| 278 | + if (/[",]/.test(text)) { | |
| 279 | + return '"' + text.replace(/"/g, '""') + '"'; | |
| 280 | + } | |
| 281 | + return text; | |
| 282 | + }; | |
| 283 | + | |
| 284 | + const tableRowsToCSV = ($view) => { | |
| 285 | + const rows = $view.find(".king-addons-table-row"); | |
| 286 | + const data = []; | |
| 287 | + rows.each((_, row) => { | |
| 288 | + const cols = row.querySelectorAll(".king-addons-table-text"); | |
| 289 | + const rowData = Array.from(cols).map(cell => csvEscape(cell.innerText)); | |
| 290 | + data.push(rowData.join(",")); | |
| 291 | + }); | |
| 292 | + return data.join("\r\n"); | |
| 293 | + }; | |
| 294 | + | |
| 295 | + const triggerDownload = (blob, filename) => { | |
| 296 | + const url = URL.createObjectURL(blob); | |
| 297 | + const downloadLink = document.createElement("a"); | |
| 298 | + downloadLink.download = filename; | |
| 299 | + downloadLink.href = url; | |
| 300 | + downloadLink.rel = "noopener"; | |
| 301 | + downloadLink.style.position = "fixed"; | |
| 302 | + downloadLink.style.left = "-9999px"; | |
| 303 | + document.body.appendChild(downloadLink); | |
| 304 | + downloadLink.click(); | |
| 305 | + setTimeout(() => { | |
| 306 | + URL.revokeObjectURL(url); | |
| 307 | + downloadLink.remove(); | |
| 308 | + }, 1500); | |
| 309 | + }; | |
| 310 | + | |
| 311 | + exportBtn.each(function () { | |
| 312 | + const $btn = $(this); | |
| 313 | + if ($btn.hasClass('king-addons-xls')) { | |
| 314 | + $btn.on('click', (event) => { | |
| 315 | + event.preventDefault(); | |
| 316 | + const table = $scope.find('table').get(0); | |
| 317 | + if (!table) { | |
| 318 | + return; | |
| 319 | + } | |
| 320 | + const html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="UTF-8"></head><body>' + table.outerHTML + '</body></html>'; | |
| 321 | + triggerDownload(new Blob(['\uFEFF' + html], { type: 'application/vnd.ms-excel' }), 'export.xls'); | |
| 322 | + }); | |
| 323 | + } else if ($btn.hasClass('king-addons-csv')) { | |
| 324 | + $btn.on('click', (event) => { | |
| 325 | + event.preventDefault(); | |
| 326 | + const csv = tableRowsToCSV($scope.find('.king-addons-data-table')); | |
| 327 | + triggerDownload(new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }), 'export.csv'); | |
| 328 | + }); | |
| 329 | + } | |
| 330 | + }); | |
| 331 | + }; | |
| 332 | + | |
| 333 | + /** | |
| 334 | + * Initializes custom pagination if enabled. | |
| 335 | + */ | |
| 336 | + const initCustomPagination = () => { | |
| 337 | + if ($tableContainer.attr('data-custom-pagination') !== 'yes') return; | |
| 338 | + | |
| 339 | + // Show only the first set of rows (page 1) initially | |
| 340 | + const itemsPerPage = getItemsPerPage(); | |
| 341 | + const firstPageRows = initialRows.filter((i) => i < itemsPerPage); | |
| 342 | + $tableBody.html(firstPageRows); | |
| 343 | + | |
| 344 | + adjustPaginationList(); | |
| 345 | + | |
| 346 | + // When clicking on a specific page number | |
| 347 | + paginationListItems.on('click', function () { | |
| 348 | + // Remove the old active, set the new active | |
| 349 | + paginationListItems.removeClass('king-addons-active-pagination-item'); | |
| 350 | + $(this).addClass('king-addons-active-pagination-item'); | |
| 351 | + adjustPaginationList(); | |
| 352 | + | |
| 353 | + const pageIndex = +$(this).text(); | |
| 354 | + // For direct page clicks, we do NOT revert to beforeFilter if search is empty | |
| 355 | + displayRowsForPage(pageIndex, false); | |
| 356 | + }); | |
| 357 | + | |
| 358 | + // Handle prev/next clicks | |
| 359 | + $scope.find('.king-addons-table-prev-next').each(function () { | |
| 360 | + if ($(this).hasClass('king-addons-table-custom-pagination-prev')) { | |
| 361 | + $(this).on('click', () => changePage(-1)); | |
| 362 | + } else { | |
| 363 | + $(this).on('click', () => changePage(1)); | |
| 364 | + } | |
| 365 | + }); | |
| 366 | + }; | |
| 367 | + | |
| 368 | + // --------------------------------------------------- | |
| 369 | + // Execute initializations | |
| 370 | + // --------------------------------------------------- | |
| 371 | + initCustomPagination(); | |
| 372 | + initLiveSearch(); | |
| 373 | + initTableSorting(); | |
| 374 | + initRowPagination(); | |
| 375 | + initTableExport(); | |
| 376 | + | |
| 377 | + // Finally, remove "hidden" class so the table becomes visible | |
| 378 | + $tableContainer.removeClass('king-addons-hide-table-before-arrange'); | |
| 379 | + updateEntryInfo(); | |
| 380 | + }, | |
| 381 | + }), | |
| 382 | + { $element: $scope } | |
| 383 | + ); | |
| 384 | + }); | |
| 385 | + }); | |
| 386 | +})(jQuery); | |