wikimedia/mediawiki-extensions-MobileFrontend

View on GitHub
src/mobile.special.watchlist.scripts/ScrollEndEventEmitter.js

Summary

Maintainability
A
0 mins
Test Coverage
const util = require( '../mobile.startup/util' ),
    mfExtend = require( '../mobile.startup/mfExtend' );

/**
 * Class to assist a view in implementing infinite scrolling on some DOM
 * element. This module itself is only responsible for emitting an Event when
 * the bottom of an Element is scrolled to.
 *
 * @class ScrollEndEventEmitter
 * @mixes OO.EventEmitter
 *
 * Use this class in a view to help it do infinite scrolling.
 *
 * 1. Initialize it in the constructor `initialize` and listen to the
 *   EVENT_SCROLL_END event it emits (and call your loading function then)
 * 2. On preRender (once we have the DOM element) set it into the infinite
 *   scrolling object and disable it until we've loaded.
 * 3. Once you have loaded the list and put it in the DOM, enable the
 *   infinite scrolling detection.
 *   - Every time the scroller detection triggers a load, it auto disables
 *     to not trigger multiple times. After you have loaded, manually
 *     re-enable it.
 *
 * Example:
 *     @example
 *     <code>
 *       var
 *         mfExtend = require( './mfExtend' ),
 *         ScrollEndEventEmitter = require( './ScrollEndEventEmitter' ),
 *         eventBus = require( './eventBusSingleton' );
 *       mfExtend( PhotoList, View, {
 *         //...
 *         initialize: function ( options ) {
 *           this.gateway = new PhotoListGateway( {
 *             username: options.username
 *           } );
 *           // 1. Set up infinite scroll helper and listen to events
 *           this.scrollEndEventEmitter = new ScrollEndEventEmitter( eventBus, 1000 );
 *           this.scrollEndEventEmitter.on( ScrollEndEventEmitter.EVENT_SCROLL_END,
 *             this._loadPhotos.bind( this ) );
 *           View.prototype.initialize.apply( this, arguments );
 *         },
 *         preRender: function () {
 *           // 2. Disable until we've got the list rendered and set DOM el
 *           this.scrollEndEventEmitter.setElement( this.$el );
 *           this.scrollEndEventEmitter.disable();
 *         },
 *         _loadPhotos: function () {
 *           var self = this;
 *           this.gateway.getPhotos().then( function ( photos ) {
 *             // load photos into the DOM ...
 *             // 3. and (re-)enable infinite scrolling
 *             self.scrollEndEventEmitter.enable();
 *           } );
 *         }
 *       } );
 *     </code>
 *
 * @fires ScrollEndEventEmitter#ScrollEndEventEmitter-scrollEnd
 * @param {Object} eventBus object to listen for scroll:throttled events
 * @param {number} [threshold=100] distance in pixels used to calculate if scroll
 * position is near the end of the $el
 */
function ScrollEndEventEmitter( eventBus, threshold ) {
    this.threshold = threshold || 100;
    this.eventBus = eventBus;
    this.enable();
    OO.EventEmitter.call( this );
}
OO.mixinClass( ScrollEndEventEmitter, OO.EventEmitter );

/**
 * Fired when scroll bottom has been reached.
 *
 * @event ScrollEndEventEmitter#ScrollEndEventEmitter-scrollEnd
 */
ScrollEndEventEmitter.EVENT_SCROLL_END = 'ScrollEndEventEmitter-scrollEnd';

mfExtend( ScrollEndEventEmitter, {
    /**
     * Listen to scroll on window and notify this._onScroll
     *
     * @memberof ScrollEndEventEmitter
     * @instance
     * @private
     */
    _bindScroll() {
        if ( !this._scrollHandler ) {
            this._scrollHandler = this._onScroll.bind( this );
            this.eventBus.on( 'scroll:throttled', this._scrollHandler );
        }
    },
    /**
     * Unbind scroll handler
     *
     * @memberof ScrollEndEventEmitter
     * @instance
     * @private
     */
    _unbindScroll() {
        if ( this._scrollHandler ) {
            this.eventBus.off( 'scroll:throttled', this._scrollHandler );
            this._scrollHandler = null;
        }
    },
    /**
     * Scroll handler. Triggers load event when near the end of the container.
     *
     * @memberof ScrollEndEventEmitter
     * @instance
     * @private
     */
    _onScroll() {
        if ( this.$el && this.enabled && this.scrollNearEnd() ) {
            // Disable when triggering an event. Won't trigger again until
            // re-enabled.
            this.disable();
            this.emit( ScrollEndEventEmitter.EVENT_SCROLL_END );
        }
    },
    /**
     * Is the scroll position near the end of the container element?
     *
     * @memberof ScrollEndEventEmitter
     * @instance
     * @private
     * @return {boolean}
     */
    scrollNearEnd() {
        if ( !this.$el || !this.$el.offset() ) {
            return false;
        }
        const $window = util.getWindow(),
            scrollBottom = $window.scrollTop() + $window.height(),
            endPosition = this.$el.offset().top + this.$el.outerHeight();
        return scrollBottom + this.threshold > endPosition;
    },
    /**
     * Enable the ScrollEndEventEmitter so that it triggers events.
     *
     * @memberof ScrollEndEventEmitter
     * @instance
     */
    enable() {
        this.enabled = true;
        this._bindScroll();
    },
    /**
     * Disable the ScrollEndEventEmitter so that it doesn't trigger events.
     *
     * @memberof ScrollEndEventEmitter
     * @instance
     */
    disable() {
        this.enabled = false;
        this._unbindScroll();
    },
    /**
     * Set the element to compare to scroll position to
     *
     * @memberof ScrollEndEventEmitter
     * @instance
     * @param {jQuery.Object} $el jQuery element where we want to listen for
     * scroll end.
     */
    setElement( $el ) {
        this.$el = $el;
    }
} );

module.exports = ScrollEndEventEmitter;