searx_keyboard.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. searx.ready(function() {
  2. searx.on('.result', 'click', function() {
  3. highlightResult(this)(true);
  4. });
  5. searx.on('.result a', 'focus', function(e) {
  6. var el = e.target;
  7. while (el !== undefined) {
  8. if (el.classList.contains('result')) {
  9. if (el.getAttribute("data-vim-selected") === null) {
  10. highlightResult(el)(true);
  11. }
  12. break;
  13. }
  14. el = el.parentNode;
  15. }
  16. }, true);
  17. var vimKeys = {
  18. 27: {
  19. key: 'Escape',
  20. fun: removeFocus,
  21. des: 'remove focus from the focused input',
  22. cat: 'Control'
  23. },
  24. 73: {
  25. key: 'i',
  26. fun: searchInputFocus,
  27. des: 'focus on the search input',
  28. cat: 'Control'
  29. },
  30. 66: {
  31. key: 'b',
  32. fun: scrollPage(-window.innerHeight),
  33. des: 'scroll one page up',
  34. cat: 'Navigation'
  35. },
  36. 70: {
  37. key: 'f',
  38. fun: scrollPage(window.innerHeight),
  39. des: 'scroll one page down',
  40. cat: 'Navigation'
  41. },
  42. 85: {
  43. key: 'u',
  44. fun: scrollPage(-window.innerHeight / 2),
  45. des: 'scroll half a page up',
  46. cat: 'Navigation'
  47. },
  48. 68: {
  49. key: 'd',
  50. fun: scrollPage(window.innerHeight / 2),
  51. des: 'scroll half a page down',
  52. cat: 'Navigation'
  53. },
  54. 71: {
  55. key: 'g',
  56. fun: scrollPageTo(-document.body.scrollHeight, 'top'),
  57. des: 'scroll to the top of the page',
  58. cat: 'Navigation'
  59. },
  60. 86: {
  61. key: 'v',
  62. fun: scrollPageTo(document.body.scrollHeight, 'bottom'),
  63. des: 'scroll to the bottom of the page',
  64. cat: 'Navigation'
  65. },
  66. 75: {
  67. key: 'k',
  68. fun: highlightResult('up'),
  69. des: 'select previous search result',
  70. cat: 'Results'
  71. },
  72. 74: {
  73. key: 'j',
  74. fun: highlightResult('down'),
  75. des: 'select next search result',
  76. cat: 'Results'
  77. },
  78. 80: {
  79. key: 'p',
  80. fun: pageButtonClick(0),
  81. des: 'go to previous page',
  82. cat: 'Results'
  83. },
  84. 78: {
  85. key: 'n',
  86. fun: pageButtonClick(1),
  87. des: 'go to next page',
  88. cat: 'Results'
  89. },
  90. 79: {
  91. key: 'o',
  92. fun: openResult(false),
  93. des: 'open search result',
  94. cat: 'Results'
  95. },
  96. 84: {
  97. key: 't',
  98. fun: openResult(true),
  99. des: 'open the result in a new tab',
  100. cat: 'Results'
  101. },
  102. 82: {
  103. key: 'r',
  104. fun: reloadPage,
  105. des: 'reload page from the server',
  106. cat: 'Control'
  107. },
  108. 72: {
  109. key: 'h',
  110. fun: toggleHelp,
  111. des: 'toggle help window',
  112. cat: 'Other'
  113. }
  114. };
  115. searx.on(document, "keyup", function(e) {
  116. // check for modifiers so we don't break browser's hotkeys
  117. if (vimKeys.hasOwnProperty(e.keyCode) && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
  118. var tagName = e.target.tagName.toLowerCase();
  119. if (e.keyCode === 27) {
  120. if (tagName === 'input' || tagName === 'select' || tagName === 'textarea') {
  121. vimKeys[e.keyCode].fun();
  122. }
  123. } else {
  124. if (e.target === document.body || tagName === 'a' || tagName === 'button') {
  125. vimKeys[e.keyCode].fun();
  126. }
  127. }
  128. }
  129. });
  130. function highlightResult(which) {
  131. return function(noScroll) {
  132. var current = document.querySelector('.result[data-vim-selected]'),
  133. effectiveWhich = which;
  134. if (current === null) {
  135. // no selection : choose the first one
  136. current = document.querySelector('.result');
  137. if (current === null) {
  138. // no first one : there are no results
  139. return;
  140. }
  141. // replace up/down actions by selecting first one
  142. if (which === "down" || which === "up") {
  143. effectiveWhich = current;
  144. }
  145. }
  146. var next, results = document.querySelectorAll('.result');
  147. if (typeof effectiveWhich !== 'string') {
  148. next = effectiveWhich;
  149. } else {
  150. switch (effectiveWhich) {
  151. case 'visible':
  152. var top = document.documentElement.scrollTop || document.body.scrollTop;
  153. var bot = top + document.documentElement.clientHeight;
  154. for (var i = 0; i < results.length; i++) {
  155. next = results[i];
  156. var etop = next.offsetTop;
  157. var ebot = etop + next.clientHeight;
  158. if ((ebot <= bot) && (etop > top)) {
  159. break;
  160. }
  161. }
  162. break;
  163. case 'down':
  164. next = current.nextElementSibling;
  165. if (next === null) {
  166. next = results[0];
  167. }
  168. break;
  169. case 'up':
  170. next = current.previousElementSibling;
  171. if (next === null) {
  172. next = results[results.length - 1];
  173. }
  174. break;
  175. case 'bottom':
  176. next = results[results.length - 1];
  177. break;
  178. case 'top':
  179. /* falls through */
  180. default:
  181. next = results[0];
  182. }
  183. }
  184. if (next) {
  185. current.removeAttribute('data-vim-selected');
  186. next.setAttribute('data-vim-selected', 'true');
  187. var link = next.querySelector('h3 a') || next.querySelector('a');
  188. if (link !== null) {
  189. link.focus();
  190. }
  191. if (!noScroll) {
  192. scrollPageToSelected();
  193. }
  194. }
  195. };
  196. }
  197. function reloadPage() {
  198. document.location.reload(true);
  199. }
  200. function removeFocus() {
  201. if (document.activeElement) {
  202. document.activeElement.blur();
  203. }
  204. }
  205. function pageButtonClick(num) {
  206. return function() {
  207. var buttons = $('div#pagination button[type="submit"]');
  208. if (buttons.length !== 2) {
  209. console.log('page navigation with this theme is not supported');
  210. return;
  211. }
  212. if (num >= 0 && num < buttons.length) {
  213. buttons[num].click();
  214. } else {
  215. console.log('pageButtonClick(): invalid argument');
  216. }
  217. };
  218. }
  219. function scrollPageToSelected() {
  220. var sel = document.querySelector('.result[data-vim-selected]');
  221. if (sel === null) {
  222. return;
  223. }
  224. var wtop = document.documentElement.scrollTop || document.body.scrollTop,
  225. wheight = document.documentElement.clientHeight,
  226. etop = sel.offsetTop,
  227. ebot = etop + sel.clientHeight,
  228. offset = 120;
  229. // first element ?
  230. if ((sel.previousElementSibling === null) && (ebot < wheight)) {
  231. // set to the top of page if the first element
  232. // is fully included in the viewport
  233. window.scroll(window.scrollX, 0);
  234. return;
  235. }
  236. if (wtop > (etop - offset)) {
  237. window.scroll(window.scrollX, etop - offset);
  238. } else {
  239. var wbot = wtop + wheight;
  240. if (wbot < (ebot + offset)) {
  241. window.scroll(window.scrollX, ebot - wheight + offset);
  242. }
  243. }
  244. }
  245. function scrollPage(amount) {
  246. return function() {
  247. window.scrollBy(0, amount);
  248. highlightResult('visible')();
  249. };
  250. }
  251. function scrollPageTo(position, nav) {
  252. return function() {
  253. window.scrollTo(0, position);
  254. highlightResult(nav)();
  255. };
  256. }
  257. function searchInputFocus() {
  258. window.scrollTo(0, 0);
  259. document.querySelector('#q').focus();
  260. }
  261. function openResult(newTab) {
  262. return function() {
  263. var link = document.querySelector('.result[data-vim-selected] h3 a');
  264. if (link !== null) {
  265. var url = link.getAttribute('href');
  266. if (newTab) {
  267. window.open(url);
  268. } else {
  269. window.location.href = url;
  270. }
  271. }
  272. };
  273. }
  274. function toggleHelp() {
  275. var helpPanel = document.querySelector('#vim-hotkeys-help');
  276. if (helpPanel.length) {
  277. helpPanel.classList.toggle('hidden');
  278. return;
  279. }
  280. var categories = {};
  281. for (var k in vimKeys) {
  282. var key = vimKeys[k];
  283. categories[key.cat] = categories[key.cat] || [];
  284. categories[key.cat].push(key);
  285. }
  286. var sorted = Object.keys(categories).sort(function(a, b) {
  287. return categories[b].length - categories[a].length;
  288. });
  289. if (sorted.length === 0) {
  290. return;
  291. }
  292. var html = '<div id="vim-hotkeys-help" class="well vim-hotkeys-help">';
  293. html += '<div class="container-fluid">';
  294. html += '<div class="row">';
  295. html += '<div class="col-sm-12">';
  296. html += '<h3>How to navigate searx with Vim-like hotkeys</h3>';
  297. html += '</div>'; // col-sm-12
  298. html += '</div>'; // row
  299. for (var i = 0; i < sorted.length; i++) {
  300. var cat = categories[sorted[i]];
  301. var lastCategory = i === (sorted.length - 1);
  302. var first = i % 2 === 0;
  303. if (first) {
  304. html += '<div class="row dflex">';
  305. }
  306. html += '<div class="col-sm-' + (first && lastCategory ? 12 : 6) + ' dflex">';
  307. html += '<div class="panel panel-default iflex">';
  308. html += '<div class="panel-heading">' + cat[0].cat + '</div>';
  309. html += '<div class="panel-body">';
  310. html += '<ul class="list-unstyled">';
  311. for (var cj in cat) {
  312. html += '<li><kbd>' + cat[cj].key + '</kbd> ' + cat[cj].des + '</li>';
  313. }
  314. html += '</ul>';
  315. html += '</div>'; // panel-body
  316. html += '</div>'; // panel
  317. html += '</div>'; // col-sm-*
  318. if (!first || lastCategory) {
  319. html += '</div>'; // row
  320. }
  321. }
  322. html += '</div>'; // container-fluid
  323. html += '</div>'; // vim-hotkeys-help
  324. $('body').append(html);
  325. }
  326. });