photo_folder.js | |
|---|---|
|
PhotoFolder is an MIT licensed hackable photo gallery system built using jQuery View and jQuery Routes. To create your own gallery:
| PhotoFolder = (function(){
var PhotoFolder = {
routes: {
'/': 'PhotoFolder.LayoutView#displayFirstPhoto',
'/*': 'PhotoFolder.LayoutView#displayPhoto'
}, |
| Photo objects | photos: [], |
| An index of the paths is kept for quick lookups of the photos array | photoPaths: [], |
| Album objects | albums: [],
albumNames: [],
lastPositionInAlbum: 0, //used during data load
load: function(config,callback){
PhotoFolder.config = $.extend({
initialPath: '/',
jsonLocation: 'photos.json',
fadeLength: 333,
fadeOutDelay: 50, //makes for a smoother looking transition
bindKeyboardEvents: true,
loadingTimeout: 1.25,
useFilenamesAsTitles: false,
setWindowTitle: true
},config || {});
$(document).ready(function(){
$.getJSON(PhotoFolder.config.jsonLocation,function(photos){
for(var i = 0; i < photos.length; ++i){
new PhotoFolder.Photo(photos[i],i);
}
$.routes(PhotoFolder.routes);
callback(PhotoFolder.LayoutView.instance());
if(!$.routes("get") || $.routes("get") == ""){
$.routes("set",PhotoFolder.config.initialPath);
}
});
});
}
}; |
| The Photo class | PhotoFolder.Photo = function(src,position){
this.src = src;
this.position = position;
var path_components = src.split('/');
var file_name = path_components.pop();
var album_name = path_components.pop();
if($.inArray(album_name,PhotoFolder.albumNames) == -1){
PhotoFolder.lastPositionInAlbum = 0;
this.album = new PhotoFolder.Album(src);
PhotoFolder.albums.push(this.album);
PhotoFolder.albumNames.push(album_name);
}else{
this.album = PhotoFolder.albums[$.inArray(album_name,PhotoFolder.albumNames)];
}
if(PhotoFolder.config.useFilenamesAsTitles){ |
| un camel case | this.name = file_name.replace(/([a-z])([A-Z])/g,'$1 $2'); |
| remove file extension | this.name = this.name.replace(/\.(jpg|png)$/,''); |
| replace 01-IMG with IMG | this.name = this.name.replace(/(\/|^)[\d]{1,4}\-/,'$1'); |
| replace untitled images with album name + # | if(this.name.toLowerCase().match(/(^(dcim|img)_|^[\d]+\.(png|jpg)$)/i)){
this.name = album_name + ' #' + (PhotoFolder.lastPositionInAlbum + 1);
} |
| replace dash and underscore | this.name = this.name.replace(/[\-\_]+/g,' ');
}else{
this.name = this.album.name + ' #' + (PhotoFolder.lastPositionInAlbum + 1);
}
this.path = this.album.path + '/' + (PhotoFolder.lastPositionInAlbum + 1);
PhotoFolder.photoPaths.push(this.path);
PhotoFolder.photos.push(this);
++PhotoFolder.lastPositionInAlbum;
};
PhotoFolder.Photo.preloadedImages = {};
PhotoFolder.Photo.prototype.preload = function(callback){
if(this.src in PhotoFolder.Photo.preloadedImages){
if(callback){
callback();
}
}else{
var image = new Image();
$(image).load($.proxy(function(){
this.height = image.height;
this.width = image.width;
PhotoFolder.Photo.preloadedImages[this.src] = true;
if(callback){
callback();
}
},this));
image.src = this.src;
}
};
PhotoFolder.Album = function(src){
var path_components = src.split('/');
path_components.pop();
this.name = path_components[path_components.length - 1]; |
| un camel case | this.name = this.name.replace(/([a-z])([A-Z])/g,'$1 $2'); |
| replace dash and underscore | this.name = this.name.replace(/[\-\_]+/g,' ');
this.parentName = path_components[path_components.length - 2];
this.depth = path_components.length - 1;
path_components.shift();
this.path = '/' + path_components.join('/');
};
PhotoFolder.LayoutView = $.view(function(){
this.queuedAction = false;
this.lastAlbum = false;
this.ready(this.resizeLinkElementsToFill);
$(window).resize(this.resizeLinkElementsToFill);
return this.div({className:'photo_folder'},
PhotoFolder.AlbumListView.instance(),
PhotoFolder.LoadingIndicatorView.instance(),
this.nextLink = this.a({
href: '#',
title: 'Next (Use Right Arrow Key)',
className:'next'
}),
this.previousLink = this.a({
href: '#',
title: 'Previous (Use Left Arrow Key)',
className:'previous'
}),
this.photoElement = this.div()
);
},{ |
| called from routing / link clicks | displayFirstPhoto: function(){
this.displayPhotoAtIndex(0);
}, |
| called from routing / link clicks | displayPhoto: function(params){
var path = ('/' + params.path).replace(/\/\//g,'/');
var index = $.inArray(path,PhotoFolder.photoPaths);
this.displayPhotoAtIndex(index);
}, |
| called from keyboard events | displayNextPhoto: function(){
this.displayPhoto({
path: $(this.nextLink).attr('href').replace(/#/,'')
});
}, |
| called from keyboard events | displayPreviousPhoto: function(){
this.displayPhoto({
path: $(this.previousLink).attr('href').replace(/#/,'')
});
}, |
| sets the current, next and previous indicies, and writes correct href attributes of navigation links | setCurrentIndex: function(index){
var next_index = this.getIndexAfter(index);
var previous_index = this.getIndexBefore(index);
$(this.nextLink).attr('href','#' + PhotoFolder.photoPaths[next_index]);
$(this.previousLink).attr('href','#' + PhotoFolder.photoPaths[previous_index]);
PhotoFolder.photos[index].preload();
PhotoFolder.photos[next_index].preload();
PhotoFolder.photos[previous_index].preload();
PhotoFolder.photos[this.getIndexAfter(next_index)].preload();
PhotoFolder.photos[this.getIndexBefore(previous_index)].preload();
},
getIndexBefore: function(index){
return index - 1 < 0 ? PhotoFolder.photoPaths.length - 1 : index - 1;
},
getIndexAfter: function(index){
return index + 1 > PhotoFolder.photoPaths.length - 1 ? 0 : index + 1;
},
displayPhotoAtIndex: function(index){
if(!PhotoFolder.locked){
this.setCurrentIndex(index); |
| tells the loading indicator to display if the next photo isn't ready before the loading timeout length | PhotoFolder.LoadingIndicatorView.instance().waitForTimeout();
PhotoFolder.photos[index].preload($.proxy(function(){
if(this.currentPhotoView){
this.removePhotoView(this.currentPhotoView);
}
this.currentPhotoView = new PhotoFolder.PhotoView({
photo: PhotoFolder.photos[index]
});
this.trigger('change',this.currentPhotoView.get('photo'),this.lastPhotoView
? this.lastPhotoView.get('photo')
: false
);
PhotoFolder.locked = true;
PhotoFolder.LoadingIndicatorView.instance().hide();
$(this.currentPhotoView)
.hide()
.appendTo(this.photoElement)
.fadeIn(PhotoFolder.config.fadeLength,$.proxy(function(){
PhotoFolder.locked = false;
if(this.queuedAction){ |
| timeout prevents a race condition and old photos from not being removed if the user clicks to rapidly | setTimeout($.proxy(function(){
if(this.queuedAction){
this.queuedAction.apply(this,[]);
this.queuedAction = null;
}
},this),50);
}
},this)
);
},this));
}else if(!this.queuedAction){
this.queuedAction = function(){
this.displayPhotoAtIndex(index);
};
}
},
removePhotoView: function(photo_view){
setTimeout($.proxy(function(){
$(photo_view).fadeOut(PhotoFolder.config.fadeLength,$.proxy(function(){
$(photo_view).remove();
},this));
},this),PhotoFolder.config.fadeOutDelay);
},
resizeLinkElementsToFill: function(){
$([this.nextLink,this.previousLink]).height($(this).height());
}
});
PhotoFolder.LayoutView.bind('change',function(view,current_photo,old_photo){
if(PhotoFolder.config.setWindowTitle){
document.title = current_photo.name;
}
});
|
| Once LayoutView is ready bind up/down/left/right keys to navigation events | PhotoFolder.LayoutView.ready(function(){
if(PhotoFolder.config.bindKeyboardEvents){
$(document).keydown(function(event){
switch(event.keyCode){
case 32: //space
break;
case 37: //left
case 38: //up
PhotoFolder.LayoutView.instance().displayPreviousPhoto();
return false;
case 39: //right
case 40: //down
PhotoFolder.LayoutView.instance().displayNextPhoto();
return false;
}
});
}
});
PhotoFolder.LoadingIndicatorView = $.view(function(){
this.timeout = null;
return $(this.div({className:'loading_indicator'},
this.div()
)).fadeOut(1);
},{
waitForTimeout: function(){
if(!this.timeout){
this.timeout = setTimeout(this.show,PhotoFolder.config.loadingTimeout);
}
},
show: function(){
$(this).fadeIn(PhotoFolder.config.fadeLength);
},
hide: function(){
$(this).fadeOut(PhotoFolder.config.fadeLength);
clearTimeout(this.timeout);
this.timeout = null;
}
}); |
| AlbumListView creates a | PhotoFolder.AlbumListView = $.view(function(){
PhotoFolder.LayoutView.bind('change',function(view,photo){
this.activateAlbumByPath(photo.album.path);
},this);
var last_depth = 1;
this.listItemsIndexedByAlbumPath = {};
return this.ul(
this.map(PhotoFolder.albums,function(album){
var first_photo_path = album.path + '/1'; |
| Preload the first photo in each album, but defer it so that the first photo to display loads first | setTimeout(function(){
PhotoFolder.photos[$.inArray(first_photo_path,PhotoFolder.photoPaths)].preload();
},1000);
var nodes = [
(last_depth != album.depth && album.depth > 1) ? this.li(album.parentName) : false,
this.listItemsIndexedByAlbumPath[album.path] = this.li({
className: 'indent_' + album.depth
},
$(this.a({
href: '#' + first_photo_path
},album.name))
)
];
last_depth = album.depth;
return nodes;
})
);
},{
activateAlbumByPath: function(path){
$('li',this).removeClass('active');
$(this.listItemsIndexedByAlbumPath[path]).addClass('active');
}
});
|
| PhotoView contains the img element of a given photo the image will always have been pre loaded when this view is initialized. Multiple PhotoView instances will be on the screen at once when the photos are cross fading. | PhotoFolder.PhotoView = $.view(function(){
this.ready(this.center);
$(window).resize(this.center);
return this.img({
className: 'photo',
src: this.get('photo').src,
height: this.get('photo').height,
width: this.get('photo').width
});
},{ |
| center the image element on screen | center: function(){
$(this).css({
top: + (($(PhotoFolder.LayoutView.instance()).height() - $(this).outerHeight()) / 2) + 'px',
left: + (($(PhotoFolder.LayoutView.instance()).width() - $(this).outerWidth()) / 2) + 'px'
});
}
});
return PhotoFolder;
})();
|