#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); | |
| }); | |
| }); | |
| }); |