Skip to content

Commit

Permalink
Merge pull request #8807 from manucorporat/improve-tapclick
Browse files Browse the repository at this point in the history
fix(tapclick): several improvements
  • Loading branch information
manucorporat authored Oct 20, 2016
2 parents 272acfc + 35d12ef commit 780717e
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 147 deletions.
7 changes: 5 additions & 2 deletions src/components/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class App {
private _title: string = '';
private _titleSrv: Title = new Title();
private _rootNav: NavController = null;
private _canDisableScroll: boolean;

/**
* @private
Expand Down Expand Up @@ -70,6 +71,7 @@ export class App {
// listen for hardware back button events
// register this back button action with a default priority
_platform.registerBackButtonAction(this.navPop.bind(this));
this._canDisableScroll = this._config.get('canDisableScroll', true);
}

/**
Expand Down Expand Up @@ -122,7 +124,7 @@ export class App {
* scrolling is enabled. When set to `true`, scrolling is disabled.
*/
setScrollDisabled(disableScroll: boolean) {
if (this._config.get('canDisableScroll', true)) {
if (this._canDisableScroll) {
this._appRoot._disableScroll(disableScroll);
}
}
Expand All @@ -148,7 +150,7 @@ export class App {
* @return {boolean} returns true or false
*/
isScrolling(): boolean {
return (this._scrollTime + 48 > Date.now());
return ((this._scrollTime + ACTIVE_SCROLLING_TIME) > Date.now());
}

/**
Expand Down Expand Up @@ -275,4 +277,5 @@ export class App {

}

const ACTIVE_SCROLLING_TIME = 100;
const CLICK_BLOCK_BUFFER_IN_MILLIS = 64;
3 changes: 1 addition & 2 deletions src/components/content/content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ ion-content.js-scroll > .scroll-content {
}

.disable-scroll .ion-page .scroll-content {
overflow-y: hidden;
overflow-x: hidden;
pointer-events: none;
}


Expand Down
5 changes: 4 additions & 1 deletion src/components/content/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export class Content extends Ion {

this._zone.runOutsideAngular(() => {
this._scroll = new ScrollView(this._scrollEle);
this._scLsn = this.addScrollListener(this._app.setScrolling);
this._scLsn = this.addScrollListener(this._app.setScrolling.bind(this._app));
});
}

Expand Down Expand Up @@ -252,6 +252,9 @@ export class Content extends Ion {
return this._addListener('mousemove', handler);
}

/**
* @private
*/
_addListener(type: string, handler: any): Function {
assert(handler, 'handler must be valid');
assert(this._scrollEle, '_scrollEle must be valid');
Expand Down
6 changes: 3 additions & 3 deletions src/components/tap-click/activator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class Activator {
// queue to have this element activated
this._queue.push(activatableEle);

rafFrames(2, () => {
rafFrames(6, () => {
let activatableEle: HTMLElement;
for (let i = 0; i < this._queue.length; i++) {
activatableEle = this._queue[i];
Expand All @@ -30,7 +30,7 @@ export class Activator {
activatableEle.classList.add(this._css);
}
}
this._queue = [];
this._queue.length = 0;
});
}

Expand Down Expand Up @@ -59,7 +59,7 @@ export class Activator {

deactivate() {
// remove the active class from all active elements
this._queue = [];
this._queue.length = 0;

rafFrames(2, () => {
for (var i = 0; i < this._active.length; i++) {
Expand Down
195 changes: 73 additions & 122 deletions src/components/tap-click/tap-click.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,156 +5,89 @@ import { App } from '../app/app';
import { Config } from '../../config/config';
import { hasPointerMoved, pointerCoord } from '../../util/dom';
import { RippleActivator } from './ripple';

import { UIEventManager, PointerEvents, PointerEventType } from '../../util/ui-event-manager';

/**
* @private
*/
@Injectable()
export class TapClick {
private lastTouch: number = 0;
private disableClick: number = 0;
private lastActivated: number = 0;
private usePolyfill: boolean;
private activator: Activator;
private startCoord: any;
private pointerMove: any;
private events: UIEventManager = new UIEventManager(false);
private pointerEvents: PointerEvents;

constructor(
config: Config,
private app: App,
zone: NgZone
) {
if (config.get('activator') === 'ripple') {
let activator = config.get('activator');
if (activator === 'ripple') {
this.activator = new RippleActivator(app, config);

} else if (config.get('activator') === 'highlight') {
} else if (activator === 'highlight') {
this.activator = new Activator(app, config);
}

this.usePolyfill = (config.get('tapPolyfill') === true);

zone.runOutsideAngular(() => {
addListener('click', this.click.bind(this), true);

addListener('touchstart', this.touchStart.bind(this));
addListener('touchend', this.touchEnd.bind(this));
addListener('touchcancel', this.pointerCancel.bind(this));

addListener('mousedown', this.mouseDown.bind(this), true);
addListener('mouseup', this.mouseUp.bind(this), true);
this.events.listen(document, 'click', this.click.bind(this), true);
this.pointerEvents = this.events.pointerEvents({
element: <any>document,
pointerDown: this.pointerStart.bind(this),
pointerMove: this.pointerMove.bind(this),
pointerUp: this.pointerEnd.bind(this),
passive: true
});

this.pointerMove = (ev: UIEvent) => {
if (!this.startCoord || hasPointerMoved(POINTER_MOVE_UNTIL_CANCEL, this.startCoord, pointerCoord(ev)) ) {
this.pointerCancel(ev);
}
};
}

touchStart(ev: UIEvent) {
this.lastTouch = Date.now();
this.pointerStart(ev);
this.pointerEvents.mouseWait = DISABLE_NATIVE_CLICK_AMOUNT;
}

touchEnd(ev: UIEvent) {
this.lastTouch = Date.now();

if (this.usePolyfill && this.startCoord && this.app.isEnabled()) {
// only dispatch mouse click events from a touchend event
// when tapPolyfill config is true, and the startCoordand endCoord
// are not too far off from each other
let endCoord = pointerCoord(ev);

if (!hasPointerMoved(POINTER_TOLERANCE, this.startCoord, endCoord)) {
// prevent native mouse click events for XX amount of time
this.disableClick = this.lastTouch + DISABLE_NATIVE_CLICK_AMOUNT;

if (this.app.isScrolling()) {
// do not fire off a click event while the app was scrolling
console.debug('click from touch prevented by scrolling ' + Date.now());

} else {
// dispatch a mouse click event
console.debug('create click from touch ' + Date.now());

let clickEvent: any = 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;
ev.target.dispatchEvent(clickEvent);
}
}
pointerStart(ev: any): boolean {
if (this.startCoord) {
return false;
}

this.pointerEnd(ev);
}

mouseDown(ev: any) {
if (this.isDisabledNativeClick()) {
console.debug('mouseDown prevent ' + ev.target.tagName + ' ' + Date.now());
// does not prevent default on purpose
// so native blur events from inputs can happen
ev.stopPropagation();

} else if (this.lastTouch + DISABLE_NATIVE_CLICK_AMOUNT < Date.now()) {
this.pointerStart(ev);
let activatableEle = getActivatableTarget(ev.target);
if (!activatableEle) {
this.startCoord = null;
return false;
}
this.startCoord = pointerCoord(ev);
this.activator && this.activator.downAction(ev, activatableEle, this.startCoord);
return true;
}

mouseUp(ev: any) {
if (this.isDisabledNativeClick()) {
console.debug('mouseUp prevent ' + ev.target.tagName + ' ' + Date.now());
ev.preventDefault();
ev.stopPropagation();
}

if (this.lastTouch + DISABLE_NATIVE_CLICK_AMOUNT < Date.now()) {
this.pointerEnd(ev);
pointerMove(ev: UIEvent) {
if (!this.startCoord ||
hasPointerMoved(POINTER_TOLERANCE, this.startCoord, pointerCoord(ev)) ||
this.app.isScrolling()) {
this.pointerCancel(ev);
}
}

pointerStart(ev: any) {
let activatableEle = getActivatableTarget(ev.target);

if (activatableEle) {
this.startCoord = pointerCoord(ev);

let now = Date.now();
if (this.lastActivated + 150 < now && !this.app.isScrolling()) {
this.activator && this.activator.downAction(ev, activatableEle, this.startCoord);
this.lastActivated = now;
}

this.moveListeners(true);

} else {
this.startCoord = null;
pointerEnd(ev: any, type: PointerEventType) {
if (!this.startCoord) {
return;
}
}

pointerEnd(ev: any) {
if (this.startCoord && this.activator) {
if (type === PointerEventType.TOUCH && this.usePolyfill && this.app.isEnabled()) {
this.handleTapPolyfill(ev);
}
if (this.activator) {
let activatableEle = getActivatableTarget(ev.target);
if (activatableEle) {
this.activator.upAction(ev, activatableEle, this.startCoord);
}
}

this.moveListeners(false);
this.startCoord = null;
}

pointerCancel(ev: UIEvent) {
console.debug('pointerCancel from ' + ev.type + ' ' + Date.now());
this.startCoord = null;
this.activator && this.activator.clearState();
this.moveListeners(false);
}

moveListeners(shouldAdd: boolean) {
removeListener(this.usePolyfill ? 'touchmove' : 'mousemove', this.pointerMove);

if (shouldAdd) {
addListener(this.usePolyfill ? 'touchmove' : 'mousemove', this.pointerMove);
}
this.pointerEvents.stop();
}

click(ev: any) {
Expand All @@ -174,6 +107,34 @@ export class TapClick {
}
}

handleTapPolyfill(ev: any) {
// only dispatch mouse click events from a touchend event
// when tapPolyfill config is true, and the startCoordand endCoord
// are not too far off from each other
let endCoord = pointerCoord(ev);

if (hasPointerMoved(POINTER_TOLERANCE, this.startCoord, endCoord)) {
console.debug('click from touch prevented by pointer moved');
return;
}
// prevent native mouse click events for XX amount of time
this.disableClick = Date.now() + DISABLE_NATIVE_CLICK_AMOUNT;

if (this.app.isScrolling()) {
// do not fire off a click event while the app was scrolling
console.debug('click from touch prevented by scrolling ' + Date.now());

} else {
// dispatch a mouse click event
console.debug('create click from touch ' + Date.now());

let clickEvent: any = 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;
ev.target.dispatchEvent(clickEvent);
}
}

isDisabledNativeClick() {
return this.disableClick > Date.now();
}
Expand All @@ -194,33 +155,23 @@ function getActivatableTarget(ele: HTMLElement) {
/**
* @private
*/
export const isActivatable = function(ele: HTMLElement) {
if (ACTIVATABLE_ELEMENTS.test(ele.tagName)) {
export const isActivatable = function (ele: HTMLElement) {
if (ACTIVATABLE_ELEMENTS.indexOf(ele.tagName) > -1) {
return true;
}

let attributes = ele.attributes;
for (let i = 0, l = attributes.length; i < l; i++) {
if (ACTIVATABLE_ATTRIBUTES.test(attributes[i].name)) {
if (ACTIVATABLE_ATTRIBUTES.indexOf(attributes[i].name) > -1) {
return true;
}
}

return false;
};

function addListener(type: string, listener: any, useCapture?: boolean) {
document.addEventListener(type, listener, useCapture);
}

function removeListener(type: string, listener: any) {
document.removeEventListener(type, listener);
}

const ACTIVATABLE_ELEMENTS = /^(A|BUTTON)$/;
const ACTIVATABLE_ATTRIBUTES = /tappable|button/i;
const POINTER_TOLERANCE = 4;
const POINTER_MOVE_UNTIL_CANCEL = 10;
const ACTIVATABLE_ELEMENTS = ['A', 'BUTTON'];
const ACTIVATABLE_ATTRIBUTES = ['tappable', 'button'];
const POINTER_TOLERANCE = 60;
const DISABLE_NATIVE_CLICK_AMOUNT = 2500;

export function setupTapClick(config: Config, app: App, zone: NgZone) {
Expand Down
2 changes: 1 addition & 1 deletion src/platform/platform-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const PLATFORM_CONFIGS: {[key: string]: PlatformConfig} = {
swipeBackThreshold: 40,
tapPolyfill: isIOSDevice,
virtualScrollEventAssist: !(window.indexedDB),
canDisableScroll: !!(window.indexedDB),
canDisableScroll: isIOSDevice,
},
isMatch(p: Platform) {
return p.isPlatformMatch('ios', ['iphone', 'ipad', 'ipod'], ['windows phone']);
Expand Down
Loading

0 comments on commit 780717e

Please sign in to comment.