diff --git a/ionic/components/action-menu/action-menu.ts b/ionic/components/action-menu/action-menu.ts index c675a5649a..257b27f05a 100644 --- a/ionic/components/action-menu/action-menu.ts +++ b/ionic/components/action-menu/action-menu.ts @@ -8,7 +8,6 @@ import {View, Injectable, NgFor, NgIf} from 'angular2/angular2'; -import {TapClick} from '../button/button'; import {Icon} from '../icon/icon'; import {Overlay} from '../overlay/overlay'; import {Animation} from '../../animations/animation'; @@ -75,7 +74,7 @@ import * as util from 'ionic/util'; '' + '' + '', - directives: [NgFor, NgIf, TapClick, Icon] + directives: [NgFor, NgIf, Icon] }) class ActionMenuDirective { diff --git a/ionic/components/app/app.ts b/ionic/components/app/app.ts index ee1bdb8153..61283f11d9 100644 --- a/ionic/components/app/app.ts +++ b/ionic/components/app/app.ts @@ -6,6 +6,7 @@ import {Platform} from '../../platform/platform'; import * as util from '../../util/util'; // injectables +import {Activator} from '../../util/activator'; import {ActionMenu} from '../action-menu/action-menu'; import {Modal} from '../modal/modal'; import {Popup} from '../popup/popup'; @@ -272,6 +273,7 @@ export function ionicBootstrap(rootComponentType, config) { Platform.prepareReady(config); // TODO: probs need a better way to inject global injectables + let activator = new Activator(app, config, window, document); let actionMenu = new ActionMenu(app, config); let modal = new Modal(app, config); let popup = new Popup(app, config); @@ -280,6 +282,7 @@ export function ionicBootstrap(rootComponentType, config) { let appBindings = Injector.resolve([ bind(IonicApp).toValue(app), bind(IonicConfig).toValue(config), + bind(Activator).toValue(activator), bind(ActionMenu).toValue(actionMenu), bind(Modal).toValue(modal), bind(Popup).toValue(popup), diff --git a/ionic/components/app/test/tap/index.ts b/ionic/components/app/test/tap/index.ts index 170ff78341..683bf1bc13 100644 --- a/ionic/components/app/test/tap/index.ts +++ b/ionic/components/app/test/tap/index.ts @@ -4,7 +4,13 @@ import {App} from 'ionic/ionic'; @App({ templateUrl: 'main.html' }) -class E2EApp {} +class E2EApp { + + tapTest(eleType) { + console.debug('test click', eleType); + } + +} function onEvent(ev) { @@ -74,6 +80,8 @@ console.debug = function() { if(arguments[0] === 'click') msg = '' + msg + ''; + if(arguments[0] === 'test click') msg = '' + msg + ''; + msgs.unshift( msg ); if(msgs.length > 25) { diff --git a/ionic/components/app/test/tap/main.html b/ionic/components/app/test/tap/main.html index af748eb855..7c90b37702 100644 --- a/ionic/components/app/test/tap/main.html +++ b/ionic/components/app/test/tap/main.html @@ -1,6 +1,41 @@ - + + + + + + + + + Link + + + + + + + + +
+ Div w/ (click) +
+
+ + +
+ Div w/out (click) +
+
+ +
+
+ + diff --git a/ionic/components/button/button.ts b/ionic/components/button/button.ts index a3d466990c..917f99d78d 100644 --- a/ionic/components/button/button.ts +++ b/ionic/components/button/button.ts @@ -1,9 +1,5 @@ -import {Directive, ElementRef, Optional, Host, onDestroy, NgZone, Query, QueryList} from 'angular2/angular2'; +import {Directive} from 'angular2/angular2'; -import {Icon} from '../icon/icon'; -import {IonicConfig} from '../../config/config'; -import {Activator} from '../../util/activator'; -import * as dom from '../../util/dom'; /** * TODO @@ -17,9 +13,7 @@ import * as dom from '../../util/dom'; } }) export class Button { - /** - * TODO - */ + constructor() { this.iconLeft = this.iconRight = this.iconOnly = false; } @@ -34,190 +28,3 @@ export class Button { this.iconOnly = icon.iconOnly; } } - -/** - * TODO - */ -@Directive({ - selector: '[tap-disabled]' -}) -export class TapDisabled {} - -/** - * TODO - */ -@Directive({ - selector: 'button,[button],[tappable],ion-checkbox,ion-radio', - host: { - '(^touchstart)': 'touchStart($event)', - '(^touchend)': 'touchEnd($event)', - '(^touchcancel)': 'pointerCancel()', - '(^mousedown)': 'mouseDown($event)', - '(^mouseup)': 'mouseUp($event)', - '(^click)': 'click($event)', - } -}) -export class TapClick { - /** - * TODO - * @param {ElementRef} elementRef TODO - * @param {IonicConfig} config TODO - * @param {NgZone} ngZone TODO - * @param {TapDisabled=} tapDisabled TODO - */ - constructor( - elementRef: ElementRef, - config: IonicConfig, - ngZone: NgZone, - @Optional() @Host() tapDisabled: TapDisabled - ) { - this.ele = elementRef.nativeElement; - this.tapEnabled = !tapDisabled; - this.tapPolyfill = config.setting('tapPolyfill'); - this.zone = ngZone; - - let self = this; - self.pointerMove = function(ev) { - let moveCoord = dom.pointerCoord(ev); - console.log('pointerMove', moveCoord, self.start) - - if ( dom.hasPointerMoved(10, self.start, moveCoord) ) { - self.pointerCancel(); - } - }; - } - - /** - * TODO - * @param {TODO} ev TODO - */ - touchStart(ev) { - this.pointerStart(ev); - } - - /** - * TODO - * @param {TODO} ev TODO - */ - touchEnd(ev) { - let self = this; - - if (this.tapPolyfill && this.tapEnabled) { - - let endCoord = dom.pointerCoord(ev); - - this.disableClick = true; - this.zone.runOutsideAngular(() => { - clearTimeout(self.disableTimer); - self.disableTimer = setTimeout(() => { - self.disableClick = false; - }, 600); - }); - - if ( this.start && !dom.hasPointerMoved(3, this.start, endCoord) ) { - let clickEvent = document.createEvent('MouseEvents'); - clickEvent.initMouseEvent('click', true, true, window, 1, 0, 0, endCoord.x, endCoord.y, false, false, false, false, 0, null); - clickEvent.isIonicTap = true; - this.ele.dispatchEvent(clickEvent); - } - - } - - this.pointerEnd(); - } - - /** - * TODO - * @param {TODO} ev TODO - */ - mouseDown(ev) { - if (this.disableClick) { - ev.preventDefault(); - ev.stopPropagation(); - - } else { - this.pointerStart(ev); - } - } - - /** - * TODO - * @param {TODO} ev TODO - */ - mouseUp(ev) { - if (this.disableClick) { - ev.preventDefault(); - ev.stopPropagation(); - } - - this.pointerEnd(); - } - - /** - * TODO - * @param {TODO} ev TODO - */ - pointerStart(ev) { - this.start = dom.pointerCoord(ev); - - this.zone.runOutsideAngular(() => { - Activator.start(ev.currentTarget); - Activator.moveListeners(this.pointerMove, true); - }); - } - - /** - * TODO - */ - pointerEnd() { - this.zone.runOutsideAngular(() => { - Activator.end(); - Activator.moveListeners(this.pointerMove, false); - }); - } - - /** - * TODO - */ - pointerCancel() { - this.start = null; - - this.zone.runOutsideAngular(() => { - Activator.clear(); - Activator.moveListeners(this.pointerMove, false); - }); - } - - /** - * Whether the supplied click event should be allowed or not. - * @param {MouseEvent} ev The click event. - * @return {boolean} True if click event should be allowed, otherwise false. - */ - allowClick(ev) { - if (!ev.isIonicTap) { - if (this.disableClick || !this.start) { - return false; - } - } - return true; - } - - /** - * TODO - * @param {MouseEvent} ev TODO - */ - click(ev) { - if (!this.allowClick(ev)) { - ev.preventDefault(); - ev.stopPropagation(); - } - } - - /** - * TODO - */ - onDestroy() { - this.ele = null; - } - -} diff --git a/ionic/components/checkbox/checkbox.ts b/ionic/components/checkbox/checkbox.ts index 45367b92e0..1893946d40 100644 --- a/ionic/components/checkbox/checkbox.ts +++ b/ionic/components/checkbox/checkbox.ts @@ -10,7 +10,6 @@ import {Ion} from '../ion'; import {IonInput} from '../form/input'; import {IonicConfig} from '../../config/config'; import {IonicComponent, IonicView} from '../../config/annotations'; -import {TapClick} from '../button/button'; /** * @name ionCheckbox @@ -37,6 +36,7 @@ import {TapClick} from '../button/button'; host: { 'class': 'item', 'role': 'checkbox', + 'tappable': 'true', '[attr.tab-index]': 'tabIndex', '[attr.aria-checked]': 'checked', '[attr.aria-disabled]': 'disabled', @@ -59,16 +59,13 @@ export class Checkbox extends Ion { * @param {ElementRef} elementRef TODO * @param {IonicConfig} ionicConfig TODO * @param {NgControl=} ngControl TODO - * @param {TapClick=} tapClick TODO */ constructor( elementRef: ElementRef, config: IonicConfig, - @Optional() ngControl: NgControl, - tapClick: TapClick + @Optional() ngControl: NgControl ) { super(elementRef, config); - this.tapClick = tapClick; this.tabIndex = 0; this.id = IonInput.nextId(); @@ -102,11 +99,9 @@ export class Checkbox extends Ion { * @param {MouseEvent} ev The click event. */ click(ev) { - if (this.tapClick.allowClick(ev)) { - ev.preventDefault(); - ev.stopPropagation(); - this.toggle(); - } + ev.preventDefault(); + ev.stopPropagation(); + this.toggle(); } /** diff --git a/ionic/components/radio/radio.ts b/ionic/components/radio/radio.ts index b3bd25fb61..70f5ecab50 100644 --- a/ionic/components/radio/radio.ts +++ b/ionic/components/radio/radio.ts @@ -3,7 +3,6 @@ import {ElementRef, Host, Optional, NgControl, Query, QueryList} from 'angular2/ import {IonicDirective, IonicComponent, IonicView} from '../../config/annotations'; import {IonicConfig} from '../../config/config'; import {Ion} from '../ion'; -import {TapClick} from '../button/button'; import {ListHeader} from '../list/list'; @@ -11,37 +10,37 @@ import {ListHeader} from '../list/list'; * @name ionRadioGroup * @classdesc * A radio group is a group of radio components. - * + * * Selecting a radio button in the group unselects all others in the group. - * - * New radios can be registered dynamically. + * + * New radios can be registered dynamically. * * See the [Angular 2 Docs](https://angular.io/docs/js/latest/api/forms/) for more info on forms and input. - * + * * @example * ```html * - * + * * * Clientside * - * + * * * Ember * - * + * * * Angular 1 * - * + * * * Angular 2 * - * + * * * React * - * + * * * ``` */ @@ -153,7 +152,7 @@ export class RadioGroup extends Ion { /** * @name ionRadio * @classdesc - * A single radio component. + * A single radio component. * * See the [Angular 2 Docs](https://angular.io/docs/js/latest/api/forms/) for more info on forms and input. * @@ -176,6 +175,7 @@ export class RadioGroup extends Ion { host: { 'class': 'item', 'role': 'radio', + 'tappable': 'true', '[attr.id]': 'id', '[attr.tab-index]': 'tabIndex', '[attr.aria-checked]': 'checked', @@ -199,16 +199,13 @@ export class RadioButton extends Ion { * @param {RadioGroup=} group The parent radio group, if any. * @param {ElementRef} elementRef TODO * @param {IonicConfig} config TODO - * @param {TapClick} tapClick TODO */ constructor( @Host() @Optional() group: RadioGroup, elementRef: ElementRef, - config: IonicConfig, - tapClick: TapClick + config: IonicConfig ) { - super(elementRef, config); - this.tapClick = tapClick; + super(elementRef, config) this.group = group; this.tabIndex = 0; } @@ -220,11 +217,9 @@ export class RadioButton extends Ion { } click(ev) { - if (this.tapClick.allowClick(ev)) { - ev.preventDefault(); - ev.stopPropagation(); - this.check(); - } + ev.preventDefault(); + ev.stopPropagation(); + this.check(); } /** diff --git a/ionic/components/switch/switch.ts b/ionic/components/switch/switch.ts index 687eb1df51..0d6efeb9f0 100644 --- a/ionic/components/switch/switch.ts +++ b/ionic/components/switch/switch.ts @@ -23,6 +23,7 @@ import {pointerCoord} from '../../util/dom'; @Directive({ selector: '.media-switch', host: { + 'tappable': 'true', '(^touchstart)': 'swtch.pointerDown($event)', '(^mousedown)': 'swtch.pointerDown($event)', '[class.activated]': 'swtch.isActivated' diff --git a/ionic/config/annotations.ts b/ionic/config/annotations.ts index 3ff092ee81..e2168bd114 100644 --- a/ionic/config/annotations.ts +++ b/ionic/config/annotations.ts @@ -15,7 +15,6 @@ import { Segment, SegmentButton, SegmentControlValueAccessor, RadioGroup, RadioButton, SearchBar, Nav, NavbarTemplate, Navbar, NavPush, NavPop, NavRouter, - TapClick, TapDisabled, IdRef, ShowWhen, HideWhen, @@ -87,10 +86,6 @@ export const IonicDirectives = [ forwardRef(() => ShowWhen), forwardRef(() => HideWhen), - // Gestures - forwardRef(() => TapClick), - forwardRef(() => TapDisabled), - // Material forwardRef(() => MaterialButton) ]; diff --git a/ionic/util/activator.ts b/ionic/util/activator.ts index 6025ca3a4b..2adb03c28c 100644 --- a/ionic/util/activator.ts +++ b/ionic/util/activator.ts @@ -1,61 +1,289 @@ -import {raf} from './dom'; - -var queueElements = {}; // elements that should get an active state in XX milliseconds -var activeElements = {}; // elements that are currently active -var keyId = 0; // a counter for unique keys for the above ojects -var ACTIVATED_CLASS = 'activated'; -var DEACTIVATE_TIMEOUT = 180; +import {raf, pointerCoord, hasPointerMoved} from './dom'; export class Activator { - static start(ele) { - queueElements[++keyId] = ele; - if (keyId > 9) keyId = 0; - raf(Activator.activate); + constructor(app: IonicApp, config: IonicConfig, window, document) { + const self = this; + self.app = app; + self.config = config; + self.win = window; + self.doc = document; + + self.id = 0; + self.queue = {}; + self.active = {}; + self.activatedClass = 'activated'; + self.deactivateTimeout = 180; + self.pointerTolerance = 4; + self.isTouch = false; + self.disableClick = 0; + self.disableClickLimit = 2500; + + self.tapPolyfill = config.setting('tapPolyfill'); + + function bindDom(type, listener, useCapture) { + document.addEventListener(type, listener, useCapture); + } + + bindDom('click', function(ev) { + self.click(ev); + }, true); + + bindDom('touchstart', function(ev) { + self.isTouch = true; + self.pointerStart(ev); + }); + + bindDom('touchend', function(ev) { + self.isTouch = true; + self.touchEnd(ev); + }); + + bindDom('touchcancel', function(ev) { + self.isTouch = true; + self.touchCancel(ev); + }); + + bindDom('mousedown', function(ev) { + self.mouseDown(ev); + }, true); + + bindDom('mouseup', function(ev) { + self.mouseUp(ev); + }, true); + + + self.pointerMove = function(ev) { + let moveCoord = pointerCoord(ev); + console.log('pointerMove', moveCoord, self.start) + + if ( hasPointerMoved(10, self.start, moveCoord) ) { + self.pointerCancel(); + } + }; + + + self.moveListeners = function(shouldAdd) { + document.removeEventListener('touchmove', self.pointerMove); + document.removeEventListener('mousemove', self.pointerMove); + + if (shouldAdd) { + bindDom('touchmove', self.pointerMove); + bindDom('mousemove', self.pointerMove); + } + }; + } - static activate() { - // activate all elements in the queue - for (var key in queueElements) { - if (queueElements[key]) { - queueElements[key].classList.add(ACTIVATED_CLASS); - activeElements[key] = queueElements[key]; + + /** + * TODO + * @param {TODO} ev TODO + */ + touchEnd(ev) { + let self = this; + + if (self.tapPolyfill && self.start) { + let endCoord = pointerCoord(ev); + + if (!hasPointerMoved(self.pointerTolerance, self.start, endCoord)) { + console.debug('create click'); + + self.disableClick = Date.now(); + + let clickEvent = self.doc.createEvent('MouseEvents'); + clickEvent.initMouseEvent('click', true, true, self.win, 1, 0, 0, endCoord.x, endCoord.y, false, false, false, false, 0, null); + clickEvent.isIonicTap = true; + ev.target.dispatchEvent(clickEvent); } } - queueElements = {}; + + self.pointerEnd(ev); } - static end() { - setTimeout(Activator.clear, DEACTIVATE_TIMEOUT); + /** + * TODO + * @param {TODO} ev TODO + */ + mouseDown(ev) { + if (this.isDisabledClick()) { + console.debug('mouseDown prevent'); + preventEvent(ev); + + } else if (!self.isTouch) { + this.pointerStart(ev); + } } - static clear() { + /** + * TODO + * @param {TODO} ev TODO + */ + mouseUp(ev) { + if (this.isDisabledClick()) { + console.debug('mouseUp prevent'); + preventEvent(ev); + } + + if (!self.isTouch) { + this.pointerEnd(ev); + } + } + + /** + * TODO + * @param {TODO} ev TODO + */ + pointerStart(ev) { + let targetEle = this.getActivatableTarget(ev.target); + + if (targetEle) { + this.start = pointerCoord(ev); + + this.queueActivate(targetEle); + this.moveListeners(true); + + } else { + this.start = null; + } + } + + /** + * TODO + */ + pointerEnd(ev) { + this.endActive(); + this.moveListeners(false); + } + + /** + * TODO + */ + pointerCancel() { + console.debug('pointerCancel') + this.clearActive(); + this.moveListeners(false); + this.disableClick = Date.now(); + } + + isDisabledClick() { + return this.disableClick + this.disableClickLimit > Date.now(); + } + + /** + * Whether the supplied click event should be allowed or not. + * @param {MouseEvent} ev The click event. + * @return {boolean} True if click event should be allowed, otherwise false. + */ + allowClick(ev) { + if (!ev.isIonicTap) { + if (this.isDisabledClick()) { + return false; + } + } + return true; + } + + /** + * TODO + * @param {MouseEvent} ev TODO + */ + click(ev) { + if (!this.allowClick(ev)) { + console.debug('click prevent'); + preventEvent(ev); + } + this.isTouch = false; + } + + getActivatableTarget(ele) { + var targetEle = ele; + for (var x = 0; x < 4; x++) { + if (!targetEle) break; + if (this.isActivatable(targetEle)) return targetEle; + targetEle = targetEle.parentElement; + } + return null; + } + + isActivatable(ele) { + if (/^(A|BUTTON)$/.test(ele.tagName)) { + return true; + } + + let attributes = ele.attributes; + for (let i = 0, l = attributes.length; i < l; i++) { + if (/click|tappable/.test(attributes[i].name)) { + return true; + } + } + + return false; + } + + queueActivate(ele) { + const self = this; + + self.queue[++self.id] = ele; + if (self.id > 19) self.id = 0; + + raf(function(){ + // activate all elements in the queue + for (var key in self.queue) { + if (self.queue[key]) { + self.queue[key].classList.add(self.activatedClass); + self.active[key] = self.queue[key]; + } + } + self.queue = {}; + }); + } + + endActive() { + const self = this; + + setTimeout(function() { + self.clearActive(); + }, this.deactivateTimeout); + } + + clearActive() { + const self = this; + // clear out any elements that are queued to be set to active - queueElements = {}; + self.queue = {}; // in the next frame, remove the active class from all active elements - raf(Activator.deactivate); - } - - static deactivate() { - - for (var key in activeElements) { - if (activeElements[key]) { - activeElements[key].classList.remove(ACTIVATED_CLASS); + raf(function() { + for (var key in self.active) { + if (self.active[key]) { + self.active[key].classList.remove(self.activatedClass); + } + delete self.active[key]; } - delete activeElements[key]; - } + }); } - static moveListeners(pointerMove, shouldAdd) { - document.removeEventListener('touchmove', pointerMove); - document.removeEventListener('mousemove', pointerMove); + clickBlock(enable) { + console.log('clickBlock', enable); - if (shouldAdd) { - document.addEventListener('touchmove', pointerMove); - document.addEventListener('mousemove', pointerMove); + this.doc.removeEventListener('click', preventEvent, true); + this.doc.removeEventListener('touchmove', preventEvent, true); + this.doc.removeEventListener('touchstart', preventEvent, true); + this.doc.removeEventListener('touchend', preventEvent, true); + + if (enable) { + this.doc.addEventListener('click', preventEvent, true); + this.doc.addEventListener('touchmove', preventEvent, true); + this.doc.addEventListener('touchstart', preventEvent, true); + this.doc.addEventListener('touchend', preventEvent, true); } } } + +function preventEvent(ev) { + ev.preventDefault(); + ev.stopPropagation(); +}