feat(toggle): boolean input directive

This commit is contained in:
Adam Bradley
2017-04-23 21:54:34 -05:00
parent 9a533f430f
commit ccfce9d034
35 changed files with 1529 additions and 464 deletions

View File

@@ -15,7 +15,7 @@ ion-badge,
:host {
display: inline-block;
visibility: inherit !important;
contain: layout content;
contain: content;
}
.badge {

View File

@@ -1,4 +1,4 @@
import { Component } from '../../util/decorators';
import { Component } from '../../index';
@Component({

View File

@@ -0,0 +1,3 @@
@import "../../themes/ionic.globals";
@import "./card-content";

View File

@@ -0,0 +1,3 @@
@import "../../themes/ionic.globals";
@import "./card-content";

View File

@@ -0,0 +1,9 @@
@import "../../themes/ionic.globals";
// Card
// --------------------------------------------------
ion-card-content,
:host {
display: block;
}

View File

@@ -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 {}

View File

@@ -0,0 +1,3 @@
@import "../../themes/ionic.globals";
@import "./card-content";

View File

@@ -0,0 +1,2 @@
@import "../../themes/ionic.globals";
@import "./card-header";

View File

@@ -0,0 +1,2 @@
@import "../../themes/ionic.globals";
@import "./card-header";

View File

@@ -0,0 +1,14 @@
@import "../../themes/ionic.globals";
// Card Header
// --------------------------------------------------
ion-card-header,
:host {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -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 {}

View File

@@ -0,0 +1,2 @@
@import "../../themes/ionic.globals";
@import "./card-header";

View File

@@ -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 {}

View File

@@ -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;
}
}

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -0,0 +1,173 @@
export class GestureController {
private id: number = 0;
private requestedStart: { [eventId: number]: number } = {};
private disabledGestures: { [eventName: string]: Set<number> } = {};
private disabledScroll: Set<number> = new Set<number>();
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<number>();
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;
}
}

View File

@@ -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 = (<GestureController>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();
}

View File

@@ -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;
}
}

View File

@@ -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: []
};
}
}

View File

@@ -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;
}

View File

@@ -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,
});
});
});

View File

@@ -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);
}
}

View File

@@ -1,4 +1,6 @@
@import "../../themes/ionic.globals.ios";
@import "./toggle";
// iOS Toggle
// --------------------------------------------------

View File

@@ -1,4 +1,6 @@
@import "../../themes/ionic.globals.md";
@import "./toggle";
// Material Design Toggle
// --------------------------------------------------

View File

@@ -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;
}

View File

@@ -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
*
* <ion-list>
*
* <ion-item>
* <ion-label>Pepperoni</ion-label>
* <ion-toggle [(ngModel)]="pepperoni"></ion-toggle>
* </ion-item>
*
* <ion-item>
* <ion-label>Sausage</ion-label>
* <ion-toggle [(ngModel)]="sausage" disabled="true"></ion-toggle>
* </ion-item>
*
* <ion-item>
* <ion-label>Mushrooms</ion-label>
* <ion-toggle [(ngModel)]="mushrooms"></ion-toggle>
* </ion-item>
*
* </ion-list>
* ```
*
* @demo /docs/demos/src/toggle/
* @see {@link /docs/components#toggle Toggle Component Docs}
*/
@Component({
selector: 'ion-toggle',
template:
'<div class="toggle-icon">' +
'<div class="toggle-inner"></div>' +
'</div>' +
'<button role="checkbox" ' +
'type="button" ' +
'ion-button="item-cover" ' +
'[id]="id" ' +
'[attr.aria-checked]="_value" ' +
'[attr.aria-labelledby]="_labelId" ' +
'[attr.aria-disabled]="_disabled" ' +
'class="item-cover" disable-activated>' +
'</button>',
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<boolean> 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
}
})
]
)
);
}
}

View File

@@ -1,4 +1,6 @@
@import "../../themes/ionic.globals.wp";
@import "./toggle";
// Windows Toggle
// --------------------------------------------------