galetahub/ckeditor

View on GitHub
app/assets/javascripts/ckeditor/filebrowser/javascripts/jquery.endless-scroll.js

Summary

Maintainability
B
5 hrs
Test Coverage
/**
 * Author: Sergey Kukunin
 * See: https://github.com/Kukunin/jquery-endless-scroll
 */
(function($) {
    'use strict';

    // Is pushState supported by this browser?
    $.support.pushState =
        window.history && window.history.pushState && window.history.replaceState &&
        // pushState isn't reliable on iOS until 5.
        !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)

    //Declaration of modules
    var scrollModule = {
        init: function(options, obj) {
            obj.options = $.extend({
                scrollContainer: window,
                scrollPadding: 100,
                scrollEventDelay: 300
            }, options);
            this.options = obj.options;
            this.container = obj.container;
            obj.scrollModule = this;

            this._toplock = true;
            this._bottomlock = true;

            this.scrollContainer = $(this.options.scrollContainer);

            this.updateEntities();
            this.sortMarkers();

            this.currentPage = null;

            this.container.on("jes:afterPageLoad", $.proxy(function(event, url, placement) {
                this.updateEntities();
                this.sortMarkers();
                this.checkMarker();

                if ( placement == "top" ) {
                    //Get offset between first and second pages
                    var offset = this.markers[1].top,
                        scrollTop = this.scrollContainer.scrollTop();

                    this.scrollContainer.scrollTop(scrollTop + offset);
                }
            }, this));

            this.bind();
        },
        updateEntities: function() {
            this.entities = $(this.options.entity, this.container);
        },
        sortMarkers: function() {
            var markers = [];
            $(".jes-marker", this.container).each(function() {
                markers.push({ top: $(this).position().top, url: $(this).data("jesUrl") });
            });
            this.markers = markers;
        },
        //Check, whether user scrolled to another page
        checkMarker: function() {
            var newPage = this.markers[0],
                scrollTop = this.scrollContainer.scrollTop();

            //Determine, what is current page
            for ( var i = 1; i < this.markers.length; i++ ) {
                if ( scrollTop > this.markers[i].top ) {
                    newPage = this.markers[i];
                } else {
                    break;
                }
            }
            if ( newPage.url != this.currentPage ) {
                this.currentPage = newPage.url;
                $(this.container).trigger("jes:scrollToPage", newPage.url);
            }
        },
        bind: function() {
            this.scrollContainer.on("scroll.jes", $.proxy(function(event) {
                if ( this._tId ) { return; }

                this.scrollHandler(event);
                //Clean up mark
                this._tId= setTimeout($.proxy(function(){
                    this._tId = null;
                },this), this.options.scrollEventDelay);

            }, this));
        },
        unbind: function() {
            $(this.options.scrollContainer).off("scroll.jes");
        },
        scrollHandler: function(ev) {
            var $scrollable = this.scrollContainer,
                $entities = this.entities,
                $firstEntity = $entities.first(),
                $lastEntity = $entities.last();

            var scrollTop = $scrollable.scrollTop(),
                height = $scrollable.height(),
                scrollBottom = scrollTop + height;

            var topEntityPosition = $firstEntity.position().top,
                topTarget = topEntityPosition + this.options.scrollPadding,
                bottomEntityPosition = $lastEntity.outerHeight() + $lastEntity.position().top,
                bottomTarget = bottomEntityPosition - this.options.scrollPadding;

            //Don't trigger event again, if already fired
            //Visitor have to leave the area and get back to fire event again
            //Process top threshold
            if ( scrollTop < topTarget ) {
                if ( !this._toplock ) {
                    $(this.container).trigger("jes:topThreshold");
                    this._toplock = true;
                }
            } else {
                this._toplock = false;
            }

            //Process bottom threshold
            if ( scrollBottom > bottomTarget ) {
                if ( !this._bottomlock ) {
                    $(this.container).trigger("jes:bottomThreshold");
                    this._bottomlock = true;
                }
            } else {
                this._bottomlock = false;
            }

            this.checkMarker();
        }
    }

    var ajaxModule = {
        init: function(options, obj) {
            obj.options = $.extend({
            }, options);

            this.options = obj.options;
            this.container = obj.container;

            //markers
            this.setMarker($(this.options.entity, this.container).first(), location.href);

            obj.ajaxModule = this;
        },
        loadPage: function(url, placement, callback) {
            //The hash with methods list
            //depends from placement
            var actions = {
                    top: {
                        find: 'first',
                        inject: 'insertBefore'
                    },
                    bottom: {
                        find: 'last',
                        inject: 'insertAfter'
                    }
                },
                action = actions[placement];

            this.container.trigger("jes:beforePageLoad", [url, placement]);

            //Make AJAX query
            $.get(url, null, $.proxy(function (_data) {
                var data = $("<div>").html(_data),
                containerSelector = this.options.container,
                container = $(containerSelector, data).first();

                if ( container.length ) {
                    //Find entities
                    var entities = container.find(this.options.entity);

                    if ( placement == "bottom" ) {
                        //Remove duplicated (staled) entities from page
                        entities.each(function(i) {
                            var id = $(this).attr("id");
                            if ( id ) {
                                $('#' + id, this.container).remove();
                            }
                        });
                    }

                    //Find the cursor
                    var cursor = $(this.options.entity, containerSelector)[action.find]();

                    //Find and insert entities
                    entities[action.inject](cursor);
                    this.setMarker(entities.first(), url);
                }

                if ( $.isFunction(callback) ) {
                    callback(data);
                }
                this.container.trigger("jes:afterPageLoad", [url, placement, data, entities]);
            }, this), 'html');
        },
        setMarker: function(entity, url) {
            entity.addClass("jes-marker").data("jesUrl", url);
        }
    }

    var navigationModule = {
        init: function(options, obj) {
            obj.options = $.extend({
                nextPage: ".pagination a[rel=next]",
                previousPage: ".pagination a[rel=previous]"
            }, options);

            this.options = obj.options;
            this.container = obj.container;

            $.each([{
                selector: this.options.nextPage,
                event: "jes:bottomThreshold.navigation",
                placement: 'bottom'
            }, {
                selector: this.options.previousPage,
                event: "jes:topThreshold.navigation",
                placement: 'top'
            }], $.proxy(function(i, v) {
                v.element = $(v.selector);
                if ( v.element.length ) {
                    var url = v.element.prop("href"),
                    lock = false;

                    var handler = function() {
                        //this object is container
                        if ( lock || !url ) return;

                        lock = true;
                        obj.ajaxModule.loadPage(url, v.placement, $.proxy(function( data ) {
                            //Search new next link
                            var newElement = $(v.selector, $(data));
                            if ( newElement.length ) {
                                //Update URL and remove lock
                                lock = false;
                                url = newElement.prop("href");
                                v.element.attr("href", url);
                            } else {
                                //Remove event at all
                                $(this).off(v.event);
                                v.element.remove();
                            }
                        }, this));
                    };

                    $(this.container).on(v.event, handler);
                    $(v.element).on("click", $.proxy(function(ev) {
                        ev.preventDefault();
                        handler.apply(this.container)
                    }, this));
                }
            },this));
        }
    }

    var pushStateHistory = {
        init: function(options, obj) {
            if ( !$.support.pushState ) {
                return;
            }

            obj.container.on("jes:scrollToPage", function(event, url) {
                history.replaceState({}, null, url);
            });
        }
    }

    $.endlessScroll = function(options) {

        //Initialize modules
        this.options = $.extend(true, {
            container: "#container",
            entity: ".entity",
            _modules: [ ajaxModule, scrollModule, navigationModule, pushStateHistory ],
            modules: []
        }, options);

        this.container = $(this.options.container);
        if ( !this.container.length ) {
            throw "Container for endless scroll isn't available on the page";
            return;
        }


        //Merge custom options with default
        $.merge(this.options.modules, this.options._modules);
        //Init modules
        this.options.modules.forEach($.proxy(function(module) {
            module.init(this.options, this);
        },this));


        return this;
    }

})(jQuery);