PageSourceSearch

https://shsselect.com/assets/controllers/school_search_controller-6173dbeb.js

js shsselect.com collected 2026-09-24 18:25:04 UTC 7,230 bytes, 185 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2import { debounce } from "utils/debounce"
3
4export default class extends Controller {
5  static targets = ["item", "noResults", "showMoreContainer", "listContainer", "totalCount", "sortSelect", "searchInput"]
6  static values = { initialItems: Number }
7
8  connect() {
9    console.log("School search controller connected with", this.itemTargets.length, "items");
10    this.searchQuery = "";
11    this.sortCriteria = this.hasSortSelectTarget ? this.sortSelectTarget.value : 'score_desc';
12    
13    // Set the default sort select value to match our default sortCriteria
14    if (this.hasSortSelectTarget) {
15      this.sortSelectTarget.value = 'score_desc';
16    }
17    
18    this.itemsPerBatch = Number.MAX_SAFE_INTEGER; // Show all items at once, effectively disabling pagination
19    this.currentlyVisibleItems = 0;
20    
21    // Store all items as an array of objects with data for sorting
22    this.allSchoolData = this.itemTargets.map(item => {
23      return {
24        element: item,
25        name: item.dataset.name || "",
26        region: item.dataset.region || "",
27        category: item.dataset.category || this.extractCategory(item),
28        score: parseFloat(item.dataset.score || 0)
29      };
30    });
31
32    console.log("Initial data loaded:", this.allSchoolData.length, "schools");
33    this.debouncedFilter = debounce(this.applyFiltersAndSort.bind(this), 300);
34    
35    // Wait for next frame to apply initial rendering
36    // This gives the browser time to properly layout elements first
37    requestAnimationFrame(() => {
38      this.applyFiltersAndSort(); // Initial render
39    });
40    
41    // Hide the show more button since we're showing all items
42    if (this.hasShowMoreContainerTarget) {
43      this.showMoreContainerTarget.classList.add('hidden');
44    }
45  }
46
47  extractCategory(itemElement) {
48    // Use data-category attribute if present
49    if (itemElement.dataset.category) {
50      return itemElement.dataset.category.toUpperCase();
51    }
52    
53    // Fallback to extracting from HTML content
54    const categorySpan = itemElement.querySelector('span[class*="bg-accent-"]'); 
55    if (categorySpan && categorySpan.textContent) {
56      const match = categorySpan.textContent.match(/Category\s+([A-Z0-9_-]+)/i);
57      return match ? match[1].toUpperCase() : "Z";
58    }
59    return "Z";
60  }
61
62  filter(event) {
63    console.log("Search input:", event.target.value);
64    this.searchQuery = event.target.value.toLowerCase();
65    this.debouncedFilter();
66  }
67
68  sortResults() {
69    this.sortCriteria = this.sortSelectTarget.value;
70    this.applyFiltersAndSort();
71  }
72
73  clearSearchAndFilters() {
74    if (this.hasSearchInputTarget) {
75      this.searchInputTarget.value = '';
76    }
77    this.searchQuery = '';
78    if (this.hasSortSelectTarget) {
79      this.sortSelectTarget.value = 'score_desc'; // Reset to default sort
80    }
81    this.sortCriteria = 'score_desc';
82    this.applyFiltersAndSort();
83  }
84
85  applyFiltersAndSort() {
86    console.log("Applying filters with search query:", this.searchQuery);
87    this.currentlyVisibleItems = 0; // Reset for new filter/sort application
88    let itemsToProcess = [...this.allSchoolData]; // Start with a copy of all data
89
90    // 1. Apply Search Filter
91    if (this.searchQuery) {
92      const beforeCount = itemsToProcess.length;
93      itemsToProcess = itemsToProcess.filter(school => 
94        school.name.includes(this.searchQuery) || school.region.includes(this.searchQuery)
95      );
96      console.log(`Filtered from ${beforeCount} to ${itemsToProcess.length} schools matching "${this.searchQuery}"`);
97    }
98
99    // 2. Apply Sorting
100    const [sortKey, sortOrder] = this.sortCriteria.split('_');
101    itemsToProcess.sort((a, b) => {
102      let valA = a[sortKey];
103      let valB = b[sortKey];
104
105      // Basic case-insensitive string sort
106      if (typeof valA === 'string' && typeof valB === 'string') {
107        valA = valA.toLowerCase();
108        valB = valB.toLowerCase();
109      }
110
111      if (valA < valB) return sortOrder === 'asc' ? -1 : 1;
112      if (valA > valB) return sortOrder === 'asc' ? 1 : -1;
113      return 0;
114    });
115
116    this.renderVisibleItems(itemsToProcess);
117    this.updateTotalCountDisplayAfterFilter(itemsToProcess);
118  }
119
120  renderVisibleItems(sortedAndFilteredSchoolData) {
121    // Hide all original DOM elements first
122    this.allSchoolData.forEach(school => school.element.classList.add('hidden'));
123    
124    // Detach all items from their current parents (listContainers
124)
125    // and re-append them in the new sorted order to the correct container.
126    const mobileListContainer = this.listContainerTargets.find(c => c.id === 'schoolsListMobile');
127    const desktopListContainer = this.listContainerTargets.find(c => !c.id); // Desktop tbody has no id
128
129    if (mobileListContainer) mobileListContainer.innerHTML = ''; // Clear previous items
130    if (desktopListContainer) desktopListContainer.innerHTML = ''; // Clear previous items
131
132    let itemsShownThisRenderCycle = 0;
133    
134    // Show all items at once - no pagination
135    sortedAndFilteredSchoolData.forEach((schoolData) => {
136      schoolData.element.classList.remove('hidden');
137      
138      if (mobileListContainer && schoolData.element.tagName !== 'TR') { 
139          mobileListContainer.appendChild(schoolData.element);
140      } else if (desktopListContainer && schoolData.element.tagName === 'TR') { 
141          desktopListContainer.appendChild(schoolData.element);
142      }
143      itemsShownThisRenderCycle++;
144    });
145    
146    this.currentlyVisibleItems = itemsShownThisRenderCycle;
147
148    const hasAnyMatchingItems = sortedAndFilteredSchoolData.length > 0;
149    const isInitialLoadAndEmpty = this.allSchoolData.length === 0;
150
151    if (this.hasNoResultsTarget) {
152      // Show "No Results" if: EITHER the initial data from server was empty, 
153      // OR if there are no items matching the current client-side filters/search.
154      const showNoResults = isInitialLoadAndEmpty || (!hasAnyMatchingItems && (this.searchQuery !== '' || this.allSchoolData.length > 0));
155      this.noResultsTarget.classList.toggle('hidden', !showNoResults);
156    }
157
158    // Always hide the "Show More" button since we're showing all items
159    if (this.hasShowMoreContainerTarget) {
160      this.showMoreContainerTarget.classList.add('hidden');
161    }
162  }
163
164  updateTotalCountDisplayAfterFilter(filteredItems) {
165    if (this.hasTotalCountTarget) {
166      const totalMatching = filteredItems.length;
167      const visibleNow = totalMatching; // All items are visible now
168      let textContent = ``;
169      if (this.allSchoolData.length === 0) {
170        textContent = "(0 schools found)";
171      } else if (this.searchQuery || (this.sortCriteria !== 'score_desc' && this.allSchoolData.length !== totalMatching)) {
172        textContent = `(${totalMatching} schools matching criteria - Total ${this.allSchoolData.length} schools)`;
173      } else {
174        textContent = `(${this.allSchoolData.length} schools)`;
175      }
176      this.totalCountTarget.textContent = textContent;
177      }
178  }
179  
180  // We can keep this method for compatibility, but it won't be used
181  showMore() {
182    // Method left for compatibility, but we're now showing all items at once
183    console.log("Show more clicked - but all items are already visible");
184  }
185} 

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.