Skip to content

Commit 35d12ef

Browse files
committed
fix(tapclick): several improvements
- refactors code using UIEventManager - improved performance by using passive event listeners - fixes isScrolling() - click tolerance has been increased to match native behavior - click is immediately prevented if the content is scrolled.
1 parent 272acfc commit 35d12ef

File tree

7 files changed

+152
-147
lines changed

7 files changed

+152
-147
lines changed

src/components/app/app.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class App {
2222
private _title: string = '';
2323
private _titleSrv: Title = new Title();
2424
private _rootNav: NavController = null;
25+
private _canDisableScroll: boolean;
2526

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

7577
/**
@@ -122,7 +124,7 @@ export class App {
122124
* scrolling is enabled. When set to `true`, scrolling is disabled.
123125
*/
124126
setScrollDisabled(disableScroll: boolean) {
125-
if (this._config.get('canDisableScroll', true)) {
127+
if (this._canDisableScroll) {
126128
this._appRoot._disableScroll(disableScroll);
127129
}
128130
}
@@ -148,7 +150,7 @@ export class App {
148150
* @return {boolean} returns true or false
149151
*/
150152
isScrolling(): boolean {
151-
return (this._scrollTime + 48 > Date.now());
153+
return ((this._scrollTime + ACTIVE_SCROLLING_TIME) > Date.now());
152154
}
153155

154156
/**
@@ -275,4 +277,5 @@ export class App {
275277

276278
}
277279

280+
const ACTIVE_SCROLLING_TIME = 100;
278281
const CLICK_BLOCK_BUFFER_IN_MILLIS = 64;

src/components/content/content.scss

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ ion-content.js-scroll > .scroll-content {
5252
}
5353

5454
.disable-scroll .ion-page .scroll-content {
55-
overflow-y: hidden;
56-
overflow-x: hidden;
55+
pointer-events: none;
5756
}
5857

5958

src/components/content/content.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export class Content extends Ion {
190190

191191
this._zone.runOutsideAngular(() => {
192192
this._scroll = new ScrollView(this._scrollEle);
193-
this._scLsn = this.addScrollListener(this._app.setScrolling);
193+
this._scLsn = this.addScrollListener(this._app.setScrolling.bind(this._app));
194194
});
195195
}
196196

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

255+
/**
256+
* @private
257+
*/
255258
_addListener(type: string, handler: any): Function {
256259
assert(handler, 'handler must be valid');
257260
assert(this._scrollEle, '_scrollEle must be valid');

src/components/tap-click/activator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class Activator {
2121
// queue to have this element activated
2222
this._queue.push(activatableEle);
2323

24-
rafFrames(2, () => {
24+
rafFrames(6, () => {
2525
let activatableEle: HTMLElement;
2626
for (let i = 0; i < this._queue.length; i++) {
2727
activatableEle = this._queue[i];
@@ -30,7 +30,7 @@ export class Activator {
3030
activatableEle.classList.add(this._css);
3131
}
3232
}
33-
this._queue = [];
33+
this._queue.length = 0;
3434
});
3535
}
3636

@@ -59,7 +59,7 @@ export class Activator {
5959

6060
deactivate() {
6161
// remove the active class from all active elements
62-
this._queue = [];
62+
this._queue.length = 0;
6363

6464
rafFrames(2, () => {
6565
for (var i = 0; i < this._active.length; i++) {

src/components/tap-click/tap-click.ts

Lines changed: 73 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -5,156 +5,89 @@ import { App } from '../app/app';
55
import { Config } from '../../config/config';
66
import { hasPointerMoved, pointerCoord } from '../../util/dom';
77
import { RippleActivator } from './ripple';
8-
8+
import { UIEventManager, PointerEvents, PointerEventType } from '../../util/ui-event-manager';
99

1010
/**
1111
* @private
1212
*/
1313
@Injectable()
1414
export class TapClick {
15-
private lastTouch: number = 0;
1615
private disableClick: number = 0;
17-
private lastActivated: number = 0;
1816
private usePolyfill: boolean;
1917
private activator: Activator;
2018
private startCoord: any;
21-
private pointerMove: any;
19+
private events: UIEventManager = new UIEventManager(false);
20+
private pointerEvents: PointerEvents;
2221

2322
constructor(
2423
config: Config,
2524
private app: App,
2625
zone: NgZone
2726
) {
28-
if (config.get('activator') === 'ripple') {
27+
let activator = config.get('activator');
28+
if (activator === 'ripple') {
2929
this.activator = new RippleActivator(app, config);
3030

31-
} else if (config.get('activator') === 'highlight') {
31+
} else if (activator === 'highlight') {
3232
this.activator = new Activator(app, config);
3333
}
3434

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

37-
zone.runOutsideAngular(() => {
38-
addListener('click', this.click.bind(this), true);
39-
40-
addListener('touchstart', this.touchStart.bind(this));
41-
addListener('touchend', this.touchEnd.bind(this));
42-
addListener('touchcancel', this.pointerCancel.bind(this));
43-
44-
addListener('mousedown', this.mouseDown.bind(this), true);
45-
addListener('mouseup', this.mouseUp.bind(this), true);
37+
this.events.listen(document, 'click', this.click.bind(this), true);
38+
this.pointerEvents = this.events.pointerEvents({
39+
element: <any>document,
40+
pointerDown: this.pointerStart.bind(this),
41+
pointerMove: this.pointerMove.bind(this),
42+
pointerUp: this.pointerEnd.bind(this),
43+
passive: true
4644
});
47-
48-
this.pointerMove = (ev: UIEvent) => {
49-
if (!this.startCoord || hasPointerMoved(POINTER_MOVE_UNTIL_CANCEL, this.startCoord, pointerCoord(ev)) ) {
50-
this.pointerCancel(ev);
51-
}
52-
};
53-
}
54-
55-
touchStart(ev: UIEvent) {
56-
this.lastTouch = Date.now();
57-
this.pointerStart(ev);
45+
this.pointerEvents.mouseWait = DISABLE_NATIVE_CLICK_AMOUNT;
5846
}
5947

60-
touchEnd(ev: UIEvent) {
61-
this.lastTouch = Date.now();
62-
63-
if (this.usePolyfill && this.startCoord && this.app.isEnabled()) {
64-
// only dispatch mouse click events from a touchend event
65-
// when tapPolyfill config is true, and the startCoordand endCoord
66-
// are not too far off from each other
67-
let endCoord = pointerCoord(ev);
68-
69-
if (!hasPointerMoved(POINTER_TOLERANCE, this.startCoord, endCoord)) {
70-
// prevent native mouse click events for XX amount of time
71-
this.disableClick = this.lastTouch + DISABLE_NATIVE_CLICK_AMOUNT;
72-
73-
if (this.app.isScrolling()) {
74-
// do not fire off a click event while the app was scrolling
75-
console.debug('click from touch prevented by scrolling ' + Date.now());
76-
77-
} else {
78-
// dispatch a mouse click event
79-
console.debug('create click from touch ' + Date.now());
80-
81-
let clickEvent: any = document.createEvent('MouseEvents');
82-
clickEvent.initMouseEvent('click', true, true, window, 1, 0, 0, endCoord.x, endCoord.y, false, false, false, false, 0, null);
83-
clickEvent.isIonicTap = true;
84-
ev.target.dispatchEvent(clickEvent);
85-
}
86-
}
48+
pointerStart(ev: any): boolean {
49+
if (this.startCoord) {
50+
return false;
8751
}
88-
89-
this.pointerEnd(ev);
90-
}
91-
92-
mouseDown(ev: any) {
93-
if (this.isDisabledNativeClick()) {
94-
console.debug('mouseDown prevent ' + ev.target.tagName + ' ' + Date.now());
95-
// does not prevent default on purpose
96-
// so native blur events from inputs can happen
97-
ev.stopPropagation();
98-
99-
} else if (this.lastTouch + DISABLE_NATIVE_CLICK_AMOUNT < Date.now()) {
100-
this.pointerStart(ev);
52+
let activatableEle = getActivatableTarget(ev.target);
53+
if (!activatableEle) {
54+
this.startCoord = null;
55+
return false;
10156
}
57+
this.startCoord = pointerCoord(ev);
58+
this.activator && this.activator.downAction(ev, activatableEle, this.startCoord);
59+
return true;
10260
}
10361

104-
mouseUp(ev: any) {
105-
if (this.isDisabledNativeClick()) {
106-
console.debug('mouseUp prevent ' + ev.target.tagName + ' ' + Date.now());
107-
ev.preventDefault();
108-
ev.stopPropagation();
109-
}
110-
111-
if (this.lastTouch + DISABLE_NATIVE_CLICK_AMOUNT < Date.now()) {
112-
this.pointerEnd(ev);
62+
pointerMove(ev: UIEvent) {
63+
if (!this.startCoord ||
64+
hasPointerMoved(POINTER_TOLERANCE, this.startCoord, pointerCoord(ev)) ||
65+
this.app.isScrolling()) {
66+
this.pointerCancel(ev);
11367
}
11468
}
11569

116-
pointerStart(ev: any) {
117-
let activatableEle = getActivatableTarget(ev.target);
118-
119-
if (activatableEle) {
120-
this.startCoord = pointerCoord(ev);
121-
122-
let now = Date.now();
123-
if (this.lastActivated + 150 < now && !this.app.isScrolling()) {
124-
this.activator && this.activator.downAction(ev, activatableEle, this.startCoord);
125-
this.lastActivated = now;
126-
}
127-
128-
this.moveListeners(true);
129-
130-
} else {
131-
this.startCoord = null;
70+
pointerEnd(ev: any, type: PointerEventType) {
71+
if (!this.startCoord) {
72+
return;
13273
}
133-
}
134-
135-
pointerEnd(ev: any) {
136-
if (this.startCoord && this.activator) {
74+
if (type === PointerEventType.TOUCH && this.usePolyfill && this.app.isEnabled()) {
75+
this.handleTapPolyfill(ev);
76+
}
77+
if (this.activator) {
13778
let activatableEle = getActivatableTarget(ev.target);
13879
if (activatableEle) {
13980
this.activator.upAction(ev, activatableEle, this.startCoord);
14081
}
14182
}
142-
143-
this.moveListeners(false);
83+
this.startCoord = null;
14484
}
14585

14686
pointerCancel(ev: UIEvent) {
14787
console.debug('pointerCancel from ' + ev.type + ' ' + Date.now());
88+
this.startCoord = null;
14889
this.activator && this.activator.clearState();
149-
this.moveListeners(false);
150-
}
151-
152-
moveListeners(shouldAdd: boolean) {
153-
removeListener(this.usePolyfill ? 'touchmove' : 'mousemove', this.pointerMove);
154-
155-
if (shouldAdd) {
156-
addListener(this.usePolyfill ? 'touchmove' : 'mousemove', this.pointerMove);
157-
}
90+
this.pointerEvents.stop();
15891
}
15992

16093
click(ev: any) {
@@ -174,6 +107,34 @@ export class TapClick {
174107
}
175108
}
176109

110+
handleTapPolyfill(ev: any) {
111+
// only dispatch mouse click events from a touchend event
112+
// when tapPolyfill config is true, and the startCoordand endCoord
113+
// are not too far off from each other
114+
let endCoord = pointerCoord(ev);
115+
116+
if (hasPointerMoved(POINTER_TOLERANCE, this.startCoord, endCoord)) {
117+
console.debug('click from touch prevented by pointer moved');
118+
return;
119+
}
120+
// prevent native mouse click events for XX amount of time
121+
this.disableClick = Date.now() + DISABLE_NATIVE_CLICK_AMOUNT;
122+
123+
if (this.app.isScrolling()) {
124+
// do not fire off a click event while the app was scrolling
125+
console.debug('click from touch prevented by scrolling ' + Date.now());
126+
127+
} else {
128+
// dispatch a mouse click event
129+
console.debug('create click from touch ' + Date.now());
130+
131+
let clickEvent: any = document.createEvent('MouseEvents');
132+
clickEvent.initMouseEvent('click', true, true, window, 1, 0, 0, endCoord.x, endCoord.y, false, false, false, false, 0, null);
133+
clickEvent.isIonicTap = true;
134+
ev.target.dispatchEvent(clickEvent);
135+
}
136+
}
137+
177138
isDisabledNativeClick() {
178139
return this.disableClick > Date.now();
179140
}
@@ -194,33 +155,23 @@ function getActivatableTarget(ele: HTMLElement) {
194155
/**
195156
* @private
196157
*/
197-
export const isActivatable = function(ele: HTMLElement) {
198-
if (ACTIVATABLE_ELEMENTS.test(ele.tagName)) {
158+
export const isActivatable = function (ele: HTMLElement) {
159+
if (ACTIVATABLE_ELEMENTS.indexOf(ele.tagName) > -1) {
199160
return true;
200161
}
201162

202163
let attributes = ele.attributes;
203164
for (let i = 0, l = attributes.length; i < l; i++) {
204-
if (ACTIVATABLE_ATTRIBUTES.test(attributes[i].name)) {
165+
if (ACTIVATABLE_ATTRIBUTES.indexOf(attributes[i].name) > -1) {
205166
return true;
206167
}
207168
}
208-
209169
return false;
210170
};
211171

212-
function addListener(type: string, listener: any, useCapture?: boolean) {
213-
document.addEventListener(type, listener, useCapture);
214-
}
215-
216-
function removeListener(type: string, listener: any) {
217-
document.removeEventListener(type, listener);
218-
}
219-
220-
const ACTIVATABLE_ELEMENTS = /^(A|BUTTON)$/;
221-
const ACTIVATABLE_ATTRIBUTES = /tappable|button/i;
222-
const POINTER_TOLERANCE = 4;
223-
const POINTER_MOVE_UNTIL_CANCEL = 10;
172+
const ACTIVATABLE_ELEMENTS = ['A', 'BUTTON'];
173+
const ACTIVATABLE_ATTRIBUTES = ['tappable', 'button'];
174+
const POINTER_TOLERANCE = 60;
224175
const DISABLE_NATIVE_CLICK_AMOUNT = 2500;
225176

226177
export function setupTapClick(config: Config, app: App, zone: NgZone) {

src/platform/platform-registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export const PLATFORM_CONFIGS: {[key: string]: PlatformConfig} = {
110110
swipeBackThreshold: 40,
111111
tapPolyfill: isIOSDevice,
112112
virtualScrollEventAssist: !(window.indexedDB),
113-
canDisableScroll: !!(window.indexedDB),
113+
canDisableScroll: isIOSDevice,
114114
},
115115
isMatch(p: Platform) {
116116
return p.isPlatformMatch('ios', ['iphone', 'ipad', 'ipod'], ['windows phone']);

0 commit comments

Comments
 (0)