This commit is contained in:
Adam Bradley
2015-07-16 14:04:15 -05:00
parent a5e5789323
commit f240083535
23 changed files with 729 additions and 36 deletions

View File

@ -0,0 +1,106 @@
import {raf} from '../util/dom';
export class ScrollTo {
constructor(ele, x, y, duration) {
if (typeof ele === 'string') {
// string query selector
ele = document.querySelector(ele);
}
if (ele) {
if (ele.nativeElement) {
// angular ElementRef
ele = ele.nativeElement;
}
if (ele.nodeType === 1) {
this._el = ele;
}
}
}
start(x, y, duration, tolerance) {
// scroll animation loop w/ easing
// credit https://gist.github.com/dezinezync/5487119
let self = this;
if (!self._el) {
// invalid element
return Promise.resolve();
}
x = x || 0;
y = y || 0;
tolerance = tolerance || 0;
let ele = self._el;
let fromY = ele.scrollTop;
let fromX = ele.scrollLeft;
let xDistance = Math.abs(x - fromX);
let yDistance = Math.abs(y - fromY);
if (yDistance <= tolerance && xDistance <= tolerance) {
// prevent scrolling if already close to there
this._el = ele = null;
return Promise.resolve();
}
return new Promise((resolve, reject) => {
let start = Date.now();
// start scroll loop
self.isPlaying = true;
raf(step);
// decelerating to zero velocity
function easeOutCubic(t) {
return (--t) * t * t + 1;
}
// scroll loop
function step() {
let time = Math.min(1, ((Date.now() - start) / duration));
// where .5 would be 50% of time on a linear scale easedT gives a
// fraction based on the easing method
let easedT = easeOutCubic(time);
if (fromY != y) {
ele.scrollTop = parseInt((easedT * (y - fromY)) + fromY, 10);
}
if (fromX != x) {
ele.scrollLeft = parseInt((easedT * (x - fromX)) + fromX, 10);
}
if (time < 1 && self.isPlaying) {
raf(step);
} else if (!self.isPlaying) {
// stopped
this._el = ele = null;
reject();
} else {
// done
this._el = ele = null;
resolve();
}
}
});
}
stop() {
this.isPlaying = false;
}
dispose() {
this.stop();
this._el = null;
}
}

View File

@ -8,8 +8,8 @@ export * from 'ionic/components/icon/icon'
export * from 'ionic/components/item/item' export * from 'ionic/components/item/item'
export * from 'ionic/components/item/item-group' export * from 'ionic/components/item/item-group'
export * from 'ionic/components/form/form' export * from 'ionic/components/form/form'
export * from 'ionic/components/form/input/input' export * from 'ionic/components/form/input'
export * from 'ionic/components/form/label/label' export * from 'ionic/components/form/label'
export * from 'ionic/components/list/list' export * from 'ionic/components/list/list'
export * from 'ionic/components/modal/modal' export * from 'ionic/components/modal/modal'
export * from 'ionic/components/nav/nav' export * from 'ionic/components/nav/nav'

View File

@ -11,6 +11,7 @@ import * as util from '../../util/util';
// injectables // injectables
import {ActionMenu} from '../action-menu/action-menu'; import {ActionMenu} from '../action-menu/action-menu';
import {Modal} from '../modal/modal'; import {Modal} from '../modal/modal';
import {FocusHolder} from '../form/focus-holder';
export class IonicApp { export class IonicApp {
@ -29,6 +30,13 @@ export class IonicApp {
this._zone = this.injector().get(NgZone); this._zone = this.injector().get(NgZone);
} }
focusHolder(val) {
if (arguments.length) {
this._focusHolder = val;
}
return this._focusHolder;
}
title(val) { title(val) {
document.title = val; document.title = val;
} }
@ -206,6 +214,13 @@ export function ionicBootstrap(component, config, router) {
bootstrap(component, injectableBindings).then(appRef => { bootstrap(component, injectableBindings).then(appRef => {
app.load(appRef); app.load(appRef);
// append the focus holder if its needed
if (config.setting('keyboardScrollAssist')) {
app.appendComponent(FocusHolder).then(ref => {
app.focusHolder(ref.instance);
});
}
router.load(window, app, config).then(() => { router.load(window, app, config).then(() => {
// resolve that the app has loaded // resolve that the app has loaded
resolve(app); resolve(app);

View File

@ -1,10 +1,11 @@
html, html,
body { body {
width: 100%;
height: 100%; height: 100%;
} }
body { body {
position: relative; position: fixed;
overflow: hidden; overflow: hidden;
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;

View File

@ -90,7 +90,7 @@ $content-padding: 10px !default;
left: 0; left: 0;
opacity: 0; opacity: 0;
z-index: $z-index-click-block; z-index: $z-index-click-block;
transform: translate3d(-9999px, 0px, 0px); transform: translate3d(-9999px, 0px, 0px);;
//background: red; //background: red;
//opacity: .3; //opacity: .3;

View File

@ -3,13 +3,17 @@ import {Component, View, ElementRef} from 'angular2/angular2';
import {Ion} from '../ion'; import {Ion} from '../ion';
import {IonicConfig} from '../../config/config'; import {IonicConfig} from '../../config/config';
import {IonicComponent} from '../../config/annotations'; import {IonicComponent} from '../../config/annotations';
import {ScrollTo} from '../../animations/scroll-to';
@Component({ @Component({
selector: 'ion-content', selector: 'ion-content',
properties: [ properties: [
'parallax' 'parallax'
] ],
host: {
['[class.scroll-padding]']: 'scrollPadding'
}
}) })
@View({ @View({
template: '<div class="scroll-content"><content></content></div>' template: '<div class="scroll-content"><content></content></div>'
@ -17,6 +21,7 @@ import {IonicComponent} from '../../config/annotations';
export class Content extends Ion { export class Content extends Ion {
constructor(elementRef: ElementRef, ionicConfig: IonicConfig) { constructor(elementRef: ElementRef, ionicConfig: IonicConfig) {
super(elementRef, ionicConfig); super(elementRef, ionicConfig);
this.scrollPadding = false;
} }
onIonInit() { onIonInit() {
@ -32,4 +37,33 @@ export class Content extends Ion {
this.scrollElement.removeEventListener('scroll', handler); this.scrollElement.removeEventListener('scroll', handler);
} }
} }
addTouchMoveListener(handler) {
if(!this.scrollElement) { return; }
this.scrollElement.addEventListener('touchmove', handler);
return () => {
this.scrollElement.removeEventListener('touchmove', handler);
}
}
scrollTo(x, y, duration, tolerance) {
if (this._scrollTo) {
this._scrollTo.dispose();
}
this._scrollTo = new ScrollTo(this.scrollElement);
return this._scrollTo.start(x, y, duration, tolerance);
}
get scrollPadding() {
return this._sp;
}
set scrollPadding(val) {
this._sp = val;
}
} }

View File

@ -0,0 +1,108 @@
import {Component, Directive, View, Parent, ElementRef, forwardRef} from 'angular2/angular2';
import {IonicConfig} from '../../config/config';
import * as dom from '../../util/dom';
import {Platform} from '../../platform/platform';
import {IonInput} from './form';
@Component({
selector: 'focus-holder'
})
@View({
template: '<input><input><input>',
directives: [forwardRef(() => FocusInput)]
})
export class FocusHolder {
constructor() {
this.i = [];
}
setFocusHolder(inputType) {
IonInput.clearTabIndexes();
this.i[1].tabIndex = ACTIVE_TAB_INDEX;
this.i[1].type = inputType;
this.i[1].focus();
}
setActiveInput(input) {
IonInput.clearTabIndexes();
this.i[1].tabIndex = -1;
input.tabIndex = ACTIVE_TAB_INDEX;
}
receivedFocus(tabIndex) {
if (tabIndex === PREVIOUS_TAB_INDEX) {
// they tabbed back one input
// reset the focus to the center focus holder
this.i[1].focus();
// focus on the previous input
IonInput.focusPrevious();
} else if (tabIndex === NEXT_TAB_INDEX) {
// they tabbed to the next input
// reset the focus to the center focus holder
this.i[1].focus();
// focus on the next input
IonInput.focusNext();
}
}
register(input) {
// register each of the focus holder inputs
// assign them their correct tab indexes
input.tabIndex = PREVIOUS_TAB_INDEX + this.i.length;
this.i.push(input);
}
}
@Directive({
selector: 'input',
properties: [
'tabIndex'
],
host: {
'[tabIndex]': 'tabIndex',
'[type]': 'type',
'(focus)': 'holder.receivedFocus(tabIndex)',
'(keydown)': 'keydown($event)'
}
})
class FocusInput {
constructor(elementRef: ElementRef, @Parent() holder: FocusHolder) {
this.elementRef = elementRef;
holder.register(this);
this.holder = holder;
}
focus() {
this.elementRef.nativeElement.focus();
}
keydown(ev) {
// prevent any keyboard typing when a holder has focus
if (ev.keyCode !== 9) {
ev.preventDefault();
ev.stopPropagation();
}
}
get type() {
// default to text type if unknown
return this._t || 'text';
}
set type(val) {
this._t = val;
}
}
const PREVIOUS_TAB_INDEX = 999;
const ACTIVE_TAB_INDEX = 1000;
const NEXT_TAB_INDEX = 1001;

View File

@ -1 +1,87 @@
// form import {IonicConfig} from '../../config/config';
import * as dom from '../../util/dom';
let inputRegistry = [];
let activeInput = null;
let lastInput = null;
export class IonInput {
constructor(
elementRef: ElementRef,
app: IonicApp,
scrollView: Content
) {
this.elementRef = elementRef;
this.app = app;
this.scrollView = scrollView;
inputRegistry.push(this);
}
hasFocus() {
return dom.hasFocus(this.elementRef);
}
focus() {
this.setFocus();
}
setFocus() {
// TODO: How do you do this w/ NG2?
this.elementRef.nativeElement.focus();
}
setFocusHolder(type) {
let focusHolder = this.app.focusHolder();
focusHolder && focusHolder.setFocusHolder(type);
}
isActiveInput(shouldBeActive) {
if (shouldBeActive) {
if (activeInput && activeInput !== lastInput) {
lastInput = activeInput;
}
activeInput = this;
let focusHolder = this.app.focusHolder();
focusHolder && focusHolder.setActiveInput(activeInput);
} else if (activeInput === this) {
lastInput = activeInput;
activeInput = null;
}
}
sibling(inc) {
let index = inputRegistry.indexOf(this);
if (index > -1) {
return inputRegistry[index + inc];
}
}
static focusPrevious() {
this.focusMove(-1);
}
static focusNext() {
this.focusMove(1);
}
static focusMove(inc) {
let input = activeInput || lastInput;
if (input) {
let siblingInput = input.sibling(inc);
siblingInput && siblingInput.focus();
}
}
static clearTabIndexes() {
for (let i = 0; i < inputRegistry.length; i++) {
inputRegistry[i].tabIndex = -1;
}
}
}

View File

@ -2,6 +2,28 @@ $item-input-padding: 6px 0 5px 0px;
ion-input { ion-input {
display: block; display: block;
}
input.disable-focus,
textarea.disable-focus,
select.disable-focus {
//pointer-events: none;
}
focus-holder input {
position: fixed;
top: 1px;
width: 9px;
left: -9999px;
z-index: 9999;
}
.scroll-padding.scroll-padding .scroll-content {
padding-bottom: 1000px;
}
/*ion-input {
display: block;
position: relative; position: relative;
@ -28,3 +50,4 @@ ion-input {
background-color: transparent; background-color: transparent;
} }
} }
*/

View File

@ -0,0 +1,198 @@
import {Directive, View, Parent, Ancestor, Optional, ElementRef, Attribute, forwardRef} from 'angular2/angular2';
import {IonicDirective} from '../../config/annotations';
import {IonicConfig} from '../../config/config';
import {IonInput} from './form';
import {Ion} from '../ion';
import {IonicApp} from '../app/app';
import {Content} from '../content/content';
import {ClickBlock} from '../../util/click-block';
import * as dom from '../../util/dom';
import {Platform} from '../../platform/platform';
@IonicDirective({
selector: 'ion-input'
})
export class Input extends Ion {
constructor(
elementRef: ElementRef,
ionicConfig: IonicConfig
) {
super(elementRef, ionicConfig);
this.id = ++inputIds;
}
onInit() {
if (this.input) {
this.input.id = 'input-' + this.id;
}
if (this.label) {
this.label.id = 'label-' + this.id;
this.input.labelledBy = this.label.id;
}
}
registerInput(directive) {
this.input = directive;
}
registerLabel(directive) {
this.label = directive;
}
}
@Directive({
selector: 'textarea,input[type=text],input[type=password],input[type=number],input[type=search],input[type=email],input[type=url]',
property: [
'tabIndex'
],
host: {
'[tabIndex]': 'tabIndex',
'(focus)': 'receivedFocus(true)',
'(blur)': 'receivedFocus(false)',
'(touchstart)': 'pointerStart($event)',
'(touchend)': 'pointerEnd($event)',
'(mousedown)': 'pointerStart($event)',
'(mouseup)': 'pointerEnd($event)',
'[attr.id]': 'id',
'[attr.aria-labelledby]': 'labelledBy',
'[class.disable-focus]': 'disableFocus'
}
})
export class TextInput extends IonInput {
constructor(
@Optional() @Parent() container: Input,
@Optional() @Ancestor() scrollView: Content,
@Attribute('type') type: string,
elementRef: ElementRef,
app: IonicApp,
config: IonicConfig
) {
super(elementRef, app, scrollView);
if (container) {
container.registerInput(this);
this.container = container;
}
this.type = type;
this.elementRef = elementRef;
this.tabIndex = this.tabIndex || '';
this.scrollAssist = config.setting('keyboardScrollAssist');
}
pointerStart(ev) {
if (this.scrollAssist) {
this.startCoord = dom.pointerCoord(ev);
this.disableFocus = true;
}
}
pointerEnd(ev) {
if (this.scrollAssist) {
let endCoord = dom.pointerCoord(ev);
if (this.startCoord && !dom.hasPointerMoved(20, this.startCoord, endCoord) && !this.hasFocus()) {
ev.preventDefault();
ev.stopPropagation();
this.focus();
} else {
this.disableFocus = false;
}
this.startCoord = null;
}
}
focus() {
let scrollView = this.scrollView;
if (scrollView && this.scrollAssist) {
this.disableFocus = true;
// this input is inside of a scroll view
// scroll the input to the top
let inputY = this.elementRef.nativeElement.offsetTop - 8;
// do not allow any clicks while it's scrolling
ClickBlock(true, SCROLL_INTO_VIEW_DURATION + 200);
// used to put a lot of padding on the bottom of the scroll view
scrollView.scrollPadding = true;
// 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
this.setFocusHolder(this.type);
// scroll the input into place
scrollView.scrollTo(0, inputY, SCROLL_INTO_VIEW_DURATION, 8).then(() => {
// the scroll view is in the correct position now
// give the native input the focus
this.setFocus();
// all good, allow clicks again
ClickBlock(false);
this.disableFocus = false;
});
} else {
// not inside of a scroll view, just focus it
this.setFocus();
this.disableFocus = false;
}
}
receivedFocus(receivedFocus) {
let self = this;
let scrollView = self.scrollView;
self.isActiveInput(receivedFocus);
function touchMove(ev) {
self.setFocusHolder(self.type);
self.deregTouchMove();
}
if (scrollView && this.scrollAssist) {
if (receivedFocus) {
// when the input has focus, then the focus holder
// should not be able to be focused
self.deregTouchMove = scrollView && scrollView.addTouchMoveListener(touchMove);
} else {
// the input no longer has focus
self.deregTouchMove && self.deregTouchMove();
}
}
}
}
@Directive({
selector: 'label',
host: {
'[attr.id]': 'id'
}
})
export class InputLabel {
constructor(@Optional() @Parent() container: Input) {
if (container) {
container.registerLabel(this);
}
}
}
const SCROLL_INTO_VIEW_DURATION = 500;
let inputIds = -1;

View File

@ -1,12 +0,0 @@
import {Component, Directive} from 'angular2/angular2';
@Directive({
selector: 'ion-input'
})
export class Input {
constructor() {
//this.config = Button.config.invoke(this)
console.log('INPUT');
}
}

View File

@ -0,0 +1,7 @@
import {Directive} from 'angular2/angular2';
@Directive({
selector: 'ion-label'
})
export class Label {}

View File

@ -1,10 +0,0 @@
import {Component, Directive} from 'angular2/angular2';
@Directive({
selector: 'ion-label'
})
export class Label {
constructor() {
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,7 @@
import {App} from 'ionic/ionic';
@App({
templateUrl: 'main.html'
})
class IonicApp {}

View File

@ -0,0 +1,92 @@
<ion-toolbar><ion-title>Header</ion-title></ion-toolbar>
<ion-content class="padding">
<ion-input>
<input value="1" type="text">
</ion-input>
<ion-input>
<input value="2" type="text">
</ion-input>
<ion-input>
<input value="3" type="number">
</ion-input>
<ion-input>
<textarea>4</textarea>
</ion-input>
<ion-input>
<input value="5" type="text">
</ion-input>
<ion-input>
<input value="6" type="text">
</ion-input>
<ion-input>
<textarea>7</textarea>
</ion-input>
<ion-input>
<input value="8" type="text">
</ion-input>
<ion-input>
<input value="9" type="number">
</ion-input>
<ion-input>
<textarea>10</textarea>
</ion-input>
<ion-input>
<input value="11" type="text">
</ion-input>
<ion-input>
<input value="12" type="number">
</ion-input>
<ion-input>
<textarea>13</textarea>
</ion-input>
<ion-input>
<textarea>14</textarea>
</ion-input>
</ion-content>
<ion-toolbar><ion-title>Footer</ion-title></ion-toolbar>
<style>
input, textarea {
border: 1px solid red !important;
display: inline-block !important;
}
focus-holder input:focus {
background: red !important;
}
focus-holder input:nth-child(1) {
left: 200px !important;
}
focus-holder input:nth-child(2) {
left: 240px !important;
}
focus-holder input:nth-child(3) {
left: 280px !important;
}
</style>

View File

@ -11,7 +11,8 @@ import {
List, Item, ItemGroup, ItemGroupTitle, List, Item, ItemGroup, ItemGroupTitle,
Toolbar, Toolbar,
Icon, Icon,
Checkbox, Switch, Label, Input, Checkbox, Switch,
Input, TextInput, InputLabel,
Segment, SegmentButton, SegmentControlValueAccessor, Segment, SegmentButton, SegmentControlValueAccessor,
RadioGroup, RadioButton, SearchBar, RadioGroup, RadioButton, SearchBar,
Nav, NavbarTemplate, Navbar, NavPush, NavPop, Nav, NavbarTemplate, Navbar, NavPush, NavPop,
@ -50,13 +51,18 @@ export const IonicDirectives = [
// Media // Media
forwardRef(() => Icon), forwardRef(() => Icon),
// Form elements // Form
forwardRef(() => Segment), forwardRef(() => Segment),
forwardRef(() => SegmentButton), forwardRef(() => SegmentButton),
forwardRef(() => SegmentControlValueAccessor), forwardRef(() => SegmentControlValueAccessor),
//Checkbox, Switch, Label, Input //Checkbox, Switch
//RadioGroup, RadioButton, SearchBar, //RadioGroup, RadioButton, SearchBar,
// Input
forwardRef(() => Input),
forwardRef(() => TextInput),
forwardRef(() => InputLabel),
// Nav // Nav
forwardRef(() => Nav), forwardRef(() => Nav),
forwardRef(() => NavbarTemplate), forwardRef(() => NavbarTemplate),

View File

@ -33,8 +33,8 @@
"components/content/content", "components/content/content",
"components/item/item", "components/item/item",
"components/form/form", "components/form/form",
"components/form/label/label", "components/form/label",
"components/form/input/input", "components/form/input",
"components/layout/layout", "components/layout/layout",
"components/list/list", "components/list/list",
"components/modal/modal", "components/modal/modal",

View File

@ -71,7 +71,8 @@ Platform.register({
settings: { settings: {
mode: 'ios', mode: 'ios',
viewTransition: 'ios', viewTransition: 'ios',
tapPolyfill: true tapPolyfill: true,
keyboardScrollAssist: true
}, },
isMatch(p) { isMatch(p) {
return p.isPlatform('ios', 'iphone|ipad|ipod'); return p.isPlatform('ios', 'iphone|ipad|ipod');

View File

@ -3,6 +3,10 @@ const DEFAULT_EXPIRE = 330;
let cbEle, fallbackTimerId; let cbEle, fallbackTimerId;
let isShowing = false; let isShowing = false;
function disableInput(ev) {
ev.preventDefault();
ev.stopPropagation();
}
function show(expire) { function show(expire) {
clearTimeout(fallbackTimerId); clearTimeout(fallbackTimerId);
@ -18,6 +22,7 @@ function show(expire) {
cbEle.className = 'click-block ' + CSS_CLICK_BLOCK; cbEle.className = 'click-block ' + CSS_CLICK_BLOCK;
document.body.appendChild(cbEle); document.body.appendChild(cbEle);
} }
cbEle.addEventListener('touchmove', disableInput);
} }
} }
@ -26,6 +31,7 @@ function hide() {
if (isShowing) { if (isShowing) {
cbEle.classList.remove(CSS_CLICK_BLOCK); cbEle.classList.remove(CSS_CLICK_BLOCK);
isShowing = false; isShowing = false;
cbEle.removeEventListener('touchmove', disableInput);
} }
} }

View File

@ -158,3 +158,27 @@ export function windowLoad(callback) {
return promise; return promise;
} }
export function pointerCoord(ev) {
// get coordinates for either a mouse click
// or a touch depending on the given event
let c = { x: 0, y: 0 };
if (ev) {
const touches = ev.touches && ev.touches.length ? ev.touches : [ev];
const e = (ev.changedTouches && ev.changedTouches[0]) || touches[0];
if (e) {
c.x = e.clientX || e.pageX || 0;
c.y = e.clientY || e.pageY || 0;
}
}
return c;
}
export function hasPointerMoved(tolerance, startCoord, endCoord) {
return Math.abs(startCoord.x - endCoord.x) > tolerance ||
Math.abs(startCoord.y - endCoord.y) > tolerance;
}
export function hasFocus(ele) {
return !!(ele && (document.activeElement === ele.nativeElement || document.activeElement === ele));
}

View File

@ -103,8 +103,8 @@ var tapEventListeners = {
Platform.ready().then(config => { Platform.ready().then(config => {
if (config.setting('tapPolyfill')) { if (config.setting('tapPolyfill')) {
console.log('Tap.register, tapPolyfill') // console.log('Tap.register, tapPolyfill')
Tap.register(document); // Tap.register(document);
} }
}); });