Keyboard navigation

Keyboard based navigation

Від 25.06.2022. Дивіться остання версія.

// ==UserScript==
// @name Keyboard navigation
// @version 1.9
// @author Sly_North
// @description Keyboard based navigation
// @grant none
// @run-at document-start
// @namespace Sly_North
// @include *
// @license MIT
// ==/UserScript==

// 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 lastSel; // previous selected link
function SelectLink(link, links) {
  if (lastSel) lastSel.style.backgroundColor = "";
  lastSel = 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])) {
        // link.click();
        SelectLink(link, links);
        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);
        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);
  }
}

// Skip most simple cookie forms.
function SkipCookieWall() {
  var elts = document.getElementsByTagName('span');
  for (i in elts) {
    var t = elts[i].innerText;
    if (!t) continue;
    if (t.match(/Continuer sans accepter/) || t.match(/CONTINUER SANS ACCEPTER/) || t.match(/Continue without agree/) || t.match(/Only allow essential cookies/)) {
      console.log("Clic ", t);
      elts[i].click();
      return;
    }
  }
}

function focusSearchInput() {
  var inputs = document.getElementsByTagName('input');
  for (let i of inputs) {
    if (i.type === 'text') {
      let rect = i.getBoundingClientRect();
      if (rect.width > 0 && rect.height > 0) {
    	  // console.log(' - Found text input');
        i.focus();
        return;
      }
    }
  }
}


// Unblock the page scrolling.
function Unfreeze() {
  document.body.classList.remove('content-blurred');
  document.body.classList.remove('noscroll');
  document.documentElement.style.overflow = "scroll";
  document.body.style.overflow = "scroll";
  console.log('Unfreeze overflow');
}

// Listen for a few control+alt + key
document.addEventListener('keydown', function(e) {
  // console.log('ctrl=',e.ctrlKey,' alt=',e.altKey, ' meta=', e.metaKey, ' key=',e.keyCode);
  if (e.shiftKey || !e.ctrlKey || (!e.altKey && !e.metaKey)) return;
  var dx = 0, dy = 0, scaleX = 1;
  var maxDX = 10000000, maxDY = 10000000;

  var scaleWidth = 0;
  // Space=select best link, enter=click best link, #/del=GMail's delete old messages in trash, o/p=previous/next page
  if (e.keyCode == 32 || e.keyCode == 220) { SelectBestLink(); return; }
  else if (e.keyCode == 13 || e.keyCode == 90) { ClickLink('a', [/cliquez sur ce lien/, /click this link/, /unread#unread/]); return; }
  else if (e.keyCode == 46 || e.keyCode == 51) { ClickLink('span', [/^delete forever/]); return; }
  else if (e.keyCode == 191) { focusSearchInput(); return; }
  else if (e.keyCode == 79 || e.keyCode == 219) {
    if (!ClickLink('a', [/Précédent/i, /Prev/, /Previous page/, /Older/]))
      ClickLink('i', [/ chevron_left /]);
    return;
  }
  else if (e.keyCode == 80 || e.keyCode == 221) {
    if (!ClickLink('a', [/Suivant/i, /Next/, /Next page/, /Newer/])) {
      ClickLink('i', [/ chevron_right /]);
    }
    return;
  }
  else if (e.keyCode == 85) { Unfreeze(); return; }
  else if (e.keyCode == 67) { SkipCookieWall(); return; }

  // Select direction in which to look for 'best' link
  else if (e.keyCode == 37 || e.keyCode == 72) { dx = -1; scaleWidth = 1; maxDY = 50; } // left
  else if (e.keyCode == 39 || e.keyCode == 76) { dx = +1; scaleWidth = 1; maxDY = 50; } // right
  else if (e.keyCode == 38 || e.keyCode == 75) { dy = -1; scaleX = 0.5; } // up
  else if (e.keyCode == 40 || e.keyCode == 74) { dy = +1; scaleX = 0.5; } // down
  else {
    // console.log('Key ignored:', e.keyCode);
    return;
  }

  // Look for next link in a given direction
  var adx = Math.abs(dx), ady = Math.abs(dy);
  var curr = document.activeElement;
  var br = curr.getBoundingClientRect();
  var sx = br.left + br.width*scaleWidth; // + br.width/2;
  var sy = br.top  + br.height/2;
  var links = document.getElementsByTagName('a');
  var best;
  var minD = 1500;
  // console.log('Arrow in dir x =',dx,' y =',dy);
  for (var i = 0; i < links.length; ++i) {
    var link = links[i];
    if (link != curr) {
      var br2 = link.getBoundingClientRect();
      var x = (br2.left + br2.width*scaleWidth /*+ br2.width/2*/ - sx) * scaleX;
      var y = br2.top + br2.height/2 - sy;
      var ax = Math.abs(x), ay = Math.abs(y);
      var distX = x*dx, distY = y*dy;
      if (ax <= maxDX && ay <= maxDY && distX >= adx && distY >= ady) {
        // console.log(Math.abs(x*dx + y*dy) , " vs ", Math.abs(x*dy + y*dx));

        var d = ax + ay;
        if (Math.abs(x*dx + y*dy) < Math.abs(x*dy + y*dx))
          d *= 2; // Penalize off-direction links
        if (d < minD) {
          if ((dx != 0 || ax/(1+ay) < 3) && (dy != 0 || ay < 100)) {
            // console.log("   new best link x = ",x," y = ", y, ' distX=',distX,' distY=', distY, "  minD = ", minD, " ", link.text);
            minD = d;
            best = link;
          }
        }
      }
    }
  }
  if (best) SelectLink(best, links);
}, false);

setTimeout(() => {SkipCookieWall();}, 1000);
// setTimeout(() => {SkipCookieWall();}, 1500);

console.log('key navigation handler ready');