3
This is a generic carousel that's designed to handle proportional resizing of the viewport.
4
Here are the currently supported features:
6
* Moving 1 or more slides at a time
7
* Continuous motion through cloning of first and last slide sets
8
* Resize handling including optional proportional font re-sizing.
10
* Configurable Auto advance
11
* Pagination and control generation
13
TODO Future additions:
16
* Data source plugins (Rather than expecting *all* markup up front)
17
* Handle irregular total of items.
18
* Keyboard navigation with arrow keys.
20
By defatult the slide width is determined by calculating the offsetWidth of the srcNode
22
Typical markup would be as follows:
33
So in this case the srcNode would be #carousel. Should you need to override
34
the heights rather than use calculations then you can do so using the config parameter
40
/* Any frequently used shortcuts, strings and constants */
42
NodeList = Y.NodeList;
46
Carousel.superclass.constructor.apply(this, arguments);
49
Carousel.NAME = "carousel";
51
// Whether to auto advance slides or not
52
autoPlay: { value: true },
53
// Whether to preserve the aspect ration onResize.
54
preserveAspectRatio: { value: false },
55
// Describe the aspect ratio for fluid width
56
// need to maintain an aspect ratio based on content
59
setter: function(aspectRatio) {
62
// Create error function
63
logError = function() {
64
Y.log("aspectRatio: should be in the format of 'w:h' e.g: '4:1'");
66
if (typeof aspectRatio === "object" && Y.Object.hasKey(aspectRatio, "width") && Y.Object.hasKey(aspectRatio, "height")) {
69
// Parse config value in to object.
70
if (aspectRatio && aspectRatio.indexOf(":") > -1){
71
arPieces = aspectRatio.split(":");
72
if (arPieces.length !== 2) {
76
return { "width": parseFloat(arPieces[0], 10), "height": parseFloat(arPieces[1], 10) };
83
noMqAspectRatio: { value: null },
84
noMqNumSlidesVisible: { value: null },
85
noMqNumSlidesAdvance: { value: null },
86
// Selector to identify the carousel parent (usually a ul/ol).
87
carouselClassName: { value: "carousel"},
88
// Should the carousel continue automatically.
89
isContinuous: { value: true },
90
// Should fonts be resizeed relatively onResize?
91
resizeFonts: { value: false },
92
// Node selector to find nodes whose fonts
95
setter: function(sel) {
101
// ContainerWidth. Needed for fontResizing.
102
containerWidth: { value: null },
103
// How many slides are visble at once
104
numSlidesVisible: { value: 1 },
105
// How many slides should we advance by in one go.
106
numSlidesAdvance: { value: 1 },
107
// numSlidesAdvance setting as it was at intialisation.
108
defaultNumSlidesVisible: { value: null },
109
// How many slides should we advance by in one go.
110
defaultNumSlidesAdvance: { value: null },
112
// How long should the animation last.
113
slideAnimDuration: { value: 1 },
114
// What interval should be used for advance the slides.
115
slideAnimInterval: { value: 5000 },
116
// What kind of easing should be used.
117
slideAnimEasing: { value: Y.Easing.easeBoth },
118
// Pixel Rounding - in some situations can help to unify rounding.
119
// e.g: Math.floor or Math.ceil
120
pixelRoundingFunc: { value: null },
122
Allows for conf changes based on viewport size
123
Checked at resize. Example:
143
viewportConfig: { value: null },
144
// Ref node fo append/prepend/insertBefore/After pagination
145
pagingAppendRefNode: {
146
setter: function(sel) {
151
// Where param should be one of before/after/prepend/append
152
pagingAppendRefWhere: { value: null },
153
// Max pagination items
154
// When pagination exceeds this value it shouldn't be generated
155
maxPagingItems: { value: 5 },
156
// Ref node fo append/prepend/insertBefore/After nav
158
setter: function(sel) {
163
// Where param should be one of before/after/prepend/append
164
controlsRefWhere: { value: null },
165
// Loading class name (removed after initialisation)
166
// Classname is not added programmatically so needs to
167
// be present in the markup.
168
loadingClassname: { value: "loading" },
169
// Loaded class name (added to the srcNode after initialisation)
170
loadedClassname: { value: "loaded" },
172
debug: { value: false }
176
/* Carousel extends the base Widget class */
177
Y.extend(Carousel, Y.Widget, {
179
initializer: function() {
180
var defaultVpConf = {},
181
validVpConfOverrides,
183
noMqNumSlidesAdvance,
184
noMqNumSlidesVisible,
185
viewportConfig = this.get("viewportConfig"),
186
preserveAspectRatio = this.get("preserveAspectRatio"),
189
// Aspect ratio for old versions of IE that don't grok MQs
190
if (preserveAspectRatio && viewportConfig && (Y.UA.ie && Y.UA.ie < 9)) {
191
noMqAspectRatio = this.get("noMqAspectRatio");
192
noMqNumSlidesVisible = this.get("noMqNumSlidesVisible");
193
noMqNumSlidesAdvance = this.get("noMqNumSlidesAdvance");
195
if (noMqAspectRatio && noMqNumSlidesVisible && noMqNumSlidesAdvance) {
196
this.set("aspectRatio", noMqAspectRatio);
197
this.set("numSlidesAdvance", noMqNumSlidesAdvance);
198
this.set("numSlidesVisible", noMqNumSlidesVisible);
200
throw new Error("please configure explicit values for noMqAspectRatio/noMqNumSlidesAdvance/noMqNumSlidesVisible");
204
this.set("defaultNumSlidesAdvance", this.get("numSlidesAdvance"));
205
this.set("defaultNumSlidesVisible", this.get("numSlidesVisible"));
208
this.set("defaultAspectRatio", this.get("aspectRatio"));
209
validVpConfOverrides = this.validVpConfOverrides = ["numSlidesVisible", "numSlidesAdvance", "aspectRatio", "autoPlay"];
210
for (i=0, j=validVpConfOverrides.length; i < j; i++) {
211
key = validVpConfOverrides[i];
212
defaultVpConf[key] = this.get(key);
214
this.defaultVpConf = defaultVpConf;
217
destructor : function() {
219
* destructor is part of the lifecycle introduced by
220
* the Widget class. It is invoked during destruction,
221
* and can be used to cleanup instance specific state.
223
* Anything under the boundingBox will be cleaned up by the Widget base class
224
* We only need to clean up nodes/events attached outside of the bounding Box
226
* It does not need to invoke the superclass destructor.
227
* destroy() will call initializer() for all classes in the hierarchy.
231
renderUI : function() {
233
* renderUI is intended to be used by the Widget subclass
234
* to create or insert new elements into the DOM.
237
srcNode = this.get("srcNode"),
238
carouselClassName = this.get("carouselClassName"),
239
isContinuous = this.get("isContinuous"),
240
loadingClass = this.get("loadingClassname"),
241
loadedClass = this.get("loadedClassname");
245
this.interval = null;
248
nodeListSlides = this.getCarouselNodeList();
249
this.nodeCarousel = srcNode.one("."+ carouselClassName);
250
this.numSlides = nodeListSlides.size();
252
this.caroAnim = new Y.Anim({
253
node: this.nodeCarousel
255
this.caroAnim.set("duration", this.get("slideAnimDuration"));
256
this.caroAnim.set("easing", this.get("slideAnimEasing"));
257
this.caroAnim.on("end", function(){
259
this.updatePagination();
262
// Always run the resize calcs at initialisation
264
if (this.get("autoPlay")) {
269
this.generateControls();
271
this.generatePagination();
274
// If we are wanting a continuous carousel
275
if (isContinuous === true) {
279
// We are ready remove the loading classname.
280
this.get("boundingBox").all("." + loadingClass).removeClass(loadingClass);
281
srcNode.addClass(loadedClass);
285
bindUI : function() {
286
Y.on('windowresize', Y.bind(this.handleResize, this));
289
syncUI : function() {
290
var isContinuous = this.get("isContinuous");
291
this.updatePagination();
292
if (isContinuous === true) {
293
this.gotoSlide(0, 0);
297
// Beyond this point is the Carousel specific application and rendering logic
299
getCarouselNodeList: function() {
300
var srcNode = this.get("srcNode"),
301
carouselClassName = this.get("carouselClassName");
302
return srcNode.all("." + carouselClassName + " > li");
305
updatePagination: function() {
306
var srcNode = this.get("srcNode"),
307
numSlidesVisible = this.get("numSlidesVisible"),
308
paginationRef = this.get("pagingAppendRefNode") || srcNode,
309
paginationListItems = paginationRef.get("parentNode").all(".pagination li a");
311
paginationListItems.each(function(a){
312
var nodeClassName = a.get("className");
313
if (parseInt(nodeClassName.replace("p-", ""), 10) === Math.ceil(parseInt(this.curSlide / numSlidesVisible, 10))) {
314
a.addClass("active");
316
a.removeClass("active");
323
* insert a node based on ref and where
324
* Defaults to appending to ref.
327
insertItem: function(node, ref, where) {
333
ref.insert(node, "before");
336
ref.insert(node, "after");
343
generatePagination: function() {
344
var pageText = 'Page',
345
li, a, sp, txt, olNode, ol,
346
numSlidesVisible = this.get("numSlidesVisible"),
347
pagingAppendRefNode = this.get("pagingAppendRefNode") || this.get("srcNode"),
348
pagingAppendRefWhere = this.get("pagingAppendRefWhere"),
349
maxPagingItems = this.get("maxPagingItems"),
351
existingPagination, i;
353
// Kill pagination if it already exists.
354
existingPagination = pagingAppendRefNode.get("parentNode").one(".pagination");
355
if (existingPagination) {
356
existingPagination.remove();
358
numPages = Math.ceil(this.numSlides / numSlidesVisible);
359
// Generate it if it's under the max paging items
360
if (numPages <= maxPagingItems) {
361
ol = document.createElement('ol');
362
ol.className = "pagination";
363
for (i=0; i < numPages; i++){
364
li = document.createElement('li');
365
a = document.createElement('a');
366
sp = document.createElement('span');
368
txt = document.createTextNode(pageText+(i+1));
371
a.className = 'p-'+i;
376
this.insertItem(ol, pagingAppendRefNode, pagingAppendRefWhere);
379
// delegate the clicks on the olNode.
380
olNode.delegate("click", this.handlePageClick, 'a', this);
384
handlePageClick: function(e) {
386
numSlidesAdvance = this.get("numSlidesAdvance");
388
if (this.caroAnim.get("running") === false) {
389
if (e && this.autoPlayTimer) {
390
window.clearInterval(this.autoPlayTimer);
392
targetClass = e.currentTarget.get("className");
393
this.gotoSlide(parseInt(targetClass.replace("p-", ""), 10) * numSlidesAdvance);
397
generateControls: function() {
398
var prev = Node.create('<a href="#" class="prev"><span>Previous Slide</span></a>'),
399
next = Node.create('<a href="#" class="next"><span>Next Slide</span></a>'),
400
controlsRefNode = this.get("controlsRefNode") || this.get("srcNode"),
401
controlsRefWhere = this.get("controlsRefWhere");
403
next.on("click", this.next, this);
404
prev.on("click", this.prev, this);
406
this.insertItem(prev, controlsRefNode, controlsRefWhere);
407
this.insertItem(next, controlsRefNode, controlsRefWhere);
410
advance: function(e, val){
414
if (e && this.autoPlayTimer) {
415
window.clearInterval(this.autoPlayTimer);
417
if (this.caroAnim.get("running") === true) {
424
* Show the next slide
427
this.advance(e, this.curSlide + this.get("numSlidesAdvance"));
431
* Show the previous slide
434
this.advance(e, this.curSlide - this.get("numSlidesAdvance"));
438
* Goto slide n (zero-indexed)
440
gotoSlide: function(nSlideIndex, duration) {
443
isContinuous = this.get("isContinuous"),
444
numSlidesAdvance = this.get("numSlidesAdvance"),
445
slideWidth = this.slideWidth;
447
this.requestedIndex = nSlideIndex;
449
Y.log("gotoSlide: nSlideIndex: " + nSlideIndex);
450
Y.log("gotoSlide: nSlideWidth: " + slideWidth);
451
duration = (duration === 0) ? duration : (duration || this.get("slideAnimDuration"));
452
if (nSlideIndex < 0) {
453
if (isContinuous === true) {
454
// Fold over to the start of the cloned slides
455
this.animateTo(0, duration);
456
this.curSlide = this.numSlides - numSlidesAdvance;
458
// Fast-forward to the previous end.
459
this.animateTo(-(slideWidth * (this.numSlides - numSlidesAdvance)), duration);
460
this.curSlide = this.numSlides - numSlidesAdvance;
462
} else if (nSlideIndex >= (this.numSlides)) {
463
if (isContinuous === true) {
464
xOffSet = -((slideWidth) * (this.numSlides + numSlidesAdvance));
465
this.animateTo(xOffSet, duration);
466
this.curSlide = this.numSlides;
468
this.animateTo(0, duration);
472
cloneSlide = (isContinuous === true) ? numSlidesAdvance : 0;
473
xOffSet = -(slideWidth * (nSlideIndex + cloneSlide));
474
this.animateTo(xOffSet, duration);
475
this.curSlide = nSlideIndex;
480
autoPlay: function() {
482
this.autoPlayTimer = window.setInterval(function(){
484
}, that.get("slideAnimInterval"));
487
animateTo: function(value, duration) {
488
Y.log("value: " + value + "duration: " + duration);
490
if (duration === 0) {
491
this.nodeCarousel.setStyle("left", value);
494
origDuration = this.caroAnim.get("duration");
495
this.caroAnim.set("duration", duration);
497
this.caroAnim.set("to", { "left": value });
499
this.caroAnim.set("duration", origDuration);
503
transitionTo: function(value, duration) {
504
this.nodeCarousel.transition({
511
var isContinuous = this.get("isContinuous"),
512
numSlidesAdvance = this.get("numSlidesAdvance"),
515
Y.log("flip: isContinuous: " + isContinuous);
516
if (isContinuous === true && this.requestedIndex) {
517
slideWidth = this.slideWidth;
518
Y.log("flip: numSlides: " + this.numSlides);
519
Y.log("flip: requestedIndex: " + this.requestedIndex);
520
if (this.requestedIndex < 0) {
521
Y.log("flipped to end");
522
xOffSet = -(slideWidth * (this.numSlides));
523
this.animateTo(xOffSet, 0);
524
this.curSlide = this.numSlides - numSlidesAdvance;
525
} else if (this.requestedIndex > (this.numSlides - 1)) {
526
Y.log("flipped to start");
527
this.animateTo(-(slideWidth * numSlidesAdvance), 0);
530
Y.log("After flipping this.curSlide: " + this.curSlide);
535
* Clones the first and last slides so we can display the
536
* slideshow continuously.
538
cloneSlides: function(){
545
numSlidesVisible = this.get("numSlidesVisible"),
546
carouselNodeListSize,
547
srcNode = this.get("srcNode"), i, j, k, l;
549
clones = srcNode.all(".clone");
551
Y.log("clones size:" + clones.size());
552
if (clones.size() === 0 || (clones.size() / 2) !== numSlidesVisible) {
554
carouselNodeList = this.getCarouselNodeList();
555
carouselNodeListSize = carouselNodeList.size();
557
// Get references to first and last slides.
558
firstSlide = carouselNodeList.item(0);
559
lastSlide = carouselNodeList.item(this.numSlides-1);
561
// Create new nodelists to store clones.
562
firstCloneList = new NodeList([]);
563
lastCloneList = new NodeList([]);
565
// Grab clones of last n slides
566
for (i=carouselNodeListSize - numSlidesVisible, j=carouselNodeListSize; i<j; i++) {
567
lastCloneList.push(carouselNodeList.item(i).cloneNode(true));
569
// Grab clones of first n slides
570
for (k=0, l=numSlidesVisible; k<l; k++) {
571
firstCloneList.push(carouselNodeList.item(k).cloneNode(true));
574
// Add class as a way to identify the clones.
575
firstCloneList.addClass("clone");
576
lastCloneList.addClass("clone");
578
// Add the cloned slides to either end of the slide list
579
lastSlide.insert(firstCloneList, "after");
580
firstSlide.insert(lastCloneList, "before");
582
Y.log("carouselSize: " + this.getCarouselNodeList().size());
587
* Applies configoverrides assuming config properties are
588
* within the validVpConfOverrides list.
590
applyConfigOverrides: function(confObj) {
592
Y.Object.each(confObj, function(v,k){
594
// Check item is in the list of overridable keys.
595
if (Y.Array.indexOf(this.validVpConfOverrides, confKey) > -1 ) {
598
throw new Error("viewportConfKey: "+confKey +" is not a valid key");
605
* Handle resize events gracefully so we can
606
* scale the carousel accordingly.
608
handleResize: function(){
611
srcNode = this.get("srcNode"),
612
carouselNodeList = this.getCarouselNodeList(),
621
viewportConfig = this.get("viewportConfig"),
625
containerWidth = this.get("containerWidth"),
626
preserveAspectRatio = this.get("preserveAspectRatio"),
627
resizeFonts = this.get("resizeFonts"),
632
if (preserveAspectRatio) {
634
// Ignore viewPort config if IE < 9
635
if (viewportConfig && (!Y.UA.ie || Y.UA.ie >= 9)) {
636
/* Handle config overrides as supplied in config.
637
* Loops through conf in reverse to match largest
638
* width first as these confs match given
639
* resolution or higher.
641
vpWidth = Y.one("body").get("winWidth");
642
widthKeys = Y.Object.keys(viewportConfig);
643
widthKeys.sort(Y.Array.numericSort);
644
// These are the defaults
645
overridesObject = this.defaultVpConf;
646
for (i=0, j=widthKeys.length; i < j; i++ ) {
647
if (vpWidth >= widthKeys[i]) {
648
// for each conf that matches extend the conf object
649
// to provide cascading inheritance.
650
// TODO: Might consider caching each step of
651
// this if performance is an issue.
652
// though should be fairly quick as there's a minimal
653
// set of keys that can be overriden.
654
overridesObject = Y.merge(overridesObject, viewportConfig[widthKeys[i]]);
658
// Apply the configurations changes.
659
this.applyConfigOverrides(overridesObject);
662
// Check these values now in case view port config changed the values.
663
aspectRatio = this.get("aspectRatio");
664
numSlidesAdvance = this.get("numSlidesAdvance");
665
numSlidesVisible = this.get("numSlidesVisible");
667
// Container measurements now
668
curContainerWidth = srcNode.get("offsetWidth");
670
// Get height relative to width change
671
relativeHeight = parseInt((curContainerWidth / aspectRatio.width) * aspectRatio.height, 10);
672
srcNode.setStyle("height", relativeHeight+"px");
674
newHeight = relativeHeight;
675
newWidth = (curContainerWidth / numSlidesVisible);
677
pixelRoundingFunc = this.get("pixelRoundingFunc");
678
if (pixelRoundingFunc) {
679
newHeight = pixelRoundingFunc(newHeight);
680
newWidth = pixelRoundingFunc(newWidth);
682
carouselNodeList.each(function(slideNode){
683
slideNode.setStyle("width", newWidth);
684
slideNode.setStyle("height", newHeight);
687
this.slideWidth = newWidth;
688
this.slideHeight = newHeight;
690
// We could do this with an event but want to ensure linear handling of
691
// cloning within the rest of this flow.
692
// It'll only do work if the number of clones/2 is != numSlidesVisible.
693
if (this.get("isContinuous")) {
696
this.generatePagination();
698
// If resize fonts is turned on
699
if (resizeFonts === true) {
700
percDiffWidth = Math.round((((100 / containerWidth) * curContainerWidth)*100)) / 100;
701
// Updates the font-size based on the current width and the original
702
// font-size when we started.
703
fontNodes = this.get("fontNodes");
704
fontNodes.each(function(node){
705
var origFontSize = this.getFontSize(node);
706
if ( percDiffWidth === 100 ) {
707
node.setStyle("fontSize", origFontSize + "px");
709
node.setStyle("fontSize", (parseInt(origFontSize, 10) / 100) * percDiffWidth + "px");
715
// Put us back to where we should be.
716
slideToMoveTo = this.curSlide;
717
curSlideModulo = (slideToMoveTo % numSlidesVisible);
718
if (curSlideModulo !== 0) {
719
slideToMoveTo = slideToMoveTo - curSlideModulo;
721
if (this.caroAnim.get("running") === false) {
722
this.gotoSlide(slideToMoveTo, 0);
724
// Single event so it's detached immediately when fired.
725
this.caroAnim.once("end", function(){
726
this.gotoSlide(slideToMoveTo, 0);
735
* Get's the font-size of a node and stashes it's value as a custom
736
* attr on the first run. Subsequent lookups return the attr value.
738
getFontSize: function(node){
739
var origFontSize = node.getAttribute("origFontSize"),
742
if (this.IE && this.IE < 9) {
743
// Fix to actually get a font-size from ie < 9 that reflects reality
744
curStyle = node.get("currentStyle");
745
origFontSize = (parseInt(curStyle.fontSize, 10)/100 * 13);
747
origFontSize = parseInt(node.getStyle("fontSize"), 10);
749
node.setAttribute("origFontSize", origFontSize);
757
Y.u1.Carousel = Carousel;