fix(activator): cancel remove .activated timeout

This commit is contained in:
Manu Mtz.-Almeida
2016-11-18 21:25:04 +01:00
parent bb800339f8
commit 9510a2bb3b
6 changed files with 290 additions and 61 deletions

View File

@ -0,0 +1,36 @@
import { PointerCoordinates } from '../../util/dom';
export abstract class ActivatorBase {
abstract clickAction(ev, activatableEle: HTMLElement, startCoord: PointerCoordinates);
abstract downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates);
abstract upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates);
abstract clearState();
}
export function isActivatedDisabled(ev: any, activatableEle: any): boolean {
if (!activatableEle || !activatableEle.parentNode) {
return true;
}
if (!ev) {
return false;
}
if (ev.defaultPrevented) {
return true;
}
let targetEle = ev.target;
for (let i = 0; i < 4; i++) {
if (!targetEle) {
break;
}
if (targetEle.hasAttribute('disable-activated')) {
return true;
}
targetEle = targetEle.parentElement;
}
return false;
}

View File

@ -1,13 +1,17 @@
import { App } from '../app/app';
import { Config } from '../../config/config';
import { PointerCoordinates, nativeTimeout, rafFrames } from '../../util/dom';
import { ActivatorBase, isActivatedDisabled } from './activator-base';
export class Activator {
protected _css: string;
export class Activator implements ActivatorBase {
protected _queue: HTMLElement[] = [];
protected _active: HTMLElement[] = [];
protected _activeRafDefer: Function;
protected _clearRafDefer: Function;
_css: string;
activatedDelay = ADD_ACTIVATED_DEFERS;
clearDelay = CLEAR_STATE_DEFERS;
constructor(protected app: App, config: Config) {
this._css = config.get('activatedClass') || 'activated';
@ -15,7 +19,8 @@ export class Activator {
clickAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
// a click happened, so immediately deactive all activated elements
this._clearDeferred();
this._scheduleClear();
this._queue.length = 0;
for (var i = 0; i < this._active.length; i++) {
@ -32,21 +37,22 @@ export class Activator {
downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
// the user just pressed down
if (this.disableActivated(ev)) {
if (isActivatedDisabled(ev, activatableEle)) {
return;
}
this.unscheduleClear();
this.deactivate();
// queue to have this element activated
this._queue.push(activatableEle);
this._activeRafDefer = rafFrames(6, () => {
this._activeRafDefer = rafFrames(this.activatedDelay, () => {
let activatableEle: HTMLElement;
for (let i = 0; i < this._queue.length; i++) {
activatableEle = this._queue[i];
if (activatableEle && activatableEle.parentNode) {
this._active.push(activatableEle);
activatableEle.classList.add(this._css);
}
this._active.push(activatableEle);
activatableEle.classList.add(this._css);
}
this._queue.length = 0;
this._clearDeferred();
@ -55,16 +61,28 @@ export class Activator {
// the user was pressing down, then just let up
upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
this._clearDeferred();
this._scheduleClear();
}
rafFrames(CLEAR_STATE_DEFERS, () => {
_scheduleClear() {
if (this._clearRafDefer) {
return;
}
this._clearRafDefer = rafFrames(this.clearDelay, () => {
this.clearState();
this._clearRafDefer = null;
});
}
unscheduleClear() {
if (this._clearRafDefer) {
this._clearRafDefer();
this._clearRafDefer = null;
}
}
// all states should return to normal
clearState() {
if (!this.app.isEnabled()) {
// the app is actively disabled, so don't bother deactivating anything.
// this makes it easier on the GPU so it doesn't have to redraw any
@ -85,12 +103,10 @@ export class Activator {
this._queue.length = 0;
rafFrames(2, () => {
for (var i = 0; i < this._active.length; i++) {
this._active[i].classList.remove(this._css);
}
this._active.length = 0;
});
for (var i = 0; i < this._active.length; i++) {
this._active[i].classList.remove(this._css);
}
this._active.length = 0;
}
_clearDeferred() {
@ -100,25 +116,7 @@ export class Activator {
this._activeRafDefer = null;
}
}
disableActivated(ev: any) {
if (ev.defaultPrevented) {
return true;
}
let targetEle = ev.target;
for (let i = 0; i < 4; i++) {
if (!targetEle) {
break;
}
if (targetEle.hasAttribute('disable-activated')) {
return true;
}
targetEle = targetEle.parentElement;
}
return false;
}
}
const CLEAR_STATE_DEFERS = 5;
const ADD_ACTIVATED_DEFERS = 6;
const CLEAR_STATE_DEFERS = 6;

View File

@ -1,3 +1,4 @@
import { ActivatorBase, isActivatedDisabled } from './activator-base';
import { Activator } from './activator';
import { App } from '../app/app';
import { PointerCoordinates, CSS, hasPointerMoved, pointerCoord, rafFrames } from '../../util/dom';
@ -7,19 +8,47 @@ import { Config } from '../../config/config';
/**
* @private
*/
export class RippleActivator extends Activator {
export class RippleActivator implements ActivatorBase {
protected _queue: HTMLElement[] = [];
protected _active: HTMLElement[] = [];
protected highlight: Activator;
constructor(app: App, config: Config) {
super(app, config);
this.highlight = new Activator(app, config);
this.highlight.activatedDelay = 0;
}
clickAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
this.downAction(ev, activatableEle, startCoord);
this.upAction(ev, activatableEle, startCoord);
// Highlight
this.highlight && this.highlight.clickAction(ev, activatableEle, startCoord);
// Ripple
this._clickAction(ev, activatableEle, startCoord);
}
downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
if (this.disableActivated(ev) || !activatableEle || !activatableEle.parentNode) {
// Highlight
this.highlight && this.highlight.downAction(ev, activatableEle, startCoord);
// Ripple
this._downAction(ev, activatableEle, startCoord);
}
upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
// Highlight
this.highlight && this.highlight.upAction(ev, activatableEle, startCoord);
// Ripple
this._upAction(ev, activatableEle, startCoord);
}
clearState() {
// Highlight
this.highlight && this.highlight.clearState();
}
_downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
if (isActivatedDisabled(ev, activatableEle)) {
return;
}
@ -38,12 +67,9 @@ export class RippleActivator extends Activator {
break;
}
}
// DOM WRITE
activatableEle.classList.add(this._css);
}
upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
_upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
if (!hasPointerMoved(6, startCoord, pointerCoord(ev))) {
let i = activatableEle.childElementCount;
while (i--) {
@ -55,8 +81,10 @@ export class RippleActivator extends Activator {
}
}
}
}
super.upAction(ev, activatableEle, startCoord);
_clickAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
// NOTHING
}
startRippleEffect(rippleEle: any, activatableEle: HTMLElement, startCoord: PointerCoordinates) {
@ -107,16 +135,6 @@ export class RippleActivator extends Activator {
});
}
deactivate() {
rafFrames(2, () => {
for (var i = 0; i < this._active.length; i++) {
// DOM WRITE
this._active[i].classList.remove(this._css);
}
this._active.length = 0;
});
}
}
const TOUCH_DOWN_ACCEL = 300;

View File

@ -1,5 +1,6 @@
import { Injectable, NgZone } from '@angular/core';
import { ActivatorBase } from './activator-base';
import { Activator } from './activator';
import { App } from '../app/app';
import { Config } from '../../config/config';
@ -15,7 +16,7 @@ import { UIEventManager, PointerEvents, PointerEventType } from '../../util/ui-e
export class TapClick {
private disableClick: number = 0;
private usePolyfill: boolean;
private activator: Activator;
private activator: ActivatorBase;
private startCoord: any;
private events: UIEventManager = new UIEventManager(false);
private pointerEvents: PointerEvents;

View File

@ -0,0 +1,170 @@
import { Activator } from '../activator';
import { Config } from '../../../config/config';
describe('Activator', () => {
it('should config css', () => {
let activator = mockActivator(true, null);
expect(activator._css).toEqual('activated');
activator = mockActivator(true, 'enabled');
expect(activator._css).toEqual('enabled');
});
it('should sync add/remove css class', () => {
const {ev, ele, pos} = testValues();
let activator = mockActivator(true, 'activo');
activator.activatedDelay = 0;
activator.clearDelay = 0;
activator.downAction(ev, ele, pos);
expect(ele.classList.contains('activo')).toBeTruthy();
activator.upAction(ev, ele, pos);
expect(ele.classList.contains('activo')).toBeFalsy();
activator.downAction(ev, ele, pos);
expect(ele.classList.contains('activo')).toBeTruthy();
activator.upAction(null, null, pos);
expect(ele.classList.contains('activo')).toBeFalsy();
activator.downAction(ev, ele, pos);
expect(ele.classList.contains('activo')).toBeTruthy();
activator.clickAction(null, null, pos);
expect(ele.classList.contains('activo')).toBeFalsy();
});
it('should async down/up/click action (normal flow)', (done) => {
const {ev, ele, pos} = testValues();
let activator = mockActivator(true, null);
activator.activatedDelay = 6;
activator.clearDelay = 6;
activator.downAction(ev, ele, pos);
expect(ele.classList.contains('activated')).toBeFalsy();
// upAction
setTimeout(() => {
expect(ele.classList.contains('activated')).toBeTruthy();
activator.upAction(ev, ele, pos);
expect(ele.classList.contains('activated')).toBeTruthy();
}, (6 + 2) * 16);
// clickAction
setTimeout(() => {
expect(ele.classList.contains('activated')).toBeTruthy();
activator.clickAction(ev, ele, pos);
expect(ele.classList.contains('activated')).toBeTruthy();
}, (6 + 2 + 2) * 16);
// Read final results
setTimeout(() => {
expect(ele.classList.contains('activated')).toBeFalsy();
done();
}, (6 + 6 + 4) * 16);
}, 10000);
it('should async down then down', (done) => {
const {ev, ele, pos} = testValues();
let activator = mockActivator(true, null);
activator.activatedDelay = 6;
activator.clearDelay = 6;
activator.downAction(ev, ele, pos);
setTimeout(() => {
expect(ele.classList.contains('activated')).toBeTruthy();
activator.downAction(ev, ele, pos);
expect(ele.classList.contains('activated')).toBeFalsy();
}, (6 + 2) * 16);
setTimeout(() => {
expect(ele.classList.contains('activated')).toBeTruthy();
done();
}, (6 + 6 + 4) * 16);
}, 10000);
it('should async down then click', (done) => {
const {ev, ele, pos} = testValues();
let activator = mockActivator(true, null);
activator.activatedDelay = 6;
activator.clearDelay = 6;
activator.downAction(ev, ele, pos);
setTimeout(() => {
expect(ele.classList.contains('activated')).toBeFalsy();
activator.clickAction(ev, ele, pos);
expect(ele.classList.contains('activated')).toBeTruthy();
}, 16);
setTimeout(() => {
expect(ele.classList.contains('activated')).toBeFalsy();
}, (6 + 3) * 16);
// Check the value is stable
setTimeout(() => {
expect(ele.classList.contains('activated')).toBeFalsy();
done();
}, 20 * 16);
}, 10000);
it('should async down then click then down (fast clicking)', (done) => {
const {ev, ele, pos} = testValues();
let activator = mockActivator(true, null);
activator.activatedDelay = 6;
activator.clearDelay = 6;
activator.downAction(ev, ele, pos);
setTimeout(() => {
expect(ele.classList.contains('activated')).toBeFalsy();
activator.clickAction(ev, ele, pos);
expect(ele.classList.contains('activated')).toBeTruthy();
}, 16);
setTimeout(() => {
expect(ele.classList.contains('activated')).toBeTruthy();
activator.downAction(ev, ele, pos);
expect(ele.classList.contains('activated')).toBeFalsy();
}, 16 * 2);
setTimeout(() => {
expect(ele.classList.contains('activated')).toBeTruthy();
done();
}, 16 * 12);
}, 10000);
});
function testValues() {
let parent = document.createElement('div');
let ele = document.createElement('a');
parent.appendChild(ele);
return {
ev: null,
ele: ele,
pos: { x: 0, y: 0 },
};
}
function mockActivator(appEnabled: boolean, css: string) {
let app = {
isEnabled: () => { return appEnabled; },
};
let config = new Config();
if (css) {
config.set('activatedClass', css);
}
return new Activator(<any>app, config);
}

View File

@ -48,7 +48,10 @@ export function rafFrames(framesToWait: number, callback: Function) {
let rafId: any;
let timeoutId: any;
if (framesToWait < 2) {
if (framesToWait === 0) {
callback();
}else if (framesToWait < 2) {
rafId = nativeRaf(callback);
} else {
@ -67,7 +70,10 @@ export function rafFrames(framesToWait: number, callback: Function) {
export function zoneRafFrames(framesToWait: number, callback: Function) {
framesToWait = Math.ceil(framesToWait);
if (framesToWait < 2) {
if (framesToWait === 0) {
callback();
} else if (framesToWait < 2) {
raf(callback);
} else {