mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
perf(gesture): lazy loaded dynamic ES module
This commit is contained in:
200
core/src/components.d.ts
vendored
200
core/src/components.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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/)*
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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/)*
|
||||
@@ -10,6 +10,8 @@ ion-item-sliding {
|
||||
width: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.item-sliding-active-slide .item {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
ion-gesture,
|
||||
.range-slider {
|
||||
position: relative;
|
||||
|
||||
|
||||
@@ -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>
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TestWindow, spyOnEvent } from '@stencil/core/testing';
|
||||
import { TestWindow, spyOnEvent } from '@stencil/core/dist/testing';
|
||||
|
||||
import { Toggle } from '../toggle';
|
||||
|
||||
|
||||
@@ -26,15 +26,6 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
ion-gesture {
|
||||
display: block;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
visibility: inherit;
|
||||
}
|
||||
|
||||
input {
|
||||
@include input-cover();
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
contain: content;
|
||||
overflow: hidden;
|
||||
z-index: $z-index-toolbar;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// Transparent Toolbar
|
||||
|
||||
4
core/src/interface.d.ts
vendored
4
core/src/interface.d.ts
vendored
@@ -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';
|
||||
|
||||
@@ -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)';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Animation } from '../../interface';
|
||||
import { TransitionOptions } from '../../utils/transition';
|
||||
import { TransitionOptions } from '../transition';
|
||||
|
||||
const TRANSLATEY = 'translateY';
|
||||
const OFF_BOTTOM = '40px';
|
||||
|
||||
@@ -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);
|
||||
346
core/src/utils/gesture/gesture.ts
Normal file
346
core/src/utils/gesture/gesture.ts
Normal 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();
|
||||
}
|
||||
50
core/src/utils/gesture/listener.ts
Normal file
50
core/src/utils/gesture/listener.ts
Normal 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);
|
||||
};
|
||||
}
|
||||
144
core/src/utils/gesture/pointer-events.ts
Normal file
144
core/src/utils/gesture/pointer-events.ts
Normal 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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pointerCoord } from '../../../utils/helpers';
|
||||
import { pointerCoord } from '../../helpers';
|
||||
|
||||
import { isFocused, relocateInput } from './common';
|
||||
import { getScrollData } from './scroll-data';
|
||||
|
||||
Reference in New Issue
Block a user