I’m not the only one disappointed in the way the web has worked out
in lots of ways. From <blink>
onwards, so much semantic information is
missing from the average website, sometimes wilfully it appears. Why is
there so little structural data on what the components of a page are?
One particular peccadillo I dislike is “Previous/Next Page” elements on
a list page. Nobody ever uses
<a rel="next" ...>
.
If you’re lucky, there’s an aria-label
attribute for accessibility
purposes, but as it’s a free-form text, and there isn’t even a
convention, it could be pretty much anything.
For reasons unclear to me, almost no sites make use of the left/right arrow keys for navigation. So if I want to map those keys to prev/next, instead of a nice little bit of configuration, I have to resort to this user script:
(function() {
'use strict';
/* NB: we already tested for prefix/suffix, so this RE is OK. */
function wholeWordMatch(haystack, needle) {
let r = new RegExp("\\s" + needle + "\\s");
return r.test(haystack);
};
const LEFT_KEY_CODE = 37;
const RIGHT_KEY_CODE = 39;
const prevStrings = [
"previous page",
"previous",
"prev"
];
const nextStrings = [
"next page",
"next"
];
document.addEventListener("keyup", function(e) {
if (!e) {
e = window.event;
}
if (e.isComposing) {
return;
}
switch (e.target.tagName) {
case "TEXTAREA":
case "INPUT":
return;
}
const key = e.keyCode ? e.keyCode : e.which;
var matches = undefined;
if (key == LEFT_KEY_CODE) {
matches = prevStrings;
} else if (key == RIGHT_KEY_CODE) {
matches = nextStrings;
} else {
return;
}
let found = undefined;
let score = 0;
document.querySelectorAll("a").forEach((link) => {
let strs = [ link.textContent ];
if (!link.href) {
return;
}
/* This is often a good match if the text itself isn't. */
if (link.attributes["aria-label"]) {
strs.push(link.attributes["aria-label"].nodeValue);
}
for (let str of strs) {
if (typeof str === "undefined") {
return;
}
str = str.toLowerCase();
/*
* There's no perfect way to find the "best" link, but in
* practice this works on a reasonable number of sites: an exact
* match, or exact prefix or suffix, always wins; otherwise, we
* match a whole-word sub-string: "Go to prev <<" will match,
* but not "dpreview.com".
*/
for (let match of matches) {
if (str === match) {
found = link;
break;
}
if (str.startsWith(match) || str.endsWith(match)) {
found = link;
break;
}
if (score < 1 && wholeWordMatch(str, match)) {
found = link;
score = 1;
}
}
}
});
if (found) {
found.click();
}
}, true);
})();
Yet again, hacky, but it mostly works. It’s pretty cool that this is even possible though.