fix(angular): add swipe-to-go-back gesture

This commit is contained in:
Manu Mtz.-Almeida
2018-11-14 13:09:32 +01:00
committed by Manu MA
parent bfbbeca389
commit 108691dc44
29 changed files with 351 additions and 338 deletions

View File

@@ -5,7 +5,6 @@ export { RadioValueAccessor } from './control-value-accessors/radio-value-access
export { SelectValueAccessor } from './control-value-accessors/select-value-accessor';
export { TextValueAccessor } from './control-value-accessors/text-value-accessor';
export { RouterDirection } from './navigation/router-direction';
export { IonBackButton } from './navigation/ion-back-button';
export { NavDelegate } from './navigation/nav-delegate';
export { TabDelegate } from './navigation/tab-delegate';

View File

@@ -1,11 +1,16 @@
import { Directive, ElementRef, HostListener, Input, Optional } from '@angular/core';
import { Router } from '@angular/router';
import { NavController, NavIntent } from '../../providers/nav-controller';
export type RouterDirection = 'forward' | 'back' | 'root' | 'auto';
@Directive({
selector: 'ion-anchor,ion-button,ion-item'
selector: '[routerDirection],ion-anchor,ion-button,ion-item'
})
export class HrefDelegate {
@Input() routerDirection: RouterDirection = 'forward';
@Input()
set routerLink(_: any) {
this.elementRef.nativeElement.button = true;
@@ -21,6 +26,7 @@ export class HrefDelegate {
constructor(
@Optional() private router: Router,
private navCtrl: NavController,
private elementRef: ElementRef
) {}
@@ -29,7 +35,17 @@ export class HrefDelegate {
const url = this.href;
if (this.router && url != null && url[0] !== '#' && url.indexOf('://') === -1) {
ev.preventDefault();
this.navCtrl.setIntent(textToIntent(this.routerDirection));
this.router.navigateByUrl(url);
}
}
}
function textToIntent(direction: RouterDirection) {
switch (direction) {
case 'forward': return NavIntent.Forward;
case 'back': return NavIntent.Back;
case 'root': return NavIntent.Root;
default: return NavIntent.Auto;
}
}

View File

@@ -1,6 +1,6 @@
import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, Input, OnDestroy, OnInit, Optional, Output, ViewContainerRef } from '@angular/core';
import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, Input, NgZone, OnDestroy, OnInit, Optional, Output, ViewContainerRef } from '@angular/core';
import { ActivatedRoute, ChildrenOutletContexts, OutletContext, PRIMARY_OUTLET, Router } from '@angular/router';
import { RouteView, StackController } from './router-controller';
import { RouteView, StackController } from './stack-controller';
import { NavController } from '../../providers/nav-controller';
import { bindLifecycleEvents } from '../../providers/angular-delegate';
@@ -14,32 +14,47 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
private activatedView: RouteView | null = null;
private _activatedRoute: ActivatedRoute | null = null;
private _swipeGesture?: boolean;
private name: string;
private stackCtrl: StackController;
private nativeEl: HTMLIonRouterOutletElement;
private hasStack = false;
@Output('activate') activateEvents = new EventEmitter<any>();
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
@Input()
set animated(animated: boolean) {
(this.elementRef.nativeElement as HTMLIonRouterOutletElement).animated = animated;
this.nativeEl.animated = animated;
}
@Input()
set swipeGesture(swipe: boolean) {
this._swipeGesture = swipe;
this.nativeEl.swipeHandler = (swipe && this.hasStack) ? {
canStart: () => this.stackCtrl.canGoBack(1),
onStart: () => this.stackCtrl.startBackTransition(),
onEnd: shouldContinue => this.stackCtrl.endBackTransition(shouldContinue)
} : undefined;
}
constructor(
private parentContexts: ChildrenOutletContexts,
private location: ViewContainerRef,
private resolver: ComponentFactoryResolver,
private elementRef: ElementRef,
@Attribute('name') name: string,
@Optional() @Attribute('stack') stack: any,
private changeDetector: ChangeDetectorRef,
private navCtrl: NavController,
router: Router
elementRef: ElementRef,
router: Router,
zone: NgZone
) {
this.nativeEl = elementRef.nativeElement;
this.name = name || PRIMARY_OUTLET;
parentContexts.onChildOutletCreated(this.name, this as any);
const hasStack = stack !== 'false' && stack !== false;
this.stackCtrl = new StackController(hasStack, elementRef.nativeElement, router, this.navCtrl);
this.hasStack = stack !== 'false' && stack !== false;
this.stackCtrl = new StackController(this.hasStack, this.nativeEl, router, this.navCtrl, zone);
}
ngOnDestroy(): void {
@@ -60,9 +75,16 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
this.activateWith(context.route, context.resolver || null);
}
}
this.nativeEl.componentOnReady().then(() => {
if (this._swipeGesture === undefined) {
this.swipeGesture = this.nativeEl.mode === 'ios';
}
});
}
get isActivated(): boolean { return !!this.activated; }
get isActivated(): boolean {
return !!this.activated;
}
get component(): object {
if (!this.activated) {
@@ -151,9 +173,8 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
this.activatedView = enteringView;
this.stackCtrl.setActive(enteringView, direction, animated).then(() => {
this.activateEvents.emit(cmpRef.instance);
emitEvent(this.elementRef.nativeElement);
emitEvent(this.nativeEl);
});
}
canGoBack(deep = 1) {

View File

@@ -1,28 +0,0 @@
import { Directive, HostListener, Input } from '@angular/core';
import { NavController, NavIntent } from '../../providers/nav-controller';
@Directive({
selector: '[routerDirection]',
})
export class RouterDirection {
@Input() routerDirection: string;
constructor(
private navCtrl: NavController,
) {}
@HostListener('click')
onClick() {
this.navCtrl.setIntent(textToIntent(this.routerDirection));
}
}
function textToIntent(direction: string) {
switch (direction) {
case 'forward': return NavIntent.Forward;
case 'back': return NavIntent.Back;
case 'root': return NavIntent.Root;
default: return NavIntent.Auto;
}
}

View File

@@ -1,4 +1,4 @@
import { ComponentRef } from '@angular/core';
import { ComponentRef, NgZone } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { NavController } from '../../providers/nav-controller';
@@ -8,12 +8,15 @@ export class StackController {
private viewsSnapshot: RouteView[] = [];
private views: RouteView[] = [];
private runningTransition?: Promise<boolean>;
private skipTransition = false;
constructor(
private stack: boolean,
private containerEl: HTMLIonRouterOutletElement,
private router: Router,
private navCtrl: NavController,
private zone: NgZone,
) {}
createView(enteringRef: ComponentRef<any>, route: ActivatedRoute): RouteView {
@@ -22,7 +25,6 @@ export class StackController {
element: (enteringRef && enteringRef.location && enteringRef.location.nativeElement) as HTMLElement,
url: this.getUrl(route),
fullpath: document.location!.pathname,
deactivatedId: -1
};
}
@@ -31,22 +33,43 @@ export class StackController {
return this.views.find(vw => vw.url === activatedUrlKey);
}
async setActive(enteringView: RouteView, direction: number, animated: boolean) {
const leavingView = this.getActive();
this.insertView(enteringView, direction);
await this.transition(enteringView, leavingView, direction, animated, this.canGoBack(1), false);
this.cleanup();
}
canGoBack(deep: number): boolean {
return this.views.length > deep;
}
async setActive(enteringView: RouteView, direction: number, animated: boolean) {
const leavingView = this.getActive();
this.insertView(enteringView, direction);
await this.transition(enteringView, leavingView, direction, animated, this.canGoBack(1));
this.cleanup();
pop(deep: number) {
this.zone.run(() => {
const view = this.views[this.views.length - deep - 1];
this.navCtrl.navigateBack(view.url);
});
}
pop(deep: number) {
const view = this.views[this.views.length - deep - 1];
this.navCtrl.navigateBack(view.url);
startBackTransition() {
this.transition(
this.views[this.views.length - 2],
this.views[this.views.length - 1],
-1,
true,
true,
true
);
}
endBackTransition(shouldComplete: boolean) {
if (shouldComplete) {
this.skipTransition = true;
this.pop(1);
}
}
private insertView(enteringView: RouteView, direction: number) {
// no stack
if (!this.stack) {
@@ -100,8 +123,17 @@ export class StackController {
leavingView: RouteView | undefined,
direction: number,
animated: boolean,
showGoBack: boolean
showGoBack: boolean,
progressAnimation: boolean
) {
if (this.runningTransition) {
await this.runningTransition;
this.runningTransition = undefined;
}
if (this.skipTransition) {
this.skipTransition = false;
return;
}
const enteringEl = enteringView ? enteringView.element : undefined;
const leavingEl = leavingView ? leavingView.element : undefined;
const containerEl = this.containerEl;
@@ -112,12 +144,14 @@ export class StackController {
}
await containerEl.componentOnReady();
await containerEl.commit(enteringEl, leavingEl, {
this.runningTransition = containerEl.commit(enteringEl, leavingEl, {
duration: !animated ? 0 : undefined,
direction: direction === 1 ? 'forward' : 'back',
deepWait: true,
showGoBack
showGoBack,
progressAnimation
});
await this.runningTransition;
}
}
@@ -125,33 +159,19 @@ export class StackController {
const urlTree = this.router.createUrlTree(['.'], { relativeTo: activatedRoute });
return this.router.serializeUrl(urlTree);
}
}
export function destroyView(view: RouteView) {
function destroyView(view: RouteView) {
if (view) {
// TODO lifecycle event
view.ref.destroy();
}
}
export function getLastDeactivatedRef(views: RouteView[]) {
if (views.length < 2) {
return null;
}
return views.sort((a, b) => {
if (a.deactivatedId > b.deactivatedId) return -1;
if (a.deactivatedId < b.deactivatedId) return 1;
return 0;
})[0].ref;
}
export interface RouteView {
url: string;
fullpath: string;
element: HTMLElement;
ref: ComponentRef<any>;
deactivatedId: number;
savedData?: any;
}

View File

@@ -97,7 +97,6 @@ const DECLARATIONS = [
// navigation
c.IonBackButton,
c.IonRouterOutlet,
c.RouterDirection,
c.NavDelegate,
c.TabDelegate,
c.TabsDelegate,

View File

@@ -56,6 +56,7 @@ import {
Side,
SpinnerTypes,
StyleEvent,
SwipeGestureHandler,
TabBarChangedDetail,
TabButtonClickDetail,
TabButtonLayout,
@@ -403,7 +404,7 @@ export namespace Components {
/**
* When using a router, it specifies the transition direction when navigating to another page using `href`.
*/
'routerDirection'?: RouterDirection;
'routerDirection': RouterDirection;
}
interface IonAnchorAttributes extends StencilHTMLAttributes {
/**
@@ -565,7 +566,7 @@ export namespace Components {
/**
* When using a router, it specifies the transition direction when navigating to another page using `href`.
*/
'routerDirection'?: RouterDirection;
'routerDirection': RouterDirection;
/**
* The button shape.
*/
@@ -1343,7 +1344,7 @@ export namespace Components {
/**
* When using a router, it specifies the transition direction when navigating to another page using `href`.
*/
'routerDirection'?: RouterDirection;
'routerDirection': RouterDirection;
/**
* If `true`, the fab button will show when in a fab-list.
*/
@@ -2021,7 +2022,7 @@ export namespace Components {
/**
* When using a router, it specifies the transition direction when navigating to another page using `href`.
*/
'routerDirection'?: RouterDirection;
'routerDirection': RouterDirection;
/**
* The type of the button. Only used when an `onclick` or `button` property is present.
*/
@@ -3614,11 +3615,9 @@ export namespace Components {
'commit': (enteringEl: HTMLElement, leavingEl: HTMLElement | undefined, opts?: RouterOutletOptions | undefined) => Promise<boolean>;
'delegate'?: FrameworkDelegate;
'getRouteId': () => Promise<RouteID | undefined>;
/**
* Set the root component for the given navigation stack
*/
'setRoot': (component: ComponentRef, params?: { [key: string]: any; } | undefined, opts?: RouterOutletOptions | undefined) => Promise<boolean>;
'mode': Mode;
'setRouteId': (id: string, params: { [key: string]: any; } | undefined, direction: number) => Promise<RouteWrite>;
'swipeHandler'?: SwipeGestureHandler;
}
interface IonRouterOutletAttributes extends StencilHTMLAttributes {
/**
@@ -3630,9 +3629,11 @@ export namespace Components {
*/
'animation'?: AnimationBuilder;
'delegate'?: FrameworkDelegate;
'mode'?: Mode;
'onIonNavDidChange'?: (event: CustomEvent<void>) => void;
'onIonNavWillChange'?: (event: CustomEvent<void>) => void;
'onIonNavWillLoad'?: (event: CustomEvent<void>) => void;
'swipeHandler'?: SwipeGestureHandler;
}
interface IonRouter {

View File

@@ -29,7 +29,7 @@ export class Anchor implements ComponentInterface {
* When using a router, it specifies the transition direction when navigating to
* another page using `href`.
*/
@Prop() routerDirection?: RouterDirection;
@Prop() routerDirection: RouterDirection = 'forward';
hostData() {
return {

View File

@@ -8,11 +8,11 @@ The Anchor component is used for navigating to a specified link. Similar to the
## Properties
| Property | Attribute | Description | Type | Default |
| ----------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ----------- |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root" \| undefined` | `undefined` |
| Property | Attribute | Description | Type | Default |
| ----------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ----------- |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root"` | `'forward'` |
## CSS Custom Properties

View File

@@ -58,7 +58,7 @@ export class Button implements ComponentInterface {
* When using a router, it specifies the transition direction when navigating to
* another page using `href`.
*/
@Prop() routerDirection?: RouterDirection;
@Prop() routerDirection: RouterDirection = 'forward';
/**
* Contains a URL or a URL fragment that the hyperlink points to.

View File

@@ -46,7 +46,7 @@ This attribute specifies the size of the button. Setting this attribute will cha
| `fill` | `fill` | Set to `"clear"` for a transparent button, to `"outline"` for a transparent button with a border, or to `"solid"`. The default style is `"solid"` except inside of a toolbar, where the default is `"clear"`. | `"clear" \| "default" \| "outline" \| "solid" \| undefined` | `undefined` |
| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root" \| undefined` | `undefined` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root"` | `'forward'` |
| `shape` | `shape` | The button shape. | `"round" \| undefined` | `undefined` |
| `size` | `size` | The button size. | `"default" \| "large" \| "small" \| undefined` | `undefined` |
| `strong` | `strong` | If `true`, activates a button with a heavier font weight. | `boolean` | `false` |

View File

@@ -50,7 +50,7 @@ export class FabButton implements ComponentInterface {
* When using a router, it specifies the transition direction when navigating to
* another page using `href`.
*/
@Prop() routerDirection?: RouterDirection;
@Prop() routerDirection: RouterDirection = 'forward';
/**
* If `true`, the fab button will show when in a fab-list.

View File

@@ -9,17 +9,17 @@ If the FAB button is not wrapped with `<ion-fab>`, it will scroll with the conte
## Properties
| Property | Attribute | Description | Type | Default |
| ----------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ----------- |
| `activated` | `activated` | If `true`, the fab button will be show a close icon. | `boolean` | `false` |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the fab button. | `boolean` | `false` |
| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root" \| undefined` | `undefined` |
| `show` | `show` | If `true`, the fab button will show when in a fab-list. | `boolean` | `false` |
| `translucent` | `translucent` | If `true`, the fab button will be translucent. | `boolean` | `false` |
| `type` | `type` | The type of the button. | `"button" \| "reset" \| "submit"` | `'button'` |
| Property | Attribute | Description | Type | Default |
| ----------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ----------- |
| `activated` | `activated` | If `true`, the fab button will be show a close icon. | `boolean` | `false` |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the fab button. | `boolean` | `false` |
| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root"` | `'forward'` |
| `show` | `show` | If `true`, the fab button will show when in a fab-list. | `boolean` | `false` |
| `translucent` | `translucent` | If `true`, the fab button will be translucent. | `boolean` | `false` |
| `type` | `type` | The type of the button. | `"button" \| "reset" \| "submit"` | `'button'` |
## Events

View File

@@ -68,7 +68,7 @@ export class Item implements ComponentInterface {
* When using a router, it specifies the transition direction when navigating to
* another page using `href`.
*/
@Prop() routerDirection?: RouterDirection;
@Prop() routerDirection: RouterDirection = 'forward';
/**
* The type of the button. Only used when an `onclick` or `button` property is present.

View File

@@ -56,18 +56,18 @@ The highlight color changes based on the item state, but all of the states use I
## Properties
| Property | Attribute | Description | Type | Default |
| ----------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | --------------------- |
| `button` | `button` | If `true`, a button tag will be rendered and the item will be tappable. | `boolean` | `false` |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `detailIcon` | `detail-icon` | The icon to use when `detail` is set to `true`. | `string` | `'ios-arrow-forward'` |
| `detail` | `detail` | If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href`, `onclick` or `button` property is present. | `boolean \| undefined` | `undefined` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the item. | `boolean` | `false` |
| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` |
| `lines` | `lines` | How the bottom border should be displayed on the item. | `"full" \| "inset" \| "none" \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root" \| undefined` | `undefined` |
| `type` | `type` | The type of the button. Only used when an `onclick` or `button` property is present. | `"button" \| "reset" \| "submit"` | `'button'` |
| Property | Attribute | Description | Type | Default |
| ----------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------- |
| `button` | `button` | If `true`, a button tag will be rendered and the item will be tappable. | `boolean` | `false` |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `detailIcon` | `detail-icon` | The icon to use when `detail` is set to `true`. | `string` | `'ios-arrow-forward'` |
| `detail` | `detail` | If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href`, `onclick` or `button` property is present. | `boolean \| undefined` | `undefined` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the item. | `boolean` | `false` |
| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` |
| `lines` | `lines` | How the bottom border should be displayed on the item. | `"full" \| "inset" \| "none" \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root"` | `'forward'` |
| `type` | `type` | The type of the button. Only used when an `onclick` or `button` property is present. | `"button" \| "reset" \| "submit"` | `'button'` |
## CSS Custom Properties

View File

@@ -137,7 +137,6 @@ export class Menu implements ComponentInterface, MenuI {
/**
* Emitted when the menu state is changed.
*
* @internal
*/
@Event() protected ionMenuChange!: EventEmitter<MenuChangeEventDetail>;

View File

@@ -12,6 +12,12 @@ export interface NavResult {
direction?: NavDirection;
}
export interface SwipeGestureHandler {
canStart(): boolean;
onStart(): void;
onEnd(shouldComplete: boolean): void;
}
export interface RouterOutletOptions {
animated?: boolean;
animationBuilder?: AnimationBuilder;
@@ -23,6 +29,7 @@ export interface RouterOutletOptions {
mode?: Mode;
keyboardClose?: boolean;
skipIfBusy?: boolean;
progressAnimation?: boolean;
}
export interface NavOptions extends RouterOutletOptions {

View File

@@ -9,22 +9,3 @@
overflow: hidden;
z-index: $z-index-page-container;
}
.nav-decor {
display: none;
}
:host(.show-decor) .nav-decor {
@include position(0, 0, 0, 0);
display: block;
// when ios pages transition, the leaving page grays out
// this is the black square behind all pages so they gray out
position: absolute;
background: #000;
z-index: 0;
pointer-events: none;
}

View File

@@ -1,7 +1,7 @@
import { Build, Component, Element, Event, EventEmitter, Method, Prop, QueueApi, Watch } from '@stencil/core';
import { ViewLifecycle } from '../..';
import { Animation, AnimationBuilder, ComponentProps, Config, FrameworkDelegate, Gesture, GestureDetail, Mode, NavComponent, NavOptions, NavOutlet, NavResult, RouteID, RouteWrite, TransitionDoneFn, TransitionInstruction, ViewController } from '../../interface';
import { Animation, AnimationBuilder, ComponentProps, Config, FrameworkDelegate, Gesture, 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';
@@ -15,7 +15,7 @@ import { ViewState, convertToViews, matches } from './view-controller';
export class Nav implements NavOutlet {
private transInstr: TransitionInstruction[] = [];
private sbTrns?: Animation;
private sbAni?: Animation;
private useRouter = false;
private isTransitioning = false;
private destroyed = false;
@@ -111,23 +111,20 @@ export class Nav implements NavOutlet {
async componentDidLoad() {
this.rootChanged();
this.gesture = (await import('../../utils/gesture/gesture')).createGesture({
el: this.win.document.body,
queue: this.queue,
gestureName: 'goback-swipe',
gesturePriority: 30,
threshold: 10,
canStart: () => this.canStart(),
onStart: () => this.onStart(),
onMove: ev => this.onMove(ev),
onEnd: ev => this.onEnd(ev),
});
this.gesture = (await import('../../utils/gesture/swipe-back')).createSwipeBackGesture(
this.el,
this.queue,
this.canStart.bind(this),
this.onStart.bind(this),
this.onMove.bind(this),
this.onEnd.bind(this)
);
this.swipeGestureChanged();
}
componentDidUnload() {
for (const view of this.views) {
lifecycle(this.win, view.element!, ViewLifecycle.WillUnload);
lifecycle(view.element!, ViewLifecycle.WillUnload);
view._destroy();
}
@@ -136,11 +133,7 @@ export class Nav implements NavOutlet {
}
// release swipe back gesture and transition
if (this.sbTrns) {
this.sbTrns.destroy();
}
this.transInstr.length = this.views.length = 0;
this.sbTrns = undefined;
this.destroyed = true;
}
@@ -756,9 +749,9 @@ export class Nav implements NavOutlet {
// let's make sure, callbacks are zoned
if (destroyQueue && destroyQueue.length > 0) {
for (const view of destroyQueue) {
lifecycle(this.win, view.element, ViewLifecycle.WillLeave);
lifecycle(this.win, view.element, ViewLifecycle.DidLeave);
lifecycle(this.win, view.element, ViewLifecycle.WillUnload);
lifecycle(view.element, ViewLifecycle.WillLeave);
lifecycle(view.element, ViewLifecycle.DidLeave);
lifecycle(view.element, ViewLifecycle.WillUnload);
}
// once all lifecycle events has been delivered, we can safely detroy the views
@@ -773,19 +766,12 @@ export class Nav implements NavOutlet {
leavingView: ViewController | undefined,
ti: TransitionInstruction
): Promise<NavResult> {
if (this.sbTrns) {
this.sbTrns.destroy();
this.sbTrns = undefined;
}
// we should animate (duration > 0) if the pushed page is not the first one (startup)
// or if it is a portal (modal, actionsheet, etc.)
const opts = ti.opts!;
const progressCallback = opts.progressAnimation
? (animation: Animation) => {
this.sbTrns = animation;
}
? (ani: Animation | undefined) => this.sbAni = ani
: undefined;
const enteringEl = enteringView.element!;
@@ -887,7 +873,7 @@ export class Nav implements NavOutlet {
if (i > activeViewIndex) {
// this view comes after the active view
// let's unload it
lifecycle(this.win, element, ViewLifecycle.WillUnload);
lifecycle(element, ViewLifecycle.WillUnload);
this.destroyView(view);
} else if (i < activeViewIndex) {
// this view comes before the active view
@@ -898,72 +884,40 @@ export class Nav implements NavOutlet {
}
private canStart(): boolean {
return !!this.swipeGesture &&
return (
!!this.swipeGesture &&
!this.isTransitioning &&
this.canGoBackSync();
}
private onStart() {
if (this.isTransitioning || this.transInstr.length > 0) {
return;
}
// default the direction to "back";
const opts: NavOptions = {
direction: 'back',
progressAnimation: true
};
this.queueTrns(
{
removeStart: -1,
removeCount: 1,
opts
},
undefined
this.transInstr.length === 0 &&
this.canGoBackSync()
);
}
private onMove(detail: GestureDetail) {
if (this.sbTrns) {
// continue to disable the app while actively dragging
this.isTransitioning = true;
private onStart() {
this.queueTrns({
removeStart: -1,
removeCount: 1,
opts: {
direction: 'back',
progressAnimation: true
}
}, undefined);
}
// set the transition animation's progress
const delta = detail.deltaX;
const stepValue = delta / this.win.innerWidth;
// set the transition animation's progress
this.sbTrns.progressStep(stepValue);
private onMove(stepValue: number) {
if (this.sbAni) {
this.sbAni.progressStep(stepValue);
}
}
private onEnd(detail: GestureDetail) {
if (this.sbTrns) {
// the swipe back gesture has ended
const delta = detail.deltaX;
const width = this.win.innerWidth;
const stepValue = delta / width;
const velocity = detail.velocityX;
const z = width / 2.0;
const shouldComplete =
velocity >= 0 && (velocity > 0.2 || detail.deltaX > z);
const missing = shouldComplete ? 1 - stepValue : stepValue;
const missingDistance = missing * width;
let realDur = 0;
if (missingDistance > 5) {
const dur = missingDistance / Math.abs(velocity);
realDur = Math.min(dur, 300);
}
this.sbTrns.progressEnd(shouldComplete, stepValue, realDur);
private onEnd(shouldComplete: boolean, stepValue: number, dur: number) {
if (this.sbAni) {
this.sbAni.progressEnd(shouldComplete, stepValue, dur);
}
}
render() {
return [
this.mode === 'ios' && <div class="nav-decor" />,
return (
<slot></slot>
];
);
}
}

View File

@@ -13,11 +13,13 @@ While RouterOutlet has methods for navigating around, it's recommended to use th
## Properties
| Property | Attribute | Description | Type | Default |
| ----------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ----------- |
| `animated` | `animated` | If `true`, the router-outlet should animate the transition of components. | `boolean` | `true` |
| `animation` | -- | By default `ion-nav` animates transition between pages based in the mode (ios or material design). However, this property allows to create custom transition using `AnimateBuilder` functions. | `AnimationBuilder \| undefined` | `undefined` |
| `delegate` | -- | | `FrameworkDelegate \| undefined` | `undefined` |
| Property | Attribute | Description | Type | Default |
| -------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ----------- |
| `animated` | `animated` | If `true`, the router-outlet should animate the transition of components. | `boolean` | `true` |
| `animation` | -- | By default `ion-nav` animates transition between pages based in the mode (ios or material design). However, this property allows to create custom transition using `AnimateBuilder` functions. | `AnimationBuilder \| undefined` | `undefined` |
| `delegate` | -- | | `FrameworkDelegate \| undefined` | `undefined` |
| `mode` | `mode` | | `"ios" \| "md"` | `undefined` |
| `swipeHandler` | -- | | `SwipeGestureHandler \| undefined` | `undefined` |
## Methods
@@ -50,24 +52,6 @@ Type: `Promise<RouteID | undefined>`
### `setRoot(component: ComponentRef, params?: { [key: string]: any; } | undefined, opts?: RouterOutletOption...`
Set the root component for the given navigation stack
#### Parameters
| Name | Type | Description |
| ----------- | ------------------------------------------- | ----------- |
| `component` | `Function \| HTMLElement \| null \| string` | |
| `params` | `undefined \| { [key: string]: any; }` | |
| `opts` | `RouterOutletOptions \| undefined` | |
#### Returns
Type: `Promise<boolean>`
### `setRouteId(id: string, params: { [key: string]: any; } | undefined, direction: number) => Promise<RouteWrite>`

View File

@@ -9,22 +9,3 @@
overflow: hidden;
z-index: $z-index-page-container;
}
.nav-decor {
display: none;
}
:host(.show-decor) .nav-decor {
@include position(0, 0, 0, 0);
display: block;
// when ios pages transition, the leaving page grays out
// this is the black square behind all pages so they gray out
position: absolute;
background: #000;
z-index: 0;
pointer-events: none;
}

View File

@@ -1,6 +1,6 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Prop, QueueApi } from '@stencil/core';
import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Prop, QueueApi, Watch } from '@stencil/core';
import { AnimationBuilder, ComponentProps, ComponentRef, Config, FrameworkDelegate, Mode, NavOutlet, RouteID, RouteWrite, RouterOutletOptions } from '../../interface';
import { Animation, AnimationBuilder, ComponentProps, ComponentRef, Config, FrameworkDelegate, Gesture, Mode, NavOutlet, RouteID, RouteWrite, RouterOutletOptions, SwipeGestureHandler } from '../../interface';
import { transition } from '../../utils';
import { attachComponent, detachComponent } from '../../utils/framework-delegate';
@@ -14,8 +14,8 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
private activeEl: HTMLElement | undefined;
private activeComponent: any;
private waitPromise?: Promise<void>;
mode!: Mode;
private gesture?: Gesture;
private ani?: Animation;
@Element() el!: HTMLElement;
@@ -24,6 +24,9 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
@Prop({ context: 'window' }) win!: Window;
@Prop({ context: 'queue' }) queue!: QueueApi;
/** @internal */
@Prop() mode!: Mode;
/** @internal */
@Prop() delegate?: FrameworkDelegate;
@@ -38,50 +41,49 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
*/
@Prop() animation?: AnimationBuilder;
/**
* @internal
*/
/** @internal */
@Prop() swipeHandler?: SwipeGestureHandler;
@Watch('swipeHandler')
swipeHandlerChanged() {
if (this.gesture) {
this.gesture.setDisabled(this.swipeHandler === undefined);
}
}
/** @internal */
@Event() ionNavWillLoad!: EventEmitter<void>;
/**
* @internal
*/
/** @internal */
@Event() ionNavWillChange!: EventEmitter<void>;
/**
* @internal
*/
/** @internal */
@Event() ionNavDidChange!: EventEmitter<void>;
componentWillLoad() {
this.ionNavWillLoad.emit();
}
componentDidUnload() {
this.activeEl = this.activeComponent = undefined;
async componentDidLoad() {
this.gesture = (await import('../../utils/gesture/swipe-back')).createSwipeBackGesture(
this.el,
this.queue,
() => !!this.swipeHandler && this.swipeHandler.canStart(),
() => this.swipeHandler && this.swipeHandler.onStart(),
step => this.ani && this.ani.progressStep(step),
(shouldComplete, step, dur) => {
if (this.ani) {
this.ani.progressEnd(shouldComplete, step, dur);
}
if (this.swipeHandler) {
this.swipeHandler.onEnd(shouldComplete);
}
}
);
this.swipeHandlerChanged();
}
/**
* Set the root component for the given navigation stack
*/
@Method()
async setRoot(component: ComponentRef, params?: ComponentProps, opts?: RouterOutletOptions): Promise<boolean> {
if (this.activeComponent === component) {
return false;
}
// attach entering view to DOM
const leavingEl = this.activeEl;
const enteringEl = await attachComponent(this.delegate, this.el, component, ['ion-page', 'ion-page-invisible'], params);
this.activeComponent = component;
this.activeEl = enteringEl;
// commit animation
await this.commit(enteringEl, leavingEl, opts);
await detachComponent(this.delegate, leavingEl);
return true;
componentDidUnload() {
this.activeEl = this.activeComponent = undefined;
}
/** @internal */
@@ -121,19 +123,26 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
} : undefined;
}
private async lock() {
const p = this.waitPromise;
let resolve!: () => void;
this.waitPromise = new Promise(r => resolve = r);
if (p !== undefined) {
await p;
private async setRoot(component: ComponentRef, params?: ComponentProps, opts?: RouterOutletOptions): Promise<boolean> {
if (this.activeComponent === component) {
return false;
}
return resolve;
// attach entering view to DOM
const leavingEl = this.activeEl;
const enteringEl = await attachComponent(this.delegate, this.el, component, ['ion-page', 'ion-page-invisible'], params);
this.activeComponent = component;
this.activeEl = enteringEl;
// commit animation
await this.commit(enteringEl, leavingEl, opts);
await detachComponent(this.delegate, leavingEl);
return true;
}
async transition(enteringEl: HTMLElement, leavingEl: HTMLElement | undefined, opts?: RouterOutletOptions): Promise<boolean> {
// isTransitioning acts as a lock to prevent reentering
private async transition(enteringEl: HTMLElement, leavingEl: HTMLElement | undefined, opts: RouterOutletOptions = {}): Promise<boolean> {
if (leavingEl === enteringEl) {
return false;
}
@@ -141,8 +150,6 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
// emit nav will change event
this.ionNavWillChange.emit();
opts = opts || {};
const { mode, queue, animationCtrl, win, el } = this;
const animated = this.animated && this.config.getBoolean('animated', true);
const animationBuilder = this.animation || opts.animationBuilder || this.config.get('navAnimation');
@@ -157,7 +164,10 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
enteringEl,
leavingEl,
baseEl: el,
progressCallback: (opts.progressAnimation
? ani => this.ani = ani
: undefined
),
...opts
});
@@ -167,10 +177,20 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
return true;
}
private async lock() {
const p = this.waitPromise;
let resolve!: () => void;
this.waitPromise = new Promise(r => resolve = r);
if (p !== undefined) {
await p;
}
return resolve;
}
render() {
return [
this.mode === 'ios' && <div class="nav-decor"/>,
return (
<slot></slot>
];
);
}
}

View File

@@ -11,3 +11,4 @@ export const enum ViewLifecycle {
// util functions
export * from './utils/platform';
export * from './utils/config';
export * from './utils/gesture/swipe-back';

View File

@@ -32,9 +32,7 @@ export function iosTransitionAnimation(AnimationC: Animation, navEl: HTMLElement
if (leavingEl && navEl) {
const navDecor = new AnimationC();
navDecor
.addElement(navEl)
.beforeAddClass('show-decor')
.afterRemoveClass('show-decor');
.addElement(navEl);
rootTransition.add(navDecor);
}

View File

@@ -60,8 +60,8 @@ export class GestureController {
this.capturedId = id;
requestedStart.clear();
const event = new CustomEvent('ionGestureCaptured', { detail: gestureName });
this.doc.body.dispatchEvent(event);
const event = new CustomEvent('ionGestureCaptured', { detail: { gestureName } });
this.doc.dispatchEvent(event);
return true;
}
requestedStart.delete(id);
@@ -144,14 +144,16 @@ export class GestureController {
export class GestureDelegate {
private ctrl?: GestureController;
private priority: number;
constructor(
ctrl: GestureController,
private id: number,
private name: string,
private priority: number,
priority: number,
private disableScroll: boolean
) {
this.priority = priority * 1000000 + id;
this.ctrl = ctrl;
}

View File

@@ -0,0 +1,56 @@
import { QueueApi } from '@stencil/core';
import { Gesture, GestureDetail, createGesture } from './gesture';
export function createSwipeBackGesture(
el: HTMLElement,
queue: QueueApi,
canStartHandler: () => boolean,
onStartHandler: () => void,
onMoveHandler: (step: number) => void,
onEndHandler: (shouldComplete: boolean, step: number, dur: number) => void,
): Gesture {
const win = el.ownerDocument!.defaultView!;
function canStart(detail: GestureDetail) {
return detail.startX <= 5000 && canStartHandler();
}
function onMove(detail: GestureDetail) {
// set the transition animation's progress
const delta = detail.deltaX;
const stepValue = delta / win.innerWidth;
onMoveHandler(stepValue);
}
function onEnd(detail: GestureDetail) {
// the swipe back gesture has ended
const delta = detail.deltaX;
const width = win.innerWidth;
const stepValue = delta / width;
const velocity = detail.velocityX;
const z = width / 2.0;
const shouldComplete =
velocity >= 0 && (velocity > 0.2 || detail.deltaX > z);
const missing = shouldComplete ? 1 - stepValue : stepValue;
const missingDistance = missing * width;
let realDur = 0;
if (missingDistance > 5) {
const dur = missingDistance / Math.abs(velocity);
realDur = Math.min(dur, 300);
}
onEndHandler(shouldComplete, stepValue, realDur);
}
return createGesture({
el,
queue,
gestureName: 'goback-swipe',
gesturePriority: 30,
threshold: 10,
canStart,
onStart: onStartHandler,
onMove,
onEnd
});
}

View File

@@ -137,15 +137,15 @@ export function startTapClick(doc: Document) {
}
}
doc.body.addEventListener('click', onBodyClick, true);
doc.body.addEventListener('ionScrollStart', () => {
doc.addEventListener('click', onBodyClick, true);
doc.addEventListener('ionScrollStart', () => {
scrolling = true;
cancelActive();
});
doc.body.addEventListener('ionScrollEnd', () => {
doc.addEventListener('ionScrollEnd', () => {
scrolling = false;
});
doc.body.addEventListener('ionGestureCaptured', cancelActive);
doc.addEventListener('ionGestureCaptured', cancelActive);
doc.addEventListener('touchstart', onTouchStart, true);
doc.addEventListener('touchcancel', onTouchEnd, true);

View File

@@ -38,7 +38,7 @@ export function getClassMap(classes: string | string[] | undefined): CssClassMap
return map;
}
export async function openURL(win: Window, url: string | undefined | null, ev: Event | undefined | null, direction?: RouterDirection): Promise<boolean> {
export async function openURL(win: Window, url: string | undefined | null, ev: Event | undefined | null, direction: RouterDirection): Promise<boolean> {
if (url != null && url[0] !== '#' && url.indexOf('://') === -1) {
const router = win.document.querySelector('ion-router');
if (router) {

View File

@@ -76,16 +76,19 @@ async function getAnimationBuilder(opts: TransitionOptions): Promise<AnimationBu
async function animation(animationBuilder: AnimationBuilder, opts: TransitionOptions): Promise<TransitionResult> {
await waitForReady(opts, true);
const trns = await opts.animationCtrl.create(animationBuilder, opts.baseEl, opts);
fireWillEvents(opts.window, opts.enteringEl, opts.leavingEl);
await playTransition(trns, opts);
const trans = await opts.animationCtrl.create(animationBuilder, opts.baseEl, opts);
fireWillEvents(opts.enteringEl, opts.leavingEl);
await playTransition(trans, opts);
if (opts.progressCallback) {
opts.progressCallback(undefined);
}
if (trns.hasCompleted) {
fireDidEvents(opts.window, opts.enteringEl, opts.leavingEl);
if (trans.hasCompleted) {
fireDidEvents(opts.enteringEl, opts.leavingEl);
}
return {
hasCompleted: trns.hasCompleted,
animation: trns
hasCompleted: trans.hasCompleted,
animation: trans
};
}
@@ -95,8 +98,8 @@ async function noAnimation(opts: TransitionOptions): Promise<TransitionResult> {
await waitForReady(opts, false);
fireWillEvents(opts.window, enteringEl, leavingEl);
fireDidEvents(opts.window, enteringEl, leavingEl);
fireWillEvents(enteringEl, leavingEl);
fireDidEvents(enteringEl, leavingEl);
return {
hasCompleted: true
@@ -144,20 +147,19 @@ function playTransition(trans: Animation, opts: TransitionOptions): Promise<Anim
return promise;
}
function fireWillEvents(win: Window, enteringEl: HTMLElement | undefined, leavingEl: HTMLElement | undefined) {
lifecycle(win, leavingEl, ViewLifecycle.WillLeave);
lifecycle(win, enteringEl, ViewLifecycle.WillEnter);
function fireWillEvents(enteringEl: HTMLElement | undefined, leavingEl: HTMLElement | undefined) {
lifecycle(leavingEl, ViewLifecycle.WillLeave);
lifecycle(enteringEl, ViewLifecycle.WillEnter);
}
function fireDidEvents(win: Window, enteringEl: HTMLElement | undefined, leavingEl: HTMLElement | undefined) {
lifecycle(win, enteringEl, ViewLifecycle.DidEnter);
lifecycle(win, leavingEl, ViewLifecycle.DidLeave);
function fireDidEvents(enteringEl: HTMLElement | undefined, leavingEl: HTMLElement | undefined) {
lifecycle(enteringEl, ViewLifecycle.DidEnter);
lifecycle(leavingEl, ViewLifecycle.DidLeave);
}
export function lifecycle(win: Window, el: HTMLElement | undefined, eventName: ViewLifecycle) {
export function lifecycle(el: HTMLElement | undefined, eventName: ViewLifecycle) {
if (el) {
const CEvent: typeof CustomEvent = (win as any).CustomEvent;
const event = new CEvent(eventName, {
const event = new CustomEvent(eventName, {
bubbles: false,
cancelable: false,
});
@@ -214,7 +216,7 @@ function setZIndex(
export interface TransitionOptions extends NavOptions {
animationCtrl: HTMLIonAnimationControllerElement;
queue: QueueApi;
progressCallback?: ((ani: Animation) => void);
progressCallback?: ((ani: Animation | undefined) => void);
window: Window;
baseEl: HTMLElement;
enteringEl: HTMLElement;