diff --git a/packages/core/src/components/app/app.tsx b/packages/core/src/components/app/app.tsx index d8e7d40aae..d4a7039a7c 100644 --- a/packages/core/src/components/app/app.tsx +++ b/packages/core/src/components/app/app.tsx @@ -26,12 +26,13 @@ export class App { @State() modeCode: string; @State() hoverCSS = false; - @State() useRouter = false; @Prop({ context: 'config' }) config: Config; externalNavPromise: void | Promise = null; externalNavOccuring = false; + didScroll = false; + /** * Returns the promise set by an external navigation system @@ -75,7 +76,6 @@ export class App { componentWillLoad() { this.modeCode = this.config.get('mode'); - this.useRouter = this.config.getBoolean('useRouter', false); this.hoverCSS = this.config.getBoolean('hoverCSS', false); } @@ -124,6 +124,7 @@ export class App { @Method() setScrolling() { this.scrollTime = Date.now() + ACTIVE_SCROLLING_TIME; + this.didScroll = true; } @Method() diff --git a/packages/core/src/components/input/input-device-utils.ts b/packages/core/src/components/input/input-device-utils.ts new file mode 100644 index 0000000000..aeae6c2a7b --- /dev/null +++ b/packages/core/src/components/input/input-device-utils.ts @@ -0,0 +1,267 @@ +import { assert } from "../../utils/helpers"; +import { CSS_PROP } from "../animation-controller/constants"; +import { App } from "../.."; + +const SCROLL_DATA_MAP = new WeakMap(); +const SCROLL_ASSIST_SPEED = 0.3; + +export interface ScrollData { + scrollAmount: number; + scrollPadding: number; + scrollDuration: number; +} + +export function calcScrollData( + inputRect: ClientRect, + contentRect: ClientRect, + keyboardHeight: number, + plaformHeight: number +): ScrollData { + // compute input's Y values relative to the body + const inputTop = inputRect.top; + const inputBottom = inputRect.bottom; + + // compute safe area + const safeAreaTop = contentRect.top; + const safeAreaBottom = Math.min(contentRect.bottom, plaformHeight - keyboardHeight); + + // figure out if each edge of teh input is within the safe area + const distanceToBottom = safeAreaBottom - inputBottom; + const distanceToTop = safeAreaTop - inputTop; + + const scrollAmount = Math.round((distanceToBottom < 0 ) + ? distanceToBottom + : (distanceToTop < 0 ) + ? distanceToTop + : 0); + + const distance = Math.abs(scrollAmount); + const duration = distance / SCROLL_ASSIST_SPEED; + const scrollDuration = Math.min(400, Math.max(150, duration)); + + return { + scrollAmount, + scrollDuration, + scrollPadding: 0, + }; +} + +function getScrollData(componentEl: HTMLElement, contentEl: HTMLElement, keyboardHeight: number): ScrollData { + if (!contentEl) { + return { + scrollAmount: 0, + scrollPadding: 0, + scrollDuration: 0, + }; + } + const scrollData = SCROLL_DATA_MAP.get(componentEl); + if (scrollData) { + return scrollData; + } + const ele = componentEl.closest('ion-item,[ion-item]') || componentEl; + const newScrollData = calcScrollData( + ele.getBoundingClientRect(), + contentEl.getBoundingClientRect(), + keyboardHeight, + window.innerHeight + ); + SCROLL_DATA_MAP.set(componentEl, newScrollData); + return newScrollData; +} + +export function enableScrollPadding(_componentEl: HTMLElement, inputEl: HTMLElement, _contentEl: HTMLElement, _keyboardHeight: number) { + console.debug('Input: enableScrollPadding'); + + const onFocus = () => { + // const scrollPadding = getScrollData(componentEl, contentEl, keyboardHeight).scrollPadding; + // content.addScrollPadding(scrollPadding); + // content.clearScrollPaddingFocusOut(); + }; + inputEl.addEventListener('focus', onFocus); + + return () => { + inputEl.removeEventListener('focus', onFocus); + } +} + +export function enableScrollMove( + componentEl: HTMLElement, + contentEl: HTMLIonContentElement, + keyboardHeight: number +) { + console.debug('Input: enableAutoScroll'); + this.ionFocus.subscribe(() => { + const scrollData = getScrollData(componentEl, contentEl, keyboardHeight) + if (Math.abs(scrollData.scrollAmount) > 4) { + contentEl.scrollBy(0, scrollData.scrollAmount); + } + }); +} + +const SKIP_BLURRING = ['INPUT', 'TEXTAREA', 'ION-INPUT', 'ION-TEXTAREA']; + +export function enableInputBlurring(app: App) { + let focused = true; + + function onFocusin() { + focused = true; + } + + document.addEventListener('focusin', onFocusin, true); + document.addEventListener('touchend', onTouchend, false); + + function onTouchend(ev: any) { + // if app did scroll return early + if (app.didScroll) { + app.didScroll = false; + return; + } + const active = document.activeElement as HTMLElement; + if (!active) { + return; + } + // only blur if the active element is a text-input or a textarea + if (SKIP_BLURRING.indexOf(active.tagName) === -1) { + return; + } + + // if the selected target is the active element, do not blur + const tapped = ev.target; + if (tapped === active) { + return; + } + if (SKIP_BLURRING.indexOf(tapped.tagName) >= 0) { + return; + } + + // skip if div is a cover + if (tapped.classList.contains('input-cover')) { + return; + } + + focused = false; + // TODO: find a better way, why 50ms? + setTimeout(() => { + if (!focused) { + active.blur(); + } + }, 50); + } + return () => { + document.removeEventListener('focusin', onFocusin, true); + document.removeEventListener('touchend', onTouchend, false); + } +} + +export function enableHideCaretOnScroll(componentEl: HTMLElement, inputEl: HTMLInputElement, scrollEl: HTMLIonScrollElement) { + + console.debug('Input: enableHideCaretOnScroll'); + + function scrollHideCaret(shouldHideCaret: boolean) { + if(isFocused(inputEl)) { + relocateInput(componentEl, inputEl, shouldHideCaret); + } + } + + const onBlur = () => relocateInput(componentEl, inputEl, false); + const hideCaret = () => scrollHideCaret(true); + const showCaret = () => scrollHideCaret(false); + + scrollEl.addEventListener('ionScrollStart', hideCaret); + scrollEl.addEventListener('ionScrollEnd',showCaret); + inputEl.addEventListener('blur', onBlur); + + return () => { + scrollEl.removeEventListener('ionScrollStart', hideCaret); + scrollEl.removeEventListener('ionScrollEnd',showCaret); + inputEl.addEventListener('ionBlur', onBlur); + }; +} + + +function removeClone(componentEl: HTMLElement, nativeInputEl: HTMLElement) { + if (componentEl && componentEl.parentElement) { + const clonedInputEles = componentEl.parentElement.querySelectorAll('.cloned-input'); + for (let i = 0; i < clonedInputEles.length; i++) { + clonedInputEles[i].parentNode.removeChild(clonedInputEles[i]); + } + + componentEl.style.pointerEvents = ''; + } + (nativeInputEl.style)[CSS_PROP.transformProp] = ''; + nativeInputEl.style.opacity = ''; +} + +function cloneInputComponent(componentEle: HTMLElement, nativeInputEle: HTMLInputElement) { + // Make sure we kill all the clones before creating new ones + // It is a defensive, removeClone() should do nothing + // removeClone(plt, srcComponentEle, srcNativeInputEle); + assert(componentEle.parentElement.querySelector('.cloned-input') === null, 'leaked cloned input'); + // given a native or