diff --git a/config/build.config.js b/config/build.config.js index 13d21681c9..f08dae4c41 100644 --- a/config/build.config.js +++ b/config/build.config.js @@ -16,13 +16,15 @@ module.exports = { ' *\n' + ' * Licensed under the MIT license. Please see LICENSE for more information.\n'+ ' *\n' + - ' */\n\n', + ' */\n\n' + + '(function() {\n', bundleBanner: '/*!\n' + ' * ionic.bundle.js is a concatenation of:\n' + ' * ionic.js, angular.js, angular-animate.js,\n'+ ' * angular-ui-router.js, and ionic-angular.js\n'+ ' */\n\n', + footer: '\n})();', ionicFiles: [ // Base diff --git a/js/ext/angular/src/controller/ionicScrollController.js b/js/ext/angular/src/controller/ionicScrollController.js index c395df3749..a54bbb3806 100644 --- a/js/ext/angular/src/controller/ionicScrollController.js +++ b/js/ext/angular/src/controller/ionicScrollController.js @@ -1,8 +1,173 @@ -(function() { -'use strict'; - angular.module('ionic.ui.scroll') +/** + * @ngdoc service + * @name $ionicScrollDelegate + * @module ionic + * @description + * Delegate for controlling scrollViews (created by + * {@link ionic.directive:ionContent} and + * {@link ionic.directive:ionScroll} directives). + * + * Each method on $ionicScrollDelegate can be called on the service itself to control all scrollViews. Alternatively, one can control one specific scrollView using `withHandle` and `delegate-handle`. See the example below. + * + * @usage + * + * Basic Usage: + * + * ```html + * + * + * + * + * + * ``` + * ```js + * function MainCtrl($scope, $ionicScrollDelegate) { + * $scope.scrollTop = function() { + * $ionicScrollDelegate.scrollTop(); + * }; + * } + * ``` + * + * Example of advanced usage, with two scroll areas using `delegate-handle` + * for fine control. + * + * ```html + * + * + * + * + * + * + * + * + * ``` + * ```js + * function MainCtrl($scope, $ionicScrollDelegate) { + * $scope.scrollMainToTop = function() { + * $ionicScrollDelegate.withHandle('mainScroll').scrollTop(); + * }; + * $scope.scrollSmallToTop = function() { + * $ionicScrollDelegate.withHandle('small').scrollTop(); + * }; + * } + * ``` + */ + +/** + * @ngdoc method + * @name $ionicScrollDelegate#withHandle + * @param {string} handle + * @returns `delegateInstance` A delegate instance that controls only the + * scrollView with delegate-handle matching the given handle. + */ +.service('$ionicScrollDelegate', delegateService([ + /** + * @ngdoc method + * @name $ionicScrollDelegate#resize + * @description Tell the scrollView to recalculate the size of its container. + */ + 'resize', + /** + * @ngdoc method + * @name $ionicScrollDelegate#scrollTop + * @param {boolean=} shouldAnimate Whether the scroll should animate. + */ + 'scrollTop', + /** + * @ngdoc method + * @name $ionicScrollDelegate#scrollBottom + * @param {boolean=} shouldAnimate Whether the scroll should animate. + */ + 'scrollBottom', + /** + * @ngdoc method + * @name $ionicScrollDelegate#scroll + * @param {number} left The x-value to scroll to. + * @param {number} top The y-value to scroll to. + * @param {boolean=} shouldAnimate Whether the scroll should animate. + */ + 'scrollTo', + /** + * @ngdoc method + * @name $ionicScrollDelegate#anchorScroll + * @description Tell the scrollView to scroll to the element with an id + * matching window.location.hash. + * + * If no matching element is found, it will scroll to top. + * + * @param {boolean=} shouldAnimate Whether the scroll should animate. + */ + 'anchorScroll', + /** + * @ngdoc method + * @name $ionicScrollDelegate#rememberScrollPosition + * @description + * Will make it so, when this scrollView is destroyed (user leaves the page), + * the last scroll position the page was on will be saved, indexed by the + * given id. + * + * Note: for pages associated with a view under an ion-nav-view, + * rememberScrollPosition automatically saves their scroll. + * + * Related methods: scrollToRememberedPosition, forgetScrollPosition (below). + * + * In the following example, the scroll position of the ion-scroll element + * will persist, even when the user changes the toggle switch. + * + * ```html + * + * + *
+ * + * {{i}} + * + *
+ *
+ * ``` + * ```js + * function ScrollCtrl($scope, $ionicScrollDelegate) { + * var delegate = $ionicScrollDelegate.withHandle('myScroll'); + * + * // Put any unique ID here. The point of this is: every time the controller is recreated + * // we want to load the correct remembered scroll values. + * delegate.rememberScrollPosition('my-scroll-id'); + * delegate.scrollToRememberedPosition(); + * $scope.items = []; + * for (var i=0; i<100; i++) { + * $scope.items.push(i); + * } + * } + * ``` + * + * @param {string} id The id to remember the scroll position of this + * scrollView by. + */ + 'rememberScrollPosition', + /** + * @ngdoc method + * @name $ionicScrollDelegate#forgetScrollPosition + * @description + * Stop remembering the scroll position for this scrollView. + */ + 'forgetScrollPosition', + /** + * @ngdoc method + * @name $ionicScrollDelegate#scrollToRememberedPosition + * @description + * If this scrollView has an id associated with its scroll position, + * (through calling rememberScrollPosition), and that position is remembered, + * load the position and scroll to it. + * @param {boolean=} shouldAnimate Whether to animate the scroll. + */ + 'scrollToRememberedPosition' +])) + /** * @private */ @@ -10,14 +175,6 @@ angular.module('ionic.ui.scroll') return {}; }) -/** - * @ngdoc controller - * @name ionicScroll - * @module ionic - * @description - * Controller for the {@link ionic.directive:ionContent} and - * {@link ionic.directive:ionScroll} directives. - */ .controller('$ionicScroll', [ '$scope', 'scrollViewOptions', @@ -25,13 +182,18 @@ angular.module('ionic.ui.scroll') '$window', '$$scrollValueCache', '$location', - '$parse', '$rootScope', '$document', -function($scope, scrollViewOptions, $timeout, $window, $$scrollValueCache, $location, $parse, $rootScope, $document) { + '$ionicScrollDelegate', + '$parse', //DEPRECATED +function($scope, scrollViewOptions, $timeout, $window, $$scrollValueCache, $location, $rootScope, $document, $ionicScrollDelegate, $parse) { var self = this; + this._scrollViewOptions = scrollViewOptions; //for testing + + $parse('$ionicScrollController').assign($scope.$parent || $scope, this); + var element = this.element = scrollViewOptions.el; var $element = this.$element = angular.element(element); var scrollView = this.scrollView = new ionic.views.Scroll(scrollViewOptions); @@ -42,8 +204,9 @@ function($scope, scrollViewOptions, $timeout, $window, $$scrollValueCache, $loca ($element.parent().length ? $element.parent() : $element) .data('$$ionicScrollController', this); - $parse(scrollViewOptions.controllerBind || '$ionicScrollController') - .assign($scope.$parent || $scope, this); + var deregisterInstance = $ionicScrollDelegate._registerInstance( + this, scrollViewOptions.delegateHandle + ); if (!angular.isDefined(scrollViewOptions.bouncing)) { ionic.Platform.ready(function() { @@ -58,6 +221,7 @@ function($scope, scrollViewOptions, $timeout, $window, $$scrollValueCache, $loca var backListenDone = angular.noop; $scope.$on('$destroy', function() { + deregisterInstance(); ionic.off('resize', resize, $window); $window.removeEventListener('resize', resize); backListenDone(); @@ -101,31 +265,16 @@ function($scope, scrollViewOptions, $timeout, $window, $$scrollValueCache, $loca this._rememberScrollId = null; - /** - * @ngdoc method - * @name ionicScroll#resize - * @description Tell the scrollView to recalculate the size of its container. - */ this.resize = function() { return $timeout(resize); }; - /** - * @ngdoc method - * @name ionicScroll#scrollTop - * @param {boolean=} shouldAnimate Whether the scroll should animate. - */ this.scrollTop = function(shouldAnimate) { this.resize().then(function() { scrollView.scrollTo(0, 0, !!shouldAnimate); }); }; - /** - * @ngdoc method - * @name ionicScroll#scrollBottom - * @param {boolean=} shouldAnimate Whether the scroll should animate. - */ this.scrollBottom = function(shouldAnimate) { this.resize().then(function() { var max = scrollView.getScrollMax(); @@ -133,29 +282,12 @@ function($scope, scrollViewOptions, $timeout, $window, $$scrollValueCache, $loca }); }; - /** - * @ngdoc method - * @name ionicScroll#scroll - * @param {number} left The x-value to scroll to. - * @param {number} top The y-value to scroll to. - * @param {boolean=} shouldAnimate Whether the scroll should animate. - */ this.scrollTo = function(left, top, shouldAnimate) { this.resize().then(function() { scrollView.scrollTo(left, top, !!shouldAnimate); }); }; - /** - * @ngdoc method - * @name ionicScroll#anchorScroll - * @description Tell the scrollView to scroll to the element with an id - * matching window.location.hash. - * - * If no matching element is found, it will scroll to top. - * - * @param {boolean=} shouldAnimate Whether the scroll should animate. - */ this.anchorScroll = function(shouldAnimate) { this.resize().then(function() { var hash = $location.hash(); @@ -169,75 +301,16 @@ function($scope, scrollViewOptions, $timeout, $window, $$scrollValueCache, $loca }); }; - /** - * @ngdoc method - * @name ionicScroll#rememberScrollPosition - * @description - * Will make it so, when this scrollView is destroyed (user leaves the page), - * the last scroll position the page was on will be saved, indexed by the - * given id. - * - * Note: for pages associated with a view under an ion-nav-view, - * rememberScrollPosition automatically saves their scroll. - * - * Related methods: scrollToRememberedPosition, forgetScrollPosition (below). - * - * In the following example, the scroll position of the ion-scroll element - * will persist, even when the user changes the toggle switch. - * - * ```html - * - * - *
- * - * {{i}} - * - *
- *
- * ``` - * ```js - * function ScrollCtrl($scope) { - * // Put any unique ID here. The point of this is: every time the controller is recreated - * // we want to load the correct remembered scroll values. - * $scope.$ionicScrollController.rememberScrollPosition('my-scroll-id'); - * - * $scope.$ionicScrollController.scrollToRememberedPosition(); - - * $scope.items = []; - * for (var i=0; i<100; i++) { - * $scope.items.push(i); - * } - * } - * ``` - * - * @param {string} id The id to remember the scroll position of this - * scrollView by. - */ this.rememberScrollPosition = function(id) { if (!id) { throw new Error("Must supply an id to remember the scroll by!"); } this._rememberScrollId = id; }; - /** - * @ngdoc method - * @name ionicScroll#forgetScrollPosition - * @description - * Stop remembering the scroll position for this scrollView. - */ this.forgetScrollPosition = function() { delete $$scrollValueCache[this._rememberScrollId]; this._rememberScrollId = null; }; - /** - * @ngdoc method - * @name ionicScroll#scrollToRememberedPosition - * @description - * If this scrollView has an id associated with its scroll position, - * (through calling rememberScrollPosition), and that position is remembered, - * load the position and scroll to it. - * @param {boolean=} shouldAnimate Whether to animate the scroll. - */ this.scrollToRememberedPosition = function(shouldAnimate) { var values = $$scrollValueCache[this._rememberScrollId]; if (values) { @@ -268,4 +341,3 @@ function($scope, scrollViewOptions, $timeout, $window, $$scrollValueCache, $loca }; }]); -})(); diff --git a/js/ext/angular/src/directive/ionicContent.js b/js/ext/angular/src/directive/ionicContent.js index f25443212a..129daa0a73 100644 --- a/js/ext/angular/src/directive/ionicContent.js +++ b/js/ext/angular/src/directive/ionicContent.js @@ -28,7 +28,6 @@ angular.module('ionic.ui.content', ['ionic.ui.scroll']) * @ngdoc directive * @name ionContent * @module ionic - * @controller ionicScroll as $scope.$ionicScrollController * @restrict E * * @description @@ -44,9 +43,8 @@ angular.module('ionic.ui.content', ['ionic.ui.scroll']) * directive, and infinite scrolling with the {@link ionic.directive:ionInfiniteScroll} * directive. * - * @param {string=} controller-bind The scope variable to bind this element's scrollView's - * {@link ionic.controller:ionicScroll ionicScroll controller} to. - * Default: $scope.$ionicScrollController. + * @param {string=} delegate-handle The handle used to identify this scrollView + * with {@link ionic.service:$ionicScrollDelegate}. * @param {boolean=} padding Whether to add padding to the content. * of the content. Defaults to true on iOS, false on Android. * @param {boolean=} scroll Whether to allow scrolling of content. Defaults to true. @@ -122,7 +120,7 @@ function($parse, $timeout, $controller, $ionicBind) { $scope: $scope, scrollViewOptions: { el: $element[0], - controllerBind: $attr.controllerBind, + delegateHandle: attr.delegateHandle, bouncing: $scope.$eval($scope.hasBouncing), startX: $scope.$eval($scope.startX) || 0, startY: $scope.$eval($scope.startY) || 0, diff --git a/js/ext/angular/src/directive/ionicScroll.js b/js/ext/angular/src/directive/ionicScroll.js index a946019a2f..5d5b49baea 100644 --- a/js/ext/angular/src/directive/ionicScroll.js +++ b/js/ext/angular/src/directive/ionicScroll.js @@ -7,15 +7,13 @@ angular.module('ionic.ui.scroll', []) * @ngdoc directive * @name ionScroll * @module ionic - * @controller ionicScroll as $scope.$ionicScrollController * @restrict E * * @description * Creates a scrollable container for all content inside. * - * @param {string=} controller-bind The scope variable to bind this element's scrollView's - * {@link ionic.controller:ionicScroll ionicScroll controller} to. - * Default: $scope.$ionicScrollController. + * @param {string=} delegate-handle The handle used to identify this scrollView + * with {@link ionic.service:$ionicScrollDelegate}. * @param {string=} direction Which way to scroll. 'x' or 'y'. Default 'y'. * @param {boolean=} paging Whether to scroll with paging. * @param {expression=} on-refresh Called on pull-to-refresh, triggered by an {@link ionic.directive:ionRefresher}. @@ -63,7 +61,7 @@ angular.module('ionic.ui.scroll', []) var scrollViewOptions= { el: $element[0], - controllerBind: $attr.controllerBind, + delegateHandle: $attr.delegateHandle, paging: isPaging, scrollbarX: $scope.$eval($scope.scrollbarX) !== false, scrollbarY: $scope.$eval($scope.scrollbarY) !== false, diff --git a/js/ext/angular/src/service/delegateFactory.js b/js/ext/angular/src/service/delegateFactory.js new file mode 100644 index 0000000000..30f8b03707 --- /dev/null +++ b/js/ext/angular/src/service/delegateFactory.js @@ -0,0 +1,66 @@ + +function delegateService(methodNames) { + return ['$log', function($log) { + var delegate = this; + + this._instances = {}; + this._registerInstance = function(instance, handle) { + handle || (handle = ionic.Utils.nextUid()); + delegate._instances[handle] = instance; + + return function deregister() { + delete delegate._instances[handle]; + }; + }; + + this.withHandle = function(handle) { + if (!handle) { + return delegate; + } + return new InstanceWithHandle(handle); + }; + + /* + * Creates a new object that will have all the methodNames given, + * and call them on the given the controller instance matching given + * handle. + * The reason we don't just let withHandle return the controller instance + * itself is that the controller instance might not exist yet. + * + * We want people to be able to do + * `var instance = $ionicScrollDelegate.withHandle('foo')` on controller + * instantiation, but on controller instantiation a child directive + * may not have been compiled yet! + * + * So this is our way of solving this problem: we create an object + * that will only try to fetch the controller with given handle + * once the methods are actually called. + */ + function InstanceWithHandle(handle) { + this.handle = handle; + } + methodNames.forEach(function(methodName) { + InstanceWithHandle.prototype[methodName] = function() { + var instance = delegate._instances[this.handle]; + if (!instance) { + return $log.error( + 'Delegate with handle "'+this.handle+'" could not find a', + 'corresponding element with delegate-handle="'+this.handle+'"!', + methodName, 'was not called!'); + } + return instance[methodName].apply(instance, arguments); + }; + delegate[methodName] = function() { + var args = arguments; + var returnValue; + angular.forEach(delegate._instances, function(instance) { + var result = instance[methodName].apply(instance, args); + if (!angular.isDefined(returnValue)) { + returnValue = result; + } + }); + return returnValue; + }; + }); + }]; +} diff --git a/js/ext/angular/test/anchorScroll.html b/js/ext/angular/test/anchorScroll.html index ef8e910175..ce1823525c 100644 --- a/js/ext/angular/test/anchorScroll.html +++ b/js/ext/angular/test/anchorScroll.html @@ -35,7 +35,7 @@ diff --git a/js/ext/angular/test/controller/ionicScrollController.unit.js b/js/ext/angular/test/controller/ionicScrollController.unit.js index 91b9fff0f0..d9c9556a6f 100644 --- a/js/ext/angular/test/controller/ionicScrollController.unit.js +++ b/js/ext/angular/test/controller/ionicScrollController.unit.js @@ -27,17 +27,28 @@ describe('$ionicScroll Controller', function() { }); } - it('should set $scope.$ionicScrollController by default', function() { - setup() - expect(scope.$ionicScrollController).toBe(ctrl); - }); + it('should register with no handle', inject(function($ionicScrollDelegate) { + spyOn($ionicScrollDelegate, '_registerInstance'); + var el = setup(); + expect($ionicScrollDelegate._registerInstance) + .toHaveBeenCalledWith(ctrl, undefined); + })); - it('should set options.controllerBind to ctrl', function() { - setup({ - controllerBind: 'something' + it('should register with given handle and deregister on destroy', inject(function($ionicScrollDelegate) { + var deregisterSpy = jasmine.createSpy('deregister'); + spyOn($ionicScrollDelegate, '_registerInstance').andCallFake(function() { + return deregisterSpy; }); - expect(scope.something).toBe(ctrl); - }); + setup({ + delegateHandle: 'something' + }); + expect($ionicScrollDelegate._registerInstance) + .toHaveBeenCalledWith(ctrl, 'something'); + + expect(deregisterSpy).not.toHaveBeenCalled(); + scope.$destroy(); + expect(deregisterSpy).toHaveBeenCalled(); + })); it('should set bouncing to option if given', function() { spyOn(ionic.Platform, 'isAndroid'); diff --git a/js/ext/angular/test/directive/ionicContent.unit.js b/js/ext/angular/test/directive/ionicContent.unit.js index cde7db7bd2..7b7cc5b0cf 100644 --- a/js/ext/angular/test/directive/ionicContent.unit.js +++ b/js/ext/angular/test/directive/ionicContent.unit.js @@ -16,6 +16,12 @@ describe('Ionic Content directive', function() { expect(element.controller('$ionicScroll').element).toBe(element[0]); }); + it('passes delegateHandle attribute', function() { + var element = compile('')(scope); + expect(element.controller('$ionicScroll')._scrollViewOptions.delegateHandle) + .toBe('handleMe'); + }); + it('Has content class', function() { var element = compile('')(scope); expect(element.hasClass('scroll-content')).toBe(true); @@ -46,18 +52,12 @@ describe('Ionic Content directive', function() { it('Should set start x and y', function() { var element = compile('')(scope); scope.$apply(); - var scrollView = scope.$ionicScrollController.scrollView; + var scrollView = element.controller('$ionicScroll').scrollView; var vals = scrollView.getValues(); expect(vals.left).toBe(100); expect(vals.top).toBe(300); }); - it('should pass attr.controllerBind ionicScrollController', function() { - var element = compile('')(scope); - scope.$apply(); - expect(scope.scrolly).toBe(element.controller('$ionicScroll')); - }); - }); /* Tests #555 */ describe('Ionic Content Directive scoping', function() { diff --git a/js/ext/angular/test/directive/ionicScroll.unit.js b/js/ext/angular/test/directive/ionicScroll.unit.js index d235432b6c..f424635ee0 100644 --- a/js/ext/angular/test/directive/ionicScroll.unit.js +++ b/js/ext/angular/test/directive/ionicScroll.unit.js @@ -14,9 +14,10 @@ describe('Ionic Scroll Directive', function() { }); })); - it('Has $ionicScroll controller', function() { - element = compile('')(scope); - expect(element.controller('$ionicScroll').element).toBe(element[0]); + it('passes delegateHandle attribute', function() { + var element = compile('')(scope); + expect(element.controller('$ionicScroll')._scrollViewOptions.delegateHandle) + .toBe('handleThis'); }); it('has $onScroll (used by $ionicScrollController)', function() { @@ -44,16 +45,10 @@ describe('Ionic Scroll Directive', function() { expect(scrollElement.hasClass('padding')).toEqual(true); }); - it('should pass attr.controllerBind ionicScrollController', function() { - var element = compile('')(scope); - scope.$apply(); - expect(scope.scrolly).toBe(element.controller('$ionicScroll')); - }); - it('Should set start x and y', function() { element = compile('')(scope); scope.$apply(); - var scrollView = scope.$ionicScrollController.scrollView; + var scrollView = element.controller('$ionicScroll').scrollView; var vals = scrollView.getValues(); expect(vals.left).toBe(100); expect(vals.top).toBe(300); diff --git a/js/ext/angular/test/rememberScroll.html b/js/ext/angular/test/rememberScroll.html index f6ac6b2ea6..f6f4af14df 100644 --- a/js/ext/angular/test/rememberScroll.html +++ b/js/ext/angular/test/rememberScroll.html @@ -10,7 +10,7 @@ - +
{{i}} @@ -18,9 +18,11 @@