/** * jCarousel Core Plugin * * Depends: * core.js */ (function($, window) { 'use strict'; var $window = $(window); var toFloat = function(val) { return parseFloat(val) || 0; }; $.jCarousel.plugin('jcarousel', { animating: false, tail: 0, inTail: false, resizeState: null, 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: $(), _first: $(), _last: $(), _visible: $(), _fullyvisible: $(), _init: function() { var self = this; self.resizeState = $window.width() + 'x' + $window.height(); this.onWindowResize = function() { if (self.resizeTimer) { clearTimeout(self.resizeTimer); } self.resizeTimer = setTimeout(function() { var currentResizeState = $window.width() + 'x' + $window.height(); // Check if the window size actually changed. // iOS might trigger resize events on page scroll. if (currentResizeState === self.resizeState) { return; } self.resizeState = currentResizeState; self.reload(); }, 100); }; return this; }, _create: function() { this._reload(); $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 = toFloat(this.list().height()) > toFloat(this.list().width()); } this.rtl = this.options('rtl'); if (this.rtl == null) { this.rtl = (function(element) { if (('' + element.attr('dir')).toLowerCase() === 'rtl') { return true; } var found = false; element.parents('[dir]').each(function() { if ((/rtl/i).test($(this).attr('dir'))) { found = true; return false; } }); return found; }(this._element)); } this.lt = this.vertical ? 'top' : 'left'; // Ensure before closest() call this.relative = this.list().css('position') === 'relative'; // Force list and items reload this._list = null; this._items = null; var item = this.index(this._target) >= 0 ? this._target : this.closest(); // _prepare() needs this here this.circular = this.options('wrap') === 'circular'; this.underflow = false; var props = {'left': 0, 'top': 0}; if (item.length > 0) { this._prepare(item); this.list().find('[data-jcarousel-clone]').remove(); // Force items reload this._items = null; this.underflow = this._fullyvisible.length >= this.items().length; this.circular = this.circular && !this.underflow; props[this.lt] = this._position(item) + 'px'; } this.move(props); return this; }, list: function() { if (this._list === null) { var option = this.options('list'); this._list = $.isFunction(option) ? option.call(this) : this._element.find(option); } 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]'); } 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; if (this.rtl && this.relative && !this.vertical) { pos += toFloat(this.list().width()) - this.clipping(); } this.items().each(function() { closest = $(this); if (stop) { return false; } var dim = self.dimension(closest); pos += dim; if (pos >= 0) { width = dim - toFloat(closest.css('margin-' + lrb)); if ((Math.abs(pos) - dim + (width / 2)) <= 0) { stop = true; } else { return false; } } }); 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; } var wrap = this.options('wrap'), end = this.items().length - 1, check = this.options('center') ? this._target : this._last; return end >= 0 && !this.underflow && ((wrap && wrap !== 'first') || (this.index(check) < 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 && !this.underflow && ((wrap && wrap !== 'last') || (this.index(this._first) > 0) || (this.tail && this.inTail)) ? true : false; }, clipping: function() { return toFloat(this._element['inner' + (this.vertical ? 'Height' : 'Width')]()); }, dimension: function(element) { return toFloat(element['outer' + (this.vertical ? 'Height' : 'Width')](true)); }, scroll: function(target, animate, callback) { if (this.animating) { return this; } if (false === this._trigger('scroll', null, [target, animate])) { return this; } if ($.isFunction(animate)) { callback = animate; animate = 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'); 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 = toFloat(this.list().width()) - this.clipping(); } } if (properties.left) { properties.left = (toFloat(position.left) + correction + toFloat(properties.left) * multiplier) + 'px'; } if (properties.top) { properties.top = (toFloat(position.top) + correction + toFloat(properties.top) * multiplier) + 'px'; } 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(); if (!transitions && duration > 0) { list.animate(properties, opts); return; } var complete = opts.complete || $.noop, css = {}; if (transitions) { var backup = { transitionDuration: list.css('transitionDuration'), transitionTimingFunction: list.css('transitionTimingFunction'), transitionProperty: list.css('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' }; } 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 (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; } if (typeof item !== 'object') { item = this.items().eq(item); } else if (typeof item.jquery === 'undefined') { item = $(item); } 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 = toFloat(this.list().position()[this.lt]); if (pos === currPos) { if ($.isFunction(callback)) { callback.call(this, false); } return this; } 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 += toFloat(this.list().width()) - this.clipping(); } if (this.rtl && !this.vertical) { pos += this.tail; } else { pos -= this.tail; } 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 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(); } this._trigger('animateend'); 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]; } opts.complete = function() { complete(); oldComplete.call(this); }; this.move(properties, opts); 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; } if (wh < clip) { while (true) { curr = this.items().eq(++idx); if (curr.length === 0) { if (!this.circular) { break; } curr = this.items().eq(0); if (item.get(0) === curr.get(0)) { break; } isVisible = this._visible.index(curr) >= 0; if (isVisible) { curr.after(curr.clone(true).attr('data-jcarousel-clone', true)); } this.list().append(curr); if (!isVisible) { var props = {}; props[this.lt] = this.dimension(curr); this.moveBy(props); } // Force items reload this._items = null; } dim = this.dimension(curr); if (dim === 0) { break; } 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.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; } } } this._update(update); this.tail = 0; if (!center && this.options('wrap') !== 'circular' && this.options('wrap') !== 'custom' && this.index(update.last) === (this.items().length - 1)) { // Remove right/bottom margin from total width wh -= toFloat(update.last.css('margin-' + lrb)); if (wh > clip) { this.tail = wh - clip; } } return this; }, _position: function(item) { var first = this._first, pos = toFloat(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 -= toFloat(this.list().width()) - this.dimension(first); } else { pos -= this.clipping() - this.dimension(first); } pos += centerOffset; } else { pos -= centerOffset; } 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(); } self._trigger(key + 'in', $(elIn)); self._trigger(key + 'out', $(elOut)); self['_' + key] = update[key]; }; for (key in update) { doUpdate(key); } return this; } }); }(jQuery, window));