diff --git a/searx/plugins/infinite_scroll.py b/searx/plugins/infinite_scroll.py deleted file mode 100644 index e3726671a..000000000 --- a/searx/plugins/infinite_scroll.py +++ /dev/null @@ -1,9 +0,0 @@ -from flask_babel import gettext - -name = gettext('Infinite scroll') -description = gettext('Automatically load next page when scrolling to bottom of current page') -default_on = False -preference_section = 'ui' - -js_dependencies = ('plugins/js/infinite_scroll.js',) -css_dependencies = ('plugins/css/infinite_scroll.css',) diff --git a/searx/preferences.py b/searx/preferences.py index 570d0901b..e493dadc0 100644 --- a/searx/preferences.py +++ b/searx/preferences.py @@ -394,6 +394,17 @@ class Preferences: 'False': False } ), + 'infinite_scroll': MapSetting( + settings['ui']['infinite_scroll'], + locked=is_locked('infinite_scroll'), + map={ + '': settings['ui']['infinite_scroll'], + '0': False, + '1': True, + 'True': True, + 'False': False + } + ), # fmt: on } diff --git a/searx/settings.yml b/searx/settings.yml index cec9889b2..7e069a831 100644 --- a/searx/settings.yml +++ b/searx/settings.yml @@ -169,7 +169,6 @@ outgoing: # - 'Ahmia blacklist' # activation depends on outgoing.using_tor_proxy # # these plugins are disabled if nothing is configured .. # - 'Hostname replace' # see hostname_replace configuration below -# - 'Infinite scroll' # - 'Open Access DOI rewrite' # - 'Vim-like hotkeys' diff --git a/searx/settings_defaults.py b/searx/settings_defaults.py index 15b4524c6..0721899a2 100644 --- a/searx/settings_defaults.py +++ b/searx/settings_defaults.py @@ -186,6 +186,7 @@ SCHEMA = { 'results_on_new_tab': SettingsValue(bool, False), 'advanced_search': SettingsValue(bool, False), 'query_in_title': SettingsValue(bool, False), + 'infinite_scroll': SettingsValue(bool, False), }, 'preferences': { 'lock': SettingsValue(list, []), diff --git a/searx/static/plugins/js/infinite_scroll.js b/searx/static/plugins/js/infinite_scroll.js deleted file mode 100644 index cd8096571..000000000 --- a/searx/static/plugins/js/infinite_scroll.js +++ /dev/null @@ -1,40 +0,0 @@ -function hasScrollbar() { - var root = document.compatMode=='BackCompat'? document.body : document.documentElement; - return root.scrollHeight>root.clientHeight; -} - -function loadNextPage() { - var formData = $('#pagination form:last').serialize(); - if (formData) { - $('#pagination').html('
'); - $.ajax({ - type: "POST", - url: $('#search_form').prop('action'), - data: formData, - dataType: 'html', - success: function(data) { - var body = $(data); - $('#pagination').remove(); - $('#main_results').append('
'); - $('#main_results').append(body.find('.result')); - $('#main_results').append(body.find('#pagination')); - if(!hasScrollbar()) { - loadNextPage(); - } - } - }); - } -} - -$(document).ready(function() { - var win = $(window); - if(!hasScrollbar()) { - loadNextPage(); - } - win.scroll(function() { - $("#pagination button").css("visibility", "hidden"); - if ($(document).height() - win.height() - win.scrollTop() < 150) { - loadNextPage(); - } - }); -}); diff --git a/searx/static/themes/__common__/js/image_layout.js b/searx/static/themes/__common__/js/image_layout.js index e37058dfa..329fa46a8 100644 --- a/searx/static/themes/__common__/js/image_layout.js +++ b/searx/static/themes/__common__/js/image_layout.js @@ -29,7 +29,8 @@ this.verticalMargin = verticalMargin; this.horizontalMargin = horizontalMargin; this.maxHeight = maxHeight; - this.isAlignDone = true; + this.trottleCallToAlign = null; + this.alignAfterThrotteling = false; } /** @@ -72,12 +73,12 @@ // not loaded image : make it square as _getHeigth said it imgWidth = height; } - img.style.width = imgWidth + 'px'; - img.style.height = height + 'px'; - img.style.marginLeft = this.horizontalMargin + 'px'; - img.style.marginTop = this.horizontalMargin + 'px'; - img.style.marginRight = this.verticalMargin - 7 + 'px'; // -4 is the negative margin of the inline element - img.style.marginBottom = this.verticalMargin - 7 + 'px'; + img.setAttribute('width', Math.round(imgWidth)); + img.setAttribute('height', Math.round(height)); + img.style.marginLeft = Math.round(this.horizontalMargin) + 'px'; + img.style.marginTop = Math.round(this.horizontalMargin) + 'px'; + img.style.marginRight = Math.round(this.verticalMargin - 7) + 'px'; // -4 is the negative margin of the inline element + img.style.marginBottom = Math.round(this.verticalMargin - 7) + 'px'; resultNode = img.parentNode.parentNode; if (!resultNode.classList.contains('js')) { resultNode.classList.add('js'); @@ -112,6 +113,23 @@ } }; + ImageLayout.prototype.throttleAlign = function () { + var obj = this; + if (obj.trottleCallToAlign) { + obj.alignAfterThrotteling = true; + } else { + obj.alignAfterThrotteling = false; + obj.align(); + obj.trottleCallToAlign = setTimeout(function () { + if (obj.alignAfterThrotteling) { + obj.align(); + } + obj.alignAfterThrotteling = false; + obj.trottleCallToAlign = null; + }, 20); + } + } + ImageLayout.prototype.align = function () { var i; var results_selectorNode = d.querySelectorAll(this.results_selector); @@ -141,9 +159,9 @@ } }; - ImageLayout.prototype.watch = function () { + ImageLayout.prototype._monitorImages = function () { var i, img; - var obj = this; + var objthrottleAlign = this.throttleAlign.bind(this); var results_nodes = d.querySelectorAll(this.results_selector); var results_length = results_nodes.length; @@ -152,34 +170,53 @@ event.originalTarget.src = w.searxng.static_path + w.searxng.theme.img_load_error; } - function throttleAlign () { - if (obj.isAlignDone) { - obj.isAlignDone = false; - setTimeout(function () { - obj.align(); - obj.isAlignDone = true; - }, 100); - } - } - - // https://developer.mozilla.org/en-US/docs/Web/API/Window/pageshow_event - w.addEventListener('pageshow', throttleAlign); - // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/load_event - w.addEventListener('load', throttleAlign); - // https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event - w.addEventListener('resize', throttleAlign); - for (i = 0; i < results_length; i++) { img = results_nodes[i].querySelector(this.img_selector); - if (img !== null && img !== undefined) { - img.addEventListener('load', throttleAlign); + if (img !== null && img !== undefined && !img.classList.contains('aligned')) { + img.addEventListener('load', objthrottleAlign); // https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror - img.addEventListener('error', throttleAlign); + img.addEventListener('error', objthrottleAlign); + img.addEventListener('timeout', objthrottleAlign); if (w.searxng.theme.img_load_error) { img.addEventListener('error', img_load_error, {once: true}); } + img.classList.add('aligned'); } } + } + + ImageLayout.prototype.watch = function () { + var objthrottleAlign = this.throttleAlign.bind(this); + + // https://developer.mozilla.org/en-US/docs/Web/API/Window/pageshow_event + w.addEventListener('pageshow', objthrottleAlign); + // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/load_event + w.addEventListener('load', objthrottleAlign); + // https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event + w.addEventListener('resize', objthrottleAlign); + + this._monitorImages(); + + var obj = this; + + let observer = new MutationObserver(entries => { + let newElement = false; + for (let i = 0; i < entries.length; i++) { + if (entries[i].addedNodes.length > 0 && entries[i].addedNodes[0].classList.contains('result')) { + newElement = true; + break; + } + } + if (newElement) { + obj._monitorImages(); + } + }); + observer.observe(d.querySelector(this.container_selector), { + childList: true, + subtree: true, + attributes: false, + characterData: false, + }) }; w.searxng.ImageLayout = ImageLayout; diff --git a/searx/static/themes/oscar/gruntfile.js b/searx/static/themes/oscar/gruntfile.js index 8e118afd6..2f87e289f 100644 --- a/searx/static/themes/oscar/gruntfile.js +++ b/searx/static/themes/oscar/gruntfile.js @@ -78,7 +78,7 @@ module.exports = function(grunt) { } }, jshint: { - files: ['gruntfile.js', 'src/js/*.js', '../__common__/js/image_layout.js'], + files: ['gruntfile.js', 'src/js/*.js'], // files in __common__ are linted by es lint in simple theme options: { reporterOutput: "", esversion: 6, diff --git a/searx/static/themes/oscar/src/js/01_init.js b/searx/static/themes/oscar/src/js/01_init.js index 8853d9909..f72b0078b 100644 --- a/searx/static/themes/oscar/src/js/01_init.js +++ b/searx/static/themes/oscar/src/js/01_init.js @@ -19,6 +19,7 @@ window.searxng = (function(d) { return { autocompleter: script.getAttribute('data-autocompleter') === 'true', + infinite_scroll: script.getAttribute('data-infinite-scroll') === 'true', method: script.getAttribute('data-method'), translations: JSON.parse(script.getAttribute('data-translations')) }; diff --git a/searx/static/themes/oscar/src/js/infinite_scroll.js b/searx/static/themes/oscar/src/js/infinite_scroll.js new file mode 100644 index 000000000..6dbff5fef --- /dev/null +++ b/searx/static/themes/oscar/src/js/infinite_scroll.js @@ -0,0 +1,50 @@ +/** + * @license + * (C) Copyright Contributors to the SearXNG project. + * (C) Copyright Contributors to the searx project (2014 - 2021). + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +$(document).ready(function() { + function hasScrollbar() { + var root = document.compatMode=='BackCompat'? document.body : document.documentElement; + return root.scrollHeight>root.clientHeight; + } + + function loadNextPage() { + var formData = $('#pagination form:last').serialize(); + if (formData) { + $('#pagination').html('
'); + $.ajax({ + type: "POST", + url: $('#search_form').prop('action'), + data: formData, + dataType: 'html', + success: function(data) { + var body = $(data); + $('#pagination').remove(); + $('#main_results').append('
'); + $('#main_results').append(body.find('.result')); + $('#main_results').append(body.find('#pagination')); + if(!hasScrollbar()) { + loadNextPage(); + } + } + }); + } + } + + if (searxng.infinite_scroll) { + var win = $(window); + $("html").addClass('infinite_scroll'); + if(!hasScrollbar()) { + loadNextPage(); + } + win.on('scroll', function() { + if ($(document).height() - win.height() - win.scrollTop() < 150) { + loadNextPage(); + } + }); + } + +}); diff --git a/searx/static/plugins/css/infinite_scroll.css b/searx/static/themes/oscar/src/less/infinite_scroll.less similarity index 91% rename from searx/static/plugins/css/infinite_scroll.css rename to searx/static/themes/oscar/src/less/infinite_scroll.less index 07b9f6de9..f66373651 100644 --- a/searx/static/plugins/css/infinite_scroll.css +++ b/searx/static/themes/oscar/src/less/infinite_scroll.less @@ -2,6 +2,7 @@ 0% { transform: rotate(0deg) } 100% { transform: rotate(360deg) } } + .loading-spinner { animation-duration: 0.75s; animation-iteration-count: infinite; @@ -14,6 +15,7 @@ border-radius: 50% !important; margin: 0 auto; } -#pagination button { + +html.infinite_scroll #pagination button { visibility: hidden; } diff --git a/searx/static/themes/oscar/src/less/logicodev-dark/oscar.less b/searx/static/themes/oscar/src/less/logicodev-dark/oscar.less index 14f23111f..71821a259 100644 --- a/searx/static/themes/oscar/src/less/logicodev-dark/oscar.less +++ b/searx/static/themes/oscar/src/less/logicodev-dark/oscar.less @@ -4,6 +4,7 @@ @import "../../../../__common__/less/result_templates.less"; @import "../../less/result_templates.less"; @import "../../less/preferences.less"; +@import "../infinite_scroll.less"; @import "../../generated/pygments-logicodev.less"; @stacked-bar-chart: rgb(213, 216, 215, 1); diff --git a/searx/static/themes/oscar/src/less/logicodev/oscar.less b/searx/static/themes/oscar/src/less/logicodev/oscar.less index 187368f71..61e03745b 100644 --- a/searx/static/themes/oscar/src/less/logicodev/oscar.less +++ b/searx/static/themes/oscar/src/less/logicodev/oscar.less @@ -4,6 +4,7 @@ @import "../../../../__common__/less/result_templates.less"; @import "../../less/result_templates.less"; @import "../../less/preferences.less"; +@import "../infinite_scroll.less"; @import "../../generated/pygments-logicodev.less"; @import "navbar.less"; diff --git a/searx/static/themes/oscar/src/less/pointhi/oscar.less b/searx/static/themes/oscar/src/less/pointhi/oscar.less index e9851458d..d54fa28d9 100644 --- a/searx/static/themes/oscar/src/less/pointhi/oscar.less +++ b/searx/static/themes/oscar/src/less/pointhi/oscar.less @@ -4,6 +4,7 @@ @import "../../../../__common__/less/result_templates.less"; @import "../../less/result_templates.less"; @import "../../less/preferences.less"; +@import "../infinite_scroll.less"; @import "../../generated/pygments-pointhi.less"; @import "footer.less"; diff --git a/searx/static/themes/simple/src/js/main/00_toolkit.js b/searx/static/themes/simple/src/js/main/00_toolkit.js index c5b7fe578..f53842d72 100644 --- a/searx/static/themes/simple/src/js/main/00_toolkit.js +++ b/searx/static/themes/simple/src/js/main/00_toolkit.js @@ -59,43 +59,45 @@ window.searxng = (function (w, d) { } }; - searxng.http = function (method, url) { - var req = new XMLHttpRequest(), - resolve = function () {}, - reject = function () {}, - promise = { - then: function (callback) { resolve = callback; return promise; }, - catch: function (callback) { reject = callback; return promise; } - }; + searxng.http = function (method, url, data = null) { + return new Promise(function (resolve, reject) { + try { + var req = new XMLHttpRequest(); + req.open(method, url, true); + req.timeout = 20000; - try { - req.open(method, url, true); + // On load + req.onload = function () { + if (req.status == 200) { + resolve(req.response, req.responseType); + } else { + reject(Error(req.statusText)); + } + }; - // On load - req.onload = function () { - if (req.status == 200) { - resolve(req.response, req.responseType); - } else { - reject(Error(req.statusText)); + // Handle network errors + req.onerror = function () { + reject(Error("Network Error")); + }; + + req.onabort = function () { + reject(Error("Transaction is aborted")); + }; + + req.ontimeout = function () { + reject(Error("Timeout")); } - }; - // Handle network errors - req.onerror = function () { - reject(Error("Network Error")); - }; - - req.onabort = function () { - reject(Error("Transaction is aborted")); - }; - - // Make the request - req.send(); - } catch (ex) { - reject(ex); - } - - return promise; + // Make the request + if (data) { + req.send(data) + } else { + req.send(); + } + } catch (ex) { + reject(ex); + } + }); }; searxng.loadStyle = function (src) { @@ -148,5 +150,16 @@ window.searxng = (function (w, d) { this.parentNode.classList.add('invisible'); }); + function getEndpoint () { + for (var className of d.getElementsByTagName('body')[0].classList.values()) { + if (className.endsWith('_endpoint')) { + return className.split('_')[0]; + } + } + return ''; + } + + searxng.endpoint = getEndpoint(); + return searxng; })(window, document); diff --git a/searx/static/themes/simple/src/js/main/infinite_scroll.js b/searx/static/themes/simple/src/js/main/infinite_scroll.js new file mode 100644 index 000000000..b900e66e2 --- /dev/null +++ b/searx/static/themes/simple/src/js/main/infinite_scroll.js @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/* global searxng */ + +searxng.ready(function () { + 'use strict'; + + searxng.infinite_scroll_supported = ( + 'IntersectionObserver' in window && + 'IntersectionObserverEntry' in window && + 'intersectionRatio' in window.IntersectionObserverEntry.prototype); + + if (searxng.endpoint !== 'results') { + return; + } + + if (!searxng.infinite_scroll_supported) { + console.log('IntersectionObserver not supported'); + return; + } + + let d = document; + var onlyImages = d.getElementById('results').classList.contains('only_template_images'); + + function newLoadSpinner () { + var loader = d.createElement('div'); + loader.classList.add('loader'); + return loader; + } + + function replaceChildrenWith (element, children) { + element.textContent = ''; + children.forEach(child => element.appendChild(child)); + } + + function loadNextPage (callback) { + var form = d.querySelector('#pagination form.next_page'); + if (!form) { + return + } + replaceChildrenWith(d.querySelector('#pagination'), [ newLoadSpinner() ]); + var formData = new FormData(form); + searxng.http('POST', d.querySelector('#search').getAttribute('action'), formData).then( + function (response) { + var nextPageDoc = new DOMParser().parseFromString(response, 'text/html'); + var articleList = nextPageDoc.querySelectorAll('#urls article'); + var paginationElement = nextPageDoc.querySelector('#pagination'); + d.querySelector('#pagination').remove(); + if (articleList.length > 0 && !onlyImages) { + // do not add
element when there are only images + d.querySelector('#urls').appendChild(d.createElement('hr')); + } + articleList.forEach(articleElement => { + d.querySelector('#urls').appendChild(articleElement); + }); + if (paginationElement) { + d.querySelector('#results').appendChild(paginationElement); + callback(); + } + } + ).catch( + function (err) { + console.log(err); + var e = d.createElement('div'); + e.textContent = searxng.translations.error_loading_next_page; + e.classList.add('dialog-error'); + e.setAttribute('role', 'alert'); + replaceChildrenWith(d.querySelector('#pagination'), [ e ]); + } + ) + } + + if (searxng.infinite_scroll && searxng.infinite_scroll_supported) { + const intersectionObserveOptions = { + rootMargin: "20rem", + }; + const observedSelector = 'article.result:last-child'; + const observer = new IntersectionObserver(entries => { + const paginationEntry = entries[0]; + if (paginationEntry.isIntersecting) { + observer.unobserve(paginationEntry.target); + loadNextPage(() => observer.observe(d.querySelector(observedSelector), intersectionObserveOptions)); + } + }); + observer.observe(d.querySelector(observedSelector), intersectionObserveOptions); + } + +}); diff --git a/searx/static/themes/simple/src/js/main/preferences.js b/searx/static/themes/simple/src/js/main/preferences.js index 343f20826..09f9cdde4 100644 --- a/searx/static/themes/simple/src/js/main/preferences.js +++ b/searx/static/themes/simple/src/js/main/preferences.js @@ -2,6 +2,10 @@ (function (w, d, searxng) { 'use strict'; + if (searxng.endpoint !== 'preferences') { + return; + } + searxng.ready(function () { let engine_descriptions = null; function load_engine_descriptions () { @@ -19,10 +23,8 @@ } } - if (d.querySelector('body[class="preferences_endpoint"]')) { - for (const el of d.querySelectorAll('[data-engine-name]')) { - searxng.on(el, 'mouseenter', load_engine_descriptions); - } + for (const el of d.querySelectorAll('[data-engine-name]')) { + searxng.on(el, 'mouseenter', load_engine_descriptions); } }); })(window, document, window.searxng); diff --git a/searx/static/themes/simple/src/js/main/results.js b/searx/static/themes/simple/src/js/main/results.js index b9bd43394..609bd8ecd 100644 --- a/searx/static/themes/simple/src/js/main/results.js +++ b/searx/static/themes/simple/src/js/main/results.js @@ -2,6 +2,10 @@ (function (w, d, searxng) { 'use strict'; + if (searxng.endpoint !== 'results') { + return; + } + searxng.ready(function () { searxng.image_thumbnail_layout = new searxng.ImageLayout('#urls', '#urls .result-images', 'img.image_thumbnail', 14, 6, 200); searxng.image_thumbnail_layout.watch(); diff --git a/searx/static/themes/simple/src/less/style.less b/searx/static/themes/simple/src/less/style.less index dd038cdf7..29cf554b0 100644 --- a/searx/static/themes/simple/src/less/style.less +++ b/searx/static/themes/simple/src/less/style.less @@ -771,15 +771,19 @@ article[data-vim-selected].category-social { margin: 1rem @results-tablet-offset 0 @results-tablet-offset; display: grid; grid-template-columns: 100%; - grid-template-rows: min-content min-content 1fr min-content min-content; + grid-template-rows: min-content min-content min-content 1fr min-content; gap: 0; grid-template-areas: "corrections" - "urls" "answers" "sidebar" + "urls" "pagination"; + #sidebar { + display: none; + } + #urls { width: inherit; margin: 0; diff --git a/searx/templates/oscar/base.html b/searx/templates/oscar/base.html index de7d05bf6..dbc0699df 100644 --- a/searx/templates/oscar/base.html +++ b/searx/templates/oscar/base.html @@ -100,6 +100,7 @@ {% for script in scripts %} {{""}} diff --git a/searx/templates/oscar/preferences.html b/searx/templates/oscar/preferences.html index 71ee57a62..1a5219b07 100644 --- a/searx/templates/oscar/preferences.html +++ b/searx/templates/oscar/preferences.html @@ -248,6 +248,17 @@ {{ preferences_item_footer(info, label, rtl) }} {% endif %} + {% if 'infinite_scroll' not in locked_preferences %} + {% set label = _('Infinite scroll') %} + {% set info = _('Automatically load next page when scrolling to bottom of current page') %} + {{ preferences_item_header(info, label, rtl, 'infinite_scroll') }} + + {{ preferences_item_footer(info, label, rtl) }} + {% endif %} + {{ plugin_of_category('ui' )}} diff --git a/searx/templates/simple/base.html b/searx/templates/simple/base.html index ffd648171..644c6df9a 100644 --- a/searx/templates/simple/base.html +++ b/searx/templates/simple/base.html @@ -23,7 +23,7 @@ data-method="{{ method or 'POST' }}" data-autocompleter="{% if autocomplete %}true{% else %}false{% endif %}" data-search-on-category-select="{{ 'true' if 'plugins/js/search_on_category_select.js' in scripts else 'false'}}" - data-infinite-scroll="{{ 'true' if 'plugins/js/infinite_scroll.js' in scripts else 'false' }}" + data-infinite-scroll="{% if infinite_scroll %}true{% else %}false{% endif %}" data-hotkeys="{{ 'true' if 'plugins/js/vim_hotkeys.js' in scripts else 'false' }}" data-static-path="{{ url_for('static', filename='themes/simple') }}/" data-translations="{{ translations }}"> diff --git a/searx/templates/simple/preferences.html b/searx/templates/simple/preferences.html index b47cbc774..275a53bf7 100644 --- a/searx/templates/simple/preferences.html +++ b/searx/templates/simple/preferences.html @@ -226,6 +226,18 @@
{{_('Open result links on new browser tabs') }}
{% endif %} + {% if 'infinite_scroll' not in locked_preferences %} +
+ {{ _('Infinite scroll') }} +

+ +

+
{{ _('Automatically load next page when scrolling to bottom of current page') }}
+
+ {% endif %} {{ plugin_preferences('ui') }} {{ tab_footer() }} diff --git a/searx/webapp.py b/searx/webapp.py index eb08d63d9..1314fc276 100755 --- a/searx/webapp.py +++ b/searx/webapp.py @@ -431,6 +431,8 @@ def get_translations(): 'no_item_found': gettext('No item found'), # /preferences: the source of the engine description (wikipedata, wikidata, website) 'Source': gettext('Source'), + # infinite scroll + 'error_loading_next_page': gettext('Error loading the next page'), } @@ -463,6 +465,7 @@ def render(template_name: str, override_theme: str = None, **kwargs): kwargs['preferences'] = request.preferences kwargs['method'] = request.preferences.get_value('method') kwargs['autocomplete'] = request.preferences.get_value('autocomplete') + kwargs['infinite_scroll'] = request.preferences.get_value('infinite_scroll') kwargs['results_on_new_tab'] = request.preferences.get_value('results_on_new_tab') kwargs['advanced_search'] = request.preferences.get_value('advanced_search') kwargs['query_in_title'] = request.preferences.get_value('query_in_title')