Files
2015-09-14 21:43:55 -05:00

289 lines
6.2 KiB
TypeScript

import {raf, pointerCoord, hasPointerMoved} from '../../util/dom';
export class Activator {
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.deactivateAttempt = 0;
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.pointerCancel(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);
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);
}
};
}
/**
* TODO
* @param {TODO} ev TODO
*/
touchEnd(ev) {
let self = this;
if (self.tapPolyfill && self.start && !self.app.isTransitioning()) {
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');
ev.preventDefault();
ev.stopPropagation();
} else if (!self.isTouch) {
this.pointerStart(ev);
}
}
/**
* TODO
* @param {TODO} ev TODO
*/
mouseUp(ev) {
if (this.isDisabledClick()) {
console.debug('mouseUp prevent');
ev.preventDefault();
ev.stopPropagation();
}
if (!self.isTouch) {
this.pointerEnd(ev);
}
}
/**
* TODO
* @param {TODO} ev TODO
*/
pointerStart(ev) {
let targetEle = this.getActivatableTarget(ev.target);
if (targetEle && !this.app.isTransitioning()) {
this.start = pointerCoord(ev);
this.queueActivate(targetEle);
this.moveListeners(true);
} else {
this.start = null;
}
}
/**
* TODO
*/
pointerEnd(ev) {
this.queueDeactivate();
this.moveListeners(false);
}
/**
* TODO
*/
pointerCancel() {
console.debug('pointerCancel')
this.deactivate();
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 (this.app.isTransitioning()) {
return false;
}
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');
ev.preventDefault();
ev.stopPropagation();
}
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 = {};
});
}
queueDeactivate() {
const self = this;
setTimeout(function() {
self.deactivate();
}, self.deactivateTimeout);
}
deactivate() {
const self = this;
if (self.app.isTransitioning() && self.deactivateAttempt < 30) {
// the app is actively transitioning, don't bother deactivating
// anything this makes it easier on the GPU so it doesn't
// have to redraw any buttons during a transition
// retry
++self.deactivateAttempt;
self.queueDeactivate();
} else {
// not actively transitioning, good to deactivate any elements
// 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];
}
});
self.deactivateAttempt = 0;
}
}
}