diff --git a/bas-search.user.js b/bas-search.user.js index 20d6e57..90382d2 100644 --- a/bas-search.user.js +++ b/bas-search.user.js @@ -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 @@ -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() { @@ -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; }); @@ -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); @@ -109,7 +187,7 @@ }); selectedIndex = index; - + if (index >= 0 && index < menuItems.length) { menuItems[index].style.backgroundColor = '#007acc'; menuItems[index].style.color = 'white'; @@ -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'; @@ -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'); } } } @@ -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) { @@ -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') { @@ -201,11 +317,6 @@ navigateTab('previous'); } } - - // Hide search interface on Escape - if (event.key === 'Escape') { - hideSearchInterface(); - } }); // Input box event handlers @@ -213,7 +324,7 @@ 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)}`; @@ -221,7 +332,7 @@ 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(); @@ -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(); diff --git a/doc/bas-search-lowres.gif b/doc/bas-search-lowres.gif new file mode 100644 index 0000000..9fc63b3 Binary files /dev/null and b/doc/bas-search-lowres.gif differ diff --git a/doc/bas-search.gif b/doc/bas-search.gif deleted file mode 100644 index 5fa8ee0..0000000 Binary files a/doc/bas-search.gif and /dev/null differ diff --git a/doc/bas-search.md b/doc/bas-search.md index f0ea0ed..1937bf9 100644 --- a/doc/bas-search.md +++ b/doc/bas-search.md @@ -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. \ No newline at end of file + - 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) \ No newline at end of file