keyhinter/keyhinter.js

165 lines
4.9 KiB
JavaScript

// Checks if the provided child is descendant of the parent (any level)
function is_descendant(parent, child) {
var node = child.parentNode;
while (node !== null) {
console.log(node)
if (node === parent) return true;
node = node.parentNode;
}
return false;
}
Number.prototype.base26 = (function () {
return function base26() {
n = this/* w ww .j a v a 2s. co m*/
ret = "";
while (parseInt(n) > 0) {
--n;
ret += String.fromCharCode("A".charCodeAt(0) + (n % 26));
n /= 26;
}
return ret.split("").reverse().join("").toLowerCase();
};
}());
function isVisible(element) {
// Element is obviously not visible
if (element.offsetWidth === 0 || element.offsetHeight === 0) return false;
let rect = element.getClientRects()[0];
if (!rect) return false;
// Get the element clicked in the center of the element rect
var element_in_position = document.elementFromPoint((rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2);
// If there's no result, assume not visible
if (!element_in_position) return false;
// If the element clicked is the same as provided, the element is clearly visible
if (element_in_position === element) return true;
// If the element is not in the position we clicked, check if it's at least a child node
return is_descendant(element, element_in_position);
}
class KeyHinter {
constructor() {
this.number = 0;
this.nodes = {};
this.currentSelection = [];
}
// Highlight
highlight() {
if (this.number !== 0)
return false
this.listenKeybinds()
var _this = this;
document.querySelectorAll('a, form, input').forEach(function (nodeEl) {
if (isVisible(nodeEl)) {
_this.number++;
_this.nodes[_this.number.base26()] = nodeEl;
var el = _this.generateLabelFor(nodeEl);
el.textContent = _this.number.base26();
el.setAttribute('data-keyhinter-label', _this.number.base26());
document.body.appendChild(el);
}
})
}
// Label generation
generateLabelFor(node) {
var el = document.createElement('div');
var nodePosition = node.getBoundingClientRect();
el.classList.add('keyhinter-label');
el.style.left = nodePosition.left + 'px';
el.style.top = nodePosition.top + 'px';
if (node.nodeName === 'A') {
el.classList.add('keyhinter-label-anchor');
} else if (node.nodeName === 'FORM') {
el.classList.add('keyhinter-label-form');
} else if (node.nodeName === 'INPUT') {
el.classList.add('keyhinter-label-input');
}
return el;
}
doClick(obj) {
switch (obj.nodeName) {
case 'INPUT':
obj.focus();
break;
case 'FORM':
obj.submit();
break;
default:
obj.click();
break;
}
}
// Keybinds
listenKeybinds() {
var _this = this;
document.onkeydown = function (event) {
if (event.code === 'Escape') {
event.preventDefault();
_this.destroy();
} else if (event.code.startsWith("Key")) {
event.preventDefault();
var letter = event.key;
_this.currentSelection.push(letter);
document.querySelectorAll('[data-keyhinter-label]').forEach(function (item) {
item.style.visibility = 'hidden';
})
var query = document.querySelectorAll('[data-keyhinter-label^="' + _this.currentSelection.join('') + '"]')
if (query.length === 1) {
var node = _this.nodes[_this.currentSelection.join('')];
_this.doClick(node);
_this.destroy();
} else {
query.forEach(function (item) {
item.style.visibility = 'visible'
})
}
} else {
console.log("Default:" + event.code + "," + event.key)
}
}
}
stopListeningKeybinds() {
document.onkeydown = null;
}
// Non-safe methods
destroy() {
document.querySelectorAll('.keyhinter-label').forEach(function (node) {
document.body.removeChild(node);
})
this.stopListeningKeybinds();
keyHinter = new KeyHinter()
}
}
var keyHinter = new KeyHinter();
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
// If the received message has the expected format...
if (msg.action === 'start_keyhinter') {
// Call the specified callback, passing
// the web-page's DOM content as argument
keyHinter.highlight();
}
});