-
Notifications
You must be signed in to change notification settings - Fork 11
autocomplete so far, needs html fixes #1186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
1d5c6bf
d3c8869
eb56903
f3e3470
c05c6ed
a5167c4
a08e602
e65d41f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| document.addEventListener("DOMContentLoaded", function () { | ||
| const searchInput = document.getElementById("search-input"); | ||
| if (!searchInput) return; | ||
|
|
||
| // Create container for autocomplete suggestions | ||
| const suggestionsContainer = document.createElement("div"); | ||
| suggestionsContainer.classList.add("autocomplete-suggestions"); | ||
|
|
||
| const searchbarWrapper = searchInput.closest(".searchbar-wrapper"); | ||
| if (searchbarWrapper) { | ||
| searchbarWrapper.appendChild(suggestionsContainer); | ||
| } else { | ||
| return; | ||
| } | ||
|
|
||
| let debounceTimeout = null; | ||
| let currentRequestController = null; | ||
|
|
||
| // Debounce to reduce API calls | ||
| function debounce(func, delay) { | ||
| return function (...args) { | ||
| clearTimeout(debounceTimeout); | ||
| debounceTimeout = setTimeout(() => func(...args), delay); | ||
| }; | ||
| } | ||
|
|
||
| function getSearchMode() { | ||
| const checked = document.querySelector('input[name="mode"]:checked'); | ||
| return checked ? checked.value : "courses"; | ||
| } | ||
|
|
||
| async function fetchSuggestions(query) { | ||
| if (!query.trim()) { | ||
| clearSuggestions(); | ||
| return; | ||
| } | ||
|
|
||
| // Cancel previous request if still in-flight | ||
| if (currentRequestController) { | ||
| currentRequestController.abort(); | ||
| } | ||
|
|
||
| currentRequestController = new AbortController(); | ||
| const signal = currentRequestController.signal; | ||
|
|
||
| const mode = getSearchMode(); | ||
|
|
||
| try { | ||
| const response = await fetch( | ||
| `/api/autocomplete/?q=${encodeURIComponent(query)}&mode=${mode}`, | ||
| { signal: signal }, | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| clearSuggestions(); | ||
| return; | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| renderSuggestions(data); | ||
| } catch (error) { | ||
| if (error.name !== "AbortError") { | ||
| console.error("Autocomplete fetch error:", error); | ||
| } | ||
| clearSuggestions(); | ||
| } | ||
| } | ||
|
|
||
| // Render suggestions | ||
| function renderSuggestions(data) { | ||
| suggestionsContainer.innerHTML = ""; | ||
|
|
||
| const hasCourses = data?.courses?.length > 0; | ||
| const hasInstructors = data?.instructors?.length > 0; | ||
| const hasClubs = data?.clubs?.length > 0; | ||
|
|
||
| if (!hasCourses && !hasInstructors && !hasClubs) { | ||
| suggestionsContainer.style.display = "none"; | ||
| return; | ||
| } | ||
|
|
||
| const MAX_RESULTS = 8; | ||
| const courses = (data.courses || []).slice(0, MAX_RESULTS); | ||
| const instructors = (data.instructors || []).slice(0, MAX_RESULTS); | ||
| const clubs = (data.clubs || []).slice(0, MAX_RESULTS); | ||
|
|
||
| // Add group headers for clarity | ||
| if (hasCourses) { | ||
| const header = document.createElement("div"); | ||
| header.classList.add("autocomplete-header"); | ||
| header.textContent = "Courses"; | ||
| suggestionsContainer.appendChild(header); | ||
| } | ||
|
|
||
| // Courses first | ||
| courses.forEach((course) => { | ||
| const item = document.createElement("div"); | ||
| item.classList.add("autocomplete-item"); | ||
| item.style.cursor = "pointer"; | ||
| item.textContent = `${course.subdepartment} ${course.number} — ${course.title}`; | ||
| item.addEventListener("click", () => { | ||
| searchInput.value = `${course.subdepartment} ${course.number}`; | ||
| clearSuggestions(); | ||
| if (searchInput.form) { | ||
| searchInput.form.submit(); | ||
| } | ||
| }); | ||
| suggestionsContainer.appendChild(item); | ||
| }); | ||
|
|
||
| if (instructors.length > 0) { | ||
| const header = document.createElement("div"); | ||
| header.classList.add("autocomplete-header"); | ||
| header.textContent = "Instructors"; | ||
| suggestionsContainer.appendChild(header); | ||
| } | ||
|
|
||
| // Instructor results | ||
| instructors.forEach((instructor) => { | ||
| const item = document.createElement("div"); | ||
| item.classList.add("autocomplete-item"); | ||
| item.style.cursor = "pointer"; | ||
| item.textContent = instructor.full_name; | ||
| item.addEventListener("click", () => { | ||
| searchInput.value = instructor.full_name; | ||
| clearSuggestions(); | ||
| if (searchInput.form) { | ||
| searchInput.form.submit(); | ||
| } | ||
| }); | ||
| suggestionsContainer.appendChild(item); | ||
| }); | ||
|
|
||
| if (clubs.length > 0) { | ||
| const header = document.createElement("div"); | ||
| header.classList.add("autocomplete-header"); | ||
| header.textContent = "Clubs"; | ||
| suggestionsContainer.appendChild(header); | ||
| } | ||
|
|
||
| // Club results | ||
| clubs.forEach((club) => { | ||
| const item = document.createElement("div"); | ||
| item.classList.add("autocomplete-item"); | ||
| item.style.cursor = "pointer"; | ||
| item.textContent = club.name; | ||
| item.addEventListener("click", () => { | ||
| searchInput.value = club.name; | ||
| clearSuggestions(); | ||
| if (searchInput.form) { | ||
| searchInput.form.submit(); | ||
| } | ||
| }); | ||
| suggestionsContainer.appendChild(item); | ||
| }); | ||
|
|
||
| suggestionsContainer.style.display = "block"; | ||
| } | ||
|
|
||
| // Helper to clear dropdown | ||
| function clearSuggestions() { | ||
| suggestionsContainer.innerHTML = ""; | ||
| suggestionsContainer.style.display = "none"; | ||
| } | ||
|
|
||
| // Hide dropdown when clicking outside | ||
| document.addEventListener("click", (event) => { | ||
| if ( | ||
| !suggestionsContainer.contains(event.target) && | ||
| event.target !== searchInput | ||
| ) { | ||
| clearSuggestions(); | ||
| } | ||
| }); | ||
|
|
||
| // Debounced input listener | ||
| searchInput.addEventListener( | ||
| "input", | ||
| debounce((event) => { | ||
| fetchSuggestions(event.target.value); | ||
| }, 250), | ||
| ); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -303,3 +303,58 @@ | |
| background-color: #cbd5e0; | ||
| border-radius: 2px; | ||
| } | ||
|
|
||
| /* Autocomplete suggestions positioning */ | ||
| .searchbar-wrapper { | ||
| position: relative; | ||
| } | ||
|
|
||
| /* Remove focus outline from search input */ | ||
| #search-input:focus { | ||
| outline: none !important; | ||
| box-shadow: none !important; | ||
| border-color: inherit; | ||
| } | ||
|
|
||
| .autocomplete-suggestions { | ||
| position: absolute; | ||
| top: 100%; | ||
| left: 0; | ||
| right: 0; | ||
| z-index: 1000; | ||
| background: white; | ||
| border: 1px solid #e2e8f0; | ||
| border-radius: 6px; | ||
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| margin-top: 0; | ||
| max-height: 400px; | ||
| overflow: hidden; | ||
| display: none; | ||
| padding: 0.5rem 0; | ||
| } | ||
|
|
||
| .autocomplete-header { | ||
| padding: 0.75rem 1rem; | ||
| font-weight: 600; | ||
| font-size: 0.875rem; | ||
| color: #4a5568; | ||
| background-color: #f7fafc; | ||
| border-bottom: 1px solid #e2e8f0; | ||
| margin: 0; | ||
| } | ||
|
|
||
| .autocomplete-item { | ||
| padding: 0.75rem 1rem; | ||
| cursor: pointer; | ||
| border-bottom: 1px solid #f7fafc; | ||
| margin: 0; | ||
| transition: background-color 0.15s ease; | ||
| } | ||
|
|
||
| .autocomplete-item:hover { | ||
| background-color: #f7fafc; | ||
| } | ||
|
|
||
| .autocomplete-item:last-child { | ||
| border-bottom: none; | ||
| } | ||
|
Comment on lines
+306
to
+360
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Restore visible focus indication and allow dropdown scrolling
Consider: -/* Remove focus outline from search input */
-#search-input:focus {
- outline: none !important;
- box-shadow: none !important;
- border-color: inherit;
-}
+/* Provide a visible, accessible focus style for the search input */
+#search-input:focus {
+ outline: 2px solid #d75626;
+ outline-offset: 2px;
+}and: - max-height: 400px;
- overflow: hidden;
+ max-height: 400px;
+ overflow-y: auto;This keeps the custom look while preserving keyboard focus visibility and avoiding clipped suggestions. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix typo in docstring.
The docstring has "DEF Serializer" but should be "DRF Serializer" (Django REST Framework).
🔎 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents