Bug fixes, scrolling fixes on touch devices, javascript minifier+caching
authorDarren <darren@darrenwhitlen.com>
Sat, 16 Jul 2011 20:56:19 +0000 (21:56 +0100)
committerDarren <darren@darrenwhitlen.com>
Sat, 16 Jul 2011 20:56:19 +0000 (21:56 +0100)
assets.php [new file with mode: 0644]
css/default.css
css/touchscreen_tweaks.css
index.php
js/front.js
js/gateway.js
js/iscroll.js [new file with mode: 0644]
libs/jsmin.php [new file with mode: 0644]

diff --git a/assets.php b/assets.php
new file mode 100644 (file)
index 0000000..93bc378
--- /dev/null
@@ -0,0 +1,53 @@
+<?php\r
+\r
+       function auto_load($class){\r
+               try     {\r
+                       $file = str_replace('_', '/', strtolower($class));\r
+                       $path = 'libs/'.$file.'.php';\r
+\r
+                       if (file_exists($path)){\r
+                               require $path;\r
+                               return true;\r
+                       }\r
+\r
+                       return false;\r
+               }catch (Exception $e){\r
+                       // Err...\r
+               }\r
+       }\r
+       spl_autoload_register('auto_load');\r
+\r
+\r
+\r
+       define('CACHE_DIR', './cache/');\r
+\r
+       function cacheReadfile($file){\r
+               if(file_exists(CACHE_DIR.$file)){\r
+                       readfile(CACHE_DIR.$file);\r
+                       exit;\r
+               }\r
+       }\r
+\r
+       function writeToCache($file, $data){\r
+               file_put_contents(CACHE_DIR.$file, $data);\r
+       }\r
+\r
+\r
+\r
+       if(isset($_GET['js'])){\r
+               $files = explode(',', $_GET['js']);\r
+               arsort($files);\r
+\r
+               $cache_filename = 'js';\r
+               foreach($files as $file) $cache_filename .= '_'.$file;\r
+               cacheReadfile($cache_filename);\r
+\r
+               $js = '';\r
+               foreach($files as $file){\r
+               $js .= JSMin::minify(file_get_contents("js/$file.js"));\r
+               }\r
+               //writeToCache($cache_filename, $js);\r
+               echo $js;\r
+       }\r
+\r
+       
\ No newline at end of file
index b55f526c2e2703882662dca24e7d203e3ab2ce30..7ba60a4238e893dde254dccf4ef23205f191b6e6 100644 (file)
@@ -110,11 +110,13 @@ body, html {
        \r
 }\r
 \r
-#kiwi .messages {\r
+#kiwi .windows {\r
        position: absolute;\r
        top:92px; left:0px;\r
        right: 110px; bottom:30px;\r
        overflow-y:scroll;\r
+}\r
+#kiwi .messages {\r
        overflow-x:wrap;\r
        border:none;\r
        display: none;\r
@@ -140,6 +142,10 @@ body, html {
 #kiwi .messages .msg.motd .text { color:#666; }\r
 #kiwi .messages .msg.whois .nick { font-weight:normal; }\r
 #kiwi .messages .msg.whois .text { margin-left:18em; padding-left:1em; border-left:1px dashed #999; }\r
+#kiwi .messages .msg.error .text {\r
+       border:1px solid #A33F3F; background-color:#D28A8A;\r
+       padding:0.5em; margin-top:1em; margin-bottom:1em; margin-right:2em;\r
+}\r
 \r
 \r
 \r
index a90bce5f4c76a2cd6386901535e2988ab2f6ecd1..84ffadf3377d8afa6c05ddbdf96faee1aa128481 100644 (file)
@@ -1,6 +1,6 @@
-#kiwi.touchscreen.portrait .userlist { right:-100px; }\r
+#kiwi.touchscreen.portrait .userlist { display:none; }\r
 \r
-#kiwi.touchscreen.portrait .messages { right:10px; }\r
+#kiwi.touchscreen.portrait .messages { right:0px; }\r
 \r
 #kiwi.touchscreen .windowlist ul li { padding:10px; }\r
 \r
index 9624789a3d83ba19b474865eea7a5cdcb1aa00b2..b7e340b7e171cdc467172e98722627f482da96df 100644 (file)
--- a/index.php
+++ b/index.php
@@ -54,6 +54,8 @@
 <script type="text/javascript" src="js/util.js"></script>\r
 <script type="text/javascript" src="js/gateway.js"></script>\r
 <script type="text/javascript" src="js/front.js"></script>\r
+<script type="text/javascript" src="js/iscroll.js"></script>\r
+<script type="text/javascript" src="assets.php?js=jquery.json-2.2.min,util,gateway,front,iscroll"></script>\r
 \r
 <?php if(in_array($agent, array("android", "ipad", "iphone", "ipad"))){ ?>\r
 <script type="text/javascript" src="js/touchscreen_tweaks.js"></script>\r
        var touchscreen = <?php echo ($touchscreen) ? 'true' : 'false'; ?>;\r
        var init_data = {};\r
        var kiwi_server = 'wss://<?php echo $node_server; ?>:7777/';\r
+       var touch_scroll;\r
        \r
        $(document).ready(function(){\r
-               if(touchscreen) $('#kiwi').addClass('touchscreen');\r
+               if(touchscreen){\r
+                       $('#kiwi').addClass('touchscreen');\r
+\r
+                       // Single touch scrolling through scrollback for touchscreens\r
+                       scroll_opts = {};\r
+                       touch_scroll = new iScroll('windows', scroll_opts);\r
+               }\r
                \r
                //#nick@irc.anonnet.org:6679+/#channel,##channel,&channel\r
                var chans = document.location.hash.match(/[#&]+[^ ,\007]+/g);\r
                <ul></ul>\r
        </div>\r
        \r
+       <div id="windows" class="windows"><div class="scroller" style="width:100%;"></div></div>\r
+\r
        <div class="control">\r
                <div class="msginput">\r
                        <div class="nick"><a href="#"></a>:</div>\r
index 9dcb86da48347511082c5ba9ce11de0f660d3174..ec6fb0a35429703516005b36bf334a72bdeb507a 100644 (file)
@@ -19,6 +19,7 @@ var front = {
                $(gateway).bind("onmotd", front.onMOTD);
                $(gateway).bind("onoptions", front.onOptions);
                $(gateway).bind("onconnect", front.onConnect);
+               $(gateway).bind("ondisconnect", front.onDisconnect);
                $(gateway).bind("onnick", front.onNick);
                $(gateway).bind("onuserlist", front.onUserList);
                $(gateway).bind("onuserlist_end", front.onUserListEnd);
@@ -118,7 +119,7 @@ var front = {
         top = top + parseInt(ct.css('padding-bottom').replace('px', ''));
         top = top + 1;
 
-               $('#kiwi .messages').css('top', top + "px");
+               $('#kiwi .windows').css('top', top + "px");
                $('#kiwi .userlist').css('top', top + "px");
        },
 
@@ -290,6 +291,12 @@ var front = {
                        front.tabviews.server.addMsg(null, ' ', '=== Failed to connect :(', 'status');
                }
        },
+       onDisconnect: function(e, data){
+               var tab;
+               for(tab in front.tabviews){
+                       front.tabviews[tab].addMsg(null, '', 'Disconnected from server!', 'error')
+               }
+       },
        onOptions: function (e, data) {
                if (typeof gateway.network_name === "string" && gateway.network_name !== "") {
                        front.tabviews.server.tab.text(gateway.network_name);
@@ -607,7 +614,7 @@ var front = {
                var tmp_userlistname = 'kiwi_userlist_' + htmlsafe_name;
                var tmp_tabname = 'kiwi_tab_' + htmlsafe_name
                
-               $('#kiwi').append('<div id="' + tmp_divname + '" class="messages"></div>');
+               $('#kiwi .windows .scroller').append('<div id="' + tmp_divname + '" class="messages"></div>');
                $('#kiwi .userlist').append('<ul id="' + tmp_userlistname + '"></ul>');
                $('#kiwi .windowlist ul').append('<li id="' + tmp_tabname + '" onclick="front.tabviews[\'' + v_name.toLowerCase() + '\'].show();">' + v_name + '</li>');
                //$('#kiwi .windowlist ul .window_'+v_name).click(function(){ front.windowShow(v_name); });
@@ -812,6 +819,9 @@ tabview.prototype.show = function(){
        front.setTopicText(this.topic);
        front.cur_channel = this;
        
+       // If we're using fancy scrolling, refresh it
+       if(touch_scroll) touch_scroll.refresh();
+
        this.scrollBottom();
        if(!touchscreen) $('#kiwi_msginput').focus();
 }
@@ -909,13 +919,24 @@ tabview.prototype.addMsg = function(time, nick, msg, type, style){
        }
        
        
-       var line_msg = '<div class="msg '+type+'"><div class="time">'+time+'</div><div class="nick">'+html_nick+'</div><div class="text" style="'+style+'">'+msg+' </div></div>';
+       var line_msg = $('<div class="msg '+type+'"><div class="time">'+time+'</div><div class="nick">'+html_nick+'</div><div class="text" style="'+style+'">'+msg+' </div></div>');
        this.div.append(line_msg);
-       this.scrollBottom();
+
+       if(!touchscreen){
+               this.scrollBottom();
+       } else {
+               touch_scroll.refresh();
+               //console.log(this.div.attr("scrollHeight") +" - "+ $('#windows').height());
+               this.scrollBottom();
+               //if(this.div.attr("scrollHeight") > $('#windows').height()){
+               //      touch_scroll.scrollTo(0, this.div.height());
+               //}
+       }
 }
 
 tabview.prototype.scrollBottom = function(){
-       this.div.attr({ scrollTop: this.div.attr("scrollHeight") });
+       var w = $('#windows');
+       w.attr({ scrollTop: w.attr("scrollHeight") });
 }
 
 tabview.prototype.changeNick = function(newNick, oldNick){
@@ -942,8 +963,8 @@ tabview.prototype.userlistSort = function(){
                var compB = $(b).text().toUpperCase();
                
                // Sort by prefixes first
-               for (var prefix in gateway.user_prefixes) {
-                       mode = gateway.user_prefixes[prefix];
+               for (var i in gateway.user_prefixes) {
+                       prefix = gateway.user_prefixes[i];
                        
                        if(compA.charAt(0) == prefix && compB.charAt(0) == prefix){
                                // Both have the same prefix, string compare time
index b5b252c4bf6a276e64310e2c3f021e3f38ea8b65..0c85a8f6981d8fbe66d34193c0aa15a642876418 100644 (file)
@@ -9,7 +9,7 @@ var gateway = {
     syncing: false,
     channel_prefix: '#',
     network_name: '',
-    user_prefixes: [],
+    user_prefixes: ['~','&','@','+'],
     socket: null,
        
     start: function (kiwi_server) {
@@ -21,6 +21,8 @@ var gateway = {
             gateway.socket.on('message', gateway.parse);
             gateway.socket.on('disconnect', function () {
                 // Teardown procedure here
+                // TODO: Ask Ehm if teardown with socket.io is required
+                $(gateway).trigger("ondisconnect", {});
             });
         });
     },
diff --git a/js/iscroll.js b/js/iscroll.js
new file mode 100644 (file)
index 0000000..eca1eed
--- /dev/null
@@ -0,0 +1,963 @@
+/*!\r
+ * iScroll v4.1.4 ~ Copyright (c) 2011 Matteo Spinelli, http://cubiq.org\r
+ * Released under MIT license, http://cubiq.org/license\r
+ */\r
+\r
+(function(){\r
+var m = Math,\r
+       vendor = (/webkit/i).test(navigator.appVersion) ? 'webkit' :\r
+               (/firefox/i).test(navigator.userAgent) ? 'Moz' :\r
+               'opera' in window ? 'O' : '',\r
+\r
+       // Browser capabilities\r
+       has3d = 'WebKitCSSMatrix' in window && 'm11' in new WebKitCSSMatrix(),\r
+       hasTouch = 'ontouchstart' in window,\r
+       hasTransform = vendor + 'Transform' in document.documentElement.style,\r
+       isAndroid = (/android/gi).test(navigator.appVersion),\r
+       isIDevice = (/iphone|ipad/gi).test(navigator.appVersion),\r
+       isPlaybook = (/playbook/gi).test(navigator.appVersion),\r
+       hasTransitionEnd = (isIDevice || isPlaybook) && 'onwebkittransitionend' in window,\r
+       nextFrame = (function() {\r
+           return window.requestAnimationFrame\r
+                       || window.webkitRequestAnimationFrame\r
+                       || window.mozRequestAnimationFrame\r
+                       || window.oRequestAnimationFrame\r
+                       || window.msRequestAnimationFrame\r
+                       || function(callback) { return setTimeout(callback, 17); }\r
+       })(),\r
+       cancelFrame = (function () {\r
+           return window.cancelRequestAnimationFrame\r
+                       || window.webkitCancelRequestAnimationFrame\r
+                       || window.mozCancelRequestAnimationFrame\r
+                       || window.oCancelRequestAnimationFrame\r
+                       || window.msCancelRequestAnimationFrame\r
+                       || clearTimeout\r
+       })(),\r
+\r
+       // Events\r
+       RESIZE_EV = 'onorientationchange' in window ? 'orientationchange' : 'resize',\r
+       START_EV = hasTouch ? 'touchstart' : 'mousedown',\r
+       MOVE_EV = hasTouch ? 'touchmove' : 'mousemove',\r
+       END_EV = hasTouch ? 'touchend' : 'mouseup',\r
+       CANCEL_EV = hasTouch ? 'touchcancel' : 'mouseup',\r
+       WHEEL_EV = vendor == 'Moz' ? 'DOMMouseScroll' : 'mousewheel',\r
+\r
+       // Helpers\r
+       trnOpen = 'translate' + (has3d ? '3d(' : '('),\r
+       trnClose = has3d ? ',0)' : ')',\r
+\r
+       // Constructor\r
+       iScroll = function (el, options) {\r
+               var that = this,\r
+                       doc = document,\r
+                       i;\r
+\r
+               that.wrapper = typeof el == 'object' ? el : doc.getElementById(el);\r
+               that.wrapper.style.overflow = 'hidden';\r
+               that.scroller = that.wrapper.children[0];\r
+\r
+               // Default options\r
+               that.options = {\r
+                       hScroll: true,\r
+                       vScroll: true,\r
+                       bounce: true,\r
+                       bounceLock: false,\r
+                       momentum: true,\r
+                       lockDirection: true,\r
+                       useTransform: true,\r
+                       useTransition: false,\r
+\r
+                       // Scrollbar\r
+                       hScrollbar: true,\r
+                       vScrollbar: true,\r
+                       fixedScrollbar: isAndroid,\r
+                       hideScrollbar: isIDevice,\r
+                       fadeScrollbar: isIDevice && has3d,\r
+                       scrollbarClass: '',\r
+\r
+                       // Zoom\r
+                       zoom: false,\r
+                       zoomMin: 1,\r
+                       zoomMax: 4,\r
+                       doubleTapZoom: 2,\r
+\r
+                       // Snap\r
+                       snap: false,\r
+                       snapThreshold: 1,\r
+\r
+                       // Events\r
+                       onRefresh: null,\r
+                       onBeforeScrollStart: function (e) { e.preventDefault(); },\r
+                       onScrollStart: null,\r
+                       onBeforeScrollMove: null,\r
+                       onScrollMove: null,\r
+                       onBeforeScrollEnd: null,\r
+                       onScrollEnd: null,\r
+                       onTouchEnd: null,\r
+                       onDestroy: null\r
+               };\r
+\r
+               // User defined options\r
+               for (i in options) that.options[i] = options[i];\r
+\r
+               // Normalize options\r
+               that.options.useTransform = hasTransform ? that.options.useTransform : false;\r
+               that.options.hScrollbar = that.options.hScroll && that.options.hScrollbar;\r
+               that.options.vScrollbar = that.options.vScroll && that.options.vScrollbar;\r
+               that.options.zoom = that.options.useTransform && that.options.zoom;\r
+               that.options.useTransition = hasTransitionEnd && that.options.useTransition;\r
+               \r
+               // Set some default styles\r
+               that.scroller.style[vendor + 'TransitionProperty'] = that.options.useTransform ? '-' + vendor.toLowerCase() + '-transform' : 'top left';\r
+               that.scroller.style[vendor + 'TransitionDuration'] = '0';\r
+               that.scroller.style[vendor + 'TransformOrigin'] = '0 0';\r
+               if (that.options.useTransition) that.scroller.style[vendor + 'TransitionTimingFunction'] = 'cubic-bezier(0.33,0.66,0.66,1)';\r
+               \r
+               if (that.options.useTransform) that.scroller.style[vendor + 'Transform'] = trnOpen + '0,0' + trnClose;\r
+               else that.scroller.style.cssText += ';top:0;left:0';\r
+\r
+               if (that.options.useTransition) that.options.fixedScrollbar = true;\r
+\r
+               that.refresh();\r
+\r
+               that._bind(RESIZE_EV, window);\r
+               that._bind(START_EV);\r
+               if (!hasTouch) {\r
+                       that._bind('mouseout', that.wrapper);\r
+                       that._bind(WHEEL_EV);\r
+               }\r
+       };\r
+\r
+// Prototype\r
+iScroll.prototype = {\r
+       enabled: true,\r
+       x: 0,\r
+       y: 0,\r
+       steps: [],\r
+       scale: 1,\r
+       currPageX: 0, currPageY: 0,\r
+       pagesX: [], pagesY: [],\r
+       aniTime: null,\r
+       \r
+       handleEvent: function (e) {\r
+               var that = this;\r
+               switch(e.type) {\r
+                       case START_EV: that._start(e); break;\r
+                       case MOVE_EV: that._move(e); break;\r
+                       case END_EV:\r
+                       case CANCEL_EV: that._end(e); break;\r
+                       case RESIZE_EV: that._resize(); break;\r
+                       case WHEEL_EV: that._wheel(e); break;\r
+                       case 'mouseout': that._mouseout(e); break;\r
+                       case 'webkitTransitionEnd': that._transitionEnd(e); break;\r
+               }\r
+       },\r
+       \r
+       _scrollbar: function (dir) {\r
+               var that = this,\r
+                       doc = document,\r
+                       bar;\r
+\r
+               if (!that[dir + 'Scrollbar']) {\r
+                       if (that[dir + 'ScrollbarWrapper']) {\r
+                               if (hasTransform) that[dir + 'ScrollbarIndicator'].style[vendor + 'Transform'] = '';\r
+                               that[dir + 'ScrollbarWrapper'].parentNode.removeChild(that[dir + 'ScrollbarWrapper']);\r
+                               that[dir + 'ScrollbarWrapper'] = null;\r
+                               that[dir + 'ScrollbarIndicator'] = null;\r
+                       }\r
+\r
+                       return;\r
+               }\r
+\r
+               if (!that[dir + 'ScrollbarWrapper']) {\r
+                       // Create the scrollbar wrapper\r
+                       bar = doc.createElement('div');\r
+\r
+                       if (that.options.scrollbarClass) bar.className = that.options.scrollbarClass + dir.toUpperCase();\r
+                       else bar.style.cssText = 'position:absolute;z-index:100;' + (dir == 'h' ? 'height:7px;bottom:1px;left:2px;right:' + (that.vScrollbar ? '7' : '2') + 'px' : 'width:7px;bottom:' + (that.hScrollbar ? '7' : '2') + 'px;top:2px;right:1px');\r
+\r
+                       bar.style.cssText += ';pointer-events:none;-' + vendor + '-transition-property:opacity;-' + vendor + '-transition-duration:' + (that.options.fadeScrollbar ? '350ms' : '0') + ';overflow:hidden;opacity:' + (that.options.hideScrollbar ? '0' : '1');\r
+\r
+                       that.wrapper.appendChild(bar);\r
+                       that[dir + 'ScrollbarWrapper'] = bar;\r
+\r
+                       // Create the scrollbar indicator\r
+                       bar = doc.createElement('div');\r
+                       if (!that.options.scrollbarClass) {\r
+                               bar.style.cssText = 'position:absolute;z-index:100;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.9);-' + vendor + '-background-clip:padding-box;-' + vendor + '-box-sizing:border-box;' + (dir == 'h' ? 'height:100%' : 'width:100%') + ';-' + vendor + '-border-radius:3px;border-radius:3px';\r
+                       }\r
+                       bar.style.cssText += ';pointer-events:none;-' + vendor + '-transition-property:-' + vendor + '-transform;-' + vendor + '-transition-timing-function:cubic-bezier(0.33,0.66,0.66,1);-' + vendor + '-transition-duration:0;-' + vendor + '-transform:' + trnOpen + '0,0' + trnClose;\r
+                       if (that.options.useTransition) bar.style.cssText += ';-' + vendor + '-transition-timing-function:cubic-bezier(0.33,0.66,0.66,1)';\r
+\r
+                       that[dir + 'ScrollbarWrapper'].appendChild(bar);\r
+                       that[dir + 'ScrollbarIndicator'] = bar;\r
+               }\r
+\r
+               if (dir == 'h') {\r
+                       that.hScrollbarSize = that.hScrollbarWrapper.clientWidth;\r
+                       that.hScrollbarIndicatorSize = m.max(m.round(that.hScrollbarSize * that.hScrollbarSize / that.scrollerW), 8);\r
+                       that.hScrollbarIndicator.style.width = that.hScrollbarIndicatorSize + 'px';\r
+                       that.hScrollbarMaxScroll = that.hScrollbarSize - that.hScrollbarIndicatorSize;\r
+                       that.hScrollbarProp = that.hScrollbarMaxScroll / that.maxScrollX;\r
+               } else {\r
+                       that.vScrollbarSize = that.vScrollbarWrapper.clientHeight;\r
+                       that.vScrollbarIndicatorSize = m.max(m.round(that.vScrollbarSize * that.vScrollbarSize / that.scrollerH), 8);\r
+                       that.vScrollbarIndicator.style.height = that.vScrollbarIndicatorSize + 'px';\r
+                       that.vScrollbarMaxScroll = that.vScrollbarSize - that.vScrollbarIndicatorSize;\r
+                       that.vScrollbarProp = that.vScrollbarMaxScroll / that.maxScrollY;\r
+               }\r
+\r
+               // Reset position\r
+               that._scrollbarPos(dir, true);\r
+       },\r
+       \r
+       _resize: function () {\r
+               this.refresh();\r
+       },\r
+       \r
+       _pos: function (x, y) {\r
+               x = this.hScroll ? x : 0;\r
+               y = this.vScroll ? y : 0;\r
+\r
+               if (this.options.useTransform) {\r
+                       this.scroller.style[vendor + 'Transform'] = trnOpen + x + 'px,' + y + 'px' + trnClose + ' scale(' + this.scale + ')';\r
+               } else {\r
+                       x = m.round(x);\r
+                       y = m.round(y);\r
+                       this.scroller.style.left = x + 'px';\r
+                       this.scroller.style.top = y + 'px';\r
+               }\r
+\r
+               this.x = x;\r
+               this.y = y;\r
+\r
+               this._scrollbarPos('h');\r
+               this._scrollbarPos('v');\r
+       },\r
+\r
+       _scrollbarPos: function (dir, hidden) {\r
+               var that = this,\r
+                       pos = dir == 'h' ? that.x : that.y,\r
+                       size;\r
+               \r
+               if (!that[dir + 'Scrollbar']) return;\r
+               \r
+               pos = that[dir + 'ScrollbarProp'] * pos;\r
+       \r
+               if (pos < 0) {\r
+                       if (!that.options.fixedScrollbar) {\r
+                               size = that[dir + 'ScrollbarIndicatorSize'] + m.round(pos * 3);\r
+                               if (size < 8) size = 8;\r
+                               that[dir + 'ScrollbarIndicator'].style[dir == 'h' ? 'width' : 'height'] = size + 'px';\r
+                       }\r
+                       pos = 0;\r
+               } else if (pos > that[dir + 'ScrollbarMaxScroll']) {\r
+                       if (!that.options.fixedScrollbar) {\r
+                               size = that[dir + 'ScrollbarIndicatorSize'] - m.round((pos - that[dir + 'ScrollbarMaxScroll']) * 3);\r
+                               if (size < 8) size = 8;\r
+                               that[dir + 'ScrollbarIndicator'].style[dir == 'h' ? 'width' : 'height'] = size + 'px';\r
+                               pos = that[dir + 'ScrollbarMaxScroll'] + (that[dir + 'ScrollbarIndicatorSize'] - size);\r
+                       } else {\r
+                               pos = that[dir + 'ScrollbarMaxScroll'];\r
+                       }\r
+               }\r
+\r
+               that[dir + 'ScrollbarWrapper'].style[vendor + 'TransitionDelay'] = '0';\r
+               that[dir + 'ScrollbarWrapper'].style.opacity = hidden && that.options.hideScrollbar ? '0' : '1';\r
+               that[dir + 'ScrollbarIndicator'].style[vendor + 'Transform'] = trnOpen + (dir == 'h' ? pos + 'px,0' : '0,' + pos + 'px') + trnClose;\r
+       },\r
+       \r
+       _start: function (e) {\r
+               var that = this,\r
+                       point = hasTouch ? e.touches[0] : e,\r
+                       matrix, x, y,\r
+                       c1, c2, target;\r
+\r
+               if (!that.enabled) return;\r
+\r
+               if (that.options.onBeforeScrollStart) that.options.onBeforeScrollStart.call(that, e);\r
+\r
+               if (that.options.useTransition) that._transitionTime(0);\r
+\r
+               that.moved = false;\r
+               that.animating = false;\r
+               that.zoomed = false;\r
+               that.distX = 0;\r
+               that.distY = 0;\r
+               that.absDistX = 0;\r
+               that.absDistY = 0;\r
+               that.dirX = 0;\r
+               that.dirY = 0;\r
+\r
+               // Gesture start\r
+               if (that.options.zoom && hasTouch && e.touches.length > 1) {\r
+                       c1 = m.abs(e.touches[0].pageX-e.touches[1].pageX);\r
+                       c2 = m.abs(e.touches[0].pageY-e.touches[1].pageY);\r
+                       that.touchesDistStart = m.sqrt(c1 * c1 + c2 * c2);\r
+\r
+                       that.originX = m.abs(e.touches[0].pageX + e.touches[1].pageX - that.wrapperOffsetLeft * 2) / 2 - that.x;\r
+                       that.originY = m.abs(e.touches[0].pageY + e.touches[1].pageY - that.wrapperOffsetTop * 2) / 2 - that.y;\r
+               }\r
+\r
+               if (that.options.momentum) {\r
+                       if (that.options.useTransform) {\r
+                               // Very lame general purpose alternative to CSSMatrix\r
+                               matrix = getComputedStyle(that.scroller, null)[vendor + 'Transform'].replace(/[^0-9-.,]/g, '').split(',');\r
+                               x = matrix[4] * 1;\r
+                               y = matrix[5] * 1;\r
+                       } else {\r
+                               x = getComputedStyle(that.scroller, null).left.replace(/[^0-9-]/g, '') * 1;\r
+                               y = getComputedStyle(that.scroller, null).top.replace(/[^0-9-]/g, '') * 1;\r
+                       }\r
+                       \r
+                       if (x != that.x || y != that.y) {\r
+                               if (that.options.useTransition) that._unbind('webkitTransitionEnd');\r
+                               else cancelFrame(that.aniTime);\r
+                               that.steps = [];\r
+                               that._pos(x, y);\r
+                       }\r
+               }\r
+\r
+               that.absStartX = that.x;        // Needed by snap threshold\r
+               that.absStartY = that.y;\r
+\r
+               that.startX = that.x;\r
+               that.startY = that.y;\r
+               that.pointX = point.pageX;\r
+               that.pointY = point.pageY;\r
+\r
+               that.startTime = e.timeStamp || (new Date()).getTime();\r
+\r
+               if (that.options.onScrollStart) that.options.onScrollStart.call(that, e);\r
+\r
+               that._bind(MOVE_EV);\r
+               that._bind(END_EV);\r
+               that._bind(CANCEL_EV);\r
+       },\r
+       \r
+       _move: function (e) {\r
+               var that = this,\r
+                       point = hasTouch ? e.touches[0] : e,\r
+                       deltaX = point.pageX - that.pointX,\r
+                       deltaY = point.pageY - that.pointY,\r
+                       newX = that.x + deltaX,\r
+                       newY = that.y + deltaY,\r
+                       c1, c2, scale,\r
+                       timestamp = e.timeStamp || (new Date()).getTime();\r
+\r
+               if (that.options.onBeforeScrollMove) that.options.onBeforeScrollMove.call(that, e);\r
+\r
+               // Zoom\r
+               if (that.options.zoom && hasTouch && e.touches.length > 1) {\r
+                       c1 = m.abs(e.touches[0].pageX - e.touches[1].pageX);\r
+                       c2 = m.abs(e.touches[0].pageY - e.touches[1].pageY);\r
+                       that.touchesDist = m.sqrt(c1*c1+c2*c2);\r
+\r
+                       that.zoomed = true;\r
+\r
+                       scale = 1 / that.touchesDistStart * that.touchesDist * this.scale;\r
+                       if (scale < 0.5) scale = 0.5;\r
+                       else if (scale > that.options.zoomMax) scale = that.options.zoomMax;\r
+                       that.lastScale = scale / this.scale;\r
+\r
+                       newX = this.originX - this.originX * that.lastScale + this.x,\r
+                       newY = this.originY - this.originY * that.lastScale + this.y;\r
+\r
+                       this.scroller.style[vendor + 'Transform'] = trnOpen + newX + 'px,' + newY + 'px' + trnClose + ' scale(' + scale + ')';\r
+\r
+                       return;\r
+               }\r
+\r
+               that.pointX = point.pageX;\r
+               that.pointY = point.pageY;\r
+\r
+               // Slow down if outside of the boundaries\r
+               if (newX > 0 || newX < that.maxScrollX) {\r
+                       newX = that.options.bounce ? that.x + (deltaX / 2) : newX >= 0 || that.maxScrollX >= 0 ? 0 : that.maxScrollX;\r
+               }\r
+               if (newY > 0 || newY < that.maxScrollY) { \r
+                       newY = that.options.bounce ? that.y + (deltaY / 2) : newY >= 0 || that.maxScrollY >= 0 ? 0 : that.maxScrollY;\r
+               }\r
+\r
+               if (that.absDistX < 6 && that.absDistY < 6) {\r
+                       that.distX += deltaX;\r
+                       that.distY += deltaY;\r
+                       that.absDistX = m.abs(that.distX);\r
+                       that.absDistY = m.abs(that.distY);\r
+\r
+                       return;\r
+               }\r
+\r
+               // Lock direction\r
+               if (that.options.lockDirection) {\r
+                       if (that.absDistX > that.absDistY + 5) {\r
+                               newY = that.y;\r
+                               deltaY = 0;\r
+                       } else if (that.absDistY > that.absDistX + 5) {\r
+                               newX = that.x;\r
+                               deltaX = 0;\r
+                       }\r
+               }\r
+\r
+               that.moved = true;\r
+               that._pos(newX, newY);\r
+               that.dirX = deltaX > 0 ? -1 : deltaX < 0 ? 1 : 0;\r
+               that.dirY = deltaY > 0 ? -1 : deltaY < 0 ? 1 : 0;\r
+\r
+               if (timestamp - that.startTime > 300) {\r
+                       that.startTime = timestamp;\r
+                       that.startX = that.x;\r
+                       that.startY = that.y;\r
+               }\r
+               \r
+               if (that.options.onScrollMove) that.options.onScrollMove.call(that, e);\r
+       },\r
+       \r
+       _end: function (e) {\r
+               if (hasTouch && e.touches.length != 0) return;\r
+\r
+               var that = this,\r
+                       point = hasTouch ? e.changedTouches[0] : e,\r
+                       target, ev,\r
+                       momentumX = { dist:0, time:0 },\r
+                       momentumY = { dist:0, time:0 },\r
+                       duration = (e.timeStamp || (new Date()).getTime()) - that.startTime,\r
+                       newPosX = that.x,\r
+                       newPosY = that.y,\r
+                       distX, distY,\r
+                       newDuration;\r
+\r
+               that._unbind(MOVE_EV);\r
+               that._unbind(END_EV);\r
+               that._unbind(CANCEL_EV);\r
+\r
+               if (that.options.onBeforeScrollEnd) that.options.onBeforeScrollEnd.call(that, e);\r
+\r
+               if (that.zoomed) {\r
+                       that.scale = that.scale * that.lastScale;\r
+                       that.x = that.originX - that.originX * that.lastScale + that.x;\r
+                       that.y = that.originY - that.originY * that.lastScale + that.y;\r
+\r
+                       that.scroller.style.webkitTransform = trnOpen + that.x + 'px,' + that.y + 'px' + trnClose + ' scale(' + that.scale + ')';\r
+\r
+                       that.refresh();\r
+                       return;\r
+               }\r
+\r
+               if (!that.moved) {\r
+                       if (hasTouch) {\r
+                               if (that.doubleTapTimer && that.options.zoom) {\r
+                                       // Double tapped\r
+                                       clearTimeout(that.doubleTapTimer);\r
+                                       that.doubleTapTimer = null;\r
+                                       that.zoom(that.pointX, that.pointY, that.scale == 1 ? that.options.doubleTapZoom : 1);\r
+                               } else {\r
+                                       that.doubleTapTimer = setTimeout(function () {\r
+                                               that.doubleTapTimer = null;\r
+\r
+                                               // Find the last touched element\r
+                                               target = point.target;\r
+                                               while (target.nodeType != 1) target = target.parentNode;\r
+\r
+                                               if (target.tagName != 'SELECT' && target.tagName != 'INPUT' && target.tagName != 'TEXTAREA') {\r
+                                                       ev = document.createEvent('MouseEvents');\r
+                                                       ev.initMouseEvent('click', true, true, e.view, 1,\r
+                                                               point.screenX, point.screenY, point.clientX, point.clientY,\r
+                                                               e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,\r
+                                                               0, null);\r
+                                                       ev._fake = true;\r
+                                                       target.dispatchEvent(ev);\r
+                                               }\r
+                                       }, that.options.zoom ? 250 : 0);\r
+                               }\r
+                       }\r
+\r
+                       that._resetPos(200);\r
+\r
+                       if (that.options.onTouchEnd) that.options.onTouchEnd.call(that, e);\r
+                       return;\r
+               }\r
+\r
+               if (duration < 300 && that.options.momentum) {\r
+                       momentumX = newPosX ? that._momentum(newPosX - that.startX, duration, -that.x, that.scrollerW - that.wrapperW + that.x, that.options.bounce ? that.wrapperW : 0) : momentumX;\r
+                       momentumY = newPosY ? that._momentum(newPosY - that.startY, duration, -that.y, (that.maxScrollY < 0 ? that.scrollerH - that.wrapperH + that.y : 0), that.options.bounce ? that.wrapperH : 0) : momentumY;\r
+\r
+                       newPosX = that.x + momentumX.dist;\r
+                       newPosY = that.y + momentumY.dist;\r
+\r
+                       if ((that.x > 0 && newPosX > 0) || (that.x < that.maxScrollX && newPosX < that.maxScrollX)) momentumX = { dist:0, time:0 };\r
+                       if ((that.y > 0 && newPosY > 0) || (that.y < that.maxScrollY && newPosY < that.maxScrollY)) momentumY = { dist:0, time:0 };\r
+               }\r
+\r
+               if (momentumX.dist || momentumY.dist) {\r
+                       newDuration = m.max(m.max(momentumX.time, momentumY.time), 10);\r
+\r
+                       // Do we need to snap?\r
+                       if (that.options.snap) {\r
+                               distX = newPosX - that.absStartX;\r
+                               distY = newPosY - that.absStartY;\r
+                               if (m.abs(distX) < that.options.snapThreshold && m.abs(distY) < that.options.snapThreshold) { that.scrollTo(that.absStartX, that.absStartY, 200); }\r
+                               else {\r
+                                       snap = that._snap(newPosX, newPosY);\r
+                                       newPosX = snap.x;\r
+                                       newPosY = snap.y;\r
+                                       newDuration = m.max(snap.time, newDuration);\r
+                               }\r
+                       }\r
+\r
+                       that.scrollTo(newPosX, newPosY, newDuration);\r
+\r
+                       if (that.options.onTouchEnd) that.options.onTouchEnd.call(that, e);\r
+                       return;\r
+               }\r
+\r
+               // Do we need to snap?\r
+               if (that.options.snap) {\r
+                       distX = newPosX - that.absStartX;\r
+                       distY = newPosY - that.absStartY;\r
+                       if (m.abs(distX) < that.options.snapThreshold && m.abs(distY) < that.options.snapThreshold) that.scrollTo(that.absStartX, that.absStartY, 200);\r
+                       else {\r
+                               snap = that._snap(that.x, that.y);\r
+                               if (snap.x != that.x || snap.y != that.y) that.scrollTo(snap.x, snap.y, snap.time);\r
+                       }\r
+\r
+                       if (that.options.onTouchEnd) that.options.onTouchEnd.call(that, e);\r
+                       return;\r
+               }\r
+\r
+               that._resetPos(200);\r
+               if (that.options.onTouchEnd) that.options.onTouchEnd.call(that, e);\r
+       },\r
+       \r
+       _resetPos: function (time) {\r
+               var that = this,\r
+                       resetX = that.x >= 0 ? 0 : that.x < that.maxScrollX ? that.maxScrollX : that.x,\r
+                       resetY = that.y >= 0 || that.maxScrollY > 0 ? 0 : that.y < that.maxScrollY ? that.maxScrollY : that.y;\r
+\r
+               if (resetX == that.x && resetY == that.y) {\r
+                       if (that.moved) {\r
+                               that.moved = false;\r
+                               if (that.options.onScrollEnd) that.options.onScrollEnd.call(that);              // Execute custom code on scroll end\r
+                       }\r
+\r
+                       if (that.hScrollbar && that.options.hideScrollbar) {\r
+                               if (vendor == 'webkit') that.hScrollbarWrapper.style[vendor + 'TransitionDelay'] = '300ms';\r
+                               that.hScrollbarWrapper.style.opacity = '0';\r
+                       }\r
+                       if (that.vScrollbar && that.options.hideScrollbar) {\r
+                               if (vendor == 'webkit') that.vScrollbarWrapper.style[vendor + 'TransitionDelay'] = '300ms';\r
+                               that.vScrollbarWrapper.style.opacity = '0';\r
+                       }\r
+\r
+                       return;\r
+               }\r
+\r
+               that.scrollTo(resetX, resetY, time || 0);\r
+       },\r
+\r
+       _wheel: function (e) {\r
+               var that = this,\r
+                       deltaX, deltaY;\r
+\r
+               if ('wheelDeltaX' in e) {\r
+                       deltaX = that.x + e.wheelDeltaX / 12,\r
+                       deltaY = that.y + e.wheelDeltaY / 12;\r
+               } else if ('detail' in e) {\r
+                       deltaX = that.x - e.detail * 3,\r
+                       deltaY = that.y - e.detail * 3;\r
+               } else {\r
+                       deltaX = that.x - e.wheelDelta,\r
+                       deltaY = that.y - e.wheelDelta;\r
+               }\r
+\r
+               if (deltaX > 0) deltaX = 0;\r
+               else if (deltaX < that.maxScrollX) deltaX = that.maxScrollX;\r
+\r
+               if (deltaY > 0) deltaY = 0;\r
+               else if (deltaY < that.maxScrollY) deltaY = that.maxScrollY;\r
+\r
+               that.scrollTo(deltaX, deltaY, 0);\r
+       },\r
+       \r
+       _mouseout: function (e) {\r
+               var t = e.relatedTarget;\r
+\r
+               if (!t) {\r
+                       this._end(e);\r
+                       return;\r
+               }\r
+\r
+               while (t = t.parentNode) if (t == this.wrapper) return;\r
+               \r
+               this._end(e);\r
+       },\r
+\r
+       _transitionEnd: function (e) {\r
+               var that = this;\r
+\r
+               if (e.target != that.scroller) return;\r
+\r
+               that._unbind('webkitTransitionEnd');\r
+               \r
+               that._startAni();\r
+       },\r
+\r
+\r
+       /**\r
+        *\r
+        * Utilities\r
+        *\r
+        */\r
+       _startAni: function () {\r
+               var that = this,\r
+                       startX = that.x, startY = that.y,\r
+                       startTime = (new Date).getTime(),\r
+                       step, easeOut;\r
+\r
+               if (that.animating) return;\r
+               \r
+               if (!that.steps.length) {\r
+                       that._resetPos(400);\r
+                       return;\r
+               }\r
+               \r
+               step = that.steps.shift();\r
+               \r
+               if (step.x == startX && step.y == startY) step.time = 0;\r
+\r
+               that.animating = true;\r
+               that.moved = true;\r
+               \r
+               if (that.options.useTransition) {\r
+                       that._transitionTime(step.time);\r
+                       that._pos(step.x, step.y);\r
+                       that.animating = false;\r
+                       if (step.time) that._bind('webkitTransitionEnd');\r
+                       return;\r
+               }\r
+\r
+               (function animate () {\r
+                       var now = (new Date).getTime(),\r
+                               newX, newY;\r
+\r
+                       if (now >= startTime + step.time) {\r
+                               that._pos(step.x, step.y);\r
+                               that.animating = false;\r
+                               if (that.options.onAnimationEnd) that.options.onAnimationEnd.call(that);                        // Execute custom code on animation end\r
+                               that._startAni();\r
+                               return;\r
+                       }\r
+\r
+                       now = (now - startTime) / step.time - 1;\r
+                       easeOut = m.sqrt(1 - now * now);\r
+                       newX = (step.x - startX) * easeOut + startX;\r
+                       newY = (step.y - startY) * easeOut + startY;\r
+                       that._pos(newX, newY);\r
+                       if (that.animating) that.aniTime = nextFrame(animate);\r
+               })();\r
+       },\r
+\r
+       _transitionTime: function (time) {\r
+               time += 'ms';\r
+               this.scroller.style[vendor + 'TransitionDuration'] = time;\r
+               if (this.hScrollbar) this.hScrollbarIndicator.style[vendor + 'TransitionDuration'] = time;\r
+               if (this.vScrollbar) this.vScrollbarIndicator.style[vendor + 'TransitionDuration'] = time;\r
+       },\r
+\r
+       _momentum: function (dist, time, maxDistUpper, maxDistLower, size) {\r
+               var deceleration = 0.0006,\r
+                       speed = m.abs(dist) / time,\r
+                       newDist = (speed * speed) / (2 * deceleration),\r
+                       newTime = 0, outsideDist = 0;\r
+\r
+               // Proportinally reduce speed if we are outside of the boundaries \r
+               if (dist > 0 && newDist > maxDistUpper) {\r
+                       outsideDist = size / (6 / (newDist / speed * deceleration));\r
+                       maxDistUpper = maxDistUpper + outsideDist;\r
+                       speed = speed * maxDistUpper / newDist;\r
+                       newDist = maxDistUpper;\r
+               } else if (dist < 0 && newDist > maxDistLower) {\r
+                       outsideDist = size / (6 / (newDist / speed * deceleration));\r
+                       maxDistLower = maxDistLower + outsideDist;\r
+                       speed = speed * maxDistLower / newDist;\r
+                       newDist = maxDistLower;\r
+               }\r
+\r
+               newDist = newDist * (dist < 0 ? -1 : 1);\r
+               newTime = speed / deceleration;\r
+\r
+               return { dist: newDist, time: m.round(newTime) };\r
+       },\r
+\r
+       _offset: function (el) {\r
+               var left = -el.offsetLeft,\r
+                       top = -el.offsetTop;\r
+                       \r
+               while (el = el.offsetParent) {\r
+                       left -= el.offsetLeft;\r
+                       top -= el.offsetTop;\r
+               }\r
+               \r
+               if (el != this.wrapper) {\r
+                       left *= this.scale;\r
+                       top *= this.scale;\r
+               }\r
+\r
+               return { left: left, top: top };\r
+       },\r
+\r
+       _snap: function (x, y) {\r
+               var that = this,\r
+                       i, l,\r
+                       page, time,\r
+                       sizeX, sizeY;\r
+\r
+               // Check page X\r
+               page = that.pagesX.length - 1;\r
+               for (i=0, l=that.pagesX.length; i<l; i++) {\r
+                       if (x >= that.pagesX[i]) {\r
+                               page = i;\r
+                               break;\r
+                       }\r
+               }\r
+               if (page == that.currPageX && page > 0 && that.dirX < 0) page--;\r
+               x = that.pagesX[page];\r
+               sizeX = m.abs(x - that.pagesX[that.currPageX]);\r
+               sizeX = sizeX ? m.abs(that.x - x) / sizeX * 500 : 0;\r
+               that.currPageX = page;\r
+\r
+               // Check page Y\r
+               page = that.pagesY.length-1;\r
+               for (i=0; i<page; i++) {\r
+                       if (y >= that.pagesY[i]) {\r
+                               page = i;\r
+                               break;\r
+                       }\r
+               }\r
+               if (page == that.currPageY && page > 0 && that.dirY < 0) page--;\r
+               y = that.pagesY[page];\r
+               sizeY = m.abs(y - that.pagesY[that.currPageY]);\r
+               sizeY = sizeY ? m.abs(that.y - y) / sizeY * 500 : 0;\r
+               that.currPageY = page;\r
+\r
+               // Snap with constant speed (proportional duration)\r
+               time = m.round(m.max(sizeX, sizeY)) || 200;\r
+\r
+               return { x: x, y: y, time: time };\r
+       },\r
+\r
+       _bind: function (type, el, bubble) {\r
+               (el || this.scroller).addEventListener(type, this, !!bubble);\r
+       },\r
+\r
+       _unbind: function (type, el, bubble) {\r
+               (el || this.scroller).removeEventListener(type, this, !!bubble);\r
+       },\r
+\r
+\r
+       /**\r
+        *\r
+        * Public methods\r
+        *\r
+        */\r
+       destroy: function () {\r
+               var that = this;\r
+\r
+               that.scroller.style[vendor + 'Transform'] = '';\r
+\r
+               // Remove the scrollbars\r
+               that.hScrollbar = false;\r
+               that.vScrollbar = false;\r
+               that._scrollbar('h');\r
+               that._scrollbar('v');\r
+\r
+               // Remove the event listeners\r
+               that._unbind(RESIZE_EV);\r
+               that._unbind(START_EV);\r
+               that._unbind(MOVE_EV);\r
+               that._unbind(END_EV);\r
+               that._unbind(CANCEL_EV);\r
+               \r
+               if (that.options.hasTouch) {\r
+                       that._unbind('mouseout', that.wrapper);\r
+                       that._unbind(WHEEL_EV);\r
+               }\r
+               \r
+               if (that.options.useTransition) that._unbind('webkitTransitionEnd');\r
+               \r
+               if (that.options.onDestroy) that.options.onDestroy.call(that);\r
+       },\r
+\r
+       refresh: function () {\r
+               var that = this,\r
+                       offset,\r
+                       pos = 0,\r
+                       page = 0;\r
+\r
+               if (that.scale < that.options.zoomMin) that.scale = that.options.zoomMin;\r
+               that.wrapperW = that.wrapper.clientWidth || 1;\r
+               that.wrapperH = that.wrapper.clientHeight || 1;\r
+\r
+               that.scrollerW = m.round(that.scroller.offsetWidth * that.scale);\r
+               that.scrollerH = m.round(that.scroller.offsetHeight * that.scale);\r
+               that.maxScrollX = that.wrapperW - that.scrollerW;\r
+               that.maxScrollY = that.wrapperH - that.scrollerH;\r
+               that.dirX = 0;\r
+               that.dirY = 0;\r
+\r
+               that.hScroll = that.options.hScroll && that.maxScrollX < 0;\r
+               that.vScroll = that.options.vScroll && (!that.options.bounceLock && !that.hScroll || that.scrollerH > that.wrapperH);\r
+\r
+               that.hScrollbar = that.hScroll && that.options.hScrollbar;\r
+               that.vScrollbar = that.vScroll && that.options.vScrollbar && that.scrollerH > that.wrapperH;\r
+\r
+               offset = that._offset(that.wrapper);\r
+               that.wrapperOffsetLeft = -offset.left;\r
+               that.wrapperOffsetTop = -offset.top;\r
+\r
+               // Prepare snap\r
+               if (typeof that.options.snap == 'string') {\r
+                       that.pagesX = [];\r
+                       that.pagesY = [];\r
+                       els = that.scroller.querySelectorAll(that.options.snap);\r
+                       for (i=0, l=els.length; i<l; i++) {\r
+                               pos = that._offset(els[i]);\r
+                               pos.left += that.wrapperOffsetLeft;\r
+                               pos.top += that.wrapperOffsetTop;\r
+                               that.pagesX[i] = pos.left < that.maxScrollX ? that.maxScrollX : pos.left * that.scale;\r
+                               that.pagesY[i] = pos.top < that.maxScrollY ? that.maxScrollY : pos.top * that.scale;\r
+                       }\r
+               } else if (that.options.snap) {\r
+                       that.pagesX = [];\r
+                       while (pos >= that.maxScrollX) {\r
+                               that.pagesX[page] = pos;\r
+                               pos = pos - that.wrapperW;\r
+                               page++;\r
+                       }\r
+                       if (that.maxScrollX%that.wrapperW) that.pagesX[that.pagesX.length] = that.maxScrollX - that.pagesX[that.pagesX.length-1] + that.pagesX[that.pagesX.length-1];\r
+\r
+                       pos = 0;\r
+                       page = 0;\r
+                       that.pagesY = [];\r
+                       while (pos >= that.maxScrollY) {\r
+                               that.pagesY[page] = pos;\r
+                               pos = pos - that.wrapperH;\r
+                               page++;\r
+                       }\r
+                       if (that.maxScrollY%that.wrapperH) that.pagesY[that.pagesY.length] = that.maxScrollY - that.pagesY[that.pagesY.length-1] + that.pagesY[that.pagesY.length-1];\r
+               }\r
+\r
+               // Prepare the scrollbars\r
+               that._scrollbar('h');\r
+               that._scrollbar('v');\r
+\r
+               that.scroller.style[vendor + 'TransitionDuration'] = '0';\r
+\r
+               that._resetPos(200);\r
+       },\r
+\r
+       scrollTo: function (x, y, time, relative) {\r
+               var that = this,\r
+                       step = x,\r
+                       i, l;\r
+\r
+               that.stop();\r
+\r
+               if (!step.length) step = [{ x: x, y: y, time: time, relative: relative }];\r
+               \r
+               for (i=0, l=step.length; i<l; i++) {\r
+                       if (step[i].relative) { step[i].x = that.x - step[i].x; step[i].y = that.y - step[i].y; }\r
+                       that.steps.push({ x: step[i].x, y: step[i].y, time: step[i].time || 0 });\r
+               }\r
+\r
+               that._startAni();\r
+       },\r
+\r
+       scrollToElement: function (el, time) {\r
+               var that = this, pos;\r
+               el = el.nodeType ? el : that.scroller.querySelector(el);\r
+               if (!el) return;\r
+\r
+               pos = that._offset(el);\r
+               pos.left += that.wrapperOffsetLeft;\r
+               pos.top += that.wrapperOffsetTop;\r
+\r
+               pos.left = pos.left > 0 ? 0 : pos.left < that.maxScrollX ? that.maxScrollX : pos.left;\r
+               pos.top = pos.top > 0 ? 0 : pos.top < that.maxScrollY ? that.maxScrollY : pos.top;\r
+               time = time === undefined ? m.max(m.abs(pos.left)*2, m.abs(pos.top)*2) : time;\r
+\r
+               that.scrollTo(pos.left, pos.top, time);\r
+       },\r
+\r
+       scrollToPage: function (pageX, pageY, time) {\r
+               var that = this, x, y;\r
+               \r
+               if (that.options.snap) {\r
+                       pageX = pageX == 'next' ? that.currPageX+1 : pageX == 'prev' ? that.currPageX-1 : pageX;\r
+                       pageY = pageY == 'next' ? that.currPageY+1 : pageY == 'prev' ? that.currPageY-1 : pageY;\r
+\r
+                       pageX = pageX < 0 ? 0 : pageX > that.pagesX.length-1 ? that.pagesX.length-1 : pageX;\r
+                       pageY = pageY < 0 ? 0 : pageY > that.pagesY.length-1 ? that.pagesY.length-1 : pageY;\r
+\r
+                       that.currPageX = pageX;\r
+                       that.currPageY = pageY;\r
+                       x = that.pagesX[pageX];\r
+                       y = that.pagesY[pageY];\r
+               } else {\r
+                       x = -that.wrapperW * pageX;\r
+                       y = -that.wrapperH * pageY;\r
+                       if (x < that.maxScrollX) x = that.maxScrollX;\r
+                       if (y < that.maxScrollY) y = that.maxScrollY;\r
+               }\r
+\r
+               that.scrollTo(x, y, time || 400);\r
+       },\r
+\r
+       disable: function () {\r
+               this.stop();\r
+               this._resetPos(0);\r
+               this.enabled = false;\r
+\r
+               // If disabled after touchstart we make sure that there are no left over events\r
+               this._unbind(MOVE_EV);\r
+               this._unbind(END_EV);\r
+               this._unbind(CANCEL_EV);\r
+       },\r
+       \r
+       enable: function () {\r
+               this.enabled = true;\r
+       },\r
+       \r
+       stop: function () {\r
+               if (this.options.useTransition) this._unbind('webkitTransitionEnd');\r
+               else cancelFrame(this.aniTime);\r
+               this.steps = [];\r
+               this.moved = false;\r
+               this.animating = false;\r
+       },\r
+       \r
+       zoom: function (x, y, scale, time) {\r
+               var that = this,\r
+                       relScale = scale / that.scale;\r
+\r
+               if (!that.options.useTransform) return;\r
+\r
+               time = (time || 200) + 'ms'\r
+               x = x - that.wrapperOffsetLeft - that.x;\r
+               y = y - that.wrapperOffsetTop - that.y;\r
+               that.x = x - x * relScale + that.x;\r
+               that.y = y - y * relScale + that.y;\r
+\r
+               that.scale = scale;\r
+\r
+               that.scroller.style[vendor + 'TransitionDuration'] = time;\r
+               that.scroller.style[vendor + 'Transform'] = trnOpen + that.x + 'px,' + that.y + 'px' + trnClose + ' scale(' + scale + ')';\r
+\r
+               that.refresh();\r
+       }\r
+};\r
+\r
+if (typeof exports !== 'undefined') exports.iScroll = iScroll;\r
+else window.iScroll = iScroll;\r
+\r
+})();
\ No newline at end of file
diff --git a/libs/jsmin.php b/libs/jsmin.php
new file mode 100644 (file)
index 0000000..5c3f881
--- /dev/null
@@ -0,0 +1,375 @@
+<?php
+/**
+ * jsmin.php - PHP implementation of Douglas Crockford's JSMin.
+ *
+ * This is pretty much a direct port of jsmin.c to PHP with just a few
+ * PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and
+ * outputs to stdout, this library accepts a string as input and returns another
+ * string as output.
+ *
+ * PHP 5 or higher is required.
+ *
+ * Permission is hereby granted to use this version of the library under the
+ * same terms as jsmin.c, which has the following license:
+ *
+ * --
+ * Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * The Software shall be used for Good, not Evil.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ * --
+ *
+ * @package JSMin
+ * @author Ryan Grove <ryan@wonko.com>
+ * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
+ * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
+ * @license http://opensource.org/licenses/mit-license.php MIT License
+ * @version 1.1.1 (2008-03-02)
+ * @link https://github.com/rgrove/jsmin-php/
+ */
+
+class JSMin {
+  const ORD_LF            = 10;
+  const ORD_SPACE         = 32;
+  const ACTION_KEEP_A     = 1;
+  const ACTION_DELETE_A   = 2;
+  const ACTION_DELETE_A_B = 3;
+
+  protected $a           = '';
+  protected $b           = '';
+  protected $input       = '';
+  protected $inputIndex  = 0;
+  protected $inputLength = 0;
+  protected $lookAhead   = null;
+  protected $output      = '';
+
+  // -- Public Static Methods --------------------------------------------------
+
+  /**
+   * Minify Javascript
+   *
+   * @uses __construct()
+   * @uses min()
+   * @param string $js Javascript to be minified
+   * @return string
+   */
+  public static function minify($js) {
+    $jsmin = new JSMin($js);
+    return $jsmin->min();
+  }
+
+  // -- Public Instance Methods ------------------------------------------------
+
+  /**
+   * Constructor
+   *
+   * @param string $input Javascript to be minified
+   */
+  public function __construct($input) {
+    $this->input       = str_replace("\r\n", "\n", $input);
+    $this->inputLength = strlen($this->input);
+  }
+
+  // -- Protected Instance Methods ---------------------------------------------
+
+  /**
+   * Action -- do something! What to do is determined by the $command argument.
+   *
+   * action treats a string as a single character. Wow!
+   * action recognizes a regular expression if it is preceded by ( or , or =.
+   *
+   * @uses next()
+   * @uses get()
+   * @throws JSMinException If parser errors are found:
+   *         - Unterminated string literal
+   *         - Unterminated regular expression set in regex literal
+   *         - Unterminated regular expression literal
+   * @param int $command One of class constants:
+   *      ACTION_KEEP_A      Output A. Copy B to A. Get the next B.
+   *      ACTION_DELETE_A    Copy B to A. Get the next B. (Delete A).
+   *      ACTION_DELETE_A_B  Get the next B. (Delete B).
+  */
+  protected function action($command) {
+    switch($command) {
+      case self::ACTION_KEEP_A:
+        $this->output .= $this->a;
+
+      case self::ACTION_DELETE_A:
+        $this->a = $this->b;
+
+        if ($this->a === "'" || $this->a === '"') {
+          for (;;) {
+            $this->output .= $this->a;
+            $this->a       = $this->get();
+
+            if ($this->a === $this->b) {
+              break;
+            }
+
+            if (ord($this->a) <= self::ORD_LF) {
+              throw new JSMinException('Unterminated string literal.');
+            }
+
+            if ($this->a === '\\') {
+              $this->output .= $this->a;
+              $this->a       = $this->get();
+            }
+          }
+        }
+
+      case self::ACTION_DELETE_A_B:
+        $this->b = $this->next();
+
+        if ($this->b === '/' && (
+            $this->a === '(' || $this->a === ',' || $this->a === '=' ||
+            $this->a === ':' || $this->a === '[' || $this->a === '!' ||
+            $this->a === '&' || $this->a === '|' || $this->a === '?' ||
+            $this->a === '{' || $this->a === '}' || $this->a === ';' ||
+            $this->a === "\n" )) {
+
+          $this->output .= $this->a . $this->b;
+
+          for (;;) {
+            $this->a = $this->get();
+
+            if ($this->a === '[') {
+              /*
+                inside a regex [...] set, which MAY contain a '/' itself. Example: mootools Form.Validator near line 460:
+                  return Form.Validator.getValidator('IsEmpty').test(element) || (/^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]\.?){0,63}[a-z0-9!#$%&'*+/=?^_`{|}~-]@(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\])$/i).test(element.get('value'));
+              */
+              for (;;) {
+                $this->output .= $this->a;
+                $this->a = $this->get();
+
+                if ($this->a === ']') {
+                    break;
+                } elseif ($this->a === '\\') {
+                  $this->output .= $this->a;
+                  $this->a       = $this->get();
+                } elseif (ord($this->a) <= self::ORD_LF) {
+                  throw new JSMinException('Unterminated regular expression set in regex literal.');
+                }
+              }
+            } elseif ($this->a === '/') {
+              break;
+            } elseif ($this->a === '\\') {
+              $this->output .= $this->a;
+              $this->a       = $this->get();
+            } elseif (ord($this->a) <= self::ORD_LF) {
+              throw new JSMinException('Unterminated regular expression literal.');
+            }
+
+            $this->output .= $this->a;
+          }
+
+          $this->b = $this->next();
+        }
+    }
+  }
+
+  /**
+   * Get next char. Convert ctrl char to space.
+   *
+   * @return string|null
+   */
+  protected function get() {
+    $c = $this->lookAhead;
+    $this->lookAhead = null;
+
+    if ($c === null) {
+      if ($this->inputIndex < $this->inputLength) {
+        $c = substr($this->input, $this->inputIndex, 1);
+        $this->inputIndex += 1;
+      } else {
+        $c = null;
+      }
+    }
+
+    if ($c === "\r") {
+      return "\n";
+    }
+
+    if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) {
+      return $c;
+    }
+
+    return ' ';
+  }
+
+  /**
+   * Is $c a letter, digit, underscore, dollar sign, or non-ASCII character.
+   *
+   * @return bool
+   */
+  protected function isAlphaNum($c) {
+    return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1;
+  }
+
+  /**
+   * Perform minification, return result
+   *
+   * @uses action()
+   * @uses isAlphaNum()
+   * @return string
+   */
+  protected function min() {
+    $this->a = "\n";
+    $this->action(self::ACTION_DELETE_A_B);
+
+    while ($this->a !== null) {
+      switch ($this->a) {
+        case ' ':
+          if ($this->isAlphaNum($this->b)) {
+            $this->action(self::ACTION_KEEP_A);
+          } else {
+            $this->action(self::ACTION_DELETE_A);
+          }
+          break;
+
+        case "\n":
+          switch ($this->b) {
+            case '{':
+            case '[':
+            case '(':
+            case '+':
+            case '-':
+              $this->action(self::ACTION_KEEP_A);
+              break;
+
+            case ' ':
+              $this->action(self::ACTION_DELETE_A_B);
+              break;
+
+            default:
+              if ($this->isAlphaNum($this->b)) {
+                $this->action(self::ACTION_KEEP_A);
+              }
+              else {
+                $this->action(self::ACTION_DELETE_A);
+              }
+          }
+          break;
+
+        default:
+          switch ($this->b) {
+            case ' ':
+              if ($this->isAlphaNum($this->a)) {
+                $this->action(self::ACTION_KEEP_A);
+                break;
+              }
+
+              $this->action(self::ACTION_DELETE_A_B);
+              break;
+
+            case "\n":
+              switch ($this->a) {
+                case '}':
+                case ']':
+                case ')':
+                case '+':
+                case '-':
+                case '"':
+                case "'":
+                  $this->action(self::ACTION_KEEP_A);
+                  break;
+
+                default:
+                  if ($this->isAlphaNum($this->a)) {
+                    $this->action(self::ACTION_KEEP_A);
+                  }
+                  else {
+                    $this->action(self::ACTION_DELETE_A_B);
+                  }
+              }
+              break;
+
+            default:
+              $this->action(self::ACTION_KEEP_A);
+              break;
+          }
+      }
+    }
+
+    return $this->output;
+  }
+
+  /**
+   * Get the next character, skipping over comments. peek() is used to see
+   *  if a '/' is followed by a '/' or '*'.
+   *
+   * @uses get()
+   * @uses peek()
+   * @throws JSMinException On unterminated comment.
+   * @return string
+   */
+  protected function next() {
+    $c = $this->get();
+
+    if ($c === '/') {
+      switch($this->peek()) {
+        case '/':
+          for (;;) {
+            $c = $this->get();
+
+            if (ord($c) <= self::ORD_LF) {
+              return $c;
+            }
+          }
+
+        case '*':
+          $this->get();
+
+          for (;;) {
+            switch($this->get()) {
+              case '*':
+                if ($this->peek() === '/') {
+                  $this->get();
+                  return ' ';
+                }
+                break;
+
+              case null:
+                throw new JSMinException('Unterminated comment.');
+            }
+          }
+
+        default:
+          return $c;
+      }
+    }
+
+    return $c;
+  }
+
+  /**
+   * Get next char. If is ctrl character, translate to a space or newline.
+   *
+   * @uses get()
+   * @return string|null
+   */
+  protected function peek() {
+    $this->lookAhead = $this->get();
+    return $this->lookAhead;
+  }
+}
+
+// -- Exceptions ---------------------------------------------------------------
+class JSMinException extends Exception {}
+?>
\ No newline at end of file