/** * tools.scrollable 1.1.2 - Scroll your HTML with eye candy. * * Copyright (c) 2009 Tero Piirainen * http://flowplayer.org/tools/scrollable.html * * Dual licensed under MIT and GPL 2+ licenses * http://www.opensource.org/licenses * * Launch : March 2008 * Date: ${date} * Revision: ${revision} */ (function($) { // static constructs $.tools = $.tools || {}; $.tools.scrollable = { version: '1.1.2', conf: { // basics size: 4, vertical: false, speed: 400, keyboard: false, // by default this is the same as size keyboardSteps: null, // other disabledClass: 'disabled', hoverClass: null, clickable: false, activeClass: 'active', easing: 'swing', loop: false, items: '.items', item: null, // navigational elements prev: '.prev', next: '.next', prevPage: '.prevPage', nextPage: '.nextPage', api: false // CALLBACKS: onBeforeSeek, onSeek, onReload } }; var current; // constructor function Scrollable(root, conf) { // current instance var self = this, $self = $(this), horizontal = !conf.vertical, wrap = root.children(), index = 0, forward; if (!current) { current = self; } // bind all callbacks from configuration $.each(conf, function(name, fn) { if ($.isFunction(fn)) { $self.bind(name, fn); } }); if (wrap.length > 1) { wrap = $(conf.items, root); } // navigational items can be anywhere when globalNav = true function find(query) { var els = $(query); return conf.globalNav ? els : root.parent().find(query); } // to be used by plugins root.data("finder", find); // get handle to navigational elements var prev = find(conf.prev), next = find(conf.next), prevPage = find(conf.prevPage), nextPage = find(conf.nextPage); // methods $.extend(self, { getIndex: function() { return index; }, getClickIndex: function() { var items = self.getItems(); return items.index(items.filter("." + conf.activeClass)); }, getConf: function() { return conf; }, getSize: function() { return self.getItems().size(); }, getPageAmount: function() { return Math.ceil(this.getSize() / conf.size); }, getPageIndex: function() { return Math.ceil(index / conf.size); }, getNaviButtons: function() { return prev.add(next).add(prevPage).add(nextPage); }, getRoot: function() { return root; }, getItemWrap: function() { return wrap; }, getItems: function() { return wrap.children(conf.item); }, getVisibleItems: function() { return self.getItems().slice(index, index + conf.size); }, /* all seeking functions depend on this */ seekTo: function(i, time, fn) { if (i < 0) { i = 0; } // nothing happens if (index === i) { return self; } // function given as second argument if ($.isFunction(time)) { fn = time; } // seeking exceeds the end if (i > self.getSize() - conf.size) { return conf.loop ? self.begin() : this.end(); } var item = self.getItems().eq(i); if (!item.length) { return self; } // onBeforeSeek var e = $.Event("onBeforeSeek"); $self.trigger(e, [i]); if (e.isDefaultPrevented()) { return self; } // get the (possibly altered) speed if (time === undefined || $.isFunction(time)) { time = conf.speed; } function callback() { if (fn) { fn.call(self, i); } $self.trigger("onSeek", [i]); } if (horizontal) { wrap.animate({left: -item.position().left}, time, conf.easing, callback); } else { wrap.animate({top: -item.position().top}, time, conf.easing, callback); } current = self; index = i; // onStart e = $.Event("onStart"); $self.trigger(e, [i]); if (e.isDefaultPrevented()) { return self; } /* default behaviour */ // prev/next buttons disabled flags prev.add(prevPage).toggleClass(conf.disabledClass, i === 0); next.add(nextPage).toggleClass(conf.disabledClass, i >= self.getSize() - conf.size); return self; }, move: function(offset, time, fn) { forward = offset > 0; return this.seekTo(index + offset, time, fn); }, next: function(time, fn) { return this.move(1, time, fn); }, prev: function(time, fn) { return this.move(-1, time, fn); }, movePage: function(offset, time, fn) { forward = offset > 0; var steps = conf.size * offset; var i = index % conf.size; if (i > 0) { steps += (offset > 0 ? -i : conf.size - i); } return this.move(steps, time, fn); }, prevPage: function(time, fn) { return this.movePage(-1, time, fn); }, nextPage: function(time, fn) { return this.movePage(1, time, fn); }, setPage: function(page, time, fn) { return this.seekTo(page * conf.size, time, fn); }, begin: function(time, fn) { forward = false; return this.seekTo(0, time, fn); }, end: function(time, fn) { forward = true; var to = this.getSize() - conf.size; return to > 0 ? this.seekTo(to, time, fn) : self; }, reload: function() { $self.trigger("onReload"); return self; }, focus: function() { current = self; return self; }, click: function(i) { var item = self.getItems().eq(i), klass = conf.activeClass, size = conf.size; // check that i is sane if (i < 0 || i >= self.getSize()) { return self; } // size == 1 if (size == 1) { if (conf.loop) { return self.next(); } if (i === 0 || i == self.getSize() -1) { forward = (forward === undefined) ? true : !forward; } return forward === false ? self.prev() : self.next(); } // size == 2 if (size == 2) { if (i == index) { i--; } self.getItems().removeClass(klass); item.addClass(klass); return self.seekTo(i, time, fn); } if (!item.hasClass(klass)) { self.getItems().removeClass(klass); item.addClass(klass); var delta = Math.floor(size / 2); var to = i - delta; // next to last item must work if (to > self.getSize() - size) { to = self.getSize() - size; } if (to !== i) { return self.seekTo(to); } } return self; }, // bind / unbind bind: function(name, fn) { $self.bind(name, fn); return self; }, unbind: function(name) { $self.unbind(name); return self; } }); // callbacks $.each("onBeforeSeek,onStart,onSeek,onReload".split(","), function(i, ev) { self[ev] = function(fn) { return self.bind(ev, fn); }; }); // prev button prev.addClass(conf.disabledClass).click(function() { self.prev(); }); // next button next.click(function() { self.next(); }); // prev page button nextPage.click(function() { self.nextPage(); }); if (self.getSize() < conf.size) { next.add(nextPage).addClass(conf.disabledClass); } // next page button prevPage.addClass(conf.disabledClass).click(function() { self.prevPage(); }); // hover var hc = conf.hoverClass, keyId = "keydown." + Math.random().toString().substring(10); self.onReload(function() { // hovering if (hc) { self.getItems().hover(function() { $(this).addClass(hc); }, function() { $(this).removeClass(hc); }); } // clickable if (conf.clickable) { self.getItems().each(function(i) { $(this).unbind("click.scrollable").bind("click.scrollable", function(e) { if ($(e.target).is("a")) { return; } return self.click(i); }); }); } // keyboard if (conf.keyboard) { // keyboard works on one instance at the time. thus we need to unbind first $(document).unbind(keyId).bind(keyId, function(evt) { // do nothing with CTRL / ALT buttons if (evt.altKey || evt.ctrlKey) { return; } // do nothing for unstatic and unfocused instances if (conf.keyboard != 'static' && current != self) { return; } var s = conf.keyboardSteps; if (horizontal && (evt.keyCode == 37 || evt.keyCode == 39)) { self.move(evt.keyCode == 37 ? -s : s); return evt.preventDefault(); } if (!horizontal && (evt.keyCode == 38 || evt.keyCode == 40)) { self.move(evt.keyCode == 38 ? -s : s); return evt.preventDefault(); } return true; }); } else { $(document).unbind(keyId); } }); self.reload(); } // jQuery plugin implementation $.fn.scrollable = function(conf) { // already constructed --> return API var el = this.eq(typeof conf == 'number' ? conf : 0).data("scrollable"); if (el) { return el; } var globals = $.extend({}, $.tools.scrollable.conf); conf = $.extend(globals, conf); conf.keyboardSteps = conf.keyboardSteps || conf.size; this.each(function() { el = new Scrollable($(this), conf); $(this).data("scrollable", el); }); return conf.api ? el: this; }; })(jQuery);