Skip to content

Commit

Permalink
fix(scroll): keyboard support for native scroll views
Browse files Browse the repository at this point in the history
  • Loading branch information
tlancina authored and mhartington committed Jun 18, 2015
1 parent d3c3e8c commit a293a23
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 35 deletions.
40 changes: 17 additions & 23 deletions js/utils/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ function keyboardFocusIn(e) {
if (!e.target ||
e.target.readOnly ||
!ionic.tap.isKeyboardElement(e.target) ||
!(scrollView = inputScrollView(e.target))) {
!(scrollView = ionic.DomUtil.getParentWithClass(e.target, SCROLL_CONTAINER_CSS))) {
keyboardActiveElement = null;
return;
}
Expand All @@ -319,12 +319,23 @@ function keyboardFocusIn(e) {

// if using JS scrolling, undo the effects of native overflow scroll so the
// scroll view is positioned correctly
document.body.scrollTop = 0;
scrollView.scrollTop = 0;
ionic.requestAnimationFrame(function(){
if (!scrollView.classList.contains("overflow-scroll")) {
document.body.scrollTop = 0;
scrollView.scrollTop = 0;
});
ionic.requestAnimationFrame(function(){
document.body.scrollTop = 0;
scrollView.scrollTop = 0;
});

// any showing part of the document that isn't within the scroll the user
// could touchmove and cause some ugly changes to the app, so disable
// any touchmove events while the keyboard is open using e.preventDefault()
if (window.navigator.msPointerEnabled) {
document.addEventListener("MSPointerMove", keyboardPreventDefault, false);
} else {
document.addEventListener('touchmove', keyboardPreventDefault, false);
}
}

if (!ionic.keyboard.isOpen || ionic.keyboard.isClosing) {
ionic.keyboard.isOpening = true;
Expand All @@ -336,14 +347,7 @@ function keyboardFocusIn(e) {
// keyboard
document.addEventListener('keydown', keyboardOnKeyDown, false);

// any showing part of the document that isn't within the scroll the user
// could touchmove and cause some ugly changes to the app, so disable
// any touchmove events while the keyboard is open using e.preventDefault()
if (window.navigator.msPointerEnabled) {
document.addEventListener("MSPointerMove", keyboardPreventDefault, false);
} else {
document.addEventListener('touchmove', keyboardPreventDefault, false);
}


// if we aren't using the plugin and the keyboard isn't open yet, wait for the
// window to resize so we can get an accurate estimate of the keyboard size,
Expand Down Expand Up @@ -725,16 +729,6 @@ function getViewportHeight() {
return windowHeight;
}

function inputScrollView(ele) {
while(ele) {
if (ele.classList.contains(SCROLL_CONTAINER_CSS)) {
return ele;
}
ele = ele.parentElement;
}
return null;
}

function keyboardHasPlugin() {
return !!(window.cordova && cordova.plugins && cordova.plugins.Keyboard);
}
Expand Down
138 changes: 128 additions & 10 deletions js/views/scrollViewNative.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@
// scroll animation loop w/ easing
// credit https://gist.github.com/dezinezync/5487119
var start = Date.now(),
duration = 1000, //milliseconds
duration = 250, //milliseconds
fromY = self.el.scrollTop,
fromX = self.el.scrollLeft;

Expand Down Expand Up @@ -239,6 +239,7 @@

} else {
// done
ionic.tap.removeClonedInputs(self.__container, self);
self.resize();
}
}
Expand Down Expand Up @@ -293,28 +294,144 @@

// Event Handler
var container = self.__container;
// save height when scroll view is shrunk so we don't need to reflow
var scrollViewOffsetHeight;

// should be unnecessary in native scrolling, but keep in case bugs show up
self.scrollChildIntoView = NOOP;
/**
* 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) {
//console.log("scrollChildIntoView at: " + Date.now());

// D
var scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
// D - A
scrollViewOffsetHeight = container.offsetHeight;
var alreadyShrunk = self.isShrunkForKeyboard;

var isModal = container.parentNode.classList.contains('modal');
// 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)
*/
if (!alreadyShrunk) {
// 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 ( 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 = Math.max(0, e.detail.keyboardHeight - scrollBottomOffsetToBottom);

ionic.requestAnimationFrame(function(){
// D - A or B - A if D > B D - A max(0, D - B)
scrollViewOffsetHeight = scrollViewOffsetHeight - keyboardOffset;
container.style.height = scrollViewOffsetHeight + "px";

//update scroll view
self.resize();
});
}

self.isShrunkForKeyboard = true;
}

/*
* _______
* |---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(){
// update D if we shrunk
if (self.isShrunkForKeyboard && !alreadyShrunk) {
scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
}

// middle of the scrollview, this is where we want to scroll to
// (D - A) / 2
var scrollMidpointOffset = scrollViewOffsetHeight * 0.5;
//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);

// distance from middle of input to the bottom of the scroll view
// C - D C D
var inputMidpointOffsetToScrollBottom = inputMidpoint - scrollBottomOffsetToTop;

//C - D + (D - A)/2 C - D (D - A)/ 2
var scrollTop = inputMidpointOffsetToScrollBottom + scrollMidpointOffset;

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.isScrolledIntoView) {
self.isScrolledIntoView = false;
if (self.isShrunkForKeyboard) {
self.isShrunkForKeyboard = false;
container.style.height = "";
container.style.overflow = "";
self.resize();
ionic.scroll.isScrolling = false;
}
self.resize();
};

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);

// 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() {
Expand All @@ -336,6 +453,7 @@
delete self.options.el;

self.resize = self.scrollTo = self.onScroll = self.resetScrollView = NOOP;
self.scrollChildIntoView = NOOP;
container = null;
}
});
Expand Down
7 changes: 5 additions & 2 deletions test/unit/views/scrollViewNative.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ describe('Scroll View', function() {

it('Should bind to event listeners', function() {
spyOn(sc,'addEventListener');
spyOn(document,'addEventListener');
var sv = new ionic.views.ScrollNative({
el: sc
});

expect(document.addEventListener).toHaveBeenCalled();
expect(document.addEventListener.mostRecentCall.args[0]).toBe('resetScrollView');
expect(sc.addEventListener).toHaveBeenCalled();
expect(sc.addEventListener.callCount).toBe(4);
expect(sc.addEventListener.mostRecentCall.args[0]).toBe('resetScrollView');
expect(sc.addEventListener.callCount).toBe(2);
expect(sc.addEventListener.mostRecentCall.args[0]).toBe('scrollChildIntoView');
});

it('Should remove event listeners on cleanup', function() {
Expand Down

0 comments on commit a293a23

Please sign in to comment.