diff --git a/js/ext/angular/src/controller/ionicScrollController.js b/js/ext/angular/src/controller/ionicScrollController.js index c7b167147d..3c48ae3568 100644 --- a/js/ext/angular/src/controller/ionicScrollController.js +++ b/js/ext/angular/src/controller/ionicScrollController.js @@ -36,28 +36,36 @@ angular.module('ionic.ui.scroll') scrollView.resize(); } + this.setRefresher = function(refresherScope, refresherElement) { + var refresher = this.refresher = refresherElement; + var refresherHeight = self.refresher.clientHeight || 0; + scrollView.activatePullToRefresh(refresherHeight, function() { + refresher.classList.add('active'); + refresherScope.$onRefreshOpening(); + }, function() { + refresher.classList.remove('refreshing'); + refresher.classList.remove('active'); + }, function() { + refresher.classList.add('refreshing'); + refresherScope.$onRefresh(); + }); + }; + $timeout(function() { scrollView.run(); - - self.refresher = element.querySelector('.scroll-refresher'); - - // Activate pull-to-refresh - if(self.refresher) { - var refresherHeight = self.refresher.clientHeight || 0; - scrollView.activatePullToRefresh(refresherHeight, function() { - self.refresher.classList.add('active'); - $scope.$onRefreshOpening && $scope.$onRefreshOpening(); - }, function() { - self.refresher.classList.remove('refreshing'); - self.refresher.classList.remove('active'); - }, function() { - self.refresher.classList.add('refreshing'); - $scope.$onRefresh && $scope.$onRefresh(); - $scope.$parent.$broadcast('scroll.onRefresh'); - }); - } }); - }]); })(); + +var popups = []; +function showPopup() { + var newPopupDeferred = $q.defer(); + $q.all(popups).then(showThisPopup); + + popups.push(newPopupDeferred); + + function showThisPopup() { + popups.splice(popups.indexOf(newPopupDeferred.promise), 1); + } +} diff --git a/js/ext/angular/src/directive/ionicContent.js b/js/ext/angular/src/directive/ionicContent.js index fc45c4c87d..9aa162e985 100644 --- a/js/ext/angular/src/directive/ionicContent.js +++ b/js/ext/angular/src/directive/ionicContent.js @@ -86,15 +86,10 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) { $ionicBind($scope, $attr, { //Use $ to stop onRefresh from recursively calling itself - //DEPRECATED, use $onRefresh: '&onRefresh', $onRefreshOpening: '&onRefreshOpening', $onScroll: '&onScroll', $onScrollComplete: '&onScrollComplete', - //DEPRECATED, use - $onInfiniteScroll: '&onInfiniteScroll', - refreshComplete: '=', - infiniteScrollDistance: '@', hasBouncing: '@', scroll: '@', padding: '@', @@ -154,8 +149,6 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) { if(attr.refreshComplete) { $scope.refreshComplete = function() { if($scope.scrollView) { - scrollCtrl.refresher && scrollCtrl.refresher.classList.remove('active'); - scrollView.finishPullToRefresh(); $scope.$parent.$broadcast('scroll.onRefreshComplete'); } }; @@ -175,24 +168,99 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) { }; }]) -.directive('ionRefresher', function() { +/** + * @ngdoc directive + * @name ionRefresher + * @module ionic + * @restrict E + * @parent ionContent, ionScroll + * @description + * Allows you to add pull-to-refresh to a scrollView. + * + * Place it as the first child of your {@link ionic.directive:ionContent} or + * {@link ionic.directive:ionScroll} element. + * + * When refreshing is complete, $broadcast the 'scroll.refreshComplete' event + * from your controller. + * + * @param {expression=} on-refresh Called when the user pulls down enough and lets go + * of the refresher. + * @param {expression=} on-pulling Called when the user starts to pull down + * on the refresher. + * @param {string=} pulling-icon The icon to display while the user is pulling down. + * Default: 'ion-arrow-down-c'. + * @param {string=} pulling-text The text to display while the user is pulling down. + * @param {string=} refreshing-icon The icon to display after user lets go of the + * refresher. + * @param {string=} refreshing-text The text to display after the user lets go of + * the refresher. + * + * @usage + * ```html + * + * + * + * + * + * + * + * ``` + * ```js + * angular.module('testApp', ['ionic']) + * .controller('MyController', function($scope, $http) { + * $scope.items = [1,2,3]; + * $scope.doRefresh = function() { + * $http.get('/new-items').success(function(newItems) { + * $scope.items = newItems; + * //Stop the ion-refresher from spinning + * $scope.$broadcast('scroll.refreshComplete'); + * }); + * }; + * }); + * ``` + */ +.directive('ionRefresher', ['$ionicBind', function($ionicBind) { return { restrict: 'E', replace: true, - require: ['^?ionContent', '^?ionList'], - template: '
', - scope: true - }; -}) + require: '^$ionicScroll', + template: + '
' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '
', + compile: function($element, $attrs) { + if (angular.isUndefined($attrs.pullingIcon)) { + $attrs.$set('pullingIcon', 'ion-arrow-down-c'); + } + if (angular.isUndefined($attrs.refreshingIcon)) { + $attrs.$set('refreshingIcon', 'ion-loading-d'); + } + return function($scope, $element, $attrs, scrollCtrl) { + $ionicBind($scope, $attrs, { + pullingIcon: '@', + pullingText: '@', + refreshingIcon: '@', + refreshingText: '@', + $onRefresh: '&onRefresh', + $onRefreshOpening: '&onRefreshOpening' + }); -.directive('ionScrollRefresher', function() { - return { - restrict: 'E', - replace: true, - transclude: true, - template: '
' + scrollCtrl.setRefresher($scope, $element[0]); + $scope.$on('scroll.refreshComplete', function() { + $element[0].classList.remove('active'); + scrollCtrl.scrollView.finishPullToRefresh(); + }); + }; + } }; -}) +}]) /** * @ngdoc directive @@ -261,10 +329,7 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) { this.isLoading = false; this.scrollView = null; //given by link function this.getMaxScroll = function() { - var dist = $attrs.distance || - //deprecated: allow infiniteScrollDistance from ionContent - $scope.infiniteScrollDistance || - '1%'; + var dist = $attrs.distance || '1%'; return dist.indexOf('%') > -1 ? this.scrollView.getScrollMax().top * (1 - parseInt(dist,10) / 100) : this.scrollView.getScrollMax().top - parseInt(dist, 10); @@ -292,9 +357,7 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) { scrollView.getValues().top >= infiniteScrollCtrl.getMaxScroll()) { $element[0].classList.add('active'); infiniteScrollCtrl.isLoading = true; - - //deprecated: allow $onInfiniteScroll from parent - $scope.$apply($attrs.onInfinite || $scope.$onInfiniteScroll); + $scope.$parent.$apply($attrs.onInfinite || ''); } })); } diff --git a/js/ext/angular/test/controller/ionicScrollController.unit.js b/js/ext/angular/test/controller/ionicScrollController.unit.js index e6106e8129..ee55a82cbb 100644 --- a/js/ext/angular/test/controller/ionicScrollController.unit.js +++ b/js/ext/angular/test/controller/ionicScrollController.unit.js @@ -69,7 +69,7 @@ describe('$ionicScroll Controller', function() { expect($ionicScrollDelegate.register).toHaveBeenCalledWith(scope, ctrl.$element, ctrl.scrollView); })); - it('should not setup if no child .scroll-refresher', function() { + it('should not activatePullToRefresh if setRefresher is not called', function() { setup(); timeout.flush(); expect(ctrl.refresher).toBeFalsy(); @@ -77,16 +77,7 @@ describe('$ionicScroll Controller', function() { expect(ctrl.scrollView.activatePullToRefresh).not.toHaveBeenCalled(); }); - it('should not setup ctrl.refresher until after timeout', function() { - setup({ - el: angular.element('
')[0] - }); - expect(ctrl.refresher).toBeUndefined(); - timeout.flush(); - expect(ctrl.refresher).toBe(ctrl.element.children[0]); - }); - - it('should work with .scroll-refresher child and proper refresher', function() { + it('should activatePullToRefresh and work when setRefresher', function() { var startCb, refreshingCb, doneCb, refresherEl; setup({ el: angular.element('
')[0] @@ -96,6 +87,9 @@ describe('$ionicScroll Controller', function() { refreshingCb = refreshing; doneCb = done; }); + ctrl.setRefresher(scope, ctrl.element); + + var scrollOnRefreshSpy = jasmine.createSpy('scroll.onRefresh'); scope.$onRefresh = jasmine.createSpy('onRefresh'); scope.$onRefreshOpening = jasmine.createSpy('onRefreshOpening'); diff --git a/js/ext/angular/test/directive/ionicInfiniteScroll.unit.js b/js/ext/angular/test/directive/ionicInfiniteScroll.unit.js index ba17b364dd..39fc3f65e7 100644 --- a/js/ext/angular/test/directive/ionicInfiniteScroll.unit.js +++ b/js/ext/angular/test/directive/ionicInfiniteScroll.unit.js @@ -84,16 +84,6 @@ describe('ionicInfiniteScroll directive', function() { var el = setup('distance=5%'); expect(ctrl.getMaxScroll()).toBe(101 * 0.95); }); - - it('getMaxScroll should use scope.infiniteScrolDistance as number', function() { - var el = setup('', { infiniteScrollDistance: '11' }); - expect(ctrl.getMaxScroll()).toBe(90); - }); - - it('getMaxScroll should use scope.infiniteScrolDistance as percent', function() { - var el = setup('', { infiniteScrollDistance: '50%' }); - expect(ctrl.getMaxScroll()).toBe(101 * 0.5); - }); }); describe('scroll event', function() { @@ -107,16 +97,6 @@ describe('ionicInfiniteScroll directive', function() { expect(ctrl.isLoading).toBe(true); expect(el.scope().foo).toBe(1); }); - it('should add active and call $scope.$onInfiniteScroll if past top', function() { - var onScrollSpy = jasmine.createSpy('onInfiniteScroll'); - var el = setup('', { $onInfiniteScroll: onScrollSpy }); - scrollTopValue = scrollMaxValue; - el.controller('$ionicScroll').$element.triggerHandler('scroll'); - - expect(el.hasClass('active')).toBe(true); - expect(ctrl.isLoading).toBe(true); - expect(onScrollSpy).toHaveBeenCalled(); - }); it('should not run the event twice if isLoading is true', function() { var onScrollSpy = jasmine.createSpy('onInfiniteScroll'); var el = setup('', { $onInfiniteScroll: onScrollSpy }); diff --git a/js/ext/angular/test/directive/ionicRefresher.unit.js b/js/ext/angular/test/directive/ionicRefresher.unit.js new file mode 100644 index 0000000000..a8addb14f1 --- /dev/null +++ b/js/ext/angular/test/directive/ionicRefresher.unit.js @@ -0,0 +1,102 @@ +describe('ionRefresher directive', function() { + beforeEach(module('ionic')); + function setup(attrs, scopeProps) { + var el; + inject(function($compile, $rootScope) { + var scope = $rootScope.$new(); + var ionicScrollCtrl = { + setRefresher: jasmine.createSpy('setRefresher'), + scrollView: { + finishPullToRefresh: jasmine.createSpy('finishPullToRefresh') + } + }; + + angular.extend(scope, scopeProps || {}); + + el = angular.element(''); + el.data('$$ionicScrollController', ionicScrollCtrl); + + $compile(el)(scope); + $rootScope.$apply(); + }); + return el; + } + + it('should error without ionicScroll', inject(function($compile, $rootScope) { + expect(function() { + $compile('')($rootScope); + }).toThrow(); + })); + + it('should bind $onRefresh', function() { + var refreshSpy = jasmine.createSpy('onRefresh'); + var el = setup('on-refresh="refreshSpy()"', { + refreshSpy: refreshSpy + }); + expect(refreshSpy).not.toHaveBeenCalled(); + el.scope().$onRefresh(); + expect(refreshSpy).toHaveBeenCalled(); + }); + + it('should bind $onRefreshOpening', function() { + var spyMe = jasmine.createSpy('onRefreshOpening'); + var el = setup('on-refresh-opening="spyMe()"', { + spyMe: spyMe + }); + expect(spyMe).not.toHaveBeenCalled(); + el.scope().$onRefreshOpening(); + expect(spyMe).toHaveBeenCalled(); + }); + + it('should setRefresher on scrollCtrl', function() { + var el = setup(); + expect(el.controller('$ionicScroll').setRefresher.callCount).toBe(1); + expect(el.controller('$ionicScroll').setRefresher).toHaveBeenCalledWith( + el.scope(), el[0] + ) + }); + + it('should listen for scroll.refreshComplete', function() { + var el = setup(); + el.addClass('active'); + var ctrl = el.controller('$ionicScroll'); + expect(ctrl.scrollView.finishPullToRefresh).not.toHaveBeenCalled(); + el.scope().$broadcast('scroll.refreshComplete'); + expect(el.hasClass('active')).toBe(false); + expect(ctrl.scrollView.finishPullToRefresh).toHaveBeenCalled(); + }); + + it('should have default pullingIcon', function() { + var el = setup(); + expect(el[0].querySelector('.icon.icon-pulling.ion-arrow-down-c')).toBeTruthy(); + }); + it('should allow custom pullingIcon', function() { + var el = setup('pulling-icon="super-icon"'); + expect(el[0].querySelector('.icon.icon-pulling.ion-arrow-down-c')).toBeFalsy(); + expect(el[0].querySelector('.icon.icon-pulling.super-icon')).toBeTruthy(); + }); + + it('should have default refreshingIcon', function() { + var el = setup(); + expect(el[0].querySelector('.icon.icon-refreshing.ion-loading-d')).toBeTruthy(); + }); + it('should allow custom refreshingIcon', function() { + var el = setup('refreshing-icon="monkey-icon"'); + expect(el[0].querySelector('.icon.icon-refreshing.ion-arrow-down-c')).toBeFalsy(); + expect(el[0].querySelector('.icon.icon-refreshing.monkey-icon')).toBeTruthy(); + }); + + it('should have no text by default', function() { + var el = setup(); + expect(el.text().trim()).toBe(''); + }); + it('should allow pullingText', function() { + var el = setup('pulling-text="{{2+2}} some text"'); + expect(el[0].querySelector('span.icon-pulling').innerHTML).toBe('4 some text'); + }); + it('should allow refreshingText', function() { + var el = setup('refreshing-text="{{3+2}} text"'); + expect(el[0].querySelector('span.icon-refreshing').innerHTML).toBe('5 text'); + }); + +}); diff --git a/js/ext/angular/test/list-fit.html b/js/ext/angular/test/list-fit.html index 3f0e65f841..b9f7543153 100644 --- a/js/ext/angular/test/list-fit.html +++ b/js/ext/angular/test/list-fit.html @@ -14,9 +14,9 @@ - + - +
  • This ion-list should *exactly* fit
  • @@ -48,7 +48,7 @@
  • more {{$index}}
- +
diff --git a/scss/_scaffolding.scss b/scss/_scaffolding.scss index 3b35ad5326..8d8ae45658 100644 --- a/scss/_scaffolding.scss +++ b/scss/_scaffolding.scss @@ -211,7 +211,7 @@ body.grade-c { } .ionic-refresher-content { - .icon-pulling { + .icon.icon-pulling { @include animation-name(refresh-spin); } }