diff --git a/js/ext/angular/src/controller/ionicScrollController.js b/js/ext/angular/src/controller/ionicScrollController.js index 1318628f92..fd0b066069 100644 --- a/js/ext/angular/src/controller/ionicScrollController.js +++ b/js/ext/angular/src/controller/ionicScrollController.js @@ -6,7 +6,7 @@ angular.module('ionic.ui.scroll') /** * @private */ -.controller('$ionicScroll', ['$scope', 'scrollViewOptions', '$timeout', '$ionicScrollDelegate', '$window', '$ionicViewService', function($scope, scrollViewOptions, $timeout, $ionicScrollDelegate, $window, $ionicViewService) { +.controller('$ionicScroll', ['$scope', 'scrollViewOptions', '$timeout', '$ionicScrollDelegate', '$window', function($scope, scrollViewOptions, $timeout, $ionicScrollDelegate, $window) { var self = this; @@ -34,13 +34,27 @@ angular.module('ionic.ui.scroll') var resize = angular.bind(scrollView, scrollView.resize); $window.addEventListener('resize', resize); + $scope.$on('$viewContentLoaded', function(e, historyData) { + if (e.defaultPrevented) { + return; + } + //only the top-most scroll area under a view should remember that view's + //scroll position + e.preventDefault(); + + var values = historyData && historyData.rememberedScrollValues; + if (values) { + $timeout(function() { + scrollView.scrollTo(+values.left || null, +values.top || null); + }, 0, false); + } + $scope.$on('$destroy', function() { + historyData && (historyData.rememberedScrollValues = scrollView.getValues()); + }); + }); + $scope.$on('$destroy', function() { $window.removeEventListener('resize', resize); - - var view = $ionicViewService.getCurrentView(); - if (view) { - view.rememberedScrollValues = scrollView.getValues(); - } }); this.setRefresher = function(refresherScope, refresherElement) { diff --git a/js/ext/angular/src/directive/ionicContent.js b/js/ext/angular/src/directive/ionicContent.js index 2b017ebfe4..5d1625ee0f 100644 --- a/js/ext/angular/src/directive/ionicContent.js +++ b/js/ext/angular/src/directive/ionicContent.js @@ -61,10 +61,9 @@ angular.module('ionic.ui.content', ['ionic.ui.service', 'ionic.ui.scroll']) .directive('ionContent', [ '$parse', '$timeout', - '$ionicScrollDelegate', '$controller', '$ionicBind', -function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) { +function($parse, $timeout, $controller, $ionicBind) { return { restrict: 'E', replace: true, @@ -135,9 +134,6 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) { }); //Publish scrollView to parent so children can access it scrollView = $scope.$parent.scrollView = scrollCtrl.scrollView; - - var delegate = $ionicScrollDelegate($scope); - delegate.rememberScrollPosition(); } transclude($scope, function(clone) { diff --git a/js/ext/angular/src/service/delegates/ionicScrollDelegate.js b/js/ext/angular/src/service/delegates/ionicScrollDelegate.js index 7d2cd8747c..f55f6de847 100644 --- a/js/ext/angular/src/service/delegates/ionicScrollDelegate.js +++ b/js/ext/angular/src/service/delegates/ionicScrollDelegate.js @@ -14,10 +14,17 @@ angular.module('ionic.ui.service.scrollDelegate', []) * directive). * * Inject it into a controller, create a new instance based upon the current scope, - * and its methods will send messages to the nearest scrollView and all of - * its children. + * and its methods will send messages to the nearest scrollView and its children. * * @usage + * ```js + * function MyController($scope, $ionicScrollDelegate) { + * var delegate = $ionicScrollDelegate($scope); + * $scope.scrollToTop = function() { + * delegate.scrollTop(); + * }; + * } + * ``` * ```html * * * * ``` - * ```js - * function MyController($scope, $ionicScrollDelegate) { - * $scope.scrollToTop = function() { - * var delegate = $ionicScrollDelegate($scope); - * delegate.scrollTop(); - * }; - * } - * ``` */ .factory('$ionicScrollDelegate', ['$rootScope', '$timeout', '$location', '$ionicViewService', function($rootScope, $timeout, $location, $ionicViewService) { + //Exposed for testing + var rememberedScrollValues = ionicScrollDelegate._rememberedScrollValues = {}; function getScrollCtrl($scope) { var ctrl; @@ -55,7 +56,7 @@ angular.module('ionic.ui.service.scrollDelegate', []) /** * @ngdoc method * @name $ionicScrollDelegate#scrollTop - * @description Used on an instance of $ionicScrollDelegate. + * @description * @param {boolean=} shouldAnimate Whether the scroll should animate. */ scrollTop: function(animate) { @@ -64,7 +65,7 @@ angular.module('ionic.ui.service.scrollDelegate', []) /** * @ngdoc method * @name $ionicScrollDelegate#scrollBottom - * @description Used on an instance of $ionicScrollDelegate. + * @description * @param {boolean=} shouldAnimate Whether the scroll should animate. */ scrollBottom: function(animate) { @@ -73,7 +74,7 @@ angular.module('ionic.ui.service.scrollDelegate', []) /** * @ngdoc method * @name $ionicScrollDelegate#scroll - * @description Used on an instance of $ionicScrollDelegate. + * @description * @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. @@ -84,7 +85,7 @@ angular.module('ionic.ui.service.scrollDelegate', []) /** * @ngdoc method * @name $ionicScrollDelegate#anchorScroll - * @description Used on an instance of $ionicScrollDelegate. + * @description * * Tell the scrollView to scroll to the element with an id * matching window.location.hash. @@ -99,7 +100,7 @@ angular.module('ionic.ui.service.scrollDelegate', []) /** * @ngdoc method * @name $ionicScrollDelegate#resize - * @description Used on an instance of $ionicScrollDelegate. + * @description * * Tell the scrollView to recalculate the size of its container. */ @@ -134,13 +135,36 @@ angular.module('ionic.ui.service.scrollDelegate', []) /** * @ngdoc method * @name $ionicScrollDelegate#rememberScrollPosition - * @description Used on an instance of $ionicScrollDelegate. + * @description * - * If this scroll area is associated with a view in the history, - * load the last scroll position from the last time this view was shown. + * When this scroll area is destroyed, its last scroll position will be + * saved using the given id. + * + * @param {string} id The identifier for this saved scroll position. */ - rememberScrollPosition: function(animate) { - scrollScope.$broadcast('scroll.rememberPosition', !!animate); + rememberScrollPosition: function(id) { + if (!id) { + throw new Error("Must supply a unique id!"); + } + scrollScope.$broadcast('scroll.rememberPosition', id); + }, + + /** + * @ngdoc method + * @name $ionicScrollDelegate#scrollToRememberedPosition + * @description + * + * If a scroll position was remembered using the given id, loads the + * remembered scroll position and scrolls there. + * + * @param {string} id The identifier for this saved scroll position. + * @param {boolean=} shouldAnimate Whether to animate the scroll. + */ + scrollToRememberedPosition: function(id, animate) { + if (!id) { + throw new Error("Must supply a unique id!"); + } + scrollScope.$broadcast('scroll.scrollToRememberedPosition', id, !!animate); }, /** @@ -216,14 +240,24 @@ angular.module('ionic.ui.service.scrollDelegate', []) } }); }); - $scope.$on('scroll.rememberPosition', function(e, animate) { - scrollViewResize().then(function() { - var view = $ionicViewService.getCurrentView(); - var values = view && view.rememberedScrollValues; - if (view && values) { + + var rememberScrollId; + $scope.$on('scroll.rememberPosition', function(e, id) { + rememberScrollId = id; + }); + $scope.$on('$destroy', function() { + if (rememberScrollId) { + rememberedScrollValues[rememberScrollId] = scrollView.getValues(); + } + }); + + $scope.$on('scroll.scrollToRememberedPosition', function(e, id, animate) { + var values = rememberedScrollValues[id]; + if (values) { + scrollViewResize().then(function() { scrollView.scrollTo(+values.left || null, +values.top || null, animate); - } - }); + }); + } }); }; diff --git a/js/ext/angular/src/service/ionicView.js b/js/ext/angular/src/service/ionicView.js index a5d0fb7de0..e1fabea1c6 100644 --- a/js/ext/angular/src/service/ionicView.js +++ b/js/ext/angular/src/service/ionicView.js @@ -157,7 +157,9 @@ angular.module('ionic.service.view', ['ui.router', 'ionic.service.platform']) // they went back one, set the old current view as a forward view rsp.viewId = backView.viewId; rsp.navAction = 'moveBack'; - currentView.scrollValues = {}; //when going back, erase scrollValues + rsp.viewId = backView.viewId; + //when going back, erase scrollValues + currentView.rememberedScrollValues = {}; if(backView.historyId === currentView.historyId) { // went back in the same history rsp.navDirection = 'back'; diff --git a/js/ext/angular/test/controller/ionicScrollController.unit.js b/js/ext/angular/test/controller/ionicScrollController.unit.js index eee1f0e60e..3e0682319c 100644 --- a/js/ext/angular/test/controller/ionicScrollController.unit.js +++ b/js/ext/angular/test/controller/ionicScrollController.unit.js @@ -63,6 +63,27 @@ describe('$ionicScroll Controller', function() { expect(ctrl.scrollView.resize).toHaveBeenCalled(); }); + it('should remember scroll position on $viewContentLoaded event', function() { + var historyData = { rememberedScrollValues: { left: 1, top: 2 } }; + setup(); + spyOn(ctrl.scrollView, 'scrollTo'); + scope.$broadcast('$viewContentLoaded', historyData); + timeout.flush(); + expect(ctrl.scrollView.scrollTo).toHaveBeenCalledWith(1, 2); + + spyOn(ctrl.scrollView, 'getValues').andCallFake(function() { + return { + left: 33, + top: 44 + }; + }); + scope.$broadcast('$destroy'); + expect(historyData.rememberedScrollValues).toEqual({ + left: 33, + top: 44 + }); + }); + it('should unbind window event listener on scope destroy', function() { spyOn(window, 'removeEventListener'); spyOn(window, 'addEventListener'); diff --git a/js/ext/angular/test/directive/ionicContent.unit.js b/js/ext/angular/test/directive/ionicContent.unit.js index 41f18159db..b8212410da 100644 --- a/js/ext/angular/test/directive/ionicContent.unit.js +++ b/js/ext/angular/test/directive/ionicContent.unit.js @@ -81,63 +81,6 @@ describe('Ionic Content directive', function() { expect(vals.top).toBe(300); }); - describe('save scroll', function() { - - function compileWithParent() { - var parent = angular.element('
'); - //Make a phony element that tells the world it's a navView when in reality it's just a div - parent.data('$navViewController', true); - parent.append('
hello

'); - compile(parent)(scope); - scope.$apply(); - - /* Mock setting and getting scroll because we don't have time for the dom to load */ - var scrollValues = {}; - spyOn(scope.scrollView, 'scrollTo').andCallFake(function(left, top, a, zoom) { - scrollValues = { - left: left || 0, - top: top || 0, - zoom: zoom || 1 - }; - }); - spyOn(scope.scrollView, 'getValues').andCallFake(function() { - return scrollValues; - }); - } - - it('should set x and y with historyData.scrollValues passed in through $viewContentLoaded', function() { - compileWithParent(); - var scrollValues = { top: 40, left: -20, zoom: 3 }; - scope.$broadcast('$viewContentLoaded', { - scrollValues: scrollValues - }); - timeout.flush(); - expect(scope.scrollView.scrollTo.mostRecentCall.args).toEqual([-20, 40]); - }); - - it('should set null with historyData.scrollValues not valid', function() { - compileWithParent(); - var scrollValues = { left: 'bar', top: 'foo' }; - scope.$broadcast('$viewContentLoaded', { scrollValues: scrollValues }); - timeout.flush(); - expect(scope.scrollView.scrollTo.mostRecentCall.args).toEqual([null, null]); - }); - - it('should save scroll on the historyData passed in on $destroy', function() { - compileWithParent(); - var historyData = {}; - scope.$broadcast('$viewContentLoaded', historyData); - timeout.flush(); - scope.scrollView.scrollTo(null, 9, false); - expect(historyData.scrollValues).toBeUndefined(); //sanity test - scope.$destroy(); - expect(historyData.scrollValues).toEqual({ - left: 0, - top: 9, - zoom: 1 - }); - }); - }); }); /* Tests #555 */ describe('Ionic Content Directive scoping', function() { diff --git a/js/ext/angular/test/service/delegates/ionicScrollDelegate.unit.js b/js/ext/angular/test/service/delegates/ionicScrollDelegate.unit.js index 461af502ac..ca7c7e0560 100644 --- a/js/ext/angular/test/service/delegates/ionicScrollDelegate.unit.js +++ b/js/ext/angular/test/service/delegates/ionicScrollDelegate.unit.js @@ -11,6 +11,13 @@ describe('Ionic ScrollDelegate Service', function() { compile = $compile; })); + it('should just return rootScope if no scrollCtrl', inject(function($rootScope) { + expect(function() { + $ionicScrollDelegate($rootScope); + }).not.toThrow(); + expect($ionicScrollDelegate($rootScope).getScrollView()).toBeFalsy(); + })); + it('Should get scroll view', function() { var scope = rootScope.$new(); var el = compile('')(scope); @@ -137,6 +144,50 @@ describe('Ionic ScrollDelegate Service', function() { expect(sv.resize).toHaveBeenCalled(); expect(sv.scrollTo.mostRecentCall.args).toEqual([2, 3, animate]); }); + + it('should throw error on rememberScroll if no id', function() { + var scope = rootScope.$new(); + var el = compile(''); + var del = $ionicScrollDelegate(scope); + expect(del.rememberScrollPosition).toThrow(); + }); + + it('scrollToRememberedPosition should scroll if exists', function() { + var scope = rootScope.$new(); + var el = compile('')(scope); + var del = $ionicScrollDelegate(scope); + var sv = del.getScrollView(); + scope.$apply(); + $ionicScrollDelegate._rememberedScrollValues['1'] = { + left: 3, + top: 4 + }; + del.scrollToRememberedPosition('1', animate); + spyOn(sv, 'scrollTo'); + timeout.flush(); + expect(sv.scrollTo).toHaveBeenCalledWith(3, 4, animate); + }); + + it('should save on destroy for rememberScrollPosition', function() { + var scope = rootScope.$new(); + var el = compile('')(scope); + var del = $ionicScrollDelegate(scope); + var sv = del.getScrollView(); + scope.$apply(); + $ionicScrollDelegate._rememberedScrollValues['1'] = { + left: -1, + top: -1 + }; + del.rememberScrollPosition('1', animate); + spyOn(sv, 'getValues').andCallFake(function() { + return { foo: 'bar' }; + }); + scope.$destroy(); + expect(sv.getValues).toHaveBeenCalled(); + expect($ionicScrollDelegate._rememberedScrollValues['1']).toEqual({ + foo: 'bar' + }); + }); }); } }); diff --git a/js/ext/angular/test/service/ionicViewService.unit.js b/js/ext/angular/test/service/ionicViewService.unit.js index 56521ab418..0395085a1f 100644 --- a/js/ext/angular/test/service/ionicViewService.unit.js +++ b/js/ext/angular/test/service/ionicViewService.unit.js @@ -141,7 +141,7 @@ describe('Ionic View Service', function() { registerData = viewService.register({}); currentView = viewService.getCurrentView(); //Set test value for remembered scroll - currentView.scrollValues = 'foo'; + currentView.rememberedScrollValues = 'foo'; backView = viewService.getBackView(); forwardView = viewService.getForwardView(); expect(backView.stateName).toEqual('about'); @@ -159,7 +159,7 @@ describe('Ionic View Service', function() { currentView = viewService.getCurrentView(); backView = viewService.getBackView(); forwardView = viewService.getForwardView(); - expect(forwardView.scrollValues).toEqual({}); + expect(forwardView.rememberedScrollValues).toEqual({}); expect(currentView.stateName).toEqual('about'); expect(currentView.backViewId).toEqual(backView.viewId); expect(currentView.forwardViewId).toEqual(forwardView.viewId);