// 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(); } });