on master help sign in
Merge pull request #547 from ukwt/vim-hotkeys

Vim-inspired hotkeys plugin

commit3c6a54012c157667f1efcb23a9652b973b80cbce authorAdam Tauber <asciimoo@gmail.com> dateMon 25 Apr 2016, 13:22 (10 years ago) parent603ecbec12e41a
4 files changed+375−1
searx/plugins/__init__.py+3−1 searx/plugins/vim_hotkeys.py+10 searx/static/plugins/css/vim_hotkeys.css+26 searx/static/plugins/js/vim_hotkeys.js+336
searx/plugins/__init__.py+3−1
@@ -23,7 +23,8 @@ from searx.plugins import (https_rewrite,
2323 open_results_on_new_tab,
2424 self_info,
2525 search_on_category_select,
26− tracker_url_remover)
26+ tracker_url_remover,
27+ vim_hotkeys)
2728
2829required_attrs = (('name', str),
2930 ('description', str),
@@ -77,3 +78,4 @@ plugins.register(open_results_on_new_tab)
7778plugins.register(self_info)
7879plugins.register(search_on_category_select)
7980plugins.register(tracker_url_remover)
81+plugins.register(vim_hotkeys)
searx/plugins/vim_hotkeys.pynew file+10
@@ -0,0 +1,10 @@
1+from flask.ext.babel import gettext
2+
3+name = gettext('Vim-like hotkeys')
4+description = gettext('Navigate search results with Vim-like hotkeys '
5+ '(JavaScript required). '
6+ 'Press "h" key on main or result page to get help.')
7+default_on = False
8+
9+js_dependencies = ('plugins/js/vim_hotkeys.js',)
10+css_dependencies = ('plugins/css/vim_hotkeys.css',)
searx/static/plugins/css/vim_hotkeys.cssnew file+26
@@ -0,0 +1,26 @@
1+.vim-hotkeys-help {
2+ position: fixed;
3+ top: 50%;
4+ left: 50%;
5+ transform: translate(-50%, -50%);
6+ z-index: 9999999;
7+ overflow-y: auto;
8+ max-height: 80%;
9+ box-shadow: 0 0 1em;
10+}
11+
12+.dflex {
13+ display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
14+ display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */
15+ display: -ms-flexbox; /* TWEENER - IE 10 */
16+ display: -webkit-flex; /* NEW - Chrome */
17+ display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */
18+}
19+
20+.iflex {
21+ -webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */
22+ -moz-box-flex: 1; /* OLD - Firefox 19- */
23+ -webkit-flex: 1; /* Chrome */
24+ -ms-flex: 1; /* IE 10 */
25+ flex: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */
26+}
searx/static/plugins/js/vim_hotkeys.jsnew file+336
@@ -0,0 +1,336 @@
1+$(document).ready(function() {
2+ highlightResult('top')();
3+
4+ $('.result').on('click', function() {
5+ highlightResult($(this))();
6+ });
7+
8+ var vimKeys = {
9+ 27: {
10+ key: 'Escape',
11+ fun: removeFocus,
12+ des: 'remove focus from the focused input',
13+ cat: 'Control'
14+ },
15+ 73: {
16+ key: 'i',
17+ fun: searchInputFocus,
18+ des: 'focus on the search input',
19+ cat: 'Control'
20+ },
21+ 66: {
22+ key: 'b',
23+ fun: scrollPage(-window.innerHeight),
24+ des: 'scroll one page up',
25+ cat: 'Navigation'
26+ },
27+ 70: {
28+ key: 'f',
29+ fun: scrollPage(window.innerHeight),
30+ des: 'scroll one page down',
31+ cat: 'Navigation'
32+ },
33+ 85: {
34+ key: 'u',
35+ fun: scrollPage(-window.innerHeight / 2),
36+ des: 'scroll half a page up',
37+ cat: 'Navigation'
38+ },
39+ 68: {
40+ key: 'd',
41+ fun: scrollPage(window.innerHeight / 2),
42+ des: 'scroll half a page down',
43+ cat: 'Navigation'
44+ },
45+ 71: {
46+ key: 'g',
47+ fun: scrollPageTo(-document.body.scrollHeight, 'top'),
48+ des: 'scroll to the top of the page',
49+ cat: 'Navigation'
50+ },
51+ 86: {
52+ key: 'v',
53+ fun: scrollPageTo(document.body.scrollHeight, 'bottom'),
54+ des: 'scroll to the bottom of the page',
55+ cat: 'Navigation'
56+ },
57+ 75: {
58+ key: 'k',
59+ fun: highlightResult('up'),
60+ des: 'select previous search result',
61+ cat: 'Results'
62+ },
63+ 74: {
64+ key: 'j',
65+ fun: highlightResult('down'),
66+ des: 'select next search result',
67+ cat: 'Results'
68+ },
69+ 80: {
70+ key: 'p',
71+ fun: pageButtonClick(0),
72+ des: 'go to previous page',
73+ cat: 'Results'
74+ },
75+ 78: {
76+ key: 'n',
77+ fun: pageButtonClick(1),
78+ des: 'go to next page',
79+ cat: 'Results'
80+ },
81+ 79: {
82+ key: 'o',
83+ fun: openResult(false),
84+ des: 'open search result',
85+ cat: 'Results'
86+ },
87+ 84: {
88+ key: 't',
89+ fun: openResult(true),
90+ des: 'open the result in a new tab',
91+ cat: 'Results'
92+ },
93+ 82: {
94+ key: 'r',
95+ fun: reloadPage,
96+ des: 'reload page from the server',
97+ cat: 'Control'
98+ },
99+ 72: {
100+ key: 'h',
101+ fun: toggleHelp,
102+ des: 'toggle help window',
103+ cat: 'Other'
104+ }
105+ };
106+
107+ $(document).keyup(function(e) {
108+ // check for modifiers so we don't break browser's hotkeys
109+ if (vimKeys.hasOwnProperty(e.keyCode)
110+ && !e.ctrlKey
111+ && !e.altKey
112+ && !e.shiftKey
113+ && !e.metaKey)
114+ {
115+ if (e.keyCode === 27) {
116+ if (e.target.tagName.toLowerCase() === 'input') {
117+ vimKeys[e.keyCode].fun();
118+ }
119+ } else {
120+ if (e.target === document.body) {
121+ vimKeys[e.keyCode].fun();
122+ }
123+ }
124+ }
125+ });
126+
127+ function highlightResult(which) {
128+ return function() {
129+ var current = $('.result[data-vim-selected]');
130+ if (current.length === 0) {
131+ current = $('.result:first');
132+ if (current.length === 0) {
133+ return;
134+ }
135+ }
136+
137+ var next;
138+
139+ if (typeof which !== 'string') {
140+ next = which;
141+ } else {
142+ switch (which) {
143+ case 'visible':
144+ var top = $(window).scrollTop();
145+ var bot = top + $(window).height();
146+ var results = $('.result');
147+
148+ for (var i = 0; i < results.length; i++) {
149+ next = $(results[i]);
150+ var etop = next.offset().top;
151+ var ebot = etop + next.height();
152+
153+ if ((ebot <= bot) && (etop > top)) {
154+ break;
155+ }
156+ }
157+ break;
158+ case 'down':
159+ next = current.next('.result');
160+ if (next.length === 0) {
161+ next = $('.result:first');
162+ }
163+ break;
164+ case 'up':
165+ next = current.prev('.result');
166+ if (next.length === 0) {
167+ next = $('.result:last');
168+ }
169+ break;
170+ case 'bottom':
171+ next = $('.result:last');
172+ break;
173+ case 'top':
174+ default:
175+ next = $('.result:first');
176+ }
177+ }
178+
179+ if (next) {
180+ current.removeAttr('data-vim-selected').removeClass('well well-sm');
181+ next.attr('data-vim-selected', 'true').addClass('well well-sm');
182+ scrollPageToSelected();
183+ }
184+ }
185+ }
186+
187+ function reloadPage() {
188+ document.location.reload(false);
189+ }
190+
191+ function removeFocus() {
192+ if (document.activeElement) {
193+ document.activeElement.blur();
194+ }
195+ }
196+
197+ function pageButtonClick(num) {
198+ return function() {
199+ var buttons = $('div#pagination button[type="submit"]');
200+ if (buttons.length !== 2) {
201+ console.log('page navigation with this theme is not supported');
202+ return;
203+ }
204+ if (num >= 0 && num < buttons.length) {
205+ buttons[num].click();
206+ } else {
207+ console.log('pageButtonClick(): invalid argument');
208+ }
209+ }
210+ }
211+
212+ function scrollPageToSelected() {
213+ var sel = $('.result[data-vim-selected]');
214+ if (sel.length !== 1) {
215+ return;
216+ }
217+
218+ var wnd = $(window);
219+
220+ var wtop = wnd.scrollTop();
221+ var etop = sel.offset().top;
222+
223+ var offset = 30;
224+
225+ if (wtop > etop) {
226+ wnd.scrollTop(etop - offset);
227+ } else {
228+ var ebot = etop + sel.height();
229+ var wbot = wtop + wnd.height();
230+
231+ if (wbot < ebot) {
232+ wnd.scrollTop(ebot - wnd.height() + offset);
233+ }
234+ }
235+ }
236+
237+ function scrollPage(amount) {
238+ return function() {
239+ window.scrollBy(0, amount);
240+ highlightResult('visible')();
241+ }
242+ }
243+
244+ function scrollPageTo(position, nav) {
245+ return function() {
246+ window.scrollTo(0, position);
247+ highlightResult(nav)();
248+ }
249+ }
250+
251+ function searchInputFocus() {
252+ $('input#q').focus();
253+ }
254+
255+ function openResult(newTab) {
256+ return function() {
257+ var link = $('.result[data-vim-selected] .result_header a');
258+ if (link.length) {
259+ var url = link.attr('href');
260+ if (newTab) {
261+ window.open(url);
262+ } else {
263+ window.location.href = url;
264+ }
265+ }
266+ };
267+ }
268+
269+ function toggleHelp() {
270+ var helpPanel = $('#vim-hotkeys-help');
271+ if (helpPanel.length) {
272+ helpPanel.toggleClass('hidden');
273+ return;
274+ }
275+
276+ var categories = {};
277+
278+ for (var k in vimKeys) {
279+ var key = vimKeys[k];
280+ categories[key.cat] = categories[key.cat] || [];
281+ categories[key.cat].push(key);
282+ }
283+
284+ var sorted = Object.keys(categories).sort(function(a, b) {
285+ return categories[b].length - categories[a].length;
286+ });
287+
288+ if (sorted.length === 0) {
289+ return;
290+ }
291+
292+ var html = '<div id="vim-hotkeys-help" class="well vim-hotkeys-help">';
293+ html += '<div class="container-fluid">';
294+
295+ html += '<div class="row">';
296+ html += '<div class="col-sm-12">';
297+ html += '<h3>How to navigate searx with Vim-like hotkeys</h3>';
298+ html += '</div>'; // col-sm-12
299+ html += '</div>'; // row
300+
301+ for (var i = 0; i < sorted.length; i++) {
302+ var cat = categories[sorted[i]];
303+
304+ var lastCategory = i === (sorted.length - 1);
305+ var first = i % 2 === 0;
306+
307+ if (first) {
308+ html += '<div class="row dflex">';
309+ }
310+ html += '<div class="col-sm-' + (first && lastCategory ? 12 : 6) + ' dflex">';
311+
312+ html += '<div class="panel panel-default iflex">';
313+ html += '<div class="panel-heading">' + cat[0].cat + '</div>';
314+ html += '<div class="panel-body">';
315+ html += '<ul class="list-unstyled">';
316+
317+ for (var cj in cat) {
318+ html += '<li><kbd>' + cat[cj].key + '</kbd> ' + cat[cj].des + '</li>';
319+ }
320+
321+ html += '</ul>';
322+ html += '</div>'; // panel-body
323+ html += '</div>'; // panel
324+ html += '</div>'; // col-sm-*
325+
326+ if (!first || lastCategory) {
327+ html += '</div>'; // row
328+ }
329+ }
330+
331+ html += '</div>'; // container-fluid
332+ html += '</div>'; // vim-hotkeys-help
333+
334+ $('body').append(html);
335+ }
336+});