// ==UserScript==
// @name Keyboard navigation and Cookie Wall skipper
// @version 1.12.4
// @author Sly_North
// @description Keyboard based navigation
// @grant none
// @run-at document-start
// @namespace Sly_North
// @include *
// require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// @license MIT
// ==/UserScript==
console.log('Key navigation script...');
// Click first link (tag='a' or 'span') with text matching one of 'keywords' regex
function ClickLink(tag, keywords) {
var elts = document.getElementsByTagName(tag);
for (var i = 0; i < elts.length; ++i) {
var elt = elts[i];
for (k in keywords)
{
var regexp = keywords[k];
if (elt.textContent.match(keywords[k])) {
console.log('Clicking ', elt.textContent);
elt.click();
return true;
}
}
}
return false;
}
// Make 'link' visibly selected.
var lastSelected; // previous selected link
function SelectLink(link, links) {
if (lastSelected) lastSelected.style.backgroundColor = "";
lastSelected = link;
link.focus();
link.style.backgroundColor = "orange";
}
// Select either link with preferred text or closer to the center.
function SelectBestLink() {
var links = document.getElementsByTagName('a');
var curr = document.activeElement;
var viewport = window.visualViewport;
var screenHeight = viewport.height;
var midX = viewport.width/2;
var midY = screenHeight/2;
var minD = 1000000000;
var best;
var keywords = [/cliquez sur ce lien/i, /#unread/, /Discussions/];
for (var i = 0; i < links.length; ++i) {
var link = links[i];
var br = link.getBoundingClientRect();
var d = Math.abs(br.left - midX) + Math.abs(br.top - midY);
if ((link == curr)) continue;
// Insert here other preferred link texts.
for (var k in keywords) {
if (link.text.match(keywords[k])) {
if ((br.top >= 0) && (br.top < screenHeight)) {
// link.click();
SelectLink(link, links);
console.log('Select link to current page [', link.innerText, '] - boundingRect.top=', br.top);
return;
}
}
}
// Select link to current page, if not already selected
if (link.href == document.URL) {
if ((br.top >= 0) && (br.top < screenHeight)) {
// If on start, focus it, without highlighting it.
SelectLink(link, links);
console.log('Select link to current page [', link.innerText, '] - boundingRect.top=', br.top);
return;
}
}
// Select link closer to the center
if (d < minD) {
minD = d;
best = link;
}
}
if (best) {
// If on start, focus it, without highlighting it.
console.log("Select link", best.text);
SelectLink(best, links);
console.log('Select link [', link.innerText, ']');
}
else console.log('Could not find best link');
}
// Skip most simple cookie forms.
function SkipCookieWall() {
console.log('SkipCookieWall...');
for (let tag of ['button', 'span', 'a']) {
let elts = document.getElementsByTagName(tag);
for (i in elts) {
var t = elts[i].innerText;
if (!t) continue;
if (t.match(/Reject all/i) || t.match(/Tout refuser/) || t.match(/Refuser tout/i) || t.match(/Continuer sans accepter/i) ||
t.match(/refuse tous les cookies/) || t.match(/Reject non-essential/) ||
t.match(/Continue without agree/) || t.match(/Only allow essential cookies/) || t.match(/Refuse non-essential cookies/)) {
console.log("Clic ", t);
elts[i].click();
return;
}
}
}
// console.log('Cookie skip not found');
}
function focusSearchInput() {
var curr = document.activeElement;
var inputs = Array.from(document.getElementsByTagName('input')).filter((e)=>{return e.type=="text" || e.type=="search";});
for (let i of inputs) {
if (i == curr) continue;
let rect = i.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
// console.log(' - Found text input');
i.focus();
return;
}
}
console.log('focusSearchInput - did not found a search text box');
}
// Unblock the page scrolling.
function Unfreeze() {
console.log('Unfreeze overflow');
document.body.classList.remove('content-blurred');
document.body.classList.remove('noscroll');
if (document.body.style.overflow) document.body.style.overflow = "scroll";
if (document.documentElement.style.overflow) document.documentElement.style.overflow = "scroll";
// Site specifics
document.body.classList.remove('didomi-popup-open');
document.body.classList.remove('popin-gdpr-no-scroll');
// NY-Times
var siteContent = document.getElementById('site-content')
if (siteContent) siteContent.style.position = "relative"
// Quora
for (let e of document.getElementsByClassName('q-box')) if (e.style.filter) e.style.filter = "";
for (let bluredDiv of document.getElementsByClassName('content-blurred'))
bluredDiv.classList.remove('content-blurred');
}
function TranslateYoutubeCaptions() {
console.log('L144');
$('.ytp-subtitles-button[aria-pressed="false"]').click();
$('.ytp-settings-button').click();
console.log('L148');
var sub = $('[role="menuitem"]:contains("Subtitles")');
if(!sub.length) { console.log('No subs available'); return; }
sub.click();
var subc = $('[role="menuitemradio"]:contains("English")');
if (subc.length) {
console.log('Enabling available English captions');
subc.click();
} else {
var autoTrans = $('[role="menuitemradio"]:contains("Auto-translate")');
if (!autoTrans.length) { console.log('YT autotranslate not available'); return; }
autoTrans.click();
var autoTransC = $('[role="menuitemradio"]:contains("English")');
if (!autoTransC.length) { console.log('YT autotranslate not available in English'); return; }
autoTransC.click();
}
console.log('end');
}
// Select direction in which to look for 'best' link
function MoveSelectedElement(keyCode) {
var dirX = 0, dirY = 0;
var scaleX = 1; // used to allow going up/down to a link not on a straight alignement (angle > 45°)
var maxDY = 10000000; // used to strongly limit vertical search when going left/right
var scaleWidth = 0; // To select left/center/right of elements
if (keyCode == 37 || keyCode == 72) { dirX = -1; scaleWidth = 0; maxDY = 50; scaleX = 0.25; } // left
else if (keyCode == 39 || keyCode == 76) { dirX = +1; scaleWidth = 1; maxDY = 50; scaleX = 0.25; } // right
else if (keyCode == 38 || keyCode == 75) { dirY = -1; scaleWidth = 0.5; scaleX = 0.25; } // up
else if (keyCode == 40 || keyCode == 74) { dirY = +1; scaleWidth = 0.5; scaleX = 0.25; } // down
else return false;
// Look for next link in a given direction
var adx = Math.abs(dirX), ady = Math.abs(dirY);
if (!lastSelected) {
console.log('1st select best link');
SelectBestLink();
}
var curr = document.activeElement;
var currBR = curr.getBoundingClientRect();
var currX = currBR.left + currBR.width*scaleWidth;
var currY = currBR.top + currBR.height/2;
var links = document.getElementsByTagName('a');
var best;
var minD = 1500;
for (var i = 0; i < links.length; ++i) {
var link = links[i];
if (link != curr) {
var br2 = link.getBoundingClientRect();
if (br2.width == 0 && br2.height == 0) continue;
var dx = (br2.left + br2.width*scaleWidth - currX) * scaleX;
var dy = br2.top + br2.height/2 - currY;
var adx = Math.abs(dx);
var ady = Math.abs(dy);
if (ady > maxDY) continue;
if ((adx > ady) != (dirX != 0)) continue; // Wrong angle
var dot = dx * dirX + dy * dirY;
if (dot <= 0) continue;
var dist = Math.abs(dx) + Math.abs(dy);
if (dist < minD) {
best = link;
minD = dist;
// console.log(" new best link dx = ",dx," dy = ", dy, " dist = ", dist, ' dot=', dot, 'dot/dist=', dot/dist, ' ', link.text);
}
}
}
if (best) {
SelectLink(best, links);
console.log('Select link [', link.innerText, ']');
}
return true;
}
function AdvanceSeveralPages(nbrPages) {
let url = document.location.href;
let currPage = url.match(/page=\d*/).join();
let newPage = Number(currPage.match(/\d\d*/).join()) + nbrPages;
url = url.replace(/page=\d*/, "page=" + newPage);
document.location.href = url;
}
// Listen for a few control+alt + key
console.log('Install key navigation key listener');
document.addEventListener('keydown', function(e) {
// console.log('ctrl=',e.ctrlKey,' alt=',e.altKey, ' meta=', e.metaKey, ' key=',e.keyCode);
// console.log(`Key ${e.keyCode} shift=${e.shiftKey} ctrl=${e.ctrlKey} metal=${e.metaKey}`);
if (e.shiftKey || !e.ctrlKey || (!e.altKey && !e.metaKey)) return;
// Space=select best link, enter=click best link, #/del=GMail's delete old messages in trash, o[/p]=previous/next page, /=search box
switch (e.keyCode) {
case 32: case 220: SelectBestLink(); return; // Space
case 13: case 90: ClickLink('a', [/cliquez sur ce lien/, /click this link/, /unread#unread/]); return; // Enter or Z
case 46: case 51: ClickLink('span', [/^delete forever/]); return; // # or del
case 191: focusSearchInput(); return; // '/'
case 83: case 89: TranslateYoutubeCaptions(); return; // S or Y
case 85: Unfreeze(); return; // U
case 67: SkipCookieWall(); return; // C
case 79: case 219: // O or [
if (!ClickLink('a', [/Précédent/i, /Prev/, /Previous page/, /Older/]))
ClickLink('i', [/ chevron_left /]);
return;
case 80: case 221: // P or ]
if (!ClickLink('a', [/Suivant/i, /Next/, /Next page/, /Newer/]))
ClickLink('i', [/ chevron_right /]);
return;
case 222: AdvanceSeveralPages(3); return; // "/'
}
if (!MoveSelectedElement(e.keyCode)) {
console.log('Key ignored:', e.keyCode);
}
}, false);
for (let i of [500, 1000, 2000, 3000, 4000]) setTimeout(() => {SkipCookieWall();}, i);
console.log('key navigation handler ready');