mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-21 04:53:58 +08:00
fix(input): device support
This commit is contained in:
37
packages/core/src/components.d.ts
vendored
37
packages/core/src/components.d.ts
vendored
@ -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';
|
||||
|
@ -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<ExitAppEventDetail>;
|
||||
|
||||
@State() modeCode: string;
|
||||
@State() hoverCSS = false;
|
||||
|
||||
@Prop({ context: 'config' }) config: Config;
|
||||
|
||||
externalNavPromise: void | Promise<any> = null;
|
||||
externalNavOccuring = false;
|
||||
didScroll = false;
|
||||
|
||||
@Element() element: HTMLElement;
|
||||
@Event() exitApp: EventEmitter<ExitAppEventDetail>;
|
||||
|
||||
@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 && <ion-tap-click />,
|
||||
isDevice && <ion-status-tap />,
|
||||
<ion-platform />,
|
||||
this.deviceHacks && <ion-device-hacks app={this} />,
|
||||
this.isDevice && <ion-tap-click />,
|
||||
this.isDevice && <ion-status-tap />,
|
||||
<slot></slot>
|
||||
];
|
||||
}
|
||||
|
71
packages/core/src/components/device-hacks/device-hacks.tsx
Normal file
71
packages/core/src/components/device-hacks/device-hacks.tsx
Normal file
@ -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<HTMLElement, Function>();
|
||||
|
||||
@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<HTMLElement>) {
|
||||
if (this.didLoad) {
|
||||
this.registerInput(event.detail);
|
||||
}
|
||||
}
|
||||
|
||||
@Listen('body:ionInputDidUnload')
|
||||
protected onInputDidUnload(event: CustomEvent<HTMLElement>) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
129
packages/core/src/components/device-hacks/hacks/hide-caret.ts
Normal file
129
packages/core/src/components/device-hacks/hacks/hide-caret.ts
Normal file
@ -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 = '';
|
||||
}
|
||||
(<any>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 <input> or <textarea> element
|
||||
// find its parent wrapping component like <ion-input> or <ion-textarea>
|
||||
// then clone the entire component
|
||||
const parentElement = componentEl.parentElement;
|
||||
if (componentEl && parentElement) {
|
||||
// DOM READ
|
||||
const srcTop = componentEl.offsetTop;
|
||||
const srcLeft = componentEl.offsetLeft;
|
||||
const srcWidth = componentEl.offsetWidth;
|
||||
const srcHeight = componentEl.offsetHeight;
|
||||
|
||||
// DOM WRITE
|
||||
// not using deep clone so we don't pull in unnecessary nodes
|
||||
const clonedComponentEle = document.createElement('div');
|
||||
const clonedStyle = clonedComponentEle.style;
|
||||
clonedComponentEle.classList.add(...Array.from(componentEl.classList));
|
||||
clonedComponentEle.classList.add('cloned-input');
|
||||
clonedComponentEle.setAttribute('aria-hidden', 'true');
|
||||
clonedStyle.pointerEvents = 'none';
|
||||
clonedStyle.position = 'absolute';
|
||||
clonedStyle.top = srcTop + 'px';
|
||||
clonedStyle.left = srcLeft + 'px';
|
||||
clonedStyle.width = srcWidth + 'px';
|
||||
clonedStyle.height = srcHeight + 'px';
|
||||
|
||||
const clonedInputEl = document.createElement('input');
|
||||
clonedInputEl.classList.add(...Array.from(inputEl.classList));
|
||||
// Object.assign(clonedInputEl, input);
|
||||
//const clonedInputEl = <HTMLInputElement>inputEl.cloneNode(false);
|
||||
clonedInputEl.value = inputEl.value;
|
||||
clonedInputEl.type = inputEl.type;
|
||||
|
||||
clonedInputEl.tabIndex = -1;
|
||||
|
||||
clonedComponentEle.appendChild(clonedInputEl);
|
||||
parentElement.appendChild(clonedComponentEle);
|
||||
|
||||
componentEl.style.pointerEvents = 'none';
|
||||
}
|
||||
inputEl.style.transform = 'scale(0)';
|
||||
}
|
||||
|
||||
function relocateInput(
|
||||
componentEl: HTMLElement,
|
||||
inputEle: HTMLInputElement,
|
||||
_keyboardHeight: number,
|
||||
shouldRelocate: boolean
|
||||
) {
|
||||
console.log('relocateInput',shouldRelocate );
|
||||
if ((componentEl as any)[RELOCATED_KEY] === shouldRelocate) {
|
||||
return;
|
||||
}
|
||||
console.debug(`native-input, hideCaret, shouldHideCaret: ${shouldRelocate}, input value: ${inputEle.value}`);
|
||||
if (shouldRelocate) {
|
||||
// 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) invisiable by making it scale(0),
|
||||
cloneInputComponent(componentEl, inputEle);
|
||||
// TODO
|
||||
// const inputRelativeY = getScrollData(componentEl, inputEle, keyboardHeight).scrollAmount;
|
||||
// const inputRelativeY = 9999;
|
||||
// fix for #11817
|
||||
const tx = document.dir === 'rtl' ? 9999 : -9999;
|
||||
inputEle.style.transform= `translate3d(${tx}px,${0}px,0)`;
|
||||
// inputEle.style.opacity = '0';
|
||||
|
||||
} else {
|
||||
removeClone(componentEl, inputEle);
|
||||
}
|
||||
(componentEl as any)[RELOCATED_KEY] = shouldRelocate;
|
||||
}
|
||||
|
||||
function isFocused(input: HTMLInputElement): boolean {
|
||||
return input === document.activeElement;
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import { App } from "../../..";
|
||||
|
||||
const SKIP_BLURRING = ['INPUT', 'TEXTAREA', 'ION-INPUT', 'ION-TEXTAREA'];
|
||||
|
||||
export default function enableInputBlurring(app: App) {
|
||||
console.debug('Input: enableInputBlurring');
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
|
||||
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: keyboardHeight,
|
||||
};
|
||||
}
|
||||
|
||||
export function getScrollData(componentEl: HTMLElement, contentEl: HTMLElement, keyboardHeight: number): ScrollData {
|
||||
if (!contentEl) {
|
||||
return {
|
||||
scrollAmount: 0,
|
||||
scrollPadding: 0,
|
||||
scrollDuration: 0,
|
||||
};
|
||||
}
|
||||
// const scrollData = (componentEl as any)[SCROLL_DATA_KEY];
|
||||
// if (scrollData) {
|
||||
// return scrollData;
|
||||
// }
|
||||
const itemEl = <HTMLElement>componentEl.closest('ion-item,[ion-item]') || componentEl;
|
||||
const newScrollData = calcScrollData(
|
||||
itemEl.getBoundingClientRect(),
|
||||
contentEl.getBoundingClientRect(),
|
||||
keyboardHeight,
|
||||
window.innerHeight
|
||||
);
|
||||
// (componentEl as any)[SCROLL_DATA_KEY] = newScrollData;
|
||||
return newScrollData;
|
||||
}
|
35
packages/core/src/components/device-hacks/hacks/wip.ts
Normal file
35
packages/core/src/components/device-hacks/hacks/wip.ts
Normal file
@ -0,0 +1,35 @@
|
||||
|
||||
// export function enableScrollPadding(componentEl: HTMLElement, inputEl: HTMLElement, contentEl: HTMLElement, keyboardHeight: number) {
|
||||
// console.debug('Input: enableScrollPadding');
|
||||
// debugger;
|
||||
// const onFocus = () => {
|
||||
// const scrollPadding = getScrollData(componentEl, contentEl, keyboardHeight).scrollPadding;
|
||||
// contentEl.addScrollPadding(scrollPadding);
|
||||
// content.clearScrollPaddingFocusOut();
|
||||
// };
|
||||
// inputEl.addEventListener('focus', onFocus);
|
||||
|
||||
// return () => {
|
||||
// inputEl.removeEventListener('focus', onFocus);
|
||||
// };
|
||||
// }
|
||||
|
||||
// export function enableScrollMove(
|
||||
// componentEl: HTMLElement,
|
||||
// inputEl: HTMLElement,
|
||||
// contentEl: HTMLIonContentElement,
|
||||
// keyboardHeight: number
|
||||
// ) {
|
||||
// console.debug('Input: enableAutoScroll');
|
||||
// const onFocus = () => {
|
||||
// const scrollData = getScrollData(componentEl, contentEl, keyboardHeight);
|
||||
// if (Math.abs(scrollData.scrollAmount) > 4) {
|
||||
// contentEl.scrollBy(0, scrollData.scrollAmount);
|
||||
// }
|
||||
// };
|
||||
|
||||
// inputEl.addEventListener('focus', onFocus);
|
||||
// return () => {
|
||||
// inputEl.removeEventListener('focus', onFocus);
|
||||
// };
|
||||
// }
|
25
packages/core/src/components/device-hacks/readme.md
Normal file
25
packages/core/src/components/device-hacks/readme.md
Normal file
@ -0,0 +1,25 @@
|
||||
# ion-app
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
#### app
|
||||
|
||||
|
||||
|
||||
|
||||
## Attributes
|
||||
|
||||
#### app
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
@ -1,278 +0,0 @@
|
||||
import { assert } from '../../utils/helpers';
|
||||
import { CSS_PROP } from '../animation-controller/constants';
|
||||
import { App } from '../..';
|
||||
|
||||
const SCROLL_DATA_MAP = new WeakMap<HTMLElement, ScrollData>();
|
||||
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 = <HTMLElement>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,
|
||||
inputEl: HTMLElement,
|
||||
contentEl: HTMLIonContentElement,
|
||||
keyboardHeight: number
|
||||
) {
|
||||
console.debug('Input: enableAutoScroll');
|
||||
const onFocus = () => {
|
||||
const scrollData = getScrollData(componentEl, contentEl, keyboardHeight);
|
||||
if (Math.abs(scrollData.scrollAmount) > 4) {
|
||||
contentEl.scrollBy(0, scrollData.scrollAmount);
|
||||
}
|
||||
};
|
||||
|
||||
inputEl.addEventListener('focus', onFocus);
|
||||
return () => {
|
||||
inputEl.removeEventListener('focus', onFocus);
|
||||
};
|
||||
}
|
||||
|
||||
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 = '';
|
||||
}
|
||||
(<any>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);
|
||||
// given a native <input> or <textarea> element
|
||||
// find its parent wrapping component like <ion-input> or <ion-textarea>
|
||||
// then clone the entire component
|
||||
const parentElement = componentEle.parentElement;
|
||||
if (componentEle && parentElement) {
|
||||
assert(parentElement.querySelector('.cloned-input') === null, 'leaked cloned input');
|
||||
|
||||
// DOM READ
|
||||
const srcTop = componentEle.offsetTop;
|
||||
const srcLeft = componentEle.offsetLeft;
|
||||
const srcWidth = componentEle.offsetWidth;
|
||||
const srcHeight = componentEle.offsetHeight;
|
||||
|
||||
// DOM WRITE
|
||||
// not using deep clone so we don't pull in unnecessary nodes
|
||||
const clonedComponentEle = <HTMLElement>componentEle.cloneNode(false);
|
||||
const clonedStyle = clonedComponentEle.style;
|
||||
clonedComponentEle.classList.add('cloned-input');
|
||||
clonedComponentEle.setAttribute('aria-hidden', 'true');
|
||||
clonedStyle.pointerEvents = 'none';
|
||||
clonedStyle.position = 'absolute';
|
||||
clonedStyle.top = srcTop + 'px';
|
||||
clonedStyle.left = srcLeft + 'px';
|
||||
clonedStyle.width = srcWidth + 'px';
|
||||
clonedStyle.height = srcHeight + 'px';
|
||||
|
||||
const clonedNativeInputEle = <HTMLInputElement>nativeInputEle.cloneNode(false);
|
||||
clonedNativeInputEle.value = nativeInputEle.value;
|
||||
clonedNativeInputEle.tabIndex = -1;
|
||||
|
||||
clonedComponentEle.appendChild(clonedNativeInputEle);
|
||||
parentElement.appendChild(clonedComponentEle);
|
||||
|
||||
clonedComponentEle.style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
(<any>nativeInputEle.style)[CSS_PROP.transformProp] = 'scale(0)';
|
||||
}
|
||||
|
||||
function relocateInput(componentEl: HTMLElement, inputEle: HTMLInputElement, shouldRelocate: boolean) {
|
||||
|
||||
if ((componentEl as any)['$ionRelocated'] === shouldRelocate) {
|
||||
return;
|
||||
}
|
||||
console.debug(`native-input, hideCaret, shouldHideCaret: ${shouldRelocate}, input value: ${inputEle.value}`);
|
||||
if (shouldRelocate) {
|
||||
// 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) invisiable by making it scale(0),
|
||||
cloneInputComponent(componentEl, inputEle);
|
||||
// TODO
|
||||
// const inputRelativeY = this._getScrollData().inputSafeY;
|
||||
const inputRelativeY = 0;
|
||||
|
||||
// fix for #11817
|
||||
const tx = document.dir === 'rtl' ? 9999 : -9999;
|
||||
(inputEle.style as any)[CSS_PROP.transformProp] = `translate3d(${tx}px,${inputRelativeY}px,0)`;
|
||||
inputEle.style.opacity = '0';
|
||||
|
||||
} else {
|
||||
removeClone(componentEl, inputEle);
|
||||
}
|
||||
(componentEl as any)['$ionRelocated'] = shouldRelocate;
|
||||
}
|
||||
|
||||
function isFocused(input: HTMLInputElement): boolean {
|
||||
return input === document.activeElement;
|
||||
}
|
@ -85,6 +85,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// iOS Input After Label
|
||||
// --------------------------------------------------
|
||||
|
||||
.label-ios + .input .native-input,
|
||||
.label-ios + .input + .cloned-input {
|
||||
@include margin-horizontal($input-ios-by-label-margin-start, null);
|
||||
}
|
||||
|
||||
// iOS Stacked & Floating Inputs
|
||||
// --------------------------------------------------
|
||||
@ -102,14 +109,6 @@
|
||||
}
|
||||
|
||||
|
||||
// iOS Input After Label
|
||||
// --------------------------------------------------
|
||||
|
||||
.label-ios + ion-input .native-input,
|
||||
.label-ios + .input + .cloned-input {
|
||||
@include margin-horizontal($input-ios-by-label-margin-start, null);
|
||||
}
|
||||
|
||||
// iOS Clear Input Icon
|
||||
// --------------------------------------------------
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
// Input
|
||||
// --------------------------------------------------
|
||||
|
||||
ion-input {
|
||||
.input {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
@ -15,7 +15,7 @@ ion-input {
|
||||
// Input Within An Item
|
||||
// --------------------------------------------------
|
||||
|
||||
.item-input ion-input {
|
||||
.item-input .input {
|
||||
position: static;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Element, Event, EventEmitter, Prop, Watch } from '@stencil/core';
|
||||
import { Component, Event, EventEmitter, Prop, Watch, Element } from '@stencil/core';
|
||||
|
||||
import { debounceEvent } from '../../utils/helpers';
|
||||
import { createThemedClasses } from '../../utils/theme';
|
||||
@ -16,6 +16,8 @@ import { InputComponent } from './input-base';
|
||||
}
|
||||
})
|
||||
export class Input implements InputComponent {
|
||||
|
||||
private nativeInput: HTMLInputElement;
|
||||
mode: string;
|
||||
color: string;
|
||||
|
||||
@ -44,6 +46,16 @@ export class Input implements InputComponent {
|
||||
*/
|
||||
@Event() ionFocus: EventEmitter;
|
||||
|
||||
/**
|
||||
* Emitted when the input has been created.
|
||||
*/
|
||||
@Event() ionInputDidLoad: EventEmitter;
|
||||
|
||||
/**
|
||||
* Emitted when the input has been removed.
|
||||
*/
|
||||
@Event() ionInputDidUnload: EventEmitter;
|
||||
|
||||
/**
|
||||
* If the value of the type attribute is `"file"`, then this attribute will indicate the types of files that the server accepts, otherwise it will be ignored. The value must be a comma-separated list of unique content type specifiers.
|
||||
*/
|
||||
@ -200,7 +212,7 @@ export class Input implements InputComponent {
|
||||
*/
|
||||
@Watch('value')
|
||||
protected valueChanged() {
|
||||
const inputEl = this.el.querySelector('input');
|
||||
const inputEl = this.nativeInput;
|
||||
if (inputEl && inputEl.value !== this.value) {
|
||||
inputEl.value = this.value;
|
||||
}
|
||||
@ -214,6 +226,12 @@ export class Input implements InputComponent {
|
||||
if (this.type === 'password' && this.clearOnEdit !== false) {
|
||||
this.clearOnEdit = true;
|
||||
}
|
||||
this.ionInputDidLoad.emit(this.el);
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
this.nativeInput = null;
|
||||
this.ionInputDidUnload.emit(this.el);
|
||||
}
|
||||
|
||||
private emitStyle() {
|
||||
@ -288,7 +306,7 @@ export class Input implements InputComponent {
|
||||
|
||||
hasFocus(): boolean {
|
||||
// check if an input has focus or not
|
||||
return this.el && (this.el.querySelector(':focus') === this.el.querySelector('input'));
|
||||
return this.nativeInput === document.activeElement;
|
||||
}
|
||||
|
||||
hasValue(): boolean {
|
||||
@ -301,6 +319,7 @@ export class Input implements InputComponent {
|
||||
|
||||
return [
|
||||
<input
|
||||
ref={input => this.nativeInput = input as any}
|
||||
aria-disabled={this.disabled ? 'true' : false}
|
||||
accept={this.accept}
|
||||
autoCapitalize={this.autocapitalize}
|
||||
|
@ -404,6 +404,16 @@ Emitted when the input has focus.
|
||||
Emitted when the input value has changed.
|
||||
|
||||
|
||||
#### ionInputDidLoad
|
||||
|
||||
Emitted when the input has been created.
|
||||
|
||||
|
||||
#### ionInputDidUnload
|
||||
|
||||
Emitted when the input has been removed.
|
||||
|
||||
|
||||
#### ionStyle
|
||||
|
||||
Emitted when the styles change.
|
||||
|
@ -70,6 +70,11 @@
|
||||
<ion-label>Toggle</ion-label>
|
||||
<ion-toggle checked slot="end"></ion-toggle>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label fixed>Type #</ion-label>
|
||||
<div type="number" value="333" class="input input-md hydrated"><!----><input aria-disabled="false" autocapitalize="none" autocomplete="off" autocorrect="off" autofocus="false" class="native-input native-input-md" spellcheck="false" type="number"><button type="button" class="input-clear-icon" hidden=""></button></div>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<div text-center>
|
||||
|
@ -69,7 +69,7 @@ ion-item-group {
|
||||
}
|
||||
|
||||
[vertical-align-top],
|
||||
ion-input.item {
|
||||
.input.item {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
@include margin(-$list-md-margin-top, null, null, null);
|
||||
}
|
||||
|
||||
.list-md > ion-input:last-child::after {
|
||||
.list-md > .input:last-child::after {
|
||||
@include position-horizontal(0, null);
|
||||
}
|
||||
|
||||
|
@ -33,9 +33,11 @@ export const PLATFORM_CONFIGS: PlatformConfig[] = [
|
||||
{
|
||||
name: IOS,
|
||||
settings: {
|
||||
mode: IOS,
|
||||
mode: "ios",
|
||||
tabsHighlight: false,
|
||||
statusbarPadding: isCordova(),
|
||||
isDevice: true,
|
||||
deviceHacks: true,
|
||||
},
|
||||
isMatch: (url, userAgent) => isPlatformMatch(url, userAgent, IOS, [IPHONE, IPAD, 'ipod'], WINDOWS_PHONE)
|
||||
},
|
||||
@ -43,7 +45,7 @@ export const PLATFORM_CONFIGS: PlatformConfig[] = [
|
||||
{
|
||||
name: ANDROID,
|
||||
settings: {
|
||||
activator: 'ripple',
|
||||
isDevice: true,
|
||||
mode: 'md',
|
||||
},
|
||||
isMatch: (url, userAgent) => isPlatformMatch(url, userAgent, ANDROID, [ANDROID, 'silk'], WINDOWS_PHONE)
|
||||
|
@ -46,6 +46,7 @@ exports.config = {
|
||||
{ components: ['ion-toggle'] },
|
||||
{ components: ['ion-toast', 'ion-toast-controller'] },
|
||||
{ components: ['ion-tap-click', 'ion-status-tap'] },
|
||||
{ components: ['ion-device-hacks'] },
|
||||
{ components: ['ion-platform', 'ion-cordova-platform'] },
|
||||
{ components: ['ion-nav-pop'] },
|
||||
{ components: ['ion-hide-when', 'ion-show-when'] },
|
||||
|
Reference in New Issue
Block a user