Show:

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

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

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

/**
 * jQuery plugin for scrolling 'overflow: hidden' containers on touch devices.
 * But it also can be used on desktop devices where mouse events will be used instead of touch events.
 * Supposed to be replacement for iScroll and has similar look, functionality and API.
 * @class Q touchscroll
 * @constructor
 * @param {Mixed} [Object_or_String] Mixed function parameter that could be Object or String
 *   @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.x] Integer which sets initial horizontal scroll position. Optional.
 *	   @default 0
 *	   @param {Number} [Object_or_String.Object.y] Integer which sets initial vertical scroll position. Optional.
 *	   @default 0
 *	   @param {Boolean} [Object_or_String.Object.inertia] Boolean which indicates whether to apply 'intertial scrolling', meaning that after
 *	   releasing the finger while in movement the scrolling will continue with initial finger-given speed
 *	   that is slowly fading down.
 *	   @default true
 *	   @param {String} [Object_or_String.Object.scrollbar] String value which can be either 'inner' or 'outer' determining where to actually place
 *	   scrollbar element in the DOM tree. If the value is 'inner' then scrollbar is appended to the element to which
 *	   plugin is applied. If the value is 'outer' then scrollbar is appended to document.body. Optional.
 *	   @default 'inner'
 *	   @param {Boolean} [Object_or_String.Object.hideScrollbar] Boolean which indicates whether to hide scrollbar when no scrolling occurs.
 *	   @default true.
 *	   @param {Boolean} [Object_or_String.Object.fadeScrollbar] Boolean which indicates whether to fade scrollbars instead of just hiding them.
 *	   Depends on 'hideScrollbar' options and works only it it's true.
 *	   @default true
 *	   @param {Boolean} [Object_or_String.Object.indicators] Boolean which indicates whether to show scroll indicators
 *	   (apply Q/scrollIndicators plugin).
 *	   @default true
 *	   @param {Q.Event}  [Object_or_String.Object.onRefresh]  Q.Event callback which is called when touchscroll('refresh') is called.
 *	   @param {Q.Event} [Object_or_String.Object.onScrollStart] Q.Event callback which is called when scrolling is just started. Optional.
 *	   @param {Q.Event} [Object_or_String.Object.onScrollMove] Q.Event callback which is called when scrolling is in progress (while finger is moving or when inertial scrolling is applied). Optional.
 *	   @param {Q.Event} [Object_or_String.Object.onScrollEnd] Q.Event callback which is called when scrolling is ended. Optional.
 *	   @param {Q.Event} [Object_or_String.Object.onTouchEnd] Q.Event callback which is called when finger is released but scrolling still may continue
 *	   (due to inertial scrolling). Optional.
 *   @param {String} [Object_or_String.String]
 *	 If a string then it's a command and it can have following values:
 *	 "remove": Destroys the plugin so the container won't have touch scrolling functionality anymore.
 *	 "refresh": Refreshes the plugin. Useful then container size is changed to update scrollbar and other stuff.
 *	 "bind": Allows to bind Q.Event handlers to previously described events, such as 'onScrollMove'.
 *					 In this case 'reserved1' is event name and 'reserved2' is a callback function.
 * @param {Mixed} reserved1 varying
 * Some additional parameter in case if 'options' is a command.
 * @param {Mixed} reserved2 varying
 * Some additional parameter in case if 'options' is a command.
 */
Q.Tool.jQuery('Q/touchscroll',

    function _Q_touchscroll(o)
    {

        return this.each(function(index)
        {
            var $this = $(this);
            $this.data('Q_touchscroll_options', o);
            $this.data('Q_touchscroll_previous_overflow', $this.css('overflow'));
            $this.addClass('Q_unselectable').css({ 'overflow': 'hidden' });
            this.scrollTop = o.y;
            this.scrollLeft = o.x;

            var borderWidths = {
                'top': parseInt($this.css('border-top-width')),
                'right': parseInt($this.css('border-right-width')),
                'bottom': parseInt($this.css('border-bottom-width')),
                'left': parseInt($this.css('border-left-width'))
            };
            var thisPos = o.scrollbar == 'inner' ? $this.position() : $this.offset();
            var vScrollbarSpace = this.offsetHeight - borderWidths.top - borderWidths.bottom - 2;
            var hScrollbarSpace = this.offsetWidth - borderWidths.left - borderWidths.right - 2;
            var vScrollbarHeight = Math.floor(this.offsetHeight / this.scrollHeight * vScrollbarSpace);
            var hScrollbarWidth = Math.floor(this.offsetWidth / this.scrollWidth * hScrollbarSpace);
            var vScrollbarTop = thisPos.top + borderWidths.top + 1;
            var vScrollbarLeft = thisPos.left + $this.outerWidth() - borderWidths.right - 6;
            var hScrollbarTop = thisPos.top + $this.outerHeight() - borderWidths.bottom - 6;
            var hScrollbarLeft = thisPos.left + borderWidths.left + 1;
            var vScrollbar = $('<div class="Q_touch_scrollbar_v" />').css({
                'height': vScrollbarHeight + 'px',
                'top': vScrollbarTop + 'px',
                'left': vScrollbarLeft + 'px',
                'opacity': o.hideScrollbar ? '0' : '1'
            });
            var hScrollbar = $('<div class="Q_touch_scrollbar_h" />').css({
                'width': hScrollbarWidth + 'px',
                'top': hScrollbarTop + 'px',
                'left': hScrollbarLeft + 'px',
                'opacity': o.hideScrollbar ? '0' : '1'
            });
            if (this.scrollWidth > this.offsetWidth)
            {
                if (o.scrollbar == 'inner')
                    $this.append(hScrollbar);
                else if (o.scrollbar == 'outer')
                    $(document.body).append(hScrollbar);
                $this.data('Q_touchscroll_scrollbar_h', hScrollbar);
            }
            if (this.scrollHeight > this.offsetHeight)
            {
                if (o.scrollbar == 'inner')
                    $this.append(vScrollbar);
                else if (o.scrollbar == 'outer')
                    $(document.body).append(vScrollbar);
                $this.data('Q_touchscroll_scrollbar_v', vScrollbar);
            }

            var hideScrollbarsTimeout = 0;
            var hideScrollbars = function()
            {
                if (o.hideScrollbar)
                {
                    clearTimeout(hideScrollbarsTimeout);
                    hideScrollbarsTimeout = setTimeout(function()
                    {
                        if (o.fadeScrollbar)
                        {
                            hScrollbar.clearQueue().fadeTo(300, 0);
                            vScrollbar.clearQueue().fadeTo(300, 0);
                        }
                        else
                        {
                            hScrollbar.css({ 'opacity': '0' });
                            vScrollbar.css({ 'opacity': '0' });
                        }
                    }, 500);
                }
            };

            var scrollStartX = 0, scrollStartY = 0, scrollStartPageX = 0, scrollStartPageY = 0;
            var speedX = 0, speedY = 0;
            var curX = 0, curY = 0, curTime = 0, lastX = 0, lastY = 0, lastTime = 0;
            var handleMove = false;
            var scrollResetTimeout = 0;
            var inertiaIntervalKey = 'Q_touchscroll_inertia', speedMeauseIntervalKey = 'Q_touchscroll_speed_measure';
            function setScrollbarPos()
            {
                var elem = $this[0];
                vScrollbar.css({
                    'top': (vScrollbarTop + Math.round(elem.scrollTop / elem.scrollHeight * vScrollbarSpace)) + 'px'
                });
                hScrollbar.css({
                    'left': (hScrollbarLeft + Math.round(elem.scrollLeft / elem.scrollWidth * hScrollbarSpace)) + 'px'
                });
            }
            setScrollbarPos();
            $this.on([Q.Pointer.start, '.Q_touchscroll'], function(e)
            {
                e.preventDefault();

                if (Q.Interval.exists(inertiaIntervalKey))
                    Q.Interval.clear(inertiaIntervalKey);
                if (Q.Interval.exists(speedMeauseIntervalKey))
                    Q.Interval.clear(speedMeauseIntervalKey);
                clearTimeout(hideScrollbarsTimeout);
                handleMove = true;
                e = e.originalEvent.touches ? e.originalEvent.touches[0] : e;
                scrollStartPageX = e.pageX;
                scrollStartPageY = e.pageY;
                scrollStartX = this.scrollLeft + scrollStartPageX;
                scrollStartY = this.scrollTop + scrollStartPageY;
                curX = lastX = this.scrollLeft;
                curY = lastY = this.scrollTop;
                lastTime = (new Date()).getTime();
                speedX = speedY = 0;
                Q.Interval.set(function()
                {
                    curTime = (new Date()).getTime();
                    speedX = (curX - lastX) / (curTime - lastTime) * p.speedFactor;
                    speedY = (curY - lastY) / (curTime - lastTime) * p.speedFactor;
                    lastX = curX;
                    lastY = curY;
                    lastTime = curTime;
                }, 20, speedMeauseIntervalKey);

                hScrollbar.clearQueue().css({ 'opacity': '1' });
                vScrollbar.clearQueue().css({ 'opacity': '1' });

                Q.handle(o.onScrollStart, $this[0], [$this]);
            });
            $this.on(Q.Pointer.move.eventName + '.Q_touchscroll', function(e)
            {
                e.preventDefault();

                if (handleMove)
                {
                    e = e.originalEvent.touches ? e.originalEvent.touches[0] : e;
                    var thisOffset = $this.offset();
                    if (e.pageX < $this.offset().left || e.pageX > $this.offset().left + $this.outerWidth() ||
                        e.pageY < $this.offset().top || e.pageY > $this.offset().top + $this.outerHeight())
                    {
                        $this.data('Q_touchscroll_end_handler')();
                        return;
                    }
                    // strange bug noticed while testing in Chrome: sometimes arrived event contained same coordinates as on 'touchstart'
                    // but they are wrong, namely outdated; maybe it's only in Chrome, but anyway, this is to fix that bug
                    if (e.pageX == scrollStartPageX && e.pageY == scrollStartPageY)
                    {
                        return;
                    }
                    this.scrollLeft = scrollStartX - e.pageX;
                    this.scrollTop = scrollStartY - e.pageY;
                    curX = this.scrollLeft;
                    curY = this.scrollTop;

                    setScrollbarPos();

                    clearTimeout(scrollResetTimeout);
                    scrollResetTimeout = setTimeout(function()
                    {
                        $this.data('Q_touchscroll_end_handler')();
                    }, 2000);

                    hScrollbar.css({ 'opacity': '1' });
                    vScrollbar.css({ 'opacity': '1' });

                    Q.handle(o.onScrollMove, $this[0], [$this]);
                }
            });
            $this.data('Q_touchscroll_end_handler', function() {
                if (handleMove)
                {
                    Q.handle(o.onTouchEnd, $this[0], [$this]);
                    handleMove = false;
                    Q.Interval.clear(speedMeauseIntervalKey);
                    clearTimeout(scrollResetTimeout);
                    if (Math.abs(speedX) > 0.001 || Math.abs(speedY) > 0.001)
                    {
                        scrollStartX = $this[0].scrollLeft;
                        scrollStartY = $this[0].scrollTop;
                        lastTime = (new Date()).getTime();
                        var accelX = (speedX > 0 ? -1 : 1) * p.inertiaAccel;
                        var accelY = (speedY > 0 ? -1 : 1) * p.inertiaAccel;
                        var timeDiff = 0;
                        var xStopped = false, yStopped = false;
                        if (Q.Interval.exists(inertiaIntervalKey))
                            Q.Interval.clear(inertiaIntervalKey);
                        Q.Interval.set(function()
                        {
                            timeDiff = ((new Date()).getTime() - lastTime);

                            if (Math.abs(speedX) - Math.abs(accelX * timeDiff) <= 0)
                                xStopped = true;
                            else
                                $this[0].scrollLeft = scrollStartX + speedX * timeDiff + (accelX * timeDiff * timeDiff) / 2;

                            if (Math.abs(speedY) - Math.abs(accelY * timeDiff) <= 0)
                                yStopped = true;
                            else
                                $this[0].scrollTop = scrollStartY + speedY * timeDiff + (accelY * timeDiff * timeDiff) / 2;

                            setScrollbarPos();

                            hScrollbar.css({ 'opacity': '1' });
                            vScrollbar.css({ 'opacity': '1' });

                            Q.handle(o.onScrollMove, $this[0], [$this]);

                            if (xStopped && yStopped)
                            {
                                Q.Interval.clear(inertiaIntervalKey);
                                speedX = speedY = 0;
                                hideScrollbars();
                                Q.handle(o.onScrollEnd, $this[0], [$this]);
                            }
                        }, 20, inertiaIntervalKey);
                    }
                    else if (o.hideScrollbar)
                    {
                        hideScrollbars();
                        Q.handle(o.onScrollEnd, $this[0], [$this]);
                    }
                }
            });
            $(document.body).on(Q.Pointer.end, $this.data('Q_touchscroll_end_handler'));
            if (!Q.info.isTouchscreen)
            {
                $this.on('mouseleave.Q_touchscroll', function(e)
                {
                    $this.data('Q_touchscroll_end_handler')();
                });
            }

            $this.data('Q_touchscroll_cleaning_func', function()
            {
                if (Q.Interval.exists(inertiaIntervalKey))
                    Q.Interval.clear(inertiaIntervalKey);
                if (Q.Interval.exists(speedMeauseIntervalKey))
                    Q.Interval.clear(speedMeauseIntervalKey);
                clearTimeout(hideScrollbarsTimeout);
                clearTimeout(scrollResetTimeout);
            });

            if (o.indicators)
            {
                $this.plugin('Q/scrollIndicators', {
                    'type': 'touchscroll',
                    'scroller': $this,
                    'orientation': (this.scrollHeight > this.offsetHeight ? 'v' : 'h')
                });
            }
        });
    },

    {
        'x': 0,
        'y': 0,
        'inertia': true,
        'scrollbar': 'inner',
        'hideScrollbar': true,
        'fadeScrollbar': true,
        'indicators': true,
        'onRefresh': new Q.Event(function() {}),
        'onScrollStart': new Q.Event(function() {}),
        'onScrollMove': new Q.Event(function() {}),
        'onScrollEnd': new Q.Event(function() {}),
        'onTouchEnd': new Q.Event(function() {})
    },

    {
        bind: function (reserved1, reserved2) {
            return this.each(function(index)
            {
                var o = $(this).data('Q_touchscroll_options');
                if (reserved1 in o && Q.typeOf(o[reserved1]) == 'Q.Event')
                {
                    o[reserved1].set(reserved2);
                }
            });
        },

        refresh: function (reserved1, reserved2) {
            return this.each(function(index) {
                var $this = $(this);
                var o = $this.data('Q_touchscroll_options');
                if (o) {
                    $this.plugin('Q/touchscroll', 'remove')
                        .plugin('Q/touchscroll', o);
                }
            });
        },

        remove: function (reserved1, reserved2) {
            return this.each(function(index) {
                var $this = $(this);
                if ($this.data('Q_touchscroll_previous_overflow')) {
                    $this.removeClass('Q_unselectable').css({
                        'overflow': $this.data('Q_touchscroll_previous_overflow')
                    });
                }
                if ($this.data('Q_touchscroll_scrollbar_h')) {
                    $this.data('Q_touchscroll_scrollbar_h').remove();
                }
                if ($this.data('Q_touchscroll_scrollbar_v')) {
                    $this.data('Q_touchscroll_scrollbar_v').remove();
                }
                $this.off([Q.Pointer.start, '.Q_touchscroll']);
                $this.off([Q.Pointer.move, '.Q_touchscroll']);
                if ($this.data('Q_touchscroll_end_handler')) {
                    $(document.body).off(Q.Pointer.end, $this.data('Q_touchscroll_end_handler'));
                }
                if ($this.data('Q_touchscroll_cleaning_func')) {
                    $this.data('Q_touchscroll_cleaning_func')();
                }
                $this.removeData(
                    'Q_touchscroll_options Q_touchscroll_previous_overflow Q_touchscroll_scrollbar_h ' +
                        'Q_touchscroll_scrollbar_v Q_touchscroll_end_handler Q_touchscroll_cleaning_func'
                );
                $this.plugin('Q/scrollIndicators', 'remove');
            });
        }
    }

);

var p = {
    speedFactor: 0.5,
    inertiaAccel: 0.0007
}

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