(function($){ $.widget("ui.rcarousel", { _create: function(){ var data, $root = $(this.element), _self = this, options = this.options; // if options were default there should be no problem // check if user set options before init: $('element').rcarousel({with: "foo", visible: 3}); // in above example exception will be thrown because 'with' should be a number! this._checkOptionsValidity(this.options); // for every carousel create a data object and keeps it in the element this._createDataObject(); data = $root.data("data"); // create wrapper inside root element; this is needed for animating $root .addClass("ui-carousel") .children() .wrapAll("
"); // save all children of root element in ‘paths’ array this._saveElements(); // make pages using paginate algorithm this._generatePages(); this._loadElements(); this._setCarouselWidth(); this._setCarouselHeight(); // handle default event handlers $(options.navigation.next).click( function(event){ _self.next(); event.preventDefault(); } ); $(options.navigation.prev).click( function(event){ _self.prev(); event.preventDefault(); } ); data.navigation.next = $(options.navigation.next); data.navigation.prev = $(options.navigation.prev); // stop on hover feature $root.hover( function(){ if(options.auto.enabled){ clearInterval(data.interval); data.hoveredOver = true; } }, function(){ if(options.auto.enabled){ data.hoveredOver = false; _self._autoMode(options.auto.direction); } } ); this._setStep(); // if auto mode is enabled run it if(options.auto.enabled){ this._autoMode(options.auto.direction); } // broadcast event this._trigger("start"); }, _addElement: function(jQueryElement, direction){ var $root = $(this.element), $content = $root.find("div.wrapper"), options = this.options; jQueryElement .width(options.width) .height(options.height); if(options.orientation === "horizontal"){ $(jQueryElement).css("marginRight", options.margin); } else { $(jQueryElement).css({ marginBottom: options.margin, "float": "none" }); } if(direction === "prev"){ // clone event handlers and data as well $content.prepend(jQueryElement.clone(true, true)); } else { $content.append(jQueryElement.clone(true, true)); } }, append: function(jqElements){ var $root = $(this.element), data = $root.data("data"); // add new elements jqElements.each( function(i, el){ data.paths.push($(el)); } ); data.oldPage = data.pages[data.oldPageIndex].slice(0); data.appended = true; // rebuild pages this._generatePages(); }, _autoMode: function(direction){ var options = this.options, data = $(this.element).data("data"); if(direction === "next"){ data.interval = setTimeout($.proxy(this.next, this), options.auto.interval); } else { data.interval = setTimeout($.proxy(this.prev, this), options.auto.interval); } }, _checkOptionsValidity: function(options){ var i, self = this, _correctSteps = ""; // for every element in options object check its validity $.each(options, function(key, value){ switch(key){ case "visible": // visible should be a positive integer if(!value || typeof value !== "number" || value <= 0 ||(Math.ceil(value)- value > 0)){ throw new Error("visible should be defined as a positive integer"); } break; case "step": if(!value || typeof value !== "number" || value <= 0 ||(Math.ceil(value)- value > 0)){ throw new Error("step should be defined as a positive integer"); } else if(value > self.options.visible){ // for example for visible: 3 the following array of values for 'step' is valid // 3 <= step >= 1 by 1 ==> [1,2,3] // output correct values for(i = 1; i <= Math.floor(options.visible); i++){ _correctSteps +=(i < Math.floor(value))? i + ", " : i; } throw new Error("Only following step values are correct: " + _correctSteps); } break; case "width": // width & height is defined by default so you can omit them to some extent if(!value || typeof value !== "number" || value <= 0 || Math.ceil(value)- value > 0){ throw new Error("width should be defined as a positive integer"); } break; case "height": if(!value || typeof value !== "number" || value <= 0 || Math.ceil(value)- value > 0){ throw new Error("height should be defined as a positive integer"); } break; case "speed": if(!value && value !== 0){ throw new Error("speed should be defined as a number or a string"); } if(typeof value === "number" && value < 0){ throw new Error("speed should be a positive number"); } else if(typeof value === "string" && !(value === "slow" || value === "normal" || value === "fast")){ throw new Error('Only "slow", "normal" and "fast" values are valid'); } break; case "navigation": if(!value || $.isPlainObject(value)=== false){ throw new Error("navigation should be defined as an object with at least one of the properties: 'prev' or 'next' in it"); } if(value.prev && typeof value.prev !== "string"){ throw new Error("navigation.prev should be defined as a string and point to '.class' or '#id' of an element"); } if(value.next && typeof value.next !== "string"){ throw new Error(" navigation.next should be defined as a string and point to '.class' or '#id' of an element"); } break; case "auto": if(typeof value.direction !== "string"){ throw new Error("direction should be defined as a string"); } if(!(value.direction === "next" || value.direction === "prev")){ throw new Error("direction: only 'right' and 'left' values are valid"); } if(isNaN(value.interval)|| typeof value.interval !== "number" || value.interval < 0 || Math.ceil(value.interval)- value.interval > 0){ throw new Error("interval should be a positive number"); } break; case "margin": if(isNaN(value)|| typeof value !== "number" || value < 0 || Math.ceil(value)- value > 0){ throw new Error("margin should be a positive number"); } break; } } ); }, _createDataObject: function(){ var $root = $(this.element); $root.data("data", { paths: [], pathsLen: 0, pages: [], lastPage: [], oldPageIndex: 0, pageIndex: 0, navigation: {}, animated: false, appended: false, hoveredOver: false } ); }, _generatePages: function(){ var self = this, options = this.options, data = $(this.element).data("data"), _visible = options.visible, _pathsLen = data.paths.length; // having 10 elements: A, B, C, D, E, F, G, H, I, J the algorithm // creates 3 pages for ‘visible: 5’ and ‘step: 4’: // [ABCDE], [EFGHI], [FGHIJ] function _init(){ // init creates the last page [FGHIJ] and remembers it data.pages = []; data.lastPage = []; data.pages[0] = []; // init last page for(var i = _pathsLen - 1; i >= _pathsLen - _visible; i--){ data.lastPage.unshift(data.paths[i]); } // and first page for(var i = 0; i < _visible; i++){ data.pages[0][data.pages[0].length] = data.paths[i]; } } function _islastPage(page){ var _isLast = false; for(var i = 0; i < data.lastPage.length; i++){ if(data.lastPage[i].get(0)=== page[i].get(0)){ _isLast = true; } else { _isLast = false; break; } } return _isLast; } function _append(start, end, atIndex){ var _index = atIndex || data.pages.length; if(!atIndex){ data.pages[_index] = []; } for(var i = start; i < end; i++){ data.pages[_index].push(data.paths[i]); } return _index; } function _paginate(){ var _isBeginning = true, _complement = false, _start = options.step, _end, _index, _oldFirstEl, _oldLastEl; // continue until you reach the last page // we start from the 2nd page(1st page has been already initiated) while(!_islastPage(data.pages[data.pages.length - 1])|| _isBeginning){ _isBeginning = false; _end = _start + _visible; // we cannot exceed _pathsLen if(_end > _pathsLen){ _end = _pathsLen; } // when we run ouf of elements(_end - _start < _visible)we must add the difference at the begining // in our example the 3rd page is [FGHIJ] and J element is added in the second step // first we add [FGHI] as old elements // we must assure that we have always ‘visible’(5 in our example)elements if(_end - _start < _visible){ _complement = true; } else { _complement = false; } if(_complement){ // first add old elemets; for 3rd page it adds [FGHI…] // remember the page we add to(_index) _oldFirstEl = _start -(_visible -(_end - _start)); _oldLastEl = _oldFirstEl +(_visible -(_end - _start)); _index = _append(_oldFirstEl, _oldLastEl); // then add new elements; for 3th page it is J element: // [fghiJ] _append(_start, _end, _index); } else { // normal pages like [ABCDE], [EFGHI] _append(_start, _end); // next step _start += options.step; } } } // go! _init(); _paginate(); }, getCurrentPage: function(){ var data = $(this.element).data("data"); return data.pageIndex + 1; }, getTotalPages: function(){ var data = $(this.element).data("data"); return data.pages.length; }, goToPage: function(page){ var _by, data = $(this.element).data("data"); if(!data.animated && page !== data.pageIndex){ data.animated = true; if(page > data.pages.length - 1){ page = data.pages.length - 1; } else if(page < 0){ page = 0; } data.pageIndex = page; _by = page - data.oldPageIndex; if(_by >= 0){ //move by n elements from current index this._goToNextPage(_by); } else { this._goToPrevPage(_by); } data.oldPageIndex = page; } }, _loadElements: function(elements, direction){ var options = this.options, data = $(this.element).data("data"), _dir = direction || "next", _elem = elements || data.pages[options.startAtPage], _start = 0, _end = _elem.length; if(_dir === "next"){ for(var i = _start; i < _end; i++){ this._addElement(_elem[i], _dir); } } else { for(var i = _end - 1; i >= _start; i--){ this._addElement(_elem[i], _dir); } } }, _goToPrevPage: function(by){ var _page, _oldPage, _dist, _index, _animOpts, $lastEl, _unique, _pos, _theSame, $root = $(this.element), self = this, options = this.options, data = $(this.element).data("data"); // pick pages if(data.appended){ _oldPage = data.oldPage; } else { _oldPage = data.pages[data.oldPageIndex]; } _index = data.oldPageIndex + by; _page = data.pages[_index].slice(0); // For example, the first time widget was initiated there were 5 // elements: A, B, C, D, E and 3 pages for visible 2 and step 2: // AB, CD, DE. Then a user loaded next 5 elements so there were // 10 already: A, B, C, D, E, F, G, H, I, J and 5 pages: // AB, CD, EF, GH and IJ. If the other elemets were loaded when // CD page was shown(from 5 elements)‘_theSame’ is true because // we compare the same // pages, that is, the 2nd page from 5 elements and the 2nd from // 10 elements. Thus what we do next is to decrement the index and // loads the first page from 10 elements. $(_page).each( function(i, el){ if(el.get(0)=== $(_oldPage[i]).get(0)){ _theSame = true; } else { _theSame = false; } } ); if(data.appended && _theSame){ if(data.pageIndex === 0){ _index = data.pageIndex = data.pages.length - 1; } else { _index = --data.pageIndex; } _page = data.pages[_index].slice(0); } // check if last element from _page appears in _oldPage // for [ABCDFGHIJ] elements there are 3 pages for ‘visible’ = 6 and // ‘step’ = 2: [ABCDEF], [CDEFGH] and [EFGHIJ]; going from the 3rd // to the 2nd page we only loads 2 elements: [CD] because all // remaining were loaded already $lastEl = _page[_page.length - 1].get(0); for(var i = _oldPage.length - 1; i >= 0; i--){ if($lastEl === $(_oldPage[i]).get(0)){ _unique = false; _pos = i; break; } else { _unique = true; } } if(!_unique){ while(_pos >= 0){ if(_page[_page.length - 1].get(0)=== _oldPage[_pos].get(0)){ // this element is unique _page.pop(); } --_pos; } } // load new elements self._loadElements(_page, "prev"); // calculate the distance _dist = options.width * _page.length +(options.margin * _page.length); if(options.orientation === "horizontal"){ _animOpts = {scrollLeft: 0}; $root.scrollLeft(_dist); } else { _animOpts = {scrollTop: 0}; $root.scrollTop(_dist); } $root .animate(_animOpts, options.speed, function(){ self._removeOldElements("last", _page.length); data.animated = false; if(!data.hoveredOver && options.auto.enabled){ // if autoMode is on and you change page manually clearInterval(data.interval); self._autoMode(options.auto.direction); } // scrolling is finished, send an event self._trigger("pageLoaded", null, {page: _index}); }); // reset to deafult data.appended = false; }, _goToNextPage: function(by){ var _page, _oldPage, _dist, _index, _animOpts, $firstEl, _unique, _pos, _theSame, $root = $(this.element), options = this.options, data = $root.data("data"), self = this; // pick pages if(data.appended){ _oldPage = data.oldPage; } else { _oldPage = data.pages[data.oldPageIndex]; } _index = data.oldPageIndex + by; _page = data.pages[_index].slice(0); // For example, the first time widget was initiated there were 5 // elements: A, B, C, D, E and 2 pages for visible 4 and step 3: // ABCD and BCDE. Then a user loaded next 5 elements so there were // 10 already: A, B, C, D, E, F, G, H, I, J and 3 pages: // ABCD, DEFG and GHIJ. If the other elemets were loaded when // ABCD page was shown(from 5 elements)‘_theSame’ is true because // we compare the same // pages, that is, the first pages from 5 elements and the first from // 10 elements. Thus what we do next is to increment the index and // loads the second page from 10 elements. $(_page).each( function(i, el){ if(el.get(0)=== $(_oldPage[i]).get(0)){ _theSame = true; } else { _theSame = false; } } ); if(data.appended && _theSame){ _page = data.pages[++data.pageIndex].slice(0); } // check if 1st element from _page appears in _oldPage // for [ABCDFGHIJ] elements there are 3 pages for ‘visible’ = 6 and // ‘step’ = 2: [ABCDEF], [CDEFGH] and [EFGHIJ]; going from the 2nd // to the 3rd page we only loads 2 elements: [IJ] because all // remaining were loaded already $firstEl = _page[0].get(0); for(var i = 0; i < _page.length; i++){ if($firstEl === $(_oldPage[i]).get(0)){ _unique = false; _pos = i; break; } else { _unique = true; } } if(!_unique){ while(_pos < _oldPage.length){ if(_page[0].get(0)=== _oldPage[_pos].get(0)){ _page.shift(); } ++_pos; } } // load new elements this._loadElements(_page, "next"); // calculate the distance _dist = options.width * _page.length +(options.margin * _page.length); if(options.orientation === "horizontal"){ _animOpts = {scrollLeft: "+=" + _dist}; } else { _animOpts = {scrollTop: "+=" + _dist}; } $root .animate(_animOpts, options.speed, function(){ self._removeOldElements("first", _page.length); if(options.orientation === "horizontal"){ $root.scrollLeft(0); } else { $root.scrollTop(0); } data.animated = false; if(!data.hoveredOver && options.auto.enabled){ // if autoMode is on and you change page manually clearInterval(data.interval); self._autoMode(options.auto.direction); } // scrolling is finished, send an event self._trigger("pageLoaded", null, {page: _index}); }); // reset to deafult data.appended = false; }, next: function(){ var options = this.options, data = $(this.element).data("data"); if(!data.animated){ data.animated = true; if(!data.appended ){ ++data.pageIndex; } if(data.pageIndex > data.pages.length - 1){ data.pageIndex = 0; } // move by one element from current index this._goToNextPage(data.pageIndex - data.oldPageIndex); data.oldPageIndex = data.pageIndex; } }, prev: function(){ var options = this.options, data = $(this.element).data("data"); if(!data.animated){ data.animated = true; if(!data.appended){ --data.pageIndex; } if(data.pageIndex < 0){ data.pageIndex = data.pages.length - 1; } // move left by one element from current index this._goToPrevPage(data.pageIndex - data.oldPageIndex); data.oldPageIndex = data.pageIndex; } }, _removeOldElements: function(position, length){ // remove 'step' elements var $root = $(this.element); for(var i = 0; i < length; i++){ if(position === "first"){ $root .find("div.wrapper") .children() .first() .remove(); } else { $root .find("div.wrapper") .children() .last() .remove(); } } }, _saveElements: function(){ var $el, $root = $(this.element), $elements = $root.find("div.wrapper").children(), data = $root.data("data"); $elements.each( function(i, el){ $el = $(el); // keep element’s data and events data.paths.push($el.clone(true, true)); $el.remove(); } ); }, _setOption: function(key, value){ var _newOptions, options = this.options, data = $(this.element).data("data"); switch(key){ case "speed": this._checkOptionsValidity({speed: value}); options.speed = value; $.Widget.prototype._setOption.apply(this, arguments); break; case "auto": _newOptions = $.extend(options.auto, value); this._checkOptionsValidity({auto: _newOptions}); if(options.auto.enabled){ this._autoMode(options.auto.direction); } } }, _setStep: function(s){ // calculate a step var _step, options = this.options, data = $(this.element).data("data"); _step = s || options.step; options.step = _step; data.step = options.width * _step; }, _setCarouselHeight: function(){ var _newHeight, $root = $(this.element), data = $(this.element).data("data"), options = this.options; if(options.orientation === "vertical"){ _newHeight = options.visible * options.height + options.margin *(options.visible - 1); } else { _newHeight = options.height; } $root.height(_newHeight); }, _setCarouselWidth: function(){ var _newWidth, $root = $(this.element), options = this.options, data = $(this.element).data("data"); if(options.orientation === "horizontal"){ _newWidth = options.visible * options.width + options.margin *(options.visible - 1); } else { _newWidth = options.width; } // set carousel width and disable overflow: auto $root.css({ width: _newWidth, overflow: "hidden" }); }, options: { visible: 3, step: 3, width: 100, height: 100, speed: 1000, margin: 0, orientation: "horizontal", auto: { enabled: false, direction: "next", interval: 5000 }, startAtPage: 0, navigation: { next: "#ui-carousel-next", prev: "#ui-carousel-prev" } } }); }(jQuery));