Tagger: Saka-key like keyboard navigation link tagging

This commit is contained in:
James Magahern
2021-02-26 17:43:53 -08:00
parent 48a7c07551
commit 225761473d
4 changed files with 215 additions and 5 deletions

181
App/Resources/Tagger.js Normal file
View File

@@ -0,0 +1,181 @@
class ElementTagger {
constructor() {
this.isTagged = false;
this.tagList = {};
const baseMnemonics = [
'a', 's', 'd', /* 'f', */ 'g',
'q', 'w', 'e', 'r', 't',
'z', 'x', 'c', 'v', 'b',
'y', 'u', 'i', 'o', 'p',
'h', 'j', 'k', 'l',
'n', 'm'
];
let additionalMnemonics = [];
baseMnemonics.forEach( (letter) => {
additionalMnemonics.push("" + letter.toUpperCase() + letter);
});
let mnemonics = baseMnemonics.concat(additionalMnemonics);
mnemonics.forEach( (letter) => {
this.tagList[letter] = undefined;
});
}
tagElement(elem) {
if (!this.isBoundedByViewport(elem)) {
return;
}
let keys = Object.keys(this.tagList);
for (let key_i in keys) {
let key = keys[key_i];
if (this.tagList[key] === undefined) {
if ( this.addOverlay(elem, key) ) {
this.tagList[key] = elem;
}
break;
}
}
}
isBoundedByViewport(elem) {
const viewport = window.visualViewport;
const rect = elem.getClientRects()[0];
if (rect == undefined) {
return false;
}
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const elemTop = (scrollTop + rect.top);
if (elemTop > viewport.pageTop && elemTop < (viewport.pageTop + viewport.height)) {
return true;
}
return false;
}
addOverlay(parentElem, mnemonic) {
var rects = parentElem.getClientRects();
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
if (parentElem._overlayElem == undefined) {
var rect = undefined;
for (let i = 0; i < rects.length; i++) {
if (rects[i] !== undefined) {
rect = rects[i];
break;
}
}
if (rect === undefined) {
return false;
}
let elem = document.createElement('div');
elem.style.position = 'absolute';
elem.style.border = '1px solid blue';
elem.style.borderRadius = '5px';
elem.style.background = 'rgba(0, 0, 255, 0.8)';
elem.style.color = 'white';
elem.style.font = '12px Helvetica bold';
elem.style.padding = '2px';
elem.innerText = "[" + mnemonic + "]";
elem.style.top = (rect.top + scrollTop) + 'px';
elem.style.left = (rect.left + scrollLeft) + 'px';
document.body.appendChild(elem);
parentElem._overlayElem = elem;
return true;
}
return false;
}
removeOverlay(elem) {
let overlayElem = elem._overlayElem;
if (overlayElem != undefined) {
document.body.removeChild(overlayElem);
elem._overlayElem = undefined;
}
}
tagDocument() {
var elt = document.getElementsByTagName("a");
for (let i = 0; i < elt.length; i++) {
this.tagElement(elt[i]);
}
this.isTagged = true;
}
untagDocument() {
const keys = Object.keys(this.tagList);
for (let key_i in keys) {
let elem = this.tagList[keys[key_i]];
if (elem !== undefined) {
this.removeOverlay(elem);
this.tagList[keys[key_i]] = undefined;
}
}
this.isTagged = false;
}
clickLinkWithTag(tag) {
if (this.tagList[tag] !== undefined) {
this.tagList[tag].click();
}
}
}
(function() {
let tagger = new ElementTagger();
var counter = 0;
var tagAccum = [];
document.addEventListener('keypress', (event) => {
if (document.activeElement !== document.body) {
return;
}
const keyName = String.fromCharCode(event.charCode);
if (keyName == undefined) {
return;
}
if (keyName == 'f') {
if (++counter == 2) {
counter = 0;
if (tagger.isTagged) {
tagger.untagDocument();
} else {
tagger.tagDocument();
}
}
} else {
// Uppercase
if (tagAccum.length > 0 || keyName.toUpperCase() == keyName) {
tagAccum.push(keyName);
}
if (tagAccum.length == 2) {
tagger.clickLinkWithTag(tagAccum.join(''));
tagger.untagDocument();
tagAccum = [];
} else if (tagAccum.length == 0) {
tagger.clickLinkWithTag(keyName);
tagger.untagDocument();
}
}
});
})();