PageSourceSearch

https://railsblocks.com/assets/controllers/datatable_preview_contr…1998b89b2eeb56c67a72a786bce23b39f48fa1ee.js

js railsblocks.com collected 2026-09-25 20:23:06 UTC 6,789 bytes, 192 lines download raw bytes

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.