source: Dev/branches/rest-dojo-ui/client/dojox/mobile/scrollable.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 39.2 KB
Line 
1//>>includeStart("standaloneScrollable", kwArgs.standaloneScrollable);
2
3// The code block surrounded by the includeStart/End pragma is to simulate
4// several dojo APIs that are used in this module for non-dojo applications.
5// For dojo applications, this code block is removed by the build tool.
6
7if(typeof define === "undefined"){ // assumes dojo.js is not loaded
8        dojo = {doc:document, global:window};
9        dojox = {mobile:{}};
10
11        dojo.has = function(name){
12                var ua = navigator.userAgent;
13                if(name === "webkit"){
14                        return ua.indexOf("WebKit") != -1;
15                }
16                if(name === "android"){
17                        return parseFloat(ua.split("Android ")[1]) || undefined;
18                }
19                if(name === "iphone"){
20                        return ua.match(/(iPhone|iPod|iPad)/);
21                }
22                if(name === "ie"){
23                        return parseFloat(ua.split("MSIE ")[1]) || undefined;
24                }
25                if(name === "touch"){
26                        return (typeof dojo.doc.documentElement.ontouchstart != "undefined" &&
27                                navigator.appVersion.indexOf("Mobile") != -1) || !!dojo.has("android");
28                }
29        };
30
31        dojo.stopEvent = function(evt){
32                if(evt.preventDefault){
33                        evt.preventDefault();
34                        evt.stopPropagation();
35                }else{
36                        evt.cancelBubble = true;
37                }
38                return false;
39        };
40
41        dojo.setStyle = function(node, style, value){
42                if(typeof style === "string"){
43                        var obj = {};
44                        obj[style] = value;
45                        style = obj;
46                }
47                for(var s in style){
48                        if(style.hasOwnProperty(s)){
49                                node.style[s] = style[s];
50                                if(s === "opacity" && typeof(node.style.filter) !== "undefined"){
51                                        node.style.filter = " progid:DXImageTransform.Microsoft.alpha(opacity="+ (style[s]*100) +")";
52                                }
53                        }
54                }
55        };
56
57        dojo.create = function(tag, attrs, refNode){
58                return refNode.appendChild(dojo.doc.createElement(tag));
59        };
60
61        dojo.hasClass = function(node, s){
62                return (node.className.indexOf(s) != -1);
63        };
64        dojo.addClass = function(node, s){
65                if(!dojo.hasClass(node, s)){
66                        node.className += " " + s;
67                }
68        };
69        dojo.removeClass = function(node, s){
70                node.className = node.className.replace(" " + s, "");
71        };
72
73        dojo.connect = function(node, eventName, scope, method){
74                var handler = function(e){
75                        e = e || dojo.global.event;
76                        if(!e.target){
77                                e.target = e.srcElement;
78                                e.pageX = e.offsetX;
79                                e.pageY = e.offsetY;
80                        }
81                        scope[method](e);
82                };
83                if(node.addEventListener){
84                        node.addEventListener(eventName.replace(/^on/,""), handler, false);
85                }else{
86                        node.attachEvent(eventName, handler);
87                }
88                return {node:node, eventName:eventName, handler:handler};
89        };
90        dojo.disconnect = function(handle){
91                if(handle.node.removeEventListener){
92                        handle.node.removeEventListener(handle.eventName.replace(/^on/,""), handle.handler, false);
93                }else{
94                        handle.node.detachEvent(handle.eventName, handle.handler);
95                }
96        };
97
98        define = function(module, deps, def){
99                var _def = (arguments.length === 2) ? arguments[1] : arguments[2];
100                _def(
101                        dojo, // dojo
102                        { // connect
103                                connect: dojo.connect,
104                                disconnect: dojo.disconnect
105                        },
106                        { // event
107                                stop: dojo.stopEvent
108                        },
109                        { // lang
110                                getObject: function(){}
111                        },
112                        dojo, // win
113                        { // domClass
114                                contains: dojo.hasClass,
115                                add: dojo.addClass,
116                                remove: dojo.removeClass
117                        },
118                        { // domConstruct
119                                create: dojo.create
120                        },
121                        { // domStyle
122                                set: dojo.setStyle
123                        },
124                        dojo.has // has
125                );
126        };
127}
128//>>includeEnd("standaloneScrollable");
129
130
131define([
132        "dojo/_base/kernel",
133        "dojo/_base/connect",
134        "dojo/_base/event",
135        "dojo/_base/lang",
136        "dojo/_base/window",
137        "dojo/dom-class",
138        "dojo/dom-construct",
139        "dojo/dom-style",
140        "./sniff"
141], function(dojo, connect, event, lang, win, domClass, domConstruct, domStyle, has){
142
143        var dm = lang.getObject("dojox.mobile", true);
144
145/*=====
146// summary:
147//              Utility for enabling touch scrolling capability.
148// description:
149//              Mobile WebKit browsers do not allow scrolling inner DIVs. (You need
150//              the two-finger operation to scroll them.)
151//              That means you cannot have fixed-positioned header/footer bars.
152//              To solve this issue, this module disables the browsers default scrolling
153//              behavior, and re-builds its own scrolling machinery by handling touch
154//              events. In this module, this.domNode has height "100%" and is fixed to
155//              the window, and this.containerNode scrolls. If you place a bar outside
156//              of this.containerNode, then it will be fixed-positioned while
157//              this.containerNode is scrollable.
158//
159//              This module has the following features:
160//              - Scrolls inner DIVs vertically, horizontally, or both.
161//              - Vertical and horizontal scroll bars.
162//              - Flashes the scroll bars when a view is shown.
163//              - Simulates the flick operation using animation.
164//              - Respects header/footer bars if any.
165//
166//              dojox.mobile.scrollable is a simple function object, which holds
167//              several properties and functions in it. But if you transform it to a
168//              dojo class, it can be used as a mix-in class for any custom dojo
169//              widgets. dojox.mobile._ScrollableMixin is such a class.
170//
171//              Also, it can be used even for non-dojo applications. In such cases,
172//              several dojo APIs used in this module, such as dojo.connect,
173//              dojo.create, etc., are re-defined so that the code works without dojo.
174//              When in dojo, of course those re-defined functions are not necessary.
175//              So, they are surrounded by the includeStart and includeEnd directives
176//              so that they can be excluded from the build.
177//
178//              If you use this module for non-dojo application, you need to explicitly
179//              assign your outer fixed node and inner scrollable node to this.domNode
180//              and this.containerNode respectively.
181//
182//              Non-dojo application should capture the onorientationchange or
183//              the onresize event and call resize() in the event handler.
184//
185// example:
186//              Use this module from a non-dojo applicatoin:
187//              | function onLoad(){
188//              |       var scrollable = new dojox.mobile.scrollable(dojo, dojox);
189//              |       scrollable.init({
190//              |               domNode: "outer", // id or node
191//              |               containerNode: "inner" // id or node
192//              |       });
193//              | }
194//              | <body onload="onLoad()">
195//              |       <h1 id="hd1" style="position:relative;width:100%;z-index:1;">
196//              |               Fixed Header
197//              |       </h1>
198//              |       <div id="outer" style="position:relative;height:100%;overflow:hidden;">
199//              |               <div id="inner" style="position:absolute;width:100%;">
200//              |                       ... content ...
201//              |               </div>
202//              |       </div>
203//              | </body>
204=====*/
205
206var scrollable = function(/*Object?*/dojo, /*Object?*/dojox){
207        this.fixedHeaderHeight = 0; // height of a fixed header
208        this.fixedFooterHeight = 0; // height of a fixed footer
209        this.isLocalFooter = false; // footer is view-local (as opposed to application-wide)
210        this.scrollBar = true; // show scroll bar or not
211        this.scrollDir = "v"; // v: vertical, h: horizontal, vh: both, f: flip
212        this.weight = 0.6; // frictional drag
213        this.fadeScrollBar = true;
214        this.disableFlashScrollBar = false;
215        this.threshold = 4; // drag threshold value in pixels
216        this.constraint = true; // bounce back to the content area
217        this.touchNode = null; // a node that will have touch event handlers
218        this.isNested = false; // this scrollable's parent is also a scrollable
219        this.dirLock = false; // disable the move handler if scroll starts in the unexpected direction
220        this.height = ""; // explicitly specified height of this widget (ex. "300px")
221        this.androidWorkaroud = true; // workaround input field jumping issue
222
223//>>includeStart("standaloneScrollable", kwArgs.standaloneScrollable);
224        if(!dojo){ // namespace objects are not passed
225                dojo = window.dojo;
226                dojox = window.dojox;
227        }
228//>>includeEnd("standaloneScrollable");
229
230        this.init = function(/*Object?*/params){
231                if(params){
232                        for(var p in params){
233                                if(params.hasOwnProperty(p)){
234                                        this[p] = ((p == "domNode" || p == "containerNode") && typeof params[p] == "string") ?
235                                                win.doc.getElementById(params[p]) : params[p]; // mix-in params
236                                }
237                        }
238                }
239                this.touchNode = this.touchNode || this.containerNode;
240                this._v = (this.scrollDir.indexOf("v") != -1); // vertical scrolling
241                this._h = (this.scrollDir.indexOf("h") != -1); // horizontal scrolling
242                this._f = (this.scrollDir == "f"); // flipping views
243
244                this._ch = []; // connect handlers
245                this._ch.push(connect.connect(this.touchNode,
246                        has('touch') ? "touchstart" : "onmousedown", this, "onTouchStart"));
247                if(has("webkit")){
248                        this._ch.push(connect.connect(this.domNode, "webkitAnimationEnd", this, "onFlickAnimationEnd"));
249                        this._ch.push(connect.connect(this.domNode, "webkitAnimationStart", this, "onFlickAnimationStart"));
250
251                        this._aw = this.androidWorkaroud &&
252                                has("android") >= 2.2 && has("android") < 3;
253                        if(this._aw){
254                                this._ch.push(connect.connect(win.global, "onresize", this, "onScreenSizeChanged"));
255                                this._ch.push(connect.connect(win.global, "onfocus", this, function(e){
256                                        if(this.containerNode.style.webkitTransform){
257                                                this.stopAnimation();
258                                                this.toTopLeft();
259                                        }
260                                }));
261                                this._sz = this.getScreenSize();
262                        }
263
264                        // Creation of keyframes takes a little time. If they are created
265                        // in a lazy manner, a slight delay is noticeable when you start
266                        // scrolling for the first time. This is to create keyframes up front.
267                        for(var i = 0; i < 3; i++){
268                                this.setKeyframes(null, null, i);
269                        }
270                }
271                // Workaround for iPhone flicker issue
272                if(has('iphone')){
273                        domStyle.set(this.containerNode, "webkitTransform", "translate3d(0,0,0)");
274                }
275               
276                this._speed = {x:0, y:0};
277                this._appFooterHeight = 0;
278                if(this.isTopLevel() && !this.noResize){
279                        this.resize();
280                }
281                var _this = this;
282                setTimeout(function(){
283                        _this.flashScrollBar();
284                }, 600);
285        };
286
287        this.isTopLevel = function(){
288                // subclass may want to override
289                return true;
290        };
291
292        this.cleanup = function(){
293                if(this._ch){
294                        for(var i = 0; i < this._ch.length; i++){
295                                connect.disconnect(this._ch[i]);
296                        }
297                        this._ch = null;
298                }
299        };
300
301        this.findDisp = function(/*DomNode*/node){
302                // summary:
303                //              Finds the currently displayed view node from my sibling nodes.
304                if(!node.parentNode){ return null; }
305                var nodes = node.parentNode.childNodes;
306                for(var i = 0; i < nodes.length; i++){
307                        var n = nodes[i];
308                        if(n.nodeType === 1 && domClass.contains(n, "mblView") && n.style.display !== "none"){
309                                return n;
310                        }
311                }
312                return node;
313        };
314
315        this.getScreenSize = function(){
316                // summary:
317                //              Returns the dimensions of the browser window.
318                return {
319                        h: win.global.innerHeight||win.doc.documentElement.clientHeight||win.doc.documentElement.offsetHeight,
320                        w: win.global.innerWidth||win.doc.documentElement.clientWidth||win.doc.documentElement.offsetWidth
321                };
322        };
323
324        this.isKeyboardShown = function(e){
325                // summary:
326                //              Internal function for android workaround.
327                // description:
328                //              Returns true if a virtual keyboard is shown.
329                //              Indirectly detects whether a virtual keyboard is shown or not by
330                //              examining the screen size.
331                // TODO: need more reliable detection logic
332                if(!this._sz){ return false; }
333                var sz = this.getScreenSize();
334                return (sz.w * sz.h) / (this._sz.w * this._sz.h) < 0.8;
335        };
336
337        this.disableScroll = function(/*Boolean*/v){
338                // summary:
339                //              Internal function for android workaround.
340                // description:
341                //              Disables the touch scrolling and enables the browser's default
342                //              scrolling.
343                if(this.disableTouchScroll === v || this.domNode.style.display === "none"){ return; }
344                this.disableTouchScroll = v;
345                this.scrollBar = !v;
346                dm.disableHideAddressBar = dm.disableResizeAll = v;
347                var of = v ? "visible" : "hidden";
348                domStyle.set(this.domNode, "overflow", of);
349                domStyle.set(win.doc.documentElement, "overflow", of);
350                domStyle.set(win.body(), "overflow", of);
351                var c = this.containerNode;
352                if(v){
353                        if(!c.style.webkitTransform){
354                                // stop animation when soft keyborad is shown before animation ends.
355                                // TODO: there might be a better way to wait for animation ending.
356                                this.stopAnimation();
357                                this.toTopLeft();
358                        }
359                        var mt = parseInt(c.style.marginTop) || 0;
360                        var h = c.offsetHeight + mt + this.fixedFooterHeight - this._appFooterHeight;
361                        domStyle.set(this.domNode, "height", h + "px");
362                       
363                        this._cPos = { // store containerNode's position
364                                x: parseInt(c.style.left) || 0,
365                                y: parseInt(c.style.top) || 0
366                        };
367                        domStyle.set(c, {
368                                top: "0px",
369                                left: "0px"
370                        });
371                       
372                        var a = win.doc.activeElement; // focused input field
373                        if(a){ // scrolling to show focused input field
374                                var at = 0; // top position of focused input field
375                                for(var n = a; n.tagName != "BODY"; n = n.offsetParent){
376                                        at += n.offsetTop;
377                                }
378                                var st = at + a.clientHeight + 10 - this.getScreenSize().h; // top postion of browser scroll bar
379                                if(st > 0){
380                                        win.body().scrollTop = st;
381                                }
382                        }       
383                }else{
384                        if(this._cPos){ // restore containerNode's position
385                                domStyle.set(c, {
386                                        top: this._cPos.y + "px",
387                                        left: this._cPos.x + "px"
388                                });
389                                this._cPos = null;
390                        }
391                        var tags = this.domNode.getElementsByTagName("*");
392                        for(var i = 0; i < tags.length; i++){
393                                tags[i].blur && tags[i].blur();
394                        }
395                        // Call dojox.mobile.resizeAll if exists.
396                        dm.resizeAll && dm.resizeAll();
397                }
398        };
399
400        this.onScreenSizeChanged = function(e){
401                // summary:
402                //              Internal function for android workaround.
403                var sz = this.getScreenSize();
404                if(sz.w * sz.h > this._sz.w * this._sz.h){
405                        this._sz = sz; // update the screen size
406                }
407                this.disableScroll(this.isKeyboardShown());
408        };
409
410        this.toTransform = function(e){
411                // summary:
412                //              Internal function for android workaround.
413                var c = this.containerNode;
414                if(c.offsetTop === 0 && c.offsetLeft === 0 || !c._webkitTransform){ return; }
415                domStyle.set(c, {
416                        webkitTransform: c._webkitTransform,
417                        top: "0px",
418                        left: "0px"
419                });
420                c._webkitTransform = null;
421        };
422
423        this.toTopLeft = function(){
424                // summary:
425                //              Internal function for android workaround.
426                var c = this.containerNode;
427                if(!c.style.webkitTransform){ return; } // already converted to top/left
428                c._webkitTransform = c.style.webkitTransform;
429                var pos = this.getPos();
430                domStyle.set(c, {
431                        webkitTransform: "",
432                        top: pos.y + "px",
433                        left: pos.x + "px"
434                });
435        };
436       
437        this.resize = function(e){
438                // summary:
439                //              Adjusts the height of the widget.
440                // description:
441                //              If the height property is 'inherit', the height is inherited
442                //              from its offset parent. If 'auto', the content height, which
443                //              could be smaller than the entire screen height, is used. If an
444                //              explicit height value (ex. "300px"), it is used as the new
445                //              height. If nothing is specified as the height property, from the
446                //              current top position of the widget to the bottom of the screen
447                //              will be the new height.
448
449                // moved from init() to support dynamically added fixed bars
450                this._appFooterHeight = (this.fixedFooterHeight && !this.isLocalFooter) ?
451                        this.fixedFooterHeight : 0;
452                if(this.isLocalHeader){
453                        this.containerNode.style.marginTop = this.fixedHeaderHeight + "px";
454                }
455
456                // Get the top position. Same as dojo.position(node, true).y
457                var top = 0;
458                for(var n = this.domNode; n && n.tagName != "BODY"; n = n.offsetParent){
459                        n = this.findDisp(n); // find the first displayed view node
460                        if(!n){ break; }
461                        top += n.offsetTop;
462                }
463
464                // adjust the height of this view
465                var     h,
466                        screenHeight = this.getScreenSize().h,
467                        dh = screenHeight - top - this._appFooterHeight; // default height
468                if(this.height === "inherit"){
469                        if(this.domNode.offsetParent){
470                                h = this.domNode.offsetParent.offsetHeight + "px";
471                        }
472                }else if(this.height === "auto"){
473                        var parent = this.domNode.offsetParent;
474                        if(parent){
475                                this.domNode.style.height = "0px";
476                                var     parentRect = parent.getBoundingClientRect(),
477                                        scrollableRect = this.domNode.getBoundingClientRect(),
478                                        contentBottom = parentRect.bottom - this._appFooterHeight;
479                                if(scrollableRect.bottom >= contentBottom){ // use entire screen
480                                        dh = screenHeight - (scrollableRect.top - parentRect.top) - this._appFooterHeight;
481                                }else{ // stretch to fill predefined area
482                                        dh = contentBottom - scrollableRect.bottom;
483                                }
484                        }
485                        // content could be smaller than entire screen height
486                        var contentHeight = Math.max(this.domNode.scrollHeight, this.containerNode.scrollHeight);
487                        h = (contentHeight ? Math.min(contentHeight, dh) : dh) + "px";
488                }else if(this.height){
489                        h = this.height;
490                }
491                if(!h){
492                        h = dh + "px";
493                }
494                if(h.charAt(0) !== "-" && // to ensure that h is not negative (e.g. "-10px")
495                        h !== "default"){
496                        this.domNode.style.height = h;
497                }
498
499                // to ensure that the view is within a scrolling area when resized.
500                this.onTouchEnd();
501        };
502
503        this.onFlickAnimationStart = function(e){
504                event.stop(e);
505        };
506
507        this.onFlickAnimationEnd = function(e){
508                var an = e && e.animationName;
509                if(an && an.indexOf("scrollableViewScroll2") === -1){
510                        if(an.indexOf("scrollableViewScroll0") !== -1){ // scrollBarV
511                                domClass.remove(this._scrollBarNodeV, "mblScrollableScrollTo0");
512                        }else if(an.indexOf("scrollableViewScroll1") !== -1){ // scrollBarH
513                                domClass.remove(this._scrollBarNodeH, "mblScrollableScrollTo1");
514                        }else{ // fade or others
515                                if(this._scrollBarNodeV){ this._scrollBarNodeV.className = ""; }
516                                if(this._scrollBarNodeH){ this._scrollBarNodeH.className = ""; }
517                        }
518                        return;
519                }
520                if(e && e.srcElement){
521                        event.stop(e);
522                }
523                this.stopAnimation();
524                if(this._bounce){
525                        var _this = this;
526                        var bounce = _this._bounce;
527                        setTimeout(function(){
528                                _this.slideTo(bounce, 0.3, "ease-out");
529                        }, 0);
530                        _this._bounce = undefined;
531                }else{
532                        this.hideScrollBar();
533                        this.removeCover();
534                        if(this._aw){ this.toTopLeft(); } // android workaround
535                }
536        };
537
538        this.isFormElement = function(node){
539                if(node && node.nodeType !== 1){ node = node.parentNode; }
540                if(!node || node.nodeType !== 1){ return false; }
541                var t = node.tagName;
542                return (t === "SELECT" || t === "INPUT" || t === "TEXTAREA" || t === "BUTTON");
543        };
544
545        this.onTouchStart = function(e){
546                if(this.disableTouchScroll){ return; }
547                if(this._conn && (new Date()).getTime() - this.startTime < 500){
548                        return; // ignore successive onTouchStart calls
549                }
550                if(!this._conn){
551                        this._conn = [];
552                        this._conn.push(connect.connect(win.doc, has('touch') ? "touchmove" : "onmousemove", this, "onTouchMove"));
553                        this._conn.push(connect.connect(win.doc, has('touch') ? "touchend" : "onmouseup", this, "onTouchEnd"));
554                }
555
556                this._aborted = false;
557                if(domClass.contains(this.containerNode, "mblScrollableScrollTo2")){
558                        this.abort();
559                }else{ // reset scrollbar class especially for reseting fade-out animation
560                        if(this._scrollBarNodeV){ this._scrollBarNodeV.className = ""; }
561                        if(this._scrollBarNodeH){ this._scrollBarNodeH.className = ""; }
562                }
563                if(this._aw){ this.toTransform(e); } // android workaround
564                this.touchStartX = e.touches ? e.touches[0].pageX : e.clientX;
565                this.touchStartY = e.touches ? e.touches[0].pageY : e.clientY;
566                this.startTime = (new Date()).getTime();
567                this.startPos = this.getPos();
568                this._dim = this.getDim();
569                this._time = [0];
570                this._posX = [this.touchStartX];
571                this._posY = [this.touchStartY];
572                this._locked = false;
573
574                if(!this.isFormElement(e.target) && !this.isNested){
575                        event.stop(e);
576                }
577        };
578
579        this.onTouchMove = function(e){
580                if(this._locked){ return; }
581                var x = e.touches ? e.touches[0].pageX : e.clientX;
582                var y = e.touches ? e.touches[0].pageY : e.clientY;
583                var dx = x - this.touchStartX;
584                var dy = y - this.touchStartY;
585                var to = {x:this.startPos.x + dx, y:this.startPos.y + dy};
586                var dim = this._dim;
587
588                dx = Math.abs(dx);
589                dy = Math.abs(dy);
590                if(this._time.length == 1){ // the first TouchMove after TouchStart
591                        if(this.dirLock){
592                                if(this._v && !this._h && dx >= this.threshold && dx >= dy ||
593                                        (this._h || this._f) && !this._v && dy >= this.threshold && dy >= dx){
594                                        this._locked = true;
595                                        return;
596                                }
597                        }
598                        if(this._v && Math.abs(dy) < this.threshold ||
599                                (this._h || this._f) && Math.abs(dx) < this.threshold){
600                                return;
601                        }
602                        this.addCover();
603                        this.showScrollBar();
604                }
605
606                var weight = this.weight;
607                if(this._v && this.constraint){
608                        if(to.y > 0){ // content is below the screen area
609                                to.y = Math.round(to.y * weight);
610                        }else if(to.y < -dim.o.h){ // content is above the screen area
611                                if(dim.c.h < dim.d.h){ // content is shorter than display
612                                        to.y = Math.round(to.y * weight);
613                                }else{
614                                        to.y = -dim.o.h - Math.round((-dim.o.h - to.y) * weight);
615                                }
616                        }
617                }
618                if((this._h || this._f) && this.constraint){
619                        if(to.x > 0){
620                                to.x = Math.round(to.x * weight);
621                        }else if(to.x < -dim.o.w){
622                                if(dim.c.w < dim.d.w){
623                                        to.x = Math.round(to.x * weight);
624                                }else{
625                                        to.x = -dim.o.w - Math.round((-dim.o.w - to.x) * weight);
626                                }
627                        }
628                }
629                this.scrollTo(to);
630
631                var max = 10;
632                var n = this._time.length; // # of samples
633                if(n >= 2){
634                        // Check the direction of the finger move.
635                        // If the direction has been changed, discard the old data.
636                        var d0, d1;
637                        if(this._v && !this._h){
638                                d0 = this._posY[n - 1] - this._posY[n - 2];
639                                d1 = y - this._posY[n - 1];
640                        }else if(!this._v && this._h){
641                                d0 = this._posX[n - 1] - this._posX[n - 2];
642                                d1 = x - this._posX[n - 1];
643                        }
644                        if(d0 * d1 < 0){ // direction changed
645                                // leave only the latest data
646                                this._time = [this._time[n - 1]];
647                                this._posX = [this._posX[n - 1]];
648                                this._posY = [this._posY[n - 1]];
649                                n = 1;
650                        }
651                }
652                if(n == max){
653                        this._time.shift();
654                        this._posX.shift();
655                        this._posY.shift();
656                }
657                this._time.push((new Date()).getTime() - this.startTime);
658                this._posX.push(x);
659                this._posY.push(y);
660        };
661
662        this.onTouchEnd = function(e){
663                if(this._locked){ return; }
664                var speed = this._speed = {x:0, y:0};
665                var dim = this._dim;
666                var pos = this.getPos();
667                var to = {}; // destination
668                if(e){
669                        if(!this._conn){ return; } // if we get onTouchEnd without onTouchStart, ignore it.
670                        for(var i = 0; i < this._conn.length; i++){
671                                connect.disconnect(this._conn[i]);
672                        }
673                        this._conn = null;
674       
675                        var n = this._time.length; // # of samples
676                        var clicked = false;
677                        if(!this._aborted){
678                                if(n <= 1){
679                                        clicked = true;
680                                }else if(n == 2 && Math.abs(this._posY[1] - this._posY[0]) < 4
681                                        && has('touch')){ // for desktop browsers, posY could be the same, since we're using clientY, see onTouchMove()
682                                        clicked = true;
683                                }
684                        }
685                        var isFormElem = this.isFormElement(e.target);
686                        if(clicked && !isFormElem){ // clicked, not dragged or flicked
687                                this.hideScrollBar();
688                                this.removeCover();
689                                if(has('touch')){
690                                        var elem = e.target;
691                                        if(elem.nodeType != 1){
692                                                elem = elem.parentNode;
693                                        }
694                                        var ev = win.doc.createEvent("MouseEvents");
695                                        ev.initMouseEvent("click", true, true, win.global, 1, e.screenX, e.screenY, e.clientX, e.clientY);
696                                        setTimeout(function(){
697                                                elem.dispatchEvent(ev);
698                                        }, 0);
699                                }
700                                return;
701                        }else if(this._aw && clicked && isFormElem){ // clicked input fields
702                                this.hideScrollBar();
703                                this.toTopLeft();
704                                return;
705                        }
706                        speed = this._speed = this.getSpeed();
707                }else{
708                        if(pos.x == 0 && pos.y == 0){ return; } // initializing
709                        dim = this.getDim();
710                }
711
712                if(this._v){
713                        to.y = pos.y + speed.y;
714                }
715                if(this._h || this._f){
716                        to.x = pos.x + speed.x;
717                }
718
719                this.adjustDestination(to, pos);
720
721                if(this.scrollDir == "v" && dim.c.h < dim.d.h){ // content is shorter than display
722                        this.slideTo({y:0}, 0.3, "ease-out"); // go back to the top
723                        return;
724                }else if(this.scrollDir == "h" && dim.c.w < dim.d.w){ // content is narrower than display
725                        this.slideTo({x:0}, 0.3, "ease-out"); // go back to the left
726                        return;
727                }else if(this._v && this._h && dim.c.h < dim.d.h && dim.c.w < dim.d.w){
728                        this.slideTo({x:0, y:0}, 0.3, "ease-out"); // go back to the top-left
729                        return;
730                }
731
732                var duration, easing = "ease-out";
733                var bounce = {};
734                if(this._v && this.constraint){
735                        if(to.y > 0){ // going down. bounce back to the top.
736                                if(pos.y > 0){ // started from below the screen area. return quickly.
737                                        duration = 0.3;
738                                        to.y = 0;
739                                }else{
740                                        to.y = Math.min(to.y, 20);
741                                        easing = "linear";
742                                        bounce.y = 0;
743                                }
744                        }else if(-speed.y > dim.o.h - (-pos.y)){ // going up. bounce back to the bottom.
745                                if(pos.y < -dim.o.h){ // started from above the screen top. return quickly.
746                                        duration = 0.3;
747                                        to.y = dim.c.h <= dim.d.h ? 0 : -dim.o.h; // if shorter, move to 0
748                                }else{
749                                        to.y = Math.max(to.y, -dim.o.h - 20);
750                                        easing = "linear";
751                                        bounce.y = -dim.o.h;
752                                }
753                        }
754                }
755                if((this._h || this._f) && this.constraint){
756                        if(to.x > 0){ // going right. bounce back to the left.
757                                if(pos.x > 0){ // started from right of the screen area. return quickly.
758                                        duration = 0.3;
759                                        to.x = 0;
760                                }else{
761                                        to.x = Math.min(to.x, 20);
762                                        easing = "linear";
763                                        bounce.x = 0;
764                                }
765                        }else if(-speed.x > dim.o.w - (-pos.x)){ // going left. bounce back to the right.
766                                if(pos.x < -dim.o.w){ // started from left of the screen top. return quickly.
767                                        duration = 0.3;
768                                        to.x = dim.c.w <= dim.d.w ? 0 : -dim.o.w; // if narrower, move to 0
769                                }else{
770                                        to.x = Math.max(to.x, -dim.o.w - 20);
771                                        easing = "linear";
772                                        bounce.x = -dim.o.w;
773                                }
774                        }
775                }
776                this._bounce = (bounce.x !== undefined || bounce.y !== undefined) ? bounce : undefined;
777
778                if(duration === undefined){
779                        var distance, velocity;
780                        if(this._v && this._h){
781                                velocity = Math.sqrt(speed.x+speed.x + speed.y*speed.y);
782                                distance = Math.sqrt(Math.pow(to.y - pos.y, 2) + Math.pow(to.x - pos.x, 2));
783                        }else if(this._v){
784                                velocity = speed.y;
785                                distance = to.y - pos.y;
786                        }else if(this._h){
787                                velocity = speed.x;
788                                distance = to.x - pos.x;
789                        }
790                        if(distance === 0 && !e){ return; } // #13154
791                        duration = velocity !== 0 ? Math.abs(distance / velocity) : 0.01; // time = distance / velocity
792                }
793                this.slideTo(to, duration, easing);
794        };
795
796        this.adjustDestination = function(to, pos){
797                // subclass may want to implement
798        };
799
800        this.abort = function(){
801                this.scrollTo(this.getPos());
802                this.stopAnimation();
803                this._aborted = true;
804        };
805
806        this.stopAnimation = function(){
807                // stop the currently running animation
808                domClass.remove(this.containerNode, "mblScrollableScrollTo2");
809                if(has("android")){
810                        domStyle.set(this.containerNode, "webkitAnimationDuration", "0s"); // workaround for android screen flicker problem
811                }
812                if(this._scrollBarV){
813                        this._scrollBarV.className = "";
814                }
815                if(this._scrollBarH){
816                        this._scrollBarH.className = "";
817                }
818        };
819
820        this.getSpeed = function(){
821                var x = 0, y = 0, n = this._time.length;
822                // if the user holds the mouse or finger more than 0.5 sec, do not move.
823                if(n >= 2 && (new Date()).getTime() - this.startTime - this._time[n - 1] < 500){
824                        var dy = this._posY[n - (n > 3 ? 2 : 1)] - this._posY[(n - 6) >= 0 ? n - 6 : 0];
825                        var dx = this._posX[n - (n > 3 ? 2 : 1)] - this._posX[(n - 6) >= 0 ? n - 6 : 0];
826                        var dt = this._time[n - (n > 3 ? 2 : 1)] - this._time[(n - 6) >= 0 ? n - 6 : 0];
827                        y = this.calcSpeed(dy, dt);
828                        x = this.calcSpeed(dx, dt);
829                }
830                return {x:x, y:y};
831        };
832
833        this.calcSpeed = function(/*Number*/d, /*Number*/t){
834                return Math.round(d / t * 100) * 4;
835        };
836
837        this.scrollTo = function(/*Object*/to, /*Boolean?*/doNotMoveScrollBar, /*DomNode?*/node){ // to: {x, y}
838                // summary:
839                //              Scrolls to the given position.
840                var s = (node || this.containerNode).style;
841                if(has("webkit")){
842                        s.webkitTransform = this.makeTranslateStr(to);
843                }else{
844                        if(this._v){
845                                s.top = to.y + "px";
846                        }
847                        if(this._h || this._f){
848                                s.left = to.x + "px";
849                        }
850                }
851                if(!doNotMoveScrollBar){
852                        this.scrollScrollBarTo(this.calcScrollBarPos(to));
853                }
854        };
855
856        this.slideTo = function(/*Object*/to, /*Number*/duration, /*String*/easing){
857                // summary:
858                //              Scrolls to the given position with slide animation.
859                this._runSlideAnimation(this.getPos(), to, duration, easing, this.containerNode, 2);
860                this.slideScrollBarTo(to, duration, easing);
861        };
862
863        this.makeTranslateStr = function(to){
864                var y = this._v && typeof to.y == "number" ? to.y+"px" : "0px";
865                var x = (this._h||this._f) && typeof to.x == "number" ? to.x+"px" : "0px";
866                return dm.hasTranslate3d ?
867                                "translate3d("+x+","+y+",0px)" : "translate("+x+","+y+")";
868        };
869
870        this.getPos = function(){
871                // summary:
872                //              Get the top position in the midst of animation
873                if(has("webkit")){
874                        var m = win.doc.defaultView.getComputedStyle(this.containerNode, '')["-webkit-transform"];
875                        if(m && m.indexOf("matrix") === 0){
876                                var arr = m.split(/[,\s\)]+/);
877                                return {y:arr[5] - 0, x:arr[4] - 0};
878                        }
879                        return {x:0, y:0};
880                }else{
881                        // this.containerNode.offsetTop does not work here,
882                        // because it adds the height of the top margin.
883                        var y = parseInt(this.containerNode.style.top) || 0;
884                        return {y:y, x:this.containerNode.offsetLeft};
885                }
886        };
887
888        this.getDim = function(){
889                var d = {};
890                // content width/height
891                d.c = {h:this.containerNode.offsetHeight, w:this.containerNode.offsetWidth};
892
893                // view width/height
894                d.v = {h:this.domNode.offsetHeight + this._appFooterHeight, w:this.domNode.offsetWidth};
895
896                // display width/height
897                d.d = {h:d.v.h - this.fixedHeaderHeight - this.fixedFooterHeight, w:d.v.w};
898
899                // overflowed width/height
900                d.o = {h:d.c.h - d.v.h + this.fixedHeaderHeight + this.fixedFooterHeight, w:d.c.w - d.v.w};
901                return d;
902        };
903
904        this.showScrollBar = function(){
905                if(!this.scrollBar){ return; }
906
907                var dim = this._dim;
908                if(this.scrollDir == "v" && dim.c.h <= dim.d.h){ return; }
909                if(this.scrollDir == "h" && dim.c.w <= dim.d.w){ return; }
910                if(this._v && this._h && dim.c.h <= dim.d.h && dim.c.w <= dim.d.w){ return; }
911
912                var createBar = function(self, dir){
913                        var bar = self["_scrollBarNode" + dir];
914                        if(!bar){
915                                var wrapper = domConstruct.create("div", null, self.domNode);
916                                var props = { position: "absolute", overflow: "hidden" };
917                                if(dir == "V"){
918                                        props.right = "2px";
919                                        props.width = "5px";
920                                }else{
921                                        props.bottom = (self.isLocalFooter ? self.fixedFooterHeight : 0) + 2 + "px";
922                                        props.height = "5px";
923                                }
924                                domStyle.set(wrapper, props);
925                                wrapper.className = "mblScrollBarWrapper";
926                                self["_scrollBarWrapper"+dir] = wrapper;
927
928                                bar = domConstruct.create("div", null, wrapper);
929                                domStyle.set(bar, {
930                                        opacity: 0.6,
931                                        position: "absolute",
932                                        backgroundColor: "#606060",
933                                        fontSize: "1px",
934                                        webkitBorderRadius: "2px",
935                                        MozBorderRadius: "2px",
936                                        webkitTransformOrigin: "0 0",
937                                        zIndex: 2147483647 // max of signed 32-bit integer
938                                });
939                                domStyle.set(bar, dir == "V" ? {width: "5px"} : {height: "5px"});
940                                self["_scrollBarNode" + dir] = bar;
941                        }
942                        return bar;
943                };
944                if(this._v && !this._scrollBarV){
945                        this._scrollBarV = createBar(this, "V");
946                }
947                if(this._h && !this._scrollBarH){
948                        this._scrollBarH = createBar(this, "H");
949                }
950                this.resetScrollBar();
951        };
952
953        this.hideScrollBar = function(){
954                var fadeRule;
955                if(this.fadeScrollBar && has("webkit")){
956                        if(!dm._fadeRule){
957                                var node = domConstruct.create("style", null, win.doc.getElementsByTagName("head")[0]);
958                                node.textContent =
959                                        ".mblScrollableFadeScrollBar{"+
960                                        "  -webkit-animation-duration: 1s;"+
961                                        "  -webkit-animation-name: scrollableViewFadeScrollBar;}"+
962                                        "@-webkit-keyframes scrollableViewFadeScrollBar{"+
963                                        "  from { opacity: 0.6; }"+
964                                        "  to { opacity: 0; }}";
965                                dm._fadeRule = node.sheet.cssRules[1];
966                        }
967                        fadeRule = dm._fadeRule;
968                }
969                if(!this.scrollBar){ return; }
970                var f = function(bar, self){
971                        domStyle.set(bar, {
972                                opacity: 0,
973                                webkitAnimationDuration: ""
974                        });
975                        if(self._aw){ // android workaround
976                                bar.style.webkitTransform = "";
977                        }else{
978                                bar.className = "mblScrollableFadeScrollBar";
979                        }
980                };
981                if(this._scrollBarV){
982                        f(this._scrollBarV, this);
983                        this._scrollBarV = null;
984                }
985                if(this._scrollBarH){
986                        f(this._scrollBarH, this);
987                        this._scrollBarH = null;
988                }
989        };
990
991        this.calcScrollBarPos = function(/*Object*/to){ // to: {x, y}
992                var pos = {};
993                var dim = this._dim;
994                var f = function(wrapperH, barH, t, d, c){
995                        var y = Math.round((d - barH - 8) / (d - c) * t);
996                        if(y < -barH + 5){
997                                y = -barH + 5;
998                        }
999                        if(y > wrapperH - 5){
1000                                y = wrapperH - 5;
1001                        }
1002                        return y;
1003                };
1004                if(typeof to.y == "number" && this._scrollBarV){
1005                        pos.y = f(this._scrollBarWrapperV.offsetHeight, this._scrollBarV.offsetHeight, to.y, dim.d.h, dim.c.h);
1006                }
1007                if(typeof to.x == "number" && this._scrollBarH){
1008                        pos.x = f(this._scrollBarWrapperH.offsetWidth, this._scrollBarH.offsetWidth, to.x, dim.d.w, dim.c.w);
1009                }
1010                return pos;
1011        };
1012
1013        this.scrollScrollBarTo = function(/*Object*/to){ // to: {x, y}
1014                if(!this.scrollBar){ return; }
1015                if(this._v && this._scrollBarV && typeof to.y == "number"){
1016                        if(has("webkit")){
1017                                this._scrollBarV.style.webkitTransform = this.makeTranslateStr({y:to.y});
1018                        }else{
1019                                this._scrollBarV.style.top = to.y + "px";
1020                        }
1021                }
1022                if(this._h && this._scrollBarH && typeof to.x == "number"){
1023                        if(has("webkit")){
1024                                this._scrollBarH.style.webkitTransform = this.makeTranslateStr({x:to.x});
1025                        }else{
1026                                this._scrollBarH.style.left = to.x + "px";
1027                        }
1028                }
1029        };
1030
1031        this.slideScrollBarTo = function(/*Object*/to, /*Number*/duration, /*String*/easing){
1032                if(!this.scrollBar){ return; }
1033                var fromPos = this.calcScrollBarPos(this.getPos());
1034                var toPos = this.calcScrollBarPos(to);
1035                if(this._v && this._scrollBarV){
1036                        this._runSlideAnimation({y:fromPos.y}, {y:toPos.y}, duration, easing, this._scrollBarV, 0);
1037                }
1038                if(this._h && this._scrollBarH){
1039                        this._runSlideAnimation({x:fromPos.x}, {x:toPos.x}, duration, easing, this._scrollBarH, 1);
1040                }
1041        };
1042
1043        this._runSlideAnimation = function(/*Object*/from, /*Object*/to, /*Number*/duration, /*String*/easing, node, idx){
1044                // idx: 0:scrollbarV, 1:scrollbarH, 2:content
1045                if(has("webkit")){
1046                        this.setKeyframes(from, to, idx);
1047                        domStyle.set(node, {
1048                                webkitAnimationDuration: duration + "s",
1049                                webkitAnimationTimingFunction: easing
1050                        });
1051                        domClass.add(node, "mblScrollableScrollTo"+idx);
1052                        if(idx == 2){
1053                                this.scrollTo(to, true, node);
1054                        }else{
1055                                this.scrollScrollBarTo(to);
1056                        }
1057//>>excludeStart("webkitMobile", kwArgs.webkitMobile);
1058                }else if(dojo.fx && dojo.fx.easing && duration){
1059                        // If you want to support non-webkit browsers,
1060                        // your application needs to load necessary modules as follows:
1061                        //
1062                        // | dojo.require("dojo.fx");
1063                        // | dojo.require("dojo.fx.easing");
1064                        //
1065                        // This module itself does not make dependency on them.
1066                        var s = dojo.fx.slideTo({
1067                                node: node,
1068                                duration: duration*1000,
1069                                left: to.x,
1070                                top: to.y,
1071                                easing: (easing == "ease-out") ? dojo.fx.easing.quadOut : dojo.fx.easing.linear
1072                        }).play();
1073                        if(idx == 2){
1074                                connect.connect(s, "onEnd", this, "onFlickAnimationEnd");
1075                        }
1076                }else{
1077                        // directly jump to the destination without animation
1078                        if(idx == 2){
1079                                this.scrollTo(to, false, node);
1080                                this.onFlickAnimationEnd();
1081                        }else{
1082                                this.scrollScrollBarTo(to);
1083                        }
1084//>>excludeEnd("webkitMobile");
1085                }
1086        };
1087
1088        this.resetScrollBar = function(){
1089                //      summary:
1090                //              Resets the scroll bar length, position, etc.
1091                var f = function(wrapper, bar, d, c, hd, v){
1092                        if(!bar){ return; }
1093                        var props = {};
1094                        props[v ? "top" : "left"] = hd + 4 + "px"; // +4 is for top or left margin
1095                        var t = (d - 8) <= 0 ? 1 : d - 8;
1096                        props[v ? "height" : "width"] = t + "px";
1097                        domStyle.set(wrapper, props);
1098                        var l = Math.round(d * d / c); // scroll bar length
1099                        l = Math.min(Math.max(l - 8, 5), t); // -8 is for margin for both ends
1100                        bar.style[v ? "height" : "width"] = l + "px";
1101                        domStyle.set(bar, {"opacity": 0.6});
1102                };
1103                var dim = this.getDim();
1104                f(this._scrollBarWrapperV, this._scrollBarV, dim.d.h, dim.c.h, this.fixedHeaderHeight, true);
1105                f(this._scrollBarWrapperH, this._scrollBarH, dim.d.w, dim.c.w, 0);
1106                this.createMask();
1107        };
1108
1109        this.createMask = function(){
1110                //      summary:
1111                //              Creates a mask for a scroll bar edge.
1112                // description:
1113                //              This function creates a mask that hides corners of one scroll
1114                //              bar edge to make it round edge. The other side of the edge is
1115                //              always visible and round shaped with the border-radius style.
1116                if(!has("webkit")){ return; }
1117                var ctx;
1118                if(this._scrollBarWrapperV){
1119                        var h = this._scrollBarWrapperV.offsetHeight;
1120                        ctx = win.doc.getCSSCanvasContext("2d", "scrollBarMaskV", 5, h);
1121                        ctx.fillStyle = "rgba(0,0,0,0.5)";
1122                        ctx.fillRect(1, 0, 3, 2);
1123                        ctx.fillRect(0, 1, 5, 1);
1124                        ctx.fillRect(0, h - 2, 5, 1);
1125                        ctx.fillRect(1, h - 1, 3, 2);
1126                        ctx.fillStyle = "rgb(0,0,0)";
1127                        ctx.fillRect(0, 2, 5, h - 4);
1128                        this._scrollBarWrapperV.style.webkitMaskImage = "-webkit-canvas(scrollBarMaskV)";
1129                }
1130                if(this._scrollBarWrapperH){
1131                        var w = this._scrollBarWrapperH.offsetWidth;
1132                        ctx = win.doc.getCSSCanvasContext("2d", "scrollBarMaskH", w, 5);
1133                        ctx.fillStyle = "rgba(0,0,0,0.5)";
1134                        ctx.fillRect(0, 1, 2, 3);
1135                        ctx.fillRect(1, 0, 1, 5);
1136                        ctx.fillRect(w - 2, 0, 1, 5);
1137                        ctx.fillRect(w - 1, 1, 2, 3);
1138                        ctx.fillStyle = "rgb(0,0,0)";
1139                        ctx.fillRect(2, 0, w - 4, 5);
1140                        this._scrollBarWrapperH.style.webkitMaskImage = "-webkit-canvas(scrollBarMaskH)";
1141                }
1142        };
1143
1144        this.flashScrollBar = function(){
1145                if(this.disableFlashScrollBar || !this.domNode){ return; }
1146                this._dim = this.getDim();
1147                if(this._dim.d.h <= 0){ return; } // dom is not ready
1148                this.showScrollBar();
1149                var _this = this;
1150                setTimeout(function(){
1151                        _this.hideScrollBar();
1152                }, 300);
1153        };
1154
1155        this.addCover = function(){
1156//>>excludeStart("webkitMobile", kwArgs.webkitMobile);
1157                if(!has('touch') && !this.noCover){
1158                        if(!this._cover){
1159                                this._cover = domConstruct.create("div", null, win.doc.body);
1160                                domStyle.set(this._cover, {
1161                                        backgroundColor: "#ffff00",
1162                                        opacity: 0,
1163                                        position: "absolute",
1164                                        top: "0px",
1165                                        left: "0px",
1166                                        width: "100%",
1167                                        height: "100%",
1168                                        zIndex: 2147483647 // max of signed 32-bit integer
1169                                });
1170                                this._ch.push(connect.connect(this._cover,
1171                                        has('touch') ? "touchstart" : "onmousedown", this, "onTouchEnd"));
1172                        }else{
1173                                this._cover.style.display = "";
1174                        }
1175                        this.setSelectable(this._cover, false);
1176                        this.setSelectable(this.domNode, false);
1177                }
1178//>>excludeEnd("webkitMobile");
1179        };
1180
1181        this.removeCover = function(){
1182//>>excludeStart("webkitMobile", kwArgs.webkitMobile);
1183                if(!has('touch') && this._cover){
1184                        this._cover.style.display = "none";
1185                        this.setSelectable(this._cover, true);
1186                        this.setSelectable(this.domNode, true);
1187                }
1188//>>excludeEnd("webkitMobile");
1189        };
1190
1191        this.setKeyframes = function(/*Object*/from, /*Object*/to, /*Number*/idx){
1192                if(!dm._rule){
1193                        dm._rule = [];
1194                }
1195                // idx: 0:scrollbarV, 1:scrollbarH, 2:content
1196                if(!dm._rule[idx]){
1197                        var node = domConstruct.create("style", null, win.doc.getElementsByTagName("head")[0]);
1198                        node.textContent =
1199                                ".mblScrollableScrollTo"+idx+"{-webkit-animation-name: scrollableViewScroll"+idx+";}"+
1200                                "@-webkit-keyframes scrollableViewScroll"+idx+"{}";
1201                        dm._rule[idx] = node.sheet.cssRules[1];
1202                }
1203                var rule = dm._rule[idx];
1204                if(rule){
1205                        if(from){
1206                                rule.deleteRule("from");
1207                                rule.insertRule("from { -webkit-transform: "+this.makeTranslateStr(from)+"; }");
1208                        }
1209                        if(to){
1210                                if(to.x === undefined){ to.x = from.x; }
1211                                if(to.y === undefined){ to.y = from.y; }
1212                                rule.deleteRule("to");
1213                                rule.insertRule("to { -webkit-transform: "+this.makeTranslateStr(to)+"; }");
1214                        }
1215                }
1216        };
1217
1218        this.setSelectable = function(node, selectable){
1219                // dojo.setSelectable has dependency on dojo.query. Re-define our own.
1220                node.style.KhtmlUserSelect = selectable ? "auto" : "none";
1221                node.style.MozUserSelect = selectable ? "" : "none";
1222                node.onselectstart = selectable ? null : function(){return false;};
1223                if(has("ie")){
1224                        node.unselectable = selectable ? "" : "on";
1225                        var nodes = node.getElementsByTagName("*");
1226                        for(var i = 0; i < nodes.length; i++){
1227                                nodes[i].unselectable = selectable ? "" : "on";
1228                        }
1229                }
1230        };
1231
1232        // feature detection
1233        if(has("webkit")){
1234                var elem = win.doc.createElement("div");
1235                elem.style.webkitTransform = "translate3d(0px,1px,0px)";
1236                win.doc.documentElement.appendChild(elem);
1237                var v = win.doc.defaultView.getComputedStyle(elem, '')["-webkit-transform"];
1238                dm.hasTranslate3d = v && v.indexOf("matrix") === 0;
1239                win.doc.documentElement.removeChild(elem);
1240        }
1241};
1242
1243//>>includeStart("standaloneScrollable", kwArgs.standaloneScrollable);
1244        if(!dm){ dm = dojox.mobile; }
1245//>>includeEnd("standaloneScrollable");
1246dm.scrollable = scrollable; // for backward compatibility
1247return scrollable;
1248});
Note: See TracBrowser for help on using the repository browser.