keyboard.js 9.6 KB

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