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);