This repository has been archived on 2018-10-12. You can view files and clone it, but cannot push or open issues or pull requests.
node-task/public/js/ink.carousel.js

480 lines
17 KiB
JavaScript

/**
* Flexible Carousel
* @module Ink.UI.Carousel_1
* @version 1
*/
Ink.createModule('Ink.UI.Carousel', '1',
['Ink.UI.Common_1', 'Ink.Dom.Event_1', 'Ink.Dom.Css_1', 'Ink.Dom.Element_1', 'Ink.UI.Pagination_1', 'Ink.Dom.Browser_1', 'Ink.Dom.Selector_1'],
function(Common, InkEvent, Css, InkElement, Pagination, Browser/*, Selector*/) {
'use strict';
/*
* TODO:
* keyboardSupport
*/
function limitRange(n, min, max) {
return Math.min(max, Math.max(min, n));
}
var requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
function (cb) {return setTimeout(cb, 1000 / 30); };
/**
* @class Ink.UI.Carousel_1
* @constructor
*
* @param {String|DOMElement} selector DOM element or element id
* @param {Object} [options] Carousel Options
* @param {Integer} [options.autoAdvance] Milliseconds to wait before auto-advancing pages. Set to 0 to disable auto-advance. Defaults to 0.
* @param {String} [options.axis] Axis of the carousel. Set to 'y' for a vertical carousel. Defaults to 'x'.
* @param {Number} [options.initialPage] Initial index page of the carousel. Defaults to 0.
* @param {Boolean} [options.spaceAfterLastSlide=true] If there are not enough slides to fill the full width of the last page, leave white space. Defaults to `true`.
* @param {Boolean} [options.swipe] Enable swipe support if available. Defaults to true.
* @param {Mixed} [options.pagination] Either an ul element to add pagination markup to or an `Ink.UI.Pagination` instance to use.
* @param {Function} [options.onChange] Callback to be called when the page changes.
*
* @sample Ink_UI_Carousel_1.html
*/
function Carousel() {
Common.BaseUIComponent.apply(this, arguments);
}
Carousel._name = 'Carousel_1';
Carousel._optionDefinition = {
autoAdvance: ['Integer', 0],
axis: ['String', 'x'],
initialPage: ['Integer', 0],
spaceAfterLastSlide: ['Boolean', true],
hideLast: ['Boolean', false],
// [3.1.0] Deprecate "center". It is only needed when things are of unknown widths.
center: ['Boolean', false],
keyboardSupport:['Boolean', false],
pagination: ['String', null],
onChange: ['Function', null],
onInit: ['Function', function () {}],
swipe: ['Boolean', true]
// TODO exponential swipe
// TODO specify break point for next page when moving finger
};
Carousel.prototype = {
_init: function () {
this._handlers = {
paginationChange: Ink.bindMethod(this, '_onPaginationChange'),
windowResize: InkEvent.throttle(Ink.bindMethod(this, 'refit'), 200)
};
InkEvent.observe(window, 'resize', this._handlers.windowResize);
this._isY = (this._options.axis === 'y');
var ulEl = Ink.s('ul.stage', this._element);
this._ulEl = ulEl;
InkElement.removeTextNodeChildren(ulEl);
if (this._options.pagination == null) {
this._currentPage = this._options.initialPage;
}
this.refit(); // recalculate this._numPages
if (this._isY) {
// Override white-space: no-wrap which is only necessary to make sure horizontal stuff stays horizontal, but breaks stuff intended to be vertical.
this._ulEl.style.whiteSpace = 'normal';
}
if (this._options.swipe) {
InkEvent.observe(this._element, 'touchstart', Ink.bindMethod(this, '_onTouchStart'));
InkEvent.observe(this._element, 'touchmove', Ink.bindMethod(this, '_onTouchMove'));
InkEvent.observe(this._element, 'touchend', Ink.bindMethod(this, '_onTouchEnd'));
}
this._setUpPagination();
this._setUpAutoAdvance();
this._setUpHider();
this._options.onInit.call(this, this);
},
/**
* Repositions elements around.
* Measure the carousel once again, adjusting the involved elements' sizes. This is called automatically when the window resizes, in order to cater for changes from responsive media queries, for instance.
*
* @method refit
* @public
*/
refit: function() {
var _isY = this._isY;
var size = function (elm, perpendicular) {
if (!elm) { return 0; }
if (!perpendicular) {
return InkElement.outerDimensions(elm)[_isY ? 1 : 0];
} else {
return InkElement.outerDimensions(elm)[_isY ? 0 : 1];
}
};
this._liEls = Ink.ss('li.slide', this._ulEl);
var numSlides = this._liEls.length;
var contRect = this._ulEl.getBoundingClientRect();
this._ctnLength = _isY ? contRect.bottom - contRect.top : contRect.right - contRect.left;
this._elLength = size(this._liEls[0]);
this._slidesPerPage = Math.floor( this._ctnLength / this._elLength ) || 1;
if (!isFinite(this._slidesPerPage)) { this._slidesPerPage = 1; }
var numPages = Math.ceil( numSlides / this._slidesPerPage );
var numPagesChanged = this._numPages !== numPages;
this._numPages = numPages;
this._deltaLength = this._slidesPerPage * this._elLength;
this._center();
this._updateHider();
this._IE7();
if (this._pagination && numPagesChanged) {
this._pagination.setSize(this._numPages);
}
this.setPage(limitRange(this.getPage(), 0, this._numPages));
},
_setUpPagination: function () {
if (this._options.pagination) {
if (Common.isDOMElement(this._options.pagination) ||
typeof this._options.pagination === 'string') {
// if dom element or css selector string...
this._pagination = new Pagination(this._options.pagination, {
size: this._numPages,
onChange: this._handlers.paginationChange
});
} else {
// assumes instantiated pagination
this._pagination = this._options.pagination;
this._pagination._options.onChange = this._handlers.paginationChange;
this._pagination.setSize(this._numPages);
}
this._pagination.setCurrent(this._options.initialPage || 0);
} else {
this._currentPage = this._options.initialPage || 0;
}
},
_setUpAutoAdvance: function () {
if (!this._options.autoAdvance) { return; }
var self = this;
setTimeout(function autoAdvance() {
self.nextPage(true /* wrap */);
setTimeout(autoAdvance, self._options.autoAdvance);
}, this._options.autoAdvance);
},
_setUpHider: function () {
if (this._options.hideLast) {
var hiderEl = InkElement.create('div', {
className: 'hider',
insertBottom: this._element
});
hiderEl.style.position = 'absolute';
hiderEl.style[ this._isY ? 'left' : 'top' ] = '0'; // fix to top..
hiderEl.style[ this._isY ? 'right' : 'bottom' ] = '0'; // and bottom...
hiderEl.style[ this._isY ? 'bottom' : 'right' ] = '0'; // and move to the end.
this._hiderEl = hiderEl;
}
},
// [3.1.0] Deprecate this already
_center: function() {
if (!this._options.center) { return; }
var gap = Math.floor( (this._ctnLength - (this._elLength * this._slidesPerPage) ) / 2 );
var pad;
if (this._isY) {
pad = [gap, 'px 0'];
} else {
pad = ['0 ', gap, 'px'];
}
this._ulEl.style.padding = pad.join('');
},
// [3.1.0] Deprecate this already
_updateHider: function() {
if (!this._hiderEl) { return; }
if (this.getPage() === 0) {
var gap = Math.floor( this._ctnLength - (this._elLength * this._slidesPerPage) );
if (this._options.center) {
gap /= 2;
}
this._hiderEl.style[ this._isY ? 'height' : 'width' ] = gap + 'px';
} else {
this._hiderEl.style[ this._isY ? 'height' : 'width' ] = '0px';
}
},
/**
* Refits elements for IE7 because it doesn't support inline-block.
*
* @method _IE7
* @private
*/
_IE7: function () {
if (Browser.IE && '' + Browser.version.split('.')[0] === '7') {
// var numPages = this._numPages;
var slides = Ink.ss('li.slide', this._ulEl);
var stl = function (prop, val) {slides[i].style[prop] = val; };
for (var i = 0, len = slides.length; i < len; i++) {
stl('position', 'absolute');
stl(this._isY ? 'top' : 'left', (i * this._elLength) + 'px');
}
}
},
_onTouchStart: function (event) {
if (event.touches.length > 1) { return; }
this._swipeData = {
x: InkEvent.pointerX(event),
y: InkEvent.pointerY(event)
};
var ulRect = this._ulEl.getBoundingClientRect();
this._swipeData.firstUlPos = ulRect[this._isY ? 'top' : 'left'];
this._swipeData.inUlX = this._swipeData.x - ulRect.left;
this._swipeData.inUlY = this._swipeData.y - ulRect.top;
setTransitionProperty(this._ulEl, 'none');
this._touchMoveIsFirstTouchMove = true;
},
_onTouchMove: function (event) {
if (event.touches.length > 1) { return; /* multitouch event, not my problem. */ }
var pointerX = InkEvent.pointerX(event);
var pointerY = InkEvent.pointerY(event);
var deltaY = Math.abs(pointerY - this._swipeData.y);
var deltaX = Math.abs(pointerX - this._swipeData.x);
if (this._touchMoveIsFirstTouchMove) {
this._touchMoveIsFirstTouchMove = undefined;
this._scrolling = this._isY ?
deltaX > deltaY :
deltaY > deltaX ;
if (!this._scrolling) {
this._onAnimationFrame();
}
}
if (!this._scrolling && this._swipeData) {
InkEvent.stopDefault(event);
this._swipeData.pointerPos = this._isY ? pointerY : pointerX;
}
},
_onAnimationFrame: function () {
var swipeData = this._swipeData;
if (!swipeData || this._scrolling || this._touchMoveIsFirstTouchMove) { return; }
var elRect = this._element.getBoundingClientRect();
var newPos;
if (!this._isY) {
newPos = swipeData.pointerPos - swipeData.inUlX - elRect.left;
} else {
newPos = swipeData.pointerPos - swipeData.inUlY - elRect.top;
}
this._ulEl.style[this._isY ? 'top' : 'left'] = newPos + 'px';
requestAnimationFrame(Ink.bindMethod(this, '_onAnimationFrame'));
},
_onTouchEnd: function (event) {
if (this._swipeData && this._swipeData.pointerPos && !this._scrolling && !this._touchMoveIsFirstTouchMove) {
var snapToNext = 0.1; // swipe 10% of the way to change page
var relProgress = this._swipeData.firstUlPos -
this._ulEl.getBoundingClientRect()[this._isY ? 'top' : 'left'];
var curPage = this.getPage();
// How many pages were advanced? May be fractional.
var progressInPages = relProgress / this._elLength / this._slidesPerPage;
// Have we advanced enough to change page?
if (Math.abs(progressInPages) > snapToNext) {
curPage += Math[ relProgress < 0 ? 'floor' : 'ceil' ](progressInPages);
}
// If something used to calculate progressInPages was zero, we get NaN here.
if (!isNaN(curPage)) {
this.setPage(curPage);
}
InkEvent.stopDefault(event);
}
setTransitionProperty(this._ulEl, null /* transition: left, top */);
this._swipeData = null;
this._touchMoveIsFirstTouchMove = undefined;
this._scrolling = undefined;
},
_onPaginationChange: function(pgn) {
this._setPage(pgn.getCurrent());
},
/**
* Gets the current page index
* @method getPage
* @return The current page number
**/
getPage: function () {
if (this._pagination) {
return this._pagination.getCurrent();
} else {
return this._currentPage || 0;
}
},
/**
* Sets the current page index
* @method setPage
* @param {Number} page Index of the destination page.
* @param {Boolean} [wrap] Flag to activate circular counting.
**/
setPage: function (page, wrap) {
if (wrap) {
// Pages outside the range [0..this._numPages] are wrapped.
page = page % this._numPages;
if (page < 0) { page = this._numPages - page; }
}
page = limitRange(page, 0, this._numPages - 1);
if (this._pagination) {
this._pagination.setCurrent(page); // _setPage is called by pagination because it listens to its Change event.
} else {
this._setPage(page);
}
},
_setPage: function (page) {
var _lengthToGo = page * this._deltaLength;
var isLastPage = page === (this._numPages - 1);
if (!this._options.spaceAfterLastSlide && isLastPage && page > 0) {
var _itemsInLastPage = this._liEls.length - (page * this._slidesPerPage);
if(_itemsInLastPage < this._slidesPerPage) {
_lengthToGo = ((page - 1) * this._deltaLength) + (_itemsInLastPage * this._elLength);
}
}
this._ulEl.style[ this._isY ? 'top' : 'left'] =
['-', _lengthToGo, 'px'].join('');
if (this._options.onChange) {
this._options.onChange.call(this, page);
}
this._currentPage = page;
this._updateHider();
},
/**
* Goes to the next page
* @method nextPage
* @param {Boolean} [wrap] Flag to loop from last page to first page.
**/
nextPage: function (wrap) {
this.setPage(this.getPage() + 1, wrap);
},
/**
* Goes to the previous page
* @method previousPage
* @param {Boolean} [wrap] Flag to loop from first page to last page.
**/
previousPage: function (wrap) { this.setPage(this.getPage() - 1, wrap); },
/**
* Returns how many slides fit into a page
* @method getSlidesPerPage
* @return {Number} The number of slides per page
* @public
*/
getSlidesPerPage: function() {
return this._slidesPerPage;
},
/**
* Get the amount of pages in the carousel.
* @method getTotalPages
* @return {Number} The number of pages
* @public
*/
getTotalPages: function() {
return this._numPages;
},
/**
* Get the stage element (your UL with the class ".stage").
* @method getStageElm
* @public
* @return {DOMElement} Stage element
**/
getStageElm: function() {
return this._ulEl;
},
/**
* Get a list of your slides (elements with the ".slide" class inside your stage)
* @method getSlidesList
* @return {DOMElement[]} Array containing the slides.
* @public
*/
getSlidesList: function() {
return this._liEls;
},
/**
* Get the total number of slides
* @method getTotalSlides
* @return {Number} The number of slides
* @public
*/
getTotalSlides: function() {
return this.getSlidesList().length;
}
};
function setTransitionProperty(el, newTransition) {
el.style.transitionProperty =
el.style.oTransitionProperty =
el.style.msTransitionProperty =
el.style.mozTransitionProperty =
el.style.webkitTransitionProperty = newTransition;
}
Common.createUIComponent(Carousel);
return Carousel;
});