From f4131dd839f0ea5807b609060143ae2d58c996bd Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Tue, 24 Sep 2013 13:06:06 -0500 Subject: [PATCH] Modals with test and slide in up animation --- dist/ionic.css | 39 +- dist/ionic.js | 2337 +++++++++++++++++++++++++++++++++++ scss/_variables.scss | 9 + scss/ionic.scss | 1 + scss/ionic/_animations.scss | 20 + scss/ionic/_modal.scss | 35 + test/modals.html | 29 +- 7 files changed, 2467 insertions(+), 3 deletions(-) create mode 100644 dist/ionic.js create mode 100644 scss/ionic/_modal.scss diff --git a/dist/ionic.css b/dist/ionic.css index f85594e84c..81fae96569 100644 --- a/dist/ionic.css +++ b/dist/ionic.css @@ -1430,6 +1430,26 @@ address { -moz-transition: -moz-transform 200ms ease; transition: transform 200ms ease; } +/** + * Modals are independent windows that slide in from off-screen. + */ +.modal { + position: fixed; + z-index: 10; + top: 0; + width: 100%; + min-height: 100%; + overflow: hidden; + background-color: white; + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); } + .modal.active { + opacity: 1; + height: 100%; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); } + .list { margin-bottom: 20px; padding-left: 0; @@ -2110,8 +2130,23 @@ a.button { .alert-info h4 { color: #3a87ad; } +/* .slide-in-up.enter { -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); - -webkit-transition: -webkit-transform 0.25s ease-in-out, opacity 1ms 0.25s; - transition: transform 0.25s ease-in-out, opacity 1ms 0.25s; } + + -webkit-transition: -webkit-transform .25s ease-in-out, opacity 1ms .25s; + transition: transform .25s ease-in-out, opacity 1ms .25s; +} +*/ +.slide-in-up { + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + -webkit-transition: -webkit-transform 0.25s, opacity 1ms 0.25s; + transition: transform 0.25s, opacity 1ms 0.25s; + -webkit-transition-timing-function: cubic-bezier(0.1, 0.5, 0.1, 1); } + +.slide-in-up.active { + -webkit-transition: -webkit-transform 0.25s; + transition: transform 0.25s; + -webkit-transition-timing-function: cubic-bezier(0.1, 0.5, 0.1, 1); } diff --git a/dist/ionic.js b/dist/ionic.js new file mode 100644 index 0000000000..69a8b5b445 --- /dev/null +++ b/dist/ionic.js @@ -0,0 +1,2337 @@ +window.ionic = {}; + +function initalize() { + // remove the ready listeners + document.removeEventListener( "DOMContentLoaded", initalize, false ); + window.removeEventListener( "load", initalize, false ); + + /* + // trigger that the DOM is ready + ionic.trigger("ready"); + + // trigger that the start page is in view + ionic.trigger("pageview"); + + // trigger that the webapp has been initalized + ionic.trigger("initalized"); + */ +} + +// When the DOM is ready, initalize the webapp +if ( document.readyState === "complete" ) { + // DOM is already ready + setTimeout( initalize ); +} else { + // DOM isn't ready yet, add event listeners + document.addEventListener( "DOMContentLoaded", initalize, false ); + window.addEventListener( "load", initalize, false ); +} +;(function(ionic) { + + ionic.Platform = { + detect: function() { + var platforms = []; + + console.log('Checking platforms'); + var platform = this._checkPlatforms(platforms); + console.log('Got platforms', platforms); + + for(var i = 0; i < platforms.length; i++) { + document.body.classList.add('platform-' + platforms[i]); + } + + }, + _checkPlatforms: function(platforms) { + if(this.isCordova()) { + platforms.push('cordova'); + } + if(this.isIOS7()) { + platforms.push('ios7'); + } + }, + + // Check if we are running in Cordova, which will have + // window.device available. + isCordova: function() { + return 'device' in window; + }, + isIOS7: function() { + if(!window.device) { + return false; + } + return parseFloat(window.device.version) >= 7.0; + } + } + + ionic.Platform.detect(); +})(ionic = window.ionic || {}); +;(function(ionic) { + + ionic.Utils = { + /** + * extend method, + * also used for cloning when dest is an empty object + * @param {Object} dest + * @param {Object} src + * @parm {Boolean} merge do a merge + * @returns {Object} dest + */ + extend: function extend(dest, src, merge) { + for (var key in src) { + if(dest[key] !== undefined && merge) { + continue; + } + dest[key] = src[key]; + } + return dest; + }, + } +})(ionic = window.ionic || {}); +;/** + * ion-events.js + * + * Author: Max Lynch + * + * 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! + */ + +(function(ionic) { + ionic.EventController = { + VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'], + + // Trigger a new event + trigger: function(eventType, data) { + // TODO: Do we need to use the old-school createEvent stuff? + var event = new CustomEvent(eventType, data); + + // 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.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 + for(var i = 0, j = this.VIRTUALIZED_EVENTS.length; i < j; i++) { + if(type == this.VIRTUALIZED_EVENTS[i]) { + var gesture = new ionic.Gesture(element); + gesture.on(type, callback); + return gesture; + } + } + + // Otherwise bind a normal event + e.addEventListener(type, callback); + }, + + off: function(type, callback, element) { + element.removeEventListener(type, callback); + }, + + // Register for a new gesture event on the given element + onGesture: function(type, callback, element) { + var gesture = new ionic.Gesture(element); + gesture.on(type, callback); + return gesture; + }, + + // Unregister a previous gesture event + offGesture: function(gesture, type, callback) { + gesture.off(type, callback); + }, + + // With a click event, we need to check the target + // and if it's an internal target that doesn't want + // a click, cancel it + handleClick: function(e) { + var target = e.target; + + if(ionic.Gestures.HAS_TOUCHEVENTS) { + // We don't allow any clicks on mobile + e.preventDefault(); + return false; + } + + if ( + ! target + || e.which > 1 + || e.metaKey + || e.ctrlKey + //|| isScrolling + || location.protocol !== target.protocol + || location.host !== target.host + // Not sure abotu this one + //|| !target.hash && /#/.test(target.href) + || target.hash && target.href.replace(target.hash, '') === location.href.replace(location.hash, '') + //|| target.getAttribute('data-ignore') == 'push' + ) { + // Allow it + return; + } + // We need to cancel this one + e.preventDefault(); + + }, + + handlePopState: function(event) { + console.log("EVENT: popstate", 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 = function() { ionic.EventController.trigger.apply(ionic.EventController.trigger, arguments); } + ionic.onGesture = function() { ionic.EventController.onGesture.apply(ionic.EventController.onGesture, arguments); } + ionic.offGesture = function() { ionic.EventController.offGesture.apply(ionic.EventController.offGesture, arguments); } + + // Set up various listeners + window.addEventListener('click', ionic.EventController.handleClick); + +})(ionic = window.ionic || {}); +;/** + * Simple gesture controllers with some common gestures that emit + * gesture events. + * + * Ported from github.com/EightMedia/hammer.js - thanks! + */ +(function(ionic) { + + /** + * ionic.Gestures + * use this to create instances + * @param {HTMLElement} element + * @param {Object} options + * @returns {ionic.Gestures.Instance} + * @constructor + */ + ionic.Gesture = function(element, options) { + return new ionic.Gestures.Instance(element, options || {}); + }; + + 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 + // 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 + }; + + // detect touchevents + ionic.Gestures.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled; + ionic.Gestures.HAS_TOUCHEVENTS = ('ontouchstart' in window); + + // dont use mouseevents on mobile devices + ionic.Gestures.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i; + ionic.Gestures.NO_MOUSEEVENTS = ionic.Gestures.HAS_TOUCHEVENTS && window.navigator.userAgent.match(ionic.Gestures.MOBILE_REGEX); + + // eventtypes per touchevent (start, move, end) + // are filled by ionic.Gestures.event.determineEventTypes on setup + ionic.Gestures.EVENT_TYPES = {}; + + // direction defines + ionic.Gestures.DIRECTION_DOWN = 'down'; + ionic.Gestures.DIRECTION_LEFT = 'left'; + ionic.Gestures.DIRECTION_UP = 'up'; + ionic.Gestures.DIRECTION_RIGHT = 'right'; + + // pointer type + ionic.Gestures.POINTER_MOUSE = 'mouse'; + ionic.Gestures.POINTER_TOUCH = 'touch'; + ionic.Gestures.POINTER_PEN = 'pen'; + + // touch event defines + ionic.Gestures.EVENT_START = 'start'; + ionic.Gestures.EVENT_MOVE = 'move'; + ionic.Gestures.EVENT_END = 'end'; + + // hammer document where the base events are added at + ionic.Gestures.DOCUMENT = window.document; + + // plugins namespace + ionic.Gestures.plugins = {}; + + // if the window events are set... + ionic.Gestures.READY = false; + + /** + * setup events to detect gestures on the document + */ + function setup() { + if(ionic.Gestures.READY) { + return; + } + + // find what eventtypes we add listeners to + ionic.Gestures.event.determineEventTypes(); + + // Register all gestures inside ionic.Gestures.gestures + for(var name in ionic.Gestures.gestures) { + if(ionic.Gestures.gestures.hasOwnProperty(name)) { + ionic.Gestures.detection.register(ionic.Gestures.gestures[name]); + } + } + + // Add touch events on the document + ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_MOVE, ionic.Gestures.detection.detect); + ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_END, ionic.Gestures.detection.detect); + + // ionic.Gestures is ready...! + ionic.Gestures.READY = true; + } + + /** + * 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} + * @constructor + */ + ionic.Gestures.Instance = function(element, options) { + var self = this; + + // A null element was passed into the instance, which means + // whatever lookup was done to find this element failed to find it + // so we can't listen for events on it. + if(element === null) { + console.error('Null element passed to gesture (element does not exist). Not listening for gesture'); + return; + } + + // setup ionic.GesturesJS window events and register all gestures + // this also sets up the default options + setup(); + + this.element = element; + + // start/stop detection option + this.enabled = true; + + // merge options + this.options = ionic.Gestures.utils.extend( + ionic.Gestures.utils.extend({}, ionic.Gestures.defaults), + options || {}); + + // add some css to the element to prevent the browser from doing its native behavoir + if(this.options.stop_browser_behavior) { + ionic.Gestures.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior); + } + + // start detection on touchstart + ionic.Gestures.event.onTouch(element, ionic.Gestures.EVENT_START, function(ev) { + if(self.enabled) { + ionic.Gestures.detection.startDetect(self, ev); + } + }); + + // return instance + return this; + }; + + + ionic.Gestures.Instance.prototype = { + /** + * bind events to the instance + * @param {String} gesture + * @param {Function} handler + * @returns {ionic.Gestures.Instance} + */ + on: function onEvent(gesture, handler){ + var gestures = gesture.split(' '); + for(var t=0; t 0 && eventType == ionic.Gestures.EVENT_END) { + eventType = ionic.Gestures.EVENT_MOVE; + } + // no touches, force the end event + else if(!count_touches) { + eventType = ionic.Gestures.EVENT_END; + } + + // store the last move event + if(count_touches || last_move_event === null) { + last_move_event = ev; + } + + // trigger the handler + handler.call(ionic.Gestures.detection, self.collectEventData(element, eventType, self.getTouchList(last_move_event, eventType), ev)); + + // remove pointerevent from list + if(ionic.Gestures.HAS_POINTEREVENTS && eventType == ionic.Gestures.EVENT_END) { + count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev); + } + } + + //debug(sourceEventType +" "+ eventType); + + // on the end we reset everything + if(!count_touches) { + last_move_event = null; + enable_detect = false; + touch_triggered = false; + ionic.Gestures.PointerEvent.reset(); + } + }); + }, + + + /** + * we have different events for each device/browser + * determine what we need and set them in the ionic.Gestures.EVENT_TYPES constant + */ + determineEventTypes: function determineEventTypes() { + // determine the eventtype we want to set + var types; + + // pointerEvents magic + if(ionic.Gestures.HAS_POINTEREVENTS) { + types = ionic.Gestures.PointerEvent.getEvents(); + } + // on Android, iOS, blackberry, windows mobile we dont want any mouseevents + else if(ionic.Gestures.NO_MOUSEEVENTS) { + types = [ + 'touchstart', + 'touchmove', + 'touchend touchcancel']; + } + // for non pointer events browsers and mixed browsers, + // like chrome on windows8 touch laptop + else { + types = [ + 'touchstart mousedown', + 'touchmove mousemove', + 'touchend touchcancel mouseup']; + } + + ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_START] = types[0]; + ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_MOVE] = types[1]; + ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_END] = types[2]; + }, + + + /** + * create touchlist depending on the event + * @param {Object} ev + * @param {String} eventType used by the fakemultitouch plugin + */ + getTouchList: function getTouchList(ev/*, eventType*/) { + // get the fake pointerEvent touchlist + if(ionic.Gestures.HAS_POINTEREVENTS) { + return ionic.Gestures.PointerEvent.getTouchList(); + } + // get the touchlist + else if(ev.touches) { + return ev.touches; + } + // make fake touchlist from mouse position + else { + ev.indentifier = 1; + return [ev]; + } + }, + + + /** + * collect event data for ionic.Gestures js + * @param {HTMLElement} element + * @param {String} eventType like ionic.Gestures.EVENT_MOVE + * @param {Object} eventData + */ + collectEventData: function collectEventData(element, eventType, touches, ev) { + + // find out pointerType + var pointerType = ionic.Gestures.POINTER_TOUCH; + if(ev.type.match(/mouse/) || ionic.Gestures.PointerEvent.matchType(ionic.Gestures.POINTER_MOUSE, ev)) { + pointerType = ionic.Gestures.POINTER_MOUSE; + } + + return { + center : ionic.Gestures.utils.getCenter(touches), + timeStamp : new Date().getTime(), + target : ev.target, + touches : touches, + eventType : eventType, + pointerType : pointerType, + srcEvent : ev, + + /** + * prevent the browser default actions + * mostly used to disable scrolling of the browser + */ + preventDefault: function() { + if(this.srcEvent.preventManipulation) { + this.srcEvent.preventManipulation(); + } + + if(this.srcEvent.preventDefault) { + this.srcEvent.preventDefault(); + } + }, + + /** + * stop bubbling the event up to its parents + */ + stopPropagation: function() { + this.srcEvent.stopPropagation(); + }, + + /** + * immediately stop gesture detection + * might be useful after a swipe was detected + * @return {*} + */ + stopDetect: function() { + return ionic.Gestures.detection.stopDetect(); + } + }; + } + }; + + ionic.Gestures.PointerEvent = { + /** + * holds all pointers + * @type {Object} + */ + pointers: {}, + + /** + * get a list of pointers + * @returns {Array} touchlist + */ + getTouchList: function() { + var self = this; + var touchlist = []; + + // we can use forEach since pointerEvents only is in IE10 + Object.keys(self.pointers).sort().forEach(function(id) { + touchlist.push(self.pointers[id]); + }); + return touchlist; + }, + + /** + * update the position of a pointer + * @param {String} type ionic.Gestures.EVENT_END + * @param {Object} pointerEvent + */ + updatePointer: function(type, pointerEvent) { + if(type == ionic.Gestures.EVENT_END) { + this.pointers = {}; + } + else { + pointerEvent.identifier = pointerEvent.pointerId; + this.pointers[pointerEvent.pointerId] = pointerEvent; + } + + return Object.keys(this.pointers).length; + }, + + /** + * check if ev matches pointertype + * @param {String} pointerType ionic.Gestures.POINTER_MOUSE + * @param {PointerEvent} ev + */ + matchType: function(pointerType, ev) { + if(!ev.pointerType) { + return false; + } + + var types = {}; + types[ionic.Gestures.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == ionic.Gestures.POINTER_MOUSE); + types[ionic.Gestures.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == ionic.Gestures.POINTER_TOUCH); + types[ionic.Gestures.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == ionic.Gestures.POINTER_PEN); + return types[pointerType]; + }, + + + /** + * get events + */ + getEvents: function() { + return [ + 'pointerdown MSPointerDown', + 'pointermove MSPointerMove', + 'pointerup pointercancel MSPointerUp MSPointerCancel' + ]; + }, + + /** + * reset the list + */ + reset: function() { + this.pointers = {}; + } + }; + + + ionic.Gestures.utils = { + /** + * extend method, + * also used for cloning when dest is an empty object + * @param {Object} dest + * @param {Object} src + * @parm {Boolean} merge do a merge + * @returns {Object} dest + */ + extend: function extend(dest, src, merge) { + for (var key in src) { + if(dest[key] !== undefined && merge) { + continue; + } + dest[key] = src[key]; + } + return dest; + }, + + + /** + * find if a node is in the given parent + * used for event delegation tricks + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @returns {boolean} has_parent + */ + hasParent: function(node, parent) { + while(node){ + if(node == parent) { + return true; + } + node = node.parentNode; + } + return false; + }, + + + /** + * get the center of all the touches + * @param {Array} touches + * @returns {Object} center + */ + getCenter: function getCenter(touches) { + var valuesX = [], valuesY = []; + + for(var t= 0,len=touches.length; t= y) { + return touch1.pageX - touch2.pageX > 0 ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT; + } + else { + return touch1.pageY - touch2.pageY > 0 ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN; + } + }, + + + /** + * calculate the distance between two touches + * @param {Touch} touch1 + * @param {Touch} touch2 + * @returns {Number} distance + */ + getDistance: function getDistance(touch1, touch2) { + var x = touch2.pageX - touch1.pageX, + y = touch2.pageY - touch1.pageY; + return Math.sqrt((x*x) + (y*y)); + }, + + + /** + * calculate the scale factor between two touchLists (fingers) + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @param {Array} start + * @param {Array} end + * @returns {Number} scale + */ + getScale: function getScale(start, end) { + // need two fingers... + if(start.length >= 2 && end.length >= 2) { + return this.getDistance(end[0], end[1]) / + this.getDistance(start[0], start[1]); + } + return 1; + }, + + + /** + * calculate the rotation degrees between two touchLists (fingers) + * @param {Array} start + * @param {Array} end + * @returns {Number} rotation + */ + getRotation: function getRotation(start, end) { + // need two fingers + if(start.length >= 2 && end.length >= 2) { + return this.getAngle(end[1], end[0]) - + this.getAngle(start[1], start[0]); + } + return 0; + }, + + + /** + * boolean if the direction is vertical + * @param {String} direction + * @returns {Boolean} is_vertical + */ + isVertical: function isVertical(direction) { + return (direction == ionic.Gestures.DIRECTION_UP || direction == ionic.Gestures.DIRECTION_DOWN); + }, + + + /** + * stop browser default behavior with css props + * @param {HtmlElement} element + * @param {Object} css_props + */ + 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') { + element.onselectstart = function() { + return false; + }; + } + } + }; + + + ionic.Gestures.detection = { + // contains all registred ionic.Gestures.gestures in the correct order + gestures: [], + + // data of the current ionic.Gestures.gesture detection session + current: null, + + // the previous ionic.Gestures.gesture session data + // is a full clone of the previous gesture.current object + previous: null, + + // when this becomes true, no gestures are fired + stopped: false, + + + /** + * start ionic.Gestures.gesture detection + * @param {ionic.Gestures.Instance} inst + * @param {Object} eventData + */ + startDetect: function startDetect(inst, eventData) { + // already busy with a ionic.Gestures.gesture detection on an element + if(this.current) { + return; + } + + this.stopped = false; + + this.current = { + inst : inst, // reference to ionic.GesturesInstance we're working for + startEvent : ionic.Gestures.utils.extend({}, eventData), // start eventData for distances, timing etc + lastEvent : false, // last eventData + name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc + }; + + this.detect(eventData); + }, + + + /** + * ionic.Gestures.gesture detection + * @param {Object} eventData + */ + detect: function detect(eventData) { + if(!this.current || this.stopped) { + return; + } + + // extend event data with calculations about scale, distance etc + eventData = this.extendEventData(eventData); + + // instance options + var inst_options = this.current.inst.options; + + // call ionic.Gestures.gesture handlers + for(var g=0,len=this.gestures.length; g b.index) { + return 1; + } + return 0; + }); + + return this.gestures; + } + }; + + + ionic.Gestures.gestures = ionic.Gestures.gestures || {}; + + /** + * Custom gestures + * ============================== + * + * Gesture object + * -------------------- + * The object structure of a gesture: + * + * { name: 'mygesture', + * index: 1337, + * defaults: { + * mygesture_option: true + * } + * handler: function(type, ev, inst) { + * // trigger gesture event + * inst.trigger(this.name, ev); + * } + * } + + * @param {String} name + * this should be the name of the gesture, lowercase + * it is also being used to disable/enable the gesture per instance config. + * + * @param {Number} [index=1000] + * the index of the gesture, where it is going to be in the stack of gestures detection + * like when you build an gesture that depends on the drag gesture, it is a good + * idea to place it after the index of the drag gesture. + * + * @param {Object} [defaults={}] + * the default settings of the gesture. these are added to the instance settings, + * and can be overruled per instance. you can also add the name of the gesture, + * but this is also added by default (and set to true). + * + * @param {Function} handler + * this handles the gesture detection of your custom gesture and receives the + * following arguments: + * + * @param {Object} eventData + * event data containing the following properties: + * timeStamp {Number} time the event occurred + * target {HTMLElement} target element + * touches {Array} touches (fingers, pointers, mouse) on the screen + * pointerType {String} kind of pointer that was used. matches ionic.Gestures.POINTER_MOUSE|TOUCH + * center {Object} center position of the touches. contains pageX and pageY + * deltaTime {Number} the total time of the touches in the screen + * deltaX {Number} the delta on x axis we haved moved + * deltaY {Number} the delta on y axis we haved moved + * velocityX {Number} the velocity on the x + * velocityY {Number} the velocity on y + * angle {Number} the angle we are moving + * direction {String} the direction we are moving. matches ionic.Gestures.DIRECTION_UP|DOWN|LEFT|RIGHT + * distance {Number} the distance we haved moved + * scale {Number} scaling of the touches, needs 2 touches + * rotation {Number} rotation of the touches, needs 2 touches * + * eventType {String} matches ionic.Gestures.EVENT_START|MOVE|END + * srcEvent {Object} the source event, like TouchStart or MouseDown * + * startEvent {Object} contains the same properties as above, + * but from the first touch. this is used to calculate + * distances, deltaTime, scaling etc + * + * @param {ionic.Gestures.Instance} inst + * the instance we are doing the detection for. you can get the options from + * the inst.options object and trigger the gesture event by calling inst.trigger + * + * + * Handle gestures + * -------------------- + * inside the handler you can get/set ionic.Gestures.detectionic.current. This is the current + * detection sessionic. It has the following properties + * @param {String} name + * contains the name of the gesture we have detected. it has not a real function, + * only to check in other gestures if something is detected. + * like in the drag gesture we set it to 'drag' and in the swipe gesture we can + * check if the current gesture is 'drag' by accessing ionic.Gestures.detectionic.current.name + * + * @readonly + * @param {ionic.Gestures.Instance} inst + * the instance we do the detection for + * + * @readonly + * @param {Object} startEvent + * contains the properties of the first gesture detection in this sessionic. + * Used for calculations about timing, distance, etc. + * + * @readonly + * @param {Object} lastEvent + * contains all the properties of the last gesture detect in this sessionic. + * + * after the gesture detection session has been completed (user has released the screen) + * the ionic.Gestures.detectionic.current object is copied into ionic.Gestures.detectionic.previous, + * this is usefull for gestures like doubletap, where you need to know if the + * previous gesture was a tap + * + * options that have been set by the instance can be received by calling inst.options + * + * You can trigger a gesture event by calling inst.trigger("mygesture", event). + * The first param is the name of your gesture, the second the event argument + * + * + * Register gestures + * -------------------- + * When an gesture is added to the ionic.Gestures.gestures object, it is auto registered + * at the setup of the first ionic.Gestures instance. You can also call ionic.Gestures.detectionic.register + * manually and pass your gesture object as a param + * + */ + + /** + * Hold + * Touch stays at the same place for x time + * @events hold + */ + ionic.Gestures.gestures.Hold = { + name: 'hold', + index: 10, + defaults: { + hold_timeout : 500, + hold_threshold : 1 + }, + timer: null, + handler: function holdGesture(ev, inst) { + switch(ev.eventType) { + case ionic.Gestures.EVENT_START: + // clear any running timers + clearTimeout(this.timer); + + // set the gesture so we can check in the timeout if it still is + ionic.Gestures.detection.current.name = this.name; + + // set timer and if after the timeout it still is hold, + // we trigger the hold event + this.timer = setTimeout(function() { + if(ionic.Gestures.detection.current.name == 'hold') { + inst.trigger('hold', ev); + } + }, inst.options.hold_timeout); + break; + + // when you move or end we clear the timer + case ionic.Gestures.EVENT_MOVE: + if(ev.distance > inst.options.hold_threshold) { + clearTimeout(this.timer); + } + break; + + case ionic.Gestures.EVENT_END: + clearTimeout(this.timer); + break; + } + } + }; + + + /** + * Tap/DoubleTap + * Quick touch at a place or double at the same place + * @events tap, doubletap + */ + ionic.Gestures.gestures.Tap = { + name: 'tap', + index: 100, + defaults: { + tap_max_touchtime : 250, + tap_max_distance : 10, + tap_always : true, + doubletap_distance : 20, + doubletap_interval : 300 + }, + handler: function tapGesture(ev, inst) { + if(ev.eventType == ionic.Gestures.EVENT_END) { + // previous gesture, for the double tap since these are two different gesture detections + var prev = ionic.Gestures.detection.previous, + did_doubletap = false; + + // when the touchtime is higher then the max touch time + // or when the moving distance is too much + if(ev.deltaTime > inst.options.tap_max_touchtime || + ev.distance > inst.options.tap_max_distance) { + return; + } + + // check if double tap + if(prev && prev.name == 'tap' && + (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval && + ev.distance < inst.options.doubletap_distance) { + inst.trigger('doubletap', ev); + did_doubletap = true; + } + + // do a single tap + if(!did_doubletap || inst.options.tap_always) { + ionic.Gestures.detection.current.name = 'tap'; + inst.trigger(ionic.Gestures.detection.current.name, ev); + } + } + } + }; + + + /** + * Swipe + * triggers swipe events when the end velocity is above the threshold + * @events swipe, swipeleft, swiperight, swipeup, swipedown + */ + ionic.Gestures.gestures.Swipe = { + name: 'swipe', + index: 40, + defaults: { + // set 0 for unlimited, but this can conflict with transform + swipe_max_touches : 1, + swipe_velocity : 0.7 + }, + handler: function swipeGesture(ev, inst) { + if(ev.eventType == ionic.Gestures.EVENT_END) { + // max touches + if(inst.options.swipe_max_touches > 0 && + ev.touches.length > inst.options.swipe_max_touches) { + return; + } + + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.velocityX > inst.options.swipe_velocity || + ev.velocityY > inst.options.swipe_velocity) { + // trigger swipe events + inst.trigger(this.name, ev); + inst.trigger(this.name + ev.direction, ev); + } + } + } + }; + + + /** + * Drag + * Move with x fingers (default 1) around on the page. Blocking the scrolling when + * moving left and right is a good practice. When all the drag events are blocking + * you disable scrolling on that area. + * @events drag, drapleft, dragright, dragup, dragdown + */ + ionic.Gestures.gestures.Drag = { + name: 'drag', + index: 50, + defaults: { + drag_min_distance : 10, + // Set correct_for_drag_min_distance to true to make the starting point of the drag + // be calculated from where the drag was triggered, not from where the touch started. + // Useful to avoid a jerk-starting drag, which can make fine-adjustments + // through dragging difficult, and be visually unappealing. + correct_for_drag_min_distance : true, + // set 0 for unlimited, but this can conflict with transform + drag_max_touches : 1, + // prevent default browser behavior when dragging occurs + // be careful with it, it makes the element a blocking element + // when you are using the drag gesture, it is a good practice to set this true + drag_block_horizontal : true, + drag_block_vertical : true, + // drag_lock_to_axis keeps the drag gesture on the axis that it started on, + // It disallows vertical directions if the initial direction was horizontal, and vice versa. + drag_lock_to_axis : false, + // drag lock only kicks in when distance > drag_lock_min_distance + // This way, locking occurs only when the distance has become large enough to reliably determine the direction + drag_lock_min_distance : 25 + }, + triggered: false, + handler: function dragGesture(ev, inst) { + // current gesture isnt drag, but dragged is true + // this means an other gesture is busy. now call dragend + if(ionic.Gestures.detection.current.name != this.name && this.triggered) { + inst.trigger(this.name +'end', ev); + this.triggered = false; + return; + } + + // max touches + if(inst.options.drag_max_touches > 0 && + ev.touches.length > inst.options.drag_max_touches) { + return; + } + + switch(ev.eventType) { + case ionic.Gestures.EVENT_START: + this.triggered = false; + break; + + case ionic.Gestures.EVENT_MOVE: + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.distance < inst.options.drag_min_distance && + ionic.Gestures.detection.current.name != this.name) { + return; + } + + // we are dragging! + if(ionic.Gestures.detection.current.name != this.name) { + ionic.Gestures.detection.current.name = this.name; + if (inst.options.correct_for_drag_min_distance) { + // When a drag is triggered, set the event center to drag_min_distance pixels from the original event center. + // Without this correction, the dragged distance would jumpstart at drag_min_distance pixels instead of at 0. + // It might be useful to save the original start point somewhere + var factor = Math.abs(inst.options.drag_min_distance/ev.distance); + ionic.Gestures.detection.current.startEvent.center.pageX += ev.deltaX * factor; + ionic.Gestures.detection.current.startEvent.center.pageY += ev.deltaY * factor; + + // recalculate event data using new start point + ev = ionic.Gestures.detection.extendEventData(ev); + } + } + + // lock drag to axis? + if(ionic.Gestures.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) { + ev.drag_locked_to_axis = true; + } + var last_direction = ionic.Gestures.detection.current.lastEvent.direction; + if(ev.drag_locked_to_axis && last_direction !== ev.direction) { + // keep direction on the axis that the drag gesture started on + if(ionic.Gestures.utils.isVertical(last_direction)) { + ev.direction = (ev.deltaY < 0) ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN; + } + else { + ev.direction = (ev.deltaX < 0) ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT; + } + } + + // first time, trigger dragstart event + if(!this.triggered) { + inst.trigger(this.name +'start', ev); + this.triggered = true; + } + + // trigger normal event + inst.trigger(this.name, ev); + + // direction event, like dragdown + inst.trigger(this.name + ev.direction, ev); + + // block the browser events + if( (inst.options.drag_block_vertical && ionic.Gestures.utils.isVertical(ev.direction)) || + (inst.options.drag_block_horizontal && !ionic.Gestures.utils.isVertical(ev.direction))) { + ev.preventDefault(); + } + break; + + case ionic.Gestures.EVENT_END: + // trigger dragend + if(this.triggered) { + inst.trigger(this.name +'end', ev); + } + + this.triggered = false; + break; + } + } + }; + + + /** + * Transform + * User want to scale or rotate with 2 fingers + * @events transform, pinch, pinchin, pinchout, rotate + */ + ionic.Gestures.gestures.Transform = { + name: 'transform', + index: 45, + defaults: { + // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 + transform_min_scale : 0.01, + // rotation in degrees + transform_min_rotation : 1, + // prevent default browser behavior when two touches are on the screen + // but it makes the element a blocking element + // when you are using the transform gesture, it is a good practice to set this true + transform_always_block : false + }, + triggered: false, + handler: function transformGesture(ev, inst) { + // current gesture isnt drag, but dragged is true + // this means an other gesture is busy. now call dragend + if(ionic.Gestures.detection.current.name != this.name && this.triggered) { + inst.trigger(this.name +'end', ev); + this.triggered = false; + return; + } + + // atleast multitouch + if(ev.touches.length < 2) { + return; + } + + // prevent default when two fingers are on the screen + if(inst.options.transform_always_block) { + ev.preventDefault(); + } + + switch(ev.eventType) { + case ionic.Gestures.EVENT_START: + this.triggered = false; + break; + + case ionic.Gestures.EVENT_MOVE: + var scale_threshold = Math.abs(1-ev.scale); + var rotation_threshold = Math.abs(ev.rotation); + + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(scale_threshold < inst.options.transform_min_scale && + rotation_threshold < inst.options.transform_min_rotation) { + return; + } + + // we are transforming! + ionic.Gestures.detection.current.name = this.name; + + // first time, trigger dragstart event + if(!this.triggered) { + inst.trigger(this.name +'start', ev); + this.triggered = true; + } + + inst.trigger(this.name, ev); // basic transform event + + // trigger rotate event + if(rotation_threshold > inst.options.transform_min_rotation) { + inst.trigger('rotate', ev); + } + + // trigger pinch event + if(scale_threshold > inst.options.transform_min_scale) { + inst.trigger('pinch', ev); + inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev); + } + break; + + case ionic.Gestures.EVENT_END: + // trigger dragend + if(this.triggered) { + inst.trigger(this.name +'end', ev); + } + + this.triggered = false; + break; + } + } + }; + + + /** + * Touch + * Called as first, tells the user has touched the screen + * @events touch + */ + ionic.Gestures.gestures.Touch = { + name: 'touch', + index: -Infinity, + defaults: { + // call preventDefault at touchstart, and makes the element blocking by + // disabling the scrolling of the page, but it improves gestures like + // transforming and dragging. + // be careful with using this, it can be very annoying for users to be stuck + // on the page + prevent_default: false, + + // disable mouse events, so only touch (or pen!) input triggers events + prevent_mouseevents: false + }, + handler: function touchGesture(ev, inst) { + if(inst.options.prevent_mouseevents && ev.pointerType == ionic.Gestures.POINTER_MOUSE) { + ev.stopDetect(); + return; + } + + if(inst.options.prevent_default) { + ev.preventDefault(); + } + + if(ev.eventType == ionic.Gestures.EVENT_START) { + inst.trigger(this.name, ev); + } + } + }; + + + /** + * Release + * Called as last, tells the user has released the screen + * @events release + */ + ionic.Gestures.gestures.Release = { + name: 'release', + index: Infinity, + handler: function releaseGesture(ev, inst) { + if(ev.eventType == ionic.Gestures.EVENT_END) { + inst.trigger(this.name, ev); + } + } + }; +})(ionic = window.ionic || {}); +;(function(ionic) { + ionic.ViewController = function(options) { + this.init(); + }; + + ionic.ViewController.prototype = { + // Initialize this view controller + init: function() { + }, + // Destroy this view controller, including all child views + destroy: function() { + } + }; +})(ionic = window.ionic || {}); +;(function(ionic) { + +ionic.ui = ionic.ui || {}; + +ionic.ui.NavBar = function(opts) { + this.el = opts.el; + + this._titleEl = this.el.querySelector('.title'); +}; + +ionic.ui.NavBar.prototype = { + shouldGoBack: function() {}, + + setTitle: function(title) { + if(!this._titleEl) { + return; + } + this._titleEl.innerHTML = title; + }, + + showBackButton: function(shouldShow) { + var _this = this; + + if(!this._currentBackButton) { + var back = document.createElement('a'); + back.className = 'button back'; + back.innerHTML = 'Back'; + + this._currentBackButton = back; + this._currentBackButton.onclick = function(event) { + _this.shouldGoBack && _this.shouldGoBack(); + } + } + + if(shouldShow && !this._currentBackButton.parentNode) { + // Prepend the back button + this.el.insertBefore(this._currentBackButton, this.el.firstChild); + } else if(!shouldShow && this._currentBackButton.parentNode) { + // Remove the back button if it's there + this._currentBackButton.parentNode.removeChild(this._currentBackButton); + } + } +}; +})(ionic = window.ionic || {}); +;(function(ionic) { + +ionic.ui = ionic.ui || {}; + +ionic.ui.SideMenu = function(opts) { + this.el = opts.el; + this.width = opts.width; + this.isEnabled = opts.isEnabled || true; +}; + +ionic.ui.SideMenu.prototype = { + getFullWidth: function() { + return this.width; + }, + setIsEnabled: function(isEnabled) { + this.isEnabled = isEnabled; + }, + bringUp: function() { + this.el.style.zIndex = 0; + }, + pushDown: function() { + this.el.style.zIndex = -1; + } +}; +})(ionic = window.ionic || {}); +;(function(ionic) { + +ionic.ui = ionic.ui || {}; + +ionic.ui.TabBarItem = function(el) { + this.el = el; + + this._buildItem(); +}; +ionic.ui.TabBarItem.prototype = { + // Factory for creating an item from a given javascript object + create: function(itemData) { + var item = document.createElement('a'); + item.className = 'tab-item'; + + // If there is an icon, add the icon element + if(itemData.icon) { + var icon = document.createElement('i'); + icon.className = itemData.icon; + item.appendChild(icon); + } + item.appendChild(document.createTextNode(itemData.title)); + + return new ionic.ui.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; + } + + } + + // Set the title to the text content of the tab. + this.title = this.el.innerText.trim(); + + this._tapHandler = function(e) { + _this.onTap && _this.onTap(e); + }; + + ionic.on('tap', this._tapHandler, this.el); + }, + onTap: function(e) { + console.log('On tap'); + }, + + // Remove the event listeners from this object + destroy: function() { + ionic.off('tap', this._tapHandler, this.el); + }, + + getIcon: function() { + return this.icon; + }, + + getTitle: function() { + return this.title; + }, + + setSelected: function(isSelected) { + this.isSelected = isSelected; + if(isSelected) { + this.el.classList.add('active'); + } else { + this.el.classList.remove('active'); + } + } +}; + +ionic.ui.TabBar = function(opts) { + this.el = opts.el; + + this.items = []; + + this._buildItems(); +}; + +ionic.ui.TabBar.prototype = { + // get all the items for the TabBar + getItems: function() { + return this.items; + }, + + // Add an item to the tab bar + addItem: function(item) { + // Create a new TabItem + var tabItem = ionic.ui.TabBarItem.prototype.create(item); + + this.appendItemElement(tabItem); + + this.items.push(tabItem); + this._bindEventsOnItem(tabItem); + }, + + appendItemElement: function(item) { + if(!this.el) { + return; + } + this.el.appendChild(item.el); + }, + + // Remove an item from the tab bar + removeItem: function(index) { + var item = this.items[index]; + if(!item) { + return; + } + item.onTap = undefined; + item.destroy(); + }, + + _bindEventsOnItem: function(item) { + var _this = this; + + if(!this._itemTapHandler) { + this._itemTapHandler = function(e) { + //_this.selectItem(this); + _this.trySelectItem(this); + }; + } + item.onTap = this._itemTapHandler; + }, + + // Get the currently selected item + getSelectedItem: function() { + return this.selectedItem; + }, + + // Set the currently selected item by index + setSelectedItem: function(index) { + this.selectedItem = this.items[index]; + + // Deselect all + for(var i = 0, j = this.items.length; i < j; i += 1) { + this.items[i].setSelected(false); + } + + // Select the new item + if(this.selectedItem) { + this.selectedItem.setSelected(true); + //this.onTabSelected && this.onTabSelected(this.selectedItem, index); + } + }, + + // Select the given item assuming we can find it in our + // item list. + selectItem: function(item) { + for(var i = 0, j = this.items.length; i < j; i += 1) { + if(this.items[i] == item) { + this.setSelectedItem(i); + return; + } + } + }, + + // Try to select a given item. This triggers an event such + // that the view controller managing this tab bar can decide + // whether to select the item or cancel it. + trySelectItem: function(item) { + for(var i = 0, j = this.items.length; i < j; i += 1) { + if(this.items[i] == item) { + this.tryTabSelect && this.tryTabSelect(i); + return; + } + } + }, + + // Build the initial items list from the given DOM node. + _buildItems: function() { + + var item, items = Array.prototype.slice.call(this.el.children); + + for(var i = 0, j = items.length; i < j; i += 1) { + item = new ionic.ui.TabBarItem(items[i]); + this.items[i] = item; + this._bindEventsOnItem(item); + } + + if(this.items.length > 0) { + this.selectedItem = this.items[0]; + } + + }, + + // Destroy this tab bar + destroy: function() { + for(var i = 0, j = this.items.length; i < j; i += 1) { + this.items[i].destroy(); + } + this.items.length = 0; + } +}; + +})(ionic = window.ionic || {}); +;(function(ionic) { + +ionic.controllers = ionic.controllers || {}; + +ionic.controllers.NavController = function(opts) { + var _this = this; + + this.navBar = opts.navBar; + this.content = opts.content; + this.controllers = opts.controllers || []; + + this._updateNavBar(); + + // TODO: Is this the best way? + this.navBar.shouldGoBack = function() { + _this.pop(); + } +}; + +ionic.controllers.NavController.prototype = { + getControllers: function() { + return this.controllers; + }, + getTopController: function() { + return this.controllers[this.controllers.length-1]; + }, + push: function(controller) { + var last = this.controllers[this.controllers.length - 1]; + + this.controllers.push(controller); + + // Indicate we are switching controllers + var shouldSwitch = this.switchingController && this.switchingController(controller) || true; + + // Return if navigation cancelled + if(shouldSwitch === false) + return; + + // Actually switch the active controllers + + // Remove the old one + //last && last.detach(); + if(last) { + last.isVisible = false; + last.visibilityChanged && last.visibilityChanged(); + } + + // Grab the top controller on the stack + var next = this.controllers[this.controllers.length - 1]; + + // TODO: No DOM stuff here + //this.content.el.appendChild(next.el); + next.isVisible = true; + next.visibilityChanged && next.visibilityChanged(); + + this._updateNavBar(); + + return controller; + }, + + pop: function() { + var next, last; + + // Make sure we keep one on the stack at all times + if(this.controllers.length < 2) { + return; + } + + // Grab the controller behind the top one on the stack + last = this.controllers.pop(); + if(last) { + last.isVisible = false; + last.visibilityChanged && last.visibilityChanged(); + } + + // Remove the old one + //last && last.detach(); + + next = this.controllers[this.controllers.length - 1]; + + // TODO: No DOM stuff here + //this.content.el.appendChild(next.el); + next.isVisible = true; + next.visibilityChanged && next.visibilityChanged(); + + // Switch to it (TODO: Animate or such things here) + + this._updateNavBar(); + + return last; + }, + + _updateNavBar: function() { + if(!this.getTopController()) { + return; + } + + this.navBar.setTitle(this.getTopController().title); + + if(this.controllers.length > 1) { + this.navBar.showBackButton(true); + } else { + this.navBar.showBackButton(false); + } + }, + +}; +})(ionic = window.ionic || {}); +;(function(ionic) { + +ionic.controllers = ionic.controllers || {}; + +ionic.controllers.SideMenuController = function(options) { + var _this = this; + + + this.left = options.left; + this.right = options.right; + this.content = options.content; + + this._rightShowing = false; + this._leftShowing = false; + + this.content.onDrag = function(e) { + _this._handleDrag(e); + }; + + this.content.endDrag = function(e) { + _this._endDrag(e); + }; + + /* + // Bind release and drag listeners + window.ion.onGesture('release', function(e) { + _this._endDrag(e); + }, this.center); + + window.ion.onGesture('drag', function(e) { + _this._handleDrag(e); + }, this.center); + */ +}; + +ionic.controllers.SideMenuController.prototype = { + toggleLeft: function() { + var openAmount = this.getOpenAmount(); + if(openAmount > 0) { + this.openPercentage(0); + } else { + this.openPercentage(100); + } + }, + toggleRight: function() { + var openAmount = this.getOpenAmount(); + if(openAmount < 0) { + this.openPercentage(0); + } else { + this.openPercentage(-100); + } + }, + getOpenAmount: function() { + return this.content.getTranslateX() || 0; + }, + getOpenRatio: function() { + var amount = this.getOpenAmount(); + if(amount >= 0) { + return amount / this.left.width; + } + return amount / this.right.width; + }, + getOpenPercentage: function() { + return this.getOpenRatio() * 100; + }, + openPercentage: function(percentage) { + var p = percentage / 100; + var maxLeft = this.left.width; + var maxRight = this.right.width; + if(percentage >= 0) { + this.openAmount(maxLeft * p); + } else { + this.openAmount(maxRight * p); + } + }, + openAmount: function(amount) { + var maxLeft = this.left.width; + var maxRight = this.right.width; + + // Check if we can move to that side, depending if the left/right panel is enabled + if((!this.left.isEnabled && amount > 0) || (!this.right.isEnabled && amount < 0)) { + return; + } + + if((this._leftShowing && amount > maxLeft) || (this._rightShowing && amount < -maxRight)) { + return; + } + + this.content.setTranslateX(amount); + + if(amount >= 0) { + this._leftShowing = true; + this._rightShowing = false; + + // Push the z-index of the right menu down + this.right.pushDown(); + // Bring the z-index of the left menu up + this.left.bringUp(); + } else { + this._rightShowing = true; + this._leftShowing = false; + + // Bring the z-index of the right menu up + this.right.bringUp(); + // Push the z-index of the left menu down + this.left.pushDown(); + } + }, + snapToRest: function(e) { + // We want to animate at the end of this + this.content.enableAnimation(); + this._isDragging = false; + + // Check how much the panel is open after the drag, and + // what the drag velocity is + var ratio = this.getOpenRatio(); + + if(ratio == 0) + return; + + var velocityThreshold = 0.3; + var velocityX = e.gesture.velocityX + var direction = e.gesture.direction; + + // Less than half, going left + //if(ratio > 0 && ratio < 0.5 && direction == 'left' && velocityX < velocityThreshold) { + //this.openPercentage(0); + //} + + // Going right, less than half, too slow (snap back) + if(ratio > 0 && ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) { + this.openPercentage(0); + } + + // Going left, more than half, too slow (snap back) + else if(ratio > 0.5 && direction == 'left' && velocityX < velocityThreshold) { + this.openPercentage(100); + } + + // Going left, less than half, too slow (snap back) + else if(ratio < 0 && ratio > -0.5 && direction == 'left' && velocityX < velocityThreshold) { + this.openPercentage(0); + } + + // Going right, more than half, too slow (snap back) + else if(ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) { + this.openPercentage(-100); + } + + // Going right, more than half, or quickly (snap open) + else if(direction == 'right' && ratio >= 0 && (ratio >= 0.5 || velocityX > velocityThreshold)) { + this.openPercentage(100); + } + + // Going left, more than half, or quickly (span open) + else if(direction == 'left' && ratio <= 0 && (ratio <= -0.5 || velocityX > velocityThreshold)) { + this.openPercentage(-100); + } + + // Snap back for safety + else { + this.openPercentage(0); + } + }, + _endDrag: function(e) { + this.snapToRest(e); + }, + _initDrag: function(e) { + this.content.disableAnimation(); + this._isDragging = true; + this._startX = 0; + this._offsetX = 0; + this._lastX = 0; + }, + _handleDrag: function(e) { + if(!this._isDragging) { + this._initDrag(e); + + this._startX = e.gesture.touches[0].pageX; + this._lastX = this._startX; + + this._offsetX = this.getOpenAmount(); + } + //console.log('Dragging page', this._startX, this._lastX, this._offsetX, e); + var newX = this._offsetX + (this._lastX - this._startX); + + this.openAmount(newX); + + this._lastX = e.gesture.touches[0].pageX; + } +}; + +})(ionic = window.ionic || {}); +;(function(ionic) { + +ionic.controllers = ionic.controllers || {}; + +ionic.controllers.TabBarController = function(options) { + this.tabBar = options.tabBar; + + this._bindEvents(); + + this.controllers = []; + + var controllers = options.controllers || []; + + for(var i = 0; i < controllers.length; i++) { + this.addController(controllers[i]); + } + + // Bind or set our tabWillChange callback + this.controllerWillChange = options.controllerWillChange || function(controller) {}; + this.controllerChanged = options.controllerChanged || function(controller) {}; + + this.setSelectedController(0); +}; + +ionic.controllers.TabBarController.prototype = { + // Start listening for events on our tab bar + _bindEvents: function() { + var _this = this; + + this.tabBar.tryTabSelect = function(index) { + _this.setSelectedController(index); + }; + }, + + + selectController: function(index) { + var shouldChange = true; + + // Check if we should switch to this tab. This lets the app + // cancel tab switches if the context isn't right, for example. + if(this.controllerWillChange) { + if(this.controllerWillChange(this.controllers[index], index) === false) { + shouldChange = false; + } + } + + if(shouldChange) { + this.setSelectedController(index); + this.controllerChanged && this.controllerChanged(this.selectedController, this.selectedIndex); + } + }, + + // Force the selection of a controller at the given index + setSelectedController: function(index) { + if(index >= this.controllers.length) { + return; + } + this.selectedController = this.controllers[index]; + this.selectedIndex = index; + + this._showController(index); + this.tabBar.setSelectedItem(index); + }, + + _showController: function(index) { + var c; + + for(var i = 0, j = this.controllers.length; i < j; i ++) { + c = this.controllers[i]; + //c.detach && c.detach(); + c.isVisible = false; + c.visibilityChanged && c.visibilityChanged(); + } + + c = this.controllers[index]; + //c.attach && c.attach(); + c.isVisible = true; + c.visibilityChanged && c.visibilityChanged(); + }, + + _clearSelected: function() { + this.selectedController = null; + this.selectedIndex = -1; + }, + + // Return the tab at the given index + getController: function(index) { + return this.controllers[index]; + }, + + // Return the current tab list + getControllers: function() { + return this.controllers; + }, + + // Get the currently selected tab + getSelectedController: function() { + return this.selectedController; + }, + + // Add a tab + addController: function(controller) { + this.controllers.push(controller); + + this.tabBar.addItem({ + title: controller.title, + icon: controller.icon + }); + + // If we don't have a selected controller yet, select the first one. + if(!this.selectedController) { + this.setSelectedController(0); + } + }, + + // Set the tabs and select the first + setControllers: function(controllers) { + this.controllers = controllers; + this._clearSelected(); + this.selectController(0); + }, +} + +})(ionic = window.ionic || {}); diff --git a/scss/_variables.scss b/scss/_variables.scss index 3377967591..6768310d50 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -344,6 +344,11 @@ $menu-inset-border-color: #bbb; $menu-width: 270px; $menu-animation-speed: 200ms; +// Modals +// ------------------------------- + +$modal-bg-color: #fff !default; + // Badges // ------------------------- @@ -408,3 +413,7 @@ $container-desktop: ((940px + $grid-gutter-width)) !default; // Large screen / wide desktop $container-lg-desktop: ((1140px + $grid-gutter-width)) !default; + +// Z-Indexes + +$zindex-modal: 10; diff --git a/scss/ionic.scss b/scss/ionic.scss index d5305a103a..cc94bbd5f1 100644 --- a/scss/ionic.scss +++ b/scss/ionic.scss @@ -18,6 +18,7 @@ "ionic/bar", "ionic/tabs", "ionic/menu", + "ionic/modal", "ionic/listview", // Forms diff --git a/scss/ionic/_animations.scss b/scss/ionic/_animations.scss index 4ec1ff5626..2b55fa52cc 100644 --- a/scss/ionic/_animations.scss +++ b/scss/ionic/_animations.scss @@ -1,3 +1,6 @@ +$bezier-function: cubic-bezier(.1, .5, .1, 1); + +/* .slide-in-up.enter { -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); @@ -5,3 +8,20 @@ -webkit-transition: -webkit-transform .25s ease-in-out, opacity 1ms .25s; transition: transform .25s ease-in-out, opacity 1ms .25s; } +*/ + +.slide-in-up { + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + + -webkit-transition: -webkit-transform .25s, opacity 1ms .25s; + transition: transform .25s, opacity 1ms .25s; + + -webkit-transition-timing-function: $bezier-function; +} + +.slide-in-up.active { + -webkit-transition: -webkit-transform .25s; + transition: transform .25s; + -webkit-transition-timing-function: $bezier-function; +} diff --git a/scss/ionic/_modal.scss b/scss/ionic/_modal.scss new file mode 100644 index 0000000000..236b24f939 --- /dev/null +++ b/scss/ionic/_modal.scss @@ -0,0 +1,35 @@ +/** + * Modals are independent windows that slide in from off-screen. + */ + +.modal { + position: fixed; + + z-index: $zindex-modal; + + top: 0; + + width: 100%; + min-height: 100%; + + overflow: hidden; + + background-color: $modal-bg-color; + + // Start it hidden + opacity: 0; + + // Start it down low + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + + // Active modal + &.active { + opacity: 1; + height: 100%; + // Put it up + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + diff --git a/test/modals.html b/test/modals.html index c323316801..eb2243371d 100644 --- a/test/modals.html +++ b/test/modals.html @@ -17,11 +17,38 @@
- todo + Open Modal

Homepage

+ + +