(function(ionic) {
var NOOP = function() {};
var deprecated = 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;
// Whether scrolling is frozen or not
self.__frozen = false;
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);
if(options.startY >= 0 || options.startX >= 0) {
ionic.requestAnimationFrame(function() {
self.el.scrollTop = options.startY || 0;
self.el.scrollLeft = options.startX || 0;
self.__scrollTop = self.el.scrollTop;
self.__scrollLeft = self.el.scrollLeft;
});
}
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() {
if (!ionic.scroll.isScrolling) {
ionic.scroll.isScrolling = true;
}
clearTimeout(self.scrollTimer);
self.scrollTimer = setTimeout(function() {
ionic.scroll.isScrolling = false;
}, 80);
};
self.freeze = function(shouldFreeze) {
self.__frozen = shouldFreeze;
};
// A more powerful freeze pop that dominates all other freeze pops
self.freezeShut = function(shouldFreezeShut) {
self.__frozenShut = shouldFreezeShut;
};
self.__initEventHandlers();
},
/** Methods not used in native scrolling */
__callback: function() { deprecated('__callback'); },
zoomTo: function() { deprecated('zoomTo'); },
zoomBy: function() { deprecated('zoomBy'); },
activatePullToRefresh: function() { deprecated('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;
}
var oldOverflowX = self.el.style.overflowX;
var oldOverflowY = self.el.style.overflowY;
clearTimeout(self.__scrollToCleanupTimeout);
self.__scrollToCleanupTimeout = setTimeout(function() {
self.el.style.overflowX = oldOverflowX;
self.el.style.overflowY = oldOverflowY;
}, 500);
self.el.style.overflowY = 'hidden';
self.el.style.overflowX = 'hidden';
animateScroll(top, left);
function animateScroll(Y, X) {
// scroll animation loop w/ easing
// credit https://gist.github.com/dezinezync/5487119
var start = Date.now(),
duration = 250, //milliseconds
fromY = self.el.scrollTop,
fromX = self.el.scrollLeft;
if (fromY === Y && fromX === X) {
self.el.style.overflowX = oldOverflowX;
self.el.style.overflowY = oldOverflowY;
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
ionic.tap.removeClonedInputs(self.__container, self);
self.el.style.overflowX = oldOverflowX;
self.el.style.overflowY = oldOverflowY;
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;
// save height when scroll view is shrunk so we don't need to reflow
var scrollViewOffsetHeight;
var lastKeyboardHeight;
/**
* Shrink the scroll view when the keyboard is up if necessary and if the
* focused input is below the bottom of the shrunk scroll view, scroll it
* into view.
*/
self.scrollChildIntoView = function(e) {
var rect = container.getBoundingClientRect();
if(!self.__originalContainerHeight) {
self.__originalContainerHeight = rect.height;
}
// D
//var scrollBottomOffsetToTop = rect.bottom;
// D - A
scrollViewOffsetHeight = self.__originalContainerHeight;
//console.log('Scroll view offset height', scrollViewOffsetHeight);
//console.dir(container);
var alreadyShrunk = self.isShrunkForKeyboard;
var isModal = container.parentNode.classList.contains('modal');
var isPopover = container.parentNode.classList.contains('popover');
// 680px is when the media query for 60% modal width kicks in
var isInsetModal = isModal && window.innerWidth >= 680;
/*
* _______
* |---A---| <- top of scroll view
* | |
* |---B---| <- keyboard
* | C | <- input
* |---D---| <- initial bottom of scroll view
* |___E___| <- bottom of viewport
*
* All commented calculations relative to the top of the viewport (ie E
* is the viewport height, not 0)
*/
var changedKeyboardHeight = lastKeyboardHeight && (lastKeyboardHeight !== e.detail.keyboardHeight);
if (!alreadyShrunk || changedKeyboardHeight) {
// shrink scrollview so we can actually scroll if the input is hidden
// if it isn't shrink so we can scroll to inputs under the keyboard
// inset modals won't shrink on Android on their own when the keyboard appears
if ( !isPopover && (ionic.Platform.isIOS() || ionic.Platform.isFullScreen || isInsetModal) ) {
// if there are things below the scroll view account for them and
// subtract them from the keyboard height when resizing
// E - D E D
//var scrollBottomOffsetToBottom = e.detail.viewportHeight - scrollBottomOffsetToTop;
// 0 or D - B if D > B E - B E - D
//var keyboardOffset = e.detail.keyboardHeight - scrollBottomOffsetToBottom;
ionic.requestAnimationFrame(function(){
// D - A or B - A if D > B D - A max(0, D - B)
scrollViewOffsetHeight = Math.max(0, Math.min(self.__originalContainerHeight, self.__originalContainerHeight - (e.detail.keyboardHeight - 43)));//keyboardOffset >= 0 ? scrollViewOffsetHeight - keyboardOffset : scrollViewOffsetHeight + keyboardOffset;
//console.log('Old container height', self.__originalContainerHeight, 'New container height', scrollViewOffsetHeight, 'Keyboard height', e.detail.keyboardHeight);
container.style.height = scrollViewOffsetHeight + "px";
/*
if (ionic.Platform.isIOS()) {
// Force redraw to avoid disappearing content
var disp = container.style.display;
container.style.display = 'none';
var trick = container.offsetHeight;
container.style.display = disp;
}
*/
container.classList.add('keyboard-up');
//update scroll view
self.resize();
});
}
self.isShrunkForKeyboard = true;
}
lastKeyboardHeight = e.detail.keyboardHeight;
/*
* _______
* |---A---| <- top of scroll view
* | * | <- where we want to scroll to
* |--B-D--| <- keyboard, bottom of scroll view
* | C | <- input
* | |
* |___E___| <- bottom of viewport
*
* All commented calculations relative to the top of the viewport (ie E
* is the viewport height, not 0)
*/
// if the element is positioned under the keyboard scroll it into view
if (e.detail.isElementUnderKeyboard) {
ionic.requestAnimationFrame(function(){
var pos = ionic.DomUtil.getOffsetTop(e.detail.target);
setTimeout(function() {
if (ionic.Platform.isIOS()) {
ionic.tap.cloneFocusedInput(container, self);
}
// Scroll the input into view, with a 100px buffer
self.scrollTo(0, pos - (rect.top + 100), true);
self.onScroll();
}, 32);
/*
// update D if we shrunk
if (self.isShrunkForKeyboard && !alreadyShrunk) {
scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
console.log('Scroll bottom', scrollBottomOffsetToTop);
}
// middle of the scrollview, this is where we want to scroll to
// (D - A) / 2
var scrollMidpointOffset = scrollViewOffsetHeight * 0.5;
console.log('Midpoint', scrollMidpointOffset);
//console.log("container.offsetHeight: " + scrollViewOffsetHeight);
// middle of the input we want to scroll into view
// C
var inputMidpoint = ((e.detail.elementBottom + e.detail.elementTop) / 2);
console.log('Input midpoint');
// distance from middle of input to the bottom of the scroll view
// C - D C D
var inputMidpointOffsetToScrollBottom = inputMidpoint - scrollBottomOffsetToTop;
console.log('Input midpoint offset', inputMidpointOffsetToScrollBottom);
//C - D + (D - A)/2 C - D (D - A)/ 2
var scrollTop = inputMidpointOffsetToScrollBottom + scrollMidpointOffset;
console.log('Scroll top', scrollTop);
if ( scrollTop > 0) {
if (ionic.Platform.isIOS()) {
//just shrank scroll view, give it some breathing room before scrolling
setTimeout(function(){
ionic.tap.cloneFocusedInput(container, self);
self.scrollBy(0, scrollTop, true);
self.onScroll();
}, 32);
} else {
self.scrollBy(0, scrollTop, true);
self.onScroll();
}
}
*/
});
}
// Only the first scrollView parent of the element that broadcasted this event
// (the active element that needs to be shown) should receive this event
e.stopPropagation();
};
self.resetScrollView = function() {
//return scrollview to original height once keyboard has hidden
if (self.isShrunkForKeyboard) {
self.isShrunkForKeyboard = false;
container.style.height = "";
/*
if (ionic.Platform.isIOS()) {
// Force redraw to avoid disappearing content
var disp = container.style.display;
container.style.display = 'none';
var trick = container.offsetHeight;
container.style.display = disp;
}
*/
self.__originalContainerHeight = container.getBoundingClientRect().height;
if (ionic.Platform.isIOS()) {
ionic.requestAnimationFrame(function() {
container.classList.remove('keyboard-up');
});
}
}
self.resize();
};
self.handleTouchMove = function(e) {
if (self.__frozenShut) {
e.preventDefault();
e.stopPropagation();
return false;
} else if ( self.__frozen ){
e.preventDefault();
// let it propagate so other events such as drag events can happen,
// but don't let it actually scroll
return false;
}
return true;
};
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(ionic.EVENTS.touchstart, self.handleTouchMove);
container.addEventListener(ionic.EVENTS.touchmove, self.handleTouchMove);
// Listen on document because container may not have had the last
// keyboardActiveElement, for example after closing a modal with a focused
// input and returning to a previously resized scroll view in an ion-content.
// Since we can only resize scroll views that are currently visible, just resize
// the current scroll view when the keyboard is closed.
document.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);
container.removeEventListener(ionic.EVENTS.touchstart, self.handleTouchMove);
container.removeEventListener(ionic.EVENTS.touchmove, self.handleTouchMove);
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;
self.scrollChildIntoView = NOOP;
container = null;
}
});
})(ionic);