You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

528 lines
23 KiB

  1. /*
  2. * Snap.js
  3. *
  4. * Copyright 2013, Jacob Kelley - http://jakiestfu.com/
  5. * Released under the MIT Licence
  6. * http://opensource.org/licenses/MIT
  7. *
  8. * Github: http://github.com/jakiestfu/Snap.js/
  9. * Version: 1.7.10
  10. */
  11. /*jslint browser: true*/
  12. /*global define, module, ender*/
  13. (function(win, doc) {
  14. 'use strict';
  15. var Snap = Snap || function(userOpts) {
  16. var settings = {
  17. element: null,
  18. disable: 'none',
  19. addBodyClasses: true,
  20. resistance: 0.5,
  21. flickThreshold: 50,
  22. transitionSpeed: 0.3,
  23. easing: 'ease',
  24. maxPosition: 266,
  25. minPosition: -266,
  26. tapToClose: true,
  27. touchToDrag: true,
  28. slideIntent: 40, // degrees
  29. minDragDistance: 5
  30. },
  31. cache = {
  32. simpleStates: {
  33. opening: null,
  34. towards: null,
  35. hyperExtending: null,
  36. halfway: null,
  37. flick: null,
  38. translation: {
  39. absolute: 0,
  40. relative: 0,
  41. sinceDirectionChange: 0,
  42. percentage: 0
  43. }
  44. }
  45. },
  46. eventList = {},
  47. utils = {
  48. hasTouch: (doc.ontouchstart === null),
  49. eventType: function(action) {
  50. var eventTypes = {
  51. down: (utils.hasTouch ? 'touchstart' : 'mousedown'),
  52. move: (utils.hasTouch ? 'touchmove' : 'mousemove'),
  53. up: (utils.hasTouch ? 'touchend' : 'mouseup'),
  54. out: (utils.hasTouch ? 'touchcancel' : 'mouseout')
  55. };
  56. return eventTypes[action];
  57. },
  58. page: function(t, e){
  59. return (utils.hasTouch && e.touches.length && e.touches[0]) ? e.touches[0]['page'+t] : e['page'+t];
  60. },
  61. klass: {
  62. has: function(el, name){
  63. return (el.className).indexOf(name) !== -1;
  64. },
  65. add: function(el, name){
  66. if(!utils.klass.has(el, name)){
  67. el.className += " "+name;
  68. }
  69. },
  70. remove: function(el, name){
  71. el.className = (el.className).replace(" "+name, "");
  72. }
  73. },
  74. dispatchEvent: function(type) {
  75. if (typeof eventList[type] === 'function') {
  76. return eventList[type].call();
  77. }
  78. },
  79. vendor: function(){
  80. var tmp = doc.createElement("div"),
  81. prefixes = 'webkit Moz O ms'.split(' '),
  82. i;
  83. for (i in prefixes) {
  84. if (typeof tmp.style[prefixes[i] + 'Transition'] !== 'undefined') {
  85. return prefixes[i];
  86. }
  87. }
  88. },
  89. transitionCallback: function(){
  90. return (cache.vendor==='Moz' || cache.vendor=='ms') ? 'transitionend' : cache.vendor+'TransitionEnd';
  91. },
  92. canTransform: function(){
  93. return typeof settings.element.style[cache.vendor+'Transform'] !== 'undefined';
  94. },
  95. deepExtend: function(destination, source) {
  96. var property;
  97. for (property in source) {
  98. if (source[property] && source[property].constructor && source[property].constructor === Object) {
  99. destination[property] = destination[property] || {};
  100. utils.deepExtend(destination[property], source[property]);
  101. } else {
  102. destination[property] = source[property];
  103. }
  104. }
  105. return destination;
  106. },
  107. angleOfDrag: function(x, y) {
  108. var degrees, theta;
  109. // Calc Theta
  110. theta = Math.atan2(-(cache.startDragY - y), (cache.startDragX - x));
  111. if (theta < 0) {
  112. theta += 2 * Math.PI;
  113. }
  114. // Calc Degrees
  115. degrees = Math.floor(theta * (180 / Math.PI) - 180);
  116. if (degrees < 0 && degrees > -180) {
  117. degrees = 360 - Math.abs(degrees);
  118. }
  119. return Math.abs(degrees);
  120. },
  121. events: {
  122. addEvent: function addEvent(element, eventName, func) {
  123. if (element.addEventListener) {
  124. return element.addEventListener(eventName, func, false);
  125. } else if (element.attachEvent) {
  126. return element.attachEvent("on" + eventName, func);
  127. }
  128. },
  129. removeEvent: function addEvent(element, eventName, func) {
  130. if (element.addEventListener) {
  131. return element.removeEventListener(eventName, func, false);
  132. } else if (element.attachEvent) {
  133. return element.detachEvent("on" + eventName, func);
  134. }
  135. },
  136. prevent: function(e) {
  137. if (e.preventDefault) {
  138. e.preventDefault();
  139. } else {
  140. e.returnValue = false;
  141. }
  142. }
  143. },
  144. parentUntil: function(el, attr) {
  145. while (el.parentNode) {
  146. if (el.getAttribute && el.getAttribute(attr)){
  147. return el;
  148. }
  149. el = el.parentNode;
  150. }
  151. return null;
  152. }
  153. },
  154. action = {
  155. translate: {
  156. get: {
  157. matrix: function(index) {
  158. if( !utils.canTransform() ){
  159. return parseInt(settings.element.style.left, 10);
  160. } else {
  161. var matrix = win.getComputedStyle(settings.element)[cache.vendor+'Transform'].match(/\((.*)\)/),
  162. ieOffset = 8;
  163. if (matrix) {
  164. matrix = matrix[1].split(',');
  165. if(matrix.length==16){
  166. index+=ieOffset;
  167. }
  168. return parseInt(matrix[index], 10);
  169. }
  170. return 0;
  171. }
  172. }
  173. },
  174. easeCallback: function(){
  175. settings.element.style[cache.vendor+'Transition'] = '';
  176. cache.translation = action.translate.get.matrix(4);
  177. cache.easing = false;
  178. clearInterval(cache.animatingInterval);
  179. if(cache.easingTo===0){
  180. utils.klass.remove(doc.body, 'snapjs-right');
  181. utils.klass.remove(doc.body, 'snapjs-left');
  182. }
  183. utils.dispatchEvent('animated');
  184. utils.events.removeEvent(settings.element, utils.transitionCallback(), action.translate.easeCallback);
  185. },
  186. easeTo: function(n) {
  187. if( !utils.canTransform() ){
  188. cache.translation = n;
  189. action.translate.x(n);
  190. } else {
  191. cache.easing = true;
  192. cache.easingTo = n;
  193. settings.element.style[cache.vendor+'Transition'] = 'all ' + settings.transitionSpeed + 's ' + settings.easing;
  194. cache.animatingInterval = setInterval(function() {
  195. utils.dispatchEvent('animating');
  196. }, 1);
  197. utils.events.addEvent(settings.element, utils.transitionCallback(), action.translate.easeCallback);
  198. action.translate.x(n);
  199. }
  200. },
  201. x: function(n) {
  202. if( (settings.disable=='left' && n>0) ||
  203. (settings.disable=='right' && n<0)
  204. ){ return; }
  205. n = parseInt(n, 10);
  206. if(isNaN(n)){
  207. n = 0;
  208. }
  209. if( utils.canTransform() ){
  210. var theTranslate = 'translate3d(' + n + 'px, 0,0)';
  211. settings.element.style[cache.vendor+'Transform'] = theTranslate;
  212. } else {
  213. settings.element.style.width = (win.innerWidth || doc.documentElement.clientWidth)+'px';
  214. settings.element.style.left = n+'px';
  215. settings.element.style.right = '';
  216. }
  217. }
  218. },
  219. drag: {
  220. listen: function() {
  221. cache.translation = 0;
  222. cache.easing = false;
  223. utils.events.addEvent(settings.element, utils.eventType('down'), action.drag.startDrag);
  224. utils.events.addEvent(settings.element, utils.eventType('move'), action.drag.dragging);
  225. utils.events.addEvent(settings.element, utils.eventType('up'), action.drag.endDrag);
  226. },
  227. stopListening: function() {
  228. utils.events.removeEvent(settings.element, utils.eventType('down'), action.drag.startDrag);
  229. utils.events.removeEvent(settings.element, utils.eventType('move'), action.drag.dragging);
  230. utils.events.removeEvent(settings.element, utils.eventType('up'), action.drag.endDrag);
  231. },
  232. startDrag: function(e) {
  233. // No drag on ignored elements
  234. var ignoreParent = utils.parentUntil(e.target ? e.target : e.srcElement, 'data-snap-ignore');
  235. if (ignoreParent) {
  236. utils.dispatchEvent('ignore');
  237. return;
  238. }
  239. utils.dispatchEvent('start');
  240. settings.element.style[cache.vendor+'Transition'] = '';
  241. cache.isDragging = true;
  242. cache.hasIntent = null;
  243. cache.intentChecked = false;
  244. cache.startDragX = utils.page('X', e);
  245. cache.startDragY = utils.page('Y', e);
  246. cache.dragWatchers = {
  247. current: 0,
  248. last: 0,
  249. hold: 0,
  250. state: ''
  251. };
  252. cache.simpleStates = {
  253. opening: null,
  254. towards: null,
  255. hyperExtending: null,
  256. halfway: null,
  257. flick: null,
  258. translation: {
  259. absolute: 0,
  260. relative: 0,
  261. sinceDirectionChange: 0,
  262. percentage: 0
  263. }
  264. };
  265. },
  266. dragging: function(e) {
  267. if (cache.isDragging && settings.touchToDrag) {
  268. var thePageX = utils.page('X', e),
  269. thePageY = utils.page('Y', e),
  270. translated = cache.translation,
  271. absoluteTranslation = action.translate.get.matrix(4),
  272. whileDragX = thePageX - cache.startDragX,
  273. openingLeft = absoluteTranslation > 0,
  274. translateTo = whileDragX,
  275. diff;
  276. // Shown no intent already
  277. if((cache.intentChecked && !cache.hasIntent)){
  278. return;
  279. }
  280. if(settings.addBodyClasses){
  281. if((absoluteTranslation)>0){
  282. utils.klass.add(doc.body, 'snapjs-left');
  283. utils.klass.remove(doc.body, 'snapjs-right');
  284. } else if((absoluteTranslation)<0){
  285. utils.klass.add(doc.body, 'snapjs-right');
  286. utils.klass.remove(doc.body, 'snapjs-left');
  287. }
  288. }
  289. if (cache.hasIntent === false || cache.hasIntent === null) {
  290. var deg = utils.angleOfDrag(thePageX, thePageY),
  291. inRightRange = (deg >= 0 && deg <= settings.slideIntent) || (deg <= 360 && deg > (360 - settings.slideIntent)),
  292. inLeftRange = (deg >= 180 && deg <= (180 + settings.slideIntent)) || (deg <= 180 && deg >= (180 - settings.slideIntent));
  293. if (!inLeftRange && !inRightRange) {
  294. cache.hasIntent = false;
  295. } else {
  296. cache.hasIntent = true;
  297. }
  298. cache.intentChecked = true;
  299. }
  300. if (
  301. (settings.minDragDistance>=Math.abs(thePageX-cache.startDragX)) && // Has user met minimum drag distance?
  302. (cache.hasIntent === false)
  303. ) {
  304. return;
  305. }
  306. utils.events.prevent(e);
  307. utils.dispatchEvent('drag');
  308. cache.dragWatchers.current = thePageX;
  309. // Determine which direction we are going
  310. if (cache.dragWatchers.last > thePageX) {
  311. if (cache.dragWatchers.state !== 'left') {
  312. cache.dragWatchers.state = 'left';
  313. cache.dragWatchers.hold = thePageX;
  314. }
  315. cache.dragWatchers.last = thePageX;
  316. } else if (cache.dragWatchers.last < thePageX) {
  317. if (cache.dragWatchers.state !== 'right') {
  318. cache.dragWatchers.state = 'right';
  319. cache.dragWatchers.hold = thePageX;
  320. }
  321. cache.dragWatchers.last = thePageX;
  322. }
  323. if (openingLeft) {
  324. // Pulling too far to the right
  325. if (settings.maxPosition < absoluteTranslation) {
  326. diff = (absoluteTranslation - settings.maxPosition) * settings.resistance;
  327. translateTo = whileDragX - diff;
  328. }
  329. cache.simpleStates = {
  330. opening: 'left',
  331. towards: cache.dragWatchers.state,
  332. hyperExtending: settings.maxPosition < absoluteTranslation,
  333. halfway: absoluteTranslation > (settings.maxPosition / 2),
  334. flick: Math.abs(cache.dragWatchers.current - cache.dragWatchers.hold) > settings.flickThreshold,
  335. translation: {
  336. absolute: absoluteTranslation,
  337. relative: whileDragX,
  338. sinceDirectionChange: (cache.dragWatchers.current - cache.dragWatchers.hold),
  339. percentage: (absoluteTranslation/settings.maxPosition)*100
  340. }
  341. };
  342. } else {
  343. // Pulling too far to the left
  344. if (settings.minPosition > absoluteTranslation) {
  345. diff = (absoluteTranslation - settings.minPosition) * settings.resistance;
  346. translateTo = whileDragX - diff;
  347. }
  348. cache.simpleStates = {
  349. opening: 'right',
  350. towards: cache.dragWatchers.state,
  351. hyperExtending: settings.minPosition > absoluteTranslation,
  352. halfway: absoluteTranslation < (settings.minPosition / 2),
  353. flick: Math.abs(cache.dragWatchers.current - cache.dragWatchers.hold) > settings.flickThreshold,
  354. translation: {
  355. absolute: absoluteTranslation,
  356. relative: whileDragX,
  357. sinceDirectionChange: (cache.dragWatchers.current - cache.dragWatchers.hold),
  358. percentage: (absoluteTranslation/settings.minPosition)*100
  359. }
  360. };
  361. }
  362. action.translate.x(translateTo + translated);
  363. }
  364. },
  365. endDrag: function(e) {
  366. if (cache.isDragging) {
  367. utils.dispatchEvent('end');
  368. var translated = action.translate.get.matrix(4);
  369. // Tap Close
  370. if (cache.dragWatchers.current === 0 && translated !== 0 && settings.tapToClose) {
  371. utils.events.prevent(e);
  372. action.translate.easeTo(0);
  373. cache.isDragging = false;
  374. cache.startDragX = 0;
  375. return;
  376. }
  377. // Revealing Left
  378. if (cache.simpleStates.opening === 'left') {
  379. // Halfway, Flicking, or Too Far Out
  380. if ((cache.simpleStates.halfway || cache.simpleStates.hyperExtending || cache.simpleStates.flick)) {
  381. if (cache.simpleStates.flick && cache.simpleStates.towards === 'left') { // Flicking Closed
  382. action.translate.easeTo(0);
  383. } else if (
  384. (cache.simpleStates.flick && cache.simpleStates.towards === 'right') || // Flicking Open OR
  385. (cache.simpleStates.halfway || cache.simpleStates.hyperExtending) // At least halfway open OR hyperextending
  386. ) {
  387. action.translate.easeTo(settings.maxPosition); // Open Left
  388. }
  389. } else {
  390. action.translate.easeTo(0); // Close Left
  391. }
  392. // Revealing Right
  393. } else if (cache.simpleStates.opening === 'right') {
  394. // Halfway, Flicking, or Too Far Out
  395. if ((cache.simpleStates.halfway || cache.simpleStates.hyperExtending || cache.simpleStates.flick)) {
  396. if (cache.simpleStates.flick && cache.simpleStates.towards === 'right') { // Flicking Closed
  397. action.translate.easeTo(0);
  398. } else if (
  399. (cache.simpleStates.flick && cache.simpleStates.towards === 'left') || // Flicking Open OR
  400. (cache.simpleStates.halfway || cache.simpleStates.hyperExtending) // At least halfway open OR hyperextending
  401. ) {
  402. action.translate.easeTo(settings.minPosition); // Open Right
  403. }
  404. } else {
  405. action.translate.easeTo(0); // Close Right
  406. }
  407. }
  408. cache.isDragging = false;
  409. cache.startDragX = utils.page('X', e);
  410. }
  411. }
  412. }
  413. },
  414. init = function(opts) {
  415. if (opts.element) {
  416. utils.deepExtend(settings, opts);
  417. cache.vendor = utils.vendor();
  418. action.drag.listen();
  419. }
  420. };
  421. /*
  422. * Public
  423. */
  424. this.open = function(side) {
  425. utils.klass.remove(doc.body, 'snapjs-expand-left');
  426. utils.klass.remove(doc.body, 'snapjs-expand-right');
  427. if (side === 'left') {
  428. cache.simpleStates.opening = 'left';
  429. cache.simpleStates.towards = 'right';
  430. utils.klass.add(doc.body, 'snapjs-left');
  431. utils.klass.remove(doc.body, 'snapjs-right');
  432. action.translate.easeTo(settings.maxPosition);
  433. } else if (side === 'right') {
  434. cache.simpleStates.opening = 'right';
  435. cache.simpleStates.towards = 'left';
  436. utils.klass.remove(doc.body, 'snapjs-left');
  437. utils.klass.add(doc.body, 'snapjs-right');
  438. action.translate.easeTo(settings.minPosition);
  439. }
  440. };
  441. this.close = function() {
  442. action.translate.easeTo(0);
  443. };
  444. this.expand = function(side){
  445. var to = win.innerWidth || doc.documentElement.clientWidth;
  446. if(side==='left'){
  447. utils.klass.add(doc.body, 'snapjs-expand-left');
  448. utils.klass.remove(doc.body, 'snapjs-expand-right');
  449. } else {
  450. utils.klass.add(doc.body, 'snapjs-expand-right');
  451. utils.klass.remove(doc.body, 'snapjs-expand-left');
  452. to *= -1;
  453. }
  454. action.translate.easeTo(to);
  455. };
  456. this.on = function(evt, fn) {
  457. eventList[evt] = fn;
  458. return this;
  459. };
  460. this.off = function(evt) {
  461. if (eventList[evt]) {
  462. eventList[evt] = false;
  463. }
  464. };
  465. this.enable = function() {
  466. action.drag.listen();
  467. };
  468. this.disable = function() {
  469. action.drag.stopListening();
  470. };
  471. this.settings = function(opts){
  472. utils.deepExtend(settings, opts);
  473. };
  474. this.state = function() {
  475. var state,
  476. fromLeft = action.translate.get.matrix(4);
  477. if (fromLeft === settings.maxPosition) {
  478. state = 'left';
  479. } else if (fromLeft === settings.minPosition) {
  480. state = 'right';
  481. } else {
  482. state = 'closed';
  483. }
  484. return {
  485. state: state,
  486. info: cache.simpleStates
  487. };
  488. };
  489. init(userOpts);
  490. };
  491. if ((typeof module !== 'undefined') && module.exports) {
  492. module.exports = Snap;
  493. }
  494. if (typeof ender === 'undefined') {
  495. this.Snap = Snap;
  496. }
  497. if ((typeof define === "function") && define.amd) {
  498. define("snap", [], function() {
  499. return Snap;
  500. });
  501. }
  502. }).call(this, window, document);