Google Tabindexer

Adds tabindex = 1 on heading elements for comfortable [TAB] key navigation.

当前为 2019-10-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Google Tabindexer
  3. // @name:ja Google Tabindexer
  4. // @namespace knoa.jp
  5. // @description Adds tabindex = 1 on heading elements for comfortable [TAB] key navigation.
  6. // @description:ja 主要要素に tabindex = 1 を追加して、[TAB]キーによる操作を快適にします。
  7. // @include https://www.google.*/search?*
  8. // @version 1.0.3
  9. // @grant none
  10. // ==/UserScript==
  11. /*
  12. On the Mac, you confirm "All controls" is checked on System Preferences > Keyboard > Shortcuts.
  13. Macでは システム環境設定 > キーボード > ショートカット で「すべてのコントロール」でTabが効くように設定してください。
  14. */
  15.  
  16. (function(){
  17. const SCRIPTNAME = 'GoogleTabindexer';
  18. const DEBUG = false;/*
  19. */
  20. if(window === top && console.time) console.time(SCRIPTNAME);
  21. const SELECTORS = [
  22. 'input[title]',/* search */
  23. '#hdtbSum a:not([tabindex="-1"])',/* top navigations */
  24. '.r > a:first-of-type',/* main headings */
  25. '#nav a',/* paging */
  26. '#tads a:not([style])[id]',/* ads */
  27. 'h3[role="heading"] a',/* images */
  28. '[data-init-vis="true"] g-inner-card a',/* videos */
  29. 'lazy-load-item a',/* news */
  30. ];
  31. const FOCUSFIRST = '.r > a:first-of-type';
  32. const INDEX = '1';/* set 1 to prevent default tab focuses */
  33. const FLAGNAME = 'tabindexer';/* should be lowercase */
  34. let elements = {}, indexedElements = [];
  35. let core = {
  36. initialize: function(){
  37. core.addTabindex(document.body);
  38. core.focusFirst();
  39. core.observe();
  40. core.tabToScroll();
  41. core.addStyle();
  42. },
  43. addTabindex: function(node){
  44. for(let i = 0; SELECTORS[i]; i++){
  45. let es = node.querySelectorAll(SELECTORS[i]);
  46. for(let j = 0; es[j]; j++){
  47. es[j].tabIndex = INDEX;
  48. es[j].dataset[FLAGNAME] = 'true';
  49. }
  50. }
  51. indexedElements = document.querySelectorAll(`[data-${FLAGNAME}="true"]`);
  52. for(let i = 0; indexedElements[i]; i++){
  53. indexedElements[i].previousTabindexElement = indexedElements[i - 1];
  54. indexedElements[i].nextTabindexElement = indexedElements[i + 1];
  55. }
  56. },
  57. focusFirst: function(){
  58. let target = document.querySelector(FOCUSFIRST);
  59. core.showTarget(target);
  60. target.focus();
  61. },
  62. observe: function(){
  63. document.body.addEventListener('AutoPagerize_DOMNodeInserted', function(e){
  64. core.addTabindex(e.target);
  65. }, true);
  66. },
  67. tabToScroll: function(){
  68. document.body.addEventListener('keypress', function(e){
  69. if(e.key !== 'Tab') return;/* catch only Tab key */
  70. if(e.altKey || e.ctrlKey || e.metaKey) return;
  71. let target = (e.shiftKey) ? e.target.previousTabindexElement : e.target.nextTabindexElement;
  72. if(target) core.showTarget(target);
  73. }, true);
  74. },
  75. showTarget: function(target){
  76. let scroll = function(x, y, deltaY){
  77. setTimeout(function(){window.scrollTo(x, y + deltaY*(100 - 9**2)/100)}, 0*(1000/60));
  78. setTimeout(function(){window.scrollTo(x, y + deltaY*(100 - 8**2)/100)}, 1*(1000/60));
  79. setTimeout(function(){window.scrollTo(x, y + deltaY*(100 - 7**2)/100)}, 2*(1000/60));
  80. setTimeout(function(){window.scrollTo(x, y + deltaY*(100 - 6**2)/100)}, 3*(1000/60));
  81. setTimeout(function(){window.scrollTo(x, y + deltaY*(100 - 5**2)/100)}, 4*(1000/60));
  82. setTimeout(function(){window.scrollTo(x, y + deltaY*(100 - 4**2)/100)}, 5*(1000/60));
  83. setTimeout(function(){window.scrollTo(x, y + deltaY*(100 - 3**2)/100)}, 6*(1000/60));
  84. setTimeout(function(){window.scrollTo(x, y + deltaY*(100 - 2**2)/100)}, 7*(1000/60));
  85. setTimeout(function(){window.scrollTo(x, y + deltaY*(100 - 1**2)/100)}, 8*(1000/60));
  86. setTimeout(function(){window.scrollTo(x, y + deltaY*(100 - 0**2)/100)}, 9*(1000/60));
  87. };
  88. let innerHeight = window.innerHeight, scrollX = window.scrollX, scrollY = window.scrollY;
  89. let rect = target.getBoundingClientRect()/* rect.top: from top of the window */;
  90. switch(true){
  91. case(rect.top < innerHeight*(25/100)):
  92. scroll(scrollX, scrollY, rect.top - innerHeight*(25/100));/* position the target to 25% from top */
  93. break;
  94. case(innerHeight*(75/100) < rect.top):
  95. scroll(scrollX, scrollY, rect.top - innerHeight*(75/100));/* position the target to 75% from top */
  96. break;
  97. default:
  98. /* stay scrollY */
  99. break;
  100. }
  101. },
  102. addStyle: function(name = 'style'){
  103. let style = createElement(core.html[name]());
  104. document.head.appendChild(style);
  105. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  106. elements[name] = style;
  107. },
  108. html: {
  109. style: () => `
  110. <style type="text/css">
  111. a:focus{
  112. text-decoration: underline !important;
  113. }
  114. .r{
  115. position: relative;
  116. overflow: visible !important;
  117. }
  118. .r a:focus:before{
  119. content: "▶";
  120. font-size: medium;
  121. color: lightgray;
  122. position: absolute;
  123. left: -1.25em;
  124. top: 0.1em;
  125. }
  126. </style>
  127. `,
  128. },
  129. };
  130. const createElement = function(html){
  131. let outer = document.createElement('div');
  132. outer.innerHTML = html;
  133. return outer.firstElementChild;
  134. };
  135. const log = function(){
  136. if(!DEBUG) return;
  137. let l = log.last = log.now || new Date(), n = log.now = new Date();
  138. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  139. //console.log(error.stack);
  140. console.log(
  141. SCRIPTNAME + ':',
  142. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  143. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  144. /* :00 */ ':' + line,
  145. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  146. /* caller */ (callers[1] || '') + '()',
  147. ...arguments
  148. );
  149. };
  150. log.formats = [{
  151. name: 'Firefox Scratchpad',
  152. detector: /MARKER@Scratchpad/,
  153. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  154. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  155. }, {
  156. name: 'Firefox Console',
  157. detector: /MARKER@debugger/,
  158. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  159. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  160. }, {
  161. name: 'Firefox Greasemonkey 3',
  162. detector: /\/gm_scripts\//,
  163. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  164. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  165. }, {
  166. name: 'Firefox Greasemonkey 4+',
  167. detector: /MARKER@user-script:/,
  168. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  169. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  170. }, {
  171. name: 'Firefox Tampermonkey',
  172. detector: /MARKER@moz-extension:/,
  173. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  174. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  175. }, {
  176. name: 'Chrome Console',
  177. detector: /at MARKER \(<anonymous>/,
  178. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  179. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  180. }, {
  181. name: 'Chrome Tampermonkey',
  182. detector: /at MARKER \((userscript\.html|chrome-extension:)/,
  183. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+)\)$/)[1] - 6,
  184. getCallers: (e) => e.stack.match(/[^ ]+(?= \((userscript\.html|chrome-extension:))/gm),
  185. }, {
  186. name: 'Edge Console',
  187. detector: /at MARKER \(eval/,
  188. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  189. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  190. }, {
  191. name: 'Edge Tampermonkey',
  192. detector: /at MARKER \(Function/,
  193. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  194. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  195. }, {
  196. name: 'Safari',
  197. detector: /^MARKER$/m,
  198. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  199. getCallers: (e) => e.stack.split('\n'),
  200. }, {
  201. name: 'Default',
  202. detector: /./,
  203. getLine: (e) => 0,
  204. getCallers: (e) => [],
  205. }];
  206. log.format = log.formats.find(function MARKER(f){
  207. if(!f.detector.test(new Error().stack)) return false;
  208. //console.log('//// ' + f.name + '\n' + new Error().stack);
  209. return true;
  210. });
  211. core.initialize();
  212. if(window === top && console.timeEnd) console.timeEnd(SCRIPTNAME);
  213. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址