ux/Tscroll.js
[roojs1] / ux / Tscroll.js
1 /**
2  *
3  * usage:
4  *
5  * new Roo.ux.TouchScroll( { el : id| dom element | roo element } )
6  *
7  *
8  */
9
10
11 Roo.ux.TouchScroll = function (cfg) {
12     Roo.apply(this,cfg);
13     this.el = Roo.get(this.el);
14     this.init();
15 }
16     
17 Roo.apply(Roo.ux.TouchScroll.prototype, {
18     
19         // Define default scroll settings
20          
21         y: 0,
22         scrollHeight: 0,
23         elastic: !navigator.userAgent.match(/android/i),
24         momentum: true,
25         elasticDamp: 0.6,
26         elasticTime: 50,
27         reboundTime: 400,
28         momentumDamp: 0.9,
29         momentumTime: 300,
30         iPadMomentumDamp: 0.95,
31         iPadMomentumTime: 1200,
32         touchTags: ['select', 'input', 'textarea'],
33         
34         _init : false,
35         
36         init: function( ) {
37                 
38                 
39               // Prevent double-init, just update instead
40             if (this._init) {
41                 return this.update();
42             }
43             
44             this._init = true;
45                         
46             // Define element variables
47             var 
48                 scrollY = -this.el.getY(),
49                 touchY = 0,
50                 movedY = 0,
51                 pollY = 0,
52                 height = 0,
53                 maxHeight = 0,
54                 scrollHeight = 0,
55                 scrolling = false,
56                 bouncing = false,
57                 moved = false,
58                 timeoutID,
59                 isiPad = navigator.platform.match(/ipad/i) ? true : false,
60                 hasMatrix = 'WebKitCSSMatrix' in window,
61                 has3d = hasMatrix && 'm11' in new WebKitCSSMatrix();
62                         
63                 // Keep bottom of scroll area at the bottom on resize
64                 var update = this.update = function() {
65                         height = el.getHeight();
66                         if (el.dom.scrollHeight) {
67                                 scrollHeight = el.dom.scrollHeight;
68                         } else {
69                                 scrollHeight = el.dom.getAttribute('scrollHeight');
70                         }
71                         if (scrollHeight < height) {
72                                 scrollHeight = height;
73                         }
74                         maxHeight = height - scrollHeight;
75                         clearTimeout(timeoutID);
76                         clampScroll(false);
77                 };
78                         
79                 // Set up initial variables
80                 update();
81                 
82                         // Set up transform CSS
83                 this.el.setStyle({
84                     '-webkit-transition-property': '-webkit-transform',
85                     '-webkit-transition-timing-function': 'cubic-bezier(0,0,0.2,1)',
86                     '-webkit-transition-duration': '0',
87                     '-webkit-transform': cssTranslate(scrollY)
88                 });
89                         
90                         // Listen for screen size change event
91                 window.addEventListener('onorientationchange' in window ?
92                                 'orientationchange' : 'resize', update, false);
93                         
94                 // Listen for touch events
95                 this.el.on('touchstart.touchScroll', touchStart);
96                 this.el.on('touchmove.touchScroll', touchMove);
97                 this.el.on('touchend.touchScroll touchcancel.touchScroll', touchEnd);
98                 this.el.on('webkitTransitionEnd.touchScroll', transitionEnd);
99                         
100                         // Set the position of the scroll area using transform CSS
101                 var setPosition = this.setPosition = function(y) {
102                         scrollY = y;
103                         this.el.setStyle('-webkit-transform', cssTranslate(scrollY));
104                 };
105                 
106                         // Transform using a 3D translate if available
107                 function cssTranslate(y) {
108                         return 'translate' + (has3d ? '3d(0,' : '(0,') + y + 'px' + (has3d ? ',0)' : ')');
109                 }
110                         
111                         // Set CSS transition time
112                 function setTransitionTime(time) {
113                         time = time || '0';
114                         this.el.setStyle('-webkit-transition-duration', time + 'ms');
115                 }
116     
117                             // Get the actual pixel position made by transform CSS
118                 function getPosition() {
119                         if (hasMatrix) {
120                                 var transform = window.getComputedStyle(this.el.dom).webkitTransform;
121                                 if (!!transform && transform !== 'none') {
122                                         var matrix = new WebKitCSSMatrix(transform);
123                                         return matrix.f;
124                                 }
125                         }
126                         return scrollY;
127                 }
128                             
129                     // Expose getPosition API
130                 this.getPosition = function() {
131                         return getPosition();
132                 };
133
134                         // Bounce back to the bounds after momentum scrolling
135                         function reboundScroll() {
136                                 if (scrollY > 0) {
137                                         scrollTo(0, o.reboundTime);
138                                 } else if (scrollY < maxHeight) {
139                                         scrollTo(maxHeight, o.reboundTime);
140                                 }
141                         }
142
143                         // Stop everything once the CSS transition in complete
144                         function transitionEnd() {
145                                 if (bouncing) {
146                                         bouncing = false;
147                                         reboundScroll();
148                                 }
149
150                                 clearTimeout(timeoutID);
151                         }
152                         
153                         // Limit the scrolling to within the bounds
154                         function clampScroll(poll) {
155                                 if (!hasMatrix || bouncing) {
156                                         return;
157                                 }
158
159                                 var oldY = pollY;
160                                 pollY = getPosition();
161                                 
162                                 if (pollY > 0) {
163                                         if (o.elastic) {
164                                                 // Slow down outside top bound
165                                                 bouncing = true;
166                                                 scrollY = 0;
167                                                 momentumScroll(pollY - oldY, o.elasticDamp, 1, height, o.elasticTime);
168                                         } else {
169                                                 // Stop outside top bound
170                                                 setTransitionTime(0);
171                                                 setPosition(0);
172                                         }
173                                 } else if (pollY < maxHeight) {
174                                         if (o.elastic) {
175                                                 // Slow down outside bottom bound
176                                                 bouncing = true;
177                                                 scrollY = maxHeight;
178                                                 momentumScroll(pollY - oldY, o.elasticDamp, 1, height, o.elasticTime);
179                                         } else {
180                                                 // Stop outside bottom bound
181                                                 setTransitionTime(0);
182                                                 setPosition(maxHeight);
183                                         }
184                                 } else if (poll) {
185                                         // Poll the computed position to check if element is out of bounds
186                                         timeoutID = setTimeout(clampScroll, 20, true);
187                                 }
188                         }
189                         
190                         // Animate to a position using CSS
191                         function scrollTo(destY, time) {
192                                 if (destY === scrollY) {
193                                         return;
194                                 }
195                                 
196                                 moved = true;
197                                 setTransitionTime(time);
198                                 setPosition(destY);
199                         }
200                         
201                         // Perform a momentum-based scroll using CSS
202                         function momentumScroll(d, k, minDist, maxDist, t) {
203                                 var ad = Math.abs(d),
204                                         dy = 0;
205                                 
206                                 // Calculate the total distance
207                                 while (ad > 0.1) {
208                                         ad *= k;
209                                         dy += ad;
210                                 }
211                                 
212                                 // Limit to within min and max distances
213                                 if (dy > maxDist) {
214                                         dy = maxDist;
215                                 }
216                                 if (dy > minDist) {
217                                         if (d < 0) {
218                                                 dy = -dy;
219                                         }
220                                         
221                                         dy += scrollY;
222                                         
223                                         // If outside the bounds, don't go too far
224                                         if (height > 0) {
225                                                 if (dy > height * 2) {
226                                                         var ody = dy;
227                                                         dy = height * 2;
228                                                 } else if (dy < maxHeight - height * 2) {
229                                                         dy = maxHeight - height * 2;
230                                                 }
231                                         }
232                                 
233                                         // Perform scroll
234                                         scrollTo(Math.round(dy), t);
235                                 }
236                                 
237                                 clampScroll(true);
238                         }
239                         
240                         // Get the touch points from this event
241                         function getTouches(e) {
242                                 if (e.originalEvent) {
243                                         if (e.originalEvent.touches && e.originalEvent.touches.length) {
244                                                 return e.originalEvent.touches;
245                                         } else if (e.originalEvent.changedTouches && e.originalEvent.changedTouches.length) {
246                                                 return e.originalEvent.changedTouches;
247                                         }
248                                 }
249                                 return e.touches;
250                         }
251                         
252                         // Dispatches a fake mouse event from a touch event
253                         function dispatchMouseEvent(name, touch, target) {
254                                 var e = document.createEvent('MouseEvent');
255                                 e.initMouseEvent(name, true, true, touch.view, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
256                                 target.dispatchEvent(e);
257                         }
258                         
259                         // Find the root node of this target
260                         function getRootNode(target) {
261                                 while (target.nodeType !== 1) {
262                                         target = target.parentNode;
263                                 }
264                                 return target;
265                         }
266                         
267                         // Perform a touch start event
268                         function touchStart(e) {
269                                 // Allow certain HTML tags to receive touch events
270                                 if ($.inArray(e.target.tagName.toLowerCase(), o.touchTags) !== -1) {
271                                         return;
272                                 }
273                                 
274                                 // Stop the default touches
275                                 e.preventDefault();
276                                 e.stopPropagation();
277                                 
278                                 var touch = getTouches(e)[0];
279                                 
280                                 // Dispatch a fake mouse down event             
281                                 dispatchMouseEvent('mousedown', touch, getRootNode(touch.target));
282                                 
283                                 scrolling = true;
284                                 moved = false;
285                                 movedY = 0;
286                                 
287                                 clearTimeout(timeoutID);
288                                 setTransitionTime(0);
289                                 
290                                 // Check scroll position
291                                 if (o.momentum) {
292                                         var y = getPosition();
293                                         if (y !== scrollY) {
294                                                 setPosition(y);
295                                                 moved = true;
296                                         }
297                                 }
298
299                                 touchY = touch.pageY - scrollY;
300                         }
301                         
302                         // Perform a touch move event
303                         function touchMove(e) {
304                                 if (!scrolling) {
305                                         return;
306                                 }
307                                 
308                                 var dy = getTouches(e)[0].pageY - touchY;
309                                 
310                                 // Elastic-drag or stop when moving outside of boundaries
311                                 if (dy > 0) {
312                                         if (o.elastic) {
313                                                 dy /= 2;
314                                         } else {
315                                                 dy = 0;
316                                         }
317                                 } else if (dy < maxHeight) {
318                                         if (o.elastic) {
319                                                 dy = (dy + maxHeight) / 2;
320                                         } else {
321                                                 dy = maxHeight;
322                                         }
323                                 }
324                                 
325                                 movedY = dy - scrollY;
326                                 moved = true;
327                                 setPosition(dy);
328                         }
329                         
330                         // Perform a touch end event
331                         function touchEnd(e) {
332                                 if (!scrolling) {
333                                         return;
334                                 }
335                                 
336                                 scrolling = false;
337                                 
338                                 if (moved) {
339                                         // Ease back to within boundaries
340                                         if (scrollY > 0 || scrollY < maxHeight) {
341                                                 reboundScroll();
342                                         } else if (o.momentum) {
343                                                 // Free scroll with momentum
344                                                 momentumScroll(movedY, isiPad ? o.iPadMomentumDamp : o.momentumDamp, 40, 2000, isiPad ? o.iPadMomentumTime : o.momentumTime);
345                                         }                       
346                                 } else {
347                                         var touch = getTouches(e)[0],
348                                                 target = getRootNode(touch.target);
349                                         
350                                         // Dispatch fake mouse up and click events if this touch event did not move
351                                         dispatchMouseEvent('mouseup', touch, target);
352                                         dispatchMouseEvent('click', touch, target);
353                                 }
354                         }
355                 
356                 });
357         },
358         
359         update: function() {
360                 return this.each(function() {
361                         this.update();
362                 });
363         },
364         
365         getPosition: function() {
366                 var a = [];
367                 this.each(function() {
368                         a.push(-this.getPosition());
369                 });
370                 return a;
371         },
372         
373         setPosition: function(y) {
374                 return this.each(function() {
375                         this.setPosition(-y);
376                 });
377         }
378         
379 };
380         
381         // Public method for touchScroll
382         $.fn.touchScroll = function(method) {
383             if (methods[method]) {
384                         return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
385                 } else if (typeof method === 'object' || !method) {
386                         return methods.init.apply(this, arguments);
387                 } else {
388                         $.error('Method ' +  method + ' does not exist on jQuery.touchScroll');
389                 }
390         };
391
392 })(jQuery);