(function() {
window.Microfiche = function(options) { this.initialize(options); return this; };
Microfiche.VERSION = '1.8.3';
CALIBRATE_FIRST_GUESS = 100000;
$.extend(Microfiche.prototype, {
$('.my-slideshow').microfiche();
$('.my-slideshow').microfiche({ cyclic: true, button: false });
$('.my-slideshow').microfiche({ slideByPages: 1 });
The following options can be passed the first time microfiche
is called
on an element.
If true, microfiche wraps around at front and beginning of the slideshow. This option is false by default.
$('.my-slideshow').microfiche({ cyclic: true });
If true, microfiche will create previous/next buttons. This option is true by default.
$('.my-slideshow').microfiche({ buttons: false });
If true, microfiche will create bullets for the pages available. This option is also true by default.
$('.my-slideshow').microfiche({ bullets: false });
The following commands can be run on a microfiche’d element at any point, including in the first call.
Slides n
screenfuls (negative n
goes backwards).
$('.my-slideshow').microfiche({ slideByPages: n });
Slides to the nth
screenful.
$('.my-slideshow').microfiche({ slideToPage: n });
Slides to point x
(rounded and constrained appropriately).
$('.my-slideshow).microfiche({ slideToPoint: x });
Jumps without animation to point x (again, rounded and constrained).
$('.my-slideshow').microfiche({ jumpToPoint: x });
Automatically advances every n
seconds.
$('.my-slideshow').microfiche({ autoplay: n });
Automatically pause autoplay when the user hovers over the carousel.
$('.my-slideshow').microfiche({ autoplay: n, autopause: true });
To refresh an existing Microfiche’s controls and content to adjust
to a new container size, call the refresh
method.
$('.my-slideshow').microfiche({ refresh: true });
Automatically refresh microfiche filmstrip and controls on window
resize event. Set true
to refresh with a 250ms debounce, or specify
a custom debounce rate in ms. The default value is false.
$(‘.my-slideshow’).microfiche({ refreshOnResize: 100 });
Destroys the microfiche instance and clear related events
$(‘my-slideshow’).microfiche({ destroy: true }); // or $(‘my-slideshow’).data(‘microfiche’).destroy();
Defines left, right, or center filmstrip alignment in the event that all items are visible on screen and no scrolling is required.
$(‘.my-slideshow’).microfiche({ noScrollAlign: ‘left’ });
(function() {
window.Microfiche = function(options) { this.initialize(options); return this; };
Microfiche.VERSION = '1.8.3';
CALIBRATE_FIRST_GUESS = 100000;
$.extend(Microfiche.prototype, {
options: {
autoplay : false,
autopause : false,
buttons : true,
bullets : true,
cyclic : false,
keyboard : false,
swipe : true,
clickToAdvance : false,
minDuration : 250,
duration : 500,
maxDuration : 500,
dragThreshold : 25,
elasticity : 0.5,
swipeThreshold : 0.125,
refreshOnResize : false,
prevButtonLabel : '←',
nextButtonLabel : '→',
noScrollAlign : 'left'
},
Rather than relying on the literal position of this.film
,
we keep a tab on the current destination.
x: 0,
Build microfiche in steps.
initialize: function(options) {
this.options = $.extend({}, this.options, options);
this.el = $(options.el);
this.initialContents = this.el.contents();
this.el.data('microfiche', this);
this.createFilm();
this.createScreen();
this.calibrate(CALIBRATE_FIRST_GUESS);
if (this.film.width() <= this.screen.width()) {
this.noScrollAlign(this.options.noScrollAlign);
this.refreshOnResize(this.options.refreshOnResize);
return;
}
this.createControls();
this.enableTouch();
this.enableKeyboard();
this.enableClick();
this.prepareCyclic();
this.run(this.options);
},
We create our film element, which we’ll slide back and forth in the screen. Before appending any extra elements, we detach the existing children, append them to film, and tell them to float so they’ll (hopefully) lay-out nicely along the horizontal.
createFilm: function() {
this.film = $('<div class="microfiche-film">').
css({ position: 'absolute' });
this.el.children().appendTo(this.film).css({ float: 'left' });
this.prepareFilm && this.prepareFilm();
},
The screen is created and appended to our element, then the film is appended to the screen. Screen manually takes its height from film.
createScreen: function() {
this.screen = $('<div class="microfiche-screen">').
css({ position: 'relative', overflow: 'hidden' }).
appendTo(this.el).
append(this.film);
},
Prepare duplicate content at either end, for our cyclic behaviour.
prepareCyclic: function() {
if (!this.options.cyclic) return;
var cloneL = this.film.clone(),
cloneR = this.film.clone(),
w = this.film.width();
cloneL.prependTo(this.film).css({ position: 'absolute', left: -w + 'px' });
cloneR.appendTo(this.film).css({ position: 'absolute', left: w + 'px' });
},
This slightly strange process tries to ensure we don’t get any wrapping
in this.film
, then fixes the dimensions of this.film
and this.screen
.
calibrate: function(width) {
this.screen.width(width);
var w = this.film.width(),
h = this.film.height();
if (w === width) {
return this.calibrate(width * 2);
}
this.film.width(w).height(h);
this.screen.width('auto').height(h);
},
Create prev/next buttons and page bullets.
createControls: function() {
var self = this;
this.controls = $('<span class="microfiche-controls" />').appendTo(this.el);
this.controls.on('click', 'a, button', function(e) { self.didClickControl(e) });
if (this.options.bullets) this.createBullets();
if (this.options.buttons) this.createButtons();
this.updateControls();
},
Create page bullets.
createBullets: function() {
var container = $('<span class="microfiche-bullets" />').appendTo(this.controls);
for (var i = 0; i < this.totalPageCount(); i++) {
$('<button>')
.addClass('microfiche-bullet')
.attr('data-microfiche-page', i)
.data('action', 'slideToPage')
.data('arguments', [i])
.html(i + 1)
.appendTo(container);
}
},
Create prev/next buttons.
createButtons: function() {
$('<button>')
.addClass('microfiche-button microfiche-prev-button')
.attr('rel', 'prev')
.data('action', 'prev')
.data('arguments', [])
.html(this.options.prevButtonLabel)
.prependTo(this.controls);
$('<button>')
.addClass('microfiche-button microfiche-next-button')
.attr('rel', 'next')
.data('action', 'next')
.data('arguments', [])
.html(this.options.nextButtonLabel)
.appendTo(this.controls);
},
Add in the appropriate touch events. This requires a bit of scope-locking.
enableTouch: function() {
if (!this.options.swipe) return;
var self = this;
var thisTouchstart = this.touchstart,
thisTouchmove = this.touchmove,
thisTouchend = this.touchend;
this.touchstart = function() { thisTouchstart.apply(self, arguments) };
this.touchmove = function() { thisTouchmove.apply(self, arguments) };
this.touchend = function() { thisTouchend.apply(self, arguments) };
this.film.on('touchstart', this.touchstart);
},
Add in left-right keyboard events.
enableKeyboard: function() {
if (!this.options.keyboard) return;
var self = this;
this.screen.attr('data-microfiche-keyboard', true);
var thisOnkeydown = this.onkeydown;
this.onkeydown = function() { thisOnkeydown.apply(self, arguments) };
$(document).on('keydown', this.onkeydown);
},
Add in mosuedown event.
enableClick: function() {
if (!this.options.clickToAdvance) return;
var self = this;
var thisOnmousedown = this.onmousedown;
this.onmousedown = function() { thisOnmousedown.apply(self, arguments) };
this.film.on('mousedown', this.onmousedown);
},
When anything in this.controls
is clicked.
didClickControl: function(e) {
e.preventDefault();
var control = $(e.target),
action = control.data('action'),
args = control.data('arguments');
this[action].apply(this, args);
},
When touch starts, record the origin point and time.
touchstart: function(e) {
var touches = e.originalEvent.targetTouches;
if (!touches || touches.length > 1) return;
this.touchState = {
then : new Date(),
ox : touches[0].pageX,
oy : touches[0].pageY,
isDrag : false
}
$(document).on('touchmove', this.touchmove).
on('touchend', this.touchend);
},
Touchmove begins by getting the deltas on both axis.
If we’re not already in drag-mode, we check to see if the horizontal delta is above the treshold. If the vertical delta crosses the threshold, we duck out altogether.
After that, we ask this.film
to follow the touch, and record a few
details about position and velocity for good measure.
touchmove: function(e) {
var t = e.originalEvent.targetTouches[0],
dx = t.pageX - this.touchState.ox,
dy = t.pageY - this.touchState.oy;
if (!this.touchState.isDrag) {
if (Math.abs(dy) >= this.options.dragThreshold) {
this.touchend();
return;
} else if (Math.abs(dx) >= this.options.dragThreshold) {
this.touchState.isDrag = true;
}
}
if (this.touchState.isDrag) {
e.preventDefault();
var now = new Date(),
t = now - this.touchState.then;
this.touchState.vx = (dx - this.touchState.dx) / t;
this.touchState.vy = (dy - this.touchState.dy) / t;
this.touchState.dx = dx;
this.touchState.dy = dy;
this.touchState.then = now;
this.touchState.cx = this.x - dx;
if (!this.options.cyclic) {
if (this.touchState.cx < this.min()) {
var bx = this.min() - this.touchState.cx;
bx = bx * this.options.elasticity;
this.touchState.cx = this.min() - bx;
}
if (this.touchState.cx > this.max()) {
var bx = this.touchState.cx - this.max();
bx = bx * this.options.elasticity;
this.touchState.cx = this.max() + bx;
}
}
this.film.css({
WebkitTransition: 'none',
WebkitTransform: 'translate3d(' + -this.touchState.cx + 'px, 0px, 0px)'
});
}
},
When the touch is finished, we unbind events. If the touch was decided to be a drag, we’ll deduce the new target value for x, ensure Microfiche knows about it, and animate into place.
touchend: function(e) {
$(document).off('touchmove', this.touchmove).
off('touchend', this.touchend);
if (this.touchState.isDrag) {
var dx = this.touchState.dx,
w = this.screenWidth(),
vx = this.touchState.vx,
th = this.options.swipeThreshold;
if (dx <= -w * th) {
this.slideByPages(1, vx);
} else if (dx >= w * th) {
this.slideByPages(-1, vx);
} else {
this.slideByPages(0);
}
}
},
Slide centermost instance of microfiche left / right on key press.
onkeydown: function(e) {
if (e.keyCode !== 37 && e.keyCode !== 39 || !this.isCentral('[data-microfiche-keyboard]')) return;
if (e.keyCode === 37) this.slideByPages(-1);
else if (e.keyCode === 39) this.slideByPages(1);
},
Advance microfiche on mousedown.
onmousedown: function(e) {
this.slideByPages(1);
},
Enable/disable controls based on current position.
updateControls: function() {
if (this.options.bullets) this.updateBullets();
if (this.options.buttons) this.updateButtons();
},
Update selected state of bullets.
updateBullets: function() {
this.controls.find('.microfiche-bullet').removeClass('selected');
this.controls.find('[data-microfiche-page="' + this.currentPageIndex() + '"]').addClass('selected');
},
Update enabled state of prev/next buttons.
updateButtons: function() {
if (this.options.cyclic) return;
this.controls.find('[rel="prev"]').attr('disabled', this.x <= this.min());
this.controls.find('[rel="next"]').attr('disabled', this.x >= this.max());
},
Round x
to a factor of screenWidth
.
round: function(x) {
var w = this.screenWidth();
return Math.round(x / w) * w;
},
Return x
constrained between limits min
and max
.
constrain: function(x, min, max) {
if (min === undefined) min = this.min();
if (max === undefined) max = this.max();
return Math.max(min, Math.min(x, max));
},
Round and constrain x
.
roundAndConstrain: function(x, min, max) {
return this.constrain(this.round(x), min, max);
},
Returns true if the given point is within our upper/lower bounds.
withinBounds: function(x) {
return this.min() <= x && x <= this.max();
},
Returns the lower limit - simply 0.
min: function() {
return 0;
},
Returns the upper limit - the width of this.film
less the width of
this.screen
.
max: function() {
return this.film.width() - this.screenWidth();
},
Returns the current page index.
currentPageIndex: function() {
return Math.round(this.x / this.screenWidth());
},
Returns the number of pages.
totalPageCount: function() {
return Math.ceil(this.film.width() / this.screenWidth());
},
Returns the width of the containing element.
screenWidth: function() {
return this.el.width();
},
Returns true if this microfiche instance is closest to the center of the screen
isCentral: function(selector) {
var closest = $($(selector || '.microfiche-screen').sort(function(a,b){
return Math.abs(1 - (($(window).scrollTop()+$(window).height()/2-$(a).height()/2) / $(a).offset().top)) -
Math.abs(1 - (($(window).scrollTop()+$(window).height()/2-$(b).height()/2) / $(b).offset().top))
})[0]).parent().data('microfiche');
return (closest === this);
},
Perform an instant transition to our new destination.
jump: function() {
this.el.trigger('microfiche:willMove');
this.performJump();
this.updateControls();
this.el.trigger('microfiche:didMove');
},
Default jump transform.
performJump: function() {
this.film.css({ left: -this.x });
},
Sets up environment, but allows the real transition to be overridden.
transition: function(duration) {
var self = this;
if (this.options.cyclic) this.handleWrappingTransition();
if (duration == null) duration = this.options.duration;
var callback = function() { self.afterTransition() };
this.el.trigger('microfiche:willMove');
setTimeout(function() {
self.performTransition(duration, callback);
});
},
Handle what happens in cyclic mode if we’ve slipped off at either end.
handleWrappingTransition: function() {
if (this.x > this.max()) {
this.x = this.min() - this.screenWidth();
if (this.touchState && this.touchState.dx) this.x -= this.touchState.dx;
this.jump();
this.x = this.min();
this.updateControls();
} else if (this.x < this.min()) {
this.x = this.max() + this.screenWidth();
if (this.touchState && this.touchState.dx) this.x -= this.touchState.dx;
this.jump();
this.x = this.max();
this.updateControls();
}
},
Default transition animation.
performTransition: function(duration, callback) {
this.film.stop().animate({ left: -this.x + 'px' }, duration, callback);
},
Called when a transition finishes.
afterTransition: function() {
delete this.touchState;
this.el.trigger('microfiche:didMove');
},
Slides by n
pages. If n
is negative, it will slide in reverse.
Also takes vx
, which is the velocity on the x-axis. This is used
internally by the touch event handlers, but can be used to perform
a faster slide.
slideByPages: function(n, vx) {
var ox = this.x,
w = this.screenWidth();
this.x = this.constrain(Math.round(((this.x / w) + n) * w));
if (this.options.cyclic && this.x == ox) this.x += n * w;
if (vx) {
var duration = this.constrain(
Math.abs((this.x - ox) / vx),
this.options.minDuration,
this.options.maxDuration
);
} else {
var duration = this.options.duration;
}
this.updateControls();
this.transition(duration);
},
Slides to the given page
.
slideToPage: function(page) {
this.x = this.constrain(page * this.screenWidth());
this.updateControls();
this.transition();
},
Animate to the given point (constrained to an acceptable value).
slideToPoint: function(x) {
this.x = this.roundAndConstrain(x);
this.updateControls();
this.transition();
},
Jump to the given page
jumpToPage: function(page) {
this.x = this.constrain(page * this.screenWidth());
this.updateControls();
this.jump();
},
Jump to the given point (constrained to an acceptable value).
jumpToPoint: function(x) {
this.x = this.roundAndConstrain(x);
this.updateControls();
this.jump();
},
Slide to the previous screen’s-worth of slides.
prev: function() {
this.slideByPages(-1);
},
Slide to the next screen’s-worth of slides.
next: function() {
this.slideByPages(1);
},
Automatically call next every n
seconds.
autoplay: function(n) {
if (this.autoplayTimeout) {
clearInterval(this.autoplayTimeout);
delete this.autoplayTimeout;
}
n = +n;
if (isNaN(n) || n <= 0) return;
var self = this;
this.autoplayTimeout = setInterval(function () {
if (!self.pause) {
self.next();
}
}, n * 1000);
},
Pause autoplay when hovering over carousel
autopause: function () {
var self = this;
this.el.hover(function () {
self.pause = true;
}, function () {
self.pause = false;
});
},
Destroy the microfiche instance and clear related events
destroy: function() {
this.el.empty();
this.el.off();
this.clearResizeHandler();
this.el.removeData('microfiche');
},
Refresh the microfiche instance by deleting the contents and associated data, then restoring the original contents, and re-initializing them. This is particularly useful for refreshing microfiche on page or container element resize, as it will redraw the controls if needed.
refresh: function() {
var options = this.el.data('microfiche').options,
contents = this.getContents();
this.destroy();
this.el.append(contents);
new Microfiche($.extend({ el: this.el }, options));
return this.el;
},
getContents: function() {
if(this.contentsChanged()) {
return this.el.html();
} else {
return this.el.data('microfiche').initialContents;
}
},
Have the contents changed?
contentsChanged: function() {
return this.el.find('.microfiche-screen').length === 0;
},
Refresh microfiche automatically on window resize
refreshOnResize: function(delay) {
Overwrite previous settings
this.options.refreshOnResize = delay;
if (this.resizeHandler) this.clearResizeHandler();
if (delay === false) return;
if (delay === true) delay = 250;
var self = this,
oldWindowWidth = 0,
timeout;
Debounce so microfiche will only refresh once for each time a visitor resizes the window
self.resizeHandler = function() {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function() {
self.refresh();
}, delay);
};
$(window).on('resize', function() {
iOS scroll events are sometimes triggered as resize. If the window’s width is not changed by the resize, it must be a scroll, so don’t refresh.
var currentWindowWidth = $(window).width();
if ($(window).width() !== oldWindowWidth) {
oldWindowWidth = currentWindowWidth;
self.resizeHandler();
}
});
},
clearResizeHandler: function() {
$(window).off('resize', this.resizeHandler);
},
noScrollAlign: function(alignment) {
if(this.film.width() > this.screen.width()) return;
switch(alignment) {
case 'center':
this.film.css({
left: '50%',
marginLeft: (this.film.width() / 2 * -1) + 'px',
right: 'auto'
});
break;
case 'right':
this.film.css({
left: 'auto',
marginLeft: 'auto',
right: 0
});
break;
default:
this.film.css({
left: 0,
marginLeft: 'auto',
right: 'auto'
});
}
},
run: function(options) {
for (var key in options) {
var property = this[key];
if ($.isFunction(property)) property.call(this, options[key]);
}
}
});
if (('WebKitCSSMatrix' in window && 'm11' in new WebKitCSSMatrix())) {
If we have webkit transition support, then override prepareFilm
and transition
to take advantage of hardware acceleration.
$.extend(Microfiche.prototype, {
prepareFilm: function() {
this.film.css({ WebkitTransform: 'translate3d(0px, 0px, 0px)' });
},
performTransition: function(duration, callback) {
this.film.one('webkitTransitionEnd', callback).css({
WebkitTransition: '-webkit-transform ' + duration + 'ms',
WebkitTransform: 'translate3d(' + -this.x + 'px, 0px, 0px)'
});
},
performJump: function() {
this.film.css({
WebkitTransition: '-webkit-transform 0ms',
WebkitTransform: 'translate3d(' + -this.x + 'px, 0px, 0px)'
});
}
});
}
jQuery.fn.microfiche = function(options) {
return this.each(function() {
var microfiche = $(this).data('microfiche');
if (microfiche) {
microfiche.run(options);
} else {
new Microfiche($.extend({ el: this }, options));
}
});
}
})();