').addclass('pbthumbs')
)
);
///////////////////////////////////////////////
// should remove this and use underscore/lodash if possible
function throttle(callback, duration){
var wait = false;
return function(){
if( !wait ){
callback.call();
wait = true;
settimeout(function(){wait = false; }, duration);
}
}
}
///////////////////////////////////////////////
// initialization (on dom ready)
function preparedom( force ){
// do not procceed if already called, unless forced to
if( document.body.contains(overlay[0]) && !force )
return;
nopointerevents && overlay.hide();
$doc.on('touchstart.testmouse', function(){
$doc.off('touchstart.testmouse');
istouchdevice = true;
overlay.addclass('mobile');
});
autoplaybtn.off().on('click', apcontrol.toggle);
// attach a delegated event on the thumbs container
thumbs.off().on('click', 'a', thumbsstripe.click);
// if useragent is ie < 10 (user deserves a slap on the face, but i gotta support them still...)
isoldie && overlay.addclass('msie');
// cancel prorogation up to the overlay container so it won't close
overlay.off().on('click', 'img', function(e){
e.stoppropagation();
});
$(doc.body).append(overlay);
// need this for later:
docelm = doc.documentelement;
}
// @param [list of elements to work on, custom settings, callback after image is loaded]
$.fn.photobox = function(target, settings, callback){
preparedom();
return this.each(function(){
var pb_data = $(this).data('_photobox');
if( pb_data ){ // don't initiate the plugin more than once on the same element
if( target === 'destroy')
pb_data.destroy();
return this;
}
if( typeof target != 'string' )
target = 'a';
if( target === 'preparedom' ){
preparedom( true );
return this;
}
// merge the user settings with the default settings object
settings = $.extend({}, defaults, settings || {});
// create an instance og photobox
photobox = new photobox(settings, this, target);
// saves the insance on the gallery's target element
$(this).data('_photobox', photobox);
// add a callback to the specific gallery
photobox.callback = callback;
});
}
photobox = function(_options, object, target){
this.options = $.extend({}, _options);
this.target = target;
this.selector = $(object || doc);
this.thumbslist = null;
// filter the links which actually has an image as a child
var filtered = this.imagelinksfilter( this.selector.find(target) );
this.imagelinks = filtered[0]; // array of jquery links
this.images = filtered[1]; // 2d array of image url & title
this.init();
};
photobox.prototype = {
init : function(){
// cache dom elements for this instance
this.dom = this.dom();
this.dom.rotatebtn.toggleclass('show', this.options.rotatable);
// if any node was added or removed from the selector of the gallery
this.observertimeout = null;
this.events.binding.call(this);
},
// happens only once
dom : function(){
var dom = {}
dom.scope = overlay;
dom.rotatebtn = dom.scope.find('.rotatebtn');
return dom;
},
//check if dom nodes were added or removed, to re-build the imagelinks and thumbnails
observedom : (function(){
var mutationobserver = win.mutationobserver || win.webkitmutationobserver,
eventlistenersupported = win.addeventlistener;
return function(obj, callback){
if( mutationobserver ){
var that = this,
// define a new observer
obs = new mutationobserver(function(mutations, observer){
if( mutations[0].addednodes.length || mutations[0].removednodes.length )
callback(that);
});
// have the observer observe for changes in children
obs.observe( obj, { childlist:true, subtree:true });
}
else if( eventlistenersupported ){
obj.addeventlistener('domnodeinserted', $.proxy( callback, that ), false);
obj.addeventlistener('domnoderemoved', $.proxy( callback, that ), false);
}
}
})(),
open : function(link){
var startimage = $.inarray(link, this.imagelinks);
// if image link does not exist in the imagelinks array (probably means it's not a valid part of the gallery)
if( startimage == -1 )
return false;
// load the right gallery selector...
options = this.options;
images = this.images;
imagelinks = this.imagelinks;
photobox = this;
this.setup(1);
overlay.on(transitionend, function(){
overlay.off(transitionend).addclass('on'); // class 'on' is set when the initial fade-in of the overlay is done
changeimage(startimage, true);
}).addclass('show');
if( isoldie )
overlay.trigger('mstransitionend');
return false;
},
imagelinksfilter : function(linksobj){
var that = this,
images = [],
caption = {},
captionlink;
function linksobjfiler(i){
// search for the thumb inside the link, if not found then see if there's a 'that.settings.thumb' pointer to the thumbnail
var link = $(this),
thumbimg,
thumbsrc = '';
caption.content = link[0].getattribute('title') || '';
if( that.options.thumb )
thumbimg = link.find(that.options.thumb)[0];
// try a direct child lookup
if( !that.options.thumb || !thumbimg )
thumbimg = link.find('img')[0];
// if no img child found in the link
if( thumbimg ){
captionlink = thumbimg.getattribute('data-pb-captionlink');
thumbsrc = thumbimg.getattribute(that.options.thumbattr) || thumbimg.getattribute('src');
caption.content = ( thumbimg.getattribute('alt') || thumbimg.getattribute('title') || '');
}
// if there is a caption link to be added:
if( captionlink ){
captionlink = captionlink.split('[');
// parse complex links: text[www.site.com]
if( captionlink.length == 2 ){
caption.linktext = captionlink[0];
caption.linkhref = captionlink[1].slice(0,-1);
}
else{
caption.linktext = captionlink;
caption.linkhref = captionlink;
}
caption.content += '
' + caption.linktext + '';
}
images.push( [link[0].href, caption.content, thumbsrc] );
return true;
}
return [linksobj.filter(linksobjfiler), images];
},
// things that should happen every time the gallery opens or closes (some messed up code below..)
setup : function (open){
var fn = open ? "on" : "off";
// thumbs stuff
if( options.thumbs ){
if( !istouchdevice ){
thumbs[fn]('mouseenter.photobox', thumbsstripe.calc)
[fn]('mousemove.photobox', thumbsstripe.move);
}
}
if( open ){
image.css({'transition':'0s'}).removeattr('style'); // reset any transition that might be on the element (yes it's ugly)
overlay.show();
// clean up if another gallery was viewed before, which had a thumbslist
thumbs
.html( this.thumbslist )
.trigger('mouseenter.photobox');
if( options.thumbs ){
overlay.addclass('thumbs');
}
else{
thumbstoggler.prop('checked', false);
overlay.removeclass('thumbs');
}
// things to hide if there are less than 2 images
if( this.images.length < 2 || options.single )
overlay.removeclass('thumbs hasarrows hascounter hasautoplay');
else{
overlay.addclass('hasarrows hascounter')
// check is the autoplay button should be visible (per gallery) and if so, should it autoplay or not.
if( options.time > 1000 ){
overlay.addclass('hasautoplay');
if( options.autoplay )
apcontrol.progress.start();
else
apcontrol.pause();
}
else
overlay.removeclass('hasautoplay');
}
options.hideflash && $('iframe, object, embed').css('visibility', 'hidden');
} else {
$win.off('resize.photobox');
}
$doc.off("keydown.photobox")[fn]({ "keydown.photobox": keydown });
if( istouchdevice ){
overlay.removeclass('hasarrows'); // no need for arrows on touch-enabled
wrapper[fn]('swipe', onswipe);
}
if( options.zoomable ){
overlay[fn]({"mousewheel.photobox": $.proxy(this.events.callbacks.onscrollzoom, this) });
if( !isoldie) thumbs[fn]({"mousewheel.photobox": thumbsresize });
}
if( !options.single && options.wheelnextprev ){
overlay[fn]({"mousewheel.photobox": throttle(wheelnextprev,1000) });
}
},
destroy : function(){
options = this.options;
this.selector
.off('click.photobox', this.target)
.removedata('_photobox');
close();
},
events : {
binding : function(){
var that = this;
// only generates the thumbstripe once, and listen for any dom changes on the selector element, if so, re-generate
// this is done on "mouseenter" so images will not get called unless it's liekly that they would be needed
this.selector.one('mouseenter.photobox', this.target, function(e){
that.thumbslist = thumbsstripe.generate.apply(that);
});
this.selector.on('click.photobox', this.target, function(e){
e.preventdefault();
that.open(this);
});
if( !isoldie && this.selector[0].nodetype == 1 ) // observe normal nodes
this.observedom( this.selector[0], $.proxy( this.events.callbacks.ondomchanges, this ));
this.dom.rotatebtn.on('click', this.events.callbacks.onrotatebtnclick);
},
callbacks : {
ondomchanges : function(){
var that = this;
// use a timeout to prevent more than one dom change event firing at once, and also to overcome the fact that ie's domnoderemoved is fired before elements were actually removed
cleartimeout(this.observertimeout);
that.observertimeout = settimeout( function(){
var filtered = that.imagelinksfilter( that.selector.find(that.target) ),
activeindex = 0,
isactiveurl = false,
i;
// make sure that only dom changes in the photobox number of items will trigger a change
if(that.imagelinks.length == filtered[0].length)
return;
that.imagelinks = filtered[0];
that.images = filtered[1];
// if photobox is opened
if( photobox ){
// if gallery which was changed is the currently viewed one:
if( that.selector == photobox.selector ){
images = that.images;
imagelinks = that.imagelinks;
// check if the currently viewed photo has been detached from a photobox set
// if so, remove navigation arrows
// todo: fix the "images" to be an object and not an array.
for( i = images.length; i--; ){
if( images[i][0] == activeurl )
isactiveurl = true;
// if not exits any more
}
// if( isactiveurl ){
// overlay.removeclass('hasarrows');
// }
}
}
// if this gallery has thumbs
//if( that.options.thumbs ){
that.thumbslist = thumbsstripe.generate.apply(that);
thumbs.html( that.thumbslist );
//}
if( that.images.length && activeurl && that.options.thumbs ){
activeindex = that.thumbslist.find('a[href="'+activeurl+'"]').eq(0).parent().index();
if( activeindex == -1 )
activeindex = 0;
// updateindexes(activeindex);
thumbsstripe.changeactive(activeindex, 0);
}
}, 50);
},
onrotatebtnclick : function(){
var rotation = image.data('rotation') || 0, // in "deg"
imgscale = image.data('zoom') || 1
rotation += 90;
image.removeclass('zoomable').addclass('rotating');
image.css('transform', 'rotate('+ rotation +'deg) scale('+ imgscale + ')')
.data('rotation', rotation)
.on(transitionend, function(){
image.addclass('zoomable').removeclass('rotating');
});
},
onscrollzoom : function(e, deltay, deltax){
if( deltax ) return false;
var that = this;
if( activetype == 'video' ){
var zoomlevel = video.data('zoom') || 1;
zoomlevel += (deltay / 10);
if( zoomlevel < 0.5 )
return false;
video.data('zoom', zoomlevel).css({width:624*zoomlevel, height:351*zoomlevel});
}
else{
raf(function() {
var zoomlevel = image.data('zoom') || 1,
rotation = image.data('rotation') || 0,
position = image.data('position') || '50% 50%',
boundingclientrect = image[0].getboundingclientrect(),
value;
zoomlevel += (deltay / 10);
if( zoomlevel < 0.1 )
zoomlevel = 0.1;
image.data('zoom', zoomlevel);
value = 'scale('+ zoomlevel +') rotate('+ rotation +'deg)';
// if the image was zoomed and now is larger than the window size, allow mouse movemenet reposition
if( boundingclientrect.height > docelm.clientheight || boundingclientrect.width > docelm.clientwidth ){
$doc.on('mousemove.photobox', that.events.callbacks.onmousemoveimagereposition);
value += ' translate('+ position +')';
}
else{
$doc.off('mousemove.photobox');
// image[0].style[transformorigin] = '50% 50%';
}
image.css({ 'transform':value });
});
}
return false;
},
// moves the image around during zoom mode on mousemove event
onmousemoveimagereposition : function(e){
raf(function() {
var //y = (e.clienty / docelm.clientheight) * (docelm.clientheight + 200) - 100, // extend the range of the y axis by 100 each side
sensitivity = 1.5, // 1 = same as mouse more, and higher value is less sensitive to mouse move
ydelta = (e.clienty / docelm.clientheight * 100 - 50) / sensitivity, // subtract 50 because the real center is at "0%"
xdelta = (e.clientx / docelm.clientwidth * 100 - 50) / sensitivity, // subtract 50 because the real center is at "0%"
position,
rotationangel = image.data('rotation') || 0,
rotation = (rotationangel/90)%4 || 0,
imgscale = image.data('zoom') || 1;
if( rotation == 1 || rotation == 3 )
position = ydelta.tofixed(2)+'%, ' + -xdelta.tofixed(2) +'%';
else
position = xdelta.tofixed(2)+'%, ' + ydelta.tofixed(2) +'%';
image.data('position', position);
// image[0].style[transformorigin] = origin;
image[0].style.transform = 'rotate('+ rotationangel +'deg) scale('+ imgscale + ') translate(' + position + ')';
});
}
}
}
}
// on touch-devices only
function onswipe(e, dx, dy){
if( dx == 1 ){
image.css({transform:'translatex(25%)', transition:'.2s', opacity:0});
settimeout(function(){ changeimage(previmage) }, 200);
}
else if( dx == -1 ){
image.css({transform:'translatex(-25%)', transition:'.2s', opacity:0});
settimeout(function(){ changeimage(nextimage) }, 200);
}
if( dy == 1 )
thumbstoggler.prop('checked', true);
else if( dy == -1 )
thumbstoggler.prop('checked', false);
}
// manage the (bottom) thumbs strip
thumbsstripe = (function(){
var containerwidth = 0,
scrollwidth = 0,
posfromleft = 0, // stripe position from the left of the screen
stripepos = 0, // when relative mouse position inside the thumbs stripe
animated = null,
padding, // in percentage to the containerwidth
el, $el, ratio, scrollpos, pos;
return{
// returns a
element which is populated with all the gallery links and thumbs
generate : function(){
var thumbslist = $(''),
elements = [],
len = this.imagelinks.length,
title, thumbsrc, link, type, i;
for( i = 0; i < len; i++ ){
link = this.imagelinks[i];
thumbsrc = this.images[i][2];
// continue if has thumb
if( !thumbsrc )
continue;
title = this.images[i][1];
type = link.rel ? " class='" + link.rel +"'" : '';
elements.push(' ');
};
thumbslist.html( elements.join('') );
return thumbslist;
},
click : function(e){
e.preventdefault();
activethumb.removeclass('active');
activethumb = $(this).parent().addclass('active');
var imageindex = $(this.parentnode).index();
return changeimage(imageindex, 0, 1);
},
changeactivetimeout : null,
/** highlights the thumb which represents the photo and centres the thumbs viewer on it.
** @thumbclick - if a user clicked on a thumbnail, don't center on it
*/
changeactive : function(index, delay, thumbclick){
if( !options.thumbs )
return;
var lastindex = activethumb.index();
activethumb.removeclass('active');
activethumb = thumbs.find('li').eq(index).addclass('active');
if( thumbclick || !activethumb[0] ) return;
// set the scrollleft position of the thumbs list to show the active thumb
cleartimeout(this.changeactivetimeout);
// give the images time to to settle on their new sizes (because of css transition) and then calculate the center...
this.changeactivetimeout = settimeout(
function(){
var pos = activethumb[0].offsetleft + activethumb[0].clientwidth/2 - docelm.clientwidth/2;
delay ? thumbs.delay(800) : thumbs.stop();
thumbs.animate({scrollleft: pos}, 500, 'swing');
}, 200);
},
// calculate the thumbs container width, if the window has been resized
calc : function(e){
el = thumbs[0];
containerwidth = el.clientwidth;
scrollwidth = el.scrollwidth;
padding = 0.15 * containerwidth;
posfromleft = thumbs.offset().left;
stripepos = e.pagex - padding - posfromleft;
pos = stripepos / (containerwidth - padding*2);
scrollpos = (scrollwidth - containerwidth ) * pos;
thumbs.animate({scrollleft:scrollpos}, 200);
cleartimeout(animated);
animated = settimeout(function(){
animated = null;
}, 200);
return this;
},
// move the stripe left or right according to mouse position
move : function(e){
// don't move anything until initial movement on 'mouseenter' has finished
if( animated ) return;
var ratio = scrollwidth / containerwidth,
stripepos = e.pagex - padding - posfromleft, // the mouse x position, "normalized" to the carousel position
pos, scrollpos;
if( stripepos < 0) stripepos = 0; //
pos = stripepos / (containerwidth - padding*2); // calculated position between 0 to 1
// calculate the percentage of the mouse position within the carousel
scrollpos = (scrollwidth - containerwidth ) * pos;
raf(function(){
el.scrollleft = scrollpos;
});
}
}
})();
// autoplay controller
apcontrol = {
autoplaytimer : false,
play : function(){
apcontrol.autoplaytimer = settimeout(function(){ changeimage(nextimage) }, options.time);
apcontrol.progress.start();
autoplaybtn.removeclass('play');
apcontrol.settitle('click to stop autoplay');
options.autoplay = true;
},
pause : function(){
cleartimeout(apcontrol.autoplaytimer);
apcontrol.progress.reset();
autoplaybtn.addclass('play');
apcontrol.settitle('click to resume autoplay');
options.autoplay = false;
},
progress : {
reset : function(){
autoplaybtn.find('div').removeattr('style');
settimeout(function(){ autoplaybtn.removeclass('playing') },200);
},
start : function(){
if( !isoldie)
autoplaybtn.find('div').css(transition, options.time+'ms');
autoplaybtn.addclass('playing');
}
},
// sets the button title property
settitle : function(text){
if(text)
autoplaybtn.prop('title', text + ' (every ' + options.time/1000 + ' seconds)' );
},
// the button onclick handler
toggle : function(e){
e.stoppropagation();
apcontrol[ options.autoplay ? 'pause' : 'play']();
}
}
function getprefixed(prop){
var i, s = doc.createelement('p').style, v = ['ms','o','moz','webkit'];
if( s[prop] == '' ) return prop;
prop = prop.charat(0).touppercase() + prop.slice(1);
for( i = v.length; i--; )
if( s[v[i] + prop] == '' )
return (v[i] + prop);
}
function keydown(event){
var code = event.keycode, ok = options.keys, result;
// prevent default keyboard action (like navigating inside the page)
return $.inarray(code, ok.close) >= 0 && close() ||
$.inarray(code, ok.next) >= 0 && !options.single && loophole(nextimage) ||
$.inarray(code, ok.prev) >= 0 && !options.single && loophole(previmage) || true;
}
function wheelnextprev(e, dy, dx){
if( dx == 1 )
loophole(nextimage);
else if( dx == -1 )
loophole(previmage);
}
// serves as a callback for pbprevbtn / pbnextbtn buttons but also is called on keypress events
function next_prev(){
// don't get crazy when user clicks next or prev buttons rapidly
//if( !image.hasclass('zoomable') )
// return false;
var idx = (this.id == 'pbprevbtn') ? previmage : nextimage;
loophole(idx);
return false;
}
function updateindexes(idx){
lastactive = activeimage;
activeimage = idx;
activeurl = images[idx][0];
previmage = (activeimage || (options.loop ? images.length : 0)) - 1;
nextimage = ((activeimage + 1) % images.length) || (options.loop ? 0 : -1);
}
// check if looping is allowed before changing image/video.
// a pre-changeimage function, only for linear changes
function loophole(idx){
if( !options.loop ){
var afterlast = activeimage == images.length-1 && idx == nextimage,
beforefirst = activeimage == 0 && idx == previmage;
if( afterlast || beforefirst )
return;
}
changeimage(idx);
}
changeimage = (function(){
var timer;
return function(imageindex, firsttime, thumbclick){
// throttle mechanism
if( timer )
return;
timer = settimeout(function(){
timer = null;
}, 150);
$doc.off('mousemove.photobox');
if( !imageindex || imageindex < 0 )
imageindex = 0;
// hide/show next-prev buttons
if( !options.loop ){
//nextbtn[ imageindex == images.length-1 ? 'addclass' : 'removeclass' ]('pbhide');
nextbtn.toggleclass('pbhide', imageindex == images.length-1);
//prevbtn[ imageindex == 0 ? 'addclass' : 'removeclass' ]('pbhide');
prevbtn.toggleclass('pbhide', imageindex == 0);
}
// if there's a callback for this point:
if( typeof options.beforeshow == "function")
options.beforeshow(imagelinks[imageindex]);
overlay.removeclass('error');
if( activeimage >= 0 )
overlay.addclass( imageindex > activeimage ? 'next' : 'prev' );
updateindexes(imageindex);
// reset things
stop();
video.empty();
preload.onerror = null;
image.add(video).data('zoom', 1);
activetype = imagelinks[imageindex].rel == 'video' ? 'video' : 'image';
// check if current link is a video
if( activetype == 'video' ){
video.html( newvideo() ).addclass('pbhide');
showcontent(firsttime);
}
else{
// give a tiny delay to the preloader, so it won't be showed when images load very quickly
var loadertimeout = settimeout(function(){ overlay.addclass('pbloading'); }, 50);
if( isoldie ) overlay.addclass('pbhide'); // should wait for the image onload. just hide the image while old ie display the preloader
options.autoplay && apcontrol.progress.reset();
preload = new image();
preload.onload = function(){
preload.onload = null;
if( previmage >= 0 ) preloadprev.src = images[previmage][0];
if( nextimage >= 0 ) preloadnext.src = images[nextimage][0];
cleartimeout(loadertimeout);
showcontent(firsttime);
};
preload.onerror = imageerror;
preload.src = activeurl;
}
// show caption text
captiontext.on(transitionend, captiontextchange).addclass('change');
if( firsttime || isoldie ) captiontextchange();
thumbsstripe.changeactive(imageindex, firsttime, thumbclick);
// save url hash for current image
history.save();
}
})();
function newvideo(){
var url = images[activeimage][0],
sign = $('').prop('href',images[activeimage][0])[0].search ? '&' : '?';
url += sign + 'vq=hd720&wmode=opaque';
return $("