#A simple Slideshow module wrapped in a Backbone View
- Dependencies ** underscore.js ** backbone.js
Viewable in action in this jsfiddle
APP.Slideshow = new APP.Views.Slideshow({ | |
collection: new APP.Collections.Slides([ | |
{ | |
id:1, | |
headline: 'Welcome to APP', | |
caption: 'The best online tool for finding eLearning materials.' | |
}, | |
{ | |
id:2, | |
headline: 'Thousands of Products', | |
caption: 'APP provides a huge catalogue of eLearning products.<br/> Search the products tab below to get started.' | |
}, | |
{ | |
id:3, | |
headline: 'Mobile Ready', | |
caption: 'Are you ready for the mobile web?' | |
}, | |
{ | |
id:4, | |
headline: 'Rich Media', | |
caption: 'Search the APP media collection for access to thousands of videos and audio files to enrich your stuff' | |
}, | |
{ | |
id:5, | |
headline: 'Built for Medical Professionals', | |
caption: 'APP provides tools for people that allow you to manage and organize your purchases with ease.' | |
} | |
]) | |
}).render(); |
<!-- Initial Markup Required --> | |
<!-- note: .icon is a simple vertical image sprite --> | |
<div id="slideshow"> | |
<ul class="slides"></ul> | |
<ul class="controls"> | |
<li class="slide-control toggle-play-pause icon"> </li> | |
</ul> | |
<span class="toggler icon"></span> | |
</div> |
APP.Views.Slideshow = Backbone.View.extend({ | |
el: '#slideshow', | |
slides: '#slideshow .slides', | |
controls: '#slideshow .controls', | |
playPauseControl: '#slideshow .controls .toggle-play-pause', | |
delay: 10000, | |
currentIndex: 0, | |
events: { | |
'click .toggler' : 'toggleVisibility', | |
'click .toggle-play-pause' : 'togglePlayPause', | |
'click .jump-to' : 'jumpTo' | |
}, | |
slideTemplate: _.template( | |
'<li id="slide-{{ id }}" class="slide {{ layout }}" style="background: url(images/slideshow/{{ id }}.jpg) no-repeat;">' + | |
'<p class="headline">{{ headline }}</p>' + | |
'<p class="caption">{{ caption }}</p>' + | |
'</li>' | |
), | |
controlTemplate: _.template( | |
'<li class="slide-control jump-to" data-index="{{ index }}">{{ human_readable_index }}</li>' | |
), | |
initialize: function() { | |
_.bindAll(this, 'render', 'rotateSlides', 'togglePlayPause', 'play', 'pause', 'initialPlay', 'transition', 'jumpTo'); | |
}, | |
render: function() { | |
var self = this; | |
this.collection.each(function(slide, i) { | |
$(self.slides).append(self.slideTemplate(slide.toJSON())); | |
$(self.controls).append(self.controlTemplate({ index: i, human_readable_index: ++i })); | |
}); | |
this.initialPlay(); | |
return this; | |
}, | |
rotateSlides: function() { | |
var current = this.currentIndex; | |
var next = this.currentIndex === (this.collection.length - 1) ? 0 : this.currentIndex + 1; | |
this.transition(current, next); | |
}, | |
transition: function(from, to) { | |
var current = this.collection.at(from); | |
var next = this.collection.at(to); | |
current.getEl().fadeOut('slow', function() { | |
next.getEl().fadeIn('slow'); | |
}); | |
current.getControl().toggleClass('current'); | |
next.getControl().toggleClass('current'); | |
this.currentIndex = to; | |
}, | |
toggleVisibility: function() { | |
var slides = $(this.slides); | |
slides.toggle(); | |
$(this.el).toggleClass('collapsed'); | |
if(slides.is(":visible")) { | |
this.play(); | |
} else { | |
this.pause(); | |
} | |
}, | |
togglePlayPause: function() { | |
if(this.isPlaying()) { | |
this.pause(); | |
} else { | |
this.play(); | |
} | |
}, | |
initialPlay: function() { | |
this.collection.at(0).show(); | |
this.collection.at(0).getControl().toggleClass('current'); | |
this.play(); | |
}, | |
pause: function() { | |
if(this.isPaused()) { return; } | |
this.state = 'paused'; | |
clearInterval(this.intervalID); | |
$(this.playPauseControl).toggleClass('playing', false); | |
}, | |
play: function() { | |
if(this.isPlaying()) { return; } | |
this.state = 'playing'; | |
this.intervalID = setInterval(this.rotateSlides, this.delay); | |
$(this.playPauseControl).toggleClass('playing', true); | |
}, | |
jumpTo: function(e) { | |
var next = $(e.currentTarget).data('index'); | |
this.pause(); | |
this.transition(this.currentIndex, next); | |
}, | |
isPlaying: function() { | |
return this.state === 'playing'; | |
}, | |
isPaused: function() { | |
return !this.isPlaying(); | |
} | |
}); |
APP.Collections.Slides = Backbone.Collection.extend({ model: APP.Models.Slide }); |
#slideshow { | |
height: 150px; | |
border: 1px solid #ccc; | |
border-top: none; | |
position: relative; | |
overflow: hidden; | |
} | |
#slideshow.collapsed { | |
height: 20px; | |
border: none; | |
} | |
.slides { | |
overflow: hidden; | |
} | |
.slide { | |
height: 150px; | |
position: relative; | |
display: none; /** slides are hidden by default **/ | |
} | |
.slide.right { | |
background-position: right !important; | |
} | |
.slide.left { | |
background-position: left !important; | |
} | |
.slide .headline { | |
font-size: xx-large; | |
font-weight: bold; | |
} | |
.slide .caption { | |
color: #999; | |
} | |
.slide .headline, | |
.slide .caption { | |
position: absolute; | |
} | |
.slide.right .headline { | |
top: 30px; | |
left: 30px; | |
} | |
.slide.right .caption { | |
left: 30px; | |
top: 70px; | |
} | |
.slide.left .headline { | |
top: 30px; | |
right: 30px; | |
} | |
.slide.left .caption { | |
top: 70px; | |
right: 30px; | |
} | |
#slideshow .toggler { | |
display: block; | |
height: 19px; | |
width: 19px; | |
background-color: white; | |
background-position: 0 -493px; | |
border: 1px solid #CCCCCC; | |
bottom: -1px; | |
color: black; | |
position: absolute; | |
right: -1px; | |
} | |
#slideshow.collapsed .toggler { | |
right: 0; | |
bottom: 0; | |
background-position: 0 -438px; | |
} | |
#slideshow .controls { | |
position: absolute; | |
bottom: -1px; | |
left: -1px; | |
} | |
#slideshow.collapsed .controls { | |
display: none; | |
} | |
#slideshow .toggler:hover, | |
.slide-control:hover, | |
.jump-to.current { | |
cursor: pointer; | |
background-color: #333; | |
color: white; | |
} | |
.toggle-play-pause { | |
background-position: 0 -527px; | |
} | |
.toggle-play-pause.playing { | |
background-position: 0 -509px; | |
} | |
.slide-control { | |
display: inline-block; | |
width: 20px; | |
height: 17px; | |
padding-top: 3px; | |
text-align: center; | |
margin-right: 2px; | |
border: 1px solid #ccc; | |
background-color: white; | |
} |
APP.Models.Slide = Backbone.Model.extend({ | |
defaults: { | |
id: 1, | |
headline: 'Welcome to APP', | |
caption: 'This is an awesome slide', | |
layout: 'right' | |
}, | |
show: function() { | |
this.getEl().show(); | |
}, | |
getEl: function() { | |
return $('#slide-' + this.id); | |
}, | |
getControl: function() { | |
return $('.jump-to').eq(this.id - 1); | |
} | |
}); |
describe('Slideshow View', function() { | |
var view, models, slideshow, slides, controls, events; | |
beforeEach(function() { | |
spyOn(window, 'setInterval'); | |
spyOn(window, 'clearInterval'); | |
slideshow = $.jasmine.inject('<div id="slideshow"><ul class="slides"></ul><ul class="controls"><li class="slide-control toggle-play-pause"></li></ul></div>'); | |
slides = slideshow.find('.slides'); | |
controls = slideshow.find('.controls'); | |
models = [ | |
new APP.Models.Slide({id:1}), | |
new APP.Models.Slide({id:2}), | |
new APP.Models.Slide({id:3}) | |
]; | |
view = new views.Slideshow({collection: new APP.Collections.Slides(models)}); | |
spyOn(view, 'initialPlay'); | |
}); | |
describe('events', function() { | |
it('defines these default events', function() { | |
events = { | |
'click .toggler' : 'toggleVisibility', | |
'click .toggle-play-pause' : 'togglePlayPause', | |
'click .jump-to' : 'jumpTo' | |
}; | |
expect(view.events).toEqual(events); | |
}); | |
}); | |
describe('#render', function() { | |
beforeEach(function() { | |
view.render(); | |
}); | |
it('renders 3 slides out to the .slides element', function() { | |
expect(slides).toContain('li.slide'); | |
expect(slides.find('.slide').length).toBe(3); | |
}); | |
it('renders .jump-to controls for the slides to the .controls element', function() { | |
expect(controls).toContain('li.jump-to'); | |
expect(controls.find('.jump-to').length).toBe(3); | |
}); | |
it('shows the first slide', function() { | |
expect($('.slide:first')).toBeVisible(); | |
}); | |
it('initializes play', function() { | |
expect(view.initialPlay).toHaveBeenCalled(); | |
}); | |
}); | |
describe('#rotateSlides', function() { | |
beforeEach(function() { | |
spyOn(view, 'transition'); | |
}); | |
context('current slide is not at the end', function() { | |
it('should transition from the current slide to the next slide', function() { | |
view.rotateSlides(); | |
expect(view.transition).toHaveBeenCalledWith(0, 1); | |
}); | |
}); | |
context('current slide is at the end', function() { | |
it('should show the first slide and hide the last slide', function() { | |
view.currentIndex = 2; | |
view.rotateSlides(); | |
expect(view.transition).toHaveBeenCalledWith(2, 0); | |
}); | |
}); | |
}); | |
describe('#transition', function() { | |
beforeEach(function() { | |
spyOn($.fn, 'fadeOut'); | |
$.jasmine.inject( | |
'<li class="slide-control jump-to current" data-index="0">1</li>' + | |
'<li class="slide-control jump-to" data-index="1">2</li>' + | |
'<li class="slide-control jump-to" data-index="2">3</li>' | |
); | |
$.jasmine.inject( | |
'<li id="slide-1">1</li>' + | |
'<li id="slide-2">2</li>' + | |
'<li id="slide-3">3</li>' | |
); | |
}); | |
it('fades out the current slide', function() { | |
view.transition(0, 1); | |
expect($.fn.fadeOut).toHaveBeenInvokedOnSelector('#slide-1'); | |
}); | |
it('toggles the "current" class on the controls representing the current and next slides', function() { | |
view.transition(0, 1); | |
expect(view.collection.at(0).getControl()).not.toHaveClass('current'); | |
expect(view.collection.at(1).getControl()).toHaveClass('current'); | |
}); | |
it('sets the currentIndex to the value of "to"', function() { | |
view.transition(0, 1); | |
expect(view.currentIndex).toBe(1); | |
}); | |
}); | |
describe('#toggleVisibility', function() { | |
context('visible slideshow', function() { | |
it('should hide .slides and set the class collapsed on #slideshow', function() { | |
view.toggleVisibility(); | |
expect(slides).toBeHidden(); | |
expect(slideshow).toHaveClass('collapsed'); | |
}); | |
}); | |
context('hidden slideshow', function() { | |
it('should show .slides and remove the class collapsed from #slideshow', function() { | |
view.toggleVisibility(); | |
view.toggleVisibility(); | |
expect(slides).toBeVisible(); | |
expect(slideshow).not.toHaveClass('collapsed'); | |
}); | |
}); | |
}); | |
describe('#play', function() { | |
beforeEach(function() { | |
view.play(); | |
}); | |
it('sets the state of the view to "playing"', function() { | |
expect(view.state).toBe('playing'); | |
}); | |
it('sets the views intervalID via setInterval', function() { | |
expect(view.intervalID).not.toBeNull(); | |
expect(window.setInterval).toHaveBeenCalledWith(view.rotateSlides, view.delay); | |
}); | |
}); | |
describe('#pause', function() { | |
beforeEach(function() { | |
view.state = 'playing'; | |
view.pause(); | |
}); | |
it('sets the state of the view to "paused"', function() { | |
expect(view.state).toBe('paused'); | |
}); | |
it('clears the interval to stop rotating', function() { | |
expect(window.clearInterval).toHaveBeenCalledWith(view.intervalID); | |
}); | |
}); | |
describe('#jumpTo', function() { | |
var eventMock; | |
beforeEach(function() { | |
spyOn(view, 'pause'); | |
spyOn(view, 'transition'); | |
eventMock = { | |
currentTarget: $.jasmine.inject('<li class="slide-control jump-to current" data-index="3">4</li>') | |
}; | |
view.jumpTo(eventMock); | |
}); | |
it('pauses play of the slideshow', function() { | |
expect(view.pause).toHaveBeenCalled(); | |
}); | |
it('initiates a transition between the current slide and the one passed in via event', function() { | |
expect(view.transition).toHaveBeenCalledWith(view.currentIndex, 3); | |
}); | |
}); | |
}); |