peppermint.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. function Peppermint(_this, options) {
  2. var slider = {
  3. slides: [],
  4. dots: [],
  5. left: 0
  6. },
  7. slidesNumber,
  8. flickThreshold = 200, //Flick threshold (ms)
  9. activeSlide = 0,
  10. slideWidth,
  11. dotBlock,
  12. slideBlock,
  13. slideshowTimeoutId,
  14. slideshowActive,
  15. animationTimer;
  16. //default options
  17. var o = {
  18. speed: 300, //transition between slides in ms
  19. touchSpeed: 300, //transition between slides in ms after touch
  20. slideshow: false, //launch slideshow at start
  21. slideshowInterval: 4000,
  22. stopSlideshowAfterInteraction: false, //stop slideshow after user interaction
  23. startSlide: 0, //first slide to show
  24. mouseDrag: true, //enable mouse drag
  25. disableIfOneSlide: true,
  26. cssPrefix: 'peppermint-',
  27. dots: false, //show dots
  28. dotsPrepend: false, //dots before slides
  29. dotsContainer: undefined,
  30. slidesContainer: undefined,
  31. onSlideChange: undefined, //slide change callback
  32. onSetup: undefined //setup callback
  33. };
  34. //merge user options into defaults
  35. options && mergeObjects(o, options);
  36. var classes = {
  37. inactive: o.cssPrefix + 'inactive',
  38. active: o.cssPrefix + 'active',
  39. mouse: o.cssPrefix + 'mouse',
  40. drag: o.cssPrefix + 'drag',
  41. slides: o.cssPrefix + 'slides',
  42. dots: o.cssPrefix + 'dots',
  43. activeDot: o.cssPrefix + 'active-dot',
  44. mouseClicked: o.cssPrefix + 'mouse-clicked'
  45. };
  46. //feature detects
  47. var support = {
  48. transforms: testProp('transform'),
  49. transitions: testProp('transition')
  50. };
  51. function mergeObjects(targetObj, sourceObject) {
  52. for (var key in sourceObject) {
  53. if (sourceObject.hasOwnProperty(key)) {
  54. targetObj[key] = sourceObject[key];
  55. }
  56. }
  57. }
  58. function testProp(prop) {
  59. var prefixes = ['Webkit', 'Moz', 'O', 'ms'],
  60. block = document.createElement('div');
  61. if (block.style[prop] !== undefined) return true;
  62. prop = prop.charAt(0).toUpperCase() + prop.slice(1);
  63. for (var i in prefixes) {
  64. if (block.style[prefixes[i]+prop] !== undefined) return true;
  65. }
  66. return false;
  67. }
  68. function addClass(el, cl) {
  69. if (!new RegExp('(\\s|^)'+cl+'(\\s|$)').test(el.className)) {
  70. el.className += ' ' + cl;
  71. }
  72. }
  73. function removeClass(el, cl) {
  74. el.className = el.className.replace(new RegExp('(\\s+|^)'+cl+'(\\s+|$)', 'g'), ' ').replace(/^\s+|\s+$/g, '');
  75. }
  76. //n - slide number (starting from 0)
  77. //speed - transition in ms, can be omitted
  78. function changeActiveSlide(n, speed) {
  79. if (n<0) {
  80. n = 0;
  81. }
  82. else if (n>slidesNumber-1) {
  83. n = slidesNumber-1;
  84. }
  85. //change active dot
  86. for (var i = slider.dots.length - 1; i >= 0; i--) {
  87. removeClass(slider.dots[i], classes.activeDot);
  88. }
  89. addClass(slider.dots[n], classes.activeDot);
  90. activeSlide = n;
  91. changePos(-n*slider.width, (speed===undefined?o.speed:speed));
  92. //reset slideshow timeout whenever active slide is changed for whatever reason
  93. stepSlideshow();
  94. //API callback
  95. o.onSlideChange && o.onSlideChange(n);
  96. return n;
  97. }
  98. //changes position of the slider (in px) with given speed (in ms)
  99. function changePos(pos, speed) {
  100. var time = speed?speed+'ms':'';
  101. slideBlock.style.webkitTransitionDuration =
  102. slideBlock.style.MozTransitionDuration =
  103. slideBlock.style.msTransitionDuration =
  104. slideBlock.style.OTransitionDuration =
  105. slideBlock.style.transitionDuration = time;
  106. setPos(pos);
  107. }
  108. //fallback to `setInterval` animations for UAs with no CSS transitions
  109. function changePosFallback(pos, speed) {
  110. animationTimer && clearInterval(animationTimer);
  111. if (!speed) {
  112. setPos(pos);
  113. return;
  114. }
  115. var startTime = +new Date,
  116. startPos = slider.left;
  117. animationTimer = setInterval(function() {
  118. //rough bezier emulation
  119. var diff, y,
  120. elapsed = +new Date - startTime,
  121. f = elapsed / speed,
  122. bezier = [0, 0.7, 1, 1];
  123. function getPoint(p1, p2) {
  124. return (p2-p1)*f + p1;
  125. }
  126. if (f >= 1) {
  127. setPos(pos);
  128. clearInterval(animationTimer);
  129. return;
  130. }
  131. diff = pos - startPos;
  132. y = getPoint(
  133. getPoint(getPoint(bezier[0], bezier[1]), getPoint(bezier[1], bezier[2])),
  134. getPoint(getPoint(bezier[1], bezier[2]), getPoint(bezier[2], bezier[3]))
  135. );
  136. setPos(Math.floor(y*diff + startPos));
  137. }, 15);
  138. }
  139. //sets position of the slider (in px)
  140. function setPos(pos) {
  141. slideBlock.style.webkitTransform = 'translate('+pos+'px,0) translateZ(0)';
  142. slideBlock.style.msTransform =
  143. slideBlock.style.MozTransform =
  144. slideBlock.style.OTransform =
  145. slideBlock.style.transform = 'translateX('+pos+'px)';
  146. slider.left = pos;
  147. }
  148. //`setPos` fallback for UAs with no CSS transforms support
  149. function setPosFallback(pos) {
  150. slideBlock.style.left = pos+'px';
  151. slider.left = pos;
  152. }
  153. function nextSlide() {
  154. var n = activeSlide + 1;
  155. if (n > slidesNumber - 1) {
  156. n = 0;
  157. }
  158. return changeActiveSlide(n);
  159. }
  160. function prevSlide() {
  161. var n = activeSlide - 1;
  162. if (n < 0) {
  163. n = slidesNumber - 1;
  164. }
  165. return changeActiveSlide(n);
  166. }
  167. function startSlideshow() {
  168. slideshowActive = true;
  169. stepSlideshow();
  170. }
  171. //sets or resets timeout before switching to the next slide
  172. function stepSlideshow() {
  173. if (slideshowActive) {
  174. slideshowTimeoutId && clearTimeout(slideshowTimeoutId);
  175. slideshowTimeoutId = setTimeout(function() {
  176. nextSlide();
  177. },
  178. o.slideshowInterval);
  179. }
  180. }
  181. //pauses slideshow until `stepSlideshow` is invoked
  182. function pauseSlideshow() {
  183. slideshowTimeoutId && clearTimeout(slideshowTimeoutId);
  184. }
  185. function stopSlideshow() {
  186. slideshowActive = false;
  187. slideshowTimeoutId && clearTimeout(slideshowTimeoutId);
  188. }
  189. //this should be invoked when the width of the slider is changed
  190. function onWidthChange() {
  191. slider.width = _this.offsetWidth;
  192. //have to do this in `px` because of webkit's rounding errors :-(
  193. slideBlock.style.width = slider.width*slidesNumber+'px';
  194. for (var i = 0; i < slidesNumber; i++) {
  195. slider.slides[i].style.width = slider.width+'px';
  196. }
  197. changePos(-activeSlide*slider.width);
  198. }
  199. function addEvent(el, event, func, bool) {
  200. if (!event) return;
  201. el.addEventListener? el.addEventListener(event, func, !!bool): el.attachEvent('on'+event, func);
  202. }
  203. //init touch events
  204. function touchInit() {
  205. EventBurrito(slideBlock, {
  206. mouse: o.mouseDrag,
  207. start: function(event, start) {
  208. //firefox doesn't want to apply cursor from `:active` CSS rule, have to add a class :-/
  209. addClass(_this, classes.drag);
  210. },
  211. move: function(event, start, diff, speed) {
  212. pauseSlideshow(); //pause the slideshow when touch is in progress
  213. //if it's first slide and moving left or last slide and moving right -- resist!
  214. diff.x =
  215. diff.x /
  216. (
  217. (!activeSlide && diff.x > 0
  218. || activeSlide == slidesNumber - 1 && diff.x < 0)
  219. ?
  220. (Math.abs(diff.x)/slider.width*2 + 1)
  221. :
  222. 1
  223. );
  224. //change position of the slider appropriately
  225. changePos(diff.x - slider.width*activeSlide);
  226. },
  227. end: function(event, start, diff, speed) {
  228. if (diff.x) {
  229. var ratio = Math.abs(diff.x)/slider.width,
  230. //How many slides to skip. Remainder > 0.25 counts for one slide.
  231. skip = Math.floor(ratio) + (ratio - Math.floor(ratio) > 0.25?1:0),
  232. //Super-duper formula to detect a flick.
  233. //First, it's got to be fast enough.
  234. //Second, if `skip==0`, 20px move is enough to switch to the next slide.
  235. //If `skip>0`, it's enough to slide to the middle of the slide minus `slider.width/9` to skip even further.
  236. flick = diff.time < flickThreshold+flickThreshold*skip/1.8 && Math.abs(diff.x) - skip*slider.width > (skip?-slider.width/9:20);
  237. skip += (flick?1:0);
  238. if (diff.x < 0) {
  239. changeActiveSlide(activeSlide+skip, o.touchSpeed);
  240. }
  241. else {
  242. changeActiveSlide(activeSlide-skip, o.touchSpeed);
  243. }
  244. o.stopSlideshowAfterInteraction && stopSlideshow();
  245. }
  246. //remove the drag class
  247. removeClass(_this, classes.drag);
  248. }
  249. });
  250. }
  251. function setup() {
  252. var slideSource = o.slidesContainer || _this,
  253. dotsTarget = o.dotsContainer || _this;
  254. if (o.disableIfOneSlide && slideSource.children.length <= 1) return;
  255. //If current UA doesn't support css transforms or transitions -- use fallback functions.
  256. //(Using separate functions instead of checks for better performance)
  257. if (!support.transforms || !!window.opera) setPos = setPosFallback;
  258. if (!support.transitions || !!window.opera) changePos = changePosFallback;
  259. slideBlock = o.slidesContainer || document.createElement('div');
  260. addClass(slideBlock, classes.slides);
  261. //get slides & generate dots
  262. for (var i = 0, l = slideSource.children.length; i < l; i++) {
  263. var slide = slideSource.children[i],
  264. dot = document.createElement('li');
  265. slider.slides.push(slide);
  266. //`tabindex` makes dots tabbable
  267. dot.setAttribute('tabindex', '0');
  268. dot.setAttribute('role', 'button');
  269. dot.innerHTML = '<span></span>';
  270. (function(x, dotClosure) {
  271. //bind events to dots
  272. addEvent(dotClosure, 'click', function(event) {
  273. changeActiveSlide(x);
  274. o.stopSlideshowAfterInteraction && stopSlideshow();
  275. });
  276. //Bind the same function to Enter key except for the `blur` part -- I dont't want
  277. //the focus to be lost when the user is using his keyboard to navigate.
  278. addEvent(dotClosure, 'keyup', function(event) {
  279. if (event.keyCode == 13) {
  280. changeActiveSlide(x);
  281. o.stopSlideshowAfterInteraction && stopSlideshow();
  282. }
  283. });
  284. //Don't want to disable outlines completely for accessibility reasons.
  285. //Instead, add class with `outline: 0` on mouseup and remove it on blur.
  286. addEvent(dotClosure, 'mouseup', function(event) {
  287. addClass(dotClosure, classes.mouseClicked);
  288. });
  289. //capturing fixes IE bug
  290. addEvent(dotClosure, 'blur', function(){
  291. removeClass(dotClosure, classes.mouseClicked);
  292. }, true);
  293. //This solves tabbing problems:
  294. //When an element inside a slide catches focus we switch to that slide
  295. //and reset `scrollLeft` of the slider block.
  296. //`SetTimeout` solves Chrome's bug.
  297. //Event capturing is used to catch events on the slide level.
  298. //Since older IEs don't have capturing, `onfocusin` is used as a fallback.
  299. addEvent(slide, 'focus', slide.onfocusin = function(e) {
  300. _this.scrollLeft = 0;
  301. setTimeout(function() {
  302. _this.scrollLeft = 0;
  303. }, 0);
  304. changeActiveSlide(x);
  305. }, true);
  306. })(i, dot)
  307. slider.dots.push(dot);
  308. }
  309. slidesNumber = slider.slides.length;
  310. slideWidth = 100/slidesNumber;
  311. addClass(_this, classes.active);
  312. removeClass(_this, classes.inactive);
  313. o.mouseDrag && addClass(_this, classes.mouse);
  314. slider.width = _this.offsetWidth;
  315. //had to do this in `px` because of webkit's rounding errors :-(
  316. slideBlock.style.width = slider.width*slidesNumber+'px';
  317. for (var i = 0; i < slidesNumber; i++) {
  318. slider.slides[i].style.width = slider.width+'px';
  319. slideBlock.appendChild(slider.slides[i]);
  320. }
  321. if (!o.slidesContainer) _this.appendChild(slideBlock);
  322. //append dots
  323. if (o.dots && slidesNumber > 1) {
  324. dotBlock = document.createElement('ul');
  325. addClass(dotBlock, classes.dots);
  326. for (var i = 0, l = slider.dots.length; i < l; i++) {
  327. dotBlock.appendChild(slider.dots[i]);
  328. }
  329. if (o.dotsPrepend) {
  330. dotsTarget.insertBefore(dotBlock,dotsTarget.firstChild);
  331. }
  332. else {
  333. dotsTarget.appendChild(dotBlock);
  334. }
  335. }
  336. //watch for slider width changes
  337. addEvent(window, 'resize', onWidthChange);
  338. addEvent(window, 'orientationchange', onWidthChange);
  339. //init first slide, timeout to expose the API first
  340. setTimeout(function() {
  341. changeActiveSlide(o.startSlide, 0);
  342. }, 0);
  343. //init slideshow
  344. if (o.slideshow) startSlideshow();
  345. touchInit();
  346. //API callback, timeout to expose the API first
  347. setTimeout(function() {
  348. o.onSetup && o.onSetup(slidesNumber);
  349. }, 0);
  350. }
  351. //Init
  352. setup();
  353. //expose the API
  354. return {
  355. slideTo: function(slide) {
  356. return changeActiveSlide(parseInt(slide, 10));
  357. },
  358. next: nextSlide,
  359. prev: prevSlide,
  360. //start slideshow
  361. start: startSlideshow,
  362. //stop slideshow
  363. stop: stopSlideshow,
  364. //pause slideshow until the next slide change
  365. pause: pauseSlideshow,
  366. //get current slide number
  367. getCurrentPos: function() {
  368. return activeSlide;
  369. },
  370. //get total number of slides
  371. getSlidesNumber: function() {
  372. return slidesNumber;
  373. },
  374. //invoke this when the slider's width is changed
  375. recalcWidth: onWidthChange
  376. };
  377. };
  378. //if jQuery is present -- create a plugin
  379. if (window.jQuery) {
  380. (function($) {
  381. $.fn.Peppermint = function(options) {
  382. this.each(function() {
  383. $(this).data('Peppermint', Peppermint(this, options));
  384. });
  385. return this;
  386. };
  387. })(window.jQuery);
  388. }