fix(gesture/tapclick): ios support

This commit is contained in:
Manu Mtz.-Almeida
2018-02-20 12:38:36 +01:00
parent 83062221a3
commit 9fded75502
12 changed files with 139 additions and 98 deletions

View File

@ -1000,6 +1000,36 @@ declare global {
} }
import {
GestureController as IonGestureController
} from './components/gesture-controller/gesture-controller';
declare global {
interface HTMLIonGestureControllerElement extends IonGestureController, HTMLStencilElement {
}
var HTMLIonGestureControllerElement: {
prototype: HTMLIonGestureControllerElement;
new (): HTMLIonGestureControllerElement;
};
interface HTMLElementTagNameMap {
"ion-gesture-controller": HTMLIonGestureControllerElement;
}
interface ElementTagNameMap {
"ion-gesture-controller": HTMLIonGestureControllerElement;
}
namespace JSX {
interface IntrinsicElements {
"ion-gesture-controller": JSXElements.IonGestureControllerAttributes;
}
}
namespace JSXElements {
export interface IonGestureControllerAttributes extends HTMLAttributes {
}
}
}
import { import {
Gesture as IonGesture Gesture as IonGesture
} from './components/gesture/gesture'; } from './components/gesture/gesture';
@ -2660,7 +2690,6 @@ declare global {
onionScroll?: ScrollCallback; onionScroll?: ScrollCallback;
onionScrollEnd?: ScrollCallback; onionScrollEnd?: ScrollCallback;
onionScrollStart?: ScrollCallback; onionScrollStart?: ScrollCallback;
scrollEvents?: boolean;
} }
} }
} }

View File

@ -1,3 +1,9 @@
import { Component, Method, EventEmitter, Event } from "@stencil/core";
@Component({
tag: 'ion-gesture-controller'
})
export class GestureController { export class GestureController {
private gestureId = 0; private gestureId = 0;
@ -6,21 +12,21 @@ export class GestureController {
private disabledScroll = new Set<number>(); private disabledScroll = new Set<number>();
private capturedId: number|null = null; private capturedId: number|null = null;
createGesture(gestureName: string, gesturePriority: number, disableScroll: boolean): GestureDelegate { @Event() ionGestureCaptured: EventEmitter<string>;
return new GestureDelegate(this, this.newID(), gestureName, gesturePriority, disableScroll);
@Method()
create(config: GestureConfig): Promise<GestureDelegate> {
return Promise.resolve(new GestureDelegate(this, this.newID(), config.name, config.priority, config.disableScroll));
} }
createBlocker(opts: BlockerOptions = {}): BlockerDelegate { @Method()
createBlocker(opts: BlockerConfig = {}): BlockerDelegate {
return new BlockerDelegate(this.newID(), this, return new BlockerDelegate(this.newID(), this,
opts.disable, opts.disable,
!!opts.disableScroll !!opts.disableScroll
); );
} }
newID(): number {
return this.gestureId++;
}
start(gestureName: string, id: number, priority: number): boolean { start(gestureName: string, id: number, priority: number): boolean {
if (!this.canStart(gestureName)) { if (!this.canStart(gestureName)) {
this.requestedStart.delete(id); this.requestedStart.delete(id);
@ -43,6 +49,7 @@ export class GestureController {
if (maxPriority === priority) { if (maxPriority === priority) {
this.capturedId = id; this.capturedId = id;
this.requestedStart.clear(); this.requestedStart.clear();
this.ionGestureCaptured.emit(gestureName);
return true; return true;
} }
requestedStart.delete(id); requestedStart.delete(id);
@ -121,6 +128,9 @@ export class GestureController {
return false; return false;
} }
private newID(): number {
return this.gestureId++;
}
} }
@ -180,14 +190,11 @@ export class GestureDelegate {
this.release(); this.release();
this.ctrl = null; this.ctrl = null;
} }
} }
export class BlockerDelegate { export class BlockerDelegate {
blocked = false;
private ctrl: GestureController|null; private ctrl: GestureController|null;
constructor( constructor(
@ -212,7 +219,6 @@ export class BlockerDelegate {
if (this.disableScroll) { if (this.disableScroll) {
this.ctrl.disableScroll(this.blockerDelegateId); this.ctrl.disableScroll(this.blockerDelegateId);
} }
this.blocked = true;
} }
unblock() { unblock() {
@ -227,7 +233,6 @@ export class BlockerDelegate {
if (this.disableScroll) { if (this.disableScroll) {
this.ctrl.enableScroll(this.blockerDelegateId); this.ctrl.enableScroll(this.blockerDelegateId);
} }
this.blocked = false;
} }
destroy() { destroy() {
@ -237,13 +242,20 @@ export class BlockerDelegate {
} }
export interface BlockerOptions { export interface GestureConfig {
disableScroll?: boolean; name: string;
disable?: string[]; priority: number;
disableScroll: boolean;
} }
export const BLOCK_ALL: BlockerOptions = { export interface BlockerConfig {
disable?: string[];
disableScroll?: boolean;
}
export const BLOCK_ALL: BlockerConfig = {
disable: ['menu-swipe', 'goback-swipe'], disable: ['menu-swipe', 'goback-swipe'],
disableScroll: true disableScroll: true
}; };

View File

@ -5,7 +5,20 @@
<!-- Auto Generated Below --> <!-- Auto Generated Below -->
## Events
#### ionGestureCaptured
## Methods
#### create()
#### createBlocker()
---------------------------------------------- ----------------------------------------------
*Built by [StencilJS](https://stenciljs.com/)* *Built with [StencilJS](https://stenciljs.com/)*

View File

@ -1,11 +1,9 @@
import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Prop, Watch } from '@stencil/core'; import { Component, Event, EventEmitter, EventListenerEnable, Listen, Prop, Watch } from '@stencil/core';
import { ElementRef, applyStyles, assert, getElementReference, now, updateDetail } from '../../utils/helpers'; import { ElementRef, assert, now, updateDetail } from '../../utils/helpers';
import { BLOCK_ALL, BlockerDelegate, GestureController, GestureDelegate } from '../gesture-controller/gesture-controller'; import { BlockerDelegate, GestureDelegate, BlockerConfig, BLOCK_ALL } from '../gesture-controller/gesture-controller';
import { DomController } from '../../index'; import { DomController } from '../../index';
import { PanRecognizer } from './recognizers'; import { PanRecognizer } from './recognizers';
declare const Ionic: { gesture: GestureController };
@Component({ @Component({
tag: 'ion-gesture' tag: 'ion-gesture'
@ -14,7 +12,6 @@ export class Gesture {
private detail: GestureDetail = {}; private detail: GestureDetail = {};
private positions: number[] = []; private positions: number[] = [];
private ctrl: GestureController;
private gesture: GestureDelegate; private gesture: GestureDelegate;
private lastTouch = 0; private lastTouch = 0;
private pan: PanRecognizer; private pan: PanRecognizer;
@ -25,8 +22,7 @@ export class Gesture {
private isMoveQueued = false; private isMoveQueued = false;
private blocker: BlockerDelegate; private blocker: BlockerDelegate;
@Element() private el: HTMLElement; @Prop({ connect: 'ion-gesture-controller' }) gestureCtrl: HTMLIonGestureControllerElement;
@Prop({ context: 'dom' }) dom: DomController; @Prop({ context: 'dom' }) dom: DomController;
@Prop({ context: 'enableListener' }) enableListener: EventListenerEnable; @Prop({ context: 'enableListener' }) enableListener: EventListenerEnable;
@ -76,13 +72,18 @@ export class Gesture {
*/ */
@Event() ionPress: EventEmitter; @Event() ionPress: EventEmitter;
componentWillLoad() {
return this.gestureCtrl.create({
name: this.gestureName,
priority: this.gesturePriority,
disableScroll: this.disableScroll
}).then((gesture) => this.gesture = gesture);
}
componentDidLoad() { componentDidLoad() {
// in this case, we already know the GestureController and Gesture are already // in this case, we already know the GestureController and Gesture are already
// apart of the same bundle, so it's safe to load it this way // apart of the same bundle, so it's safe to load it this way
// only create one instance of GestureController, and reuse the same one later // only create one instance of GestureController, and reuse the same one later
this.ctrl = Ionic.gesture = Ionic.gesture || new GestureController();
this.gesture = this.ctrl.createGesture(this.gestureName, this.gesturePriority, this.disableScroll);
const types = this.type.replace(/\s/g, '').toLowerCase().split(','); const types = this.type.replace(/\s/g, '').toLowerCase().split(',');
if (types.indexOf('pan') > -1) { if (types.indexOf('pan') > -1) {
@ -91,15 +92,9 @@ export class Gesture {
this.hasPress = (types.indexOf('press') > -1); this.hasPress = (types.indexOf('press') > -1);
this.disabledChanged(this.disabled); this.disabledChanged(this.disabled);
if (this.pan || this.hasPress) {
this.dom.write(() => {
applyStyles(getElementReference(this.el, this.attachTo), GESTURE_INLINE_STYLES);
});
}
if (this.autoBlockAll) { if (this.autoBlockAll) {
this.blocker = this.ctrl.createBlocker(BLOCK_ALL); this.setBlocker(BLOCK_ALL).then(b => b.block());
this.blocker.block();
} }
} }
@ -116,12 +111,19 @@ export class Gesture {
@Watch('block') @Watch('block')
protected blockChanged(block: string) { protected blockChanged(block: string) {
this.setBlocker({ disable: block.split(',')});
}
private setBlocker(config: BlockerConfig) {
if (this.blocker) { if (this.blocker) {
this.blocker.destroy(); this.blocker.destroy();
} }
if (block) { if (config) {
this.blocker = this.ctrl.createBlocker({ disable: block.split(',')}); return this.gestureCtrl.componentOnReady()
.then(ctrl => ctrl.createBlocker(config))
.then(blocker => this.blocker = blocker);
} }
return Promise.resolve(null);
} }
// DOWN ************************* // DOWN *************************
@ -459,19 +461,12 @@ export class Gesture {
this.blocker = null; this.blocker = null;
} }
this.gesture && this.gesture.destroy(); this.gesture && this.gesture.destroy();
this.ctrl = this.gesture = this.pan = this.detail = this.detail.event = null; this.gesture = this.pan = this.detail = this.detail.event = null;
} }
} }
const GESTURE_INLINE_STYLES = {
'touch-action': 'none',
'user-select': 'none',
'-webkit-user-drag': 'none',
'-webkit-tap-highlight-color': 'rgba(0,0,0,0)'
};
const MOUSE_WAIT = 2500; const MOUSE_WAIT = 2500;

View File

@ -221,9 +221,6 @@ export class InfiniteScroll {
private enableScrollEvents(shouldListen: boolean) { private enableScrollEvents(shouldListen: boolean) {
if (this.scrollEl) { if (this.scrollEl) {
if (shouldListen) {
this.scrollEl.scrollEvents = true;
}
this.enableListener(this, 'ionScroll', shouldListen, this.scrollEl); this.enableListener(this, 'ionScroll', shouldListen, this.scrollEl);
} }
} }

View File

@ -22,11 +22,6 @@
#### scrollEvents
boolean
## Attributes ## Attributes
#### onion-scroll #### onion-scroll
@ -44,11 +39,6 @@ boolean
#### scroll-events
boolean
## Events ## Events
#### ionScroll #### ionScroll

View File

@ -1,8 +1,6 @@
import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Method, Prop, Watch } from '@stencil/core'; import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Method, Prop } from '@stencil/core';
import { Config, DomController, GestureDetail } from '../../index'; import { Config, DomController, GestureDetail } from '../../index';
import { GestureController, GestureDelegate } from '../gesture-controller/gesture-controller'; import { GestureDelegate } from '../gesture-controller/gesture-controller';
declare const Ionic: { gesture: GestureController };
@Component({ @Component({
@ -20,6 +18,7 @@ export class Scroll {
@Element() private el: HTMLElement; @Element() private el: HTMLElement;
@Prop({ connect: 'ion-gesture-controller'}) gestureCtrl: HTMLIonGestureControllerElement;
@Prop({ context: 'config'}) config: Config; @Prop({ context: 'config'}) config: Config;
@Prop({ context: 'enableListener'}) enableListener: EventListenerEnable; @Prop({ context: 'enableListener'}) enableListener: EventListenerEnable;
@Prop({ context: 'dom' }) dom: DomController; @Prop({ context: 'dom' }) dom: DomController;
@ -44,22 +43,19 @@ export class Scroll {
*/ */
@Event() ionScrollEnd: EventEmitter; @Event() ionScrollEnd: EventEmitter;
componentWillLoad() {
@Prop() scrollEvents = false; return this.gestureCtrl.create({
@Watch('scrollEvents') name: 'scroll',
scrollChanged(enabled: boolean) { priority: 100,
this.enableListener(this, 'scroll', enabled); disableScroll: false,
}).then(gesture => this.gesture = gesture);
} }
componentDidLoad() { componentDidLoad() {
if (this.isServer) { if (this.isServer) {
return; return;
} }
const gestureCtrl = Ionic.gesture = Ionic.gesture || new GestureController();
this.gesture = gestureCtrl.createGesture('scroll', 100, false);
this.app = this.el.closest('ion-app') as HTMLIonAppElement; this.app = this.el.closest('ion-app') as HTMLIonAppElement;
this.scrollChanged(this.scrollEvents);
} }
componentDidUnload() { componentDidUnload() {
@ -69,7 +65,7 @@ export class Scroll {
// Native Scroll ************************* // Native Scroll *************************
@Listen('scroll', { passive: true, enabled: false }) @Listen('scroll', { passive: true })
onNativeScroll() { onNativeScroll() {
if (!this.queued) { if (!this.queued) {
this.queued = true; this.queued = true;
@ -219,6 +215,7 @@ export class Scroll {
if (this.onionScrollStart) { if (this.onionScrollStart) {
this.onionScrollStart(detail); this.onionScrollStart(detail);
} }
this.gesture.capture();
this.ionScrollStart.emit(detail); this.ionScrollStart.emit(detail);
} }
detail.deltaY = (detail.scrollTop - detail.startY); detail.deltaY = (detail.scrollTop - detail.startY);
@ -277,6 +274,8 @@ export class Scroll {
detail.timeStamp = timeStamp; detail.timeStamp = timeStamp;
// emit that the scroll has ended // emit that the scroll has ended
this.gesture.release();
if (this.onionScrollEnd) { if (this.onionScrollEnd) {
this.onionScrollEnd(detail); this.onionScrollEnd(detail);
} }

View File

@ -1,8 +1,5 @@
import { Component, Element, EventListenerEnable, Listen, Prop } from '@stencil/core'; import { Component, Element, EventListenerEnable, Listen, Prop } from '@stencil/core';
import { now, pointerCoordX, pointerCoordY } from '../../utils/helpers'; import { now, pointerCoordX, pointerCoordY } from '../../utils/helpers';
import { GestureController } from '../gesture-controller/gesture-controller';
declare const Ionic: { gesture: GestureController };
@Component({ @Component({
@ -13,8 +10,7 @@ export class TapClick {
private app: HTMLIonAppElement; private app: HTMLIonAppElement;
private lastTouch = -MOUSE_WAIT*10; private lastTouch = -MOUSE_WAIT*10;
private lastActivated = 0; private lastActivated = 0;
private cancelled = false;
private gestureCtrl: GestureController;
private activatableEle: HTMLElement | null; private activatableEle: HTMLElement | null;
private activeDefer: any; private activeDefer: any;
@ -30,40 +26,37 @@ export class TapClick {
if (this.isServer) { if (this.isServer) {
return; return;
} }
this.gestureCtrl = Ionic.gesture = Ionic.gesture || new GestureController();
this.app = this.el.closest('ion-app') as HTMLIonAppElement; this.app = this.el.closest('ion-app') as HTMLIonAppElement;
} }
@Listen('document:click', {passive: false, capture: true}) @Listen('body:click', {passive: false, capture: true})
onBodyClick(ev: Event) { onBodyClick(ev: Event) {
if (this.shouldCancel()) { if (this.cancelled || this.shouldCancel()) {
debugger;
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
} }
} }
// Touch Events // Touch Events
@Listen('document:touchstart', { passive: true }) @Listen('document:touchstart', { passive: true, capture: true })
onTouchStart(ev: TouchEvent) { onTouchStart(ev: TouchEvent) {
this.lastTouch = now(ev); this.lastTouch = now(ev);
this.pointerDown(ev); this.pointerDown(ev);
} }
@Listen('document:touchcancel', { passive: true }) @Listen('document:touchcancel', { passive: true, capture: true })
onTouchCancel(ev: TouchEvent) { onTouchCancel(ev: TouchEvent) {
this.lastTouch = now(ev); this.lastTouch = now(ev);
this.pointerUp(ev); this.pointerUp(ev);
} }
@Listen('document:touchend', { passive: true }) @Listen('document:touchend', { passive: false, capture: true })
onTouchEnd(ev: TouchEvent) { onTouchEnd(ev: TouchEvent) {
this.lastTouch = now(ev); this.lastTouch = now(ev);
this.pointerUp(ev); this.pointerUp(ev);
} }
@Listen('document:mousedown', { passive: true }) @Listen('document:mousedown', { passive: true, capture: true })
onMouseDown(ev: MouseEvent) { onMouseDown(ev: MouseEvent) {
const t = now(ev) - MOUSE_WAIT; const t = now(ev) - MOUSE_WAIT;
if (this.lastTouch < t) { if (this.lastTouch < t) {
@ -71,7 +64,7 @@ export class TapClick {
} }
} }
@Listen('document:mouseup', { passive: true }) @Listen('document:mouseup', { passive: false, capture: true })
onMouseUp(ev: TouchEvent) { onMouseUp(ev: TouchEvent) {
const t = now(ev) - MOUSE_WAIT; const t = now(ev) - MOUSE_WAIT;
if (this.lastTouch < t) { if (this.lastTouch < t) {
@ -80,25 +73,32 @@ export class TapClick {
} }
@Listen('body:ionScrollStart') @Listen('body:ionScrollStart')
scrollStarted() { @Listen('body:ionGestureCaptured')
cancelActive() {
clearTimeout(this.activeDefer); clearTimeout(this.activeDefer);
if (this.activatableEle) { if (this.activatableEle) {
this.removeActivated(false); this.removeActivated(false);
this.activatableEle = null; this.activatableEle = null;
} }
this.cancelled = true;
} }
private pointerDown(ev: any) { private pointerDown(ev: any) {
if (this.activatableEle) { if (this.activatableEle) {
return; return;
} }
if (!this.shouldCancel()) { this.cancelled = this.shouldCancel();
if (!this.cancelled) {
this.setActivatedElement(getActivatableTarget(ev.target), ev); this.setActivatedElement(getActivatableTarget(ev.target), ev);
} }
} }
private pointerUp(ev: UIEvent) { private pointerUp(ev: UIEvent) {
this.setActivatedElement(null, ev); this.setActivatedElement(null, ev);
if (this.cancelled) {
ev.preventDefault();
}
} }
private setActivatedElement(el: HTMLElement | null, ev: UIEvent) { private setActivatedElement(el: HTMLElement | null, ev: UIEvent) {
@ -175,10 +175,6 @@ export class TapClick {
console.debug('click prevent: appDisabled'); console.debug('click prevent: appDisabled');
return true; return true;
} }
if (this.gestureCtrl.isCaptured()) {
console.debug('click prevent: tap-click (gesture is captured)');
return true;
}
return false; return false;
} }
} }

View File

@ -7,6 +7,9 @@ ion-toggle {
display: inline-block; display: inline-block;
contain: content; contain: content;
touch-action: none;
user-select: none;
} }
ion-toggle ion-gesture { ion-toggle ion-gesture {
@ -52,8 +55,10 @@ ion-toggle input {
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
border: 0;
pointer-events: none; pointer-events: none;
// touch-action: pan-x;
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;

View File

@ -16,13 +16,13 @@ import { debounce } from '../../utils/helpers';
} }
}) })
export class Toggle implements CheckboxInput { export class Toggle implements CheckboxInput {
private didLoad: boolean; private didLoad: boolean;
private gestureConfig: any; private gestureConfig: any;
private inputId: string; private inputId: string;
private nativeInput: HTMLInputElement; private nativeInput: HTMLInputElement;
private pivotX: number; private pivotX: number;
@State() activated = false; @State() activated = false;
@State() keyFocus: boolean; @State() keyFocus: boolean;
@ -88,6 +88,7 @@ export class Toggle implements CheckboxInput {
'onMove': this.onDragMove.bind(this), 'onMove': this.onDragMove.bind(this),
'onEnd': this.onDragEnd.bind(this), 'onEnd': this.onDragEnd.bind(this),
'gestureName': 'toggle', 'gestureName': 'toggle',
'passive': false,
'gesturePriority': 30, 'gesturePriority': 30,
'type': 'pan', 'type': 'pan',
'direction': 'x', 'direction': 'x',
@ -141,6 +142,10 @@ export class Toggle implements CheckboxInput {
private onDragStart(detail: GestureDetail) { private onDragStart(detail: GestureDetail) {
this.pivotX = detail.currentX; this.pivotX = detail.currentX;
this.activated = true; this.activated = true;
// touch-action does not work in iOS
detail.event.preventDefault();
return true;
} }
private onDragMove(detail: GestureDetail) { private onDragMove(detail: GestureDetail) {

View File

@ -36,9 +36,9 @@ export { PanRecognizer } from './components/gesture/recognizers';
export { export {
BLOCK_ALL, BLOCK_ALL,
BlockerDelegate, BlockerDelegate,
BlockerOptions, GestureDelegate,
GestureController, BlockerConfig,
GestureDelegate GestureConfig,
} from './components/gesture-controller/gesture-controller'; } from './components/gesture-controller/gesture-controller';
export { Grid } from './components/grid/grid'; export { Grid } from './components/grid/grid';
export { Header } from './components/header/header'; export { Header } from './components/header/header';

View File

@ -20,7 +20,7 @@ exports.config = {
{ components: ['ion-datetime', 'ion-picker', 'ion-picker-column', 'ion-picker-controller'] }, { components: ['ion-datetime', 'ion-picker', 'ion-picker-column', 'ion-picker-controller'] },
{ components: ['ion-events'] }, { components: ['ion-events'] },
{ components: ['ion-fab', 'ion-fab-button', 'ion-fab-list'] }, { components: ['ion-fab', 'ion-fab-button', 'ion-fab-list'] },
{ components: ['ion-gesture'] }, { components: ['ion-gesture', 'ion-gesture-controller'] },
{ components: ['ion-grid', 'ion-row', 'ion-col'] }, { components: ['ion-grid', 'ion-row', 'ion-col'] },
{ components: ['ion-item', 'ion-item-divider', 'ion-item-group', 'ion-label', 'ion-list', 'ion-list-header', 'ion-skeleton-text'] }, { components: ['ion-item', 'ion-item-divider', 'ion-item-group', 'ion-label', 'ion-list', 'ion-list-header', 'ion-skeleton-text'] },
{ components: ['ion-item-sliding', 'ion-item-options', 'ion-item-option'] }, { components: ['ion-item-sliding', 'ion-item-options', 'ion-item-option'] },