// ==UserScript== // @namespace https://gist.github.com/amacfie/04b1da41137ed0bb20c554468f34b67c // @description Add keybindings for focusing the next/previous form element // @include http* // @run-at document-start // @noframes // ==/UserScript== var isGoodEl = function (node) { var formTags = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA']; var isFormEl = 'tagName' in node && formTags.includes(node.tagName); if (!isFormEl) return false; if ('type' in node && node.type === 'hidden') return false; if ('readOnly' in node && node.readOnly) return false; if ('disabled' in node && node.disabled) return false; return true; }; // will start from selected/focused element but not highlighted search match // because the DOM has no access to that var focusNext = function (forwards, nodePred) { var start; if (document.activeElement !== document.body) { start = document.activeElement; } else if (document.getSelection().focusNode) { start = document.getSelection().focusNode; } else { start = document.body; } var walker = document.createTreeWalker( document.body, 5, // NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT doesn't work in GM null, false ); var node; var nodes = [document.body]; while(node = walker.nextNode()) { nodes.push(node); } var startIndex = nodes.indexOf(start); for (var i=1; i <= nodes.length - 1; ++i) { var pos; if (forwards) { pos = (startIndex + i) % nodes.length; } else { pos = (startIndex - i + nodes.length) % nodes.length; } if (nodePred(nodes[pos])) { var prev = document.activeElement; nodes[pos].focus(); if (prev !== document.activeElement) break; } } }; // bind ctrl-` and ctrl-~ var handleKey = function (e) { if (e.ctrlKey && e.key === '`') { focusNext(true, isGoodEl); } else if (e.ctrlKey && e.key === '~') { focusNext(false, isGoodEl); } }; document.addEventListener('keydown', handleKey, false);