/*! * Copyright 2014 Drifty Co. * http://drifty.com/ * * Ionic, v0.9.20-alpha * A powerful HTML5 mobile app framework. * http://ionicframework.com/ * * By @maxlynch, @helloimben, @adamdbradley <3 * * Licensed under the MIT license. Please see LICENSE for more information. * */; /** * Create a wrapping module to ease having to include too many * modules. */ angular.module('ionic.service', [ 'ionic.service.platform', 'ionic.service.actionSheet', 'ionic.service.gesture', 'ionic.service.loading', 'ionic.service.modal', 'ionic.service.popup', 'ionic.service.templateLoad', 'ionic.service.view' ]); // UI specific services and delegates angular.module('ionic.ui.service', [ 'ionic.ui.service.scrollDelegate', 'ionic.ui.service.slideBoxDelegate', ]); angular.module('ionic.ui', [ 'ionic.ui.content', 'ionic.ui.scroll', 'ionic.ui.tabs', 'ionic.ui.viewState', 'ionic.ui.header', 'ionic.ui.sideMenu', 'ionic.ui.slideBox', 'ionic.ui.list', 'ionic.ui.checkbox', 'ionic.ui.toggle', 'ionic.ui.radio' ]); angular.module('ionic', [ 'ionic.service', 'ionic.ui.service', 'ionic.ui', // Angular deps 'ngAnimate', 'ngTouch', 'ngSanitize', 'ui.router' ]); ; (function() { 'use strict'; angular.module('ionic.ui.service.scrollDelegate', []) .factory('$ionicScrollDelegate', ['$rootScope', '$timeout', function($rootScope, $timeout) { return { /** * Trigger a scroll-to-top event on child scrollers. */ scrollTop: function(animate) { $rootScope.$broadcast('scroll.scrollTop', animate); }, tapScrollToTop: function(element) { var _this = this; ionic.on('tap', function(e) { var el = element[0]; var bounds = el.getBoundingClientRect(); if(ionic.DomUtil.rectContains(e.gesture.touches[0].pageX, e.gesture.touches[0].pageY, bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + 20)) { _this.scrollTop(); } }, element[0]); }, /** * Register a scope for scroll event handling. * $scope {Scope} the scope to register and listen for events */ register: function($scope, $element) { $element.bind('scroll', function(e) { $scope.onScroll({ event: e, scrollTop: e.detail ? e.detail.scrollTop : e.originalEvent ? e.originalEvent.detail.scrollTop : 0, scrollLeft: e.detail ? e.detail.scrollLeft: e.originalEvent ? e.originalEvent.detail.scrollLeft : 0 }); }); $scope.$parent.$on('scroll.resize', function(e) { // Run the resize after this digest $timeout(function() { $scope.$parent.scrollView && $scope.$parent.scrollView.resize(); }); }); // Called to stop refreshing on the scroll view $scope.$parent.$on('scroll.refreshComplete', function(e) { $scope.$parent.scrollView && $scope.$parent.scrollView.finishPullToRefresh(); }); /** * Called to scroll to the top of the content * * @param animate {boolean} whether to animate or just snap */ $scope.$parent.$on('scroll.scrollTop', function(e, animate) { $scope.$parent.scrollView && $scope.$parent.scrollView.scrollTo(0, 0, animate === false ? false : true); }); } }; }]); })(ionic); ; (function() { 'use strict'; angular.module('ionic.ui.service.slideBoxDelegate', []) .factory('$ionicSlideBoxDelegate', ['$rootScope', '$timeout', function($rootScope, $timeout) { return { /** * Trigger a slidebox to update and resize itself */ update: function(animate) { $rootScope.$broadcast('slideBox.update'); }, register: function($scope, $element) { $scope.$parent.$on('slideBox.update', function(e) { if(e.defaultPrevented) { return; } $timeout(function() { $scope.$parent.slideBox.setup(); }); e.preventDefault(); }); } }; }]); })(ionic); ; angular.module('ionic.service.actionSheet', ['ionic.service.templateLoad', 'ionic.service.platform', 'ionic.ui.actionSheet', 'ngAnimate']) .factory('$ionicActionSheet', ['$rootScope', '$document', '$compile', '$animate', '$timeout', '$ionicTemplateLoader', '$ionicPlatform', function($rootScope, $document, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform) { return { /** * Load an action sheet with the given template string. * * A new isolated scope will be created for the * action sheet and the new element will be appended into the body. * * @param {object} opts the options for this ActionSheet (see docs) */ show: function(opts) { var scope = $rootScope.$new(true); angular.extend(scope, opts); // Compile the template var element = $compile('')(scope); // Grab the sheet element for animation var sheetEl = angular.element(element[0].querySelector('.action-sheet')); var hideSheet = function(didCancel) { $animate.leave(sheetEl, function() { if(didCancel) { opts.cancel(); } }); $animate.removeClass(element, 'active', function() { scope.$destroy(); }); }; var onHardwareBackButton = function() { hideSheet(); }; scope.$on('$destroy', function() { $ionicPlatform.offHardwareBackButton(onHardwareBackButton); }); // Support Android back button to close $ionicPlatform.onHardwareBackButton(onHardwareBackButton); scope.cancel = function() { hideSheet(true); }; scope.buttonClicked = function(index) { // Check if the button click event returned true, which means // we can close the action sheet if((opts.buttonClicked && opts.buttonClicked(index)) === true) { hideSheet(false); } }; scope.destructiveButtonClicked = function() { // Check if the destructive button click event returned true, which means // we can close the action sheet if((opts.destructiveButtonClicked && opts.destructiveButtonClicked()) === true) { hideSheet(false); } }; $document[0].body.appendChild(element[0]); var sheet = new ionic.views.ActionSheet({el: element[0] }); scope.sheet = sheet; $animate.addClass(element, 'active'); $animate.enter(sheetEl, element, null, function() { }); return sheet; } }; }]); ; angular.module('ionic.service.gesture', []) .factory('$ionicGesture', [function() { return { on: function(eventType, cb, $element) { return window.ionic.onGesture(eventType, cb, $element[0]); }, off: function(gesture, eventType, cb) { return window.ionic.offGesture(gesture, eventType, cb); } }; }]); ; angular.module('ionic.service.loading', ['ionic.ui.loading']) .factory('$ionicLoading', ['$rootScope', '$document', '$compile', function($rootScope, $document, $compile) { return { /** * Load an action sheet with the given template string. * * A new isolated scope will be created for the * action sheet and the new element will be appended into the body. * * @param {object} opts the options for this ActionSheet (see docs) */ show: function(opts) { var defaults = { content: '', animation: 'fade-in', showBackdrop: true, maxWidth: 200, showDelay: 2000 }; opts = angular.extend(defaults, opts); var scope = $rootScope.$new(true); angular.extend(scope, opts); // Make sure there is only one loading element on the page at one point in time var existing = angular.element($document[0].querySelector('.loading-backdrop')); if(existing.length) { scope = existing.scope(); if(scope.loading) { scope.loading.show(); return scope.loading; } } // Compile the template var element = $compile('' + opts.content + '')(scope); $document[0].body.appendChild(element[0]); var loading = new ionic.views.Loading({ el: element[0], maxWidth: opts.maxWidth, showDelay: opts.showDelay }); loading.show(); scope.loading = loading; return loading; } }; }]); ; angular.module('ionic.service.modal', ['ionic.service.templateLoad', 'ionic.service.platform', 'ngAnimate']) .factory('$ionicModal', ['$rootScope', '$document', '$compile', '$animate', '$q', '$ionicPlatform', '$ionicTemplateLoader', function($rootScope, $document, $compile, $animate, $q, $ionicPlatform, $ionicTemplateLoader) { var ModalView = ionic.views.Modal.inherit({ initialize: function(opts) { ionic.views.Modal.prototype.initialize.call(this, opts); this.animation = opts.animation || 'slide-in-up'; }, // Show the modal show: function() { var _this = this; var element = angular.element(this.el); if(!element.parent().length) { element.addClass(this.animation); $animate.enter(element, angular.element($document[0].body), null, function() { }); ionic.views.Modal.prototype.show.call(_this); } else { $animate.addClass(element, this.animation, function() { }); } var onHardwareBackButton = function() { _this.hide(); }; _this.scope.$on('$destroy', function() { $ionicPlatform.offHardwareBackButton(onHardwareBackButton); }); // Support Android back button to close $ionicPlatform.onHardwareBackButton(onHardwareBackButton); }, // Hide the modal hide: function() { var element = angular.element(this.el); $animate.removeClass(element, this.animation); ionic.views.Modal.prototype.hide.call(this); }, // Remove and destroy the modal scope remove: function() { var self = this, element = angular.element(this.el); $animate.leave(angular.element(this.el), function() { self.scope.$destroy(); }); } }); var createModal = function(templateString, options) { // Create a new scope for the modal var scope = options.scope && options.scope.$new() || $rootScope.$new(true); // Compile the template var element = $compile(templateString)(scope); options.el = element[0]; var modal = new ModalView(options); modal.scope = scope; // If this wasn't a defined scope, we can assign 'modal' to the isolated scope // we created if(!options.scope) { scope.modal = modal; } return modal; }; return { /** * Load a modal with the given template string. * * A new isolated scope will be created for the * modal and the new element will be appended into the body. */ fromTemplate: function(templateString, options) { var modal = createModal(templateString, options || {}); return modal; }, fromTemplateUrl: function(url, cb, options) { return $ionicTemplateLoader.load(url).then(function(templateString) { var modal = createModal(templateString, options || {}); cb ? cb(modal) : null; return modal; }); }, }; }]); ; (function() { 'use strict'; angular.module('ionic.service.platform', []) /** * The platformProvider makes it easy to set and detect which platform * the app is currently running on. It has some auto detection built in * for PhoneGap and Cordova. This provider also takes care of * initializing some defaults that depend on the platform, such as the * height of header bars on iOS 7. */ .provider('$ionicPlatform', function() { var platform = 'web'; var isPlatformReady = false; if(window.cordova || window.PhoneGap || window.phonegap) { platform = 'cordova'; } var isReady = function() { if(platform == 'cordova') { return window.device || window.Cordova; } return true; }; // We need to do some stuff as soon as we know the platform, // like adjust header margins for iOS 7, etc. setTimeout(function afterReadyWait() { if(isReady() && ionic.Platform.detect()) { return; } else { setTimeout(afterReadyWait, 50); } }, 10); return { setPlatform: function(p) { platform = p; }, $get: ['$q', '$timeout', function($q, $timeout) { return { /** * Some platforms have hardware back buttons, so this is one way to bind to it. * * @param {function} cb the callback to trigger when this event occurs */ onHardwareBackButton: function(cb) { this.ready(function() { document.addEventListener('backbutton', cb, false); }); }, /** * Remove an event listener for the backbutton. * * @param {function} fn the listener function that was originally bound. */ offHardwareBackButton: function(fn) { this.ready(function() { document.removeEventListener('backbutton', fn); }); }, is: function(type) { return ionic.Platform.is(type); }, /** * Trigger a callback once the device is ready, or immediately if the device is already * ready. */ ready: function(cb) { var self = this; var q = $q.defer(); $timeout(function readyWait() { if(isReady()) { isPlatformReady = true; q.resolve(); cb(); } else { $timeout(readyWait, 50); } }, 50); return q.promise; } }; }] }; }); })(ionic); ; angular.module('ionic.service.popup', ['ionic.service.templateLoad']) .factory('$ionicPopup', ['$rootScope', '$document', '$compile', 'TemplateLoader', function($rootScope, $document, $compile, TemplateLoader) { var getPopup = function() { // Make sure there is only one loading element on the page at one point in time var existing = angular.element($document[0].querySelector('.popup')); if(existing.length) { var scope = existing.scope(); if(scope.popup) { return scope; } } }; return { alert: function(message, $scope) { // If there is an existing popup, just show that one var existing = getPopup(); if(existing) { return existing.popup.alert(message); } var defaults = { title: message, animation: 'fade-in', }; opts = angular.extend(defaults, opts); var scope = $scope && $scope.$new() || $rootScope.$new(true); angular.extend(scope, opts); // Compile the template var element = $compile('' + opts.content + '')(scope); $document[0].body.appendChild(element[0]); var popup = new ionic.views.Popup({el: element[0] }); popup.alert(message); scope.popup = popup; return popup; }, confirm: function(cb) { }, prompt: function(cb) { }, show: function(data) { // data.title // data.template // data.buttons } }; }]); ; angular.module('ionic.service.templateLoad', []) .factory('$ionicTemplateLoader', ['$q', '$http', '$templateCache', function($q, $http, $templateCache) { return { load: function(url) { var deferred = $q.defer(); $http({ method: 'GET', url: url, cache: $templateCache }).success(function(html) { deferred.resolve(html && html.trim()); }).error(function(err) { deferred.reject(err); }); return deferred.promise; } }; }]); ; angular.module('ionic.service.view', ['ui.router']) .run( ['$rootScope', '$state', '$location', '$document', function( $rootScope, $state, $location, $document) { // init the variables that keep track of the view history $rootScope.$viewHistory = { histories: { root: { historyId: 'root', parentHistoryId: null, stack: [], cursor: -1 } }, backView: null, forwardView: null, currentView: null }; $rootScope.$on('viewState.changeHistory', function(e, data) { if(!data) return; var hist = (data.historyId ? $rootScope.$viewHistory.histories[ data.historyId ] : null ); if(hist && hist.cursor > -1 && hist.cursor < hist.stack.length) { // the history they're going to already exists // go to it's last view in its stack var view = hist.stack[ hist.cursor ]; return view.go(data); } // this history does not have a URL, but it does have a uiSref // figure out its URL from the uiSref if(!data.url && data.uiSref) { data.url = $state.href(data.uiSref); } if(data.url) { // don't let it start with a #, messes with $location.url() if(data.url.indexOf('#') === 0) { data.url = data.url.replace('#', ''); } if(data.url !== $location.url()) { // we've got a good URL, ready GO! $location.url(data.url); } } }); // Set the document title when a new view is shown $rootScope.$on('viewState.viewEnter', function(e, data) { if(data && data.title) { $document[0].title = data.title; } }); }]) .factory('$ionicViewService', ['$rootScope', '$state', '$location', '$window', '$injector', function( $rootScope, $state, $location, $window, $injector) { var $animate = $injector.has('$animate') ? $injector.get('$animate') : false; var View = function(){}; View.prototype.initialize = function(data) { if(data) { for(var name in data) this[name] = data[name]; return this; } return null; }; View.prototype.go = function(opts) { if(this.stateName) { return $state.go(this.stateName, this.stateParams); } if(this.url && this.url !== $location.url()) { if($rootScope.$viewHistory.backView === this) { return $window.history.go(-1); } else if($rootScope.$viewHistory.forwardView === this) { return $window.history.go(1); } $location.url(this.url); return; } return null; }; View.prototype.destory = function() { if(this.scope) { this.scope.destory && this.scope.destory(); this.scope = null; } }; function createViewId(stateId) { return ('_' + stateId + '_' + Math.round(Math.random() * 99999999)).replace(/\./g, '_'); } return { register: function(containerScope) { var viewHistory = $rootScope.$viewHistory, currentStateId = this.getCurrentStateId(), hist = this._getHistory(containerScope), currentView = viewHistory.currentView, backView = viewHistory.backView, forwardView = viewHistory.forwardView, rsp = { viewId: null, navAction: null, navDirection: null, historyId: hist.historyId }; if(currentView && currentView.stateId === currentStateId && currentView.historyId === hist.historyId) { // do nothing if its the same stateId in the same history rsp.navAction = 'noChange'; return rsp; } if(backView && backView.stateId === currentStateId) { // they went back one, set the old current view as a forward view rsp.viewId = backView.viewId; rsp.navAction = 'moveBack'; if(backView.historyId === currentView.historyId) { // went back in the same history rsp.navDirection = 'back'; } } else if(forwardView && forwardView.stateId === currentStateId) { // they went to the forward one, set the forward view to no longer a forward view rsp.viewId = forwardView.viewId; rsp.navAction = 'moveForward'; if(forwardView.historyId === currentView.historyId) { rsp.navDirection = 'forward'; } var parentHistory = this._getParentHistoryObj(containerScope); if(forwardView.historyId && parentHistory.scope) { // if a history has already been created by the forward view then make sure it stays the same parentHistory.scope.$historyId = forwardView.historyId; rsp.historyId = forwardView.historyId; } } else if(currentView && currentView.historyId !== hist.historyId && hist.cursor > -1 && hist.stack.length > 0 && hist.cursor < hist.stack.length && hist.stack[hist.cursor].stateId === currentStateId) { // they just changed to a different history and the history already has views in it rsp.viewId = hist.stack[hist.cursor].viewId; rsp.navAction = 'moveBack'; } else { // set a new unique viewId rsp.viewId = createViewId(currentStateId); if(currentView) { // set the forward view if there is a current view (ie: if its not the first view) currentView.forwardViewId = rsp.viewId; // its only moving forward if its in the same history if(hist.historyId === currentView.historyId) { rsp.navDirection = 'forward'; } rsp.navAction = 'newView'; // check if there is a new forward view if(forwardView && currentView.stateId !== forwardView.stateId) { // they navigated to a new view but the stack already has a forward view // since its a new view remove any forwards that existed var forwardsHistory = this._getView(forwardView.historyId); if(forwardsHistory) { // the forward has a history for(var x=forwardsHistory.stack.length - 1; x >= forwardView.index; x--) { // starting from the end destory all forwards in this history from this point forwardsHistory.stack[x].destory(); forwardsHistory.stack.splice(x); } } } } else { // there's no current view, so this must be the initial view rsp.navAction = 'initialView'; } // add the new view to the stack viewHistory.histories[rsp.viewId] = this.createView({ viewId: rsp.viewId, index: hist.stack.length, historyId: hist.historyId, backViewId: (currentView && currentView.viewId ? currentView.viewId : null), forwardViewId: null, stateId: currentStateId, stateName: this.getCurrentStateName(), stateParams: this.getCurrentStateParams(), url: $location.url() }); // add the new view to this history's stack hist.stack.push(viewHistory.histories[rsp.viewId]); } this.setNavViews(rsp.viewId); hist.cursor = viewHistory.currentView.index; return rsp; }, setNavViews: function(viewId) { var viewHistory = $rootScope.$viewHistory; viewHistory.currentView = this._getView(viewId); viewHistory.backView = this._getBackView(viewHistory.currentView); viewHistory.forwardView = this._getForwardView(viewHistory.currentView); $rootScope.$broadcast('$viewHistory.historyChange', { showBack: (viewHistory.backView && viewHistory.backView.historyId === viewHistory.currentView.historyId) }); }, registerHistory: function(scope) { scope.$historyId = 'h' + Math.round(Math.random() * 99999999999); }, createView: function(data) { var newView = new View(); return newView.initialize(data); }, getCurrentView: function() { return $rootScope.$viewHistory.currentView; }, getBackView: function() { return $rootScope.$viewHistory.backView; }, getForwardView: function() { return $rootScope.$viewHistory.forwardView; }, getNavDirection: function() { return $rootScope.$viewHistory.navDirection; }, getCurrentStateName: function() { return ($state && $state.current ? $state.current.name : null); }, isCurrentStateNavView: function(navView) { return ($state && $state.current && $state.current.views && $state.current.views[navView] ? true : false); }, getCurrentStateParams: function() { var rtn; if ($state && $state.params) { for(var key in $state.params) { if($state.params.hasOwnProperty(key)) { rtn = rtn || {}; rtn[key] = $state.params[key]; } } } return rtn; }, getCurrentStateId: function() { var id; if($state && $state.current && $state.current.name) { id = $state.current.name; if($state.params) { for(var key in $state.params) { if($state.params.hasOwnProperty(key) && $state.params[key]) { id += "_" + key + "=" + $state.params[key]; } } } return id; } // if something goes wrong make sure its got a unique stateId return 'r' + Math.round(Math.random() * 9999999); }, _getView: function(viewId) { return (viewId ? $rootScope.$viewHistory.histories[ viewId ] : null ); }, _getBackView: function(view) { return (view ? this._getView(view.backViewId) : null ); }, _getForwardView: function(view) { return (view ? this._getView(view.forwardViewId) : null ); }, _getHistory: function(scope) { var histObj = this._getParentHistoryObj(scope); if( !$rootScope.$viewHistory.histories[ histObj.historyId ] ) { // this history object exists in parent scope, but doesn't // exist in the history data yet $rootScope.$viewHistory.histories[ histObj.historyId ] = { historyId: histObj.historyId, parentHistoryId: this._getParentHistoryObj(histObj.scope.$parent).historyId, stack: [], cursor: -1 }; } return $rootScope.$viewHistory.histories[ histObj.historyId ]; }, _getParentHistoryObj: function(scope) { var parentScope = scope; while(parentScope) { if(parentScope.hasOwnProperty('$historyId')) { // this parent scope has a historyId return { historyId: parentScope.$historyId, scope: parentScope }; } // nothing found keep climbing up parentScope = parentScope.$parent; } // no history for for the parent, use the root return { historyId: 'root', scope: $rootScope }; }, transition: function(opts) { if(!opts || !opts.enteringElement) return; if (opts.leavingScope) { opts.leavingScope.$destroy(); opts.leavingScope = null; } // use the directive's animation attribute first // if it doesn't exist, then use the given animation var animationClass = opts.animation || getAnimationClass(); if($animate && animationClass && opts.doAnimation !== false && opts.navDirection) { // set the animation we're gonna use this.setAnimationClass(opts.parentElement, animationClass, opts.navDirection); // start the animations if(opts.leavingElement) { $animate.leave(opts.leavingElement); } $animate.enter(opts.enteringElement, opts.parentElement); } else { // no animation, just plain ol' add/remove DOM elements if(opts.leavingElement) { opts.leavingElement.remove(); } opts.parentElement.append(opts.enteringElement); } function getAnimationClass(){ // go up the ancestors looking for an animation value var climbScope = opts.enteringScope; while(climbScope) { if(climbScope.animation) { return climbScope.animation; } climbScope = climbScope.$parent; } } }, setAnimationClass: function(element, animationClass, navDirection) { // add the animation we're gonna use element[0].classList.add(animationClass); if(navDirection === 'back') { // animate backward element[0].classList.add('reverse'); } else { // defaults to animate forward // make sure the reverse class isn't already added element[0].classList.remove('reverse'); } }, clearHistory: function() { var historyId, x, view, histories = $rootScope.$viewHistory.histories, currentView = $rootScope.$viewHistory.currentView; for(historyId in histories) { if(histories[historyId].stack) { histories[historyId].stack = []; histories[historyId].cursor = -1; } if(currentView.historyId === historyId) { currentView.backViewId = null; currentView.forwardViewId = null; histories[historyId].stack.push(currentView); } else if(histories[historyId].destroy) { histories[historyId].destroy(); } } this.setNavViews(currentView.viewId); } }; }]); ; (function() { 'use strict'; angular.module('ionic.ui.actionSheet', []) .directive('actionSheet', ['$document', function($document) { return { restrict: 'E', scope: true, replace: true, link: function($scope, $element){ var keyUp = function(e) { if(e.which == 27) { $scope.cancel(); $scope.$apply(); } }; var backdropClick = function(e) { if(e.target == $element[0]) { $scope.cancel(); $scope.$apply(); } }; $scope.$on('$destroy', function() { $element.remove(); $document.unbind('keyup', keyUp); }); $document.bind('keyup', keyUp); $element.bind('click', backdropClick); }, template: '
' + '
' + '
' + '
{{titleText}}
' + '' + '
' + '
' + '' + '
' + '
' + '' + '
' + '
' + '
' }; }]); })(); ; (function(ionic) { 'use strict'; angular.module('ionic.ui.header', ['ngAnimate']) .directive('barHeader', ['$ionicScrollDelegate', function($ionicScrollDelegate) { return { restrict: 'C', link: function($scope, $element, $attr) { // We want to scroll to top when the top of this element is clicked $ionicScrollDelegate.tapScrollToTop($element); } }; }]) .directive('headerBar', ['$ionicScrollDelegate', function($ionicScrollDelegate) { return { restrict: 'E', replace: true, transclude: true, template: '
\
\ \
\

\
\ \
\
', scope: { leftButtons: '=', rightButtons: '=', title: '=', type: '@', alignTitle: '@' }, link: function($scope, $element, $attr) { var hb = new ionic.views.HeaderBar({ el: $element[0], alignTitle: $scope.alignTitle || 'center' }); $element.addClass($scope.type); $scope.headerBarView = hb; $scope.$watch('leftButtons', function(val) { // Resize the title since the buttons have changed hb.align(); }); $scope.$watch('rightButtons', function(val) { console.log('Right buttons changed'); // Resize the title since the buttons have changed hb.align(); }); $scope.$watch('title', function(val) { // Resize the title since the title has changed hb.align(); }); } }; }]) .directive('footerBar', function() { return { restrict: 'E', replace: true, transclude: true, template: '', scope: { type: '@', }, link: function($scope, $element, $attr) { $element.addClass($scope.type); } }; }); })(ionic); ; (function() { 'use strict'; angular.module('ionic.ui.checkbox', []) .directive('checkbox', function() { return { restrict: 'E', replace: true, require: '?ngModel', scope: {}, transclude: true, template: '
  • \ \
    \
    \
  • ', link: function($scope, $element, $attr, ngModel) { var checkbox; if(!ngModel) { return; } checkbox = angular.element($element[0].querySelector('input[type="checkbox"]')); if(!checkbox.length) { return; } checkbox.bind('change', function(e) { ngModel.$setViewValue(checkbox[0].checked); $scope.$apply(function() { e.alreadyHandled = true; }); }); if(ngModel) { ngModel.$render = function() { checkbox[0].checked = ngModel.$viewValue; }; } } }; }); })(); ; (function() { 'use strict'; angular.module('ionic.ui.content', ['ionic.ui.service']) /** * Panel is a simple 100% width and height, fixed panel. It's meant for content to be * added to it, or animated around. */ .directive('pane', function() { return { restrict: 'E', link: function(scope, element, attr) { element.addClass('pane'); } }; }) // The content directive is a core scrollable content area // that is part of many View hierarchies .directive('content', ['$parse', '$timeout', '$ionicPlatform', '$ionicScrollDelegate', function($parse, $timeout, $ionicPlatform, $ionicScrollDelegate) { return { restrict: 'E', replace: true, template: '
    ', transclude: true, scope: { onRefresh: '&', onRefreshOpening: '&', onScroll: '&', onScrollComplete: '&', refreshComplete: '=', onInfiniteScroll: '=', infiniteScrollDistance: '@', hasBouncing: '@', scroll: '@', padding: '@', hasScrollX: '@', hasScrollY: '@', scrollbarX: '@', scrollbarY: '@', scrollEventInterval: '@' }, compile: function(element, attr, transclude) { if(attr.hasHeader == "true") { element.addClass('has-header'); } if(attr.hasSubheader == "true") { element.addClass('has-subheader'); } if(attr.hasFooter == "true") { element.addClass('has-footer'); } if(attr.hasTabs == "true") { element.addClass('has-tabs'); } return function link($scope, $element, $attr) { var clone, sc, sv, c = angular.element($element.children()[0]); // if padding attribute is true, then add padding if it wasn't added to the .scroll if($scope.$eval($scope.padding) === true) { c.addClass('padding'); } if($scope.scroll === "false") { // No scrolling return; } // If they want plain overflow scrolling, add that as a class if(attr.overflowScroll === "true") { $element.addClass('overflow-scroll'); } else { var refresher = $element[0].querySelector('.scroll-refresher'); var refresherHeight = refresher && refresher.clientHeight || 0; if(attr.refreshComplete) { $scope.refreshComplete = function() { if($scope.scrollView) { refresher && refresher.classList.remove('active'); $scope.scrollView.finishPullToRefresh(); $scope.$parent.$broadcast('scroll.onRefreshComplete'); } }; } // Otherwise, supercharge this baby! $timeout(function() { var hasBouncing = $scope.$eval($scope.hasBouncing); var enableBouncing = !$ionicPlatform.is('Android') && hasBouncing !== false; // No bouncing by default for Android users, lest they take up pitchforks // to our bouncing goodness sv = new ionic.views.Scroll({ el: $element[0], bouncing: enableBouncing, scrollbarX: $scope.$eval($scope.scrollbarX) !== false, scrollbarY: $scope.$eval($scope.scrollbarY) !== false, scrollingX: $scope.$eval($scope.hasScrollX) === true, scrollingY: $scope.$eval($scope.hasScrollY) !== false, scrollEventInterval: parseInt($scope.scrollEventInterval, 10) || 20, scrollingComplete: function() { $scope.onScrollComplete({ scrollTop: this.__scrollTop, scrollLeft: this.__scrollLeft }); } }); // Activate pull-to-refresh if(refresher) { sv.activatePullToRefresh(50, function() { refresher.classList.add('active'); }, function() { refresher.classList.remove('refreshing'); refresher.classList.remove('active'); }, function() { refresher.classList.add('refreshing'); $scope.onRefresh(); $scope.$parent.$broadcast('scroll.onRefresh'); }); } // Register for scroll delegate event handling $ionicScrollDelegate.register($scope, $element); // Let child scopes access this $scope.$parent.scrollView = sv; }); // Check if this supports infinite scrolling and listen for scroll events // to trigger the infinite scrolling var infiniteScroll = $element.find('infinite-scroll'); var infiniteStarted = false; if(infiniteScroll) { // Parse infinite scroll distance var distance = attr.infiniteScrollDistance || '1%'; var maxScroll; if(distance.indexOf('%')) { // It's a multiplier maxScroll = function() { return sv.getScrollMax().top * ( 1 - parseInt(distance, 10) / 100 ); }; } else { // It's a pixel value maxScroll = function() { return sv.getScrollMax().top - parseInt(distance, 10); }; } $element.bind('scroll', function(e) { if( sv && !infiniteStarted && (sv.getValues().top > maxScroll() ) ) { infiniteStarted = true; infiniteScroll.addClass('active'); var cb = function() { sv.resize(); infiniteStarted = false; infiniteScroll.removeClass('active'); }; $scope.$apply(angular.bind($scope, $scope.onInfiniteScroll, cb)); } }); } } }; } }; }]) .directive('refresher', function() { return { restrict: 'E', replace: true, require: ['^?content', '^?list'], template: '
    ', scope: true }; }) .directive('scrollRefresher', function() { return { restrict: 'E', replace: true, transclude: true, template: '
    ' }; }) .directive('infiniteScroll', function() { return { restrict: 'E', replace: false, template: '
    ' }; }); })(); ; (function() { 'use strict'; angular.module('ionic.ui.list', ['ngAnimate']) .directive('item', ['$timeout', '$parse', function($timeout, $parse) { return { restrict: 'E', require: '?^list', replace: true, transclude: true, scope: { item: '=', itemType: '@', canDelete: '@', canReorder: '@', canSwipe: '@', onDelete: '&', optionButtons: '&', deleteIcon: '@', reorderIcon: '@' }, template: '
    \
    \ \
    \ \
    \ \
    \
    \ \
    \
    ', link: function($scope, $element, $attr, list) { if(!list) return; var $parentScope = list.scope; var $parentAttrs = list.attrs; $attr.$observe('href', function(value) { if(value) $scope.href = value.trim(); }); if(!$scope.itemType) { $scope.itemType = $parentScope.itemType; } // Set this item's class, first from the item directive attr, and then the list attr if item not set $element.addClass($scope.itemType || $parentScope.itemType); $scope.itemClass = $scope.itemType; // Decide if this item can do stuff, and follow a certain priority // depending on where the value comes from if(($attr.canDelete ? $scope.canDelete : $parentScope.canDelete) !== "false") { if($attr.onDelete || $parentAttrs.onDelete) { // only assign this method when we need to // and use its existence to decide if the delete should show or not $scope.deleteClick = function() { if($attr.onDelete) { // this item has an on-delete attribute $scope.onDelete({ item: $scope.item }); } else if($parentAttrs.onDelete) { // run the parent list's onDelete method // if it doesn't exist nothing will happen $parentScope.onDelete({ item: $scope.item }); } }; // Set which icons to use for deleting $scope.deleteIconClass = $scope.deleteIcon || $parentScope.deleteIcon || 'ion-minus-circled'; } } // set the reorder Icon Class only if the item or list set can-reorder="true" if(($attr.canReorder ? $scope.canReorder : $parentScope.canReorder) === "true") { $scope.reorderIconClass = $scope.reorderIcon || $parentScope.reorderIcon || 'ion-navicon'; } // Set the option buttons which can be revealed by swiping to the left // if canSwipe was set to false don't even bother if(($attr.canSwipe ? $scope.canSwipe : $parentScope.canSwipe) !== "false") { $scope.itemOptionButtons = $scope.optionButtons(); if(typeof $scope.itemOptionButtons === "undefined") { $scope.itemOptionButtons = $parentScope.optionButtons(); } } } }; }]) .directive('list', ['$timeout', function($timeout) { return { restrict: 'E', replace: true, transclude: true, scope: { itemType: '@', canDelete: '@', canReorder: '@', canSwipe: '@', showDelete: '=', showReorder: '=', onDelete: '&', onReorder: '&', optionButtons: '&', deleteIcon: '@', reorderIcon: '@' }, template: '
    ', controller: ['$scope', '$attrs', function($scope, $attrs) { this.scope = $scope; this.attrs = $attrs; }], link: function($scope, $element, $attr) { $scope.listView = new ionic.views.ListView({ el: $element[0], listEl: $element[0].children[0] }); if($attr.animation) { $element[0].classList.add($attr.animation); } var destroyShowReorderWatch = $scope.$watch('showReorder', function(val) { if(val) { $element[0].classList.add('item-options-hide'); } else if(val === false) { // false checking is because it could be undefined // if its undefined then we don't care to do anything $timeout(function(){ $element[0].classList.remove('item-options-hide'); }, 250); } }); $scope.$on('$destroy', function () { destroyShowReorderWatch(); }); } }; }]); })(); ; (function() { 'use strict'; angular.module('ionic.ui.loading', []) .directive('loading', function() { return { restrict: 'E', replace: true, transclude: true, link: function($scope, $element){ $element.addClass($scope.animation || ''); }, template: '
    ' + '
    ' + '
    ' + '
    ' }; }); })(); ; (function(ionic) { 'use strict'; angular.module('ionic.ui.radio', []) // The radio button is a radio powered element with only // one possible selection in a set of options. .directive('radio', function() { return { restrict: 'E', replace: true, require: '?ngModel', scope: { value: '@' }, transclude: true, template: '', link: function($scope, $element, $attr, ngModel) { var radio; if(!ngModel) { return; } radio = $element.children().eq(0); if(!radio.length) { return; } if(ngModel) { radio.bind('click', function(e) { console.log('RADIO CLICK'); $scope.$apply(function() { ngModel.$setViewValue($scope.$eval($attr.ngValue)); }); e.alreadyHandled = true; }); ngModel.$render = function() { var val = $scope.$eval($attr.ngValue); if(val === ngModel.$viewValue) { radio.attr('checked', 'checked'); } else { radio.removeAttr('checked'); } }; } } }; }) // The radio button is a radio powered element with only // one possible selection in a set of options. .directive('radioButtons', function() { return { restrict: 'E', replace: true, require: '?ngModel', scope: { value: '@' }, transclude: true, template: '
    ', controller: ['$scope', '$element', function($scope, $element) { this.select = function(element) { var c, children = $element.children(); for(var i = 0; i < children.length; i++) { c = children[i]; if(c != element[0]) { c.classList.remove('active'); } } }; }], link: function($scope, $element, $attr, ngModel) { var radio; if(ngModel) { //$element.bind('tap', tapHandler); ngModel.$render = function() { var children = $element.children(); for(var i = 0; i < children.length; i++) { children[i].classList.remove('active'); } $scope.$parent.$broadcast('radioButton.select', ngModel.$viewValue); }; } } }; }) .directive('buttonRadio', function() { return { restrict: 'CA', require: ['?^ngModel', '?^radioButtons'], link: function($scope, $element, $attr, ctrls) { var ngModel = ctrls[0]; var radioButtons = ctrls[1]; if(!ngModel || !radioButtons) { return; } var setIt = function() { console.log('SET'); $element.addClass('active'); ngModel.$setViewValue($scope.$eval($attr.ngValue)); radioButtons.select($element); }; var clickHandler = function(e) { console.log('CLICK'); setIt(); }; $scope.$on('radioButton.select', function(e, val) { if(val == $scope.$eval($attr.ngValue)) { $element.addClass('active'); } }); ionic.on('tap', clickHandler, $element[0]); $scope.$on('$destroy', function() { ionic.off('tap', clickHandler); }); } }; }); })(window.ionic); ; (function() { 'use strict'; angular.module('ionic.ui.scroll', []) .directive('scroll', ['$parse', '$timeout', function($parse, $timeout) { return { restrict: 'E', replace: true, template: '
    ', transclude: true, scope: { direction: '@', paging: '@', onRefresh: '&', onScroll: '&', refreshComplete: '=', scroll: '@', scrollbarX: '@', scrollbarY: '@', }, controller: function() {}, compile: function(element, attr, transclude) { return function($scope, $element, $attr) { var clone, sv, sc = document.createElement('div'); // Create the internal scroll div sc.className = 'scroll'; if(attr.padding == "true") { sc.classList.add('padding'); } if($scope.$eval($scope.paging) === true) { sc.classList.add('scroll-paging'); } $element.append(sc); // Pass the parent scope down to the child clone = transclude($scope.$parent); angular.element($element[0].firstElementChild).append(clone); // Get refresher size var refresher = $element[0].querySelector('.scroll-refresher'); var refresherHeight = refresher && refresher.clientHeight || 0; if(!$scope.direction) { $scope.direction = 'y'; } var hasScrollingX = $scope.direction.indexOf('x') >= 0; var hasScrollingY = $scope.direction.indexOf('y') >= 0; $timeout(function() { var options = { el: $element[0], paging: $scope.$eval($scope.paging) === true, scrollbarX: $scope.$eval($scope.scrollbarX) !== false, scrollbarY: $scope.$eval($scope.scrollbarY) !== false, scrollingX: hasScrollingX, scrollingY: hasScrollingY }; if(options.paging) { options.speedMultiplier = 0.8; options.bouncing = false; } sv = new ionic.views.Scroll(options); // Activate pull-to-refresh if(refresher) { sv.activatePullToRefresh(refresherHeight, function() { refresher.classList.add('active'); }, function() { refresher.classList.remove('refreshing'); refresher.classList.remove('active'); }, function() { refresher.classList.add('refreshing'); $scope.onRefresh(); $scope.$parent.$broadcast('scroll.onRefresh'); }); } $element.bind('scroll', function(e) { $scope.onScroll({ event: e, scrollTop: e.detail ? e.detail.scrollTop : e.originalEvent ? e.originalEvent.detail.scrollTop : 0, scrollLeft: e.detail ? e.detail.scrollLeft: e.originalEvent ? e.originalEvent.detail.scrollLeft : 0 }); }); $scope.$parent.$on('scroll.resize', function(e) { // Run the resize after this digest $timeout(function() { sv && sv.resize(); }); }); $scope.$parent.$on('scroll.refreshComplete', function(e) { sv && sv.finishPullToRefresh(); }); // Let child scopes access this $scope.$parent.scrollView = sv; }); }; } }; }]); })(); ; (function() { 'use strict'; /** * @description * The sideMenuCtrl lets you quickly have a draggable side * left and/or right menu, which a center content area. */ angular.module('ionic.ui.sideMenu', ['ionic.service.gesture']) /** * The internal controller for the side menu controller. This * extends our core Ionic side menu controller and exposes * some side menu stuff on the current scope. */ .directive('sideMenus', function() { return { restrict: 'ECA', controller: ['$scope', function($scope) { var _this = this; angular.extend(this, ionic.controllers.SideMenuController.prototype); ionic.controllers.SideMenuController.call(this, { // Our quick implementation of the left side menu left: { width: 275, }, // Our quick implementation of the right side menu right: { width: 275, } }); $scope.sideMenuContentTranslateX = 0; $scope.sideMenuController = this; }], replace: true, transclude: true, template: '
    ' }; }) .directive('sideMenuContent', ['$timeout', '$ionicGesture', function($timeout, $ionicGesture) { return { restrict: 'AC', require: '^sideMenus', scope: true, compile: function(element, attr, transclude) { return function($scope, $element, $attr, sideMenuCtrl) { $element.addClass('menu-content'); $scope.dragContent = $scope.$eval($attr.dragContent) === false ? false : true; var defaultPrevented = false; var isDragging = false; // Listen for taps on the content to close the menu /* ionic.on('tap', function(e) { sideMenuCtrl.close(); }, $element[0]); */ var dragFn = function(e) { if($scope.dragContent) { if(defaultPrevented || e.gesture.srcEvent.defaultPrevented) { return; } isDragging = true; sideMenuCtrl._handleDrag(e); e.gesture.srcEvent.preventDefault(); } }; var dragVertFn = function(e) { if(isDragging) { e.gesture.srcEvent.preventDefault(); } }; //var dragGesture = Gesture.on('drag', dragFn, $element); var dragRightGesture = $ionicGesture.on('dragright', dragFn, $element); var dragLeftGesture = $ionicGesture.on('dragleft', dragFn, $element); var dragUpGesture = $ionicGesture.on('dragup', dragVertFn, $element); var dragDownGesture = $ionicGesture.on('dragdown', dragVertFn, $element); var dragReleaseFn = function(e) { isDragging = false; if(!defaultPrevented) { sideMenuCtrl._endDrag(e); } defaultPrevented = false; }; var releaseGesture = $ionicGesture.on('release', dragReleaseFn, $element); sideMenuCtrl.setContent({ onDrag: function(e) {}, endDrag: function(e) {}, getTranslateX: function() { return $scope.sideMenuContentTranslateX || 0; }, setTranslateX: function(amount) { $element[0].style.webkitTransform = 'translate3d(' + amount + 'px, 0, 0)'; $timeout(function() { $scope.sideMenuContentTranslateX = amount; }); }, enableAnimation: function() { //this.el.classList.add(this.animateClass); $scope.animationEnabled = true; $element[0].classList.add('menu-animated'); }, disableAnimation: function() { //this.el.classList.remove(this.animateClass); $scope.animationEnabled = false; $element[0].classList.remove('menu-animated'); } }); // Cleanup $scope.$on('$destroy', function() { $ionicGesture.off(dragLeftGesture, 'dragleft', dragFn); $ionicGesture.off(dragRightGesture, 'dragright', dragFn); $ionicGesture.off(dragUpGesture, 'dragup', dragFn); $ionicGesture.off(dragDownGesture, 'dragdown', dragFn); $ionicGesture.off(releaseGesture, 'release', dragReleaseFn); }); }; } }; }]) .directive('sideMenu', function() { return { restrict: 'E', require: '^sideMenus', replace: true, transclude: true, scope: true, template: '', compile: function(element, attr, transclude) { return function($scope, $element, $attr, sideMenuCtrl) { $scope.side = $attr.side; if($scope.side == 'left') { sideMenuCtrl.left.isEnabled = true; sideMenuCtrl.left.pushDown = function() { $element[0].style.zIndex = -1; }; sideMenuCtrl.left.bringUp = function() { $element[0].style.zIndex = 0; }; } else if($scope.side == 'right') { sideMenuCtrl.right.isEnabled = true; sideMenuCtrl.right.pushDown = function() { $element[0].style.zIndex = -1; }; sideMenuCtrl.right.bringUp = function() { $element[0].style.zIndex = 0; }; } $element.append(transclude($scope)); }; } }; }); })(); ; (function() { 'use strict'; /** * @description * The slideBoxCtrol lets you quickly create a multi-page * container where each page can be swiped or dragged between */ angular.module('ionic.ui.slideBox', []) /** * The internal controller for the slide box controller. */ .directive('slideBox', ['$timeout', '$compile', '$ionicSlideBoxDelegate', function($timeout, $compile, $ionicSlideBoxDelegate) { return { restrict: 'E', replace: true, transclude: true, scope: { doesContinue: '@', slideInterval: '@', showPager: '@', disableScroll: '@', onSlideChanged: '&', activeSlide: '=?' }, controller: ['$scope', '$element', function($scope, $element) { var _this = this; var continuous = $scope.$eval($scope.doesContinue) === true; var slideInterval = continuous ? $scope.$eval($scope.slideInterval) || 4000 : 0; var slider = new ionic.views.Slider({ el: $element[0], auto: slideInterval, disableScroll: ($scope.$eval($scope.disableScroll) === true) || false, continuous: continuous, slidesChanged: function() { $scope.currentSlide = slider.getPos(); // Try to trigger a digest $timeout(function() {}); }, callback: function(slideIndex) { $scope.currentSlide = slideIndex; $scope.onSlideChanged({index:$scope.currentSlide}); $scope.$parent.$broadcast('slideBox.slideChanged', slideIndex); $scope.activeSlide = slideIndex; // Try to trigger a digest $timeout(function() {}); } }); $scope.$watch('activeSlide', function(nv) { if(angular.isDefined(nv)){ slider.slide(nv); } }); $scope.$on('slideBox.nextSlide', function() { slider.next(); }); $scope.$on('slideBox.prevSlide', function() { slider.prev(); }); $scope.$on('slideBox.setSlide', function(e, index) { slider.slide(index); }); $scope.$parent.slideBox = slider; $ionicSlideBoxDelegate.register($scope, $element); this.getNumSlides = function() { return slider.getNumSlides(); }; $timeout(function() { slider.load(); }); }], template: '
    \
    \
    \
    ', link: function($scope, $element, $attr, slideBoxCtrl) { // If the pager should show, append it to the slide box if($scope.$eval($scope.showPager) !== false) { var childScope = $scope.$new(); var pager = angular.element(''); $element.append(pager); $compile(pager)(childScope); } } }; }]) .directive('slide', function() { return { restrict: 'E', require: '^slideBox', compile: function(element, attr) { element.addClass('slider-slide'); return function($scope, $element, $attr) {}; }, }; }) .directive('pager', function() { return { restrict: 'E', replace: true, require: '^slideBox', template: '
    ', link: function($scope, $element, $attr, slideBox) { var selectPage = function(index) { var children = $element[0].children; var length = children.length; for(var i = 0; i < length; i++) { if(i == index) { children[i].classList.add('active'); } else { children[i].classList.remove('active'); } } }; $scope.numSlides = function() { return new Array(slideBox.getNumSlides()); }; $scope.$watch('currentSlide', function(v) { selectPage(v); }); } }; }); })(); ; angular.module('ionic.ui.tabs', ['ionic.service.view']) /** * @description * * The Tab Controller renders a set of pages that switch based on taps * on a tab bar. Modelled off of UITabBarController. */ .directive('tabs', [function() { return { restrict: 'E', replace: true, scope: true, transclude: true, controller: ['$scope', '$element', function($scope, $element) { var _this = this; $scope.tabCount = 0; $scope.selectedIndex = -1; $scope.$enableViewRegister = false; angular.extend(this, ionic.controllers.TabBarController.prototype); ionic.controllers.TabBarController.call(this, { controllerChanged: function(oldC, oldI, newC, newI) { $scope.controllerChanged && $scope.controllerChanged({ oldController: oldC, oldIndex: oldI, newController: newC, newIndex: newI }); }, tabBar: { tryTabSelect: function() {}, setSelectedItem: function(index) {}, addItem: function(item) {} } }); this.add = function(tabScope) { tabScope.tabIndex = $scope.tabCount; this.addController(tabScope); if(tabScope.tabIndex === 0) { this.select(0); } $scope.tabCount++; }; this.select = function(tabIndex, emitChange) { if(tabIndex !== $scope.selectedIndex) { $scope.selectedIndex = tabIndex; $scope.activeAnimation = $scope.animation; _this.selectController(tabIndex); var viewData = { type: 'tab', typeIndex: tabIndex }; for(var x=0; x', compile: function(element, attr, transclude, tabsCtrl) { return function link($scope, $element, $attr) { var tabs = $element[0].querySelector('.tabs'); $scope.tabsType = $attr.tabsType || 'tabs-positive'; $scope.tabsStyle = $attr.tabsStyle; $scope.animation = $attr.animation; $scope.animateNav = $scope.$eval($attr.animateNav); if($scope.animateNav !== false) { $scope.animateNav = true; } $attr.$observe('tabsStyle', function(val) { if(tabs) { angular.element(tabs).addClass($attr.tabsStyle); } }); $attr.$observe('tabsType', function(val) { if(tabs) { angular.element(tabs).addClass($attr.tabsType); } }); $scope.$watch('activeAnimation', function(value) { $element.addClass($scope.activeAnimation); }); transclude($scope, function(cloned) { $element.prepend(cloned); }); }; } }; }]) // Generic controller directive .directive('tab', ['$ionicViewService', '$rootScope', '$parse', function($ionicViewService, $rootScope, $parse) { return { restrict: 'E', require: '^tabs', scope: true, transclude: 'element', compile: function(element, attr, transclude) { return function link($scope, $element, $attr, tabsCtrl) { var childScope, childElement; $ionicViewService.registerHistory($scope); $scope.title = $attr.title; $scope.icon = $attr.icon; $scope.iconOn = $attr.iconOn; $scope.iconOff = $attr.iconOff; $scope.viewSref = $attr.uiSref; $scope.url = $attr.href; if($scope.url && $scope.url.indexOf('#') === 0) { $scope.url = $scope.url.replace('#', ''); } // Should we hide a back button when this tab is shown $scope.hideBackButton = $scope.$eval($attr.hideBackButton); if($scope.hideBackButton !== true) { $scope.hideBackButton = false; } // Whether we should animate on tab change, also impacts whether we // tell any parent nav controller to animate $scope.animate = $scope.$eval($attr.animate); var leftButtonsGet = $parse($attr.leftButtons); $scope.$watch(leftButtonsGet, function(value) { $scope.leftButtons = value; if($scope.doesUpdateNavRouter) { $scope.$emit('viewState.leftButtonsChanged', $scope.rightButtons); } }); var rightButtonsGet = $parse($attr.rightButtons); $scope.$watch(rightButtonsGet, function(value) { $scope.rightButtons = value; }); tabsCtrl.add($scope); $scope.$watch('isVisible', function(value) { if(childElement) { childElement.remove(); childElement = null; $scope.$broadcast('tab.hidden'); } if(childScope) { childScope.$destroy(); childScope = null; } if(value) { childScope = $scope.$new(); transclude(childScope, function(clone) { clone.addClass('pane'); clone.removeAttr('title'); childElement = clone; $element.parent().append(childElement); $scope.$broadcast('tab.shown'); }); } }); // on link, check if it has a nav-view in it transclude($scope.$new(), function(clone) { var navViewEle = clone[0].getElementsByTagName("nav-view"); $scope.hasNavView = (navViewEle.length > 0); if($scope.hasNavView) { // this tab has a ui-view $scope.navViewName = navViewEle[0].getAttribute('name'); if( $ionicViewService.isCurrentStateNavView( $scope.navViewName ) ) { // this tab's ui-view is the current one, go to it! tabsCtrl.select($scope.tabIndex); } } }); $rootScope.$on('$stateChangeSuccess', function(value){ if( $ionicViewService.isCurrentStateNavView($scope.navViewName) && $scope.tabIndex !== tabsCtrl.selectedIndex) { tabsCtrl.select($scope.tabIndex); } }); }; } }; }]) .directive('tabControllerBar', function() { return { restrict: 'E', require: '^tabs', transclude: true, replace: true, scope: true, template: '
    ' + '' + '
    ', link: function($scope, $element, $attr, tabsCtrl) { $element.addClass($scope.tabsType); $element.addClass($scope.tabsStyle); } }; }) .directive('tabControllerItem', ['$window', function($window) { return { restrict: 'E', replace: true, require: '^tabs', scope: { iconTitle: '@', icon: '@', iconOn: '@', iconOff: '@', active: '=', tabSelected: '@', index: '=' }, link: function(scope, element, attrs, tabsCtrl) { if(attrs.icon) { scope.iconOn = scope.iconOff = attrs.icon; } scope.selectTab = function() { tabsCtrl.select(scope.index, true); }; }, template: '' + '' + '' + ' {{iconTitle}}' + '' }; }]) .directive('tabBar', function() { return { restrict: 'E', replace: true, transclude: true, template: '
    ' }; }); ; (function(ionic) { 'use strict'; angular.module('ionic.ui.toggle', []) // The Toggle directive is a toggle switch that can be tapped to change // its value .directive('toggle', function() { return { restrict: 'E', replace: true, require: '?ngModel', scope: {}, template: '
    ', link: function($scope, $element, $attr, ngModel) { var checkbox, handle; if(!ngModel) { return; } checkbox = $element.children().eq(0); handle = $element.children().eq(1); if(!checkbox.length || !handle.length) { return; } $scope.toggle = new ionic.views.Toggle({ el: $element[0], checkbox: checkbox[0], handle: handle[0] }); $scope.toggleIt = function(e) { $scope.toggle.tap(e); ngModel.$setViewValue(checkbox[0].checked); }; ngModel.$render = function() { $scope.toggle.val(ngModel.$viewValue); }; } }; }); })(window.ionic); ; (function() { 'use strict'; /** * @description * The NavController is a navigation stack View Controller modelled off of * UINavigationController from Cocoa Touch. With the Nav Controller, you can * "push" new "pages" on to the navigation stack, and then pop them off to go * back. The NavController controls a navigation bar with a back button and title * which updates as the pages switch. * * The NavController makes sure to not recycle scopes of old pages * so that a pop will still show the same state that the user left. * * However, once a page is popped, its scope is destroyed and will have to be * recreated then next time it is pushed. * */ angular.module('ionic.ui.viewState', ['ionic.service.view', 'ionic.service.gesture']) /** * Our Nav Bar directive which updates as the controller state changes. */ .directive('navBar', ['$ionicViewService', '$rootScope', '$animate', '$compile', function( $ionicViewService, $rootScope, $animate, $compile) { /** * Perform an animation between one tab bar state and the next. * Right now this just animates the titles. */ var animate = function($scope, $element, oldTitle, data, cb) { var title, nTitle, oTitle, titles = $element[0].querySelectorAll('.title'); var newTitle = data.title; if(!oldTitle || oldTitle === newTitle) { cb(); return; } // Clone the old title and add a new one so we can show two animating in and out // add ng-leave and ng-enter during creation to prevent flickering when they are swapped during animation title = angular.element(titles[0]); oTitle = $compile('

    ')($scope); title.replaceWith(oTitle); nTitle = $compile('

    ')($scope); var insert = $element[0].firstElementChild || null; // Insert the new title $animate.enter(nTitle, $element, insert && angular.element(insert), function() { cb(); }); // Remove the old title $animate.leave(angular.element(oTitle), function() { }); }; return { restrict: 'E', replace: true, scope: { type: '@', backButtonType: '@', backButtonLabel: '@', backButtonIcon: '@', alignTitle: '@' }, template: '', link: function($scope, $element, $attr) { // Create the back button content and show/hide it based on scope settings $scope.enableBackButton = true; $scope.backButtonClass = $attr.backButtonType; if($attr.backButtonIcon) { $scope.backButtonClass += ' icon ' + $attr.backButtonIcon; } $rootScope.$on('viewState.showNavBar', function(e, showNavBar) { if(showNavBar === false) { $element[0].classList.add('invisible'); } else { $element[0].classList.remove('invisible'); } }); // Initialize our header bar view which will handle resizing and aligning our title labels var hb = new ionic.views.HeaderBar({ el: $element[0], alignTitle: $scope.alignTitle || 'center' }); $scope.headerBarView = hb; // Add the type of header bar class to this element $element.addClass($scope.type); var updateHeaderData = function(data) { $scope.oldTitle = $scope.currentTitle; $scope.currentTitle = (data && data.title ? data.title : ''); $scope.leftButtons = data.leftButtons; $scope.rightButtons = data.rightButtons; if(typeof data.hideBackButton !== 'undefined') { $scope.enableBackButton = data.hideBackButton !== true; } if(data.animate !== false && $attr.animation && data.title && data.navDirection) { $element[0].classList.add($attr.animation); if(data.navDirection === 'back') { $element[0].classList.add('reverse'); } else { $element[0].classList.remove('reverse'); } animate($scope, $element, $scope.oldTitle, data, function() { hb.align(); }); } else { hb.align(); } }; $rootScope.$on('viewState.viewEnter', function(e, data) { updateHeaderData(data); }); // If a nav page changes the left or right buttons, update our scope vars $scope.$parent.$on('viewState.leftButtonsChanged', function(e, data) { $scope.leftButtons = data; }); $scope.$parent.$on('viewState.rightButtonsChanged', function(e, data) { $scope.rightButtons = data; }); } }; }]) .directive('view', ['$ionicViewService', '$rootScope', '$animate', function( $ionicViewService, $rootScope, $animate) { return { restrict: 'EA', priority: 1000, scope: { leftButtons: '=', rightButtons: '=', title: '=', icon: '@', iconOn: '@', iconOff: '@', type: '@', alignTitle: '@', hideBackButton: '@', hideNavBar: '@', animation: '@' }, compile: function(tElement, tAttrs, transclude) { tElement.addClass('pane'); tElement[0].removeAttribute('title'); return function link($scope, $element, $attr) { $rootScope.$broadcast('viewState.viewEnter', { title: $scope.title, navDirection: $scope.$navDirection || $scope.$parent.$navDirection }); // Should we hide a back button when this tab is shown $scope.hideBackButton = $scope.$eval($scope.hideBackButton); if($scope.hideBackButton) { $rootScope.$broadcast('viewState.showBackButton', false); } // Should the nav bar be hidden for this view or not? $rootScope.$broadcast('viewState.showNavBar', ($scope.hideNavBar !== 'true') ); // watch for changes in the left buttons var deregLeftButtons = $scope.$watch('leftButtons', function(value) { $scope.$emit('viewState.leftButtonsChanged', $scope.leftButtons); }); var deregRightButtons = $scope.$watch('rightButtons', function(val) { $scope.$emit('viewState.rightButtonsChanged', $scope.rightButtons); }); $scope.$on('$destroy', function(){ // deregister on destroy deregLeftButtons(); deregRightButtons(); }); }; } }; }]) .directive('viewBack', ['$ionicViewService', '$rootScope', function($ionicViewService, $rootScope) { var goBack = function(e) { var backView = $ionicViewService.getBackView(); backView && backView.go(); e.alreadyHandled = true; return false; }; return { restrict: 'AC', compile: function(tElement) { tElement.addClass('hide'); return function link($scope, $element) { $element.bind('tap', goBack); $scope.showButton = function(val) { if(val) { $element[0].classList.remove('hide'); } else { $element[0].classList.add('hide'); } }; $rootScope.$on('$viewHistory.historyChange', function(e, data) { $scope.showButton(data.showBack); }); $rootScope.$on('viewState.showBackButton', function(e, data) { $scope.showButton(data); }); }; } }; }]) .directive('navView', ['$ionicViewService', '$state', '$anchorScroll', '$compile', '$controller', '$animate', function( $ionicViewService, $state, $anchorScroll, $compile, $controller, $animate) { var viewIsUpdating = false; var animation; var directive = { restrict: 'E', terminal: true, priority: 2000, transclude: true, link: function(scope, $element, attr, ctrl, $transclude) { var currentElement, autoScrollExp = attr.autoscroll, onloadExp = attr.onload || '', viewLocals, viewScope, name = attr[directive.name] || attr.name || '', parent = $element.parent().inheritedData('$uiView'); if (name.indexOf('@') < 0) name = name + '@' + (parent ? parent.state.name : ''); var view = { name: name, state: null, animation: null }; $element.data('$uiView', view); var climbElement = $element[0]; while(!animation && climbElement) { animation = climbElement.getAttribute('animation'); climbElement = climbElement.parentElement; } var eventHook = function() { if (viewIsUpdating) return; viewIsUpdating = true; try { update(true); } catch (e) { viewIsUpdating = false; throw e; } viewIsUpdating = false; }; scope.$on('$stateChangeSuccess', eventHook); scope.$on('$viewContentLoading', eventHook); update(false); function update(doAnimation) { var locals = $state.$current && $state.$current.locals[name], template = (locals && locals.$template ? locals.$template : null); if (locals === viewLocals) return; // nothing to do here, go about your business var transitionOptions = { parentElement: $element, doAnimation: doAnimation, leavingScope: viewScope, leavingElement: currentElement, navDirection: null }; if (template) { currentElement = angular.element(template.trim()); var registerData = {}; if(currentElement[0].tagName !== 'TABS') { // the tabs directive shouldn't register in the view history (its tab will) registerData = $ionicViewService.register(scope); transitionOptions.navDirection = registerData.navDirection; } viewLocals = locals; view.state = locals.$$state; var link = $compile(currentElement), current = $state.current; viewScope = current.scope = scope.$new(); viewScope.$navDirection = transitionOptions.navDirection; if (locals.$$controller) { locals.$scope = viewScope; var controller = $controller(locals.$$controller, locals); if (current.controllerAs) { viewScope[current.controllerAs] = controller; } currentElement.data('$ngControllerController', controller); currentElement.children().data('$ngControllerController', controller); } link(viewScope); viewScope.$emit('$viewContentLoaded'); viewScope.$eval(onloadExp); viewScope.animation = animation; transitionOptions.enteringScope = viewScope; transitionOptions.enteringElement = currentElement; } $ionicViewService.transition(transitionOptions); } } }; return directive; }]); })(); ; (function() { 'use strict'; angular.module('ionic.ui.virtRepeat', []) .directive('virtRepeat', function() { return { require: ['?ngModel', '^virtualList'], transclude: 'element', priority: 1000, terminal: true, compile: function(element, attr, transclude) { return function($scope, $element, $attr, ctrls) { var virtualList = ctrls[1]; virtualList.listView.renderViewport = function(high, low, start, end) { }; }; } }; }); })(ionic); ; (function() { 'use strict'; // Turn the expression supplied to the directive: // // a in b // // into `{ value: "a", collection: "b" }` function parseRepeatExpression(expression){ var match = expression.match(/^\s*([\$\w]+)\s+in\s+(\S*)\s*$/); if (! match) { throw new Error("Expected sfVirtualRepeat in form of '_item_ in _collection_' but got '" + expression + "'."); } return { value: match[1], collection: match[2] }; } // Utility to filter out elements by tag name function isTagNameInList(element, list){ var t, tag = element.tagName.toUpperCase(); for( t = 0; t < list.length; t++ ){ if( list[t] === tag ){ return true; } } return false; } // Utility to find the viewport/content elements given the start element: function findViewportAndContent(startElement){ /*jshint eqeqeq:false, curly:false */ var root = $rootElement[0]; var e, n; // Somewhere between the grandparent and the root node for( e = startElement.parent().parent()[0]; e !== root; e = e.parentNode ){ // is an element if( e.nodeType != 1 ) break; // that isn't in the blacklist (tables etc.), if( isTagNameInList(e, DONT_WORK_AS_VIEWPORTS) ) continue; // has a single child element (the content), if( e.childElementCount != 1 ) continue; // which is not in the blacklist if( isTagNameInList(e.firstElementChild, DONT_WORK_AS_CONTENT) ) continue; // and no text. for( n = e.firstChild; n; n = n.nextSibling ){ if( n.nodeType == 3 && /\S/g.test(n.textContent) ){ break; } } if( n === null ){ // That element should work as a viewport. return { viewport: angular.element(e), content: angular.element(e.firstElementChild) }; } } throw new Error("No suitable viewport element"); } // Apply explicit height and overflow styles to the viewport element. // // If the viewport has a max-height (inherited or otherwise), set max-height. // Otherwise, set height from the current computed value or use // window.innerHeight as a fallback // function setViewportCss(viewport){ var viewportCss = {'overflow': 'auto'}, style = window.getComputedStyle ? window.getComputedStyle(viewport[0]) : viewport[0].currentStyle, maxHeight = style && style.getPropertyValue('max-height'), height = style && style.getPropertyValue('height'); if( maxHeight && maxHeight !== '0px' ){ viewportCss.maxHeight = maxHeight; }else if( height && height !== '0px' ){ viewportCss.height = height; }else{ viewportCss.height = window.innerHeight; } viewport.css(viewportCss); } // Apply explicit styles to the content element to prevent pesky padding // or borders messing with our calculations: function setContentCss(content){ var contentCss = { margin: 0, padding: 0, border: 0, 'box-sizing': 'border-box' }; content.css(contentCss); } // TODO: compute outerHeight (padding + border unless box-sizing is border) function computeRowHeight(element){ var style = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle, maxHeight = style && style.getPropertyValue('max-height'), height = style && style.getPropertyValue('height'); if( height && height !== '0px' && height !== 'auto' ){ $log.info('Row height is "%s" from css height', height); }else if( maxHeight && maxHeight !== '0px' && maxHeight !== 'none' ){ height = maxHeight; $log.info('Row height is "%s" from css max-height', height); }else if( element.clientHeight ){ height = element.clientHeight+'px'; $log.info('Row height is "%s" from client height', height); }else{ throw new Error("Unable to compute height of row"); } angular.element(element).css('height', height); return parseInt(height, 10); } angular.module('ionic.ui.virtualRepeat', []) /** * A replacement for ng-repeat that supports virtual lists. * This is not a 1 to 1 replacement for ng-repeat. However, in situations * where you have huge lists, this repeater will work with our virtual * scrolling to only render items that are showing or will be showing * if a scroll is made. */ .directive('virtualRepeat', ['$log', function($log) { return { require: ['?ngModel, ^virtualList'], transclude: 'element', priority: 1000, terminal: true, compile: function(element, attr, transclude) { var ident = parseRepeatExpression(attr.sfVirtualRepeat); return function(scope, iterStartElement, attrs, ctrls, b) { var virtualList = ctrls[1]; var rendered = []; var rowHeight = 0; var sticky = false; var dom = virtualList.element; //var dom = findViewportAndContent(iterStartElement); // The list structure is controlled by a few simple (visible) variables: var state = 'ngModel' in attrs ? scope.$eval(attrs.ngModel) : {}; function makeNewScope (idx, collection, containerScope) { var childScope = containerScope.$new(); childScope[ident.value] = collection[idx]; childScope.$index = idx; childScope.$first = (idx === 0); childScope.$last = (idx === (collection.length - 1)); childScope.$middle = !(childScope.$first || childScope.$last); childScope.$watch(function updateChildScopeItem(){ childScope[ident.value] = collection[idx]; }); return childScope; } // Given the collection and a start and end point, add the current function addElements (start, end, collection, containerScope, insPoint) { var frag = document.createDocumentFragment(); var newElements = [], element, idx, childScope; for( idx = start; idx !== end; idx ++ ){ childScope = makeNewScope(idx, collection, containerScope); element = linker(childScope, angular.noop); //setElementCss(element); newElements.push(element); frag.appendChild(element[0]); } insPoint.after(frag); return newElements; } function recomputeActive() { // We want to set the start to the low water mark unless the current // start is already between the low and high water marks. var start = clip(state.firstActive, state.firstVisible - state.lowWater, state.firstVisible - state.highWater); // Similarly for the end var end = clip(state.firstActive + state.active, state.firstVisible + state.visible + state.lowWater, state.firstVisible + state.visible + state.highWater ); state.firstActive = Math.max(0, start); state.active = Math.min(end, state.total) - state.firstActive; } function sfVirtualRepeatOnScroll(evt){ if( !rowHeight ){ return; } // Enter the angular world for the state change to take effect. scope.$apply(function(){ state.firstVisible = Math.floor(evt.target.scrollTop / rowHeight); state.visible = Math.ceil(dom.viewport[0].clientHeight / rowHeight); $log.log('scroll to row %o', state.firstVisible); sticky = evt.target.scrollTop + evt.target.clientHeight >= evt.target.scrollHeight; recomputeActive(); $log.log(' state is now %o', state); $log.log(' sticky = %o', sticky); }); } function sfVirtualRepeatWatchExpression(scope){ var coll = scope.$eval(ident.collection); if( coll.length !== state.total ){ state.total = coll.length; recomputeActive(); } return { start: state.firstActive, active: state.active, len: coll.length }; } function destroyActiveElements (action, count) { var dead, ii, remover = Array.prototype[action]; for( ii = 0; ii < count; ii++ ){ dead = remover.call(rendered); dead.scope().$destroy(); dead.remove(); } } // When the watch expression for the repeat changes, we may need to add // and remove scopes and elements function sfVirtualRepeatListener(newValue, oldValue, scope){ var oldEnd = oldValue.start + oldValue.active, collection = scope.$eval(ident.collection), newElements; if(newValue === oldValue) { $log.info('initial listen'); newElements = addElements(newValue.start, oldEnd, collection, scope, iterStartElement); rendered = newElements; if(rendered.length) { rowHeight = computeRowHeight(newElements[0][0]); } } else { var newEnd = newValue.start + newValue.active; var forward = newValue.start >= oldValue.start; var delta = forward ? newValue.start - oldValue.start : oldValue.start - newValue.start; var endDelta = newEnd >= oldEnd ? newEnd - oldEnd : oldEnd - newEnd; var contiguous = delta < (forward ? oldValue.active : newValue.active); $log.info('change by %o,%o rows %s', delta, endDelta, forward ? 'forward' : 'backward'); if(!contiguous) { $log.info('non-contiguous change'); destroyActiveElements('pop', rendered.length); rendered = addElements(newValue.start, newEnd, collection, scope, iterStartElement); } else { if(forward) { $log.info('need to remove from the top'); destroyActiveElements('shift', delta); } else if(delta) { $log.info('need to add at the top'); newElements = addElements( newValue.start, oldValue.start, collection, scope, iterStartElement); rendered = newElements.concat(rendered); } if(newEnd < oldEnd) { $log.info('need to remove from the bottom'); destroyActiveElements('pop', oldEnd - newEnd); } else if(endDelta) { var lastElement = rendered[rendered.length-1]; $log.info('need to add to the bottom'); newElements = addElements( oldEnd, newEnd, collection, scope, lastElement); rendered = rendered.concat(newElements); } } if(!rowHeight && rendered.length) { rowHeight = computeRowHeight(rendered[0][0]); } dom.content.css({'padding-top': newValue.start * rowHeight + 'px'}); } dom.content.css({'height': newValue.len * rowHeight + 'px'}); if(sticky) { dom.viewport[0].scrollTop = dom.viewport[0].clientHeight + dom.viewport[0].scrollHeight; } } // - The index of the first active element state.firstActive = 0; // - The index of the first visible element state.firstVisible = 0; // - The number of elements visible in the viewport. state.visible = 0; // - The number of active elements state.active = 0; // - The total number of elements state.total = 0; // - The point at which we add new elements state.lowWater = state.lowWater || 100; // - The point at which we remove old elements state.highWater = state.highWater || 300; // TODO: now watch the water marks setContentCss(dom.content); setViewportCss(dom.viewport); // When the user scrolls, we move the `state.firstActive` dom.bind('momentumScrolled', sfVirtualRepeatOnScroll); scope.$on('$destroy', function () { dom.unbind('momentumScrolled', sfVirtualRepeatOnScroll); }); // The watch on the collection is just a watch on the length of the // collection. We don't care if the content changes. scope.$watch(sfVirtualRepeatWatchExpression, sfVirtualRepeatListener, true); }; } }; }]); })(ionic);