mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 19:21:34 +08:00
fix(input): keyboard focus/scrolling/tabbing
This commit is contained in:
@ -4,7 +4,7 @@ import {Ion} from '../ion';
|
||||
import {IonicConfig} from '../../config/config';
|
||||
import {IonicKeyboard} from '../../util/keyboard';
|
||||
import {ViewController} from '../nav/view-controller';
|
||||
import {Tab} from '../tabs/tab';
|
||||
import {Animation} from '../../animations/animation';
|
||||
import {ScrollTo} from '../../animations/scroll-to';
|
||||
|
||||
|
||||
@ -25,8 +25,10 @@ import {ScrollTo} from '../../animations/scroll-to';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ion-content',
|
||||
inputs: ['parallax'],
|
||||
template: '<scroll-content><ng-content></ng-content></scroll-content>'
|
||||
template:
|
||||
'<scroll-content>' +
|
||||
'<ng-content></ng-content>' +
|
||||
'</scroll-content>'
|
||||
})
|
||||
export class Content extends Ion {
|
||||
/**
|
||||
@ -38,7 +40,7 @@ export class Content extends Ion {
|
||||
this.scrollPadding = 0;
|
||||
this.keyboard = keyboard;
|
||||
|
||||
if(viewCtrl) {
|
||||
if (viewCtrl) {
|
||||
viewCtrl.setContent(this);
|
||||
}
|
||||
}
|
||||
@ -58,7 +60,7 @@ export class Content extends Ion {
|
||||
* @returns {Function} A function that removes the scroll handler.
|
||||
*/
|
||||
addScrollEventListener(handler) {
|
||||
if(!this.scrollElement) { return; }
|
||||
if (!this.scrollElement) { return; }
|
||||
|
||||
// ensure we're not creating duplicates
|
||||
this.scrollElement.removeEventListener('scroll', handler);
|
||||
@ -76,7 +78,7 @@ export class Content extends Ion {
|
||||
* @returns {Function} A function that removes the touchmove handler.
|
||||
*/
|
||||
addTouchMoveListener(handler) {
|
||||
if(!this.scrollElement) { return; }
|
||||
if (!this.scrollElement) { return; }
|
||||
|
||||
// ensure we're not creating duplicates
|
||||
this.scrollElement.removeEventListener('touchmove', handler);
|
||||
@ -161,17 +163,29 @@ export class Content extends Ion {
|
||||
* Adds padding to the bottom of the scroll element when the keyboard is open
|
||||
* so content below the keyboard can be scrolled into view.
|
||||
*/
|
||||
addKeyboardPadding(addPadding) {
|
||||
if (addPadding > this.scrollPadding) {
|
||||
this.scrollPadding = addPadding;
|
||||
this.scrollElement.style.paddingBottom = addPadding + 'px';
|
||||
addScrollPadding(newScrollPadding) {
|
||||
if (newScrollPadding > this.scrollPadding) {
|
||||
console.debug('addScrollPadding', newScrollPadding);
|
||||
|
||||
this.scrollPadding = newScrollPadding;
|
||||
this.scrollElement.style.paddingBottom = newScrollPadding + 'px';
|
||||
|
||||
if (!this.keyboardPromise) {
|
||||
console.debug('add scroll keyboard close callback', newScrollPadding);
|
||||
|
||||
this.keyboardPromise = this.keyboard.onClose(() => {
|
||||
console.debug('scroll keyboard closed', newScrollPadding);
|
||||
|
||||
if (this) {
|
||||
if (this.scrollPadding && this.scrollElement) {
|
||||
let close = new Animation(this.scrollElement);
|
||||
close
|
||||
.duration(150)
|
||||
.fromTo('paddingBottom', this.scrollPadding + 'px', '0px')
|
||||
.play();
|
||||
}
|
||||
|
||||
this.scrollPadding = 0;
|
||||
if (this.scrollElement) this.scrollElement.style.paddingBottom = '';
|
||||
this.keyboardPromise = null;
|
||||
}
|
||||
});
|
||||
|
@ -25,9 +25,13 @@ export class Label {
|
||||
* TODO
|
||||
* @param {IonicConfig} config
|
||||
*/
|
||||
constructor(config: IonicConfig, @Optional() textInput: TextInput) {
|
||||
this.scrollAssist = config.get('keyboardScrollAssist');
|
||||
textInput && textInput.registerLabel(this);
|
||||
constructor(config: IonicConfig, @Optional() container: TextInput) {
|
||||
this.scrollAssist = config.get('scrollAssist');
|
||||
if (!this.id) {
|
||||
this.id = 'lbl-' + (++labelIds);
|
||||
}
|
||||
this.container = container;
|
||||
container && container.registerLabel(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,7 +59,7 @@ export class Label {
|
||||
if (!hasPointerMoved(20, this.startCoord, endCoord)) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.container.focus();
|
||||
this.container.initFocus();
|
||||
}
|
||||
|
||||
this.startCoord = null;
|
||||
@ -63,3 +67,5 @@ export class Label {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let labelIds = -1;
|
||||
|
7
ionic/components/text-input/test/input-focus/index.ts
Normal file
7
ionic/components/text-input/test/input-focus/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {App} from 'ionic/ionic';
|
||||
|
||||
|
||||
@App({
|
||||
templateUrl: 'main.html'
|
||||
})
|
||||
class E2EApp {}
|
188
ionic/components/text-input/test/input-focus/main.html
Normal file
188
ionic/components/text-input/test/input-focus/main.html
Normal file
@ -0,0 +1,188 @@
|
||||
|
||||
<ion-toolbar><ion-title>Input Focus</ion-title></ion-toolbar>
|
||||
|
||||
|
||||
<ion-content>
|
||||
|
||||
<p>Paragraph text with a <a href="#">link</a>.</p>
|
||||
|
||||
<ion-list>
|
||||
|
||||
<ion-input>
|
||||
<ion-label>Text 1:</ion-label>
|
||||
<input type="text">
|
||||
</ion-input>
|
||||
|
||||
<ion-item>
|
||||
Item with button right
|
||||
<button item-right>Button 1</button>
|
||||
</ion-item>
|
||||
|
||||
<ion-input>
|
||||
<ion-label id="my-label1">Text 2:</ion-label>
|
||||
<input value="value" type="text">
|
||||
</ion-input>
|
||||
|
||||
<button ion-item>
|
||||
Button Item
|
||||
</button>
|
||||
|
||||
<ion-input>
|
||||
<ion-label>Text 3:</ion-label>
|
||||
<input type="text">
|
||||
<button clear item-right>
|
||||
<icon power></icon>
|
||||
</button>
|
||||
</ion-input>
|
||||
|
||||
<ion-input>
|
||||
<ion-label>Comments:</ion-label>
|
||||
<textarea>Comment value</textarea>
|
||||
</ion-input>
|
||||
|
||||
<ion-input>
|
||||
<icon globe item-left></icon>
|
||||
<ion-label>Website:</ion-label>
|
||||
<input value="http://ionic.io/" type="url">
|
||||
</ion-input>
|
||||
|
||||
<ion-input>
|
||||
<icon mail item-left></icon>
|
||||
<ion-label>Email:</ion-label>
|
||||
<input value="email6@email.com" type="email">
|
||||
</ion-input>
|
||||
|
||||
<ion-input>
|
||||
<icon create item-left></icon>
|
||||
<ion-label>Feedback:</ion-label>
|
||||
<textarea placeholder="Placeholder Text"></textarea>
|
||||
</ion-input>
|
||||
|
||||
<ion-input>
|
||||
<ion-label>More Info:</ion-label>
|
||||
<input placeholder="Placeholder Text" type="text">
|
||||
<icon flag item-right></icon>
|
||||
</ion-input>
|
||||
|
||||
<ion-input>
|
||||
<ion-label>Score:</ion-label>
|
||||
<input value="10" type="number">
|
||||
<button outline item-right>Update</button>
|
||||
</ion-input>
|
||||
|
||||
<ion-input>
|
||||
<ion-label>First Name:</ion-label>
|
||||
<input value="Lightning" type="text">
|
||||
</ion-input>
|
||||
|
||||
<ion-input>
|
||||
<ion-label>Last Name:</ion-label>
|
||||
<input value="McQueen" type="text">
|
||||
</ion-input>
|
||||
|
||||
<ion-input>
|
||||
<ion-label>Message:</ion-label>
|
||||
<textarea>KA-CHOW!</textarea>
|
||||
</ion-input>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
|
||||
<!--
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
Item
|
||||
</ion-item> -->
|
||||
|
||||
</ion-list>
|
||||
|
||||
</ion-content>
|
@ -2,9 +2,6 @@
|
||||
// Text Input
|
||||
// --------------------------------------------------
|
||||
|
||||
$input-focus-border-color: #51a7e8 !default;
|
||||
$input-focus-box-shadow: inset 0px 0px 8px 0px $input-focus-border-color !default;
|
||||
|
||||
$text-input-background-color: $list-background-color !default;
|
||||
|
||||
|
||||
@ -37,18 +34,6 @@ ion-input.item {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.key-input ion-input {
|
||||
|
||||
&.has-focus {
|
||||
border-color: $input-focus-border-color;
|
||||
box-shadow: $input-focus-box-shadow;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
ion-input [text-input] {
|
||||
flex: 1;
|
||||
background-color: $text-input-background-color;
|
||||
@ -59,6 +44,13 @@ ion-input.has-focus [text-input] {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
ion-input input[scroll-assist] {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ion-input textarea {
|
||||
padding-top: 9px;
|
||||
}
|
||||
@ -80,4 +72,3 @@ input,
|
||||
textarea {
|
||||
@include placeholder();
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Directive, Host, Optional, ElementRef, Renderer, Attribute, Query, QueryList, NgZone} from 'angular2/angular2';
|
||||
import {Component, Directive, NgIf, forwardRef, Host, Optional, ElementRef, Renderer, Attribute, Query, QueryList, NgZone} from 'angular2/angular2';
|
||||
|
||||
import {IonicConfig} from '../../config/config';
|
||||
import {IonicForm} from '../../util/form';
|
||||
@ -12,29 +12,20 @@ import {IonicPlatform} from '../../platform/platform';
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@Directive({
|
||||
@Component({
|
||||
selector: 'ion-input',
|
||||
host: {
|
||||
'(focus)': 'receivedFocus(true)',
|
||||
'(blur)': 'receivedFocus(false)',
|
||||
'(touchstart)': 'pointerStart($event)',
|
||||
'(touchend)': 'pointerEnd($event)',
|
||||
'(mouseup)': 'pointerEnd($event)',
|
||||
'[class.has-focus]': 'hasFocus',
|
||||
'[class.has-value]': 'hasValue'
|
||||
}
|
||||
'(mouseup)': 'pointerEnd($event)'
|
||||
},
|
||||
template:
|
||||
'<ng-content></ng-content>' +
|
||||
'<input [type]="type" aria-hidden="true" scroll-assist *ng-if="scrollAssist">',
|
||||
directives: [NgIf, forwardRef(() => InputScrollAssist)]
|
||||
})
|
||||
export class TextInput {
|
||||
/**
|
||||
* TODO
|
||||
* @param {ElementRef} elementRef TODO
|
||||
* @param {IonicConfig} config TODO
|
||||
* @param {IonicApp} app TODO
|
||||
* @param {NgZone} ngZone TODO
|
||||
* @param {Content=} scrollView The parent scroll view.
|
||||
* @param {QueryList<TextInputElement>} inputQry TODO
|
||||
* @param {QueryList<Label>} labelQry TODO
|
||||
*/
|
||||
|
||||
constructor(
|
||||
form: IonicForm,
|
||||
elementRef: ElementRef,
|
||||
@ -46,40 +37,44 @@ export class TextInput {
|
||||
@Optional() @Host() scrollView: Content
|
||||
) {
|
||||
renderer.setElementClass(elementRef, 'item', true);
|
||||
this.renderer = renderer;
|
||||
|
||||
this.form = form;
|
||||
form.register(this);
|
||||
|
||||
this.type = 'text';
|
||||
this.lastTouch = 0;
|
||||
|
||||
this.app = app;
|
||||
this.elementRef = elementRef;
|
||||
this.zone = zone;
|
||||
this.platform = platform;
|
||||
|
||||
this.scrollView = scrollView;
|
||||
this.scrollAssist = config.get('keyboardScrollAssist');
|
||||
this.scrollAssist = config.get('scrollAssist');
|
||||
this.keyboardHeight = config.get('keyboardHeight');
|
||||
}
|
||||
|
||||
registerInput(textInputElement) {
|
||||
this.input = textInputElement;
|
||||
this.type = textInputElement.type;
|
||||
this.type = textInputElement.type || 'text';
|
||||
}
|
||||
|
||||
registerLabel(label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
onInit() {
|
||||
if (this.input && this.label) {
|
||||
this.label.id = (this.label.id || 'label-' + this.inputId)
|
||||
// if there is an input and an label
|
||||
// then give the label an ID
|
||||
// and tell the input the ID of who it's labelled by
|
||||
this.input.labelledBy(this.label.id);
|
||||
}
|
||||
|
||||
let self = this;
|
||||
self.scrollMove = (ev) => {
|
||||
console.debug('content scrollMove');
|
||||
self.deregListeners();
|
||||
|
||||
if (self.hasFocus) {
|
||||
@ -88,10 +83,6 @@ export class TextInput {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* @param {Event} ev TODO
|
||||
*/
|
||||
pointerStart(ev) {
|
||||
if (this.scrollAssist && this.app.isEnabled()) {
|
||||
// remember where the touchstart/mousedown started
|
||||
@ -99,10 +90,6 @@ export class TextInput {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* @param {Event} ev TODO
|
||||
*/
|
||||
pointerEnd(ev) {
|
||||
if (!this.app.isEnabled()) {
|
||||
ev.preventDefault();
|
||||
@ -122,29 +109,21 @@ export class TextInput {
|
||||
this.initFocus();
|
||||
|
||||
// temporarily prevent mouseup's from focusing
|
||||
this.preventMouse = true;
|
||||
clearTimeout(this.mouseTimer);
|
||||
this.mouseTimer = setTimeout(() => {
|
||||
this.preventMouse = false;
|
||||
}, 500);
|
||||
this.lastTouch = Date.now();
|
||||
});
|
||||
}
|
||||
|
||||
} else if (!this.preventMouse) {
|
||||
} else if (this.lastTouch + 500 < Date.now()) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.setFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* @returns {TODO} TODO
|
||||
*/
|
||||
initFocus() {
|
||||
// begin the process of setting focus to the inner input element
|
||||
|
||||
let scrollView = this.scrollView;
|
||||
|
||||
if (scrollView && this.scrollAssist) {
|
||||
@ -161,7 +140,7 @@ export class TextInput {
|
||||
}
|
||||
|
||||
// add padding to the bottom of the scroll view (if needed)
|
||||
scrollView.addKeyboardPadding(scrollData.scrollPadding);
|
||||
scrollView.addScrollPadding(scrollData.scrollPadding);
|
||||
|
||||
// manually scroll the text input to the top
|
||||
// do not allow any clicks while it's scrolling
|
||||
@ -308,33 +287,42 @@ export class TextInput {
|
||||
return scrollData;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
deregListeners() {
|
||||
this.deregScroll && this.deregScroll();
|
||||
focusChange(hasFocus) {
|
||||
this.renderer.setElementClass(this.elementRef, 'has-focus', hasFocus);
|
||||
}
|
||||
|
||||
hasValue(inputValue) {
|
||||
this.renderer.setElementClass(this.elementRef, 'has-value', inputValue && inputValue !== '');
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
setFocus() {
|
||||
if (this.input) {
|
||||
|
||||
this.zone.run(() => {
|
||||
// set focus on the input element
|
||||
this.input && this.input.initFocus();
|
||||
|
||||
this.form.setAsFocused(this);
|
||||
|
||||
// set focus on the actual input element
|
||||
this.input.setFocus();
|
||||
|
||||
// ensure the body hasn't scrolled down
|
||||
document.body.scrollTop = 0;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (this.scrollAssist && this.scrollView) {
|
||||
setTimeout(() => {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.deregListeners();
|
||||
this.deregScroll = this.scrollView.addScrollEventListener(this.scrollMove);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
deregListeners() {
|
||||
this.deregScroll && this.deregScroll();
|
||||
}
|
||||
|
||||
tempFocusMove() {
|
||||
this.form.setFocusHolder(this.type);
|
||||
}
|
||||
@ -343,33 +331,6 @@ export class TextInput {
|
||||
return !!this.input && this.input.hasFocus;
|
||||
}
|
||||
|
||||
get hasValue() {
|
||||
return !!this.input && this.input.hasValue;
|
||||
}
|
||||
|
||||
get tabIndex() {
|
||||
return this.input && this.input.tabIndex;
|
||||
}
|
||||
|
||||
set tabIndex(val) {
|
||||
if (this.input) {
|
||||
this.input.tabIndex = val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* @param {boolean} receivedFocus TODO
|
||||
*/
|
||||
receivedFocus(receivedFocus) {
|
||||
if (receivedFocus && !this.hasFocus) {
|
||||
this.initFocus();
|
||||
|
||||
} else {
|
||||
this.deregListeners();
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.deregListeners();
|
||||
this.form.deregister(this);
|
||||
@ -377,73 +338,80 @@ export class TextInput {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
* TODO
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'textarea,input[type=text],input[type=password],input[type=number],input[type=search],input[type=email],input[type=url],input[type=tel]',
|
||||
inputs: [
|
||||
'tabIndex'
|
||||
],
|
||||
inputs: ['value'],
|
||||
host: {
|
||||
'[tabIndex]': 'tabIndex'
|
||||
'(focus)': 'wrapper.focusChange(true)',
|
||||
'(blur)': 'wrapper.focusChange(false)',
|
||||
'(keyup)': 'onKeyup($event)'
|
||||
}
|
||||
})
|
||||
export class TextInputElement {
|
||||
|
||||
constructor(
|
||||
form: IonicForm,
|
||||
@Attribute('type') type: string,
|
||||
elementRef: ElementRef,
|
||||
renderer: Renderer,
|
||||
@Optional() textInputWrapper: TextInput
|
||||
@Optional() wrapper: TextInput
|
||||
) {
|
||||
this.form = form;
|
||||
this.type = type;
|
||||
this.elementRef = elementRef;
|
||||
this.tabIndex = 0;
|
||||
this.wrapper = wrapper;
|
||||
|
||||
this.renderer = renderer;
|
||||
renderer.setElementAttribute(this.elementRef, 'text-input', '');
|
||||
|
||||
if (textInputWrapper) {
|
||||
if (wrapper) {
|
||||
// it's within ionic's ion-input, let ion-input handle what's up
|
||||
textInputWrapper.registerInput(this);
|
||||
|
||||
} else {
|
||||
// not within ion-input
|
||||
form.register(this);
|
||||
wrapper.registerInput(this);
|
||||
}
|
||||
}
|
||||
|
||||
onKeyup(ev) {
|
||||
this.wrapper.hasValue(ev.target.value);
|
||||
}
|
||||
|
||||
onInit() {
|
||||
this.wrapper.hasValue(this.value);
|
||||
}
|
||||
|
||||
labelledBy(val) {
|
||||
this.renderer.setElementAttribute(this.elementRef, 'aria-labelledby', val);
|
||||
val && this.renderer.setElementAttribute(this.elementRef, 'aria-labelledby', val);
|
||||
}
|
||||
|
||||
initFocus() {
|
||||
this.elementRef.nativeElement.focus();
|
||||
setFocus() {
|
||||
this.getNativeElement().focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the input has focus or not.
|
||||
* @returns {boolean} true if the input has focus, otherwise false.
|
||||
*/
|
||||
get hasFocus() {
|
||||
return dom.hasFocus(this.elementRef.nativeElement);
|
||||
return dom.hasFocus(this.getNativeElement());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the input has a value.
|
||||
* @returns {boolean} true if the input has a value, otherwise false.
|
||||
*/
|
||||
get hasValue() {
|
||||
return (this.elementRef.nativeElement.value !== '');
|
||||
getNativeElement() {
|
||||
return this.elementRef.nativeElement;
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.form.deregister(this);
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[scroll-assist]',
|
||||
host: {
|
||||
'(focus)': 'receivedFocus($event)'
|
||||
}
|
||||
})
|
||||
class InputScrollAssist {
|
||||
|
||||
constructor(form: IonicForm, textInput: TextInput) {
|
||||
this.form = form;
|
||||
this.textInput = textInput;
|
||||
}
|
||||
|
||||
receivedFocus(ev) {
|
||||
this.form.focusNext(this.textInput);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -49,7 +49,7 @@ IonicPlatform.register({
|
||||
settings: {
|
||||
mode: 'md',
|
||||
keyboardHeight: 290,
|
||||
keyboardScrollAssist: true,
|
||||
scrollAssist: true,
|
||||
hoverCSS: false,
|
||||
},
|
||||
isMatch(p) {
|
||||
@ -71,7 +71,7 @@ IonicPlatform.register({
|
||||
],
|
||||
settings: {
|
||||
mode: 'ios',
|
||||
keyboardScrollAssist: function(p) {
|
||||
scrollAssist: function(p) {
|
||||
return /iphone|ipad|ipod/i.test(p.navigatorPlatform());
|
||||
},
|
||||
keyboardHeight: 290,
|
||||
|
@ -1,14 +1,11 @@
|
||||
import {Injectable, NgZone} from 'angular2/angular2';
|
||||
|
||||
import {IonicConfig} from '../config/config';
|
||||
import {raf} from './dom';
|
||||
|
||||
|
||||
/**
|
||||
* The Input component is used to focus text input elements.
|
||||
*
|
||||
* The `focusNext()` and `focusPrevious()` methods make it easy to focus input elements across all devices.
|
||||
*
|
||||
* @usage
|
||||
* ```html
|
||||
* <ion-input>
|
||||
@ -25,150 +22,18 @@ export class IonicForm {
|
||||
this._zone = zone;
|
||||
|
||||
this._inputs = [];
|
||||
this._ids = -1;
|
||||
this._focused = null;
|
||||
|
||||
zone.runOutsideAngular(() => {
|
||||
this.initHolders(document, this._config.get('keyboardScrollAssist'));
|
||||
|
||||
if (this._config.get('keyboardInputListener') !== false) {
|
||||
this.initKeyInput(document);
|
||||
}
|
||||
this.focusCtrl(document);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
initKeyInput(document) {
|
||||
/* Focus Outline
|
||||
* --------------------------------------------------
|
||||
* When a keydown event happens, from a tab key, then the
|
||||
* 'key-input' class is added to the body element so focusable
|
||||
* elements have an outline. On a mousedown or touchstart
|
||||
* event then the 'key-input' class is removed.
|
||||
*/
|
||||
|
||||
let isKeyInputEnabled = false;
|
||||
|
||||
function keyDown(ev) {
|
||||
if (!isKeyInputEnabled && ev.keyCode == 9) {
|
||||
isKeyInputEnabled = true;
|
||||
raf(enableKeyInput);
|
||||
}
|
||||
}
|
||||
|
||||
function pointerDown() {
|
||||
isKeyInputEnabled = false;
|
||||
raf(enableKeyInput);
|
||||
}
|
||||
|
||||
|
||||
function enableKeyInput() {
|
||||
document.body.classList[isKeyInputEnabled ? 'add' : 'remove']('key-input');
|
||||
|
||||
document.removeEventListener('mousedown', pointerDown);
|
||||
document.removeEventListener('touchstart', pointerDown);
|
||||
|
||||
if (isKeyInputEnabled) {
|
||||
document.addEventListener('mousedown', pointerDown);
|
||||
document.addEventListener('touchstart', pointerDown);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keyDown);
|
||||
}
|
||||
|
||||
initHolders(document, scrollAssist) {
|
||||
// raw DOM fun
|
||||
this._ctrl = document.createElement('focus-ctrl');
|
||||
this._ctrl.setAttribute('aria-hidden', true);
|
||||
|
||||
if (scrollAssist) {
|
||||
this._prev = document.createElement('input');
|
||||
this._prev.tabIndex = NO_FOCUS_TAB_INDEX;
|
||||
this._ctrl.appendChild(this._prev);
|
||||
|
||||
this._next = document.createElement('input');
|
||||
this._next.tabIndex = NO_FOCUS_TAB_INDEX;
|
||||
this._ctrl.appendChild(this._next);
|
||||
|
||||
this._temp = document.createElement('input');
|
||||
this._temp.tabIndex = NO_FOCUS_TAB_INDEX;
|
||||
this._ctrl.appendChild(this._temp);
|
||||
}
|
||||
|
||||
this._blur = document.createElement('button');
|
||||
this._blur.tabIndex = NO_FOCUS_TAB_INDEX;
|
||||
this._ctrl.appendChild(this._blur);
|
||||
|
||||
document.body.appendChild(this._ctrl);
|
||||
|
||||
function preventDefault(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
if (scrollAssist) {
|
||||
this._prev.addEventListener('keydown', preventDefault);
|
||||
this._next.addEventListener('keydown', preventDefault);
|
||||
this._temp.addEventListener('keydown', preventDefault);
|
||||
|
||||
this._prev.addEventListener('focus', () => {
|
||||
this.focusPrevious();
|
||||
});
|
||||
|
||||
this._next.addEventListener('focus', () => {
|
||||
this.focusNext();
|
||||
});
|
||||
|
||||
let self = this;
|
||||
let resetTimer;
|
||||
function queueReset() {
|
||||
clearTimeout(resetTimer);
|
||||
|
||||
resetTimer = setTimeout(function() {
|
||||
self._zone.run(() => {
|
||||
self.resetInputs();
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
document.addEventListener('focusin', queueReset);
|
||||
document.addEventListener('focusout', queueReset);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
focusOut() {
|
||||
console.debug('focusOut')
|
||||
this._blur.tabIndex = NORMAL_FOCUS_TAB_INDEX;
|
||||
this._blur.focus();
|
||||
this._blur.tabIndex = NO_FOCUS_TAB_INDEX;
|
||||
}
|
||||
|
||||
setFocusHolder(type) {
|
||||
if (this._temp) {
|
||||
this._temp.tabIndex = TEMP_TAB_INDEX;
|
||||
|
||||
this._temp.type = type || 'text';
|
||||
console.debug('setFocusHolder', this._temp.type);
|
||||
this._temp.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TODO} input TODO
|
||||
*/
|
||||
register(input) {
|
||||
console.debug('register input', input);
|
||||
|
||||
input.inputId = ++this._ids;
|
||||
input.tabIndex = NORMAL_FOCUS_TAB_INDEX;
|
||||
this._inputs.push(input);
|
||||
}
|
||||
|
||||
deregister(input) {
|
||||
console.debug('deregister input', input);
|
||||
|
||||
let index = this._inputs.indexOf(input);
|
||||
if (index > -1) {
|
||||
this._inputs.splice(index, 1);
|
||||
@ -178,72 +43,72 @@ export class IonicForm {
|
||||
}
|
||||
}
|
||||
|
||||
resetInputs() {
|
||||
this._focused = null;
|
||||
focusCtrl(document) {
|
||||
let scrollAssist = this._config.get('scrollAssist');
|
||||
|
||||
for (let i = 0, ii = this._inputs.length; i < ii; i++) {
|
||||
if (!this._focused && this._inputs[i].hasFocus) {
|
||||
this._focused = this._inputs[i];
|
||||
this._focused.tabIndex = ACTIVE_FOCUS_TAB_INDEX;
|
||||
// raw DOM fun
|
||||
let focusCtrl = document.createElement('focus-ctrl');
|
||||
focusCtrl.setAttribute('aria-hidden', true);
|
||||
|
||||
} else {
|
||||
this._inputs[i].tabIndex = NORMAL_FOCUS_TAB_INDEX;
|
||||
}
|
||||
if (scrollAssist) {
|
||||
this._tmp = document.createElement('input');
|
||||
this._tmp.tabIndex = -1;
|
||||
focusCtrl.appendChild(this._tmp);
|
||||
}
|
||||
|
||||
if (this._temp) {
|
||||
this._temp.tabIndex = NO_FOCUS_TAB_INDEX;
|
||||
this._blur = document.createElement('button');
|
||||
this._blur.tabIndex = -1;
|
||||
focusCtrl.appendChild(this._blur);
|
||||
|
||||
if (this._focused) {
|
||||
// there's a focused input
|
||||
this._prev.tabIndex = PREV_TAB_INDEX;
|
||||
this._next.tabIndex = NEXT_TAB_INDEX;
|
||||
document.body.appendChild(focusCtrl);
|
||||
|
||||
} else {
|
||||
this._prev.tabIndex = this._next.tabIndex = NO_FOCUS_TAB_INDEX;
|
||||
}
|
||||
if (scrollAssist) {
|
||||
this._tmp.addEventListener('keydown', (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the previous input element, if it exists.
|
||||
*/
|
||||
focusPrevious() {
|
||||
console.debug('focusPrevious');
|
||||
this.focusMove(-1);
|
||||
focusOut() {
|
||||
console.debug('focusOut');
|
||||
this._blur.focus();
|
||||
}
|
||||
|
||||
setFocusHolder(type) {
|
||||
if (this._tmp && this._config.get('scrollAssist')) {
|
||||
this._tmp.type = type;
|
||||
console.debug('setFocusHolder', this._tmp.type);
|
||||
this._tmp.focus();
|
||||
}
|
||||
}
|
||||
|
||||
setAsFocused(input) {
|
||||
this._focused = input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the next input element, if it exists.
|
||||
*/
|
||||
focusNext() {
|
||||
focusNext(currentInput) {
|
||||
console.debug('focusNext');
|
||||
this.focusMove(1);
|
||||
|
||||
let index = this._inputs.indexOf(currentInput);
|
||||
if (index > -1 && (index + 1) < this._inputs.length) {
|
||||
let nextInput = this._inputs[index + 1];
|
||||
if (nextInput !== this._focused) {
|
||||
return nextInput.initFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} inc TODO
|
||||
*/
|
||||
focusMove(inc) {
|
||||
let input = this._focused;
|
||||
if (input) {
|
||||
let index = this._inputs.indexOf(input);
|
||||
if (index > -1 && (index + inc) < this._inputs.length) {
|
||||
let siblingInput = this._inputs[index + inc];
|
||||
if (siblingInput) {
|
||||
return siblingInput.initFocus();
|
||||
index = this._inputs.indexOf(this._focused);
|
||||
if (index > 0) {
|
||||
let previousInput = this._inputs[index - 1];
|
||||
if (previousInput) {
|
||||
previousInput.initFocus();
|
||||
}
|
||||
}
|
||||
this._focused.initFocus();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const NO_FOCUS_TAB_INDEX = -1;
|
||||
const NORMAL_FOCUS_TAB_INDEX = 0;
|
||||
const PREV_TAB_INDEX = 999;
|
||||
const ACTIVE_FOCUS_TAB_INDEX = 1000;
|
||||
const NEXT_TAB_INDEX = 1001;
|
||||
const TEMP_TAB_INDEX = 2000;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {Injectable} from 'angular2/angular2';
|
||||
import {Injectable, NgZone} from 'angular2/angular2';
|
||||
|
||||
import {IonicConfig} from '../config/config';
|
||||
import {IonicForm} from './form';
|
||||
import * as dom from './dom';
|
||||
|
||||
@ -7,8 +8,13 @@ import * as dom from './dom';
|
||||
@Injectable()
|
||||
export class IonicKeyboard {
|
||||
|
||||
constructor(form: IonicForm) {
|
||||
constructor(config: IonicConfig, form: IonicForm, zone: NgZone) {
|
||||
this.form = form;
|
||||
this.zone = zone;
|
||||
|
||||
zone.runOutsideAngular(() => {
|
||||
this.focusOutline(config.get('focusOutline'), document);
|
||||
});
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
@ -25,16 +31,23 @@ export class IonicKeyboard {
|
||||
promise = new Promise(resolve => { callback = resolve; });
|
||||
}
|
||||
|
||||
self.zone.runOutsideAngular(() => {
|
||||
|
||||
function checkKeyboard() {
|
||||
if (!self.isOpen()) {
|
||||
self.zone.run(() => {
|
||||
console.debug('keyboard closed');
|
||||
callback();
|
||||
});
|
||||
|
||||
} else {
|
||||
setTimeout(checkKeyboard, 500);
|
||||
setTimeout(checkKeyboard, KEYBOARD_CLOSE_POLLING);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(checkKeyboard, 100);
|
||||
setTimeout(checkKeyboard, KEYBOARD_CLOSE_POLLING);
|
||||
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
@ -48,4 +61,64 @@ export class IonicKeyboard {
|
||||
});
|
||||
}
|
||||
|
||||
focusOutline(setting, document) {
|
||||
/* Focus Outline
|
||||
* --------------------------------------------------
|
||||
* By default, when a keydown event happens from a tab key, then
|
||||
* the 'focus-outline' css class is added to the body element
|
||||
* so focusable elements have an outline. On a mousedown or
|
||||
* touchstart event, then the 'focus-outline' css class is removed.
|
||||
*
|
||||
* Config default overrides:
|
||||
* focusOutline: true - Always add the focus-outline
|
||||
* focusOutline: false - Do not add the focus-outline
|
||||
*/
|
||||
|
||||
|
||||
let isKeyInputEnabled = false;
|
||||
|
||||
function cssClass() {
|
||||
dom.raf(() => {
|
||||
document.body.classList[isKeyInputEnabled ? 'add' : 'remove']('focus-outline');
|
||||
});
|
||||
}
|
||||
|
||||
if (setting === true) {
|
||||
isKeyInputEnabled = true;
|
||||
return cssClass();
|
||||
|
||||
} else if (setting === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// default is to add the focus-outline when the tab key is used
|
||||
function keyDown(ev) {
|
||||
if (!isKeyInputEnabled && ev.keyCode == 9) {
|
||||
isKeyInputEnabled = true;
|
||||
enableKeyInput();
|
||||
}
|
||||
}
|
||||
|
||||
function pointerDown() {
|
||||
isKeyInputEnabled = false;
|
||||
enableKeyInput();
|
||||
}
|
||||
|
||||
function enableKeyInput() {
|
||||
cssClass();
|
||||
|
||||
document.removeEventListener('mousedown', pointerDown);
|
||||
document.removeEventListener('touchstart', pointerDown);
|
||||
|
||||
if (isKeyInputEnabled) {
|
||||
document.addEventListener('mousedown', pointerDown);
|
||||
document.addEventListener('touchstart', pointerDown);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keyDown);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const KEYBOARD_CLOSE_POLLING = 150;
|
||||
|
@ -27,17 +27,17 @@
|
||||
|
||||
// Focus Outline
|
||||
// --------------------------------------------------
|
||||
// When a keydown event happens, from a tab key, then the
|
||||
// 'key-input' class is added to the body element so focusable
|
||||
// elements have an outline. On a mousedown or touchstart
|
||||
// event then the 'key-input' class is removed.
|
||||
|
||||
$focus-outline-border-color: #51a7e8 !default;
|
||||
$focus-outline-box-shadow: 0px 0px 8px 0px $focus-outline-border-color !default;
|
||||
|
||||
|
||||
:focus,
|
||||
:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.key-input {
|
||||
.focus-outline {
|
||||
|
||||
:focus {
|
||||
outline-offset: -1px;
|
||||
@ -46,8 +46,20 @@
|
||||
|
||||
button:focus,
|
||||
[button]:focus {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid red;
|
||||
border-color: $focus-outline-border-color;
|
||||
box-shadow: $focus-outline-box-shadow;
|
||||
outline: thin solid $focus-outline-border-color;
|
||||
}
|
||||
|
||||
ion-input.has-focus,
|
||||
button[ion-item]:focus,
|
||||
a[ion-item]:focus {
|
||||
border-color: $focus-outline-border-color;
|
||||
box-shadow: inset $focus-outline-box-shadow !important;
|
||||
}
|
||||
|
||||
ion-input :focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user