/*! * jQuery Sly v0.9.6 * https://github.com/Darsain/sly * * Licensed under the MIT license. * http://www.opensource.org/licenses/MIT */ /*jshint eqeqeq: true, noempty: true, strict: true, undef: true, expr: true, smarttabs: true, browser: true */ /*global jQuery:false */ ;(function($, undefined){ 'use strict'; // Plugin names var pluginName = 'sly', namespace = 'plugin_' + pluginName; /** * Plugin class * * @class * @param {Element} frame DOM element of sly container * @param {Object} o Object with plugin options */ function Plugin( frame, o ){ // Alias for this var self = this, // Frame variables $frame = $(frame), $slidee = $frame.children().eq(0), frameSize = 0, slideeSize = 0, pos = { cur: 0, max: 0, min: 0 }, // Scrollbar variables $sb = $(o.scrollBar).eq(0), $handle = $sb.length ? $sb.children().eq(0) : 0, sbSize = 0, handleSize = 0, hPos = { cur: 0, max: 0, min: 0 }, // Pagesbar variables $pb = $(o.pagesBar), $pages = 0, pages = [], // Navigation type booleans basicNav = o.itemNav === 'basic', smartNav = o.itemNav === 'smart', forceCenteredNav = o.itemNav === 'forceCentered', centeredNav = o.itemNav === 'centered' || forceCenteredNav, itemNav = basicNav || smartNav || centeredNav || forceCenteredNav, // Other variables $items = 0, items = [], rel = { firstItem: 0, lastItem: 1, centerItem: 1, activeItem: -1, activePage: 0, items: 0, pages: 0 }, $scrollSource = o.scrollSource ? $( o.scrollSource ) : $frame, $dragSource = o.dragSource ? $( o.dragSource ) : $frame, $prevButton = $(o.prev), $nextButton = $(o.next), $prevPageButton = $(o.prevPage), $nextPageButton = $(o.nextPage), cycleIndex = 0, cycleIsPaused = 0, isDragging = 0, callbacks = {}; /** * (Re)Loading function * * Populates arrays, sets sizes, binds events, ... * * @public */ var load = this.reload = function(){ // Local variables var ignoredMargin = 0, oldPos = $.extend({}, pos); // Clear cycling timeout clearTimeout( cycleIndex ); // Reset global variables frameSize = o.horizontal ? $frame.width() : $frame.height(); sbSize = o.horizontal ? $sb.width() : $sb.height(); slideeSize = o.horizontal ? $slidee.outerWidth() : $slidee.outerHeight(); $items = $slidee.children(); items = []; pages = []; // Set position limits & relatives pos.min = 0; pos.max = slideeSize > frameSize ? slideeSize - frameSize : 0; rel.items = $items.length; // Sizes & offsets logic, but only when needed if( itemNav ){ var marginStart = getPx( $items, o.horizontal ? 'marginLeft' : 'marginTop' ), marginEnd = getPx( $items.slice(-1), o.horizontal ? 'marginRight' : 'marginBottom' ), centerOffset = 0, paddingStart = getPx( $slidee, o.horizontal ? 'paddingLeft' : 'paddingTop' ), paddingEnd = getPx( $slidee, o.horizontal ? 'paddingRight' : 'paddingBottom' ), areFloated = $items.css('float') !== 'none'; // Update ignored margin ignoredMargin = marginStart ? 0 : marginEnd; // Reset slideeSize slideeSize = 0; // Iterate through items $items.each(function(i,e){ // Item var item = $(e), itemSize = o.horizontal ? item.outerWidth(true) : item.outerHeight(true), marginTop = getPx( item, 'marginTop' ), marginBottom = getPx( item, 'marginBottom'), marginLeft = getPx( item, 'marginLeft'), marginRight = getPx( item, 'marginRight'), itemObj = { size: itemSize, offStart: slideeSize - ( !i || o.horizontal ? 0 : marginTop ), offCenter: slideeSize - Math.round( frameSize / 2 - itemSize / 2 ), offEnd: slideeSize - frameSize + itemSize - ( marginStart ? 0 : marginRight ), margins: { top: marginTop, bottom: marginBottom, left: marginLeft, right: marginRight } }; // Account for centerOffset & slidee padding if( !i ){ centerOffset = -( forceCenteredNav ? Math.round( frameSize / 2 - itemSize / 2 ) : 0 ) + paddingStart; slideeSize += paddingStart; } // Increment slidee size for size of the active element slideeSize += itemSize; // Try to account for vertical margin collapsing in vertical mode // It's not bulletproof, but should work in 99% of cases if( !o.horizontal && !areFloated ){ // Subtract smaller margin, but only when top margin is not 0, and this is not the first element if( marginBottom && marginTop && i > 0 ){ slideeSize -= marginTop < marginBottom ? marginTop : marginBottom; } } // Things to be done at last item if( i === $items.length - 1 ){ slideeSize += paddingEnd; } // Add item object to items array items.push(itemObj); }); // Resize slidee $slidee.css( o.horizontal ? { width: slideeSize+'px' } : { height: slideeSize+'px' } ); // Adjust slidee size for last margin slideeSize -= ignoredMargin; // Set limits pos.min = centerOffset; pos.max = forceCenteredNav ? items[items.length-1].offCenter : slideeSize > frameSize ? slideeSize - frameSize : 0; // Fix overflowing activeItem rel.activeItem >= items.length && self.activate( items.length-1 ); } // Assign relative position indexes assignRelatives(); // Scrollbar if( $handle ){ // Stretch scrollbar handle to represent the visible area handleSize = o.dynamicHandle ? Math.round( sbSize * frameSize / slideeSize ) : o.horizontal ? $handle.width() : $handle.height(); handleSize = handleSize > sbSize ? sbSize : handleSize; handleSize = handleSize < o.minHandleSize ? o.minHandleSize : handleSize; hPos.max = sbSize - handleSize; // Resize handle $handle.css( o.horizontal ? { width: handleSize+'px' } : { height: handleSize+'px' } ); } // Pages var tempPagePos = 0, pagesHtml = '', pageIndex = 0; // Populate pages array if( forceCenteredNav ){ pages = $.map( items, function( o ){ return o.offCenter; } ); } else { while( tempPagePos - frameSize < pos.max ){ var pagePos = tempPagePos > pos.max ? pos.max : tempPagePos; pages.push( pagePos ); tempPagePos += frameSize; // When item navigation, and last page is smaller than half of the last item size, // adjust the last page position to pos.max and break the loop if( tempPagePos > pos.max && itemNav && pos.max - pagePos < ( items[items.length-1].size - ignoredMargin ) / 2 ){ pages[pages.length-1] = pos.max; break; } } } // Pages bar if( $pb.length ){ for( var i = 0; i < pages.length; i++ ){ pagesHtml += o.pageBuilder( pageIndex++ ); } // Bind page navigation, append to pagesbar, and save to $pages variable $pages = $(pagesHtml).bind('click.' + namespace, function(){ self.activatePage( $pages.index(this) ); }).appendTo( $pb.empty() ); } // Bind activating to items $items.unbind('.' + namespace).bind('mouseup.' + namespace, function(e){ e.which === 1 && !isDragging && self.activate( this ); }); // Fix overflowing pos.cur < pos.min && slide( pos.min ); pos.cur > pos.max && slide( pos.max ); // Extend relative variables object with some useful info rel.pages = pages.length; rel.slideeSize = slideeSize; rel.frameSize = frameSize; rel.sbSize = sbSize; rel.handleSize = handleSize; // Synchronize scrollbar syncBars(0); // Disable buttons disableButtons(); // Automatic cycling if( itemNav && o.cycleBy ){ var pauseEvents = 'mouseenter.' + namespace + ' mouseleave.' + namespace; // Pause on hover o.pauseOnHover && $frame.unbind(pauseEvents).bind(pauseEvents, function(e){ !cycleIsPaused && self.cycle( e.type === 'mouseenter', 1 ); }); // Initiate cycling self.cycle( o.startPaused ); } // Trigger :load event $frame.trigger( pluginName + ':load', [ $.extend({}, pos, { old: oldPos }), $items, rel ] ); }; /** * Slide the slidee * * @private * * @param {Int} newPos New slidee position in relation to frame * @param {Bool} align Whetner to Align elements to the frame border * @param {Int} speed Animation speed in milliseconds */ function slide( newPos, align, speed ){ speed = isNumber( speed ) ? speed : o.speed; // Align items if( align && itemNav ){ var tempRel = getRelatives( newPos ); if( centeredNav ){ newPos = items[tempRel.centerItem].offCenter; self[ forceCenteredNav ? 'activate' : 'toCenter']( tempRel.centerItem, 1 ); } else if( newPos > pos.min && newPos < pos.max ){ newPos = items[tempRel.firstItem].offStart; } } // Fix overflowing position if( !isDragging || !o.elasticBounds ){ newPos = newPos < pos.min ? pos.min : newPos; newPos = newPos > pos.max ? pos.max : newPos; } // Stop if position has not changed if( newPos === pos.cur ) { return; } else { pos.cur = newPos; } // Reassign relative indexes assignRelatives(); // Add disabled classes disableButtons(); // halt ongoing animations stop(); // Trigger :move event !isDragging && $frame.trigger( pluginName + ':move', [ pos, $items, rel ] ); var newProp = o.horizontal ? { left: -pos.cur+'px' } : { top: -pos.cur+'px' }; // Slidee move if( speed > 16 ){ $slidee.animate( newProp, speed, isDragging ? 'swing' : o.easing, function(e){ // Trigger :moveEnd event !isDragging && $frame.trigger( pluginName + ':moveEnd', [ pos, $items, rel ] ); }); } else { $slidee.css( newProp ); // Trigger :moveEnd event !isDragging && $frame.trigger( pluginName + ':moveEnd', [ pos, $items, rel ] ); } } /** * Synchronizes scrollbar & pagesbar positions with the slidee * * @private * * @param {Int} speed Animation speed for scrollbar synchronization */ function syncBars( speed ){ // Scrollbar synchronization if ($handle) { hPos.cur = Math.round( ( pos.cur - pos.min ) / ( pos.max - pos.min ) * hPos.max ); hPos.cur = hPos.cur < hPos.min ? hPos.min : hPos.cur > hPos.max ? hPos.max : hPos.cur; $handle.stop().animate( o.horizontal ? { left: hPos.cur+'px' } : { top: hPos.cur+'px' }, isNumber(speed) ? speed : o.speed, o.easing ); } // Pagesbar synchronization syncPages(); } /** * Synchronizes pagesbar * * @private */ function syncPages(){ if (!$pages.length) { return; } // Classes $pages.removeClass(o.activeClass).eq(rel.activePage).addClass(o.activeClass); } /** * Activate previous item * * @public */ this.prev = function(){ self.activate( rel.activeItem - 1 ); }; /** * Activate next item * * @public */ this.next = function(){ self.activate( rel.activeItem + 1 ); }; /** * Activate previous page * * @public */ this.prevPage = function(){ self.activatePage( rel.activePage - 1 ); }; /** * Activate next page * * @public */ this.nextPage = function(){ self.activatePage( rel.activePage + 1 ); }; /** * Stop ongoing animations * * @private */ function stop(){ $slidee.add($handle).stop(); } /** * Animate element or the whole slidee to the start of the frame * * @public * * @param {Element|Int} el DOM element, or index of element in items array */ this.toStart = function( el ){ if( itemNav ){ var index = getIndex( el ); if( el === undefined ){ slide( pos.min, 1 ); } else if( index !== -1 ){ // You can't align items to the start of the frame when centeredNav is enabled if (centeredNav) { return; } index !== -1 && slide( items[index].offStart ); } } else { if( el === undefined ){ slide( pos.min ); } else { var $el = $slidee.find(el).eq(0); if( $el.length ){ var offset = o.horizontal ? $el.offset().left - $slidee.offset().left : $el.offset().top - $slidee.offset().top; slide( offset ); } } } syncBars(); }; /** * Animate element or the whole slidee to the end of the frame * * @public * * @param {Element|Int} el DOM element, or index of element in items array */ this.toEnd = function( el ){ if( itemNav ){ var index = getIndex( el ); if( el === undefined ){ slide( pos.max, 1 ); } else if( index !== -1 ){ // You can't align items to the end of the frame when centeredNav is enabled if (centeredNav) { return; } slide( items[index].offEnd ); } } else { if( el === undefined ){ slide( pos.max ); } else { var $el = $slidee.find(el).eq(0); if( $el.length ){ var offset = o.horizontal ? $el.offset().left - $slidee.offset().left : $el.offset().top - $slidee.offset().top; slide( offset - frameSize + $el[o.horizontal ? 'outerWidth' : 'outerHeight']() ); } } } syncBars(); }; /** * Animate element or the whole slidee to the center of the frame * * @public * * @param {Element|Int} el DOM element, or index of element in items array */ this.toCenter = function( el ){ if( itemNav ){ var index = getIndex( el ); if( el === undefined ){ slide( Math.round( pos.max / 2 + pos.min / 2 ), 1 ); } else if( index !== -1 ){ slide( items[index].offCenter ); forceCenteredNav && self.activate( index, 1 ); } } else { if( el === undefined ){ slide( Math.round( pos.max / 2 ) ); } else { var $el = $slidee.find(el).eq(0); if( $el.length ){ var offset = o.horizontal ? $el.offset().left - $slidee.offset().left : $el.offset().top - $slidee.offset().top; slide( offset - frameSize / 2 + $el[o.horizontal ? 'outerWidth' : 'outerHeight']() / 2 ); } } } syncBars(); }; /** * Get an index of the element * * @private * * @param {Element|Int} el DOM element, or index of element in items array */ function getIndex( el ){ return isNumber(el) ? el < 0 ? 0 : el > items.length-1 ? items.length-1 : el : el === undefined ? -1 : $items.index( el ); } /** * Parse style to pixels * * @private * * @param {Object} $item jQuery object with element * @param {Property} property Property to get the pixels from */ function getPx( $item, property ){ return parseInt( $item.css( property ), 10 ); } /** * Activates an element * * Element is positioned to one of the sides of the frame, based on it's current position. * If the element is close to the right frame border, it will be animated to the start of the left border, * and vice versa. This helps user to navigate through the elements only by clicking on them, without * the need for navigation buttons, scrolling, or keyboard arrows. * * @public * * @param {Element|Int} el DOM element, or index of element in items array * @param {Bool} noReposition Activate item without repositioning it */ this.activate = function( el, noReposition ){ if (!itemNav || el === undefined) { return; } var index = getIndex( el ), oldActive = rel.activeItem; // Update activeItem index rel.activeItem = index; // Add active class to the active element $items.removeClass(o.activeClass).eq(index).addClass(o.activeClass); // Trigget :active event if a new element is being activated index !== oldActive && $items.eq( index ).trigger( pluginName + ':active', [ $items, rel ] ); if( !noReposition ){ // When centeredNav is enabled, center the element if( centeredNav ){ self.toCenter( index ); // Otherwise determine where to position the element } else if( smartNav ) { // If activated element is currently on the far right side of the frame, assume that // user is moving forward and animate it to the start of the visible frame, and vice versa if (index >= rel.lastItem) { self.toStart(index); } else if (index <= rel.firstItem) { self.toEnd(index); } } } // Add disabled classes disableButtons(); }; /** * Activates a page * * @public * * @param {Int} index Page index, starting from 0 */ this.activatePage = function( index ){ // Fix overflowing index = index < 0 ? 0 : index >= pages.length ? pages.length-1 : index; slide( pages[index], itemNav ); syncBars(); }; /** * Return relative positions of items based on their location within visible frame * * @private * * @param {Int} sPos Position of slidee */ function getRelatives( sPos ){ var newRel = {}, centerOffset = forceCenteredNav ? 0 : frameSize / 2; // Determine active page for( var p = 0; p < pages.length; p++ ){ if( sPos >= pos.max || p === pages.length - 1 ){ newRel.activePage = pages.length - 1; break; } if( sPos <= pages[p] + centerOffset ){ newRel.activePage = p; break; } } // Relative item indexes if( itemNav ){ var first = false, last = false, center = false; /* From start */ for( var i=0; i < items.length; i++ ){ // First item if (first === false && sPos <= items[i].offStart) { first = i; } // Centered item if (center === false && sPos - items[i].size / 2 <= items[i].offCenter) { center = i; } // Last item if (i === items.length - 1 || (last === false && sPos < items[i + 1].offEnd)) { last = i; } // Terminate if all are assigned if (last !== false) { break; } } // Safe assignment, just to be sure the false won't be returned newRel.firstItem = isNumber( first ) ? first : 0; newRel.centerItem = isNumber( center ) ? center : newRel.firstItem; newRel.lastItem = isNumber( last ) ? last : newRel.centerItem; } return newRel; } /** * Assign element indexes to the relative positions * * @private */ function assignRelatives(){ $.extend( rel, getRelatives( pos.cur ) ); } /** * Disable buttons when needed * * Adds disabledClass, and when the button is