%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /proc/985914/root/data/old/home/stash/atlassian-stash/static/widget/
Upload File :
Create Path :
Current File : //proc/985914/root/data/old/home/stash/atlassian-stash/static/widget/paged-scrollable.js

define('widget/paged-scrollable', [
    'aui',
    'jquery',
    'underscore',
    'util/deprecation',
    'util/events',
    'util/function',
    'widget/loaded-range'
], function(
    AJS,
    $,
    _,
    deprecate,
    events,
    fn,
    LoadedRange
    ) {

    'use strict';

    var isIE = $.browser.msie;


    /**
     * An abstract widget that will handle scroll events and load new data as the user nears the edge of the page.
     *
     * To extend PagedScrollable, you must implement:
     * this.requestData(start, limit) : must return a promise with a RestPage object as the first done() argument.
     * this.attachNewContent(data, attachmentMethod) : given a RestPage object and an attachmentMethod ('prepend', 'append', or 'html'),
     * should add new content to the element specified by scrollContentSelector
     *
     * @param scrollPaneSelector - the element with overflow: auto | scroll
     * @param options see PagedScrollable.defaults.
     */
    function PagedScrollable(scrollPaneSelector, options) {

        this.options = $.extend({}, PagedScrollable.defaults, options);

        this.$scrollElement = $(scrollPaneSelector || window);

        if ($.isWindow(this.$scrollElement[0])) {
            // we still want to attach to window.scroll, but documentElement has the properties we need to look at.
            var docEl = window.document.documentElement;
            this.getPaneHeight = function() { return docEl.clientHeight; };
            this.getContentHeight = function() { return docEl.scrollHeight; };
        }

        this._eventHandlers = [];
    }
    /**
     * pageSize: used as the limit parameter to requestData,
     * scrollDelay: the number of milliseconds to debounce before handling a scroll event.
     * bufferPixels: load more data if the user scrolls within this many pixels of the edge of the loaded data.
     * precedingSpaceMaintained: Set this to true if your implementation will add blank space above the loaded content as a placeholder for content
     *              that will be loaded as the user scrolls up.  Setting this to true means PagedScrollable will load a previous page when
     *              the proportion of the content scrolled is less than the start of your loaded range (i.e., wandering into the 'placeholder' territory you've created.
     *              <code>scrollElement.scrollTop < bufferPixels + (loadedRange.start / loadedRange.nextPageStart) * scrollContent.height</code>
     *              Setting it false means a previous page will be loaded when
     *              <code>scrollElement.scrollTop < bufferPixels</code>
     * suspendOnFailure : When enabled, the paged-scrollable will enter a suspended mode, as if PagedScrollable.suspend() was called after a data request fails.
     *              To resume requesting data, call PagedScrollable.resume().
     * dataLoadedEvent : name of the events event that will be fired when data is loaded
     * autoLoad : Whether to automatically load the previous/next page as you scroll to the top/bottom. Either 'previous', 'next', true (for both directions) or false.
     * preventOverscroll: Whether to prevent scrolling past the beginning or end of a scrollable element from causing the window to scroll
     */
    PagedScrollable.defaults = {
        pageSize: 50,
        scrollDelay : 250,
        bufferPixels : 0,
        precedingSpaceMaintained : true,
        suspendOnFailure : true,
        dataLoadedEvent: 'stash.widget.pagedscrollable.dataLoaded',
        autoLoad: true,
        preventOverscroll: false,
        idForEntity: null
    };

    //load the initial data
    PagedScrollable.prototype.init = function(options) {
        PagedScrollable.prototype.reset.call(this);

        options = options || {};

        this.loadedRange = options.loadedRange || new LoadedRange();
        var self = this;
        var pageSize = this.options.pageSize;
        var startAtItem = options.targetedItem ?
                Math.floor(options.targetedItem / pageSize) * pageSize :
                0;

        if (options.suspended) {
            this.suspend();
        }

        // if the start item is already loaded we probably don't have to load any more.
        if (this.loadedRange.isLoaded(startAtItem)) {

            // but it's possible the window is larger than the page size, so trigger a fake scroll anyway just to see if that causes any new loads.
            return (this.loadIfRequired() || $.Deferred().resolve()).done(function() {
                // do our onFirstDataLoaded call now if we can, or after whatever loads next.
                self.onFirstDataLoaded();
            });
        }

        return loadInternal(this, startAtItem, pageSize).then(undefined, function() {
            // TODO: base this on the error returned from REST. Only do it if the line is out of range for the file.
            var isPastEnd = startAtItem !== 0;

            if (isPastEnd) {
                // fallback to the first page.
                return loadInternal(self, 0, pageSize);

            } else {
                // fail the same way as before.
                return $.Deferred().rejectWith(this, arguments);
            }
        }).fail(function(xhr, text, error, data) {
            if (data && data.errors && data.errors.length) {
                self.handleErrors(data.errors);
            }
            // else assume error was already handled.
        });
    };

    PagedScrollable.prototype.reset = function() {
        if (this.currentXHR) {
            this.cancelRequest();
        }

        this.clearListeners();

        if (this._resizeHandler) {
            $(window).off('resize', this._resizeHandler);
            this._resizeHandler = null;
        }

        if (this.options.idForEntity) {
            this._ids = {};
        }

        // must happen after this.cancelRequest() to avoid the scrollable becoming suspended on a reinit.
        this._suspended = false;
    };

    PagedScrollable.prototype.destroy = function() {
        this.reset();
        delete this.$scrollElement;
    };

    /**
     * Stop requesting new data.  Any requests already in the pipeline will complete.
     * To resume requesting data, call PagedScrollable.resume();
     */
    PagedScrollable.prototype.suspend = function () {
        this._suspended = true;
    };

    /**
     * Resume requesting new data.
     */
    PagedScrollable.prototype.resume = function () {
        this._suspended = false;

        // if they are near the top/bottom of the page, request the data they need immediately.
        return this.loadIfRequired();
    };

    PagedScrollable.prototype.isSuspended = function() {
        return this._suspended;
    };

    /**
     * @returns {Number} the scroll top of the scrollable element
     */
    PagedScrollable.prototype.getScrollTop = function() { return this.$scrollElement.scrollTop(); };

    /**
     * Set the scroll top of the scrollable element
     *
     * @param {Number} scrollTop the scroll to to set
     */
    PagedScrollable.prototype.setScrollTop = function(scrollTop) { this.$scrollElement.scrollTop(scrollTop); };

    /**
     * @returns {jQuery} a jQuery object for the scrollable element
     */
    PagedScrollable.prototype.getPane = function() {
        return this.$scrollElement;
    };

    /**
     * @returns {Number} the visible height of the scrollable element
     */
    PagedScrollable.prototype.getPaneHeight = function() {
        return this.$scrollElement[0].clientHeight;
    };

    /**
     * @returns {Number} the actual height of the scrollable element
     */
    PagedScrollable.prototype.getContentHeight = function() {
        return this.$scrollElement[0].scrollHeight;
    };

    /**
     * @param {String} opt the option to return
     * @returns {*} the value of the object
     */
    PagedScrollable.prototype.getOption = function(opt) {
        if (Object.prototype.hasOwnProperty.call(this.options, opt)) {
            return this.options[opt];
        }
        return undefined;
    };

    /**
     * Override the existing options
     *
     * @param {Object} opts the options to override
     */
    PagedScrollable.prototype.setOptions = function(opts) {
        if ($.isPlainObject(opts)) {
            this.options = $.extend(this.options, opts);
        }
    };

    /**
     * Bind a scroll listener to the paged scrollable
     *
     * @param {Function} func the scroll listener
     */
    PagedScrollable.prototype.addScrollListener = function(func) {
        var handler = this.scrollDelay ? _.debounce(func, this.scrollDelay) : func;
        this._eventHandlers.push(handler);
        this.$scrollElement.on('scroll.paged-scrollable', handler);
    };

    /**
     * @deprecated since 2.6. Clear all listeners using clearListeners().
     * @type {Function}
     */
    PagedScrollable.prototype.clearScrollListeners = deprecate.fn(PagedScrollable.prototype.clearListeners,
        'widget/paged-scrollable::clearScrollListeners', 'widget/paged-scrollable::clearListeners', '2.6', '3.0');

    PagedScrollable.prototype._bindOverscrollPrevention = function() {
        function overscrollPrevention(e, delta) {
            var height = $(this).outerHeight();
            var scrollHeight = this.scrollHeight;

            if((this.scrollTop === (scrollHeight - height) && delta < 0) || (this.scrollTop === 0 && delta > 0)) {
                //If at the bottom scrolling down, or at the top scrolling up
                e.preventDefault();
            }
        }
        this._eventHandlers.push(overscrollPrevention);
        this.$scrollElement.on('mousewheel.paged-scrollable', overscrollPrevention);
    };

    /**
     * Unbind all listeners
     */
    PagedScrollable.prototype.clearListeners = function() {
        var self = this;
        _.each(this._eventHandlers, function (handler) {
            self.$scrollElement.unbind('.paged-scrollable', handler);
        });
        this._eventHandlers.length = 0;
    };

    PagedScrollable.prototype.loadIfRequired = function() {
        if (this.isSuspended() || (this.loadedRange.reachedEnd() && this.loadedRange.reachedStart())) {
            return;
        }

        var scrollTop = this.getScrollTop(),
            scrollPaneHeight = this.getPaneHeight(),
            contentHeight = this.getContentHeight(),
            scrollBottom = scrollPaneHeight + scrollTop;

        if (!$.isWindow(this.getPane()[0]) && this.getPane().is(":hidden")) {
            return;
        }

        // paging previous might not work if page is filtered and deeplinked
        if (_.any([true, 'previous'], fn.eq(this.options.autoLoad)) &&
            scrollTop  < this.options.bufferPixels + (this.loadedRange.start / this.loadedRange.nextPageStart) * contentHeight) {

            var pageBefore = this.loadedRange.pageBefore(this.options.pageSize);
            if (pageBefore) {
                return this.load(pageBefore.start, pageBefore.limit);
            }
        }

        // In Chrome on Windows at some font sizes (Ctrl +), the scrollPaneHeight is rounded down, but contentHeight is
        // rounded up (I think). This means there is a 1px difference between them and the event won't fire.
        var chromeWindowsFontChangeBuffer = 1;

        if (_.any([true, 'next'], fn.eq(this.options.autoLoad)) &&
            scrollBottom + chromeWindowsFontChangeBuffer >= contentHeight - this.options.bufferPixels) {
            var pageAfter = this.loadedRange.pageAfter(this.options.pageSize);
            if (pageAfter) {
                return this.load(pageAfter.start, pageAfter.limit);
            }
        }
    };

    function loadInternal(self, start, limit) {
        if (self.currentXHR) {
            return $.Deferred().reject();
        }

        self.currentXHR = self.requestData(start, limit);

        return self.currentXHR.always(function () {
                self.currentXHR = null;
            })
            .done(function(data) {
                self.onDataLoaded(start, limit, data);
            })
            .fail(function() {
                self.suspend();
            });
    }

    PagedScrollable.prototype.load = function(start, limit) {
        var self = this;
        return loadInternal(this, start, limit).fail(function(xhr, text, error, data) {
            if (data && data.errors) {
                self.handleErrors(data.errors);
            }
        });
    };
    PagedScrollable.prototype.loadAfter = function() {
        var pageAfter = this.loadedRange.pageAfter(this.options.pageSize);
        return pageAfter && this.load(pageAfter.start, pageAfter.limit);
    };
    PagedScrollable.prototype.loadBefore = function() {
        var pageBefore = this.loadedRange.pageBefore(this.options.pageSize);
        return pageBefore && this.load(pageBefore.start, pageBefore.limit);
    };

    PagedScrollable.prototype.onDataLoaded = function(start, limit, data) {
        if (data.start !== undefined) {
            start = data.start;
        }
        var firstLoad = this.loadedRange.isEmpty(),
            attachmentMethod = this.loadedRange.getAttachmentMethod(start, data.size),
            isPrepend = attachmentMethod === 'prepend';

        this.loadedRange.add(start, data.size, data.isLastPage, data.nextPageStart);

        var oldHeight,
            oldScrollTop;
        if (isPrepend || isIE) { // values for calculating offset
            oldScrollTop = this.getScrollTop();
            oldHeight = this.getContentHeight();
        }

        data = this._addPage(data, attachmentMethod);

        // scroll to where the user was before we added new data.  IE reverts to the initial position (top or line
        // specified in hash) when you append content, so we need to always rescroll in IE.
        if (isPrepend || isIE) {
            var heightAddedAbove = isPrepend ? this.getContentHeight() - oldHeight : 0;
            this.setScrollTop(oldScrollTop + heightAddedAbove);
        }

        if (firstLoad) {
            this.onFirstDataLoaded(start, limit, data);
        }

        events.trigger(this.options.dataLoadedEvent, this, start, limit, data );

        //retrigger scroll - load more if we're still at the edges.
        this.loadIfRequired();
    };

    /**
     * Filters duplicates and adds the page to the table.
     *
     * @param {Object} data the original page data
     * @param attachmentMethod: <code>prepend</code>, <code>append</code> or <code>html</code>
     * @returns {Object} data the filtered page data added to the table
     * @private
     */
    PagedScrollable.prototype._addPage = function(data, attachmentMethod) {
        data = this._dedupe(data);
        this.attachNewContent(data, attachmentMethod);
        return data;
    };

    /**
     * Remove any duplicate entities which have already appeared in the PagedScrollable
     *
     * @param {Object} data the original data which will not be modified
     * @returns {Object} the data modified
     * @private
     */
    PagedScrollable.prototype._dedupe = function(data) {
        // Only dedupe data when a idForEntity is provided and
        // the data is a page

        if (data && data.values && this.options.idForEntity) {
            var ids = this._ids;
            var idForEntity = this.options.idForEntity;

            data = $.extend({}, data, {
                values: _.filter(data.values, function(entity) {
                    var id = idForEntity(entity);
                    if (!_.has(ids, id)) {
                        ids[id] = true;
                        return true;
                    }
                    return false;
                })
            });
        }

        return data;
    };

    PagedScrollable.prototype.onFirstDataLoaded = function() {
        var self = this;
        this.addScrollListener(function() { self.loadIfRequired(); });

        if (this.options.preventOverscroll) {
            this._bindOverscrollPrevention();
        }

        $(window).on('resize', this._resizeHandler = function() {
            self.loadIfRequired();
        });
    };

    PagedScrollable.prototype.cancelRequest = function() {
        if (this.currentXHR) {
            if (this.currentXHR.abort) {
                this.currentXHR.abort();

            } else if (this.currentXHR.reject) {
                this.currentXHR.reject();

            } else {
                AJS.log("Couldn't cancel the current request.");
            }

            this.currentXHR = null;
        }
    };

    PagedScrollable.prototype.add = function(entities, attachmentMethod) {
        if (entities.length) {
            entities = this._addPage({
                values: entities,
                size: entities.length
            }, attachmentMethod || 'prepend');
            return true;
        }
        return false;
    };

    PagedScrollable.prototype.remove = function(entity) {
        // We only adjust the loadedRange if an idForEntity function
        // is provided. Otherwise there is the chance that by decrementing
        // nextPageStart duplicates will appear in the list
        if (this.options.idForEntity) {
            var id = this.options.idForEntity(entity);
            if (_.has(this._ids, id)) {
                delete this._ids[id];
                if (typeof this.loadedRange.nextPageStart === 'number') {
                    this.loadedRange.nextPageStart = Math.max(0, this.loadedRange.nextPageStart - 1);
                }
                return true;
            }
        }
        return false;
    };

    PagedScrollable.prototype.attachNewContent = function(data, attachmentMethod) {
        throw new Error('attachNewContent is abstract and must be implemented.');
    };

    PagedScrollable.prototype.requestData = function(start, limit) {
        throw new Error('requestData is abstract and must be implemented.  It must return a promise. It is preferred to return a jqXHR.');
    };

    PagedScrollable.prototype.handleErrors = function(errors) {
        throw new Error('handleErrors is abstract and must be implemented.');
    };


    return PagedScrollable;
});

Zerion Mini Shell 1.0