TouchScroll.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. // FIXME:
  2. // * fully make use of transitions in glide routines
  3. define(["dojo/_base/declare", "dojo/on", "./util/touch", "./util/has-css3", "put-selector/put", "xstyle/css!./css/TouchScroll.css"],
  4. function(declare, on, touchUtil, has, put){
  5. var
  6. calcTimerRes = 50, // ms between drag velocity measurements
  7. glideTimerRes = 30, // ms between glide animation ticks
  8. current = {}, // records info for widget(s) currently being scrolled
  9. previous = {}, // records info for widget(s) that were in the middle of being scrolled when someone decided to scroll again
  10. glideThreshold = 1, // speed (in px) below which to stop glide - TODO: remove
  11. scrollbarAdjustment = 8, // number of px to adjust scrollbar dimension calculations
  12. // RegExps for parsing relevant x/y from translate and matrix values:
  13. translateRx = /^translate(?:3d)?\((-?\d+)(?:\.\d*)?(?:px)?, (-?\d+)/,
  14. matrixRx = /^matrix\(1, 0, 0, 1, (-?\d+)(?:\.\d*)?(?:px)?, (-?\d+)/,
  15. // store has-features we need, for computing property/function names:
  16. hasTransitions = has("css-transitions"),
  17. hasTransitionEnd = has("transitionend"),
  18. hasTransforms = has("css-transforms"),
  19. hasTransforms3d = has("css-transforms3d"),
  20. // and declare vars to store info on the properties/functions we'll need
  21. cssPrefix, transitionPrefix, transformProp, translatePrefix, translateSuffix;
  22. if(hasTransforms3d){
  23. translatePrefix = "translate3d(";
  24. translateSuffix = ",0)";
  25. }else if(hasTransforms){
  26. translatePrefix = "translate(";
  27. translateSuffix = ")";
  28. }
  29. if(!hasTransitions || !translatePrefix){
  30. console.warn("CSS3 features unavailable for touch scroll effects.");
  31. return function(){};
  32. }
  33. // figure out strings for use later in events
  34. transformProp = hasTransforms3d || hasTransforms;
  35. transformProp = transformProp === true ? "transform" : transformProp + "Transform";
  36. transitionPrefix = hasTransitions === true ? "transition" :
  37. hasTransitions + "Transition";
  38. cssPrefix = hasTransforms === true ? "" :
  39. "-" + hasTransforms.toLowerCase() + "-";
  40. function showScrollbars(widget, curr){
  41. // Handles displaying of X/Y scrollbars as appropriate when a touchstart
  42. // occurs.
  43. var node = widget.touchNode,
  44. parentNode = node.parentNode,
  45. adjustedParentWidth = parentNode.offsetWidth - scrollbarAdjustment,
  46. adjustedParentHeight = parentNode.offsetHeight - scrollbarAdjustment,
  47. // Also populate scroll/offset properties on curr for reuse,
  48. // to avoid having to repeatedly hit the DOM.
  49. scrollWidth = curr.scrollWidth = node.scrollWidth,
  50. scrollHeight = curr.scrollHeight = node.scrollHeight,
  51. parentWidth = curr.parentWidth = parentNode.offsetWidth,
  52. parentHeight = curr.parentHeight = parentNode.offsetHeight,
  53. scrollbarNode;
  54. if(scrollWidth > parentWidth){
  55. if(!widget._scrollbarXNode){
  56. scrollbarNode = put(parentNode, "div.touchscroll-x");
  57. }
  58. scrollbarNode = widget._scrollbarXNode =
  59. widget._scrollbarXNode || put(scrollbarNode, "div.touchscroll-bar");
  60. scrollbarNode.style.width =
  61. adjustedParentWidth * adjustedParentWidth / scrollWidth + "px";
  62. scrollbarNode.style.left = node.offsetLeft + "px";
  63. put(parentNode, ".touchscroll-scrollable-x");
  64. curr.scrollableX = true;
  65. }else{
  66. put(parentNode, "!touchscroll-scrollable-x");
  67. }
  68. if(scrollHeight > parentHeight){
  69. if(!widget._scrollbarYNode){
  70. scrollbarNode = put(parentNode, "div.touchscroll-y");
  71. }
  72. scrollbarNode = widget._scrollbarYNode =
  73. widget._scrollbarYNode || put(scrollbarNode, "div.touchscroll-bar");
  74. scrollbarNode.style.height =
  75. adjustedParentHeight * adjustedParentHeight / scrollHeight + "px";
  76. scrollbarNode.style.top = node.offsetTop + "px";
  77. put(parentNode, ".touchscroll-scrollable-y");
  78. curr.scrollableY = true;
  79. }else{
  80. put(parentNode, "!touchscroll-scrollable-y");
  81. }
  82. put(parentNode, "!touchscroll-fadeout");
  83. }
  84. function scroll(widget, options){
  85. // Handles updating of scroll position (from touchmove or glide).
  86. var node = widget.touchNode,
  87. curr = current[widget.id],
  88. pos, hasX, hasY, x, y;
  89. if(typeof options !== "object"){
  90. // Allow x, y to be passed directly w/o extra object creation.
  91. // (e.g. from ontouchmove)
  92. x = options;
  93. y = arguments[2];
  94. options = arguments[3];
  95. hasX = hasY = true;
  96. }else{
  97. hasX = "x" in options;
  98. hasY = "y" in options;
  99. // If either x or y weren't specified, pass through the current value.
  100. if(!hasX || !hasY){
  101. pos = widget.getScrollPosition();
  102. }
  103. x = hasX ? options.x : pos.x;
  104. y = hasY ? options.y : pos.y;
  105. }
  106. // Update transform on touchNode
  107. node.style[transformProp] =
  108. translatePrefix + -x + "px," + -y + "px" + translateSuffix;
  109. // Update scrollbar positions
  110. if(curr && hasX && widget._scrollbarXNode){
  111. widget._scrollbarXNode.style[transformProp] = translatePrefix +
  112. (x * curr.parentWidth / curr.scrollWidth) + "px,0" + translateSuffix;
  113. }
  114. if(curr && hasY && widget._scrollbarYNode){
  115. widget._scrollbarYNode.style[transformProp] = translatePrefix + "0," +
  116. (y * curr.parentHeight / curr.scrollHeight) + "px" + translateSuffix;
  117. }
  118. // Emit a scroll event that can be captured by handlers, passing along
  119. // scroll information in the event itself (since we already have the info,
  120. // and it'd be difficult to get from the node).
  121. on.emit(widget.touchNode.parentNode, "scroll", {
  122. scrollLeft: x,
  123. scrollTop: y
  124. });
  125. }
  126. function getScrollStyle(widget){
  127. // Returns match object for current scroll position based on transform.
  128. if(current[widget.id]){
  129. // Mid-transition: determine current X/Y from computed values.
  130. return matrixRx.exec(window.getComputedStyle(widget.touchNode)[transformProp]);
  131. }
  132. // Otherwise, determine current X/Y from applied style.
  133. return translateRx.exec(widget.touchNode.style[transformProp]);
  134. }
  135. function resetEffects(options){
  136. // Function to cut glide/bounce short, called in context of an object
  137. // from the current hash; attached only when a glide or bounce occurs.
  138. // Called on touchstart, when touch scrolling is canceled, when
  139. // momentum/bounce finishes, and by scrollTo on instances (in case it's
  140. // called directly during a glide/bounce).
  141. var widget = this.widget,
  142. nodes = [this.node, widget._scrollbarXNode, widget._scrollbarYNode],
  143. i = nodes.length;
  144. // Clear glide timer.
  145. if(this.timer){
  146. clearTimeout(this.timer);
  147. this.timer = null;
  148. }
  149. // Clear transition handlers, as we're about to cut it short.
  150. if(this.transitionHandler){
  151. // Unhook any existing transitionend handler, since we'll be
  152. // canceling the transition.
  153. this.transitionHandler.remove();
  154. }
  155. // Clear transition duration on main node and scrollbars.
  156. while(i--){
  157. if(nodes[i]){ nodes[i].style[transitionPrefix + "Duration"] = "0"; }
  158. }
  159. // Fade out scrollbars unless indicated otherwise (e.g. re-touch).
  160. if(!options || !options.preserveScrollbars){
  161. put(this.node.parentNode, ".touchscroll-fadeout");
  162. }
  163. // Remove this method so it can't be called again.
  164. delete this.resetEffects;
  165. }
  166. // functions for handling touch events on node to be scrolled
  167. function ontouchstart(evt){
  168. var widget = evt.widget,
  169. node = widget.touchNode,
  170. id = widget.id,
  171. posX = 0,
  172. posY = 0,
  173. touch, match, curr;
  174. // Check touches count (which hasn't counted this event yet);
  175. // ignore touch events on inappropriate number of contact points.
  176. if(touchUtil.countCurrentTouches(evt, node) !== widget.touchesToScroll){
  177. return;
  178. }
  179. match = getScrollStyle(widget);
  180. if(match){
  181. posX = +match[1];
  182. posY = +match[2];
  183. }
  184. if((curr = current[id])){
  185. // stop any active glide or bounce, since it's been re-touched
  186. if(curr.resetEffects){
  187. curr.resetEffects({ preserveScrollbars: true });
  188. }
  189. node.style[transformProp] =
  190. translatePrefix + posX + "px," + posY + "px" + translateSuffix;
  191. previous[id] = curr;
  192. }
  193. touch = evt.targetTouches[0];
  194. curr = current[id] = {
  195. widget: widget,
  196. node: node,
  197. // Subtract touch coords now, then add back later, so that translation
  198. // goes further negative when moving upwards.
  199. startX: posX - touch.pageX,
  200. startY: posY - touch.pageY,
  201. // Initialize lastX/Y, in case of a fast flick (< 1 full calc cycle).
  202. lastX: posX,
  203. lastY: posY,
  204. // Also store original pageX/Y for threshold check.
  205. pageX: touch.pageX,
  206. pageY: touch.pageY,
  207. tickFunc: function(){ calcTick(id); }
  208. };
  209. curr.timer = setTimeout(curr.tickFunc, calcTimerRes);
  210. }
  211. function ontouchmove(evt){
  212. var widget = evt.widget,
  213. id = widget.id,
  214. touchesToScroll = widget.touchesToScroll,
  215. curr = current[id],
  216. activeTouches, targetTouches, touch, nx, ny, minX, minY, i;
  217. // Ignore touchmove events with inappropriate number of contact points.
  218. if(!curr || (activeTouches = touchUtil.countCurrentTouches(evt, widget.touchNode)) !== touchesToScroll){
  219. // Also cancel touch scrolling if there are too many contact points.
  220. if(activeTouches > touchesToScroll){
  221. widget.cancelTouchScroll();
  222. }
  223. return;
  224. }
  225. targetTouches = evt.targetTouches;
  226. touch = targetTouches[0];
  227. // Show touch scrollbars on first sign of drag.
  228. if(!curr.scrollbarsShown){
  229. if(previous[id] || (
  230. Math.abs(touch.pageX - curr.pageX) > widget.scrollThreshold ||
  231. Math.abs(touch.pageY - curr.pageY) > widget.scrollThreshold)){
  232. showScrollbars(widget, curr);
  233. curr.scrollbarsShown = true;
  234. // Add flag to involved touches to provide indication to other handlers.
  235. for(i = targetTouches.length; i--;){
  236. targetTouches[i].touchScrolled = true;
  237. }
  238. }
  239. }
  240. if(curr.scrollbarsShown && (curr.scrollableX || curr.scrollableY)){
  241. // If area can be scrolled, prevent default behavior and perform scroll.
  242. evt.preventDefault();
  243. nx = curr.scrollableX ? curr.startX + touch.pageX : 0;
  244. ny = curr.scrollableY ? curr.startY + touch.pageY : 0;
  245. minX = curr.scrollableX ? -(curr.scrollWidth - curr.parentWidth) : 0;
  246. minY = curr.scrollableY ? -(curr.scrollHeight - curr.parentHeight) : 0;
  247. // If dragged beyond edge, halve the distance between.
  248. if(nx > 0){
  249. nx = nx / 2;
  250. }else if(nx < minX){
  251. nx = minX - (minX - nx) / 2;
  252. }
  253. if(ny > 0){
  254. ny = ny / 2;
  255. }else if(ny < minY){
  256. ny = minY - (minY - ny) / 2;
  257. }
  258. scroll(widget, -nx, -ny); // call scroll with positive coordinates
  259. }
  260. }
  261. function ontouchend(evt){
  262. var widget = evt.widget,
  263. id = widget.id,
  264. curr = current[id];
  265. if(!curr || touchUtil.countCurrentTouches(evt, widget.touchNode) != widget.touchesToScroll - 1){
  266. return;
  267. }
  268. startGlide(id);
  269. }
  270. // glide-related functions
  271. function calcTick(id){
  272. // Calculates current speed of touch drag
  273. var curr = current[id],
  274. node, match, x, y;
  275. if(!curr){ return; } // no currently-scrolling widget; abort
  276. node = curr.node;
  277. match = translateRx.exec(node.style[transformProp]);
  278. if(match){
  279. x = +match[1];
  280. y = +match[2];
  281. // If previous reference point already exists, calculate velocity
  282. curr.velX = x - curr.lastX;
  283. curr.velY = y - curr.lastY;
  284. // set previous reference point for future iteration or calculation
  285. curr.lastX = x;
  286. curr.lastY = y;
  287. } else {
  288. curr.lastX = curr.lastY = 0;
  289. }
  290. curr.timer = setTimeout(curr.tickFunc, calcTimerRes);
  291. }
  292. function bounce(id, lastX, lastY){
  293. // Function called when a scroll ends, to handle rubber-banding beyond edges.
  294. var curr = current[id],
  295. widget = curr.widget,
  296. node = curr.node,
  297. scrollbarNode,
  298. x = curr.scrollableX ?
  299. Math.max(Math.min(0, lastX), -(curr.scrollWidth - curr.parentWidth)) :
  300. lastX,
  301. y = curr.scrollableY ?
  302. Math.max(Math.min(0, lastY), -(curr.scrollHeight - curr.parentHeight)) :
  303. lastY;
  304. function end(){
  305. // Performs reset operations upon end of scroll process.
  306. // Since transitions have run, delete transitionHandler up-front
  307. // (since it auto-removed itself anyway), then let
  308. // resetEffects do the rest of its usual job.
  309. delete curr.transitionHandler;
  310. curr.resetEffects();
  311. delete current[id];
  312. }
  313. // Timeout will have been cleared before bounce call, so remove timer.
  314. delete curr.timer;
  315. if (x != lastX || y != lastY){
  316. curr.transitionHandler = on.once(node, hasTransitionEnd, end);
  317. node.style[transitionPrefix + "Duration"] = widget.bounceDuration + "ms";
  318. node.style[transformProp] =
  319. translatePrefix + x + "px," + y + "px" + translateSuffix;
  320. // Also handle transitions for scrollbars.
  321. if(x != lastX && curr.scrollableX){
  322. scrollbarNode = curr.widget._scrollbarXNode;
  323. scrollbarNode.style[transitionPrefix + "Duration"] =
  324. widget.bounceDuration + "ms";
  325. if(lastX > x){
  326. // Further left; bounce back right
  327. scrollbarNode.style[transformProp] =
  328. translatePrefix + "0,0" + translateSuffix;
  329. }else{
  330. // Further right; bounce back left
  331. scrollbarNode.style[transformProp] =
  332. translatePrefix +
  333. (scrollbarNode.parentNode.offsetWidth - scrollbarNode.offsetWidth) +
  334. "px,0" + translateSuffix;
  335. }
  336. }
  337. if(y != lastY && curr.scrollableY){
  338. scrollbarNode = curr.widget._scrollbarYNode;
  339. scrollbarNode.style[transitionPrefix + "Duration"] =
  340. widget.bounceDuration + "ms";
  341. if(lastY > y){
  342. // Above top; bounce back down
  343. scrollbarNode.style[transformProp] =
  344. translatePrefix + "0,0" + translateSuffix;
  345. }else{
  346. // Below bottom; bounce back up
  347. scrollbarNode.style[transformProp] =
  348. translatePrefix + "0," +
  349. (scrollbarNode.parentNode.offsetHeight - scrollbarNode.offsetHeight) +
  350. "px" + translateSuffix;
  351. }
  352. }
  353. }else{
  354. end(); // no rubber-banding necessary; just reset
  355. }
  356. }
  357. function startGlide(id){
  358. // starts glide operation when drag ends
  359. var curr = current[id],
  360. prev = previous[id],
  361. match, posX, posY,
  362. INERTIA_ACCELERATION = 1.15;
  363. delete previous[id];
  364. if(curr.timer){ clearTimeout(curr.timer); }
  365. // Enable usage of resetEffects during glide or bounce.
  366. curr.resetEffects = resetEffects;
  367. // calculate velocity based on time and displacement since last tick
  368. match = translateRx.exec(curr.node.style[transformProp]);
  369. if(match){
  370. posX = +match[1];
  371. posY = +match[2];
  372. } else {
  373. posX = posY = 0;
  374. }
  375. // If there is no glide to perform (no exit velocity), or if we are
  376. // beyond boundaries on all applicable edges, immediately bounce back.
  377. if((!curr.velX && !curr.velY) ||
  378. ((posX >= 0 || posX <= -(curr.scrollWidth - curr.parentWidth)) &&
  379. (posY >= 0 || posY <= -(curr.scrollHeight - curr.parentHeight)))){
  380. bounce(id, posX, posY);
  381. return;
  382. }
  383. function sameSign(a, b){
  384. return ((a.velX <= 0 && b.velX <= 0) || (a.velX >= 0 && b.velX >= 0)) &&
  385. ((a.velY <= 0 && b.velY <= 0) || (a.velY >= 0 && b.velY >= 0));
  386. }
  387. if(prev && (prev.velX || prev.velY) && sameSign(curr, prev)){
  388. curr.velX = (curr.velX + prev.velX) * INERTIA_ACCELERATION;
  389. curr.velY = (curr.velY + prev.velY) * INERTIA_ACCELERATION;
  390. }
  391. // update lastX/Y with current position, for glide calculations
  392. curr.lastX = posX;
  393. curr.lastY = posY;
  394. curr.calcFunc = function(){ calcGlide(id); };
  395. curr.timer = setTimeout(curr.calcFunc, glideTimerRes);
  396. }
  397. function calcGlide(id){
  398. // performs glide and decelerates according to widget's glideDecel method
  399. var curr = current[id],
  400. node, parentNode, widget, i,
  401. nx, ny, nvx, nvy, // old/new coords and new velocities
  402. BOUNCE_DECELERATION_AMOUNT = 6;
  403. if(!curr){ return; }
  404. node = curr.node;
  405. parentNode = node.parentNode;
  406. widget = curr.widget;
  407. nvx = widget.glideDecel(curr.velX);
  408. nvy = widget.glideDecel(curr.velY);
  409. if(Math.abs(nvx) >= glideThreshold || Math.abs(nvy) >= glideThreshold){
  410. // still above stop threshold; update transformation
  411. nx = curr.lastX + nvx;
  412. ny = curr.lastY + nvy;
  413. // If glide has traveled beyond any edges, institute rubber-band effect
  414. // by further decelerating.
  415. if(nx > 0 || nx < -(curr.scrollWidth - curr.parentWidth)){
  416. for(i = BOUNCE_DECELERATION_AMOUNT; i--;){
  417. nvx = widget.glideDecel(nvx);
  418. }
  419. }
  420. if(ny > 0 || ny < -(curr.scrollHeight - curr.parentHeight)){
  421. for(i = BOUNCE_DECELERATION_AMOUNT; i--;){
  422. nvy = widget.glideDecel(nvy);
  423. }
  424. }
  425. // still scrollable; update offsets/velocities and schedule next tick
  426. scroll(widget, -nx, -ny); // call scroll with positive coordinates
  427. // update information
  428. curr.lastX = nx;
  429. curr.lastY = ny;
  430. curr.velX = nvx;
  431. curr.velY = nvy;
  432. curr.timer = setTimeout(curr.calcFunc, glideTimerRes);
  433. }else{
  434. bounce(id, curr.lastX, curr.lastY);
  435. }
  436. }
  437. return declare(null, {
  438. // touchesToScroll: Number
  439. // Number of touches to require on the component's touch target node
  440. // in order to trigger scrolling behavior.
  441. touchesToScroll: 1,
  442. // touchNode: DOMNode?
  443. // Node upon which scroll behavior will be based; transformations will be
  444. // applied to this node, and events and some DOM/styles will be applied
  445. // to its *parent*. If not specified, defaults to containerNode.
  446. touchNode: null,
  447. // scrollThreshold: Number
  448. // Minimum number of pixels to wait for user to scroll (in any direction)
  449. // before initiating scroll.
  450. scrollThreshold: 10,
  451. // bounceDuration: Number
  452. // Number of milliseconds which "rubber-banding" transitions
  453. // (i.e. bouncing back from beyond edges) should take.
  454. bounceDuration: 300,
  455. postCreate: function(){
  456. this._initTouch();
  457. this.inherited(arguments);
  458. },
  459. _initTouch: function(){
  460. var node = this.touchNode = this.touchNode || this.containerNode,
  461. widget = this,
  462. parentNode;
  463. if(!node || !node.parentNode){
  464. // Bail out if we have no touchNode or containerNode, or if we don't
  465. // seem to have a parent node to work with.
  466. console.warn("TouchScroll requires a nested node upon which to operate.");
  467. return;
  468. }
  469. parentNode = node.parentNode;
  470. // Set overflow to hidden in order to prevent any native scroll logic.
  471. parentNode.style.overflow = "hidden";
  472. node.style[transitionPrefix + "Property"] = cssPrefix + "transform";
  473. node.style[transitionPrefix + "TimingFunction"] =
  474. "cubic-bezier(0.33, 0.66, 0.66, 1)";
  475. function cancelTouchScroll(){
  476. widget.cancelTouchScroll();
  477. }
  478. function wrapHandler(func){
  479. return function(evt){
  480. evt.widget = widget;
  481. evt.cancelTouchScroll = cancelTouchScroll;
  482. func.call(this, evt);
  483. };
  484. }
  485. this._touchScrollListeners = [
  486. on(parentNode, "touchstart", wrapHandler(ontouchstart)),
  487. on(parentNode, "touchmove", wrapHandler(ontouchmove)),
  488. on(parentNode, "touchend,touchcancel", wrapHandler(ontouchend))
  489. ];
  490. },
  491. destroy: function(){
  492. var i = this._touchScrollListeners.length;
  493. while(i--){
  494. this._touchScrollListeners[i].remove();
  495. }
  496. delete current[this.id];
  497. this.inherited(arguments);
  498. },
  499. scrollTo: function(options){
  500. // summary:
  501. // Scrolls the widget to a specific position.
  502. // options: Object
  503. // Object containing target x and/or y position to scroll to
  504. // (if unspecified, scroll in that direction will be preserved).
  505. // Also supports the following other options:
  506. // * preserveMomentum: if true, will not reset any active
  507. // momentum or bounce on the widget
  508. var curr = current[this.id],
  509. touchNode = this.touchNode,
  510. parentNode = touchNode.parentNode;
  511. if(!options.preserveMomentum && curr && curr.resetEffects){
  512. // Stop any glide or bounce occurring before scrolling.
  513. curr.resetEffects();
  514. }
  515. // Constrain coordinates within scrollable boundaries.
  516. if(options.x){
  517. options.x = Math.max(0, Math.min(options.x,
  518. touchNode.scrollWidth - parentNode.offsetWidth));
  519. }
  520. if(options.y){
  521. options.y = Math.max(0, Math.min(options.y,
  522. touchNode.scrollHeight - parentNode.offsetHeight));
  523. }
  524. scroll(this, options);
  525. },
  526. getScrollPosition: function(){
  527. // summary:
  528. // Determines current translation from computed style
  529. // (if mid-transition), or applied style.
  530. var match = getScrollStyle(this);
  531. return match ? { x: -match[1], y: -match[2] } : { x: 0, y: 0 };
  532. },
  533. cancelTouchScroll: function(){
  534. // summary:
  535. // Removes any existing scroll information for this component from the
  536. // current map, effectively canceling any TouchScroll behavior for
  537. // that particular touch gesture.
  538. var curr = current[this.id];
  539. if(!curr){ return; }
  540. if(curr.resetEffects){ curr.resetEffects(); }
  541. else{
  542. if(curr.timer){ clearTimeout(curr.timer); }
  543. put(curr.node.parentNode, ".touchscroll-fadeout");
  544. }
  545. delete current[this.id];
  546. },
  547. glideDecel: function(n){
  548. // summary:
  549. // Deceleration algorithm. Given a number representing velocity,
  550. // returns a new velocity to impose for the next "tick".
  551. // (Don't forget that velocity can be positive or negative!)
  552. return n * 0.9; // Number
  553. }
  554. });
  555. });