Skip to content

Commit

Permalink
added menu, auto-focus and ctrl+left/right nav
Browse files Browse the repository at this point in the history
  • Loading branch information
on committed Aug 5, 2025
1 parent 0ed10c4 commit f7b0af3
Show file tree
Hide file tree
Showing 2 changed files with 251 additions and 20 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
README.md
test.txt
test.txt
test.html
268 changes: 249 additions & 19 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.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
Expand All @@ -14,46 +14,276 @@
(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)}`;
} 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();
// 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();
}
});
})();
})();

0 comments on commit f7b0af3

Please sign in to comment.