bring back the keyboard navigation in Google Search
当前为
// ==UserScript==
// @name Google Search keyboard navigation
// @namespace http://tampermonkey.net/
// @version 2025-05-28-2
// @description bring back the keyboard navigation in Google Search
// @author victor141516
// @match https://www.google.com/search?q=*
// @icon https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant none
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// Configuration constants
const CONFIG = {
CSS_CLASSES: {
BASE: "x-keyboard-utils",
ACTIVE: "x-keyboard-utils-active"
},
SELECTORS: {
RESULTS_CONTAINER: "div[data-async-context^=query]",
RESULT_ITEM: "div:has(>div[lang]>div[data-snc])",
LINK: "a"
},
STYLES: {
ARROW_SIZE: "15px",
ARROW_OFFSET: "-20px",
Z_INDEX: "100"
},
KEYS: {
ARROW_DOWN: "ArrowDown",
ARROW_UP: "ArrowUp",
ENTER: "Enter"
}
};
// State variables
let activeIndex = 0;
let results = [];
let observer = null;
function createStyleSheet() {
return `
.${CONFIG.CSS_CLASSES.BASE}.${CONFIG.CSS_CLASSES.ACTIVE}:before {
content: '';
z-index: ${CONFIG.STYLES.Z_INDEX};
display: block;
position: absolute;
pointer-events: none;
border: ${CONFIG.STYLES.ARROW_SIZE} solid transparent;
border-left-color: blue;
left: ${CONFIG.STYLES.ARROW_OFFSET};
}
`;
}
function injectStyles() {
const styleElement = document.createElement("style");
styleElement.textContent = createStyleSheet();
document.head.appendChild(styleElement);
}
function isNavigationKey(key) {
return Object.values(CONFIG.KEYS).includes(key);
}
function handleKeyDown(event) {
const { key } = event;
if (!isNavigationKey(key) || results.length === 0) {
return;
}
event.preventDefault();
switch (key) {
case CONFIG.KEYS.ARROW_DOWN:
moveToNextResult();
break;
case CONFIG.KEYS.ARROW_UP:
moveToPreviousResult();
break;
case CONFIG.KEYS.ENTER:
activateCurrentResult();
return; // Don't update visual state for Enter
}
updateActiveState();
}
function moveToNextResult() {
activeIndex = Math.min(activeIndex + 1, results.length - 1);
}
function moveToPreviousResult() {
activeIndex = Math.max(activeIndex - 1, 0);
}
function activateCurrentResult() {
const currentResult = results[activeIndex];
if (!currentResult) {
console.warn('No active result to activate');
return;
}
const link = currentResult.querySelector(CONFIG.SELECTORS.LINK);
if (link) {
link.click();
} else {
console.warn('No link found in active result');
}
}
function clearActiveStates() {
results.forEach((result) => {
result.classList.remove(CONFIG.CSS_CLASSES.ACTIVE);
});
}
function setActiveState(element) {
element.classList.add(CONFIG.CSS_CLASSES.ACTIVE);
element.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center",
});
}
function updateActiveState() {
clearActiveStates();
const currentResult = results[activeIndex];
if (currentResult) {
setActiveState(currentResult);
}
}
function findResultElements() {
const container = document.querySelector(CONFIG.SELECTORS.RESULTS_CONTAINER);
if (!container) {
console.warn('Results container not found');
return [];
}
return Array.from(container.querySelectorAll(CONFIG.SELECTORS.RESULT_ITEM));
}
function addBaseClassToResults(resultElements) {
resultElements.forEach((result) => {
if (!result.classList.contains(CONFIG.CSS_CLASSES.BASE)) {
result.classList.add(CONFIG.CSS_CLASSES.BASE);
}
});
}
function validateActiveIndex() {
if (activeIndex >= results.length && results.length > 0) {
activeIndex = results.length - 1;
} else if (results.length === 0) {
activeIndex = 0;
}
}
function updateResults() {
results = findResultElements();
addBaseClassToResults(results);
validateActiveIndex();
}
function handleMutations(mutationList) {
const hasChildListChanges = mutationList.some(
mutation => mutation.type === "childList"
);
if (hasChildListChanges) {
updateResults();
updateActiveState();
}
}
function setupMutationObserver() {
const targetNode = document.querySelector(CONFIG.SELECTORS.RESULTS_CONTAINER);
if (!targetNode) {
console.warn('Cannot setup observer: target node not found');
return;
}
observer = new MutationObserver(handleMutations);
observer.observe(targetNode, {
attributes: false,
childList: true,
subtree: false,
});
}
function setupEventListeners() {
document.addEventListener("keydown", handleKeyDown);
}
function initialize() {
injectStyles();
setupEventListeners();
setupMutationObserver();
updateResults();
updateActiveState();
}
// Cleanup function (though not used in this context)
function cleanup() {
if (observer) {
observer.disconnect();
observer = null;
}
}
// Start the application
initialize();
})();