Skip to content

Commit

Permalink
feat($ionicScrollDelegate): rememberScrollPosition, scrollToRemembere…
Browse files Browse the repository at this point in the history
…dPosition

/**
 * @ngdoc method
 * @name $ionicScrollDelegate#rememberScrollPosition
 * @description
 *
 * 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.
 */

/**
 * @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.
 */
  • Loading branch information
ajoslin committed Mar 17, 2014
1 parent cc0a4ef commit 5a0efec
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 98 deletions.
26 changes: 20 additions & 6 deletions js/ext/angular/src/controller/ionicScrollController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -34,13 +34,27 @@ angular.module('ionic.ui.scroll')
var resize = angular.bind(scrollView, scrollView.resize);
$window.addEventListener('resize', resize);

$scope.$on('$destroy', function() {
$window.removeEventListener('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 view = $ionicViewService.getCurrentView();
if (view) {
view.rememberedScrollValues = scrollView.getValues();
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);
});

this.setRefresher = function(refresherScope, refresherElement) {
Expand Down
6 changes: 1 addition & 5 deletions js/ext/angular/src/directive/ionicContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
88 changes: 61 additions & 27 deletions js/ext/angular/src/service/delegates/ionicScrollDelegate.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,28 @@ 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
* ```html
* <ion-content ng-controller="MyController">
* <button class="button" ng-click="scrollToTop()">
* Scroll To Top
* </button>
* </ion-content>
* ```
* ```js
* function MyController($scope, $ionicScrollDelegate) {
* var delegate = $ionicScrollDelegate($scope);
* $scope.scrollToTop = function() {
* var delegate = $ionicScrollDelegate($scope);
* delegate.scrollTop();
* };
* }
* ```
* ```html
* <ion-content ng-controller="MyController">
* <button class="button" ng-click="scrollToTop()">
* Scroll To Top
* </button>
* </ion-content>
* ```
*/
.factory('$ionicScrollDelegate', ['$rootScope', '$timeout', '$location', '$ionicViewService', function($rootScope, $timeout, $location, $ionicViewService) {
//Exposed for testing
var rememberedScrollValues = ionicScrollDelegate._rememberedScrollValues = {};

function getScrollCtrl($scope) {
var ctrl;
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
*/
Expand Down Expand Up @@ -134,13 +135,36 @@ angular.module('ionic.ui.service.scrollDelegate', [])
/**
* @ngdoc method
* @name $ionicScrollDelegate#rememberScrollPosition
* @description Used on an instance of $ionicScrollDelegate.
* @description
*
* When this scroll area is destroyed, its last scroll position will be
* saved using the given id.
*
* 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.
* @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);
},

/**
Expand Down Expand Up @@ -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);
}
});
});
}
});
};

Expand Down
4 changes: 3 additions & 1 deletion js/ext/angular/src/service/ionicView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
21 changes: 21 additions & 0 deletions js/ext/angular/test/controller/ionicScrollController.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
57 changes: 0 additions & 57 deletions js/ext/angular/test/directive/ionicContent.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,63 +81,6 @@ describe('Ionic Content directive', function() {
expect(vals.top).toBe(300);
});

describe('save scroll', function() {

function compileWithParent() {
var parent = angular.element('<div>');
//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('<ion-content><br/><div>hello</div><br/></ion-content>');
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() {
Expand Down
51 changes: 51 additions & 0 deletions js/ext/angular/test/service/delegates/ionicScrollDelegate.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<ion-content></ion-content>')(scope);
Expand Down Expand Up @@ -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('<ion-content></ion-content>');
var del = $ionicScrollDelegate(scope);
expect(del.rememberScrollPosition).toThrow();
});

it('scrollToRememberedPosition should scroll if exists', function() {
var scope = rootScope.$new();
var el = compile('<ion-content></ion-content>')(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('<ion-content></ion-content>')(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'
});
});
});
}
});
Expand Down
Loading

0 comments on commit 5a0efec

Please sign in to comment.