1import { Controller } from "@hotwired/stimulus"; 2 3/** 4 * Client-side sorting and pagination controller for datatable previews. 5 * Sorts and paginates table rows in the browser without server requests. 6 */ 7export default class extends Controller { 8 static targets = ["header", "body", "info", "pageInfo", "prevBtn", "nextBtn"]; 9 10 static values = { 11 sortColumn: { type: String, default: "name" }, 12 sortDirection: { type: String, default: "asc" }, 13 page: { type: Number, default: 1 }, 14 perPage: { type: Number, default: 3 }, 15 total: { type: Number, default: 0 }, 16 }; 17 18 connect() { 19 this.allRows = Array.from(this.bodyTarget.querySelectorAll("tr")); 20 this.updateDisplay(); 21 } 22 23 sort(event) { 24 const header = event.currentTarget; 25 const column = header.dataset.column; 26 27 if (!column) return; 28 29 // Toggle direction if clicking the same column 30 const direction = this.sortColumnValue === column && this.sortDirectionValue === "asc" ? "desc" : "asc"; 31 32 // Update values 33 this.sortColumnValue = column; 34 this.sortDirectionValue = direction; 35 36 // Reset to page 1 when sorting 37 this.pageValue = 1; 38 39 // Update visual indicators 40 this.updateIndicators(); 41 42 // Sort the rows 43 this.sortRows(column, direction); 44 45 // Update pagination display 46 this.updateDisplay(); 47 } 48 49 prevPage() { 50 if (this.pageValue > 1) { 51 this.pageValue--; 52 this.updateDisplay(); 53 } 54 } 55 56 nextPage() { 57 const totalPages = Math.ceil(this.totalValue / this.perPageValue); 58 if (this.pageValue < totalPages) { 59 this.pageValue++; 60 this.updateDisplay(); 61 } 62 } 63 64 updateDisplay() { 65 const start = (this.pageValue - 1) * this.perPageValue; 66 const end = start + this.perPageValue; 67 const totalPages = Math.ceil(this.totalValue / this.perPageValue); 68 69 // Show/hide rows based on current page 70 this.allRows.forEach((row, index) => { 71 if (index >= start && index < end) { 72 row.classList.remove("hidden"); 73 } else { 74 row.classList.add("hidden"); 75 } 76 }); 77 78 // Update row borders for visible rows 79 this.updateRowBorders(); 80 81 // Update info text 82 if (this.hasInfoTarget) { 83 const displayStart = start + 1; 84 const displayEnd = Math.min(end, this.totalValue); 85 this.infoTarget.textContent = `${displayStart}â${displayEnd} of ${this.totalValue}`; 86 } 87 88 // Update page info 89 if (this.hasPageInfoTarget) { 90 this.pageInfoTarget.textContent = `${this.pageValue} / ${totalPages}`; 91 } 92 93 // Update button states 94 if (this.hasPrevBtnTarget) { 95 this.prevBtnTarget.disabled = this.pageValue <= 1; 96 this.updateButtonStyle(this.prevBtnTarget, this.pageValue <= 1); 97 } 98 99 if (this.hasNextBtnTarget) { 100 this.nextBtnTarget.disabled = this.pageValue >= totalPages; 101 this.updateButtonStyle(this.nextBtnTarget, this.pageValue >= totalPages); 102 } 103 } 104 105 updateButtonStyle(button, isDisabled) { 106 if (isDisabled) { 107 button.classList.remove("text-neutral-600", "dark:text-neutral-300"); 108 button.classList.add("text-neutral-400", "dark:text-neutral-500"); 109 } else { 110 button.classList.remove("text-neutral-400", "dark:text-neutral-500"); 111 button.classList.add("text-neutral-600", "dark:text-neutral-300"); 112 } 113 } 114 115 updateRowBorders() { 116 const visibleRows = this.allRows.filter((row) => !row.classList.contains("hidden")); 117 118 visibleRows.forEach((row, index) => { 119 // Remove existing border classes 120 row.classList.remove( 121 "[box-shadow:inset_0_-1px_0_0_rgb(229_229_229)]", 122 "dark:[box-shadow:inset_0_-1px_0_0_rgb(46_46_46)]", 123 ); 124 125 // Add border to all but last visible row 126 if (index < visibleRows.length - 1) { 127 row.classList.add( 128 "[box-shadow:inset_0_-1px_0_0_rgb(229_229_229)]", 129 "dark:[box-shadow:inset_0_-1px_0_0_rgb(46_46_46)]", 130 ); 131 } 132 }); 133 } 134 135 updateIndicators() { 136 this.headerTargets.forEach((header) => { 137 const column = header.dataset.column; 138 const indicatorContainer = header.querySelector(".sort-indicator-mini"); 139 const indicator = indicatorContainer?.querySelector("[data-datatable-preview-target='indicator']"); 140 141 if (!indicatorContainer) return; 142 143 const isSorted = column === this.sortColumnValue; 144 145 // Update container styling 146 if (isSorted) { 147 indicatorContainer.classList.remove("text-neutral-400", "dark:text-neutral-500"); 148 indicatorContainer.classList.add("text-neutral-900", "dark:text-white"); 149 } else { 150 indicatorContainer.classList.remove("text-neutral-900", "dark:text-white"); 151 indicatorContainer.classList.add("text-neutral-400", "dark:text-neutral-500"); 152 } 153 154 // Update the SVG icon 155 if (indicator) { 156 if (isSorted && this.sortDirectionValue === "asc") { 157 indicator.innerHTML = `<svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> 158 <path fill-rule="evenodd" d="M14.77 12.79a.75.75 0 01-1.06-.02L10 8.832 6.29 12.77a.75.75 0 11-1.08-1.04l4.25-4.5a.75.75 0 011.08 0l4.25 4.5a.75.75 0 01-.02 1.06z" clip-rule="evenodd"></path> 159 </svg>`; 160 } else if (isSorted && this.sortDirectionValue === "desc") { 161 indicator.innerHTML = `<svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> 162 <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"></path> 163 </svg>`; 164 } else { 165 indicator.innerHTML = `<svg class="size-2.5 opacity-50 group-hover:opacity-100 transition-opacity" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> 166 <path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd"></path> 167 </svg>`; 168 } 169 } 170 }); 171 } 172 173 sortRows(column, direction) { 174 if (!this.hasBodyTarget) return; 175 176 const tbody = this.bodyTarget; 177 178 this.allRows.sort((a, b) => { 179 const aValue = (a.dataset[column] || "").toLowerCase(); 180 const bValue = (b.dataset[column] || "").toLowerCase(); 181 182 if (aValue < bValue) return direction === "asc" ? -1 : 1; 183 if (aValue > bValue) return direction === "asc" ? 1 : -1; 184 return 0; 185 }); 186 187 // Re-append rows in sorted order 188 this.allRows.forEach((row) => { 189 tbody.appendChild(row); 190 }); 191 } 192};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.