/*! * sly 1.6.1 - 8th Aug 2015 * https://github.com/darsain/sly * * Licensed under the MIT license. * http://opensource.org/licenses/MIT */ ;(function ($, w, undefined) { 'use strict'; var pluginName = 'sly'; var className = 'Sly'; var namespace = pluginName; // Local WindowAnimationTiming interface var cAF = w.cancelAnimationFrame || w.cancelRequestAnimationFrame; var rAF = w.requestAnimationFrame; // Support indicators var transform, gpuAcceleration; // Other global values var $doc = $(document); var dragInitEvents = 'touchstart.' + namespace + ' mousedown.' + namespace; var dragMouseEvents = 'mousemove.' + namespace + ' mouseup.' + namespace; var dragTouchEvents = 'touchmove.' + namespace + ' touchend.' + namespace; var wheelEvent = (document.implementation.hasFeature('Event.wheel', '3.0') ? 'wheel.' : 'mousewheel.') + namespace; var clickEvent = 'click.' + namespace; var mouseDownEvent = 'mousedown.' + namespace; var interactiveElements = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA']; var tmpArray = []; var time; // Math shorthands var abs = Math.abs; var sqrt = Math.sqrt; var pow = Math.pow; var round = Math.round; var max = Math.max; var min = Math.min; // Keep track of last fired global wheel event var lastGlobalWheel = 0; $doc.on(wheelEvent, function (event) { var sly = event.originalEvent[namespace]; var time = +new Date(); // Update last global wheel time, but only when event didn't originate // in Sly frame, or the origin was less than scrollHijack time ago if (!sly || sly.options.scrollHijack < time - lastGlobalWheel) lastGlobalWheel = time; }); /** * Sly. * * @class * * @param {Element} frame DOM element of sly container. * @param {Object} options Object with options. * @param {Object} callbackMap Callbacks map. */ function Sly(frame, options, callbackMap) { if (!(this instanceof Sly)) return new Sly(frame, options, callbackMap); // Extend options var o = $.extend({}, Sly.defaults, options); // Private variables var self = this; var parallax = isNumber(frame); // Frame var $frame = $(frame); var $slidee = o.slidee ? $(o.slidee).eq(0) : $frame.children().eq(0); var frameSize = 0; var slideeSize = 0; var pos = { start: 0, center: 0, end: 0, cur: 0, dest: 0 }; // Scrollbar var $sb = $(o.scrollBar).eq(0); var $handle = $sb.children().eq(0); var sbSize = 0; var handleSize = 0; var hPos = { start: 0, end: 0, cur: 0 }; // Pagesbar var $pb = $(o.pagesBar); var $pages = 0; var pages = []; // Items var $items = 0; var items = []; var rel = { firstItem: 0, lastItem: 0, centerItem: 0, activeItem: null, activePage: 0 }; // Styles var frameStyles = new StyleRestorer($frame[0]); var slideeStyles = new StyleRestorer($slidee[0]); var sbStyles = new StyleRestorer($sb[0]); var handleStyles = new StyleRestorer($handle[0]); // Navigation type booleans var basicNav = o.itemNav === 'basic'; var forceCenteredNav = o.itemNav === 'forceCentered'; var centeredNav = o.itemNav === 'centered' || forceCenteredNav; var itemNav = !parallax && (basicNav || centeredNav || forceCenteredNav); // Miscellaneous var $scrollSource = o.scrollSource ? $(o.scrollSource) : $frame; var $dragSource = o.dragSource ? $(o.dragSource) : $frame; var $forwardButton = $(o.forward); var $backwardButton = $(o.backward); var $prevButton = $(o.prev); var $nextButton = $(o.next); var $prevPageButton = $(o.prevPage); var $nextPageButton = $(o.nextPage); var callbacks = {}; var last = {}; var animation = {}; var move = {}; var dragging = { released: 1 }; var scrolling = { last: 0, delta: 0, resetTime: 200 }; var renderID = 0; var historyID = 0; var cycleID = 0; var continuousID = 0; var i, l; // Normalizing frame if (!parallax) { frame = $frame[0]; } // Expose properties self.initialized = 0; self.frame = frame; self.slidee = $slidee[0]; self.pos = pos; self.rel = rel; self.items = items; self.pages = pages; self.isPaused = 0; self.options = o; self.dragging = dragging; /** * Loading function. * * Populate arrays, set sizes, bind events, ... * * @param {Boolean} [isInit] Whether load is called from within self.init(). * @return {Void} */ function load(isInit) { // Local variables var lastItemsCount = 0; var lastPagesCount = pages.length; // Save old position pos.old = $.extend({}, pos); // Reset global variables frameSize = parallax ? 0 : $frame[o.horizontal ? 'width' : 'height'](); sbSize = $sb[o.horizontal ? 'width' : 'height'](); slideeSize = parallax ? frame : $slidee[o.horizontal ? 'outerWidth' : 'outerHeight'](); pages.length = 0; // Set position limits & relatives pos.start = 0; pos.end = max(slideeSize - frameSize, 0); // Sizes & offsets for item based navigations if (itemNav) { // Save the number of current items lastItemsCount = items.length; // Reset itemNav related variables $items = $slidee.children(o.itemSelector); items.length = 0; // Needed variables var paddingStart = getPx($slidee, o.horizontal ? 'paddingLeft' : 'paddingTop'); var paddingEnd = getPx($slidee, o.horizontal ? 'paddingRight' : 'paddingBottom'); var borderBox = $($items).css('boxSizing') === 'border-box'; var areFloated = $items.css('float') !== 'none'; var ignoredMargin = 0; var lastItemIndex = $items.length - 1; var lastItem; // Reset slideeSize slideeSize = 0; // Iterate through items $items.each(function (i, element) { // Item var $item = $(element); var rect = element.getBoundingClientRect(); var itemSize = round(o.horizontal ? rect.width || rect.right - rect.left : rect.height || rect.bottom - rect.top); var itemMarginStart = getPx($item, o.horizontal ? 'marginLeft' : 'marginTop'); var itemMarginEnd = getPx($item, o.horizontal ? 'marginRight' : 'marginBottom'); var itemSizeFull = itemSize + itemMarginStart + itemMarginEnd; var singleSpaced = !itemMarginStart || !itemMarginEnd; var item = {}; item.el = element; item.size = singleSpaced ? itemSize : itemSizeFull; item.half = item.size / 2; item.start = slideeSize + (singleSpaced ? itemMarginStart : 0); item.center = item.start - round(frameSize / 2 - item.size / 2); item.end = item.start - frameSize + item.size; // Account for slidee padding if (!i) { slideeSize += paddingStart; } // Increment slidee size for size of the active element slideeSize += itemSizeFull; // 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 (itemMarginEnd && itemMarginStart && i > 0) { slideeSize -= min(itemMarginStart, itemMarginEnd); } } // Things to be done on last item if (i === lastItemIndex) { item.end += paddingEnd; slideeSize += paddingEnd; ignoredMargin = singleSpaced ? itemMarginEnd : 0; } // Add item object to items array items.push(item); lastItem = item; }); // Resize SLIDEE to fit all items $slidee[0].style[o.horizontal ? 'width' : 'height'] = (borderBox ? slideeSize: slideeSize - paddingStart - paddingEnd) + 'px'; // Adjust internal SLIDEE size for last margin slideeSize -= ignoredMargin; // Set limits if (items.length) { pos.start = items[0][forceCenteredNav ? 'center' : 'start']; pos.end = forceCenteredNav ? lastItem.center : frameSize < slideeSize ? lastItem.end : pos.start; } else { pos.start = pos.end = 0; } } // Calculate SLIDEE center position pos.center = round(pos.end / 2 + pos.start / 2); // Update relative positions updateRelatives(); // Scrollbar if ($handle.length && sbSize > 0) { // Stretch scrollbar handle to represent the visible area if (o.dynamicHandle) { handleSize = pos.start === pos.end ? sbSize : round(sbSize * frameSize / slideeSize); handleSize = within(handleSize, o.minHandleSize, sbSize); $handle[0].style[o.horizontal ? 'width' : 'height'] = handleSize + 'px'; } else { handleSize = $handle[o.horizontal ? 'outerWidth' : 'outerHeight'](); } hPos.end = sbSize - handleSize; if (!renderID) { syncScrollbar(); } } // Pages if (!parallax && frameSize > 0) { var tempPagePos = pos.start; var pagesHtml = ''; // Populate pages array if (itemNav) { $.each(items, function (i, item) { if (forceCenteredNav) { pages.push(item.center); } else if (item.start + item.size > tempPagePos && tempPagePos <= pos.end) { tempPagePos = item.start; pages.push(tempPagePos); tempPagePos += frameSize; if (tempPagePos > pos.end && tempPagePos < pos.end + frameSize) { pages.push(pos.end); } } }); } else { while (tempPagePos - frameSize < pos.end) { pages.push(tempPagePos); tempPagePos += frameSize; } } // Pages bar if ($pb[0] && lastPagesCount !== pages.length) { for (var i = 0; i < pages.length; i++) { pagesHtml += o.pageBuilder.call(self, i); } $pages = $pb.html(pagesHtml).children(); $pages.eq(rel.activePage).addClass(o.activeClass); } } // Extend relative variables object with some useful info rel.slideeSize = slideeSize; rel.frameSize = frameSize; rel.sbSize = sbSize; rel.handleSize = handleSize; // Activate requested position if (itemNav) { if (isInit && o.startAt != null) { activate(o.startAt); self[centeredNav ? 'toCenter' : 'toStart'](o.startAt); } // Fix possible overflowing var activeItem = items[rel.activeItem]; slideTo(centeredNav && activeItem ? activeItem.center : within(pos.dest, pos.start, pos.end)); } else { if (isInit) { if (o.startAt != null) slideTo(o.startAt, 1); } else { // Fix possible overflowing slideTo(within(pos.dest, pos.start, pos.end)); } } // Trigger load event trigger('load'); } self.reload = function () { load(); }; /** * Animate to a position. * * @param {Int} newPos New position. * @param {Bool} immediate Reposition immediately without an animation. * @param {Bool} dontAlign Do not align items, use the raw position passed in first argument. * * @return {Void} */ function slideTo(newPos, immediate, dontAlign) { // Align items if (itemNav && dragging.released && !dontAlign) { var tempRel = getRelatives(newPos); var isNotBordering = newPos > pos.start && newPos < pos.end; if (centeredNav) { if (isNotBordering) { newPos = items[tempRel.centerItem].center; } if (forceCenteredNav && o.activateMiddle) { activate(tempRel.centerItem); } } else if (isNotBordering) { newPos = items[tempRel.firstItem].start; } } // Handle overflowing position limits if (dragging.init && dragging.slidee && o.elasticBounds) { if (newPos > pos.end) { newPos = pos.end + (newPos - pos.end) / 6; } else if (newPos < pos.start) { newPos = pos.start + (newPos - pos.start) / 6; } } else { newPos = within(newPos, pos.start, pos.end); } // Update the animation object animation.start = +new Date(); animation.time = 0; animation.from = pos.cur; animation.to = newPos; animation.delta = newPos - pos.cur; animation.tweesing = dragging.tweese || dragging.init && !dragging.slidee; animation.immediate = !animation.tweesing && (immediate || dragging.init && dragging.slidee || !o.speed); // Reset dragging tweesing request dragging.tweese = 0; // Start animation rendering if (newPos !== pos.dest) { pos.dest = newPos; trigger('change'); if (!renderID) { render(); } } // Reset next cycle timeout resetCycle(); // Synchronize states updateRelatives(); updateButtonsState(); syncPagesbar(); } /** * Render animation frame. * * @return {Void} */ function render() { if (!self.initialized) { return; } // If first render call, wait for next animationFrame if (!renderID) { renderID = rAF(render); if (dragging.released) { trigger('moveStart'); } return; } // If immediate repositioning is requested, don't animate. if (animation.immediate) { pos.cur = animation.to; } // Use tweesing for animations without known end point else if (animation.tweesing) { animation.tweeseDelta = animation.to - pos.cur; // Fuck Zeno's paradox if (abs(animation.tweeseDelta) < 0.1) { pos.cur = animation.to; } else { pos.cur += animation.tweeseDelta * (dragging.released ? o.swingSpeed : o.syncSpeed); } } // Use tweening for basic animations with known end point else { animation.time = min(+new Date() - animation.start, o.speed); pos.cur = animation.from + animation.delta * $.easing[o.easing](animation.time/o.speed, animation.time, 0, 1, o.speed); } // If there is nothing more to render break the rendering loop, otherwise request new animation frame. if (animation.to === pos.cur) { pos.cur = animation.to; dragging.tweese = renderID = 0; } else { renderID = rAF(render); } trigger('move'); // Update SLIDEE position if (!parallax) { if (transform) { $slidee[0].style[transform] = gpuAcceleration + (o.horizontal ? 'translateX' : 'translateY') + '(' + (-pos.cur) + 'px)'; } else { $slidee[0].style[o.horizontal ? 'left' : 'top'] = -round(pos.cur) + 'px'; } } // When animation reached the end, and dragging is not active, trigger moveEnd if (!renderID && dragging.released) { trigger('moveEnd'); } syncScrollbar(); } /** * Synchronizes scrollbar with the SLIDEE. * * @return {Void} */ function syncScrollbar() { if ($handle.length) { hPos.cur = pos.start === pos.end ? 0 : (((dragging.init && !dragging.slidee) ? pos.dest : pos.cur) - pos.start) / (pos.end - pos.start) * hPos.end; hPos.cur = within(round(hPos.cur), hPos.start, hPos.end); if (last.hPos !== hPos.cur) { last.hPos = hPos.cur; if (transform) { $handle[0].style[transform] = gpuAcceleration + (o.horizontal ? 'translateX' : 'translateY') + '(' + hPos.cur + 'px)'; } else { $handle[0].style[o.horizontal ? 'left' : 'top'] = hPos.cur + 'px'; } } } } /** * Synchronizes pagesbar with SLIDEE. * * @return {Void} */ function syncPagesbar() { if ($pages[0] && last.page !== rel.activePage) { last.page = rel.activePage; $pages.removeClass(o.activeClass).eq(rel.activePage).addClass(o.activeClass); trigger('activePage', last.page); } } /** * Returns the position object. * * @param {Mixed} item * * @return {Object} */ self.getPos = function (item) { if (itemNav) { var index = getIndex(item); return index !== -1 ? items[index] : false; } else { var $item = $slidee.find(item).eq(0); if ($item[0]) { var offset = o.horizontal ? $item.offset().left - $slidee.offset().left : $item.offset().top - $slidee.offset().top; var size = $item[o.horizontal ? 'outerWidth' : 'outerHeight'](); return { start: offset, center: offset - frameSize / 2 + size / 2, end: offset - frameSize + size, size: size }; } else { return false; } } }; /** * Continuous move in a specified direction. * * @param {Bool} forward True for forward movement, otherwise it'll go backwards. * @param {Int} speed Movement speed in pixels per frame. Overrides options.moveBy value. * * @return {Void} */ self.moveBy = function (speed) { move.speed = speed; // If already initiated, or there is nowhere to move, abort if (dragging.init || !move.speed || pos.cur === (move.speed > 0 ? pos.end : pos.start)) { return; } // Initiate move object move.lastTime = +new Date(); move.startPos = pos.cur; // Set dragging as initiated continuousInit('button'); dragging.init = 1; // Start movement trigger('moveStart'); cAF(continuousID); moveLoop(); }; /** * Continuous movement loop. * * @return {Void} */ function moveLoop() { // If there is nowhere to move anymore, stop if (!move.speed || pos.cur === (move.speed > 0 ? pos.end : pos.start)) { self.stop(); } // Request new move loop if it hasn't been stopped continuousID = dragging.init ? rAF(moveLoop) : 0; // Update move object move.now = +new Date(); move.pos = pos.cur + (move.now - move.lastTime) / 1000 * move.speed; // Slide slideTo(dragging.init ? move.pos : round(move.pos)); // Normally, this is triggered in render(), but if there // is nothing to render, we have to do it manually here. if (!dragging.init && pos.cur === pos.dest) { trigger('moveEnd'); } // Update times for future iteration move.lastTime = move.now; } /** * Stops continuous movement. * * @return {Void} */ self.stop = function () { if (dragging.source === 'button') { dragging.init = 0; dragging.released = 1; } }; /** * Activate previous item. * * @return {Void} */ self.prev = function () { self.activate(rel.activeItem == null ? 0 : rel.activeItem - 1); }; /** * Activate next item. * * @return {Void} */ self.next = function () { self.activate(rel.activeItem == null ? 0 : rel.activeItem + 1); }; /** * Activate previous page. * * @return {Void} */ self.prevPage = function () { self.activatePage(rel.activePage - 1); }; /** * Activate next page. * * @return {Void} */ self.nextPage = function () { self.activatePage(rel.activePage + 1); }; /** * Slide SLIDEE by amount of pixels. * * @param {Int} delta Pixels/Items. Positive means forward, negative means backward. * @param {Bool} immediate Reposition immediately without an animation. * * @return {Void} */ self.slideBy = function (delta, immediate) { if (!delta) { return; } if (itemNav) { self[centeredNav ? 'toCenter' : 'toStart']( within((centeredNav ? rel.centerItem : rel.firstItem) + o.scrollBy * delta, 0, items.length) ); } else { slideTo(pos.dest + delta, immediate); } }; /** * Animate SLIDEE to a specific position. * * @param {Int} pos New position. * @param {Bool} immediate Reposition immediately without an animation. * * @return {Void} */ self.slideTo = function (pos, immediate) { slideTo(pos, immediate); }; /** * Core method for handling `toLocation` methods. * * @param {String} location * @param {Mixed} item * @param {Bool} immediate * * @return {Void} */ function to(location, item, immediate) { // Optional arguments logic if (type(item) === 'boolean') { immediate = item; item = undefined; } if (item === undefined) { slideTo(pos[location], immediate); } else { // You can't align items to sides of the frame // when centered navigation type is enabled if (centeredNav && location !== 'center') { return; } var itemPos = self.getPos(item); if (itemPos) { slideTo(itemPos[location], immediate, !centeredNav); } } } /** * Animate element or the whole SLIDEE to the start of the frame. * * @param {Mixed} item Item DOM element, or index starting at 0. Omitting will animate SLIDEE. * @param {Bool} immediate Reposition immediately without an animation. * * @return {Void} */ self.toStart = function (item, immediate) { to('start', item, immediate); }; /** * Animate element or the whole SLIDEE to the end of the frame. * * @param {Mixed} item Item DOM element, or index starting at 0. Omitting will animate SLIDEE. * @param {Bool} immediate Reposition immediately without an animation. * * @return {Void} */ self.toEnd = function (item, immediate) { to('end', item, immediate); }; /** * Animate element or the whole SLIDEE to the center of the frame. * * @param {Mixed} item Item DOM element, or index starting at 0. Omitting will animate SLIDEE. * @param {Bool} immediate Reposition immediately without an animation. * * @return {Void} */ self.toCenter = function (item, immediate) { to('center', item, immediate); }; /** * Get the index of an item in SLIDEE. * * @param {Mixed} item Item DOM element. * * @return {Int} Item index, or -1 if not found. */ function getIndex(item) { return item != null ? isNumber(item) ? item >= 0 && item < items.length ? item : -1 : $items.index(item) : -1; } // Expose getIndex without lowering the compressibility of it, // as it is used quite often throughout Sly. self.getIndex = getIndex; /** * Get index of an item in SLIDEE based on a variety of input types. * * @param {Mixed} item DOM element, positive or negative integer. * * @return {Int} Item index, or -1 if not found. */ function getRelativeIndex(item) { return getIndex(isNumber(item) && item < 0 ? item + items.length : item); } /** * Activates an item. * * @param {Mixed} item Item DOM element, or index starting at 0. * * @return {Mixed} Activated item index or false on fail. */ function activate(item, force) { var index = getIndex(item); if (!itemNav || index < 0) { return false; } // Update classes, last active index, and trigger active event only when there // has been a change. Otherwise just return the current active index. if (last.active !== index || force) { // Update classes $items.eq(rel.activeItem).removeClass(o.activeClass); $items.eq(index).addClass(o.activeClass); last.active = rel.activeItem = index; updateButtonsState(); trigger('active', index); } return index; } /** * Activates an item and helps with further navigation when o.smart is enabled. * * @param {Mixed} item Item DOM element, or index starting at 0. * @param {Bool} immediate Whether to reposition immediately in smart navigation. * * @return {Void} */ self.activate = function (item, immediate) { var index = activate(item); // Smart navigation if (o.smart && index !== false) { // When centeredNav is enabled, center the element. // Otherwise, determine where to position the element based on its current position. // If the element is currently on the far end side of the frame, assume that user is // moving forward and animate it to the start of the visible frame, and vice versa. if (centeredNav) { self.toCenter(index, immediate); } else if (index >= rel.lastItem) { self.toStart(index, immediate); } else if (index <= rel.firstItem) { self.toEnd(index, immediate); } else { resetCycle(); } } }; /** * Activates a page. * * @param {Int} index Page index, starting from 0. * @param {Bool} immediate Whether to reposition immediately without animation. * * @return {Void} */ self.activatePage = function (index, immediate) { if (isNumber(index)) { slideTo(pages[within(index, 0, pages.length - 1)], immediate); } }; /** * Return relative positions of items based on their visibility within FRAME. * * @param {Int} slideePos Position of SLIDEE. * * @return {Void} */ function getRelatives(slideePos) { slideePos = within(isNumber(slideePos) ? slideePos : pos.dest, pos.start, pos.end); var relatives = {}; var centerOffset = forceCenteredNav ? 0 : frameSize / 2; // Determine active page if (!parallax) { for (var p = 0, pl = pages.length; p < pl; p++) { if (slideePos >= pos.end || p === pages.length - 1) { relatives.activePage = pages.length - 1; break; } if (slideePos <= pages[p] + centerOffset) { relatives.activePage = p; break; } } } // Relative item indexes if (itemNav) { var first = false; var last = false; var center = false; // From start for (var i = 0, il = items.length; i < il; i++) { // First item if (first === false && slideePos <= items[i].start + items[i].half) { first = i; } // Center item if (center === false && slideePos <= items[i].center + items[i].half) { center = i; } // Last item if (i === il - 1 || slideePos <= items[i].end + items[i].half) { last = i; break; } } // Safe assignment, just to be sure the false won't be returned relatives.firstItem = isNumber(first) ? first : 0; relatives.centerItem = isNumber(center) ? center : relatives.firstItem; relatives.lastItem = isNumber(last) ? last : relatives.centerItem; } return relatives; } /** * Update object with relative positions. * * @param {Int} newPos * * @return {Void} */ function updateRelatives(newPos) { $.extend(rel, getRelatives(newPos)); } /** * Disable navigation buttons when needed. * * Adds disabledClass, and when the button is