mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-23 05:58:26 +08:00
fix(activator): cancel remove .activated timeout
This commit is contained in:
36
src/components/tap-click/activator-base.ts
Normal file
36
src/components/tap-click/activator-base.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
170
src/components/tap-click/test/activator.spec.ts
Normal file
170
src/components/tap-click/test/activator.spec.ts
Normal 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);
|
||||
}
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user