diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 46e6bc8ffa..7ccabe8029 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -20,6 +20,10 @@ import { AlertButton, AlertInput, } from './components/alert/alert'; +import { + App, + FrameworkDelegate as FrameworkDelegate2, +} from '.'; import { ElementRef, Side, @@ -38,9 +42,6 @@ import { import { SelectPopoverOption, } from './components/select-popover/select-popover'; -import { - FrameworkDelegate as FrameworkDelegate2, -} from '.'; import { DomRenderFn, HeaderFn, @@ -847,6 +848,36 @@ declare global { } +import { + DeviceHacks as IonDeviceHacks +} from './components/device-hacks/device-hacks'; + +declare global { + interface HTMLIonDeviceHacksElement extends IonDeviceHacks, HTMLStencilElement { + } + var HTMLIonDeviceHacksElement: { + prototype: HTMLIonDeviceHacksElement; + new (): HTMLIonDeviceHacksElement; + }; + interface HTMLElementTagNameMap { + "ion-device-hacks": HTMLIonDeviceHacksElement; + } + interface ElementTagNameMap { + "ion-device-hacks": HTMLIonDeviceHacksElement; + } + namespace JSX { + interface IntrinsicElements { + "ion-device-hacks": JSXElements.IonDeviceHacksAttributes; + } + } + namespace JSXElements { + export interface IonDeviceHacksAttributes extends HTMLAttributes { + app?: App; + } + } +} + + import { Events as IonEvents } from './components/events/events'; diff --git a/packages/core/src/components/app/app.tsx b/packages/core/src/components/app/app.tsx index b8f9d43a96..6289b09ec7 100644 --- a/packages/core/src/components/app/app.tsx +++ b/packages/core/src/components/app/app.tsx @@ -1,4 +1,4 @@ -import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core'; import { Config, NavEvent, OverlayController, PublicNav, PublicViewController } from '../../index'; import { getOrAppendElement } from '../../utils/helpers'; @@ -19,20 +19,23 @@ let backButtonActions: BackButtonAction[] = []; }) export class App { + private isDevice = false; + private deviceHacks = false; private scrollTime = 0; - @Element() element: HTMLElement; - @Event() exitApp: EventEmitter; - - @State() modeCode: string; - @State() hoverCSS = false; - - @Prop({ context: 'config' }) config: Config; - externalNavPromise: void | Promise = null; externalNavOccuring = false; didScroll = false; + @Element() element: HTMLElement; + @Event() exitApp: EventEmitter; + + @Prop({ context: 'config' }) config: Config; + + componentWillLoad() { + this.isDevice = this.config.getBoolean('isDevice', false); + this.deviceHacks = this.config.getBoolean('deviceHacks', false); + } /** * Returns the promise set by an external navigation system @@ -74,11 +77,6 @@ export class App { this.externalNavOccuring = status; } - componentWillLoad() { - this.modeCode = this.config.get('mode'); - this.hoverCSS = this.config.getBoolean('hoverCSS', false); - } - @Listen('body:navInit') protected registerRootNav(event: NavEvent) { rootNavs.set(event.target.getId(), event.target); @@ -232,20 +230,23 @@ export class App { } hostData() { + const mode = this.config.get('mode'); + const hoverCSS = this.config.getBoolean('hoverCSS', false); + return { class: { - [this.modeCode]: true, - 'enable-hover': this.hoverCSS + [mode]: true, + 'enable-hover': hoverCSS } }; } render() { - const isDevice = true; return [ - isDevice && , - isDevice && , , + this.deviceHacks && , + this.isDevice && , + this.isDevice && , ]; } diff --git a/packages/core/src/components/device-hacks/device-hacks.tsx b/packages/core/src/components/device-hacks/device-hacks.tsx new file mode 100644 index 0000000000..f6cc5731da --- /dev/null +++ b/packages/core/src/components/device-hacks/device-hacks.tsx @@ -0,0 +1,71 @@ +import { Component, Listen, Prop } from "@stencil/core"; +import { App, Config } from "../.."; + +import enableHideCaretOnScroll from "./hacks/hide-caret"; +import enableInputBlurring from "./hacks/input-blurring"; + +@Component({ + tag: 'ion-device-hacks', +}) +export class DeviceHacks { + + private didLoad = false; + private hideCaret = false; + private keyboardHeight = 0; + private hideCaretMap = new WeakMap(); + + @Prop({context: 'config'}) config: Config; + @Prop() app: App; + + componentDidLoad() { + this.keyboardHeight = this.config.getNumber('keyboardHeight', 200); + this.hideCaret = this.config.getBoolean('hideCaretOnScroll', true); + + const inputBlurring = this.config.getBoolean('inputBlurring', true); + if (inputBlurring) { + enableInputBlurring(this.app); + } + + // Input might be already loaded in the DOM before ion-device-hacks did. + // At this point we need to look for all the ion-inputs not registered yet + // and register them. + const inputs = Array.from(document.querySelectorAll('ion-input')); + for (let input of inputs) { + this.registerInput(input); + } + this.didLoad = true; + } + + @Listen('body:ionInputDidLoad') + protected onInputDidLoad(event: CustomEvent) { + if (this.didLoad) { + this.registerInput(event.detail); + } + } + + @Listen('body:ionInputDidUnload') + protected onInputDidUnload(event: CustomEvent) { + if (this.didLoad) { + this.unregisterInput(event.detail); + } + } + + private registerInput(componentEl: HTMLElement) { + if (this.hideCaret && !this.hideCaretMap.has(componentEl)) { + const rmFn = enableHideCaretOnScroll( + componentEl, + componentEl.querySelector('input'), + componentEl.closest('ion-scroll'), + this.keyboardHeight + ); + this.hideCaretMap.set(componentEl, rmFn); + } + } + + private unregisterInput(componentEl: HTMLElement) { + if (this.hideCaret) { + const fn = this.hideCaretMap.get(componentEl); + fn && fn(); + } + } +} diff --git a/packages/core/src/components/device-hacks/hacks/hide-caret.ts b/packages/core/src/components/device-hacks/hacks/hide-caret.ts new file mode 100644 index 0000000000..a18590b429 --- /dev/null +++ b/packages/core/src/components/device-hacks/hacks/hide-caret.ts @@ -0,0 +1,129 @@ + +const RELOCATED_KEY= '$ionRelocated'; + +export default function enableHideCaretOnScroll(componentEl: HTMLElement, inputEl: HTMLInputElement, scrollEl: HTMLIonScrollElement, keyboardHeight: number) { + if(!scrollEl || !inputEl) { + return () => {}; + } + console.debug('Input: enableHideCaretOnScroll'); + + const scrollHideCaret = (shouldHideCaret: boolean) => { + // console.log('scrollHideCaret', shouldHideCaret) + if (isFocused(inputEl)) { + relocateInput(componentEl, inputEl, keyboardHeight, shouldHideCaret); + } + }; + + const onBlur = () => relocateInput(componentEl, inputEl, keyboardHeight, false); + const hideCaret = () => scrollHideCaret(true); + const showCaret = () => scrollHideCaret(false); + + scrollEl && scrollEl.addEventListener('ionScrollStart', hideCaret); + scrollEl && 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, inputEl: 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 = ''; + } + (inputEl.style)['transform'] = ''; + inputEl.style.opacity = ''; +} + +function cloneInputComponent(componentEl: HTMLElement, inputEl: HTMLInputElement) { + // Make sure we kill all the clones before creating new ones + // It is a defensive, removeClone() should do nothing + // removeClone(plt, srcComponentEle, srcNativeInputEle); + // given a native or