From ccfce9d03433809b7d0795bfe5931e6c338778cc Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Sun, 23 Apr 2017 21:54:34 -0500 Subject: [PATCH] feat(toggle): boolean input directive --- .../angular/components/boolean-input.ts | 124 +++++ .../angular/providers}/ionic-core.ts | 9 +- src/components/badge/badge.scss | 2 +- src/components/badge/badge.ts | 2 +- src/components/card/card-content.ios.scss | 3 + src/components/card/card-content.md.scss | 3 + src/components/card/card-content.scss | 9 + src/components/card/card-content.ts | 23 +- src/components/card/card-content.wp.scss | 3 + src/components/card/card-header.ios.scss | 2 + src/components/card/card-header.md.scss | 2 + src/components/card/card-header.scss | 14 + src/components/card/card-header.ts | 23 +- src/components/card/card-header.wp.scss | 2 + src/components/card/card-title.ts | 23 +- src/components/card/card.scss | 20 +- src/components/card/card.ts | 24 +- .../basic/pages/root-page/root-page.module.ts | 5 +- src/components/gesture/gesture-controller.ts | 173 +++++++ src/components/gesture/gesture.ts | 365 +++++++++++++++ src/components/gesture/recognizers.ts | 66 +++ src/components/tabs/tabs.module.ts | 37 -- .../test/basic/pages/root-page/root-page.ts | 9 +- src/components/toggle/test/toggle.spec.ts | 29 -- src/components/toggle/toggle-gesture.ts | 53 --- src/components/toggle/toggle.ios.scss | 2 + src/components/toggle/toggle.md.scss | 2 + src/components/toggle/toggle.scss | 35 ++ src/components/toggle/toggle.ts | 339 +++++--------- src/components/toggle/toggle.wp.scss | 2 + src/index.ts | 27 +- src/module.ts | 32 +- src/util/decorators.ts | 19 - src/util/dom.ts | 86 ++++ src/util/interfaces.ts | 424 ++++++++++++++++++ 35 files changed, 1529 insertions(+), 464 deletions(-) create mode 100644 src/bindings/angular/components/boolean-input.ts rename src/{util => bindings/angular/providers}/ionic-core.ts (91%) create mode 100644 src/components/card/card-content.ios.scss create mode 100644 src/components/card/card-content.md.scss create mode 100644 src/components/card/card-content.scss create mode 100644 src/components/card/card-content.wp.scss create mode 100644 src/components/card/card-header.ios.scss create mode 100644 src/components/card/card-header.md.scss create mode 100644 src/components/card/card-header.scss create mode 100644 src/components/card/card-header.wp.scss create mode 100644 src/components/gesture/gesture-controller.ts create mode 100644 src/components/gesture/gesture.ts create mode 100644 src/components/gesture/recognizers.ts delete mode 100644 src/components/tabs/tabs.module.ts delete mode 100644 src/components/toggle/test/toggle.spec.ts delete mode 100644 src/components/toggle/toggle-gesture.ts create mode 100644 src/components/toggle/toggle.scss delete mode 100644 src/util/decorators.ts create mode 100644 src/util/interfaces.ts diff --git a/src/bindings/angular/components/boolean-input.ts b/src/bindings/angular/components/boolean-input.ts new file mode 100644 index 0000000000..4dd0f724da --- /dev/null +++ b/src/bindings/angular/components/boolean-input.ts @@ -0,0 +1,124 @@ +import { Directive, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; + + +/** + * @hidden + */ +@Directive({ + selector: 'ion-toggle', + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: BooleanInput, multi: true }], +}) +export class BooleanInput { + id: string; + _labelId: string; + _chg: Function; + _tch: Function; + _dis: Function; + + @Output() ionChange: EventEmitter = new EventEmitter(); + @Output() ionFocus: EventEmitter = new EventEmitter(); + @Output() ionBlur: EventEmitter = new EventEmitter(); + + + constructor(public _el: ElementRef) {} + + /** + * @input {boolean} If true, the element is selected. + */ + @Input() + get checked(): boolean { + return this._el.nativeElement.checked; + } + set checked(val: boolean) { + this._el.nativeElement.checked = val; + } + + /** + * @input {boolean} If true, the user cannot interact with this element. + */ + @Input() + get disabled(): boolean { + return this._el.nativeElement.disabled; + } + set disabled(val: boolean) { + this._el.nativeElement.disabled = val; + } + + /** + * @input {string} + */ + @Input() + get value(): string { + return this._el.nativeElement.value; + } + set value(val: string) { + this._el.nativeElement.value = val; + } + + /** + * @hidden + */ + @HostListener('$ionChange', ['$event']) + _onChange(ev: any) { + ev.stopPropagation(); + this._chg && this._chg(ev.detail.checked); + this._tch && this._tch(); + this.ionChange.emit(this); + } + + /** + * @hidden + */ + @HostListener('$ionFocus', ['$event']) + _onFocus(ev: any) { + ev.stopPropagation(); + this.ionFocus.emit(this); + } + + /** + * @hidden + */ + @HostListener('$ionBlur', ['$event']) + _onBlur(ev: any) { + ev.stopPropagation(); + this.ionBlur.emit(this); + } + + /** + * @hidden + */ + writeValue(val: any) { + if (this.checked !== val) { + this.checked = val; + } + } + + /** + * @hidden + */ + registerOnChange(fn: any) { + this._chg = fn; + } + + /** + * @hidden + */ + registerOnTouched(fn: any) { + this._tch = fn; + } + + /** + * @hidden + */ + registerOnDisabledChange(fn: any) { + this._dis = fn; + } + + /** + * @hidden + */ + setDisabledState(isDisabled: boolean) { + this.disabled = isDisabled; + } +} diff --git a/src/util/ionic-core.ts b/src/bindings/angular/providers/ionic-core.ts similarity index 91% rename from src/util/ionic-core.ts rename to src/bindings/angular/providers/ionic-core.ts index 709b9d0232..9653e68ce5 100644 --- a/src/util/ionic-core.ts +++ b/src/bindings/angular/providers/ionic-core.ts @@ -1,7 +1,7 @@ -import { Config } from '../config/config'; -import { DomController } from '../platform/dom-controller'; +import { Config } from '../../../config/config'; +import { DomController } from '../../../platform/dom-controller'; import { NgZone } from '@angular/core'; -import { Platform } from '../platform/platform'; +import { Platform } from '../../../platform/platform'; /** @@ -27,6 +27,9 @@ export function setupCore(config: Config, plt: Platform, domCtrl: DomController, // keep core and angular dom reads/writes *nsync ionic['domCtrl'] = domCtrl; + // keep core and angular dom reads/writes *nsync + ionic['eventNamePrefix'] = '$'; + // next tick controller created here so that it can // be created to run outside of angular ionic['nextTickCtrl'] = getNextTickController(zone, plt.userAgent().toLowerCase()); diff --git a/src/components/badge/badge.scss b/src/components/badge/badge.scss index 4e1eaa6daf..5503a6e397 100644 --- a/src/components/badge/badge.scss +++ b/src/components/badge/badge.scss @@ -15,7 +15,7 @@ ion-badge, :host { display: inline-block; visibility: inherit !important; - contain: layout content; + contain: content; } .badge { diff --git a/src/components/badge/badge.ts b/src/components/badge/badge.ts index b268116e8e..543012dba8 100644 --- a/src/components/badge/badge.ts +++ b/src/components/badge/badge.ts @@ -1,4 +1,4 @@ -import { Component } from '../../util/decorators'; +import { Component } from '../../index'; @Component({ diff --git a/src/components/card/card-content.ios.scss b/src/components/card/card-content.ios.scss new file mode 100644 index 0000000000..3c75fec439 --- /dev/null +++ b/src/components/card/card-content.ios.scss @@ -0,0 +1,3 @@ +@import "../../themes/ionic.globals"; +@import "./card-content"; + diff --git a/src/components/card/card-content.md.scss b/src/components/card/card-content.md.scss new file mode 100644 index 0000000000..3c75fec439 --- /dev/null +++ b/src/components/card/card-content.md.scss @@ -0,0 +1,3 @@ +@import "../../themes/ionic.globals"; +@import "./card-content"; + diff --git a/src/components/card/card-content.scss b/src/components/card/card-content.scss new file mode 100644 index 0000000000..9d8f45cddb --- /dev/null +++ b/src/components/card/card-content.scss @@ -0,0 +1,9 @@ +@import "../../themes/ionic.globals"; + +// Card +// -------------------------------------------------- + +ion-card-content, +:host { + display: block; +} diff --git a/src/components/card/card-content.ts b/src/components/card/card-content.ts index 4387d3a1b6..992b0b9f11 100644 --- a/src/components/card/card-content.ts +++ b/src/components/card/card-content.ts @@ -1,17 +1,12 @@ -import { Directive, ElementRef, Renderer } from '@angular/core'; +import { Component } from '../../index'; -import { Config } from '../../config/config'; -import { Ion } from '../ion'; -/** - * @hidden - */ -@Directive({ - selector: 'ion-card-content' -}) -export class CardContent extends Ion { - - constructor(config: Config, elementRef: ElementRef, renderer: Renderer) { - super(config, elementRef, renderer, 'card-content'); +@Component({ + tag: 'ion-card-content', + styleUrls: { + ios: 'card-content.ios.scss', + md: 'card-content.md.scss', + wp: 'card-content.wp.scss' } -} +}) +export class CardContent {} diff --git a/src/components/card/card-content.wp.scss b/src/components/card/card-content.wp.scss new file mode 100644 index 0000000000..3c75fec439 --- /dev/null +++ b/src/components/card/card-content.wp.scss @@ -0,0 +1,3 @@ +@import "../../themes/ionic.globals"; +@import "./card-content"; + diff --git a/src/components/card/card-header.ios.scss b/src/components/card/card-header.ios.scss new file mode 100644 index 0000000000..605e7f43b8 --- /dev/null +++ b/src/components/card/card-header.ios.scss @@ -0,0 +1,2 @@ +@import "../../themes/ionic.globals"; +@import "./card-header"; diff --git a/src/components/card/card-header.md.scss b/src/components/card/card-header.md.scss new file mode 100644 index 0000000000..605e7f43b8 --- /dev/null +++ b/src/components/card/card-header.md.scss @@ -0,0 +1,2 @@ +@import "../../themes/ionic.globals"; +@import "./card-header"; diff --git a/src/components/card/card-header.scss b/src/components/card/card-header.scss new file mode 100644 index 0000000000..707b9d5a54 --- /dev/null +++ b/src/components/card/card-header.scss @@ -0,0 +1,14 @@ +@import "../../themes/ionic.globals"; + +// Card Header +// -------------------------------------------------- + + +ion-card-header, +:host { + display: block; + overflow: hidden; + + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/components/card/card-header.ts b/src/components/card/card-header.ts index 887158c096..28ba5a2c6f 100644 --- a/src/components/card/card-header.ts +++ b/src/components/card/card-header.ts @@ -1,17 +1,12 @@ -import { Directive, ElementRef, Renderer } from '@angular/core'; +import { Component } from '../../index'; -import { Config } from '../../config/config'; -import { Ion } from '../ion'; -/** - * @hidden - */ -@Directive({ - selector: 'ion-card-header' -}) -export class CardHeader extends Ion { - - constructor(config: Config, elementRef: ElementRef, renderer: Renderer) { - super(config, elementRef, renderer, 'card-header'); +@Component({ + tag: 'ion-card-header', + styleUrls: { + ios: 'card-header.ios.scss', + md: 'card-header.md.scss', + wp: 'card-header.wp.scss' } -} +}) +export class CardHeader {} diff --git a/src/components/card/card-header.wp.scss b/src/components/card/card-header.wp.scss new file mode 100644 index 0000000000..605e7f43b8 --- /dev/null +++ b/src/components/card/card-header.wp.scss @@ -0,0 +1,2 @@ +@import "../../themes/ionic.globals"; +@import "./card-header"; diff --git a/src/components/card/card-title.ts b/src/components/card/card-title.ts index f24cc2bde9..709f6768a5 100644 --- a/src/components/card/card-title.ts +++ b/src/components/card/card-title.ts @@ -1,17 +1,12 @@ -import { Directive, ElementRef, Renderer } from '@angular/core'; +import { Component } from '../../index'; -import { Config } from '../../config/config'; -import { Ion } from '../ion'; -/** - * @hidden - */ -@Directive({ - selector: 'ion-card-title' -}) -export class CardTitle extends Ion { - - constructor(config: Config, elementRef: ElementRef, renderer: Renderer) { - super(config, elementRef, renderer, 'card-title'); +@Component({ + tag: 'ion-card-title', + styleUrls: { + ios: 'card.ios.scss', + md: 'card.md.scss', + wp: 'card.wp.scss' } -} +}) +export class CardTitle {} diff --git a/src/components/card/card.scss b/src/components/card/card.scss index 53530627eb..facebf4d63 100644 --- a/src/components/card/card.scss +++ b/src/components/card/card.scss @@ -4,25 +4,15 @@ // -------------------------------------------------- -ion-card { +ion-card, +:host { display: block; overflow: hidden; } -ion-card img { +ion-card img, +:host img { display: block; width: 100%; -} - -ion-card-header { - display: block; - overflow: hidden; - - text-overflow: ellipsis; - white-space: nowrap; -} - -ion-card-content { - display: block; -} +} \ No newline at end of file diff --git a/src/components/card/card.ts b/src/components/card/card.ts index 6089b7338a..845f5b7661 100644 --- a/src/components/card/card.ts +++ b/src/components/card/card.ts @@ -1,18 +1,12 @@ -import { Directive, ElementRef, Renderer } from '@angular/core'; - -import { Config } from '../../config/config'; -import { Ion } from '../ion'; +import { Component } from '../../index'; -/** - * @hidden - */ -@Directive({ - selector: 'ion-card' -}) -export class Card extends Ion { - - constructor(config: Config, elementRef: ElementRef, renderer: Renderer) { - super(config, elementRef, renderer, 'card'); +@Component({ + tag: 'ion-card', + styleUrls: { + ios: 'card.ios.scss', + md: 'card.md.scss', + wp: 'card.wp.scss' } -} +}) +export class Card {} diff --git a/src/components/card/test/basic/pages/root-page/root-page.module.ts b/src/components/card/test/basic/pages/root-page/root-page.module.ts index 85fba79181..1ef16a67da 100644 --- a/src/components/card/test/basic/pages/root-page/root-page.module.ts +++ b/src/components/card/test/basic/pages/root-page/root-page.module.ts @@ -1,4 +1,4 @@ -import { NgModule } from '@angular/core'; +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { IonicPageModule } from '../../../../../..'; import { RootPage } from './root-page'; @@ -9,6 +9,9 @@ import { RootPage } from './root-page'; ], imports: [ IonicPageModule.forChild(RootPage) + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA ] }) export class RootPageModule {} diff --git a/src/components/gesture/gesture-controller.ts b/src/components/gesture/gesture-controller.ts new file mode 100644 index 0000000000..a41d3bb8ac --- /dev/null +++ b/src/components/gesture/gesture-controller.ts @@ -0,0 +1,173 @@ + + +export class GestureController { + private id: number = 0; + private requestedStart: { [eventId: number]: number } = {}; + private disabledGestures: { [eventName: string]: Set } = {}; + private disabledScroll: Set = new Set(); + private capturedID: number = null; + + + createGesture(gestureName: string, gesturePriority: number, disableScroll: boolean): GestureDelegate { + return new GestureDelegate(this, ++this.id, gestureName, gesturePriority, disableScroll); + } + + start(gestureName: string, id: number, priority: number): boolean { + if (!this.canStart(gestureName)) { + delete this.requestedStart[id]; + return false; + } + + this.requestedStart[id] = priority; + return true; + } + + capture(gestureName: string, id: number, priority: number): boolean { + if (!this.start(gestureName, id, priority)) { + return false; + } + let requestedStart = this.requestedStart; + let maxPriority = -10000; + for (let gestureID in requestedStart) { + maxPriority = Math.max(maxPriority, requestedStart[gestureID]); + } + + if (maxPriority === priority) { + this.capturedID = id; + this.requestedStart = {}; + return true; + } + delete requestedStart[id]; + + return false; + } + + release(id: number) { + delete this.requestedStart[id]; + + if (this.capturedID && id === this.capturedID) { + this.capturedID = null; + } + } + + disableGesture(gestureName: string, id: number) { + let set = this.disabledGestures[gestureName]; + if (!set) { + set = new Set(); + this.disabledGestures[gestureName] = set; + } + set.add(id); + } + + enableGesture(gestureName: string, id: number) { + let set = this.disabledGestures[gestureName]; + if (set) { + set.delete(id); + } + } + + disableScroll(id: number) { + // let isEnabled = !this.isScrollDisabled(); + this.disabledScroll.add(id); + // if (this._app && isEnabled && this.isScrollDisabled()) { + // console.debug('GestureController: Disabling scrolling'); + // this._app._setDisableScroll(true); + // } + } + + enableScroll(id: number) { + // let isDisabled = this.isScrollDisabled(); + this.disabledScroll.delete(id); + // if (this._app && isDisabled && !this.isScrollDisabled()) { + // console.debug('GestureController: Enabling scrolling'); + // this._app._setDisableScroll(false); + // } + } + + canStart(gestureName: string): boolean { + if (this.capturedID) { + // a gesture already captured + return false; + } + + if (this.isDisabled(gestureName)) { + return false; + } + + return true; + } + + isCaptured(): boolean { + return !!this.capturedID; + } + + isScrollDisabled(): boolean { + return this.disabledScroll.size > 0; + } + + isDisabled(gestureName: string): boolean { + let disabled = this.disabledGestures[gestureName]; + if (disabled && disabled.size > 0) { + return true; + } + return false; + } + +} + + +export class GestureDelegate { + + constructor( + private ctrl: GestureController, + private id: number, + private name: string, + private priority: number, + private disableScroll: boolean + ) { } + + canStart(): boolean { + if (!this.ctrl) { + return false; + } + + return this.ctrl.canStart(this.name); + } + + start(): boolean { + if (!this.ctrl) { + return false; + } + + return this.ctrl.start(this.name, this.id, this.priority); + } + + capture(): boolean { + if (!this.ctrl) { + return false; + } + + let captured = this.ctrl.capture(this.name, this.id, this.priority); + if (captured && this.disableScroll) { + this.ctrl.disableScroll(this.id); + } + + return captured; + } + + release() { + if (this.ctrl) { + this.ctrl.release(this.id); + + if (this.disableScroll) { + this.ctrl.enableScroll(this.id); + } + } + } + + destroy() { + this.release(); + this.ctrl = null; + } + +} diff --git a/src/components/gesture/gesture.ts b/src/components/gesture/gesture.ts new file mode 100644 index 0000000000..27e59a015e --- /dev/null +++ b/src/components/gesture/gesture.ts @@ -0,0 +1,365 @@ +import { applyStyles, getElementReference, pointerCoordX, pointerCoordY } from '../../util/dom'; +import { Component, Listen, Ionic, Prop } from '../../index'; +import { GestureCallback, GestureDetail } from '../../util/interfaces'; +import { GestureController, GestureDelegate } from './gesture-controller'; +import { PanRecognizer } from './recognizers'; + + +@Component({ + tag: 'ion-gesture', + shadow: false +}) +export class Gesture { + private $el: HTMLElement; + private detail: GestureDetail = {}; + private positions: number[] = []; + private gesture: GestureDelegate; + private lastTouch = 0; + private pan: PanRecognizer; + private hasCapturedPan = false; + private hasPress = false; + private hasStartedPan = false; + private requiresMove = false; + + @Prop() direction: string = 'x'; + @Prop() gestureName: string = ''; + @Prop() gesturePriority: number = 0; + @Prop() listenOn: string = 'child'; + @Prop() maxAngle: number = 40; + @Prop() threshold: number = 20; + @Prop() type: string = 'pan'; + + @Prop() canStart: GestureCallback; + @Prop() onStart: GestureCallback; + @Prop() onMove: GestureCallback; + @Prop() onEnd: GestureCallback; + @Prop() onPress: GestureCallback; + @Prop() notCaptured: GestureCallback; + + + ionViewDidLoad() { + Ionic.controllers.gesture = (Ionic.controllers.gesture || new GestureController()); + + this.gesture = (Ionic.controllers.gesture).createGesture(this.gestureName, this.gesturePriority, false); + + const types = this.type.replace(/\s/g, '').toLowerCase().split(','); + + if (types.indexOf('pan') > -1) { + this.pan = new PanRecognizer(this.direction, this.threshold, this.maxAngle); + this.requiresMove = true; + } + this.hasPress = (types.indexOf('press') > -1); + + if (this.pan || this.hasPress) { + Ionic.listener.enable(this, 'touchstart', true, this.listenOn); + Ionic.listener.enable(this, 'mousedown', true, this.listenOn); + + Ionic.dom.write(() => { + applyStyles(getElementReference(this.$el, this.listenOn), GESTURE_INLINE_STYLES); + }); + } + } + + + // DOWN ************************* + + @Listen('touchstart', { passive: true, enabled: false }) + onTouchStart(ev: TouchEvent) { + this.lastTouch = now(ev); + + this.enableMouse(false); + this.enableTouch(true); + + this.pointerDown(ev, this.lastTouch); + } + + + @Listen('mousedown', { passive: true, enabled: false }) + onMouseDown(ev: MouseEvent) { + const timeStamp = now(ev); + + if (this.lastTouch === 0 || (this.lastTouch + MOUSE_WAIT < timeStamp)) { + this.enableMouse(true); + this.enableTouch(false); + + this.pointerDown(ev, timeStamp); + } + } + + + private pointerDown(ev: UIEvent, timeStamp: number): boolean { + if (!this.gesture || this.hasStartedPan) { + return false; + } + + const detail = this.detail; + + detail.startX = detail.currentX = pointerCoordX(ev); + detail.startY = detail.currentY = pointerCoordY(ev); + detail.startTimeStamp = detail.timeStamp = timeStamp; + detail.velocityX = detail.velocityY = detail.deltaX = detail.deltaY = 0; + detail.directionX = detail.directionY = detail.velocityDirectionX = detail.velocityDirectionY = null; + detail.event = ev; + this.positions.length = 0; + + if (this.canStart && this.canStart(detail) === false) { + return false; + } + + this.positions.push(detail.currentX, detail.currentY, timeStamp); + + // Release fallback + this.gesture.release(); + + // Start gesture + if (!this.gesture.start()) { + return false; + } + + if (this.pan) { + this.hasStartedPan = true; + this.hasCapturedPan = false; + + this.pan.start(detail.startX, detail.startY); + } + + return true; + } + + + // MOVE ************************* + + @Listen('touchmove', { passive: true, enabled: false }) + onTouchMove(ev: TouchEvent) { + this.lastTouch = this.detail.timeStamp = now(ev); + + this.pointerMove(ev); + } + + + @Listen('document:mousemove', { passive: true, enabled: false }) + onMoveMove(ev: TouchEvent) { + const timeStamp = now(ev); + + if (this.lastTouch === 0 || (this.lastTouch + MOUSE_WAIT < timeStamp)) { + this.detail.timeStamp = timeStamp; + this.pointerMove(ev); + } + } + + private pointerMove(ev: UIEvent) { + const detail = this.detail; + this.calcGestureData(ev); + + if (this.pan) { + if (this.hasCapturedPan) { + Ionic.dom.write(() => { + detail.type = 'pan'; + + if (this.onMove) { + this.onMove(detail); + } else { + Ionic.emit(this, 'ionGestureMove', this.detail); + } + }); + + } else if (this.pan.detect(detail.currentX, detail.currentY)) { + if (this.pan.isGesture() !== 0) { + if (!this.tryToCapturePan(ev)) { + this.abortGesture(); + } + } + } + } + } + + private calcGestureData(ev: UIEvent) { + const detail = this.detail; + detail.currentX = pointerCoordX(ev); + detail.currentY = pointerCoordY(ev); + detail.deltaX = (detail.currentX - detail.startX); + detail.deltaY = (detail.currentY - detail.startY); + detail.event = ev; + + // figure out which direction we're movin' + detail.directionX = detail.velocityDirectionX = (detail.deltaX > 0 ? 'left' : (detail.deltaX < 0 ? 'right' : null)); + detail.directionY = detail.velocityDirectionY = (detail.deltaY > 0 ? 'up' : (detail.deltaY < 0 ? 'down' : null)); + + const positions = this.positions; + positions.push(detail.currentX, detail.currentY, detail.timeStamp); + + var endPos = (positions.length - 1); + var startPos = endPos; + var timeRange = (detail.timeStamp - 100); + + // move pointer to position measured 100ms ago + for (var i = endPos; i > 0 && positions[i] > timeRange; i -= 3) { + startPos = i; + } + + if (startPos !== endPos) { + // compute relative movement between these two points + var movedX = (positions[startPos - 2] - positions[endPos - 2]); + var movedY = (positions[startPos - 1] - positions[endPos - 1]); + var factor = 16 / (positions[endPos] - positions[startPos]); + + // based on XXms compute the movement to apply for each render step + detail.velocityX = movedX * factor; + detail.velocityY = movedY * factor; + + detail.velocityDirectionX = (detail.velocityX > 0 ? 'left' : (detail.velocityX < 0 ? 'right' : null)); + detail.velocityDirectionY = (detail.velocityY > 0 ? 'up' : (detail.velocityY < 0 ? 'down' : null)); + } + } + + private tryToCapturePan(ev: UIEvent): boolean { + if (this.gesture && !this.gesture.capture()) { + return false; + } + + this.detail.event = ev; + + if (this.onStart) { + this.onStart(this.detail); + } else { + Ionic.emit(this, 'ionGestureStart', this.detail); + } + + this.hasCapturedPan = true; + + return true; + } + + private abortGesture() { + this.hasStartedPan = false; + this.hasCapturedPan = false; + + this.gesture.release(); + + this.enable(false); + this.notCaptured(this.detail); + } + + + // END ************************* + + @Listen('touchend', { passive: true, enabled: false }) + onTouchEnd(ev: TouchEvent) { + this.lastTouch = this.detail.timeStamp = now(ev); + + this.pointerUp(ev); + this.enableTouch(false); + } + + + @Listen('document:mouseup', { passive: true, enabled: false }) + onMouseUp(ev: TouchEvent) { + const timeStamp = now(ev); + + if (this.lastTouch === 0 || (this.lastTouch + MOUSE_WAIT < timeStamp)) { + this.detail.timeStamp = timeStamp; + this.pointerUp(ev); + this.enableMouse(false); + } + } + + + private pointerUp(ev: UIEvent) { + const detail = this.detail; + + this.gesture && this.gesture.release(); + + detail.event = ev; + + this.calcGestureData(ev); + + if (this.pan) { + if (this.hasCapturedPan) { + detail.type = 'pan'; + if (this.onEnd) { + this.onEnd(detail); + } else { + Ionic.emit(this, 'ionGestureEnd', detail); + } + + } else if (this.hasPress) { + this.detectPress(); + + } else { + if (this.notCaptured) { + this.notCaptured(detail); + } else { + Ionic.emit(this, 'ionGestureNotCaptured', detail); + } + } + + } else if (this.hasPress) { + this.detectPress(); + } + + this.hasCapturedPan = false; + this.hasStartedPan = false; + } + + + private detectPress() { + const detail = this.detail; + + if (Math.abs(detail.startX - detail.currentX) < 10 && Math.abs(detail.startY - detail.currentY) < 10) { + detail.type = 'press'; + + if (this.onPress) { + this.onPress(detail); + } else { + Ionic.emit(this, 'ionPress', detail); + } + } + } + + + // ENABLE LISTENERS ************************* + + private enableMouse(shouldEnable: boolean) { + if (this.requiresMove) { + Ionic.listener.enable(this, 'document:mousemove', shouldEnable); + } + Ionic.listener.enable(this, 'document:mouseup', shouldEnable); + } + + + private enableTouch(shouldEnable: boolean) { + if (this.requiresMove) { + Ionic.listener.enable(this, 'touchmove', shouldEnable); + } + Ionic.listener.enable(this, 'touchend', shouldEnable); + } + + + private enable(shouldEnable: boolean) { + this.enableMouse(shouldEnable); + this.enableTouch(shouldEnable); + } + + + ionViewWillUnload() { + this.gesture && this.gesture.destroy(); + this.gesture = this.pan = this.detail = this.detail.event = null; + } + +} + + +const GESTURE_INLINE_STYLES = { + 'touch-action': 'none', + 'user-select': 'none', + '-webkit-user-drag': 'none', + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' +}; + +const MOUSE_WAIT = 2500; + + +function now(ev: UIEvent) { + return ev.timeStamp || Date.now(); +} + diff --git a/src/components/gesture/recognizers.ts b/src/components/gesture/recognizers.ts new file mode 100644 index 0000000000..230b65e289 --- /dev/null +++ b/src/components/gesture/recognizers.ts @@ -0,0 +1,66 @@ + + +export class PanRecognizer { + private startX: number; + private startY: number; + + private dirty: boolean = false; + private threshold: number; + private maxCosine: number; + + private angle = 0; + private isPan = 0; + + + constructor(private direction: string, threshold: number, maxAngle: number) { + const radians = maxAngle * (Math.PI / 180); + this.maxCosine = Math.cos(radians); + this.threshold = threshold * threshold; + } + + start(x: number, y: number) { + this.startX = x; + this.startY = y; + this.angle = 0; + this.isPan = 0; + this.dirty = true; + } + + detect(x: number, y: number): boolean { + if (!this.dirty) { + return false; + } + + const deltaX = (x - this.startX); + const deltaY = (y - this.startY); + const distance = deltaX * deltaX + deltaY * deltaY; + + if (distance >= this.threshold) { + var angle = Math.atan2(deltaY, deltaX); + var cosine = (this.direction === 'y') + ? Math.sin(angle) + : Math.cos(angle); + + this.angle = angle; + + if (cosine > this.maxCosine) { + this.isPan = 1; + + } else if (cosine < -this.maxCosine) { + this.isPan = -1; + + } else { + this.isPan = 0; + } + + this.dirty = false; + return true; + } + + return false; + } + + isGesture(): number { + return this.isPan; + } +} diff --git a/src/components/tabs/tabs.module.ts b/src/components/tabs/tabs.module.ts deleted file mode 100644 index ec64c0e3dd..0000000000 --- a/src/components/tabs/tabs.module.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule, ModuleWithProviders, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; - -import { IconModule } from '../icon/icon.module'; - -import { Tab } from './tab'; -import { TabButton } from './tab-button'; -import { TabHighlight } from './tab-highlight'; -import { Tabs } from './tabs'; - -/** @hidden */ -@NgModule({ - imports: [ - CommonModule, - IconModule - ], - declarations: [ - Tab, - TabButton, - TabHighlight, - Tabs - ], - exports: [ - Tab, - TabButton, - TabHighlight, - Tabs - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] -}) -export class TabsModule { - public static forRoot(): ModuleWithProviders { - return { - ngModule: TabsModule, providers: [] - }; - } -} diff --git a/src/components/toggle/test/basic/pages/root-page/root-page.ts b/src/components/toggle/test/basic/pages/root-page/root-page.ts index 86fd1566d8..cc8dc7cb2d 100644 --- a/src/components/toggle/test/basic/pages/root-page/root-page.ts +++ b/src/components/toggle/test/basic/pages/root-page/root-page.ts @@ -1,7 +1,6 @@ import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { Toggle } from '../../../../../../'; @Component({ templateUrl: 'root-page.html' @@ -38,20 +37,20 @@ export class RootPage { this.grapeCtrl.enabled ? this.grapeCtrl.disable() : this.grapeCtrl.enable(); } - appleChange(toggle: Toggle) { + appleChange(toggle: any) { console.log('appleChange', toggle); } - bananaChange(toggle: Toggle) { + bananaChange(toggle: any) { console.log('bananaChange', toggle); } - kiwiChange(toggle: Toggle) { + kiwiChange(toggle: any) { console.log('kiwiChange', toggle); this.kiwiValue = toggle.checked; } - strawberryChange(toggle: Toggle) { + strawberryChange(toggle: any) { console.log('strawberryChange', toggle); this.strawberryValue = toggle.checked; } diff --git a/src/components/toggle/test/toggle.spec.ts b/src/components/toggle/test/toggle.spec.ts deleted file mode 100644 index ee27d02c75..0000000000 --- a/src/components/toggle/test/toggle.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ - -import { Toggle } from '../toggle'; -import { mockConfig, mockPlatform, mockHaptic, mockElementRef, mockGestureController, mockRenderer, mockItem, mockForm, mockChangeDetectorRef, mockZone } from '../../../util/mock-providers'; -import { commonInputTest, BOOLEAN_CORPUS } from '../../../util/input-tester'; - -describe('Toggle', () => { - - it('should pass common test', () => { - - const platform = mockPlatform(); - const config = mockConfig(); - const elementRef = mockElementRef(); - const renderer = mockRenderer(); - const item: any = mockItem(); - const form = mockForm(); - const haptic = mockHaptic(); - const cd = mockChangeDetectorRef(); - const gesture = mockGestureController(); - const zone = mockZone(); - const toggle = new Toggle(form, config, platform, elementRef, renderer, haptic, item, gesture, null, cd, zone); - - commonInputTest(toggle, { - defaultValue: false, - corpus: BOOLEAN_CORPUS, - }); - - }); - -}); diff --git a/src/components/toggle/toggle-gesture.ts b/src/components/toggle/toggle-gesture.ts deleted file mode 100644 index 45d4e0084a..0000000000 --- a/src/components/toggle/toggle-gesture.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { GestureController, GESTURE_PRIORITY_TOGGLE, GESTURE_TOGGLE } from '../../gestures/gesture-controller'; -import { DomController } from '../../platform/dom-controller'; -import { PanGesture } from '../../gestures/pan-gesture'; -import { Platform } from '../../platform/platform'; -import { pointerCoord } from '../../util/dom'; -import { Toggle } from './toggle'; - -/** - * @hidden - */ -export class ToggleGesture extends PanGesture { - - constructor( - plt: Platform, - public toggle: Toggle, - gestureCtrl: GestureController, - domCtrl: DomController - ) { - super( - plt, - toggle.getNativeElement(), { - threshold: 0, - zone: false, - domController: domCtrl, - gesture: gestureCtrl.createGesture({ - name: GESTURE_TOGGLE, - priority: GESTURE_PRIORITY_TOGGLE - }) - }); - } - - canStart(ev: any): boolean { - return true; - } - - onDragStart(ev: any) { - ev.preventDefault(); - - this.toggle._onDragStart(pointerCoord(ev).x); - } - - onDragMove(ev: any) { - ev.preventDefault(); - - this.toggle._onDragMove(pointerCoord(ev).x); - } - - onDragEnd(ev: any) { - ev.preventDefault(); - - this.toggle._onDragEnd(pointerCoord(ev).x); - } -} diff --git a/src/components/toggle/toggle.ios.scss b/src/components/toggle/toggle.ios.scss index 2277d17350..6c821cacf9 100644 --- a/src/components/toggle/toggle.ios.scss +++ b/src/components/toggle/toggle.ios.scss @@ -1,4 +1,6 @@ @import "../../themes/ionic.globals.ios"; +@import "./toggle"; + // iOS Toggle // -------------------------------------------------- diff --git a/src/components/toggle/toggle.md.scss b/src/components/toggle/toggle.md.scss index 7d0b6bc528..4146c5f621 100644 --- a/src/components/toggle/toggle.md.scss +++ b/src/components/toggle/toggle.md.scss @@ -1,4 +1,6 @@ @import "../../themes/ionic.globals.md"; +@import "./toggle"; + // Material Design Toggle // -------------------------------------------------- diff --git a/src/components/toggle/toggle.scss b/src/components/toggle/toggle.scss new file mode 100644 index 0000000000..c539e5ba7c --- /dev/null +++ b/src/components/toggle/toggle.scss @@ -0,0 +1,35 @@ +@import "../../themes/ionic.globals"; + + +// Toggle +// -------------------------------------------------- + +:host { + display: inline-block; + visibility: inherit !important; + contain: content; +} + + +.toggle-cover { + position: absolute; + top: 0; + left: 0; + + width: 100%; + height: 100%; + + border: 0; + + background: transparent; + + font-family: inherit; + font-style: inherit; + font-variant: inherit; + + line-height: 1; + text-transform: none; + + cursor: pointer; + outline: none; +} diff --git a/src/components/toggle/toggle.ts b/src/components/toggle/toggle.ts index f92400a374..8a86f95c14 100644 --- a/src/components/toggle/toggle.ts +++ b/src/components/toggle/toggle.ts @@ -1,238 +1,147 @@ -import { NgZone, AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostListener, Input, OnDestroy, Optional, Renderer, ViewEncapsulation } from '@angular/core'; -import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { BooleanInputComponent, GestureDetail } from '../../util/interfaces'; +import { Component, h, Ionic, Listen, Prop, Watch } from '../../index'; -import { Config } from '../../config/config'; -import { DomController } from '../../platform/dom-controller'; -import { Form, IonicTapInput } from '../../util/form'; -import { GestureController } from '../../gestures/gesture-controller'; -import { Haptic } from '../../tap-click/haptic'; -import { assert, isTrueProperty } from '../../util/util'; -import { BaseInput } from '../../util/base-input'; -import { Item } from '../item/item'; -import { KEY_ENTER, KEY_SPACE } from '../../platform/key'; -import { Platform } from '../../platform/platform'; -import { ToggleGesture } from './toggle-gesture'; -/** - * @name Toggle - * @description - * A toggle technically is the same thing as an HTML checkbox input, - * except it looks different and is easier to use on a touch device. - * Toggles can also have colors assigned to them, by adding any color - * attribute. - * - * See the [Angular 2 Docs](https://angular.io/docs/ts/latest/guide/forms.html) - * for more info on forms and inputs. - * - * @usage - * ```html - * - * - * - * - * Pepperoni - * - * - * - * - * Sausage - * - * - * - * - * Mushrooms - * - * - * - * - * ``` - * - * @demo /docs/demos/src/toggle/ - * @see {@link /docs/components#toggle Toggle Component Docs} - */ @Component({ - selector: 'ion-toggle', - template: - '
' + - '
' + - '
' + - '', - host: { - '[class.toggle-disabled]': '_disabled', - '[class.toggle-checked]': '_value', - '[class.toggle-activated]': '_activated', - }, - providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: Toggle, multi: true } ], - encapsulation: ViewEncapsulation.None, + tag: 'ion-toggle', + styleUrls: { + ios: 'toggle.ios.scss', + md: 'toggle.md.scss', + wp: 'toggle.wp.scss' + } }) -export class Toggle extends BaseInput implements IonicTapInput, AfterViewInit, OnDestroy { +export class Toggle implements BooleanInputComponent { + activated: boolean; + hasFocus: boolean; + id: string; + labelId: string; + startX: number; - _activated: boolean = false; - _startX: number; - _msPrv: number = 0; - _gesture: ToggleGesture; + @Prop() checked: boolean; + @Prop() disabled: boolean; + @Prop() value: string; - /** - * @input {boolean} If true, the element is selected. - */ - @Input() - get checked(): boolean { - return this.value; + + @Watch('checked') + changed(val: boolean) { + Ionic.emit(this, 'ionChange', { checked: val }); } - set checked(val: boolean) { - this.value = val; + + canStart() { + return !this.disabled; } - constructor( - form: Form, - config: Config, - private _plt: Platform, - elementRef: ElementRef, - renderer: Renderer, - private _haptic: Haptic, - @Optional() item: Item, - private _gestureCtrl: GestureController, - private _domCtrl: DomController, - private _cd: ChangeDetectorRef, - private _zone: NgZone, - ) { - super(config, elementRef, renderer, 'toggle', false, form, item, null); + + onDragStart(detail: GestureDetail) { + this.startX = detail.startX; + this.fireFocus(); } - /** - * @hidden - */ - ngAfterViewInit() { - this._initialize(); - this._gesture = new ToggleGesture(this._plt, this, this._gestureCtrl, this._domCtrl); - this._gesture.listen(); - } - /** - * @hidden - */ - _inputCheckHasValue() {} - - /** - * @hidden - */ - _inputNormalize(val: any): boolean { - return isTrueProperty(val); - } - - /** - * @hidden - */ - _onDragStart(startX: number) { - assert(startX, 'startX must be valid'); - console.debug('toggle, _onDragStart', startX); - - this._zone.run(() => { - this._startX = startX; - this._fireFocus(); - this._activated = true; - }); - } - - /** - * @hidden - */ - _onDragMove(currentX: number) { - if (!this._startX) { - assert(false, '_startX must be valid'); - return; - } - - let dirty = false; - let value: boolean; - let activated: boolean; - - if (this._value) { - if (currentX + 15 < this._startX) { - dirty = true; - value = false; - activated = true; + onDragMove(detail: GestureDetail) { + if (this.checked) { + if (detail.currentX + 15 < this.startX) { + this.checked = false; + this.activated = true; + this.startX = detail.currentX; } - } else if (currentX - 15 > this._startX) { - dirty = true; - value = true; - activated = (currentX < this._startX + 5); + } else if (detail.currentX - 15 > this.startX) { + this.checked = true; + this.activated = (detail.currentX < this.startX + 5); + this.startX = detail.currentX; } - - if (dirty) { - this._zone.run(() => { - this.value = value; - this._startX = currentX; - this._activated = activated; - this._haptic.selection(); - }); - } - } - /** - * @hidden - */ - _onDragEnd(endX: number) { - if (!this._startX) { - assert(false, '_startX must be valid'); - return; - } - console.debug('toggle, _onDragEnd', endX); - this._zone.run(() => { - if (this._value) { - if (this._startX + 4 > endX) { - this.value = false; - this._haptic.selection(); + onDragEnd(detail: GestureDetail) { + if (this.checked) { + if (detail.startX + 4 > detail.currentX) { + this.checked = false; + } + + } else if (detail.startX - 4 < detail.currentX) { + this.checked = true; + } + + this.activated = false; + this.fireBlur(); + this.startX = null; + } + + + @Listen('keydown.space') + onSpace(ev: KeyboardEvent) { + this.toggle(); + ev.stopPropagation(); + ev.preventDefault(); + } + + + toggle() { + if (!this.disabled) { + this.checked = !this.checked; + this.fireFocus(); + } + } + + + fireFocus() { + if (!this.hasFocus) { + this.hasFocus = true; + Ionic.emit(this, 'ionFocus'); + } + } + + + fireBlur() { + if (this.hasFocus) { + this.hasFocus = false; + Ionic.emit(this, 'ionBlur'); + } + } + + + render() { + return h(this, + h('ion-gesture', Ionic.theme(this, 'toggle', { + class: { + 'toggle-activated': this.activated, + 'toggle-checked': this.checked, + 'toggle-disabled': this.disabled, + }, + props: { + 'canStart': this.canStart.bind(this), + 'onStart': this.onDragStart.bind(this), + 'onMove': this.onDragMove.bind(this), + 'onEnd': this.onDragEnd.bind(this), + 'onPress': this.toggle.bind(this), + 'gestureName': 'toggle', + 'gesturePriority': 30, + 'type': 'pan,press', + 'direction': 'x', + 'threshold': 20, + 'listenOn': 'parent' } - - } else if (this._startX - 4 < endX) { - this.value = true; - this._haptic.selection(); - } - - this._activated = false; - this._fireBlur(); - this._startX = null; - }); - } - - /** - * @hidden - */ - @HostListener('keyup', ['$event']) _keyup(ev: KeyboardEvent) { - if (ev.keyCode === KEY_SPACE || ev.keyCode === KEY_ENTER) { - console.debug(`toggle, keyup: ${ev.keyCode}`); - ev.preventDefault(); - ev.stopPropagation(); - this.value = !this.value; - } - } - - /** - * @hidden - */ - initFocus() { - this._elementRef.nativeElement.querySelector('button').focus(); - } - - /** - * @hidden - */ - ngOnDestroy() { - super.ngOnDestroy(); - this._gesture && this._gesture.destroy(); + }), + [ + h('div.toggle-icon', + h('div.toggle-inner') + ), + h('div.toggle-cover', { + attrs: { + 'id': this.id, + 'aria-checked': this.checked ? 'true' : false, + 'aria-disabled': this.disabled ? 'true' : false, + 'aria-labelledby': this.labelId, + 'role': 'checkbox', + 'tabindex': 0 + } + }) + ] + ) + ); } } diff --git a/src/components/toggle/toggle.wp.scss b/src/components/toggle/toggle.wp.scss index 8c39b66ec0..e2262add74 100644 --- a/src/components/toggle/toggle.wp.scss +++ b/src/components/toggle/toggle.wp.scss @@ -1,4 +1,6 @@ @import "../../themes/ionic.globals.wp"; +@import "./toggle"; + // Windows Toggle // -------------------------------------------------- diff --git a/src/index.ts b/src/index.ts index c9ad2a50a1..0d8cfd7402 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,12 +12,7 @@ export { AlertCmp } from './components/alert/alert-component'; export { App } from './components/app/app'; export { Avatar } from './components/avatar/avatar'; export { Backdrop } from './components/backdrop/backdrop'; -export { Badge } from './components/badge/badge'; export { Button } from './components/button/button'; -export { Card } from './components/card/card'; -export { CardContent } from './components/card/card-content'; -export { CardHeader } from './components/card/card-header'; -export { CardTitle } from './components/card/card-title'; export { Checkbox } from './components/checkbox/checkbox'; export { Chip } from './components/chip/chip'; export { Content, ScrollEvent } from './components/content/content'; @@ -102,7 +97,6 @@ export { Toast } from './components/toast/toast'; export { ToastCmp } from './components/toast/toast-component'; export { ToastController } from './components/toast/toast-controller'; export { ToastOptions } from './components/toast/toast-options'; -export { Toggle } from './components/toggle/toggle'; export { Footer } from './components/toolbar/toolbar-footer'; export { Header } from './components/toolbar/toolbar-header'; export { Toolbar } from './components/toolbar/toolbar'; @@ -115,6 +109,7 @@ export { VirtualFooter } from './components/virtual-scroll/virtual-footer'; export { VirtualHeader } from './components/virtual-scroll/virtual-header'; export { VirtualItem } from './components/virtual-scroll/virtual-item'; export { VirtualScroll } from './components/virtual-scroll/virtual-scroll'; +export { BooleanInput } from './bindings/angular/components/boolean-input'; /** @@ -130,6 +125,7 @@ export { NavController } from './navigation/nav-controller'; export { NavControllerBase } from './navigation/nav-controller-base'; export { NavParams } from './navigation/nav-params'; export { NavLink, NavOptions, DeepLinkConfig, DeepLinkMetadata, DeepLinkMetadataFactory } from './navigation/nav-util'; +export { setupCore } from './bindings/angular/providers/ionic-core'; export { TapClick, setupTapClick, isActivatable } from './tap-click/tap-click'; export { UrlSerializer, DeepLinkConfigToken } from './navigation/url-serializer'; export { ViewController } from './navigation/view-controller'; @@ -169,3 +165,22 @@ export { IonicGestureConfig } from './gestures/gesture-config'; export { IonicModule, IonicPageModule, provideLocationStrategy } from './module'; + + +/** + * Core + */ + +import * as interfaces from './util/interfaces'; + +export declare const Component: interfaces.ComponentDecorator; + +export declare const h: interfaces.Hyperscript; + +export declare const Ionic: interfaces.Ionic; + +export declare const Listen: interfaces.ListenDecorator; + +export declare const Prop: interfaces.PropDecorator; + +export declare const Watch: interfaces.WatchDecorator; diff --git a/src/module.ts b/src/module.ts index b210ade49c..5db8ffea27 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,7 +1,7 @@ /** * Import Angular */ -import { ANALYZE_FOR_ENTRY_COMPONENTS, APP_INITIALIZER, ComponentFactoryResolver, Inject, Injector, ModuleWithProviders, NgModule, NgZone, Optional } from '@angular/core'; +import { ANALYZE_FOR_ENTRY_COMPONENTS, APP_INITIALIZER, ComponentFactoryResolver, CUSTOM_ELEMENTS_SCHEMA, Inject, Injector, ModuleWithProviders, NgModule, NgZone, Optional } from '@angular/core'; import { APP_BASE_HREF, Location, LocationStrategy, HashLocationStrategy, PathLocationStrategy, PlatformLocation } from '@angular/common'; import { DOCUMENT, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -31,6 +31,7 @@ import { ModuleLoader, provideModuleLoader, setupPreloading, LAZY_LOADED_TOKEN } import { NgModuleLoader } from './util/ng-module-loader'; import { Platform, setupPlatform } from './platform/platform'; import { PlatformConfigToken, providePlatformConfigs } from './platform/platform-registry'; +import { setupCore } from './bindings/angular/providers/ionic-core'; import { TapClick, setupTapClick } from './tap-click/tap-click'; import { registerModeConfigs } from './config/mode-registry'; import { TransitionController } from './transitions/transition-controller'; @@ -49,12 +50,7 @@ import { IonicApp } from './components/app/app-root'; import { OverlayPortal } from './components/app/overlay-portal'; import { Avatar } from './components/avatar/avatar'; import { Backdrop } from './components/backdrop/backdrop'; -import { Badge } from './components/badge/badge'; import { Button } from './components/button/button'; -import { Card } from './components/card/card'; -import { CardContent } from './components/card/card-content'; -import { CardHeader } from './components/card/card-header'; -import { CardTitle } from './components/card/card-title'; import { Checkbox } from './components/checkbox/checkbox'; import { Chip } from './components/chip/chip'; import { Content } from './components/content/content'; @@ -128,7 +124,6 @@ import { Tabs } from './components/tabs/tabs'; import { Thumbnail } from './components/thumbnail/thumbnail'; import { ToastCmp } from './components/toast/toast-component'; import { ToastController } from './components/toast/toast-controller'; -import { Toggle } from './components/toggle/toggle'; import { Footer } from './components/toolbar/toolbar-footer'; import { Header } from './components/toolbar/toolbar-header'; import { Toolbar } from './components/toolbar/toolbar'; @@ -140,6 +135,7 @@ import { VirtualFooter } from './components/virtual-scroll/virtual-footer'; import { VirtualHeader } from './components/virtual-scroll/virtual-header'; import { VirtualItem } from './components/virtual-scroll/virtual-item'; import { VirtualScroll } from './components/virtual-scroll/virtual-scroll'; +import { BooleanInput } from './bindings/angular/components/boolean-input'; /** * @name IonicModule @@ -190,12 +186,7 @@ import { VirtualScroll } from './components/virtual-scroll/virtual-scroll'; OverlayPortal, Avatar, Backdrop, - Badge, Button, - Card, - CardContent, - CardHeader, - CardTitle, Checkbox, Chip, Col, @@ -263,7 +254,6 @@ import { VirtualScroll } from './components/virtual-scroll/virtual-scroll'; TextInput, Thumbnail, ToastCmp, - Toggle, Footer, Header, Toolbar, @@ -274,7 +264,8 @@ import { VirtualScroll } from './components/virtual-scroll/virtual-scroll'; VirtualFooter, VirtualHeader, VirtualItem, - VirtualScroll + VirtualScroll, + BooleanInput ], imports: [ CommonModule, @@ -293,12 +284,7 @@ import { VirtualScroll } from './components/virtual-scroll/virtual-scroll'; OverlayPortal, Avatar, Backdrop, - Badge, Button, - Card, - CardContent, - CardHeader, - CardTitle, Checkbox, Chip, Col, @@ -366,7 +352,6 @@ import { VirtualScroll } from './components/virtual-scroll/virtual-scroll'; TextInput, Thumbnail, ToastCmp, - Toggle, Footer, Header, Toolbar, @@ -377,7 +362,8 @@ import { VirtualScroll } from './components/virtual-scroll/virtual-scroll'; VirtualFooter, VirtualHeader, VirtualItem, - VirtualScroll + VirtualScroll, + BooleanInput ], entryComponents: [ ActionSheetCmp, @@ -389,7 +375,8 @@ import { VirtualScroll } from './components/virtual-scroll/virtual-scroll'; PopoverCmp, SelectPopover, ToastCmp - ] + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class IonicModule { @@ -418,6 +405,7 @@ export class IonicModule { // useFactory: ionic app initializers { provide: APP_INITIALIZER, useFactory: registerModeConfigs, deps: [ Config ], multi: true }, + { provide: APP_INITIALIZER, useFactory: setupCore, deps: [ Config, Platform, DomController, NgZone ], multi: true }, { provide: APP_INITIALIZER, useFactory: setupProvideEvents, deps: [ Platform, DomController ], multi: true }, { provide: APP_INITIALIZER, useFactory: setupTapClick, deps: [ Config, Platform, DomController, App, NgZone, GestureController ], multi: true }, { provide: APP_INITIALIZER, useFactory: setupPreloading, deps: [ Config, DeepLinkConfigToken, ModuleLoader, NgZone ], multi: true }, diff --git a/src/util/decorators.ts b/src/util/decorators.ts deleted file mode 100644 index aa2e233aef..0000000000 --- a/src/util/decorators.ts +++ /dev/null @@ -1,19 +0,0 @@ - -export const Component: ComponentDecorator = function(opts?: ComponentOptions): (target: any) => any { - return function() {}; -}; - - -export interface ComponentDecorator { - (opts?: ComponentOptions): any; -} - - -export interface ComponentOptions { - tag: string; - styleUrls?: string[] | ModeStyles; -} - -export interface ModeStyles { - [modeName: string]: string | string[]; -} diff --git a/src/util/dom.ts b/src/util/dom.ts index 37412e37ca..0d93d6f157 100644 --- a/src/util/dom.ts +++ b/src/util/dom.ts @@ -113,3 +113,89 @@ export interface PointerCoordinates { x?: number; y?: number; } + + + +export function pointerCoordX(ev: any): number { + // get X coordinates for either a mouse click + // or a touch depending on the given event + if (ev) { + var changedTouches = ev.changedTouches; + if (changedTouches && changedTouches.length > 0) { + return changedTouches[0].clientX; + } + if (ev.pageX !== undefined) { + return ev.pageX; + } + } + return 0; +} + + +export function pointerCoordY(ev: any): number { + // get Y coordinates for either a mouse click + // or a touch depending on the given event + if (ev) { + var changedTouches = ev.changedTouches; + if (changedTouches && changedTouches.length > 0) { + return changedTouches[0].clientY; + } + if (ev.pageY !== undefined) { + return ev.pageY; + } + } + return 0; +} + + +export function getElementReference(elm: any, ref: string) { + if (ref === 'child') { + return elm.firstElementChild; + } + if (ref === 'parent') { + if (elm.parentElement ) { + // normal element with a parent element + return elm.parentElement; + } + if (elm.parentNode && elm.parentNode.host) { + // shadow dom's document fragment + return elm.parentNode.host; + } + } + if (ref === 'body') { + return elm.ownerDocument.body; + } + if (ref === 'document') { + return elm.ownerDocument; + } + if (ref === 'window') { + return elm.ownerDocument.defaultView; + } + return elm; +} + + +export function getKeyCodeByName(keyName: string) { + if (keyName === 'enter') { + return 13; + } + if (keyName === 'escape') { + return 27; + } + if (keyName === 'space') { + return 32; + } + if (keyName === 'tab') { + return 9; + } + return null; +} + + +export function applyStyles(elm: HTMLElement, styles: {[styleProp: string]: string|number}) { + const styleProps = Object.keys(styles); + + for (var i = 0; i < styleProps.length; i++) { + (elm.style)[styleProps[i]] = styles[styleProps[i]]; + } +} diff --git a/src/util/interfaces.ts b/src/util/interfaces.ts new file mode 100644 index 0000000000..acd71a70d8 --- /dev/null +++ b/src/util/interfaces.ts @@ -0,0 +1,424 @@ + +export interface Ionic { + emit: EventEmit; + listener: { + enable: EventListenerEnable; + }; + theme: IonicTheme; + controllers: { + gesture?: any; + }; + dom: DomControllerApi; +} + + +export interface EventEmit { + (instance: any, eventName: string, data?: any): void; +} + + +export interface EventListenerEnable { + (instance: any, eventName: string, enabled: boolean, listenOn?: string): void; +} + + +export interface EventListenerCallback { + (ev?: any): void; +} + + +export interface GestureDetail { + type?: string; + event?: UIEvent; + startX?: number; + startY?: number; + startTimeStamp?: number; + currentX?: number; + currentY?: number; + velocityX?: number; + velocityY?: number; + deltaX?: number; + deltaY?: number; + directionX?: 'left'|'right'; + directionY?: 'up'|'down'; + velocityDirectionX?: 'left'|'right'; + velocityDirectionY?: 'up'|'down'; + timeStamp?: number; +} + + +export interface GestureCallback { + (detail?: GestureDetail): boolean|void; +} + + +export interface IonicGlobal { + staticDir?: string; + components?: LoadComponents; + loadComponents?: {(bundleId: string): void}; + config?: Object; + configCtrl?: ConfigApi; + domCtrl?: DomControllerApi; + nextTickCtrl?: NextTickApi; + eventNamePrefix?: string; +} + + +export interface NextTickApi { + nextTick: NextTick; +} + + +export interface NextTick { + (cb: Function): void; +} + + +export interface DomRead { + (cb: Function): void; +} + + +export interface DomWrite { + (cb: Function): void; +} + + +export interface DomControllerApi { + read: DomRead; + write: DomWrite; +} + + +export interface RafCallback { + (timeStamp?: number): void; +} + + +export interface RequestAnimationFrame { + (cb: RafCallback): void; +} + + +export interface LoadComponents { + [tag: string]: any[]; +} + + +export interface ComponentModeData { + /** + * tag name (ion-badge) + */ + [0]: string; + + /** + * component class name (Badge) + */ + [1]: string; + + /** + * listeners + */ + [2]: ComponentListenersData[]; + + /** + * watchers + */ + [3]: ComponentWatchersData[]; + + /** + * shadow + */ + [4]: boolean; + + /** + * mode name (ios, md, wp) + */ + [5]: string; + + /** + * component mode styles + */ + [6]: string; + + /** + * import component function + */ + [7]: ComponentModeImporterFn; +} + + +export interface ComponentListenersData { + /** + * methodName + */ + [0]: string; + + /** + * eventName + */ + [1]: string; + + /** + * capture + */ + [2]: number; + + /** + * passive + */ + [3]: number; + + /** + * enabled + */ + [4]: number; +} + + +export interface ComponentWatchersData { + [methodName: string]: any; +} + + +export interface ComponentModeImporterFn { + (importer: any, h: Hyperscript, Ionic: Ionic): void; +} + + +export interface ComponentDecorator { + (opts?: ComponentOptions): any; +} + + +export interface ComponentOptions { + tag: string; + styleUrls?: string[] | ModeStyles; + shadow?: boolean; +} + +export interface ModeStyles { + [modeName: string]: string | string[]; +} + + +export interface PropDecorator { + (opts?: PropOptions): any; +} + + +export interface PropOptions { + type?: number; +} + + +export interface Props { + [propName: string]: PropOptions; +} + + +export interface ListenDecorator { + (eventName: string, opts?: ListenOpts): any; +} + + +export interface ComponentMetaListeners { + [methodName: string]: ListenOpts; +} + + +export interface ListenOpts { + eventName?: string; + capture?: boolean; + passive?: boolean; + enabled?: boolean; +} + + +export interface WatchDecorator { + (propName: string): any; +} + + +export interface WatchOpts { + fn: string; +} + + +export interface Watchers { + [propName: string]: WatchOpts; +} + + +export interface IonicTheme { + (instance: any, cssClassName: string, vnodeData: VNodeData): VNodeData; +} + + +export interface ConfigApi { + get: (key: string, fallback?: any) => any; + getBoolean: (key: string, fallback?: boolean) => boolean; + getNumber: (key: string, fallback?: number) => number; +} + + +export interface ComponentMeta { + tag?: string; + props?: Props; + listeners?: ComponentMetaListeners; + watchers?: Watchers; + shadow?: boolean; + obsAttrs?: string[]; + hostCss?: string; + componentModule?: any; + modes: {[modeName: string]: ComponentMode}; +} + + +export interface ComponentMode { + bundleId?: string; + styles?: string; + styleUrls?: string[]; + styleElm?: HTMLElement; +} + + +export interface Component { + ionViewDidLoad?: {(): void}; + ionViewWillUnload?: {(): void}; + + render?: {(): VNode}; + + mode?: string; + color?: string; + + $el?: ProxyElement; + $meta?: ComponentMeta; + $listeners?: ComponentActiveListeners; + $root?: HTMLElement | ShadowRoot; + $vnode?: VNode; + + [memberName: string]: any; +} + + +export interface ComponentActiveListeners { + [eventName: string]: Function; +} + + +export interface BaseInputComponent extends Component { + disabled: boolean; + hasFocus: boolean; + value: string; + + fireFocus: {(): void}; + fireBlur: {(): void}; +} + + +export interface BooleanInputComponent extends BaseInputComponent { + checked: boolean; + toggle: {(ev: UIEvent): void}; +} + + +export interface ComponentModule { + new (): Component; +} + + +export interface ComponentRegistry { + [tag: string]: ComponentMeta; +} + + +export interface ProxyElement extends HTMLElement { + connectedCallback: {(): void}; + attributeChangedCallback: {(attrName: string, oldVal: string, newVal: string, namespace: string): void}; + disconnectedCallback: {(): void}; + + $queued?: boolean; + $instance?: Component; + + [memberName: string]: any; +} + + +export interface RendererApi { + (oldVnode: VNode | Element, vnode: VNode): VNode; +} + + +export type Key = string | number; + + +export interface Hyperscript { + (sel: any): VNode; + (sel: Node, data: VNodeData): VNode; + (sel: any, data: VNodeData): VNode; + (sel: any, text: string): VNode; + (sel: any, children: Array): VNode; + (sel: any, data: VNodeData, text: string): VNode; + (sel: any, data: VNodeData, children: Array): VNode; + (sel: any, data: VNodeData, children: any): VNode; +} + + +export interface VNode { + sel: string | undefined; + vdata: VNodeData | undefined; + vchildren: Array | undefined; + elm: Node | undefined; + vtext: string | undefined; + vkey: Key; +} + + +export interface VNodeData { + props?: any; + attrs?: any; + class?: any; + style?: any; + dataset?: any; + on?: any; + attachData?: any; + vkey?: Key; + vns?: string; // for SVGs + [key: string]: any; // for any other 3rd party module +} + + +export interface PlatformApi { + registerComponent: (tag: string, data: any[]) => ComponentMeta; + getComponentMeta: (tag: string) => ComponentMeta; + loadComponent: (bundleId: string, cb: Function) => void; + nextTick: NextTick; + domRead: DomRead; + domWrite: DomWrite; + + isElement: (node: Node) => node is Element; + isText: (node: Node) => node is Text; + isComment: (node: Node) => node is Comment; + + $createElement(tagName: K): HTMLElementTagNameMap[K]; + $createElementNS: (namespaceURI: string, qualifiedName: string) => Element; + $createTextNode: (text: string) => Text; + $createComment: (text: string) => Comment; + $insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void; + $removeChild: (parentNode: Node, childNode: Node) => void; + $appendChild: (parentNode: Node, childNode: Node) => void; + $parentNode: (node: Node) => Node; + $nextSibling: (node: Node) => Node; + $tagName: (elm: Element) => string; + $setTextContent: (node: Node, text: string | null) => void; + $getTextContent: (node: Node) => string | null; + $getAttribute: (elm: Element, attrName: string) => string; + $attachShadow: (elm: Element, cmpMode: ComponentMode, cmpModeId: string) => ShadowRoot; +} + + +export interface ServerInitConfig { + staticDir: string; + config?: Object; +}