/* # ----------------------------------------------------------------------------- # ~/assets/themes/j1/modules/j1Scroll/js/j1scroll.js # J1 core module for j1scroll # # Product/Info: # https://jekyll.one # # Copyright (C) 2021 Juergen Adams # # J1 Template is licensed under the MIT License. # For details, see https://jekyll.one # ----------------------------------------------------------------------------- # NOTE: Based on https://github.com/jquery-boilerplate/jquery-boilerplate # See: https://www.dotnetcurry.com/jquery/1069/authoring-jquery-plugins # ----------------------------------------------------------------------------- */ // the semi-colon before function invocation is a SAFETY method against // concatenated scripts and/or other plugins which may NOT be closed // properly. // ;(function($, window, document, undefined) { 'use strict'; // Create the defaults var pluginName = 'j1Scroll', defaults = { type: 'infiniteScroll', scrollOffset: 100, elementScroll: false, firstPage: 2, lastPage: false, infoLastPage: false, loadStatus: false, onInit: function (){}, // callback after plugin has initialized onBeforeLoad: function (){}, // callback before new items are loaded onAfterLoad: function (){} // callback after new items are loaded }; // Plugin constructor function Plugin (element, options) { this.element = element; this.settings = $.extend( {}, defaults, options); this.settings.elementID = '#' + this.element.id; // call the initializer this.init(this.settings); } // Avoid Plugin.prototype conflicts $.extend(Plugin.prototype, { // ------------------------------------------------------------------------- // init: initializer // ------------------------------------------------------------------------- init: function(options) { var logger = log4javascript.getLogger('j1Scroll'); logger.info('\n' + 'initializing plugin: started'); logger.info('\n' + 'state: started'); if ( options.elementScroll ) { this.scroller = this.element; } else { this.scroller = window; } if (options.loadStatus) { var spinner = ''; $(spinner).insertAfter(options.elementID); } if (options.infoLastPage) { var message = options.lastPageInfo; $(message).insertAfter(options.elementID); } // initialize infinite scroll if ( options.type === 'infiniteScroll') { logger.info('\n' + 'processing mode: infiniteScroll'); logger.info('\n' + 'loading items from path: ' + options.path + "#void"); logger.info('\n' + 'monitoring element set to: ' + this.scroller); this.registerScrollEvent(options); } logger.info('\n' + 'initializing plugin: finished'); logger.info('\n' + 'state: finished'); }, // ----------------------------------------------------------------------- // bottomReached: detect final scroll position // NOTE: the calculation for BOTTOM position is different for // elementScroll and windowScroll. For elementScroll, the // trigger isBottomReached is TRUE, if the scroll position has // the end of the container PLUS a given scrollOffset. // For windowScroll, the trigger isBottomReached is TRUE, if // the scroll position has the end of the window MINUS // a given scrollOffset. // ----------------------------------------------------------------------- isBottomReached: function (options) { var _this = this; var bottom, scrollY; var clientHeight = $(options.elementID).height(); if ( _this.settings.elementScroll ) { // check scroll position of the container items are to be added var $window = $(window); var viewport_top = $window.scrollTop(); var viewport_height = $window.height(); var viewport_bottom = viewport_top + viewport_height - options.scrollOffset; var $elm = $(options.elementID); var top = $elm.offset().top + clientHeight; var height = $elm.height(); bottom = top + height; return (top >= viewport_top && top < viewport_bottom) || (bottom > viewport_top && bottom <= viewport_bottom) || (height > viewport_height && top <= viewport_top && bottom >= viewport_bottom); } else { // check scroll position of the (overall) window return (window.innerHeight + window.pageYOffset + options.scrollOffset >= document.body.offsetHeight); } }, // ----------------------------------------------------------------------- // detectScroll: EventHandler to load new items for infinite scroll // if final scroll position reached // ----------------------------------------------------------------------- registerScrollEvent: function (options) { var _this = this; var logger = log4javascript.getLogger('j1Scroll'); logger.info('\n' + 'scroll event: register'); var eventHandler_onscroll = function (event) { var options = _this.settings; if (_this.isBottomReached(options)) { if (options.firstPage > options.lastPage ) { logger.info('\n' + 'last page detected on: ' + options.lastPage); window.removeEventListener('scroll', eventHandler_onscroll); logger.info('\n' + 'scroll event: removed'); if (options.infoLastPage ) { _this.infoLastPage(options); } return false; } _this.getNewPost(options); } }; window.addEventListener('scroll', eventHandler_onscroll); logger.info('\n' + 'scroll event: registered'); }, // ----------------------------------------------------------------------- // getNewPost: load new items (from current path) // Note: loader flag prevents to load items if AJAX load in progress // is NOT finished // ----------------------------------------------------------------------- getNewPost: function (options) { var _this = this; var logger = log4javascript.getLogger('j1Scroll'); logger.info('\n' + 'trigger loading '); // initialze loader flag if (this.itemsLoaded === false) return false; // set loader flag (false == not loaded) this.itemsLoaded = false; // display spinner while loading if (options.loadStatus) { logger.info('\n' + 'show: spinner'); $('.loader-ellips').show(); } var xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == XMLHttpRequest.DONE) { if (xmlhttp.status == 200) { options.firstPage++; var childItems = _this.getChildItemsByAjaxHTML(options, xmlhttp.responseText); _this.appendNewItems(childItems); logger.info('\n' + 'loading new items: successful'); // hide the spinner after loading if (options.loadStatus) { logger.info('\n' + 'hide: spinner'); $('.loader-ellips').hide(); } // set loader flag (true == loaded) // return _this.itemsLoaded = true; _this.itemsLoaded = true; } else { // hide the spinner if (options.loadStatus) { logger.info('\n' + 'hide: spinner'); $('.loader-ellips').hide(); } logger.error('\n' + 'loading new items failed, HTTP response: ' + xmlhttp.status ); // set loader flag (true == loaded) // return _this.itemsLoaded = false; _this.itemsLoaded = false; } } }; logger.info('\n' + 'loading new items from path: ' + options.path + options.firstPage); xmlhttp.open("GET", location.origin + options.path + options.firstPage + '/index.html', true); xmlhttp.send(); }, // ----------------------------------------------------------------------- // getChildItemsByAjaxHTML: extract items from page loaded // ----------------------------------------------------------------------- getChildItemsByAjaxHTML: function (options, HTMLText) { var newHTML = document.createElement('html'); var logger = log4javascript.getLogger('j1Scroll'); logger.info('\n' + 'load new items'); newHTML.innerHTML = HTMLText; var childItems = newHTML.querySelectorAll(options.elementID + ' > *'); return childItems; }, // ----------------------------------------------------------------------- // appendNewItems: append items and run post processing // ----------------------------------------------------------------------- appendNewItems: function (items) { var _this = this; var logger = log4javascript.getLogger('j1Scroll'); var cookie_names = j1.getCookieNames(); var user_translate = j1.readCookie(cookie_names.user_translate); logger.info('\n' + 'append new items'); items.forEach(function (item) { _this.element.appendChild(item); }); // no dropcaps if translation enabled if (user_translate.translationEnabled) { logger.info('\n' + 'translation enabled: ' + user_translate.translationEnabled); logger.warn('\n' + 'skipped processing of dropcaps'); } else { // initialize dropcaps logger.info('\n' + 'post processing: createDropCap'); j1.core.createDropCap(); } }, // ----------------------------------------------------------------------- // getNewPost: load/append new items // Note: loader flag prevents to load items if AJAX load in progress // is NOT finished // ----------------------------------------------------------------------- infoLastPage: function (options) { var _this = this; var logger = log4javascript.getLogger('j1Scroll'); logger.info('\n' + 'show: infoLastPage'); $('.page-scroll-last').show(); } }); // END prototype // wrapper around the constructor to prevent multiple instantiations $.fn [pluginName] = function(options) { return this.each(function() { if (!$.data( this, "plugin_" + pluginName)) { $.data(this, "plugin_" + pluginName, new Plugin(this, options)); } }); }; })(jQuery, window, document);