diff --git a/config/build.config.js b/config/build.config.js
index ee9580e2898..6d0cb27eee3 100644
--- a/config/build.config.js
+++ b/config/build.config.js
@@ -55,6 +55,7 @@ module.exports = {
// Views
'js/views/view.js',
'js/views/scrollView.js',
+ 'js/views/scrollViewNative.js',
'js/views/listView.js',
'js/views/modalView.js',
'js/views/sideMenuView.js',
diff --git a/js/angular/controller/refresherController.js b/js/angular/controller/refresherController.js
index 2d3ade2685b..5c5892531b8 100644
--- a/js/angular/controller/refresherController.js
+++ b/js/angular/controller/refresherController.js
@@ -238,8 +238,8 @@ IonicModule
scrollParent = $element.parent().parent()[0];
scrollChild = $element.parent()[0];
- if (!scrollParent.classList.contains('ionic-scroll') ||
- !scrollChild.classList.contains('scroll')) {
+ if (!scrollParent || !scrollParent.classList.contains('ionic-scroll') ||
+ !scrollChild || !scrollChild.classList.contains('scroll')) {
throw new Error('Refresher must be immediate child of ion-content or ion-scroll');
}
diff --git a/js/angular/controller/scrollController.js b/js/angular/controller/scrollController.js
index 1f86c06680e..7ee055b0784 100644
--- a/js/angular/controller/scrollController.js
+++ b/js/angular/controller/scrollController.js
@@ -26,10 +26,19 @@ function($scope,
self.__timeout = $timeout;
self._scrollViewOptions = scrollViewOptions; //for testing
+ self.isNative = function() {
+ return !!scrollViewOptions.nativeScrolling;
+ };
var element = self.element = scrollViewOptions.el;
var $element = self.$element = jqLite(element);
- var scrollView = self.scrollView = new ionic.views.Scroll(scrollViewOptions);
+ var scrollView;
+ if (self.isNative()) {
+ scrollView = self.scrollView = new ionic.views.ScrollNative(scrollViewOptions);
+ } else {
+ scrollView = self.scrollView = new ionic.views.Scroll(scrollViewOptions);
+ }
+
//Attach self to element as a controller so other directives can require this controller
//through `require: '$ionicScroll'
diff --git a/js/angular/directive/content.js b/js/angular/directive/content.js
index adfe2c404ff..b5260db175c 100644
--- a/js/angular/directive/content.js
+++ b/js/angular/directive/content.js
@@ -109,37 +109,55 @@ function($timeout, $controller, $ionicBind, $ionicConfig) {
if ($attr.scroll === "false") {
//do nothing
- } else if (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling()) {
- // use native scrolling
- $element.addClass('overflow-scroll');
} else {
- var scrollViewOptions = {
- el: $element[0],
- delegateHandle: attr.delegateHandle,
- locking: (attr.locking || 'true') === 'true',
- bouncing: $scope.$eval($scope.hasBouncing),
- startX: $scope.$eval($scope.startX) || 0,
- startY: $scope.$eval($scope.startY) || 0,
- scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
- scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
- scrollingX: $scope.direction.indexOf('x') >= 0,
- scrollingY: $scope.direction.indexOf('y') >= 0,
- scrollEventInterval: parseInt($scope.scrollEventInterval, 10) || 10,
- scrollingComplete: function() {
- $scope.$onScrollComplete({
- scrollTop: this.__scrollTop,
- scrollLeft: this.__scrollLeft
- });
- }
- };
+ var scrollViewOptions = {};
+
+ if (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling()) {
+ // use native scrolling
+ $element.addClass('overflow-scroll');
+
+ scrollViewOptions = {
+ el: $element[0],
+ delegateHandle: attr.delegateHandle,
+ startX: $scope.$eval($scope.startX) || 0,
+ startY: $scope.$eval($scope.startY) || 0,
+ nativeScrolling:true
+ };
+
+ } else {
+ // Use JS scrolling
+ scrollViewOptions = {
+ el: $element[0],
+ delegateHandle: attr.delegateHandle,
+ locking: (attr.locking || 'true') === 'true',
+ bouncing: $scope.$eval($scope.hasBouncing),
+ startX: $scope.$eval($scope.startX) || 0,
+ startY: $scope.$eval($scope.startY) || 0,
+ scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
+ scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
+ scrollingX: $scope.direction.indexOf('x') >= 0,
+ scrollingY: $scope.direction.indexOf('y') >= 0,
+ scrollEventInterval: parseInt($scope.scrollEventInterval, 10) || 10,
+ scrollingComplete: function() {
+ $scope.$onScrollComplete({
+ scrollTop: this.__scrollTop,
+ scrollLeft: this.__scrollLeft
+ });
+ }
+ };
+ }
+
+ // init scroll controller with appropriate options
$controller('$ionicScroll', {
$scope: $scope,
scrollViewOptions: scrollViewOptions
});
$scope.$on('$destroy', function() {
- scrollViewOptions.scrollingComplete = noop;
- delete scrollViewOptions.el;
+ if (scrollViewOptions) {
+ scrollViewOptions.scrollingComplete = noop;
+ delete scrollViewOptions.el;
+ }
innerElement = null;
$element = null;
attr.$$element = null;
diff --git a/js/angular/directive/infiniteScroll.js b/js/angular/directive/infiniteScroll.js
index b76f5780ff0..b23933f0d1b 100644
--- a/js/angular/directive/infiniteScroll.js
+++ b/js/angular/directive/infiniteScroll.js
@@ -79,10 +79,14 @@ IonicModule
link: function($scope, $element, $attrs, ctrls) {
var infiniteScrollCtrl = ctrls[1];
var scrollCtrl = infiniteScrollCtrl.scrollCtrl = ctrls[0];
- var jsScrolling = infiniteScrollCtrl.jsScrolling = !!scrollCtrl;
+ var jsScrolling = infiniteScrollCtrl.jsScrolling = !scrollCtrl.isNative();
+
// if this view is not beneath a scrollCtrl, it can't be injected, proceed w/ native scrolling
if (jsScrolling) {
infiniteScrollCtrl.scrollView = scrollCtrl.scrollView;
+ $scope.scrollingType = 'js-scrolling';
+ //bind to JS scroll events
+ scrollCtrl.$element.on('scroll', infiniteScrollCtrl.checkBounds);
} else {
// grabbing the scrollable element, to determine dimensions, and current scroll pos
var scrollEl = ionic.DomUtil.getParentOrSelfWithClass($element[0].parentNode,'overflow-scroll');
@@ -91,14 +95,10 @@ IonicModule
if (!scrollEl) {
throw 'Infinite scroll must be used inside a scrollable div';
}
- }
- //bind to appropriate scroll event
- if (jsScrolling) {
- $scope.scrollingType = 'js-scrolling';
- scrollCtrl.$element.on('scroll', infiniteScrollCtrl.checkBounds);
- } else {
+ //bind to native scroll events
infiniteScrollCtrl.scrollEl.addEventListener('scroll', infiniteScrollCtrl.checkBounds);
}
+
// Optionally check bounds on start after scrollView is fully rendered
var doImmediateCheck = isDefined($attrs.immediateCheck) ? $scope.$eval($attrs.immediateCheck) : true;
if (doImmediateCheck) {
diff --git a/js/angular/directive/refresher.js b/js/angular/directive/refresher.js
index 06328e1bc8b..b564ddaa1ac 100644
--- a/js/angular/directive/refresher.js
+++ b/js/angular/directive/refresher.js
@@ -87,10 +87,11 @@ IonicModule
// JS Scrolling uses the scroll controller
var scrollCtrl = ctrls[0],
refresherCtrl = ctrls[1];
-
- if (!!scrollCtrl) {
+ if (!scrollCtrl || scrollCtrl.isNative()) {
+ // Kick off native scrolling
+ refresherCtrl.init();
+ } else {
$element[0].classList.add('js-scrolling');
-
scrollCtrl._setRefresher(
$scope,
$element[0],
@@ -102,10 +103,6 @@ IonicModule
scrollCtrl.scrollView.finishPullToRefresh();
});
});
-
- } else {
- // Kick off native scrolling
- refresherCtrl.init();
}
}
diff --git a/js/views/scrollViewNative.js b/js/views/scrollViewNative.js
new file mode 100644
index 00000000000..3503d4d4bd5
--- /dev/null
+++ b/js/views/scrollViewNative.js
@@ -0,0 +1,344 @@
+(function(ionic) {
+ var NOOP = function() {};
+ var depreciated = function(name) {
+ console.error('Method not available in native scrolling: ' + name);
+ };
+ ionic.views.ScrollNative = ionic.views.View.inherit({
+
+ initialize: function(options) {
+ var self = this;
+ self.__container = self.el = options.el;
+ self.__content = options.el.firstElementChild;
+ self.isNative = true;
+
+ self.__scrollTop = self.el.scrollTop;
+ self.__scrollLeft = self.el.scrollLeft;
+ self.__clientHeight = self.__content.clientHeight,
+ self.__clientWidth = self.__content.clientWidth,
+ self.__maxScrollTop = Math.max((self.__contentHeight) - self.__clientHeight, 0),
+ self.__maxScrollLeft = Math.max((self.__contentWidth) - self.__clientWidth, 0),
+
+ self.options = {
+
+ freeze: false,
+
+ getContentWidth: function() {
+ return Math.max(self.__content.scrollWidth, self.__content.offsetWidth);
+ },
+
+ getContentHeight: function() {
+ return Math.max(self.__content.scrollHeight, self.__content.offsetHeight + (self.__content.offsetTop * 2));
+ }
+
+ };
+
+ for (var key in options) {
+ self.options[key] = options[key];
+ }
+
+ /**
+ * Sets isScrolling to true, and automatically deactivates if not called again in 80ms.
+ */
+ self.onScroll = function(event) {
+ if (!ionic.scroll.isScrolling) {
+ ionic.scroll.isScrolling = true;
+ }
+
+ clearTimeout(self.scrollTimer);
+ self.scrollTimer = setTimeout(function() {
+ ionic.scroll.isScrolling = false;
+ }, 80);
+ };
+
+ self.freeze = NOOP;
+
+ self.__initEventHandlers();
+ },
+
+ /** Methods not used in native scrolling */
+ __callback: function() {depreciated('__callback');},
+ zoomTo: function() {depreciated('zoomTo');},
+ zoomBy: function() {depreciated('zoomBy');},
+ activatePullToRefresh: function() {depreciated('activatePullToRefresh');},
+
+ /**
+ * Returns the scroll position and zooming values
+ *
+ * @return {Map} `left` and `top` scroll position and `zoom` level
+ */
+ resize: function(continueScrolling) {
+ var self = this;
+ if (!self.__container || !self.options) return;
+
+ // Update Scroller dimensions for changed content
+ // Add padding to bottom of content
+ self.setDimensions(
+ self.__container.clientWidth,
+ self.__container.clientHeight,
+ self.options.getContentWidth(),
+ self.options.getContentHeight(),
+ continueScrolling
+ );
+ },
+
+ /**
+ * Initialize the scrollview
+ * In native scrolling, this only means we need to gather size information
+ */
+ run: function() {
+ this.resize();
+ },
+
+ /**
+ * Returns the scroll position and zooming values
+ *
+ * @return {Map} `left` and `top` scroll position and `zoom` level
+ */
+ getValues: function() {
+ var self = this;
+ self.update();
+ return {
+ left: self.__scrollLeft,
+ top: self.__scrollTop,
+ zoom: 1
+ };
+ },
+
+ /**
+ * Updates the __scrollLeft and __scrollTop values to el's current value
+ */
+ update: function() {
+ var self = this;
+ self.__scrollLeft = self.el.scrollLeft;
+ self.__scrollTop = self.el.scrollTop;
+ },
+
+ /**
+ * Configures the dimensions of the client (outer) and content (inner) elements.
+ * Requires the available space for the outer element and the outer size of the inner element.
+ * All values which are falsy (null or zero etc.) are ignored and the old value is kept.
+ *
+ * @param clientWidth {Integer} Inner width of outer element
+ * @param clientHeight {Integer} Inner height of outer element
+ * @param contentWidth {Integer} Outer width of inner element
+ * @param contentHeight {Integer} Outer height of inner element
+ */
+ setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight) {
+ var self = this;
+
+ if (!clientWidth && !clientHeight && !contentWidth && !contentHeight) {
+ // this scrollview isn't rendered, don't bother
+ return;
+ }
+
+ // Only update values which are defined
+ if (clientWidth === +clientWidth) {
+ self.__clientWidth = clientWidth;
+ }
+
+ if (clientHeight === +clientHeight) {
+ self.__clientHeight = clientHeight;
+ }
+
+ if (contentWidth === +contentWidth) {
+ self.__contentWidth = contentWidth;
+ }
+
+ if (contentHeight === +contentHeight) {
+ self.__contentHeight = contentHeight;
+ }
+
+ // Refresh maximums
+ self.__computeScrollMax();
+ },
+
+ /**
+ * Returns the maximum scroll values
+ *
+ * @return {Map} `left` and `top` maximum scroll values
+ */
+ getScrollMax: function() {
+ return {
+ left: this.__maxScrollLeft,
+ top: this.__maxScrollTop
+ };
+ },
+
+ /**
+ * Scrolls by the given amount in px.
+ *
+ * @param left {Number} Horizontal scroll position, keeps current if value is null
+ * @param top {Number} Vertical scroll position, keeps current if value is null
+ * @param animate {Boolean} Whether the scrolling should happen using an animation
+ */
+
+ scrollBy: function(left, top, animate) {
+ var self = this;
+
+ // update scroll vars before refferencing them
+ self.update();
+
+ var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
+ var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
+
+ self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
+ },
+
+ /**
+ * Scrolls to the given position in px.
+ *
+ * @param left {Number} Horizontal scroll position, keeps current if value is null
+ * @param top {Number} Vertical scroll position, keeps current if value is null
+ * @param animate {Boolean} Whether the scrolling should happen using an animation
+ */
+ scrollTo: function(left, top, animate) {
+ var self = this;
+ if (!animate) {
+ self.el.scrollTop = top;
+ self.el.scrollLeft = left;
+ self.resize();
+ return;
+ }
+ animateScroll(top, left);
+
+ function animateScroll(Y, X) {
+ // scroll animation loop w/ easing
+ // credit https://gist.github.com/dezinezync/5487119
+ var start = Date.now(),
+ duration = 1000, //milliseconds
+ fromY = self.el.scrollTop,
+ fromX = self.el.scrollLeft;
+
+ if (fromY === Y && fromX === X) {
+ self.resize();
+ return; /* Prevent scrolling to the Y point if already there */
+ }
+
+ // decelerating to zero velocity
+ function easeOutCubic(t) {
+ return (--t) * t * t + 1;
+ }
+
+ // scroll loop
+ function animateScrollStep() {
+ var currentTime = Date.now(),
+ time = Math.min(1, ((currentTime - start) / duration)),
+ // where .5 would be 50% of time on a linear scale easedT gives a
+ // fraction based on the easing method
+ easedT = easeOutCubic(time);
+
+ if (fromY != Y) {
+ self.el.scrollTop = parseInt((easedT * (Y - fromY)) + fromY, 10);
+ }
+ if (fromX != X) {
+ self.el.scrollLeft = parseInt((easedT * (X - fromX)) + fromX, 10);
+ }
+
+ if (time < 1) {
+ ionic.requestAnimationFrame(animateScrollStep);
+
+ } else {
+ // done
+ self.resize();
+ }
+ }
+
+ // start scroll loop
+ ionic.requestAnimationFrame(animateScrollStep);
+ }
+ },
+
+
+
+ /*
+ ---------------------------------------------------------------------------
+ PRIVATE API
+ ---------------------------------------------------------------------------
+ */
+
+ /**
+ * If the scroll view isn't sized correctly on start, wait until we have at least some size
+ */
+ __waitForSize: function() {
+ var self = this;
+
+ clearTimeout(self.__sizerTimeout);
+
+ var sizer = function() {
+ self.resize(true);
+ };
+
+ sizer();
+ self.__sizerTimeout = setTimeout(sizer, 500);
+ },
+
+
+ /**
+ * Recomputes scroll minimum values based on client dimensions and content dimensions.
+ */
+ __computeScrollMax: function() {
+ var self = this;
+
+ self.__maxScrollLeft = Math.max((self.__contentWidth) - self.__clientWidth, 0);
+ self.__maxScrollTop = Math.max((self.__contentHeight) - self.__clientHeight, 0);
+
+ if (!self.__didWaitForSize && !self.__maxScrollLeft && !self.__maxScrollTop) {
+ self.__didWaitForSize = true;
+ self.__waitForSize();
+ }
+ },
+
+ __initEventHandlers: function() {
+ var self = this;
+
+ // Event Handler
+ var container = self.__container;
+
+ // should be unnecessary in native scrolling, but keep in case bugs show up
+ self.scrollChildIntoView = NOOP;
+
+ self.resetScrollView = function(e) {
+ //return scrollview to original height once keyboard has hidden
+ if (self.isScrolledIntoView) {
+ self.isScrolledIntoView = false;
+ container.style.height = "";
+ container.style.overflow = "";
+ self.resize();
+ ionic.scroll.isScrolling = false;
+ }
+ };
+
+ container.addEventListener('resetScrollView', self.resetScrollView);
+ container.addEventListener('scroll', self.onScroll);
+
+ //Broadcasted when keyboard is shown on some platforms.
+ //See js/utils/keyboard.js
+ container.addEventListener('scrollChildIntoView', self.scrollChildIntoView);
+ container.addEventListener('resetScrollView', self.resetScrollView);
+ },
+
+ __cleanup: function() {
+ var self = this;
+ var container = self.__container;
+
+ container.removeEventListener('resetScrollView', self.resetScrollView);
+ container.removeEventListener('scroll', self.onScroll);
+
+ container.removeEventListener('scrollChildIntoView', self.scrollChildIntoView);
+ container.removeEventListener('resetScrollView', self.resetScrollView);
+
+ ionic.tap.removeClonedInputs(container, self);
+
+ delete self.__container;
+ delete self.__content;
+ delete self.__indicatorX;
+ delete self.__indicatorY;
+ delete self.options.el;
+
+ self.resize = self.scrollTo = self.onScroll = self.resetScrollView = NOOP;
+ container = null;
+ }
+ });
+
+})(ionic);
+
diff --git a/test/html/infinite-scroll.html b/test/html/infinite-scroll.html
index c060c122415..0527ba616b6 100644
--- a/test/html/infinite-scroll.html
+++ b/test/html/infinite-scroll.html
@@ -25,7 +25,9 @@