Skip to content

Commit

Permalink
fix(android): when keyboard comes up, ensure input is in view
Browse files Browse the repository at this point in the history
This requires us to set fullscreen="false" in our cordova apps.

Uses the resize event to determine when the keyboard has been shown,
then broadcasts an event from the activeElement: 'scrollChildIntoView',
which is caught by the nearest parent scrollView.  The scrollView will
then see if that element is within the new device's height (since the
keyboard resizes the screen), and if not scroll it into view.

Additionally, when the keyboard resizes the screen we add a
`.hide-footer` class to the body, which will hide tabbars and footer
bars while the keyboard is opened.

For now, this is android only.

Closes #314.
  • Loading branch information
ajoslin committed Feb 12, 2014
1 parent 63b0a31 commit 9327ac7
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 19 deletions.
9 changes: 8 additions & 1 deletion config/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ module.exports = {
'js/ionic.js',

// Utils
'js/utils/**/*.js',
'js/utils/animate.js',
'js/utils/dom.js',
'js/utils/events.js',
'js/utils/gestures.js',
'js/utils/platform.js',
'js/utils/poly.js',
'js/utils/utils.js',
'js/utils/keyboard.js',

// Views
'js/views/view.js',
Expand Down
11 changes: 9 additions & 2 deletions js/ext/angular/src/controller/ionicScrollController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

angular.module('ionic.ui.scroll')

.controller('$ionicScroll', ['$scope', 'scrollViewOptions', '$timeout', '$ionicScrollDelegate',
function($scope, scrollViewOptions, $timeout, $ionicScrollDelegate) {
.controller('$ionicScroll', ['$scope', 'scrollViewOptions', '$timeout', '$ionicScrollDelegate', '$window', function($scope, scrollViewOptions, $timeout, $ionicScrollDelegate, $window) {

scrollViewOptions.bouncing = angular.isDefined(scrollViewOptions.bouncing) ?
scrollViewOptions.bouncing :
Expand All @@ -24,6 +23,14 @@ angular.module('ionic.ui.scroll')
//Register delegate for event handling
$ionicScrollDelegate.register($scope, $element, scrollView);

$window.addEventListener('resize', resize);
$scope.$on('$destroy', function() {
$window.removeEventListener('resize', resize);
});
function resize() {
scrollView.resize();
}

$timeout(function() {
scrollView.run();

Expand Down
24 changes: 23 additions & 1 deletion js/ext/angular/test/controller/ionicScrollController.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ describe('$ionicScroll Controller', function() {
function setup(options) {
options = options || {};

options.el = options.el || document.createElement('div');
options.el = options.el ||
//scrollView requires an outer container element and a child
//content element
angular.element('<div><div></div></div>')[0];

inject(function($controller, $rootScope, $timeout) {
scope = $rootScope.$new();
Expand Down Expand Up @@ -41,6 +44,25 @@ describe('$ionicScroll Controller', function() {
expect(ctrl.scrollView.run).toHaveBeenCalled();
});

it('should resize the scrollview on window resize', function() {
setup();
timeout.flush();
spyOn(ctrl.scrollView, 'resize');
ionic.trigger('resize', { target: window });
expect(ctrl.scrollView.resize).toHaveBeenCalled();
});

it('should unbind window event listener on scope destroy', function() {
spyOn(window, 'removeEventListener');
spyOn(window, 'addEventListener');
setup();
expect(window.addEventListener).toHaveBeenCalled();
expect(window.addEventListener.mostRecentCall.args[0]).toBe('resize');
scope.$destroy();
expect(window.removeEventListener).toHaveBeenCalled();
expect(window.removeEventListener.mostRecentCall.args[0]).toBe('resize');
});

it('should register with $ionicScrollDelegate', inject(function($ionicScrollDelegate) {
spyOn($ionicScrollDelegate, 'register');
setup();
Expand Down
1 change: 1 addition & 0 deletions js/ext/angular/test/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ <h2>Nic Cage</h2>
<i class="icon {{ item.icon }}"></i>
{{ item.text }}
</a>
<input type="text" placeholder="text input">
<div class="item">
<slide-box show-pager="false">
<slide>
Expand Down
16 changes: 10 additions & 6 deletions js/utils/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Author: Max Lynch <max@drifty.com>
*
* Framework events handles various mobile browser events, and
* Framework events handles various mobile browser events, and
* detects special events like tap/swipe/etc. and emits them
* as custom events that can be used in an app.
*
Expand Down Expand Up @@ -48,14 +48,18 @@
VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'],

// Trigger a new event
trigger: function(eventType, data) {
var event = new CustomEvent(eventType, { detail: data });
trigger: function(eventType, data, bubbles, cancelable) {
var event = new CustomEvent(eventType, {
detail: data,
bubbles: !!bubbles,
cancelable: !!cancelable
});

// Make sure to trigger the event on the given target, or dispatch it from
// the window if we don't have an event target
data && data.target && data.target.dispatchEvent(event) || window.dispatchEvent(event);
},

// Bind an event
on: function(type, callback, element) {
var e = element || window;
Expand Down Expand Up @@ -92,8 +96,8 @@
handlePopState: function(event) {
},
};


// Map some convenient top-level functions for event handling
ionic.on = function() { ionic.EventController.on.apply(ionic.EventController, arguments); };
ionic.off = function() { ionic.EventController.off.apply(ionic.EventController, arguments); };
Expand Down
52 changes: 52 additions & 0 deletions js/utils/keyboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
(function(ionic) {

ionic.Platform.ready(function() {
if (ionic.Platform.is('android')) {
androidKeyboardFix();
}
});

function androidKeyboardFix() {
var rememberedDeviceWidth = window.innerWidth;
var rememberedDeviceHeight = window.innerHeight;
var keyboardHeight;

window.addEventListener('resize', resize);

function resize() {

//If the width of the window changes, we have an orientation change
if (rememberedDeviceWidth !== window.innerWidth) {
rememberedDeviceWidth = window.innerWidth;
rememberedDeviceHeight = window.innerHeight;
console.info('orientation change. deviceWidth =', rememberedDeviceWidth,
', deviceHeight =', rememberedDeviceHeight);

//If the height changes, and it's less than before, we have a keyboard open
} else if (rememberedDeviceHeight !== window.innerHeight &&
window.innerHeight < rememberedDeviceHeight) {
document.body.classList.add('hide-footer');
//Wait for next frame so document.activeElement is set
window.rAF(handleKeyboardChange);
} else {
//Otherwise we have a keyboard close or a *really* weird resize
document.body.classList.remove('hide-footer');
}

function handleKeyboardChange() {
//keyboard opens
keyboardHeight = rememberedDeviceHeight - window.innerHeight;
var activeEl = document.activeElement;
if (activeEl) {
//This event is caught by the nearest parent scrollView
//of the activeElement
ionic.trigger('scrollChildIntoView', {
target: activeEl
}, true);
}

}
}
}

})(window.ionic);
22 changes: 22 additions & 0 deletions js/views/scrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,28 @@ ionic.views.Scroll = ionic.views.View.inherit({
// Event Handler
var container = this.__container;

//Broadcasted when keyboard is shown on some platforms.
//See js/utils/keyboard.js
container.addEventListener('scrollChildIntoView', function(e) {
var deviceHeight = window.innerHeight;
var element = e.target;
var elementHeight = e.target.offsetHeight;

//getBoundingClientRect() will actually give us position relative to the viewport
var elementDeviceTop = element.getBoundingClientRect().top;
var elementScrollTop = ionic.DomUtil.getPositionInParent(element, container).top;

//If the element is positioned under the keyboard...
if (elementDeviceTop + elementHeight > deviceHeight) {
//Put element in middle of visible screen
self.scrollTo(0, elementScrollTop + elementHeight - (deviceHeight * 0.5), true);
}

//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();
});

if ('ontouchstart' in window) {

container.addEventListener("touchstart", function(e) {
Expand Down
29 changes: 20 additions & 9 deletions scss/_util.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* --------------------------------------------------
*/

.hidden,
.hide {
display: none;
.hidden,
.hide {
display: none;
}
.show {
display: block;
Expand All @@ -15,6 +15,17 @@
visibility: hidden;
}

.hide-footer {
.bar-footer,
.tabs {
display: none;
}
.has-footer,
.has-tabs {
bottom: 0;
}
}

.inline {
display: inline-block;
}
Expand All @@ -30,10 +41,10 @@
.block {
display: block;
clear: both;
&:after {
display: block;
visibility: hidden;
clear: both;
&:after {
display: block;
visibility: hidden;
clear: both;
height: 0;
content: ".";
}
Expand Down Expand Up @@ -101,8 +112,8 @@
/**
* Utility Colors
* --------------------------------------------------
* Utility colors are added to help set a naming convention. You'll
* notice we purposely do not use words like "red" or "blue", but
* Utility colors are added to help set a naming convention. You'll
* notice we purposely do not use words like "red" or "blue", but
* instead have colors which represent an emotion or generic theme.
*/

Expand Down
84 changes: 84 additions & 0 deletions test/inputs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<html ng-app="navTest">
<head>
<meta charset="utf-8">
<title>List</title>

<!-- Sets initial viewport load and disables zooming -->
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="stylesheet" href="lib/css/ionic.css">
<script src="lib/js/ionic.bundle.js"></script>
</head>
<body>

<pane>

<header class="bar bar-header bar-positive">
<div class="buttons">
<button ng-click="toggleDelete()" class="button button-clear">{{ editBtnText }}</button>
</div>
<h1 class="title">List Tests</h1>
<div class="buttons">
<button ng-click="toggleReorder()" class="button button-clear">{{ reorderBtnText }}</button>
</div>
</header>

<footer class="bar bar-footer bar-positive">
<h1 class="title">Footer time!</h1>
</footer>

<content has-header="true" has-footer="true">
<input type="text" placeholder="text me!">
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<input type="text" placeholder="text me!">
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<input type="text" placeholder="text me!">
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<input type="text" placeholder="text me!">
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<input type="text" placeholder="text me!">
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<input type="text" placeholder="text me!">
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<input type="text" placeholder="text me!">
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<input type="text" placeholder="text me!">
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<p style="margin: 10px;">...</p>
<input type="text" placeholder="text me!">
</content>

</pane>
</body>
</html>

0 comments on commit 9327ac7

Please sign in to comment.