/*global jQuery */ /*jshint bitwise: true, camelcase: true, curly: true, eqeqeq: true, forin: true, immed: true, indent: 4, latedef: true, newcap: true, nonew: true, quotmark: single, undef: true, unused: true, strict: true, trailing: true, browser: true */ /* * jquery.rs.carousel.js @VERSION * @HOMEPAGE * * Copyright (c) 2013 Richard Scarrott * http://www.richardscarrott.co.uk * * Dual licensed under the MIT and GPL licenses: * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * * Depends: * jquery.js v1.8+ * jquery.ui.widget.js v1.8+ */ (function ($, undefined) { 'use strict'; var _super = $.Widget.prototype; $.widget('rs.carousel', { version: '@VERSION', options: { // selectors mask: '> div', runner: '> ul', items: '> li', // options itemsPerTransition: 'auto', orientation: 'horizontal', loop: false, whitespace: false, nextPrevActions: true, insertPrevAction: function () { return $('Prev').appendTo(this); }, insertNextAction: function () { return $('Next').appendTo(this); }, pagination: true, insertPagination: function (pagination) { return $(pagination).insertAfter($(this).find('.rs-carousel-mask')); }, speed: 'normal', easing: 'swing', fx: 'slide', translate3d: false, // callbacks create: null, before: null, after: null }, _create: function () { // widget factory 1.8.* backwards compat this.widgetFullName = this.widgetFullName || this.widgetBaseClass; this.eventNamespace = this.eventNamespace || '.' + this.widgetName; this.index = 0; this._elements(); this._setIsHorizontal(); this._addMask(); this._addNextPrevActions(); this.refresh(false); return; }, // caches DOM elements _elements: function () { var elems = this.elements = {}, fullName = this.widgetFullName; this.element.addClass(fullName); elems.mask = this.element .find(this.options.mask) .addClass(fullName + '-mask'); elems.runner = (elems.mask.length ? elems.mask : this.element) .find(this.options.runner) .addClass(fullName + '-runner'); elems.items = elems.runner .find(this.options.items) .addClass(fullName + '-item'); return; }, _setIsHorizontal: function () { var elems = this.elements, fullName = this.widgetFullName; this.element .removeClass(fullName + '-horizontal') .removeClass(fullName + '-vertical'); if (this.options.orientation === 'horizontal') { this.isHorizontal = true; this.element.addClass(fullName + '-horizontal'); elems.runner.css('top', ''); } else { this.isHorizontal = false; this.element.addClass(fullName + '-vertical'); elems.runner.css('left', ''); } return; }, // adds masking div (aka clipper) _addMask: function () { var elems = this.elements; // already exists in markup if (elems.mask.length) { return; } elems.mask = elems.runner .wrap('
') .parent(); // indicates whether mask was dynamically added or already existed in mark-up this.maskAdded = true; return; }, // adds next and prev links _addNextPrevActions: function () { if (!this.options.nextPrevActions) { return; } var self = this, elems = this.elements, opts = this.options, eventNamespace = this.eventNamespace; this._removeNextPrevActions(); elems.prevAction = opts.insertPrevAction.apply(this.element[0]) .bind('click' + eventNamespace, function (e) { e.preventDefault(); self.prev(); }); elems.nextAction = opts.insertNextAction.apply(this.element[0]) .bind('click' + eventNamespace, function (e) { e.preventDefault(); self.next(); }); return; }, _removeNextPrevActions: function () { var elems = this.elements; if (elems.nextAction) { elems.nextAction.remove(); elems.nextAction = undefined; } if (elems.prevAction) { elems.prevAction.remove(); elems.prevAction = undefined; } return; }, // refresh carousel refresh: function (recache) { // undefined should pass condition if (recache !== false) { this._recacheItems(); } this._setPages(); this._addPagination(); this._setRunnerWidth(); this.index = this._makeValid(this.index); this.goToPage(this.index, false, undefined, true); this._checkDisabled(); return; }, // re-cache items in case new items have been added, // moved to own method so continuous can easily override // to avoid clones _recacheItems: function () { this.elements.items = this.elements.runner .find(this.options.items) .addClass(this.widgetFullName + '-item'); return; }, // sets pages array - [jQuery(li, li, li), jQuery(li, li, li, li), jQuery(li, li, li), jQuery(li, li)] _setPages: function () { var self = this, itemIndex = 0, lastItemIndex = isNaN(this.options.itemsPerTransition) ? undefined : this._getLastItemIndex(), start; this.pages = []; while (itemIndex < this.getNoOfItems()) { // if itemsPerTransition isn't a number we need to get the visible // items at each item index if (isNaN(this.options.itemsPerTransition)) { this.pages.push(self._getVisibleItems(itemIndex)); itemIndex += this.pages[this.pages.length - 1].length; } // otherwise simply slice up the items based on itemsPerTransition else { // making sure we don't go past the lastItemIndex if (itemIndex >= lastItemIndex) { this.pages.push(this.elements.items.slice(itemIndex)); break; } start = itemIndex; // #37 - allow `itemsPerTransition` to be passed in as string itemIndex += parseInt(this.options.itemsPerTransition, 10); this.pages.push(this.elements.items.slice(start, itemIndex)); } } return; }, // returns last logical item index _getLastItemIndex: function () { if (this.options.whitespace) { return; } return this.elements.items.index(this._getVisibleItems(0, true).last()); }, // returns a jquery object containing the visible items where the `itemIndex` // is considered to be the first visible item. Passing in reverse as true allows // us to, for example, return the visible items from the last item backwards. _getVisibleItems: function (itemIndex, reverse) { var self = this, page = [], items = !reverse ? this.elements.items.slice(itemIndex) : [].reverse.apply($(this.elements.items)).slice(itemIndex), maskDim = this._getMaskDim(), dim = 0; items .each(function () { dim += self.isHorizontal ? $(this).outerWidth(true) : $(this).outerHeight(true); if (dim > maskDim) { // if no items have been pushed to page then it means the // first item is larger than the mask so we still need to push before // breaking. if (page.length === 0) { page.push(this); } return false; } page.push(this); }); return $(page); }, _getMaskDim: function () { return this.elements.mask[this.isHorizontal ? 'width' : 'height'](); }, getNoOfItems: function () { return this.elements.items.length; }, // adds pagination links and binds associated events _addPagination: function () { if (!this.options.pagination) { return; } var self = this, fullName = this.widgetFullName, pagination = $('
    '), links = [], noOfPages = this.getNoOfPages(), i; this._removePagination(); for (i = 0; i < noOfPages; i++) { links[i] = ''; } pagination .append(links.join('')) .delegate('a', 'click' + this.eventNamespace, function (e) { e.preventDefault(); self.goToPage(parseInt(this.hash.split('-')[1], 10)); }); this.elements.pagination = this.options.insertPagination.call(this.element[0], pagination); return; }, _removePagination: function () { if (this.elements.pagination) { this.elements.pagination.remove(); this.elements.pagination = undefined; } return; }, // returns noOfPages getNoOfPages: function () { return this.pages.length; }, // if no of items is less than items on first page then the // carousel should be disabled. _checkDisabled: function () { if (this.getNoOfPages() <= 1) { this.disable(); this._disabled = true; } // only enable if carousel was disabled internally. else if (this._disabled) { this.enable(); this._disabled = false; } return; }, _setRunnerWidth: function () { var elems = this.elements, width = 0; // reset width in case orientation has been changed elems.runner.width(''); if (!this.isHorizontal) { return; } elems.runner .width(function () { elems.items .each(function () { width += $(this).outerWidth(true); }); return width; }); return; }, next: function (animate) { var index = this.index + 1; if (this.options.loop && index >= this.getNoOfPages()) { index = 0; } this.goToPage(index, animate, 'carousel:next'); return; }, prev: function (animate) { var index = this.index - 1; if (this.options.loop && index < 0) { index = this.getNoOfPages() - 1; } this.goToPage(index, animate, 'carousel:prev'); return; }, goToPage: function (index, animate, /* INTERNAL */ eventName, /* INTERNAL */ silent) { // undefined should pass animate = animate === false ? false : true; if (!this.options.disabled && this._isValid(index)) { this.prevIndex = this.index; this.index = index; // calling `this._slide` using `options.fx` to easily override within extensions this['_' + this.options.fx]($.Event(eventName ? eventName : 'carousel:gotopage', { animate: animate, speed: animate ? this.options.speed : 0 }), silent); } // make sure updateUi is called even when disabled this._updateUi(); return; }, // `index` can be $obj, element or 0 based index goToItem: function (index, animate) { var page, pageLength, item, itemLength; // if a number get the element if (!isNaN(index)) { index = this.elements.items.eq(index); } if (index.jquery) { // unwrap from jquery object index = index[0]; } // find item in pages array pages: for (page = 0, pageLength = this.getNoOfPages(); page < pageLength; page++) { for (item = 0, itemLength = this.getPage(page).length; item < itemLength; item++) { if (this.getPage(page)[item] === index) { break pages; } } } this.goToPage(page, animate); // return item as jquery object return $(index); }, // returns true if index is valid, false if not _isValid: function (index) { if (index < this.getNoOfPages() && index >= 0) { return true; } return false; }, // returns valid page index _makeValid: function (index) { if (index < 0) { index = 0; } else if (index >= this.getNoOfPages()) { index = this.getNoOfPages() - 1; } return index; }, // if silent is true callbacks won't be fired _slide: function (e, silent) { var self = this, animateProps = {}, lastPos = this._getAbsoluteLastPos(), page = this.getPage(), pos = page.first().position()[this.isHorizontal ? 'left' : 'top'], eventNamespace = this.eventNamespace, fullName = this.widgetFullName, transitionEndEvent; // if before returns false return and revert index back to prevIndex if (!silent && !this._trigger('before', e, this._getEventData())) { this.index = this.prevIndex; return; } // check pos doesn't go past last posible pos if (pos > lastPos) { pos = lastPos; } if (this.options.translate3d) { transitionEndEvent = [ 'transitionend' + eventNamespace, 'webkitTransitionEnd' + eventNamespace, 'oTransitionEnd' + eventNamespace ]; if (e.animate) { this.element.addClass(fullName + '-transition'); } this.elements.runner .unbind(transitionEndEvent.join(' ')) .on(transitionEndEvent.join(' '), function () { self.element.removeClass(fullName + '-transition'); if (!silent) { self._trigger('after', e, self._getEventData()); } }) .css('transform', 'translate3d(' + (this.isHorizontal ? -pos + 'px, 0, 0' : '0, ' + -pos + 'px, 0') + ')'); // if we're not animating the after callback should still be called if (!e.animate) { this.element.removeClass(fullName + '-transition'); if (!silent) { this._trigger('after', e, this._getEventData()); } } } else { animateProps[this.isHorizontal ? 'left' : 'top'] = -pos; this.elements.runner .stop() .animate(animateProps, e.speed, this.options.easing, function () { if (!silent) { self._trigger('after', e, self._getEventData()); } }); } return; }, // gets lastPos to ensure runner doesn't move beyond mask _getAbsoluteLastPos: function () { if (this.options.whitespace) { return; } var lastPos, lastItem = this.elements.items.eq(this.getNoOfItems() - 1), lastItemPos = lastItem.position()[this.isHorizontal ? 'left' : 'top'], lastItemDim = lastItem[this.isHorizontal ? 'outerWidth' : 'outerHeight'](true); lastPos = lastItemPos + lastItemDim - this._getMaskDim(); // if lastPos is less than 0 it means there aren't enough items to fill the entire mask return lastPos < 0 ? undefined : lastPos; }, // returns jQuery object of items on page getPage: function (index) { return this.pages[(typeof index !== 'undefined' ? index : this.index)] || $([]); }, // returns pages array getPages: function () { return this.pages; }, _getEventData: function () { return { page: this.getPage(), prevPage: this.getPage(this.prevIndex), elements: this.elements }; }, _getCreateEventData: function () { return this._getEventData(); }, // updates pagination, next and prev link state classes _updateUi: function () { this._updateActiveItems(); if (this.options.pagination) { this._updatePagination(); } if (this.options.nextPrevActions) { this._updateNextPrevActions(); } return; }, _updateActiveItems: function () { var fullName = this.widgetFullName, activeClass = fullName + '-item-active'; this.elements.items .removeClass(activeClass); this.getPage() .addClass(activeClass); return; }, _updatePagination: function () { var fullName = this.widgetFullName, activeClass = fullName + '-pagination-link-active', disabledClass = fullName + '-pagination-disabled', pagination = this.elements.pagination .removeClass(disabledClass); if (this.options.disabled) { pagination.addClass(disabledClass); } pagination .children('.' + fullName + '-pagination-link') .removeClass(activeClass) .eq(this.index) .addClass(activeClass); return; }, _updateNextPrevActions: function () { var elems = this.elements, actions = elems.nextAction.add(elems.prevAction), index = this.index, fullName = this.widgetFullName, activeClass = fullName + '-action-active', disabledClass = fullName + '-action-disabled'; actions .addClass(activeClass) .removeClass(disabledClass); if (this.options.disabled) { actions.addClass(disabledClass); } if (!this.options.loop) { if (index === this.getNoOfPages() - 1) { elems.nextAction .removeClass(activeClass); } if (index === 0) { elems.prevAction .removeClass(activeClass); } } return; }, _setOption: function (option, value) { _super._setOption.apply(this, arguments); switch (option) { case 'orientation': this._setIsHorizontal(); this.refresh(); break; case 'pagination': if (value) { this._addPagination(); this._updateUi(); } else { this._removePagination(); } break; case 'nextPrevActions': if (value) { this._addNextPrevActions(); this._updateUi(); } else { this._removeNextPrevActions(); } break; case 'loop': case 'disabled': this._updateUi(); break; case 'itemsPerTransition': case 'whitespace': this.refresh(); break; } return; }, add: function (items) { this.elements.runner.append(items); this.refresh(); return; }, remove: function (selector) { if (this.getNoOfItems() > 0) { this.elements.items .filter(selector) .remove(); this.refresh(); } return; }, // returns current index getIndex: function () { return this.index; }, // returns prev index getPrevIndex: function () { return this.prevIndex; }, // returns carousel to original state destroy: function () { var elems = this.elements, fullName = this.widgetFullName, cssProps = {}; this.element .removeClass(fullName) .removeClass(fullName + '-horizontal') .removeClass(fullName + '-vertical'); elems.mask .removeClass(fullName + '-mask'); elems.runner .removeClass(fullName + '-runner'); elems.items .removeClass(fullName + '-item'); if (this.maskAdded) { elems.runner .unwrap(); } cssProps[this.isHorizontal ? 'left' : 'top'] = ''; cssProps[this.isHorizontal ? 'width' : 'height'] = ''; cssProps.transform = ''; elems.runner .css(cssProps); this._removePagination(); this._removeNextPrevActions(); elems.runner .unbind(this.eventNamespace); _super.destroy.apply(this, arguments); return; } }); })(jQuery);