mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
refactor(keyboard): Scroll to inputs hidden by keyboard
This commit is contained in:
@@ -42,6 +42,7 @@ module.exports = {
|
||||
'js/utils/activator.js',
|
||||
'js/utils/utils.js',
|
||||
'js/utils/keyboard.js',
|
||||
'js/utils/viewport.js',
|
||||
|
||||
// Views
|
||||
'js/views/view.js',
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
// 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);
|
||||
data && data.target && data.target.dispatchEvent && data.target.dispatchEvent(event) || window.dispatchEvent(event);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,84 +1,214 @@
|
||||
(function(ionic) {
|
||||
|
||||
/*
|
||||
IONIC KEYBOARD
|
||||
---------------
|
||||
|
||||
*/
|
||||
|
||||
var keyboardViewportHeight = window.innerHeight;
|
||||
var keyboardIsOpen;
|
||||
var keyboardActiveElement;
|
||||
var keyboardFocusOutTimer;
|
||||
var keyboardFocusInTimer;
|
||||
|
||||
var KEYBOARD_OPEN_CSS = 'keyboard-open';
|
||||
var SCROLL_CONTAINER_CSS = 'scroll';
|
||||
|
||||
ionic.keyboard = {
|
||||
isOpen: false,
|
||||
height: null
|
||||
};
|
||||
|
||||
function keyboardInit() {
|
||||
if( keyboardHasPlugin() ) {
|
||||
window.addEventListener('native.showkeyboard', keyboardNativeShow);
|
||||
}
|
||||
|
||||
document.body.addEventListener('ionic.focusin', keyboardBrowserFocusIn);
|
||||
document.body.addEventListener('focusin', keyboardBrowserFocusIn);
|
||||
|
||||
document.body.addEventListener('focusout', keyboardFocusOut);
|
||||
document.body.addEventListener('orientationchange', keyboardOrientationChange);
|
||||
|
||||
document.removeEventListener('touchstart', keyboardInit);
|
||||
}
|
||||
|
||||
function keyboardNativeShow(e) {
|
||||
ionic.keyboard.height = e.keyboardHeight;
|
||||
}
|
||||
|
||||
function keyboardBrowserFocusIn(e) {
|
||||
if( !e.target || !ionic.tap.isTextInput(e.target) || !keyboardIsWithinScroll(e.target) ) return;
|
||||
|
||||
document.addEventListener('keydown', keyboardOnKeyDown, false);
|
||||
|
||||
document.body.scrollTop = 0;
|
||||
document.body.querySelector('.scroll-content').scrollTop = 0;
|
||||
|
||||
keyboardActiveElement = e.target;
|
||||
|
||||
keyboardSetShow(e);
|
||||
}
|
||||
|
||||
function keyboardSetShow(e) {
|
||||
clearTimeout(keyboardFocusInTimer);
|
||||
clearTimeout(keyboardFocusOutTimer);
|
||||
|
||||
keyboardFocusInTimer = setTimeout(function(){
|
||||
var keyboardHeight = keyboardGetHeight();
|
||||
var elementBounds = keyboardActiveElement.getBoundingClientRect();
|
||||
|
||||
keyboardShow(e.target, elementBounds.top, elementBounds.bottom, keyboardViewportHeight, keyboardHeight);
|
||||
}, 32);
|
||||
}
|
||||
|
||||
function keyboardShow(element, elementTop, elementBottom, viewportHeight, keyboardHeight) {
|
||||
var details = {
|
||||
target: element,
|
||||
elementTop: Math.round(elementTop),
|
||||
elementBottom: Math.round(elementBottom),
|
||||
keyboardHeight: keyboardHeight
|
||||
};
|
||||
|
||||
if( keyboardIsOverWebView() ) {
|
||||
// keyboard sits on top of the view, but doesn't adjust the view's height
|
||||
// lower the content height by subtracting the keyboard height from the view height
|
||||
details.contentHeight = viewportHeight - keyboardHeight;
|
||||
} else {
|
||||
// view's height was shrunk down and the keyboard takes up the space the view doesn't fill
|
||||
// do not add extra padding at the bottom of the scroll view, native already did that
|
||||
details.contentHeight = viewportHeight;
|
||||
}
|
||||
|
||||
console.debug('keyboardShow', keyboardHeight, details.contentHeight);
|
||||
|
||||
// distance from top of input to the top of the keyboard
|
||||
details.keyboardTopOffset = details.elementTop - details.contentHeight;
|
||||
|
||||
console.debug('keyboardTopOffset', details.elementTop, details.contentHeight, details.keyboardTopOffset);
|
||||
|
||||
// figure out if the element is under the keyboard
|
||||
details.isElementUnderKeyboard = (details.elementBottom > details.contentHeight);
|
||||
|
||||
ionic.keyboard.isOpen = true;
|
||||
|
||||
// send event so the scroll view adjusts
|
||||
keyboardActiveElement = element;
|
||||
ionic.trigger('scrollChildIntoView', details, true);
|
||||
|
||||
ionic.requestAnimationFrame(function(){
|
||||
document.body.classList.add(KEYBOARD_OPEN_CSS);
|
||||
});
|
||||
|
||||
// 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()
|
||||
document.addEventListener('touchmove', keyboardPreventDefault, false);
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
function keyboardFocusOut(e) {
|
||||
clearTimeout(keyboardFocusInTimer);
|
||||
clearTimeout(keyboardFocusOutTimer);
|
||||
|
||||
keyboardFocusOutTimer = setTimeout(keyboardHide, 350);
|
||||
}
|
||||
|
||||
function keyboardHide() {
|
||||
console.debug('keyboardHide');
|
||||
ionic.keyboard.isOpen = false;
|
||||
|
||||
ionic.trigger('resetScrollView', {
|
||||
target: keyboardActiveElement
|
||||
}, true);
|
||||
|
||||
ionic.requestAnimationFrame(function(){
|
||||
document.body.classList.remove(KEYBOARD_OPEN_CSS);
|
||||
});
|
||||
|
||||
// the keyboard is gone now, remove the touchmove that disables native scroll
|
||||
document.removeEventListener('touchmove', keyboardPreventDefault);
|
||||
document.removeEventListener('keydown', keyboardOnKeyDown);
|
||||
}
|
||||
|
||||
function keyboardUpdateViewportHeight() {
|
||||
if( window.innerHeight > keyboardViewportHeight ) {
|
||||
keyboardViewportHeight = window.innerHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function keyboardOnKeyDown(e) {
|
||||
if( ionic.scroll.isScrolling ) {
|
||||
keyboardPreventDefault(e);
|
||||
}
|
||||
}
|
||||
|
||||
function keyboardPreventDefault(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function keyboardOrientationChange() {
|
||||
keyboardViewportHeight = window.innerHeight;
|
||||
setTimeout(function(){
|
||||
keyboardViewportHeight = window.innerHeight;
|
||||
}, 999);
|
||||
}
|
||||
|
||||
function keyboardGetHeight() {
|
||||
// check if we are already have a keyboard height from the plugin
|
||||
if (ionic.keyboard.height ) {
|
||||
return ionic.keyboard.height;
|
||||
}
|
||||
|
||||
// fallback for when its the webview without the plugin
|
||||
// or for just the standard web browser
|
||||
if( ionic.Platform.isIOS() ) {
|
||||
if( ionic.Platform.isWebView() ) {
|
||||
return 260;
|
||||
}
|
||||
return 216;
|
||||
} else if( ionic.Platform.isAndroid() ) {
|
||||
if( ionic.Platform.isWebView() ) {
|
||||
return 220;
|
||||
}
|
||||
if( ionic.Platform.version() <= 4.3) {
|
||||
return 230;
|
||||
}
|
||||
}
|
||||
|
||||
// safe guess
|
||||
return 275;
|
||||
}
|
||||
|
||||
function keyboardIsWithinScroll(ele) {
|
||||
while(ele) {
|
||||
if(ele.classList.contains(SCROLL_CONTAINER_CSS)) {
|
||||
return true;
|
||||
}
|
||||
ele = ele.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function keyboardIsOverWebView() {
|
||||
return ( ionic.Platform.isIOS() ) ||
|
||||
( ionic.Platform.isAndroid() && !ionic.Platform.isWebView() );
|
||||
}
|
||||
|
||||
function keyboardHasPlugin() {
|
||||
return !!(window.cordova && cordova.plugins && cordova.plugins.Keyboard);
|
||||
}
|
||||
|
||||
ionic.Platform.ready(function() {
|
||||
var rememberedDeviceWidth = window.innerWidth;
|
||||
var rememberedDeviceHeight = window.innerHeight;
|
||||
var keyboardHeight;
|
||||
var rememberedActiveEl;
|
||||
var alreadyOpen = false;
|
||||
keyboardUpdateViewportHeight();
|
||||
|
||||
window.addEventListener('focusin', onBrowserFocusIn);
|
||||
|
||||
if(ionic.Platform.isWebView() && window.cordova && cordova.plugins && cordova.plugins.Keyboard) {
|
||||
window.addEventListener('native.showkeyboard', onNativeKeyboardShow);
|
||||
window.addEventListener('native.hidekeyboard', onNativeKeyboardHide);
|
||||
|
||||
} else if (ionic.Platform.isAndroid()){
|
||||
window.addEventListener('resize', onBrowserResize);
|
||||
}
|
||||
|
||||
function onBrowserFocusIn(e) {
|
||||
if (ionic.tap.containsOrIsTextInput(e.target) || e.srcElement.isContentEditable){
|
||||
document.body.scrollTop = 0;
|
||||
}
|
||||
|
||||
rememberedActiveEl = e.srcElement;
|
||||
}
|
||||
|
||||
function onBrowserResize() {
|
||||
if(rememberedDeviceWidth !== window.innerWidth) {
|
||||
// If the width of the window changes, we have an orientation change
|
||||
rememberedDeviceWidth = window.innerWidth;
|
||||
rememberedDeviceHeight = window.innerHeight;
|
||||
|
||||
} else if(rememberedDeviceHeight !== window.innerHeight &&
|
||||
window.innerHeight < rememberedDeviceHeight) {
|
||||
// If the height changes, and it's less than before, we have a keyboard open
|
||||
document.body.classList.add('keyboard-open');
|
||||
|
||||
keyboardHeight = rememberedDeviceHeight - window.innerHeight;
|
||||
setTimeout(function() {
|
||||
ionic.trigger('scrollChildIntoView', {
|
||||
target: rememberedActiveEl,
|
||||
}, true);
|
||||
}, 100);
|
||||
|
||||
} else {
|
||||
// Otherwise we have a keyboard close or a *really* weird resize
|
||||
document.body.classList.remove('keyboard-open');
|
||||
}
|
||||
}
|
||||
|
||||
function onNativeKeyboardShow(e) {
|
||||
if(rememberedActiveEl) {
|
||||
// This event is caught by the nearest parent scrollView
|
||||
// of the activeElement
|
||||
if(cordova.plugins.Keyboard.isVisible) {
|
||||
document.body.classList.add('keyboard-open');
|
||||
ionic.trigger('scrollChildIntoView', {
|
||||
keyboardHeight: e.keyboardHeight,
|
||||
target: rememberedActiveEl,
|
||||
firstKeyboardShow: !alreadyOpen
|
||||
}, true);
|
||||
|
||||
if(!alreadyOpen) alreadyOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onNativeKeyboardHide() {
|
||||
// wait to see if we're just switching inputs
|
||||
setTimeout(function() {
|
||||
if(!cordova.plugins.Keyboard.isVisible) {
|
||||
document.body.classList.remove('keyboard-open');
|
||||
alreadyOpen = false;
|
||||
ionic.trigger('resetScrollView', {
|
||||
target: rememberedActiveEl
|
||||
}, true);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
// Android sometimes reports bad innerHeight on window.load
|
||||
// try it again in a lil bit to play it safe
|
||||
setTimeout(keyboardUpdateViewportHeight, 999);
|
||||
|
||||
// only initialize the adjustments for the virtual keyboard
|
||||
// if a touchstart event happens
|
||||
document.addEventListener('touchstart', keyboardInit, false);
|
||||
});
|
||||
|
||||
})(window.ionic);
|
||||
|
||||
@@ -92,7 +92,8 @@ ionic.tap = {
|
||||
isTextInput: function(ele) {
|
||||
return !!ele &&
|
||||
(ele.tagName == 'TEXTAREA' ||
|
||||
(ele.tagName == 'INPUT' && !(/radio|checkbox|range|file|submit|reset/i).test(ele.type)));
|
||||
ele.contentEditable === 'true' ||
|
||||
(ele.tagName == 'INPUT' && !(/radio|checkbox|range|file|submit|reset/i).test(ele.type)) );
|
||||
},
|
||||
|
||||
isLabelWithTextInput: function(ele) {
|
||||
@@ -106,7 +107,7 @@ ionic.tap = {
|
||||
return ionic.tap.isTextInput(ele) || ionic.tap.isLabelWithTextInput(ele);
|
||||
},
|
||||
|
||||
cloneFocusedInput: function(container, instance) {
|
||||
cloneFocusedInput: function(container, scrollIntance) {
|
||||
if(ionic.tap.hasCheckedClone) return;
|
||||
ionic.tap.hasCheckedClone = true;
|
||||
|
||||
@@ -119,6 +120,7 @@ ionic.tap = {
|
||||
clonedInput.type = focusInput.type;
|
||||
clonedInput.value = focusInput.value;
|
||||
clonedInput.className = 'cloned-text-input';
|
||||
clonedInput.readOnly = true;
|
||||
focusInput.parentElement.insertBefore(clonedInput, focusInput);
|
||||
focusInput.style.top = focusInput.offsetTop;
|
||||
focusInput.classList.add('previous-input-focus');
|
||||
@@ -129,7 +131,7 @@ ionic.tap = {
|
||||
|
||||
hasCheckedClone: false,
|
||||
|
||||
removeClonedInputs: function(container) {
|
||||
removeClonedInputs: function(container, scrollIntance) {
|
||||
ionic.tap.hasCheckedClone = false;
|
||||
|
||||
ionic.requestAnimationFrame(function(){
|
||||
@@ -184,6 +186,11 @@ function triggerMouseEvent(type, ele, x, y) {
|
||||
}
|
||||
|
||||
function tapClickGateKeeper(e) {
|
||||
if(e.target.type == 'submit' && e.detail === 0) {
|
||||
// do not prevent click if it came from an "Enter" or "Go" keypress submit
|
||||
return;
|
||||
}
|
||||
|
||||
// do not allow through any click events that were not created by ionic.tap
|
||||
if( (ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target) ) ||
|
||||
(!e.isIonicTap && !tapRequiresNativeClick(e.target)) ) {
|
||||
@@ -241,6 +248,12 @@ function tapMouseDown(e) {
|
||||
}
|
||||
|
||||
function tapMouseUp(e) {
|
||||
if(tapEnabledTouchEvents) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if( tapIgnoreEvent(e) ) return;
|
||||
|
||||
if( !tapHasPointerMoved(e) ) {
|
||||
@@ -272,6 +285,19 @@ function tapTouchStart(e) {
|
||||
|
||||
tapEventListener('touchmove');
|
||||
ionic.activator.start(e);
|
||||
|
||||
if( ionic.Platform.isIOS() && ionic.tap.isLabelWithTextInput(e.target) ) {
|
||||
// if the tapped element is a label, which has a child input
|
||||
// then preventDefault so iOS doesn't ugly auto scroll to the input
|
||||
// but do not prevent default on Android or else you cannot move the text caret
|
||||
// and do not prevent default on Android or else no virtual keyboard shows up
|
||||
|
||||
var textInput = tapTargetElement( tapContainingElement(e.target) );
|
||||
if( textInput !== tapActiveEle ) {
|
||||
// don't preventDefault on an already focused input or else iOS's text caret isn't usable
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tapTouchEnd(e) {
|
||||
@@ -301,17 +327,11 @@ function tapTouchCancel(e) {
|
||||
}
|
||||
|
||||
function tapEnableTouchEvents() {
|
||||
if(!tapEnabledTouchEvents) {
|
||||
tapEventListener('mouseup', false);
|
||||
tapEnabledTouchEvents = true;
|
||||
}
|
||||
tapEnabledTouchEvents = true;
|
||||
clearTimeout(tapMouseResetTimer);
|
||||
tapMouseResetTimer = setTimeout(tapResetMouseEvent, 2500);
|
||||
}
|
||||
|
||||
function tapResetMouseEvent() {
|
||||
tapEventListener('mouseup', false);
|
||||
tapEnabledTouchEvents = false;
|
||||
tapMouseResetTimer = setTimeout(function(){
|
||||
tapEnabledTouchEvents = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function tapIgnoreEvent(e) {
|
||||
@@ -327,25 +347,35 @@ function tapIgnoreEvent(e) {
|
||||
function tapHandleFocus(ele) {
|
||||
tapTouchFocusedInput = null;
|
||||
|
||||
var triggerFocusIn = false;
|
||||
|
||||
if(ele.tagName == 'SELECT') {
|
||||
// trick to force Android options to show up
|
||||
console.debug('tapHandleFocus', ele.tagName);
|
||||
triggerMouseEvent('mousedown', ele, 0, 0);
|
||||
tapActiveElement(ele);
|
||||
ele.focus && ele.focus();
|
||||
triggerFocusIn = true;
|
||||
|
||||
} else if(tapActiveElement() !== ele) {
|
||||
if( (/input|textarea/i).test(ele.tagName) ) {
|
||||
console.debug('tapHandleFocus', ele.tagName, ele.id);
|
||||
tapActiveElement(ele);
|
||||
ele.focus && ele.focus();
|
||||
ele.value = ele.value;
|
||||
if( tapEnabledTouchEvents ) {
|
||||
tapTouchFocusedInput = ele;
|
||||
}
|
||||
} else {
|
||||
tapFocusOutActive();
|
||||
} else if(tapActiveElement() === ele) {
|
||||
// already is the active element and has focus
|
||||
triggerFocusIn = true;
|
||||
|
||||
} else if( (/input|textarea/i).test(ele.tagName) ) {
|
||||
triggerFocusIn = true;
|
||||
ele.focus && ele.focus();
|
||||
ele.value = ele.value;
|
||||
if( tapEnabledTouchEvents ) {
|
||||
tapTouchFocusedInput = ele;
|
||||
}
|
||||
|
||||
} else {
|
||||
tapFocusOutActive();
|
||||
}
|
||||
|
||||
if(triggerFocusIn) {
|
||||
tapActiveElement(ele);
|
||||
ionic.trigger('ionic.focusin', {
|
||||
target: ele
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +469,5 @@ function tapTargetElement(ele) {
|
||||
}
|
||||
|
||||
ionic.DomUtil.ready(function(){
|
||||
|
||||
ionic.tap.register(document.body);
|
||||
|
||||
ionic.tap.register(document);
|
||||
});
|
||||
|
||||
57
js/utils/viewport.js
Normal file
57
js/utils/viewport.js
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
var viewportTag;
|
||||
var viewportProperties = {};
|
||||
|
||||
|
||||
function viewportLoadTag() {
|
||||
var x;
|
||||
|
||||
for(x=0; x<document.head.children.length; x++) {
|
||||
if(document.head.children[x].name == 'viewport') {
|
||||
viewportTag = document.head.children[x];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(viewportTag) {
|
||||
var props = viewportTag.content.toLowerCase().replace(/\s+/g, '').split(',');
|
||||
var keyValue;
|
||||
for(x=0; x<props.length; x++) {
|
||||
keyValue = props[x].split('=');
|
||||
if(keyValue.length == 2) viewportProperties[ keyValue[0] ] = keyValue[1];
|
||||
}
|
||||
viewportInitWebView();
|
||||
}
|
||||
}
|
||||
|
||||
function viewportInitWebView() {
|
||||
var hasViewportChange = false;
|
||||
|
||||
if( ionic.Platform.isWebView() ) {
|
||||
if( viewportProperties.height != 'device-height' ) {
|
||||
viewportProperties.height = 'device-height';
|
||||
hasViewportChange = true;
|
||||
}
|
||||
} else if( viewportProperties.height ) {
|
||||
delete viewportProperties.height;
|
||||
hasViewportChange = true;
|
||||
}
|
||||
if(hasViewportChange) viewportUpdate();
|
||||
}
|
||||
|
||||
function viewportUpdate(updates) {
|
||||
if(!viewportTag) return;
|
||||
|
||||
ionic.Utils.extend(viewportProperties, updates);
|
||||
|
||||
var key, props = [];
|
||||
for(key in viewportProperties) {
|
||||
if(viewportProperties[key]) props.push(key + '=' + viewportProperties[key]);
|
||||
}
|
||||
|
||||
viewportTag.content = props.join(',');
|
||||
}
|
||||
|
||||
ionic.DomUtil.ready(function() {
|
||||
viewportLoadTag();
|
||||
});
|
||||
@@ -626,50 +626,31 @@ ionic.views.Scroll = ionic.views.View.inherit({
|
||||
//Broadcasted when keyboard is shown on some platforms.
|
||||
//See js/utils/keyboard.js
|
||||
container.addEventListener('scrollChildIntoView', function(e) {
|
||||
var keyboardHeight = e.detail.keyboardHeight || 0;
|
||||
var deviceHeight = window.innerHeight;
|
||||
|
||||
var frameHeight;
|
||||
if (ionic.Platform.isIOS() && ionic.Platform.version() >= 7.0 || (!ionic.Platform.isWebView() && ionic.Platform.isAndroid())){
|
||||
frameHeight = deviceHeight;
|
||||
}
|
||||
else {
|
||||
frameHeight = deviceHeight - keyboardHeight;
|
||||
}
|
||||
|
||||
var element = e.target;
|
||||
|
||||
//getBoundingClientRect() will give us position relative to the viewport
|
||||
var elementDeviceBottom = element.getBoundingClientRect().bottom;
|
||||
|
||||
if (e.detail.firstKeyboardShow){
|
||||
//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
|
||||
container.style.height = (container.clientHeight - keyboardHeight) + "px";
|
||||
if( !self.isScrolledIntoView ) {
|
||||
// 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
|
||||
container.style.height = (container.clientHeight - e.detail.keyboardHeight) + "px";
|
||||
container.style.overflow = "visible";
|
||||
|
||||
self.isScrolledIntoView = true;
|
||||
//update scroll view
|
||||
self.resize();
|
||||
}
|
||||
|
||||
//If the element is positioned under the keyboard...
|
||||
if (elementDeviceBottom > frameHeight) {
|
||||
if( e.detail.isElementUnderKeyboard ) {
|
||||
//Put element in middle of visible screen
|
||||
//Wait for resize() to reset scroll position
|
||||
ionic.scroll.isScrolling = true;
|
||||
setTimeout(function(){
|
||||
//distance from top of input to the top of the keyboard
|
||||
var keyboardTopOffset = element.getBoundingClientRect().top - frameHeight;
|
||||
//middle of the scrollview, where we want to scroll to
|
||||
var scrollViewMidpointOffset = container.clientHeight * 0.5;
|
||||
var scrollOffset = keyboardTopOffset + scrollViewMidpointOffset;
|
||||
self.scrollBy(0, scrollOffset, true);
|
||||
|
||||
//please someone tell me there's a better way to do this
|
||||
//wait until input is scrolled into view, then fix focus
|
||||
setTimeout(function(){
|
||||
element.value = element.value; //thanks @adambradley 1337h4x
|
||||
}, 600);
|
||||
}, 32);
|
||||
var scrollTop = e.detail.keyboardTopOffset + scrollViewMidpointOffset;
|
||||
console.debug('scrollChildIntoView', scrollTop);
|
||||
ionic.tap.cloneFocusedInput(container, self);
|
||||
self.scrollBy(0, scrollTop, true);
|
||||
},
|
||||
(ionic.Platform.isIOS() ? 80 : 350)
|
||||
);
|
||||
}
|
||||
|
||||
//Only the first scrollView parent of the element that broadcasted this event
|
||||
@@ -679,9 +660,11 @@ ionic.views.Scroll = ionic.views.View.inherit({
|
||||
|
||||
container.addEventListener('resetScrollView', function(e) {
|
||||
//return scrollview to original height once keyboard has hidden
|
||||
self.isScrolledIntoView = false;
|
||||
container.style.height = "";
|
||||
container.style.overflow = "";
|
||||
self.resize();
|
||||
ionic.scroll.isScrolling = false;
|
||||
});
|
||||
|
||||
|
||||
@@ -737,7 +720,7 @@ ionic.views.Scroll = ionic.views.View.inherit({
|
||||
// disabled being able to select text on an input
|
||||
// hide the input which has focus, and show a cloned one that doesn't have focus
|
||||
self.__isSelectable = false;
|
||||
ionic.tap.cloneFocusedInput(self.__container);
|
||||
ionic.tap.cloneFocusedInput(container, self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,13 +734,13 @@ ionic.views.Scroll = ionic.views.View.inherit({
|
||||
self.__enableScrollY = true;
|
||||
|
||||
if( !self.__isDragging && !self.__isDecelerating && !self.__isAnimating ) {
|
||||
ionic.tap.removeClonedInputs(self.__container);
|
||||
ionic.tap.removeClonedInputs(container, self);
|
||||
}
|
||||
};
|
||||
|
||||
self.options.orgScrollingComplete = self.options.scrollingComplete;
|
||||
self.options.scrollingComplete = function() {
|
||||
ionic.tap.removeClonedInputs(self.__container);
|
||||
ionic.tap.removeClonedInputs(container, self);
|
||||
self.options.orgScrollingComplete();
|
||||
};
|
||||
|
||||
|
||||
@@ -261,9 +261,9 @@ textarea {
|
||||
input[disabled],
|
||||
select[disabled],
|
||||
textarea[disabled],
|
||||
input[readonly],
|
||||
select[readonly],
|
||||
textarea[readonly] {
|
||||
input[readonly]:not(.cloned-text-input),
|
||||
textarea[readonly]:not(.cloned-text-input),
|
||||
select[readonly] {
|
||||
background-color: $input-bg-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Inputs</title>
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, height=device-height">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="stylesheet" href="../../dist/css/ionic.css">
|
||||
<style>
|
||||
input,
|
||||
@@ -24,21 +26,46 @@
|
||||
textarea.cloned-text-input {
|
||||
background: red !important;
|
||||
}
|
||||
|
||||
header .item {
|
||||
background: lightblue !important;
|
||||
}
|
||||
body {
|
||||
background: purple;
|
||||
}
|
||||
.pane {
|
||||
background: black;
|
||||
}
|
||||
.scroll-content {
|
||||
background: pink;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
.scroll {
|
||||
background: lightgreen;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border: 1px solid blue !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body ng-controller="AppCtrl">
|
||||
|
||||
<div id="logs" style="position:fixed; top:0; left:0; z-index:9999; background: #eee; font-size:8px; line-height:10px; display:none; "></div>
|
||||
<div id="logs" style="position:fixed; top:0; left:0; z-index:9999; background: #eee; font-size:8px; line-height:10px; display:block; "></div>
|
||||
|
||||
<ion-view id="view">
|
||||
<header>
|
||||
<ion-header-bar>
|
||||
<label class="item item-input">
|
||||
<span class="input-label">Header</span>
|
||||
<input type="text" value="header input">
|
||||
</label>
|
||||
</header>
|
||||
</ion-header-bar>
|
||||
|
||||
<ion-content class="has-header padding">
|
||||
<ion-content class="padding">
|
||||
|
||||
<form ng-submit="formSubmit()">
|
||||
|
||||
<div class="list">
|
||||
<label class="item item-input">
|
||||
@@ -128,10 +155,16 @@
|
||||
<textarea id="textarea"></textarea>
|
||||
</label>
|
||||
|
||||
<button class="button button-block button-energized" ng-click="openModal()">
|
||||
<button type="button" class="button button-block button-energized" ng-click="openModal()">
|
||||
Open Modal
|
||||
</button>
|
||||
|
||||
<button type="submit" class="button button-block button-calm">
|
||||
Submit!
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<p>
|
||||
<a href="clickTests.html">Click Tests</a> -
|
||||
<a href="tapInputs.html">Tap Inputs</a> -
|
||||
@@ -220,6 +253,10 @@
|
||||
focusFirstInput: true
|
||||
});
|
||||
|
||||
$scope.formSubmit = function(){
|
||||
alert('SUBMIT!');
|
||||
};
|
||||
|
||||
})
|
||||
|
||||
.controller('ModalCtrl', function($scope) {
|
||||
@@ -233,6 +270,11 @@
|
||||
var timeId;
|
||||
var consoleDebug = console.debug;
|
||||
|
||||
function getTime() {
|
||||
var d = new Date();
|
||||
return d.getMilliseconds();
|
||||
}
|
||||
|
||||
console.debug = function() {
|
||||
index++;
|
||||
var msg = [];
|
||||
@@ -240,6 +282,7 @@
|
||||
for (var i = 0, j = arguments.length; i < j; i++){
|
||||
msg.push(arguments[i]);
|
||||
}
|
||||
//msg.push(getTime());
|
||||
|
||||
msg = msg.join(', ');
|
||||
|
||||
|
||||
204
test/unit/utils/keyboard.unit.js
Normal file
204
test/unit/utils/keyboard.unit.js
Normal file
@@ -0,0 +1,204 @@
|
||||
|
||||
/*
|
||||
|
||||
Physical Device Testing Scenarios
|
||||
---------------------------------
|
||||
- focusing inputs below the keyboard should scroll them into the middle of the view
|
||||
- focusing inputs that are above the keyboard should not scroll, but still resize the scrollable content area
|
||||
- focusing inputs should resize the scroll view so the user can scroll to inputs at the bottom of the page
|
||||
- clicking the label of an input should focus that input
|
||||
- focusing an input that is mostly offscreen should scroll into view using js scrolling, not the browser scrolling it into view
|
||||
- focusing an input while another input already has focus should not (visibly) close and re-open the keyboard
|
||||
- focusing an input that is above the keyboard while another input already has focus should not do anything
|
||||
- focusing an input that is below the keyboard while another input already has focus should scroll it into view
|
||||
- the header should not move when an input is focused
|
||||
- entering an input on a popup or modal should resize and un-resize that scrollview
|
||||
- opening a popup or a modal while the keyboard is up should un-resize the scrollview before opening the modal or popup
|
||||
- changing the orientation of the device should not break any of the above^
|
||||
- quickly tap different text inputs then end up tapping an element that isn't a text input, the scroll resize should go away
|
||||
|
||||
- focusing inputs at the bottom of the page should scroll into view normally (broken on iOS 7.0 w/o height meta tag)
|
||||
- on iOS in safari, shrinking the view should account for the button-bar at the bottom (currently not working)
|
||||
|
||||
Tentative:
|
||||
- height=device-height not needed on iOS 6.1
|
||||
- height=device-height needed on iOS 7.0 Cordova
|
||||
** without it, fires 4 resize events when the keyboard comes up, and the scroll view resizes incorrectly, with it, does not fire resize events? **
|
||||
- height=device-height not needed on iOS 7.1
|
||||
|
||||
|
||||
Tested On
|
||||
-----------------------
|
||||
- iOS 7.1 Safari
|
||||
- iOS 7.1 Cordova
|
||||
- iOS 7.0 Safari
|
||||
- iOS 7.0 Cordova
|
||||
- iOS 6.1 Safari
|
||||
- iOS 6.1 Cordova
|
||||
- Android 4.4 Browser
|
||||
- Android 4.4 Cordova
|
||||
- Android 4.2 Browser
|
||||
- Android 4.2 Cordova
|
||||
|
||||
|
||||
|
||||
Notes:
|
||||
---------------------------------
|
||||
iOS 7 keyboard is 216px tall without the accessory bar
|
||||
iOS 7 keyboard is 260px tall with the accessory bar
|
||||
|
||||
*/
|
||||
|
||||
|
||||
describe('Ionic Keyboard', function() {
|
||||
var window;
|
||||
|
||||
beforeEach(inject(function($window) {
|
||||
window = $window;
|
||||
window._setTimeout = window.setTimeout;
|
||||
window.setTimeout = function(){};
|
||||
_activeElement = null; // the element which has focus
|
||||
window.cordova = undefined;
|
||||
window.device = undefined;
|
||||
ionic.Platform.ua = '';
|
||||
ionic.Platform.platforms = null;
|
||||
ionic.Platform.setPlatform('');
|
||||
ionic.Platform.setVersion('');
|
||||
ionic.keyboard.isOpen = false;
|
||||
}));
|
||||
|
||||
afterEach(function(){
|
||||
window.setTimeout = window._setTimeout;
|
||||
});
|
||||
|
||||
it('Should keyboardShow', function(){
|
||||
var element = document.createElement('textarea');
|
||||
var elementTop = 100;
|
||||
var elementBottom = 200;
|
||||
var keyboardHeight = 200;
|
||||
var deviceHeight = 500;
|
||||
var details = keyboardShow(element, elementTop, elementBottom, deviceHeight, keyboardHeight);
|
||||
|
||||
expect( details.keyboardHeight ).toEqual(200);
|
||||
});
|
||||
|
||||
it('Should keyboardIsOverWebView()=false if Android and not isWebView', function(){
|
||||
// Android browser places the keyboard on top of the content and doesn't resize the window
|
||||
ionic.Platform.setPlatform('Android');
|
||||
expect( ionic.Platform.isAndroid() ).toEqual(true);
|
||||
expect( ionic.Platform.isWebView() ).toEqual(false);
|
||||
|
||||
expect( ionic.Platform.isIOS() ).toEqual(false);
|
||||
|
||||
expect( keyboardIsOverWebView() ).toEqual(true);
|
||||
});
|
||||
|
||||
it('Should keyboardIsOverWebView()=false if Android and isWebView', function(){
|
||||
// Android webview gets shrunk by cordova and the keyboard fills the gap
|
||||
ionic.Platform.setPlatform('Android');
|
||||
window.cordova = {};
|
||||
expect( ionic.Platform.isAndroid() ).toEqual(true);
|
||||
expect( ionic.Platform.isWebView() ).toEqual(true);
|
||||
|
||||
expect( keyboardIsOverWebView() ).toEqual(false);
|
||||
});
|
||||
|
||||
it('Should keyboardIsOverWebView()=true if iOS 7.0 or greater', function(){
|
||||
ionic.Platform.setPlatform('iOS');
|
||||
ionic.Platform.setVersion('7.0');
|
||||
expect( ionic.Platform.isAndroid() ).toEqual(false);
|
||||
expect( ionic.Platform.isIOS() ).toEqual(true);
|
||||
|
||||
expect( keyboardIsOverWebView() ).toEqual(true);
|
||||
});
|
||||
|
||||
it('Should keyboardIsOverWebView()=true if less than iOS 7.0', function(){
|
||||
ionic.Platform.setPlatform('iOS');
|
||||
ionic.Platform.setVersion('6.0');
|
||||
expect( ionic.Platform.isAndroid() ).toEqual(false);
|
||||
expect( ionic.Platform.isIOS() ).toEqual(true);
|
||||
|
||||
expect( keyboardIsOverWebView() ).toEqual(true);
|
||||
});
|
||||
|
||||
it('Should keyboardHasPlugin', function() {
|
||||
expect( keyboardHasPlugin() ).toEqual(false);
|
||||
|
||||
window.cordova = {};
|
||||
expect( keyboardHasPlugin() ).toEqual(false);
|
||||
|
||||
window.cordova.plugins = {};
|
||||
expect( keyboardHasPlugin() ).toEqual(false);
|
||||
|
||||
window.cordova.plugins.Keyboard = {};
|
||||
expect( keyboardHasPlugin() ).toEqual(true);
|
||||
});
|
||||
|
||||
it('keyboardGetHeight() should = DEFAULT_KEYBOARD_HEIGHT if no plugin or resized view', function(){
|
||||
expect( keyboardGetHeight() ).toEqual(275);
|
||||
});
|
||||
|
||||
it('keyboardUpdateViewportHeight() should update when window.innerHeight > keyboardViewportHeight', function(){
|
||||
window.innerHeight = 460;
|
||||
keyboardViewportHeight = 320;
|
||||
keyboardUpdateViewportHeight();
|
||||
|
||||
expect( keyboardViewportHeight ).toEqual(460);
|
||||
});
|
||||
|
||||
it('keyboardUpdateViewportHeight() should not update when window.innerHeight < keyboardViewportHeight', function(){
|
||||
window.innerHeight = 100;
|
||||
keyboardViewportHeight = 320;
|
||||
keyboardUpdateViewportHeight();
|
||||
|
||||
expect( keyboardViewportHeight ).toEqual(320);
|
||||
});
|
||||
|
||||
it('Should scroll input into view if it is under the keyboard', function(){
|
||||
var element = document.createElement('textarea');
|
||||
var elementTop = 300;
|
||||
var elementBottom = 400;
|
||||
var keyboardHeight = 200;
|
||||
var deviceHeight = 260;
|
||||
var details = keyboardShow(element, elementTop, elementBottom, deviceHeight, keyboardHeight);
|
||||
|
||||
expect( details.isElementUnderKeyboard ).toEqual(true);
|
||||
});
|
||||
|
||||
it('Should not scroll input into view if it is not under the keyboard', function(){
|
||||
var element = document.createElement('textarea');
|
||||
var elementTop = 100;
|
||||
var elementBottom = 200;
|
||||
var keyboardHeight = 200;
|
||||
var deviceHeight = 500;
|
||||
var details = keyboardShow(element, elementTop, elementBottom, deviceHeight, keyboardHeight);
|
||||
|
||||
expect( details.isElementUnderKeyboard ).toEqual(false);
|
||||
});
|
||||
|
||||
it('Should not subtract the keyboard height from the contentHeight if not keyboardIsOverWebView()', function(){
|
||||
var element = document.createElement('textarea');
|
||||
var elementTop = 300;
|
||||
var elementBottom = 400;
|
||||
var keyboardHeight = 200;
|
||||
var deviceHeight = 260;
|
||||
var details = keyboardShow(element, elementTop, elementBottom, deviceHeight, keyboardHeight);
|
||||
|
||||
expect( details.contentHeight ).toEqual(260);
|
||||
});
|
||||
|
||||
it('Should subtract the keyboard height from the contentHeight if keyboardIsOverWebView()', function(){
|
||||
ionic.Platform.setPlatform('iOS');
|
||||
ionic.Platform.setVersion('7.1');
|
||||
|
||||
var element = document.createElement('textarea');
|
||||
var elementTop = 300;
|
||||
var elementBottom = 400;
|
||||
var keyboardHeight = 200;
|
||||
var deviceHeight = 568;
|
||||
var details = keyboardShow(element, elementTop, elementBottom, deviceHeight, keyboardHeight);
|
||||
|
||||
expect( details.contentHeight ).toEqual(368);
|
||||
});
|
||||
|
||||
});
|
||||
1148
test/unit/utils/tap.unit.js
Normal file
1148
test/unit/utils/tap.unit.js
Normal file
File diff suppressed because it is too large
Load Diff
125
test/unit/utils/viewport.unit.js
Normal file
125
test/unit/utils/viewport.unit.js
Normal file
@@ -0,0 +1,125 @@
|
||||
|
||||
/*
|
||||
|
||||
iOS 7.1 Cordova with AND without viewport height DOES resize, DOES NOT fire resize event
|
||||
iOS 7.1 Safari with AND without viewport height DOES NOT resize
|
||||
|
||||
iOS 7.0 Cordova with viewport height DOES resize, DOES fire resize event
|
||||
iOS 7.0 Cordova without viewport height DOES resize, DOES NOT fire resize event
|
||||
iOS 7.0 Safari with AND without viewport height DOES NOT resize
|
||||
|
||||
iOS 6.1 Cordova with AND without viewport height DOES NOT resize
|
||||
iOS 6.1 Safari without viewport height DOES NOT resize
|
||||
|
||||
NOTES:
|
||||
-iOS 7.1 Safari with viewport height screws up ionic layout
|
||||
-iOS 7.0 Safari with viewport height, the scroll view does not resize properly on keyboardhide
|
||||
-iOS 7.0 Cordova without viewport height, scroll view does not resize properly switching inputs at bottom of page
|
||||
-iOS 6.1 Cordova and Safari don't work well with viewport height
|
||||
|
||||
RECOMMENDATIONS:
|
||||
-iOS 7.1 Cordova no viewport height, keyboard is not over webview
|
||||
-iOS 7.1 Safari no viewport height, keyboard is over webview
|
||||
|
||||
-iOS 7.0 Cordova yes viewport height, keyboard is not over webview
|
||||
-iOS 7.0 Safari no viewport height, keyboard is over webview
|
||||
|
||||
-iOS 6.1 Cordova no viewport height, keyboard is over webview
|
||||
-iOS 6.1 Safari no viewport height, keyboard is over webview
|
||||
|
||||
*/
|
||||
|
||||
|
||||
describe('Ionic Viewport', function() {
|
||||
var window, vportTag;
|
||||
|
||||
beforeEach(inject(function($window) {
|
||||
window = $window;
|
||||
window._setTimeout = window.setTimeout;
|
||||
window.setTimeout = function(){};
|
||||
_activeElement = null; // the element which has focus
|
||||
window.cordova = undefined;
|
||||
window.device = undefined;
|
||||
ionic.Platform.ua = '';
|
||||
ionic.Platform.platforms = null;
|
||||
ionic.Platform.setPlatform('');
|
||||
ionic.Platform.setVersion('');
|
||||
ionic.keyboard.isOpen = false;
|
||||
viewportProperties = {};
|
||||
|
||||
vportTag = document.createElement('meta');
|
||||
vportTag.setAttribute('name', 'viewport');
|
||||
document.head.appendChild(vportTag);
|
||||
}));
|
||||
|
||||
afterEach(function(){
|
||||
window.setTimeout = window._setTimeout;
|
||||
if(vportTag) vportTag.parentNode.removeChild(vportTag);
|
||||
});
|
||||
|
||||
it('Should have height=device-height for iOS 7+ on webview', function(){
|
||||
ionic.Platform.setPlatform('iOS');
|
||||
ionic.Platform.setVersion('7.0');
|
||||
expect( ionic.Platform.isAndroid() ).toEqual(false);
|
||||
expect( ionic.Platform.isIOS() ).toEqual(true);
|
||||
|
||||
//so isWebView() is true
|
||||
window.cordova = {};
|
||||
|
||||
viewportLoadTag();
|
||||
expect( viewportProperties.height ).toEqual('device-height');
|
||||
});
|
||||
|
||||
it('Should not have height=device-height for iOS 7+ on browser', function(){
|
||||
ionic.Platform.setPlatform('iOS');
|
||||
ionic.Platform.setVersion('7.0');
|
||||
expect( ionic.Platform.isAndroid() ).toEqual(false);
|
||||
expect( ionic.Platform.isIOS() ).toEqual(true);
|
||||
|
||||
viewportLoadTag();
|
||||
expect( viewportProperties.height ).not.toEqual('device-height');
|
||||
});
|
||||
|
||||
it('Should have height=device-height for Android on webview', function(){
|
||||
ionic.Platform.setPlatform('Android');
|
||||
expect( ionic.Platform.isAndroid() ).toEqual(true);
|
||||
expect( ionic.Platform.isIOS() ).toEqual(false);
|
||||
|
||||
//so isWebView() is true
|
||||
window.cordova = {};
|
||||
|
||||
viewportLoadTag();
|
||||
expect( viewportProperties.height ).toEqual('device-height');
|
||||
});
|
||||
|
||||
it('Should not have height=device-height for Android on browser', function(){
|
||||
ionic.Platform.setPlatform('Android');
|
||||
expect( ionic.Platform.isAndroid() ).toEqual(true);
|
||||
expect( ionic.Platform.isIOS() ).toEqual(false);
|
||||
|
||||
viewportLoadTag();
|
||||
expect( viewportProperties.height ).not.toEqual('device-height');
|
||||
});
|
||||
|
||||
it('Should not re-add height=device-height for webview if its already there', function(){
|
||||
ionic.Platform.setPlatform('ios');
|
||||
window.cordova = {};
|
||||
var originalViewport = ' initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, height=device-height ';
|
||||
vportTag.setAttribute('content', originalViewport);
|
||||
viewportLoadTag();
|
||||
|
||||
// if it was changed the spaces would have been removed
|
||||
expect( vportTag.content ).toEqual(originalViewport);
|
||||
});
|
||||
|
||||
it('Should not update the viewport if its not a webview and height=device-height wasnt already in', function(){
|
||||
ionic.Platform.setPlatform('ios');
|
||||
var originalViewport = ' initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width ';
|
||||
vportTag.setAttribute('content', originalViewport);
|
||||
viewportLoadTag();
|
||||
|
||||
// if it was changed the spaces would have been removed
|
||||
expect( vportTag.content ).toEqual(originalViewport);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -34,4 +34,36 @@ describe('Scroll View', function() {
|
||||
expect(sc.children[1].classList.contains('scroll-bar')).toBe(true);
|
||||
expect(sc.children[2].classList.contains('scroll-bar')).toBe(true);
|
||||
});
|
||||
|
||||
it('Should resize when the keyboard is showing', function() {
|
||||
var element = document.createElement('textarea');
|
||||
s.appendChild(element);
|
||||
document.body.appendChild(sc);
|
||||
|
||||
var sv = new ionic.views.Scroll({
|
||||
el: sc,
|
||||
});
|
||||
|
||||
var scHeight = 500;
|
||||
sc.style.height = scHeight + "px";
|
||||
sc.style.display = "block";
|
||||
|
||||
var keyboardHeight = 200;
|
||||
details = {
|
||||
contentHeight: 260,
|
||||
elementBottom: 400,
|
||||
elementTop: 300,
|
||||
isElementUnderKeyboard: true,
|
||||
keyboardHeight: keyboardHeight,
|
||||
keyboardTopOffset: 40,
|
||||
target: element,
|
||||
}
|
||||
|
||||
expect( sv.isScrolledIntoView ).toBeFalsy();
|
||||
ionic.trigger('scrollChildIntoView', details, true);
|
||||
expect( sv.isScrolledIntoView ).toEqual(true);
|
||||
expect( sc.style.height ).toEqual(scHeight - keyboardHeight + "px");
|
||||
expect( sc.clientHeight ).toEqual(scHeight - keyboardHeight);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user