You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ofbiz.apache.org by jl...@apache.org on 2014/01/20 17:27:17 UTC

svn commit: r1559769 [36/48] - in /ofbiz/trunk: applications/humanres/widget/ applications/product/webapp/catalog/WEB-INF/ applications/product/webapp/catalog/imagemanagement/ applications/product/webapp/catalog/imagemanagement/js/ applications/product...

Modified: ofbiz/trunk/framework/images/webapp/images/jquery/plugins/jcarousel/jquery.jcarousel.js
URL: http://svn.apache.org/viewvc/ofbiz/trunk/framework/images/webapp/images/jquery/plugins/jcarousel/jquery.jcarousel.js?rev=1559769&r1=1559768&r2=1559769&view=diff
==============================================================================
--- ofbiz/trunk/framework/images/webapp/images/jquery/plugins/jcarousel/jquery.jcarousel.js (original)
+++ ofbiz/trunk/framework/images/webapp/images/jquery/plugins/jcarousel/jquery.jcarousel.js Mon Jan 20 16:27:09 2014
@@ -1,917 +1,1396 @@
-/*!
- * jCarousel - Riding carousels with jQuery
- *   http://sorgalla.com/jcarousel/
- *
- * Copyright (c) 2006 Jan Sorgalla (http://sorgalla.com)
- * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
- * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
- *
- * Built on top of the jQuery library
- *   http://jquery.com
- *
- * Inspired by the "Carousel Component" by Bill Scott
- *   http://billwscott.com/carousel/
- */
-
+/*! jCarousel - v0.3.0 - 2013-11-22
+* http://sorgalla.com/jcarousel
+* Copyright (c) 2013 Jan Sorgalla; Licensed MIT */
 (function($) {
-    /**
-     * Creates a carousel for all matched elements.
-     *
-     * @example $("#mycarousel").jcarousel();
-     * @before <ul id="mycarousel" class="jcarousel-skin-name"><li>First item</li><li>Second item</li></ul>
-     * @result
-     *
-     * <div class="jcarousel-skin-name">
-     *   <div class="jcarousel-container">
-     *     <div class="jcarousel-clip">
-     *       <ul class="jcarousel-list">
-     *         <li class="jcarousel-item-1">First item</li>
-     *         <li class="jcarousel-item-2">Second item</li>
-     *       </ul>
-     *     </div>
-     *     <div disabled="disabled" class="jcarousel-prev jcarousel-prev-disabled"></div>
-     *     <div class="jcarousel-next"></div>
-     *   </div>
-     * </div>
-     *
-     * @method jcarousel
-     * @return jQuery
-     * @param o {Hash|String} A set of key/value pairs to set as configuration properties or a method name to call on a formerly created instance.
-     */
-    $.fn.jcarousel = function(o) {
-        if (typeof o == 'string') {
-            var instance = $(this).data('jcarousel'), args = Array.prototype.slice.call(arguments, 1);
-            return instance[o].apply(instance, args);
-        } else
-            return this.each(function() {
-                $(this).data('jcarousel', new $jc(this, o));
-            });
-    };
+    'use strict';
 
-    // Default configuration properties.
-    var defaults = {
-        vertical: false,
-        rtl: false,
-        start: 1,
-        offset: 1,
-        size: null,
-        scroll: 3,
-        visible: null,
-        animation: 'normal',
-        easing: 'swing',
-        auto: 0,
-        wrap: null,
-        initCallback: null,
-        reloadCallback: null,
-        itemLoadCallback: null,
-        itemFirstInCallback: null,
-        itemFirstOutCallback: null,
-        itemLastInCallback: null,
-        itemLastOutCallback: null,
-        itemVisibleInCallback: null,
-        itemVisibleOutCallback: null,
-        buttonNextHTML: '<div></div>',
-        buttonPrevHTML: '<div></div>',
-        buttonNextEvent: 'click',
-        buttonPrevEvent: 'click',
-        buttonNextCallback: null,
-        buttonPrevCallback: null,
-        itemFallbackDimension: null
-    }, windowLoaded = false;
-
-    $(window).bind('load.jcarousel', function() { windowLoaded = true; })
-
-    /**
-     * The jCarousel object.
-     *
-     * @constructor
-     * @class jcarousel
-     * @param e {HTMLElement} The element to create the carousel for.
-     * @param o {Object} A set of key/value pairs to set as configuration properties.
-     * @cat Plugins/jCarousel
-     */
-    $.jcarousel = function(e, o) {
-        this.options    = $.extend({}, defaults, o || {});
-
-        this.locked     = false;
-
-        this.container  = null;
-        this.clip       = null;
-        this.list       = null;
-        this.buttonNext = null;
-        this.buttonPrev = null;
-
-        // Only set if not explicitly passed as option
-        if (!o || o.rtl === undefined)
-            this.options.rtl = ($(e).attr('dir') || $('html').attr('dir') || '').toLowerCase() == 'rtl';
-
-        this.wh = !this.options.vertical ? 'width' : 'height';
-        this.lt = !this.options.vertical ? (this.options.rtl ? 'right' : 'left') : 'top';
-
-        // Extract skin class
-        var skin = '', split = e.className.split(' ');
-
-        for (var i = 0; i < split.length; i++) {
-            if (split[i].indexOf('jcarousel-skin') != -1) {
-                $(e).removeClass(split[i]);
-                skin = split[i];
-                break;
+    var jCarousel = $.jCarousel = {};
+
+    jCarousel.version = '0.3.0';
+
+    var rRelativeTarget = /^([+\-]=)?(.+)$/;
+
+    jCarousel.parseTarget = function(target) {
+        var relative = false,
+            parts    = typeof target !== 'object' ?
+                           rRelativeTarget.exec(target) :
+                           null;
+
+        if (parts) {
+            target = parseInt(parts[2], 10) || 0;
+
+            if (parts[1]) {
+                relative = true;
+                if (parts[1] === '-=') {
+                    target *= -1;
+                }
             }
+        } else if (typeof target !== 'object') {
+            target = parseInt(target, 10) || 0;
         }
 
-        if (e.nodeName.toUpperCase() == 'UL' || e.nodeName.toUpperCase() == 'OL') {
-            this.list = $(e);
-            this.container = this.list.parent();
-
-            if (this.container.hasClass('jcarousel-clip')) {
-                if (!this.container.parent().hasClass('jcarousel-container'))
-                    this.container = this.container.wrap('<div></div>');
-
-                this.container = this.container.parent();
-            } else if (!this.container.hasClass('jcarousel-container'))
-                this.container = this.list.wrap('<div></div>').parent();
-        } else {
-            this.container = $(e);
-            this.list = this.container.find('ul,ol').eq(0);
-        }
+        return {
+            target: target,
+            relative: relative
+        };
+    };
 
-        if (skin != '' && this.container.parent()[0].className.indexOf('jcarousel-skin') == -1)
-            this.container.wrap('<div class=" '+ skin + '"></div>');
+    jCarousel.detectCarousel = function(element) {
+        var carousel;
 
-        this.clip = this.list.parent();
+        while (element.length > 0) {
+            carousel = element.filter('[data-jcarousel]');
 
-        if (!this.clip.length || !this.clip.hasClass('jcarousel-clip'))
-            this.clip = this.list.wrap('<div></div>').parent();
+            if (carousel.length > 0) {
+                return carousel;
+            }
 
-        this.buttonNext = $('.jcarousel-next', this.container);
-
-        if (this.buttonNext.size() == 0 && this.options.buttonNextHTML != null)
-            this.buttonNext = this.clip.after(this.options.buttonNextHTML).next();
-
-        this.buttonNext.addClass(this.className('jcarousel-next'));
-
-        this.buttonPrev = $('.jcarousel-prev', this.container);
-
-        if (this.buttonPrev.size() == 0 && this.options.buttonPrevHTML != null)
-            this.buttonPrev = this.clip.after(this.options.buttonPrevHTML).next();
-
-        this.buttonPrev.addClass(this.className('jcarousel-prev'));
-
-        this.clip.addClass(this.className('jcarousel-clip')).css({
-            overflow: 'hidden',
-            position: 'relative'
-        });
-        this.list.addClass(this.className('jcarousel-list')).css({
-            overflow: 'hidden',
-            position: 'relative',
-            top: 0,
-            margin: 0,
-            padding: 0
-        }).css((this.options.rtl ? 'right' : 'left'), 0);
-        this.container.addClass(this.className('jcarousel-container')).css({
-            position: 'relative'
-        });
-        if (!this.options.vertical && this.options.rtl)
-            this.container.addClass('jcarousel-direction-rtl').attr('dir', 'rtl');
-
-        var di = this.options.visible != null ? Math.ceil(this.clipping() / this.options.visible) : null;
-        var li = this.list.children('li');
-
-        var self = this;
-
-        if (li.size() > 0) {
-            var wh = 0, i = this.options.offset;
-            li.each(function() {
-                self.format(this, i++);
-                wh += self.dimension(this, di);
-            });
+            carousel = element.find('[data-jcarousel]');
 
-            this.list.css(this.wh, (wh + 100) + 'px');
+            if (carousel.length > 0) {
+                return carousel;
+            }
 
-            // Only set if not explicitly passed as option
-            if (!o || o.size === undefined)
-                this.options.size = li.size();
+            element = element.parent();
         }
 
-        // For whatever reason, .show() does not work in Safari...
-        this.container.css('display', 'block');
-        this.buttonNext.css('display', 'block');
-        this.buttonPrev.css('display', 'block');
-
-        this.funcNext   = function() { self.next(); };
-        this.funcPrev   = function() { self.prev(); };
-        this.funcResize = function() { self.reload(); };
-
-        if (this.options.initCallback != null)
-            this.options.initCallback(this, 'init');
-
-        if (!windowLoaded && $.browser.safari) {
-            this.buttons(false, false);
-            $(window).bind('load.jcarousel', function() { self.setup(); });
-        } else
-            this.setup();
+        return null;
     };
 
-    // Create shortcut for internal use
-    var $jc = $.jcarousel;
+    jCarousel.base = function(pluginName) {
+        return {
+            version:  jCarousel.version,
+            _options:  {},
+            _element:  null,
+            _carousel: null,
+            _init:     $.noop,
+            _create:   $.noop,
+            _destroy:  $.noop,
+            _reload:   $.noop,
+            create: function() {
+                this._element
+                    .attr('data-' + pluginName.toLowerCase(), true)
+                    .data(pluginName, this);
 
-    $jc.fn = $jc.prototype = {
-        jcarousel: '0.2.5'
-    };
+                if (false === this._trigger('create')) {
+                    return this;
+                }
 
-    $jc.fn.extend = $jc.extend = $.extend;
+                this._create();
 
-    $jc.fn.extend({
-        /**
-         * Setups the carousel.
-         *
-         * @method setup
-         * @return undefined
-         */
-        setup: function() {
-            this.first     = null;
-            this.last      = null;
-            this.prevFirst = null;
-            this.prevLast  = null;
-            this.animating = false;
-            this.timer     = null;
-            this.tail      = null;
-            this.inTail    = false;
+                this._trigger('createend');
 
-            if (this.locked)
-                return;
+                return this;
+            },
+            destroy: function() {
+                if (false === this._trigger('destroy')) {
+                    return this;
+                }
 
-            this.list.css(this.lt, this.pos(this.options.offset) + 'px');
-            var p = this.pos(this.options.start);
-            this.prevFirst = this.prevLast = null;
-            this.animate(p, false);
-
-            $(window).unbind('resize.jcarousel', this.funcResize).bind('resize.jcarousel', this.funcResize);
-        },
-
-        /**
-         * Clears the list and resets the carousel.
-         *
-         * @method reset
-         * @return undefined
-         */
-        reset: function() {
-            this.list.empty();
-
-            this.list.css(this.lt, '0px');
-            this.list.css(this.wh, '10px');
-
-            if (this.options.initCallback != null)
-                this.options.initCallback(this, 'reset');
-
-            this.setup();
-        },
-
-        /**
-         * Reloads the carousel and adjusts positions.
-         *
-         * @method reload
-         * @return undefined
-         */
-        reload: function() {
-            if (this.tail != null && this.inTail)
-                this.list.css(this.lt, $jc.intval(this.list.css(this.lt)) + this.tail);
+                this._destroy();
 
-            this.tail   = null;
-            this.inTail = false;
+                this._trigger('destroyend');
+
+                this._element
+                    .removeData(pluginName)
+                    .removeAttr('data-' + pluginName.toLowerCase());
+
+                return this;
+            },
+            reload: function(options) {
+                if (false === this._trigger('reload')) {
+                    return this;
+                }
+
+                if (options) {
+                    this.options(options);
+                }
+
+                this._reload();
+
+                this._trigger('reloadend');
+
+                return this;
+            },
+            element: function() {
+                return this._element;
+            },
+            options: function(key, value) {
+                if (arguments.length === 0) {
+                    return $.extend({}, this._options);
+                }
+
+                if (typeof key === 'string') {
+                    if (typeof value === 'undefined') {
+                        return typeof this._options[key] === 'undefined' ?
+                                null :
+                                this._options[key];
+                    }
+
+                    this._options[key] = value;
+                } else {
+                    this._options = $.extend({}, this._options, key);
+                }
+
+                return this;
+            },
+            carousel: function() {
+                if (!this._carousel) {
+                    this._carousel = jCarousel.detectCarousel(this.options('carousel') || this._element);
+
+                    if (!this._carousel) {
+                        $.error('Could not detect carousel for plugin "' + pluginName + '"');
+                    }
+                }
+
+                return this._carousel;
+            },
+            _trigger: function(type, element, data) {
+                var event,
+                    defaultPrevented = false;
+
+                data = [this].concat(data || []);
 
-            if (this.options.reloadCallback != null)
-                this.options.reloadCallback(this);
+                (element || this._element).each(function() {
+                    event = $.Event((pluginName + ':' + type).toLowerCase());
 
-            if (this.options.visible != null) {
-                var self = this;
-                var di = Math.ceil(this.clipping() / this.options.visible), wh = 0, lt = 0;
-                this.list.children('li').each(function(i) {
-                    wh += self.dimension(this, di);
-                    if (i + 1 < self.first)
-                        lt = wh;
+                    $(this).trigger(event, data);
+
+                    if (event.isDefaultPrevented()) {
+                        defaultPrevented = true;
+                    }
                 });
 
-                this.list.css(this.wh, wh + 'px');
-                this.list.css(this.lt, -lt + 'px');
+                return !defaultPrevented;
             }
+        };
+    };
 
-            this.scroll(this.first, false);
-        },
+    jCarousel.plugin = function(pluginName, pluginPrototype) {
+        var Plugin = $[pluginName] = function(element, options) {
+            this._element = $(element);
+            this.options(options);
+
+            this._init();
+            this.create();
+        };
+
+        Plugin.fn = Plugin.prototype = $.extend(
+            {},
+            jCarousel.base(pluginName),
+            pluginPrototype
+        );
+
+        $.fn[pluginName] = function(options) {
+            var args        = Array.prototype.slice.call(arguments, 1),
+                returnValue = this;
+
+            if (typeof options === 'string') {
+                this.each(function() {
+                    var instance = $(this).data(pluginName);
+
+                    if (!instance) {
+                        return $.error(
+                            'Cannot call methods on ' + pluginName + ' prior to initialization; ' +
+                            'attempted to call method "' + options + '"'
+                        );
+                    }
 
-        /**
-         * Locks the carousel.
-         *
-         * @method lock
-         * @return undefined
-         */
-        lock: function() {
-            this.locked = true;
-            this.buttons();
-        },
-
-        /**
-         * Unlocks the carousel.
-         *
-         * @method unlock
-         * @return undefined
-         */
-        unlock: function() {
-            this.locked = false;
-            this.buttons();
-        },
-
-        /**
-         * Sets the size of the carousel.
-         *
-         * @method size
-         * @return undefined
-         * @param s {Number} The size of the carousel.
-         */
-        size: function(s) {
-            if (s != undefined) {
-                this.options.size = s;
-                if (!this.locked)
-                    this.buttons();
-            }
-
-            return this.options.size;
-        },
-
-        /**
-         * Checks whether a list element exists for the given index (or index range).
-         *
-         * @method get
-         * @return bool
-         * @param i {Number} The index of the (first) element.
-         * @param i2 {Number} The index of the last element.
-         */
-        has: function(i, i2) {
-            if (i2 == undefined || !i2)
-                i2 = i;
-
-            if (this.options.size !== null && i2 > this.options.size)
-                i2 = this.options.size;
-
-            for (var j = i; j <= i2; j++) {
-                var e = this.get(j);
-                if (!e.length || e.hasClass('jcarousel-item-placeholder'))
-                    return false;
+                    if (!$.isFunction(instance[options]) || options.charAt(0) === '_') {
+                        return $.error(
+                            'No such method "' + options + '" for ' + pluginName + ' instance'
+                        );
+                    }
+
+                    var methodValue = instance[options].apply(instance, args);
+
+                    if (methodValue !== instance && typeof methodValue !== 'undefined') {
+                        returnValue = methodValue;
+                        return false;
+                    }
+                });
+            } else {
+                this.each(function() {
+                    var instance = $(this).data(pluginName);
+
+                    if (instance instanceof Plugin) {
+                        instance.reload(options);
+                    } else {
+                        new Plugin(this, options);
+                    }
+                });
             }
 
-            return true;
+            return returnValue;
+        };
+
+        return Plugin;
+    };
+}(jQuery));
+
+(function($, window) {
+    'use strict';
+
+    var toFloat = function(val) {
+        return parseFloat(val) || 0;
+    };
+
+    $.jCarousel.plugin('jcarousel', {
+        animating:   false,
+        tail:        0,
+        inTail:      false,
+        resizeTimer: null,
+        lt:          null,
+        vertical:    false,
+        rtl:         false,
+        circular:    false,
+        underflow:   false,
+        relative:    false,
+
+        _options: {
+            list: function() {
+                return this.element().children().eq(0);
+            },
+            items: function() {
+                return this.list().children();
+            },
+            animation:   400,
+            transitions: false,
+            wrap:        null,
+            vertical:    null,
+            rtl:         null,
+            center:      false
+        },
+
+        // Protected, don't access directly
+        _list:         null,
+        _items:        null,
+        _target:       null,
+        _first:        null,
+        _last:         null,
+        _visible:      null,
+        _fullyvisible: null,
+        _init: function() {
+            var self = this;
+
+            this.onWindowResize = function() {
+                if (self.resizeTimer) {
+                    clearTimeout(self.resizeTimer);
+                }
+
+                self.resizeTimer = setTimeout(function() {
+                    self.reload();
+                }, 100);
+            };
+
+            return this;
         },
+        _create: function() {
+            this._reload();
 
-        /**
-         * Returns a jQuery object with list element for the given index.
-         *
-         * @method get
-         * @return jQuery
-         * @param i {Number} The index of the element.
-         */
-        get: function(i) {
-            return $('.jcarousel-item-' + i, this.list);
-        },
-
-        /**
-         * Adds an element for the given index to the list.
-         * If the element already exists, it updates the inner html.
-         * Returns the created element as jQuery object.
-         *
-         * @method add
-         * @return jQuery
-         * @param i {Number} The index of the element.
-         * @param s {String} The innerHTML of the element.
-         */
-        add: function(i, s) {
-            var e = this.get(i), old = 0, n = $(s);
-
-            if (e.length == 0) {
-                var c, e = this.create(i), j = $jc.intval(i);
-                while (c = this.get(--j)) {
-                    if (j <= 0 || c.length) {
-                        j <= 0 ? this.list.prepend(e) : c.after(e);
-                        break;
+            $(window).on('resize.jcarousel', this.onWindowResize);
+        },
+        _destroy: function() {
+            $(window).off('resize.jcarousel', this.onWindowResize);
+        },
+        _reload: function() {
+            this.vertical = this.options('vertical');
+
+            if (this.vertical == null) {
+                this.vertical = this.list().height() > this.list().width();
+            }
+
+            this.rtl = this.options('rtl');
+
+            if (this.rtl == null) {
+                this.rtl = (function(element) {
+                    if (('' + element.attr('dir')).toLowerCase() === 'rtl') {
+                        return true;
                     }
-                }
-            } else
-                old = this.dimension(e);
 
-            if (n.get(0).nodeName.toUpperCase() == 'LI') {
-                e.replaceWith(n);
-                e = n;
-            } else
-                e.empty().append(s);
+                    var found = false;
 
-            this.format(e.removeClass(this.className('jcarousel-item-placeholder')), i);
+                    element.parents('[dir]').each(function() {
+                        if ((/rtl/i).test($(this).attr('dir'))) {
+                            found = true;
+                            return false;
+                        }
+                    });
 
-            var di = this.options.visible != null ? Math.ceil(this.clipping() / this.options.visible) : null;
-            var wh = this.dimension(e, di) - old;
+                    return found;
+                }(this._element));
+            }
 
-            if (i > 0 && i < this.first)
-                this.list.css(this.lt, $jc.intval(this.list.css(this.lt)) - wh + 'px');
+            this.lt = this.vertical ? 'top' : 'left';
 
-            this.list.css(this.wh, $jc.intval(this.list.css(this.wh)) + wh + 'px');
+            // Ensure before closest() call
+            this.relative = this.list().css('position') === 'relative';
 
-            return e;
-        },
+            // Force list and items reload
+            this._list  = null;
+            this._items = null;
 
-        /**
-         * Removes an element for the given index from the list.
-         *
-         * @method remove
-         * @return undefined
-         * @param i {Number} The index of the element.
-         */
-        remove: function(i) {
-            var e = this.get(i);
+            var item = this._target && this.index(this._target) >= 0 ?
+                           this._target :
+                           this.closest();
 
-            // Check if item exists and is not currently visible
-            if (!e.length || (i >= this.first && i <= this.last))
-                return;
+            // _prepare() needs this here
+            this.circular  = this.options('wrap') === 'circular';
+            this.underflow = false;
 
-            var d = this.dimension(e);
+            var props = {'left': 0, 'top': 0};
 
-            if (i < this.first)
-                this.list.css(this.lt, $jc.intval(this.list.css(this.lt)) + d + 'px');
+            if (item.length > 0) {
+                this._prepare(item);
+                this.list().find('[data-jcarousel-clone]').remove();
 
-            e.remove();
+                // Force items reload
+                this._items = null;
 
-            this.list.css(this.wh, $jc.intval(this.list.css(this.wh)) - d + 'px');
-        },
+                this.underflow = this._fullyvisible.length >= this.items().length;
+                this.circular  = this.circular && !this.underflow;
 
-        /**
-         * Moves the carousel forwards.
-         *
-         * @method next
-         * @return undefined
-         */
-        next: function() {
-            this.stopAuto();
-
-            if (this.tail != null && !this.inTail)
-                this.scrollTail(false);
-            else
-                this.scroll(((this.options.wrap == 'both' || this.options.wrap == 'last') && this.options.size != null && this.last == this.options.size) ? 1 : this.first + this.options.scroll);
-        },
-
-        /**
-         * Moves the carousel backwards.
-         *
-         * @method prev
-         * @return undefined
-         */
-        prev: function() {
-            this.stopAuto();
-
-            if (this.tail != null && this.inTail)
-                this.scrollTail(true);
-            else
-                this.scroll(((this.options.wrap == 'both' || this.options.wrap == 'first') && this.options.size != null && this.first == 1) ? this.options.size : this.first - this.options.scroll);
-        },
-
-        /**
-         * Scrolls the tail of the carousel.
-         *
-         * @method scrollTail
-         * @return undefined
-         * @param b {Boolean} Whether scroll the tail back or forward.
-         */
-        scrollTail: function(b) {
-            if (this.locked || this.animating || !this.tail)
-                return;
+                props[this.lt] = this._position(item) + 'px';
+            }
 
-            var pos  = $jc.intval(this.list.css(this.lt));
+            this.move(props);
 
-            !b ? pos -= this.tail : pos += this.tail;
-            this.inTail = !b;
+            return this;
+        },
+        list: function() {
+            if (this._list === null) {
+                var option = this.options('list');
+                this._list = $.isFunction(option) ? option.call(this) : this._element.find(option);
+            }
 
-            // Save for callbacks
-            this.prevFirst = this.first;
-            this.prevLast  = this.last;
-
-            this.animate(pos);
-        },
-
-        /**
-         * Scrolls the carousel to a certain position.
-         *
-         * @method scroll
-         * @return undefined
-         * @param i {Number} The index of the element to scoll to.
-         * @param a {Boolean} Flag indicating whether to perform animation.
-         */
-        scroll: function(i, a) {
-            if (this.locked || this.animating)
-                return;
+            return this._list;
+        },
+        items: function() {
+            if (this._items === null) {
+                var option = this.options('items');
+                this._items = ($.isFunction(option) ? option.call(this) : this.list().find(option)).not('[data-jcarousel-clone]');
+            }
 
-            this.animate(this.pos(i), a);
+            return this._items;
         },
+        index: function(item) {
+            return this.items().index(item);
+        },
+        closest: function() {
+            var self    = this,
+                pos     = this.list().position()[this.lt],
+                closest = $(), // Ensure we're returning a jQuery instance
+                stop    = false,
+                lrb     = this.vertical ? 'bottom' : (this.rtl && !this.relative ? 'left' : 'right'),
+                width;
 
-        /**
-         * Prepares the carousel and return the position for a certian index.
-         *
-         * @method pos
-         * @return {Number}
-         * @param i {Number} The index of the element to scoll to.
-         */
-        pos: function(i) {
-            var pos  = $jc.intval(this.list.css(this.lt));
+            if (this.rtl && this.relative && !this.vertical) {
+                pos += this.list().width() - this.clipping();
+            }
 
-            if (this.locked || this.animating)
-                return pos;
+            this.items().each(function() {
+                closest = $(this);
 
-            if (this.options.wrap != 'circular')
-                i = i < 1 ? 1 : (this.options.size && i > this.options.size ? this.options.size : i);
+                if (stop) {
+                    return false;
+                }
 
-            var back = this.first > i;
+                var dim = self.dimension(closest);
 
-            // Create placeholders, new list width/height
-            // and new list position
-            var f = this.options.wrap != 'circular' && this.first <= 1 ? 1 : this.first;
-            var c = back ? this.get(f) : this.get(this.last);
-            var j = back ? f : f - 1;
-            var e = null, l = 0, p = false, d = 0, g;
+                pos += dim;
 
-            while (back ? --j >= i : ++j < i) {
-                e = this.get(j);
-                p = !e.length;
-                if (e.length == 0) {
-                    e = this.create(j).addClass(this.className('jcarousel-item-placeholder'));
-                    c[back ? 'before' : 'after' ](e);
+                if (pos >= 0) {
+                    width = dim - toFloat(closest.css('margin-' + lrb));
 
-                    if (this.first != null && this.options.wrap == 'circular' && this.options.size !== null && (j <= 0 || j > this.options.size)) {
-                        g = this.get(this.index(j));
-                        if (g.length)
-                            e = this.add(j, g.clone(true));
+                    if ((Math.abs(pos) - dim + (width / 2)) <= 0) {
+                        stop = true;
+                    } else {
+                        return false;
                     }
                 }
+            });
 
-                c = e;
-                d = this.dimension(e);
 
-                if (p)
-                    l += d;
+            return closest;
+        },
+        target: function() {
+            return this._target;
+        },
+        first: function() {
+            return this._first;
+        },
+        last: function() {
+            return this._last;
+        },
+        visible: function() {
+            return this._visible;
+        },
+        fullyvisible: function() {
+            return this._fullyvisible;
+        },
+        hasNext: function() {
+            if (false === this._trigger('hasnext')) {
+                return true;
+            }
 
-                if (this.first != null && (this.options.wrap == 'circular' || (j >= 1 && (this.options.size == null || j <= this.options.size))))
-                    pos = back ? pos + d : pos - d;
+            var wrap = this.options('wrap'),
+                end = this.items().length - 1;
+
+            return end >= 0 &&
+                ((wrap && wrap !== 'first') ||
+                    (this.index(this._last) < end) ||
+                    (this.tail && !this.inTail)) ? true : false;
+        },
+        hasPrev: function() {
+            if (false === this._trigger('hasprev')) {
+                return true;
+            }
+
+            var wrap = this.options('wrap');
+
+            return this.items().length > 0 &&
+                ((wrap && wrap !== 'last') ||
+                    (this.index(this._first) > 0) ||
+                    (this.tail && this.inTail)) ? true : false;
+        },
+        clipping: function() {
+            return this._element['inner' + (this.vertical ? 'Height' : 'Width')]();
+        },
+        dimension: function(element) {
+            return element['outer' + (this.vertical ? 'Height' : 'Width')](true);
+        },
+        scroll: function(target, animate, callback) {
+            if (this.animating) {
+                return this;
             }
 
-            // Calculate visible items
-            var clipping = this.clipping();
-            var cache = [];
-            var visible = 0, j = i, v = 0;
-            var c = this.get(i - 1);
+            if (false === this._trigger('scroll', null, [target, animate])) {
+                return this;
+            }
 
-            while (++visible) {
-                e = this.get(j);
-                p = !e.length;
-                if (e.length == 0) {
-                    e = this.create(j).addClass(this.className('jcarousel-item-placeholder'));
-                    // This should only happen on a next scroll
-                    c.length == 0 ? this.list.prepend(e) : c[back ? 'before' : 'after' ](e);
+            if ($.isFunction(animate)) {
+                callback = animate;
+                animate  = true;
+            }
 
-                    if (this.first != null && this.options.wrap == 'circular' && this.options.size !== null && (j <= 0 || j > this.options.size)) {
-                        g = this.get(this.index(j));
-                        if (g.length)
-                            e = this.add(j, g.clone(true));
+            var parsed = $.jCarousel.parseTarget(target);
+
+            if (parsed.relative) {
+                var end    = this.items().length - 1,
+                    scroll = Math.abs(parsed.target),
+                    wrap   = this.options('wrap'),
+                    current,
+                    first,
+                    index,
+                    start,
+                    curr,
+                    isVisible,
+                    props,
+                    i;
+
+                if (parsed.target > 0) {
+                    var last = this.index(this._last);
+
+                    if (last >= end && this.tail) {
+                        if (!this.inTail) {
+                            this._scrollTail(animate, callback);
+                        } else {
+                            if (wrap === 'both' || wrap === 'last') {
+                                this._scroll(0, animate, callback);
+                            } else {
+                                if ($.isFunction(callback)) {
+                                    callback.call(this, false);
+                                }
+                            }
+                        }
+                    } else {
+                        current = this.index(this._target);
+
+                        if ((this.underflow && current === end && (wrap === 'circular' || wrap === 'both' || wrap === 'last')) ||
+                            (!this.underflow && last === end && (wrap === 'both' || wrap === 'last'))) {
+                            this._scroll(0, animate, callback);
+                        } else {
+                            index = current + scroll;
+
+                            if (this.circular && index > end) {
+                                i = end;
+                                curr = this.items().get(-1);
+
+                                while (i++ < index) {
+                                    curr = this.items().eq(0);
+                                    isVisible = this._visible.index(curr) >= 0;
+
+                                    if (isVisible) {
+                                        curr.after(curr.clone(true).attr('data-jcarousel-clone', true));
+                                    }
+
+                                    this.list().append(curr);
+
+                                    if (!isVisible) {
+                                        props = {};
+                                        props[this.lt] = this.dimension(curr);
+                                        this.moveBy(props);
+                                    }
+
+                                    // Force items reload
+                                    this._items = null;
+                                }
+
+                                this._scroll(curr, animate, callback);
+                            } else {
+                                this._scroll(Math.min(index, end), animate, callback);
+                            }
+                        }
+                    }
+                } else {
+                    if (this.inTail) {
+                        this._scroll(Math.max((this.index(this._first) - scroll) + 1, 0), animate, callback);
+                    } else {
+                        first  = this.index(this._first);
+                        current = this.index(this._target);
+                        start  = this.underflow ? current : first;
+                        index  = start - scroll;
+
+                        if (start <= 0 && ((this.underflow && wrap === 'circular') || wrap === 'both' || wrap === 'first')) {
+                            this._scroll(end, animate, callback);
+                        } else {
+                            if (this.circular && index < 0) {
+                                i    = index;
+                                curr = this.items().get(0);
+
+                                while (i++ < 0) {
+                                    curr = this.items().eq(-1);
+                                    isVisible = this._visible.index(curr) >= 0;
+
+                                    if (isVisible) {
+                                        curr.after(curr.clone(true).attr('data-jcarousel-clone', true));
+                                    }
+
+                                    this.list().prepend(curr);
+
+                                    // Force items reload
+                                    this._items = null;
+
+                                    var dim = this.dimension(curr);
+
+                                    props = {};
+                                    props[this.lt] = -dim;
+                                    this.moveBy(props);
+
+                                }
+
+                                this._scroll(curr, animate, callback);
+                            } else {
+                                this._scroll(Math.max(index, 0), animate, callback);
+                            }
+                        }
                     }
                 }
+            } else {
+                this._scroll(parsed.target, animate, callback);
+            }
+
+            this._trigger('scrollend');
 
-                c = e;
-                var d = this.dimension(e);
-                if (d == 0) {
-                    throw new Error('jCarousel: No width/height set for items. This will cause an infinite loop. Aborting...');
+            return this;
+        },
+        moveBy: function(properties, opts) {
+            var position = this.list().position(),
+                multiplier = 1,
+                correction = 0;
+
+            if (this.rtl && !this.vertical) {
+                multiplier = -1;
+
+                if (this.relative) {
+                    correction = this.list().width() - this.clipping();
                 }
+            }
 
-                if (this.options.wrap != 'circular' && this.options.size !== null && j > this.options.size)
-                    cache.push(e);
-                else if (p)
-                    l += d;
+            if (properties.left) {
+                properties.left = (position.left + correction + toFloat(properties.left) * multiplier) + 'px';
+            }
 
-                v += d;
+            if (properties.top) {
+                properties.top = (position.top + correction + toFloat(properties.top) * multiplier) + 'px';
+            }
 
-                if (v >= clipping)
-                    break;
+            return this.move(properties, opts);
+        },
+        move: function(properties, opts) {
+            opts = opts || {};
+
+            var option       = this.options('transitions'),
+                transitions  = !!option,
+                transforms   = !!option.transforms,
+                transforms3d = !!option.transforms3d,
+                duration     = opts.duration || 0,
+                list         = this.list();
 
-                j++;
+            if (!transitions && duration > 0) {
+                list.animate(properties, opts);
+                return;
             }
 
-             // Remove out-of-range placeholders
-            for (var x = 0; x < cache.length; x++)
-                cache[x].remove();
+            var complete = opts.complete || $.noop,
+                css = {};
+
+            if (transitions) {
+                var backup = list.css(['transitionDuration', 'transitionTimingFunction', 'transitionProperty']),
+                    oldComplete = complete;
+
+                complete = function() {
+                    $(this).css(backup);
+                    oldComplete.call(this);
+                };
+                css = {
+                    transitionDuration: (duration > 0 ? duration / 1000 : 0) + 's',
+                    transitionTimingFunction: option.easing || opts.easing,
+                    transitionProperty: duration > 0 ? (function() {
+                        if (transforms || transforms3d) {
+                            // We have to use 'all' because jQuery doesn't prefix
+                            // css values, like transition-property: transform;
+                            return 'all';
+                        }
+
+                        return properties.left ? 'left' : 'top';
+                    })() : 'none',
+                    transform: 'none'
+                };
+            }
 
-            // Resize list
-            if (l > 0) {
-                this.list.css(this.wh, this.dimension(this.list) + l + 'px');
+            if (transforms3d) {
+                css.transform = 'translate3d(' + (properties.left || 0) + ',' + (properties.top || 0) + ',0)';
+            } else if (transforms) {
+                css.transform = 'translate(' + (properties.left || 0) + ',' + (properties.top || 0) + ')';
+            } else {
+                $.extend(css, properties);
+            }
 
-                if (back) {
-                    pos -= l;
-                    this.list.css(this.lt, $jc.intval(this.list.css(this.lt)) - l + 'px');
+            if (transitions && duration > 0) {
+                list.one('transitionend webkitTransitionEnd oTransitionEnd otransitionend MSTransitionEnd', complete);
+            }
+
+            list.css(css);
+
+            if (duration <= 0) {
+                list.each(function() {
+                    complete.call(this);
+                });
+            }
+        },
+        _scroll: function(item, animate, callback) {
+            if (this.animating) {
+                if ($.isFunction(callback)) {
+                    callback.call(this, false);
                 }
+
+                return this;
             }
 
-            // Calculate first and last item
-            var last = i + visible - 1;
-            if (this.options.wrap != 'circular' && this.options.size && last > this.options.size)
-                last = this.options.size;
+            if (typeof item !== 'object') {
+                item = this.items().eq(item);
+            } else if (typeof item.jquery === 'undefined') {
+                item = $(item);
+            }
 
-            if (j > last) {
-                visible = 0, j = last, v = 0;
-                while (++visible) {
-                    var e = this.get(j--);
-                    if (!e.length)
-                        break;
-                    v += this.dimension(e);
-                    if (v >= clipping)
-                        break;
+            if (item.length === 0) {
+                if ($.isFunction(callback)) {
+                    callback.call(this, false);
+                }
+
+                return this;
+            }
+
+            this.inTail = false;
+
+            this._prepare(item);
+
+            var pos     = this._position(item),
+                currPos = this.list().position()[this.lt];
+
+            if (pos === currPos) {
+                if ($.isFunction(callback)) {
+                    callback.call(this, false);
                 }
+
+                return this;
             }
 
-            var first = last - visible + 1;
-            if (this.options.wrap != 'circular' && first < 1)
-                first = 1;
+            var properties = {};
+            properties[this.lt] = pos + 'px';
+
+            this._animate(properties, animate, callback);
+
+            return this;
+        },
+        _scrollTail: function(animate, callback) {
+            if (this.animating || !this.tail) {
+                if ($.isFunction(callback)) {
+                    callback.call(this, false);
+                }
+
+                return this;
+            }
+
+            var pos = this.list().position()[this.lt];
+
+            if (this.rtl && this.relative && !this.vertical) {
+                pos += this.list().width() - this.clipping();
+            }
 
-            if (this.inTail && back) {
+            if (this.rtl && !this.vertical) {
                 pos += this.tail;
-                this.inTail = false;
+            } else {
+                pos -= this.tail;
             }
 
-            this.tail = null;
-            if (this.options.wrap != 'circular' && last == this.options.size && (last - visible + 1) >= 1) {
-                var m = $jc.margin(this.get(last), !this.options.vertical ? 'marginRight' : 'marginBottom');
-                if ((v - m) > clipping)
-                    this.tail = v - clipping - m;
-            }
-
-            // Adjust position
-            while (i-- > first)
-                pos += this.dimension(this.get(i));
-
-            // Save visible item range
-            this.prevFirst = this.first;
-            this.prevLast  = this.last;
-            this.first     = first;
-            this.last      = last;
-
-            return pos;
-        },
-
-        /**
-         * Animates the carousel to a certain position.
-         *
-         * @method animate
-         * @return undefined
-         * @param p {Number} Position to scroll to.
-         * @param a {Boolean} Flag indicating whether to perform animation.
-         */
-        animate: function(p, a) {
-            if (this.locked || this.animating)
-                return;
+            this.inTail = true;
+
+            var properties = {};
+            properties[this.lt] = pos + 'px';
+
+            this._update({
+                target:       this._target.next(),
+                fullyvisible: this._fullyvisible.slice(1).add(this._visible.last())
+            });
+
+            this._animate(properties, animate, callback);
+
+            return this;
+        },
+        _animate: function(properties, animate, callback) {
+            callback = callback || $.noop;
+
+            if (false === this._trigger('animate')) {
+                callback.call(this, false);
+                return this;
+            }
 
             this.animating = true;
 
-            var self = this;
-            var scrolled = function() {
-                self.animating = false;
+            var animation = this.options('animation'),
+                complete  = $.proxy(function() {
+                    this.animating = false;
+
+                    var c = this.list().find('[data-jcarousel-clone]');
+
+                    if (c.length > 0) {
+                        c.remove();
+                        this._reload();
+                    }
 
-                if (p == 0)
-                    self.list.css(self.lt,  0);
+                    this._trigger('animateend');
 
-                if (self.options.wrap == 'circular' || self.options.wrap == 'both' || self.options.wrap == 'last' || self.options.size == null || self.last < self.options.size)
-                    self.startAuto();
+                    callback.call(this, true);
+                }, this);
+
+            var opts = typeof animation === 'object' ?
+                           $.extend({}, animation) :
+                           {duration: animation},
+                oldComplete = opts.complete || $.noop;
+
+            if (animate === false) {
+                opts.duration = 0;
+            } else if (typeof $.fx.speeds[opts.duration] !== 'undefined') {
+                opts.duration = $.fx.speeds[opts.duration];
+            }
 
-                self.buttons();
-                self.notify('onAfterAnimation');
-
-                // This function removes items which are appended automatically for circulation.
-                // This prevents the list from growing infinitely.
-                if (self.options.wrap == 'circular' && self.options.size !== null)
-                    for (var i = self.prevFirst; i <= self.prevLast; i++)
-                        if (i !== null && !(i >= self.first && i <= self.last) && (i < 1 || i > self.options.size))
-                            self.remove(i);
+            opts.complete = function() {
+                complete();
+                oldComplete.call(this);
             };
 
-            this.notify('onBeforeAnimation');
+            this.move(properties, opts);
 
-            // Animate
-            if (!this.options.animation || a == false) {
-                this.list.css(this.lt, p + 'px');
-                scrolled();
-            } else {
-                var o = !this.options.vertical ? (this.options.rtl ? {'right': p} : {'left': p}) : {'top': p};
-                this.list.animate(o, this.options.animation, this.options.easing, scrolled);
-            }
+            return this;
         },
+        _prepare: function(item) {
+            var index  = this.index(item),
+                idx    = index,
+                wh     = this.dimension(item),
+                clip   = this.clipping(),
+                lrb    = this.vertical ? 'bottom' : (this.rtl ? 'left'  : 'right'),
+                center = this.options('center'),
+                update = {
+                    target:       item,
+                    first:        item,
+                    last:         item,
+                    visible:      item,
+                    fullyvisible: wh <= clip ? item : $()
+                },
+                curr,
+                isVisible,
+                margin,
+                dim;
+
+            if (center) {
+                wh /= 2;
+                clip /= 2;
+            }
 
-        /**
-         * Starts autoscrolling.
-         *
-         * @method auto
-         * @return undefined
-         * @param s {Number} Seconds to periodically autoscroll the content.
-         */
-        startAuto: function(s) {
-            if (s != undefined)
-                this.options.auto = s;
+            if (wh < clip) {
+                while (true) {
+                    curr = this.items().eq(++idx);
 
-            if (this.options.auto == 0)
-                return this.stopAuto();
+                    if (curr.length === 0) {
+                        if (!this.circular) {
+                            break;
+                        }
 
-            if (this.timer != null)
-                return;
+                        curr = this.items().eq(0);
 
-            var self = this;
-            this.timer = setTimeout(function() { self.next(); }, this.options.auto * 1000);
-        },
+                        if (item.get(0) === curr.get(0)) {
+                            break;
+                        }
 
-        /**
-         * Stops autoscrolling.
-         *
-         * @method stopAuto
-         * @return undefined
-         */
-        stopAuto: function() {
-            if (this.timer == null)
-                return;
+                        isVisible = this._visible.index(curr) >= 0;
 
-            clearTimeout(this.timer);
-            this.timer = null;
-        },
+                        if (isVisible) {
+                            curr.after(curr.clone(true).attr('data-jcarousel-clone', true));
+                        }
 
-        /**
-         * Sets the states of the prev/next buttons.
-         *
-         * @method buttons
-         * @return undefined
-         */
-        buttons: function(n, p) {
-            if (n == undefined || n == null) {
-                var n = !this.locked && this.options.size !== 0 && ((this.options.wrap && this.options.wrap != 'first') || this.options.size == null || this.last < this.options.size);
-                if (!this.locked && (!this.options.wrap || this.options.wrap == 'first') && this.options.size != null && this.last >= this.options.size)
-                    n = this.tail != null && !this.inTail;
-            }
-
-            if (p == undefined || p == null) {
-                var p = !this.locked && this.options.size !== 0 && ((this.options.wrap && this.options.wrap != 'last') || this.first > 1);
-                if (!this.locked && (!this.options.wrap || this.options.wrap == 'last') && this.options.size != null && this.first == 1)
-                    p = this.tail != null && this.inTail;
-            }
+                        this.list().append(curr);
 
-            var self = this;
+                        if (!isVisible) {
+                            var props = {};
+                            props[this.lt] = this.dimension(curr);
+                            this.moveBy(props);
+                        }
 
-            this.buttonNext[n ? 'bind' : 'unbind'](this.options.buttonNextEvent + '.jcarousel', this.funcNext)[n ? 'removeClass' : 'addClass'](this.className('jcarousel-next-disabled')).attr('disabled', n ? false : true);
-            this.buttonPrev[p ? 'bind' : 'unbind'](this.options.buttonPrevEvent + '.jcarousel', this.funcPrev)[p ? 'removeClass' : 'addClass'](this.className('jcarousel-prev-disabled')).attr('disabled', p ? false : true);
+                        // Force items reload
+                        this._items = null;
+                    }
+
+                    dim = this.dimension(curr);
+
+                    if (dim === 0) {
+                        break;
+                    }
 
-            if (this.options.buttonNextCallback != null && this.buttonNext.data('jcarouselstate') != n) {
-                this.buttonNext.each(function() { self.options.buttonNextCallback(self, this, n); }).data('jcarouselstate', n);
+                    wh += dim;
+
+                    update.last    = curr;
+                    update.visible = update.visible.add(curr);
+
+                    // Remove right/bottom margin from total width
+                    margin = toFloat(curr.css('margin-' + lrb));
+
+                    if ((wh - margin) <= clip) {
+                        update.fullyvisible = update.fullyvisible.add(curr);
+                    }
+
+                    if (wh >= clip) {
+                        break;
+                    }
+                }
             }
 
-            if (this.options.buttonPrevCallback != null && (this.buttonPrev.data('jcarouselstate') != p)) {
-                this.buttonPrev.each(function() { self.options.buttonPrevCallback(self, this, p); }).data('jcarouselstate', p);
+            if (!this.circular && !center && wh < clip) {
+                idx = index;
+
+                while (true) {
+                    if (--idx < 0) {
+                        break;
+                    }
+
+                    curr = this.items().eq(idx);
+
+                    if (curr.length === 0) {
+                        break;
+                    }
+
+                    dim = this.dimension(curr);
+
+                    if (dim === 0) {
+                        break;
+                    }
+
+                    wh += dim;
+
+                    update.first   = curr;
+                    update.visible = update.visible.add(curr);
+
+                    // Remove right/bottom margin from total width
+                    margin = toFloat(curr.css('margin-' + lrb));
+
+                    if ((wh - margin) <= clip) {
+                        update.fullyvisible = update.fullyvisible.add(curr);
+                    }
+
+                    if (wh >= clip) {
+                        break;
+                    }
+                }
             }
-        },
 
-        /**
-         * Notify callback of a specified event.
-         *
-         * @method notify
-         * @return undefined
-         * @param evt {String} The event name
-         */
-        notify: function(evt) {
-            var state = this.prevFirst == null ? 'init' : (this.prevFirst < this.first ? 'next' : 'prev');
+            this._update(update);
+
+            this.tail = 0;
+
+            if (!center &&
+                this.options('wrap') !== 'circular' &&
+                this.options('wrap') !== 'custom' &&
+                this.index(update.last) === (this.items().length - 1)) {
 
-            // Load items
-            this.callback('itemLoadCallback', evt, state);
+                // Remove right/bottom margin from total width
+                wh -= toFloat(update.last.css('margin-' + lrb));
 
-            if (this.prevFirst !== this.first) {
-                this.callback('itemFirstInCallback', evt, state, this.first);
-                this.callback('itemFirstOutCallback', evt, state, this.prevFirst);
+                if (wh > clip) {
+                    this.tail = wh - clip;
+                }
             }
 
-            if (this.prevLast !== this.last) {
-                this.callback('itemLastInCallback', evt, state, this.last);
-                this.callback('itemLastOutCallback', evt, state, this.prevLast);
+            return this;
+        },
+        _position: function(item) {
+            var first  = this._first,
+                pos    = first.position()[this.lt],
+                center = this.options('center'),
+                centerOffset = center ? (this.clipping() / 2) - (this.dimension(first) / 2) : 0;
+
+            if (this.rtl && !this.vertical) {
+                if (this.relative) {
+                    pos -= this.list().width() - this.dimension(first);
+                } else {
+                    pos -= this.clipping() - this.dimension(first);
+                }
+
+                pos += centerOffset;
+            } else {
+                pos -= centerOffset;
             }
 
-            this.callback('itemVisibleInCallback', evt, state, this.first, this.last, this.prevFirst, this.prevLast);
-            this.callback('itemVisibleOutCallback', evt, state, this.prevFirst, this.prevLast, this.first, this.last);
+            if (!center &&
+                (this.index(item) > this.index(first) || this.inTail) &&
+                this.tail) {
+                pos = this.rtl && !this.vertical ? pos - this.tail : pos + this.tail;
+                this.inTail = true;
+            } else {
+                this.inTail = false;
+            }
+
+            return -pos;
         },
+        _update: function(update) {
+            var self = this,
+                current = {
+                    target:       this._target || $(),
+                    first:        this._first || $(),
+                    last:         this._last || $(),
+                    visible:      this._visible || $(),
+                    fullyvisible: this._fullyvisible || $()
+                },
+                back = this.index(update.first || current.first) < this.index(current.first),
+                key,
+                doUpdate = function(key) {
+                    var elIn  = [],
+                        elOut = [];
+
+                    update[key].each(function() {
+                        if (current[key].index(this) < 0) {
+                            elIn.push(this);
+                        }
+                    });
+
+                    current[key].each(function() {
+                        if (update[key].index(this) < 0) {
+                            elOut.push(this);
+                        }
+                    });
+
+                    if (back) {
+                        elIn = elIn.reverse();
+                    } else {
+                        elOut = elOut.reverse();
+                    }
 
-        callback: function(cb, evt, state, i1, i2, i3, i4) {
-            if (this.options[cb] == undefined || (typeof this.options[cb] != 'object' && evt != 'onAfterAnimation'))
-                return;
+                    self._trigger(key + 'in', $(elIn));
+                    self._trigger(key + 'out', $(elOut));
 
-            var callback = typeof this.options[cb] == 'object' ? this.options[cb][evt] : this.options[cb];
+                    self['_' + key] = update[key];
+                };
 
-            if (!$.isFunction(callback))
-                return;
+            for (key in update) {
+                doUpdate(key);
+            }
 
-            var self = this;
+            return this;
+        }
+    });
+}(jQuery, window));
 
-            if (i1 === undefined)
-                callback(self, state, evt);
-            else if (i2 === undefined)
-                this.get(i1).each(function() { callback(self, this, i1, state, evt); });
-            else {
-                for (var i = i1; i <= i2; i++)
-                    if (i !== null && !(i >= i3 && i <= i4))
-                        this.get(i).each(function() { callback(self, this, i, state, evt); });
+(function($) {
+    'use strict';
+
+    $.jcarousel.fn.scrollIntoView = function(target, animate, callback) {
+        var parsed = $.jCarousel.parseTarget(target),
+            first  = this.index(this._fullyvisible.first()),
+            last   = this.index(this._fullyvisible.last()),
+            index;
+
+        if (parsed.relative) {
+            index = parsed.target < 0 ? Math.max(0, first + parsed.target) : last + parsed.target;
+        } else {
+            index = typeof parsed.target !== 'object' ? parsed.target : this.index(parsed.target);
+        }
+
+        if (index < first) {
+            return this.scroll(index, animate, callback);
+        }
+
+        if (index >= first && index <= last) {
+            if ($.isFunction(callback)) {
+                callback.call(this, false);
             }
-        },
 
-        create: function(i) {
-            return this.format('<li></li>', i);
-        },
+            return this;
+        }
 
-        format: function(e, i) {
-            var e = $(e), split = e.get(0).className.split(' ');
-            for (var j = 0; j < split.length; j++) {
-                if (split[j].indexOf('jcarousel-') != -1) {
-                    e.removeClass(split[j]);
-                }
+        var items = this.items(),
+            clip = this.clipping(),
+            lrb  = this.vertical ? 'bottom' : (this.rtl ? 'left'  : 'right'),
+            wh   = 0,
+            curr;
+
+        while (true) {
+            curr = items.eq(index);
+
+            if (curr.length === 0) {
+                break;
             }
-            e.addClass(this.className('jcarousel-item')).addClass(this.className('jcarousel-item-' + i)).css({
-                'float': (this.options.rtl ? 'right' : 'left'),
-                'list-style': 'none'
-            }).attr('jcarouselindex', i);
-            return e;
-        },
 
-        className: function(c) {
-            return c + ' ' + c + (!this.options.vertical ? '-horizontal' : '-vertical');
-        },
+            wh += this.dimension(curr);
 
-        dimension: function(e, d) {
-            var el = e.jquery != undefined ? e[0] : e;
+            if (wh >= clip) {
+                var margin = parseFloat(curr.css('margin-' + lrb)) || 0;
+                if ((wh - margin) !== clip) {
+                    index++;
+                }
+                break;
+            }
 
-            var old = !this.options.vertical ?
-                (el.offsetWidth || $jc.intval(this.options.itemFallbackDimension)) + $jc.margin(el, 'marginLeft') + $jc.margin(el, 'marginRight') :
-                (el.offsetHeight || $jc.intval(this.options.itemFallbackDimension)) + $jc.margin(el, 'marginTop') + $jc.margin(el, 'marginBottom');
+            if (index <= 0) {
+                break;
+            }
 
-            if (d == undefined || old == d)
-                return old;
+            index--;
+        }
 
-            var w = !this.options.vertical ?
-                d - $jc.margin(el, 'marginLeft') - $jc.margin(el, 'marginRight') :
-                d - $jc.margin(el, 'marginTop') - $jc.margin(el, 'marginBottom');
+        return this.scroll(index, animate, callback);
+    };
+}(jQuery));
 
-            $(el).css(this.wh, w + 'px');
+(function($) {
+    'use strict';
 
-            return this.dimension(el);
+    $.jCarousel.plugin('jcarouselControl', {
+        _options: {
+            target: '+=1',
+            event:  'click',
+            method: 'scroll'
+        },
+        _active: null,
+        _init: function() {
+            this.onDestroy = $.proxy(function() {
+                this._destroy();
+                this.carousel()
+                    .one('jcarousel:createend', $.proxy(this._create, this));
+            }, this);
+            this.onReload = $.proxy(this._reload, this);
+            this.onEvent = $.proxy(function(e) {
+                e.preventDefault();
+
+                var method = this.options('method');
+
+                if ($.isFunction(method)) {
+                    method.call(this);
+                } else {
+                    this.carousel()
+                        .jcarousel(this.options('method'), this.options('target'));
+                }
+            }, this);
         },
+        _create: function() {
+            this.carousel()
+                .one('jcarousel:destroy', this.onDestroy)
+                .on('jcarousel:reloadend jcarousel:scrollend', this.onReload);
+
+            this._element
+                .on(this.options('event') + '.jcarouselcontrol', this.onEvent);
+
+            this._reload();
+        },
+        _destroy: function() {
+            this._element
+                .off('.jcarouselcontrol', this.onEvent);
+
+            this.carousel()
+                .off('jcarousel:destroy', this.onDestroy)
+                .off('jcarousel:reloadend jcarousel:scrollend', this.onReload);
+        },
+        _reload: function() {
+            var parsed   = $.jCarousel.parseTarget(this.options('target')),
+                carousel = this.carousel(),
+                active;
+
+            if (parsed.relative) {
+                active = carousel
+                    .jcarousel(parsed.target > 0 ? 'hasNext' : 'hasPrev');
+            } else {
+                var target = typeof parsed.target !== 'object' ?
+                                carousel.jcarousel('items').eq(parsed.target) :
+                                parsed.target;
 
-        clipping: function() {
-            return !this.options.vertical ?
-                this.clip[0].offsetWidth - $jc.intval(this.clip.css('borderLeftWidth')) - $jc.intval(this.clip.css('borderRightWidth')) :
-                this.clip[0].offsetHeight - $jc.intval(this.clip.css('borderTopWidth')) - $jc.intval(this.clip.css('borderBottomWidth'));
-        },
+                active = carousel.jcarousel('target').index(target) >= 0;
+            }
 
-        index: function(i, s) {
-            if (s == undefined)
-                s = this.options.size;
+            if (this._active !== active) {
+                this._trigger(active ? 'active' : 'inactive');
+                this._active = active;
+            }
 
-            return Math.round((((i-1) / s) - Math.floor((i-1) / s)) * s) + 1;
+            return this;
         }
     });
+}(jQuery));
 
-    $jc.extend({
-        /**
-         * Gets/Sets the global default configuration properties.
-         *
-         * @method defaults
-         * @return {Object}
-         * @param d {Object} A set of key/value pairs to set as configuration properties.
-         */
-        defaults: function(d) {
-            return $.extend(defaults, d || {});
-        },
+(function($) {
+    'use strict';
 
-        margin: function(e, p) {
-            if (!e)
-                return 0;
+    $.jCarousel.plugin('jcarouselPagination', {
+        _options: {
+            perPage: null,
+            item: function(page) {
+                return '<a href="#' + page + '">' + page + '</a>';
+            },
+            event:  'click',
+            method: 'scroll'
+        },
+        _pages: {},
+        _items: {},
+        _currentPage: null,
+        _init: function() {
+            this.onDestroy = $.proxy(function() {
+                this._destroy();
+                this.carousel()
+                    .one('jcarousel:createend', $.proxy(this._create, this));
+            }, this);
+            this.onReload = $.proxy(this._reload, this);
+            this.onScroll = $.proxy(this._update, this);
+        },
+        _create: function() {
+            this.carousel()
+                .one('jcarousel:destroy', this.onDestroy)
+                .on('jcarousel:reloadend', this.onReload)
+                .on('jcarousel:scrollend', this.onScroll);
+
+            this._reload();
+        },
+        _destroy: function() {
+            this._clear();
+
+            this.carousel()
+                .off('jcarousel:destroy', this.onDestroy)
+                .off('jcarousel:reloadend', this.onReload)
+                .off('jcarousel:scrollend', this.onScroll);
+        },
+        _reload: function() {
+            var perPage = this.options('perPage');
+
+            this._pages = {};
+            this._items = {};
+
+            // Calculate pages
+            if ($.isFunction(perPage)) {
+                perPage = perPage.call(this);
+            }
 
-            var el = e.jquery != undefined ? e[0] : e;
+            if (perPage == null) {
+                this._pages = this._calculatePages();
+            } else {
+                var pp    = parseInt(perPage, 10) || 0,
+                    items = this.carousel().jcarousel('items'),
+                    page  = 1,
+                    i     = 0,
+                    curr;
 
-            if (p == 'marginRight' && $.browser.safari) {
-                var old = {'display': 'block', 'float': 'none', 'width': 'auto'}, oWidth, oWidth2;
+                while (true) {
+                    curr = items.eq(i++);
 
-                $.swap(el, old, function() { oWidth = el.offsetWidth; });
+                    if (curr.length === 0) {
+                        break;
+                    }
 
-                old['marginRight'] = 0;
-                $.swap(el, old, function() { oWidth2 = el.offsetWidth; });
+                    if (!this._pages[page]) {
+                        this._pages[page] = curr;
+                    } else {
+                        this._pages[page] = this._pages[page].add(curr);
+                    }
 
-                return oWidth2 - oWidth;
+                    if (i % pp === 0) {
+                        page++;
+                    }
+                }
             }
 
-            return $jc.intval($.css(el, p));
+            this._clear();
+
+            var self     = this,
+                carousel = this.carousel().data('jcarousel'),
+                element  = this._element,
+                item     = this.options('item');
+
+            $.each(this._pages, function(page, carouselItems) {
+                var currItem = self._items[page] = $(item.call(self, page, carouselItems));
+
+                currItem.on(self.options('event') + '.jcarouselpagination', $.proxy(function() {
+                    var target = carouselItems.eq(0);
+
+                    // If circular wrapping enabled, ensure correct scrolling direction
+                    if (carousel.circular) {
+                        var currentIndex = carousel.index(carousel.target()),
+                            newIndex     = carousel.index(target);
+
+                        if (parseFloat(page) > parseFloat(self._currentPage)) {
+                            if (newIndex < currentIndex) {
+                                target = '+=' + (carousel.items().length - currentIndex + newIndex);
+                            }
+                        } else {
+                            if (newIndex > currentIndex) {
+                                target = '-=' + (currentIndex + (carousel.items().length - newIndex));
+                            }
+                        }
+                    }
+
+                    carousel[this.options('method')](target);
+                }, self));
+
+                element.append(currItem);
+            });
+
+            this._update();
         },
+        _update: function() {
+            var target = this.carousel().jcarousel('target'),
+                currentPage;
+
+            $.each(this._pages, function(page, carouselItems) {
+                carouselItems.each(function() {
+                    if (target.is(this)) {
+                        currentPage = page;
+                        return false;
+                    }
+                });
+
+                if (currentPage) {
+                    return false;
+                }
+            });
+
+            if (this._currentPage !== currentPage) {
+                this._trigger('inactive', this._items[this._currentPage]);
+                this._trigger('active', this._items[currentPage]);
+            }
+
+            this._currentPage = currentPage;
+        },
+        items: function() {
+            return this._items;
+        },
+        _clear: function() {
+            this._element.empty();
+            this._currentPage = null;
+        },
+        _calculatePages: function() {
+            var carousel = this.carousel().data('jcarousel'),
+                items    = carousel.items(),
+                clip     = carousel.clipping(),
+                wh       = 0,
+                idx      = 0,
+                page     = 1,
+                pages    = {},
+                curr;
+
+            while (true) {
+                curr = items.eq(idx++);
 
-        intval: function(v) {
-            v = parseInt(v);
-            return isNaN(v) ? 0 : v;
+                if (curr.length === 0) {
+                    break;
+                }
+
+                if (!pages[page]) {
+                    pages[page] = curr;
+                } else {
+                    pages[page] = pages[page].add(curr);
+                }
+
+                wh += carousel.dimension(curr);
+
+                if (wh >= clip) {
+                    page++;
+                    wh = 0;
+                }
+            }
+
+            return pages;
         }
     });
+}(jQuery));
 
-})(jQuery);
+(function($) {
+    'use strict';
+
+    $.jCarousel.plugin('jcarouselAutoscroll', {
+        _options: {
+            target:    '+=1',
+            interval:  3000,
+            autostart: true
+        },
+        _timer: null,
+        _init: function () {
+            this.onDestroy = $.proxy(function() {
+                this._destroy();
+                this.carousel()
+                    .one('jcarousel:createend', $.proxy(this._create, this));
+            }, this);
+
+            this.onAnimateEnd = $.proxy(this.start, this);
+        },
+        _create: function() {
+            this.carousel()
+                .one('jcarousel:destroy', this.onDestroy);
+
+            if (this.options('autostart')) {
+                this.start();
+            }
+        },
+        _destroy: function() {
+            this.stop();
+            this.carousel()
+                .off('jcarousel:destroy', this.onDestroy);
+        },
+        start: function() {
+            this.stop();
+
+            this.carousel()
+                .one('jcarousel:animateend', this.onAnimateEnd);
+
+            this._timer = setTimeout($.proxy(function() {
+                this.carousel().jcarousel('scroll', this.options('target'));
+            }, this), this.options('interval'));
+
+            return this;
+        },
+        stop: function() {
+            if (this._timer) {
+                this._timer = clearTimeout(this._timer);
+            }
+
+            this.carousel()
+                .off('jcarousel:animateend', this.onAnimateEnd);
+
+            return this;
+        }
+    });
+}(jQuery));