Browse Source

Merge pull request #547 from ukwt/vim-hotkeys

Vim-inspired hotkeys plugin
Adam Tauber 9 years ago
parent
commit
3c6a54012c

+ 3 - 1
searx/plugins/__init__.py

@@ -23,7 +23,8 @@ from searx.plugins import (https_rewrite,
                            open_results_on_new_tab,
                            self_info,
                            search_on_category_select,
-                           tracker_url_remover)
+                           tracker_url_remover,
+                           vim_hotkeys)
 
 required_attrs = (('name', str),
                   ('description', str),
@@ -77,3 +78,4 @@ plugins.register(open_results_on_new_tab)
 plugins.register(self_info)
 plugins.register(search_on_category_select)
 plugins.register(tracker_url_remover)
+plugins.register(vim_hotkeys)

+ 10 - 0
searx/plugins/vim_hotkeys.py

@@ -0,0 +1,10 @@
+from flask.ext.babel import gettext
+
+name = gettext('Vim-like hotkeys')
+description = gettext('Navigate search results with Vim-like hotkeys '
+                      '(JavaScript required). '
+                      'Press "h" key on main or result page to get help.')
+default_on = False
+
+js_dependencies = ('plugins/js/vim_hotkeys.js',)
+css_dependencies = ('plugins/css/vim_hotkeys.css',)

+ 26 - 0
searx/static/plugins/css/vim_hotkeys.css

@@ -0,0 +1,26 @@
+.vim-hotkeys-help {
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    z-index: 9999999;
+    overflow-y: auto;
+    max-height: 80%;
+    box-shadow: 0 0 1em;
+}
+
+.dflex {
+    display: -webkit-box;  /* OLD - iOS 6-, Safari 3.1-6 */
+    display: -moz-box;     /* OLD - Firefox 19- (buggy but mostly works) */
+    display: -ms-flexbox;  /* TWEENER - IE 10 */
+    display: -webkit-flex; /* NEW - Chrome */
+    display: flex;         /* NEW, Spec - Opera 12.1, Firefox 20+ */
+}
+
+.iflex {
+    -webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */
+    -moz-box-flex: 1;    /* OLD - Firefox 19- */
+    -webkit-flex: 1;     /* Chrome */
+    -ms-flex: 1;         /* IE 10 */
+    flex: 1;             /* NEW, Spec - Opera 12.1, Firefox 20+ */
+}

+ 336 - 0
searx/static/plugins/js/vim_hotkeys.js

@@ -0,0 +1,336 @@
+$(document).ready(function() {
+    highlightResult('top')();
+
+    $('.result').on('click', function() {
+        highlightResult($(this))();
+    });
+
+    var vimKeys = {
+        27: {
+            key: 'Escape',
+            fun: removeFocus,
+            des: 'remove focus from the focused input',
+            cat: 'Control'
+        },
+        73: {
+            key: 'i',
+            fun: searchInputFocus,
+            des: 'focus on the search input',
+            cat: 'Control'
+        },
+        66: {
+            key: 'b',
+            fun: scrollPage(-window.innerHeight),
+            des: 'scroll one page up',
+            cat: 'Navigation'
+        },
+        70: {
+            key: 'f',
+            fun: scrollPage(window.innerHeight),
+            des: 'scroll one page down',
+            cat: 'Navigation'
+        },
+        85: {
+            key: 'u',
+            fun: scrollPage(-window.innerHeight / 2),
+            des: 'scroll half a page up',
+            cat: 'Navigation'
+        },
+        68: {
+            key: 'd',
+            fun: scrollPage(window.innerHeight / 2),
+            des: 'scroll half a page down',
+            cat: 'Navigation'
+        },
+        71: {
+            key: 'g',
+            fun: scrollPageTo(-document.body.scrollHeight, 'top'),
+            des: 'scroll to the top of the page',
+            cat: 'Navigation'
+        },
+        86: {
+            key: 'v',
+            fun: scrollPageTo(document.body.scrollHeight, 'bottom'),
+            des: 'scroll to the bottom of the page',
+            cat: 'Navigation'
+        },
+        75: {
+            key: 'k',
+            fun: highlightResult('up'),
+            des: 'select previous search result',
+            cat: 'Results'
+        },
+        74: {
+            key: 'j',
+            fun: highlightResult('down'),
+            des: 'select next search result',
+            cat: 'Results'
+        },
+        80: {
+            key: 'p',
+            fun: pageButtonClick(0),
+            des: 'go to previous page',
+            cat: 'Results'
+        },
+        78: {
+            key: 'n',
+            fun: pageButtonClick(1),
+            des: 'go to next page',
+            cat: 'Results'
+        },
+        79: {
+            key: 'o',
+            fun: openResult(false),
+            des: 'open search result',
+            cat: 'Results'
+        },
+        84: {
+            key: 't',
+            fun: openResult(true),
+            des: 'open the result in a new tab',
+            cat: 'Results'
+        },
+        82: {
+            key: 'r',
+            fun: reloadPage,
+            des: 'reload page from the server',
+            cat: 'Control'
+        },
+        72: {
+            key: 'h',
+            fun: toggleHelp,
+            des: 'toggle help window',
+            cat: 'Other'
+        }
+    };
+
+    $(document).keyup(function(e) {
+        // check for modifiers so we don't break browser's hotkeys
+        if (vimKeys.hasOwnProperty(e.keyCode)
+            && !e.ctrlKey
+            && !e.altKey
+            && !e.shiftKey
+            && !e.metaKey)
+        {
+            if (e.keyCode === 27) {
+                if (e.target.tagName.toLowerCase() === 'input') {
+                    vimKeys[e.keyCode].fun();
+                }
+            } else {
+                if (e.target === document.body) {
+                    vimKeys[e.keyCode].fun();
+                }
+            }
+        }
+    });
+
+    function highlightResult(which) {
+        return function() {
+            var current = $('.result[data-vim-selected]');
+            if (current.length === 0) {
+                current = $('.result:first');
+                if (current.length === 0) {
+                    return;
+                }
+            }
+
+            var next;
+
+            if (typeof which !== 'string') {
+                next = which;
+            } else {
+                switch (which) {
+                    case 'visible':
+                        var top = $(window).scrollTop();
+                        var bot = top + $(window).height();
+                        var results = $('.result');
+
+                        for (var i = 0; i < results.length; i++) {
+                            next = $(results[i]);
+                            var etop = next.offset().top;
+                            var ebot = etop + next.height();
+
+                            if ((ebot <= bot) && (etop > top)) {
+                                break;
+                            }
+                        }
+                        break;
+                    case 'down':
+                        next = current.next('.result');
+                        if (next.length === 0) {
+                            next = $('.result:first');
+                        }
+                        break;
+                    case 'up':
+                        next = current.prev('.result');
+                        if (next.length === 0) {
+                            next = $('.result:last');
+                        }
+                        break;
+                    case 'bottom':
+                        next = $('.result:last');
+                        break;
+                    case 'top':
+                    default:
+                        next = $('.result:first');
+                }
+            }
+
+            if (next) {
+                current.removeAttr('data-vim-selected').removeClass('well well-sm');
+                next.attr('data-vim-selected', 'true').addClass('well well-sm');
+                scrollPageToSelected();
+            }
+        }
+    }
+
+    function reloadPage() {
+        document.location.reload(false);
+    }
+
+    function removeFocus() {
+        if (document.activeElement) {
+            document.activeElement.blur();
+        }
+    }
+
+    function pageButtonClick(num) {
+        return function() {
+            var buttons = $('div#pagination button[type="submit"]');
+            if (buttons.length !== 2) {
+                console.log('page navigation with this theme is not supported');
+                return;
+            }
+            if (num >= 0 && num < buttons.length) {
+                buttons[num].click();
+            } else {
+                console.log('pageButtonClick(): invalid argument');
+            }
+        }
+    }
+
+    function scrollPageToSelected() {
+        var sel = $('.result[data-vim-selected]');
+        if (sel.length !== 1) {
+            return;
+        }
+
+        var wnd = $(window);
+
+        var wtop = wnd.scrollTop();
+        var etop = sel.offset().top;
+
+        var offset = 30;
+
+        if (wtop > etop) {
+            wnd.scrollTop(etop - offset);
+        } else  {
+            var ebot = etop + sel.height();
+            var wbot = wtop + wnd.height();
+
+            if (wbot < ebot) {
+                wnd.scrollTop(ebot - wnd.height() + offset);
+            }
+        }
+    }
+
+    function scrollPage(amount) {
+        return function() {
+            window.scrollBy(0, amount);
+            highlightResult('visible')();
+        }
+    }
+
+    function scrollPageTo(position, nav) {
+        return function() {
+            window.scrollTo(0, position);
+            highlightResult(nav)();
+        }
+    }
+
+    function searchInputFocus() {
+        $('input#q').focus();
+    }
+
+    function openResult(newTab) {
+        return function() {
+            var link = $('.result[data-vim-selected] .result_header a');
+            if (link.length) {
+                var url = link.attr('href');
+                if (newTab) {
+                    window.open(url);
+                } else {
+                    window.location.href = url;
+                }
+            }
+        };
+    }
+
+    function toggleHelp() {
+        var helpPanel = $('#vim-hotkeys-help');
+        if (helpPanel.length) {
+            helpPanel.toggleClass('hidden');
+            return;
+        }
+
+        var categories = {};
+
+        for (var k in vimKeys) {
+            var key = vimKeys[k];
+            categories[key.cat] = categories[key.cat] || [];
+            categories[key.cat].push(key);
+        }
+
+        var sorted = Object.keys(categories).sort(function(a, b) {
+            return categories[b].length - categories[a].length;
+        });
+
+        if (sorted.length === 0) {
+            return;
+        }
+
+        var html = '<div id="vim-hotkeys-help" class="well vim-hotkeys-help">';
+        html += '<div class="container-fluid">';
+
+        html += '<div class="row">';
+        html += '<div class="col-sm-12">';
+        html += '<h3>How to navigate searx with Vim-like hotkeys</h3>';
+        html += '</div>'; // col-sm-12
+        html += '</div>'; // row
+
+        for (var i = 0; i < sorted.length; i++) {
+            var cat = categories[sorted[i]];
+
+            var lastCategory = i === (sorted.length - 1);
+            var first = i % 2 === 0;
+
+            if (first) {
+                html += '<div class="row dflex">';
+            }
+            html += '<div class="col-sm-' + (first && lastCategory ? 12 : 6) + ' dflex">';
+
+            html += '<div class="panel panel-default iflex">';
+            html += '<div class="panel-heading">' + cat[0].cat + '</div>';
+            html += '<div class="panel-body">';
+            html += '<ul class="list-unstyled">';
+
+            for (var cj in cat) {
+                html += '<li><kbd>' + cat[cj].key + '</kbd> ' + cat[cj].des + '</li>';
+            }
+
+            html += '</ul>';
+            html += '</div>'; // panel-body
+            html += '</div>'; // panel
+            html += '</div>'; // col-sm-*
+
+            if (!first || lastCategory) {
+                html += '</div>'; // row
+            }
+        }
+
+        html += '</div>'; // container-fluid
+        html += '</div>'; // vim-hotkeys-help
+
+        $('body').append(html);
+    }
+});