core.js 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415
  1. (function (window) {
  2. var NOOP = function () {};
  3. var core = (function Animate(global) {
  4. var time =
  5. Date.now ||
  6. function () {
  7. return +new Date();
  8. };
  9. var desiredFrames = 60;
  10. var millisecondsPerSecond = 1000;
  11. var running = {};
  12. var counter = 1;
  13. var core = { effect: {} };
  14. core.effect.Animate = {
  15. /**
  16. * A requestAnimationFrame wrapper / polyfill.
  17. *
  18. * @param callback {Function} The callback to be invoked before the next repaint.
  19. * @param root {HTMLElement} The root element for the repaint
  20. */
  21. requestAnimationFrame: (function () {
  22. // Check for request animation Frame support
  23. var requestFrame =
  24. global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame;
  25. var isNative = !!requestFrame;
  26. if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
  27. isNative = false;
  28. }
  29. if (isNative) {
  30. return function (callback, root) {
  31. requestFrame(callback, root);
  32. };
  33. }
  34. var TARGET_FPS = 60;
  35. var requests = {};
  36. var requestCount = 0;
  37. var rafHandle = 1;
  38. var intervalHandle = null;
  39. var lastActive = +new Date();
  40. return function (callback, root) {
  41. var callbackHandle = rafHandle++;
  42. // Store callback
  43. requests[callbackHandle] = callback;
  44. requestCount++;
  45. // Create timeout at first request
  46. if (intervalHandle === null) {
  47. intervalHandle = setInterval(function () {
  48. var time = +new Date();
  49. var currentRequests = requests;
  50. // Reset data structure before executing callbacks
  51. requests = {};
  52. requestCount = 0;
  53. for (var key in currentRequests) {
  54. if (currentRequests.hasOwnProperty(key)) {
  55. currentRequests[key](time);
  56. lastActive = time;
  57. }
  58. }
  59. // Disable the timeout when nothing happens for a certain
  60. // period of time
  61. if (time - lastActive > 2500) {
  62. clearInterval(intervalHandle);
  63. intervalHandle = null;
  64. }
  65. }, 1000 / TARGET_FPS);
  66. }
  67. return callbackHandle;
  68. };
  69. })(),
  70. /**
  71. * Stops the given animation.
  72. *
  73. * @param id {Integer} Unique animation ID
  74. * @return {Boolean} Whether the animation was stopped (aka, was running before)
  75. */
  76. stop: function (id) {
  77. var cleared = running[id] != null;
  78. if (cleared) {
  79. running[id] = null;
  80. }
  81. return cleared;
  82. },
  83. /**
  84. * Whether the given animation is still running.
  85. *
  86. * @param id {Integer} Unique animation ID
  87. * @return {Boolean} Whether the animation is still running
  88. */
  89. isRunning: function (id) {
  90. return running[id] != null;
  91. },
  92. /**
  93. * Start the animation.
  94. *
  95. * @param stepCallback {Function} Pointer to function which is executed on every step.
  96. * Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
  97. * @param verifyCallback {Function} Executed before every animation step.
  98. * Signature of the method should be `function() { return continueWithAnimation; }`
  99. * @param completedCallback {Function}
  100. * Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
  101. * @param duration {Integer} Milliseconds to run the animation
  102. * @param easingMethod {Function} Pointer to easing function
  103. * Signature of the method should be `function(percent) { return modifiedValue; }`
  104. * @param root {Element ? document.body} Render root, when available. Used for internal
  105. * usage of requestAnimationFrame.
  106. * @return {Integer} Identifier of animation. Can be used to stop it any time.
  107. */
  108. start: function (stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
  109. var start = time();
  110. var lastFrame = start;
  111. var percent = 0;
  112. var dropCounter = 0;
  113. var id = counter++;
  114. if (!root) {
  115. root = document.body;
  116. }
  117. // Compacting running db automatically every few new animations
  118. if (id % 20 === 0) {
  119. var newRunning = {};
  120. for (var usedId in running) {
  121. newRunning[usedId] = true;
  122. }
  123. running = newRunning;
  124. }
  125. // This is the internal step method which is called every few milliseconds
  126. var step = function (virtual) {
  127. // Normalize virtual value
  128. var render = virtual !== true;
  129. // Get current time
  130. var now = time();
  131. // Verification is executed before next animation step
  132. if (!running[id] || (verifyCallback && !verifyCallback(id))) {
  133. running[id] = null;
  134. completedCallback && completedCallback(desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond), id, false);
  135. return;
  136. }
  137. // For the current rendering to apply let's update omitted steps in memory.
  138. // This is important to bring internal state variables up-to-date with progress in time.
  139. if (render) {
  140. var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1;
  141. for (var j = 0; j < Math.min(droppedFrames, 4); j++) {
  142. step(true);
  143. dropCounter++;
  144. }
  145. }
  146. // Compute percent value
  147. if (duration) {
  148. percent = (now - start) / duration;
  149. if (percent > 1) {
  150. percent = 1;
  151. }
  152. }
  153. // Execute step callback, then...
  154. var value = easingMethod ? easingMethod(percent) : percent;
  155. if ((stepCallback(value, now, render) === false || percent === 1) && render) {
  156. running[id] = null;
  157. completedCallback &&
  158. completedCallback(desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond), id, percent === 1 || duration == null);
  159. } else if (render) {
  160. lastFrame = now;
  161. core.effect.Animate.requestAnimationFrame(step, root);
  162. }
  163. };
  164. // Mark as running
  165. running[id] = true;
  166. // Init first step
  167. core.effect.Animate.requestAnimationFrame(step, root);
  168. // Return unique animation ID
  169. return id;
  170. },
  171. };
  172. return core;
  173. })(window);
  174. /**
  175. * A pure logic 'component' for 'virtual' scrolling/zooming.
  176. */
  177. var Scroller = function (callback, options) {
  178. this.__callback = callback;
  179. // core = animate;
  180. this.options = {
  181. /** Enable scrolling on x-axis */
  182. scrollingX: true,
  183. /** Enable scrolling on y-axis */
  184. scrollingY: true,
  185. /** Enable animations for deceleration, snap back, zooming and scrolling */
  186. animating: true,
  187. /** duration for animations triggered by scrollTo/zoomTo */
  188. animationDuration: 250,
  189. /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */
  190. bouncing: true,
  191. /** Enable locking to the main axis if user moves only slightly on one of them at start */
  192. locking: true,
  193. /** Enable pagination mode (switching between full page content panes) */
  194. paging: false,
  195. /** Enable snapping of content to a configured pixel grid */
  196. snapping: false,
  197. /** Enable zooming of content via API, fingers and mouse wheel */
  198. zooming: false,
  199. /** Minimum zoom level */
  200. minZoom: 0.5,
  201. /** Maximum zoom level */
  202. maxZoom: 3,
  203. /** Multiply or decrease scrolling speed **/
  204. speedMultiplier: 1,
  205. /** Callback that is fired on the later of touch end or deceleration end,
  206. provided that another scrolling action has not begun. Used to know
  207. when to fade out a scrollbar. */
  208. scrollingComplete: NOOP,
  209. /** This configures the amount of change applied to deceleration when reaching boundaries **/
  210. penetrationDeceleration: 0.03,
  211. /** This configures the amount of change applied to acceleration when reaching boundaries **/
  212. penetrationAcceleration: 0.08,
  213. };
  214. for (var key in options) {
  215. this.options[key] = options[key];
  216. }
  217. };
  218. // Easing Equations (c) 2003 Robert Penner, all rights reserved.
  219. // Open source under the BSD License.
  220. /**
  221. * @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
  222. **/
  223. var easeOutCubic = function (pos) {
  224. return Math.pow(pos - 1, 3) + 1;
  225. };
  226. /**
  227. * @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
  228. **/
  229. var easeInOutCubic = function (pos) {
  230. if ((pos /= 0.5) < 1) {
  231. return 0.5 * Math.pow(pos, 3);
  232. }
  233. return 0.5 * (Math.pow(pos - 2, 3) + 2);
  234. };
  235. var members = {
  236. /*
  237. ---------------------------------------------------------------------------
  238. INTERNAL FIELDS :: STATUS
  239. ---------------------------------------------------------------------------
  240. */
  241. /** {Boolean} Whether only a single finger is used in touch handling */
  242. __isSingleTouch: false,
  243. /** {Boolean} Whether a touch event sequence is in progress */
  244. __isTracking: false,
  245. /** {Boolean} Whether a deceleration animation went to completion. */
  246. __didDecelerationComplete: false,
  247. /**
  248. * {Boolean} Whether a gesture zoom/rotate event is in progress. Activates when
  249. * a gesturestart event happens. This has higher priority than dragging.
  250. */
  251. __isGesturing: false,
  252. /**
  253. * {Boolean} Whether the user has moved by such a distance that we have enabled
  254. * dragging mode. Hint: It's only enabled after some pixels of movement to
  255. * not interrupt with clicks etc.
  256. */
  257. __isDragging: false,
  258. /**
  259. * {Boolean} Not touching and dragging anymore, and smoothly animating the
  260. * touch sequence using deceleration.
  261. */
  262. __isDecelerating: false,
  263. /**
  264. * {Boolean} Smoothly animating the currently configured change
  265. */
  266. __isAnimating: false,
  267. /*
  268. ---------------------------------------------------------------------------
  269. INTERNAL FIELDS :: DIMENSIONS
  270. ---------------------------------------------------------------------------
  271. */
  272. /** {Integer} Available outer left position (from document perspective) */
  273. __clientLeft: 0,
  274. /** {Integer} Available outer top position (from document perspective) */
  275. __clientTop: 0,
  276. /** {Integer} Available outer width */
  277. __clientWidth: 0,
  278. /** {Integer} Available outer height */
  279. __clientHeight: 0,
  280. /** {Integer} Outer width of content */
  281. __contentWidth: 0,
  282. /** {Integer} Outer height of content */
  283. __contentHeight: 0,
  284. /** {Integer} Snapping width for content */
  285. __snapWidth: 100,
  286. /** {Integer} Snapping height for content */
  287. __snapHeight: 100,
  288. /** {Integer} Height to assign to refresh area */
  289. __refreshHeight: null,
  290. /** {Boolean} Whether the refresh process is enabled when the event is released now */
  291. __refreshActive: false,
  292. /** {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */
  293. __refreshActivate: null,
  294. /** {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */
  295. __refreshDeactivate: null,
  296. /** {Function} Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */
  297. __refreshStart: null,
  298. /** {Number} Zoom level */
  299. __zoomLevel: 1,
  300. /** {Number} Scroll position on x-axis */
  301. __scrollLeft: 0,
  302. /** {Number} Scroll position on y-axis */
  303. __scrollTop: 0,
  304. /** {Integer} Maximum allowed scroll position on x-axis */
  305. __maxScrollLeft: 0,
  306. /** {Integer} Maximum allowed scroll position on y-axis */
  307. __maxScrollTop: 0,
  308. /* {Number} Scheduled left position (final position when animating) */
  309. __scheduledLeft: 0,
  310. /* {Number} Scheduled top position (final position when animating) */
  311. __scheduledTop: 0,
  312. /* {Number} Scheduled zoom level (final scale when animating) */
  313. __scheduledZoom: 0,
  314. /*
  315. ---------------------------------------------------------------------------
  316. INTERNAL FIELDS :: LAST POSITIONS
  317. ---------------------------------------------------------------------------
  318. */
  319. /** {Number} Left position of finger at start */
  320. __lastTouchLeft: null,
  321. /** {Number} Top position of finger at start */
  322. __lastTouchTop: null,
  323. /** {Date} Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */
  324. __lastTouchMove: null,
  325. /** {Array} List of positions, uses three indexes for each state: left, top, timestamp */
  326. __positions: null,
  327. /*
  328. ---------------------------------------------------------------------------
  329. INTERNAL FIELDS :: DECELERATION SUPPORT
  330. ---------------------------------------------------------------------------
  331. */
  332. /** {Integer} Minimum left scroll position during deceleration */
  333. __minDecelerationScrollLeft: null,
  334. /** {Integer} Minimum top scroll position during deceleration */
  335. __minDecelerationScrollTop: null,
  336. /** {Integer} Maximum left scroll position during deceleration */
  337. __maxDecelerationScrollLeft: null,
  338. /** {Integer} Maximum top scroll position during deceleration */
  339. __maxDecelerationScrollTop: null,
  340. /** {Number} Current factor to modify horizontal scroll position with on every step */
  341. __decelerationVelocityX: null,
  342. /** {Number} Current factor to modify vertical scroll position with on every step */
  343. __decelerationVelocityY: null,
  344. /*
  345. ---------------------------------------------------------------------------
  346. PUBLIC API
  347. ---------------------------------------------------------------------------
  348. */
  349. /**
  350. * Configures the dimensions of the client (outer) and content (inner) elements.
  351. * Requires the available space for the outer element and the outer size of the inner element.
  352. * All values which are falsy (null or zero etc.) are ignored and the old value is kept.
  353. *
  354. * @param clientWidth {Integer ? null} Inner width of outer element
  355. * @param clientHeight {Integer ? null} Inner height of outer element
  356. * @param contentWidth {Integer ? null} Outer width of inner element
  357. * @param contentHeight {Integer ? null} Outer height of inner element
  358. */
  359. setDimensions: function (clientWidth, clientHeight, contentWidth, contentHeight) {
  360. var self = this;
  361. // Only update values which are defined
  362. if (clientWidth === +clientWidth) {
  363. self.__clientWidth = clientWidth;
  364. }
  365. if (clientHeight === +clientHeight) {
  366. self.__clientHeight = clientHeight;
  367. }
  368. if (contentWidth === +contentWidth) {
  369. self.__contentWidth = contentWidth;
  370. }
  371. if (contentHeight === +contentHeight) {
  372. self.__contentHeight = contentHeight;
  373. }
  374. // Refresh maximums
  375. self.__computeScrollMax();
  376. // Refresh scroll position
  377. self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
  378. },
  379. /**
  380. * Sets the client coordinates in relation to the document.
  381. *
  382. * @param left {Integer ? 0} Left position of outer element
  383. * @param top {Integer ? 0} Top position of outer element
  384. */
  385. setPosition: function (left, top) {
  386. var self = this;
  387. self.__clientLeft = left || 0;
  388. self.__clientTop = top || 0;
  389. },
  390. /**
  391. * Configures the snapping (when snapping is active)
  392. *
  393. * @param width {Integer} Snapping width
  394. * @param height {Integer} Snapping height
  395. */
  396. setSnapSize: function (width, height) {
  397. var self = this;
  398. self.__snapWidth = width;
  399. self.__snapHeight = height;
  400. },
  401. /**
  402. * Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
  403. * the user event is released during visibility of this zone. This was introduced by some apps on iOS like
  404. * the official Twitter client.
  405. *
  406. * @param height {Integer} Height of pull-to-refresh zone on top of rendered list
  407. * @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
  408. * @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
  409. * @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
  410. */
  411. activatePullToRefresh: function (height, activateCallback, deactivateCallback, startCallback) {
  412. var self = this;
  413. self.__refreshHeight = height;
  414. self.__refreshActivate = activateCallback;
  415. self.__refreshDeactivate = deactivateCallback;
  416. self.__refreshStart = startCallback;
  417. },
  418. /**
  419. * Starts pull-to-refresh manually.
  420. */
  421. triggerPullToRefresh: function () {
  422. // Use publish instead of scrollTo to allow scrolling to out of boundary position
  423. // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
  424. this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true);
  425. if (this.__refreshStart) {
  426. this.__refreshStart();
  427. }
  428. },
  429. /**
  430. * Signalizes that pull-to-refresh is finished.
  431. */
  432. finishPullToRefresh: function () {
  433. var self = this;
  434. self.__refreshActive = false;
  435. if (self.__refreshDeactivate) {
  436. self.__refreshDeactivate();
  437. }
  438. self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
  439. },
  440. /**
  441. * Returns the scroll position and zooming values
  442. *
  443. * @return {Map} `left` and `top` scroll position and `zoom` level
  444. */
  445. getValues: function () {
  446. var self = this;
  447. return {
  448. left: self.__scrollLeft,
  449. top: self.__scrollTop,
  450. zoom: self.__zoomLevel,
  451. };
  452. },
  453. /**
  454. * Returns the maximum scroll values
  455. *
  456. * @return {Map} `left` and `top` maximum scroll values
  457. */
  458. getScrollMax: function () {
  459. var self = this;
  460. return {
  461. left: self.__maxScrollLeft,
  462. top: self.__maxScrollTop,
  463. };
  464. },
  465. /**
  466. * Zooms to the given level. Supports optional animation. Zooms
  467. * the center when no coordinates are given.
  468. *
  469. * @param level {Number} Level to zoom to
  470. * @param animate {Boolean ? false} Whether to use animation
  471. * @param originLeft {Number ? null} Zoom in at given left coordinate
  472. * @param originTop {Number ? null} Zoom in at given top coordinate
  473. * @param callback {Function ? null} A callback that gets fired when the zoom is complete.
  474. */
  475. zoomTo: function (level, animate, originLeft, originTop, callback) {
  476. var self = this;
  477. if (!self.options.zooming) {
  478. throw new Error('Zooming is not enabled!');
  479. }
  480. // Add callback if exists
  481. if (callback) {
  482. self.__zoomComplete = callback;
  483. }
  484. // Stop deceleration
  485. if (self.__isDecelerating) {
  486. core.effect.Animate.stop(self.__isDecelerating);
  487. self.__isDecelerating = false;
  488. }
  489. var oldLevel = self.__zoomLevel;
  490. // Normalize input origin to center of viewport if not defined
  491. if (originLeft == null) {
  492. originLeft = self.__clientWidth / 2;
  493. }
  494. if (originTop == null) {
  495. originTop = self.__clientHeight / 2;
  496. }
  497. // Limit level according to configuration
  498. level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
  499. // Recompute maximum values while temporary tweaking maximum scroll ranges
  500. self.__computeScrollMax(level);
  501. // Recompute left and top coordinates based on new zoom level
  502. var left = ((originLeft + self.__scrollLeft) * level) / oldLevel - originLeft;
  503. var top = ((originTop + self.__scrollTop) * level) / oldLevel - originTop;
  504. // Limit x-axis
  505. if (left > self.__maxScrollLeft) {
  506. left = self.__maxScrollLeft;
  507. } else if (left < 0) {
  508. left = 0;
  509. }
  510. // Limit y-axis
  511. if (top > self.__maxScrollTop) {
  512. top = self.__maxScrollTop;
  513. } else if (top < 0) {
  514. top = 0;
  515. }
  516. // Push values out
  517. self.__publish(left, top, level, animate);
  518. },
  519. /**
  520. * Zooms the content by the given factor.
  521. *
  522. * @param factor {Number} Zoom by given factor
  523. * @param animate {Boolean ? false} Whether to use animation
  524. * @param originLeft {Number ? 0} Zoom in at given left coordinate
  525. * @param originTop {Number ? 0} Zoom in at given top coordinate
  526. * @param callback {Function ? null} A callback that gets fired when the zoom is complete.
  527. */
  528. zoomBy: function (factor, animate, originLeft, originTop, callback) {
  529. var self = this;
  530. self.zoomTo(self.__zoomLevel * factor, animate, originLeft, originTop, callback);
  531. },
  532. /**
  533. * Scrolls to the given position. Respect limitations and snapping automatically.
  534. *
  535. * @param left {Number?null} Horizontal scroll position, keeps current if value is <code>null</code>
  536. * @param top {Number?null} Vertical scroll position, keeps current if value is <code>null</code>
  537. * @param animate {Boolean?false} Whether the scrolling should happen using an animation
  538. * @param zoom {Number?null} Zoom level to go to
  539. */
  540. scrollTo: function (left, top, animate, zoom) {
  541. var self = this;
  542. // Stop deceleration
  543. if (self.__isDecelerating) {
  544. core.effect.Animate.stop(self.__isDecelerating);
  545. self.__isDecelerating = false;
  546. }
  547. // Correct coordinates based on new zoom level
  548. if (zoom != null && zoom !== self.__zoomLevel) {
  549. if (!self.options.zooming) {
  550. throw new Error('Zooming is not enabled!');
  551. }
  552. left *= zoom;
  553. top *= zoom;
  554. // Recompute maximum values while temporary tweaking maximum scroll ranges
  555. self.__computeScrollMax(zoom);
  556. } else {
  557. // Keep zoom when not defined
  558. zoom = self.__zoomLevel;
  559. }
  560. if (!self.options.scrollingX) {
  561. left = self.__scrollLeft;
  562. } else {
  563. if (self.options.paging) {
  564. left = Math.round(left / self.__clientWidth) * self.__clientWidth;
  565. } else if (self.options.snapping) {
  566. left = Math.round(left / self.__snapWidth) * self.__snapWidth;
  567. }
  568. }
  569. if (!self.options.scrollingY) {
  570. top = self.__scrollTop;
  571. } else {
  572. if (self.options.paging) {
  573. top = Math.round(top / self.__clientHeight) * self.__clientHeight;
  574. } else if (self.options.snapping) {
  575. top = Math.round(top / self.__snapHeight) * self.__snapHeight;
  576. }
  577. }
  578. // Limit for allowed ranges
  579. left = Math.max(Math.min(self.__maxScrollLeft, left), 0);
  580. top = Math.max(Math.min(self.__maxScrollTop, top), 0);
  581. // Don't animate when no change detected, still call publish to make sure
  582. // that rendered position is really in-sync with internal data
  583. if (left === self.__scrollLeft && top === self.__scrollTop) {
  584. animate = false;
  585. }
  586. // Publish new values
  587. if (!self.__isTracking) {
  588. self.__publish(left, top, zoom, animate);
  589. }
  590. },
  591. /**
  592. * Scroll by the given offset
  593. *
  594. * @param left {Number ? 0} Scroll x-axis by given offset
  595. * @param top {Number ? 0} Scroll x-axis by given offset
  596. * @param animate {Boolean ? false} Whether to animate the given change
  597. */
  598. scrollBy: function (left, top, animate) {
  599. var self = this;
  600. var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
  601. var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
  602. self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
  603. },
  604. /*
  605. ---------------------------------------------------------------------------
  606. EVENT CALLBACKS
  607. ---------------------------------------------------------------------------
  608. */
  609. /**
  610. * Mouse wheel handler for zooming support
  611. */
  612. doMouseZoom: function (wheelDelta, timeStamp, pageX, pageY) {
  613. var self = this;
  614. var change = wheelDelta > 0 ? 0.97 : 1.03;
  615. return self.zoomTo(self.__zoomLevel * change, false, pageX - self.__clientLeft, pageY - self.__clientTop);
  616. },
  617. /**
  618. * Touch start handler for scrolling support
  619. */
  620. doTouchStart: function (touches, timeStamp) {
  621. // Array-like check is enough here
  622. if (touches.length == null) {
  623. throw new Error('Invalid touch list: ' + touches);
  624. }
  625. if (timeStamp instanceof Date) {
  626. timeStamp = timeStamp.valueOf();
  627. }
  628. if (typeof timeStamp !== 'number') {
  629. throw new Error('Invalid timestamp value: ' + timeStamp);
  630. }
  631. var self = this;
  632. // Reset interruptedAnimation flag
  633. self.__interruptedAnimation = true;
  634. // Stop deceleration
  635. if (self.__isDecelerating) {
  636. core.effect.Animate.stop(self.__isDecelerating);
  637. self.__isDecelerating = false;
  638. self.__interruptedAnimation = true;
  639. }
  640. // Stop animation
  641. if (self.__isAnimating) {
  642. core.effect.Animate.stop(self.__isAnimating);
  643. self.__isAnimating = false;
  644. self.__interruptedAnimation = true;
  645. }
  646. // Use center point when dealing with two fingers
  647. var currentTouchLeft, currentTouchTop;
  648. var isSingleTouch = touches.length === 1;
  649. if (isSingleTouch) {
  650. currentTouchLeft = touches[0].pageX;
  651. currentTouchTop = touches[0].pageY;
  652. } else {
  653. currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
  654. currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
  655. }
  656. // Store initial positions
  657. self.__initialTouchLeft = currentTouchLeft;
  658. self.__initialTouchTop = currentTouchTop;
  659. // Store current zoom level
  660. self.__zoomLevelStart = self.__zoomLevel;
  661. // Store initial touch positions
  662. self.__lastTouchLeft = currentTouchLeft;
  663. self.__lastTouchTop = currentTouchTop;
  664. // Store initial move time stamp
  665. self.__lastTouchMove = timeStamp;
  666. // Reset initial scale
  667. self.__lastScale = 1;
  668. // Reset locking flags
  669. self.__enableScrollX = !isSingleTouch && self.options.scrollingX;
  670. self.__enableScrollY = !isSingleTouch && self.options.scrollingY;
  671. // Reset tracking flag
  672. self.__isTracking = true;
  673. // Reset deceleration complete flag
  674. self.__didDecelerationComplete = false;
  675. // Dragging starts directly with two fingers, otherwise lazy with an offset
  676. self.__isDragging = !isSingleTouch;
  677. // Some features are disabled in multi touch scenarios
  678. self.__isSingleTouch = isSingleTouch;
  679. // Clearing data structure
  680. self.__positions = [];
  681. },
  682. /**
  683. * Touch move handler for scrolling support
  684. */
  685. doTouchMove: function (touches, timeStamp, scale) {
  686. // Array-like check is enough here
  687. if (touches.length == null) {
  688. throw new Error('Invalid touch list: ' + touches);
  689. }
  690. if (timeStamp instanceof Date) {
  691. timeStamp = timeStamp.valueOf();
  692. }
  693. if (typeof timeStamp !== 'number') {
  694. throw new Error('Invalid timestamp value: ' + timeStamp);
  695. }
  696. var self = this;
  697. // Ignore event when tracking is not enabled (event might be outside of element)
  698. if (!self.__isTracking) {
  699. return;
  700. }
  701. var currentTouchLeft, currentTouchTop;
  702. // Compute move based around of center of fingers
  703. if (touches.length === 2) {
  704. currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
  705. currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
  706. } else {
  707. currentTouchLeft = touches[0].pageX;
  708. currentTouchTop = touches[0].pageY;
  709. }
  710. var positions = self.__positions;
  711. // Are we already is dragging mode?
  712. if (self.__isDragging) {
  713. // Compute move distance
  714. var moveX = currentTouchLeft - self.__lastTouchLeft;
  715. var moveY = currentTouchTop - self.__lastTouchTop;
  716. // Read previous scroll position and zooming
  717. var scrollLeft = self.__scrollLeft;
  718. var scrollTop = self.__scrollTop;
  719. var level = self.__zoomLevel;
  720. // Work with scaling
  721. if (scale != null && self.options.zooming) {
  722. var oldLevel = level;
  723. // Recompute level based on previous scale and new scale
  724. level = (level / self.__lastScale) * scale;
  725. // Limit level according to configuration
  726. level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
  727. // Only do further compution when change happened
  728. if (oldLevel !== level) {
  729. // Compute relative event position to container
  730. var currentTouchLeftRel = currentTouchLeft - self.__clientLeft;
  731. var currentTouchTopRel = currentTouchTop - self.__clientTop;
  732. // Recompute left and top coordinates based on new zoom level
  733. scrollLeft = ((currentTouchLeftRel + scrollLeft) * level) / oldLevel - currentTouchLeftRel;
  734. scrollTop = ((currentTouchTopRel + scrollTop) * level) / oldLevel - currentTouchTopRel;
  735. // Recompute max scroll values
  736. self.__computeScrollMax(level);
  737. }
  738. }
  739. if (self.__enableScrollX) {
  740. scrollLeft -= moveX * this.options.speedMultiplier;
  741. var maxScrollLeft = self.__maxScrollLeft;
  742. if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
  743. // Slow down on the edges
  744. if (self.options.bouncing) {
  745. scrollLeft += (moveX / 2) * this.options.speedMultiplier;
  746. } else if (scrollLeft > maxScrollLeft) {
  747. scrollLeft = maxScrollLeft;
  748. } else {
  749. scrollLeft = 0;
  750. }
  751. }
  752. }
  753. // Compute new vertical scroll position
  754. if (self.__enableScrollY) {
  755. scrollTop -= moveY * this.options.speedMultiplier;
  756. var maxScrollTop = self.__maxScrollTop;
  757. if (scrollTop > maxScrollTop || scrollTop < 0) {
  758. // Slow down on the edges
  759. if (self.options.bouncing) {
  760. scrollTop += (moveY / 2) * this.options.speedMultiplier;
  761. // Support pull-to-refresh (only when only y is scrollable)
  762. if (!self.__enableScrollX && self.__refreshHeight != null) {
  763. if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) {
  764. self.__refreshActive = true;
  765. if (self.__refreshActivate) {
  766. self.__refreshActivate();
  767. }
  768. } else if (self.__refreshActive && scrollTop > -self.__refreshHeight) {
  769. self.__refreshActive = false;
  770. if (self.__refreshDeactivate) {
  771. self.__refreshDeactivate();
  772. }
  773. }
  774. }
  775. } else if (scrollTop > maxScrollTop) {
  776. scrollTop = maxScrollTop;
  777. } else {
  778. scrollTop = 0;
  779. }
  780. }
  781. }
  782. // Keep list from growing infinitely (holding min 10, max 20 measure points)
  783. if (positions.length > 60) {
  784. positions.splice(0, 30);
  785. }
  786. // Track scroll movement for decleration
  787. positions.push(scrollLeft, scrollTop, timeStamp);
  788. // Sync scroll position
  789. self.__publish(scrollLeft, scrollTop, level);
  790. // Otherwise figure out whether we are switching into dragging mode now.
  791. } else {
  792. var minimumTrackingForScroll = self.options.locking ? 3 : 0;
  793. var minimumTrackingForDrag = 5;
  794. var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft);
  795. var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop);
  796. self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll;
  797. self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll;
  798. positions.push(self.__scrollLeft, self.__scrollTop, timeStamp);
  799. self.__isDragging =
  800. (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag);
  801. if (self.__isDragging) {
  802. self.__interruptedAnimation = false;
  803. }
  804. }
  805. // Update last touch positions and time stamp for next event
  806. self.__lastTouchLeft = currentTouchLeft;
  807. self.__lastTouchTop = currentTouchTop;
  808. self.__lastTouchMove = timeStamp;
  809. self.__lastScale = scale;
  810. },
  811. /**
  812. * Touch end handler for scrolling support
  813. */
  814. doTouchEnd: function (timeStamp) {
  815. if (timeStamp instanceof Date) {
  816. timeStamp = timeStamp.valueOf();
  817. }
  818. if (typeof timeStamp !== 'number') {
  819. throw new Error('Invalid timestamp value: ' + timeStamp);
  820. }
  821. var self = this;
  822. // Ignore event when tracking is not enabled (no touchstart event on element)
  823. // This is required as this listener ('touchmove') sits on the document and not on the element itself.
  824. if (!self.__isTracking) {
  825. return;
  826. }
  827. // Not touching anymore (when two finger hit the screen there are two touch end events)
  828. self.__isTracking = false;
  829. // Be sure to reset the dragging flag now. Here we also detect whether
  830. // the finger has moved fast enough to switch into a deceleration animation.
  831. if (self.__isDragging) {
  832. // Reset dragging flag
  833. self.__isDragging = false;
  834. // Start deceleration
  835. // Verify that the last move detected was in some relevant time frame
  836. if (self.__isSingleTouch && self.options.animating && timeStamp - self.__lastTouchMove <= 100) {
  837. // Then figure out what the scroll position was about 100ms ago
  838. var positions = self.__positions;
  839. var endPos = positions.length - 1;
  840. var startPos = endPos;
  841. // Move pointer to position measured 100ms ago
  842. for (var i = endPos; i > 0 && positions[i] > self.__lastTouchMove - 100; i -= 3) {
  843. startPos = i;
  844. }
  845. // If start and stop position is identical in a 100ms timeframe,
  846. // we cannot compute any useful deceleration.
  847. if (startPos !== endPos) {
  848. // Compute relative movement between these two points
  849. var timeOffset = positions[endPos] - positions[startPos];
  850. var movedLeft = self.__scrollLeft - positions[startPos - 2];
  851. var movedTop = self.__scrollTop - positions[startPos - 1];
  852. // Based on 50ms compute the movement to apply for each render step
  853. self.__decelerationVelocityX = (movedLeft / timeOffset) * (1000 / 60);
  854. self.__decelerationVelocityY = (movedTop / timeOffset) * (1000 / 60);
  855. // How much velocity is required to start the deceleration
  856. var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? 4 : 1;
  857. // Verify that we have enough velocity to start deceleration
  858. if (
  859. Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration ||
  860. Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration
  861. ) {
  862. // Deactivate pull-to-refresh when decelerating
  863. if (!self.__refreshActive) {
  864. self.__startDeceleration(timeStamp);
  865. }
  866. } else {
  867. self.options.scrollingComplete();
  868. }
  869. } else {
  870. self.options.scrollingComplete();
  871. }
  872. } else if (timeStamp - self.__lastTouchMove > 100) {
  873. self.options.scrollingComplete();
  874. }
  875. }
  876. // If this was a slower move it is per default non decelerated, but this
  877. // still means that we want snap back to the bounds which is done here.
  878. // This is placed outside the condition above to improve edge case stability
  879. // e.g. touchend fired without enabled dragging. This should normally do not
  880. // have modified the scroll positions or even showed the scrollbars though.
  881. if (!self.__isDecelerating) {
  882. if (self.__refreshActive && self.__refreshStart) {
  883. // Use publish instead of scrollTo to allow scrolling to out of boundary position
  884. // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
  885. self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true);
  886. if (self.__refreshStart) {
  887. self.__refreshStart();
  888. }
  889. } else {
  890. if (self.__interruptedAnimation || self.__isDragging) {
  891. self.options.scrollingComplete();
  892. }
  893. self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel);
  894. // Directly signalize deactivation (nothing todo on refresh?)
  895. if (self.__refreshActive) {
  896. self.__refreshActive = false;
  897. if (self.__refreshDeactivate) {
  898. self.__refreshDeactivate();
  899. }
  900. }
  901. }
  902. }
  903. // Fully cleanup list
  904. self.__positions.length = 0;
  905. },
  906. /*
  907. ---------------------------------------------------------------------------
  908. PRIVATE API
  909. ---------------------------------------------------------------------------
  910. */
  911. /**
  912. * Applies the scroll position to the content element
  913. *
  914. * @param left {Number} Left scroll position
  915. * @param top {Number} Top scroll position
  916. * @param animate {Boolean?false} Whether animation should be used to move to the new coordinates
  917. */
  918. __publish: function (left, top, zoom, animate) {
  919. var self = this;
  920. // Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
  921. var wasAnimating = self.__isAnimating;
  922. if (wasAnimating) {
  923. core.effect.Animate.stop(wasAnimating);
  924. self.__isAnimating = false;
  925. }
  926. if (animate && self.options.animating) {
  927. // Keep scheduled positions for scrollBy/zoomBy functionality
  928. self.__scheduledLeft = left;
  929. self.__scheduledTop = top;
  930. self.__scheduledZoom = zoom;
  931. var oldLeft = self.__scrollLeft;
  932. var oldTop = self.__scrollTop;
  933. var oldZoom = self.__zoomLevel;
  934. var diffLeft = left - oldLeft;
  935. var diffTop = top - oldTop;
  936. var diffZoom = zoom - oldZoom;
  937. var step = function (percent, now, render) {
  938. if (render) {
  939. self.__scrollLeft = oldLeft + diffLeft * percent;
  940. self.__scrollTop = oldTop + diffTop * percent;
  941. self.__zoomLevel = oldZoom + diffZoom * percent;
  942. // Push values out
  943. if (self.__callback) {
  944. self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel);
  945. }
  946. }
  947. };
  948. var verify = function (id) {
  949. return self.__isAnimating === id;
  950. };
  951. var completed = function (renderedFramesPerSecond, animationId, wasFinished) {
  952. if (animationId === self.__isAnimating) {
  953. self.__isAnimating = false;
  954. }
  955. if (self.__didDecelerationComplete || wasFinished) {
  956. self.options.scrollingComplete();
  957. }
  958. if (self.options.zooming) {
  959. self.__computeScrollMax();
  960. if (self.__zoomComplete) {
  961. self.__zoomComplete();
  962. self.__zoomComplete = null;
  963. }
  964. }
  965. };
  966. // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
  967. self.__isAnimating = core.effect.Animate.start(
  968. step,
  969. verify,
  970. completed,
  971. self.options.animationDuration,
  972. wasAnimating ? easeOutCubic : easeInOutCubic
  973. );
  974. } else {
  975. self.__scheduledLeft = self.__scrollLeft = left;
  976. self.__scheduledTop = self.__scrollTop = top;
  977. self.__scheduledZoom = self.__zoomLevel = zoom;
  978. // Push values out
  979. if (self.__callback) {
  980. self.__callback(left, top, zoom);
  981. }
  982. // Fix max scroll ranges
  983. if (self.options.zooming) {
  984. self.__computeScrollMax();
  985. if (self.__zoomComplete) {
  986. self.__zoomComplete();
  987. self.__zoomComplete = null;
  988. }
  989. }
  990. }
  991. },
  992. /**
  993. * Recomputes scroll minimum values based on client dimensions and content dimensions.
  994. */
  995. __computeScrollMax: function (zoomLevel) {
  996. var self = this;
  997. if (zoomLevel == null) {
  998. zoomLevel = self.__zoomLevel;
  999. }
  1000. self.__maxScrollLeft = Math.max(self.__contentWidth * zoomLevel - self.__clientWidth, 0);
  1001. self.__maxScrollTop = Math.max(self.__contentHeight * zoomLevel - self.__clientHeight, 0);
  1002. },
  1003. /*
  1004. ---------------------------------------------------------------------------
  1005. ANIMATION (DECELERATION) SUPPORT
  1006. ---------------------------------------------------------------------------
  1007. */
  1008. /**
  1009. * Called when a touch sequence end and the speed of the finger was high enough
  1010. * to switch into deceleration mode.
  1011. */
  1012. __startDeceleration: function (timeStamp) {
  1013. var self = this;
  1014. if (self.options.paging) {
  1015. var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0);
  1016. var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0);
  1017. var clientWidth = self.__clientWidth;
  1018. var clientHeight = self.__clientHeight;
  1019. // We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
  1020. // Each page should have exactly the size of the client area.
  1021. self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth;
  1022. self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight;
  1023. self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth;
  1024. self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight;
  1025. } else {
  1026. self.__minDecelerationScrollLeft = 0;
  1027. self.__minDecelerationScrollTop = 0;
  1028. self.__maxDecelerationScrollLeft = self.__maxScrollLeft;
  1029. self.__maxDecelerationScrollTop = self.__maxScrollTop;
  1030. }
  1031. // Wrap class method
  1032. var step = function (percent, now, render) {
  1033. self.__stepThroughDeceleration(render);
  1034. };
  1035. // How much velocity is required to keep the deceleration running
  1036. var minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.001;
  1037. // Detect whether it's still worth to continue animating steps
  1038. // If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
  1039. var verify = function () {
  1040. var shouldContinue =
  1041. Math.abs(self.__decelerationVelocityX) >= minVelocityToKeepDecelerating ||
  1042. Math.abs(self.__decelerationVelocityY) >= minVelocityToKeepDecelerating;
  1043. if (!shouldContinue) {
  1044. self.__didDecelerationComplete = true;
  1045. }
  1046. return shouldContinue;
  1047. };
  1048. var completed = function (renderedFramesPerSecond, animationId, wasFinished) {
  1049. self.__isDecelerating = false;
  1050. if (self.__didDecelerationComplete) {
  1051. self.options.scrollingComplete();
  1052. }
  1053. // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
  1054. self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping);
  1055. };
  1056. // Start animation and switch on flag
  1057. self.__isDecelerating = core.effect.Animate.start(step, verify, completed);
  1058. },
  1059. /**
  1060. * Called on every step of the animation
  1061. *
  1062. * @param inMemory {Boolean?false} Whether to not render the current step, but keep it in memory only. Used internally only!
  1063. */
  1064. __stepThroughDeceleration: function (render) {
  1065. var self = this;
  1066. //
  1067. // COMPUTE NEXT SCROLL POSITION
  1068. //
  1069. // Add deceleration to scroll position
  1070. var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;
  1071. var scrollTop = self.__scrollTop + self.__decelerationVelocityY;
  1072. //
  1073. // HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
  1074. //
  1075. if (!self.options.bouncing) {
  1076. var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft);
  1077. if (scrollLeftFixed !== scrollLeft) {
  1078. scrollLeft = scrollLeftFixed;
  1079. self.__decelerationVelocityX = 0;
  1080. }
  1081. var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop);
  1082. if (scrollTopFixed !== scrollTop) {
  1083. scrollTop = scrollTopFixed;
  1084. self.__decelerationVelocityY = 0;
  1085. }
  1086. }
  1087. //
  1088. // UPDATE SCROLL POSITION
  1089. //
  1090. if (render) {
  1091. self.__publish(scrollLeft, scrollTop, self.__zoomLevel);
  1092. } else {
  1093. self.__scrollLeft = scrollLeft;
  1094. self.__scrollTop = scrollTop;
  1095. }
  1096. //
  1097. // SLOW DOWN
  1098. //
  1099. // Slow down velocity on every iteration
  1100. if (!self.options.paging) {
  1101. // This is the factor applied to every iteration of the animation
  1102. // to slow down the process. This should emulate natural behavior where
  1103. // objects slow down when the initiator of the movement is removed
  1104. var frictionFactor = 0.95;
  1105. self.__decelerationVelocityX *= frictionFactor;
  1106. self.__decelerationVelocityY *= frictionFactor;
  1107. }
  1108. //
  1109. // BOUNCING SUPPORT
  1110. //
  1111. if (self.options.bouncing) {
  1112. var scrollOutsideX = 0;
  1113. var scrollOutsideY = 0;
  1114. // This configures the amount of change applied to deceleration/acceleration when reaching boundaries
  1115. var penetrationDeceleration = self.options.penetrationDeceleration;
  1116. var penetrationAcceleration = self.options.penetrationAcceleration;
  1117. // Check limits
  1118. if (scrollLeft < self.__minDecelerationScrollLeft) {
  1119. scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft;
  1120. } else if (scrollLeft > self.__maxDecelerationScrollLeft) {
  1121. scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft;
  1122. }
  1123. if (scrollTop < self.__minDecelerationScrollTop) {
  1124. scrollOutsideY = self.__minDecelerationScrollTop - scrollTop;
  1125. } else if (scrollTop > self.__maxDecelerationScrollTop) {
  1126. scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop;
  1127. }
  1128. // Slow down until slow enough, then flip back to snap position
  1129. if (scrollOutsideX !== 0) {
  1130. if (scrollOutsideX * self.__decelerationVelocityX <= 0) {
  1131. self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration;
  1132. } else {
  1133. self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration;
  1134. }
  1135. }
  1136. if (scrollOutsideY !== 0) {
  1137. if (scrollOutsideY * self.__decelerationVelocityY <= 0) {
  1138. self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration;
  1139. } else {
  1140. self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration;
  1141. }
  1142. }
  1143. }
  1144. },
  1145. };
  1146. // Copy over members to prototype
  1147. for (var key in members) {
  1148. Scroller.prototype[key] = members[key];
  1149. }
  1150. if (typeof module != 'undefined' && module.exports) {
  1151. module.exports = Scroller;
  1152. } else if (typeof define == 'function' && define.amd) {
  1153. define(function () {
  1154. return Scroller;
  1155. });
  1156. } else {
  1157. window.Scroller = Scroller;
  1158. }
  1159. })(window);