Last active
December 21, 2015 09:39
-
-
Save lcaballero/6286882 to your computer and use it in GitHub Desktop.
Content Carousel in JS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This carousel requires the following minimum structure, where everything | |
* in [] are part of the dom which the carousel doesn't effect, and can be | |
* customized to the situation. See also: usage below. | |
* | |
* [module] | |
* [header/] | |
* .content-carousel-container | |
* ul | |
* li [content-1] | |
* /ul | |
* [content] | |
* .carousel-control | |
* .go-left | |
* ul | |
* li [item] | |
* li [item] | |
* /ul | |
* .go-right | |
* /.carousel-control | |
* /[module] | |
* | |
* The code fires an event before each needed carousel update. There are 3 kinds of | |
* updates: 1) Scroll Left, 2) Scroll Right and 3) Select an individual item. | |
* | |
* Remarks: | |
* | |
* Be aware that clicking on a bullet will send information to callbacks requesting an | |
* array of items to load. The array contains item numbers for every item to the left | |
* of the bullet not already loaded, and the bullet itself, if not already loaded. When | |
* the carousel recieves these items it then adds them to the list, and proceedes to | |
* animate the carousel to the correct index. | |
* | |
* For example, given 5 bullets (S=selected, L=Loaded, C=Clicked, *=Not Loaded, <=Go Left, | |
* >=Go Right) then when the current state of the carousel consists in this state: | |
* | |
* < (S) (L) * * * > | |
* | |
* and the user clicks item #4 | |
* | |
* < (S) (L) * (C) * > | |
* | |
* the carousel will request that the onItemSelect callback provide items at offset [2, 3]. | |
* Which will be LIs to be inserted into the '.carousel-container ul'. | |
* | |
* Also, starting with the scenario: | |
* | |
* < (S) (L) * * * > | |
* | |
* If the user clicks the left arrow the carousel will ask the onItemProvider to provide | |
* [2, 3, 4], the items needed to rotate to fifth bullet and offset 4. | |
* | |
* If the user clicks the right arrow the carousel will determine that it needs to request | |
* an empty set of items, so the array [], and will just rotate to the already loaded item | |
* which is the item #2 at offset 1. | |
* | |
* Consequently, should the user click the 5th bullet all other items to the left will | |
* need to be loaded. | |
*/ | |
(function ($) { | |
function createRotatingIndexes(items, collection, activeClass, isLoadedClass) { | |
// these provide items form the underlying collections (out of both items, and collection). | |
items.first = function () { return this[0]; }; | |
items.activeItem = function () { return this.first(); }; | |
items.getBullet = function () { return $(collection[this.activeItem()]); }; | |
items.select = function (n) { | |
// .get(n) would handle negative indexes, but that's not what we are after | |
return ( | |
n > items.length - 1 ? $() : // beyond upper bound | |
n < 0 ? $() : // beyond lower bound | |
collection.get(n)); // within bounds | |
}; | |
items.whereNotLoaded = function (n) { | |
function isLoaded(e, i) { | |
return $(e).hasClass(isLoadedClass); | |
}; | |
function toIndexes(e, i) { return parseInt($(e).attr("data-item") || "0", 10); }; | |
function isRequiredIndex(e, i) { return e <= n; }; | |
var notLoaded = $.grep(collection, isLoaded, true); | |
var indexes = $.map(notLoaded, toIndexes); | |
var required = $.grep(indexes, isRequiredIndex); | |
return required; | |
}; | |
// these are chainable | |
items.rotateRight = function () { var a = this.shift(); this.push(a); return this; }; | |
items.rotateLeft = function () { var a = this.pop(); this.unshift(a); return this; }; | |
items.deactivateBullet = function () { $(this.getBullet()).removeClass(activeClass); return this; }; | |
items.activateBullet = function () { $(this.getBullet()).addClass(activeClass); return this; }; | |
items.markAsLoaded = function () { return this.getBullet().addClass(isLoadedClass); return this }; | |
// these create clones, and are chainable | |
items.peekLeft = function () { | |
return createRotatingIndexes([].concat(items), collection, activeClass, isLoadedClass).rotateLeft(); | |
}; | |
items.peekRight = function () { | |
return createRotatingIndexes([].concat(items), collection, activeClass, isLoadedClass).rotateRight(); | |
}; | |
return items; | |
}; | |
/** | |
* Turns a jQuery collection of LIs into a rotating array filled with index numbers, | |
* that point to the underlying items position in the | |
* has the following interface: | |
* | |
* first() Returns the index at the front of the array which changes as the array | |
* is rotated through. | |
* | |
* activeItem() Returns the last index in the array, and really is an alias for | |
* what is considerred the 'active' item. | |
* | |
* rotateLeft() Moves the index at the front of the array to the end of the array | |
* consequently making the first+1 item the new 'active' index. | |
* | |
* rotateRight() Moves the index at the end of the array to the front of the array, | |
* and so making the last index the first element in the array, and the 'active' index. | |
* | |
* deactivateBullet() Removes the class provided as 'activeClass' from the li which | |
* is indexed by the activeItem index. | |
* | |
* activateBullet() Adds the class 'activeClass' to the li referred to by the activeItem | |
* index. | |
*/ | |
$.fn.rotatingBullets = function (activeClass, isLoadedClass) { | |
activeClass = activeClass || "selected"; | |
isLoadedClass = isLoadedClass || "is-loaded"; | |
var items = $.map(this, function (e, i) { return i; }); | |
items = createRotatingIndexes(items, this, activeClass, isLoadedClass); | |
return items; | |
}; | |
$.fn.contentCarousel = function (options) { | |
if (this.length == 0) { return this; } | |
var width = options.width || 476; | |
var duration = options.duration || 750; | |
var itemFn = options.itemProvider || function () { }; | |
var container = this.find(".carousel-container:first"); | |
var controls = container.next(".carousel-control"); | |
var items = controls.find("li").rotatingBullets(); | |
var dirs = { | |
toString: function () { return this.op; }, | |
isLeft: function () { return this.toString() == "+="; }, | |
isRight: function () { return this.toString() == "-="; }, | |
isItem: function () { return this.toString() == ""; } | |
}; | |
var dirLeft = $.extend({}, { op: "+=" }, dirs); | |
var dirRight = $.extend({}, { op: "-=" }, dirs); | |
var dirToItem = $.extend({}, { op: "" }, dirs); | |
var isAnimating = false; | |
var itemList = container.find("> ul:first"); | |
function success(p) { | |
return function (newItems) { | |
itemList.append(newItems); | |
// current left == items.active * width | |
// if going from [0..n-1] need to animate and arrive at n*width | |
// if going from [n-1..0] need to animate and arrive at 0 | |
var newLeft = p.dir.toString() + p.width + "px"; | |
if (p.dir.isLeft() && p.selectedItem == 0) { | |
newLeft = "-=" + p.nextLeft + "px"; | |
} else if (p.dir.isRight() && p.selectedItem == p.items.length - 1) { | |
newLeft = "+=" + p.currentLeft + "px"; | |
} | |
itemList.animate( | |
{ left: newLeft }, | |
{ | |
duration: duration, | |
complete: function () { | |
container.hideLoader(); | |
if (p.dir.isLeft()) { | |
p.items.deactivateBullet() | |
.rotateLeft() | |
.activateBullet() | |
.markAsLoaded(); | |
} else if (p.dir.isRight()) { | |
p.items | |
.deactivateBullet() | |
.rotateRight() | |
.activateBullet() | |
.markAsLoaded(); | |
} else { | |
// animate to item # | |
} | |
} | |
}); | |
}; | |
}; | |
function error() { container.hideLoader(); }; | |
function complete() { container.hideLoader(); }; | |
function createCallbackParameters(dir, num) { | |
var params = { | |
width: width, | |
dir: dir, | |
error: error, | |
currentLeft: items.activeItem() * width, | |
selectedItem: items.activeItem(), | |
nextLeft: | |
dir.isLeft() ? items.peekLeft().activeItem() * width : | |
dir.isRight() ? items.peekRight().activeItem() * width : | |
dir.isToItem() ? num * width : width, | |
complete: complete, | |
items: items, | |
requestedItems: | |
dir.isLeft() ? items.whereNotLoaded(items.peekLeft().activeItem()) : | |
dir.isRight() ? items.whereNotLoaded(items.peekRight().activeItem()) : | |
dir.isToItem() ? items.whereNotLoaded(num) : | |
[] | |
}; | |
params.success = success(params); | |
return params; | |
} | |
this.delegate( | |
".go-left", | |
"click", | |
function (ev) { | |
container.showLoader(); | |
var p = createCallbackParameters(dirLeft); | |
itemFn.apply(this, [ev, p]); | |
}); | |
this.delegate( | |
"li", | |
"click", | |
function (ev) { | |
container.showLoader(); | |
var p = createCallbackParameters(dirToItem); | |
}); | |
this.delegate( | |
".go-right", | |
"click", | |
function (ev) { | |
container.showLoader(); | |
var p = createCallbackParameters(dirRight); | |
itemFn.apply(this, [ev, p]); | |
}); | |
return this; | |
}; | |
})(jQuery); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This sounds awesome to me. I also read about the WinForms UI Carousel recently. Don't know if it is convenient in using. Anyway, its carousel control guide is not quite detailed for me. This JS carousel do make sense to me.