feat($ionicScrollDelegate): add scrollTo(left,top,animate) to delegate

Also moves $ionicScrollDelegate.register to $ionicScroll controller,
and makes `<scroll>` directive be registered with $ionicScrollDelegate.
This commit is contained in:
Andy Joslin
2014-02-11 13:32:03 -05:00
parent 801d2d7b77
commit c119498d1b
10 changed files with 158 additions and 85 deletions

View File

@@ -18,7 +18,7 @@ module.exports = function(config) {
'config/lib/js/angular/angular.js',
'config/lib/js/angular/angular-animate.js',
'config/lib/js/angular/angular-mocks.js',
'config/lib/js/angular-ui/angular-ui-router.js',
'config/lib/js/angular-ui/angular-ui-router.js'
]
.concat(buildConfig.ionicFiles)
.concat(buildConfig.angularIonicFiles)

View File

@@ -3,8 +3,8 @@
angular.module('ionic.ui.scroll')
.controller('$ionicScroll', ['$scope', 'scrollViewOptions', '$timeout',
function($scope, scrollViewOptions, $timeout) {
.controller('$ionicScroll', ['$scope', 'scrollViewOptions', '$timeout', '$ionicScrollDelegate',
function($scope, scrollViewOptions, $timeout, $ionicScrollDelegate) {
scrollViewOptions.bouncing = angular.isDefined(scrollViewOptions.bouncing) ?
scrollViewOptions.bouncing :
@@ -15,11 +15,14 @@ angular.module('ionic.ui.scroll')
var element = this.element = scrollViewOptions.el;
var scrollView = this.scrollView = new ionic.views.Scroll(scrollViewOptions);
this.$element = angular.element(element);
var $element = this.$element = angular.element(element);
//Attach self to element as a controller so other directives can require this controller
//through `require: '$ionicScroll'
this.$element.data('$$ionicScrollController', this);
$element.data('$$ionicScrollController', this);
//Register delegate for event handling
$ionicScrollDelegate.register($scope, $element, scrollView);
$timeout(function() {
scrollView.run();

View File

@@ -120,9 +120,6 @@ angular.module('ionic.ui.content', ['ionic.ui.service', 'ionic.ui.scroll'])
};
}
// Register for scroll delegate event handling
$ionicScrollDelegate.register($scope, $element);
// Check if this supports infinite scrolling and listen for scroll events
// to trigger the infinite scrolling
// TODO(ajoslin): move functionality out of this function and make testable

View File

@@ -33,8 +33,6 @@ angular.module('ionic.ui.scroll', [])
function prelink($scope, $element, $attr) {
var scrollView, scrollCtrl, sc = $element[0].children[0];
// Create the internal scroll div
sc.className = 'scroll';
if(attr.padding == "true") {
sc.classList.add('padding');
}
@@ -63,25 +61,6 @@ angular.module('ionic.ui.scroll', [])
scrollViewOptions: scrollViewOptions
});
scrollView = $scope.$parent.scrollView = scrollCtrl.scrollView;
$element.bind('scroll', function(e) {
$scope.onScroll({
event: e,
scrollTop: e.detail ? e.detail.scrollTop : e.originalEvent ? e.originalEvent.detail.scrollTop : 0,
scrollLeft: e.detail ? e.detail.scrollLeft: e.originalEvent ? e.originalEvent.detail.scrollLeft : 0
});
});
$scope.$parent.$on('scroll.resize', function(e) {
// Run the resize after this digest
$timeout(function() {
scrollView && scrollView.resize();
});
});
$scope.$parent.$on('scroll.refreshComplete', function(e) {
scrollView && scrollView.finishPullToRefresh();
});
}
}
};

View File

@@ -14,6 +14,9 @@ angular.module('ionic.ui.service.scrollDelegate', [])
scrollBottom: function(animate) {
$rootScope.$broadcast('scroll.scrollBottom', animate);
},
scrollTo: function(left, top, animate) {
$rootScope.$broadcast('scroll.scrollTo', left, top, animate);
},
resize: function() {
$rootScope.$broadcast('scroll.resize');
},
@@ -45,18 +48,14 @@ angular.module('ionic.ui.service.scrollDelegate', [])
getScrollView: function($scope) {
return $scope.scrollView;
},
/**
* Register a scope for scroll event handling.
* Register a scope and scroll view for scroll event handling.
* $scope {Scope} the scope to register and listen for events
*/
register: function($scope, $element) {
//Get scroll controller from parent
var scrollCtrl = $element.controller('$ionicScroll');
if (!scrollCtrl) {
return;
}
var scrollView = scrollCtrl.scrollView;
var scrollEl = scrollCtrl.element;
register: function($scope, $element, scrollView) {
var scrollEl = $element[0];
function scrollViewResize() {
// Run the resize after this digest
@@ -93,14 +92,14 @@ angular.module('ionic.ui.service.scrollDelegate', [])
});
});
/**
* Called to scroll to the top of the content
*
* @param animate {boolean} whether to animate or just snap
*/
$scope.$parent.$on('scroll.scrollTo', function(e, left, top, animate) {
scrollViewResize().then(function() {
scrollView.scrollTo(left, top, !!animate);
});
});
$scope.$parent.$on('scroll.scrollTop', function(e, animate) {
scrollViewResize().then(function() {
scrollView.scrollTo(0, 0, animate === false ? false : true);
scrollView.scrollTo(0, 0, !!animate);
});
});
$scope.$parent.$on('scroll.scrollBottom', function(e, animate) {
@@ -108,7 +107,7 @@ angular.module('ionic.ui.service.scrollDelegate', [])
var sv = scrollView;
if (sv) {
var max = sv.getScrollMax();
sv.scrollTo(0, max.top, animate === false ? false : true);
sv.scrollTo(max.left, max.top, !!animate);
}
});
});

View File

@@ -1,6 +1,6 @@
describe('$ionicScroll Controller', function() {
beforeEach(module('ionic.ui.scroll'));
beforeEach(module('ionic'));
var scope, ctrl, timeout;
function setup(options) {
@@ -41,6 +41,12 @@ describe('$ionicScroll Controller', function() {
expect(ctrl.scrollView.run).toHaveBeenCalled();
});
it('should register with $ionicScrollDelegate', inject(function($ionicScrollDelegate) {
spyOn($ionicScrollDelegate, 'register');
setup();
expect($ionicScrollDelegate.register).toHaveBeenCalledWith(scope, ctrl.$element, ctrl.scrollView);
}));
it('should not setup if no child .scroll-refresher', function() {
setup();
timeout.flush();
@@ -70,7 +76,6 @@ describe('$ionicScroll Controller', function() {
});
scope.onRefresh = jasmine.createSpy('onRefresh');
scope.$parent.$broadcast = jasmine.createSpy('$broadcast');
timeout.flush();
var refresher = ctrl.refresher;
@@ -87,13 +92,11 @@ describe('$ionicScroll Controller', function() {
expect(refresher.classList.contains('refreshing')).toBe(false);
expect(scope.onRefresh).not.toHaveBeenCalled();
expect(scope.$parent.$broadcast).not.toHaveBeenCalledWith('scroll.onRefresh');
doneCb();
expect(refresher.classList.contains('active')).toBe(false);
expect(refresher.classList.contains('refreshing')).toBe(true);
expect(scope.onRefresh).toHaveBeenCalled();
expect(scope.$parent.$broadcast).toHaveBeenCalledWith('scroll.onRefresh');
});
});

View File

@@ -1,7 +1,7 @@
describe('Ionic Content directive', function() {
var compile, element, scope;
beforeEach(module('ionic.ui.content'));
beforeEach(module('ionic'));
beforeEach(inject(function($compile, $rootScope, $timeout, $window) {
compile = $compile;
@@ -11,6 +11,11 @@ describe('Ionic Content directive', function() {
ionic.Platform.setPlatform('Android');
}));
it('Has $ionicScroll controller', function() {
element = compile('<content></content>')(scope);
expect(element.controller('$ionicScroll').element).toBe(element[0]);
});
it('Has content class', function() {
element = compile('<content></content>')(scope);
expect(element.hasClass('scroll-content')).toBe(true);

View File

@@ -0,0 +1,67 @@
describe('Ionic Scroll Directive', function() {
var compile, element, scope;
beforeEach(module('ionic'));
beforeEach(inject(function($compile, $rootScope, $timeout, $window) {
compile = $compile;
scope = $rootScope;
timeout = $timeout;
window = $window;
ionic.Platform.setPlatform('Android');
}));
it('Has $ionicScroll controller', function() {
element = compile('<scroll></scroll>')(scope);
expect(element.controller('$ionicScroll').element).toBe(element[0]);
});
it('Has scroll-view class', function() {
element = compile('<scroll></scroll>')(scope);
expect(element.hasClass('scroll-view')).toBe(true);
});
it('should add padding classname', function() {
element = compile('<scroll padding="true"></scroll>')(scope);
expect(element.children().eq(0).hasClass('padding')).toEqual(true);
var scrollElement = element.find('.scroll');
expect(scrollElement.hasClass('padding')).toEqual(true);
});
it('Enables bouncing by default', function() {
ionic.Platform.setPlatform('iPhone');
element = compile('<content has-header="true"></scroll>')(scope);
scope.$apply();
var newScope = element.isolateScope();
var scrollView = scope.scrollView;
expect(scrollView.options.bouncing).toBe(true);
});
it('Disables bouncing when has-bouncing = false', function() {
ionic.Platform.setPlatform('iPhone');
element = compile('<content has-header="true" has-bouncing="false"></scroll>')(scope);
scope.$apply();
var newScope = element.isolateScope();
var scrollView = scope.scrollView;
expect(scrollView.options.bouncing).toBe(false);
});
it('Disables bouncing by default on Android', function() {
ionic.Platform.setPlatform('Android');
element = compile('<content has-header="true"></scroll>')(scope);
scope.$apply();
var newScope = element.isolateScope();
var scrollView = scope.scrollView;
expect(scrollView.options.bouncing).toBe(false);
});
it('Should set start x and y', function() {
element = compile('<content start-x="100" start-y="300" has-header="true"></scroll>')(scope);
scope.$apply();
var newScope = element.isolateScope();
var scrollView = scope.scrollView;
var vals = scrollView.getValues();
expect(vals.left).toBe(100);
expect(vals.top).toBe(300);
});
});

View File

@@ -52,20 +52,24 @@
<content has-header="true" scroll="false">
<h3>Hourly Forecast</h3>
<scroll direction="x" style="width: 100%">
<scroll direction="x" style="width: 100%; height: 20%;">
<div class="hours">
<div ng-repeat="hour in hours">
<div class="icons">
<i class="icon ion-ios7-sunny-outline"></i>
<i class="icon ion-cloud"></i>
</div>
</a>
<div>
{{hour.temp}} &deg;F
</div>
</div>
</div>
</scroll>
<scroll direction="y" style="height: 400px; width: 300px">
<a class="button" ng-click="scrollTo()">
Scroll to 100
</a>
<scroll direction="y" style="height: 400px; width: 300px; background: red;">
<div style="width: 100px; height: 4000px; background: url('tree_bark.png') repeat"></div>
</scroll>
</content>
@@ -83,7 +87,7 @@
}
})
.controller('ThisCtrl', function($scope) {
.controller('ThisCtrl', function($scope, $ionicScrollDelegate) {
var header = document.getElementById('header');
var content = document.getElementById('container');
@@ -97,6 +101,10 @@
$scope.onScrollComplete = function(event, scrollTop, scrollLeft) {
console.log('Scroll complete', scrollTop, scrollLeft);
}
$scope.scrollTo = function() {
console.log('scrollTo');
$ionicScrollDelegate.scrollTo(0, 100, true);
};
$scope.onScroll = function(event, scrollTop, scrollLeft) {
/*
if(scrollTop > startTop) {

View File

@@ -11,15 +11,6 @@ describe('Ionic ScrollDelegate Service', function() {
compile = $compile;
}));
it('Should register', function() {
spyOn(del, 'register');
var scope = rootScope.$new();
var el = compile('<content></content>')(scope);
expect(del.register).toHaveBeenCalled();
});
it('Should get scroll view', function() {
var scope = rootScope.$new();
var el = compile('<content></content>')(scope);
@@ -33,43 +24,64 @@ describe('Ionic ScrollDelegate Service', function() {
var sv = del.getScrollView(scope);
spyOn(sv, 'resize');
spyOn(sv, 'scrollTo');
del.resize();
timeout.flush();
expect(sv.resize).toHaveBeenCalled();
});
it('Should resize & scroll top', function() {
var scope = rootScope.$new();
var el = compile('<content start-y="100"></content>')(scope);
testWithAnimate(true);
testWithAnimate(false);
function testWithAnimate(animate) {
describe('with animate='+animate, function() {
it('should resize & scroll top', function() {
var scope = rootScope.$new();
var el = compile('<content start-y="100"></content>')(scope);
var sv = del.getScrollView(scope);
spyOn(sv, 'resize');
var sv = del.getScrollView(scope);
spyOn(sv, 'resize');
spyOn(sv, 'scrollTo');
del.scrollTop(animate);
expect(sv.getValues().top).toBe(100);
timeout.flush();
expect(sv.resize).toHaveBeenCalled();
expect(sv.scrollTo.mostRecentCall.args).toEqual([0, 0, animate]);
});
del.scrollTop(false);
timeout.flush();
expect(sv.resize).toHaveBeenCalled();
it('should resize & scroll bottom', function() {
var scope = rootScope.$new();
var el = compile('<content start-y="100"><br/><br/></content>')(scope);
expect(sv.getValues().top).toBe(0);
});
var sv = del.getScrollView(scope);
spyOn(sv, 'getScrollMax').andCallFake(function() {
return { left: 10, top: 11 };
});
spyOn(sv, 'resize');
spyOn(sv, 'scrollTo');
var max = sv.getScrollMax();
del.scrollBottom(animate);
it('Should resize & scroll bottom', function() {
var scope = rootScope.$new();
var el = compile('<content start-y="100"></content>')(scope);
timeout.flush();
expect(sv.resize).toHaveBeenCalled();
expect(sv.scrollTo.mostRecentCall.args).toEqual([max.left, max.top, animate]);
});
var sv = del.getScrollView(scope);
spyOn(sv, 'resize');
it('should resize & scrollTo', function() {
var scope = rootScope.$new();
var el = compile('<content start-y="100"><br/><br/></content>')(scope);
expect(sv.getValues().top).toBe(100);
var sv = del.getScrollView(scope);
spyOn(sv, 'scrollTo');
spyOn(sv, 'resize');
del.scrollTo(2, 3, animate);
del.scrollBottom(false);
timeout.flush();
expect(sv.resize).toHaveBeenCalled();
expect(sv.getValues().top).toBe(sv.getScrollMax().top);
});
timeout.flush();
expect(sv.resize).toHaveBeenCalled();
expect(sv.scrollTo.mostRecentCall.args).toEqual([2, 3, animate]);
});
});
}
it('should finish refreshing', function() {
var scope = rootScope.$new();