Created
May 15, 2013 01:30
-
-
Save rawcreative/5581031 to your computer and use it in GitHub Desktop.
Backbone Image Slideshow w/ Thumbnails
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
var RawGallery = window.RawGallery || {}; | |
(function($, window, undefined) { | |
var RG = RawGallery; | |
_.templateSettings = { | |
interpolate: /\{\{(.+?)\}\}/g, | |
escape: /\{\{-(.*?)\}\}/g | |
}; | |
_.extend(RG, { | |
models: {}, | |
views: {}, | |
collections: {}, | |
controllers: {}, | |
}); | |
RG.defaults = { | |
thumbnails: true, | |
thumbStyle: '', | |
slideshowSpeed: 5000, | |
animationSpeed: 500, | |
effect: 'fade', | |
easing: 'swing', | |
autoplay: true, | |
startAt: 0, | |
initDelay: 0, | |
}; | |
RG.vent = {}; | |
_.extend(RG.vent, Backbone.Events); | |
/* Models | |
=================================================================== */ | |
RG.models.Image = Backbone.Model.extend({ | |
defaults: { | |
src: '', | |
id: '', | |
caption: '', | |
state: '' | |
}, | |
show: function() { | |
this.getEl().addClass('active'); | |
}, | |
getEl: function() { | |
return $('#item-' + this.id ); | |
}, | |
getControl: function() { | |
return $('.rg-thumbnails a').eq(this.id); | |
} | |
}); | |
/* Controllers | |
=================================================================== */ | |
RG.controllers.Transition = Backbone.Model.extend({ }); | |
RG.controllers.Stage = Backbone.Model.extend({ }); | |
RG.controllers.Thumbnails = Backbone.Model.extend({ }); | |
RG.controllers.Gallery = Backbone.Model.extend({ | |
defaults: { | |
currentIndex: 0 | |
}, | |
initialize: function(options, args) { | |
this.imageCollection = options.collection; | |
}, | |
setIndex: function(currentIndex) { | |
if(!this.imageCollection.length) return; | |
if(currentIndex < 0) { | |
currentIndex = this.imageCollection.length - 1; | |
} else if(currentIndex >= this.imageCollection.length) { | |
currentIndex = 0; | |
} | |
this.set('currentImage', currentIndex); | |
}, | |
setPrev: function() { | |
this.setIndex(this.get('currentImage') - 1); | |
}, | |
setNext: function() { | |
this.setIndex(this.get('currentImage') + 1); | |
}, | |
play: function() {}, | |
pause: function() {}, | |
ready: function() {}, | |
reset: function() {} | |
}); | |
/* Collections | |
=================================================================== */ | |
RG.collections.Images = Backbone.Collection.extend({ | |
model: RG.models.Image | |
}); | |
/* Views | |
=================================================================== */ | |
// A subview manager. | |
RG.Views = function( view, views ) { | |
this.view = view; | |
this._views = _.isArray( views ) ? { '': views } : views || {}; | |
}; | |
RG.Views.extend = Backbone.Model.extend; | |
_.extend( RG.Views.prototype, { | |
// ### Fetch all of the subviews | |
// | |
// Returns an array of all subviews. | |
all: function() { | |
return _.flatten( this._views ); | |
}, | |
// ### Get a selector's subviews | |
// | |
// Fetches all subviews that match a given `selector`. | |
// | |
// If no `selector` is provided, it will grab all subviews attached | |
// to the view's root. | |
get: function( selector ) { | |
selector = selector || ''; | |
return this._views[ selector ]; | |
}, | |
// ### Get a selector's first subview | |
// | |
// Fetches the first subview that matches a given `selector`. | |
// | |
// If no `selector` is provided, it will grab the first subview | |
// attached to the view's root. | |
// | |
// Useful when a selector only has one subview at a time. | |
first: function( selector ) { | |
var views = this.get( selector ); | |
return views && views.length ? views[0] : null; | |
}, | |
// ### Register subview(s) | |
// | |
// Registers any number of `views` to a `selector`. | |
// | |
// When no `selector` is provided, the root selector (the empty string) | |
// is used. `views` accepts a `Backbone.View` instance or an array of | |
// `Backbone.View` instances. | |
// | |
// --- | |
// | |
// Accepts an `options` object, which has a significant effect on the | |
// resulting behavior. | |
// | |
// `options.silent` – *boolean, `false`* | |
// > If `options.silent` is true, no DOM modifications will be made. | |
// | |
// `options.add` – *boolean, `false`* | |
// > Use `Views.add()` as a shortcut for setting `options.add` to true. | |
// | |
// > By default, the provided `views` will replace | |
// any existing views associated with the selector. If `options.add` | |
// is true, the provided `views` will be added to the existing views. | |
// | |
// `options.at` – *integer, `undefined`* | |
// > When adding, to insert `views` at a specific index, use | |
// `options.at`. By default, `views` are added to the end of the array. | |
set: function( selector, views, options ) { | |
var existing, next; | |
if ( ! _.isString( selector ) ) { | |
options = views; | |
views = selector; | |
selector = ''; | |
} | |
options = options || {}; | |
views = _.isArray( views ) ? views : [ views ]; | |
existing = this.get( selector ); | |
next = views; | |
if ( existing ) { | |
if ( options.add ) { | |
if ( _.isUndefined( options.at ) ) { | |
next = existing.concat( views ); | |
} else { | |
next = existing; | |
next.splice.apply( next, [ options.at, 0 ].concat( views ) ); | |
} | |
} else { | |
_.each( next, function( view ) { | |
view.__detach = true; | |
}); | |
_.each( existing, function( view ) { | |
if ( view.__detach ) | |
view.$el.detach(); | |
else | |
view.dispose(); | |
}); | |
_.each( next, function( view ) { | |
delete view.__detach; | |
}); | |
} | |
} | |
this._views[ selector ] = next; | |
_.each( views, function( subview ) { | |
var constructor = subview.Views || RG.Views, | |
subviews = subview.views = subview.views || new constructor( subview ); | |
subviews.parent = this.view; | |
subviews.selector = selector; | |
}, this ); | |
if ( ! options.silent ) | |
this._attach( selector, views, _.extend({ ready: this._isReady() }, options ) ); | |
return this; | |
}, | |
// ### Add subview(s) to existing subviews | |
// | |
// An alias to `Views.set()`, which defaults `options.add` to true. | |
// | |
// Adds any number of `views` to a `selector`. | |
// | |
// When no `selector` is provided, the root selector (the empty string) | |
// is used. `views` accepts a `Backbone.View` instance or an array of | |
// `Backbone.View` instances. | |
// | |
// Use `Views.set()` when setting `options.add` to `false`. | |
// | |
// Accepts an `options` object. By default, provided `views` will be | |
// inserted at the end of the array of existing views. To insert | |
// `views` at a specific index, use `options.at`. If `options.silent` | |
// is true, no DOM modifications will be made. | |
// | |
// For more information on the `options` object, see `Views.set()`. | |
add: function( selector, views, options ) { | |
if ( ! _.isString( selector ) ) { | |
options = views; | |
views = selector; | |
selector = ''; | |
} | |
return this.set( selector, views, _.extend({ add: true }, options ) ); | |
}, | |
// ### Stop tracking subviews | |
// | |
// Stops tracking `views` registered to a `selector`. If no `views` are | |
// set, then all of the `selector`'s subviews will be unregistered and | |
// disposed. | |
// | |
// Accepts an `options` object. If `options.silent` is set, `dispose` | |
// will *not* be triggered on the unregistered views. | |
unset: function( selector, views, options ) { | |
var existing; | |
if ( ! _.isString( selector ) ) { | |
options = views; | |
views = selector; | |
selector = ''; | |
} | |
views = views || []; | |
if ( existing = this.get( selector ) ) { | |
views = _.isArray( views ) ? views : [ views ]; | |
this._views[ selector ] = views.length ? _.difference( existing, views ) : []; | |
} | |
if ( ! options || ! options.silent ) | |
_.invoke( views, 'dispose' ); | |
return this; | |
}, | |
// ### Detach all subviews | |
// | |
// Detaches all subviews from the DOM. | |
// | |
// Helps to preserve all subview events when re-rendering the master | |
// view. Used in conjunction with `Views.render()`. | |
detach: function() { | |
$( _.pluck( this.all(), 'el' ) ).detach(); | |
return this; | |
}, | |
// ### Render all subviews | |
// | |
// Renders all subviews. Used in conjunction with `Views.detach()`. | |
render: function() { | |
var options = { | |
ready: this._isReady() | |
}; | |
_.each( this._views, function( views, selector ) { | |
this._attach( selector, views, options ); | |
}, this ); | |
this.rendered = true; | |
return this; | |
}, | |
// ### Dispose all subviews | |
// | |
// Triggers the `dispose()` method on all subviews. Detaches the master | |
// view from its parent. Resets the internals of the views manager. | |
// | |
// Accepts an `options` object. If `options.silent` is set, `unset` | |
// will *not* be triggered on the master view's parent. | |
dispose: function( options ) { | |
if ( ! options || ! options.silent ) { | |
if ( this.parent && this.parent.views ) | |
this.parent.views.unset( this.selector, this.view, { silent: true }); | |
delete this.parent; | |
delete this.selector; | |
} | |
_.invoke( this.all(), 'dispose' ); | |
this._views = []; | |
return this; | |
}, | |
// ### Replace a selector's subviews | |
// | |
// By default, sets the `$target` selector's html to the subview `els`. | |
// | |
// Can be overridden in subclasses. | |
replace: function( $target, els ) { | |
$target.html( els ); | |
return this; | |
}, | |
// ### Insert subviews into a selector | |
// | |
// By default, appends the subview `els` to the end of the `$target` | |
// selector. If `options.at` is set, inserts the subview `els` at the | |
// provided index. | |
// | |
// Can be overridden in subclasses. | |
insert: function( $target, els, options ) { | |
var at = options && options.at, | |
$children; | |
if ( _.isNumber( at ) && ($children = $target.children()).length > at ) | |
$children.eq( at ).before( els ); | |
else | |
$target.append( els ); | |
return this; | |
}, | |
// ### Trigger the ready event | |
// | |
// **Only use this method if you know what you're doing.** | |
// For performance reasons, this method does not check if the view is | |
// actually attached to the DOM. It's taking your word for it. | |
// | |
// Fires the ready event on the current view and all attached subviews. | |
ready: function() { | |
this.view.trigger('ready'); | |
// Find all attached subviews, and call ready on them. | |
_.chain( this.all() ).map( function( view ) { | |
return view.views; | |
}).flatten().where({ attached: true }).invoke('ready'); | |
}, | |
// #### Internal. Attaches a series of views to a selector. | |
// | |
// Checks to see if a matching selector exists, renders the views, | |
// performs the proper DOM operation, and then checks if the view is | |
// attached to the document. | |
_attach: function( selector, views, options ) { | |
var $selector = selector ? this.view.$( selector ) : this.view.$el, | |
managers; | |
// Check if we found a location to attach the views. | |
if ( ! $selector.length ) | |
return this; | |
managers = _.chain( views ).pluck('views').flatten().value(); | |
// Render the views if necessary. | |
_.each( managers, function( manager ) { | |
if ( manager.rendered ) | |
return; | |
manager.view.render(); | |
manager.rendered = true; | |
}, this ); | |
// Insert or replace the views. | |
this[ options.add ? 'insert' : 'replace' ]( $selector, _.pluck( views, 'el' ), options ); | |
// Set attached and trigger ready if the current view is already | |
// attached to the DOM. | |
_.each( managers, function( manager ) { | |
manager.attached = true; | |
if ( options.ready ) | |
manager.ready(); | |
}, this ); | |
return this; | |
}, | |
// #### Internal. Checks if the current view is in the DOM. | |
_isReady: function() { | |
var node = this.view.el; | |
while ( node ) { | |
if ( node === document.body ) | |
return true; | |
node = node.parentNode; | |
} | |
return false; | |
} | |
}); | |
RG.View = Backbone.View.extend({ | |
// The constructor for the `Views` manager. | |
Views: RG.Views, | |
constructor: function( options ) { | |
this.views = new this.Views( this, this.views ); | |
this.on( 'ready', this.ready, this ); | |
if ( options && options.controller ) | |
this.controller = options.controller; | |
Backbone.View.apply( this, arguments ); | |
}, | |
dispose: function() { | |
// Undelegating events, removing events from the model, and | |
// removing events from the controller mirror the code for | |
// `Backbone.View.dispose` in Backbone master. | |
this.undelegateEvents(); | |
if ( this.model && this.model.off ) | |
this.model.off( null, null, this ); | |
if ( this.collection && this.collection.off ) | |
this.collection.off( null, null, this ); | |
// Unbind controller events. | |
if ( this.controller && this.controller.off ) | |
this.controller.off( null, null, this ); | |
// Recursively dispose child views. | |
if ( this.views ) | |
this.views.dispose(); | |
return this; | |
}, | |
remove: function() { | |
this.dispose(); | |
return Backbone.View.prototype.remove.apply( this, arguments ); | |
}, | |
render: function() { | |
var options; | |
if ( this.prepare ) | |
options = this.prepare(); | |
this.views.detach(); | |
if ( this.template ) { | |
options = options || {}; | |
this.trigger( 'prepare', options ); | |
this.$el.html( this.template( options ) ); | |
} | |
this.views.render(); | |
return this; | |
}, | |
prepare: function() { | |
return this.options; | |
}, | |
ready: function() {} | |
}); | |
RG.views.Stage = RG.View.extend({ | |
tagName: 'div', | |
className: 'rg-stage', | |
events: { | |
'click .rg-prev' : 'prev', | |
'click .rg-next' : 'next' | |
}, | |
itemTemplate: _.template('<div id="item-{{ id }}" class="item"><img src="{{ src }}"></div>'), | |
controlTemplate : _.template('<div class="rg-overlay"><a class="rg-prev"></a><a class="rg-next"></a></div>'), | |
initialize: function() { | |
RG.vent.on('start', this.start); | |
RG.vent.on('goto', this.goTo); | |
RG.vent.on('transition', this.transition); | |
_.bindAll(this, 'render', 'goTo', 'prev', 'next'); | |
}, | |
render: function() { | |
var self = this; | |
if(this.options.stageControls) { | |
self.$el.html(self.controlTemplate(slide.toJSON())); | |
} | |
this.collection.each(function(slide, i) { | |
$(self.el).append(self.itemTemplate(slide.toJSON())); | |
}); | |
return this; | |
}, | |
start: function(index) { | |
console.log(index); | |
}, | |
goTo: function(e) { | |
}, | |
prev: function(e) { | |
RG.vent.trigger('prev', args); | |
}, | |
next: function(e) { | |
RG.vent.trigger('next', args); | |
}, | |
transition: function(from, to) { | |
RG.animating = true; | |
from.getEl().fadeOut(500, function() { | |
to.getEl().fadeIn(500, function() { RG.animating = false; }); | |
}); | |
from.getControl().toggleClass('active'); | |
to.getControl().toggleClass('active'); | |
}, | |
}); | |
RG.views.Thumbnails = Backbone.View.extend({ | |
className: 'rg-thumbnail-wrap', | |
tagName: 'div', | |
events: { | |
'click a' : 'goTo' | |
}, | |
template: _.template('<a data-index="{{ id }}"><img src="{{ src }}" index="{{ id }}"></a> '), | |
initialize: function(args) { | |
_.bindAll(this, 'render', 'goTo'); | |
}, | |
render: function() { | |
var self = this; | |
var thumbs = $('<div class="rg-thumbnails"></div>'); | |
this.collection.each(function(img) { | |
thumbs.append( self.template( img.toJSON() ) ); | |
}); | |
$(self.el).html(thumbs); | |
return this; | |
}, | |
goTo: function(e) { | |
var index = $(e.currentTarget).data('index'); | |
if(!RG.animating) { | |
RG.vent.trigger('goto', index); | |
this.$el.find('.active').toggleClass('active'); | |
$(e.currentTarget).toggleClass('active'); | |
} | |
} | |
}); | |
RG.views.Controls = RG.View.extend({ | |
tagName: 'div', | |
className: 'rg-controls', | |
events: {}, | |
template: _.template('<div class="rg-play rg-btn">{{ playIcon }}</div><div class="rg-prev rg-btn">{{ prevIcon }}</div><div class="rg-next rg-btn">{{ nextIcon }}</div>'), | |
controlDefaults: { | |
playIcon : null, | |
prevIcon : null, | |
nextIcon : null, | |
thumbMarkup : null | |
}, | |
initialize: function(args) { | |
var options = this.options = args.options; | |
_.extend(options.controls, this.controlDefaults); | |
this.thumbs = options.thumbnails ? new RG.views.Thumbnails({ collection: this.collection, options: options}) : null; | |
}, | |
render: function() { | |
var self = this; | |
if(self.thumbs !== null) { | |
self.options.controls.thumbMarkup = self.thumbs.render().el; | |
} | |
$(self.el).html( self.template( self.options.controls ) ); | |
$(self.el).find('.rg-prev').after(self.options.controls.thumbMarkup); | |
return this; | |
} | |
}); | |
RG.views.Gallery = RG.View.extend({ | |
el: '.raw-gallery', | |
playPauseControl: '#gallery .controls .toggle-play-pause', | |
delay: 5000, | |
currentIndex: 0, | |
events: {}, | |
initialize: function() { | |
var defaults = {}; | |
this.renderedViews = {}; | |
// this.model = new RG.controllers.Gallery({ collection: this.collection }, null); | |
this.stage = new RG.views.Stage({ collection: this.collection, options: this.options.options }); | |
this.controls = new RG.views.Controls({ collection: this.collection, options: this.options.options }); | |
RG.vent.on('goto', this.jumpTo, this); | |
_.bindAll(this, 'render', 'cycle', 'togglePlayPause', 'play', 'pause', 'start', 'transition', 'jumpTo'); | |
}, | |
render: function() { | |
var self = this; | |
this.$el.append( this.stage.render().el ); | |
this.$el.append( this.controls.render().el ); | |
//this.initialPlay(); | |
return this; | |
}, | |
cycle: 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); | |
this.currentIndex = to; | |
RG.vent.trigger('transition', current, next); | |
}, | |
togglePlayPause: function() { | |
if (this.isPlaying()) { | |
this.pause(); | |
} else { | |
this.play(); | |
} | |
}, | |
start: function() { | |
RG.vent('start', this.collection.at(0) ) | |
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.cycle, this.delay); | |
$(this.playPauseControl).toggleClass('playing', true); | |
}, | |
jumpTo: function(index) { | |
this.pause(); | |
this.transition(this.currentIndex, index); | |
}, | |
isPlaying: function() { | |
return this.state === 'playing'; | |
}, | |
isPaused: function() { | |
return !this.isPlaying(); | |
} | |
}); | |
})(jQuery); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment