Show:

File: platform/plugins/Q/web/js/fn/scroller.js

(function (Q, $, window, document, undefined) {

/**
 * Q Tools
 * @module Q-tools
 */

/**
 * Applies 'mousemove' scrolling functionality for some block element container.
 * This algorithm doesn't require any scrollbars and any actions from the user besides (mouse|touch)move over the scrollable element.
 * Contents of the block absolutely arbitrary. When applied, plugin wraps contents into a single 'div' block.
 * @class Q scroller
 * @constructor
 * @param  {Mixed} [Object_or_String] function could have String or Object parameter
 *  @param {Object} [Object_or_String.Object]
 *	 If an Object, then it's a hash of options, that can include:
 *	 @param {Number} [Object_or_String.Object.height] If provided, container height will be restricted to this value. Optional.
 *	 Otherwise container current height will be taken (useful if container already has fixed height and maybe overflow: auto / hidden)
 *	 @param {Boolean} [Object_or_String.Object.startBottom] If true, the scrollable element will be initially scrolled to the bottom of its contents.
 *	 @default false
 *	 @param {Boolean} [Object_or_String.Object.indicators] Whether to show scroll indicators (apply Q/scrollIndicators plugin).
 *	 @default true
 *	 @param {Q.Event} [Object_or_String.Object.onScroll] Q.Event or callback for handling 'onScroll' event.
 * @param {String} [Object_or_String.String]
 *	 If a string, then it's a command which may be:
 *		 "remove": Destroys scroller so it won't function anymore.
 *		 This command may have its own options object passed as reserved1 arg. It can include:
 *			 "restoreOverflow": restores 'overlow' style of the block to the value it has before applying scroller.
 *			 Defaults to true, but sometimes needed to be manually canceled.
 *			 "restoreHeight": restores height of the block to the value it has before applying scroller.
 *			 Defaults to true, but sometimes needed to be manually canceled.
 *			 "unwrap": whether to unwrap scroller contents of the special wrapper that was previously applied to the block
 *			 while applying scroller. Defaults to true, but sometimes needed to be manually canceled.
 *		 "bind": Binds some event handler to the scroller event (i.e. 'onScroll').
 *		 Bind command may must have additional parameters passed as reserved1 and reserved2 args.
 *			 reserved1 string. Name of the event to bind.
 *			 reserved2 function or Q.Event or string function name. Handler to call when the event occurs.
 * @param {Mixed} reserved1 can be any type
 *	 Reserved parameter, can be used differently in different situations.
 *	 For example for passing event type ('on<SomeEvent>') for 'bind' command.
 * @param {Mixed} reserved2 can be any type
 *	 Reserved parameter, can be used differently in different situations.
 *	 For example for passing Q.Event or callback for 'bind' command.
 */
Q.Tool.jQuery("Q/scroller", function _Q_scroller(o) {

        _initScroller(o);

        return this.each(function(index)
        {
            var $this = $(this);

            if (!o.height || (o.height && ($this.height() == 0 || ($this.height() != 0 && $this.height() > o.height))))
            {
                var scroller = { 'container': $this, 'options': o };

                $this.scrollTop(0);
                scroller.height = o.height || parseInt($this.css('height')) || parseInt($this.css('max-height')) || $this.height();
                scroller.prevStyles = {
                    'overflow': $this.css('overflow'),
                    'maxHeight': parseInt($this.css('max-height')) ? $this.css('max-height') : ''
                };
                $this.css({ 'overflow': 'hidden', 'max-height': scroller.height });
                $this.children().wrapAll('<div class="Q_scroller_wrapper" />');
                scroller.wrapper = $($this.children('.Q_scroller_wrapper').get(0));

                scroller.info = { 'inited': false, 'borderLeft': 0, 'borderRight': 0, 'borderTop': 0, 'borderBottom': 0,
                    'contWidth': 0, 'contHeight': 0, 'contLeftOffset': 0, 'contTopOffset': 0, 'wrapperHeight': 0, 'wrapperTopOffset': 0,
                    'totalScrollHeight': 0, 'scrollAreaHeight': 0, 'scrollAreaOffset': 0, 'percentage': 0
                };

                if (o.startBottom)
                {
                    scroller.info.percentage = 1;
                    setTimeout(function()
                    {
                        $this.scrollTop(0);
                        scroller.info.wrapperTopOffset = scroller.wrapper.offset().top;
                        if (Q.Browser.detect().engine == 'webkit')
                        {
                            scroller.wrapper.children(':eq(0)').css({
                                '-webkit-transition': '-webkit-transform 0ms',
                                '-webkit-transform-origin': '0px 0px',
                                '-webkit-transform': 'translate3d(0px, -' + (scroller.wrapper.outerHeight() - scroller.height) + 'px, 0px) scale(1)'
                            });
                        }
                        else
                        {
                            scroller.wrapper.css({ 'margin-top': '-' + (scroller.wrapper.outerHeight() - scroller.height) + 'px' });
                        }
                    }, 0);
                }

                var sid = _add(scroller);
                $this.data('Q_scroller_id', sid);

                if (o.indicators)
                {
                    setTimeout(function()
                    {
                        if ($this.data('Q_scroller_id') !== undefined) // this check is needed because scroller can be destroyed until timeout triggered
                            $this.plugin('Q/scrollIndicators', { 'type': 'scroller', 'scroller': $this });
                    }, 0);
                }
            }
        });
    },

    {
        'height': null,
        'eventDelegate': null,
        'startBottom': false,
        'indicators': true,
        'onScroll': new Q.Event(function() {})
    },

    {
        bind: function (reserved1, reserved2) {
            var $this = $(this);
            var sid = $this.data('Q_scroller_id');
            var scroller = p.collection[sid];
            if (scroller && scroller.options) {
                switch (reserved1) {
                    case 'onScroll':
                        if (!scroller.options.onScroll)
                            scroller.options.onScroll = new Q.Event(reserved2);
                        else
                            scroller.options.onScroll.set(reserved2);
                        break;
                }
            }
        },

        remove: function (reserved1, reserved2) {
            var $this = $(this);
            var destroyOptions = Q.extend({
                'restoreOverflow': true,
                'restoreHeight': true,
                'unwrap': true
            }, reserved1);
            var sid = $this.data('Q_scroller_id');
            var scroller = _remove(sid);
            if (scroller) {
                if (destroyOptions.restoreOverflow) {
                    $this.css({ 'overflow': scroller.prevStyles.overflow });
                }
                if (destroyOptions.restoreHeight) {
                    $this.css({ 'max-height': scroller.prevStyles.maxHeight });
                }
                if (destroyOptions.unwrap) {
                    scroller.wrapper.children().unwrap();
                }
                $this.plugin('Q/scrollIndicators', 'remove');
                $this.removeData('Q_scroller_id');
            }
        }
    }


);

var p = {
    inited: false,
    collection: []
};

function _add(scroller)
{
    p.collection.push(scroller);
    return p.collection.length - 1;
}

function _remove(sid)
{
    var col = p.collection;
    var current = col.splice(sid, 1)[0];
    for (var i = 0; i < col.length; i++)
    {
        col[i].container.data('Q_scroller_id', i);
    }
    return current;
}

function _initScroller(o) {
    // creates a closure for _eventHandler to use
    if (!p.inited)
    {
        // WARNING: the way Dima coded this, only the first o.onScroll is ever fired
        // since after that, p.inited becomes true and this never happens:
        $(document.body).on(Q.Pointer.move, _eventHandler);
        p.inited = true;
    }

    function _eventHandler(e)
    {
        var clientX = Q.Pointer.getX(e)
        var clientY = Q.Pointer.getY(e);

        var col = p.collection, container = null, wrapper, info = null, options = null;
        for (var i = 0; i < col.length; i++)
        {
            container = col[i].container;
            wrapper = col[i].wrapper;
            info = col[i].info;
            options = col[i].options;

            if (!info.inited && container.outerWidth() != 0)
            {
                info.borderLeft = parseInt(container.css('border-left-width'));
                info.borderRight = parseInt(container.css('border-right-width'));
                info.borderTop = parseInt(container.css('border-top-width'));
                info.borderBottom = parseInt(container.css('border-bottom-width'));
                info.contWidth = container.outerWidth() - info.borderLeft - info.borderRight;
                info.contHeight = container.outerHeight() - info.borderTop - info.borderBottom;
                info.contLeftOffset = container.offset().left + info.borderLeft;
                info.contTopOffset = container.offset().top + info.borderTop;
                info.wrapperHeight = wrapper.outerHeight() + parseInt(wrapper.children(':last').css('margin-bottom'));
                if (info.wrapperTopOffset == 0)
                    info.wrapperTopOffset = wrapper.offset().top;
                if (Q.info.platform == 'android')
                {
                    info.contTopOffset -= window.scrollY;
                    info.wrapperTopOffset -= window.scrollY;
                }
                info.totalScrollHeight = info.wrapperHeight + (info.wrapperTopOffset - info.contTopOffset)
                    - (info.contHeight - parseInt(container.css('padding-bottom')));
                info.scrollAreaHeight = info.contHeight * 0.8;
                info.scrollAreaOffset = (info.contHeight - info.scrollAreaHeight) / 2;

                info.inited = true;
            }

            if (container.css('display') == 'block' &&
                clientX >= info.contLeftOffset && clientX <= info.contLeftOffset + info.contWidth &&
                clientY >= info.contTopOffset && clientY <= info.contTopOffset + info.contHeight)
            {
                var relativeY = clientY - info.contTopOffset;
                info.scrollPercentage = (relativeY - info.scrollAreaOffset) / info.scrollAreaHeight;
                if (info.scrollPercentage < 0)
                    info.scrollPercentage = 0;
                else if (info.scrollPercentage > 1)
                    info.scrollPercentage = 1;

                if (Q.Browser.detect().engine == 'webkit')
                {
                    wrapper.children(':eq(0)').css({
                        '-webkit-transition': '-webkit-transform 0ms',
                        '-webkit-transform-origin': '0px 0px',
                        '-webkit-transform': 'translate3d(0px, -' + (info.scrollPercentage * info.totalScrollHeight) + 'px, 0px) scale(1)'
                    });
                }
                else
                {
                    wrapper.css({ 'margin-top': '-' + (info.scrollPercentage * info.totalScrollHeight) + 'px' });
                }

                Q.handle(o.onScroll, wrapper, [info.scrollPercentage]);
            }
        }
    };
}

})(Q, jQuery, window, document);