Last active
October 29, 2022 12:49
-
-
Save monoblaine/4001d99067cfa4ded236066edb94e321 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Traverse visible and clickable elements (shift + up or down arrows) | |
// @description Spatial navigation similar to good old Presto-based Opera | |
// @version 1.4.7 | |
// @match *://*/* | |
// @grant none | |
// ==/UserScript== | |
(function () { | |
const placeToReplace = window.EventTarget && EventTarget.prototype.addEventListener ? EventTarget : Element; | |
placeToReplace.prototype.originalAddEventListener = placeToReplace.prototype.addEventListener; | |
placeToReplace.prototype.addEventListener = function (type, listener, optionsOrUseCapture) { | |
if ((typeof type === 'string' && type.endsWith('keydown')) && listener !== navigate) { | |
const listenerIsACallbackFunction = typeof listener === 'function'; | |
const listenerIsAnObject = typeof listener === 'object' && typeof listener.handleEvent === 'function'; | |
if (listenerIsACallbackFunction || listenerIsAnObject) { | |
const originalFunction = listenerIsACallbackFunction ? listener : listener.handleEvent.bind(listener); | |
const interceptorFn = e => { | |
if (e.shiftKey) { | |
switch (e.target.tagName) { | |
case 'SUMMARY': | |
return; | |
case 'A': | |
case 'BUTTON': | |
if ( | |
e.target.getAttribute('data-toggle') === 'dropdown' || | |
e.target.getAttribute('aria-haspopup') !== 'false' | |
) { | |
return; | |
} | |
break; | |
} | |
} | |
if ( | |
originalFunction.name !== 'fireCustomKeydown' || | |
!( | |
!e.target.matches('input, textarea') || | |
e.defaultPrevented | |
) | |
) { | |
originalFunction(e); | |
} | |
}; | |
if (listenerIsACallbackFunction) { | |
listener = interceptorFn; | |
} | |
else { | |
listener.handleEvent = interceptorFn; | |
} | |
} | |
} | |
if (arguments.length < 3) { | |
this.originalAddEventListener(type, listener, false); | |
} | |
else { | |
this.originalAddEventListener(type, listener, optionsOrUseCapture); | |
} | |
} | |
let lastIndex = -1; | |
let dt = null; | |
let clickableElsCache; | |
let outlineNonesFixed = false; | |
class Focuser { | |
constructor () { | |
this.index = Number.MIN_VALUE; | |
} | |
tryFocus (clickableEls) { | |
let currentEl; | |
if (!isElementInViewport(currentEl = clickableEls[this.index])) { | |
return false; | |
} | |
lastIndex = this.index; | |
currentEl.focus(); | |
return currentEl === document.activeElement; | |
} | |
focusNextClickable () { | |
!outlineNonesFixed && fixOutlineNones(); | |
const clickableEls = getClickableEls(); | |
const len = clickableEls.length; | |
let focused = false; | |
this.initializeIndex(len); | |
while (this.condition(len) && !(focused = this.tryFocus(clickableEls))); | |
if (!focused) { | |
document.activeElement.blur(); | |
} | |
} | |
initializeIndex (length) { throw new Error('fuck'); } | |
condition (limit) { throw new Error('fuck'); } | |
} | |
class NextFocuser extends Focuser { | |
initializeIndex (length) { | |
this.index = lastIndex; | |
} | |
condition (length) { | |
return ++this.index < length; | |
} | |
} | |
class PrevFocuser extends Focuser { | |
initializeIndex (length) { | |
this.index = lastIndex < 0 ? length : lastIndex; | |
} | |
condition (length) { | |
return --this.index >= 0; | |
} | |
} | |
const nextFocuser = new NextFocuser(); | |
const prevFocuser = new PrevFocuser(); | |
document.addEventListener('keydown', navigate); | |
function navigate (e) { | |
const keyName = e.key, | |
isInput = e.target.tagName === 'TEXTAREA' || (e.target.tagName === 'INPUT' && e.target.type !== 'submit'); | |
if (keyName === 'Shift' || !e.shiftKey || isInput) { | |
return; | |
} | |
let focuser = null; | |
switch (keyName) { | |
case 'ArrowUp': | |
focuser = prevFocuser; | |
break; | |
case 'ArrowDown': | |
focuser = nextFocuser; | |
break; | |
default: | |
return; | |
} | |
e.preventDefault(); | |
focuser.focusNextClickable(); | |
} | |
function getClickableEls () { | |
const now = Date.now(); | |
if (dt === null) { | |
dt = now; | |
} | |
else if ((now - dt) < 1000) { | |
return clickableElsCache; | |
} | |
clickableElsCache = document.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), input[type=submit]:not([disabled]), summary:not([disabled]), [role="button"]:not([disabled])'); | |
if (document.activeElement != null) { | |
lastIndex = Array.prototype.indexOf.call(clickableElsCache, document.activeElement); | |
} | |
else { | |
lastIndex = -1; | |
} | |
return clickableElsCache; | |
} | |
function isElementInViewport (el) { | |
const rect = el.getBoundingClientRect(); | |
const vWidth = window.innerWidth || document.documentElement.clientWidth; | |
const vHeight = window.innerHeight || document.documentElement.clientHeight; | |
const efp = function (x, y) { return document.elementFromPoint(x, y) }; | |
if ( | |
( | |
rect.top >= 0 && | |
rect.bottom <= vHeight && | |
rect.left >= 0 && | |
rect.right <= vWidth | |
) === false | |
) { | |
return false; | |
} | |
const xCenter = rect.left + rect.width / 2; | |
const yCenter = rect.top + rect.height / 2; | |
return ( | |
el.contains(efp(rect.left, rect.top)) || | |
el.contains(efp(xCenter, rect.top)) || | |
el.contains(efp(rect.right, rect.top)) || | |
el.contains(efp(rect.right, yCenter)) || | |
el.contains(efp(rect.right, rect.bottom)) || | |
el.contains(efp(xCenter, rect.bottom)) || | |
el.contains(efp(rect.left, rect.bottom)) || | |
el.contains(efp(rect.left, yCenter)) | |
); | |
} | |
// outline: none kullananları düzelt | |
function fixOutlineNones () { | |
const someAnchor = document.querySelector('a'); | |
if (someAnchor !== null) { | |
const someAnchorFocusStyle = getComputedStyle(someAnchor, ':focus').content; | |
if (someAnchorFocusStyle === '' || /none|initial|normal/.test(someAnchorFocusStyle)) { | |
let style = document.createElement('style'); | |
style.innerHTML = '*:focus { outline: 2px dashed orange !important; transition-property: none !important; }'; | |
document.head.appendChild(style); | |
} | |
} | |
outlineNonesFixed = true; | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment