/** * 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' ]); angular.module('ionic.ui', [ 'ionic.ui.content', 'ionic.ui.tabs', 'ionic.ui.nav', 'ionic.ui.header', 'ionic.ui.sideMenu', 'ionic.ui.list', 'ionic.ui.checkbox', 'ionic.ui.toggle', 'ionic.ui.radio' ]); angular.module('ionic', [ 'ionic.service', 'ionic.ui', ]); ; angular.module('ionic.service.actionSheet', ['ionic.service.templateLoad', 'ionic.ui.actionSheet', 'ngAnimate']) .factory('ActionSheet', ['$rootScope', '$document', '$compile', '$animate', '$timeout', 'TemplateLoader', function($rootScope, $document, $compile, $animate, $timeout, TemplateLoader) { 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(); } }); $timeout(function() { $animate.removeClass(element, 'active', function() { scope.$destroy(); }); }); }; 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, function() { }); return sheet; } }; }]); ; angular.module('ionic.service.gesture', []) .factory('Gesture', [function() { return { on: function(eventType, cb, element) { return window.ionic.onGesture(eventType, cb, element); } }; }]); ; angular.module('ionic.service.loading', ['ionic.ui.loading']) .factory('Loading', ['$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) { var 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']) .factory('Modal', ['$rootScope', '$document', '$compile', '$animate', 'TemplateLoader', function($rootScope, $document, $compile, $animate, TemplateLoader) { 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 element = angular.element(this.el); if(!element.parent().length) { $animate.enter(element, angular.element($document[0].body)); } $animate.addClass(element, this.animation); ionic.views.Modal.prototype.show.call(this); }, // 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 element = angular.element(this.el); $animate.leave(angular.element(this.el), function() { 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); // 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) { TemplateLoader.load(url).then(function(templateString) { var modal = createModal(templateString, options || {}); cb(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('Platform', 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; } 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(); } 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); }); }, /** * 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('Popup', ['$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('TemplateLoader', ['$q', '$http', '$templateCache', function($q, $http, $templateCache) { return { load: function(url) { var deferred = $q.defer(); $http.get(url, { cache: $templateCache }).success(function(html) { deferred.resolve(html && html.trim()); }); return deferred.promise; } }; }]); ; (function() { 'use strict'; angular.module('ionic.ui.actionSheet', []) .directive('actionSheet', function($document) { return { restrict: 'E', scope: true, replace: true, link: function($scope, $element){ $scope.$on('$destroy', function() { $element.remove(); }); $document.bind('keyup', function(e) { if(e.which == 27) { $scope.cancel(); $scope.$apply(); } }); }, template: '
' + '
' + '
' + '
{{titleText}}
' + '' + '
' + '
' + '' + '
' + '
' + '' + '
' + '
' + '
' }; }); })(); ; (function() { 'use strict'; angular.module('ionic.ui.checkbox', []) .directive('checkbox', function() { return { restrict: 'E', replace: true, require: '?ngModel', scope: true, 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.checkbox = new ionic.views.Checkbox({ el: $element[0], checkbox: checkbox[0], handle: handle[0] }); $element.bind('click', function(e) { $scope.checkbox.tap(e); $scope.$apply(function() { ngModel.$setViewValue(checkbox[0].checked); }); }); ngModel.$render = function() { $scope.checkbox.val(ngModel.$viewValue); }; } }; }); })(); ; (function() { 'use strict'; angular.module('ionic.ui.content', []) .directive('pane', function() { return { restrict: 'E', replace: true, transclude: true, template: '
' } }) // The content directive is a core scrollable content area // that is part of many View hierarchies .directive('content', ['$parse', function($parse) { return { restrict: 'E', replace: true, template: '
', transclude: true, scope: { onRefresh: '&', onRefreshOpening: '&', scroll: '@' }, compile: function(element, attr, transclude) { return function($scope, $element, $attr) { var c = $element.eq(0); if(attr.padded) { c.addClass('padding'); } if(attr.hasHeader) { c.addClass('has-header'); } if(attr.hasFooter) { c.addClass('has-footer'); } if(attr.hasTabs) { c.addClass('has-tabs'); } // If they want plain overflows scrolling, add that as a class if($scope.scroll === "false") { // Do nothing for now } else if(attr.overflowScroll === "true") { c.addClass('overflow-scroll'); } else { // Otherwise, supercharge this baby! var sv = new ionic.views.Scroll({ el: $element[0].firstElementChild, hasPullToRefresh: (typeof $scope.onRefresh !== 'undefined'), onRefresh: function() { $scope.onRefresh(); }, onRefreshOpening: function(amt) { $scope.onRefreshOpening({amount: amt}); } }); // Let child scopes access this $scope.scrollView = sv; } // Pass the parent scope down to the child var clone = transclude($scope.$parent); angular.element($element[0].firstElementChild).append(clone); }; } }; }]) .directive('refresher', function() { return { restrict: 'E', replace: true, require: ['^?content', '^?list'], template: '
', scope: true, link: function($scope, $element, $attr, scrollCtrl) { var icon = $element[0].querySelector('.ionic-refresher'); // Scale up the refreshing icon var didHold = false; var onRefreshOpening = ionic.throttle(function(e, amt) { if(!didHold) { icon.style[ionic.CSS.TRANSFORM] = 'scale(' + Math.min((0.2 + amt), 1) + ')'; } if(amt >= 1) { didHold = true; } }, 100); $scope.$on('scroll.onRefreshing', function(e) { console.log('ON REFReSHING'); icon.style[ionic.CSS.TRANSFORM] = 'scale(2)'; }); $scope.$on('scroll.onRefresh', function(e) { console.log('ON REFReSH'); icon.style[ionic.CSS.TRANSFORM] = 'scale(1)'; }); $scope.$on('scroll.onRefreshOpening', onRefreshOpening); } } }) .directive('scroll-refresher', function() { return { restrict: 'E', replace: true, transclude: true, template: '
' } }); })(); ; (function(ionic) { 'use strict'; angular.module('ionic.ui.header', ['ngAnimate']) .directive('headerBar', function() { return { restrict: 'E', replace: true, transclude: true, template: '
', scope: { 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.$on('$destroy', function() { // }); } }; }); })(ionic); ; (function() { 'use strict'; angular.module('ionic.ui.list', ['ngAnimate']) .directive('item', ['$timeout', function($timeout) { return { restrict: 'E', require: ['?^list'], replace: true, transclude: true, scope: { item: '=', onSelect: '&', onDelete: '&', canDelete: '@', canReorder: '@', canSwipe: '@', buttons: '=', type: '@' }, template: '\
\ \
\
\
\
\ \
\
\ \
\
', link: function($scope, $element, $attr, list) { // Grab the parent list controller if(list[0]) { list = list[0]; } else if(list[1]) { list = list[1]; } // Add the list item type class $element.addClass($attr.type || 'item-slider'); if($attr.type !== 'item-slider') { $scope.canSwipe = false; } $scope.isEditing = false; $scope.deleteIcon = list.scope.deleteIcon; $scope.reorderIcon = list.scope.reorderIcon; $scope.showOptions = true; $scope.buttonClicked = function(button) { button.onButtonClicked && button.onButtonClicked($scope.item, button); }; list.scope.$watch('isEditing', function(v) { $scope.isEditing = v; // Add a delay before we allow the options layer to show, to avoid any odd // animation issues if(!v) { $timeout(function() { $scope.showOptions = true; }, 200); } else { $scope.showOptions = false; } }); } }; }]) .directive('list', function() { return { restrict: 'E', replace: true, transclude: true, scope: { isEditing: '=', deleteIcon: '@', reorderIcon: '@', hasPullToRefresh: '@', onRefresh: '&', onRefreshOpening: '&' }, controller: function($scope) { var _this = this; this.scope = $scope; $scope.$watch('isEditing', function(v) { _this.isEditing = true; }); }, template: '', compile: function(element, attr, transclude) { return function($scope, $element, $attr) { var lv = new ionic.views.ListView({ el: $element[0], listEl: $element[0].children[0], hasPullToRefresh: ($scope.hasPullToRefresh !== 'false'), onRefresh: function() { $scope.onRefresh(); $scope.$parent.$broadcast('scroll.onRefresh'); }, onRefreshOpening: function(amt) { $scope.onRefreshOpening({amount: amt}); $scope.$parent.$broadcast('scroll.onRefreshOpening', amt); } }); if(attr.animation) { $element.addClass(attr.animation); } }; } }; }); })(); ; (function() { 'use strict'; angular.module('ionic.ui.loading', []) .directive('loading', function() { return { restrict: 'E', replace: true, transclude: true, link: function($scope, $element){ $scope.$on('$destroy', function() { $element.remove(); }); $element.addClass($scope.animation || ''); }, template: '
' + '
' + '
' + '
' }; }); })(); ; (function() { 'use strict'; angular.module('ionic.ui.nav', ['ionic.service.templateLoad', 'ionic.service.gesture', 'ionic.service.platform', 'ngAnimate']) .controller('NavCtrl', ['$scope', '$element', '$animate', '$compile', 'TemplateLoader', 'Platform', function($scope, $element, $animate, $compile, TemplateLoader, Platform) { var _this = this; angular.extend(this, ionic.controllers.NavController.prototype); /** * Push a template onto the navigation stack. * @param {string} templateUrl the URL of the template to load. */ this.pushFromTemplate = ionic.throttle(function(templateUrl) { var childScope = $scope.$new(); childScope.isVisible = true; // Load the given template TemplateLoader.load(templateUrl).then(function(templateString) { // Compile the template with the new scrope, and append it to the navigation's content area var el = $compile(templateString)(childScope, function(cloned, scope) { var content = angular.element($element[0].querySelector('.content, .scroll')); $animate.enter(cloned, angular.element(content)); }); }); }, 300, { trailing: false }); // Pop function, throttled this.popController = ionic.throttle(function() { _this.pop(); $scope.$broadcast('navs.pop'); }, 300, { trailing: false }); ionic.controllers.NavController.call(this, { content: { }, navBar: { shouldGoBack: function() { }, show: function() { this.isVisible = true; }, hide: function() { this.isVisible = false; }, setTitle: function(title) { $scope.navController.title = title; }, showBackButton: function(show) { }, } }); // Support Android hardware back button (native only, not mobile web) var onHardwareBackButton = function(e) { $scope.$apply(function() { _this.popController(); }); } Platform.onHardwareBackButton(onHardwareBackButton); this.handleDrag = function(e) { // TODO: Support dragging between pages }; this.endDrag = function(e) { }; /** * Push a controller to the stack. This is called by the child * nav-content directive when it is linked to a scope on the page. */ $scope.pushController = function(scope, element) { _this.push(scope); $scope.$broadcast('navs.push', scope); }; $scope.navController = this; $scope.$on('$destroy', function() { // Remove back button listener Platform.offHardwareBackButton(onHardwareBackButton); }); }]) .directive('navs', function() { return { restrict: 'E', replace: true, transclude: true, controller: 'NavCtrl', //templateUrl: 'ext/angular/tmpl/ionicTabBar.tmpl.html', template: '
', }; }) .directive('navBar', function() { return { restrict: 'E', require: '^navs', replace: true, scope: { type: '@', backButtonType: '@', alignTitle: '@' }, template: '', link: function($scope, $element, $attr, navCtrl) { var backButton; $scope.navController = navCtrl; $scope.goBack = function() { navCtrl.popController(); }; var hb = new ionic.views.HeaderBar({ el: $element[0], alignTitle: $scope.alignTitle || 'center' }); $element.addClass($scope.type); $scope.headerBarView = hb; $scope.$parent.$on('navs.push', function() { backButton = angular.element($element[0].querySelector('.button')); backButton.addClass($scope.backButtonType); hb.align(); }); $scope.$parent.$on('navs.pop', function() { hb.align(); }); $scope.$on('$destroy', function() { // }); } }; }) .directive('navContent', ['Gesture', '$animate', '$compile', function(Gesture, $animate, $compile) { // We need to animate the new controller into view. var animatePushedController = function(childScope, clone, $element, isForward) { var parent = angular.element($element.parent().parent().parent()); var title = angular.element(parent[0].querySelector('.title')); // Clone the old title and insert it so we can animate it back into place for the new controller var newTitle = angular.element(title.clone()); $compile(newTitle)(childScope); title.after(newTitle); // Grab the button so we can slide it in var button = angular.element(parent[0].querySelector('.button')); if(isForward) { // Slide the button in $animate.addClass(button, childScope.slideButtonInAnimation, function() { $animate.removeClass(button, childScope.slideButtonInAnimation, function() {}); }) // Slide the new title in $animate.addClass(newTitle, childScope.slideTitleInAnimation, function() { $animate.removeClass(newTitle, childScope.slideTitleInAnimation, function() { newTitle.scope().$destroy(); newTitle.remove(); }); }); // Grab the old title and slide it out var title = $element.parent().parent().parent()[0].querySelector('.title'); $animate.addClass(angular.element(title), childScope.slideTitleOutAnimation, function() { $animate.removeClass(angular.element(title), childScope.slideTitleOutAnimation, function() { }); }); } else { clone.addClass(childScope.slideBackInAnimation); } }; return { restrict: 'ECA', require: '^navs', transclude: 'element', compile: function(element, attr, transclude) { return function($scope, $element, $attr, navCtrl) { var lastParent, lastIndex, childScope, childElement; // Store that we should go forwards on the animation. This toggles // based on the visibility sequence (to support reverse transitions) var lastDirection = null; $scope.title = $attr.title; $scope.pushAnimation = $attr.pushAnimation || 'slide-in-left'; $scope.popAnimation = $attr.popAnimation || 'slide-in-right'; $scope.slideTitleInAnimation = $attr.slideTitleInAnimation || 'bar-title-in'; $scope.slideTitleOutAnimation = $attr.slideTitleOutAnimation || 'bar-title-out'; $scope.slideButtonInAnimation = $attr.slideButtonInAnimation || 'bar-button-in'; $scope.slideButtonOutAnimation = $attr.slideButtonOutAnimation || 'bar-button-out'; if($attr.navBar === "false") { navCtrl.hideNavBar(); } else { navCtrl.showNavBar(); } $scope.visibilityChanged = function(direction) { lastDirection = direction; if($scope.isVisible) { $scope.$broadcast('navContent.shown'); } else { $scope.$broadcast('navContent.hidden'); } if(!childElement) { return; } var clone = childElement; if(direction == 'push') { clone.addClass(childScope.pushAnimation); clone.removeClass(childScope.popAnimation); } else if(direction == 'pop') { clone.addClass(childScope.popAnimation); clone.removeClass(childScope.pushAnimation); } }; // Push this controller onto the stack $scope.pushController($scope, $element); $scope.$watch('isVisible', function(value) { if(value) { childScope = $scope.$new(); transclude(childScope, function(clone) { childElement = clone; if(lastDirection == 'push') { clone.addClass(childScope.pushAnimation); } else if(lastDirection == 'pop') { clone.addClass(childScope.popAnimation); } $animate.enter(clone, $element.parent(), $element, function() { clone.removeClass(childScope.pushAnimation); clone.removeClass(childScope.popAnimation); }); }); } else { // Taken from ngIf if(childElement) { // Check if this is visible, and if so, create it and show it $animate.leave(childElement, function() { if(childScope) { childElement.removeClass(childScope.pushAnimation); childElement.removeClass(childScope.popAnimation); } }); childElement = undefined; } if(childScope) { childScope.$destroy(); childScope = undefined; } } }); } } }; }]); })(); ; (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; } radio.bind('click', function(e) { $scope.$apply(function() { ngModel.$setViewValue($scope.$eval($attr.ngValue)); }); }); ngModel.$render = function() { var val = $scope.$eval($attr.ngValue); if(val === ngModel.$viewValue) { radio.attr('checked', 'checked'); } else { radio.removeAttr('checked'); } }; } }; }); })(window.ionic); ; (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. */ .controller('SideMenuCtrl', 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: 270, }, // Our quick implementation of the right side menu right: { width: 270, } }); $scope.sideMenuContentTranslateX = 0; $scope.sideMenuCtrl = this; }) .directive('sideMenu', function() { return { restrict: 'ECA', controller: 'SideMenuCtrl', replace: true, transclude: true, template: '
' }; }) .directive('sideMenuContent', ['Gesture', function(Gesture) { return { restrict: 'AC', require: '^sideMenu', scope: true, compile: function(element, attr, transclude) { return function($scope, $element, $attr, sideMenuCtrl) { $element.addClass('menu-content'); var defaultPrevented = false; ionic.on('mousedown', function(e) { // If the child element prevented the drag, don't drag defaultPrevented = e.defaultPrevented; }); var dragFn = function(e) { if(defaultPrevented) { return; } sideMenuCtrl._handleDrag(e); }; Gesture.on('drag', dragFn, $element[0]); var dragReleaseFn = function(e) { if(!defaultPrevented) { sideMenuCtrl._endDrag(e); } defaultPrevented = false; }; Gesture.on('release', dragReleaseFn, $element[0]); sideMenuCtrl.setContent({ onDrag: function(e) {}, endDrag: function(e) {}, getTranslateX: function() { return $scope.sideMenuContentTranslateX || 0; }, setTranslateX: function(amount) { $scope.sideMenuContentTranslateX = amount; $element[0].style.webkitTransform = 'translate3d(' + amount + 'px, 0, 0)'; }, 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() { Gesture.off('drag', dragFn); Gesture.off('release', dragReleaseFn); }); }; } }; }]) .directive('menu', function() { return { restrict: 'E', require: '^sideMenu', 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 sideMenuCtrl lets you quickly have a draggable side * left and/or right menu, which a center content area. */ angular.module('ionic.ui.slideBox', []) /** * 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. */ .controller('SlideBoxCtrl', ['$scope', '$element', function($scope, $element) { $scope.slides = []; this.slideAdded = function() { $scope.slides.push({}); }; }]) .directive('slideBox', ['$compile', function($compile) { return { restrict: 'E', replace: true, transclude: true, controller: 'SlideBoxCtrl', scope: {}, template: '
\
\
\
', link: function($scope, $element, $attr, slideBoxCtrl) { // If the pager should show, append it to the slide box if($attr.showPager !== "false") { var childScope = $scope.$new(); var pager = $compile('')(childScope); $element.append(pager); $scope.slideBox = new ionic.views.SlideBox({ el: $element[0] }); } } } }]) .directive('slide', function() { return { restrict: 'E', replace: true, require: '^slideBox', transclude: true, template: '
', compile: function(element, attr, transclude) { return function($scope, $element, $attr, slideBoxCtrl) { slideBoxCtrl.slideAdded(); } } } }) .directive('pager', function() { return { restrict: 'E', replace: true, require: '^slideBox', template: '
' } }); })(); ; angular.module('ionic.ui.tabs', ['ngAnimate']) .controller('TabsCtrl', ['$scope', '$element', '$animate', function($scope, $element, $animate) { var _this = this; 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(controller) { this.addController(controller); this.select(0); }; this.select = function(controllerIndex) { //var oldIndex = _this.getSelectedIndex(); $scope.activeAnimation = $scope.animation; /* if(controllerIndex > oldIndex) { } else if(controllerIndex < oldIndex) { $scope.activeAnimation = $scope.animation + '-reverse'; } */ _this.selectController(controllerIndex); }; $scope.controllers = this.controllers; }]) .directive('tabs', function() { return { restrict: 'E', replace: true, scope: { animation: '@', controllerChanged: '&', tabsType: '@', tabsStyle: '@', }, transclude: true, controller: 'TabsCtrl', //templateUrl: 'ext/angular/tmpl/ionicTabBar.tmpl.html', template: '
', compile: function(element, attr, transclude, tabsCtrl) { return function($scope, $element, $attr) { $scope.$watch('activeAnimation', function(value) { //$element.removeClass($scope.animation + ' ' + $scope.animation + '-reverse'); $element.addClass($scope.activeAnimation); }); transclude($scope, function(cloned) { $element.prepend(cloned); }); }; } }; }) // Generic controller directive .directive('tab', ['$animate', function($animate) { return { restrict: 'E', replace: true, require: '^tabs', scope: true, transclude: 'element', compile: function(element, attr, transclude) { return function($scope, $element, $attr, tabsCtrl) { var childScope, childElement; $scope.title = $attr.title; $scope.icon = $attr.icon; $scope.iconOn = $attr.iconOn; $scope.iconOff = $attr.iconOff; tabsCtrl.add($scope); $scope.$watch('isVisible', function(value) { if(childElement) { $animate.leave(childElement); childElement = undefined; } if(childScope) { childScope.$destroy(); childScope = undefined; } if(value) { childScope = $scope.$new(); transclude(childScope, function(clone) { childElement = clone; childElement.addClass('view-full'); $animate.enter(clone, $element.parent(), $element); }); } }); } } }; }]) .directive('tabControllerBar', function() { return { restrict: 'E', require: '^tabs', transclude: true, replace: true, scope: true, template: '
' + '' + '
', link: function($scope, $element, $attr) { $element.addClass($scope.tabsType); $element.addClass($scope.tabsStyle); } }; }) .directive('tabControllerItem', function() { return { restrict: 'E', replace: true, require: '^tabs', scope: { title: '@', icon: '@', iconOn: '@', iconOff: '@', active: '=', tabSelected: '@', index: '=' }, link: function(scope, element, attrs, tabsCtrl) { if(attrs.icon) { scope.iconOn = scope.iconOff = attrs.icon; } scope.selectTab = function(index) { tabsCtrl.select(scope.index); }; }, template: '' + '' + '' + ' {{title}}' + '' }; }) .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: true, 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] }); $element.bind('click', function(e) { $scope.toggle.tap(e); $scope.$apply(function() { ngModel.$setViewValue(checkbox[0].checked); }); }); ngModel.$render = function() { $scope.toggle.val(ngModel.$viewValue); }; } }; }); })(window.ionic); ; (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]; var _this = this; 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); // 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);