/*
* jQuery.flickable v1.0b3
*
* Copyright (c) 2010 lagos
* Dual licensed under the MIT and GPL licenses.
*
* http://lagoscript.org
*/
(function($, window, Math, undefined) {
var flickable,
document = window.document,
// Whether browser has touchScreen
touchDevice = /iphone|ipod|ipad|android|blackberry/,
isAndroid = /android/.test(navigator.userAgent.toLowerCase()),
// Check if the target node is not needed to cancel default action.
specialNodes = /^(input|textarea|select|option|button|embed|object)$/,
specialNodesForTouch = new RegExp('^(input|textarea|select|option|button|embed|object|a' + (isAndroid ? '' : '|img') + ')$');
// Inspired by jQuery UI
$.fn.flickable = flickable = function(options) {
if (typeof options === "string") { // method call
var args = Array.prototype.slice.call(arguments, 1),
returnValue = this;
this.each(function() {
var instance = $.data(this, 'flickable'),
value = instance && $.isFunction(instance[options]) ?
instance[options].apply(instance, args) : instance;
if (value !== instance && value !== undefined) {
returnValue = value;
return false;
}
});
return returnValue;
} else {
return this.each(function() {
var instance = $.data(this, 'flickable');
if (instance) {
$.extend(true, instance.options, options)
instance.init();
} else {
$.data(this, 'flickable', new flickable.prototype.create(options, this));
}
});
}
};
flickable.prototype = {
options: {
cancel: null,
disabled: false,
elasticConstant: 0.16,
friction: 0.96,
section: null
},
// Whether we drag
canceled: false,
// Don't append paddings if true
noPadding: false,
// Style names for layout
layoutStyles: [
'background-color', 'background-repeat', 'background-attachment', 'background-position', 'background-image',
'padding-top', 'padding-right', 'padding-bottom', 'padding-left'
],
create: function(options, elem) {
var self = this,
element = $(elem);
this.originalElement = element;
this.options = $.extend(true, {}, this.options, options);
this.layoutStyles = $.extend([], this.layoutStyles);
this.client = {x:0, y:0};
this.inertialVelocity = {x:0, y:0};
this.remainder = {x:0, y:0};
this.hasScroll = {x:false, y:false};
this.padding = {};
this.stretchPosition = {};
this.sectionPostions = [];
this.preventDefaultClick = false;
if (elem == window || elem == document || /^(html|body)$/i.test(elem.nodeName)) {
// we don't support flick scrolling the entire page on touch device
if (touchDevice.test(navigator.userAgent.toLowerCase())) return;
this.box = $(window);
this.elementWrapper = $('html');
this.element = $('body');
this.position = this.positionWindow;
this.scroll = this.scrollWindow;
// We need to copy width, border and margin if element is body
this.layoutStyles.push('width');
$.each(['top', 'right', 'bottom', 'left'], function(i, posName) {
self.layoutStyles.push(['margin', posName].join('-'));
$.each(['color', 'width', 'style'], function(j, type) {
self.layoutStyles.push(['border', posName, type].join('-'));
});
});
} else {
// We don't need to sort out elements
this.box = this.elementWrapper = this.element = element;
}
this.init();
},
init: function() {
var self = this, scripts,
element = this.element;
if (!this.container) {
// Change type of script element to avoid reevaluating those
scripts = script(element).attr('type', 'text/plain');
element.addClass('ui-flickable')
.append('')
.wrapInner(
''
);
scripts.attr('type', 'text/javascript');
this.container = element.children().bind('touchstart.flickable mousedown.flickable', function(event) {
return self.dragStart(event);
}).bind('click.flickable', function(event) {
return self.clickHandler(event);
});
this.wrapper = this.container.children();
this.content = this.wrapper.children();
this.elementWrapper.bind('touchstart.flickable mousedown.flickable keydown.flickable', function() {
self.deactivate();
}).bind('touchend.flickable mouseup.flickable mouseleave.flickable keyup.flickable', function() {
self.activate();
});
// Save original style
element.data('style.flickable', element.attr('style'));
// Copy styles onto content to keep layouts
$.each(this.layoutStyles, function(i, prop) {
var style = element.css(prop);
self.content.css(prop, style);
if (prop === 'background-color') {
self.wrapper.css(prop, $.inArray(style, ['transparent', 'rgba(0, 0, 0, 0)']) >= 0 ? '#FFF' : style);
} else if (prop === 'width') {
element.css(prop, 'auto');
} else if (/^(padding-\w+|margin-\w+|border-\w+-width)$/.test(prop)) {
element.css(prop, 0);
} else if (/^(background-image|border-\w+-style)$/.test(prop)) {
element.css(prop, 'none');
}
});
if ($.nodeName(element[0], 'body')) {
this.box.bind('resize.flickable', function() {
setTimeout(function() {
!self.options.disabled && self.refresh();
}, 0);
});
} else {
if (element.css('position') === 'static') {
// Fix overflow bugs in IE
element.css('position', 'relative');
}
}
$(window).bind('unload.flickable', function() {
self.disable();
});
}
this.option(this.options);
this.activate();
},
option: function(key, value) {
var self = this, options = key;
if (arguments.length === 0) {
return $.extend({}, this.options);
}
if (typeof key === "string") {
if (value === undefined) {
return this.options[key];
}
options = {};
options[key] = value;
}
$.each(options, function(key, value) {
self.setOption(key, value);
});
return this;
},
setOption: function(key, value) {
var self = this,
refresh = false;
this.options[key] = value;
switch (key) {
case 'cancel':
this.cancel && this.cancel.removeClass('ui-flickable-canceled').unbind('.flickable');
// Add elements that has scroll
this.cancel = $('div', this.content).map(function(i, elem) {
return hasScroll(elem) ? elem : null;
}).add($('iframe,textarea,select', this.content));
if (value) {
this.cancel = this.cancel.add($(value, this.content));
}
this.cancel.addClass('ui-flickable-canceled').bind('touchstart.flickable mouseenter.flickable', function() {
self.canceled = true;
}).bind('touchend.flickable mouseleave.flickable', function() {
self.canceled = false;
});
break;
case 'disabled':
this.element[value ? 'addClass' : 'removeClass']('ui-flickable-disabled');
if (!value) {
this.selected = undefined;
}
refresh = true;
this.noPadding = value;
break;
case 'section':
this.sections = null;
if (value) {
this.sections = $(value, this.content);
refresh = true;
}
break;
}
refresh && this.refresh();
return this;
},
destroy: function() {
var self = this, scripts;
this.noPadding = true;
this.refresh();
this.element
.attr('style', this.element.data('style.flickable') || null)
.removeClass('ui-flickable ui-flickable-disabled')
.removeData('style.flickable');
this.elementWrapper.unbind('.flickable');
this.box.unbind('.flickable');
this.cancel.unbind('.flickable').removeClass('ui-flickable-canceled');
$(window).unbind('.flickable');
scripts = script(this.content).attr('type', 'text/plain');
this.content.add(this.wrapper).add(this.container)
.replaceWith(this.content.children());
scripts.attr('type', 'text/javascript');
this.container = this.content = this.wrapper = this.cancel = this.selected = undefined;
return this;
},
enable: function() {
return this.setOption('disabled', false);
},
disable: function() {
return this.setOption('disabled', true);
},
trigger: function(type, event, data) {
var callback = this.options[type];
event = $.Event(event);
event.type = (type === 'flick' ? type : 'flick' + type).toLowerCase();
data = data || {};
if (event.originalEvent) {
for (var i = $.event.props.length, prop; i;) {
prop = $.event.props[--i];
event[prop] = event.originalEvent[prop];
}
}
this.originalElement.trigger(event, data);
return !(callback && callback.call(this.element[0], event, data) === false ||
event.isDefaultPrevented());
},
refresh: function() {
var self = this,
scroll = {x:0, y:0},
width, wrapperPosition,
maxWidth, maxHeight,
content = this.content,
padding = this.padding,
_padding = $.extend({}, padding),
clientElement = document.compatMode === 'CSS1Compat' ? this.elementWrapper : this.element,
box = {};
width = content.width();
content.width('auto');
maxHeight = this.elementWrapper.get(0).scrollHeight - (_padding.height || 0) * 2;
maxWidth = this.elementWrapper.get(0).scrollWidth - (_padding.width || 0) * 2;
if (width > maxWidth) {
content.width(width);
maxWidth = content.outerWidth() + parseInt(content.css('margin-left')) + parseInt(content.css('margin-right'));
}
$.each({'Height':maxHeight, 'Width':maxWidth}, function(dimension, max) {
var _dimension = dimension.toLowerCase();
box[_dimension] = self.box[_dimension]();
// Attach paddings if element has scroll
padding[_dimension] = max > clientElement.get(0)['client' + dimension] && !self.noPadding ?
Math.round(box[_dimension] / 2) : 0;
});
this.container.width(box.width >= maxWidth ? 'auto' : maxWidth).css({
'padding-top': padding.height,
'padding-bottom': padding.height,
'padding-left': padding.width,
'padding-right': padding.width
});
maxWidth = box.width > maxWidth ? box.width : maxWidth;
this.hasScroll.x = this.elementWrapper.get(0).scrollWidth > clientElement.get(0).clientWidth;
this.hasScroll.y = this.elementWrapper.get(0).scrollHeight > clientElement.get(0).clientHeight;
wrapperPosition = this.position(this.wrapper);
this.stretchPosition = {
top: wrapperPosition.top,
bottom: wrapperPosition.top + maxHeight - clientElement.get(0).clientHeight,
left: wrapperPosition.left,
right: wrapperPosition.left + maxWidth - clientElement.get(0).clientWidth
};
this.sectionPostions = this.sections ? this.sections.map(function() {
return self.position(this);
}).get() : [];
$.each({x:'width', y:'height'}, function(axis, dimension) {
scroll[axis] = padding[dimension] - (_padding[dimension] || 0);
});
this.scroll(scroll.x, scroll.y);
return this;
},
scroll: function(x, y) {
var box = this.box;
x && box.scrollLeft(box.scrollLeft() + x);
y && box.scrollTop(box.scrollTop() + y);
return this;
},
scrollWindow: function(x, y) {
this.box[0].scrollBy(parseInt(x), parseInt(y));
return this;
},
position: function(elem) {
var offset1 = $(elem).offset(),
offset2 = this.element.offset();
return {
top: Math.floor(offset1.top - offset2.top + this.box.scrollTop()),
left: Math.floor(offset1.left - offset2.left + this.box.scrollLeft())
};
},
positionWindow: function(elem) {
var offset = $(elem).offset();
return {
top: Math.floor(offset.top),
left: Math.floor(offset.left)
};
},
activate: function() {
this.scrollBack();
return this;
},
deactivate: function() {
if (this.isScrolling()) {
this.preventDefaultClick = true;
}
this.remainder.x = 0;
this.remainder.y = 0;
this.inertialVelocity.x = 0;
this.inertialVelocity.y = 0;
// !this.options.disabled && this.refresh();
clearInterval(this.inertia);
clearInterval(this.back);
return this;
},
clickHandler: function(event) {
if (this.preventDefaultClick) {
event.preventDefault();
this.preventDefaultClick = false;
}
},
dragStart: function(event) {
var self = this,
e = event.type === 'touchstart' ? event.originalEvent.touches[0] : event;
if (event.type === 'touchstart' && event.originalEvent.touches.length > 1) {
return;
}
if (!this.canceled && !this.options.disabled) {
this.timeStamp = event.timeStamp;
$.each(['x', 'y'], function(i, axis) {
self.client[axis] = e['client' + axis.toUpperCase()];
self.inertialVelocity[axis] = 0;
});
if (this.trigger('dragStart', event) === false) return false;
this.container.unbind('touchstart.flickable mousedown.flickable')
.bind('touchmove.flickable mousemove.flickable', function(event) {
return self.drag(event);
}).bind('touchend.flickable mouseup.flickable mouseleave.flickable', function(event) {
return self.dragStop(event);
});
if (!(event.type === 'touchstart' ? specialNodesForTouch : specialNodes).test(event.target.nodeName.toLowerCase())) {
event.preventDefault();
}
}
},
drag: function(event) {
var self = this, velocity = {},
t = event.timeStamp - this.timeStamp,
e = event.type === 'touchmove' ? event.originalEvent.touches[0] : event;
if (event.type === 'touchmove' && event.originalEvent.touches.length > 1) {
return;
}
if (this.trigger('drag', event) === false) return false;
$.each(['x', 'y'], function(i, axis) {
var client = e['client' + axis.toUpperCase()];
velocity[axis] = self.hasScroll[axis] && (!self.draggableAxis || self.draggableAxis === axis)
? self.client[axis] - client : 0;
self.client[axis] = client;
if (t) self.inertialVelocity[axis] = velocity[axis] / t;
});
if (this.sections && !this.draggableAxis) {
this.draggableAxis = Math.abs(velocity.x) > Math.abs(velocity.y) ? 'x' : 'y';
velocity[this.draggableAxis === 'x' ? 'y' : 'x'] = 0;
}
this.timeStamp = event.timeStamp;
velocity = this.velocity(velocity);
this.scroll(velocity.x, velocity.y);
if (!specialNodes.test(event.target.nodeName.toLowerCase())) {
event.preventDefault();
}
this.preventDefaultClick = true;
},
dragStop: function(event) {
var self = this;
if (event.type === 'touchend' && event.originalEvent.touches.length > 1) {
return;
}
if (this.trigger('dragStop', event) === false) return false;
this.container.unbind('.flickable')
.bind('touchstart.flickable mousedown.flickable', function(event) {
return self.dragStart(event);
}).bind('click.flickable', function(event) {
return self.clickHandler(event);
});
this.flick();
},
flick: function() {
var self = this,
options = this.options,
box = this.box,
inertialVelocity = this.inertialVelocity,
friction = options.friction;
// Make sure that the method is executed only once
clearInterval(this.inertia);
$.each(inertialVelocity, function(axis, velocity) {
if (Math.abs(velocity) < 0.1) inertialVelocity[axis] = 0;
});
if (this.sections) {
var destination, distance, axis;
if (this.isScrolling()) {
var distances, nearest, scrollPos, posName, scroll,
scrollTop = box.scrollTop(),
scrollLeft = box.scrollLeft();
if (Math.abs(inertialVelocity.x) > Math.abs(inertialVelocity.y)) {
axis = 'x';
posName = 'left';
scroll = 'scrollLeft';
scrollPos = scrollLeft;
inertialVelocity.y = 0;
} else {
axis = 'y';
posName = 'top';
scroll = 'scrollTop';
scrollPos = scrollTop;
inertialVelocity.x = 0;
}
distances = $.map(this.sectionPostions, function(position) {
if ((inertialVelocity[axis] > 0 && position[posName] > scrollPos) ||
(inertialVelocity[axis] < 0 && position[posName] < scrollPos)) {
return Math.abs(position.top - scrollTop) + Math.abs(position.left - scrollLeft);
}
return Infinity;
});
nearest = Math.min.apply(Math, distances);
if (nearest !== Infinity) {
destination = this.sectionPostions[$.inArray(nearest, distances)][posName];
distance = destination - scrollPos;
friction = false;
}
}
}
inertialVelocity.x *= 13;
inertialVelocity.y *= 13;
this.inertia = setInterval(function() {
if (options.disabled || !self.isScrolling() || self.trigger('flick') === false) {
inertialVelocity.x = 0;
inertialVelocity.y = 0;
clearInterval(self.inertia);
self.scrollBack();
return;
}
if (destination !== undefined) {
var scrollPos = box[scroll]();
if ((distance > 0 && scrollPos > destination) ||
(distance < 0 && scrollPos < destination)) {
// Do scrollback when position pass over the destination
inertialVelocity[axis] = 0;
} else {
inertialVelocity[axis] = options.elasticConstant / 4 * distance;
}
}
$.extend(inertialVelocity, self.velocity(inertialVelocity, friction));
self.scroll(inertialVelocity.x, inertialVelocity.y);
}, 13);
return this;
},
scrollBack: function() {
var self = this,
options = this.options,
inertialVelocity = this.inertialVelocity,
cancel = false;
clearInterval(this.back);
this.back = setInterval(function() {
if (options.disabled || (inertialVelocity.x && inertialVelocity.y)) return;
var velocity = {},
extension = {};
if (self.sections) {
var pos, distances, index, selected,
scrollTop = self.box.scrollTop(),
scrollLeft = self.box.scrollLeft();
// Calculate distances between sections and scroll position
distances = $.map(self.sectionPostions, function(position) {
return Math.abs(position.top - scrollTop) + Math.abs(position.left - scrollLeft);
});
index = $.inArray(Math.min.apply(Math, distances), distances);
pos = self.sectionPostions[index];
extension = {
x: self.hasScroll.x ? scrollLeft - pos.left : 0,
y: self.hasScroll.y ? scrollTop - pos.top : 0
};
if (!extension.x && !extension.y && !self.isScrolling()) {
selected = self.sections.eq(index);
}
} else {
extension = self.stretch();
}
$.each(inertialVelocity, function(axis, v) {
// Don't scroll when inertia scroll is active
velocity[axis] = v ? 0 : -1 * options.elasticConstant * extension[axis];
});
velocity = self.velocity(velocity, false);
if (velocity.x || velocity.y) {
cancel = self.trigger('scrollBack') === false;
}
if ((!extension.x && !extension.y) || cancel) {
if (selected) {
// Reset draggable axis
self.draggableAxis = undefined;
if (!self.selected || self.selected[0] != selected[0]) {
var data = {
newSection: selected,
oldSection: self.selected
};
self.selected = $(selected[0]);
self.trigger('change', null, data);
}
}
clearInterval(self.back);
return;
}
self.scroll(velocity.x, velocity.y);
}, 13);
return this;
},
select: function(index) {
var self = this,
box = this.box,
scrollLeft = box.scrollLeft(),
scrollTop = box.scrollTop(),
inertialVelocity = this.inertialVelocity,
destination, distance;
clearInterval(this.back);
clearInterval(this.inertia);
destination = this.sectionPostions[index];
distance = {
x: destination.left - scrollLeft,
y: destination.top - scrollTop
}
this.inertia = setInterval(function() {
$.each({x:'Left', y:'Top'}, function(axis, posName) {
var scrollPos = box['scroll' + posName]();
posName = posName.toLowerCase();
if ((distance[axis] > 0 && scrollPos > destination[posName]) ||
(distance[axis] < 0 && scrollPos < destination[posName])) {
inertialVelocity[axis] = 0;
} else {
inertialVelocity[axis] = self.options.elasticConstant / 4 * distance[axis];
}
});
if (self.options.disabled || !self.isScrolling()) {
clearInterval(self.inertia);
self.scrollBack();
return;
}
$.extend(inertialVelocity, self.velocity(inertialVelocity, false));
self.scroll(inertialVelocity.x, inertialVelocity.y);
}, 13);
return this;
},
stretch: function() {
var self = this,
stretch = {x: 0, y: 0},
stretchPosition = this.stretchPosition;
$.each({x:['left', 'right', 'Left'], y:['top', 'bottom', 'Top']}, function(axis, posNames) {
var scrollPos;
if (!self.hasScroll[axis]) return;
scrollPos = self.box['scroll' + posNames[2]]();
if (scrollPos < stretchPosition[posNames[0]]) {
stretch[axis] = scrollPos - stretchPosition[posNames[0]];
} else if (scrollPos > stretchPosition[posNames[1]]) {
stretch[axis] = scrollPos - stretchPosition[posNames[1]];
}
});
// Stretch can be negative
return stretch;
},
velocity: function(velocity, friction) {
var self = this, f = {}, _velocity = {};
if (friction !== false) {
f = this.friction(friction);
}
$.each(velocity, function(axis, v) {
v += self.remainder[axis] || 0;
if (f[axis] !== undefined) {
v *= f[axis];
}
// Make sure that the velocity is integer
_velocity[axis] = Math.round(v);
// Save remainders to calculate precise velocity for next method call
self.remainder[axis] = v - _velocity[axis];
});
return _velocity;
},
friction: function(_default) {
var self = this, f = {},
stretch = this.stretch();
if (_default === undefined) _default = 1;
$.each({x:'width', y:'height'}, function(axis, dimension) {
f[axis] = _default;
if (stretch[axis]) {
var padding = self.padding[dimension];
f[axis] *= (padding - Math.abs(stretch[axis])) / padding;
}
});
return f;
},
isScrolling: function() {
return this.inertialVelocity.x || this.inertialVelocity.y;
}
};
flickable.prototype.create.prototype = flickable.prototype;
function hasScroll(elem) {
elem = $(elem);
if ($.inArray(elem.css('overflow'), ['scroll', 'auto']) >= 0) {
return elem.get(0).scrollWidth > elem.get(0).clientWidth ||
elem.get(0).scrollHeight > elem.get(0).clientHeight;
}
return false;
}
function script(elem) {
return $('script', elem).map(function(i, script) {
var type = $(script).attr('type');
return !type || type.toLowerCase() === 'text/javascript' ? script : null;
});
}
})(jQuery, window, Math);