From 49cac8beec4a8ecd411f0740e1e155d64675c1ab Mon Sep 17 00:00:00 2001 From: "Manu Mtz.-Almeida" Date: Wed, 1 Aug 2018 01:38:52 +0200 Subject: [PATCH] perf(gesture): lazy loaded dynamic ES module --- core/src/components.d.ts | 200 ------- core/src/components/button/button.scss | 1 + .../gesture-controller-interface.ts | 13 - .../gesture-controller-utils.ts | 107 ---- .../components/gesture-controller/readme.md | 31 -- .../test/gesture-controller.spec.tsx | 344 ------------ .../components/gesture/gesture-interface.ts | 18 - core/src/components/gesture/gesture.tsx | 498 ------------------ core/src/components/gesture/readme.md | 227 -------- .../components/item-sliding/item-sliding.scss | 2 + .../components/item-sliding/item-sliding.tsx | 44 +- core/src/components/menu/menu.tsx | 53 +- core/src/components/nav/nav.tsx | 43 +- .../picker-column/picker-column.tsx | 32 +- core/src/components/range/range.scss | 1 - core/src/components/range/range.tsx | 104 ++-- core/src/components/refresher/refresher.tsx | 68 +-- .../reorder-group/reorder-group.scss | 6 +- .../reorder-group/reorder-group.tsx | 64 +-- .../src/components/toggle/test/toggle.spec.ts | 2 +- core/src/components/toggle/toggle.scss | 9 - core/src/components/toggle/toggle.tsx | 46 +- core/src/components/toolbar/readme.md | 20 - core/src/components/toolbar/toolbar.scss | 2 + core/src/interface.d.ts | 4 +- core/src/utils/animations/ios.transition.ts | 2 +- core/src/utils/animations/md.transition.ts | 2 +- .../gesture/gesture-controller.ts} | 160 +++++- core/src/utils/gesture/gesture.ts | 346 ++++++++++++ core/src/utils/gesture/listener.ts | 50 ++ core/src/utils/gesture/pointer-events.ts | 144 +++++ .../gesture/recognizers.ts | 0 .../utils/input-shims/hacks/scroll-assist.ts | 2 +- core/stencil.config.ts | 1 - 34 files changed, 938 insertions(+), 1708 deletions(-) delete mode 100644 core/src/components/gesture-controller/gesture-controller-interface.ts delete mode 100644 core/src/components/gesture-controller/gesture-controller-utils.ts delete mode 100644 core/src/components/gesture-controller/readme.md delete mode 100644 core/src/components/gesture-controller/test/gesture-controller.spec.tsx delete mode 100644 core/src/components/gesture/gesture-interface.ts delete mode 100644 core/src/components/gesture/gesture.tsx delete mode 100644 core/src/components/gesture/readme.md rename core/src/{components/gesture-controller/gesture-controller.tsx => utils/gesture/gesture-controller.ts} (51%) create mode 100644 core/src/utils/gesture/gesture.ts create mode 100644 core/src/utils/gesture/listener.ts create mode 100644 core/src/utils/gesture/pointer-events.ts rename core/src/{components => utils}/gesture/recognizers.ts (100%) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 51afcfb8515..5dde9d371d6 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -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; - /** - * 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) => 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; - /** - * 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; - /** - * 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 { diff --git a/core/src/components/button/button.scss b/core/src/components/button/button.scss index ece4501b859..8dcbe7173a7 100644 --- a/core/src/components/button/button.scss +++ b/core/src/components/button/button.scss @@ -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 } diff --git a/core/src/components/gesture-controller/gesture-controller-interface.ts b/core/src/components/gesture-controller/gesture-controller-interface.ts deleted file mode 100644 index a2a2e7902d3..00000000000 --- a/core/src/components/gesture-controller/gesture-controller-interface.ts +++ /dev/null @@ -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; -} diff --git a/core/src/components/gesture-controller/gesture-controller-utils.ts b/core/src/components/gesture-controller/gesture-controller-utils.ts deleted file mode 100644 index 6fe0a30cbd9..00000000000 --- a/core/src/components/gesture-controller/gesture-controller-utils.ts +++ /dev/null @@ -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; - } -} diff --git a/core/src/components/gesture-controller/readme.md b/core/src/components/gesture-controller/readme.md deleted file mode 100644 index 7fc97a20354..00000000000 --- a/core/src/components/gesture-controller/readme.md +++ /dev/null @@ -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) - - - - -## 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/)* diff --git a/core/src/components/gesture-controller/test/gesture-controller.spec.tsx b/core/src/components/gesture-controller/test/gesture-controller.spec.tsx deleted file mode 100644 index 20c455a2e9e..00000000000 --- a/core/src/components/gesture-controller/test/gesture-controller.spec.tsx +++ /dev/null @@ -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); - }); - }); -}); diff --git a/core/src/components/gesture/gesture-interface.ts b/core/src/components/gesture/gesture-interface.ts deleted file mode 100644 index 1a8d0073f2e..00000000000 --- a/core/src/components/gesture/gesture-interface.ts +++ /dev/null @@ -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; diff --git a/core/src/components/gesture/gesture.tsx b/core/src/components/gesture/gesture.tsx deleted file mode 100644 index 0583ced8a6b..00000000000 --- a/core/src/components/gesture/gesture.tsx +++ /dev/null @@ -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; - - /** - * 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; -} diff --git a/core/src/components/gesture/readme.md b/core/src/components/gesture/readme.md deleted file mode 100644 index ee75672dbce..00000000000 --- a/core/src/components/gesture/readme.md +++ /dev/null @@ -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. - - - - - -## 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 - -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/)* diff --git a/core/src/components/item-sliding/item-sliding.scss b/core/src/components/item-sliding/item-sliding.scss index 29cff9d0f58..815a0034c05 100644 --- a/core/src/components/item-sliding/item-sliding.scss +++ b/core/src/components/item-sliding/item-sliding.scss @@ -10,6 +10,8 @@ ion-item-sliding { width: 100%; overflow: hidden; + + user-select: none; } .item-sliding-active-slide .item { diff --git a/core/src/components/item-sliding/item-sliding.tsx b/core/src/components/item-sliding/item-sliding.tsx index 9b75ebfca02..4d94de3bd95 100644 --- a/core/src/components/item-sliding/item-sliding.tsx +++ b/core/src/components/item-sliding/item-sliding.tsx @@ -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 ( - - - - ); - } } /** @hidden */ diff --git a/core/src/components/menu/menu.tsx b/core/src/components/menu/menu.tsx index 4aeb94388f6..4de1d51fb60 100644 --- a/core/src/components/menu/menu.tsx +++ b/core/src/components/menu/menu.tsx @@ -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} - />, - - ]; } diff --git a/core/src/components/nav/nav.tsx b/core/src/components/nav/nav.tsx index 9b4642b778c..41ddb8d6c93 100644 --- a/core/src/components/nav/nav.tsx +++ b/core/src/components/nav/nav.tsx @@ -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 && ( - - ), this.mode === 'ios' &&