123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608 |
- /*!
- * (C) Ionic http://ionicframework.com - MIT License
- */
- 'use strict';
- const index = require('./index-c8d52405.js');
- const index$1 = require('./index-31b07b9c.js');
- const helpers = require('./helpers-8a48fdea.js');
- const keyboard = require('./keyboard-0272231f.js');
- require('./index-cc858e97.js');
- require('./capacitor-c04564bf.js');
- const cloneMap = new WeakMap();
- const relocateInput = (componentEl, inputEl, shouldRelocate, inputRelativeY = 0, disabledClonedInput = false) => {
- if (cloneMap.has(componentEl) === shouldRelocate) {
- return;
- }
- if (shouldRelocate) {
- addClone(componentEl, inputEl, inputRelativeY, disabledClonedInput);
- }
- else {
- removeClone(componentEl, inputEl);
- }
- };
- const isFocused = (input) => {
- /**
- * https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode
- * Calling getRootNode on an element in standard web page will return HTMLDocument.
- * Calling getRootNode on an element inside of the Shadow DOM will return the associated ShadowRoot.
- * Calling getRootNode on an element that is not attached to a document/shadow tree will return
- * the root of the DOM tree it belongs to.
- * isFocused is used for the hide-caret utility which only considers input/textarea elements
- * that are present in the DOM, so we don't set types for that final case since it does not apply.
- */
- return input === input.getRootNode().activeElement;
- };
- const addClone = (componentEl, inputEl, inputRelativeY, disabledClonedInput = false) => {
- // this allows for the actual input to receive the focus from
- // the user's touch event, but before it receives focus, it
- // moves the actual input to a location that will not screw
- // up the app's layout, and does not allow the native browser
- // to attempt to scroll the input into place (messing up headers/footers)
- // the cloned input fills the area of where native input should be
- // while the native input fakes out the browser by relocating itself
- // before it receives the actual focus event
- // We hide the focused input (with the visible caret) invisible by making it scale(0),
- const parentEl = inputEl.parentNode;
- // DOM WRITES
- const clonedEl = inputEl.cloneNode(false);
- clonedEl.classList.add('cloned-input');
- clonedEl.tabIndex = -1;
- /**
- * Making the cloned input disabled prevents
- * Chrome for Android from still scrolling
- * the entire page since this cloned input
- * will briefly be hidden by the keyboard
- * even though it is not focused.
- *
- * This is not needed on iOS. While this
- * does not cause functional issues on iOS,
- * the input still appears slightly dimmed even
- * if we set opacity: 1.
- */
- if (disabledClonedInput) {
- clonedEl.disabled = true;
- }
- parentEl.appendChild(clonedEl);
- cloneMap.set(componentEl, clonedEl);
- const doc = componentEl.ownerDocument;
- const tx = doc.dir === 'rtl' ? 9999 : -9999;
- componentEl.style.pointerEvents = 'none';
- inputEl.style.transform = `translate3d(${tx}px,${inputRelativeY}px,0) scale(0)`;
- };
- const removeClone = (componentEl, inputEl) => {
- const clone = cloneMap.get(componentEl);
- if (clone) {
- cloneMap.delete(componentEl);
- clone.remove();
- }
- componentEl.style.pointerEvents = '';
- inputEl.style.transform = '';
- };
- /**
- * Factoring in 50px gives us some room
- * in case the keyboard shows password/autofill bars
- * asynchronously.
- */
- const SCROLL_AMOUNT_PADDING = 50;
- const enableHideCaretOnScroll = (componentEl, inputEl, scrollEl) => {
- if (!scrollEl || !inputEl) {
- return () => {
- return;
- };
- }
- const scrollHideCaret = (shouldHideCaret) => {
- if (isFocused(inputEl)) {
- relocateInput(componentEl, inputEl, shouldHideCaret);
- }
- };
- const onBlur = () => relocateInput(componentEl, inputEl, false);
- const hideCaret = () => scrollHideCaret(true);
- const showCaret = () => scrollHideCaret(false);
- helpers.addEventListener(scrollEl, 'ionScrollStart', hideCaret);
- helpers.addEventListener(scrollEl, 'ionScrollEnd', showCaret);
- inputEl.addEventListener('blur', onBlur);
- return () => {
- helpers.removeEventListener(scrollEl, 'ionScrollStart', hideCaret);
- helpers.removeEventListener(scrollEl, 'ionScrollEnd', showCaret);
- inputEl.removeEventListener('blur', onBlur);
- };
- };
- const SKIP_SELECTOR = 'input, textarea, [no-blur], [contenteditable]';
- const enableInputBlurring = () => {
- let focused = true;
- let didScroll = false;
- const doc = document;
- const onScroll = () => {
- didScroll = true;
- };
- const onFocusin = () => {
- focused = true;
- };
- const onTouchend = (ev) => {
- // if app did scroll return early
- if (didScroll) {
- didScroll = false;
- return;
- }
- const active = doc.activeElement;
- if (!active) {
- return;
- }
- // only blur if the active element is a text-input or a textarea
- if (active.matches(SKIP_SELECTOR)) {
- return;
- }
- // if the selected target is the active element, do not blur
- const tapped = ev.target;
- if (tapped === active) {
- return;
- }
- if (tapped.matches(SKIP_SELECTOR) || tapped.closest(SKIP_SELECTOR)) {
- return;
- }
- focused = false;
- // TODO FW-2796: find a better way, why 50ms?
- setTimeout(() => {
- if (!focused) {
- active.blur();
- }
- }, 50);
- };
- helpers.addEventListener(doc, 'ionScrollStart', onScroll);
- doc.addEventListener('focusin', onFocusin, true);
- doc.addEventListener('touchend', onTouchend, false);
- return () => {
- helpers.removeEventListener(doc, 'ionScrollStart', onScroll, true);
- doc.removeEventListener('focusin', onFocusin, true);
- doc.removeEventListener('touchend', onTouchend, false);
- };
- };
- const SCROLL_ASSIST_SPEED = 0.3;
- const getScrollData = (componentEl, contentEl, keyboardHeight, platformHeight) => {
- var _a;
- const itemEl = (_a = componentEl.closest('ion-item,[ion-item]')) !== null && _a !== void 0 ? _a : componentEl;
- return calcScrollData(itemEl.getBoundingClientRect(), contentEl.getBoundingClientRect(), keyboardHeight, platformHeight);
- };
- const calcScrollData = (inputRect, contentRect, keyboardHeight, platformHeight) => {
- // compute input's Y values relative to the body
- const inputTop = inputRect.top;
- const inputBottom = inputRect.bottom;
- // compute visible area
- const visibleAreaTop = contentRect.top;
- const visibleAreaBottom = Math.min(contentRect.bottom, platformHeight - keyboardHeight);
- // compute safe area
- const safeAreaTop = visibleAreaTop + 15;
- const safeAreaBottom = visibleAreaBottom - SCROLL_AMOUNT_PADDING;
- // figure out if each edge of the input is within the safe area
- const distanceToBottom = safeAreaBottom - inputBottom;
- const distanceToTop = safeAreaTop - inputTop;
- // desiredScrollAmount is the negated distance to the safe area according to our calculations.
- const desiredScrollAmount = Math.round(distanceToBottom < 0 ? -distanceToBottom : distanceToTop > 0 ? -distanceToTop : 0);
- // our calculations make some assumptions that aren't always true, like the keyboard being closed when an input
- // gets focus, so make sure we don't scroll the input above the visible area
- const scrollAmount = Math.min(desiredScrollAmount, inputTop - visibleAreaTop);
- const distance = Math.abs(scrollAmount);
- const duration = distance / SCROLL_ASSIST_SPEED;
- const scrollDuration = Math.min(400, Math.max(150, duration));
- return {
- scrollAmount,
- scrollDuration,
- scrollPadding: keyboardHeight,
- inputSafeY: -(inputTop - safeAreaTop) + 4,
- };
- };
- const PADDING_TIMER_KEY = '$ionPaddingTimer';
- /**
- * Scroll padding adds additional padding to the bottom
- * of ion-content so that there is enough scroll space
- * for an input to be scrolled above the keyboard. This
- * is needed in environments where the webview does not
- * resize when the keyboard opens.
- *
- * Example: If an input at the bottom of ion-content is
- * focused, there is no additional scrolling space below
- * it, so the input cannot be scrolled above the keyboard.
- * Scroll padding fixes this by adding padding equal to the
- * height of the keyboard to the bottom of the content.
- *
- * Common environments where this is needed:
- * - Mobile Safari: The keyboard overlays the content
- * - Capacitor/Cordova on iOS: The keyboard overlays the content
- * when the KeyboardResize mode is set to 'none'.
- */
- const setScrollPadding = (contentEl, paddingAmount, clearCallback) => {
- const timer = contentEl[PADDING_TIMER_KEY];
- if (timer) {
- clearTimeout(timer);
- }
- if (paddingAmount > 0) {
- contentEl.style.setProperty('--keyboard-offset', `${paddingAmount}px`);
- }
- else {
- contentEl[PADDING_TIMER_KEY] = setTimeout(() => {
- contentEl.style.setProperty('--keyboard-offset', '0px');
- if (clearCallback) {
- clearCallback();
- }
- }, 120);
- }
- };
- /**
- * When an input is about to be focused,
- * set a timeout to clear any scroll padding
- * on the content. Note: The clearing
- * is done on a timeout so that if users
- * are moving focus from one input to the next
- * then re-adding scroll padding to the new
- * input with cancel the timeout to clear the
- * scroll padding.
- */
- const setClearScrollPaddingListener = (inputEl, contentEl, doneCallback) => {
- const clearScrollPadding = () => {
- if (contentEl) {
- setScrollPadding(contentEl, 0, doneCallback);
- }
- };
- inputEl.addEventListener('focusout', clearScrollPadding, { once: true });
- };
- let currentPadding = 0;
- const SKIP_SCROLL_ASSIST = 'data-ionic-skip-scroll-assist';
- 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 === keyboard.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 = index.win !== undefined ? index.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;
- index.win === null || index.win === void 0 ? void 0 : index.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);
- index.win === null || index.win === void 0 ? void 0 : index.win.addEventListener('ionKeyboardDidShow', keyboardShow);
- componentEl.addEventListener('focusout', focusOut);
- };
- componentEl.addEventListener('focusin', focusIn);
- return () => {
- componentEl.removeEventListener('focusin', focusIn);
- index.win === null || index.win === void 0 ? void 0 : index.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.
- */
- helpers.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 index$1.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 index$1.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();
- }
- };
- const INPUT_BLURRING = true;
- const startInputShims = async (config, platform) => {
- /**
- * If doc is undefined then we are in an SSR environment
- * where input shims do not apply.
- */
- if (index.doc === undefined) {
- return;
- }
- const isIOS = platform === 'ios';
- const isAndroid = platform === 'android';
- /**
- * Hide Caret and Input Blurring are needed on iOS.
- * Scroll Assist and Scroll Padding are needed on iOS and Android
- * with Chrome web browser (not Chrome webview).
- */
- const keyboardHeight = config.getNumber('keyboardHeight', 290);
- const scrollAssist = config.getBoolean('scrollAssist', true);
- const hideCaret = config.getBoolean('hideCaretOnScroll', isIOS);
- /**
- * The team is evaluating if inputBlurring is still needed. As a result
- * this feature is disabled by default as of Ionic 8.0. Developers are
- * able to re-enable it temporarily. The team may remove this utility
- * if it is determined that doing so would not bring any adverse side effects.
- * TODO FW-6014 remove input blurring utility (including implementation)
- */
- const inputBlurring = config.getBoolean('inputBlurring', false);
- const scrollPadding = config.getBoolean('scrollPadding', true);
- const inputs = Array.from(index.doc.querySelectorAll('ion-input, ion-textarea'));
- const hideCaretMap = new WeakMap();
- const scrollAssistMap = new WeakMap();
- /**
- * Grab the native keyboard resize configuration
- * and pass it to scroll assist. Scroll assist requires
- * that we adjust the input right before the input
- * is about to be focused. If we called `Keyboard.getResizeMode`
- * on focusin in scroll assist, we could potentially adjust the
- * input too late since this call is async.
- */
- const keyboardResizeMode = await keyboard.Keyboard.getResizeMode();
- const registerInput = async (componentEl) => {
- await new Promise((resolve) => helpers.componentOnReady(componentEl, resolve));
- const inputRoot = componentEl.shadowRoot || componentEl;
- const inputEl = inputRoot.querySelector('input') || inputRoot.querySelector('textarea');
- const scrollEl = index$1.findClosestIonContent(componentEl);
- const footerEl = !scrollEl ? componentEl.closest('ion-footer') : null;
- if (!inputEl) {
- return;
- }
- if (!!scrollEl && hideCaret && !hideCaretMap.has(componentEl)) {
- const rmFn = enableHideCaretOnScroll(componentEl, inputEl, scrollEl);
- hideCaretMap.set(componentEl, rmFn);
- }
- /**
- * date/datetime-locale inputs on mobile devices show date picker
- * overlays instead of keyboards. As a result, scroll assist is
- * not needed. This also works around a bug in iOS <16 where
- * scroll assist causes the browser to lock up. See FW-1997.
- */
- const isDateInput = inputEl.type === 'date' || inputEl.type === 'datetime-local';
- if (!isDateInput &&
- (!!scrollEl || !!footerEl) &&
- scrollAssist &&
- !scrollAssistMap.has(componentEl)) {
- const rmFn = enableScrollAssist(componentEl, inputEl, scrollEl, footerEl, keyboardHeight, scrollPadding, keyboardResizeMode, isAndroid);
- scrollAssistMap.set(componentEl, rmFn);
- }
- };
- const unregisterInput = (componentEl) => {
- if (hideCaret) {
- const fn = hideCaretMap.get(componentEl);
- if (fn) {
- fn();
- }
- hideCaretMap.delete(componentEl);
- }
- if (scrollAssist) {
- const fn = scrollAssistMap.get(componentEl);
- if (fn) {
- fn();
- }
- scrollAssistMap.delete(componentEl);
- }
- };
- if (inputBlurring && INPUT_BLURRING) {
- enableInputBlurring();
- }
- // Input might be already loaded in the DOM before ion-device-hacks did.
- // At this point we need to look for all of the inputs not registered yet
- // and register them.
- for (const input of inputs) {
- registerInput(input);
- }
- index.doc.addEventListener('ionInputDidLoad', (ev) => {
- registerInput(ev.detail);
- });
- index.doc.addEventListener('ionInputDidUnload', (ev) => {
- unregisterInput(ev.detail);
- });
- };
- exports.startInputShims = startInputShims;
|