Activator: activated

This commit is contained in:
Adam Bradley
2015-09-08 23:05:26 -05:00
parent 1b1d91661c
commit f0cb0a8de0
10 changed files with 342 additions and 276 deletions

View File

@ -8,7 +8,6 @@
import {View, Injectable, NgFor, NgIf} from 'angular2/angular2'; import {View, Injectable, NgFor, NgIf} from 'angular2/angular2';
import {TapClick} from '../button/button';
import {Icon} from '../icon/icon'; import {Icon} from '../icon/icon';
import {Overlay} from '../overlay/overlay'; import {Overlay} from '../overlay/overlay';
import {Animation} from '../../animations/animation'; import {Animation} from '../../animations/animation';
@ -75,7 +74,7 @@ import * as util from 'ionic/util';
'</div>' + '</div>' +
'</div>' + '</div>' +
'</action-menu-wrapper>', '</action-menu-wrapper>',
directives: [NgFor, NgIf, TapClick, Icon] directives: [NgFor, NgIf, Icon]
}) })
class ActionMenuDirective { class ActionMenuDirective {

View File

@ -6,6 +6,7 @@ import {Platform} from '../../platform/platform';
import * as util from '../../util/util'; import * as util from '../../util/util';
// injectables // injectables
import {Activator} from '../../util/activator';
import {ActionMenu} from '../action-menu/action-menu'; import {ActionMenu} from '../action-menu/action-menu';
import {Modal} from '../modal/modal'; import {Modal} from '../modal/modal';
import {Popup} from '../popup/popup'; import {Popup} from '../popup/popup';
@ -272,6 +273,7 @@ export function ionicBootstrap(rootComponentType, config) {
Platform.prepareReady(config); Platform.prepareReady(config);
// TODO: probs need a better way to inject global injectables // 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 actionMenu = new ActionMenu(app, config);
let modal = new Modal(app, config); let modal = new Modal(app, config);
let popup = new Popup(app, config); let popup = new Popup(app, config);
@ -280,6 +282,7 @@ export function ionicBootstrap(rootComponentType, config) {
let appBindings = Injector.resolve([ let appBindings = Injector.resolve([
bind(IonicApp).toValue(app), bind(IonicApp).toValue(app),
bind(IonicConfig).toValue(config), bind(IonicConfig).toValue(config),
bind(Activator).toValue(activator),
bind(ActionMenu).toValue(actionMenu), bind(ActionMenu).toValue(actionMenu),
bind(Modal).toValue(modal), bind(Modal).toValue(modal),
bind(Popup).toValue(popup), bind(Popup).toValue(popup),

View File

@ -4,7 +4,13 @@ import {App} from 'ionic/ionic';
@App({ @App({
templateUrl: 'main.html' templateUrl: 'main.html'
}) })
class E2EApp {} class E2EApp {
tapTest(eleType) {
console.debug('test click', eleType);
}
}
function onEvent(ev) { function onEvent(ev) {
@ -74,6 +80,8 @@ console.debug = function() {
if(arguments[0] === 'click') msg = '<span style="color:purple">' + msg + '</span>'; if(arguments[0] === 'click') msg = '<span style="color:purple">' + msg + '</span>';
if(arguments[0] === 'test click') msg = '<span style="color:orange">' + msg + '</span>';
msgs.unshift( msg ); msgs.unshift( msg );
if(msgs.length > 25) { if(msgs.length > 25) {

View File

@ -1,6 +1,41 @@
<button> <ion-row>
<div><div><p>TAP ME</p></div></div>
</button> <ion-col>
<button (^click)="tapTest('button')" block>
<div><div><p>Button</p></div></div>
</button>
</ion-col>
<ion-col>
<a button (^click)="tapTest('link')" block>
Link
</a>
</ion-col>
</ion-row>
<ion-row>
<ion-col>
<div button (^click)="tapTest('div')" block>
Div w/ (click)
</div>
</ion-col>
<ion-col>
<div button block>
Div w/out (click)
</div>
</ion-col>
</ion-row>
<div id="logs"></div> <div id="logs"></div>
<style>
.activated {
background-color: yellow !important;
}
</style>

View File

@ -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 * TODO
@ -17,9 +13,7 @@ import * as dom from '../../util/dom';
} }
}) })
export class Button { export class Button {
/**
* TODO
*/
constructor() { constructor() {
this.iconLeft = this.iconRight = this.iconOnly = false; this.iconLeft = this.iconRight = this.iconOnly = false;
} }
@ -34,190 +28,3 @@ export class Button {
this.iconOnly = icon.iconOnly; 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;
}
}

View File

@ -10,7 +10,6 @@ import {Ion} from '../ion';
import {IonInput} from '../form/input'; import {IonInput} from '../form/input';
import {IonicConfig} from '../../config/config'; import {IonicConfig} from '../../config/config';
import {IonicComponent, IonicView} from '../../config/annotations'; import {IonicComponent, IonicView} from '../../config/annotations';
import {TapClick} from '../button/button';
/** /**
* @name ionCheckbox * @name ionCheckbox
@ -37,6 +36,7 @@ import {TapClick} from '../button/button';
host: { host: {
'class': 'item', 'class': 'item',
'role': 'checkbox', 'role': 'checkbox',
'tappable': 'true',
'[attr.tab-index]': 'tabIndex', '[attr.tab-index]': 'tabIndex',
'[attr.aria-checked]': 'checked', '[attr.aria-checked]': 'checked',
'[attr.aria-disabled]': 'disabled', '[attr.aria-disabled]': 'disabled',
@ -59,16 +59,13 @@ export class Checkbox extends Ion {
* @param {ElementRef} elementRef TODO * @param {ElementRef} elementRef TODO
* @param {IonicConfig} ionicConfig TODO * @param {IonicConfig} ionicConfig TODO
* @param {NgControl=} ngControl TODO * @param {NgControl=} ngControl TODO
* @param {TapClick=} tapClick TODO
*/ */
constructor( constructor(
elementRef: ElementRef, elementRef: ElementRef,
config: IonicConfig, config: IonicConfig,
@Optional() ngControl: NgControl, @Optional() ngControl: NgControl
tapClick: TapClick
) { ) {
super(elementRef, config); super(elementRef, config);
this.tapClick = tapClick;
this.tabIndex = 0; this.tabIndex = 0;
this.id = IonInput.nextId(); this.id = IonInput.nextId();
@ -102,12 +99,10 @@ export class Checkbox extends Ion {
* @param {MouseEvent} ev The click event. * @param {MouseEvent} ev The click event.
*/ */
click(ev) { click(ev) {
if (this.tapClick.allowClick(ev)) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.toggle(); this.toggle();
} }
}
/** /**
* @private * @private

View File

@ -3,7 +3,6 @@ import {ElementRef, Host, Optional, NgControl, Query, QueryList} from 'angular2/
import {IonicDirective, IonicComponent, IonicView} from '../../config/annotations'; import {IonicDirective, IonicComponent, IonicView} from '../../config/annotations';
import {IonicConfig} from '../../config/config'; import {IonicConfig} from '../../config/config';
import {Ion} from '../ion'; import {Ion} from '../ion';
import {TapClick} from '../button/button';
import {ListHeader} from '../list/list'; import {ListHeader} from '../list/list';
@ -176,6 +175,7 @@ export class RadioGroup extends Ion {
host: { host: {
'class': 'item', 'class': 'item',
'role': 'radio', 'role': 'radio',
'tappable': 'true',
'[attr.id]': 'id', '[attr.id]': 'id',
'[attr.tab-index]': 'tabIndex', '[attr.tab-index]': 'tabIndex',
'[attr.aria-checked]': 'checked', '[attr.aria-checked]': 'checked',
@ -199,16 +199,13 @@ export class RadioButton extends Ion {
* @param {RadioGroup=} group The parent radio group, if any. * @param {RadioGroup=} group The parent radio group, if any.
* @param {ElementRef} elementRef TODO * @param {ElementRef} elementRef TODO
* @param {IonicConfig} config TODO * @param {IonicConfig} config TODO
* @param {TapClick} tapClick TODO
*/ */
constructor( constructor(
@Host() @Optional() group: RadioGroup, @Host() @Optional() group: RadioGroup,
elementRef: ElementRef, elementRef: ElementRef,
config: IonicConfig, config: IonicConfig
tapClick: TapClick
) { ) {
super(elementRef, config); super(elementRef, config)
this.tapClick = tapClick;
this.group = group; this.group = group;
this.tabIndex = 0; this.tabIndex = 0;
} }
@ -220,12 +217,10 @@ export class RadioButton extends Ion {
} }
click(ev) { click(ev) {
if (this.tapClick.allowClick(ev)) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.check(); this.check();
} }
}
/** /**
* Update the checked state of this radio button. * Update the checked state of this radio button.

View File

@ -23,6 +23,7 @@ import {pointerCoord} from '../../util/dom';
@Directive({ @Directive({
selector: '.media-switch', selector: '.media-switch',
host: { host: {
'tappable': 'true',
'(^touchstart)': 'swtch.pointerDown($event)', '(^touchstart)': 'swtch.pointerDown($event)',
'(^mousedown)': 'swtch.pointerDown($event)', '(^mousedown)': 'swtch.pointerDown($event)',
'[class.activated]': 'swtch.isActivated' '[class.activated]': 'swtch.isActivated'

View File

@ -15,7 +15,6 @@ import {
Segment, SegmentButton, SegmentControlValueAccessor, Segment, SegmentButton, SegmentControlValueAccessor,
RadioGroup, RadioButton, SearchBar, RadioGroup, RadioButton, SearchBar,
Nav, NavbarTemplate, Navbar, NavPush, NavPop, NavRouter, Nav, NavbarTemplate, Navbar, NavPush, NavPop, NavRouter,
TapClick, TapDisabled,
IdRef, IdRef,
ShowWhen, HideWhen, ShowWhen, HideWhen,
@ -87,10 +86,6 @@ export const IonicDirectives = [
forwardRef(() => ShowWhen), forwardRef(() => ShowWhen),
forwardRef(() => HideWhen), forwardRef(() => HideWhen),
// Gestures
forwardRef(() => TapClick),
forwardRef(() => TapDisabled),
// Material // Material
forwardRef(() => MaterialButton) forwardRef(() => MaterialButton)
]; ];

View File

@ -1,61 +1,289 @@
import {raf} from './dom'; import {raf, pointerCoord, hasPointerMoved} 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;
export class Activator { export class Activator {
static start(ele) { constructor(app: IonicApp, config: IonicConfig, window, document) {
queueElements[++keyId] = ele; const self = this;
if (keyId > 9) keyId = 0; self.app = app;
raf(Activator.activate); 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);
} }
static activate() { bindDom('click', function(ev) {
// activate all elements in the queue self.click(ev);
for (var key in queueElements) { }, true);
if (queueElements[key]) {
queueElements[key].classList.add(ACTIVATED_CLASS);
activeElements[key] = queueElements[key];
}
}
queueElements = {};
}
static end() { bindDom('touchstart', function(ev) {
setTimeout(Activator.clear, DEACTIVATE_TIMEOUT); self.isTouch = true;
} self.pointerStart(ev);
});
static clear() { bindDom('touchend', function(ev) {
// clear out any elements that are queued to be set to active self.isTouch = true;
queueElements = {}; self.touchEnd(ev);
});
// in the next frame, remove the active class from all active elements bindDom('touchcancel', function(ev) {
raf(Activator.deactivate); self.isTouch = true;
} self.touchCancel(ev);
});
static deactivate() { bindDom('mousedown', function(ev) {
self.mouseDown(ev);
}, true);
for (var key in activeElements) { bindDom('mouseup', function(ev) {
if (activeElements[key]) { self.mouseUp(ev);
activeElements[key].classList.remove(ACTIVATED_CLASS); }, true);
}
delete activeElements[key];
}
}
static moveListeners(pointerMove, shouldAdd) {
document.removeEventListener('touchmove', pointerMove); self.pointerMove = function(ev) {
document.removeEventListener('mousemove', pointerMove); 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) { if (shouldAdd) {
document.addEventListener('touchmove', pointerMove); bindDom('touchmove', self.pointerMove);
document.addEventListener('mousemove', pointerMove); bindDom('mousemove', self.pointerMove);
}
};
}
/**
* 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);
}
}
self.pointerEnd(ev);
}
/**
* TODO
* @param {TODO} ev TODO
*/
mouseDown(ev) {
if (this.isDisabledClick()) {
console.debug('mouseDown prevent');
preventEvent(ev);
} else if (!self.isTouch) {
this.pointerStart(ev);
}
}
/**
* 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
self.queue = {};
// in the next frame, remove the active class from all active elements
raf(function() {
for (var key in self.active) {
if (self.active[key]) {
self.active[key].classList.remove(self.activatedClass);
}
delete self.active[key];
}
});
}
clickBlock(enable) {
console.log('clickBlock', enable);
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();
}