Skip to content

Instantly share code, notes, and snippets.

@gaabora
Last active January 10, 2026 13:20
Show Gist options
  • Select an option

  • Save gaabora/df6d1de8c5c4b267bb67cfb9dc252b40 to your computer and use it in GitHub Desktop.

Select an option

Save gaabora/df6d1de8c5c4b267bb67cfb9dc252b40 to your computer and use it in GitHub Desktop.
.// ==UserScript==
// @name Keyboard Navigation
// @namespace none
// @version 1.0
// @match *://*/*
// @match *://animepahe.*/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
/* ================= RULES ================= */
const SITE_NAV_CONFIG = [
{
url: 'https://animepahe.*/.*',
includeItems: ['.btn-group .btn', '.click-to-load'],
excludeItems: ['.anime-content'],
includeGroups: ['.player', 'form', '.theatre-settings'],
excludeGroups: ['body', 'section', 'article', 'nav'],
bindings: [{'PageDown': '.sequel a'}, {'PageUp': '.prequel a'}]
}
];
/* ================ CONFIG ================ */
const KEYBOARD_LAYOUT = [
['q','w','e','r','t','y','u','i','o','p'],
['a','s','d','f','g','h','j','k','l'],
['z','x','c','v','b','n','m'],
['SPACE','BACK','ENTER']
];
/* ================= SELECTORS ================= */
const DEFAULT_ITEM_SELECTORS = [
'button',
'a[href]',
'[role="button"]',
'[role="menuitem"]',
'[role="tab"]',
'input:not([type=hidden])',
'textarea',
'[contenteditable="true"]',
'video',
'iframe[src]',
];
const DEFAULT_GROUP_SELECTORS = [
'body',
'nav',
'main',
'section',
'article',
'aside',
'[role="navigation"]',
'[role="main"]',
'[role="dialog"]',
'[role="menu"]'
];
/* ================= STATE ================= */
let treeRoot = null;
let XX_currentGroup = null;
let XX_currentIndex = 0;
let mode = 'nav';
let navTable = [];
let navTablePos = { g: 0, i: 0 };
let osk = null;
let oskPos = { r: 0, c: 0 };
let activeInput = null;
/* ================ CSS INJECT ================ */
const style = document.createElement('style');
style.textContent = `
:focus {
outline: 3px solid #violet !important;
outline-offset: 2px !important;
}
.__KEYNAV_INCLUDE {
border: 1px dotted green !important;
}
.__KEYNAV_EXCLUDE {
border: 1px dotted red !important;
}
.__KEYNAV_ELEMENT {
outline: none;
border: 1px dotted yellow !important;
}
.__KEYNAV_ACTIVE {
border: 2px solid orange !important;
outline: 2px solid orange !important;
outline-offset: 0px;
}
.__KEYNAV_OSK {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: #111;
padding: 10px;
border-radius: 10px;
z-index: 999999;
color: white;
font-family: monospace;
}
.__KEYNAV_OSK_KEY {
display: inline-block;
margin: 3px;
padding: 10px;
border-radius: 6px;
background: #333;
min-width: 40px;
text-align: center;
}
.__KEYNAV_OSK_ACTIVE {
outline: 3px solid orange;
}
`;
document.head.appendChild(style);
/* ================= UTIL ================= */
function getNavConfig() {
return SITE_NAV_CONFIG.find(r =>
new RegExp(r.url).test(location.href)
);
}
function matchesRule(el, ruleList) {
return ruleList?.some(sel =>
el.matches(sel) || el.closest(sel)
);
}
// function ariaScore(el) {
// if (el.matches('button,[role="button"]')) return 100;
// if (el.matches('a[href]')) return 90;
// if (el.matches('input,textarea')) return 80;
// if (el.matches('[role="menuitem"],[role="tab"]')) return 70;
// if (el.matches('video')) return 60;
// if (el.isContentEditable) return 50;
// return 10;
// }
function rebuildNav() {
console.log('rebuildNav');
const navConfig = getNavConfig();
const navGroups = [...DEFAULT_GROUP_SELECTORS.filter(el => !navConfig.excludeGroups.includes(el)), ...navConfig.includeGroups];
const navItems = [...DEFAULT_ITEM_SELECTORS, ...navConfig.includeItems];
navTable = [];
document.querySelectorAll(navGroups.join(',')).forEach(groupEl => {
if (!groupEl.offsetParent) return;
// const parentGroup = groupMap.get(el.parentElement) || treeRoot;
const navTableGroup = { el: groupEl, items: [] };
groupEl.querySelectorAll(navItems.join(',')).forEach(itemEl => {
if (!itemEl.offsetParent) return;
if (matchesRule(itemEl, navConfig.excludeItems || [])) {
itemEl.classList.add('__KEYNAV_EXCLUDE');
return;
}
itemEl.classList.add('__KEYNAV_ELEMENT');
navTableGroup.items.push(itemEl);
});
if (navTableGroup.items.length) navTable.push(navTableGroup);
});
}
/* ================= NAVIGATION ================= */
function skipGroup(stepSize) {
navTablePos.g = navTablePos.g + stepSize;
if (navTablePos.g >= navTable.length) navTablePos.g = 0;
if (navTablePos.g < 0) navTablePos.g = navTable.length - 1;
focusItem();
}
function skipItem(stepSize) {
const currentGroup = navTable[navTablePos.g];
navTablePos.i = navTablePos.i + stepSize;
if (navTablePos.i >= currentGroup.items.length) navTablePos.i = 0;
if (navTablePos.i < 0) navTablePos.i = currentGroup.items.length - 1;
focusItem();
}
function activateItem() {
const el = document.querySelector('.__KEYNAV_ACTIVE');
if (!el) return;
if (isInput(el)) {
enterInputMode(el);
} else if (el.matches('video')) {
el.paused ? el.play() : el.requestFullscreen?.();
} else {
el.click();
el.focus();
}
}
function clearActive() {
document.querySelectorAll('.__KEYNAV_ACTIVE').forEach(el => {
el.classList.remove('__KEYNAV_ACTIVE');
});
}
function focusItem() {
clearActive();
const currentGroup = navTable[navTablePos.g];
if (navTablePos.i >= currentGroup.items.length) navTablePos.i = currentGroup.items.length - 1;
const itemEl = currentGroup.items[navTablePos.i];
itemEl.classList.add('__KEYNAV_ACTIVE');
itemEl.scrollIntoView({ block: 'center' });
}
/* ---------------- TYPE HELPERS ---------------- */
const isInput = el =>
el.tagName === 'INPUT' ||
el.tagName === 'TEXTAREA' ||
el.isContentEditable;
/* ---------------- OSK ---------------- */
function createOSK() {
osk = document.createElement('div');
osk.className = '__KEYNAV_OSK';
KEYBOARD_LAYOUT.forEach((row, r) => {
const rowDiv = document.createElement('div');
row.forEach((key, c) => {
const k = document.createElement('span');
k.textContent = key;
k.dataset.r = r;
k.dataset.c = c;
k.className = '__KEYNAV_OSK_KEY';
rowDiv.appendChild(k);
});
osk.appendChild(rowDiv);
});
document.body.appendChild(osk);
highlightOSK();
}
function destroyOSK() {
osk?.remove();
osk = null;
oskPos = { r: 0, c: 0 };
}
function highlightOSK() {
osk.querySelectorAll('.__KEYNAV_OSK_KEY')
.forEach(k => k.classList.remove('__KEYNAV_OSK_ACTIVE'));
osk.querySelector(
`[data-r="${oskPos.r}"][data-c="${oskPos.c}"]`
)?.classList.add('__KEYNAV_OSK_ACTIVE');
}
function oskKey() {
return KEYBOARD_LAYOUT[oskPos.r][oskPos.c];
}
function oskInsert(key) {
if (!activeInput) return;
if (key === 'SPACE') activeInput.value += ' ';
else if (key === 'BACK') activeInput.value = activeInput.value.slice(0, -1);
else if (key === 'ENTER') exitInputMode();
else activeInput.value += key;
}
/* ---------------- MODES ---------------- */
function enterInputMode(el) {
mode = 'input';
activeInput = el;
el.focus();
if (el.tagName == 'INPUT') createOSK();
}
function exitInputMode() {
mode = 'nav';
activeInput?.blur();
activeInput = null;
destroyOSK();
}
/* ================= KEY HANDLER ================= */
document.addEventListener('keydown', e => {
if (mode === 'nav') {
console.log(e?.key, XX_currentGroup, XX_currentIndex);
switch (e.key) {
case 'ArrowDown':
skipGroup(1);
break;
case 'ArrowUp':
skipGroup(-1);
break;
case 'ArrowRight':
skipItem(1);
break;
case 'ArrowLeft':
skipItem(-1);
break;
case 'Enter':
activateItem();
break;
case '>':
break;
case 'Escape':
clearActive();
break;
}
} else {
e.preventDefault();
switch (e.key) {
case 'ArrowRight': oskPos.c++; break;
case 'ArrowLeft': oskPos.c--; break;
case 'ArrowDown': oskPos.r++; break;
case 'ArrowUp': oskPos.r--; break;
case 'Enter': oskInsert(oskKey()); break;
case 'Escape': exitInputMode(); return;
}
oskPos.r = Math.max(0, Math.min(KEYBOARD_LAYOUT.length - 1, oskPos.r));
oskPos.c = Math.max(0, Math.min(KEYBOARD_LAYOUT[oskPos.r].length - 1, oskPos.c));
highlightOSK();
}
});
/* ================= INIT ================= */
rebuildNav();
focusItem();
new MutationObserver(rebuildNav)
.observe(document.body, { childList: true, subtree: true });
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment