spinningwheel.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. /**
  2. *
  3. * Find more about the Spinning Wheel function at
  4. * http://cubiq.org/spinning-wheel-on-webkit-for-iphone-ipod-touch/11
  5. *
  6. * Copyright (c) 2009 Matteo Spinelli, http://cubiq.org/
  7. * Released under MIT license
  8. * http://cubiq.org/dropbox/mit-license.txt
  9. *
  10. * Version 1.4 - Last updated: 2009.07.09
  11. *
  12. */
  13. define(function() {
  14. var cachePosition = {x: 0, y: 0};
  15. var SpinningWheel = {
  16. cellHeight: 44,
  17. friction: 0.003,
  18. slotData: [],
  19. /**
  20. *
  21. * Event handler
  22. *
  23. */
  24. handleEvent: function (e) {
  25. if (e.type == 'touchstart') {
  26. this.lockScreen(e);
  27. if (e.currentTarget.id == 'sw-cancel' || e.currentTarget.id == 'sw-done') {
  28. this.tapDown(e);
  29. cachePosition.x = e.touches[0]['pageX'];
  30. cachePosition.y = e.touches[0]['pageY'];
  31. } else if (e.currentTarget.id == 'sw-frame') {
  32. this.scrollStart(e);
  33. }
  34. } else if (e.type == 'touchmove') {
  35. this.lockScreen(e);
  36. if (e.currentTarget.id == 'sw-cancel' || e.currentTarget.id == 'sw-done') {
  37. //android 4.4以上touchmove会一直触发,判断一下偏移量
  38. if (Math.abs(e.touches[0]['pageX'] - cachePosition.x) > 10 || Math.abs(e.touches[0]['pageY'] - cachePosition.y) > 10) {
  39. this.tapCancel(e);
  40. }
  41. } else if (e.currentTarget.id == 'sw-frame') {
  42. this.scrollMove(e);
  43. }
  44. } else if (e.type == 'touchend') {
  45. if (e.currentTarget.id == 'sw-cancel' || e.currentTarget.id == 'sw-done') {
  46. this.tapUp(e);
  47. } else if (e.currentTarget.id == 'sw-frame') {
  48. this.scrollEnd(e);
  49. }
  50. } else if (e.type == 'webkitTransitionEnd') {
  51. if (e.target.id == 'sw-wrapper') {
  52. this.destroy();
  53. } else {
  54. this.backWithinBoundaries(e);
  55. }
  56. } else if (e.type == 'orientationchange') {
  57. this.onOrientationChange(e);
  58. } else if (e.type == 'scroll') {
  59. this.onScroll(e);
  60. }
  61. },
  62. /**
  63. *
  64. * Global events
  65. *
  66. */
  67. onOrientationChange: function (e) {
  68. window.scrollTo(0, 0);
  69. this.swWrapper.style.top = window.innerHeight + window.pageYOffset + 'px';
  70. this.calculateSlotsWidth();
  71. },
  72. onScroll: function (e) {
  73. this.swWrapper.style.top = window.innerHeight + window.pageYOffset + 'px';
  74. },
  75. lockScreen: function (e) {
  76. e.preventDefault();
  77. e.stopPropagation();
  78. },
  79. /**
  80. *
  81. * Initialization
  82. *
  83. */
  84. reset: function () {
  85. this.slotEl = [];
  86. this.activeSlot = null;
  87. this.swWrapper = undefined;
  88. this.swSlotWrapper = undefined;
  89. this.swSlots = undefined;
  90. this.swFrame = undefined;
  91. },
  92. calculateSlotsWidth: function () {
  93. var div = this.swSlots.getElementsByTagName('div');
  94. for (var i = 0; i < div.length; i += 1) {
  95. this.slotEl[i].slotWidth = div[i].offsetWidth;
  96. }
  97. },
  98. create: function () {
  99. var i, l, out, ul, div;
  100. this.reset(); // Initialize object variables
  101. // Create the Spinning Wheel main wrapper
  102. div = document.createElement('div');
  103. div.id = 'sw-wrapper';
  104. div.style.top = window.innerHeight + window.pageYOffset + 'px'; // Place the SW down the actual viewing screen
  105. div.style.webkitTransitionProperty = '-webkit-transform';
  106. div.innerHTML = '<div id="sw-header"><div id="sw-cancel">取消</' + 'div><div id="sw-done">确认</' + 'div></' + 'div><div id="sw-slots-wrapper"><div id="sw-slots"></' + 'div></' + 'div><div id="sw-frame"></' + 'div>';
  107. document.body.appendChild(div);
  108. this.swWrapper = div; // The SW wrapper
  109. this.swSlotWrapper = document.getElementById('sw-slots-wrapper'); // Slots visible area
  110. this.swSlots = document.getElementById('sw-slots'); // Pseudo table element (inner wrapper)
  111. this.swFrame = document.getElementById('sw-frame'); // The scrolling controller
  112. // Create HTML slot elements
  113. for (l = 0; l < this.slotData.length; l += 1) {
  114. // Create the slot
  115. ul = document.createElement('ul');
  116. out = '';
  117. for (i in this.slotData[l].values) {
  118. out += '<li>' + this.slotData[l].values[i] + '<' + '/li>';
  119. }
  120. ul.innerHTML = out;
  121. div = document.createElement('div'); // Create slot container
  122. div.className = this.slotData[l].style; // Add styles to the container
  123. div.appendChild(ul);
  124. // Append the slot to the wrapper
  125. this.swSlots.appendChild(div);
  126. ul.slotPosition = l; // Save the slot position inside the wrapper
  127. ul.slotYPosition = 0;
  128. ul.slotWidth = 0;
  129. ul.slotMaxScroll = this.swSlotWrapper.clientHeight - ul.clientHeight - 86;
  130. ul.style.webkitTransitionTimingFunction = 'cubic-bezier(0, 0, 0.2, 1)'; // Add default transition
  131. this.slotEl.push(ul); // Save the slot for later use
  132. // Place the slot to its default position (if other than 0)
  133. if (this.slotData[l].defaultValue) {
  134. this.scrollToValue(l, this.slotData[l].defaultValue);
  135. }
  136. }
  137. this.calculateSlotsWidth();
  138. // Global events
  139. document.addEventListener('touchstart', this, false); // Prevent page scrolling
  140. document.addEventListener('touchmove', this, false); // Prevent page scrolling
  141. window.addEventListener('orientationchange', this, true); // Optimize SW on orientation change
  142. window.addEventListener('scroll', this, true); // Reposition SW on page scroll
  143. // Cancel/Done buttons events
  144. document.getElementById('sw-cancel').addEventListener('touchstart', this, false);
  145. document.getElementById('sw-done').addEventListener('touchstart', this, false);
  146. // Add scrolling to the slots
  147. this.swFrame.addEventListener('touchstart', this, false);
  148. },
  149. open: function () {
  150. this.create();
  151. this.swWrapper.style.webkitTransitionTimingFunction = 'ease-out';
  152. this.swWrapper.style.webkitTransitionDuration = '400ms';
  153. this.swWrapper.style.webkitTransform = 'translate3d(0, -260px, 0)';
  154. },
  155. /**
  156. *
  157. * Unload
  158. *
  159. */
  160. destroy: function () {
  161. this.swWrapper.removeEventListener('webkitTransitionEnd', this, false);
  162. this.swFrame.removeEventListener('touchstart', this, false);
  163. document.getElementById('sw-cancel').removeEventListener('touchstart', this, false);
  164. document.getElementById('sw-done').removeEventListener('touchstart', this, false);
  165. document.removeEventListener('touchstart', this, false);
  166. document.removeEventListener('touchmove', this, false);
  167. window.removeEventListener('orientationchange', this, true);
  168. window.removeEventListener('scroll', this, true);
  169. this.slotData = [];
  170. this.cancelAction = function () {
  171. return false;
  172. };
  173. this.cancelDone = function () {
  174. return true;
  175. };
  176. this.reset();
  177. document.body.removeChild(document.getElementById('sw-wrapper'));
  178. },
  179. close: function () {
  180. this.swWrapper.style.webkitTransitionTimingFunction = 'ease-in';
  181. this.swWrapper.style.webkitTransitionDuration = '400ms';
  182. this.swWrapper.style.webkitTransform = 'translate3d(0, 0, 0)';
  183. this.swWrapper.addEventListener('webkitTransitionEnd', this, false);
  184. },
  185. /**
  186. *
  187. * Generic methods
  188. *
  189. */
  190. addSlot: function (values, style, defaultValue) {
  191. if (!style) {
  192. style = '';
  193. }
  194. style = style.split(' ');
  195. for (var i = 0; i < style.length; i += 1) {
  196. style[i] = 'sw-' + style[i];
  197. }
  198. style = style.join(' ');
  199. var obj = { 'values': values, 'style': style, 'defaultValue': defaultValue };
  200. this.slotData.push(obj);
  201. },
  202. getSelectedValues: function () {
  203. var index, count,
  204. i, l,
  205. keys = [], values = [];
  206. for (i in this.slotEl) {
  207. // Remove any residual animation
  208. this.slotEl[i].removeEventListener('webkitTransitionEnd', this, false);
  209. this.slotEl[i].style.webkitTransitionDuration = '0';
  210. if (this.slotEl[i].slotYPosition > 0) {
  211. this.setPosition(i, 0);
  212. } else if (this.slotEl[i].slotYPosition < this.slotEl[i].slotMaxScroll) {
  213. this.setPosition(i, this.slotEl[i].slotMaxScroll);
  214. }
  215. index = -Math.round(this.slotEl[i].slotYPosition / this.cellHeight);
  216. count = 0;
  217. for (l in this.slotData[i].values) {
  218. if (count == index) {
  219. keys.push(l);
  220. values.push(this.slotData[i].values[l]);
  221. break;
  222. }
  223. count += 1;
  224. }
  225. }
  226. return { 'keys': keys, 'values': values };
  227. },
  228. /**
  229. *
  230. * Rolling slots
  231. *
  232. */
  233. setPosition: function (slot, pos) {
  234. this.slotEl[slot].slotYPosition = pos;
  235. this.slotEl[slot].style.webkitTransform = 'translate3d(0, ' + pos + 'px, 0)';
  236. },
  237. scrollStart: function (e) {
  238. // Find the clicked slot
  239. var xPos = e.targetTouches[0].clientX - this.swSlots.offsetLeft; // Clicked position minus left offset (should be 11px)
  240. // Find tapped slot
  241. var slot = 0;
  242. for (var i = 0; i < this.slotEl.length; i += 1) {
  243. slot += this.slotEl[i].slotWidth;
  244. if (xPos < slot) {
  245. this.activeSlot = i;
  246. break;
  247. }
  248. }
  249. // If slot is readonly do nothing
  250. if (this.slotData[this.activeSlot].style.match('readonly')) {
  251. this.swFrame.removeEventListener('touchmove', this, false);
  252. this.swFrame.removeEventListener('touchend', this, false);
  253. return false;
  254. }
  255. this.slotEl[this.activeSlot].removeEventListener('webkitTransitionEnd', this, false); // Remove transition event (if any)
  256. this.slotEl[this.activeSlot].style.webkitTransitionDuration = '0'; // Remove any residual transition
  257. // Stop and hold slot position
  258. var theTransform = window.getComputedStyle(this.slotEl[this.activeSlot]).webkitTransform;
  259. theTransform = new WebKitCSSMatrix(theTransform).m42;
  260. if (theTransform != this.slotEl[this.activeSlot].slotYPosition) {
  261. this.setPosition(this.activeSlot, theTransform);
  262. }
  263. this.startY = e.targetTouches[0].clientY;
  264. this.scrollStartY = this.slotEl[this.activeSlot].slotYPosition;
  265. this.scrollStartTime = e.timeStamp;
  266. this.swFrame.addEventListener('touchmove', this, false);
  267. this.swFrame.addEventListener('touchend', this, false);
  268. return true;
  269. },
  270. scrollMove: function (e) {
  271. var topDelta = e.targetTouches[0].clientY - this.startY;
  272. if (this.slotEl[this.activeSlot].slotYPosition > 0 || this.slotEl[this.activeSlot].slotYPosition < this.slotEl[this.activeSlot].slotMaxScroll) {
  273. topDelta /= 2;
  274. }
  275. this.setPosition(this.activeSlot, this.slotEl[this.activeSlot].slotYPosition + topDelta);
  276. this.startY = e.targetTouches[0].clientY;
  277. // Prevent slingshot effect
  278. if (e.timeStamp - this.scrollStartTime > 80) {
  279. this.scrollStartY = this.slotEl[this.activeSlot].slotYPosition;
  280. this.scrollStartTime = e.timeStamp;
  281. }
  282. },
  283. scrollEnd: function (e) {
  284. this.swFrame.removeEventListener('touchmove', this, false);
  285. this.swFrame.removeEventListener('touchend', this, false);
  286. // If we are outside of the boundaries, let's go back to the sheepfold
  287. if (this.slotEl[this.activeSlot].slotYPosition > 0 || this.slotEl[this.activeSlot].slotYPosition < this.slotEl[this.activeSlot].slotMaxScroll) {
  288. this.scrollTo(this.activeSlot, this.slotEl[this.activeSlot].slotYPosition > 0 ? 0 : this.slotEl[this.activeSlot].slotMaxScroll);
  289. return false;
  290. }
  291. // Lame formula to calculate a fake deceleration
  292. var scrollDistance = this.slotEl[this.activeSlot].slotYPosition - this.scrollStartY;
  293. // The drag session was too short
  294. if (scrollDistance < this.cellHeight / 1.5 && scrollDistance > -this.cellHeight / 1.5) {
  295. if (this.slotEl[this.activeSlot].slotYPosition % this.cellHeight) {
  296. this.scrollTo(this.activeSlot, Math.round(this.slotEl[this.activeSlot].slotYPosition / this.cellHeight) * this.cellHeight, '100ms');
  297. }
  298. return false;
  299. }
  300. var scrollDuration = e.timeStamp - this.scrollStartTime;
  301. var newDuration = (2 * scrollDistance / scrollDuration) / this.friction;
  302. var newScrollDistance = (this.friction / 2) * (newDuration * newDuration);
  303. if (newDuration < 0) {
  304. newDuration = -newDuration;
  305. newScrollDistance = -newScrollDistance;
  306. }
  307. var newPosition = this.slotEl[this.activeSlot].slotYPosition + newScrollDistance;
  308. if (newPosition > 0) {
  309. // Prevent the slot to be dragged outside the visible area (top margin)
  310. newPosition /= 2;
  311. newDuration /= 3;
  312. if (newPosition > this.swSlotWrapper.clientHeight / 4) {
  313. newPosition = this.swSlotWrapper.clientHeight / 4;
  314. }
  315. } else if (newPosition < this.slotEl[this.activeSlot].slotMaxScroll) {
  316. // Prevent the slot to be dragged outside the visible area (bottom margin)
  317. newPosition = (newPosition - this.slotEl[this.activeSlot].slotMaxScroll) / 2 + this.slotEl[this.activeSlot].slotMaxScroll;
  318. newDuration /= 3;
  319. if (newPosition < this.slotEl[this.activeSlot].slotMaxScroll - this.swSlotWrapper.clientHeight / 4) {
  320. newPosition = this.slotEl[this.activeSlot].slotMaxScroll - this.swSlotWrapper.clientHeight / 4;
  321. }
  322. } else {
  323. newPosition = Math.round(newPosition / this.cellHeight) * this.cellHeight;
  324. }
  325. this.scrollTo(this.activeSlot, Math.round(newPosition), Math.round(newDuration) + 'ms');
  326. return true;
  327. },
  328. scrollTo: function (slotNum, dest, runtime) {
  329. this.slotEl[slotNum].style.webkitTransitionDuration = runtime ? runtime : '100ms';
  330. this.setPosition(slotNum, dest ? dest : 0);
  331. // If we are outside of the boundaries go back to the sheepfold
  332. if (this.slotEl[slotNum].slotYPosition > 0 || this.slotEl[slotNum].slotYPosition < this.slotEl[slotNum].slotMaxScroll) {
  333. this.slotEl[slotNum].addEventListener('webkitTransitionEnd', this, false);
  334. }
  335. },
  336. scrollToValue: function (slot, value) {
  337. var yPos, count, i;
  338. this.slotEl[slot].removeEventListener('webkitTransitionEnd', this, false);
  339. this.slotEl[slot].style.webkitTransitionDuration = '0';
  340. count = 0;
  341. for (i in this.slotData[slot].values) {
  342. if (i == value) {
  343. yPos = count * this.cellHeight;
  344. this.setPosition(slot, yPos);
  345. break;
  346. }
  347. count -= 1;
  348. }
  349. },
  350. backWithinBoundaries: function (e) {
  351. e.target.removeEventListener('webkitTransitionEnd', this, false);
  352. this.scrollTo(e.target.slotPosition, e.target.slotYPosition > 0 ? 0 : e.target.slotMaxScroll, '150ms');
  353. return false;
  354. },
  355. /**
  356. *
  357. * Buttons
  358. *
  359. */
  360. tapDown: function (e) {
  361. e.currentTarget.addEventListener('touchmove', this, false);
  362. e.currentTarget.addEventListener('touchend', this, false);
  363. e.currentTarget.className = 'sw-pressed';
  364. },
  365. tapCancel: function (e) {
  366. e.currentTarget.removeEventListener('touchmove', this, false);
  367. e.currentTarget.removeEventListener('touchend', this, false);
  368. e.currentTarget.className = '';
  369. },
  370. tapUp: function (e) {
  371. this.tapCancel(e);
  372. if (e.currentTarget.id == 'sw-cancel') {
  373. this.cancelAction();
  374. } else {
  375. this.doneAction();
  376. }
  377. this.close();
  378. },
  379. setCancelAction: function (action) {
  380. this.cancelAction = action;
  381. },
  382. setDoneAction: function (action) {
  383. this.doneAction = action;
  384. },
  385. cancelAction: function () {
  386. return false;
  387. },
  388. cancelDone: function () {
  389. return true;
  390. }
  391. };
  392. return SpinningWheel;
  393. })