perf(gesture): lazy loaded dynamic ES module

This commit is contained in:
Manu Mtz.-Almeida
2018-08-01 01:38:52 +02:00
parent 1b5bb67959
commit 49cac8beec
34 changed files with 939 additions and 1709 deletions

View File

@@ -35,18 +35,12 @@ import {
AlertOptions,
Animation,
AnimationBuilder,
BlockerConfig,
BlockerDelegate,
CheckedInputChangeEvent,
Color,
ComponentProps,
ComponentRef,
DomRenderFn,
FrameworkDelegate,
GestureCallback,
GestureConfig,
GestureDelegate,
GestureDetail,
HeaderFn,
InputChangeEvent,
ItemHeightFn,
@@ -2228,200 +2222,6 @@ declare global {
}
declare global {
namespace StencilComponents {
interface IonGestureController {
/**
* Creates a gesture delegate based on the GestureConfig passed
*/
'create': (config: GestureConfig) => Promise<GestureDelegate>;
/**
* Creates a blocker that will block any other gesture events from firing. Set in the ion-gesture component.
*/
'createBlocker': (opts?: BlockerConfig) => BlockerDelegate;
}
}
interface HTMLIonGestureControllerElement extends StencilComponents.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 {
/**
* Event emitted when a gesture has been captured.
*/
'onIonGestureCaptured'?: (event: CustomEvent<string>) => void;
}
}
}
declare global {
namespace StencilComponents {
interface IonGesture {
/**
* What component to attach listeners to.
*/
'attachTo': string | HTMLElement;
/**
* Function to execute to see if gesture can start. Return boolean
*/
'canStart': GestureCallback;
/**
* What direction to listen for gesture changes
*/
'direction': string;
/**
* If true, the current gesture will disabling scrolling interactions
*/
'disableScroll': boolean;
/**
* If true, the current gesture interaction is disabled
*/
'disabled': boolean;
/**
* Name for the gesture action
*/
'gestureName': string;
/**
* What priority the gesture should take. The higher the number, the higher the priority.
*/
'gesturePriority': number;
/**
* The max angle for the gesture
*/
'maxAngle': number;
/**
* Function to execute when the gesture has not been captured
*/
'notCaptured': GestureCallback;
/**
* Function to execute when the gesture has end
*/
'onEnd': GestureCallback;
/**
* Function to execute when the gesture has moved
*/
'onMove': GestureCallback;
/**
* Function to execute when the gesture has start
*/
'onStart': GestureCallback;
/**
* Function to execute when the gesture will start
*/
'onWillStart': (_: GestureDetail) => Promise<void>;
/**
* If the event should use passive event listeners
*/
'passive': boolean;
/**
* How many pixels of change the gesture should wait for before triggering the action.
*/
'threshold': number;
}
}
interface HTMLIonGestureElement extends StencilComponents.IonGesture, HTMLStencilElement {}
var HTMLIonGestureElement: {
prototype: HTMLIonGestureElement;
new (): HTMLIonGestureElement;
};
interface HTMLElementTagNameMap {
'ion-gesture': HTMLIonGestureElement;
}
interface ElementTagNameMap {
'ion-gesture': HTMLIonGestureElement;
}
namespace JSX {
interface IntrinsicElements {
'ion-gesture': JSXElements.IonGestureAttributes;
}
}
namespace JSXElements {
export interface IonGestureAttributes extends HTMLAttributes {
/**
* What component to attach listeners to.
*/
'attachTo'?: string | HTMLElement;
/**
* Function to execute to see if gesture can start. Return boolean
*/
'canStart'?: GestureCallback;
/**
* What direction to listen for gesture changes
*/
'direction'?: string;
/**
* If true, the current gesture will disabling scrolling interactions
*/
'disableScroll'?: boolean;
/**
* If true, the current gesture interaction is disabled
*/
'disabled'?: boolean;
/**
* Name for the gesture action
*/
'gestureName'?: string;
/**
* What priority the gesture should take. The higher the number, the higher the priority.
*/
'gesturePriority'?: number;
/**
* The max angle for the gesture
*/
'maxAngle'?: number;
/**
* Function to execute when the gesture has not been captured
*/
'notCaptured'?: GestureCallback;
/**
* Function to execute when the gesture has end
*/
'onEnd'?: GestureCallback;
/**
* Function to execute when the gesture has moved
*/
'onMove'?: GestureCallback;
/**
* Function to execute when the gesture has start
*/
'onStart'?: GestureCallback;
/**
* Function to execute when the gesture will start
*/
'onWillStart'?: (_: GestureDetail) => Promise<void>;
/**
* If the event should use passive event listeners
*/
'passive'?: boolean;
/**
* How many pixels of change the gesture should wait for before triggering the action.
*/
'threshold'?: number;
}
}
}
declare global {
namespace StencilComponents {

View File

@@ -18,6 +18,7 @@
white-space: nowrap;
user-select: none;
vertical-align: top; // the better option for most scenarios
vertical-align: -webkit-baseline-middle; // the best for those that support it
}

View File

@@ -1,13 +0,0 @@
export { GestureController } from './gesture-controller';
export * from './gesture-controller-utils';
export interface GestureConfig {
name: string;
priority?: number;
disableScroll?: boolean;
}
export interface BlockerConfig {
disable?: string[];
disableScroll?: boolean;
}

View File

@@ -1,107 +0,0 @@
import { GestureController } from '../../interface';
export class GestureDelegate {
private ctrl?: GestureController;
constructor(
ctrl: any,
private id: number,
private name: string,
private priority: number,
private disableScroll: boolean
) {
this.ctrl = ctrl;
}
canStart(): boolean {
if (!this.ctrl) {
return false;
}
return this.ctrl.canStart(this.name);
}
start(): boolean {
if (!this.ctrl) {
return false;
}
return this.ctrl.start(this.name, this.id, this.priority);
}
capture(): boolean {
if (!this.ctrl) {
return false;
}
const captured = this.ctrl.capture(this.name, this.id, this.priority);
if (captured && this.disableScroll) {
this.ctrl.disableScroll(this.id);
}
return captured;
}
release() {
if (this.ctrl) {
this.ctrl.release(this.id);
if (this.disableScroll) {
this.ctrl.enableScroll(this.id);
}
}
}
destroy() {
this.release();
this.ctrl = undefined;
}
}
export class BlockerDelegate {
private ctrl?: GestureController;
constructor(
private id: number,
ctrl: any,
private disable: string[] | undefined,
private disableScroll: boolean
) {
this.ctrl = ctrl;
}
block() {
if (!this.ctrl) {
return;
}
if (this.disable) {
for (const gesture of this.disable) {
this.ctrl.disableGesture(gesture, this.id);
}
}
if (this.disableScroll) {
this.ctrl.disableScroll(this.id);
}
}
unblock() {
if (!this.ctrl) {
return;
}
if (this.disable) {
for (const gesture of this.disable) {
this.ctrl.enableGesture(gesture, this.id);
}
}
if (this.disableScroll) {
this.ctrl.enableScroll(this.id);
}
}
destroy() {
this.unblock();
this.ctrl = undefined;
}
}

View File

@@ -1,31 +0,0 @@
# ion-gesture-controller
Gesture controller is a component for creating a gesture interactions.
It is not meant to be used directly, but with the [Gesture Component](../gesture)
<!-- Auto Generated Below -->
## Events
#### ionGestureCaptured
Event emitted when a gesture has been captured.
## Methods
#### create()
Creates a gesture delegate based on the GestureConfig passed
#### createBlocker()
Creates a blocker that will block any other gesture events from firing. Set in the ion-gesture component.
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -1,344 +0,0 @@
import { GestureController, } from '../gesture-controller';
describe('gesture controller', () => {
it('should create an instance of GestureController', () => {
const c = new GestureController();
expect(c.isCaptured()).toEqual(false);
expect(c.isScrollDisabled()).toEqual(false);
});
it('should test scrolling enable/disable stack', () => {
const c = new GestureController();
c.enableScroll(1);
expect(c.isScrollDisabled()).toEqual(false);
c.disableScroll(1);
expect(c.isScrollDisabled()).toEqual(true);
c.disableScroll(1);
c.disableScroll(1);
expect(c.isScrollDisabled()).toEqual(true);
c.enableScroll(1);
expect(c.isScrollDisabled()).toEqual(false);
for (let i = 0; i < 100; i++) {
for (let j = 0; j < 100; j++) {
c.disableScroll(j);
}
}
for (let i = 0; i < 100; i++) {
expect(c.isScrollDisabled()).toEqual(true);
c.enableScroll(50 - i);
c.enableScroll(i);
}
expect(c.isScrollDisabled()).toEqual(false);
});
it('should test gesture enable/disable stack', () => {
const c = new GestureController();
c.enableGesture('swipe', 1);
expect(c.isDisabled('swipe')).toEqual(false);
c.disableGesture('swipe', 1);
expect(c.isDisabled('swipe')).toEqual(true);
c.disableGesture('swipe', 1);
c.disableGesture('swipe', 1);
expect(c.isDisabled('swipe')).toEqual(true);
c.enableGesture('swipe', 1);
expect(c.isDisabled('swipe')).toEqual(false);
// Disabling gestures multiple times
for (let gestureName = 0; gestureName < 10; gestureName++) {
for (let i = 0; i < 50; i++) {
for (let j = 0; j < 50; j++) {
c.disableGesture(gestureName.toString(), j);
}
}
}
for (let gestureName = 0; gestureName < 10; gestureName++) {
for (let i = 0; i < 49; i++) {
c.enableGesture(gestureName.toString(), i);
}
expect(c.isDisabled(gestureName.toString())).toEqual(true);
c.enableGesture(gestureName.toString(), 49);
expect(c.isDisabled(gestureName.toString())).toEqual(false);
}
});
it('should test if canStart', () => {
const c = new GestureController();
expect(c.canStart('event')).toEqual(true);
expect(c.canStart('event1')).toEqual(true);
expect(c.canStart('event')).toEqual(true);
expect(c['requestedStart'].size).toEqual(0);
expect(c.isCaptured()).toEqual(false);
});
it('should initialize without options', async () => {
const c = new GestureController();
const g = await c.create({
name: 'event',
});
expect(g['name']).toEqual('event');
expect(g['priority']).toEqual(0);
expect(g['disableScroll']).toEqual(false);
expect(g['ctrl']).toEqual(c);
expect(g['id']).toEqual(1);
const g2 = await c.create({ name: 'event2' });
expect(g2['id']).toEqual(2);
});
it('should initialize without options', async () => {
const c = new GestureController();
const g = await c.create({
name: 'event',
});
const g2 = await c.create({
name: 'event2',
});
expect(g['id']).toEqual(1);
expect(g2['id']).toEqual(2);
});
it('should initialize a delegate with options', async () => {
const c = new GestureController();
const g = await c.create({
name: 'swipe',
priority: -123,
disableScroll: true,
});
expect(g['name']).toEqual('swipe');
expect(g['priority']).toEqual(-123);
expect(g['disableScroll']).toEqual(true);
expect(g['ctrl']).toEqual(c);
expect(g['id']).toEqual(1);
});
it('should test if several gestures can be started', async () => {
const c = new GestureController();
const g1 = await c.create({ name: 'swipe' });
const g2 = await c.create({ name: 'swipe1', priority: 3 });
const g3 = await c.create({ name: 'swipe2', priority: 4 });
for (let i = 0; i < 10; i++) {
expect(g1.start()).toEqual(true);
expect(g2.start()).toEqual(true);
expect(g3.start()).toEqual(true);
}
const expected = new Map();
expected.set(1, 0);
expected.set(2, 3);
expected.set(3, 4);
expect(c['requestedStart']).toEqual(expected);
g1.release();
g1.release();
const expected2 = new Map();
expected2.set(2, 3);
expected2.set(3, 4);
expect(c['requestedStart']).toEqual(expected2);
expect(g1.start()).toEqual(true);
expect(g2.start()).toEqual(true);
g3.destroy();
expect(g3['ctrl']).toBeUndefined();
const expected3 = new Map();
expected3.set(1, 0);
expected3.set(2, 3);
expect(c['requestedStart']).toEqual(expected3);
});
it('should test if several gestures try to capture at the same time', async () => {
const c = new GestureController();
const g1 = await c.create({ name: 'swipe1' });
const g2 = await c.create({ name: 'swipe2', priority: 2 });
const g3 = await c.create({ name: 'swipe3', priority: 3 });
const g4 = await c.create({ name: 'swipe4', priority: 4 });
const g5 = await c.create({ name: 'swipe5', priority: 5 });
// Low priority capture() returns false
expect(g2.start()).toEqual(true);
expect(g3.start()).toEqual(true);
expect(g1.capture()).toEqual(false);
const expected = new Map();
expected.set(2, 2);
expected.set(3, 3);
expect(c['requestedStart']).toEqual(expected);
// Low priority start() + capture() returns false
expect(g2.capture()).toEqual(false);
const expected2 = new Map();
expected2.set(3, 3);
expect(c['requestedStart']).toEqual(expected2);
// Higher priority capture() return true
expect(g4.capture()).toEqual(true);
expect(c.isScrollDisabled()).toEqual(false);
expect(c.isCaptured()).toEqual(true);
expect(c['requestedStart']).toEqual(new Map());
// Higher priority can not capture because it is already capture
expect(g5.capture()).toEqual(false);
expect(g5.canStart()).toEqual(false);
expect(g5.start()).toEqual(false);
expect(c['requestedStart']).toEqual(new Map());
// Only captured gesture can release
g1.release();
g2.release();
g3.release();
g5.release();
expect(c.isCaptured()).toEqual(true);
// G4 releases
g4.release();
expect(c.isCaptured()).toEqual(false);
// Once it was release, any gesture can capture
expect(g1.start()).toEqual(true);
expect(g1.capture()).toEqual(true);
});
it('should disable scrolling on capture', async () => {
const c = new GestureController();
const g = await c.create({
name: 'goback',
disableScroll: true,
});
const g1 = await c.create({ name: 'swipe' });
g.start();
expect(c.isScrollDisabled()).toEqual(false);
g1.capture();
g.capture();
expect(c.isScrollDisabled()).toEqual(false);
g1.release();
expect(c.isScrollDisabled()).toEqual(false);
g.capture();
expect(c.isScrollDisabled()).toEqual(true);
g.destroy();
expect(c.isScrollDisabled()).toEqual(false);
});
describe('BlockerDelegate', () => {
it('create one', async () => {
const c = new GestureController();
const b = c.createBlocker({
disableScroll: true,
disable: ['event1', 'event2', 'event3', 'event4']
});
expect(b['disable']).toEqual(['event1', 'event2', 'event3', 'event4']);
expect(b['disableScroll']).toEqual(true);
expect(b['ctrl']).toEqual(c);
expect(b['id']).toEqual(1);
const b2 = c.createBlocker({
disable: ['event2', 'event3', 'event4', 'event5']
});
expect(b2['disable']).toEqual(['event2', 'event3', 'event4', 'event5']);
expect(b2['disableScroll']).toEqual(false);
expect(b2['ctrl']).toEqual(c);
expect(b2['id']).toEqual(2);
expect(c.isDisabled('event1')).toBeFalsy();
expect(c.isDisabled('event2')).toBeFalsy();
expect(c.isDisabled('event3')).toBeFalsy();
expect(c.isDisabled('event4')).toBeFalsy();
expect(c.isDisabled('event5')).toBeFalsy();
b.block();
b.block();
expect(c.isDisabled('event1')).toBeTruthy();
expect(c.isDisabled('event2')).toBeTruthy();
expect(c.isDisabled('event3')).toBeTruthy();
expect(c.isDisabled('event4')).toBeTruthy();
expect(c.isDisabled('event5')).toBeFalsy();
b2.block();
b2.block();
b2.block();
expect(c.isDisabled('event1')).toBeTruthy();
expect(c.isDisabled('event2')).toBeTruthy();
expect(c.isDisabled('event3')).toBeTruthy();
expect(c.isDisabled('event4')).toBeTruthy();
expect(c.isDisabled('event5')).toBeTruthy();
b.unblock();
expect(c.isDisabled('event1')).toBeFalsy();
expect(c.isDisabled('event2')).toBeTruthy();
expect(c.isDisabled('event3')).toBeTruthy();
expect(c.isDisabled('event4')).toBeTruthy();
expect(c.isDisabled('event5')).toBeTruthy();
b2.destroy();
expect(b2['ctrl']).toBeUndefined();
expect(c.isDisabled('event1')).toBeFalsy();
expect(c.isDisabled('event2')).toBeFalsy();
expect(c.isDisabled('event3')).toBeFalsy();
expect(c.isDisabled('event4')).toBeFalsy();
expect(c.isDisabled('event5')).toBeFalsy();
});
it('should disable some events', async () => {
const c = new GestureController();
const goback = await c.create({ name: 'goback' });
expect(goback.canStart()).toEqual(true);
const g2 = await c.create({ name: 'goback2' });
expect(g2.canStart()).toEqual(true);
const g3 = c.createBlocker({
disable: ['range', 'goback', 'something']
});
const g4 = c.createBlocker({
disable: ['range']
});
g3.block();
g4.block();
// goback is disabled
expect(c.isDisabled('range')).toEqual(true);
expect(c.isDisabled('goback')).toEqual(true);
expect(c.isDisabled('something')).toEqual(true);
expect(c.isDisabled('goback2')).toEqual(false);
expect(goback.canStart()).toEqual(false);
expect(goback.start()).toEqual(false);
expect(goback.capture()).toEqual(false);
// Once g3 is destroyed, goback and something should be enabled
g3.destroy();
expect(c.isDisabled('range')).toEqual(true);
expect(c.isDisabled('goback')).toEqual(false);
expect(c.isDisabled('something')).toEqual(false);
// Once g4 is destroyed, range is also enabled
g4.unblock();
expect(c.isDisabled('range')).toEqual(false);
});
});
});

View File

@@ -1,18 +0,0 @@
export interface GestureDetail {
type: string;
startX: number;
startY: number;
startTimeStamp: number;
currentX: number;
currentY: number;
velocityX: number;
velocityY: number;
deltaX: number;
deltaY: number;
timeStamp: number;
event: UIEvent;
data?: any;
}
export type GestureCallback = (detail?: GestureDetail) => boolean | void;

View File

@@ -1,498 +0,0 @@
import { Component, EventListenerEnable, Listen, Prop, QueueApi, Watch } from '@stencil/core';
import { GestureCallback, GestureDelegate, GestureDetail } from '../../interface';
import { assert, now } from '../../utils/helpers';
import { PanRecognizer } from './recognizers';
@Component({
tag: 'ion-gesture'
})
export class Gesture {
private detail: GestureDetail;
private positions: number[] = [];
private gesture?: GestureDelegate;
private lastTouch = 0;
private pan!: PanRecognizer;
private hasCapturedPan = false;
private hasStartedPan = false;
private hasFiredStart = true;
private isMoveQueued = false;
@Prop({ connect: 'ion-gesture-controller' }) gestureCtrl!: HTMLIonGestureControllerElement;
@Prop({ context: 'queue' }) queue!: QueueApi;
@Prop({ context: 'enableListener' }) enableListener!: EventListenerEnable;
@Prop({ context: 'isServer' }) isServer!: boolean;
/**
* If true, the current gesture interaction is disabled
*/
@Prop() disabled = false;
/**
* What component to attach listeners to.
*/
@Prop() attachTo: string | HTMLElement = 'child';
/**
* If true, the current gesture will disabling scrolling interactions
*/
@Prop() disableScroll = false;
/**
* What direction to listen for gesture changes
*/
@Prop() direction = 'x';
/**
* Name for the gesture action
*/
@Prop() gestureName = '';
/**
* What priority the gesture should take. The higher the number, the higher the priority.
*/
@Prop() gesturePriority = 0;
/**
* If the event should use passive event listeners
*/
@Prop() passive = true;
/**
* The max angle for the gesture
*/
@Prop() maxAngle = 40;
/**
* How many pixels of change the gesture should wait for before triggering the action.
*/
@Prop() threshold = 10;
/**
* Function to execute to see if gesture can start. Return boolean
*/
@Prop() canStart?: GestureCallback;
/**
* Function to execute when the gesture will start
*/
@Prop() onWillStart?: (_: GestureDetail) => Promise<void>;
/**
* Function to execute when the gesture has start
*/
@Prop() onStart?: GestureCallback;
/**
* Function to execute when the gesture has moved
*/
@Prop() onMove?: GestureCallback;
/**
* Function to execute when the gesture has end
*/
@Prop() onEnd?: GestureCallback;
/**
* Function to execute when the gesture has not been captured
*/
@Prop() notCaptured?: GestureCallback;
constructor() {
this.detail = {
type: 'pan',
startX: 0,
startY: 0,
startTimeStamp: 0,
currentX: 0,
currentY: 0,
velocityX: 0,
velocityY: 0,
deltaX: 0,
deltaY: 0,
timeStamp: 0,
event: undefined as any,
data: undefined
};
}
async componentWillLoad() {
if (this.isServer) {
return;
}
this.gesture = await this.gestureCtrl.create({
name: this.gestureName,
priority: this.gesturePriority,
disableScroll: this.disableScroll
});
}
componentDidLoad() {
if (this.isServer) {
return;
}
// 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
// only create one instance of GestureController, and reuse the same one later
this.pan = new PanRecognizer(this.direction, this.threshold, this.maxAngle);
this.disabledChanged(this.disabled);
}
componentDidUnload() {
if (this.gesture) {
this.gesture.destroy();
}
}
@Watch('disabled')
protected disabledChanged(isDisabled: boolean) {
this.enableListener(
this,
'touchstart',
!isDisabled,
this.attachTo,
this.passive
);
this.enableListener(
this,
'mousedown',
!isDisabled,
this.attachTo,
this.passive
);
if (isDisabled) {
this.abortGesture();
}
}
// DOWN *************************
@Listen('touchstart', { passive: true, enabled: false })
onTouchStart(ev: TouchEvent) {
this.lastTouch = now(ev);
if (this.pointerDown(ev, this.lastTouch)) {
this.enableMouse(false);
this.enableTouch(true);
} else {
this.abortGesture();
}
}
@Listen('mousedown', { passive: true, enabled: false })
onMouseDown(ev: MouseEvent) {
const timeStamp = now(ev);
if (this.lastTouch === 0 || this.lastTouch + MOUSE_WAIT < timeStamp) {
if (this.pointerDown(ev, timeStamp)) {
this.enableMouse(true);
this.enableTouch(false);
} else {
this.abortGesture();
}
}
}
private pointerDown(ev: UIEvent, timeStamp: number): boolean {
if (!this.gesture || this.hasStartedPan || !this.hasFiredStart) {
return false;
}
const detail = this.detail;
updateDetail(ev, detail);
detail.startX = detail.currentX;
detail.startY = detail.currentY;
detail.startTimeStamp = detail.timeStamp = timeStamp;
detail.velocityX = detail.velocityY = detail.deltaX = detail.deltaY = 0;
detail.event = ev;
this.positions.length = 0;
assert(this.hasFiredStart, 'fired start must be false');
assert(!this.hasStartedPan, 'pan can be started at this point');
assert(!this.hasCapturedPan, 'pan can be started at this point');
assert(!this.isMoveQueued, 'some move is still queued');
assert(this.positions.length === 0, 'positions must be emprty');
// Check if gesture can start
if (this.canStart && this.canStart(detail) === false) {
return false;
}
// Release fallback
this.gesture.release();
// Start gesture
if (!this.gesture.start()) {
return false;
}
this.positions.push(detail.currentX, detail.currentY, timeStamp);
this.hasStartedPan = true;
if (this.threshold === 0) {
return this.tryToCapturePan();
}
this.pan.start(detail.startX, detail.startY);
return true;
}
// MOVE *************************
@Listen('touchmove', { passive: true, enabled: false })
onTouchMove(ev: TouchEvent) {
this.lastTouch = this.detail.timeStamp = now(ev);
this.pointerMove(ev);
}
@Listen('document:mousemove', { passive: true, enabled: false })
onMoveMove(ev: TouchEvent) {
const timeStamp = now(ev);
if (this.lastTouch === 0 || this.lastTouch + MOUSE_WAIT < timeStamp) {
this.detail.timeStamp = timeStamp;
this.pointerMove(ev);
}
}
private pointerMove(ev: UIEvent) {
// fast path, if gesture is currently captured
// do minimun job to get user-land even dispatched
if (this.hasCapturedPan) {
if (!this.isMoveQueued && this.hasFiredStart) {
this.isMoveQueued = true;
this.calcGestureData(ev);
this.queue.write(this.fireOnMove.bind(this));
}
return;
}
// gesture is currently being detected
const detail = this.detail;
this.calcGestureData(ev);
if (this.pan.detect(detail.currentX, detail.currentY)) {
if (this.pan.isGesture()) {
if (!this.tryToCapturePan()) {
this.abortGesture();
}
}
}
}
private fireOnMove() {
// Since fireOnMove is called inside a RAF, onEnd() might be called,
// we must double check hasCapturedPan
if (!this.hasCapturedPan) {
return;
}
const detail = this.detail;
this.isMoveQueued = false;
if (this.onMove) {
this.onMove(detail);
}
}
private calcGestureData(ev: UIEvent) {
const detail = this.detail;
updateDetail(ev, detail);
const currentX = detail.currentX;
const currentY = detail.currentY;
const timestamp = detail.timeStamp;
detail.deltaX = currentX - detail.startX;
detail.deltaY = currentY - detail.startY;
detail.event = ev;
const timeRange = timestamp - 100;
const positions = this.positions;
let startPos = positions.length - 1;
// move pointer to position measured 100ms ago
while (startPos > 0 && positions[startPos] > timeRange) {
startPos -= 3;
}
if (startPos > 1) {
// compute relative movement between these two points
const frequency = 1 / (positions[startPos] - timestamp);
const movedY = positions[startPos - 1] - currentY;
const movedX = positions[startPos - 2] - currentX;
// based on XXms compute the movement to apply for each render step
// velocity = space/time = s*(1/t) = s*frequency
detail.velocityX = movedX * frequency;
detail.velocityY = movedY * frequency;
} else {
detail.velocityX = 0;
detail.velocityY = 0;
}
positions.push(currentX, currentY, timestamp);
}
private tryToCapturePan(): boolean {
if (this.gesture && !this.gesture.capture()) {
return false;
}
this.hasCapturedPan = true;
this.hasFiredStart = false;
// reset start position since the real user-land event starts here
// If the pan detector threshold is big, not reseting the start position
// will cause a jump in the animation equal to the detector threshold.
// the array of positions used to calculate the gesture velocity does not
// need to be cleaned, more points in the positions array always results in a
// more acurate value of the velocity.
const detail = this.detail;
detail.startX = detail.currentX;
detail.startY = detail.currentY;
detail.startTimeStamp = detail.timeStamp;
if (this.onWillStart) {
this.onWillStart(this.detail).then(this.fireOnStart.bind(this));
} else {
this.fireOnStart();
}
return true;
}
private fireOnStart() {
assert(!this.hasFiredStart, 'has fired must be false');
if (this.onStart) {
this.onStart(this.detail);
}
this.hasFiredStart = true;
}
private abortGesture() {
this.reset();
this.enable(false);
if (this.notCaptured) {
this.notCaptured(this.detail);
}
}
private reset() {
this.hasCapturedPan = false;
this.hasStartedPan = false;
this.isMoveQueued = false;
this.hasFiredStart = true;
if (this.gesture) {
this.gesture.release();
}
}
// END *************************
@Listen('touchcancel', { passive: true, enabled: false })
@Listen('touchend', { passive: true, enabled: false })
onTouchCancel(ev: TouchEvent) {
this.lastTouch = this.detail.timeStamp = now(ev);
this.pointerUp(ev);
this.enableTouch(false);
}
@Listen('document:mouseup', { passive: true, enabled: false })
onMouseUp(ev: TouchEvent) {
const timeStamp = now(ev);
if (this.lastTouch === 0 || this.lastTouch + MOUSE_WAIT < timeStamp) {
this.detail.timeStamp = timeStamp;
this.pointerUp(ev);
this.enableMouse(false);
}
}
private pointerUp(ev: UIEvent) {
const hasCaptured = this.hasCapturedPan;
const hasFiredStart = this.hasFiredStart;
this.reset();
if (!hasFiredStart) {
return;
}
const detail = this.detail;
this.calcGestureData(ev);
// Try to capture press
if (hasCaptured) {
if (this.onEnd) {
this.onEnd(detail);
}
return;
}
// Not captured any event
if (this.notCaptured) {
this.notCaptured(detail);
}
}
// ENABLE LISTENERS *************************
private enableMouse(shouldEnable: boolean) {
this.enableListener(
this,
'document:mousemove',
shouldEnable,
undefined,
this.passive
);
this.enableListener(
this,
'document:mouseup',
shouldEnable,
undefined,
this.passive
);
}
private enableTouch(shouldEnable: boolean) {
this.enableListener(
this,
'touchmove',
shouldEnable,
this.attachTo,
this.passive
);
this.enableListener(
this,
'touchcancel',
shouldEnable,
this.attachTo,
this.passive
);
this.enableListener(
this,
'touchend',
shouldEnable,
this.attachTo,
this.passive
);
}
private enable(shouldEnable: boolean) {
this.enableMouse(shouldEnable);
this.enableTouch(shouldEnable);
}
}
const MOUSE_WAIT = 2500;
function updateDetail(ev: any, detail: GestureDetail) {
// get X coordinates for either a mouse click
// or a touch depending on the given event
let x = 0;
let y = 0;
if (ev) {
const changedTouches = ev.changedTouches;
if (changedTouches && changedTouches.length > 0) {
const touch = changedTouches[0];
x = touch.clientX;
y = touch.clientY;
} else if (ev.pageX !== undefined) {
x = ev.pageX;
y = ev.pageY;
}
}
detail.currentX = x;
detail.currentY = y;
}

View File

@@ -1,227 +0,0 @@
# ion-gesture
Gesture is a component that can be used to add gesture based interaction to it's child content.
The component properties can accept methods that will fire when the property is triggered.
<!-- Auto Generated Below -->
## Properties
#### attachTo
string
What component to attach listeners to.
#### canStart
GestureCallback
Function to execute to see if gesture can start. Return boolean
#### direction
string
What direction to listen for gesture changes
#### disableScroll
boolean
If true, the current gesture will disabling scrolling interactions
#### disabled
boolean
If true, the current gesture interaction is disabled
#### gestureName
string
Name for the gesture action
#### gesturePriority
number
What priority the gesture should take. The higher the number, the higher the priority.
#### maxAngle
number
The max angle for the gesture
#### notCaptured
GestureCallback
Function to execute when the gesture has not been captured
#### onEnd
GestureCallback
Function to execute when the gesture has end
#### onMove
GestureCallback
Function to execute when the gesture has moved
#### onStart
GestureCallback
Function to execute when the gesture has start
#### onWillStart
(_: GestureDetail) => Promise<void>
Function to execute when the gesture will start
#### passive
boolean
If the event should use passive event listeners
#### threshold
number
How many pixels of change the gesture should wait for before triggering the action.
## Attributes
#### attach-to
string
What component to attach listeners to.
#### can-start
Function to execute to see if gesture can start. Return boolean
#### direction
string
What direction to listen for gesture changes
#### disable-scroll
boolean
If true, the current gesture will disabling scrolling interactions
#### disabled
boolean
If true, the current gesture interaction is disabled
#### gesture-name
string
Name for the gesture action
#### gesture-priority
number
What priority the gesture should take. The higher the number, the higher the priority.
#### max-angle
number
The max angle for the gesture
#### not-captured
Function to execute when the gesture has not been captured
#### on-end
Function to execute when the gesture has end
#### on-move
Function to execute when the gesture has moved
#### on-start
Function to execute when the gesture has start
#### on-will-start
Function to execute when the gesture will start
#### passive
boolean
If the event should use passive event listeners
#### threshold
number
How many pixels of change the gesture should wait for before triggering the action.
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -10,6 +10,8 @@ ion-item-sliding {
width: 100%;
overflow: hidden;
user-select: none;
}
.item-sliding-active-slide .item {

View File

@@ -1,6 +1,6 @@
import { Component, Element, Event, EventEmitter, Method, State } from '@stencil/core';
import { Component, Element, Event, EventEmitter, Method, Prop, QueueApi, State } from '@stencil/core';
import { GestureDetail } from '../../interface';
import { Gesture, GestureDetail } from '../../interface';
const SWIPE_MARGIN = 30;
const ELASTIC_FACTOR = 0.55;
@@ -39,24 +39,44 @@ export class ItemSliding {
private leftOptions?: HTMLIonItemOptionsElement;
private rightOptions?: HTMLIonItemOptionsElement;
private optsDirty = true;
private gesture?: Gesture;
@Element() el!: HTMLIonItemSlidingElement;
@State() private state: SlidingState = SlidingState.Disabled;
@Prop({ context: 'queue' }) queue!: QueueApi;
/**
* Emitted when the sliding position changes.
*/
@Event() ionDrag!: EventEmitter;
componentDidLoad() {
async componentDidLoad() {
this.item = this.el.querySelector('ion-item');
this.list = this.el.closest('ion-list');
this.updateOptions();
this.gesture = (await import('../../utils/gesture/gesture')).create({
el: this.el,
queue: this.queue,
gestureName: 'item-swipe',
gesturePriority: -10,
threshold: 5,
canStart: this.canStart.bind(this),
onStart: this.onDragStart.bind(this),
onMove: this.onDragMove.bind(this),
onEnd: this.onDragEnd.bind(this),
});
this.gesture.disabled = false;
}
componentDidUnload() {
if (this.gesture) {
this.gesture.destroy();
}
this.item = this.list = null;
this.leftOptions = this.rightOptions = undefined;
}
@@ -272,24 +292,6 @@ export class ItemSliding {
}
};
}
render() {
return (
<ion-gesture
canStart={this.canStart.bind(this)}
onStart={this.onDragStart.bind(this)}
onMove={this.onDragMove.bind(this)}
onEnd={this.onDragEnd.bind(this)}
gestureName={'item-swipe'}
gesturePriority={-10}
direction={'x'}
maxAngle={20}
threshold={5}
attachTo={'parent'}>
<slot></slot>
</ion-gesture>
);
}
}
/** @hidden */

View File

@@ -1,6 +1,6 @@
import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Method, Prop, State, Watch } from '@stencil/core';
import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Method, Prop, QueueApi, State, Watch } from '@stencil/core';
import { Animation, Config, GestureDetail, MenuChangeEventDetail, Mode, Side } from '../../interface';
import { Animation, Config, Gesture, GestureDetail, MenuChangeEventDetail, Mode, Side } from '../../interface';
import { assert, isEndSide as isEnd } from '../../utils/helpers';
@Component({
@@ -12,9 +12,11 @@ import { assert, isEndSide as isEnd } from '../../utils/helpers';
shadow: true
})
export class Menu {
private animation?: Animation;
private _isOpen = false;
private lastOnEnd = 0;
private gesture?: Gesture;
mode!: Mode;
@@ -36,6 +38,8 @@ export class Menu {
@Prop({ connect: 'ion-menu-controller' }) lazyMenuCtrl!: HTMLIonMenuControllerElement;
@Prop({ context: 'enableListener' }) enableListener!: EventListenerEnable;
@Prop({ context: 'window' }) win!: Window;
@Prop({ context: 'queue' }) queue!: QueueApi;
@Prop({ context: 'document' }) doc!: Document;
/**
* The content's id the menu should use.
@@ -74,9 +78,13 @@ export class Menu {
@Prop({ mutable: true }) disabled = false;
@Watch('disabled')
protected disabledChanged(disabled: boolean) {
protected disabledChanged() {
this.updateState();
this.ionMenuChange.emit({ disabled, open: this._isOpen });
this.ionMenuChange.emit({
disabled: this.disabled,
open: this._isOpen
});
}
/**
@@ -130,7 +138,7 @@ export class Menu {
}
}
componentDidLoad() {
async componentDidLoad() {
if (this.isServer) {
return;
}
@@ -167,8 +175,22 @@ export class Menu {
this.menuCtrl!._register(this);
this.ionMenuChange.emit({ disabled: !isEnabled, open: this._isOpen });
this.gesture = (await import('../../utils/gesture/gesture')).create({
el: this.doc,
queue: this.queue,
gestureName: 'menu-swipe',
gesturePriority: 10,
threshold: 10,
canStart: this.canStart.bind(this),
onWillStart: this.onWillStart.bind(this),
onStart: this.onDragStart.bind(this),
onMove: this.onDragMove.bind(this),
onEnd: this.onDragEnd.bind(this),
});
// mask it as enabled / disabled
this.disabled = !isEnabled;
this.updateState();
}
componentDidUnload() {
@@ -176,6 +198,9 @@ export class Menu {
if (this.animation) {
this.animation.destroy();
}
if (this.gesture) {
this.gesture.destroy();
}
this.animation = undefined;
this.contentEl = this.backdropEl = this.menuInnerEl = undefined;
@@ -413,6 +438,9 @@ export class Menu {
private updateState() {
const isActive = this.isActive();
if (this.gesture) {
this.gesture.disabled = !isActive || !this.swipeEnabled;
}
// Close menu inmediately
if (!isActive && this._isOpen) {
@@ -463,21 +491,6 @@ export class Menu {
class="menu-backdrop"
tappable={false}
stopPropagation={false}
/>,
<ion-gesture
canStart={this.canStart.bind(this)}
onWillStart={this.onWillStart.bind(this)}
onStart={this.onDragStart.bind(this)}
onMove={this.onDragMove.bind(this)}
onEnd={this.onDragEnd.bind(this)}
disabled={!this.isActive() || !this.swipeEnabled}
gestureName="menu-swipe"
gesturePriority={10}
direction="x"
threshold={10}
attachTo="window"
disableScroll={true}
/>
];
}

View File

@@ -1,7 +1,7 @@
import { Build, Component, Element, Event, EventEmitter, Method, Prop, QueueApi, Watch } from '@stencil/core';
import { ViewLifecycle } from '../..';
import { Animation, ComponentProps, Config, FrameworkDelegate, GestureDetail, Mode, NavComponent, NavOptions, NavOutlet, NavResult, RouteID, RouteWrite, TransitionDoneFn, TransitionInstruction, ViewController } from '../../interface';
import { Animation, ComponentProps, Config, FrameworkDelegate, Gesture, GestureDetail, Mode, NavComponent, NavOptions, NavOutlet, NavResult, RouteID, RouteWrite, TransitionDoneFn, TransitionInstruction, ViewController } from '../../interface';
import { assert } from '../../utils/helpers';
import { TransitionOptions, lifecycle, setPageHidden, transition } from '../../utils/transition';
@@ -13,12 +13,14 @@ import { ViewState, convertToViews, matches } from './view-controller';
shadow: true
})
export class Nav implements NavOutlet {
private transInstr: TransitionInstruction[] = [];
private sbTrns?: Animation;
private useRouter = false;
private isTransitioning = false;
private destroyed = false;
private views: ViewController[] = [];
private gesture?: Gesture;
mode!: Mode;
@@ -36,6 +38,12 @@ export class Nav implements NavOutlet {
* If the nav component should allow for swipe-to-go-back
*/
@Prop({ mutable: true }) swipeBackEnabled?: boolean;
@Watch('swipeBackEnabled')
swipeBackEnabledChanged() {
if (this.gesture) {
this.gesture.disabled = !this.swipeBackEnabled;
}
}
/**
* If the nav should animate the components or not
@@ -86,6 +94,7 @@ export class Nav implements NavOutlet {
this.useRouter =
!!this.win.document.querySelector('ion-router') &&
!this.el.closest('[no-router]');
if (this.swipeBackEnabled === undefined) {
this.swipeBackEnabled = this.config.getBoolean(
'swipeBackEnabled',
@@ -98,8 +107,21 @@ export class Nav implements NavOutlet {
this.ionNavWillLoad.emit();
}
componentDidLoad() {
async componentDidLoad() {
this.rootChanged();
this.gesture = (await import('../../utils/gesture/gesture')).create({
el: this.win.document.body,
queue: this.queue,
gestureName: 'goback-swipe',
gesturePriority: 10,
threshold: 10,
canStart: this.canSwipeBack.bind(this),
onStart: this.swipeBackStart.bind(this),
onMove: this.swipeBackProgress.bind(this),
onEnd: this.swipeBackEnd.bind(this),
});
this.swipeBackEnabledChanged();
}
componentDidUnload() {
@@ -108,6 +130,10 @@ export class Nav implements NavOutlet {
view._destroy();
}
if (this.gesture) {
this.gesture.destroy();
}
// release swipe back gesture and transition
if (this.sbTrns) {
this.sbTrns.destroy();
@@ -933,19 +959,6 @@ export class Nav implements NavOutlet {
render() {
return [
this.swipeBackEnabled && (
<ion-gesture
canStart={this.canSwipeBack.bind(this)}
onStart={this.swipeBackStart.bind(this)}
onMove={this.swipeBackProgress.bind(this)}
onEnd={this.swipeBackEnd.bind(this)}
gestureName="goback-swipe"
gesturePriority={10}
direction="x"
threshold={10}
attachTo="body"
/>
),
this.mode === 'ios' && <div class="nav-decor" />,
<slot></slot>
];

View File

@@ -1,6 +1,6 @@
import { Component, Element, Prop, QueueApi } from '@stencil/core';
import { GestureDetail, Mode, PickerColumn, PickerColumnOption } from '../../interface';
import { Gesture, GestureDetail, Mode, PickerColumn, PickerColumnOption } from '../../interface';
import { hapticSelectionChanged } from '../../utils';
import { clamp } from '../../utils/helpers';
import { createThemedClasses } from '../../utils/theme';
@@ -24,6 +24,7 @@ export class PickerColumnCmp {
private startY?: number;
private velocity = 0;
private y = 0;
private gesture?: Gesture;
@Element() el!: HTMLElement;
@@ -44,7 +45,7 @@ export class PickerColumnCmp {
this.scaleFactor = pickerScaleFactor;
}
componentDidLoad() {
async componentDidLoad() {
// get the scrollable element within the column
const colEl = this.el.querySelector('.picker-opts')!;
@@ -52,6 +53,19 @@ export class PickerColumnCmp {
this.optHeight = (colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0);
this.refresh();
this.gesture = (await import('../../utils/gesture/gesture')).create({
el: this.el,
queue: this.queue,
gestureName: 'picker-swipe',
gesturePriority: 10,
threshold: 0,
canStart: this.canStart.bind(this),
onStart: this.onDragStart.bind(this),
onMove: this.onDragMove.bind(this),
onEnd: this.onDragEnd.bind(this),
});
this.gesture.disabled = false;
}
private optClick(ev: Event, index: number) {
@@ -397,7 +411,7 @@ export class PickerColumnCmp {
}
return o;
})
.filter(clientInformation => clientInformation !== null);
.filter(o => o !== null);
const results: JSX.Element[] = [];
@@ -410,18 +424,6 @@ export class PickerColumnCmp {
}
results.push(
<ion-gesture
canStart={this.canStart.bind(this)}
onStart={this.onDragStart.bind(this)}
onMove={this.onDragMove.bind(this)}
onEnd={this.onDragEnd.bind(this)}
gestureName="picker-swipe"
gesturePriority={10}
direction="y"
passive={false}
threshold={0}
attachTo="parent"
></ion-gesture>,
<div class="picker-opts" style={{ maxWidth: col.optionsWidth! }}>
{options.map((o, index) =>
<button

View File

@@ -22,7 +22,6 @@
font-size: 24px;
}
ion-gesture,
.range-slider {
position: relative;

View File

@@ -1,6 +1,6 @@
import { Component, Element, Event, EventEmitter, Listen, Prop, State, Watch } from '@stencil/core';
import { Component, Element, Event, EventEmitter, Listen, Prop, QueueApi, State, Watch } from '@stencil/core';
import { BaseInput, Color, GestureDetail, Mode, RangeInputChangeEvent, StyleEvent } from '../../interface';
import { BaseInput, Color, Gesture, GestureDetail, Mode, RangeInputChangeEvent, StyleEvent } from '../../interface';
import { clamp, debounceEvent, deferEvent } from '../../utils/helpers';
import { createColorClasses, hostContext } from '../../utils/theme';
@@ -20,9 +20,12 @@ export class Range implements BaseInput {
private rect!: ClientRect;
private hasFocus = false;
private rangeSlider?: HTMLElement;
private gesture?: Gesture;
@Element() el!: HTMLStencilElement;
@Prop({ context: 'queue' }) queue!: QueueApi;
@State() private ratioA = 0;
@State() private ratioB = 0;
@State() private pressedKnob: Knob;
@@ -94,6 +97,9 @@ export class Range implements BaseInput {
@Prop() disabled = false;
@Watch('disabled')
protected disabledChanged() {
if (this.gesture) {
this.gesture.disabled = this.disabled;
}
this.emitStyle();
}
@@ -138,6 +144,20 @@ export class Range implements BaseInput {
this.emitStyle();
}
async componentDidLoad() {
this.gesture = (await import('../../utils/gesture/gesture')).create({
el: this.rangeSlider!,
queue: this.queue,
gestureName: 'range',
gesturePriority: 30,
threshold: 0,
onStart: this.onDragStart.bind(this),
onMove: this.onDragMove.bind(this),
onEnd: this.onDragEnd.bind(this),
});
this.gesture.disabled = this.disabled;
}
@Listen('ionIncrease')
@Listen('ionDecrease')
keyChng(ev: CustomEvent<RangeEventDetail>) {
@@ -323,61 +343,49 @@ export class Range implements BaseInput {
return [
<slot name="start"></slot>,
<ion-gesture
disableScroll={true}
onStart={this.onDragStart.bind(this)}
onMove={this.onDragMove.bind(this)}
onEnd={this.onDragEnd.bind(this)}
disabled={this.disabled}
gestureName="range"
gesturePriority={30}
direction="x"
threshold={0}
>
<div class="range-slider" ref={el => this.rangeSlider = el}>
{ticks.map(t => (
<div
style={{ left: t.left }}
role="presentation"
class={{
'range-tick': true,
'range-tick-active': t.active
}}
/>
))}
<div class="range-bar" role="presentation" />
<div class="range-slider" ref={el => this.rangeSlider = el}>
{ticks.map(t => (
<div
class="range-bar range-bar-active"
style={{ left: t.left }}
role="presentation"
style={{
left: barL,
right: barR
class={{
'range-tick': true,
'range-tick-active': t.active
}}
/>
))}
<div class="range-bar" role="presentation" />
<div
class="range-bar range-bar-active"
role="presentation"
style={{
left: barL,
right: barR
}}
/>
<ion-range-knob
knob="A"
pressed={this.pressedKnob === 'A'}
value={this.valA}
ratio={this.ratioA}
pin={this.pin}
min={min}
max={max}
/>
{this.dualKnobs && (
<ion-range-knob
knob="A"
pressed={this.pressedKnob === 'A'}
value={this.valA}
ratio={this.ratioA}
knob="B"
pressed={this.pressedKnob === 'B'}
value={this.valB}
ratio={this.ratioB}
pin={this.pin}
min={min}
max={max}
/>
{this.dualKnobs && (
<ion-range-knob
knob="B"
pressed={this.pressedKnob === 'B'}
value={this.valB}
ratio={this.ratioB}
pin={this.pin}
min={min}
max={max}
/>
)}
</div>
</ion-gesture>,
)}
</div>,
<slot name="end"></slot>
];
}

View File

@@ -1,19 +1,8 @@
import { Component, Element, Event, EventEmitter, Method, Prop, QueueApi, State } from '@stencil/core';
import { Component, Element, Event, EventEmitter, Method, Prop, QueueApi, State, Watch } from '@stencil/core';
import { GestureDetail, Mode } from '../../interface';
import { Gesture, GestureDetail, Mode } from '../../interface';
import { createThemedClasses } from '../../utils/theme';
const enum RefresherState {
Inactive = 1 << 0,
Pulling = 1 << 1,
Ready = 1 << 2,
Refreshing = 1 << 3,
Cancelling = 1 << 4,
Completing = 1 << 5,
_BUSY_ = Refreshing | Cancelling | Completing,
}
@Component({
tag: 'ion-refresher',
styleUrls: {
@@ -27,6 +16,7 @@ export class Refresher {
private didStart = false;
private progress = 0;
private scrollEl?: HTMLIonScrollElement;
private gesture?: Gesture;
mode!: Mode;
@@ -40,7 +30,7 @@ export class Refresher {
* - `cancelling` - The user pulled down the refresher and let go, but did not pull down far enough to kick off the `refreshing` state. After letting go, the refresher is in the `cancelling` state while it is closing, and will go back to the `inactive` state once closed.
* - `ready` - The user has pulled down the refresher far enough that if they let go, it'll begin the `refreshing` state.
* - `refreshing` - The refresher is actively waiting on the async operation to end. Once the refresh handler calls `complete()` it will begin the `completing` state.
* - `completing` - The `refreshing` state has finished and the refresher is in the process of closing itself. Once closed, the refresher will go back to the `inactive` state.
* - `completing` - The `refreshing` state has finished and the refresher is in the way of closing itself. Once closed, the refresher will go back to the `inactive` state.
*/
@State() private state: RefresherState = RefresherState.Inactive;
@@ -74,6 +64,12 @@ export class Refresher {
* If true, the refresher will be hidden. Defaults to `false`.
*/
@Prop() disabled = false;
@Watch('disabled')
disabledChanged() {
if (this.gesture) {
this.gesture.disabled = this.disabled;
}
}
/**
* Emitted when the user lets go of the content and has pulled down
@@ -105,6 +101,22 @@ export class Refresher {
} else {
console.error('ion-refresher did not attach, make sure the parent is an ion-content.');
}
this.gesture = (await import('../../utils/gesture/gesture')).create({
el: this.el.closest('ion-content') as any,
queue: this.queue,
gestureName: 'refresher',
gesturePriority: 10,
direction: 'y',
threshold: 5,
passive: false,
canStart: this.canStart.bind(this),
onStart: this.onStart.bind(this),
onMove: this.onMove.bind(this),
onEnd: this.onEnd.bind(this),
});
this.disabledChanged();
}
componentDidUnload() {
@@ -181,7 +193,7 @@ export class Refresher {
}
// do nothing if it's actively refreshing
// or it's in the process of closing
// or it's in the way of closing
// or this was never a startY
if (this.state & RefresherState._BUSY_) {
return 2;
@@ -341,21 +353,15 @@ export class Refresher {
}
};
}
render() {
return <ion-gesture
canStart={this.canStart.bind(this)}
onStart={this.onStart.bind(this)}
onMove={this.onMove.bind(this)}
onEnd={this.onEnd.bind(this)}
gestureName="refresher"
gesturePriority={10}
passive={false}
direction="y"
threshold={5}
attachTo={this.el.closest('ion-content') as any}
disabled={this.disabled}>
<slot></slot>
</ion-gesture>;
}
}
const enum RefresherState {
Inactive = 1 << 0,
Pulling = 1 << 1,
Ready = 1 << 2,
Refreshing = 1 << 3,
Cancelling = 1 << 4,
Completing = 1 << 5,
_BUSY_ = Refreshing | Cancelling | Completing,
}

View File

@@ -3,11 +3,7 @@
// Reorder Group
// --------------------------------------------------
.reorder-group > ion-gesture {
display: block;
}
.reorder-list-active ion-gesture > * {
.reorder-list-active > * {
transition: transform 300ms;
will-change: transform;
}

View File

@@ -1,6 +1,6 @@
import { Component, Element, Prop, QueueApi, State } from '@stencil/core';
import { Component, Element, Prop, QueueApi, State, Watch } from '@stencil/core';
import { GestureDetail, Mode } from '../../interface';
import { Gesture, GestureDetail, Mode } from '../../interface';
import { hapticSelectionChanged, hapticSelectionEnd, hapticSelectionStart } from '../../utils/haptic';
import { createThemedClasses } from '../../utils/theme';
@@ -14,8 +14,8 @@ export class ReorderGroup {
private selectedItemHeight!: number;
private lastToIndex!: number;
private cachedHeights: number[] = [];
private containerEl!: HTMLElement;
private scrollEl?: HTMLIonScrollElement;
private gesture?: Gesture;
private scrollElTop = 0;
private scrollElBottom = 0;
@@ -31,19 +31,41 @@ export class ReorderGroup {
@Element() el!: HTMLElement;
@Prop({ context: 'queue' }) queue!: QueueApi;
@Prop({ context: 'document' }) doc!: Document;
/**
* If true, the reorder will be hidden. Defaults to `true`.
*/
@Prop() disabled = true;
@Watch('disabled')
disabledChanged() {
if (this.gesture) {
this.gesture.disabled = this.disabled;
}
}
async componentDidLoad() {
this.containerEl = this.el.querySelector('ion-gesture')!;
const contentEl = this.el.closest('ion-content');
if (contentEl) {
await contentEl.componentOnReady();
this.scrollEl = contentEl.getScrollElement();
}
this.gesture = (await import('../../utils/gesture/gesture')).create({
el: this.doc.body,
queue: this.queue,
gestureName: 'reorder',
gesturePriority: 30,
disableScroll: true,
threshold: 0,
direction: 'y',
passive: false,
canStart: this.canStart.bind(this),
onStart: this.onDragStart.bind(this),
onMove: this.onDragMove.bind(this),
onEnd: this.onDragEnd.bind(this),
});
this.disabledChanged();
}
componentDidUnload() {
@@ -59,7 +81,7 @@ export class ReorderGroup {
if (!reorderEl) {
return false;
}
const item = findReorderItem(reorderEl, this.containerEl);
const item = findReorderItem(reorderEl, this.el);
if (!item) {
console.error('reorder node not found');
return false;
@@ -69,10 +91,12 @@ export class ReorderGroup {
}
private onDragStart(ev: GestureDetail) {
ev.event.preventDefault();
const item = this.selectedItemEl = ev.data;
const heights = this.cachedHeights;
heights.length = 0;
const el = this.containerEl;
const el = this.el;
const children: any = el.children;
if (!children || children.length === 0) {
return;
@@ -86,7 +110,7 @@ export class ReorderGroup {
child.$ionIndex = i;
}
const box = this.containerEl.getBoundingClientRect();
const box = this.el.getBoundingClientRect();
this.containerTop = box.top;
this.containerBottom = box.bottom;
@@ -144,7 +168,7 @@ export class ReorderGroup {
return;
}
const children = this.containerEl.children as any;
const children = this.el.children as any;
const toIndex = this.lastToIndex;
const fromIndex = indexForItem(selectedItem);
@@ -152,7 +176,7 @@ export class ReorderGroup {
? children[toIndex + 1]
: children[toIndex];
this.containerEl.insertBefore(selectedItem, ref);
this.el.insertBefore(selectedItem, ref);
const len = children.length;
for (let i = 0; i < len; i++) {
@@ -194,7 +218,7 @@ export class ReorderGroup {
/********* DOM WRITE ********* */
private reorderMove(fromIndex: number, toIndex: number) {
const itemHeight = this.selectedItemHeight;
const children = this.containerEl.children;
const children = this.el.children;
for (let i = 0; i < children.length; i++) {
const style = (children[i] as any).style;
let value = '';
@@ -234,26 +258,6 @@ export class ReorderGroup {
}
};
}
render() {
return (
<ion-gesture
canStart={this.canStart.bind(this)}
onStart={this.onDragStart.bind(this)}
onMove={this.onDragMove.bind(this)}
onEnd={this.onDragEnd.bind(this)}
disabled={this.disabled}
disableScroll={true}
gestureName="reorder"
gesturePriority={30}
direction="y"
threshold={0}
attachTo="window"
>
<slot></slot>
</ion-gesture>
);
}
}
function indexForItem(element: any): number {

View File

@@ -1,4 +1,4 @@
import { TestWindow, spyOnEvent } from '@stencil/core/testing';
import { TestWindow, spyOnEvent } from '@stencil/core/dist/testing';
import { Toggle } from '../toggle';

View File

@@ -26,15 +26,6 @@
outline: none;
}
ion-gesture {
display: block;
width: 100%;
height: 100%;
visibility: inherit;
}
input {
@include input-cover();

View File

@@ -1,6 +1,6 @@
import { Component, Element, Event, EventEmitter, Prop, State, Watch } from '@stencil/core';
import { Component, Element, Event, EventEmitter, Prop, QueueApi, State, Watch } from '@stencil/core';
import { CheckboxInput, CheckedInputChangeEvent, Color, GestureDetail, Mode, StyleEvent } from '../../interface';
import { CheckboxInput, CheckedInputChangeEvent, Color, Gesture, GestureDetail, Mode, StyleEvent } from '../../interface';
import { hapticSelection } from '../../utils/haptic';
import { deferEvent, renderHiddenInput } from '../../utils/helpers';
import { createColorClasses, hostContext } from '../../utils/theme';
@@ -18,9 +18,12 @@ export class Toggle implements CheckboxInput {
private inputId = `ion-tg-${toggleIds++}`;
private nativeInput!: HTMLInputElement;
private pivotX = 0;
private gesture?: Gesture;
@Element() el!: HTMLElement;
@Prop({ context: 'queue' }) queue!: QueueApi;
@State() activated = false;
@State() keyFocus = false;
@@ -86,18 +89,20 @@ export class Toggle implements CheckboxInput {
}
@Watch('disabled')
emitStyle() {
disabledChanged() {
this.ionStyle.emit({
'interactive-disabled': this.disabled,
});
if (this.gesture) {
this.gesture.disabled = this.disabled;
}
}
componentWillLoad() {
this.ionStyle = deferEvent(this.ionStyle);
this.emitStyle();
}
componentDidLoad() {
async componentDidLoad() {
const parentItem = this.nativeInput.closest('ion-item');
if (parentItem) {
const itemLabel = parentItem.querySelector('ion-label');
@@ -106,6 +111,18 @@ export class Toggle implements CheckboxInput {
this.nativeInput.setAttribute('aria-labelledby', itemLabel.id);
}
}
this.gesture = (await import('../../utils/gesture/gesture')).create({
el: this.el,
queue: this.queue,
gestureName: 'toggle',
gesturePriority: 30,
threshold: 0,
onStart: this.onDragStart.bind(this),
onMove: this.onDragMove.bind(this),
onEnd: this.onDragEnd.bind(this),
});
this.disabledChanged();
}
private onDragStart(detail: GestureDetail) {
@@ -172,22 +189,9 @@ export class Toggle implements CheckboxInput {
renderHiddenInput(this.el, this.name, this.value, this.disabled);
return [
<ion-gesture
onStart={this.onDragStart.bind(this)}
onMove={this.onDragMove.bind(this)}
onEnd={this.onDragEnd.bind(this)}
gestureName="toggle"
passive={false}
gesturePriority={30}
direction="x"
threshold={0}
attachTo="parent"
disabled={this.disabled}
tabIndex={-1}>
<div class="toggle-icon">
<div class="toggle-inner"/>
</div>
</ion-gesture>,
<div class="toggle-icon">
<div class="toggle-inner"/>
</div>,
<input
type="checkbox"
onChange={this.onChange.bind(this)}

View File

@@ -43,16 +43,6 @@ The mode determines which platform styles to use.
Possible values are: `"ios"` or `"md"`.
#### translucent
boolean
If true, the toolbar will be translucent.
Note: In order to scroll content behind the toolbar, the `fullscreen`
attribute needs to be set on the content.
Defaults to `false`.
## Attributes
#### color
@@ -72,16 +62,6 @@ The mode determines which platform styles to use.
Possible values are: `"ios"` or `"md"`.
#### translucent
boolean
If true, the toolbar will be translucent.
Note: In order to scroll content behind the toolbar, the `fullscreen`
attribute needs to be set on the content.
Defaults to `false`.
----------------------------------------------

View File

@@ -45,6 +45,8 @@
contain: content;
overflow: hidden;
z-index: $z-index-toolbar;
box-sizing: border-box;
}
// Transparent Toolbar

View File

@@ -3,8 +3,6 @@ export * from './components';
export * from './components/animation-controller/animation-interface';
export * from './components/alert/alert-interface';
export * from './components/action-sheet/action-sheet-interface';
export * from './components/gesture/gesture-interface';
export * from './components/gesture-controller/gesture-controller-interface';
export * from './components/menu/menu-interface';
export * from './components/modal/modal-interface';
export * from './components/picker/picker-interface';
@@ -24,6 +22,8 @@ export * from './components/virtual-scroll/virtual-scroll-interface';
// Other types
export * from './components/nav/view-controller';
export { Gesture, GestureDetail } from './utils/gesture/gesture';
export * from './utils/input-interface';
export * from './global/config';
export { OverlayEventDetail, OverlayInterface } from './utils/overlays';

View File

@@ -1,5 +1,5 @@
import { Animation } from '../../interface';
import { TransitionOptions } from '../../utils/transition';
import { TransitionOptions } from '../transition';
const DURATION = 500;
const EASING = 'cubic-bezier(0.36,0.66,0.04,1)';

View File

@@ -1,5 +1,5 @@
import { Animation } from '../../interface';
import { TransitionOptions } from '../../utils/transition';
import { TransitionOptions } from '../transition';
const TRANSLATEY = 'translateY';
const OFF_BOTTOM = '40px';

View File

@@ -1,12 +1,4 @@
import { Component, Event, EventEmitter, Method } from '@stencil/core';
import { BlockerConfig, BlockerDelegate, GestureConfig, GestureDelegate } from '../../interface';
import { BlockerDelegate as BD, GestureDelegate as GD } from './gesture-controller-utils';
@Component({
tag: 'ion-gesture-controller'
})
export class GestureController {
private gestureId = 0;
@@ -15,33 +7,28 @@ export class GestureController {
private disabledScroll = new Set<number>();
private capturedId: number | null = null;
/**
* Event emitted when a gesture has been captured.
*/
@Event() ionGestureCaptured!: EventEmitter<string>;
constructor(
private doc: Document
) {}
/**
* Creates a gesture delegate based on the GestureConfig passed
*/
@Method()
create(config: GestureConfig): Promise<GestureDelegate> {
return Promise.resolve(
new GD(
this,
this.newID(),
config.name,
config.priority ? config.priority : 0,
!!config.disableScroll
)
createGesture(config: GestureConfig): GestureDelegate {
return new GestureDelegate(
this,
this.newID(),
config.name,
config.priority ? config.priority : 0,
!!config.disableScroll
);
}
/**
* Creates a blocker that will block any other gesture events from firing. Set in the ion-gesture component.
*/
@Method()
createBlocker(opts: BlockerConfig = {}): BlockerDelegate {
return new BD(
return new BlockerDelegate(
this.newID(),
this,
opts.disable,
@@ -72,9 +59,9 @@ export class GestureController {
if (maxPriority === priority) {
this.capturedId = id;
requestedStart.clear();
if (this.ionGestureCaptured) {
this.ionGestureCaptured.emit(gestureName);
}
const event = new CustomEvent('ionGestureCaptured', { detail: gestureName });
this.doc.body.dispatchEvent(event);
return true;
}
requestedStart.delete(id);
@@ -148,3 +135,122 @@ export class GestureController {
return this.gestureId;
}
}
export class GestureDelegate {
private ctrl?: GestureController;
constructor(
ctrl: any,
private id: number,
private name: string,
private priority: number,
private disableScroll: boolean
) {
this.ctrl = ctrl;
}
canStart(): boolean {
if (!this.ctrl) {
return false;
}
return this.ctrl.canStart(this.name);
}
start(): boolean {
if (!this.ctrl) {
return false;
}
return this.ctrl.start(this.name, this.id, this.priority);
}
capture(): boolean {
if (!this.ctrl) {
return false;
}
const captured = this.ctrl.capture(this.name, this.id, this.priority);
if (captured && this.disableScroll) {
this.ctrl.disableScroll(this.id);
}
return captured;
}
release() {
if (this.ctrl) {
this.ctrl.release(this.id);
if (this.disableScroll) {
this.ctrl.enableScroll(this.id);
}
}
}
destroy() {
this.release();
this.ctrl = undefined;
}
}
export class BlockerDelegate {
private ctrl?: GestureController;
constructor(
private id: number,
ctrl: any,
private disable: string[] | undefined,
private disableScroll: boolean
) {
this.ctrl = ctrl;
}
block() {
if (!this.ctrl) {
return;
}
if (this.disable) {
for (const gesture of this.disable) {
this.ctrl.disableGesture(gesture, this.id);
}
}
if (this.disableScroll) {
this.ctrl.disableScroll(this.id);
}
}
unblock() {
if (!this.ctrl) {
return;
}
if (this.disable) {
for (const gesture of this.disable) {
this.ctrl.enableGesture(gesture, this.id);
}
}
if (this.disableScroll) {
this.ctrl.enableScroll(this.id);
}
}
destroy() {
this.unblock();
this.ctrl = undefined;
}
}
export interface GestureConfig {
name: string;
priority?: number;
disableScroll?: boolean;
}
export interface BlockerConfig {
disable?: string[];
disableScroll?: boolean;
}
export const gestureController = new GestureController(document);

View File

@@ -0,0 +1,346 @@
import { QueueApi } from '@stencil/core';
import { PanRecognizer } from './recognizers';
import { GestureDelegate, gestureController } from './gesture-controller';
import { PointerEvents } from './pointer-events';
export interface GestureDetail {
type: string;
startX: number;
startY: number;
startTimeStamp: number;
currentX: number;
currentY: number;
velocityX: number;
velocityY: number;
deltaX: number;
deltaY: number;
timeStamp: number;
event: UIEvent;
data?: any;
}
export type GestureCallback = (detail?: GestureDetail) => boolean | void;
export interface GestureConfig {
el: Node;
disableScroll?: boolean;
queue: QueueApi;
direction?: 'x' | 'y';
gestureName: string;
gesturePriority?: number;
passive?: boolean;
maxAngle?: number;
threshold?: number;
canStart?: GestureCallback;
onWillStart?: (_: GestureDetail) => Promise<void>;
onStart?: GestureCallback;
onMove?: GestureCallback;
onEnd?: GestureCallback;
notCaptured?: GestureCallback;
}
export function create(config: GestureConfig) {
return new Gesture(config);
}
export class Gesture {
private detail: GestureDetail;
private positions: number[] = [];
private gesture: GestureDelegate;
private pan: PanRecognizer;
private hasCapturedPan = false;
private hasStartedPan = false;
private hasFiredStart = true;
private isMoveQueued = false;
private pointerEvents: PointerEvents;
private canStart?: GestureCallback;
private onWillStart?: (_: GestureDetail) => Promise<void>;
private onStart?: GestureCallback;
private onMove?: GestureCallback;
private onEnd?: GestureCallback;
private notCaptured?: GestureCallback;
private threshold: number;
private queue: QueueApi;
constructor(config: GestureConfig) {
const finalConfig = {
disableScroll: false,
direction: 'x',
gesturePriority: 0,
passive: true,
maxAngle: 40,
threshold: 10,
...config
};
this.canStart = finalConfig.canStart;
this.onWillStart = finalConfig.onWillStart;
this.onStart = finalConfig.onStart;
this.onEnd = finalConfig.onEnd;
this.onMove = finalConfig.onMove;
this.threshold = finalConfig.threshold;
this.queue = finalConfig.queue;
this.detail = {
type: 'pan',
startX: 0,
startY: 0,
startTimeStamp: 0,
currentX: 0,
currentY: 0,
velocityX: 0,
velocityY: 0,
deltaX: 0,
deltaY: 0,
timeStamp: 0,
event: undefined as any,
data: undefined
};
this.pointerEvents = new PointerEvents(
finalConfig.el,
this.pointerDown.bind(this),
this.pointerMove.bind(this),
this.pointerUp.bind(this),
{
capture: false,
}
);
this.pan = new PanRecognizer(finalConfig.direction, finalConfig.threshold, finalConfig.maxAngle);
this.gesture = gestureController.createGesture({
name: config.gestureName,
priority: config.gesturePriority,
disableScroll: config.disableScroll
});
}
set disabled(disabled: boolean) {
this.pointerEvents.disabled = disabled;
}
destroy() {
this.gesture.destroy();
this.pointerEvents.destroy();
}
private pointerDown(ev: UIEvent): boolean {
const timeStamp = now(ev);
if (this.hasStartedPan || !this.hasFiredStart) {
return false;
}
const detail = this.detail;
updateDetail(ev, detail);
detail.startX = detail.currentX;
detail.startY = detail.currentY;
detail.startTimeStamp = detail.timeStamp = timeStamp;
detail.velocityX = detail.velocityY = detail.deltaX = detail.deltaY = 0;
detail.event = ev;
this.positions.length = 0;
// Check if gesture can start
if (this.canStart && this.canStart(detail) === false) {
return false;
}
// Release fallback
this.gesture.release();
// Start gesture
if (!this.gesture.start()) {
return false;
}
this.positions.push(detail.currentX, detail.currentY, timeStamp);
this.hasStartedPan = true;
if (this.threshold === 0) {
return this.tryToCapturePan();
}
this.pan.start(detail.startX, detail.startY);
return true;
}
private pointerMove(ev: UIEvent) {
// fast path, if gesture is currently captured
// do minimun job to get user-land even dispatched
if (this.hasCapturedPan) {
if (!this.isMoveQueued && this.hasFiredStart) {
this.isMoveQueued = true;
this.calcGestureData(ev);
this.queue.write(this.fireOnMove.bind(this));
}
return;
}
// gesture is currently being detected
const detail = this.detail;
this.calcGestureData(ev);
if (this.pan.detect(detail.currentX, detail.currentY)) {
if (this.pan.isGesture()) {
if (!this.tryToCapturePan()) {
this.abortGesture();
}
}
}
}
private fireOnMove() {
// Since fireOnMove is called inside a RAF, onEnd() might be called,
// we must double check hasCapturedPan
if (!this.hasCapturedPan) {
return;
}
const detail = this.detail;
this.isMoveQueued = false;
if (this.onMove) {
this.onMove(detail);
}
}
private calcGestureData(ev: UIEvent) {
const detail = this.detail;
updateDetail(ev, detail);
const currentX = detail.currentX;
const currentY = detail.currentY;
const timestamp = detail.timeStamp = now(ev);
detail.deltaX = currentX - detail.startX;
detail.deltaY = currentY - detail.startY;
detail.event = ev;
const timeRange = timestamp - 100;
const positions = this.positions;
let startPos = positions.length - 1;
// move pointer to position measured 100ms ago
while (startPos > 0 && positions[startPos] > timeRange) {
startPos -= 3;
}
if (startPos > 1) {
// compute relative movement between these two points
const frequency = 1 / (positions[startPos] - timestamp);
const movedY = positions[startPos - 1] - currentY;
const movedX = positions[startPos - 2] - currentX;
// based on XXms compute the movement to apply for each render step
// velocity = space/time = s*(1/t) = s*frequency
detail.velocityX = movedX * frequency;
detail.velocityY = movedY * frequency;
} else {
detail.velocityX = 0;
detail.velocityY = 0;
}
positions.push(currentX, currentY, timestamp);
}
private tryToCapturePan(): boolean {
if (this.gesture && !this.gesture.capture()) {
return false;
}
this.hasCapturedPan = true;
this.hasFiredStart = false;
// reset start position since the real user-land event starts here
// If the pan detector threshold is big, not reseting the start position
// will cause a jump in the animation equal to the detector threshold.
// the array of positions used to calculate the gesture velocity does not
// need to be cleaned, more points in the positions array always results in a
// more acurate value of the velocity.
const detail = this.detail;
detail.startX = detail.currentX;
detail.startY = detail.currentY;
detail.startTimeStamp = detail.timeStamp;
if (this.onWillStart) {
this.onWillStart(this.detail).then(this.fireOnStart.bind(this));
} else {
this.fireOnStart();
}
return true;
}
private fireOnStart() {
if (this.onStart) {
this.onStart(this.detail);
}
this.hasFiredStart = true;
}
private abortGesture() {
this.reset();
this.pointerEvents.stop();
if (this.notCaptured) {
this.notCaptured(this.detail);
}
}
private reset() {
this.hasCapturedPan = false;
this.hasStartedPan = false;
this.isMoveQueued = false;
this.hasFiredStart = true;
if (this.gesture) {
this.gesture.release();
}
}
// END *************************
private pointerUp(ev: UIEvent) {
const hasCaptured = this.hasCapturedPan;
const hasFiredStart = this.hasFiredStart;
this.reset();
if (!hasFiredStart) {
return;
}
this.calcGestureData(ev);
const detail = this.detail;
// Try to capture press
if (hasCaptured) {
if (this.onEnd) {
this.onEnd(detail);
}
return;
}
// Not captured any event
if (this.notCaptured) {
this.notCaptured(detail);
}
}
}
function updateDetail(ev: any, detail: GestureDetail) {
// get X coordinates for either a mouse click
// or a touch depending on the given event
let x = 0;
let y = 0;
if (ev) {
const changedTouches = ev.changedTouches;
if (changedTouches && changedTouches.length > 0) {
const touch = changedTouches[0];
x = touch.clientX;
y = touch.clientY;
} else if (ev.pageX !== undefined) {
x = ev.pageX;
y = ev.pageY;
}
}
detail.currentX = x;
detail.currentY = y;
}
function now(ev: UIEvent) {
return ev.timeStamp || Date.now();
}

View File

@@ -0,0 +1,50 @@
let _sPassive: boolean | undefined;
function supportsPassive(node: Node) {
if (_sPassive === undefined) {
try {
const opts = Object.defineProperty({}, 'passive', {
get: () => {
_sPassive = true;
}
});
node.addEventListener('optsTest', () => { return; }, opts);
} catch {
_sPassive = false;
}
}
return !!_sPassive;
}
export function addEventListener(
el: any,
eventName: string,
callback: EventListenerOrEventListenerObject,
opts: {
passive?: boolean;
capture?: boolean
}
): () => void {
// use event listener options when supported
// otherwise it's just a boolean for the "capture" arg
const listenerOpts = supportsPassive(el) ? {
'capture': !!opts.capture,
'passive': !!opts.passive,
} : !!opts.capture;
let add: string;
let remove: string;
if (el['__zone_symbol__addEventListener']) {
add = '__zone_symbol__addEventListener';
remove = '__zone_symbol__removeEventListener';
} else {
add = 'addEventListener';
remove = 'removeEventListener';
}
el[add](eventName, callback, listenerOpts);
return () => {
el[remove](eventName, callback, listenerOpts);
};
}

View File

@@ -0,0 +1,144 @@
import { addEventListener } from './listener';
const MOUSE_WAIT = 2000;
export class PointerEvents {
private rmTouchStart?: () => void;
private rmTouchMove?: () => void;
private rmTouchEnd?: () => void;
private rmTouchCancel?: () => void;
private rmMouseStart?: () => void;
private rmMouseMove?: () => void;
private rmMouseUp?: () => void;
private bindTouchEnd: any;
private bindMouseUp: any;
private lastTouchEvent = 0;
constructor(
private el: Node,
private pointerDown: any,
private pointerMove: any,
private pointerUp: any,
private options: EventListenerOptions
) {
this.bindTouchEnd = this.handleTouchEnd.bind(this);
this.bindMouseUp = this.handleMouseUp.bind(this);
}
set disabled(disabled: boolean) {
if (disabled) {
if (this.rmTouchStart) {
this.rmTouchStart();
}
if (this.rmMouseStart) {
this.rmMouseStart();
}
this.rmTouchStart = this.rmMouseStart = undefined;
this.stop();
} else {
if (!this.rmTouchStart) {
this.rmTouchStart = addEventListener(this.el, 'touchstart', this.handleTouchStart.bind(this), this.options);
}
if (!this.rmMouseStart) {
this.rmMouseStart = addEventListener(this.el, 'mousedown', this.handleMouseDown.bind(this), this.options);
}
}
}
stop() {
this.stopTouch();
this.stopMouse();
}
destroy() {
this.disabled = true;
this.pointerUp = this.pointerMove = this.pointerDown = undefined;
}
private handleTouchStart(ev: any) {
this.lastTouchEvent = Date.now() + MOUSE_WAIT;
if (!this.pointerDown(ev, POINTER_EVENT_TYPE_TOUCH)) {
return;
}
if (!this.rmTouchMove && this.pointerMove) {
this.rmTouchMove = addEventListener(this.el, 'touchmove', this.pointerMove, this.options);
}
if (!this.rmTouchEnd) {
this.rmTouchEnd = addEventListener(this.el, 'touchend', this.bindTouchEnd, this.options);
}
if (!this.rmTouchCancel) {
this.rmTouchCancel = addEventListener(this.el, 'touchcancel', this.bindTouchEnd, this.options);
}
}
private handleMouseDown(ev: any) {
if (this.lastTouchEvent > Date.now()) {
console.debug('mousedown event dropped because of previous touch');
return;
}
if (!this.pointerDown(ev, POINTER_EVENT_TYPE_MOUSE)) {
return;
}
if (!this.rmMouseMove && this.pointerMove) {
this.rmMouseMove = addEventListener(this.el.ownerDocument, 'mousemove', this.pointerMove, this.options);
}
if (!this.rmMouseUp) {
this.rmMouseUp = addEventListener(this.el.ownerDocument, 'mouseup', this.bindMouseUp, this.options);
}
}
private handleTouchEnd(ev: any) {
this.stopTouch();
if (this.pointerUp) {
this.pointerUp(ev, POINTER_EVENT_TYPE_TOUCH);
}
}
private handleMouseUp(ev: any) {
this.stopMouse();
if (this.pointerUp) {
this.pointerUp(ev, POINTER_EVENT_TYPE_MOUSE);
}
}
private stopTouch() {
if (this.rmTouchMove) {
this.rmTouchMove();
}
if (this.rmTouchEnd) {
this.rmTouchEnd();
}
if (this.rmTouchCancel) {
this.rmTouchCancel();
}
this.rmTouchMove = this.rmTouchEnd = this.rmTouchCancel = undefined;
}
private stopMouse() {
if (this.rmMouseMove) {
this.rmMouseMove();
}
if (this.rmMouseUp) {
this.rmMouseUp();
}
this.rmMouseMove = this.rmMouseUp = undefined;
}
}
export const POINTER_EVENT_TYPE_MOUSE = 1;
export const POINTER_EVENT_TYPE_TOUCH = 2;
export interface PointerEventsConfig {
element?: HTMLElement;
pointerDown: (ev: any) => boolean;
pointerMove?: (ev: any) => void;
pointerUp?: (ev: any) => void;
zone?: boolean;
capture?: boolean;
passive?: boolean;
}

View File

@@ -1,4 +1,4 @@
import { pointerCoord } from '../../../utils/helpers';
import { pointerCoord } from '../../helpers';
import { isFocused, relocateInput } from './common';
import { getScrollData } from './scroll-data';