@ionic/core
Version:
Base components for Ionic
262 lines (261 loc) • 11.7 kB
JavaScript
/*!
* (C) Ionic http://ionicframework.com - MIT License
*/
import { win } from "../../browser/index";
import { getScrollElement, scrollByPoint } from "../../content";
import { raf } from "../../helpers";
import { KeyboardResize } from "../../native/keyboard";
import { relocateInput, SCROLL_AMOUNT_PADDING } from "./common";
import { getScrollData } from "./scroll-data";
import { setScrollPadding, setClearScrollPaddingListener } from "./scroll-padding";
let currentPadding = 0;
const SKIP_SCROLL_ASSIST = 'data-ionic-skip-scroll-assist';
export const enableScrollAssist = (componentEl, inputEl, contentEl, footerEl, keyboardHeight, enableScrollPadding, keyboardResize, disableClonedInput = false) => {
/**
* Scroll padding should only be added if:
* 1. The global scrollPadding config option
* is set to true.
* 2. The native keyboard resize mode is either "none"
* (keyboard overlays webview) or undefined (resize
* information unavailable)
* Resize info is available on Capacitor 4+
*/
const addScrollPadding = enableScrollPadding && (keyboardResize === undefined || keyboardResize.mode === KeyboardResize.None);
/**
* This tracks whether or not the keyboard has been
* presented for a single focused text field. Note
* that it does not track if the keyboard is open
* in general such as if the keyboard is open for
* a different focused text field.
*/
let hasKeyboardBeenPresentedForTextField = false;
/**
* When adding scroll padding we need to know
* how much of the viewport the keyboard obscures.
* We do this by subtracting the keyboard height
* from the platform height.
*
* If we compute this value when switching between
* inputs then the webview may already be resized.
* At this point, `win.innerHeight` has already accounted
* for the keyboard meaning we would then subtract
* the keyboard height again. This will result in the input
* being scrolled more than it needs to.
*/
const platformHeight = win !== undefined ? win.innerHeight : 0;
/**
* Scroll assist is run when a text field
* is focused. However, it may need to
* re-run when the keyboard size changes
* such that the text field is now hidden
* underneath the keyboard.
* This function re-runs scroll assist
* when that happens.
*
* One limitation of this is on a web browser
* where native keyboard APIs do not have cross-browser
* support. `ionKeyboardDidShow` relies on the Visual Viewport API.
* This means that if the keyboard changes but does not change
* geometry, then scroll assist will not re-run even if
* the user has scrolled the text field under the keyboard.
* This is not a problem when running in Cordova/Capacitor
* because `ionKeyboardDidShow` uses the native events
* which fire every time the keyboard changes.
*/
const keyboardShow = (ev) => {
/**
* If the keyboard has not yet been presented
* for this text field then the text field has just
* received focus. In that case, the focusin listener
* will run scroll assist.
*/
if (hasKeyboardBeenPresentedForTextField === false) {
hasKeyboardBeenPresentedForTextField = true;
return;
}
/**
* Otherwise, the keyboard has already been presented
* for the focused text field.
* This means that the keyboard likely changed
* geometry, and we need to re-run scroll assist.
* This can happen when the user rotates their device
* or when they switch keyboards.
*
* Make sure we pass in the computed keyboard height
* rather than the estimated keyboard height.
*
* Since the keyboard is already open then we do not
* need to wait for the webview to resize, so we pass
* "waitForResize: false".
*/
jsSetFocus(componentEl, inputEl, contentEl, footerEl, ev.detail.keyboardHeight, addScrollPadding, disableClonedInput, platformHeight, false);
};
/**
* Reset the internal state when the text field loses focus.
*/
const focusOut = () => {
hasKeyboardBeenPresentedForTextField = false;
win === null || win === void 0 ? void 0 : win.removeEventListener('ionKeyboardDidShow', keyboardShow);
componentEl.removeEventListener('focusout', focusOut);
};
/**
* When the input is about to receive
* focus, we need to move it to prevent
* mobile Safari from adjusting the viewport.
*/
const focusIn = async () => {
/**
* Scroll assist should not run again
* on inputs that have been manually
* focused inside of the scroll assist
* implementation.
*/
if (inputEl.hasAttribute(SKIP_SCROLL_ASSIST)) {
inputEl.removeAttribute(SKIP_SCROLL_ASSIST);
return;
}
jsSetFocus(componentEl, inputEl, contentEl, footerEl, keyboardHeight, addScrollPadding, disableClonedInput, platformHeight);
win === null || win === void 0 ? void 0 : win.addEventListener('ionKeyboardDidShow', keyboardShow);
componentEl.addEventListener('focusout', focusOut);
};
componentEl.addEventListener('focusin', focusIn);
return () => {
componentEl.removeEventListener('focusin', focusIn);
win === null || win === void 0 ? void 0 : win.removeEventListener('ionKeyboardDidShow', keyboardShow);
componentEl.removeEventListener('focusout', focusOut);
};
};
/**
* Use this function when you want to manually
* focus an input but not have scroll assist run again.
*/
const setManualFocus = (el) => {
/**
* If element is already focused then
* a new focusin event will not be dispatched
* to remove the SKIL_SCROLL_ASSIST attribute.
*/
if (document.activeElement === el) {
return;
}
el.setAttribute(SKIP_SCROLL_ASSIST, 'true');
el.focus();
};
const jsSetFocus = async (componentEl, inputEl, contentEl, footerEl, keyboardHeight, enableScrollPadding, disableClonedInput = false, platformHeight = 0, waitForResize = true) => {
if (!contentEl && !footerEl) {
return;
}
const scrollData = getScrollData(componentEl, (contentEl || footerEl), keyboardHeight, platformHeight);
if (contentEl && Math.abs(scrollData.scrollAmount) < 4) {
// the text input is in a safe position that doesn't
// require it to be scrolled into view, just set focus now
setManualFocus(inputEl);
/**
* Even though the input does not need
* scroll assist, we should preserve the
* the scroll padding as users could be moving
* focus from an input that needs scroll padding
* to an input that does not need scroll padding.
* If we remove the scroll padding now, users will
* see the page jump.
*/
if (enableScrollPadding && contentEl !== null) {
setScrollPadding(contentEl, currentPadding);
setClearScrollPaddingListener(inputEl, contentEl, () => (currentPadding = 0));
}
return;
}
// temporarily move the focus to the focus holder so the browser
// doesn't freak out while it's trying to get the input in place
// at this point the native text input still does not have focus
relocateInput(componentEl, inputEl, true, scrollData.inputSafeY, disableClonedInput);
setManualFocus(inputEl);
/**
* Relocating/Focusing input causes the
* click event to be cancelled, so
* manually fire one here.
*/
raf(() => componentEl.click());
/**
* If enabled, we can add scroll padding to
* the bottom of the content so that scroll assist
* has enough room to scroll the input above
* the keyboard.
*/
if (enableScrollPadding && contentEl) {
currentPadding = scrollData.scrollPadding;
setScrollPadding(contentEl, currentPadding);
}
if (typeof window !== 'undefined') {
let scrollContentTimeout;
const scrollContent = async () => {
// clean up listeners and timeouts
if (scrollContentTimeout !== undefined) {
clearTimeout(scrollContentTimeout);
}
window.removeEventListener('ionKeyboardDidShow', doubleKeyboardEventListener);
window.removeEventListener('ionKeyboardDidShow', scrollContent);
// scroll the input into place
if (contentEl) {
await scrollByPoint(contentEl, 0, scrollData.scrollAmount, scrollData.scrollDuration);
}
// the scroll view is in the correct position now
// give the native text input focus
relocateInput(componentEl, inputEl, false, scrollData.inputSafeY);
// ensure this is the focused input
setManualFocus(inputEl);
/**
* When the input is about to be blurred
* we should set a timeout to remove
* any scroll padding.
*/
if (enableScrollPadding) {
setClearScrollPaddingListener(inputEl, contentEl, () => (currentPadding = 0));
}
};
const doubleKeyboardEventListener = () => {
window.removeEventListener('ionKeyboardDidShow', doubleKeyboardEventListener);
window.addEventListener('ionKeyboardDidShow', scrollContent);
};
if (contentEl) {
const scrollEl = await getScrollElement(contentEl);
/**
* scrollData will only consider the amount we need
* to scroll in order to properly bring the input
* into view. It will not consider the amount
* we can scroll in the content element.
* As a result, scrollData may request a greater
* scroll position than is currently available
* in the DOM. If this is the case, we need to
* wait for the webview to resize/the keyboard
* to show in order for additional scroll
* bandwidth to become available.
*/
const totalScrollAmount = scrollEl.scrollHeight - scrollEl.clientHeight;
if (waitForResize && scrollData.scrollAmount > totalScrollAmount - scrollEl.scrollTop) {
/**
* On iOS devices, the system will show a "Passwords" bar above the keyboard
* after the initial keyboard is shown. This prevents the webview from resizing
* until the "Passwords" bar is shown, so we need to wait for that to happen first.
*/
if (inputEl.type === 'password') {
// Add 50px to account for the "Passwords" bar
scrollData.scrollAmount += SCROLL_AMOUNT_PADDING;
window.addEventListener('ionKeyboardDidShow', doubleKeyboardEventListener);
}
else {
window.addEventListener('ionKeyboardDidShow', scrollContent);
}
/**
* This should only fire in 2 instances:
* 1. The app is very slow.
* 2. The app is running in a browser on an old OS
* that does not support Ionic Keyboard Events
*/
scrollContentTimeout = setTimeout(scrollContent, 1000);
return;
}
}
scrollContent();
}
};