OSDN Git Service

e7d56d615a12b301415f8e18259fb7338f5ac5cb
[keitairc/keitairc.git] / data / public / iscroll / iscroll.js
1 /**
2  * 
3  * Find more about the scrolling function at
4  * http://cubiq.org/scrolling-div-for-mobile-webkit-turns-3/16
5  *
6  * Copyright (c) 2009 Matteo Spinelli, http://cubiq.org/
7  * Released under MIT license
8  * http://cubiq.org/dropbox/mit-license.txt
9  * 
10  * Version 3.2.1 - Last updated: 2010.06.03
11  * 
12  */
13
14 (function(){
15
16 function iScroll (el, options) {
17         this.element = typeof el == 'object' ? el : document.getElementById(el);
18         this.wrapper = this.element.parentNode;
19
20         this.element.style.webkitTransitionProperty = '-webkit-transform';
21         this.element.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
22         this.element.style.webkitTransitionDuration = '0';
23         this.element.style.webkitTransform = 'translate3d(0,0,0)';
24
25         // Get options
26         this.options = {
27                 bounce: true,
28                 checkDOMChanges: true,
29                 topOnDOMChanges: false,
30                 hScrollBar: true,
31             vScrollBar: true,
32             min_move: 5
33         };
34         
35         if (typeof options == 'object') {
36                 for (var i in options) {
37                         this.options[i] = options[i];
38                 }
39         }
40
41         this.refresh();
42         
43         this.element.addEventListener('touchstart', this);
44         this.element.addEventListener('touchmove', this);
45         this.element.addEventListener('touchend', this);
46         window.addEventListener('orientationchange', this);
47
48         if (this.options.checkDOMChanges) {
49                 this.element.addEventListener('DOMSubtreeModified', this);
50         }
51 }
52
53 iScroll.prototype = {
54         x: 0,
55         y: 0,
56
57         handleEvent: function (e) {
58                 switch (e.type) {
59                         case 'touchstart': this.onTouchStart(e); break;
60                         case 'touchmove': this.onTouchMove(e); break;
61                         case 'touchend': this.onTouchEnd(e); break;
62                         case 'webkitTransitionEnd': this.onTransitionEnd(e); break;
63                         case 'orientationchange': this.refresh(); break;
64                         case 'DOMSubtreeModified': this.onDOMModified(e); break;
65                 }
66         },
67         
68         onDOMModified: function (e) {
69                 this.refresh();
70                 
71                 if (this.options.topOnDOMChanges && (this.x!=0 || this.y!=0)) {
72                         this.scrollTo(0,0,'0');
73                 }
74         },
75
76         refresh: function () {
77                 this.scrollWidth = this.wrapper.clientWidth;
78                 this.scrollHeight = this.wrapper.clientHeight;
79                 this.maxScrollX = this.scrollWidth - this.element.offsetWidth;
80                 this.maxScrollY = this.scrollHeight - this.element.offsetHeight;
81
82                 var resetX = this.x, resetY = this.y;
83                 if (this.scrollX) {
84                         if (this.maxScrollX >= 0) {
85                                 resetX = 0;
86                         } else if (this.x < this.maxScrollX) {
87                                 resetX = this.maxScrollX;
88                         }
89                 }
90                 if (this.scrollY) {
91                         if (this.maxScrollY >= 0) {
92                                 resetY = 0;
93                         } else if (this.y < this.maxScrollY) {
94                                 resetY = this.maxScrollY;
95                         }
96                 }
97                 if (resetX!=this.x || resetY!=this.y) {
98                         this.scrollTo(resetX,resetY,'0');
99                 }
100
101                 this.scrollX = this.element.offsetWidth > this.scrollWidth ? true : false;
102                 this.scrollY = this.element.offsetHeight > this.scrollHeight ? true : false;
103
104                 // Update horizontal scrollbar
105                 if (this.options.hScrollBar && this.scrollX) {
106                         this.scrollBarX = (this.scrollBarX instanceof scrollbar) ? this.scrollBarX : new scrollbar('horizontal', this.wrapper);
107                         this.scrollBarX.init(this.scrollWidth, this.element.offsetWidth);
108                 } else if (this.scrollBarX) {
109                         this.scrollBarX = this.scrollBarX.remove();
110                 }
111
112                 // Update vertical scrollbar
113                 if (this.options.vScrollBar && this.scrollY) {
114                         this.scrollBarY = (this.scrollBarY instanceof scrollbar) ? this.scrollBarY : new scrollbar('vertical', this.wrapper);
115                         this.scrollBarY.init(this.scrollHeight, this.element.offsetHeight);
116                 } else if (this.scrollBarY) {
117                         this.scrollBarY = this.scrollBarY.remove();
118                 }
119         },
120
121         setPosition: function (x, y) { 
122                 this.x = x !== null ? x : this.x;
123                 this.y = y !== null ? y : this.y;
124
125                 this.element.style.webkitTransform = 'translate3d(' + this.x + 'px,' + this.y + 'px,0)';
126
127                 // Move the scrollbars
128                 if (this.scrollBarX) {
129                         this.scrollBarX.setPosition(this.scrollBarX.maxScroll / this.maxScrollX * this.x);
130                 }
131                 if (this.scrollBarY) {
132                         this.scrollBarY.setPosition(this.scrollBarY.maxScroll / this.maxScrollY * this.y);
133                 }
134         },
135                 
136         onTouchStart: function(e) {
137             if (e.targetTouches.length != 1) {
138                 return false;
139         }
140
141             /* iOS 4 以外 特に androidだと ここで preventDefault しないと
142                まともにスクロールしない/ブラウザがスクロールしてしまう/
143                スクロールしたあとの位置にあるリンクがクリックされちゃうとか
144                いろいろ起きる。逆に iOS4だと ここで preventDefault しちゃうと
145                長押ししたときの ディフォルトのポップアップが出てこなくなる */
146             /* iOS4 意外だと ディフォルトのポップアップが使えなくなるが どうやら 打つ手なし... */
147             if (!navigator.appVersion.match(/iPhone OS 4/)) {
148                 e.preventDefault();
149             }
150                 e.stopPropagation();
151                 
152                 this.element.style.webkitTransitionDuration = '0';
153                 
154                 if (this.scrollBarX) {
155                         this.scrollBarX.bar.style.webkitTransitionDuration = '0, 250ms';
156                 }
157                 if (this.scrollBarY) {
158                         this.scrollBarY.bar.style.webkitTransitionDuration = '0, 250ms';
159                 }
160
161                 // Check if elem is really where it should be
162                 var theTransform = new WebKitCSSMatrix(window.getComputedStyle(this.element).webkitTransform);
163                 if (theTransform.m41 != this.x || theTransform.m42 != this.y) {
164                         this.setPosition(theTransform.m41, theTransform.m42);
165                 }
166
167                 this.touchStartX = e.touches[0].pageX;
168                 this.scrollStartX = this.x;
169
170                 this.touchStartY = e.touches[0].pageY;
171                 this.scrollStartY = this.y;
172
173                 this.scrollStartTime = e.timeStamp;
174                 this.moved = false;
175         },
176         
177         onTouchMove: function(e) {
178                 if (e.targetTouches.length != 1) {
179                         return false;
180                 }
181
182             /* touchstart の preventDefault やめて ここに追加すると iPhone4 だといい感じ*/
183             if (navigator.appVersion.match(/iPhone OS 4/)) {
184                 e.preventDefault();
185             }
186                 var leftDelta = this.scrollX === true ? e.touches[0].pageX - this.touchStartX : 0,
187                         topDelta = this.scrollY === true ? e.touches[0].pageY - this.touchStartY : 0,
188                         newX = this.x + leftDelta,
189                         newY = this.y + topDelta;
190
191             if (Math.abs(topDelta) > this.options.min_move || Math.abs(leftDelta) > this.options.min_move) {
192                 // Slow down if outside of the boundaries
193                 if (newX > 0 || newX < this.maxScrollX) { 
194                         newX = this.options.bounce ? Math.round(this.x + leftDelta / 4) : this.x;
195                 }
196                 if (newY > 0 || newY < this.maxScrollY) { 
197                         newY = this.options.bounce ? Math.round(this.y + topDelta / 4) : this.y;
198                 }
199
200                 if (this.scrollBarX && !this.scrollBarX.visible) {
201                         this.scrollBarX.show();
202                 }
203                 if (this.scrollBarY && !this.scrollBarY.visible) {
204                         this.scrollBarY.show();
205                 }
206
207                 this.setPosition(newX, newY);
208
209                 this.touchStartX = e.touches[0].pageX;
210                 this.touchStartY = e.touches[0].pageY;
211                 this.moved = true;
212
213                 // Prevent slingshot effect
214                 if( e.timeStamp-this.scrollStartTime > 250 ) {
215                         this.scrollStartX = this.x;
216                         this.scrollStartY = this.y;
217                         this.scrollStartTime = e.timeStamp;
218                 }
219             }
220         },
221         
222         onTouchEnd: function(e) {
223                 if (e.targetTouches.length > 0) {
224                         return false;
225                 }
226
227                 if (!this.moved) {
228                     /* iOS4 では touchStart イベントは preventDefault() してないので
229                        click イベントを発生させちゃうと 2重にクリックが発生したような
230                        状態になるので、スルーでいい */
231                     if (!navigator.appVersion.match(/iPhone OS 4/)) {
232                         // Find the last touched element
233                         var theTarget = e.changedTouches[0].target;
234                         if (theTarget.nodeType == 3) {
235                                 theTarget = theTarget.parentNode;
236                         }
237                         // Create the fake event
238                         var theEvent = document.createEvent('MouseEvents');
239                         theEvent.initMouseEvent("click", true, true, document.defaultView,
240                                                                         e.detail, e.screenX, e.screenY, e.clientX, e.clientY,
241                                                                         e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
242                                                                         e.button, e.relatedTarget);
243                         theTarget.dispatchEvent(theEvent);
244                         return false;
245                     }
246                     return true;
247                 }
248                 
249                 var time = e.timeStamp - this.scrollStartTime;
250
251                 var momentumX = this.scrollX === true
252                         ? this.momentum(this.x - this.scrollStartX,
253                                                         time,
254                                                         this.options.bounce ? -this.x + this.scrollWidth/4 : -this.x,
255                                                         this.options.bounce ? this.x + this.element.offsetWidth - this.scrollWidth + this.scrollWidth/4 : this.x + this.element.offsetWidth - this.scrollWidth)
256                         : { dist: 0, time: 0 };
257
258                 var momentumY = this.scrollY === true
259                         ? this.momentum(this.y - this.scrollStartY,
260                                                         time,
261                                                         this.options.bounce ? -this.y + this.scrollHeight/4 : -this.y,
262                                                         this.options.bounce ? this.y + this.element.offsetHeight - this.scrollHeight + this.scrollHeight/4 : this.y + this.element.offsetHeight - this.scrollHeight)
263                         : { dist: 0, time: 0 };
264
265                 if (!momentumX.dist && !momentumY.dist) {
266                         this.resetPosition();
267                         return false;
268                 }
269
270                 var newDuration = Math.max(Math.max(momentumX.time, momentumY.time), 1);                // The minimum animation length must be 1ms
271                 var newPositionX = this.x + momentumX.dist;
272                 var newPositionY = this.y + momentumY.dist;
273
274                 this.element.addEventListener('webkitTransitionEnd', this);
275
276                 this.scrollTo(newPositionX, newPositionY, newDuration + 'ms');
277
278                 // Move the scrollbars
279                 if (this.scrollBarX) {
280                         this.scrollBarX.scrollTo(this.scrollBarX.maxScroll / this.maxScrollX * newPositionX, newDuration + 'ms');       
281                 }               
282                 if (this.scrollBarY) {
283                         this.scrollBarY.scrollTo(this.scrollBarY.maxScroll / this.maxScrollY * newPositionY, newDuration + 'ms');
284                 }
285         },
286         
287         onTransitionEnd: function () {
288                 this.element.removeEventListener('webkitTransitionEnd', this);
289                 this.resetPosition();
290         },
291
292         resetPosition: function () {
293                 var resetX = this.x,
294                         resetY = this.y;
295                 
296                 if (this.x >= 0) {
297                         resetX = 0;
298                 } else if (this.x < this.maxScrollX) {
299                         resetX = this.maxScrollX;
300                 }
301
302                 if (this.y >= 0) {
303                         resetY = 0;
304                 } else if (this.y < this.maxScrollY) {
305                         resetY = this.maxScrollY;
306                 }
307
308                 if (resetX != this.x || resetY != this.y) {
309                         this.scrollTo(resetX, resetY, '500ms');
310
311                         if (this.scrollBarX && resetX != this.x) {
312                                 this.scrollBarX.scrollTo(this.scrollBarX.maxScroll / this.maxScrollX * resetX, '500ms');
313                         }                       
314                         if (this.scrollBarY && resetY != this.y) {
315                                 this.scrollBarY.scrollTo(this.scrollBarY.maxScroll / this.maxScrollY * resetY, '500ms');
316                         }
317                 }
318                 
319                 // Hide the scrollbars
320                 if (this.scrollBarX) {
321                         this.scrollBarX.hide();
322                 }
323                 if (this.scrollBarY) {
324                         this.scrollBarY.hide();
325                 }
326         },
327
328         scrollTo: function (destX, destY, runtime) {
329                 this.element.style.webkitTransitionDuration = runtime || '400ms';
330                 this.setPosition(destX, destY);
331         },
332
333         momentum: function (dist, time, maxDistUpper, maxDistLower) {
334                 var friction = 0.1,
335                         deceleration = 1.5,
336                         speed = Math.abs(dist) / time * 1000,
337                         newDist = speed * speed / (20 * friction) / 1000;
338
339                 // Proportinally reduce speed if we are outside of the boundaries 
340                 if (dist > 0 && newDist > maxDistUpper) {
341                         speed = speed * maxDistUpper / newDist;
342                         newDist = maxDistUpper;
343                 }
344                 if (dist < 0 && newDist > maxDistLower) {
345                         speed = speed * maxDistLower / newDist;
346                         newDist = maxDistLower;
347                 }
348                 
349                 newDist = newDist * (dist < 0 ? -1 : 1);
350                 
351                 var newTime = speed / deceleration;
352
353                 return { dist: Math.round(newDist), time: Math.round(newTime) };
354         }
355 };
356
357 var scrollbar = function (dir, wrapper) {
358         this.dir = dir;
359         this.bar = document.createElement('div');
360         this.bar.className = 'scrollbar ' + dir;
361         this.bar.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
362         this.bar.style.webkitTransform = 'translate3d(0,0,0)';
363         this.bar.style.webkitTransitionProperty = '-webkit-transform,opacity';
364         this.bar.style.webkitTransitionDuration = '0,300ms';
365         this.bar.style.pointerEvents = 'none';
366         this.bar.style.opacity = '0';
367
368         wrapper.appendChild(this.bar);
369 }
370
371 scrollbar.prototype = {
372         size: 0,
373         maxSize: 0,
374         maxScroll: 0,
375         visible: false,
376         
377         init: function (scroll, size) {
378                 var offset = this.dir == 'horizontal' ? this.bar.offsetWidth - this.bar.clientWidth : this.bar.offsetHeight - this.bar.clientHeight;
379                 this.maxSize = scroll - 8;              // 8 = distance from top + distance from bottom
380                 this.size = Math.round(this.maxSize * this.maxSize / size) + offset;
381                 this.maxScroll = this.maxSize - this.size;
382                 this.bar.style[this.dir == 'horizontal' ? 'width' : 'height'] = (this.size - offset) + 'px';
383         },
384         
385         setPosition: function (pos) {
386                 if (pos < 0) {
387                         pos = 0;
388                 } else if (pos > this.maxScroll) {
389                         pos = this.maxScroll;
390                 }
391
392                 pos = this.dir == 'horizontal' ? 'translate3d(' + Math.round(pos) + 'px,0,0)' : 'translate3d(0,' + Math.round(pos) + 'px,0)';
393                 this.bar.style.webkitTransform = pos;
394         },
395         
396         scrollTo: function (pos, runtime) {
397                 this.bar.style.webkitTransitionDuration = (runtime || '400ms') + ',300ms';
398                 this.setPosition(pos);
399         },
400         
401         show: function () {
402                 this.visible = true;
403                 this.bar.style.opacity = '1';
404         },
405
406         hide: function () {
407                 this.visible = false;
408                 this.bar.style.opacity = '0';
409         },
410         
411         remove: function () {
412                 this.bar.parentNode.removeChild(this.bar);
413                 return null;
414         }
415 };
416
417 // Expose iScroll to the world
418 window.iScroll = iScroll;
419 })();