Version 3.17.2
Show:

File: scrollview/js/scrollview-base.js

  1. /**
  2. * The scrollview-base module provides a basic ScrollView Widget, without scrollbar indicators
  3. *
  4. * @module scrollview
  5. * @submodule scrollview-base
  6. */
  7.  
  8. // Local vars
  9. var getClassName = Y.ClassNameManager.getClassName,
  10. DOCUMENT = Y.config.doc,
  11. IE = Y.UA.ie,
  12. NATIVE_TRANSITIONS = Y.Transition.useNative,
  13. vendorPrefix = Y.Transition._VENDOR_PREFIX, // Todo: This is a private property, and alternative approaches should be investigated
  14. SCROLLVIEW = 'scrollview',
  15. CLASS_NAMES = {
  16. vertical: getClassName(SCROLLVIEW, 'vert'),
  17. horizontal: getClassName(SCROLLVIEW, 'horiz')
  18. },
  19. EV_SCROLL_END = 'scrollEnd',
  20. FLICK = 'flick',
  21. DRAG = 'drag',
  22. MOUSEWHEEL = 'mousewheel',
  23. UI = 'ui',
  24. TOP = 'top',
  25. LEFT = 'left',
  26. PX = 'px',
  27. AXIS = 'axis',
  28. SCROLL_Y = 'scrollY',
  29. SCROLL_X = 'scrollX',
  30. BOUNCE = 'bounce',
  31. DISABLED = 'disabled',
  32. DECELERATION = 'deceleration',
  33. DIM_X = 'x',
  34. DIM_Y = 'y',
  35. BOUNDING_BOX = 'boundingBox',
  36. CONTENT_BOX = 'contentBox',
  37. GESTURE_MOVE = 'gesturemove',
  38. START = 'start',
  39. END = 'end',
  40. EMPTY = '',
  41. ZERO = '0s',
  42. SNAP_DURATION = 'snapDuration',
  43. SNAP_EASING = 'snapEasing',
  44. EASING = 'easing',
  45. FRAME_DURATION = 'frameDuration',
  46. BOUNCE_RANGE = 'bounceRange',
  47. _constrain = function (val, min, max) {
  48. return Math.min(Math.max(val, min), max);
  49. };
  50.  
  51. /**
  52. * ScrollView provides a scrollable widget, supporting flick gestures,
  53. * across both touch and mouse based devices.
  54. *
  55. * @class ScrollView
  56. * @param config {Object} Object literal with initial attribute values
  57. * @extends Widget
  58. * @constructor
  59. */
  60. function ScrollView() {
  61. ScrollView.superclass.constructor.apply(this, arguments);
  62. }
  63.  
  64. Y.ScrollView = Y.extend(ScrollView, Y.Widget, {
  65.  
  66. // *** Y.ScrollView prototype
  67.  
  68. /**
  69. * Flag driving whether or not we should try and force H/W acceleration when transforming. Currently enabled by default for Webkit.
  70. * Used by the _transform method.
  71. *
  72. * @property _forceHWTransforms
  73. * @type boolean
  74. * @protected
  75. */
  76. _forceHWTransforms: Y.UA.webkit ? true : false,
  77.  
  78. /**
  79. * <p>Used to control whether or not ScrollView's internal
  80. * gesturemovestart, gesturemove and gesturemoveend
  81. * event listeners should preventDefault. The value is an
  82. * object, with "start", "move" and "end" properties used to
  83. * specify which events should preventDefault and which shouldn't:</p>
  84. *
  85. * <pre>
  86. * {
  87. * start: false,
  88. * move: true,
  89. * end: false
  90. * }
  91. * </pre>
  92. *
  93. * <p>The default values are set up in order to prevent panning,
  94. * on touch devices, while allowing click listeners on elements inside
  95. * the ScrollView to be notified as expected.</p>
  96. *
  97. * @property _prevent
  98. * @type Object
  99. * @protected
  100. */
  101. _prevent: {
  102. start: false,
  103. move: true,
  104. end: false
  105. },
  106.  
  107. /**
  108. * Contains the distance (postive or negative) in pixels by which
  109. * the scrollview was last scrolled. This is useful when setting up
  110. * click listeners on the scrollview content, which on mouse based
  111. * devices are always fired, even after a drag/flick.
  112. *
  113. * <p>Touch based devices don't currently fire a click event,
  114. * if the finger has been moved (beyond a threshold) so this
  115. * check isn't required, if working in a purely touch based environment</p>
  116. *
  117. * @property lastScrolledAmt
  118. * @type Number
  119. * @public
  120. * @default 0
  121. */
  122. lastScrolledAmt: 0,
  123.  
  124. /**
  125. * Internal state, defines the minimum amount that the scrollview can be scrolled along the X axis
  126. *
  127. * @property _minScrollX
  128. * @type number
  129. * @protected
  130. */
  131. _minScrollX: null,
  132.  
  133. /**
  134. * Internal state, defines the maximum amount that the scrollview can be scrolled along the X axis
  135. *
  136. * @property _maxScrollX
  137. * @type number
  138. * @protected
  139. */
  140. _maxScrollX: null,
  141.  
  142. /**
  143. * Internal state, defines the minimum amount that the scrollview can be scrolled along the Y axis
  144. *
  145. * @property _minScrollY
  146. * @type number
  147. * @protected
  148. */
  149. _minScrollY: null,
  150.  
  151. /**
  152. * Internal state, defines the maximum amount that the scrollview can be scrolled along the Y axis
  153. *
  154. * @property _maxScrollY
  155. * @type number
  156. * @protected
  157. */
  158. _maxScrollY: null,
  159.  
  160. /**
  161. * Designated initializer
  162. *
  163. * @method initializer
  164. * @param {Object} Configuration object for the plugin
  165. */
  166. initializer: function () {
  167. var sv = this;
  168.  
  169. // Cache these values, since they aren't going to change.
  170. sv._bb = sv.get(BOUNDING_BOX);
  171. sv._cb = sv.get(CONTENT_BOX);
  172.  
  173. // Cache some attributes
  174. sv._cAxis = sv.get(AXIS);
  175. sv._cBounce = sv.get(BOUNCE);
  176. sv._cBounceRange = sv.get(BOUNCE_RANGE);
  177. sv._cDeceleration = sv.get(DECELERATION);
  178. sv._cFrameDuration = sv.get(FRAME_DURATION);
  179. },
  180.  
  181. /**
  182. * bindUI implementation
  183. *
  184. * Hooks up events for the widget
  185. * @method bindUI
  186. */
  187. bindUI: function () {
  188. var sv = this;
  189.  
  190. // Bind interaction listers
  191. sv._bindFlick(sv.get(FLICK));
  192. sv._bindDrag(sv.get(DRAG));
  193. sv._bindMousewheel(true);
  194.  
  195. // Bind change events
  196. sv._bindAttrs();
  197.  
  198. // IE SELECT HACK. See if we can do this non-natively and in the gesture for a future release.
  199. if (IE) {
  200. sv._fixIESelect(sv._bb, sv._cb);
  201. }
  202.  
  203. // Set any deprecated static properties
  204. if (ScrollView.SNAP_DURATION) {
  205. sv.set(SNAP_DURATION, ScrollView.SNAP_DURATION);
  206. }
  207.  
  208. if (ScrollView.SNAP_EASING) {
  209. sv.set(SNAP_EASING, ScrollView.SNAP_EASING);
  210. }
  211.  
  212. if (ScrollView.EASING) {
  213. sv.set(EASING, ScrollView.EASING);
  214. }
  215.  
  216. if (ScrollView.FRAME_STEP) {
  217. sv.set(FRAME_DURATION, ScrollView.FRAME_STEP);
  218. }
  219.  
  220. if (ScrollView.BOUNCE_RANGE) {
  221. sv.set(BOUNCE_RANGE, ScrollView.BOUNCE_RANGE);
  222. }
  223.  
  224. // Recalculate dimension properties
  225. // TODO: This should be throttled.
  226. // Y.one(WINDOW).after('resize', sv._afterDimChange, sv);
  227. },
  228.  
  229. /**
  230. * Bind event listeners
  231. *
  232. * @method _bindAttrs
  233. * @private
  234. */
  235. _bindAttrs: function () {
  236. var sv = this,
  237. scrollChangeHandler = sv._afterScrollChange,
  238. dimChangeHandler = sv._afterDimChange;
  239.  
  240. // Bind any change event listeners
  241. sv.after({
  242. 'scrollEnd': sv._afterScrollEnd,
  243. 'disabledChange': sv._afterDisabledChange,
  244. 'flickChange': sv._afterFlickChange,
  245. 'dragChange': sv._afterDragChange,
  246. 'axisChange': sv._afterAxisChange,
  247. 'scrollYChange': scrollChangeHandler,
  248. 'scrollXChange': scrollChangeHandler,
  249. 'heightChange': dimChangeHandler,
  250. 'widthChange': dimChangeHandler
  251. });
  252. },
  253.  
  254. /**
  255. * Bind (or unbind) gesture move listeners required for drag support
  256. *
  257. * @method _bindDrag
  258. * @param drag {boolean} If true, the method binds listener to enable
  259. * drag (gesturemovestart). If false, the method unbinds gesturemove
  260. * listeners for drag support.
  261. * @private
  262. */
  263. _bindDrag: function (drag) {
  264. var sv = this,
  265. bb = sv._bb;
  266.  
  267. // Unbind any previous 'drag' listeners
  268. bb.detach(DRAG + '|*');
  269.  
  270. if (drag) {
  271. bb.on(DRAG + '|' + GESTURE_MOVE + START, Y.bind(sv._onGestureMoveStart, sv));
  272. }
  273. },
  274.  
  275. /**
  276. * Bind (or unbind) flick listeners.
  277. *
  278. * @method _bindFlick
  279. * @param flick {Object|boolean} If truthy, the method binds listeners for
  280. * flick support. If false, the method unbinds flick listeners.
  281. * @private
  282. */
  283. _bindFlick: function (flick) {
  284. var sv = this,
  285. bb = sv._bb;
  286.  
  287. // Unbind any previous 'flick' listeners
  288. bb.detach(FLICK + '|*');
  289.  
  290. if (flick) {
  291. bb.on(FLICK + '|' + FLICK, Y.bind(sv._flick, sv), flick);
  292.  
  293. // Rebind Drag, becuase _onGestureMoveEnd always has to fire -after- _flick
  294. sv._bindDrag(sv.get(DRAG));
  295. }
  296. },
  297.  
  298. /**
  299. * Bind (or unbind) mousewheel listeners.
  300. *
  301. * @method _bindMousewheel
  302. * @param mousewheel {Object|boolean} If truthy, the method binds listeners for
  303. * mousewheel support. If false, the method unbinds mousewheel listeners.
  304. * @private
  305. */
  306. _bindMousewheel: function (mousewheel) {
  307. var sv = this,
  308. bb = sv._bb;
  309.  
  310. // Unbind any previous 'mousewheel' listeners
  311. // TODO: This doesn't actually appear to work properly. Fix. #2532743
  312. bb.detach(MOUSEWHEEL + '|*');
  313.  
  314. // Only enable for vertical scrollviews
  315. if (mousewheel) {
  316. // Bound to document, because that's where mousewheel events fire off of.
  317. Y.one(DOCUMENT).on(MOUSEWHEEL, Y.bind(sv._mousewheel, sv));
  318. }
  319. },
  320.  
  321. /**
  322. * syncUI implementation.
  323. *
  324. * Update the scroll position, based on the current value of scrollX/scrollY.
  325. *
  326. * @method syncUI
  327. */
  328. syncUI: function () {
  329. var sv = this,
  330. scrollDims = sv._getScrollDims(),
  331. width = scrollDims.offsetWidth,
  332. height = scrollDims.offsetHeight,
  333. scrollWidth = scrollDims.scrollWidth,
  334. scrollHeight = scrollDims.scrollHeight;
  335.  
  336. // If the axis is undefined, auto-calculate it
  337. if (sv._cAxis === undefined) {
  338. // This should only ever be run once (for now).
  339. // In the future SV might post-load axis changes
  340. sv._cAxis = {
  341. x: (scrollWidth > width),
  342. y: (scrollHeight > height)
  343. };
  344.  
  345. sv._set(AXIS, sv._cAxis);
  346. }
  347.  
  348. // get text direction on or inherited by scrollview node
  349. sv.rtl = (sv._cb.getComputedStyle('direction') === 'rtl');
  350.  
  351. // Cache the disabled value
  352. sv._cDisabled = sv.get(DISABLED);
  353.  
  354. // Run this to set initial values
  355. sv._uiDimensionsChange();
  356.  
  357. // If we're out-of-bounds, snap back.
  358. if (sv._isOutOfBounds()) {
  359. sv._snapBack();
  360. }
  361. },
  362.  
  363. /**
  364. * Utility method to obtain widget dimensions
  365. *
  366. * @method _getScrollDims
  367. * @return {Object} The offsetWidth, offsetHeight, scrollWidth and
  368. * scrollHeight as an array: [offsetWidth, offsetHeight, scrollWidth,
  369. * scrollHeight]
  370. * @private
  371. */
  372. _getScrollDims: function () {
  373. var sv = this,
  374. cb = sv._cb,
  375. bb = sv._bb,
  376. TRANS = ScrollView._TRANSITION,
  377. // Ideally using CSSMatrix - don't think we have it normalized yet though.
  378. // origX = (new WebKitCSSMatrix(cb.getComputedStyle("transform"))).e,
  379. // origY = (new WebKitCSSMatrix(cb.getComputedStyle("transform"))).f,
  380. origX = sv.get(SCROLL_X),
  381. origY = sv.get(SCROLL_Y),
  382. origHWTransform,
  383. dims;
  384.  
  385. // TODO: Is this OK? Just in case it's called 'during' a transition.
  386. if (NATIVE_TRANSITIONS) {
  387. cb.setStyle(TRANS.DURATION, ZERO);
  388. cb.setStyle(TRANS.PROPERTY, EMPTY);
  389. }
  390.  
  391. origHWTransform = sv._forceHWTransforms;
  392. sv._forceHWTransforms = false; // the z translation was causing issues with picking up accurate scrollWidths in Chrome/Mac.
  393.  
  394. sv._moveTo(cb, 0, 0);
  395. dims = {
  396. 'offsetWidth': bb.get('offsetWidth'),
  397. 'offsetHeight': bb.get('offsetHeight'),
  398. 'scrollWidth': bb.get('scrollWidth'),
  399. 'scrollHeight': bb.get('scrollHeight')
  400. };
  401. sv._moveTo(cb, -(origX), -(origY));
  402.  
  403. sv._forceHWTransforms = origHWTransform;
  404.  
  405. return dims;
  406. },
  407.  
  408. /**
  409. * This method gets invoked whenever the height or width attributes change,
  410. * allowing us to determine which scrolling axes need to be enabled.
  411. *
  412. * @method _uiDimensionsChange
  413. * @protected
  414. */
  415. _uiDimensionsChange: function () {
  416. var sv = this,
  417. bb = sv._bb,
  418. scrollDims = sv._getScrollDims(),
  419. width = scrollDims.offsetWidth,
  420. height = scrollDims.offsetHeight,
  421. scrollWidth = scrollDims.scrollWidth,
  422. scrollHeight = scrollDims.scrollHeight,
  423. rtl = sv.rtl,
  424. svAxis = sv._cAxis,
  425. minScrollX = (rtl ? Math.min(0, -(scrollWidth - width)) : 0),
  426. maxScrollX = (rtl ? 0 : Math.max(0, scrollWidth - width)),
  427. minScrollY = 0,
  428. maxScrollY = Math.max(0, scrollHeight - height);
  429.  
  430. if (svAxis && svAxis.x) {
  431. bb.addClass(CLASS_NAMES.horizontal);
  432. }
  433.  
  434. if (svAxis && svAxis.y) {
  435. bb.addClass(CLASS_NAMES.vertical);
  436. }
  437.  
  438. sv._setBounds({
  439. minScrollX: minScrollX,
  440. maxScrollX: maxScrollX,
  441. minScrollY: minScrollY,
  442. maxScrollY: maxScrollY
  443. });
  444. },
  445.  
  446. /**
  447. * Set the bounding dimensions of the ScrollView
  448. *
  449. * @method _setBounds
  450. * @protected
  451. * @param bounds {Object} [duration] ms of the scroll animation. (default is 0)
  452. * @param {Number} [bounds.minScrollX] The minimum scroll X value
  453. * @param {Number} [bounds.maxScrollX] The maximum scroll X value
  454. * @param {Number} [bounds.minScrollY] The minimum scroll Y value
  455. * @param {Number} [bounds.maxScrollY] The maximum scroll Y value
  456. */
  457. _setBounds: function (bounds) {
  458. var sv = this;
  459.  
  460. // TODO: Do a check to log if the bounds are invalid
  461.  
  462. sv._minScrollX = bounds.minScrollX;
  463. sv._maxScrollX = bounds.maxScrollX;
  464. sv._minScrollY = bounds.minScrollY;
  465. sv._maxScrollY = bounds.maxScrollY;
  466. },
  467.  
  468. /**
  469. * Get the bounding dimensions of the ScrollView
  470. *
  471. * @method _getBounds
  472. * @protected
  473. */
  474. _getBounds: function () {
  475. var sv = this;
  476.  
  477. return {
  478. minScrollX: sv._minScrollX,
  479. maxScrollX: sv._maxScrollX,
  480. minScrollY: sv._minScrollY,
  481. maxScrollY: sv._maxScrollY
  482. };
  483.  
  484. },
  485.  
  486. /**
  487. * Scroll the element to a given xy coordinate
  488. *
  489. * @method scrollTo
  490. * @param x {Number} The x-position to scroll to. (null for no movement)
  491. * @param y {Number} The y-position to scroll to. (null for no movement)
  492. * @param {Number} [duration] ms of the scroll animation. (default is 0)
  493. * @param {String} [easing] An easing equation if duration is set. (default is `easing` attribute)
  494. * @param {String} [node] The node to transform. Setting this can be useful in
  495. * dual-axis paginated instances. (default is the instance's contentBox)
  496. */
  497. scrollTo: function (x, y, duration, easing, node) {
  498. // Check to see if widget is disabled
  499. if (this._cDisabled) {
  500. return;
  501. }
  502.  
  503. var sv = this,
  504. cb = sv._cb,
  505. TRANS = ScrollView._TRANSITION,
  506. callback = Y.bind(sv._onTransEnd, sv), // @Todo : cache this
  507. newX = 0,
  508. newY = 0,
  509. transition = {},
  510. transform;
  511.  
  512. // default the optional arguments
  513. duration = duration || 0;
  514. easing = easing || sv.get(EASING); // @TODO: Cache this
  515. node = node || cb;
  516.  
  517. if (x !== null) {
  518. sv.set(SCROLL_X, x, {src:UI});
  519. newX = -(x);
  520. }
  521.  
  522. if (y !== null) {
  523. sv.set(SCROLL_Y, y, {src:UI});
  524. newY = -(y);
  525. }
  526.  
  527. transform = sv._transform(newX, newY);
  528.  
  529. if (NATIVE_TRANSITIONS) {
  530. // ANDROID WORKAROUND - try and stop existing transition, before kicking off new one.
  531. node.setStyle(TRANS.DURATION, ZERO).setStyle(TRANS.PROPERTY, EMPTY);
  532. }
  533.  
  534. // Move
  535. if (duration === 0) {
  536. if (NATIVE_TRANSITIONS) {
  537. node.setStyle('transform', transform);
  538. }
  539. else {
  540. // TODO: If both set, batch them in the same update
  541. // Update: Nope, setStyles() just loops through each property and applies it.
  542. if (x !== null) {
  543. node.setStyle(LEFT, newX + PX);
  544. }
  545. if (y !== null) {
  546. node.setStyle(TOP, newY + PX);
  547. }
  548. }
  549. }
  550.  
  551. // Animate
  552. else {
  553. transition.easing = easing;
  554. transition.duration = duration / 1000;
  555.  
  556. if (NATIVE_TRANSITIONS) {
  557. transition.transform = transform;
  558. }
  559. else {
  560. transition.left = newX + PX;
  561. transition.top = newY + PX;
  562. }
  563.  
  564. node.transition(transition, callback);
  565. }
  566. },
  567.  
  568. /**
  569. * Utility method, to create the translate transform string with the
  570. * x, y translation amounts provided.
  571. *
  572. * @method _transform
  573. * @param {Number} x Number of pixels to translate along the x axis
  574. * @param {Number} y Number of pixels to translate along the y axis
  575. * @private
  576. */
  577. _transform: function (x, y) {
  578. // TODO: Would we be better off using a Matrix for this?
  579. var prop = 'translate(' + x + 'px, ' + y + 'px)';
  580.  
  581. if (this._forceHWTransforms) {
  582. prop += ' translateZ(0)';
  583. }
  584.  
  585. return prop;
  586. },
  587.  
  588. /**
  589. * Utility method, to move the given element to the given xy position
  590. *
  591. * @method _moveTo
  592. * @param node {Node} The node to move
  593. * @param x {Number} The x-position to move to
  594. * @param y {Number} The y-position to move to
  595. * @private
  596. */
  597. _moveTo : function(node, x, y) {
  598. if (NATIVE_TRANSITIONS) {
  599. node.setStyle('transform', this._transform(x, y));
  600. } else {
  601. node.setStyle(LEFT, x + PX);
  602. node.setStyle(TOP, y + PX);
  603. }
  604. },
  605.  
  606.  
  607. /**
  608. * Content box transition callback
  609. *
  610. * @method _onTransEnd
  611. * @param {EventFacade} e The event facade
  612. * @private
  613. */
  614. _onTransEnd: function () {
  615. var sv = this;
  616.  
  617. // If for some reason we're OOB, snapback
  618. if (sv._isOutOfBounds()) {
  619. sv._snapBack();
  620. }
  621. else {
  622. /**
  623. * Notification event fired at the end of a scroll transition
  624. *
  625. * @event scrollEnd
  626. * @param e {EventFacade} The default event facade.
  627. */
  628. sv.fire(EV_SCROLL_END);
  629. }
  630. },
  631.  
  632. /**
  633. * gesturemovestart event handler
  634. *
  635. * @method _onGestureMoveStart
  636. * @param e {EventFacade} The gesturemovestart event facade
  637. * @private
  638. */
  639. _onGestureMoveStart: function (e) {
  640.  
  641. if (this._cDisabled) {
  642. return false;
  643. }
  644.  
  645. var sv = this,
  646. bb = sv._bb,
  647. currentX = sv.get(SCROLL_X),
  648. currentY = sv.get(SCROLL_Y),
  649. clientX = e.clientX,
  650. clientY = e.clientY;
  651.  
  652. if (sv._prevent.start) {
  653. e.preventDefault();
  654. }
  655.  
  656. // if a flick animation is in progress, cancel it
  657. if (sv._flickAnim) {
  658. sv._cancelFlick();
  659. sv._onTransEnd();
  660. }
  661.  
  662. // Reset lastScrolledAmt
  663. sv.lastScrolledAmt = 0;
  664.  
  665. // Stores data for this gesture cycle. Cleaned up later
  666. sv._gesture = {
  667.  
  668. // Will hold the axis value
  669. axis: null,
  670.  
  671. // The current attribute values
  672. startX: currentX,
  673. startY: currentY,
  674.  
  675. // The X/Y coordinates where the event began
  676. startClientX: clientX,
  677. startClientY: clientY,
  678.  
  679. // The X/Y coordinates where the event will end
  680. endClientX: null,
  681. endClientY: null,
  682.  
  683. // The current delta of the event
  684. deltaX: null,
  685. deltaY: null,
  686.  
  687. // Will be populated for flicks
  688. flick: null,
  689.  
  690. // Create some listeners for the rest of the gesture cycle
  691. onGestureMove: bb.on(DRAG + '|' + GESTURE_MOVE, Y.bind(sv._onGestureMove, sv)),
  692.  
  693. // @TODO: Don't bind gestureMoveEnd if it's a Flick?
  694. onGestureMoveEnd: bb.on(DRAG + '|' + GESTURE_MOVE + END, Y.bind(sv._onGestureMoveEnd, sv))
  695. };
  696. },
  697.  
  698. /**
  699. * gesturemove event handler
  700. *
  701. * @method _onGestureMove
  702. * @param e {EventFacade} The gesturemove event facade
  703. * @private
  704. */
  705. _onGestureMove: function (e) {
  706. var sv = this,
  707. gesture = sv._gesture,
  708. svAxis = sv._cAxis,
  709. svAxisX = svAxis.x,
  710. svAxisY = svAxis.y,
  711. startX = gesture.startX,
  712. startY = gesture.startY,
  713. startClientX = gesture.startClientX,
  714. startClientY = gesture.startClientY,
  715. clientX = e.clientX,
  716. clientY = e.clientY;
  717.  
  718. if (sv._prevent.move) {
  719. e.preventDefault();
  720. }
  721.  
  722. gesture.deltaX = startClientX - clientX;
  723. gesture.deltaY = startClientY - clientY;
  724.  
  725. // Determine if this is a vertical or horizontal movement
  726. // @TODO: This is crude, but it works. Investigate more intelligent ways to detect intent
  727. if (gesture.axis === null) {
  728. gesture.axis = (Math.abs(gesture.deltaX) > Math.abs(gesture.deltaY)) ? DIM_X : DIM_Y;
  729. }
  730.  
  731. // Move X or Y. @TODO: Move both if dualaxis.
  732. if (gesture.axis === DIM_X && svAxisX) {
  733. sv.set(SCROLL_X, startX + gesture.deltaX);
  734. }
  735. else if (gesture.axis === DIM_Y && svAxisY) {
  736. sv.set(SCROLL_Y, startY + gesture.deltaY);
  737. }
  738. },
  739.  
  740. /**
  741. * gesturemoveend event handler
  742. *
  743. * @method _onGestureMoveEnd
  744. * @param e {EventFacade} The gesturemoveend event facade
  745. * @private
  746. */
  747. _onGestureMoveEnd: function (e) {
  748. var sv = this,
  749. gesture = sv._gesture,
  750. flick = gesture.flick,
  751. clientX = e.clientX,
  752. clientY = e.clientY,
  753. isOOB;
  754.  
  755. if (sv._prevent.end) {
  756. e.preventDefault();
  757. }
  758.  
  759. // Store the end X/Y coordinates
  760. gesture.endClientX = clientX;
  761. gesture.endClientY = clientY;
  762.  
  763. // Cleanup the event handlers
  764. gesture.onGestureMove.detach();
  765. gesture.onGestureMoveEnd.detach();
  766.  
  767. // If this wasn't a flick, wrap up the gesture cycle
  768. if (!flick) {
  769. // @TODO: Be more intelligent about this. Look at the Flick attribute to see
  770. // if it is safe to assume _flick did or didn't fire.
  771. // Then, the order _flick and _onGestureMoveEnd fire doesn't matter?
  772.  
  773. // If there was movement (_onGestureMove fired)
  774. if (gesture.deltaX !== null && gesture.deltaY !== null) {
  775.  
  776. isOOB = sv._isOutOfBounds();
  777.  
  778. // If we're out-out-bounds, then snapback
  779. if (isOOB) {
  780. sv._snapBack();
  781. }
  782.  
  783. // Inbounds
  784. else {
  785. // Fire scrollEnd unless this is a paginated instance and the gesture axis is the same as paginator's
  786. // Not totally confident this is ideal to access a plugin's properties from a host, @TODO revisit
  787. if (!sv.pages || (sv.pages && !sv.pages.get(AXIS)[gesture.axis])) {
  788. sv._onTransEnd();
  789. }
  790. }
  791. }
  792. }
  793. },
  794.  
  795. /**
  796. * Execute a flick at the end of a scroll action
  797. *
  798. * @method _flick
  799. * @param e {EventFacade} The Flick event facade
  800. * @private
  801. */
  802. _flick: function (e) {
  803. if (this._cDisabled) {
  804. return false;
  805. }
  806.  
  807. var sv = this,
  808. svAxis = sv._cAxis,
  809. flick = e.flick,
  810. flickAxis = flick.axis,
  811. flickVelocity = flick.velocity,
  812. axisAttr = flickAxis === DIM_X ? SCROLL_X : SCROLL_Y,
  813. startPosition = sv.get(axisAttr);
  814.  
  815. // Sometimes flick is enabled, but drag is disabled
  816. if (sv._gesture) {
  817. sv._gesture.flick = flick;
  818. }
  819.  
  820. // Prevent unneccesary firing of _flickFrame if we can't scroll on the flick axis
  821. if (svAxis[flickAxis]) {
  822. sv._flickFrame(flickVelocity, flickAxis, startPosition);
  823. }
  824. },
  825.  
  826. /**
  827. * Execute a single frame in the flick animation
  828. *
  829. * @method _flickFrame
  830. * @param velocity {Number} The velocity of this animated frame
  831. * @param flickAxis {String} The axis on which to animate
  832. * @param startPosition {Number} The starting X/Y point to flick from
  833. * @protected
  834. */
  835. _flickFrame: function (velocity, flickAxis, startPosition) {
  836.  
  837. var sv = this,
  838. axisAttr = flickAxis === DIM_X ? SCROLL_X : SCROLL_Y,
  839. bounds = sv._getBounds(),
  840.  
  841. // Localize cached values
  842. bounce = sv._cBounce,
  843. bounceRange = sv._cBounceRange,
  844. deceleration = sv._cDeceleration,
  845. frameDuration = sv._cFrameDuration,
  846.  
  847. // Calculate
  848. newVelocity = velocity * deceleration,
  849. newPosition = startPosition - (frameDuration * newVelocity),
  850.  
  851. // Some convinience conditions
  852. min = flickAxis === DIM_X ? bounds.minScrollX : bounds.minScrollY,
  853. max = flickAxis === DIM_X ? bounds.maxScrollX : bounds.maxScrollY,
  854. belowMin = (newPosition < min),
  855. belowMax = (newPosition < max),
  856. aboveMin = (newPosition > min),
  857. aboveMax = (newPosition > max),
  858. belowMinRange = (newPosition < (min - bounceRange)),
  859. withinMinRange = (belowMin && (newPosition > (min - bounceRange))),
  860. withinMaxRange = (aboveMax && (newPosition < (max + bounceRange))),
  861. aboveMaxRange = (newPosition > (max + bounceRange)),
  862. tooSlow;
  863.  
  864. // If we're within the range but outside min/max, dampen the velocity
  865. if (withinMinRange || withinMaxRange) {
  866. newVelocity *= bounce;
  867. }
  868.  
  869. // Is the velocity too slow to bother?
  870. tooSlow = (Math.abs(newVelocity).toFixed(4) < 0.015);
  871.  
  872. // If the velocity is too slow or we're outside the range
  873. if (tooSlow || belowMinRange || aboveMaxRange) {
  874. // Cancel and delete sv._flickAnim
  875. if (sv._flickAnim) {
  876. sv._cancelFlick();
  877. }
  878.  
  879. // If we're inside the scroll area, just end
  880. if (aboveMin && belowMax) {
  881. sv._onTransEnd();
  882. }
  883.  
  884. // We're outside the scroll area, so we need to snap back
  885. else {
  886. sv._snapBack();
  887. }
  888. }
  889.  
  890. // Otherwise, animate to the next frame
  891. else {
  892. // @TODO: maybe use requestAnimationFrame instead
  893. sv._flickAnim = Y.later(frameDuration, sv, '_flickFrame', [newVelocity, flickAxis, newPosition]);
  894. sv.set(axisAttr, newPosition);
  895. }
  896. },
  897.  
  898. _cancelFlick: function () {
  899. var sv = this;
  900.  
  901. if (sv._flickAnim) {
  902. // Cancel the flick (if it exists)
  903. sv._flickAnim.cancel();
  904.  
  905. // Also delete it, otherwise _onGestureMoveStart will think we're still flicking
  906. delete sv._flickAnim;
  907. }
  908.  
  909. },
  910.  
  911. /**
  912. * Handle mousewheel events on the widget
  913. *
  914. * @method _mousewheel
  915. * @param e {EventFacade} The mousewheel event facade
  916. * @private
  917. */
  918. _mousewheel: function (e) {
  919. var sv = this,
  920. scrollY = sv.get(SCROLL_Y),
  921. bounds = sv._getBounds(),
  922. bb = sv._bb,
  923. scrollOffset = 10, // 10px
  924. isForward = (e.wheelDelta > 0),
  925. scrollToY = scrollY - ((isForward ? 1 : -1) * scrollOffset);
  926.  
  927. scrollToY = _constrain(scrollToY, bounds.minScrollY, bounds.maxScrollY);
  928.  
  929. // Because Mousewheel events fire off 'document', every ScrollView widget will react
  930. // to any mousewheel anywhere on the page. This check will ensure that the mouse is currently
  931. // over this specific ScrollView. Also, only allow mousewheel scrolling on Y-axis,
  932. // becuase otherwise the 'prevent' will block page scrolling.
  933. if (bb.contains(e.target) && sv._cAxis[DIM_Y]) {
  934.  
  935. // Reset lastScrolledAmt
  936. sv.lastScrolledAmt = 0;
  937.  
  938. // Jump to the new offset
  939. sv.set(SCROLL_Y, scrollToY);
  940.  
  941. // if we have scrollbars plugin, update & set the flash timer on the scrollbar
  942. // @TODO: This probably shouldn't be in this module
  943. if (sv.scrollbars) {
  944. // @TODO: The scrollbars should handle this themselves
  945. sv.scrollbars._update();
  946. sv.scrollbars.flash();
  947. // or just this
  948. // sv.scrollbars._hostDimensionsChange();
  949. }
  950.  
  951. // Fire the 'scrollEnd' event
  952. sv._onTransEnd();
  953.  
  954. // prevent browser default behavior on mouse scroll
  955. e.preventDefault();
  956. }
  957. },
  958.  
  959. /**
  960. * Checks to see the current scrollX/scrollY position beyond the min/max boundary
  961. *
  962. * @method _isOutOfBounds
  963. * @param x {Number} [optional] The X position to check
  964. * @param y {Number} [optional] The Y position to check
  965. * @return {Boolean} Whether the current X/Y position is out of bounds (true) or not (false)
  966. * @private
  967. */
  968. _isOutOfBounds: function (x, y) {
  969. var sv = this,
  970. svAxis = sv._cAxis,
  971. svAxisX = svAxis.x,
  972. svAxisY = svAxis.y,
  973. currentX = x || sv.get(SCROLL_X),
  974. currentY = y || sv.get(SCROLL_Y),
  975. bounds = sv._getBounds(),
  976. minX = bounds.minScrollX,
  977. minY = bounds.minScrollY,
  978. maxX = bounds.maxScrollX,
  979. maxY = bounds.maxScrollY;
  980.  
  981. return (svAxisX && (currentX < minX || currentX > maxX)) || (svAxisY && (currentY < minY || currentY > maxY));
  982. },
  983.  
  984. /**
  985. * Bounces back
  986. * @TODO: Should be more generalized and support both X and Y detection
  987. *
  988. * @method _snapBack
  989. * @private
  990. */
  991. _snapBack: function () {
  992. var sv = this,
  993. currentX = sv.get(SCROLL_X),
  994. currentY = sv.get(SCROLL_Y),
  995. bounds = sv._getBounds(),
  996. minX = bounds.minScrollX,
  997. minY = bounds.minScrollY,
  998. maxX = bounds.maxScrollX,
  999. maxY = bounds.maxScrollY,
  1000. newY = _constrain(currentY, minY, maxY),
  1001. newX = _constrain(currentX, minX, maxX),
  1002. duration = sv.get(SNAP_DURATION),
  1003. easing = sv.get(SNAP_EASING);
  1004.  
  1005. if (newX !== currentX) {
  1006. sv.set(SCROLL_X, newX, {duration:duration, easing:easing});
  1007. }
  1008. else if (newY !== currentY) {
  1009. sv.set(SCROLL_Y, newY, {duration:duration, easing:easing});
  1010. }
  1011. else {
  1012. sv._onTransEnd();
  1013. }
  1014. },
  1015.  
  1016. /**
  1017. * After listener for changes to the scrollX or scrollY attribute
  1018. *
  1019. * @method _afterScrollChange
  1020. * @param e {EventFacade} The event facade
  1021. * @protected
  1022. */
  1023. _afterScrollChange: function (e) {
  1024. if (e.src === ScrollView.UI_SRC) {
  1025. return false;
  1026. }
  1027.  
  1028. var sv = this,
  1029. duration = e.duration,
  1030. easing = e.easing,
  1031. val = e.newVal,
  1032. scrollToArgs = [];
  1033.  
  1034. // Set the scrolled value
  1035. sv.lastScrolledAmt = sv.lastScrolledAmt + (e.newVal - e.prevVal);
  1036.  
  1037. // Generate the array of args to pass to scrollTo()
  1038. if (e.attrName === SCROLL_X) {
  1039. scrollToArgs.push(val);
  1040. scrollToArgs.push(sv.get(SCROLL_Y));
  1041. }
  1042. else {
  1043. scrollToArgs.push(sv.get(SCROLL_X));
  1044. scrollToArgs.push(val);
  1045. }
  1046.  
  1047. scrollToArgs.push(duration);
  1048. scrollToArgs.push(easing);
  1049.  
  1050. sv.scrollTo.apply(sv, scrollToArgs);
  1051. },
  1052.  
  1053. /**
  1054. * After listener for changes to the flick attribute
  1055. *
  1056. * @method _afterFlickChange
  1057. * @param e {EventFacade} The event facade
  1058. * @protected
  1059. */
  1060. _afterFlickChange: function (e) {
  1061. this._bindFlick(e.newVal);
  1062. },
  1063.  
  1064. /**
  1065. * After listener for changes to the disabled attribute
  1066. *
  1067. * @method _afterDisabledChange
  1068. * @param e {EventFacade} The event facade
  1069. * @protected
  1070. */
  1071. _afterDisabledChange: function (e) {
  1072. // Cache for performance - we check during move
  1073. this._cDisabled = e.newVal;
  1074. },
  1075.  
  1076. /**
  1077. * After listener for the axis attribute
  1078. *
  1079. * @method _afterAxisChange
  1080. * @param e {EventFacade} The event facade
  1081. * @protected
  1082. */
  1083. _afterAxisChange: function (e) {
  1084. this._cAxis = e.newVal;
  1085. },
  1086.  
  1087. /**
  1088. * After listener for changes to the drag attribute
  1089. *
  1090. * @method _afterDragChange
  1091. * @param e {EventFacade} The event facade
  1092. * @protected
  1093. */
  1094. _afterDragChange: function (e) {
  1095. this._bindDrag(e.newVal);
  1096. },
  1097.  
  1098. /**
  1099. * After listener for the height or width attribute
  1100. *
  1101. * @method _afterDimChange
  1102. * @param e {EventFacade} The event facade
  1103. * @protected
  1104. */
  1105. _afterDimChange: function () {
  1106. this._uiDimensionsChange();
  1107. },
  1108.  
  1109. /**
  1110. * After listener for scrollEnd, for cleanup
  1111. *
  1112. * @method _afterScrollEnd
  1113. * @param e {EventFacade} The event facade
  1114. * @protected
  1115. */
  1116. _afterScrollEnd: function () {
  1117. var sv = this;
  1118.  
  1119. if (sv._flickAnim) {
  1120. sv._cancelFlick();
  1121. }
  1122.  
  1123. // Ideally this should be removed, but doing so causing some JS errors with fast swiping
  1124. // because _gesture is being deleted after the previous one has been overwritten
  1125. // delete sv._gesture; // TODO: Move to sv.prevGesture?
  1126. },
  1127.  
  1128. /**
  1129. * Setter for 'axis' attribute
  1130. *
  1131. * @method _axisSetter
  1132. * @param val {Mixed} A string ('x', 'y', 'xy') to specify which axis/axes to allow scrolling on
  1133. * @param name {String} The attribute name
  1134. * @return {Object} An object to specify scrollability on the x & y axes
  1135. *
  1136. * @protected
  1137. */
  1138. _axisSetter: function (val) {
  1139.  
  1140. // Turn a string into an axis object
  1141. if (Y.Lang.isString(val)) {
  1142. return {
  1143. x: val.match(/x/i) ? true : false,
  1144. y: val.match(/y/i) ? true : false
  1145. };
  1146. }
  1147. },
  1148.  
  1149. /**
  1150. * The scrollX, scrollY setter implementation
  1151. *
  1152. * @method _setScroll
  1153. * @private
  1154. * @param {Number} val
  1155. * @param {String} dim
  1156. *
  1157. * @return {Number} The value
  1158. */
  1159. _setScroll : function(val) {
  1160.  
  1161. // Just ensure the widget is not disabled
  1162. if (this._cDisabled) {
  1163. val = Y.Attribute.INVALID_VALUE;
  1164. }
  1165.  
  1166. return val;
  1167. },
  1168.  
  1169. /**
  1170. * Setter for the scrollX attribute
  1171. *
  1172. * @method _setScrollX
  1173. * @param val {Number} The new scrollX value
  1174. * @return {Number} The normalized value
  1175. * @protected
  1176. */
  1177. _setScrollX: function(val) {
  1178. return this._setScroll(val, DIM_X);
  1179. },
  1180.  
  1181. /**
  1182. * Setter for the scrollY ATTR
  1183. *
  1184. * @method _setScrollY
  1185. * @param val {Number} The new scrollY value
  1186. * @return {Number} The normalized value
  1187. * @protected
  1188. */
  1189. _setScrollY: function(val) {
  1190. return this._setScroll(val, DIM_Y);
  1191. }
  1192.  
  1193. // End prototype properties
  1194.  
  1195. }, {
  1196.  
  1197. // Static properties
  1198.  
  1199. /**
  1200. * The identity of the widget.
  1201. *
  1202. * @property NAME
  1203. * @type String
  1204. * @default 'scrollview'
  1205. * @readOnly
  1206. * @protected
  1207. * @static
  1208. */
  1209. NAME: 'scrollview',
  1210.  
  1211. /**
  1212. * Static property used to define the default attribute configuration of
  1213. * the Widget.
  1214. *
  1215. * @property ATTRS
  1216. * @type {Object}
  1217. * @protected
  1218. * @static
  1219. */
  1220. ATTRS: {
  1221.  
  1222. /**
  1223. * Specifies ability to scroll on x, y, or x and y axis/axes.
  1224. *
  1225. * @attribute axis
  1226. * @type String
  1227. */
  1228. axis: {
  1229. setter: '_axisSetter',
  1230. writeOnce: 'initOnly'
  1231. },
  1232.  
  1233. /**
  1234. * The current scroll position in the x-axis
  1235. *
  1236. * @attribute scrollX
  1237. * @type Number
  1238. * @default 0
  1239. */
  1240. scrollX: {
  1241. value: 0,
  1242. setter: '_setScrollX'
  1243. },
  1244.  
  1245. /**
  1246. * The current scroll position in the y-axis
  1247. *
  1248. * @attribute scrollY
  1249. * @type Number
  1250. * @default 0
  1251. */
  1252. scrollY: {
  1253. value: 0,
  1254. setter: '_setScrollY'
  1255. },
  1256.  
  1257. /**
  1258. * Drag coefficent for inertial scrolling. The closer to 1 this
  1259. * value is, the less friction during scrolling.
  1260. *
  1261. * @attribute deceleration
  1262. * @default 0.93
  1263. */
  1264. deceleration: {
  1265. value: 0.93
  1266. },
  1267.  
  1268. /**
  1269. * Drag coefficient for intertial scrolling at the upper
  1270. * and lower boundaries of the scrollview. Set to 0 to
  1271. * disable "rubber-banding".
  1272. *
  1273. * @attribute bounce
  1274. * @type Number
  1275. * @default 0.1
  1276. */
  1277. bounce: {
  1278. value: 0.1
  1279. },
  1280.  
  1281. /**
  1282. * The minimum distance and/or velocity which define a flick. Can be set to false,
  1283. * to disable flick support (note: drag support is enabled/disabled separately)
  1284. *
  1285. * @attribute flick
  1286. * @type Object
  1287. * @default Object with properties minDistance = 10, minVelocity = 0.3.
  1288. */
  1289. flick: {
  1290. value: {
  1291. minDistance: 10,
  1292. minVelocity: 0.3
  1293. }
  1294. },
  1295.  
  1296. /**
  1297. * Enable/Disable dragging the ScrollView content (note: flick support is enabled/disabled separately)
  1298. * @attribute drag
  1299. * @type boolean
  1300. * @default true
  1301. */
  1302. drag: {
  1303. value: true
  1304. },
  1305.  
  1306. /**
  1307. * The default duration to use when animating the bounce snap back.
  1308. *
  1309. * @attribute snapDuration
  1310. * @type Number
  1311. * @default 400
  1312. */
  1313. snapDuration: {
  1314. value: 400
  1315. },
  1316.  
  1317. /**
  1318. * The default easing to use when animating the bounce snap back.
  1319. *
  1320. * @attribute snapEasing
  1321. * @type String
  1322. * @default 'ease-out'
  1323. */
  1324. snapEasing: {
  1325. value: 'ease-out'
  1326. },
  1327.  
  1328. /**
  1329. * The default easing used when animating the flick
  1330. *
  1331. * @attribute easing
  1332. * @type String
  1333. * @default 'cubic-bezier(0, 0.1, 0, 1.0)'
  1334. */
  1335. easing: {
  1336. value: 'cubic-bezier(0, 0.1, 0, 1.0)'
  1337. },
  1338.  
  1339. /**
  1340. * The interval (ms) used when animating the flick for JS-timer animations
  1341. *
  1342. * @attribute frameDuration
  1343. * @type Number
  1344. * @default 15
  1345. */
  1346. frameDuration: {
  1347. value: 15
  1348. },
  1349.  
  1350. /**
  1351. * The default bounce distance in pixels
  1352. *
  1353. * @attribute bounceRange
  1354. * @type Number
  1355. * @default 150
  1356. */
  1357. bounceRange: {
  1358. value: 150
  1359. }
  1360. },
  1361.  
  1362. /**
  1363. * List of class names used in the scrollview's DOM
  1364. *
  1365. * @property CLASS_NAMES
  1366. * @type Object
  1367. * @static
  1368. */
  1369. CLASS_NAMES: CLASS_NAMES,
  1370.  
  1371. /**
  1372. * Flag used to source property changes initiated from the DOM
  1373. *
  1374. * @property UI_SRC
  1375. * @type String
  1376. * @static
  1377. * @default 'ui'
  1378. */
  1379. UI_SRC: UI,
  1380.  
  1381. /**
  1382. * Object map of style property names used to set transition properties.
  1383. * Defaults to the vendor prefix established by the Transition module.
  1384. * The configured property names are `_TRANSITION.DURATION` (e.g. "WebkitTransitionDuration") and
  1385. * `_TRANSITION.PROPERTY (e.g. "WebkitTransitionProperty").
  1386. *
  1387. * @property _TRANSITION
  1388. * @private
  1389. */
  1390. _TRANSITION: {
  1391. DURATION: (vendorPrefix ? vendorPrefix + 'TransitionDuration' : 'transitionDuration'),
  1392. PROPERTY: (vendorPrefix ? vendorPrefix + 'TransitionProperty' : 'transitionProperty')
  1393. },
  1394.  
  1395. /**
  1396. * The default bounce distance in pixels
  1397. *
  1398. * @property BOUNCE_RANGE
  1399. * @type Number
  1400. * @static
  1401. * @default false
  1402. * @deprecated (in 3.7.0)
  1403. */
  1404. BOUNCE_RANGE: false,
  1405.  
  1406. /**
  1407. * The interval (ms) used when animating the flick
  1408. *
  1409. * @property FRAME_STEP
  1410. * @type Number
  1411. * @static
  1412. * @default false
  1413. * @deprecated (in 3.7.0)
  1414. */
  1415. FRAME_STEP: false,
  1416.  
  1417. /**
  1418. * The default easing used when animating the flick
  1419. *
  1420. * @property EASING
  1421. * @type String
  1422. * @static
  1423. * @default false
  1424. * @deprecated (in 3.7.0)
  1425. */
  1426. EASING: false,
  1427.  
  1428. /**
  1429. * The default easing to use when animating the bounce snap back.
  1430. *
  1431. * @property SNAP_EASING
  1432. * @type String
  1433. * @static
  1434. * @default false
  1435. * @deprecated (in 3.7.0)
  1436. */
  1437. SNAP_EASING: false,
  1438.  
  1439. /**
  1440. * The default duration to use when animating the bounce snap back.
  1441. *
  1442. * @property SNAP_DURATION
  1443. * @type Number
  1444. * @static
  1445. * @default false
  1446. * @deprecated (in 3.7.0)
  1447. */
  1448. SNAP_DURATION: false
  1449.  
  1450. // End static properties
  1451.  
  1452. });
  1453.