diff --git a/.gitignore b/.gitignore index 6295411..b71ea44 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ README.md -test.txt \ No newline at end of file +test.txt +test.html \ No newline at end of file diff --git a/bas-search.user.js b/bas-search.user.js index 020ee64..b776154 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.4.4 -// @description Show a textbox on `Alt` + `/` key press and redirect to Cereweb search on Enter key press. Supports group search with "g:" prefix and accounts if you use no prefix or "a:" prefix. +// @version 1.6.2 +// @description Enhanced search with `Ctrl` + `Shift` + `F` hotkey, menu with keyboard navigation. Supports group search with "g:" prefix and accounts with no prefix or "a:" prefix. Navigate tabs with `Ctrl` + Arrow keys. Auto-focus username input on login page. // @author Øyvind Nilsen (on@ntnu.no) // @match https://bas.ntnu.no/* // @grant none @@ -14,33 +14,208 @@ (function() { 'use strict'; + // Create the search container + const searchContainer = document.createElement('div'); + searchContainer.style.position = 'fixed'; + searchContainer.style.top = '10px'; + searchContainer.style.left = '50%'; + searchContainer.style.transform = 'translateX(-50%)'; + searchContainer.style.zIndex = '10000'; + searchContainer.style.display = 'none'; + searchContainer.style.fontFamily = 'Arial, sans-serif'; + document.body.appendChild(searchContainer); + // Create and style the input box const inputBox = document.createElement('input'); inputBox.type = 'text'; - inputBox.style.position = 'fixed'; - inputBox.style.top = '10px'; - inputBox.style.left = '50%'; - inputBox.style.transform = 'translateX(-50%)'; - inputBox.style.zIndex = '10000'; - inputBox.style.display = 'none'; - inputBox.style.padding = '5px'; + inputBox.style.padding = '8px 12px'; inputBox.style.fontSize = '16px'; - document.body.appendChild(inputBox); + inputBox.style.border = '2px solid #007acc'; + inputBox.style.borderRadius = '4px'; + inputBox.style.outline = 'none'; + inputBox.style.width = '300px'; + inputBox.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)'; + inputBox.placeholder = 'Search accounts, groups (g:name)...'; + searchContainer.appendChild(inputBox); + + // Create the dropdown menu + const dropdown = document.createElement('div'); + dropdown.style.position = 'absolute'; + dropdown.style.top = '100%'; + dropdown.style.left = '0'; + dropdown.style.width = '100%'; + dropdown.style.backgroundColor = 'white'; + dropdown.style.border = '2px solid #007acc'; + dropdown.style.borderTop = 'none'; + dropdown.style.borderRadius = '0 0 4px 4px'; + dropdown.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)'; + dropdown.style.overflowY = 'visible'; // Changed from 'auto' to 'visible' + dropdown.style.display = 'block'; // Always visible when container is shown + // Removed maxHeight constraint + searchContainer.appendChild(dropdown); + + let menuItems = []; + let selectedIndex = -1; + + // Function to populate dropdown menu with tabs + function populateDropdown() { + const tabview = document.getElementById('tabview'); + if (!tabview) return; + + const tabs = tabview.querySelectorAll('.yui-nav li a'); + dropdown.innerHTML = ''; + menuItems = []; + + tabs.forEach((tab, index) => { + const menuItem = document.createElement('div'); + menuItem.style.padding = '8px 12px'; + menuItem.style.cursor = 'pointer'; + menuItem.style.borderBottom = index < tabs.length - 1 ? '1px solid #eee' : 'none'; // No border on last item + menuItem.textContent = tab.textContent.trim(); + menuItem.setAttribute('data-href', tab.href); + + menuItem.addEventListener('mouseenter', () => { + selectMenuItem(index); + }); + + menuItem.addEventListener('click', () => { + window.location.href = tab.href; + }); + + dropdown.appendChild(menuItem); + menuItems.push(menuItem); + }); + + // Auto-adjust container position if it goes off-screen + adjustContainerPosition(); + } + + // Function to adjust container position to keep it on screen + function adjustContainerPosition() { + // Get viewport dimensions + const viewportHeight = window.innerHeight; + const containerRect = searchContainer.getBoundingClientRect(); + + // Check if container extends below viewport + if (containerRect.bottom > viewportHeight) { + // Move container up to fit in viewport + const overflowAmount = containerRect.bottom - viewportHeight + 20; // 20px margin + const currentTop = parseInt(searchContainer.style.top); + const newTop = Math.max(10, currentTop - overflowAmount); // Don't go above 10px from top + searchContainer.style.top = newTop + 'px'; + } + } + + // Function to select a menu item + function selectMenuItem(index) { + // Remove previous selection + menuItems.forEach(item => { + item.style.backgroundColor = ''; + item.style.color = ''; + }); + + selectedIndex = index; + + if (index >= 0 && index < menuItems.length) { + menuItems[index].style.backgroundColor = '#007acc'; + menuItems[index].style.color = 'white'; + } + } + + // Function to show search interface + function showSearchInterface() { + // Reset position before showing + searchContainer.style.top = '10px'; + + populateDropdown(); + searchContainer.style.display = 'block'; + inputBox.focus(); + inputBox.select(); + selectedIndex = -1; // Start with no menu item selected + } + + // Function to hide search interface + function hideSearchInterface() { + searchContainer.style.display = 'none'; + selectedIndex = -1; + inputBox.value = ''; + } + + // Function to focus on username input if it exists + function focusUsernameInput() { + const usernameInput = document.querySelector('input[name="name"]'); + if (usernameInput) { + usernameInput.focus(); + } + } + + // Function to navigate tabs + function navigateTab(direction) { + const tabview = document.getElementById('tabview'); + if (!tabview) return; + + const tabs = tabview.querySelectorAll('.yui-nav li a'); + if (tabs.length === 0) return; + + let currentIndex = -1; + for (let i = 0; i < tabs.length; i++) { + if (tabs[i].parentElement.classList.contains('selected')) { + currentIndex = i; + break; + } + } + + let nextIndex; + if (direction === 'next') { + nextIndex = (currentIndex + 1) % tabs.length; + } else { + nextIndex = currentIndex <= 0 ? tabs.length - 1 : currentIndex - 1; + } + + tabs[nextIndex].click(); + } - // Show the input box when "/" is pressed + // Focus username input after page load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', focusUsernameInput); + } else { + focusUsernameInput(); + } + + setTimeout(focusUsernameInput, 500); + + // Global keyboard event handlers document.addEventListener('keydown', function(event) { - if ((event.altKey || event.metaKey) && event.shiftKey && event.code === 'Digit7') { + // Show search box on Ctrl+Shift+F + if (event.ctrlKey && event.shiftKey && event.code === 'KeyF') { event.preventDefault(); - inputBox.style.display = 'block'; - inputBox.focus(); + showSearchInterface(); + 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') { + event.preventDefault(); + navigateTab('next'); + } else if (event.code === 'ArrowLeft') { + event.preventDefault(); + navigateTab('previous'); + } + } + + // Hide search interface on Escape + if (event.key === 'Escape') { + hideSearchInterface(); } }); - // Redirect to the search URL when Enter is pressed + // 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)}`; @@ -48,12 +223,67 @@ 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(); + // Move to first menu item + if (menuItems.length > 0) { + selectMenuItem(0); + dropdown.focus(); + } + } else if (event.key === 'Escape') { + event.preventDefault(); + hideSearchInterface(); } }); - // Hide the input box when it loses focus - inputBox.addEventListener('blur', function() { - inputBox.style.display = 'none'; + // Dropdown navigation + dropdown.addEventListener('keydown', function(event) { + if (event.key === 'ArrowDown') { + event.preventDefault(); + const nextIndex = (selectedIndex + 1) % menuItems.length; + selectMenuItem(nextIndex); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + if (selectedIndex === 0) { + // Go back to input box + inputBox.focus(); + selectedIndex = -1; + } else { + const prevIndex = selectedIndex <= 0 ? menuItems.length - 1 : selectedIndex - 1; + selectMenuItem(prevIndex); + } + } else if (event.key === 'Enter') { + event.preventDefault(); + if (selectedIndex >= 0 && selectedIndex < menuItems.length) { + const href = menuItems[selectedIndex].getAttribute('data-href'); + window.location.href = href; + } + } else if (event.key === 'Escape') { + event.preventDefault(); + hideSearchInterface(); + } + }); + + // Global click handler to focus dropdown when needed + document.addEventListener('keydown', function(event) { + if (searchContainer.style.display === 'block' && + (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter')) { + // If dropdown is visible and arrow keys are pressed, make sure it can receive focus + if (document.activeElement !== dropdown && document.activeElement !== inputBox) { + dropdown.focus(); + } + } + }); + + // Make dropdown focusable + dropdown.setAttribute('tabindex', '-1'); + + // Hide search interface when clicking outside + document.addEventListener('click', function(event) { + if (!searchContainer.contains(event.target)) { + hideSearchInterface(); + } }); -})(); +})(); \ No newline at end of file