Skip to content

Commit

Permalink
navigate search results
Browse files Browse the repository at this point in the history
  • Loading branch information
on committed Aug 6, 2025
1 parent 40b11c6 commit 3f6730e
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 25 deletions.
153 changes: 132 additions & 21 deletions bas-search.user.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// ==UserScript==
// @name BAS Quick Search
// @namespace http://tampermonkey.net/
// @version 1.6.4
// @description Popup quick search menu (Hotkey: `Ctrl` + `Shift` + `F`), Use "g:" prefix for group search and no prefix for account search. Navigate tabs with `Ctrl` + `←` / `→` keys. Auto-focus username input on login page.
// @version 1.7.1
// @description Enhanced search with Ctrl+Shift+F hotkey, auto-sizing dropdown tab menu, and keyboard navigation. Supports group search with "g:" prefix and accounts with no prefix or "a:" prefix. Navigate tabs with Ctrl+Arrow keys. Navigate search results with arrow keys. Improved focus management with Escape key.
// @author Øyvind Nilsen (on@ntnu.no)
// @match https://bas.ntnu.no/*
// @grant none
Expand Down Expand Up @@ -55,6 +55,84 @@

let menuItems = [];
let selectedIndex = -1;
let searchResultLinks = [];
let selectedResultIndex = -1;
let searchResultsActive = false;

// Function to initialize search result navigation
function initializeSearchResultNavigation() {
// Find all account links in the search results table
const searchResultTable = document.querySelector('#searchresult table tbody');
if (!searchResultTable) return;

searchResultLinks = Array.from(searchResultTable.querySelectorAll('tr td:first-child a'));

if (searchResultLinks.length > 0) {
selectSearchResult(0); // Highlight first result
searchResultsActive = true;
}
}

// Function to clear search result selection
function clearSearchResultSelection() {
searchResultLinks.forEach(link => {
link.style.backgroundColor = '';
link.style.color = '';
link.style.outline = '';
});
selectedResultIndex = -1;
searchResultsActive = false;
}

// Function to select a search result
function selectSearchResult(index) {
// Remove previous highlighting
searchResultLinks.forEach(link => {
link.style.backgroundColor = '';
link.style.color = '';
link.style.outline = '';
});

selectedResultIndex = index;
searchResultsActive = true;

if (index >= 0 && index < searchResultLinks.length) {
const selectedLink = searchResultLinks[index];
selectedLink.style.backgroundColor = '#007acc';
selectedLink.style.color = 'white';
selectedLink.style.outline = '2px solid #005999';

// Scroll into view if needed
selectedLink.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
}

// Function to navigate search results
function navigateSearchResults(direction) {
if (searchResultLinks.length === 0) return;

// If no result is currently selected, start from first or last
if (selectedResultIndex === -1) {
selectSearchResult(direction === 'down' ? 0 : searchResultLinks.length - 1);
return;
}

let nextIndex;
if (direction === 'down') {
nextIndex = (selectedResultIndex + 1) % searchResultLinks.length;
} else {
nextIndex = selectedResultIndex <= 0 ? searchResultLinks.length - 1 : selectedResultIndex - 1;
}

selectSearchResult(nextIndex);
}

// Function to follow selected search result
function followSelectedResult() {
if (selectedResultIndex >= 0 && selectedResultIndex < searchResultLinks.length) {
window.location.href = searchResultLinks[selectedResultIndex].href;
}
}

// Function to populate dropdown menu with tabs
function populateDropdown() {
Expand All @@ -72,11 +150,11 @@
menuItem.style.borderBottom = index < tabs.length - 1 ? '1px solid #eee' : 'none';
menuItem.textContent = tab.textContent.trim();
menuItem.setAttribute('data-href', tab.href);

menuItem.addEventListener('mouseenter', () => {
selectMenuItem(index);
});

menuItem.addEventListener('click', () => {
window.location.href = tab.href;
});
Expand All @@ -92,7 +170,7 @@
function adjustContainerPosition() {
const viewportHeight = window.innerHeight;
const containerRect = searchContainer.getBoundingClientRect();

if (containerRect.bottom > viewportHeight) {
const overflowAmount = containerRect.bottom - viewportHeight + 20;
const currentTop = parseInt(searchContainer.style.top);
Expand All @@ -109,7 +187,7 @@
});

selectedIndex = index;

if (index >= 0 && index < menuItems.length) {
menuItems[index].style.backgroundColor = '#007acc';
menuItems[index].style.color = 'white';
Expand All @@ -118,6 +196,9 @@

// Function to show search interface
function showSearchInterface() {
// Clear search result selection when opening search interface
clearSearchResultSelection();

searchContainer.style.top = '10px';
populateDropdown();
searchContainer.style.display = 'block';
Expand All @@ -135,14 +216,10 @@

// Function to focus on username input if it exists (only on login page)
function focusUsernameInput() {
// Only focus username input if we're on the login page
if (window.location.pathname === '/login' || window.location.pathname === '/login/') {
const usernameInput = document.querySelector('input[name="username"]');
if (usernameInput) {
usernameInput.focus();
console.log('Username input focused on login page');
} else {
console.log('Username input not found on login page');
}
}
}
Expand Down Expand Up @@ -173,14 +250,21 @@
tabs[nextIndex].click();
}

// Focus username input after page load (only on login page)
// Initialize on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', focusUsernameInput);
document.addEventListener('DOMContentLoaded', () => {
focusUsernameInput();
initializeSearchResultNavigation();
});
} else {
focusUsernameInput();
initializeSearchResultNavigation();
}

setTimeout(focusUsernameInput, 500);
setTimeout(() => {
focusUsernameInput();
initializeSearchResultNavigation();
}, 500);

// Global keyboard event handlers
document.addEventListener('keydown', function(event) {
Expand All @@ -191,6 +275,38 @@
return;
}

// Handle Escape key
if (event.key === 'Escape') {
if (searchContainer.style.display === 'block') {
// Hide search interface
hideSearchInterface();
} else if (searchResultsActive) {
// Clear search result selection
event.preventDefault();
clearSearchResultSelection();
}
return;
}

// Search result navigation (when search interface is not visible and results are active)
if (searchContainer.style.display === 'none' && searchResultLinks.length > 0) {
if (event.code === 'ArrowDown') {
event.preventDefault();
navigateSearchResults('down');
return;
} else if (event.code === 'ArrowUp') {
event.preventDefault();
navigateSearchResults('up');
return;
} else if (event.code === 'Enter' || event.code === 'NumpadEnter') {
if (searchResultsActive && selectedResultIndex >= 0) {
event.preventDefault();
followSelectedResult();
return;
}
}
}

// Tab navigation with Ctrl + Arrow keys (when search is not visible)
if (event.ctrlKey && !event.altKey && !event.shiftKey && searchContainer.style.display === 'none') {
if (event.code === 'ArrowRight') {
Expand All @@ -201,27 +317,22 @@
navigateTab('previous');
}
}

// Hide search interface on Escape
if (event.key === 'Escape') {
hideSearchInterface();
}
});

// Input box event handlers
inputBox.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
const searchQuery = inputBox.value.trim();
let url;

if (searchQuery.toLowerCase().startsWith('g:')) {
const groupName = searchQuery.substring(2).trim();
url = `https://bas.ntnu.no/group/search/?name=${encodeURIComponent(groupName)}`;
} else {
const accountName = searchQuery.toLowerCase().startsWith('a:') ? searchQuery.substring(2).trim() : searchQuery;
url = `https://bas.ntnu.no/account/search/?name=${encodeURIComponent(accountName)}`;
}

window.location.href = url;
} else if (event.key === 'ArrowDown') {
event.preventDefault();
Expand Down Expand Up @@ -264,7 +375,7 @@

// Global click handler to focus dropdown when needed
document.addEventListener('keydown', function(event) {
if (searchContainer.style.display === 'block' &&
if (searchContainer.style.display === 'block' &&
(event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter')) {
if (document.activeElement !== dropdown && document.activeElement !== inputBox) {
dropdown.focus();
Expand Down
Binary file added doc/bas-search-lowres.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed doc/bas-search.gif
Binary file not shown.
10 changes: 6 additions & 4 deletions doc/bas-search.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
![Search0 GIF](bas-search.gif)

#### Feaures:
- Press `Ctrl` + `Alt` + `F` to open the search box/menu. Use `` and `` to navigate.
- Press `Ctrl` + `` / `` to navigate to the next/previous tab.
- Press `Ctrl` + `Alt` + `F` to open the search box/menu. Use `` and `` to navigate. And press `Enter` to select.
- In the search result pages, use `` and `` to navigate through the results. And press `Enter` to open the selected result.
- Press `Ctrl` + `` / `` to navigate to the next/previous tab.

#### Video
![Search0 GIF](bas-search-lowres.gif)

0 comments on commit 3f6730e

Please sign in to comment.