From 9327ac71c778fa7ad48eb5570687e9380f5ff0db Mon Sep 17 00:00:00 2001 From: Andy Joslin Date: Mon, 10 Feb 2014 16:22:57 -0500 Subject: [PATCH] fix(android): when keyboard comes up, ensure input is in view 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. --- config/build.js | 9 +- .../src/controller/ionicScrollController.js | 11 ++- .../controller/ionicScrollController.unit.js | 24 +++++- js/ext/angular/test/list.html | 1 + js/utils/events.js | 16 ++-- js/utils/keyboard.js | 52 ++++++++++++ js/views/scrollView.js | 22 +++++ scss/_util.scss | 29 +++++-- test/inputs.html | 84 +++++++++++++++++++ 9 files changed, 229 insertions(+), 19 deletions(-) create mode 100644 js/utils/keyboard.js create mode 100644 test/inputs.html diff --git a/config/build.js b/config/build.js index b95e070772..42b3b811b0 100644 --- a/config/build.js +++ b/config/build.js @@ -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', diff --git a/js/ext/angular/src/controller/ionicScrollController.js b/js/ext/angular/src/controller/ionicScrollController.js index 8a5198de26..60e6200faf 100644 --- a/js/ext/angular/src/controller/ionicScrollController.js +++ b/js/ext/angular/src/controller/ionicScrollController.js @@ -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 : @@ -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(); diff --git a/js/ext/angular/test/controller/ionicScrollController.unit.js b/js/ext/angular/test/controller/ionicScrollController.unit.js index ac11a8b789..eee232bddc 100644 --- a/js/ext/angular/test/controller/ionicScrollController.unit.js +++ b/js/ext/angular/test/controller/ionicScrollController.unit.js @@ -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('
')[0]; inject(function($controller, $rootScope, $timeout) { scope = $rootScope.$new(); @@ -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(); diff --git a/js/ext/angular/test/list.html b/js/ext/angular/test/list.html index dcb07bb83a..c3320148fc 100644 --- a/js/ext/angular/test/list.html +++ b/js/ext/angular/test/list.html @@ -157,6 +157,7 @@ {{ item.text }} +
diff --git a/js/utils/events.js b/js/utils/events.js index c4014fccf8..d2def899ec 100644 --- a/js/utils/events.js +++ b/js/utils/events.js @@ -3,7 +3,7 @@ * * Author: Max Lynch * - * 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. * @@ -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; @@ -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); }; diff --git a/js/utils/keyboard.js b/js/utils/keyboard.js new file mode 100644 index 0000000000..a1c3e01240 --- /dev/null +++ b/js/utils/keyboard.js @@ -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); diff --git a/js/views/scrollView.js b/js/views/scrollView.js index 446675ae25..c31e853d02 100644 --- a/js/views/scrollView.js +++ b/js/views/scrollView.js @@ -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) { diff --git a/scss/_util.scss b/scss/_util.scss index fa84a34f6d..40472910ef 100644 --- a/scss/_util.scss +++ b/scss/_util.scss @@ -4,9 +4,9 @@ * -------------------------------------------------- */ -.hidden, -.hide { - display: none; +.hidden, +.hide { + display: none; } .show { display: block; @@ -15,6 +15,17 @@ visibility: hidden; } +.hide-footer { + .bar-footer, + .tabs { + display: none; + } + .has-footer, + .has-tabs { + bottom: 0; + } +} + .inline { display: inline-block; } @@ -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: "."; } @@ -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. */ diff --git a/test/inputs.html b/test/inputs.html new file mode 100644 index 0000000000..808fd86498 --- /dev/null +++ b/test/inputs.html @@ -0,0 +1,84 @@ + + + + List + + + + + + + + + + +
+
+ +
+

List Tests

+
+ +
+
+ +
+

Footer time!

+
+ + + +

...

+

...

+

...

+

...

+

...

+ +

...

+

...

+

...

+

...

+

...

+ +

...

+

...

+

...

+

...

+

...

+ +

...

+

...

+

...

+

...

+

...

+ +

...

+

...

+

...

+

...

+

...

+ +

...

+

...

+

...

+

...

+

...

+ +

...

+

...

+

...

+

...

+

...

+ +

...

+

...

+

...

+

...

+

...

+ +
+ +
+ + +