garancia/themes/default/_mobile/js/jquery.touch-gallery.js
2016-10-10 15:24:25 +02:00

1131 lines
31 KiB
JavaScript
Executable File

/*!
* NETEYE Activity Indicator jQuery Plugin
*
* Copyright (c) 2010 NETEYE GmbH
* Licensed under the MIT license
*
* Author: Felix Gnass [fgnass at neteye dot de]
* Version: 1.0.0
*/
/**
* Plugin that renders a customisable activity indicator (spinner) using SVG or VML.
*/
(function($) {
$.fn.activity = function(opts) {
this.each(function() {
var $this = $(this);
var el = $this.data('activity');
if (el) {
clearInterval(el.data('interval'));
el.remove();
$this.removeData('activity');
}
if (opts !== false) {
opts = $.extend({color: $this.css('color')}, $.fn.activity.defaults, opts);
el = render($this, opts).css('position', 'absolute').prependTo(opts.outside ? 'body' : $this);
var h = $this.outerHeight() - el.height();
var w = $this.outerWidth() - el.width();
var margin = {
top: opts.valign == 'top' ? opts.padding : opts.valign == 'bottom' ? h - opts.padding : Math.floor(h / 2),
left: opts.align == 'left' ? opts.padding : opts.align == 'right' ? w - opts.padding : Math.floor(w / 2)
};
var offset = $this.offset();
if (opts.outside) {
el.css({top: offset.top + 'px', left: offset.left + 'px'});
}
else {
margin.top -= el.offset().top - offset.top;
margin.left -= el.offset().left - offset.left;
}
el.css({marginTop: margin.top + 'px', marginLeft: margin.left + 'px'});
animate(el, opts.segments, Math.round(10 / opts.speed) / 10);
$this.data('activity', el);
}
});
return this;
};
$.fn.activity.defaults = {
segments: 12,
space: 3,
length: 7,
width: 4,
speed: 1.2,
align: 'center',
valign: 'center',
padding: 4
};
$.fn.activity.getOpacity = function(opts, i) {
var steps = opts.steps || opts.segments-1;
var end = opts.opacity !== undefined ? opts.opacity : 1/steps;
return 1 - Math.min(i, steps) * (1 - end) / steps;
};
/**
* Default rendering strategy. If neither SVG nor VML is available, a div with class-name 'busy'
* is inserted, that can be styled with CSS to display an animated gif as fallback.
*/
var render = function() {
return $('<div>').addClass('busy');
};
/**
* The default animation strategy does nothing as we expect an animated gif as fallback.
*/
var animate = function() {
};
/**
* Utility function to create elements in the SVG namespace.
*/
function svg(tag, attr) {
var el = document.createElementNS("http://www.w3.org/2000/svg", tag || 'svg');
if (attr) {
$.each(attr, function(k, v) {
el.setAttributeNS(null, k, v);
});
}
return $(el);
}
if (document.createElementNS && document.createElementNS( "http://www.w3.org/2000/svg", "svg").createSVGRect) {
// =======================================================================================
// SVG Rendering
// =======================================================================================
/**
* Rendering strategy that creates a SVG tree.
*/
render = function(target, d) {
var innerRadius = d.width*2 + d.space;
var r = (innerRadius + d.length + Math.ceil(d.width / 2) + 1);
var el = svg().width(r*2).height(r*2);
var g = svg('g', {
'stroke-width': d.width,
'stroke-linecap': 'round',
stroke: d.color
}).appendTo(svg('g', {transform: 'translate('+ r +','+ r +')'}).appendTo(el));
for (var i = 0; i < d.segments; i++) {
g.append(svg('line', {
x1: 0,
y1: innerRadius,
x2: 0,
y2: innerRadius + d.length,
transform: 'rotate(' + (360 / d.segments * i) + ', 0, 0)',
opacity: $.fn.activity.getOpacity(d, i)
}));
}
return $('<div>').append(el).width(2*r).height(2*r);
};
// Check if Webkit CSS animations are available, as they work much better on the iPad
// than setTimeout() based animations.
if (document.createElement('div').style.WebkitAnimationName !== undefined) {
var animations = {};
/**
* Animation strategy that uses dynamically created CSS animation rules.
*/
animate = function(el, steps, duration) {
if (!animations[steps]) {
var name = 'spin' + steps;
var rule = '@-webkit-keyframes '+ name +' {';
for (var i=0; i < steps; i++) {
var p1 = Math.round(100000 / steps * i) / 1000;
var p2 = Math.round(100000 / steps * (i+1) - 1) / 1000;
var value = '% { -webkit-transform:rotate(' + Math.round(360 / steps * i) + 'deg); }\n';
rule += p1 + value + p2 + value;
}
rule += '100% { -webkit-transform:rotate(100deg); }\n}';
document.styleSheets[0].insertRule(rule);
animations[steps] = name;
}
el.css('-webkit-animation', animations[steps] + ' ' + duration +'s linear infinite');
};
}
else {
/**
* Animation strategy that transforms a SVG element using setInterval().
*/
animate = function(el, steps, duration) {
var rotation = 0;
var g = el.find('g g').get(0);
el.data('interval', setInterval(function() {
g.setAttributeNS(null, 'transform', 'rotate(' + (++rotation % steps * (360 / steps)) + ')');
}, duration * 1000 / steps));
};
}
}
else {
// =======================================================================================
// VML Rendering
// =======================================================================================
var s = $('<shape>').css('behavior', 'url(#default#VML)').appendTo('body');
if (s.get(0).adj) {
// VML support detected. Insert CSS rules for group, shape and stroke.
var sheet = document.createStyleSheet();
$.each(['group', 'shape', 'stroke'], function() {
sheet.addRule(this, "behavior:url(#default#VML);");
});
/**
* Rendering strategy that creates a VML tree.
*/
render = function(target, d) {
var innerRadius = d.width*2 + d.space;
var r = (innerRadius + d.length + Math.ceil(d.width / 2) + 1);
var s = r*2;
var o = -Math.ceil(s/2);
var el = $('<group>', {coordsize: s + ' ' + s, coordorigin: o + ' ' + o}).css({top: o, left: o, width: s, height: s});
for (var i = 0; i < d.segments; i++) {
el.append($('<shape>', {path: 'm ' + innerRadius + ',0 l ' + (innerRadius + d.length) + ',0'}).css({
width: s,
height: s,
rotation: (360 / d.segments * i) + 'deg'
}).append($('<stroke>', {color: d.color, weight: d.width + 'px', endcap: 'round', opacity: $.fn.activity.getOpacity(d, i)})));
}
return $('<group>', {coordsize: s + ' ' + s}).css({width: s, height: s, overflow: 'hidden'}).append(el);
};
/**
* Animation strategy that modifies the VML rotation property using setInterval().
*/
animate = function(el, steps, duration) {
var rotation = 0;
var g = el.get(0);
el.data('interval', setInterval(function() {
g.style.rotation = ++rotation % steps * (360 / steps);
}, duration * 1000 / steps));
};
}
$(s).remove();
}
})(jQuery);
/*!
* NETEYE Activity Indicator jQuery Plugin
*
* Copyright (c) 2010 NETEYE GmbH
* Licensed under the MIT license
*
* Author: Felix Gnass [fgnass at neteye dot de]
* Version: 1.0.0
*/
/**
* Plugin that renders a customisable activity indicator (spinner) using SVG or VML.
*/
(function($) {
$.fn.activity = function(opts) {
this.each(function() {
var $this = $(this);
var el = $this.data('activity');
if (el) {
clearInterval(el.data('interval'));
el.remove();
$this.removeData('activity');
}
if (opts !== false) {
opts = $.extend({color: $this.css('color')}, $.fn.activity.defaults, opts);
el = render($this, opts).css('position', 'absolute').prependTo(opts.outside ? 'body' : $this);
var margin = {
top: Math.floor(($this.outerHeight() - el.height()) / 2),
left: Math.floor(($this.outerWidth() - el.width()) / 2)
};
var offset = $this.offset();
if (opts.outside) {
el.css({top: offset.top + 'px', left: offset.left + 'px'});
}
else {
margin.top -= el.offset().top - offset.top;
margin.left -= el.offset().left - offset.left;
}
el.css({marginTop: margin.top + 'px', marginLeft: margin.left + 'px'});
animate(el, opts.segments, Math.round(10 / opts.speed) / 10);
$this.data('activity', el);
}
});
return this;
};
$.fn.activity.defaults = {
segments: 12,
space: 3,
length: 7,
width: 4,
speed: 1.2
};
$.fn.activity.getOpacity = function(opts, i) {
var steps = opts.steps || opts.segments-1;
var end = opts.opacity !== undefined ? opts.opacity : 1/steps;
return 1 - Math.min(i, steps) * (1 - end) / steps;
};
/**
* Default rendering strategy. If neither SVG nor VML is available, a div with class-name 'busy'
* is inserted, that can be styled with CSS to display an animated gif as fallback.
*/
var render = function() {
return $('<div>').addClass('busy');
};
/**
* The default animation strategy does nothing as we expect an animated gif as fallback.
*/
var animate = function() {
};
/**
* Utility function to create elements in the SVG namespace.
*/
function svg(tag, attr) {
var el = document.createElementNS("http://www.w3.org/2000/svg", tag || 'svg');
if (attr) {
$.each(attr, function(k, v) {
el.setAttributeNS(null, k, v);
});
}
return $(el);
}
if (document.createElementNS && document.createElementNS( "http://www.w3.org/2000/svg", "svg").createSVGRect) {
// =======================================================================================
// SVG Rendering
// =======================================================================================
/**
* Rendering strategy that creates a SVG tree.
*/
render = function(target, d) {
var innerRadius = d.width*2 + d.space;
var r = (innerRadius + d.length + Math.ceil(d.width / 2) + 1);
var el = svg().width(r*2).height(r*2);
var g = svg('g', {
'stroke-width': d.width,
'stroke-linecap': 'round',
stroke: d.color
}).appendTo(svg('g', {transform: 'translate('+ r +','+ r +')'}).appendTo(el));
for (var i = 0; i < d.segments; i++) {
g.append(svg('line', {
x1: 0,
y1: innerRadius,
x2: 0,
y2: innerRadius + d.length,
transform: 'rotate(' + (360 / d.segments * i) + ', 0, 0)',
opacity: $.fn.activity.getOpacity(d, i)
}));
}
return $('<div>').append(el).width(2*r).height(2*r);
};
// Check if Webkit CSS animations are available, as they work much better on the iPad
// than setTimeout() based animations.
if (document.createElement('div').style.WebkitAnimationName !== undefined) {
var animations = {};
/**
* Animation strategy that uses dynamically created CSS animation rules.
*/
animate = function(el, steps, duration) {
if (!animations[steps]) {
var name = 'spin' + steps;
var rule = '@-webkit-keyframes '+ name +' {';
for (var i=0; i < steps; i++) {
var p1 = Math.round(100000 / steps * i) / 1000;
var p2 = Math.round(100000 / steps * (i+1) - 1) / 1000;
var value = '% { -webkit-transform:rotate(' + Math.round(360 / steps * i) + 'deg); }\n';
rule += p1 + value + p2 + value;
}
rule += '100% { -webkit-transform:rotate(100deg); }\n}';
document.styleSheets[0].insertRule(rule);
animations[steps] = name;
}
el.css('-webkit-animation', animations[steps] + ' ' + duration +'s linear infinite');
};
}
else {
/**
* Animation strategy that transforms a SVG element using setInterval().
*/
animate = function(el, steps, duration) {
var rotation = 0;
var g = el.find('g g').get(0);
el.data('interval', setInterval(function() {
g.setAttributeNS(null, 'transform', 'rotate(' + (++rotation % steps * (360 / steps)) + ')');
}, duration * 1000 / steps));
};
}
}
else {
// =======================================================================================
// VML Rendering
// =======================================================================================
var s = $('<shape>').css('behavior', 'url(#default#VML)').appendTo('body');
if (s.get(0).adj) {
// VML support detected. Insert CSS rules for group, shape and stroke.
var sheet = document.createStyleSheet();
$.each(['group', 'shape', 'stroke'], function() {
sheet.addRule(this, "behavior:url(#default#VML);");
});
/**
* Rendering strategy that creates a VML tree.
*/
render = function(target, d) {
var innerRadius = d.width*2 + d.space;
var r = (innerRadius + d.length + Math.ceil(d.width / 2) + 1);
var s = r*2;
var o = -Math.ceil(s/2);
var el = $('<group>', {coordsize: s + ' ' + s, coordorigin: o + ' ' + o}).css({top: o, left: o, width: s, height: s});
for (var i = 0; i < d.segments; i++) {
el.append($('<shape>', {path: 'm ' + innerRadius + ',0 l ' + (innerRadius + d.length) + ',0'}).css({
width: s,
height: s,
rotation: (360 / d.segments * i) + 'deg'
}).append($('<stroke>', {color: d.color, weight: d.width + 'px', endcap: 'round', opacity: $.fn.activity.getOpacity(d, i)})));
}
return $('<group>', {coordsize: s + ' ' + s}).css({width: s, height: s, overflow: 'hidden'}).append(el);
};
/**
* Animation strategy that modifies the VML rotation property using setInterval().
*/
animate = function(el, steps, duration) {
var rotation = 0;
var g = el.get(0);
el.data('interval', setInterval(function() {
g.style.rotation = ++rotation % steps * (360 / steps);
}, duration * 1000 / steps));
};
}
$(s).remove();
}
})(jQuery);
/*!
* NETEYE Transform & Transition Plugin
*
* Copyright (c) 2010 NETEYE GmbH
* Licensed under the MIT license
*
* Author: Felix Gnass [fgnass at neteye dot de]
* Version: 1.0.0
*/
(function($) {
// ==========================================================================================
// Private functions
// ==========================================================================================
var props = (function() {
var prefixes = ['Webkit', 'Moz', 'O'];
var style = document.createElement('div').style;
function findProp(name) {
var result = '';
if (style[name] !== undefined) {
return name;
}
$.each(prefixes, function() {
var p = this + name.charAt(0).toUpperCase() + name.substring(1);
if (style[p] !== undefined) {
result = p;
return false;
}
});
return result;
}
var result = {};
$.each(['transitionDuration', 'transitionProperty', 'transform', 'transformOrigin'], function() {
result[this] = findProp(this);
});
return result;
})();
var supports3d = (function() {
var s = document.createElement('div').style;
try {
s[props.transform] = 'translate3d(0,0,0)';
return s[props.transform].length > 0;
}
catch (ex) {
return false;
}
})();
function transform(el, commands) {
var t = el.data('transform');
if (!t) {
t = new Transformation();
el.data('transform', t);
}
if (commands !== undefined) {
if (commands === false || commands.reset) {
t.reset();
}
else {
t.exec(commands);
}
}
return t;
}
/**
* Class that keeps track of numeric values and converts them into a string representation
* that can be used as value for the -webkit-transform property. TransformFunctions are used
* internally by the Transformation class.
*
* // Example:
*
* var t = new TransformFunction('translate3d({x}px,{y}px,{z}px)', {x:0, y:0, z:0});
* t.x = 23;
* console.assert(t.format() == 'translate3d(23px,0px,0px)')
*/
function TransformFunction(pattern, defaults) {
function fillIn(pattern, data) {
return pattern.replace(/\{(\w+)\}/g, function(s, p1) { return data[p1]; });
}
this.reset = function() {
$.extend(this, defaults);
};
this.format = function() {
return fillIn(pattern, this);
};
this.reset();
}
/**
* Class that encapsulates the state of multiple TransformFunctions. The state can be modified
* using commands and converted into a string representation that can be used as CSS value.
* The class is used internally by the transform plugin.
*/
function Transformation() {
var fn = {
translate: new TransformFunction('translate({x}px,{y}px)', {x:0, y:0}),
scale: new TransformFunction('scale({x},{y})', {x:1, y:1}),
rotate: new TransformFunction('rotate({deg}deg)', {deg:0})
};
if (supports3d) {
// Use 3D transforms for better performance
fn.translate = new TransformFunction('translate3d({x}px,{y}px,0px)', {x:0, y:0});
fn.scale = new TransformFunction('scale3d({x},{y},1)', {x:1, y:1});
}
var commands = {
rotate: function(deg) {
fn.rotate.deg = deg;
},
rotateBy: function(deg) {
fn.rotate.deg += deg;
},
scale: function(s) {
if (typeof s == 'number') {
s = {x: s, y: s};
}
fn.scale.x = s.x;
fn.scale.y = s.y;
},
scaleBy: function(s) {
if (typeof s == 'number') {
s = {x: s, y: s};
}
fn.scale.x *= s.x;
fn.scale.y *= s.y;
},
translate: function(s) {
var t = fn.translate;
if (!s) {
s = {x: 0, y: 0};
}
t.x = (s.x !== undefined) ? parseInt(s.x, 10) : t.x;
t.y = (s.y !== undefined) ? parseInt(s.y, 10) : t.y;
},
translateBy: function(s) {
var t = fn.translate;
t.x += parseInt(s.x, 10) || 0;
t.y += parseInt(s.y, 10) || 0;
}
};
this.fn = fn;
this.exec = function(cmd) {
for (var n in cmd) {
if (commands[n]) {
commands[n](cmd[n]);
}
}
};
this.reset = function() {
$.each(fn, function() {
this.reset();
});
};
this.format = function() {
var s = '';
$.each(fn, function(k, v) {
s += v.format() + ' ';
});
return s;
};
}
// ==========================================================================================
// Public API
// ==========================================================================================
$.fn.transform = function(opts) {
var result = this;
if ($.fn.transform.supported) {
this.each(function() {
var $this = $(this);
var t = transform($this, opts);
if (opts === undefined) {
result = t.fn;
return false;
}
var origin = opts && opts.origin ? opts.origin : '0 0';
$this.css(props.transitionDuration, '0s')
.css(props.transformOrigin, origin)
.css(props.transform, t.format());
});
}
return result;
};
$.fn.transform.supported = !!props.transform;
$.fn.transition = function(css, opts) {
opts = $.extend({
delay: 0,
duration: 0.4
}, opts);
var property = '';
$.each(css, function(k, v) {
property += k + ',';
});
this.each(function() {
var $this = $(this);
if (!$.fn.transition.supported) {
$this.css(css);
if (opts.onFinish) {
$.proxy(opts.onFinish, $this)();
}
return;
}
var _duration = $this.css(props.transitionDuration);
function apply() {
$this.css(props.transitionProperty, property).css(props.transitionDuration, opts.duration + 's');
$this.css(css);
if (opts.duration > 0) {
$this.one('webkitTransitionEnd oTransitionEnd transitionend', afterCompletion);
}
else {
setTimeout(afterCompletion, 1);
}
}
function afterCompletion() {
$this.css(props.transitionDuration, _duration);
if (opts.onFinish) {
$.proxy(opts.onFinish, $this)();
}
}
if (opts.delay > 0) {
setTimeout(apply, opts.delay);
}
else {
apply();
}
});
return this;
};
$.fn.transition.supported = !!props.transitionProperty;
$.fn.transformTransition = function(opts) {
opts = $.extend({
origin: '0 0',
css: {}
}, opts);
var css = opts.css;
if ($.fn.transform.supported) {
css[props.transform] = transform(this, opts).format();
this.css(props.transformOrigin, opts.origin);
}
return this.transition(css, opts);
};
})(jQuery);
/*!
* NETEYE Touch-Gallery jQuery Plugin
*
* Copyright (c) 2010 NETEYE GmbH
* Licensed under the MIT license
*
* Author: Felix Gnass [fgnass at neteye dot de]
* Version: 1.0.0
*/
(function($) {
var mobileSafari = /Mobile.*Safari/.test(navigator.userAgent);
$.fn.touchGallery = function(opts) {
opts = $.extend({}, $.fn.touchGallery.defaults, opts);
var thumbs = this;
this.live('click', function(ev) {
ev.preventDefault();
var clickedThumb = $(this);
if (!clickedThumb.is('.open')) {
thumbs.addClass('open');
openGallery(thumbs, clickedThumb, opts);
}
});
return this;
};
/**
* Default options.
*/
$.fn.touchGallery.defaults = {
getSource: function() {
return this.href;
}
};
// ==========================================================================================
// Private functions
// ==========================================================================================
/**
* Opens the gallery. A spining activity indicator is displayed until the clicked image has
* been loaded. When ready, showGallery() is called.
*/
function openGallery(thumbs, clickedThumb, opts) {
clickedThumb.activity();
var img = new Image();
img.onload = function() {
clickedThumb.activity(false);
showGallery(thumbs, thumbs.index(clickedThumb), this, opts.getSource);
};
img.src = $.proxy(opts.getSource, clickedThumb.get(0))();
}
/**
* Creates DOM elements to actually show the gallery.
*/
function showGallery(thumbs, index, clickedImage, getSrcCallback) {
var viewport = fitToView(preventTouch($('<div id="galleryViewport">').css({
position: 'fixed',
top: 0,
left: 0,
overflow: 'hidden'
}).transform(false).appendTo('body')));
var stripe = $('<div id="galleryStripe">').css({
position: 'absolute',
height: '100%',
top: 0,
left: (-index * getInnerWidth()) + 'px'
}).width(thumbs.length * getInnerWidth()).transform(false).appendTo(viewport);
setupEventListeners(stripe, getInnerWidth(), index, thumbs.length-1);
$(window).bind('orientationchange.gallery', function() {
fitToView(viewport);
stripe.find('img').each(centerImage);
});
thumbs.each(function(i) {
var page = $('<div>').addClass('galleryPage').css({
display: 'block',
position: 'absolute',
left: i * getInnerWidth() + 'px',
overflow: 'hidden',
height: '100%'
}).width(getInnerWidth()).data('thumbs', thumbs).data('thumb', $(this)).transform(false).appendTo(stripe);
if (i == index) {
var $img = $(clickedImage).css({position: 'absolute', display: 'block'}).transform(false);
makeInvisible(centerImage(index, clickedImage, $img)).appendTo(page);
zoomIn($(this), $img, function() {
stripe.addClass('ready');
loadSurroundingImages(index);
});
insertShade(viewport);
}
else {
page.activity({color: '#fff'});
var img = new Image();
var src = $.proxy(getSrcCallback, this)();
page.one('loadImage', function() {
img.src = src;
});
img.onload = function() {
var $this = $(this).css({position: 'absolute', display: 'block'}).transform(false);
centerImage(i, this, $this).appendTo(page.activity(false));
page.trigger('loaded');
};
}
});
}
function hideGallery(stripe) {
if (stripe.is('.ready') && !stripe.is('.panning')) {
$('#galleryShade').remove();
var page = stripe.find('.galleryPage').eq(stripe.data('galleryIndex'));
page.data('thumbs').removeClass('open');
var thumb = page.data('thumb');
stripe.add(window).add(document).unbind('.gallery');
zoomOut(page.find('img'), thumb, function() {
makeVisible(thumb).transform(false);
$('#galleryViewport').remove();
});
}
}
/**
* Inserts a black DIV before the given target element and performs an opacity
* transition form 0 to 1.
*/
function insertShade(target, onFinish) {
var el = $('<div id="galleryShade">').css({
top: 0, left: 0, background: '#000', opacity: 0
});
if (mobileSafari) {
// Make the shade bigger so that it shadows the surface upon rotation
var l = Math.max(screen.width, screen.height) * (window.devicePixelRatio || 1) + Math.max(getScrollLeft(), getScrollTop()) + 100;
el.css({position: 'absolute'}).width(l).height(l);
}
else {
el.css({position: 'fixed', width: '100%', height: '100%'});
}
el.insertBefore(target)
.transform(false)
.transition({opacity: 1}, {delay: 200, duration: 0.8, onFinish: onFinish});
}
/**
* Scales and centers an element according to the dimensions of the given image.
* The first argument is ignored, it's just there so that the function can be used with .each()
*/
function centerImage(i, img, el) {
el = el || $(img);
if (!img.naturalWidth) {
//Work-around for Opera which doesn't support naturalWidth/Height. This works because
//the function is invoked once for each image before it is scaled.
img.naturalWidth = img.width;
img.naturalHeight = img.height;
}
var s = Math.min(getViewportScale(), Math.min(getInnerHeight()/img.naturalHeight, getInnerWidth()/img.naturalWidth));
el.css({
top: Math.round((getInnerHeight() - img.naturalHeight * s) / 2) + 'px',
left: Math.round((getInnerWidth() - img.naturalWidth * s) / 2) + 'px'
}).width(Math.round(img.naturalWidth * s));
return el;
}
/**
* Performs a zoom animation from the small to the large element. The large element is scaled
* down and centered over the small element. Then a transition is performed that
* resets the transformation.
*/
function zoomIn(small, large, onFinish) {
var b = bounds(large);
var t = bounds(small);
var s = Math.max(t.width / large.width(), t.height / large.height());
var ox = mobileSafari ? 0 : getScrollLeft();
var oy = mobileSafari ? 0 : getScrollTop();
large.transform({
translate: {
x: t.left - b.left - ox - Math.round((b.width * s - t.width) / 2),
y: t.top - b.top - oy - Math.round((b.height * s - t.height) / 2)
},
scale: s
});
setTimeout(function() {
makeVisible(large);
makeInvisible(small);
large.transformTransition({reset: true, onFinish: onFinish});
}, 1);
}
/**
* Performs a zoom animation from the large to the small element. Since the small version
* may have a different aspect ratio, the large element is wrapped inside a div and clipped
* to match the aspect of the small version. The wrapper div is appended to the body, as
* leaving it in place causes strange z-index/flickering issues.
*/
function zoomOut(large, small, onFinish) {
if (large.length === 0 || !$.fn.transition.supported) {
if (onFinish) {
onFinish();
}
return;
}
var b = bounds(large);
var t = bounds(small);
var w = Math.min(b.height * t.width / t.height, b.width);
var h = Math.min(b.width * t.height / t.width, b.height);
var s = Math.max(t.width / w, t.height / h);
var div = $('<div>').css({
overflow: 'hidden',
position: 'absolute',
width: w + 'px',
height: h + 'px',
top: getScrollTop() + Math.round((getInnerHeight()-h) / 2) + 'px',
left: getScrollLeft() + Math.round((getInnerWidth()-w) / 2) + 'px'
})
.appendTo('body').append(large.css({
top: 1-Math.floor((b.height-h) / 2) + 'px', // -1px offset to match Flickr's square crops
left: -Math.floor((b.width-w) / 2) + 'px'
}))
.transform(false);
b = bounds(div);
div.transformTransition({
translate: {
x: t.left - b.left - Math.round((w * s - t.width) / 2),
y: t.top - b.top - Math.round((h * s - t.height) / 2)
},
scale: s,
onFinish: function() {
onFinish();
div.remove();
}
});
}
function getPage(i) {
return $('#galleryStripe .galleryPage').eq(i);
}
function getThumb(i) {
return getPage(i).data('thumb');
}
function loadSurroundingImages(i) {
var page = getPage(i);
function triggerLoad() {
getPage(i-1).add(getPage(i+1)).trigger('loadImage');
}
if (page.find('img').length > 0) {
triggerLoad();
}
else {
page.one('loaded', triggerLoad);
}
}
/**
* Registers event listeners to enable flicking through the images.
*/
function setupEventListeners(el, pageWidth, currentIndex, max) {
var scale = getViewportScale();
var xOffset = parseInt(el.css('left'), 10);
el.data('galleryIndex', currentIndex);
function flick(dir) {
var i = el.data('galleryIndex');
makeVisible(getThumb(i));
i = Math.max(0, Math.min(i + dir, max));
el.data('galleryIndex', i);
makeInvisible(getThumb(i));
loadSurroundingImages(i);
if ($.fn.transform.supported) {
var x = -i * pageWidth - xOffset;
if (x != el.transform().translate.x) {
el.addClass('panning').transformTransition({translate: {x: x}, onFinish: function() { this.removeClass('panning'); }});
}
}
else {
el.css('left', -i * pageWidth + 'px');
}
}
$(document).bind('keydown.gallery', function(event) {
if (event.keyCode == 37) {
el.trigger('prev');
}
else if (event.keyCode == 39) {
el.trigger('next');
}
if (event.keyCode == 27 || event.keyCode == 32) {
el.trigger('close');
}
return false;
});
el.bind('touchstart', function() {
$(this).data('pan', {
startX: event.targetTouches[0].screenX,
lastX:event.targetTouches[0].screenX,
startTime: new Date().getTime(),
startOffset: $(this).transform().translate.x,
distance: function() {
return Math.round(scale * (this.startX - this.lastX));
},
delta: function() {
var x = event.targetTouches[0].screenX;
this.dir = this.lastX > x ? 1 : -1;
var delta = Math.round(scale * (this.lastX - x));
this.lastX = x;
return delta;
},
duration: function() {
return new Date().getTime() - this.startTime;
}
});
return false;
})
.bind('touchmove', function() {
var pan = $(this).data('pan');
$(this).transform({translateBy: {x: -pan.delta()}});
return false;
})
.bind('touchend', function() {
var pan = $(this).data('pan');
if (pan.distance() === 0 && pan.duration() < 500) {
$(event.target).trigger('click');
}
else {
flick(pan.dir);
}
return false;
})
.bind('prev', function() {
flick(-1);
})
.bind('next', function() {
flick(1);
})
.bind('click close', function() {
hideGallery(el);
});
}
/**
* Sets position and size of the given jQuery object to match the current viewport dimensions.
*/
function fitToView(el) {
if (mobileSafari) {
el.css({top: getScrollTop() + 'px', left: getScrollLeft() + 'px'});
}
return el.width(getInnerWidth()).height(getInnerHeight());
}
/**
* Returns the reciprocal of the current zoom-factor.
* @REVISIT Use screen.width / screen.availWidth instead?
*/
function getViewportScale() {
return getInnerWidth() / document.documentElement.clientWidth;
}
/**
* Returns a window property with fallback to a property on the
* documentElement in Internet Explorer.
*/
function getWindowProp(name, ie) {
if (window[name] !== undefined) {
return window[name];
}
var d = document.documentElement;
if (d && d[ie]) {
return d[ie];
}
return document.body[ie];
}
function getScrollTop() {
return getWindowProp('pageYOffset', 'scrollTop');
}
function getScrollLeft() {
return getWindowProp('pageXOffset', 'scrollLeft');
}
function getInnerWidth() {
return getWindowProp('innerWidth', 'clientWidth');
}
function getInnerHeight() {
return getWindowProp('innerHeight', 'clientHeight');
}
function makeVisible(el) {
return el.css('visibility', 'visible');
}
function makeInvisible(el) {
return el.css('visibility', 'hidden');
}
function bounds(el) {
var e = el.get(0);
if (e && e.getBoundingClientRect) {
return e.getBoundingClientRect();
}
return $.extend({width: el.width(), height: el.height()}, el.offset());
}
function preventTouch(el) {
return el.bind('touchstart', function() { return false; });
}
})(jQuery);