app/assets/javascripts/ionic/ionic.js in ionic-rails-engine-0.9.17 vs app/assets/javascripts/ionic/ionic.js in ionic-rails-engine-0.9.26

- old
+ new

@@ -1,25 +1,28 @@ /*! - * Copyright 2013 Drifty Co. + * Copyright 2014 Drifty Co. * http://drifty.com/ * - * Ionic, v0.9.17 + * Ionic, v0.9.26 * A powerful HTML5 mobile app framework. * http://ionicframework.com/ * - * By @maxlynch, @helloimben, @adamdbradley <3 + * By @maxlynch, @benjsperry, @adamdbradley <3 * * Licensed under the MIT license. Please see LICENSE for more information. * - */; + */ +; -// Create namespaces +// Create namespaces +// window.ionic = { controllers: {}, views: {}, - version: '0.9.17' -};; + version: '0.9.26' +}; +; (function(ionic) { var bezierCoord = function (x,y) { if(!x) x=0; if(!y) y=0; @@ -131,29 +134,106 @@ } }; })(ionic); ; (function(ionic) { + + var readyCallbacks = [], + domReady = function() { + for(var x=0; x<readyCallbacks.length; x++) { + ionic.requestAnimationFrame(readyCallbacks[x]); + } + readyCallbacks = []; + document.removeEventListener('DOMContentLoaded', domReady); + }; + document.addEventListener('DOMContentLoaded', domReady); + + // From the man himself, Mr. Paul Irish. + // The requestAnimationFrame polyfill + // Put it on window just to preserve its context + // without having to use .call + window._rAF = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + function( callback ){ + window.setTimeout(callback, 16); + }; + })(); + ionic.DomUtil = { + //Call with proper context + requestAnimationFrame: function(cb) { + window._rAF(cb); + }, + + /* + * When given a callback, if that callback is called 100 times between + * animation frames, Throttle will make it only call the last of 100tha call + * + * It returns a function, which will then call the passed in callback. The + * passed in callback will receive the context the returned function is called with. + * + * @example + * this.setTranslateX = ionic.animationFrameThrottle(function(x) { + * this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(' + x + 'px, 0, 0)'; + * }) + */ + animationFrameThrottle: function(cb) { + var args, isQueued, context; + return function() { + args = arguments; + context = this; + if (!isQueued) { + isQueued = true; + ionic.requestAnimationFrame(function() { + cb.apply(context, args); + isQueued = false; + }); + } + }; + }, + + /* + * Find an element's offset, then add it to the offset of the parent + * until we are at the direct child of parentEl + * use-case: find scroll offset of any element within a scroll container + */ + getPositionInParent: function(el) { + return { + left: el.offsetLeft, + top: el.offsetTop + }; + }, + + ready: function(cb) { + if(document.readyState === "complete") { + ionic.requestAnimationFrame(cb); + } else { + readyCallbacks.push(cb); + } + }, + getTextBounds: function(textNode) { if(document.createRange) { var range = document.createRange(); range.selectNodeContents(textNode); if(range.getBoundingClientRect) { var rect = range.getBoundingClientRect(); + if(rect) { + var sx = window.scrollX; + var sy = window.scrollY; - var sx = window.scrollX; - var sy = window.scrollY; - - return { - top: rect.top + sy, - left: rect.left + sx, - right: rect.left + sx + rect.width, - bottom: rect.top + sy + rect.height, - width: rect.width, - height: rect.height - }; + return { + top: rect.top + sy, + left: rect.left + sx, + right: rect.left + sx + rect.width, + bottom: rect.top + sy + rect.height, + width: rect.width, + height: rect.height + }; + } } } return null; }, @@ -197,20 +277,30 @@ return e; } e = e.parentNode; } return null; + }, + + rectContains: function(x, y, x1, y1, x2, y2) { + if(x < x1 || x > x2) return false; + if(y < y1 || y > y2) return false; + return true; } }; + + //Shortcuts + ionic.requestAnimationFrame = ionic.DomUtil.requestAnimationFrame; + ionic.animationFrameThrottle = ionic.DomUtil.animationFrameThrottle; })(window.ionic); ; /** * ion-events.js * * Author: Max Lynch <max@drifty.com> * - * Framework events handles various mobile browser events, and + * Framework events handles various mobile browser events, and * detects special events like tap/swipe/etc. and emits them * as custom events that can be used in an app. * * Portions lovingly adapted from github.com/maker/ratchet and github.com/alexgibson/tap.js - thanks guys! */ @@ -227,12 +317,21 @@ params = params || { bubbles: false, cancelable: false, detail: undefined }; - evt = document.createEvent("CustomEvent"); - evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + try { + evt = document.createEvent("CustomEvent"); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + } catch (error) { + // fallback for browsers that don't support createEvent('CustomEvent') + evt = document.createEvent("Event"); + for (var param in params) { + evt[param] = params[param]; + } + evt.initEvent(event, params.bubbles, params.cancelable); + } return evt; }; CustomEvent.prototype = window.Event.prototype; @@ -242,18 +341,22 @@ ionic.EventController = { VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'], // Trigger a new event - trigger: function(eventType, data) { - var event = new CustomEvent(eventType, { detail: data }); + trigger: function(eventType, data, bubbles, cancelable) { + var event = new CustomEvent(eventType, { + detail: data, + bubbles: !!bubbles, + cancelable: !!cancelable + }); // Make sure to trigger the event on the given target, or dispatch it from // the window if we don't have an event target data && data.target && data.target.dispatchEvent(event) || window.dispatchEvent(event); }, - + // Bind an event on: function(type, callback, element) { var e = element || window; // Bind a gesture if it's a virtual event @@ -286,12 +389,12 @@ }, handlePopState: function(event) { }, }; - - + + // Map some convenient top-level functions for event handling ionic.on = function() { ionic.EventController.on.apply(ionic.EventController, arguments); }; ionic.off = function() { ionic.EventController.off.apply(ionic.EventController, arguments); }; ionic.trigger = ionic.EventController.trigger;//function() { ionic.EventController.trigger.apply(ionic.EventController.trigger, arguments); }; ionic.onGesture = function() { return ionic.EventController.onGesture.apply(ionic.EventController.onGesture, arguments); }; @@ -301,11 +404,11 @@ ; /** * Simple gesture controllers with some common gestures that emit * gesture events. * - * Ported from github.com/EightMedia/ionic.Gestures.js - thanks! + * Ported from github.com/EightMedia/hammer.js Gestures - thanks! */ (function(ionic) { /** * ionic.Gestures @@ -321,27 +424,15 @@ ionic.Gestures = {}; // default settings ionic.Gestures.defaults = { - // add styles and attributes to the element to prevent the browser from doing - // its native behavior. this doesnt prevent the scrolling, but cancels - // the contextmenu, tap highlighting etc + // add css to the element to prevent the browser from doing + // its native behavior. this doesnt prevent the scrolling, + // but cancels the contextmenu, tap highlighting etc // set to false to disable this - stop_browser_behavior: { - // this also triggers onselectstart=false for IE - userSelect: 'none', - // this makes the element blocking in IE10 >, you could experiment with the value - // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241 - touchAction: 'none', - touchCallout: 'none', - contentZooming: 'none', - userDrag: 'none', - tapHighlightColor: 'rgba(0,0,0,0)' - } - - // more settings are defined per gesture at gestures.js + stop_browser_behavior: 'disable-user-behavior' }; // detect touchevents ionic.Gestures.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled; ionic.Gestures.HAS_TOUCHEVENTS = ('ontouchstart' in window); @@ -409,10 +500,11 @@ * create new hammer instance * all methods should return the instance itself, so it is chainable. * @param {HTMLElement} element * @param {Object} [options={}] * @returns {ionic.Gestures.Instance} + * @name Gesture.Instance * @constructor */ ionic.Gestures.Instance = function(element, options) { var self = this; @@ -1002,41 +1094,20 @@ return (direction == ionic.Gestures.DIRECTION_UP || direction == ionic.Gestures.DIRECTION_DOWN); }, /** - * stop browser default behavior with css props + * stop browser default behavior with css class * @param {HtmlElement} element - * @param {Object} css_props + * @param {Object} css_class */ - stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) { - var prop, - vendors = ['webkit','khtml','moz','Moz','ms','o','']; - - if(!css_props || !element.style) { - return; - } - - // with css properties for modern browsers - for(var i = 0; i < vendors.length; i++) { - for(var p in css_props) { - if(css_props.hasOwnProperty(p)) { - prop = p; - - // vender prefix at the property - if(vendors[i]) { - prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); - } - - // set the style - element.style[prop] = css_props[p]; - } - } - } - - // also the disable onselectstart - if(css_props.userSelect == 'none') { + stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_class) { + // changed from making many style changes to just adding a preset classname + // less DOM manipulations, less code, and easier to control in the CSS side of things + // hammer.js doesn't come with CSS, but ionic does, which is why we prefer this method + if(element && element.classList) { + element.classList.add(css_class); element.onselectstart = function() { return false; }; } } @@ -1729,178 +1800,489 @@ })(window.ionic); ; (function(ionic) { ionic.Platform = { + + isReady: false, + isFullScreen: false, + platforms: null, + grade: null, + ua: navigator.userAgent, + + ready: function(cb) { + // run through tasks to complete now that the device is ready + if(this.isReady) { + cb(); + } else { + // the platform isn't ready yet, add it to this array + // which will be called once the platform is ready + readyCallbacks.push(cb); + } + }, + detect: function() { - var platforms = []; + var i, bodyClass = document.body.className; - this._checkPlatforms(platforms); + ionic.Platform._checkPlatforms(); - var classify = function() { - if(!document.body) { return; } + // only change the body class if we got platform info + for(i = 0; i < this.platforms.length; i++) { + bodyClass += ' platform-' + this.platforms[i]; + } - for(var i = 0; i < platforms.length; i++) { - document.body.classList.add('platform-' + platforms[i]); - } - }; + bodyClass += ' grade-' + this.grade; - document.addEventListener( "DOMContentLoaded", function(){ - classify(); - }); + document.body.className = bodyClass.trim(); + }, - classify(); + device: function() { + if(window.device) return window.device; + if(this.isCordova()) console.error('device plugin required'); + return {}; }, + _checkPlatforms: function(platforms) { - if(this.isCordova()) { - platforms.push('cordova'); + this.platforms = []; + this.grade = 'a'; + + if(this.isCordova()) this.platforms.push('cordova'); + if(this.isIPad()) this.platforms.push('ipad'); + + var platform = this.platform(); + if(platform) { + this.platforms.push(platform); + + var version = this.version(); + if(version) { + var v = version.toString(); + if(v.indexOf('.') > 0) { + v = v.replace('.', '_'); + } else { + v += '_0'; + } + this.platforms.push(platform + v.split('_')[0]); + this.platforms.push(platform + v); + + if(this.isAndroid() && version < 4.4) { + this.grade = (version < 4 ? 'c' : 'b'); + } + } } - if(this.isIOS7()) { - platforms.push('ios7'); - } - if(this.isIPad()) { - platforms.push('ipad'); - } - if(this.isAndroid()) { - platforms.push('android'); - } }, - // Check if we are running in Cordova, which will have - // window.device available. + // Check if we are running in Cordova isCordova: function() { - return (window.cordova || window.PhoneGap || window.phonegap); - //&& /^file:\/{3}[^\/]/i.test(window.location.href) - //&& /ios|iphone|ipod|ipad|android/i.test(navigator.userAgent); + return !(!window.cordova && !window.PhoneGap && !window.phonegap); }, isIPad: function() { - return navigator.userAgent.toLowerCase().indexOf('ipad') >= 0; + return this.ua.toLowerCase().indexOf('ipad') >= 0; }, - isIOS7: function() { - if(!window.device) { - return false; - } - return parseFloat(window.device.version) >= 7.0; + isIOS: function() { + return this.is('ios'); }, isAndroid: function() { - if(!window.device) { - return navigator.userAgent.toLowerCase().indexOf('android') >= 0; + return this.is('android'); + }, + + platform: function() { + // singleton to get the platform name + if(platformName === null) this.setPlatform(this.device().platform); + return platformName; + }, + + setPlatform: function(n) { + if(typeof n != 'undefined' && n !== null && n.length) { + platformName = n.toLowerCase(); + } else if(this.ua.indexOf('Android') > 0) { + platformName = 'android'; + } else if(this.ua.indexOf('iPhone') > -1 || this.ua.indexOf('iPad') > -1 || this.ua.indexOf('iPod') > -1) { + platformName = 'ios'; + } else { + platformName = 'unknown'; } - return device.platform === "Android"; + }, + + version: function() { + // singleton to get the platform version + if(platformVersion === null) this.setVersion(this.device().version); + return platformVersion; + }, + + setVersion: function(v) { + if(typeof v != 'undefined' && v !== null) { + v = v.split('.'); + v = parseFloat(v[0] + '.' + (v.length > 1 ? v[1] : 0)); + if(!isNaN(v)) { + platformVersion = v; + return; + } + } + + platformVersion = 0; + + // fallback to user-agent checking + var pName = this.platform(); + var versionMatch = { + 'android': /Android (\d+).(\d+)?/, + 'ios': /OS (\d+)_(\d+)?/ + }; + if(versionMatch[pName]) { + v = this.ua.match( versionMatch[pName] ); + if(v.length > 2) { + platformVersion = parseFloat( v[1] + '.' + v[2] ); + } + } + }, + + // Check if the platform is the one detected by cordova + is: function(type) { + type = type.toLowerCase(); + // check if it has an array of platforms + if(this.platforms) { + for(var x = 0; x < this.platforms.length; x++) { + if(this.platforms[x] === type) return true; + } + } + // exact match + var pName = this.platform(); + if(pName) { + return pName === type.toLowerCase(); + } + + // A quick hack for to check userAgent + return this.ua.toLowerCase().indexOf(type) >= 0; + }, + + exitApp: function() { + this.ready(function(){ + navigator.app && navigator.app.exitApp && navigator.app.exitApp(); + }); + }, + + showStatusBar: function(val) { + // Only useful when run within cordova + this.showStatusBar = val; + this.ready(function(){ + // run this only when or if the platform (cordova) is ready + if(ionic.Platform.showStatusBar) { + // they do not want it to be full screen + StatusBar.show(); + document.body.classList.remove('status-bar-hide'); + } else { + // it should be full screen + StatusBar.hide(); + document.body.classList.add('status-bar-hide'); + } + }); + }, + + fullScreen: function(showFullScreen, showStatusBar) { + // fullScreen( [showFullScreen[, showStatusBar] ] ) + // showFullScreen: default is true if no param provided + this.isFullScreen = (showFullScreen !== false); + + // add/remove the fullscreen classname to the body + ionic.DomUtil.ready(function(){ + // run this only when or if the DOM is ready + if(ionic.Platform.isFullScreen) { + document.body.classList.add('fullscreen'); + } else { + document.body.classList.remove('fullscreen'); + } + }); + + // showStatusBar: default is false if no param provided + this.showStatusBar( (showStatusBar === true) ); } + }; - ionic.Platform.detect(); + var platformName = null, // just the name, like iOS or Android + platformVersion = null, // a float of the major and minor, like 7.1 + readyCallbacks = []; + + // setup listeners to know when the device is ready to go + function onWindowLoad() { + if(ionic.Platform.isCordova()) { + // the window and scripts are fully loaded, and a cordova/phonegap + // object exists then let's listen for the deviceready + document.addEventListener("deviceready", onPlatformReady, false); + } else { + // the window and scripts are fully loaded, but the window object doesn't have the + // cordova/phonegap object, so its just a browser, not a webview wrapped w/ cordova + onPlatformReady(); + } + window.removeEventListener("load", onWindowLoad, false); + } + window.addEventListener("load", onWindowLoad, false); + + function onPlatformReady() { + // the device is all set to go, init our own stuff then fire off our event + ionic.Platform.isReady = true; + ionic.Platform.detect(); + for(var x=0; x<readyCallbacks.length; x++) { + // fire off all the callbacks that were added before the platform was ready + readyCallbacks[x](); + } + readyCallbacks = []; + ionic.trigger('platformready', { target: document }); + document.removeEventListener("deviceready", onPlatformReady, false); + } + })(window.ionic); ; (function(window, document, ionic) { 'use strict'; - // From the man himself, Mr. Paul Irish. - // The requestAnimationFrame polyfill - window.rAF = (function(){ - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - function( callback ){ - window.setTimeout(callback, 1000 / 60); - }; - })(); - // Ionic CSS polyfills ionic.CSS = {}; - + (function() { - var d = document.createElement('div'); var keys = ['webkitTransform', 'transform', '-webkit-transform', 'webkit-transform', '-moz-transform', 'moz-transform', 'MozTransform', 'mozTransform']; for(var i = 0; i < keys.length; i++) { - if(d.style[keys[i]] !== undefined) { + if(document.documentElement.style[keys[i]] !== undefined) { ionic.CSS.TRANSFORM = keys[i]; break; } } })(); + // classList polyfill for them older Androids + // https://gist.github.com/devongovett/1381839 + if (!("classList" in document.documentElement) && Object.defineProperty && typeof HTMLElement !== 'undefined') { + Object.defineProperty(HTMLElement.prototype, 'classList', { + get: function() { + var self = this; + function update(fn) { + return function() { + var x, classes = self.className.split(/\s+/); + + for(x=0; x<arguments.length; x++) { + fn(classes, classes.indexOf(arguments[x]), arguments[x]); + } + + self.className = classes.join(" "); + }; + } + + return { + add: update(function(classes, index, value) { + ~index || classes.push(value); + }), + + remove: update(function(classes, index) { + ~index && classes.splice(index, 1); + }), + + toggle: update(function(classes, index, value) { + ~index ? classes.splice(index, 1) : classes.push(value); + }), + + contains: function(value) { + return !!~self.className.split(/\s+/).indexOf(value); + }, + + item: function(i) { + return self.className.split(/\s+/)[i] || null; + } + }; + + } + }); + } + // polyfill use to simulate native "tap" - function inputTapPolyfill(ele, e) { - if(ele.type === "radio") { - ele.checked = !ele.checked; - ionic.trigger('click', { - target: ele - }); - } else if(ele.type === "checkbox") { - ele.checked = !ele.checked; - ionic.trigger('change', { - target: ele - }); - } else if(ele.type === "submit" || ele.type === "button") { - ionic.trigger('click', { - target: ele - }); - } else { + ionic.tapElement = function(target, e) { + // simulate a normal click by running the element's click method then focus on it + + var ele = target.control || target; + + if(ele.disabled) return; + + + + var c = getCoordinates(e); + + // using initMouseEvent instead of MouseEvent for our Android friends + var clickEvent = document.createEvent("MouseEvents"); + clickEvent.initMouseEvent('click', true, true, window, + 1, 0, 0, c.x, c.y, + false, false, false, false, 0, null); + + ele.dispatchEvent(clickEvent); + + if(ele.tagName === 'INPUT' || ele.tagName === 'TEXTAREA' || ele.tagName === 'SELECT') { ele.focus(); + e.preventDefault(); + } else { + blurActive(); } - e.stopPropagation(); - e.preventDefault(); - return false; - } - function tapPolyfill(e) { - // if the source event wasn't from a touch event then don't use this polyfill - if(!e.gesture || e.gesture.pointerType !== "touch" || !e.gesture.srcEvent) return; + // remember the coordinates of this tap so if it happens again we can ignore it + // but only if the coordinates are not already being actively disabled + if( !isRecentTap(e) ) { + recordCoordinates(e); + } - // An internal Ionic indicator for angular directives that contain - // elements that normally need poly behavior, but are already processed - // (like the radio directive that has a radio button in it, but handles - // the tap stuff itself). This is in contrast to preventDefault which will - // mess up other operations like change events and such - if(e.alreadyHandled) { - return; + if(target.control) { + + return stopEvent(e); } + }; - e = e.gesture.srcEvent; // evaluate the actual source event, not the created event by gestures.js + function tapPolyfill(orgEvent) { + // if the source event wasn't from a touch event then don't use this polyfill + if(!orgEvent.gesture || !orgEvent.gesture.srcEvent) return; + var e = orgEvent.gesture.srcEvent; // evaluate the actual source event, not the created event by gestures.js var ele = e.target; + if( isRecentTap(e) ) { + // if a tap in the same area just happened, don't continue + + return stopEvent(e); + } + while(ele) { - if( ele.tagName === "INPUT" || ele.tagName === "TEXTAREA" || ele.tagName === "SELECT" ) { - return inputTapPolyfill(ele, e); - } else if( ele.tagName === "LABEL" ) { - if(ele.control) { - return inputTapPolyfill(ele.control, e); - } - } else if( ele.tagName === "A" || ele.tagName === "BUTTON" ) { - ionic.trigger('click', { - target: ele - }); - e.stopPropagation(); - e.preventDefault(); - return false; + // climb up the DOM looking to see if the tapped element is, or has a parent, of one of these + if( ele.tagName === "INPUT" || + ele.tagName === "A" || + ele.tagName === "BUTTON" || + ele.tagName === "LABEL" || + ele.tagName === "TEXTAREA" || + ele.tagName === "SELECT" ) { + + return ionic.tapElement(ele, e); } ele = ele.parentElement; } // they didn't tap one of the above elements // if the currently active element is an input, and they tapped outside // of the current input, then unset its focus (blur) so the keyboard goes away - var activeElement = document.activeElement; - if(activeElement && (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.tagName === "SELECT")) { - activeElement.blur(); - e.stopPropagation(); - e.preventDefault(); - return false; + blurActive(); + } + + function preventGhostClick(e) { + if(e.target.control) { + // this is a label that has an associated input + // the native layer will send the actual event, so stop this one + + return stopEvent(e); } + + if( isRecentTap(e) ) { + // a tap has already happened at these coordinates recently, ignore this event + + return stopEvent(e); + } + + // remember the coordinates of this click so if a tap or click in the + // same area quickly happened again we can ignore it + recordCoordinates(e); } + function isRecentTap(event) { + // loop through the tap coordinates and see if the same area has been tapped recently + var tapId, existingCoordinates, currentCoordinates; + + for(tapId in tapCoordinates) { + existingCoordinates = tapCoordinates[tapId]; + if(!currentCoordinates) currentCoordinates = getCoordinates(event); // lazy load it when needed + + if(currentCoordinates.x > existingCoordinates.x - HIT_RADIUS && + currentCoordinates.x < existingCoordinates.x + HIT_RADIUS && + currentCoordinates.y > existingCoordinates.y - HIT_RADIUS && + currentCoordinates.y < existingCoordinates.y + HIT_RADIUS) { + // the current tap coordinates are in the same area as a recent tap + return existingCoordinates; + } + } + } + + function recordCoordinates(event) { + var c = getCoordinates(event); + if(c.x && c.y) { + var tapId = Date.now(); + + // only record tap coordinates if we have valid ones + tapCoordinates[tapId] = { x: c.x, y: c.y, id: tapId }; + + setTimeout(function() { + // delete the tap coordinates after X milliseconds, basically allowing + // it so a tap can happen again in the same area in the future + delete tapCoordinates[tapId]; + }, CLICK_PREVENT_DURATION); + } + } + + function getCoordinates(event) { + // This method can get coordinates for both a mouse click + // or a touch depending on the given event + var gesture = (event.gesture ? event.gesture : event); + + if(gesture) { + var touches = gesture.touches && gesture.touches.length ? gesture.touches : [gesture]; + var e = (gesture.changedTouches && gesture.changedTouches[0]) || + (gesture.originalEvent && gesture.originalEvent.changedTouches && + gesture.originalEvent.changedTouches[0]) || + touches[0].originalEvent || touches[0]; + + if(e) return { x: e.clientX, y: e.clientY }; + } + return { x:0, y:0 }; + } + + function removeClickPrevent(e) { + setTimeout(function(){ + var tap = isRecentTap(e); + if(tap) delete tapCoordinates[tap.id]; + }, REMOVE_PREVENT_DELAY); + } + + function stopEvent(e){ + e.stopPropagation(); + e.preventDefault(); + return false; + } + + function blurActive() { + var ele = document.activeElement; + if(ele && (ele.tagName === "INPUT" || + ele.tagName === "TEXTAREA" || + ele.tagName === "SELECT")) { + // using a timeout to prevent funky scrolling while a keyboard hides + setTimeout(function(){ + ele.blur(); + }, 400); + } + } + + var tapCoordinates = {}; // used to remember coordinates to ignore if they happen again quickly + var CLICK_PREVENT_DURATION = 1500; // max milliseconds ghostclicks in the same area should be prevented + var REMOVE_PREVENT_DELAY = 375; // delay after a touchend/mouseup before removing the ghostclick prevent + var HIT_RADIUS = 15; + + // set global click handler and check if the event should stop or not + document.addEventListener('click', preventGhostClick, true); + // global tap event listener polyfill for HTML elements that were "tapped" by the user - ionic.on("tap", tapPolyfill, window); + ionic.on("tap", tapPolyfill, document); + // listeners used to remove ghostclick prevention + document.addEventListener('touchend', removeClickPrevent, false); + document.addEventListener('mouseup', removeClickPrevent, false); + })(this, document, ionic); ; (function(ionic) { + + /* for nextUid() function below */ + var uid = ['0','0','0']; /** * Various utilities used throughout Ionic * * Some of these are adopted from underscore.js and backbone.js, both also MIT licensed. @@ -2037,10 +2419,40 @@ obj[prop] = source[prop]; } } } return obj; + }, + + /** + * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric + * characters such as '012ABC'. The reason why we are not using simply a number counter is that + * the number string gets longer over time, and it can also overflow, where as the nextId + * will grow much slower, it is a string, and it will never overflow. + * + * @returns an unique alpha-numeric string + */ + nextUid: function() { + var index = uid.length; + var digit; + + while(index) { + index--; + digit = uid[index].charCodeAt(0); + if (digit == 57 /*'9'*/) { + uid[index] = 'A'; + return uid.join(''); + } + if (digit == 90 /*'Z'*/) { + uid[index] = '0'; + } else { + uid[index] = String.fromCharCode(digit + 1); + return uid.join(''); + } + } + uid.unshift('0'); + return uid.join(''); } }; // Bind a few of the most useful functions to the ionic scope ionic.inherit = ionic.Utils.inherit; @@ -2050,10 +2462,62 @@ ionic.debounce = ionic.Utils.debounce; })(window.ionic); ; (function(ionic) { + +ionic.Platform.ready(function() { + if (ionic.Platform.is('android')) { + androidKeyboardFix(); + } +}); + +function androidKeyboardFix() { + var rememberedDeviceWidth = window.innerWidth; + var rememberedDeviceHeight = window.innerHeight; + var keyboardHeight; + + window.addEventListener('resize', resize); + + function resize() { + + //If the width of the window changes, we have an orientation change + if (rememberedDeviceWidth !== window.innerWidth) { + rememberedDeviceWidth = window.innerWidth; + rememberedDeviceHeight = window.innerHeight; + + + //If the height changes, and it's less than before, we have a keyboard open + } else if (rememberedDeviceHeight !== window.innerHeight && + window.innerHeight < rememberedDeviceHeight) { + document.body.classList.add('hide-footer'); + //Wait for next frame so document.activeElement is set + ionic.requestAnimationFrame(handleKeyboardChange); + } else { + //Otherwise we have a keyboard close or a *really* weird resize + document.body.classList.remove('hide-footer'); + } + + function handleKeyboardChange() { + //keyboard opens + keyboardHeight = rememberedDeviceHeight - window.innerHeight; + var activeEl = document.activeElement; + if (activeEl) { + //This event is caught by the nearest parent scrollView + //of the activeElement + ionic.trigger('scrollChildIntoView', { + target: activeEl + }, true); + } + + } + } +} + +})(window.ionic); +; +(function(ionic) { 'use strict'; ionic.views.View = function() { this.initialize.apply(this, arguments); }; @@ -2063,10 +2527,11 @@ initialize: function() {} }); })(window.ionic); ; +var IS_INPUT_LIKE_REGEX = /input|textarea|select/i; /* * Scroller * http://github.com/zynga/scroller * * Copyright 2011, Zynga Inc. @@ -2216,11 +2681,11 @@ * @param completedCallback {Function} * Signature of the method should be `function(droppedFrames, finishedAnimation) {}` * @param duration {Integer} Milliseconds to run the animation * @param easingMethod {Function} Pointer to easing function * Signature of the method should be `function(percent) { return modifiedValue; }` - * @param root {Element ? document.body} Render root, when available. Used for internal + * @param root {Element} Render root, when available. Used for internal * usage of requestAnimationFrame. * @return {Integer} Identifier of animation. Can be used to stop it any time. */ start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) { @@ -2343,20 +2808,32 @@ return 0.5 * (Math.pow((pos - 2), 3) + 2); }; - /** - * A pure logic 'component' for 'virtual' scrolling/zooming. - */ +/** + * ionic.views.Scroll + * A powerful scroll view with support for bouncing, pull to refresh, and paging. + * @param {Object} options options for the scroll view + * @class A scroll view system + * @memberof ionic.views + */ ionic.views.Scroll = ionic.views.View.inherit({ initialize: function(options) { var self = this; this.__container = options.el; this.__content = options.el.firstElementChild; + //Remove any scrollTop attached to these elements; they are virtual scroll now + //This also stops on-load-scroll-to-window.location.hash that the browser does + setTimeout(function() { + if (self.__container && self.__content) { + self.__container.scrollTop = 0; + self.__content.scrollTop = 0; + } + }); this.options = { /** Disable scrolling on x-axis by default */ scrollingX: false, @@ -2364,10 +2841,16 @@ /** Enable scrolling on y-axis */ scrollingY: true, scrollbarY: true, + startX: 0, + startY: 0, + + /** The amount to dampen mousewheel events */ + wheelDampen: 6, + /** The minimum size the scrollbars scale to while scrolling */ minScrollbarSizeX: 5, minScrollbarSizeY: 5, /** Scrollbar fading after scrolling */ @@ -2443,15 +2926,22 @@ scrollLeft: self.__scrollLeft, target: self.__container }); }; + this.__scrollLeft = this.options.startX; + this.__scrollTop = this.options.startY; + // Get the render update function, initialize event handlers, // and calculate the size of the scroll container this.__callback = this.getRenderFn(); this.__initEventHandlers(); this.__createScrollbars(); + + }, + + run: function() { this.resize(); // Fade them out this.__fadeScrollbars('out', this.options.scrollbarResizeFadeDelay); }, @@ -2462,40 +2952,40 @@ --------------------------------------------------------------------------- INTERNAL FIELDS :: STATUS --------------------------------------------------------------------------- */ - /** {Boolean} Whether only a single finger is used in touch handling */ + /** Whether only a single finger is used in touch handling */ __isSingleTouch: false, - /** {Boolean} Whether a touch event sequence is in progress */ + /** Whether a touch event sequence is in progress */ __isTracking: false, - /** {Boolean} Whether a deceleration animation went to completion. */ + /** Whether a deceleration animation went to completion. */ __didDecelerationComplete: false, /** - * {Boolean} Whether a gesture zoom/rotate event is in progress. Activates when + * Whether a gesture zoom/rotate event is in progress. Activates when * a gesturestart event happens. This has higher priority than dragging. */ __isGesturing: false, /** - * {Boolean} Whether the user has moved by such a distance that we have enabled + * Whether the user has moved by such a distance that we have enabled * dragging mode. Hint: It's only enabled after some pixels of movement to * not interrupt with clicks etc. */ __isDragging: false, /** - * {Boolean} Not touching and dragging anymore, and smoothly animating the + * Not touching and dragging anymore, and smoothly animating the * touch sequence using deceleration. */ __isDecelerating: false, /** - * {Boolean} Smoothly animating the currently configured change + * Smoothly animating the currently configured change */ __isAnimating: false, @@ -2503,149 +2993,176 @@ --------------------------------------------------------------------------- INTERNAL FIELDS :: DIMENSIONS --------------------------------------------------------------------------- */ - /** {Integer} Available outer left position (from document perspective) */ + /** Available outer left position (from document perspective) */ __clientLeft: 0, - /** {Integer} Available outer top position (from document perspective) */ + /** Available outer top position (from document perspective) */ __clientTop: 0, - /** {Integer} Available outer width */ + /** Available outer width */ __clientWidth: 0, - /** {Integer} Available outer height */ + /** Available outer height */ __clientHeight: 0, - /** {Integer} Outer width of content */ + /** Outer width of content */ __contentWidth: 0, - /** {Integer} Outer height of content */ + /** Outer height of content */ __contentHeight: 0, - /** {Integer} Snapping width for content */ + /** Snapping width for content */ __snapWidth: 100, - /** {Integer} Snapping height for content */ + /** Snapping height for content */ __snapHeight: 100, - /** {Integer} Height to assign to refresh area */ + /** Height to assign to refresh area */ __refreshHeight: null, - /** {Boolean} Whether the refresh process is enabled when the event is released now */ + /** Whether the refresh process is enabled when the event is released now */ __refreshActive: false, - /** {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */ + /** Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */ __refreshActivate: null, - /** {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */ + /** Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */ __refreshDeactivate: null, - /** {Function} Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */ + /** Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */ __refreshStart: null, - /** {Number} Zoom level */ + /** Zoom level */ __zoomLevel: 1, - /** {Number} Scroll position on x-axis */ + /** Scroll position on x-axis */ __scrollLeft: 0, - /** {Number} Scroll position on y-axis */ + /** Scroll position on y-axis */ __scrollTop: 0, - /** {Integer} Maximum allowed scroll position on x-axis */ + /** Maximum allowed scroll position on x-axis */ __maxScrollLeft: 0, - /** {Integer} Maximum allowed scroll position on y-axis */ + /** Maximum allowed scroll position on y-axis */ __maxScrollTop: 0, - /* {Number} Scheduled left position (final position when animating) */ + /* Scheduled left position (final position when animating) */ __scheduledLeft: 0, - /* {Number} Scheduled top position (final position when animating) */ + /* Scheduled top position (final position when animating) */ __scheduledTop: 0, - /* {Number} Scheduled zoom level (final scale when animating) */ + /* Scheduled zoom level (final scale when animating) */ __scheduledZoom: 0, /* --------------------------------------------------------------------------- INTERNAL FIELDS :: LAST POSITIONS --------------------------------------------------------------------------- */ - /** {Number} Left position of finger at start */ + /** Left position of finger at start */ __lastTouchLeft: null, - /** {Number} Top position of finger at start */ + /** Top position of finger at start */ __lastTouchTop: null, - /** {Date} Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */ + /** Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */ __lastTouchMove: null, - /** {Array} List of positions, uses three indexes for each state: left, top, timestamp */ + /** List of positions, uses three indexes for each state: left, top, timestamp */ __positions: null, /* --------------------------------------------------------------------------- INTERNAL FIELDS :: DECELERATION SUPPORT --------------------------------------------------------------------------- */ - /** {Integer} Minimum left scroll position during deceleration */ + /** Minimum left scroll position during deceleration */ __minDecelerationScrollLeft: null, - /** {Integer} Minimum top scroll position during deceleration */ + /** Minimum top scroll position during deceleration */ __minDecelerationScrollTop: null, - /** {Integer} Maximum left scroll position during deceleration */ + /** Maximum left scroll position during deceleration */ __maxDecelerationScrollLeft: null, - /** {Integer} Maximum top scroll position during deceleration */ + /** Maximum top scroll position during deceleration */ __maxDecelerationScrollTop: null, - /** {Number} Current factor to modify horizontal scroll position with on every step */ + /** Current factor to modify horizontal scroll position with on every step */ __decelerationVelocityX: null, - /** {Number} Current factor to modify vertical scroll position with on every step */ + /** Current factor to modify vertical scroll position with on every step */ __decelerationVelocityY: null, - /** {String} the browser-specific property to use for transforms */ + /** the browser-specific property to use for transforms */ __transformProperty: null, __perspectiveProperty: null, - /** {Object} scrollbar indicators */ + /** scrollbar indicators */ __indicatorX: null, __indicatorY: null, /** Timeout for scrollbar fading */ __scrollbarFadeTimeout: null, - /** {Boolean} whether we've tried to wait for size already */ + /** whether we've tried to wait for size already */ __didWaitForSize: null, __sizerTimeout: null, __initEventHandlers: function() { var self = this; // Event Handler var container = this.__container; - + + //Broadcasted when keyboard is shown on some platforms. + //See js/utils/keyboard.js + container.addEventListener('scrollChildIntoView', function(e) { + var deviceHeight = window.innerHeight; + var element = e.target; + var elementHeight = e.target.offsetHeight; + + //getBoundingClientRect() will actually give us position relative to the viewport + var elementDeviceTop = element.getBoundingClientRect().top; + var elementScrollTop = ionic.DomUtil.getPositionInParent(element, container).top; + + //If the element is positioned under the keyboard... + if (elementDeviceTop + elementHeight > deviceHeight) { + //Put element in middle of visible screen + self.scrollTo(0, elementScrollTop + elementHeight - (deviceHeight * 0.5), true); + } + + //Only the first scrollView parent of the element that broadcasted this event + //(the active element that needs to be shown) should receive this event + e.stopPropagation(); + }); + + function shouldIgnorePress(e) { + // Don't react if initial down happens on a form element + return e.target.tagName.match(IS_INPUT_LIKE_REGEX) || + e.target.isContentEditable; + } + + if ('ontouchstart' in window) { - + container.addEventListener("touchstart", function(e) { - // Don't react if initial down happens on a form element - if (e.target.tagName.match(/input|textarea|select/i)) { + if (e.defaultPrevented || shouldIgnorePress(e)) { return; } - self.doTouchStart(e.touches, e.timeStamp); e.preventDefault(); }, false); document.addEventListener("touchmove", function(e) { @@ -2656,26 +3173,25 @@ }, false); document.addEventListener("touchend", function(e) { self.doTouchEnd(e.timeStamp); }, false); - + } else { - + var mousedown = false; container.addEventListener("mousedown", function(e) { - // Don't react if initial down happens on a form element - if (e.target.tagName.match(/input|textarea|select/i)) { + if (e.defaultPrevented || shouldIgnorePress(e)) { return; } - self.doTouchStart([{ pageX: e.pageX, pageY: e.pageY }], e.timeStamp); + e.preventDefault(); mousedown = true; }, false); document.addEventListener("mousemove", function(e) { if (!mousedown || e.defaultPrevented) { @@ -2697,11 +3213,14 @@ self.doTouchEnd(e.timeStamp); mousedown = false; }, false); - + + document.addEventListener("mousewheel", function(e) { + self.scrollBy(e.wheelDeltaX/self.options.wheelDampen, -e.wheelDeltaY/self.options.wheelDampen); + }); } }, /** Create a scroll bar div with the given direction **/ __createScrollbar: function(direction) { @@ -2912,11 +3431,12 @@ // Add padding to bottom of content this.setDimensions( this.__container.clientWidth, this.__container.clientHeight, Math.max(this.__content.scrollWidth, this.__content.offsetWidth), - Math.max(this.__content.scrollHeight, this.__content.offsetHeight+20)); + Math.max(this.__content.scrollHeight, this.__content.offsetHeight) + ); }, /* --------------------------------------------------------------------------- PUBLIC API --------------------------------------------------------------------------- @@ -2935,68 +3455,68 @@ } else if ('WebkitAppearance' in docStyle) { engine = 'webkit'; } else if (typeof navigator.cpuClass === 'string') { engine = 'trident'; } - + var vendorPrefix = { trident: 'ms', gecko: 'Moz', webkit: 'Webkit', presto: 'O' }[engine]; - + var helperElem = document.createElement("div"); var undef; var perspectiveProperty = vendorPrefix + "Perspective"; var transformProperty = vendorPrefix + "Transform"; var transformOriginProperty = vendorPrefix + 'TransformOrigin'; self.__perspectiveProperty = transformProperty; self.__transformProperty = transformProperty; self.__transformOriginProperty = transformOriginProperty; - + if (helperElem.style[perspectiveProperty] !== undef) { - + return function(left, top, zoom) { content.style[transformProperty] = 'translate3d(' + (-left) + 'px,' + (-top) + 'px,0)'; self.__repositionScrollbars(); self.triggerScrollEvent(); - }; - + }; + } else if (helperElem.style[transformProperty] !== undef) { - + return function(left, top, zoom) { content.style[transformProperty] = 'translate(' + (-left) + 'px,' + (-top) + 'px)'; self.__repositionScrollbars(); self.triggerScrollEvent(); }; - + } else { - + return function(left, top, zoom) { content.style.marginLeft = left ? (-left/zoom) + 'px' : ''; content.style.marginTop = top ? (-top/zoom) + 'px' : ''; content.style.zoom = zoom || ''; self.__repositionScrollbars(); self.triggerScrollEvent(); }; - + } }, /** * Configures the dimensions of the client (outer) and content (inner) elements. * Requires the available space for the outer element and the outer size of the inner element. * All values which are falsy (null or zero etc.) are ignored and the old value is kept. * - * @param clientWidth {Integer ? null} Inner width of outer element - * @param clientHeight {Integer ? null} Inner height of outer element - * @param contentWidth {Integer ? null} Outer width of inner element - * @param contentHeight {Integer ? null} Outer height of inner element + * @param clientWidth {Integer} Inner width of outer element + * @param clientHeight {Integer} Inner height of outer element + * @param contentWidth {Integer} Outer width of inner element + * @param contentHeight {Integer} Outer height of inner element */ setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight) { var self = this; @@ -3028,12 +3548,12 @@ /** * Sets the client coordinates in relation to the document. * - * @param left {Integer ? 0} Left position of outer element - * @param top {Integer ? 0} Top position of outer element + * @param left {Integer} Left position of outer element + * @param top {Integer} Top position of outer element */ setPosition: function(left, top) { var self = this; @@ -3150,13 +3670,13 @@ /** * Zooms to the given level. Supports optional animation. Zooms * the center when no coordinates are given. * * @param level {Number} Level to zoom to - * @param animate {Boolean ? false} Whether to use animation - * @param originLeft {Number ? null} Zoom in at given left coordinate - * @param originTop {Number ? null} Zoom in at given top coordinate + * @param animate {Boolean} Whether to use animation + * @param originLeft {Number} Zoom in at given left coordinate + * @param originTop {Number} Zoom in at given top coordinate */ zoomTo: function(level, animate, originLeft, originTop) { var self = this; @@ -3213,13 +3733,13 @@ /** * Zooms the content by the given factor. * * @param factor {Number} Zoom by given factor - * @param animate {Boolean ? false} Whether to use animation - * @param originLeft {Number ? 0} Zoom in at given left coordinate - * @param originTop {Number ? 0} Zoom in at given top coordinate + * @param animate {Boolean} Whether to use animation + * @param originLeft {Number} Zoom in at given left coordinate + * @param originTop {Number} Zoom in at given top coordinate */ zoomBy: function(factor, animate, originLeft, originTop) { var self = this; @@ -3229,14 +3749,14 @@ /** * Scrolls to the given position. Respect limitations and snapping automatically. * - * @param left {Number?null} Horizontal scroll position, keeps current if value is <code>null</code> - * @param top {Number?null} Vertical scroll position, keeps current if value is <code>null</code> - * @param animate {Boolean?false} Whether the scrolling should happen using an animation - * @param zoom {Number?null} Zoom level to go to + * @param left {Number} Horizontal scroll position, keeps current if value is <code>null</code> + * @param top {Number} Vertical scroll position, keeps current if value is <code>null</code> + * @param animate {Boolean} Whether the scrolling should happen using an animation + * @param zoom {Number} Zoom level to go to */ scrollTo: function(left, top, animate, zoom) { var self = this; @@ -3311,13 +3831,13 @@ /** * Scroll by the given offset * - * @param left {Number ? 0} Scroll x-axis by given offset - * @param top {Number ? 0} Scroll x-axis by given offset - * @param animate {Boolean ? false} Whether to animate the given change + * @param left {Number} Scroll x-axis by given offset + * @param top {Number} Scroll x-axis by given offset + * @param animate {Boolean} Whether to animate the given change */ scrollBy: function(left, top, animate) { var self = this; @@ -3547,11 +4067,11 @@ var maxScrollTop = self.__maxScrollTop; if (scrollTop > maxScrollTop || scrollTop < 0) { // Slow down on the edges - if (self.options.bouncing) { + if (self.options.bouncing || (self.__refreshHeight && scrollTop < 0)) { scrollTop += (moveY / 2 * this.options.speedMultiplier); // Support pull-to-refresh (only when only y is scrollable) if (!self.__enableScrollX && self.__refreshHeight != null) { @@ -3754,11 +4274,11 @@ /** * Applies the scroll position to the content element * * @param left {Number} Left scroll position * @param top {Number} Top scroll position - * @param animate {Boolean?false} Whether animation should be used to move to the new coordinates + * @param animate {Boolean} Whether animation should be used to move to the new coordinates */ __publish: function(left, top, zoom, animate) { var self = this; @@ -3869,11 +4389,11 @@ clearTimeout(self.__sizerTimeout); var sizer = function() { self.resize(); - + if((self.options.scrollingX && self.__maxScrollLeft == 0) || (self.options.scrollingY && self.__maxScrollTop == 0)) { //self.__sizerTimeout = setTimeout(sizer, 1000); } }; @@ -3922,16 +4442,17 @@ var step = function(percent, now, render) { self.__stepThroughDeceleration(render); }; // How much velocity is required to keep the deceleration running - var minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1; + self.__minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1; // Detect whether it's still worth to continue animating steps // If we are already slow enough to not being user perceivable anymore, we stop the whole process here. var verify = function() { - var shouldContinue = Math.abs(self.__decelerationVelocityX) >= minVelocityToKeepDecelerating || Math.abs(self.__decelerationVelocityY) >= minVelocityToKeepDecelerating; + var shouldContinue = Math.abs(self.__decelerationVelocityX) >= self.__minVelocityToKeepDecelerating || + Math.abs(self.__decelerationVelocityY) >= self.__minVelocityToKeepDecelerating; if (!shouldContinue) { self.__didDecelerationComplete = true; } return shouldContinue; }; @@ -3955,11 +4476,11 @@ /** * Called on every step of the animation * - * @param inMemory {Boolean?false} Whether to not render the current step, but keep it in memory only. Used internally only! + * @param inMemory {Boolean} Whether to not render the current step, but keep it in memory only. Used internally only! */ __stepThroughDeceleration: function(render) { var self = this; @@ -4036,12 +4557,12 @@ var scrollOutsideX = 0; var scrollOutsideY = 0; // This configures the amount of change applied to deceleration/acceleration when reaching boundaries - var penetrationDeceleration = self.options.penetrationDeceleration; - var penetrationAcceleration = self.options.penetrationAcceleration; + var penetrationDeceleration = self.options.penetrationDeceleration; + var penetrationAcceleration = self.options.penetrationAcceleration; // Check limits if (scrollLeft < self.__minDecelerationScrollLeft) { scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft; } else if (scrollLeft > self.__maxDecelerationScrollLeft) { @@ -4054,21 +4575,29 @@ scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop; } // Slow down until slow enough, then flip back to snap position if (scrollOutsideX !== 0) { - if (scrollOutsideX * self.__decelerationVelocityX <= 0) { + var isHeadingOutwardsX = scrollOutsideX * self.__decelerationVelocityX <= self.__minDecelerationScrollLeft; + if (isHeadingOutwardsX) { self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration; - } else { + } + var isStoppedX = Math.abs(self.__decelerationVelocityX) <= self.__minVelocityToKeepDecelerating; + //If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds + if (!isHeadingOutwardsX || isStoppedX) { self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration; } } if (scrollOutsideY !== 0) { - if (scrollOutsideY * self.__decelerationVelocityY <= 0) { + var isHeadingOutwardsY = scrollOutsideY * self.__decelerationVelocityY <= self.__minDecelerationScrollTop; + if (isHeadingOutwardsY) { self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration; - } else { + } + var isStoppedY = Math.abs(self.__decelerationVelocityY) <= self.__minVelocityToKeepDecelerating; + //If we're not heading outwards, or if the above statement got us below minDeceleration, go back towards bounds + if (!isHeadingOutwardsY || isStoppedY) { self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration; } } } } @@ -4120,82 +4649,75 @@ /** * Align the title text given the buttons in the header * so that the header text size is maximized and aligned * correctly as long as possible. */ - align: function() { - var _this = this; + align: ionic.animationFrameThrottle(function(titleSelector) { - window.rAF(ionic.proxy(function() { - var i, c, childSize; - var childNodes = this.el.childNodes; + // Find the titleEl element + var titleEl = this.el.querySelector(titleSelector || '.title'); + if(!titleEl) { + return; + } - // Find the title element - var title = this.el.querySelector('.title'); - if(!title) { - return; - } - - var leftWidth = 0; - var rightWidth = 0; - var titlePos = Array.prototype.indexOf.call(childNodes, title); + var i, c, childSize; + var childNodes = this.el.childNodes; + var leftWidth = 0; + var rightWidth = 0; + var isCountingRightWidth = true; - // Compute how wide the left children are - for(i = 0; i < titlePos; i++) { - childSize = null; - c = childNodes[i]; - if(c.nodeType == 3) { - childSize = ionic.DomUtil.getTextBounds(c); - } else if(c.nodeType == 1) { - childSize = c.getBoundingClientRect(); - } - if(childSize) { - leftWidth += childSize.width; - } + // Compute how wide the left children are + // Skip all titles (there may still be two titles, one leaving the dom) + // Once we encounter a titleEl, realize we are now counting the right-buttons, not left + for(i = 0; i < childNodes.length; i++) { + c = childNodes[i]; + if (c.tagName && c.tagName.toLowerCase() == 'h1') { + isCountingRightWidth = false; + continue; } - // Compute how wide the right children are - for(i = titlePos + 1; i < childNodes.length; i++) { - childSize = null; - c = childNodes[i]; - if(c.nodeType == 3) { - childSize = ionic.DomUtil.getTextBounds(c); - } else if(c.nodeType == 1) { - childSize = c.getBoundingClientRect(); - } - if(childSize) { + childSize = null; + if(c.nodeType == 3) { + childSize = ionic.DomUtil.getTextBounds(c); + } else if(c.nodeType == 1) { + childSize = c.getBoundingClientRect(); + } + if(childSize) { + if (isCountingRightWidth) { rightWidth += childSize.width; + } else { + leftWidth += childSize.width; } } + } - var margin = Math.max(leftWidth, rightWidth) + 10; + var margin = Math.max(leftWidth, rightWidth) + 10; - // Size and align the header title based on the sizes of the left and - // right children, and the desired alignment mode - if(this.alignTitle == 'center') { - if(margin > 10) { - title.style.left = margin + 'px'; - title.style.right = margin + 'px'; - } - if(title.offsetWidth < title.scrollWidth) { - if(rightWidth > 0) { - title.style.right = (rightWidth + 5) + 'px'; - } - } - } else if(this.alignTitle == 'left') { - title.classList.add('title-left'); - if(leftWidth > 0) { - title.style.left = (leftWidth + 15) + 'px'; - } - } else if(this.alignTitle == 'right') { - title.classList.add('title-right'); + // Size and align the header titleEl based on the sizes of the left and + // right children, and the desired alignment mode + if(this.alignTitle == 'center') { + if(margin > 10) { + titleEl.style.left = margin + 'px'; + titleEl.style.right = margin + 'px'; + } + if(titleEl.offsetWidth < titleEl.scrollWidth) { if(rightWidth > 0) { - title.style.right = (rightWidth + 15) + 'px'; + titleEl.style.right = (rightWidth + 5) + 'px'; } } - }, this)); - } + } else if(this.alignTitle == 'left') { + titleEl.classList.add('titleEl-left'); + if(leftWidth > 0) { + titleEl.style.left = (leftWidth + 15) + 'px'; + } + } else if(this.alignTitle == 'right') { + titleEl.classList.add('titleEl-right'); + if(rightWidth > 0) { + titleEl.style.right = (rightWidth + 15) + 'px'; + } + } + }) }); })(ionic); ; (function(ionic) { @@ -4243,63 +4765,61 @@ // Make sure we aren't animating as we slide content.classList.remove(ITEM_SLIDING_CLASS); // Grab the starting X point for the item (for example, so we can tell whether it is open or closed to start) - offsetX = parseFloat(content.style.webkitTransform.replace('translate3d(', '').split(',')[0]) || 0; + offsetX = parseFloat(content.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]) || 0; // Grab the buttons buttons = content.parentNode.querySelector('.' + ITEM_OPTIONS_CLASS); if(!buttons) { return; } - + buttonsWidth = buttons.offsetWidth; this._currentDrag = { buttonsWidth: buttonsWidth, content: content, startOffsetX: offsetX }; }; - SlideDrag.prototype.drag = function(e) { - var _this = this, buttonsWidth; + SlideDrag.prototype.drag = ionic.animationFrameThrottle(function(e) { + var buttonsWidth; - window.rAF(function() { - // We really aren't dragging - if(!_this._currentDrag) { - return; - } + // We really aren't dragging + if(!this._currentDrag) { + return; + } - // Check if we should start dragging. Check if we've dragged past the threshold, - // or we are starting from the open state. - if(!_this._isDragging && - ((Math.abs(e.gesture.deltaX) > _this.dragThresholdX) || - (Math.abs(_this._currentDrag.startOffsetX) > 0))) - { - _this._isDragging = true; - } + // Check if we should start dragging. Check if we've dragged past the threshold, + // or we are starting from the open state. + if(!this._isDragging && + ((Math.abs(e.gesture.deltaX) > this.dragThresholdX) || + (Math.abs(this._currentDrag.startOffsetX) > 0))) + { + this._isDragging = true; + } - if(_this._isDragging) { - buttonsWidth = _this._currentDrag.buttonsWidth; + if(this._isDragging) { + buttonsWidth = this._currentDrag.buttonsWidth; - // Grab the new X point, capping it at zero - var newX = Math.min(0, _this._currentDrag.startOffsetX + e.gesture.deltaX); + // Grab the new X point, capping it at zero + var newX = Math.min(0, this._currentDrag.startOffsetX + e.gesture.deltaX); - // If the new X position is past the buttons, we need to slow down the drag (rubber band style) - if(newX < -buttonsWidth) { - // Calculate the new X position, capped at the top of the buttons - newX = Math.min(-buttonsWidth, -buttonsWidth + (((e.gesture.deltaX + buttonsWidth) * 0.4))); - } - - _this._currentDrag.content.style.webkitTransform = 'translate3d(' + newX + 'px, 0, 0)'; - _this._currentDrag.content.style.webkitTransition = 'none'; + // If the new X position is past the buttons, we need to slow down the drag (rubber band style) + if(newX < -buttonsWidth) { + // Calculate the new X position, capped at the top of the buttons + newX = Math.min(-buttonsWidth, -buttonsWidth + (((e.gesture.deltaX + buttonsWidth) * 0.4))); } - }); - }; + this._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + newX + 'px, 0, 0)'; + this._currentDrag.content.style.webkitTransition = 'none'; + } + }); + SlideDrag.prototype.end = function(e, doneCallback) { var _this = this; // There is no drag, just end immediately if(!this._currentDrag) { @@ -4309,11 +4829,11 @@ // If we are currently dragging, we want to snap back into place // The final resting point X will be the width of the exposed buttons var restingPoint = -this._currentDrag.buttonsWidth; - // Check if the drag didn't clear the buttons mid-point + // Check if the drag didn't clear the buttons mid-point // and we aren't moving fast enough to swipe open if(e.gesture.deltaX > -(this._currentDrag.buttonsWidth/2)) { // If we are going left but too slow, or going right, go back to resting if(e.gesture.direction == "left" && Math.abs(e.gesture.velocityX) < 0.3) { @@ -4331,24 +4851,24 @@ // if(content) content.classList.remove(ITEM_SLIDING_CLASS); // } // e.target.removeEventListener('webkitTransitionEnd', onRestingAnimationEnd); // }; - window.rAF(function() { - // var currentX = parseFloat(_this._currentDrag.content.style.webkitTransform.replace('translate3d(', '').split(',')[0]) || 0; + ionic.requestAnimationFrame(function() { + // var currentX = parseFloat(_this._currentDrag.content.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]) || 0; // if(currentX !== restingPoint) { // _this._currentDrag.content.classList.add(ITEM_SLIDING_CLASS); // _this._currentDrag.content.addEventListener('webkitTransitionEnd', onRestingAnimationEnd); // } if(restingPoint === 0) { - _this._currentDrag.content.style.webkitTransform = ''; + _this._currentDrag.content.style[ionic.CSS.TRANSFORM] = ''; } else { - _this._currentDrag.content.style.webkitTransform = 'translate3d(' + restingPoint + 'px, 0, 0)'; + _this._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + restingPoint + 'px, 0, 0)'; } - _this._currentDrag.content.style.webkitTransition = ''; - + _this._currentDrag.content.style[ionic.CSS.TRANSFORM] = ''; + // Kill the current drag _this._currentDrag = null; // We are done, notify caller @@ -4358,72 +4878,104 @@ var ReorderDrag = function(opts) { this.dragThresholdY = opts.dragThresholdY || 0; this.onReorder = opts.onReorder; this.el = opts.el; + this.scrollEl = opts.scrollEl; + this.scrollView = opts.scrollView; }; ReorderDrag.prototype = new DragOp(); + ReorderDrag.prototype._moveElement = function(e) { + var y = (e.gesture.center.pageY - this._currentDrag.elementHeight/2); + this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(0, '+y+'px, 0)'; + }; + ReorderDrag.prototype.start = function(e) { var content; // Grab the starting Y point for the item - var offsetY = this.el.offsetTop;//parseFloat(this.el.style.webkitTransform.replace('translate3d(', '').split(',')[1]) || 0; + var offsetY = this.el.offsetTop;//parseFloat(this.el.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[1]) || 0; var startIndex = ionic.DomUtil.getChildIndex(this.el, this.el.nodeName.toLowerCase()); - + var elementHeight = this.el.offsetHeight; var placeholder = this.el.cloneNode(true); + // If we have a scroll pane, move our draggable element outside of it + // We do this because when we drag our element down below the edge of the page + // and scroll the scroll-pane, if the element is *part* of the scroll-pane, + // it will scroll 'with' the scroll-pane's contents and change position. + var appendToElement = (this.scrollEl || this.el).parentNode; + placeholder.classList.add(ITEM_PLACEHOLDER_CLASS); this.el.parentNode.insertBefore(placeholder, this.el); - this.el.classList.add(ITEM_REORDERING_CLASS); + appendToElement.parentNode.appendChild(this.el); + this._currentDrag = { - startOffsetTop: offsetY, + elementHeight: elementHeight, startIndex: startIndex, - placeholder: placeholder + placeholder: placeholder, + scrollHeight: scroll, + list: placeholder.parentNode }; + + this._moveElement(e); }; - ReorderDrag.prototype.drag = function(e) { - var _this = this; + ReorderDrag.prototype.drag = ionic.animationFrameThrottle(function(e) { + // We really aren't dragging + if(!this._currentDrag) { + return; + } - window.rAF(function() { - // We really aren't dragging - if(!_this._currentDrag) { - return; - } + var scrollY = 0; + var pageY = e.gesture.center.pageY; - // Check if we should start dragging. Check if we've dragged past the threshold, - // or we are starting from the open state. - if(!_this._isDragging && Math.abs(e.gesture.deltaY) > _this.dragThresholdY) { - _this._isDragging = true; - } + //If we have a scrollView, check scroll boundaries for dragged element and scroll if necessary + if (this.scrollView) { + var container = this.scrollEl; - if(_this._isDragging) { - var newY = _this._currentDrag.startOffsetTop + e.gesture.deltaY; - - _this.el.style.top = newY + 'px'; + scrollY = this.scrollView.getValues().top; - _this._currentDrag.currentY = newY; + var containerTop = container.offsetTop; + var pixelsPastTop = containerTop - pageY + this._currentDrag.elementHeight/2; + var pixelsPastBottom = pageY + this._currentDrag.elementHeight/2 - containerTop - container.offsetHeight; - _this._reorderItems(); + if (e.gesture.deltaY < 0 && pixelsPastTop > 0 && scrollY > 0) { + this.scrollView.scrollBy(null, -pixelsPastTop); } - }); - }; + if (e.gesture.deltaY > 0 && pixelsPastBottom > 0) { + if (scrollY < this.scrollView.getScrollMax().top) { + this.scrollView.scrollBy(null, pixelsPastBottom); + } + } + } + // Check if we should start dragging. Check if we've dragged past the threshold, + // or we are starting from the open state. + if(!this._isDragging && Math.abs(e.gesture.deltaY) > this.dragThresholdY) { + this._isDragging = true; + } + + if(this._isDragging) { + this._moveElement(e); + + this._currentDrag.currentY = scrollY + pageY - this._currentDrag.placeholder.parentNode.offsetTop; + + this._reorderItems(); + } + }); + // When an item is dragged, we need to reorder any items for sorting purposes ReorderDrag.prototype._reorderItems = function() { var placeholder = this._currentDrag.placeholder; var siblings = Array.prototype.slice.call(this._currentDrag.placeholder.parentNode.children); - - // Remove the floating element from the child search list - siblings.splice(siblings.indexOf(this.el), 1); var index = siblings.indexOf(this._currentDrag.placeholder); var topSibling = siblings[Math.max(0, index - 1)]; var bottomSibling = siblings[Math.min(siblings.length, index+1)]; var thisOffsetTop = this._currentDrag.currentY;// + this._currentDrag.startOffsetTop; @@ -4442,16 +4994,16 @@ doneCallback && doneCallback(); return; } var placeholder = this._currentDrag.placeholder; + var finalPosition = ionic.DomUtil.getChildIndex(placeholder, placeholder.nodeName.toLowerCase()); // Reposition the element this.el.classList.remove(ITEM_REORDERING_CLASS); - this.el.style.top = 0; + this.el.style[ionic.CSS.TRANSFORM] = ''; - var finalPosition = ionic.DomUtil.getChildIndex(placeholder, placeholder.nodeName.toLowerCase()); placeholder.parentNode.insertBefore(this.el, placeholder); placeholder.parentNode.removeChild(placeholder); this.onReorder && this.onReorder(this.el, this._currentDrag.startIndex, finalPosition); @@ -4492,11 +5044,11 @@ }, this.el); window.ionic.onGesture('release', function(e) { _this._handleEndDrag(e); }, this.el); - + window.ionic.onGesture('drag', function(e) { _this._handleDrag(e); }, this.el); // Start the drag states this._initDrag(); @@ -4597,10 +5149,12 @@ var item = this._getItem(e.target); if(item) { this._dragOp = new ReorderDrag({ el: item, + scrollEl: this.scrollEl, + scrollView: this.scrollView, onReorder: function(el, start, end) { _this.onReorder && _this.onReorder(el, start, end); } }); this._dragOp.start(e); @@ -4622,11 +5176,11 @@ }, _handleEndDrag: function(e) { var _this = this; - + if(!this._dragOp) { //ionic.views.ListView.__super__._handleEndDrag.call(this, e); return; } @@ -4645,11 +5199,11 @@ /** * Process the drag event to move the item to the left or right. */ _handleDrag: function(e) { var _this = this, content, buttons; - + // If the user has a touch timeout to highlight an element, clear it if we // get sufficient draggage if(Math.abs(e.gesture.deltaX) > 10 || Math.abs(e.gesture.deltaY) > 10) { clearTimeout(this._touchTimeout); } @@ -4660,11 +5214,11 @@ if(!this.isDragging && !this._dragOp) { this._startDrag(e); } // No drag still, pass it up - if(!this._dragOp) { + if(!this._dragOp) { //ionic.views.ListView.__super__._handleDrag.call(this, e); return; } e.gesture.srcEvent.preventDefault(); @@ -4694,45 +5248,53 @@ })(ionic); ; (function(ionic) { 'use strict'; /** - * An ActionSheet is the slide up menu popularized on iOS. + * Loading * - * You see it all over iOS apps, where it offers a set of options - * triggered after an action. + * The Loading is an overlay that can be used to indicate + * activity while blocking user interaction. */ ionic.views.Loading = ionic.views.View.inherit({ initialize: function(opts) { var _this = this; this.el = opts.el; this.maxWidth = opts.maxWidth || 200; + this.showDelay = opts.showDelay || 0; + this._loadingBox = this.el.querySelector('.loading'); }, show: function() { var _this = this; if(this._loadingBox) { var lb = _this._loadingBox; var width = Math.min(_this.maxWidth, Math.max(window.outerWidth - 40, lb.offsetWidth)); - lb.style.width = width; + lb.style.width = width + 'px'; lb.style.marginLeft = (-lb.offsetWidth) / 2 + 'px'; lb.style.marginTop = (-lb.offsetHeight) / 2 + 'px'; - _this.el.classList.add('active'); + // Wait 'showDelay' ms before showing the loading screen + this._showDelayTimeout = window.setTimeout(function() { + _this.el.classList.add('active'); + }, _this.showDelay); } }, hide: function() { // Force a reflow so the animation will actually run this.el.offsetWidth; + // Prevent unnecessary 'show' after 'hide' has already been called + window.clearTimeout(this._showDelayTimeout); + this.el.classList.remove('active'); } }); })(ionic); @@ -4742,34 +5304,43 @@ ionic.views.Modal = ionic.views.View.inherit({ initialize: function(opts) { opts = ionic.extend({ focusFirstInput: false, - unfocusOnHide: true + unfocusOnHide: true, + focusFirstDelay: 600 }, opts); ionic.extend(this, opts); this.el = opts.el; }, show: function() { + var self = this; + this.el.classList.add('active'); if(this.focusFirstInput) { - var input = this.el.querySelector('input, textarea'); - input && input.focus && input.focus(); + // Let any animations run first + window.setTimeout(function() { + var input = self.el.querySelector('input, textarea'); + input && input.focus && input.focus(); + }, this.focusFirstDelay); } }, hide: function() { this.el.classList.remove('active'); // Unfocus all elements if(this.unfocusOnHide) { var inputs = this.el.querySelectorAll('input, textarea'); - for(var i = 0; i < inputs.length; i++) { - inputs[i].blur && inputs[i].blur(); - } + // Let any animations run first + window.setTimeout(function() { + for(var i = 0; i < inputs.length; i++) { + inputs[i].blur && inputs[i].blur(); + } + }); } } }); })(ionic); @@ -4850,11 +5421,11 @@ } }, alert: function(message) { var _this = this; - window.rAF(function() { + ionic.requestAnimationFrame(function() { _this.setTitle(message); _this.el.classList.add('active'); }); }, hide: function() { @@ -4876,25 +5447,33 @@ * It takes a DOM reference to that side menu element. */ ionic.views.SideMenu = ionic.views.View.inherit({ initialize: function(opts) { this.el = opts.el; - this.width = opts.width; this.isEnabled = opts.isEnabled || true; + this.setWidth(opts.width); }, getFullWidth: function() { return this.width; }, + setWidth: function(width) { + this.width = width; + this.el.style.width = width + 'px'; + }, setIsEnabled: function(isEnabled) { this.isEnabled = isEnabled; }, bringUp: function() { - this.el.style.zIndex = 0; + if(this.el.style.zIndex !== '0') { + this.el.style.zIndex = '0'; + } }, pushDown: function() { - this.el.style.zIndex = -1; + if(this.el.style.zIndex !== '-1') { + this.el.style.zIndex = '-1'; + } } }); ionic.views.SideMenuContent = ionic.views.View.inherit({ initialize: function(opts) { @@ -4920,15 +5499,15 @@ }, enableAnimation: function() { this.el.classList.add(this.animationClass); }, getTranslateX: function() { - return parseFloat(this.el.style.webkitTransform.replace('translate3d(', '').split(',')[0]); + return parseFloat(this.el.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]); }, - setTranslateX: function(x) { - this.el.style.webkitTransform = 'translate3d(' + x + 'px, 0, 0)'; - } + setTranslateX: ionic.animationFrameThrottle(function(x) { + this.el.style[ionic.CSS.TRANSFORM] = 'translate3d(' + x + 'px, 0, 0)'; + }) }); })(ionic); ; /* @@ -5160,11 +5739,11 @@ } function stop() { - delay = 0; + delay = options.auto || 0; clearTimeout(interval); } @@ -5229,10 +5808,11 @@ element.addEventListener('touchmove', this, false); element.addEventListener('touchend', this, false); } else { element.addEventListener('mousemove', this, false); element.addEventListener('mouseup', this, false); + document.addEventListener('mouseup', this, false); } }, move: function(event) { // ensure swiping with one touch and not pinching @@ -5372,10 +5952,11 @@ element.removeEventListener('touchmove', events, false) element.removeEventListener('touchend', events, false) } else { element.removeEventListener('mousemove', events, false) element.removeEventListener('mouseup', events, false) + document.removeEventListener('mouseup', events, false); } }, transitionEnd: function(event) { @@ -5520,10 +6101,11 @@ initialize: function(el) { this.el = el; this._buildItem(); }, + // Factory for creating an item from a given javascript object create: function(itemData) { var item = document.createElement('a'); item.className = 'tab-item'; @@ -5531,34 +6113,53 @@ if(itemData.icon) { var icon = document.createElement('i'); icon.className = itemData.icon; item.appendChild(icon); } + + // If there is a badge, add the badge element + if(itemData.badge) { + var badge = document.createElement('i'); + badge.className = 'badge'; + badge.innerHTML = itemData.badge; + item.appendChild(badge); + item.className = 'tab-item has-badge'; + } + item.appendChild(document.createTextNode(itemData.title)); return new ionic.views.TabBarItem(item); }, - _buildItem: function() { var _this = this, child, children = Array.prototype.slice.call(this.el.children); for(var i = 0, j = children.length; i < j; i++) { child = children[i]; // Test if this is a "i" tag with icon in the class name // TODO: This heuristic might not be sufficient if(child.tagName.toLowerCase() == 'i' && /icon/.test(child.className)) { this.icon = child.className; - break; } + // Test if this is a "i" tag with badge in the class name + // TODO: This heuristic might not be sufficient + if(child.tagName.toLowerCase() == 'i' && /badge/.test(child.className)) { + this.badge = child.textContent.trim(); + } } - // Set the title to the text content of the tab. - this.title = this.el.textContent.trim(); + this.title = ''; + for(i = 0, j = this.el.childNodes.length; i < j; i++) { + child = this.el.childNodes[i]; + if (child.nodeName === "#text") { + this.title += child.nodeValue.trim(); + } + } + this._tapHandler = function(e) { _this.onTap && _this.onTap(e); }; ionic.on('tap', this._tapHandler, this.el); @@ -5577,10 +6178,14 @@ getTitle: function() { return this.title; }, + getBadge: function() { + return this.badge; + }, + setSelected: function(isSelected) { this.isSelected = isSelected; if(isSelected) { this.el.classList.add('active'); } else { @@ -5719,21 +6324,24 @@ ionic.views.Toggle = ionic.views.View.inherit({ initialize: function(opts) { this.el = opts.el; this.checkbox = opts.checkbox; + this.track = opts.track; this.handle = opts.handle; this.openPercent = -1; }, tap: function(e) { - this.val( !this.checkbox.checked ); + if(this.el.getAttribute('disabled') !== 'disabled') { + this.val( !this.checkbox.checked ); + } }, drag: function(e) { - var slidePageLeft = this.checkbox.offsetLeft + (this.handle.offsetWidth / 2); - var slidePageRight = this.checkbox.offsetLeft + this.checkbox.offsetWidth - (this.handle.offsetWidth / 2); + var slidePageLeft = this.track.offsetLeft + (this.handle.offsetWidth / 2); + var slidePageRight = this.track.offsetLeft + this.track.offsetWidth - (this.handle.offsetWidth / 2); if(e.pageX >= slidePageRight - 4) { this.val(true); } else if(e.pageX <= slidePageLeft) { this.val(false); @@ -5750,25 +6358,25 @@ if(openPercent === 0) { this.val(false); } else if(openPercent === 100) { this.val(true); } else { - var openPixel = Math.round( (openPercent / 100) * this.checkbox.offsetWidth - (this.handle.offsetWidth) ); + var openPixel = Math.round( (openPercent / 100) * this.track.offsetWidth - (this.handle.offsetWidth) ); openPixel = (openPixel < 1 ? 0 : openPixel); - this.handle.style.webkitTransform = 'translate3d(' + openPixel + 'px,0,0)'; + this.handle.style[ionic.CSS.TRANSFORM] = 'translate3d(' + openPixel + 'px,0,0)'; } } }, release: function(e) { this.val( this.openPercent >= 50 ); }, val: function(value) { if(value === true || value === false) { - if(this.handle.style.webkitTransform !== "") { - this.handle.style.webkitTransform = ""; + if(this.handle.style[ionic.CSS.TRANSFORM] !== "") { + this.handle.style[ionic.CSS.TRANSFORM] = ""; } this.checkbox.checked = value; this.openPercent = (value ? 100 : 0); } return this.checkbox.checked; @@ -6034,11 +6642,11 @@ /** * @return {float} The amount the side menu is open, either positive or negative for left (positive), or right (negative) */ getOpenAmount: function() { - return this.content.getTranslateX() || 0; + return this.content && this.content.getTranslateX() || 0; }, /** * @return {float} The ratio of open amount over menu width. For example, a * menu of width 100 open 50 pixels would be open 50% or a ratio of 0.5. Value is negative @@ -6346,10 +6954,11 @@ addController: function(controller) { this.controllers.push(controller); this.tabBar.addItem({ title: controller.title, - icon: controller.icon + icon: controller.icon, + badge: controller.badge }); // If we don't have a selected controller yet, select the first one. if(!this.selectedController) { this.setSelectedController(0);