swipe.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828
  1. /*!
  2. * Swipe 2.2.14
  3. *
  4. * Brad Birdsall
  5. * Copyright 2013, MIT License
  6. *
  7. */
  8. // if the module has no dependencies, the above pattern can be simplified to
  9. // eslint-disable-next-line no-extra-semi
  10. ; (function (root, factory) {
  11. // eslint-disable-next-line no-undef
  12. if (typeof define === 'function' && define.amd) {
  13. // AMD. Register as an anonymous module.
  14. // eslint-disable-next-line no-undef
  15. define([], function () {
  16. root.Swipe = factory();
  17. return root.Swipe;
  18. });
  19. } else if (typeof module === 'object' && module.exports) {
  20. // Node. Does not work with strict CommonJS, but
  21. // only CommonJS-like environments that support module.exports,
  22. // like Node.
  23. module.exports = factory();
  24. } else {
  25. // Browser globals
  26. root.Swipe = factory();
  27. }
  28. }(this, function () {
  29. // Establish the root object, `window` (`self`) in the browser, `global`
  30. // on the server, or `this` in some virtual machines. We use `self`
  31. // instead of `window` for `WebWorker` support.
  32. var root = typeof self == 'object' && self.self === self && self ||
  33. typeof global == 'object' && global.global === global && global ||
  34. this;
  35. var _document = root.document;
  36. function Swipe(container, options) {
  37. 'use strict';
  38. options = options || {};
  39. // setup initial vars
  40. var start = {};
  41. var delta = {};
  42. var isScrolling;
  43. // setup auto slideshow
  44. var delay = options.auto || 0;
  45. var interval;
  46. var disabled = false;
  47. // utilities
  48. // simple no operation function
  49. var noop = function () { };
  50. // offload a functions execution
  51. var offloadFn = function (fn) { setTimeout(fn || noop, 0); };
  52. // Returns a function, that, as long as it continues to be invoked, will not
  53. // be triggered.
  54. var throttle = function (fn, threshhold) {
  55. threshhold = threshhold || 100;
  56. var timeout = null;
  57. function cancel() {
  58. if (timeout) clearTimeout(timeout);
  59. }
  60. function throttledFn() {
  61. var context = this;
  62. var args = arguments;
  63. cancel();
  64. timeout = setTimeout(function () {
  65. timeout = null;
  66. fn.apply(context, args);
  67. }, threshhold);
  68. }
  69. // allow remove throttled timeout
  70. throttledFn.cancel = cancel;
  71. return throttledFn;
  72. };
  73. function getScrollbarWidth() {
  74. return root.innerWidth - _document.documentElement.clientWidth;
  75. }
  76. // check whether event is cancelable
  77. var isCancelable = function (event) {
  78. if (!event) return false;
  79. return typeof event.cancelable !== 'boolean' || event.cancelable;
  80. };
  81. // check browser capabilities
  82. var browser = {
  83. addEventListener: !!root.addEventListener,
  84. passiveEvents: (function () {
  85. // Test via a getter in the options object to see if the passive property is accessed
  86. var supportsPassive = false;
  87. try {
  88. var opts = Object.defineProperty({}, 'passive', {
  89. // eslint-disable-next-line getter-return
  90. get: function () {
  91. supportsPassive = true;
  92. }
  93. });
  94. root.addEventListener('testEvent', null, opts);
  95. root.removeEventListener('testEvent', null, opts);
  96. }
  97. catch (e) {
  98. supportsPassive = false;
  99. }
  100. return supportsPassive;
  101. })(),
  102. // eslint-disable-next-line no-undef
  103. touch: ('ontouchstart' in root) || root.DocumentTouch && _document instanceof DocumentTouch,
  104. transitions: (function (temp) {
  105. var props = ['transitionProperty', 'WebkitTransition', 'MozTransition', 'OTransition', 'msTransition'];
  106. for (var i in props) {
  107. if (temp.style[props[i]] !== undefined) {
  108. return true;
  109. }
  110. }
  111. return false;
  112. })(_document.createElement('swipe'))
  113. };
  114. // quit if no root element
  115. if (!container) return;
  116. var element = container.children[0];
  117. var slides, slidePos, width, length;
  118. var index = parseInt(options.startSlide, 10) || 0;
  119. var speed = options.speed || 300;
  120. options.continuous = options.continuous !== undefined ? options.continuous : true;
  121. // check text direction
  122. var slideDir = (function (el, prop, dir) {
  123. if (el.currentStyle) {
  124. dir = el.currentStyle[prop];
  125. } else if (root.getComputedStyle) {
  126. dir = root.getComputedStyle(el, null).getPropertyValue(prop);
  127. }
  128. return 'rtl' === dir ? 'right' : 'left';
  129. })(container, 'direction');
  130. // AutoRestart option: auto restart slideshow after user's touch event
  131. options.autoRestart = options.autoRestart !== undefined ? options.autoRestart : false;
  132. // throttled setup
  133. var throttledSetup = throttle(setup);
  134. // setup event capturing
  135. var events = {
  136. handleEvent: function (event) {
  137. if (disabled) return;
  138. switch (event.type) {
  139. case 'mousedown':
  140. case 'touchstart': this.start(event); break;
  141. case 'mousemove':
  142. case 'touchmove': this.move(event); break;
  143. case 'mouseup':
  144. case 'mouseleave':
  145. case 'touchend': this.end(event); break;
  146. case 'webkitTransitionEnd':
  147. case 'msTransitionEnd':
  148. case 'oTransitionEnd':
  149. case 'otransitionend':
  150. case 'transitionend': this.transitionEnd(event); break;
  151. case 'resize': throttledSetup(); break;
  152. }
  153. if (options.stopPropagation) {
  154. event.stopPropagation();
  155. }
  156. },
  157. start: function (event) {
  158. var touches;
  159. if (isMouseEvent(event)) {
  160. touches = event;
  161. event.preventDefault(); // For desktop Safari drag
  162. } else {
  163. touches = event.touches[0];
  164. }
  165. // measure start values
  166. start = {
  167. // get initial touch coords
  168. x: touches.pageX,
  169. y: touches.pageY,
  170. // store time to determine touch duration
  171. time: +new Date()
  172. };
  173. // used for testing first move event
  174. isScrolling = undefined;
  175. // reset delta and end measurements
  176. delta = {};
  177. // attach touchmove and touchend listeners
  178. if (isMouseEvent(event)) {
  179. element.addEventListener('mousemove', this, false);
  180. element.addEventListener('mouseup', this, false);
  181. element.addEventListener('mouseleave', this, false);
  182. } else {
  183. element.addEventListener('touchmove', this, browser.passiveEvents ? { passive: false } : false);
  184. element.addEventListener('touchend', this, false);
  185. }
  186. },
  187. move: function (event) {
  188. var touches;
  189. if (isMouseEvent(event)) {
  190. touches = event;
  191. } else {
  192. // ensure swiping with one touch and not pinching
  193. if (event.touches.length > 1 || event.scale && event.scale !== 1) {
  194. return;
  195. }
  196. // we can disable scrolling unless it is already in progress
  197. if (options.disableScroll && isCancelable(event)) {
  198. event.preventDefault();
  199. }
  200. touches = event.touches[0];
  201. }
  202. // measure change in x and y
  203. delta = {
  204. x: touches.pageX - start.x,
  205. y: touches.pageY - start.y
  206. };
  207. // determine if scrolling test has run - one time test
  208. if (typeof isScrolling === 'undefined') {
  209. isScrolling = !!(isScrolling || Math.abs(delta.x) < Math.abs(delta.y));
  210. }
  211. // if user is not trying to scroll vertically
  212. if (!isScrolling) {
  213. // if it is not already scrolling
  214. if (isCancelable(event)) {
  215. // prevent native scrolling
  216. event.preventDefault();
  217. }
  218. // stop slideshow
  219. stop();
  220. // increase resistance if first or last slide
  221. if (options.continuous) { // we don't add resistance at the end
  222. translate(circle(index - 1), delta.x + slidePos[circle(index - 1)], 0);
  223. translate(index, delta.x + slidePos[index], 0);
  224. translate(circle(index + 1), delta.x + slidePos[circle(index + 1)], 0);
  225. } else {
  226. delta.x =
  227. delta.x /
  228. ((!index && delta.x > 0 || // if first slide and sliding left
  229. index === slides.length - 1 && // or if last slide and sliding right
  230. delta.x < 0 // and if sliding at all
  231. ) ?
  232. (Math.abs(delta.x) / width + 1) // determine resistance level
  233. : 1); // no resistance if false
  234. // translate 1:1
  235. translate(index - 1, delta.x + slidePos[index - 1], 0);
  236. translate(index, delta.x + slidePos[index], 0);
  237. translate(index + 1, delta.x + slidePos[index + 1], 0);
  238. }
  239. }
  240. },
  241. end: function (event) {
  242. // measure duration
  243. var duration = +new Date() - start.time;
  244. // determine if slide attempt triggers next/prev slide
  245. var isValidSlide =
  246. Number(duration) < 250 && // if slide duration is less than 250ms
  247. Math.abs(delta.x) > 20 || // and if slide amt is greater than 20px
  248. Math.abs(delta.x) > width / 2; // or if slide amt is greater than half the width
  249. // determine if slide attempt is past start and end
  250. var isPastBounds =
  251. !index && delta.x > 0 || // if first slide and slide amt is greater than 0
  252. index === slides.length - 1 && delta.x < 0; // or if last slide and slide amt is less than 0
  253. if (options.continuous) {
  254. isPastBounds = false;
  255. }
  256. // OLD determine direction of swipe (true:right, false:left)
  257. // determine direction of swipe (1: backward, -1: forward)
  258. var direction = Math.abs(delta.x) / delta.x;
  259. // if not scrolling vertically
  260. if (!isScrolling) {
  261. if (isValidSlide && !isPastBounds) {
  262. // if we're moving right
  263. if (direction < 0) {
  264. if (options.continuous) { // we need to get the next in this direction in place
  265. move(circle(index - 1), -width, 0);
  266. move(circle(index + 2), width, 0);
  267. } else {
  268. move(index - 1, -width, 0);
  269. }
  270. move(index, slidePos[index] - width, speed);
  271. move(circle(index + 1), slidePos[circle(index + 1)] - width, speed);
  272. index = circle(index + 1);
  273. } else {
  274. if (options.continuous) { // we need to get the next in this direction in place
  275. move(circle(index + 1), width, 0);
  276. move(circle(index - 2), -width, 0);
  277. } else {
  278. move(index + 1, width, 0);
  279. }
  280. move(index, slidePos[index] + width, speed);
  281. move(circle(index - 1), slidePos[circle(index - 1)] + width, speed);
  282. index = circle(index - 1);
  283. }
  284. runCallback(getPos(), slides[index], direction);
  285. } else {
  286. if (options.continuous) {
  287. move(circle(index - 1), -width, speed);
  288. move(index, 0, speed);
  289. move(circle(index + 1), width, speed);
  290. } else {
  291. move(index - 1, -width, speed);
  292. move(index, 0, speed);
  293. move(index + 1, width, speed);
  294. }
  295. }
  296. }
  297. // kill touchmove and touchend event listeners until touchstart called again
  298. if (isMouseEvent(event)) {
  299. element.removeEventListener('mousemove', events, false);
  300. element.removeEventListener('mouseup', events, false);
  301. element.removeEventListener('mouseleave', events, false);
  302. } else {
  303. element.removeEventListener('touchmove', events, browser.passiveEvents ? { passive: false } : false);
  304. element.removeEventListener('touchend', events, false);
  305. }
  306. },
  307. transitionEnd: function (event) {
  308. var currentIndex = parseInt(event.target.getAttribute('data-index'), 10);
  309. if (currentIndex === index) {
  310. if (delay || options.autoRestart) restart();
  311. runTransitionEnd(getPos(), slides[index]);
  312. }
  313. }
  314. };
  315. // trigger setup
  316. setup();
  317. // start auto slideshow if applicable
  318. begin();
  319. // Expose the Swipe API
  320. return {
  321. // initialize
  322. setup: setup,
  323. // go to slide
  324. slide: function (to, speed) {
  325. stop();
  326. slide(to, speed);
  327. },
  328. // move to previous
  329. prev: function () {
  330. stop();
  331. prev();
  332. },
  333. // move to next
  334. next: function () {
  335. stop();
  336. next();
  337. },
  338. // Restart slideshow
  339. restart: restart,
  340. // cancel slideshow
  341. stop: stop,
  342. // return current index position
  343. getPos: getPos,
  344. // disable slideshow
  345. disable: disable,
  346. // enable slideshow
  347. enable: enable,
  348. // return total number of slides
  349. getNumSlides: function () { return length; },
  350. // completely remove swipe
  351. kill: kill
  352. };
  353. // remove all event listeners
  354. function detachEvents() {
  355. if (browser.addEventListener) {
  356. // remove current event listeners
  357. element.removeEventListener('touchstart', events, browser.passiveEvents ? { passive: true } : false);
  358. element.removeEventListener('mousedown', events, false);
  359. element.removeEventListener('webkitTransitionEnd', events, false);
  360. element.removeEventListener('msTransitionEnd', events, false);
  361. element.removeEventListener('oTransitionEnd', events, false);
  362. element.removeEventListener('otransitionend', events, false);
  363. element.removeEventListener('transitionend', events, false);
  364. root.removeEventListener('resize', events, false);
  365. } else {
  366. root.onresize = null;
  367. }
  368. }
  369. // add event listeners
  370. function attachEvents() {
  371. if (browser.addEventListener) {
  372. // set touchstart event on element
  373. if (browser.touch) {
  374. element.addEventListener('touchstart', events, browser.passiveEvents ? { passive: true } : false);
  375. }
  376. if (options.draggable) {
  377. element.addEventListener('mousedown', events, false);
  378. }
  379. if (browser.transitions) {
  380. element.addEventListener('webkitTransitionEnd', events, false);
  381. element.addEventListener('msTransitionEnd', events, false);
  382. element.addEventListener('oTransitionEnd', events, false);
  383. element.addEventListener('otransitionend', events, false);
  384. element.addEventListener('transitionend', events, false);
  385. }
  386. // set resize event on window
  387. root.addEventListener('resize', events, false);
  388. } else {
  389. root.onresize = throttledSetup; // to play nice with old IE
  390. }
  391. }
  392. // clone nodes when there is only two slides
  393. function cloneNode(el) {
  394. var clone = el.cloneNode(true);
  395. element.appendChild(clone);
  396. // tag these slides as clones (to remove them on kill)
  397. clone.setAttribute('data-cloned', true);
  398. // Remove id from element
  399. clone.removeAttribute('id');
  400. }
  401. function setup(opts) {
  402. // Overwrite options if necessary
  403. if (opts != null) {
  404. for (var prop in opts) {
  405. options[prop] = opts[prop];
  406. }
  407. }
  408. // cache slides
  409. slides = element.children;
  410. length = slides.length;
  411. // slides length correction, minus cloned slides
  412. for (var i = 0; i < slides.length; i++) {
  413. if (slides[i].getAttribute('data-cloned')) length--;
  414. }
  415. // set continuous to false if only one slide
  416. if (slides.length < 2) {
  417. options.continuous = false;
  418. }
  419. // special case if two slides
  420. if (browser.transitions && options.continuous && slides.length < 3) {
  421. cloneNode(slides[0]);
  422. cloneNode(slides[1]);
  423. slides = element.children;
  424. }
  425. // adjust style on rtl
  426. if ('right' === slideDir) {
  427. for (var j = 0; j < slides.length; j++) {
  428. slides[j].style.float = 'right';
  429. }
  430. }
  431. // create an array to store current positions of each slide
  432. slidePos = new Array(slides.length);
  433. // determine width of each slide
  434. //width = container.getBoundingClientRect().width || container.offsetWidth;
  435. width = root.innerWidth - getScrollbarWidth();
  436. element.style.width = (slides.length * width * 2) + 'px';
  437. // stack elements
  438. var pos = slides.length;
  439. while (pos--) {
  440. var slide = slides[pos];
  441. slide.style.width = width + 'px';
  442. slide.setAttribute('data-index', pos);
  443. if (browser.transitions) {
  444. slide.style[slideDir] = (pos * -width) + 'px';
  445. move(pos, index > pos ? -width : (index < pos ? width : 0), 0);
  446. }
  447. }
  448. // reposition elements before and after index
  449. if (options.continuous && browser.transitions) {
  450. move(circle(index - 1), -width, 0);
  451. move(circle(index + 1), width, 0);
  452. }
  453. if (!browser.transitions) {
  454. element.style[slideDir] = (index * -width) + 'px';
  455. }
  456. container.style.visibility = 'visible';
  457. // reinitialize events
  458. detachEvents();
  459. attachEvents();
  460. }
  461. function prev() {
  462. if (disabled) return;
  463. if (options.continuous) {
  464. slide(index - 1);
  465. } else if (index) {
  466. slide(index - 1);
  467. }
  468. }
  469. function next() {
  470. if (disabled) return;
  471. if (options.continuous) {
  472. slide(index + 1);
  473. } else if (index < slides.length - 1) {
  474. slide(index + 1);
  475. }
  476. }
  477. function runCallback(pos, index, dir) {
  478. if (options.callback) {
  479. options.callback(pos, index, dir);
  480. }
  481. }
  482. function runTransitionEnd(pos, index) {
  483. if (options.transitionEnd) {
  484. options.transitionEnd(pos, index);
  485. }
  486. }
  487. function circle(index) {
  488. // a simple positive modulo using slides.length
  489. return (slides.length + (index % slides.length)) % slides.length;
  490. }
  491. function getPos() {
  492. // Fix for the clone issue in the event of 2 slides
  493. var currentIndex = index;
  494. if (currentIndex >= length) {
  495. currentIndex = currentIndex - length;
  496. }
  497. return currentIndex;
  498. }
  499. function slide(to, slideSpeed) {
  500. // ensure to is of type 'number'
  501. to = typeof to !== 'number' ? parseInt(to, 10) : to;
  502. // do nothing if already on requested slide
  503. if (index === to) return;
  504. if (browser.transitions) {
  505. var direction = Math.abs(index - to) / (index - to); // 1: backward, -1: forward
  506. // get the actual position of the slide
  507. if (options.continuous) {
  508. var natural_direction = direction;
  509. direction = -slidePos[circle(to)] / width;
  510. // if going forward but to < index, use to = slides.length + to
  511. // if going backward but to > index, use to = -slides.length + to
  512. if (direction !== natural_direction) {
  513. to = -direction * slides.length + to;
  514. }
  515. }
  516. var diff = Math.abs(index - to) - 1;
  517. // move all the slides between index and to in the right direction
  518. while (diff--) {
  519. move(circle((to > index ? to : index) - diff - 1), width * direction, 0);
  520. }
  521. to = circle(to);
  522. move(index, width * direction, slideSpeed || speed);
  523. move(to, 0, slideSpeed || speed);
  524. if (options.continuous) { // we need to get the next in place
  525. move(circle(to - direction), -(width * direction), 0);
  526. }
  527. } else {
  528. to = circle(to);
  529. animate(index * -width, to * -width, slideSpeed || speed);
  530. // no fallback for a circular continuous if the browser does not accept transitions
  531. }
  532. index = to;
  533. offloadFn(function () {
  534. runCallback(getPos(), slides[index], direction);
  535. });
  536. }
  537. function move(index, dist, speed) {
  538. translate(index, dist, speed);
  539. slidePos[index] = dist;
  540. }
  541. function translate(index, dist, speed) {
  542. var slide = slides[index];
  543. var style = slide && slide.style;
  544. if (!style) return;
  545. style.webkitTransitionDuration =
  546. style.MozTransitionDuration =
  547. style.msTransitionDuration =
  548. style.OTransitionDuration =
  549. style.transitionDuration = speed + 'ms';
  550. style.webkitTransform =
  551. style.msTransform =
  552. style.MozTransform =
  553. style.OTransform =
  554. style.transform = 'translateX(' + dist + 'px)';
  555. }
  556. function animate(from, to, speed) {
  557. // if not an animation, just reposition
  558. if (!speed) {
  559. element.style[slideDir] = to + 'px';
  560. return;
  561. }
  562. var start = +new Date();
  563. var timer = setInterval(function () {
  564. var timeElap = +new Date() - start;
  565. if (timeElap > speed) {
  566. element.style[slideDir] = to + 'px';
  567. if (delay || options.autoRestart) restart();
  568. runTransitionEnd(getPos(), slides[index]);
  569. clearInterval(timer);
  570. return;
  571. }
  572. element.style[slideDir] = (((to - from) * (Math.floor((timeElap / speed) * 100) / 100)) + from) + 'px';
  573. }, 4);
  574. }
  575. function begin() {
  576. delay = options.auto || 0;
  577. if (delay) interval = setTimeout(next, delay);
  578. }
  579. function stop() {
  580. delay = 0;
  581. clearTimeout(interval);
  582. }
  583. function restart() {
  584. stop();
  585. begin();
  586. }
  587. function disable() {
  588. stop();
  589. disabled = true;
  590. }
  591. function enable() {
  592. disabled = false;
  593. restart();
  594. }
  595. function isMouseEvent(e) {
  596. return /^mouse/.test(e.type);
  597. }
  598. function kill() {
  599. // cancel slideshow
  600. stop();
  601. // remove inline styles
  602. container.style.visibility = '';
  603. // reset element
  604. element.style.width = '';
  605. element.style[slideDir] = '';
  606. // reset slides
  607. var pos = slides.length;
  608. while (pos--) {
  609. if (browser.transitions) {
  610. translate(pos, 0, 0);
  611. }
  612. var slide = slides[pos];
  613. // if the slide is tagged as clone, remove it
  614. if (slide.getAttribute('data-cloned')) {
  615. var _parent = slide.parentElement;
  616. _parent.removeChild(slide);
  617. }
  618. // remove styles
  619. slide.style.width = '';
  620. slide.style[slideDir] = '';
  621. slide.style.webkitTransitionDuration =
  622. slide.style.MozTransitionDuration =
  623. slide.style.msTransitionDuration =
  624. slide.style.OTransitionDuration =
  625. slide.style.transitionDuration = '';
  626. slide.style.webkitTransform =
  627. slide.style.msTransform =
  628. slide.style.MozTransform =
  629. slide.style.OTransform = '';
  630. // remove custom attributes (?)
  631. // slide.removeAttribute('data-index');
  632. }
  633. // remove all events
  634. detachEvents();
  635. // remove throttled function timeout
  636. throttledSetup.cancel();
  637. }
  638. }
  639. if (root.jQuery || root.Zepto) {
  640. (function ($) {
  641. $.fn.Swipe = function (params) {
  642. return this.each(function () {
  643. $(this).data('Swipe', new Swipe($(this)[0], params));
  644. });
  645. };
  646. })(root.jQuery || root.Zepto);
  647. }
  648. return Swipe;
  649. }));