mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c36b1fd2dc | ||
|
|
cc365f829d | ||
|
|
dc8b363ea8 | ||
|
|
a8b90a3eb1 | ||
|
|
d313e69013 | ||
|
|
851aa838fa | ||
|
|
a1f942d45e | ||
|
|
7b264f983b | ||
|
|
9650bec06a | ||
|
|
174d9b5a41 | ||
|
|
c8a27b7a19 |
57
BREAKING.md
57
BREAKING.md
@@ -79,7 +79,7 @@ The `small` and `large` attributes are now combined under the `size` attribute.
|
||||
|
||||
| Old Property | New Property | Property Behavior |
|
||||
| --------------------------- | ------------ | --------------------------- |
|
||||
| `small`, `large` | `size` | Sets the button size. |
|
||||
| `small`, `large` | `size` | Sets the button size. |
|
||||
| `clear`, `outline`, `solid` | `fill` | Sets the button fill style. |
|
||||
| `full`, `block` | `expand` | Sets the button width. |
|
||||
|
||||
@@ -253,7 +253,7 @@ Buttons inside of an `<ion-fab>` container should now be written as an `<ion-fab
|
||||
**New Usage Example:**
|
||||
|
||||
```html
|
||||
<ion-fab top right edge>
|
||||
<ion-fab vertical="top" horizontal="right" edge>
|
||||
<ion-fab-button>
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-fab-button>
|
||||
@@ -274,9 +274,52 @@ Buttons inside of an `<ion-fab>` container should now be written as an `<ion-fab
|
||||
</ion-fab>
|
||||
```
|
||||
|
||||
### Attributes Renamed
|
||||
|
||||
The mutually exclusive boolean attributes to position the fab have been combined into two single string attributes.
|
||||
|
||||
The attributes to align the fab horizontally are now combined under the `horizontal` attribute and the attributes to align the fab vertically are now combined under the `vertical` attribute:
|
||||
|
||||
| Old Property | New Property | Property Behavior |
|
||||
|--------------|----------------------|-------------------------------------------------------------------------|
|
||||
| left | `horizontal="left"` | Positions to the left of the viewport. |
|
||||
| right | `horizontal="right"` | Positions to the right of the viewport. |
|
||||
| center | `horizontal="center"`| Positions to the center of the viewport. |
|
||||
| start | `horizontal="start"` | Positions to the left of the viewport in LTR, and to the right in RTL. |
|
||||
| end | `horizontal="end"` | Positions to the right of the viewport in LTR, and to the left in RTL. |
|
||||
| top | `vertical="top"` | Positions at the top of the viewport. |
|
||||
| bottom | `vertical="bottom"` | Positions at the bottom of the viewport. |
|
||||
| middle | `vertical="center"` | Positions at the center of the viewport. |
|
||||
|
||||
_Note that `middle` has been changed to `center` for the vertical positioning._
|
||||
|
||||
**Old Usage Example:**
|
||||
|
||||
```html
|
||||
<ion-fab top right edge>
|
||||
<!-- fab buttons and lists -->
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab center middle>
|
||||
<!-- fab buttons and lists -->
|
||||
</ion-fab>
|
||||
```
|
||||
|
||||
**New Usage Example:**
|
||||
|
||||
```html
|
||||
<ion-fab vertical="top" horizontal="right" edge>
|
||||
<!-- fab buttons and lists -->
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab vertical="center" horizontal="center">
|
||||
<!-- fab buttons and lists -->
|
||||
</ion-fab>
|
||||
```
|
||||
|
||||
### Fixed Content
|
||||
|
||||
The `<ion-fab>` container was previously placed inside of the fixed content by default. Now, any fixed content should go inside of the `<ion-fixed>` container.
|
||||
The `<ion-fab>` container was previously placed inside of the fixed content by default. Now, any fixed content should use the `fixed` slot.
|
||||
|
||||
**Old Usage Example:**
|
||||
|
||||
@@ -292,11 +335,9 @@ The `<ion-fab>` container was previously placed inside of the fixed content by d
|
||||
**New Usage Example:**
|
||||
|
||||
```html
|
||||
<ion-fixed>
|
||||
<ion-fab top right edge>
|
||||
<!-- fab buttons and lists -->
|
||||
</ion-fab>
|
||||
</ion-fixed>
|
||||
<ion-fab vertical="top" horizontal="right" edge slot="fixed">
|
||||
<!-- fab buttons and lists -->
|
||||
</ion-fab>
|
||||
<ion-content>
|
||||
Scrollable Content
|
||||
</ion-content>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run clean && npm run compile && npm run clean-generated",
|
||||
"build.link": "node scripts/link-copy.js",
|
||||
"build.link": "npm run build && node scripts/link-copy.js",
|
||||
"clean": "node scripts/clean.js",
|
||||
"clean-generated": "node ./scripts/clean-generated.js",
|
||||
"compile": "./node_modules/.bin/ngc",
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { NavController } from '../providers/nav-controller';
|
||||
import { NavParams } from '../providers/nav-params';
|
||||
|
||||
export const NavControllerToken = new InjectionToken<any>('NavControllerToken');
|
||||
export const NavParamsToken = new InjectionToken<any>('NavParamsToken');
|
||||
|
||||
export function getProviders(element: HTMLElement, data: any) {
|
||||
const nearestNavElement = (element.tagName.toLowerCase() === 'ion-nav' ? element : element.closest('ion-nav')) as HTMLIonNavElement;
|
||||
|
||||
return [
|
||||
{
|
||||
provide: NavControllerToken, useValue: nearestNavElement
|
||||
},
|
||||
|
||||
{
|
||||
provide: NavController, useFactory: provideNavControllerInjectable, deps: [NavControllerToken]
|
||||
},
|
||||
|
||||
{
|
||||
provide: NavParamsToken, useValue: data
|
||||
},
|
||||
|
||||
{
|
||||
provide: NavParams, useFactory: provideNavParamsInjectable, deps: [NavParamsToken]
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function provideNavControllerInjectable(element: HTMLIonNavElement) {
|
||||
return new NavController(element);
|
||||
}
|
||||
|
||||
export function provideNavParamsInjectable(data: any) {
|
||||
return new NavParams(data);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import {
|
||||
ComponentFactoryResolver,
|
||||
Directive,
|
||||
ElementRef,
|
||||
Injector,
|
||||
Type,
|
||||
} from '@angular/core';
|
||||
|
||||
import { FrameworkDelegate } from '@ionic/core';
|
||||
|
||||
import { AngularComponentMounter, AngularEscapeHatch } from '..';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-nav',
|
||||
})
|
||||
export class IonNav implements FrameworkDelegate {
|
||||
|
||||
constructor(
|
||||
public elementRef: ElementRef,
|
||||
protected angularComponentMounter: AngularComponentMounter,
|
||||
protected cfr: ComponentFactoryResolver,
|
||||
protected injector: Injector
|
||||
) {
|
||||
|
||||
this.elementRef.nativeElement.delegate = this;
|
||||
}
|
||||
|
||||
attachViewToDom(elementOrContainerToMountTo: HTMLIonNavElement,
|
||||
elementOrComponentToMount: Type<any>,
|
||||
data?: any,
|
||||
classesToAdd?: string[],
|
||||
escapeHatch: AngularEscapeHatch = {}): Promise<any> {
|
||||
|
||||
// wrap whatever the user provides in an ion-page
|
||||
const cfr = escapeHatch.cfr || this.cfr;
|
||||
const injector = escapeHatch.injector || this.injector;
|
||||
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo,
|
||||
null, elementOrComponentToMount, cfr, injector, data, classesToAdd);
|
||||
}
|
||||
|
||||
removeViewFromDom(parentElement: HTMLElement, childElement: HTMLElement) {
|
||||
return this.angularComponentMounter.removeViewFromDom(parentElement, childElement);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +1,24 @@
|
||||
export { IonicAngularModule } from './module';
|
||||
|
||||
/* Navigation */
|
||||
export { IonNav } from './navigation/ion-nav';
|
||||
export { IonRouterOutlet } from './navigation/ion-router-outlet';
|
||||
export { IonTab } from './navigation/ion-tab';
|
||||
export { IonTabs } from './navigation/ion-tabs';
|
||||
|
||||
/* Directives */
|
||||
export { IonNav } from './directives/ion-nav';
|
||||
export { VirtualScroll } from './directives/virtual-scroll';
|
||||
export { VirtualItem } from './directives/virtual-item';
|
||||
export { VirtualHeader } from './directives/virtual-header';
|
||||
export { VirtualFooter } from './directives/virtual-footer';
|
||||
|
||||
/* Router */
|
||||
export { RouterOutlet } from './router/outlet';
|
||||
export { AsyncActivateRoutes } from './router/async-activated-routes';
|
||||
export { OutletInjector } from './router/outlet-injector';
|
||||
export { IonicRouterModule } from './router/router-module';
|
||||
|
||||
/* Providers */
|
||||
export { ActionSheetController, ActionSheetProxy } from './providers/action-sheet-controller';
|
||||
export { AlertController, AlertProxy } from './providers/alert-controller';
|
||||
export { AngularComponentMounter } from './providers/angular-component-mounter';
|
||||
export { App } from './providers/app';
|
||||
export { Events } from './providers/events';
|
||||
export { LoadingController, LoadingProxy } from './providers/loading-controller';
|
||||
export { MenuController } from './providers/menu-controller';
|
||||
export { ModalController, ModalProxy } from './providers/modal-controller';
|
||||
export { NavController } from './providers/nav-controller';
|
||||
export { NavParams } from './providers/nav-params';
|
||||
export { Platform } from './providers/platform';
|
||||
export { PopoverController, PopoverProxy } from './providers/popover-controller';
|
||||
export { ToastController, ToastProxy } from './providers/toast-controller';
|
||||
|
||||
@@ -12,8 +12,13 @@ import { RadioValueAccessor } from './control-value-accessors/radio-value-access
|
||||
import { SelectValueAccessor } from './control-value-accessors/select-value-accessor';
|
||||
import { TextValueAccessor } from './control-value-accessors/text-value-accessor';
|
||||
|
||||
/* Navigation */
|
||||
import { IonNav } from './navigation/ion-nav';
|
||||
import { IonRouterOutlet } from './navigation/ion-router-outlet';
|
||||
import { IonTab } from './navigation/ion-tab';
|
||||
import { IonTabs } from './navigation/ion-tabs';
|
||||
|
||||
/* Directives */
|
||||
import { IonNav } from './directives/ion-nav';
|
||||
import { VirtualScroll } from './directives/virtual-scroll';
|
||||
import { VirtualItem } from './directives/virtual-item';
|
||||
import { VirtualHeader } from './directives/virtual-header';
|
||||
@@ -22,8 +27,6 @@ import { VirtualFooter } from './directives/virtual-footer';
|
||||
/* Providers */
|
||||
import { ActionSheetController } from './providers/action-sheet-controller';
|
||||
import { AlertController } from './providers/alert-controller';
|
||||
import { AngularComponentMounter } from './providers/angular-component-mounter';
|
||||
import { App } from './providers/app';
|
||||
import { Events, setupProvideEvents } from './providers/events';
|
||||
import { LoadingController } from './providers/loading-controller';
|
||||
import { MenuController } from './providers/menu-controller';
|
||||
@@ -36,6 +39,9 @@ import { ToastController } from './providers/toast-controller';
|
||||
declarations: [
|
||||
BooleanValueAccessor,
|
||||
IonNav,
|
||||
IonRouterOutlet,
|
||||
IonTab,
|
||||
IonTabs,
|
||||
NumericValueAccessor,
|
||||
RadioValueAccessor,
|
||||
SelectValueAccessor,
|
||||
@@ -48,6 +54,9 @@ import { ToastController } from './providers/toast-controller';
|
||||
exports: [
|
||||
BooleanValueAccessor,
|
||||
IonNav,
|
||||
IonRouterOutlet,
|
||||
IonTab,
|
||||
IonTabs,
|
||||
NumericValueAccessor,
|
||||
RadioValueAccessor,
|
||||
SelectValueAccessor,
|
||||
@@ -63,8 +72,7 @@ import { ToastController } from './providers/toast-controller';
|
||||
],
|
||||
providers: [
|
||||
ModalController,
|
||||
PopoverController,
|
||||
AngularComponentMounter
|
||||
PopoverController
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
@@ -77,7 +85,6 @@ export class IonicAngularModule {
|
||||
providers: [
|
||||
AlertController,
|
||||
ActionSheetController,
|
||||
App,
|
||||
Events,
|
||||
LoadingController,
|
||||
MenuController,
|
||||
|
||||
32
packages/angular/src/navigation/ion-nav.ts
Normal file
32
packages/angular/src/navigation/ion-nav.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Component, ViewEncapsulation } from '@angular/core';
|
||||
import {
|
||||
NavigationEnd,
|
||||
NavigationStart,
|
||||
Router
|
||||
} from '@angular/router';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ion-nav',
|
||||
template: '<ng-content></ng-content>',
|
||||
styles: [`
|
||||
ion-nav > :not(.show-page) { display: none; }
|
||||
`],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class IonNav {
|
||||
|
||||
constructor(router: Router) {
|
||||
console.log('ion-nav');
|
||||
|
||||
router.events.subscribe(ev => {
|
||||
if (ev instanceof NavigationStart) {
|
||||
console.log('NavigationStart', ev.url);
|
||||
|
||||
} else if (ev instanceof NavigationEnd) {
|
||||
console.log('NavigationEnd', ev.url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
160
packages/angular/src/navigation/ion-router-outlet.ts
Normal file
160
packages/angular/src/navigation/ion-router-outlet.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, EventEmitter, Injector, OnDestroy, OnInit, Output, ViewContainerRef } from '@angular/core';
|
||||
import { ActivatedRoute, ChildrenOutletContexts } from '@angular/router';
|
||||
import * as ctrl from './router-controller';
|
||||
import { runTransition } from './router-transition';
|
||||
|
||||
|
||||
@Directive({selector: 'ion-router-outlet', exportAs: 'ionOutlet'})
|
||||
export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
private activated: ComponentRef<any>|null = null;
|
||||
private _activatedRoute: ActivatedRoute|null = null;
|
||||
private name: string;
|
||||
|
||||
private views: ctrl.RouteView[] = [];
|
||||
|
||||
@Output('activate') activateEvents = new EventEmitter<any>();
|
||||
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
|
||||
|
||||
constructor(
|
||||
private parentContexts: ChildrenOutletContexts, private location: ViewContainerRef,
|
||||
private resolver: ComponentFactoryResolver, @Attribute('name') name: string,
|
||||
private changeDetector: ChangeDetectorRef) {
|
||||
this.name = name || 'primary';
|
||||
parentContexts.onChildOutletCreated(this.name, this as any);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
ctrl.destoryViews(this.views);
|
||||
this.parentContexts.onChildOutletDestroyed(this.name);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.activated) {
|
||||
// If the outlet was not instantiated at the time the route got activated we need to populate
|
||||
// the outlet when it is initialized (ie inside a NgIf)
|
||||
const context = this.parentContexts.getContext(this.name);
|
||||
if (context && context.route) {
|
||||
if (context.attachRef) {
|
||||
// `attachRef` is populated when there is an existing component to mount
|
||||
this.attach(context.attachRef, context.route);
|
||||
} else {
|
||||
// otherwise the component defined in the configuration is created
|
||||
this.activateWith(context.route, context.resolver || null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get isActivated(): boolean { return !!this.activated; }
|
||||
|
||||
get component(): Object {
|
||||
if (!this.activated) throw new Error('Outlet is not activated');
|
||||
return this.activated.instance;
|
||||
}
|
||||
|
||||
get activatedRoute(): ActivatedRoute {
|
||||
if (!this.activated) throw new Error('Outlet is not activated');
|
||||
return this._activatedRoute as ActivatedRoute;
|
||||
}
|
||||
|
||||
get activatedRouteData() {
|
||||
if (this._activatedRoute) {
|
||||
return this._activatedRoute.snapshot.data;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the `RouteReuseStrategy` instructs to detach the subtree
|
||||
*/
|
||||
detach(): ComponentRef<any> {
|
||||
if (!this.activated) throw new Error('Outlet is not activated');
|
||||
this.location.detach();
|
||||
const cmp = this.activated;
|
||||
this.activated = null;
|
||||
this._activatedRoute = null;
|
||||
return cmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree
|
||||
*/
|
||||
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute) {
|
||||
this.activated = ref;
|
||||
this._activatedRoute = activatedRoute;
|
||||
|
||||
ctrl.attachView(this.views, this.location, ref, activatedRoute);
|
||||
}
|
||||
|
||||
deactivate(): void {
|
||||
if (this.activated) {
|
||||
const c = this.component;
|
||||
|
||||
ctrl.deactivateView(this.views, this.activated);
|
||||
|
||||
this.activated = null;
|
||||
this._activatedRoute = null;
|
||||
this.deactivateEvents.emit(c);
|
||||
}
|
||||
}
|
||||
|
||||
activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null) {
|
||||
if (this.isActivated) {
|
||||
throw new Error('Cannot activate an already activated outlet');
|
||||
}
|
||||
|
||||
this._activatedRoute = activatedRoute;
|
||||
|
||||
const existingView = ctrl.getExistingView(this.views, activatedRoute);
|
||||
if (existingView) {
|
||||
// we've already got a view hanging around
|
||||
this.activated = existingView.ref;
|
||||
|
||||
} else {
|
||||
// haven't created this view yet
|
||||
const snapshot = (activatedRoute as any)._futureSnapshot;
|
||||
|
||||
const component = <any>snapshot.routeConfig !.component;
|
||||
resolver = resolver || this.resolver;
|
||||
|
||||
const factory = resolver.resolveComponentFactory(component);
|
||||
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
|
||||
|
||||
const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
|
||||
this.activated = this.location.createComponent(factory, this.location.length, injector);
|
||||
|
||||
// keep a ref
|
||||
ctrl.initRouteViewElm(this.views, this.activated, activatedRoute);
|
||||
}
|
||||
|
||||
// Calling `markForCheck` to make sure we will run the change detection when the
|
||||
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
|
||||
this.changeDetector.markForCheck();
|
||||
|
||||
const lastDeactivatedRef = ctrl.getLastDeactivatedRef(this.views);
|
||||
|
||||
runTransition(this.activated, lastDeactivatedRef).then(() => {
|
||||
console.log('transition end');
|
||||
this.activateEvents.emit(this.activated.instance);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class OutletInjector implements Injector {
|
||||
constructor(
|
||||
private route: ActivatedRoute, private childContexts: ChildrenOutletContexts,
|
||||
private parent: Injector) {}
|
||||
|
||||
get(token: any, notFoundValue?: any): any {
|
||||
if (token === ActivatedRoute) {
|
||||
return this.route;
|
||||
}
|
||||
|
||||
if (token === ChildrenOutletContexts) {
|
||||
return this.childContexts;
|
||||
}
|
||||
|
||||
return this.parent.get(token, notFoundValue);
|
||||
}
|
||||
}
|
||||
17
packages/angular/src/navigation/ion-tab.ts
Normal file
17
packages/angular/src/navigation/ion-tab.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Directive, ElementRef, Input, OnInit } from '@angular/core';
|
||||
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-tab'
|
||||
})
|
||||
export class IonTab implements OnInit {
|
||||
|
||||
@Input() tabLink: string;
|
||||
|
||||
constructor(private elementRef: ElementRef) {}
|
||||
|
||||
ngOnInit() {
|
||||
console.log('routerLink', this.tabLink, this.elementRef.nativeElement);
|
||||
}
|
||||
|
||||
}
|
||||
25
packages/angular/src/navigation/ion-tabs.ts
Normal file
25
packages/angular/src/navigation/ion-tabs.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Directive, HostListener } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-tabs'
|
||||
})
|
||||
export class IonTabs {
|
||||
|
||||
constructor(private router: Router) {}
|
||||
|
||||
@HostListener('ionTabbarClick', ['$event'])
|
||||
ionTabbarClick(ev: UIEvent) {
|
||||
console.log('ionTabbarClick', ev);
|
||||
|
||||
const tabElm: HTMLIonTabElement = ev.detail as any;
|
||||
if (tabElm && tabElm.href) {
|
||||
console.log('tabElm', tabElm.href);
|
||||
|
||||
this.router.navigateByUrl(tabElm.href);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
81
packages/angular/src/navigation/router-controller.ts
Normal file
81
packages/angular/src/navigation/router-controller.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { ComponentRef, ViewContainerRef } from '@angular/core';
|
||||
import { ActivatedRoute, UrlSegment } from '@angular/router';
|
||||
|
||||
|
||||
export function attachView(views: RouteView[], location: ViewContainerRef, ref: ComponentRef<any>, activatedRoute: ActivatedRoute) {
|
||||
initRouteViewElm(views, ref, activatedRoute);
|
||||
location.insert(ref.hostView);
|
||||
}
|
||||
|
||||
|
||||
export function initRouteViewElm(views: RouteView[], ref: ComponentRef<any>, activatedRoute: ActivatedRoute) {
|
||||
views.push({
|
||||
ref: ref,
|
||||
urlKey: getUrlKey(activatedRoute),
|
||||
deactivatedId: -1
|
||||
});
|
||||
|
||||
(ref.location.nativeElement as HTMLElement).classList.add('ion-page');
|
||||
}
|
||||
|
||||
|
||||
export function getExistingView(views: RouteView[], activatedRoute: ActivatedRoute) {
|
||||
return views.find(vw => {
|
||||
return isMatchingActivatedRoute(vw.urlKey, activatedRoute);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function isMatchingActivatedRoute(existingUrlKey: string, activatedRoute: ActivatedRoute) {
|
||||
const activatedUrlKey = getUrlKey(activatedRoute);
|
||||
|
||||
return activatedUrlKey === existingUrlKey;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
function getUrlKey(activatedRoute: ActivatedRoute) {
|
||||
const url: UrlSegment[] = (activatedRoute.url as any).value;
|
||||
|
||||
return url.map(u => {
|
||||
return u.path + '$$' + JSON.stringify(u.parameters);
|
||||
}).join('/');
|
||||
}
|
||||
|
||||
|
||||
export function deactivateView(views: RouteView[], ref: ComponentRef<any>) {
|
||||
const view = views.find(vw => vw.ref === ref);
|
||||
if (view) {
|
||||
view.deactivatedId = deactivatedIds++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function destoryViews(views: RouteView[]) {
|
||||
views.forEach(vw => {
|
||||
vw.ref.destroy();
|
||||
});
|
||||
views.length = 0;
|
||||
}
|
||||
|
||||
|
||||
export interface RouteView {
|
||||
urlKey: string;
|
||||
ref: ComponentRef<any>;
|
||||
deactivatedId: number;
|
||||
}
|
||||
|
||||
|
||||
let deactivatedIds = 0;
|
||||
34
packages/angular/src/navigation/router-transition.ts
Normal file
34
packages/angular/src/navigation/router-transition.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { ComponentRef } from '@angular/core';
|
||||
|
||||
|
||||
export function runTransition(enteringRef: ComponentRef<any>, leavingRef: ComponentRef<any>): Promise<void> {
|
||||
const enteringElm = (enteringRef && enteringRef.location && enteringRef.location.nativeElement);
|
||||
const leavingElm = (leavingRef && leavingRef.location && leavingRef.location.nativeElement);
|
||||
|
||||
if (!enteringElm && !leavingElm) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return transition(enteringElm, leavingElm);
|
||||
}
|
||||
|
||||
|
||||
function transition(enteringElm: HTMLElement, leavingElm: HTMLElement): Promise<void> {
|
||||
console.log('transition start');
|
||||
|
||||
return new Promise(resolve => {
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
if (enteringElm) {
|
||||
enteringElm.classList.add('show-page');
|
||||
}
|
||||
|
||||
if (leavingElm) {
|
||||
leavingElm.classList.remove('show-page');
|
||||
}
|
||||
|
||||
resolve();
|
||||
}, 750);
|
||||
});
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import {
|
||||
ApplicationRef,
|
||||
ComponentFactoryResolver,
|
||||
Injectable,
|
||||
Injector,
|
||||
NgZone,
|
||||
Type
|
||||
} from '@angular/core';
|
||||
|
||||
import { getProviders } from '../di/di';
|
||||
import { AngularMountingData } from '../types/interfaces';
|
||||
|
||||
const elementToComponentRefMap = new Map<HTMLElement, AngularMountingData>();
|
||||
|
||||
@Injectable()
|
||||
export class AngularComponentMounter {
|
||||
|
||||
constructor(private defaultCfr: ComponentFactoryResolver, private zone: NgZone, private appRef: ApplicationRef) {
|
||||
}
|
||||
|
||||
attachViewToDom(parentElement: HTMLElement, hostElement: HTMLElement, componentToMount: Type<any>, componentResolveFactory: ComponentFactoryResolver, injector: Injector, data: any, classesToAdd: string[]): Promise<AngularMountingData> {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.zone.run(() => {
|
||||
|
||||
const crf = componentResolveFactory ? componentResolveFactory : this.defaultCfr;
|
||||
|
||||
const mountingData = this.attachViewToDomImpl(crf, parentElement, hostElement, componentToMount, injector, this.appRef, data, classesToAdd);
|
||||
resolve(mountingData);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeViewFromDom(parentElement: HTMLElement, childElement: HTMLElement): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
this.zone.run(() => {
|
||||
removeViewFromDom(parentElement, childElement);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
attachViewToDomImpl(crf: ComponentFactoryResolver, parentElement: HTMLElement, hostElement: HTMLElement, componentToMount: Type<any>, injector: Injector, appRef: ApplicationRef, data: any, classesToAdd: string[]): AngularMountingData {
|
||||
|
||||
|
||||
const componentFactory = crf.resolveComponentFactory(componentToMount);
|
||||
if (!hostElement) {
|
||||
hostElement = document.createElement(componentFactory.selector);
|
||||
}
|
||||
|
||||
const childInjector = Injector.create(getProviders(parentElement, data), injector);
|
||||
const componentRef = componentFactory.create(childInjector, [], hostElement);
|
||||
for (const clazz of classesToAdd) {
|
||||
hostElement.classList.add(clazz);
|
||||
}
|
||||
|
||||
parentElement.appendChild(hostElement);
|
||||
|
||||
appRef.attachView(componentRef.hostView);
|
||||
|
||||
const mountingData = {
|
||||
component: componentToMount,
|
||||
componentFactory,
|
||||
childInjector,
|
||||
componentRef,
|
||||
instance: componentRef.instance,
|
||||
angularHostElement: componentRef.location.nativeElement,
|
||||
element: hostElement,
|
||||
data
|
||||
};
|
||||
|
||||
elementToComponentRefMap.set(hostElement, mountingData);
|
||||
|
||||
return mountingData;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLElement) {
|
||||
const mountingData = elementToComponentRefMap.get(childElement);
|
||||
if (mountingData) {
|
||||
mountingData.componentRef.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { PublicNav } from '@ionic/core';
|
||||
|
||||
export class App {
|
||||
|
||||
_element: HTMLIonAppElement;
|
||||
constructor() {
|
||||
this._element = document.querySelector('ion-app');
|
||||
}
|
||||
|
||||
setTitle(title: string) {
|
||||
document.title = title;
|
||||
}
|
||||
|
||||
getRootNavs(): PublicNav[] {
|
||||
return getRootNavsImpl(this);
|
||||
}
|
||||
|
||||
getRootNavsAsync(): Promise<PublicNav[]> {
|
||||
return getRootNavsAsyncImpl(this);
|
||||
}
|
||||
|
||||
getTopNavs(rootNavId?: number): PublicNav[] {
|
||||
return getTopNavsImpl(this, rootNavId);
|
||||
}
|
||||
|
||||
getTopNavsAsync(rootNavId?: number): Promise<PublicNav[]> {
|
||||
return getTopNavsAsyncImpl(this, rootNavId);
|
||||
}
|
||||
|
||||
getNavByIdOrName(nameOrId: number | string): PublicNav {
|
||||
return getNavByIdOrNameImpl(this, nameOrId);
|
||||
}
|
||||
|
||||
getNavByIdOrNameAsync(nameOrId: number | string): Promise<PublicNav> {
|
||||
return getNavByIdOrNameAsyncImpl(this, nameOrId);
|
||||
}
|
||||
|
||||
registerBackButtonAction(fn: Function, priority = 0): Promise<() => void> {
|
||||
return this._element.componentOnReady().then(() => {
|
||||
return this._element.registerBackButtonAction(fn, priority);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getRootNavsImpl(app: App) {
|
||||
if (app._element && app._element.getRootNavs) {
|
||||
return app._element.getRootNavs();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getRootNavsAsyncImpl(app: App) {
|
||||
return app._element.componentOnReady().then(() => {
|
||||
return app._element.getRootNavs();
|
||||
});
|
||||
}
|
||||
|
||||
export function getTopNavsImpl(app: App, rootNavId?: number): PublicNav[] {
|
||||
if (app._element && app._element.getTopNavs) {
|
||||
return app._element.getTopNavs(rootNavId);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getTopNavsAsyncImpl(app: App, rootNavId?: number): Promise<PublicNav[]> {
|
||||
return app._element.componentOnReady().then(() => {
|
||||
return app._element.getTopNavs(rootNavId);
|
||||
});
|
||||
}
|
||||
|
||||
export function getNavByIdOrNameImpl(app: App, nameOrId: number | string): PublicNav {
|
||||
if (app._element && app._element.getNavByIdOrName) {
|
||||
return app._element.getNavByIdOrName(nameOrId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getNavByIdOrNameAsyncImpl(app: App, nameOrId: number | string): Promise<PublicNav> {
|
||||
return app._element.componentOnReady().then(() => {
|
||||
return app._element.getNavByIdOrName(nameOrId);
|
||||
});
|
||||
}
|
||||
@@ -1,31 +1,20 @@
|
||||
import {
|
||||
ComponentFactoryResolver,
|
||||
Injectable,
|
||||
Injector,
|
||||
Type,
|
||||
} from '@angular/core';
|
||||
|
||||
import {
|
||||
FrameworkDelegate,
|
||||
ModalDismissEvent,
|
||||
ModalOptions
|
||||
} from '@ionic/core';
|
||||
|
||||
import { AngularComponentMounter } from '../providers/angular-component-mounter';
|
||||
import { AngularMountingData } from '../types/interfaces';
|
||||
|
||||
import { ensureElementInBody, hydrateElement } from '../util/util';
|
||||
|
||||
let modalId = 0;
|
||||
|
||||
@Injectable()
|
||||
export class ModalController implements FrameworkDelegate {
|
||||
|
||||
constructor(private angularComponentMounter: AngularComponentMounter, private componentResolveFactory: ComponentFactoryResolver, private injector: Injector) {
|
||||
}
|
||||
export class ModalController {
|
||||
|
||||
create(opts?: ModalOptions): ModalProxy {
|
||||
opts.delegate = this;
|
||||
return getModalProxy(opts);
|
||||
}
|
||||
|
||||
@@ -36,14 +25,6 @@ export class ModalController implements FrameworkDelegate {
|
||||
});
|
||||
}
|
||||
|
||||
attachViewToDom(elementOrContainerToMountTo: HTMLElement, elementOrComponentToMount: Type<any>, data?: any, classesToAdd?: string[]): Promise<AngularMountingData> {
|
||||
|
||||
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo, null, elementOrComponentToMount, this.componentResolveFactory, this.injector, data, classesToAdd);
|
||||
}
|
||||
|
||||
removeViewFromDom(parentElement: HTMLElement, childElement: HTMLElement) {
|
||||
return this.angularComponentMounter.removeViewFromDom(parentElement, childElement);
|
||||
}
|
||||
}
|
||||
|
||||
export function getModalProxy(opts: ModalOptions) {
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import { NavOptions, PublicNav, PublicViewController } from '@ionic/core';
|
||||
import { hydrateElement } from '../util/util';
|
||||
|
||||
export class NavController implements PublicNav {
|
||||
constructor(public element: HTMLIonNavElement) {
|
||||
}
|
||||
|
||||
push(component: any, data?: any, opts?: NavOptions): Promise<any> {
|
||||
return hydrateElement(this.element).then((navElement: HTMLIonNavElement) => {
|
||||
return navElement.push(component, data, opts);
|
||||
});
|
||||
}
|
||||
|
||||
pop(opts?: NavOptions): Promise<any> {
|
||||
return hydrateElement(this.element).then((navElement: HTMLIonNavElement) => {
|
||||
return navElement.pop(opts);
|
||||
});
|
||||
}
|
||||
|
||||
setRoot(component: any, data?: any, opts?: NavOptions): Promise<any> {
|
||||
return hydrateElement(this.element).then((navElement: HTMLIonNavElement) => {
|
||||
return navElement.setRoot(component, data, opts);
|
||||
});
|
||||
}
|
||||
|
||||
insert(insertIndex: number, page: any, params?: any, opts?: NavOptions): Promise<any> {
|
||||
return hydrateElement(this.element).then((navElement: HTMLIonNavElement) => {
|
||||
return navElement.insert(insertIndex, page, params, opts);
|
||||
});
|
||||
}
|
||||
|
||||
insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions): Promise<any> {
|
||||
return hydrateElement(this.element).then((navElement: HTMLIonNavElement) => {
|
||||
return navElement.insertPages(insertIndex, insertPages, opts);
|
||||
});
|
||||
}
|
||||
|
||||
popToRoot(opts?: NavOptions): Promise<any> {
|
||||
return hydrateElement(this.element).then((navElement: HTMLIonNavElement) => {
|
||||
return navElement.popToRoot(opts);
|
||||
});
|
||||
}
|
||||
|
||||
popTo(indexOrViewCtrl: any, opts?: NavOptions): Promise<any> {
|
||||
return hydrateElement(this.element).then((navElement: HTMLIonNavElement) => {
|
||||
return navElement.popTo(indexOrViewCtrl, opts);
|
||||
});
|
||||
}
|
||||
|
||||
removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions): Promise<any> {
|
||||
return hydrateElement(this.element).then((navElement: HTMLIonNavElement) => {
|
||||
return navElement.removeIndex(startIndex, removeCount, opts);
|
||||
});
|
||||
}
|
||||
|
||||
removeView(viewController: PublicViewController, opts?: NavOptions): Promise<any> {
|
||||
return hydrateElement(this.element).then((navElement: HTMLIonNavElement) => {
|
||||
return navElement.removeView(viewController, opts);
|
||||
});
|
||||
}
|
||||
|
||||
setPages(componentDataPairs: any[], opts?: NavOptions): Promise<any> {
|
||||
return hydrateElement(this.element).then((navElement: HTMLIonNavElement) => {
|
||||
return navElement.setPages(componentDataPairs, opts);
|
||||
});
|
||||
}
|
||||
|
||||
getActive(): PublicViewController {
|
||||
if (this.element.getActive) {
|
||||
return this.element.getActive();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getPrevious(view?: PublicViewController): PublicViewController {
|
||||
if (this.element.getPrevious) {
|
||||
return this.element.getPrevious(view);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
canGoBack(): boolean {
|
||||
if (this.element.canGoBack) {
|
||||
return this.element.canGoBack();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
canSwipeBack(): boolean {
|
||||
if (this.element.canSwipeBack) {
|
||||
return this.element.canSwipeBack();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
first(): PublicViewController {
|
||||
if (this.element.first) {
|
||||
return this.element.first();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
last(): PublicViewController {
|
||||
if (this.element.last) {
|
||||
return this.element.last();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getViews(): PublicViewController[] {
|
||||
if (this.element.getViews) {
|
||||
return this.element.getViews();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getChildNavs(): PublicNav[] {
|
||||
if (this.element.getChildNavs) {
|
||||
return this.element.getChildNavs();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
export class NavParams {
|
||||
|
||||
constructor(public data: any = {}) {
|
||||
}
|
||||
|
||||
get(param: string): any {
|
||||
return this.data[param];
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,20 @@
|
||||
import {
|
||||
ComponentFactoryResolver,
|
||||
Injectable,
|
||||
Injector,
|
||||
Type,
|
||||
} from '@angular/core';
|
||||
|
||||
import {
|
||||
FrameworkDelegate,
|
||||
PopoverDismissEvent,
|
||||
PopoverOptions
|
||||
} from '@ionic/core';
|
||||
|
||||
import { AngularComponentMounter } from '../providers/angular-component-mounter';
|
||||
import { AngularMountingData } from '../types/interfaces';
|
||||
|
||||
import { ensureElementInBody, hydrateElement } from '../util/util';
|
||||
|
||||
let popoverId = 0;
|
||||
|
||||
@Injectable()
|
||||
export class PopoverController implements FrameworkDelegate {
|
||||
|
||||
constructor(private angularComponentMounter: AngularComponentMounter, private componentResolveFactory: ComponentFactoryResolver, private injector: Injector) {
|
||||
}
|
||||
export class PopoverController {
|
||||
|
||||
create(opts?: PopoverOptions): PopoverProxy {
|
||||
opts.delegate = this;
|
||||
return getPopoverProxy(opts);
|
||||
}
|
||||
|
||||
@@ -35,15 +24,6 @@ export class PopoverController implements FrameworkDelegate {
|
||||
return popoverController.dismiss(data, role, id);
|
||||
});
|
||||
}
|
||||
|
||||
attachViewToDom(elementOrContainerToMountTo: HTMLElement, elementOrComponentToMount: Type<any>, data?: any, classesToAdd?: string[]): Promise<AngularMountingData> {
|
||||
|
||||
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo, null, elementOrComponentToMount, this.componentResolveFactory, this.injector, data, classesToAdd);
|
||||
}
|
||||
|
||||
removeViewFromDom(parentElement: HTMLElement, childElement: HTMLElement) {
|
||||
return this.angularComponentMounter.removeViewFromDom(parentElement, childElement);
|
||||
}
|
||||
}
|
||||
|
||||
export function getPopoverProxy(opts: PopoverOptions) {
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
import {
|
||||
ComponentRef
|
||||
} from '@angular/core';
|
||||
|
||||
import {
|
||||
ActivatedRoute,
|
||||
ActivatedRouteSnapshot,
|
||||
ActivationEnd,
|
||||
ChildActivationEnd,
|
||||
ChildrenOutletContexts,
|
||||
Event,
|
||||
RouteReuseStrategy,
|
||||
RouterState,
|
||||
} from '@angular/router';
|
||||
|
||||
import {
|
||||
TreeNode,
|
||||
advanceActivatedRoute,
|
||||
forEach,
|
||||
nodeChildrenAsMap
|
||||
} from './router-utils';
|
||||
|
||||
export class AsyncActivateRoutes {
|
||||
constructor(
|
||||
protected routeReuseStrategy: RouteReuseStrategy, protected futureState: RouterState,
|
||||
protected currState: RouterState, protected forwardEvent: (evt: Event) => void) {}
|
||||
|
||||
activate(parentContexts: ChildrenOutletContexts): void | Promise<void> {
|
||||
const futureRoot = (this.futureState as any)._root;
|
||||
const currRoot = this.currState ? (this.currState as any)._root : null;
|
||||
|
||||
const result = this.deactivateChildRoutes(futureRoot, currRoot, parentContexts);
|
||||
return Promise.resolve(result)
|
||||
.then(
|
||||
() => {
|
||||
advanceActivatedRoute(this.futureState.root);
|
||||
return this.activateChildRoutes(futureRoot, currRoot, parentContexts);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// De-activate the child route that are not re-used for the future state
|
||||
protected deactivateChildRoutes(
|
||||
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>|null,
|
||||
contexts: ChildrenOutletContexts): Promise<any> {
|
||||
|
||||
const children: {[outletName: string]: TreeNode<ActivatedRoute>} = nodeChildrenAsMap(currNode);
|
||||
|
||||
const promises = futureNode.children.map((futureChild: TreeNode<ActivatedRoute>) => {
|
||||
const childOutletName = futureChild.value.outlet;
|
||||
const promise = this.deactivateRoutes(futureChild, children[childOutletName], contexts);
|
||||
promise
|
||||
.then(
|
||||
() => {
|
||||
delete children[childOutletName];
|
||||
}
|
||||
);
|
||||
return promise;
|
||||
});
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(
|
||||
() => {
|
||||
const promises: Promise<void>[] = [];
|
||||
// De-activate the routes that will not be re-used
|
||||
forEach(children, (v: TreeNode<ActivatedRoute>) => {
|
||||
promises.push(this.deactivateRouteAndItsChildren(v, contexts));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected deactivateRoutes(
|
||||
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>,
|
||||
parentContext: ChildrenOutletContexts): Promise<void> {
|
||||
const future = futureNode.value;
|
||||
const curr = currNode ? currNode.value : null;
|
||||
|
||||
if (future === curr) {
|
||||
// Reusing the node, check to see if the children need to be de-activated
|
||||
if (future.component) {
|
||||
// If we have a normal route, we need to go through an outlet.
|
||||
const context = parentContext.getContext(future.outlet);
|
||||
if (context) {
|
||||
return this.deactivateChildRoutes(futureNode, currNode, context.children);
|
||||
}
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
// if we have a componentless route, we recurse but keep the same outlet map.
|
||||
return this.deactivateChildRoutes(futureNode, currNode, parentContext);
|
||||
}
|
||||
} else {
|
||||
if (curr) {
|
||||
// Deactivate the current route which will not be re-used
|
||||
return this.deactivateRouteAndItsChildren(currNode, parentContext);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
protected deactivateRouteAndItsChildren(
|
||||
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): Promise<void> {
|
||||
if (this.routeReuseStrategy.shouldDetach(route.value.snapshot)) {
|
||||
return this.detachAndStoreRouteSubtree(route, parentContexts);
|
||||
} else {
|
||||
return this.deactivateRouteAndOutlet(route, parentContexts);
|
||||
}
|
||||
}
|
||||
|
||||
protected detachAndStoreRouteSubtree(
|
||||
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): Promise<void> {
|
||||
const context = parentContexts.getContext(route.value.outlet);
|
||||
if (context && context.outlet) {
|
||||
const componentRefOrPromise = context.outlet.detach();
|
||||
return Promise.resolve(componentRefOrPromise)
|
||||
.then(
|
||||
(componentRef: ComponentRef<any>) => {
|
||||
const contexts = context.children.onOutletDeactivated();
|
||||
this.routeReuseStrategy.store(route.value.snapshot, {componentRef, route, contexts});
|
||||
}
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
protected deactivateRouteAndOutlet(
|
||||
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): Promise<void> {
|
||||
const context = parentContexts.getContext(route.value.outlet);
|
||||
|
||||
if (context) {
|
||||
const children: {[outletName: string]: any} = nodeChildrenAsMap(route);
|
||||
const contexts = route.value.component ? context.children : parentContexts;
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
forEach(children, (v: any) => {
|
||||
promises.push(this.deactivateRouteAndItsChildren(v, contexts));
|
||||
});
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(
|
||||
() => {
|
||||
if (context.outlet) {
|
||||
// Destroy the component
|
||||
const result = context.outlet.deactivate();
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
context.children.onOutletDeactivated();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
protected activateChildRoutes(
|
||||
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>|null,
|
||||
contexts: ChildrenOutletContexts): Promise<void> {
|
||||
|
||||
const children: {[outlet: string]: any} = nodeChildrenAsMap(currNode);
|
||||
|
||||
|
||||
const promises = futureNode.children.map((c: TreeNode<ActivatedRoute>) => {
|
||||
const promise = this.activateRoutes(c, children[c.value.outlet], contexts);
|
||||
promise
|
||||
.then(
|
||||
() => {
|
||||
this.forwardEvent(new ActivationEnd(c.value.snapshot));
|
||||
}
|
||||
);
|
||||
return promise;
|
||||
});
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(
|
||||
() => {
|
||||
if (futureNode.children.length) {
|
||||
this.forwardEvent(new ChildActivationEnd(futureNode.value.snapshot));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected activateRoutes(
|
||||
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>,
|
||||
parentContexts: ChildrenOutletContexts): Promise<void> {
|
||||
const future = futureNode.value;
|
||||
const curr = currNode ? currNode.value : null;
|
||||
|
||||
advanceActivatedRoute(future);
|
||||
|
||||
// reusing the node
|
||||
if (future === curr) {
|
||||
if (future.component) {
|
||||
// If we have a normal route, we need to go through an outlet.
|
||||
const context = parentContexts.getOrCreateContext(future.outlet);
|
||||
return this.activateChildRoutes(futureNode, currNode, context.children);
|
||||
} else {
|
||||
// if we have a componentless route, we recurse but keep the same outlet map.
|
||||
return this.activateChildRoutes(futureNode, currNode, parentContexts);
|
||||
}
|
||||
} else {
|
||||
if (future.component) {
|
||||
// if we have a normal route, we need to place the component into the outlet and recurse.
|
||||
const context = parentContexts.getOrCreateContext(future.outlet);
|
||||
|
||||
if (this.routeReuseStrategy.shouldAttach(future.snapshot)) {
|
||||
const stored =
|
||||
(<any>this.routeReuseStrategy.retrieve(future.snapshot));
|
||||
this.routeReuseStrategy.store(future.snapshot, null);
|
||||
context.children.onOutletReAttached(stored.contexts);
|
||||
context.attachRef = stored.componentRef;
|
||||
context.route = stored.route.value;
|
||||
if (context.outlet) {
|
||||
// Attach right away when the outlet has already been instantiated
|
||||
// Otherwise attach from `RouterOutlet.ngOnInit` when it is instantiated
|
||||
const result = context.outlet.attach(stored.componentRef, stored.route.value);
|
||||
return Promise.resolve(result)
|
||||
.then(
|
||||
() => {
|
||||
return this.advanceActivatedRouteNodeAndItsChildren(stored.route);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(this.advanceActivatedRouteNodeAndItsChildren(stored.route));
|
||||
|
||||
} else {
|
||||
const config = this.parentLoadedConfig(future.snapshot);
|
||||
const cmpFactoryResolver = config ? config.module.componentFactoryResolver : null;
|
||||
|
||||
context.route = future;
|
||||
context.resolver = cmpFactoryResolver;
|
||||
if (context.outlet) {
|
||||
// Activate the outlet when it has already been instantiated
|
||||
// Otherwise it will get activated from its `ngOnInit` when instantiated
|
||||
const result = context.outlet.activateWith(future, cmpFactoryResolver);
|
||||
return Promise.resolve(result)
|
||||
.then(
|
||||
() => {
|
||||
return this.activateChildRoutes(futureNode, null, context.children);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return this.activateChildRoutes(futureNode, null, context.children);
|
||||
}
|
||||
} else {
|
||||
// if we have a componentless route, we recurse but keep the same outlet map.
|
||||
return this.activateChildRoutes(futureNode, null, parentContexts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
advanceActivatedRouteNodeAndItsChildren(node: TreeNode<ActivatedRoute>): void {
|
||||
advanceActivatedRoute(node.value);
|
||||
node.children.forEach(this.advanceActivatedRouteNodeAndItsChildren);
|
||||
}
|
||||
|
||||
parentLoadedConfig(snapshot: ActivatedRouteSnapshot): any|null {
|
||||
for (let s = snapshot.parent; s; s = s.parent) {
|
||||
const route = s.routeConfig as any;
|
||||
if (route && route._loadedConfig) return route._loadedConfig;
|
||||
if (route && route.component) return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import {
|
||||
Event,
|
||||
NavigationCancel,
|
||||
NavigationEnd,
|
||||
NavigationError,
|
||||
RouterState,
|
||||
UrlTree
|
||||
} from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
import { AsyncActivateRoutes } from './async-activated-routes';
|
||||
|
||||
export function monkeyPatchRouter(router: any) {
|
||||
router.activateRoutes = (state: Observable<{appliedUrl: string, state: RouterState, shouldActivate: boolean}>, storedState: RouterState,
|
||||
storedUrl: UrlTree, id: number, url: UrlTree, rawUrl: UrlTree, skipLocationChange: boolean, replaceUrl: boolean, resolvePromise: any, rejectPromise: any) => {
|
||||
|
||||
// applied the new router state
|
||||
// this operation has side effects
|
||||
let navigationIsSuccessful: boolean;
|
||||
|
||||
const routes: AsyncActivateRoutes[] = [];
|
||||
state
|
||||
.forEach(({appliedUrl, state, shouldActivate}: any) => {
|
||||
if (!shouldActivate || id !== router.navigationId) {
|
||||
navigationIsSuccessful = false;
|
||||
return;
|
||||
}
|
||||
|
||||
router.currentUrlTree = appliedUrl;
|
||||
router.rawUrlTree = router.urlHandlingStrategy.merge(router.currentUrlTree, rawUrl);
|
||||
|
||||
(router as{routerState: RouterState}).routerState = state;
|
||||
|
||||
if (!skipLocationChange) {
|
||||
const path = router.urlSerializer.serialize(router.rawUrlTree);
|
||||
if (router.location.isCurrentPathEqualTo(path) || replaceUrl) {
|
||||
router.location.replaceState(path, '');
|
||||
} else {
|
||||
router.location.go(path, '');
|
||||
}
|
||||
}
|
||||
|
||||
routes.push(new AsyncActivateRoutes(router.routeReuseStrategy, state, storedState, (evt: Event) => router.triggerEvent(evt)));
|
||||
|
||||
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
const promises = routes.map(activatedRoute => activatedRoute.activate(router.rootContexts));
|
||||
return Promise.all(promises)
|
||||
.then(
|
||||
() => {
|
||||
navigationIsSuccessful = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
if (navigationIsSuccessful) {
|
||||
router.navigated = true;
|
||||
router.lastSuccessfulId = id;
|
||||
(router.events as Subject<Event>)
|
||||
.next(new NavigationEnd(
|
||||
id, router.serializeUrl(url), router.serializeUrl(router.currentUrlTree)));
|
||||
resolvePromise(true);
|
||||
} else {
|
||||
router.resetUrlToCurrentUrlTree();
|
||||
(router.events as Subject<Event>)
|
||||
.next(new NavigationCancel(id, router.serializeUrl(url), ''));
|
||||
resolvePromise(false);
|
||||
}
|
||||
},
|
||||
(e: any) => {
|
||||
if (isNavigationCancelingError(e)) {
|
||||
router.navigated = true;
|
||||
router.resetStateAndUrl(storedState, storedUrl, rawUrl);
|
||||
(router.events as Subject<Event>)
|
||||
.next(new NavigationCancel(id, router.serializeUrl(url), e.message));
|
||||
|
||||
resolvePromise(false);
|
||||
} else {
|
||||
router.resetStateAndUrl(storedState, storedUrl, rawUrl);
|
||||
(router.events as Subject<Event>)
|
||||
.next(new NavigationError(id, router.serializeUrl(url), e));
|
||||
try {
|
||||
resolvePromise(router.errorHandler(e));
|
||||
} catch (ee) {
|
||||
rejectPromise(ee);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
function isNavigationCancelingError(error: any) {
|
||||
return error && (/** @type {?} */ (error))[NAVIGATION_CANCELING_ERROR];
|
||||
}
|
||||
|
||||
const NAVIGATION_CANCELING_ERROR = 'ngNavigationCancelingError';
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Injector } from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
ChildrenOutletContexts
|
||||
} from '@angular/router';
|
||||
|
||||
export class OutletInjector implements Injector {
|
||||
|
||||
constructor(private route: ActivatedRoute, private childContexts: ChildrenOutletContexts, private parent: Injector) {
|
||||
}
|
||||
|
||||
get(token: any, notFoundValue?: any): any {
|
||||
if (token === ActivatedRoute) {
|
||||
return this.route;
|
||||
}
|
||||
|
||||
if (token === ChildrenOutletContexts) {
|
||||
return this.childContexts;
|
||||
}
|
||||
|
||||
return this.parent.get(token, notFoundValue);
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import {
|
||||
Attribute,
|
||||
ChangeDetectorRef,
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Injector,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
Type,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import {
|
||||
ActivatedRoute,
|
||||
ChildrenOutletContexts,
|
||||
PRIMARY_OUTLET,
|
||||
Router
|
||||
} from '@angular/router';
|
||||
|
||||
import { RouterDelegate } from '@ionic/core';
|
||||
|
||||
import { OutletInjector } from './outlet-injector';
|
||||
import { RouteEventHandler } from './route-event-handler';
|
||||
|
||||
import { AngularComponentMounter, AngularEscapeHatch } from '..';
|
||||
import { getIonApp } from '../util/util';
|
||||
|
||||
let id = 0;
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-nav',
|
||||
})
|
||||
export class RouterOutlet implements OnDestroy, OnInit, RouterDelegate {
|
||||
|
||||
public name: string;
|
||||
public activationStatus = NOT_ACTIVATED;
|
||||
public componentConstructor: Type<any> = null;
|
||||
public componentInstance: any = null;
|
||||
public activatedRoute: ActivatedRoute = null;
|
||||
public activatedRouteData: any = {};
|
||||
public activeComponentRef: ComponentRef<any> = null;
|
||||
private id: number = id++;
|
||||
|
||||
@Output('activate') activateEvents = new EventEmitter<any>();
|
||||
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
|
||||
|
||||
constructor(
|
||||
public location: ViewContainerRef,
|
||||
public changeDetector: ChangeDetectorRef,
|
||||
public elementRef: ElementRef,
|
||||
protected angularComponentMounter: AngularComponentMounter,
|
||||
protected parentContexts: ChildrenOutletContexts,
|
||||
protected cfr: ComponentFactoryResolver,
|
||||
protected injector: Injector,
|
||||
protected router: Router,
|
||||
private routeEventHandler: RouteEventHandler,
|
||||
@Attribute('name') name: string) {
|
||||
|
||||
(this.elementRef.nativeElement as HTMLIonNavElement).routerDelegate = this;
|
||||
this.name = name || PRIMARY_OUTLET;
|
||||
parentContexts.onChildOutletCreated(this.name, this as any);
|
||||
}
|
||||
|
||||
pushUrlState(urlSegment: string): Promise<any> {
|
||||
return this.router.navigateByUrl(urlSegment);
|
||||
}
|
||||
|
||||
popUrlState(): Promise<any> {
|
||||
window.history.back();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
console.debug(`Nav ${this.id} ngOnDestroy`);
|
||||
this.parentContexts.onChildOutletDestroyed(this.name);
|
||||
}
|
||||
|
||||
get isActivated(): boolean {
|
||||
return this.activationStatus === ACTIVATION_IN_PROGRESS
|
||||
|| this.activationStatus === ACTIVATED;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.isActivated) {
|
||||
// If the outlet was not instantiated at the time the route got activated we need to populate
|
||||
// the outlet when it is initialized (ie inside a NgIf)
|
||||
const context = this.parentContexts.getContext(this.name);
|
||||
if (context && context.route) {
|
||||
// the component defined in the configuration is created
|
||||
// otherwise the component defined in the configuration is created
|
||||
this.activateWith(context.route, context.resolver || null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get component(): Object {
|
||||
return this.componentInstance;
|
||||
}
|
||||
|
||||
deactivate(): void {
|
||||
console.debug(`outlet ${this.id} is being deactivated`);
|
||||
this.activationStatus = NOT_ACTIVATED;
|
||||
this.deactivateEvents.emit(this.componentConstructor);
|
||||
}
|
||||
|
||||
activateWith(activatedRoute: ActivatedRoute, cfr: ComponentFactoryResolver): Promise<void> {
|
||||
|
||||
if (this.activationStatus !== NOT_ACTIVATED) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.activationStatus = ACTIVATION_IN_PROGRESS;
|
||||
this.activatedRoute = activatedRoute;
|
||||
const snapshot = (activatedRoute as any)._futureSnapshot;
|
||||
const component = snapshot.routeConfig ? snapshot.routeConfig.component : null;
|
||||
cfr = cfr || this.cfr;
|
||||
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
|
||||
const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
|
||||
|
||||
const isTopLevel = !hasChildComponent(activatedRoute);
|
||||
|
||||
return this.routeEventHandler.externalNavStart().then(() => {
|
||||
return getDataFromRoute(activatedRoute);
|
||||
}).then((dataObj: any) => {
|
||||
return activateRoute(this.elementRef.nativeElement, component, dataObj, cfr, injector, isTopLevel).then(() => {
|
||||
this.changeDetector.markForCheck();
|
||||
this.activateEvents.emit(null);
|
||||
this.activationStatus = ACTIVATED;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function activateRoute(navElement: HTMLIonNavElement,
|
||||
component: Type<any>, data: any = {}, cfr: ComponentFactoryResolver, injector: Injector, isTopLevel: boolean): Promise<any> {
|
||||
|
||||
return getIonApp().then((ionApp) => {
|
||||
if (!ionApp) {
|
||||
return Promise.reject(new Error(`<ion-app> element is required for angular router integration`));
|
||||
}
|
||||
const escapeHatch = getEscapeHatch(cfr, injector);
|
||||
return navElement.componentOnReady().then(() => {
|
||||
return navElement.reconcileFromExternalRouter(component, data, escapeHatch, isTopLevel);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const NOT_ACTIVATED = 0;
|
||||
export const ACTIVATION_IN_PROGRESS = 1;
|
||||
export const ACTIVATED = 2;
|
||||
|
||||
export function hasChildComponent(activatedRoute: ActivatedRoute): boolean {
|
||||
// don't worry about recursion for now, that's a future problem that may or may not manifest itself
|
||||
for (const childRoute of activatedRoute.children) {
|
||||
if (childRoute.component) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getEscapeHatch(cfr: ComponentFactoryResolver, injector: Injector): AngularEscapeHatch {
|
||||
return {
|
||||
cfr,
|
||||
injector,
|
||||
fromExternalRouter: true,
|
||||
url: location.pathname
|
||||
};
|
||||
}
|
||||
|
||||
export function getDataFromRoute(activatedRoute: ActivatedRoute): Promise<any> {
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
promises.push(getDataFromObservable(activatedRoute));
|
||||
promises.push(getDataFromParams(activatedRoute));
|
||||
return Promise.all(promises).then(([data, params]: any[]) => {
|
||||
const combined = {};
|
||||
Object.assign(combined, data, params);
|
||||
return combined;
|
||||
});
|
||||
}
|
||||
|
||||
export function getDataFromObservable(activatedRoute: ActivatedRoute) {
|
||||
return new Promise((resolve) => {
|
||||
activatedRoute.data.subscribe((data: any) => {
|
||||
resolve(data || {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getDataFromParams(activatedRoute: ActivatedRoute) {
|
||||
return new Promise((resolve) => {
|
||||
activatedRoute.params.subscribe((data: any) => {
|
||||
resolve(data || {});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import {
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef
|
||||
} from '@angular/core';
|
||||
|
||||
import {
|
||||
ActivatedRoute,
|
||||
ChildrenOutletContexts,
|
||||
OutletContext,
|
||||
RouterOutlet
|
||||
} from '@angular/router';
|
||||
|
||||
export class PushPopOutletContexts extends ChildrenOutletContexts {
|
||||
|
||||
// this method is a public api, but the members in the ChildrenOutletContexts are private
|
||||
// so we're gonna cheat, 'cause we play to win
|
||||
onOutletDeactivated(): Map<string, OutletContext> {
|
||||
return (this as any).contexts;
|
||||
}
|
||||
|
||||
getOrCreateContext(childName: string): OutletContext {
|
||||
let context = this.getContext(childName) as any;
|
||||
|
||||
if (!context) {
|
||||
context = {
|
||||
children: new PushPopOutletContexts()
|
||||
};
|
||||
(this as any).contexts.set(childName, context);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PushPopOutletContext {
|
||||
outlet?: RouterOutlet;
|
||||
route?: ActivatedRoute;
|
||||
resolver?: ComponentFactoryResolver;
|
||||
children?: PushPopOutletContexts;
|
||||
attachRef: ComponentRef<any>;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
Event,
|
||||
NavigationEnd,
|
||||
Router
|
||||
} from '@angular/router';
|
||||
|
||||
import { getIonApp } from '../util/util';
|
||||
|
||||
@Injectable()
|
||||
export class RouteEventHandler {
|
||||
|
||||
constructor(public router: Router) {
|
||||
|
||||
router.events.subscribe((event: Event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
getIonApp().then((appElement) => {
|
||||
appElement.updateExternalNavOccuring(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
externalNavStart() {
|
||||
return getIonApp().then((appElement) => {
|
||||
appElement.updateExternalNavOccuring(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import {
|
||||
Location
|
||||
} from '@angular/common';
|
||||
|
||||
import {
|
||||
ApplicationRef,
|
||||
Compiler,
|
||||
Injector,
|
||||
ModuleWithProviders,
|
||||
NgModule,
|
||||
NgModuleFactoryLoader,
|
||||
Optional,
|
||||
} from '@angular/core';
|
||||
|
||||
import {ɵgetDOM as getDOM} from '@angular/platform-browser';
|
||||
|
||||
import {
|
||||
ChildrenOutletContexts,
|
||||
ExtraOptions,
|
||||
ROUTER_CONFIGURATION,
|
||||
ROUTES,
|
||||
Route,
|
||||
RouteReuseStrategy,
|
||||
Router,
|
||||
UrlHandlingStrategy,
|
||||
UrlSerializer
|
||||
} from '@angular/router';
|
||||
|
||||
import { IonicAngularModule } from '../module';
|
||||
|
||||
import { PushPopOutletContexts } from './push-pop-outlet-contexts';
|
||||
import { monkeyPatchRouter } from './monkey-patch-router';
|
||||
import { RouteEventHandler } from './route-event-handler';
|
||||
import { RouterOutlet } from './outlet';
|
||||
import { flatten } from './router-utils';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
RouterOutlet
|
||||
],
|
||||
imports: [
|
||||
IonicAngularModule
|
||||
],
|
||||
exports: [
|
||||
RouterOutlet
|
||||
]
|
||||
})
|
||||
export class IonicRouterModule {
|
||||
static forRoot(): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: IonicRouterModule,
|
||||
providers: [
|
||||
{
|
||||
provide: ChildrenOutletContexts,
|
||||
useClass: PushPopOutletContexts
|
||||
},
|
||||
{
|
||||
provide: Router,
|
||||
useFactory: setupRouter,
|
||||
deps: [
|
||||
ApplicationRef, UrlSerializer, ChildrenOutletContexts, Location, Injector,
|
||||
NgModuleFactoryLoader, Compiler, ROUTES, ROUTER_CONFIGURATION,
|
||||
[UrlHandlingStrategy, new Optional()], [RouteReuseStrategy, new Optional()]
|
||||
]
|
||||
},
|
||||
RouteEventHandler
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function setupRouter(
|
||||
_ref: ApplicationRef, urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts,
|
||||
location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler,
|
||||
config: Route[][], opts: ExtraOptions = {}, urlHandlingStrategy?: UrlHandlingStrategy,
|
||||
routeReuseStrategy?: RouteReuseStrategy) {
|
||||
|
||||
const router = new Router(
|
||||
null, urlSerializer, contexts, location, injector, loader, compiler, flatten(config));
|
||||
|
||||
monkeyPatchRouter(router);
|
||||
|
||||
|
||||
if (urlHandlingStrategy) {
|
||||
router.urlHandlingStrategy = urlHandlingStrategy;
|
||||
}
|
||||
|
||||
if (routeReuseStrategy) {
|
||||
router.routeReuseStrategy = routeReuseStrategy;
|
||||
}
|
||||
|
||||
if (opts.errorHandler) {
|
||||
router.errorHandler = opts.errorHandler;
|
||||
}
|
||||
|
||||
if (opts.enableTracing) {
|
||||
const dom = getDOM();
|
||||
router.events.subscribe((e: any) => {
|
||||
dom.logGroup(`Router Event: ${(e.constructor).name}`);
|
||||
dom.log(e.toString());
|
||||
dom.log(e);
|
||||
dom.logGroupEnd();
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.onSameUrlNavigation) {
|
||||
router.onSameUrlNavigation = opts.onSameUrlNavigation;
|
||||
}
|
||||
|
||||
if (opts.paramsInheritanceStrategy) {
|
||||
router.paramsInheritanceStrategy = opts.paramsInheritanceStrategy;
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
export function nodeChildrenAsMap<T extends{outlet: string}>(node: TreeNode<T>| null) {
|
||||
const map: {[outlet: string]: TreeNode<T>} = {};
|
||||
|
||||
if (node) {
|
||||
node.children.forEach(child => map[child.value.outlet] = child);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
export function forEach<_K, V>(map: {[key: string]: V}, callback: (v: V, k: string) => void): void {
|
||||
for (const prop in map) {
|
||||
if (map.hasOwnProperty(prop)) {
|
||||
callback(map[prop], prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TreeNode<T> {
|
||||
constructor(public value: T, public children: TreeNode<T>[]) {}
|
||||
|
||||
toString(): string { return `TreeNode(${this.value})`; }
|
||||
}
|
||||
|
||||
/**
|
||||
* The expectation is that the activate route is created with the right set of parameters.
|
||||
* So we push new values into the observables only when they are not the initial values.
|
||||
* And we detect that by checking if the snapshot field is set.
|
||||
*/
|
||||
export function advanceActivatedRoute(route: any): void {
|
||||
if (route.snapshot) {
|
||||
const currentSnapshot = route.snapshot;
|
||||
const nextSnapshot = (route)._futureSnapshot;
|
||||
route.snapshot = nextSnapshot;
|
||||
if (!shallowEqual(currentSnapshot.queryParams, nextSnapshot.queryParams)) {
|
||||
(route.queryParams).next(nextSnapshot.queryParams);
|
||||
}
|
||||
if (currentSnapshot.fragment !== nextSnapshot.fragment) {
|
||||
(route.fragment).next(nextSnapshot.fragment);
|
||||
}
|
||||
if (!shallowEqual(currentSnapshot.params, nextSnapshot.params)) {
|
||||
(route.params).next(nextSnapshot.params);
|
||||
}
|
||||
if (!shallowEqualArrays(currentSnapshot.url, nextSnapshot.url)) {
|
||||
(route.url).next(nextSnapshot.url);
|
||||
}
|
||||
if (!shallowEqual(currentSnapshot.data, nextSnapshot.data)) {
|
||||
(route.data).next(nextSnapshot.data);
|
||||
}
|
||||
} else {
|
||||
route.snapshot = (route)._futureSnapshot;
|
||||
|
||||
// this is for resolved data
|
||||
(route.data).next((route)._futureSnapshot.data);
|
||||
}
|
||||
}
|
||||
|
||||
export function shallowEqualArrays(a: any[], b: any[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
if (!shallowEqual(a[i], b[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function shallowEqual(a: {[x: string]: any}, b: {[x: string]: any}): boolean {
|
||||
const k1 = Object.keys(a);
|
||||
const k2 = Object.keys(b);
|
||||
/*tslint:disable*/
|
||||
if (k1.length != k2.length) {
|
||||
return false;
|
||||
}
|
||||
let key: string;
|
||||
for (let i = 0; i < k1.length; i++) {
|
||||
key = k1[i];
|
||||
if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function flatten<T>(arr: T[][]): T[] {
|
||||
return Array.prototype.concat.apply([], arr);
|
||||
}
|
||||
@@ -1,26 +1,3 @@
|
||||
import {
|
||||
ComponentFactory,
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
Injector
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { EscapeHatch, FrameworkMountingData} from '@ionic/core';
|
||||
|
||||
export interface AngularMountingData extends FrameworkMountingData {
|
||||
componentFactory?: ComponentFactory<any>;
|
||||
childInjector?: Injector;
|
||||
componentRef?: ComponentRef<any>;
|
||||
instance?: any;
|
||||
angularHostElement?: HTMLElement;
|
||||
}
|
||||
|
||||
export interface AngularEscapeHatch extends EscapeHatch {
|
||||
activatedRoute?: ActivatedRoute;
|
||||
cfr?: ComponentFactoryResolver;
|
||||
injector?: Injector;
|
||||
}
|
||||
|
||||
export interface IonicGlobal {
|
||||
config: any;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"extends": "tslint-ionic-rules"
|
||||
"extends": "tslint-ionic-rules",
|
||||
"rules": {
|
||||
"no-non-null-assertion": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
<a name="0.1.4-1"></a>
|
||||
## [0.1.4-1](https://github.com/ionic-team/ionic/compare/v0.1.4-0...v0.1.4-1) (2018-03-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ion-router:** fixes routing algorithm ([c8a27b7](https://github.com/ionic-team/ionic/commit/c8a27b7))
|
||||
* **overlays:** bundling of overlays ([9650bec](https://github.com/ionic-team/ionic/commit/9650bec))
|
||||
* **routing:** flickering (part 1) ([7b264f9](https://github.com/ionic-team/ionic/commit/7b264f9))
|
||||
* **tabs:** do not select when using router ([174d9b5](https://github.com/ionic-team/ionic/commit/174d9b5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **virtual-scroll:** adds JSX support ([dc8b363](https://github.com/ionic-team/ionic/commit/dc8b363))
|
||||
|
||||
|
||||
|
||||
<a name="0.1.4-0"></a>
|
||||
## [0.1.4-0](https://github.com/ionic-team/ionic/compare/v0.1.3...v0.1.4-0) (2018-03-06)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ The Ionic Core package contains the Web Components that make up the reusable UI
|
||||
|
||||
Easiest way to start using Ionic Core is by adding a script tag to the CDN:
|
||||
|
||||
<script src="https://unpkg.com/@ionic/core@0.1.4-0/dist/ionic.js"></script>
|
||||
<script src="https://unpkg.com/@ionic/core@0.1.4-1/dist/ionic.js"></script>
|
||||
|
||||
Any Ionic component added to the webpage will automatically load. This includes writing the component tag directly in HTML, or using JavaScript such as `document.createElement('ion-toggle')`.
|
||||
|
||||
|
||||
2
packages/core/package-lock.json
generated
2
packages/core/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "0.1.4-0",
|
||||
"version": "0.1.4-1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "0.1.4-0",
|
||||
"version": "0.1.4-1",
|
||||
"description": "Base components for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
|
||||
6
packages/core/src/components.d.ts
vendored
6
packages/core/src/components.d.ts
vendored
@@ -937,7 +937,9 @@ declare global {
|
||||
}
|
||||
namespace JSXElements {
|
||||
export interface IonFabAttributes extends HTMLAttributes {
|
||||
|
||||
edge?: boolean;
|
||||
horizontal?: 'left' | 'right' | 'center' | 'start' | 'end';
|
||||
vertical?: 'top' | 'center' | 'bottom';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3194,6 +3196,7 @@ declare global {
|
||||
btnId?: string;
|
||||
component?: any;
|
||||
disabled?: boolean;
|
||||
href?: string;
|
||||
icon?: string;
|
||||
name?: string;
|
||||
selected?: boolean;
|
||||
@@ -3617,6 +3620,7 @@ declare global {
|
||||
itemRender?: ItemRenderFn;
|
||||
items?: any[];
|
||||
nodeHeight?: NodeHeightFn;
|
||||
renderer?: (item: any) => JSX.Element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class ActionSheetController implements OverlayController {
|
||||
*/
|
||||
@Method()
|
||||
create(opts?: ActionSheetOptions): Promise<HTMLIonActionSheetElement> {
|
||||
return createOverlay('ion-action-sheet', opts);
|
||||
return createOverlay(document.createElement('ion-action-sheet'), opts);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -30,7 +30,7 @@ export class AlertController implements OverlayController {
|
||||
*/
|
||||
@Method()
|
||||
create(opts?: AlertOptions): Promise<HTMLIonAlertElement> {
|
||||
return createOverlay('ion-alert', opts);
|
||||
return createOverlay(document.createElement('ion-alert'), opts);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -11,8 +11,4 @@ import { Component } from '@stencil/core';
|
||||
theme: 'avatar'
|
||||
}
|
||||
})
|
||||
export class Avatar {
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
export class Avatar {}
|
||||
|
||||
@@ -27,7 +27,4 @@ export class Badge {
|
||||
*/
|
||||
@Prop() mode: 'ios' | 'md';
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,4 @@ import { Component } from '@stencil/core';
|
||||
theme: 'bar-buttons'
|
||||
}
|
||||
})
|
||||
export class Buttons {
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
export class Buttons {}
|
||||
|
||||
@@ -23,7 +23,4 @@ export class CardContent {
|
||||
*/
|
||||
@Prop() mode: 'ios' | 'md';
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,4 @@ export class CardHeader {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,4 @@ export class CardSubtitle {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,4 @@ export class CardTitle {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,4 @@ export class Card {
|
||||
*/
|
||||
@Prop() mode: 'ios' | 'md';
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,4 @@ export class Chip {
|
||||
*/
|
||||
@Prop() mode: 'ios' | 'md';
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,4 @@ export class FabList {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<slot></slot>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,66 +6,69 @@
|
||||
ion-fab {
|
||||
position: absolute;
|
||||
z-index: $z-index-fixed-content;
|
||||
}
|
||||
|
||||
&[center] {
|
||||
@include position(null, null, null, 50%);
|
||||
@include margin-horizontal(-$fab-size / 2, null);
|
||||
|
||||
// FAB Horizontal Positioning
|
||||
// --------------------------------------------------
|
||||
|
||||
.fab-horizontal-left {
|
||||
// scss-lint:disable PropertySpelling
|
||||
@include multi-dir() {
|
||||
left: $fab-content-margin;
|
||||
|
||||
left: calc(#{$fab-content-margin} + constant(safe-area-inset-left));
|
||||
left: calc(#{$fab-content-margin} + env(safe-area-inset-left));
|
||||
}
|
||||
}
|
||||
|
||||
&[middle] {
|
||||
@include margin(-$fab-size / 2, null, null, null);
|
||||
.fab-horizontal-right {
|
||||
// scss-lint:disable PropertySpelling
|
||||
@include multi-dir() {
|
||||
right: $fab-content-margin;
|
||||
|
||||
top: 50%;
|
||||
right: calc(#{$fab-content-margin} + constant(safe-area-inset-right));
|
||||
right: calc(#{$fab-content-margin} + env(safe-area-inset-right));
|
||||
}
|
||||
}
|
||||
|
||||
&[top] {
|
||||
top: $fab-content-margin;
|
||||
}
|
||||
.fab-horizontal-center {
|
||||
@include position(null, null, null, 50%);
|
||||
@include margin-horizontal(-$fab-size / 2, null);
|
||||
}
|
||||
|
||||
&[right] {
|
||||
// scss-lint:disable PropertySpelling
|
||||
@include multi-dir() {
|
||||
right: $fab-content-margin;
|
||||
}
|
||||
.fab-horizontal-start {
|
||||
@include position-horizontal($fab-content-margin, null);
|
||||
@include safe-position-horizontal($fab-content-margin, null);
|
||||
}
|
||||
|
||||
@include multi-dir() {
|
||||
right: calc(#{$fab-content-margin} + constant(safe-area-inset-right));
|
||||
right: calc(#{$fab-content-margin} + env(safe-area-inset-right));
|
||||
}
|
||||
}
|
||||
|
||||
&[end] {
|
||||
@include position-horizontal(null, $fab-content-margin);
|
||||
@include safe-position-horizontal(null, $fab-content-margin);
|
||||
}
|
||||
|
||||
&[bottom] {
|
||||
bottom: $fab-content-margin;
|
||||
}
|
||||
|
||||
&[left] {
|
||||
// scss-lint:disable PropertySpelling
|
||||
@include multi-dir() {
|
||||
left: $fab-content-margin;
|
||||
}
|
||||
.fab-horizontal-end {
|
||||
@include position-horizontal(null, $fab-content-margin);
|
||||
@include safe-position-horizontal(null, $fab-content-margin);
|
||||
}
|
||||
|
||||
|
||||
@include multi-dir() {
|
||||
left: calc(#{$fab-content-margin} + constant(safe-area-inset-left));
|
||||
left: calc(#{$fab-content-margin} + env(safe-area-inset-left));
|
||||
}
|
||||
}
|
||||
// FAB Vertical Positioning
|
||||
// --------------------------------------------------
|
||||
|
||||
&[start] {
|
||||
@include position-horizontal($fab-content-margin, null);
|
||||
@include safe-position-horizontal($fab-content-margin, null);
|
||||
}
|
||||
.fab-vertical-top {
|
||||
top: $fab-content-margin;
|
||||
|
||||
&[top][edge] {
|
||||
&.fab-edge {
|
||||
top: -$fab-size / 2;
|
||||
}
|
||||
}
|
||||
|
||||
&[bottom][edge] {
|
||||
.fab-vertical-bottom {
|
||||
bottom: $fab-content-margin;
|
||||
|
||||
&.fab-edge {
|
||||
bottom: -$fab-size / 2;
|
||||
}
|
||||
}
|
||||
|
||||
.fab-vertical-center {
|
||||
@include margin(-$fab-size / 2, null, null, null);
|
||||
|
||||
top: 50%;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Element, Method, State } from '@stencil/core';
|
||||
import { Component, Element, Method, Prop, State } from '@stencil/core';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -10,6 +10,26 @@ export class Fab {
|
||||
|
||||
@State() activated = false;
|
||||
|
||||
/**
|
||||
* Where to align the fab horizontally in the viewport.
|
||||
* Possible values are: `"left"`, `"right"`, `"center"`, `"start"`, `"end"`.
|
||||
*/
|
||||
@Prop() horizontal: 'left' | 'right' | 'center' | 'start' | 'end';
|
||||
|
||||
/**
|
||||
* Where to align the fab vertically in the viewport.
|
||||
* Possible values are: `"top"`, `"center"`, `"bottom"`.
|
||||
*/
|
||||
@Prop() vertical: 'top' | 'center' | 'bottom';
|
||||
|
||||
/**
|
||||
* If true, the fab will display on the edge of the header if
|
||||
* `vertical` is `"top"`, and on the edge of the footer if
|
||||
* it is `"bottom"`. Should be used with a `fixed` slot.
|
||||
*/
|
||||
@Prop() edge: boolean;
|
||||
|
||||
|
||||
/**
|
||||
* Close an active FAB list container
|
||||
*/
|
||||
@@ -22,6 +42,16 @@ export class Fab {
|
||||
this.activated = !this.activated;
|
||||
}
|
||||
|
||||
hostData() {
|
||||
return {
|
||||
class: {
|
||||
[`fab-horizontal-${this.horizontal}`]: this.horizontal,
|
||||
[`fab-vertical-${this.vertical}`]: this.vertical,
|
||||
['fab-edge']: this.edge
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const fab = this.el.querySelector('ion-fab-button');
|
||||
if (fab) {
|
||||
|
||||
@@ -98,6 +98,60 @@ The fab should have one main fab button. Fabs can also contain fab lists which c
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
#### edge
|
||||
|
||||
boolean
|
||||
|
||||
If true, the fab will display on the edge of the header if
|
||||
`vertical` is `"top"`, and on the edge of the footer if
|
||||
it is `"bottom"`. Should be used with a `fixed` slot.
|
||||
|
||||
|
||||
#### horizontal
|
||||
|
||||
|
||||
|
||||
Where to align the fab horizontally in the viewport.
|
||||
Possible values are: `"left"`, `"right"`, `"center"`, `"start"`, `"end"`.
|
||||
|
||||
|
||||
#### vertical
|
||||
|
||||
|
||||
|
||||
Where to align the fab vertically in the viewport.
|
||||
Possible values are: `"top"`, `"center"`, `"bottom"`.
|
||||
|
||||
|
||||
## Attributes
|
||||
|
||||
#### edge
|
||||
|
||||
boolean
|
||||
|
||||
If true, the fab will display on the edge of the header if
|
||||
`vertical` is `"top"`, and on the edge of the footer if
|
||||
it is `"bottom"`. Should be used with a `fixed` slot.
|
||||
|
||||
|
||||
#### horizontal
|
||||
|
||||
|
||||
|
||||
Where to align the fab horizontally in the viewport.
|
||||
Possible values are: `"left"`, `"right"`, `"center"`, `"start"`, `"end"`.
|
||||
|
||||
|
||||
#### vertical
|
||||
|
||||
|
||||
|
||||
Where to align the fab vertically in the viewport.
|
||||
Possible values are: `"top"`, `"center"`, `"bottom"`.
|
||||
|
||||
|
||||
## Methods
|
||||
|
||||
#### close()
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<ion-button>Test</ion-button>
|
||||
<ion-fab-button>FAB</ion-fab-button>
|
||||
|
||||
<ion-fab top right edge id="fab1" slot="fixed">
|
||||
<ion-fab vertical="top" horizontal="right" edge id="fab1" slot="fixed">
|
||||
<ion-fab-button onclick="clickMainFAB('fab1')" mini class="e2eFabTopRight"><ion-icon name="add"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list>
|
||||
<ion-fab-button onclick="openSocial('facebook', 'fab1')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -33,7 +33,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab bottom right edge id="fab2" slot="fixed">
|
||||
<ion-fab vertical="bottom" horizontal="right" edge id="fab2" slot="fixed">
|
||||
<ion-fab-button onclick="clickMainFAB('fab2')" color="dark" class="e2eFabBottomRight"><ion-icon name="arrow-dropleft"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="left">
|
||||
<ion-fab-button onclick="openSocial('facebook', 'fab2')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -43,7 +43,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab top left id="fab3" slot="fixed">
|
||||
<ion-fab vertical="top" horizontal="left" id="fab3" slot="fixed">
|
||||
<ion-fab-button onclick="clickMainFAB('fab3')" color="secondary" class="e2eFabTopLeft"><ion-icon name="arrow-dropright"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="right">
|
||||
<ion-fab-button onclick="openSocial('facebook', 'fab3')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -53,7 +53,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab bottom left id="fab4" slot="fixed">
|
||||
<ion-fab vertical="bottom" horizontal="left" id="fab4" slot="fixed">
|
||||
<ion-fab-button onclick="clickMainFAB('fab4')" color="light" class="e2eFabBottomLeft"><ion-icon name="arrow-dropup"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="top">
|
||||
<ion-fab-button onclick="openSocial('facebook', 'fab4')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -63,7 +63,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab center middle id="fab5" slot="fixed">
|
||||
<ion-fab vertical="center" horizontal="center" id="fab5" slot="fixed">
|
||||
<ion-fab-button onclick="clickMainFAB('fab5')" class="e2eFabCenter"><ion-icon name="md-share"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="top">
|
||||
<ion-fab-button onclick="openSocial('vimeo', 'fab5')" color="primary"><ion-icon name="logo-vimeo"></ion-icon></ion-fab-button>
|
||||
@@ -79,7 +79,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab right middle slot="fixed">
|
||||
<ion-fab horizontal="right" vertical="center" slot="fixed">
|
||||
<ion-fab-button color="danger" onclick="add()"><ion-icon name="add"></ion-icon></ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<f></f>
|
||||
<ion-fab-button>FAB</ion-fab-button>
|
||||
|
||||
<ion-fab top right id="fab1" slot="fixed">
|
||||
<ion-fab vertical="top" horizontal="right" id="fab1" slot="fixed">
|
||||
<ion-fab-button onclick="clickMainFAB('fab1')" mini class="e2eFabTopRight"><ion-icon name="add"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list>
|
||||
<ion-fab-button onclick="openSocial('facebook', 'fab1')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -23,7 +23,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab bottom right id="fab2" slot="fixed">
|
||||
<ion-fab vertical="bottom" horizontal="right" id="fab2" slot="fixed">
|
||||
<ion-fab-button onclick="clickMainFAB('fab2')" color="dark" class="e2eFabBottomRight"><ion-icon name="arrow-dropleft"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="left">
|
||||
<ion-fab-button onclick="openSocial('facebook', 'fab2')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -33,7 +33,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab top left id="fab3" slot="fixed">
|
||||
<ion-fab vertical="top" horizontal="left" id="fab3" slot="fixed">
|
||||
<ion-fab-button onclick="clickMainFAB('fab3')" color="secondary" class="e2eFabTopLeft"><ion-icon name="arrow-dropright"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="right">
|
||||
<ion-fab-button onclick="openSocial('facebook', 'fab3')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -43,7 +43,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab bottom left id="fab4" slot="fixed">
|
||||
<ion-fab vertical="bottom" horizontal="left" id="fab4" slot="fixed">
|
||||
<ion-fab-button onclick="clickMainFAB('fab4')" color="light" class="e2eFabBottomLeft"><ion-icon name="arrow-dropup"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="top">
|
||||
<ion-fab-button onclick="openSocial('facebook', 'fab4')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -53,7 +53,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab center middle id="fab5" slot="fixed">
|
||||
<ion-fab vertical="center" horizontal="center" id="fab5" slot="fixed">
|
||||
<ion-fab-button onclick="clickMainFAB('fab5')" class="e2eFabCenter"><ion-icon name="md-share"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="top">
|
||||
<ion-fab-button onclick="openSocial('vimeo', 'fab5')" color="primary"><ion-icon name="logo-vimeo"></ion-icon></ion-fab-button>
|
||||
@@ -69,7 +69,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab right middle slot="fixed">
|
||||
<ion-fab horizontal="right" vertical="center" slot="fixed">
|
||||
<ion-fab-button color="danger" onclick="add()"><ion-icon name="add"></ion-icon></ion-fab-button>
|
||||
</ion-fab>
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<pre id="log" style="right:10px; bottom:50px; text-shadow: 0 0 2px rgba(0, 0, 0, 0.24);" slot="fixed">log</pre>
|
||||
<ion-button>Test</ion-button>
|
||||
|
||||
<ion-fab top right edge id="fab1" slot="fixed">
|
||||
<ion-fab vertical="top" horizontal="right" edge id="fab1" slot="fixed">
|
||||
<ion-fab-button translucent onclick="clickMainFAB('fab1')" mini class="e2eFabTopRight"><ion-icon name="add"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list>
|
||||
<ion-fab-button translucent onclick="openSocial('facebook', 'fab1')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -42,7 +42,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab bottom right edge id="fab2" slot="fixed">
|
||||
<ion-fab vertical="bottom" horizontal="right" edge id="fab2" slot="fixed">
|
||||
<ion-fab-button translucent onclick="clickMainFAB('fab2')" color="dark" class="e2eFabBottomRight"><ion-icon name="arrow-dropleft"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="left">
|
||||
<ion-fab-button translucent onclick="openSocial('facebook', 'fab2')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -52,7 +52,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab top left id="fab3" slot="fixed">
|
||||
<ion-fab vertical="top" horizontal="left" id="fab3" slot="fixed">
|
||||
<ion-fab-button translucent onclick="clickMainFAB('fab3')" color="secondary" class="e2eFabTopLeft"><ion-icon name="arrow-dropright"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="right">
|
||||
<ion-fab-button translucent onclick="openSocial('facebook', 'fab3')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -62,7 +62,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab bottom left id="fab4" slot="fixed">
|
||||
<ion-fab vertical="bottom" horizontal="left" id="fab4" slot="fixed">
|
||||
<ion-fab-button translucent onclick="clickMainFAB('fab4')" color="light" class="e2eFabBottomLeft"><ion-icon name="arrow-dropup"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="top">
|
||||
<ion-fab-button translucent onclick="openSocial('facebook', 'fab4')"><ion-icon name="logo-facebook"></ion-icon></ion-fab-button>
|
||||
@@ -72,7 +72,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab center middle id="fab5" slot="fixed">
|
||||
<ion-fab horizontal="center" vertical="center" id="fab5" slot="fixed">
|
||||
<ion-fab-button translucent onclick="clickMainFAB('fab5')" class="e2eFabCenter"><ion-icon name="md-share"></ion-icon></ion-fab-button>
|
||||
<ion-fab-list side="top">
|
||||
<ion-fab-button translucent onclick="openSocial('vimeo', 'fab5')" color="primary"><ion-icon name="logo-vimeo"></ion-icon></ion-fab-button>
|
||||
@@ -88,7 +88,7 @@
|
||||
</ion-fab-list>
|
||||
</ion-fab>
|
||||
|
||||
<ion-fab right middle slot="fixed">
|
||||
<ion-fab horizontal="right" vertical="center" slot="fixed">
|
||||
<ion-fab-button translucent color="danger" onclick="add()"><ion-icon name="add"></ion-icon></ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
||||
|
||||
@@ -36,7 +36,4 @@ export class Footer {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,4 @@ export class Header {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,6 @@ export class HideWhen implements DisplayWhen {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -233,8 +233,4 @@ export class InfiniteScroll {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,8 +11,4 @@ import { Component } from '@stencil/core';
|
||||
theme: 'item-group'
|
||||
}
|
||||
})
|
||||
export class ItemGroup {
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
export class ItemGroup {}
|
||||
|
||||
@@ -47,7 +47,4 @@ export class ItemOptions {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,4 @@ export class Label {
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,4 @@ export class ListHeader {
|
||||
*/
|
||||
@Prop() mode: 'ios' | 'md';
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,4 @@ export class List {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class LoadingController implements OverlayController {
|
||||
*/
|
||||
@Method()
|
||||
create(opts?: LoadingOptions): Promise<HTMLIonLoadingElement> {
|
||||
return createOverlay('ion-loading', opts);
|
||||
return createOverlay(document.createElement('ion-loading'), opts);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -60,9 +60,6 @@ export class MenuToggle {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
function getMenuController(): Promise<HTMLIonMenuControllerElement> {
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ModalController implements OverlayController {
|
||||
*/
|
||||
@Method()
|
||||
create(opts?: ModalOptions): Promise<HTMLIonModalElement> {
|
||||
return createOverlay('ion-modal', opts);
|
||||
return createOverlay(document.createElement('ion-modal'), opts);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -16,8 +16,4 @@ export class NavPop {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,8 +21,4 @@ export class NavPush {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,8 +20,4 @@ export class NavSetRoot {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -223,6 +223,11 @@ export class NavControllerBase implements NavOutlet {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Method()
|
||||
markVisible() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@Method()
|
||||
getContentElement(): HTMLElement {
|
||||
const active = this.getActive();
|
||||
|
||||
@@ -71,6 +71,9 @@ Return a view controller
|
||||
#### insertPages()
|
||||
|
||||
|
||||
#### markVisible()
|
||||
|
||||
|
||||
#### pop()
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +27,4 @@ export class Note {
|
||||
*/
|
||||
@Prop() mode: 'ios' | 'md';
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class PickerController implements OverlayController {
|
||||
*/
|
||||
@Method()
|
||||
create(opts?: PickerOptions): Promise<HTMLIonPickerElement> {
|
||||
return createOverlay('ion-picker', opts);
|
||||
return createOverlay(document.createElement('ion-picker'), opts);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -29,7 +29,7 @@ export class PopoverController implements OverlayController {
|
||||
*/
|
||||
@Method()
|
||||
create(opts?: PopoverOptions): Promise<HTMLIonPopoverElement> {
|
||||
return createOverlay('ion-popover', opts);
|
||||
return createOverlay(document.createElement('ion-popover'), opts);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -175,9 +175,6 @@ export class RadioGroup implements ComponentDidLoad, RadioGroupInput {
|
||||
return hostAttrs;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
let radioGroupIds = 0;
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
export interface NavOutlet {
|
||||
setRouteId(id: any, data: any, direction: number): Promise<boolean>;
|
||||
getRouteId(): string;
|
||||
getContentElement(): HTMLElement | null;
|
||||
}
|
||||
|
||||
export type NavOutletElement = NavOutlet & HTMLStencilElement;
|
||||
|
||||
export interface RouterEntry {
|
||||
id: any;
|
||||
path: string[];
|
||||
subroutes: RouterEntries;
|
||||
props?: any;
|
||||
}
|
||||
|
||||
export type RouterEntries = RouterEntry[];
|
||||
|
||||
export class RouterSegments {
|
||||
constructor(
|
||||
private path: string[]
|
||||
) {}
|
||||
|
||||
next(): string {
|
||||
if (this.path.length > 0) {
|
||||
return this.path.shift() as string;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function writeNavState(root: HTMLElement, chain: RouterEntries, index: number, direction: number): Promise<void> {
|
||||
if (index >= chain.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const route = chain[index];
|
||||
const node = breadthFirstSearch(root);
|
||||
if (!node) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return node.componentOnReady()
|
||||
.then(() => node.setRouteId(route.id, route.props, direction))
|
||||
.then(changed => {
|
||||
if (changed) {
|
||||
direction = 0;
|
||||
}
|
||||
const nextEl = node.getContentElement();
|
||||
if (nextEl) {
|
||||
return writeNavState(nextEl, chain, index + 1, direction);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
export function readNavState(node: HTMLElement) {
|
||||
const stack: string[] = [];
|
||||
let pivot: NavOutlet|null;
|
||||
while (true) {
|
||||
pivot = breadthFirstSearch(node);
|
||||
if (pivot) {
|
||||
const cmp = pivot.getRouteId();
|
||||
if (cmp) {
|
||||
node = pivot.getContentElement();
|
||||
stack.push(cmp.toLowerCase());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
stack: stack,
|
||||
pivot: pivot,
|
||||
};
|
||||
}
|
||||
|
||||
export function matchPath(stack: string[], routes: RouterEntries) {
|
||||
const path: string[] = [];
|
||||
for (const id of stack) {
|
||||
const route = routes.find(r => r.id === id);
|
||||
if (route) {
|
||||
path.push(...route.path);
|
||||
routes = route.subroutes;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
path: path,
|
||||
routes: routes,
|
||||
};
|
||||
}
|
||||
|
||||
export function matchRouteChain(path: string[], routes: RouterEntries): RouterEntries {
|
||||
const chain = [];
|
||||
const segments = new RouterSegments(path);
|
||||
while (routes.length > 0) {
|
||||
const route = matchRoute(segments, routes);
|
||||
if (!route) {
|
||||
break;
|
||||
}
|
||||
chain.push(route);
|
||||
routes = route.subroutes;
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
export function matchRoute(segments: RouterSegments, routes: RouterEntries): RouterEntry | null {
|
||||
if (!routes) {
|
||||
return null;
|
||||
}
|
||||
let index = 0;
|
||||
let selectedRoute: RouterEntry|null = null;
|
||||
let ambiguous = false;
|
||||
let segment: string;
|
||||
let l: number;
|
||||
|
||||
while (true) {
|
||||
routes = routes.filter(r => r.path.length > index);
|
||||
if (routes.length === 0) {
|
||||
break;
|
||||
}
|
||||
segment = segments.next();
|
||||
routes = routes.filter(r => r.path[index] === segment);
|
||||
l = routes.length;
|
||||
if (l === 0) {
|
||||
selectedRoute = null;
|
||||
ambiguous = false;
|
||||
} else {
|
||||
selectedRoute = routes[0];
|
||||
ambiguous = l > 1;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
if (ambiguous) {
|
||||
throw new Error('ambiguious match');
|
||||
}
|
||||
return selectedRoute;
|
||||
}
|
||||
|
||||
export function readRoutes(root: Element): RouterEntries {
|
||||
return (Array.from(root.children) as HTMLIonRouteElement[])
|
||||
.filter(el => el.tagName === 'ION-ROUTE')
|
||||
.map(el => ({
|
||||
path: parsePath(el.path),
|
||||
id: el.component,
|
||||
props: el.props,
|
||||
subroutes: readRoutes(el)
|
||||
}));
|
||||
}
|
||||
|
||||
export function generatePath(segments: string[]): string {
|
||||
const path = segments
|
||||
.filter(s => s.length > 0)
|
||||
.join('/');
|
||||
|
||||
return '/' + path;
|
||||
}
|
||||
|
||||
export function parsePath(path: string): string[] {
|
||||
if (path === null || path === undefined) {
|
||||
return [''];
|
||||
}
|
||||
const segments = path.split('/')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
|
||||
if (segments.length === 0) {
|
||||
return [''];
|
||||
} else {
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
|
||||
const navs = ['ION-NAV', 'ION-TABS'];
|
||||
export function breadthFirstSearch(root: HTMLElement): NavOutletElement | null {
|
||||
if (!root) {
|
||||
console.error('search root is null');
|
||||
return null;
|
||||
}
|
||||
// we do a Breadth-first search
|
||||
// Breadth-first search (BFS) is an algorithm for traversing or searching tree
|
||||
// or graph data structures.It starts at the tree root(or some arbitrary node of a graph,
|
||||
// sometimes referred to as a 'search key'[1]) and explores the neighbor nodes
|
||||
// first, before moving to the next level neighbours.
|
||||
|
||||
const queue = [root];
|
||||
let node: HTMLElement | undefined;
|
||||
while (node = queue.shift()) {
|
||||
// visit node
|
||||
if (navs.indexOf(node.tagName) >= 0) {
|
||||
return node as NavOutletElement;
|
||||
}
|
||||
|
||||
// queue children
|
||||
const children = node.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
queue.push(children[i] as NavOutletElement);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function writePath(history: History, base: string, usePath: boolean, path: string[], isPop: boolean, state: number) {
|
||||
path = [base, ...path];
|
||||
let url = generatePath(path);
|
||||
if (usePath) {
|
||||
url = '#' + url;
|
||||
}
|
||||
state++;
|
||||
if (isPop) {
|
||||
history.back();
|
||||
history.replaceState(state, null, url);
|
||||
} else {
|
||||
history.pushState(state, null, url);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export function readPath(loc: Location, base: string, useHash: boolean): string[] | null {
|
||||
const path = useHash
|
||||
? loc.hash.substr(1)
|
||||
: loc.pathname;
|
||||
|
||||
if (path.startsWith(base)) {
|
||||
return parsePath(path.slice(base.length));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Component, Element, Listen, Prop } from '@stencil/core';
|
||||
import { RouterEntries, matchPath, matchRouteChain, readNavState, readPath, readRoutes, writeNavState, writePath } from './router-utils';
|
||||
import { Config, DomController } from '../../index';
|
||||
import { flattenRouterTree, readRoutes } from './utils/parser';
|
||||
import { readNavState, writeNavState } from './utils/dom';
|
||||
import { chainToPath, readPath, writePath } from './utils/path';
|
||||
import { RouteChain } from './utils/interfaces';
|
||||
import { routerIDsToChain, routerPathToChain } from './utils/matching';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -8,7 +12,7 @@ import { Config, DomController } from '../../index';
|
||||
})
|
||||
export class Router {
|
||||
|
||||
private routes: RouterEntries;
|
||||
private routes: RouteChain[];
|
||||
private busy = false;
|
||||
private state = 0;
|
||||
|
||||
@@ -22,7 +26,8 @@ export class Router {
|
||||
|
||||
componentDidLoad() {
|
||||
// read config
|
||||
this.routes = readRoutes(this.el);
|
||||
const tree = readRoutes(this.el);
|
||||
this.routes = flattenRouterTree(tree);
|
||||
|
||||
// perform first write
|
||||
this.dom.raf(() => {
|
||||
@@ -49,16 +54,16 @@ export class Router {
|
||||
return;
|
||||
}
|
||||
console.debug('[IN] nav changed -> update URL');
|
||||
const { stack, pivot } = this.readNavState();
|
||||
const { path, routes } = matchPath(stack, this.routes);
|
||||
if (pivot) {
|
||||
const { ids, pivot } = this.readNavState();
|
||||
const { chain, matches } = routerIDsToChain(ids, this.routes);
|
||||
if (chain.length > matches) {
|
||||
// readNavState() found a pivot that is not initialized
|
||||
console.debug('[IN] pivot uninitialized -> write partial nav state');
|
||||
this.writeNavState(pivot, [], routes, 0);
|
||||
this.writeNavState(pivot, chain.slice(matches), 0);
|
||||
}
|
||||
|
||||
const isPop = ev.detail.isPop === true;
|
||||
this.writePath(path, isPop);
|
||||
this.writePath(chain, isPop);
|
||||
}
|
||||
|
||||
private writeNavStateRoot(): Promise<any> {
|
||||
@@ -66,14 +71,13 @@ export class Router {
|
||||
const currentPath = this.readPath();
|
||||
const direction = window.history.state >= this.state ? 1 : -1;
|
||||
if (currentPath) {
|
||||
return this.writeNavState(node, currentPath, this.routes, direction);
|
||||
const {chain} = routerPathToChain(currentPath, this.routes);
|
||||
return this.writeNavState(node, chain, direction);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private writeNavState(node: any, path: string[], routes: RouterEntries, direction: number): Promise<any> {
|
||||
const chain = matchRouteChain(path, routes);
|
||||
|
||||
private writeNavState(node: any, chain: RouteChain, direction: number): Promise<any> {
|
||||
this.busy = true;
|
||||
return writeNavState(node, chain, 0, direction)
|
||||
.catch(err => console.error(err))
|
||||
@@ -85,7 +89,8 @@ export class Router {
|
||||
return readNavState(root);
|
||||
}
|
||||
|
||||
private writePath(path: string[], isPop: boolean) {
|
||||
private writePath(chain: RouteChain, isPop: boolean) {
|
||||
const path = chainToPath(chain);
|
||||
this.state = writePath(window.history, this.base, this.useHash, path, isPop, this.state);
|
||||
}
|
||||
|
||||
|
||||
47
packages/core/src/components/router/test/common.spec.tsx
Normal file
47
packages/core/src/components/router/test/common.spec.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { RouterSegments, breadthFirstSearch } from '../utils/common';
|
||||
|
||||
describe('RouterSegments', () => {
|
||||
it ('should initialize with empty array', () => {
|
||||
const s = new RouterSegments([]);
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('');
|
||||
});
|
||||
|
||||
it ('should initialize with array', () => {
|
||||
const s = new RouterSegments(['', 'path', 'to', 'destination']);
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('path');
|
||||
expect(s.next()).toEqual('to');
|
||||
expect(s.next()).toEqual('destination');
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('breadthFirstSearch', () => {
|
||||
it('should search in order', () => {
|
||||
const n1 = { tagName: 'ION-TABS', children: [] as any };
|
||||
const n2 = { tagName: 'DIV', children: [n1] };
|
||||
const n3 = { tagName: 'ION-NAV', children: [n2] };
|
||||
const n4 = { tagName: 'ION-TABS', children: [] as any };
|
||||
const n5 = { tagName: 'DIV', children: [n4] };
|
||||
const n6 = { tagName: 'DIV', children: [n5, n3] };
|
||||
const n7 = { tagName: 'DIV', children: [] as any };
|
||||
const n8 = { tagName: 'DIV', children: [n6] };
|
||||
const n9 = { tagName: 'DIV', children: [n8, n7] };
|
||||
|
||||
expect(breadthFirstSearch(n9 as any)).toBe(n3);
|
||||
expect(breadthFirstSearch(n8 as any)).toBe(n3);
|
||||
expect(breadthFirstSearch(n7 as any)).toBe(null);
|
||||
expect(breadthFirstSearch(n6 as any)).toBe(n3);
|
||||
expect(breadthFirstSearch(n5 as any)).toBe(n4);
|
||||
expect(breadthFirstSearch(n4 as any)).toBe(n4);
|
||||
expect(breadthFirstSearch(n3 as any)).toBe(n3);
|
||||
expect(breadthFirstSearch(n2 as any)).toBe(n1);
|
||||
expect(breadthFirstSearch(n1 as any)).toBe(n1);
|
||||
});
|
||||
});
|
||||
|
||||
65
packages/core/src/components/router/test/e2e.spec.tsx
Normal file
65
packages/core/src/components/router/test/e2e.spec.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { RouteChain } from '../utils/interfaces';
|
||||
import { routerIDsToChain, routerPathToChain } from '../utils/matching';
|
||||
import { mockRouteElement } from './parser.spec';
|
||||
import { chainToPath, generatePath, parsePath } from '../utils/path';
|
||||
import { flattenRouterTree, readRoutes } from '../utils/parser';
|
||||
import { mockElement } from '@stencil/core/dist/testing';
|
||||
|
||||
describe('ionic-conference-app', () => {
|
||||
|
||||
it('should match conference-app routing', () => {
|
||||
const root = conferenceAppRouting();
|
||||
const tree = readRoutes(root);
|
||||
const routes = flattenRouterTree(tree);
|
||||
|
||||
expect(getRouteIDs('/', routes)).toEqual(['page-tabs', 'tab-schedule', 'page-schedule']);
|
||||
expect(getRouteIDs('/speaker', routes)).toEqual(['page-tabs', 'tab-speaker', 'page-speaker-list']);
|
||||
expect(getRouteIDs('/map', routes)).toEqual(['page-tabs', 'page-map']);
|
||||
expect(getRouteIDs('/about', routes)).toEqual(['page-tabs', 'page-about']);
|
||||
expect(getRouteIDs('/tutorial', routes)).toEqual(['page-tutorial']);
|
||||
|
||||
expect(getRoutePaths(['page-tabs', 'tab-schedule', 'page-schedule'], routes)).toEqual('/');
|
||||
expect(getRoutePaths(['page-tabs', 'tab-speaker', 'page-speaker-list'], routes)).toEqual('/speaker');
|
||||
expect(getRoutePaths(['page-tabs', 'page-map'], routes)).toEqual('/map');
|
||||
expect(getRoutePaths(['page-tabs', 'page-about'], routes)).toEqual('/about');
|
||||
expect(getRoutePaths(['page-tutorial'], routes)).toEqual('/tutorial');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function conferenceAppRouting() {
|
||||
const p2 = mockRouteElement('/', 'tab-schedule');
|
||||
const p3 = mockRouteElement('/', 'page-schedule');
|
||||
p2.appendChild(p3);
|
||||
|
||||
const p4 = mockRouteElement('/speaker', 'tab-speaker');
|
||||
const p5 = mockRouteElement('/', 'page-speaker-list');
|
||||
p4.appendChild(p5);
|
||||
|
||||
const p6 = mockRouteElement('/map', 'page-map');
|
||||
const p7 = mockRouteElement('/about', 'page-about');
|
||||
|
||||
const p1 = mockRouteElement('/', 'page-tabs');
|
||||
p1.appendChild(p2);
|
||||
p1.appendChild(p4);
|
||||
p1.appendChild(p6);
|
||||
p1.appendChild(p7);
|
||||
|
||||
const p8 = mockRouteElement('/tutorial', 'page-tutorial');
|
||||
const container = mockElement('div');
|
||||
container.appendChild(p1);
|
||||
container.appendChild(p8);
|
||||
return container;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getRouteIDs(path: string, routes: RouteChain[]): string[] {
|
||||
return routerPathToChain(parsePath(path), routes).chain.map(r => r.id);
|
||||
}
|
||||
|
||||
function getRoutePaths(ids: string[], routes: RouteChain[]): string {
|
||||
return generatePath(chainToPath(routerIDsToChain(ids, routes).chain));
|
||||
}
|
||||
|
||||
279
packages/core/src/components/router/test/matching.spec.tsx
Normal file
279
packages/core/src/components/router/test/matching.spec.tsx
Normal file
@@ -0,0 +1,279 @@
|
||||
import { RouteChain } from '../utils/interfaces';
|
||||
import { matchesIDs, matchesPath, routerPathToChain } from '../utils/matching';
|
||||
import { mockRouteElement } from './parser.spec';
|
||||
import { mockElement } from '@stencil/core/dist/testing';
|
||||
|
||||
const CHAIN_1: RouteChain = [
|
||||
{ id: '2', path: ['to'], props: undefined },
|
||||
{ id: '1', path: ['path'], props: undefined },
|
||||
{ id: '3', path: ['segment'], props: undefined },
|
||||
{ id: '4', path: [''], props: undefined },
|
||||
];
|
||||
|
||||
const CHAIN_2: RouteChain = [
|
||||
{ id: '2', path: [''], props: undefined },
|
||||
{ id: '1', path: [''], props: undefined },
|
||||
{ id: '3', path: ['segment', 'to'], props: undefined },
|
||||
{ id: '4', path: [''], props: undefined },
|
||||
{ id: '5', path: ['hola'], props: undefined },
|
||||
{ id: '6', path: [''], props: undefined },
|
||||
{ id: '7', path: [''], props: undefined },
|
||||
{ id: '8', path: ['adios', 'que', 'tal'], props: undefined },
|
||||
];
|
||||
|
||||
const CHAIN_3: RouteChain = [
|
||||
{ id: '2', path: ['this', 'to'], props: undefined },
|
||||
{ id: '1', path: ['path'], props: undefined },
|
||||
{ id: '3', path: ['segment', 'to', 'element'], props: undefined },
|
||||
{ id: '4', path: [''], props: undefined },
|
||||
];
|
||||
|
||||
|
||||
|
||||
describe('matchesIDs', () => {
|
||||
it('should match simple set of ids', () => {
|
||||
const chain: RouteChain = CHAIN_1;
|
||||
expect(matchesIDs(['2'], chain)).toBe(1);
|
||||
expect(matchesIDs(['2', '1'], chain)).toBe(2);
|
||||
expect(matchesIDs(['2', '1', '3'], chain)).toBe(3);
|
||||
expect(matchesIDs(['2', '1', '3', '4'], chain)).toBe(4);
|
||||
expect(matchesIDs(['2', '1', '3', '4', '5'], chain)).toBe(4);
|
||||
|
||||
expect(matchesIDs([], chain)).toBe(0);
|
||||
expect(matchesIDs(['1'], chain)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesPath', () => {
|
||||
it('should match simple path', () => {
|
||||
const chain: RouteChain = CHAIN_3;
|
||||
expect(matchesPath(['this'], chain)).toBe(false);
|
||||
expect(matchesPath(['this', 'to'], chain)).toBe(false);
|
||||
expect(matchesPath(['this', 'to', 'path'], chain)).toBe(false);
|
||||
expect(matchesPath(['this', 'to', 'path', 'segment'], chain)).toBe(false);
|
||||
expect(matchesPath(['this', 'to', 'path', 'segment', 'to'], chain)).toBe(false);
|
||||
expect(matchesPath(['this', 'to', 'path', 'segment', 'to', 'element'], chain)).toBe(true);
|
||||
expect(matchesPath(['this', 'to', 'path', 'segment', 'to', 'element', 'more'], chain)).toBe(false);
|
||||
|
||||
expect(matchesPath([], chain)).toBe(false);
|
||||
expect(matchesPath([''], chain)).toBe(false);
|
||||
expect(matchesPath(['path'], chain)).toBe(false);
|
||||
});
|
||||
|
||||
it('should match simple default route', () => {
|
||||
const chain: RouteChain = CHAIN_2;
|
||||
expect(matchesPath([''], chain)).toBe(false);
|
||||
expect(matchesPath(['segment'], chain)).toBe(false);
|
||||
expect(matchesPath(['segment', 'to'], chain)).toBe(false);
|
||||
expect(matchesPath(['segment', 'to', 'hola'], chain)).toBe(false);
|
||||
expect(matchesPath(['segment', 'to', 'hola', 'adios'], chain)).toBe(false);
|
||||
expect(matchesPath(['segment', 'to', 'hola', 'adios', 'que'], chain)).toBe(false);
|
||||
expect(matchesPath(['segment', 'to', 'hola', 'adios', 'que', 'tal'], chain)).toBe(true);
|
||||
|
||||
expect(matchesPath(['to'], chain)).toBe(false);
|
||||
expect(matchesPath(['path', 'to'], chain)).toBe(false);
|
||||
});
|
||||
|
||||
it('should match simple route 2', () => {
|
||||
const chain: RouteChain = [{ id: '5', path: ['hola'], props: undefined }];
|
||||
expect(matchesPath([''], chain)).toBe(false);
|
||||
expect(matchesPath(['hola'], chain)).toBe(true);
|
||||
expect(matchesPath(['hola', 'hola'], chain)).toBe(true);
|
||||
expect(matchesPath(['hola', 'adios'], chain)).toBe(true);
|
||||
});
|
||||
|
||||
it('should match simple route 3', () => {
|
||||
const chain: RouteChain = [{ id: '5', path: ['hola', 'adios'], props: undefined }];
|
||||
expect(matchesPath([''], chain)).toBe(false);
|
||||
expect(matchesPath(['hola'], chain)).toBe(false);
|
||||
expect(matchesPath(['hola', 'hola'], chain)).toBe(false);
|
||||
expect(matchesPath(['hola', 'adios'], chain)).toBe(true);
|
||||
});
|
||||
|
||||
it('should match simple route 4', () => {
|
||||
const chain: RouteChain = [
|
||||
{ id: '5', path: ['hola'], props: undefined },
|
||||
{ id: '5', path: ['adios'], props: undefined }];
|
||||
|
||||
expect(matchesPath([''], chain)).toBe(false);
|
||||
expect(matchesPath(['hola'], chain)).toBe(false);
|
||||
expect(matchesPath(['hola', 'hola'], chain)).toBe(false);
|
||||
expect(matchesPath(['hola', 'adios'], chain)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('routerPathToChain', () => {
|
||||
it('should match the route with higher priority', () => {
|
||||
const chain3: RouteChain = [{ id: '5', path: ['hola'], props: undefined }];
|
||||
const chain4: RouteChain = [
|
||||
{ id: '5', path: ['hola'], props: undefined },
|
||||
{ id: '5', path: ['adios'], props: undefined }];
|
||||
|
||||
const routes: RouteChain[] = [
|
||||
CHAIN_1,
|
||||
CHAIN_2,
|
||||
chain3,
|
||||
chain4
|
||||
];
|
||||
expect(routerPathToChain(['to'], routes)).toEqual({
|
||||
chain: null,
|
||||
matches: 0,
|
||||
});
|
||||
|
||||
expect(routerPathToChain([''], routes)).toEqual({
|
||||
chain: null,
|
||||
matches: 0,
|
||||
});
|
||||
expect(routerPathToChain(['segment', 'to'], routes)).toEqual({
|
||||
chain: null,
|
||||
matches: 0,
|
||||
});
|
||||
|
||||
expect(routerPathToChain(['hola'], routes)).toEqual({
|
||||
chain: chain3,
|
||||
matches: 1,
|
||||
});
|
||||
expect(routerPathToChain(['hola', 'hola'], routes)).toEqual({
|
||||
chain: chain3,
|
||||
matches: 1,
|
||||
});
|
||||
|
||||
expect(routerPathToChain(['hola', 'adios'], routes)).toEqual({
|
||||
chain: chain4,
|
||||
matches: 2,
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should match the default route', () => {
|
||||
const chain1: RouteChain = [
|
||||
{ id: 'tabs', path: [''], props: undefined },
|
||||
{ id: 'tab1', path: [''], props: undefined },
|
||||
{ id: 'schedule', path: [''], props: undefined }
|
||||
];
|
||||
const chain2: RouteChain = [
|
||||
{ id: 'tabs', path: [''], props: undefined },
|
||||
{ id: 'tab2', path: ['tab2'], props: undefined },
|
||||
{ id: 'page2', path: [''], props: undefined }
|
||||
];
|
||||
|
||||
expect(routerPathToChain([''], [chain1])).toEqual({chain: chain1, matches: 3});
|
||||
expect(routerPathToChain(['tab2'], [chain1])).toEqual({chain: null, matches: 0});
|
||||
|
||||
expect(routerPathToChain([''], [chain2])).toEqual({chain: null, matches: 0});
|
||||
expect(routerPathToChain(['tab2'], [chain2])).toEqual({chain: chain2, matches: 3});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
// describe('matchRoute', () => {
|
||||
// it('should match simple route', () => {
|
||||
// const path = ['path', 'to', 'component'];
|
||||
// const routes: RouteChain[] = [
|
||||
// [{ id: 2, path: ['to'], props: undefined }],
|
||||
// [{ id: 1, path: ['path'], props: undefined }],
|
||||
// [{ id: 3, path: ['segment'], props: undefined }],
|
||||
// [{ id: 4, path: [''], props: undefined }],
|
||||
// ];
|
||||
// const match = routerPathToChain(path, routes);
|
||||
// expect(match).toEqual({ id: 1, path: ['path'], children: [] });
|
||||
// expect(seg.next()).toEqual('to');
|
||||
// });
|
||||
|
||||
// it('should match default route', () => {
|
||||
// const routes: RouteTree = [
|
||||
// { id: 2, path: ['to'], children: [], props: undefined },
|
||||
// { id: 1, path: ['path'], children: [], props: undefined },
|
||||
// { id: 3, path: ['segment'], children: [], props: undefined },
|
||||
// { id: 4, path: [''], children: [], props: undefined },
|
||||
// ];
|
||||
// const seg = new RouterSegments(['hola', 'path']);
|
||||
// let match = matchRoute(seg, routes);
|
||||
// expect(match).toBeNull();
|
||||
|
||||
// match = matchRoute(seg, routes);
|
||||
// expect(match.id).toEqual(1);
|
||||
|
||||
// for (let i = 0; i < 20; i++) {
|
||||
// match = matchRoute(seg, routes);
|
||||
// expect(match.id).toEqual(4);
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('should not match any route', () => {
|
||||
// const routes: RouteTree = [
|
||||
// { id: 2, path: ['to', 'to', 'to'], children: [], props: undefined },
|
||||
// { id: 1, path: ['adam', 'manu'], children: [], props: undefined },
|
||||
// { id: 3, path: ['hola', 'adam'], children: [], props: undefined },
|
||||
// { id: 4, path: [''], children: [], props: undefined },
|
||||
// ];
|
||||
// const seg = new RouterSegments(['hola', 'manu', 'adam']);
|
||||
// const match = matchRoute(seg, routes);
|
||||
// expect(match).toBeNull();
|
||||
// });
|
||||
|
||||
// it('should not match if there are not routes', () => {
|
||||
// const routes: RouteTree = [];
|
||||
// const seg = new RouterSegments(['adam']);
|
||||
// expect(matchRoute(seg, routes)).toBeNull();
|
||||
// expect(matchRoute(seg, routes)).toBeNull();
|
||||
// expect(matchRoute(seg, routes)).toBeNull();
|
||||
// });
|
||||
|
||||
// it('should not match any route (2)', () => {
|
||||
// const routes: RouteTree = [
|
||||
// { id: 1, path: ['adam', 'manu'], children: [], props: undefined },
|
||||
// { id: 3, path: ['hola', 'adam'], children: [], props: undefined },
|
||||
// ];
|
||||
// const seg = new RouterSegments(['adam']);
|
||||
// expect(matchRoute(seg, routes)).toBeNull();
|
||||
// expect(matchRoute(seg, routes)).toBeNull();
|
||||
// expect(matchRoute(seg, routes)).toBeNull();
|
||||
// });
|
||||
|
||||
// it ('should match multiple segments', () => {
|
||||
// const routes: RouteTree = [
|
||||
// { id: 1, path: ['adam', 'manu'], children: [], props: undefined },
|
||||
// { id: 2, path: ['manu', 'hello'], children: [], props: undefined },
|
||||
// { id: 3, path: ['hello'], children: [], props: undefined },
|
||||
// { id: 4, path: [''], children: [], props: undefined },
|
||||
// ];
|
||||
// const seg = new RouterSegments(['adam', 'manu', 'hello', 'manu', 'hello']);
|
||||
// let match = matchRoute(seg, routes);
|
||||
// expect(match.id).toEqual(1);
|
||||
|
||||
// match = matchRoute(seg, routes);
|
||||
// expect(match.id).toEqual(3);
|
||||
|
||||
// match = matchRoute(seg, routes);
|
||||
// expect(match.id).toEqual(2);
|
||||
|
||||
// match = matchRoute(seg, routes);
|
||||
// expect(match.id).toEqual(4);
|
||||
|
||||
// match = matchRoute(seg, routes);
|
||||
// expect(match.id).toEqual(4);
|
||||
// });
|
||||
|
||||
// it('should match long multi segments', () => {
|
||||
// const routes: RouteTree = [
|
||||
// { id: 1, path: ['adam', 'manu', 'hello', 'menu', 'hello'], children: [], props: undefined },
|
||||
// { id: 2, path: ['adam', 'manu', 'hello', 'menu'], children: [], props: undefined },
|
||||
// { id: 3, path: ['adam', 'manu'], children: [], props: undefined },
|
||||
// ];
|
||||
// const seg = new RouterSegments(['adam', 'manu', 'hello', 'menu', 'hello']);
|
||||
// const match = matchRoute(seg, routes);
|
||||
// expect(match.id).toEqual(1);
|
||||
// expect(matchRoute(seg, routes)).toBeNull();
|
||||
// });
|
||||
|
||||
// it('should match long multi segments', () => {
|
||||
// let match = matchRoute(new RouterSegments(['']), null);
|
||||
// expect(match).toBeNull();
|
||||
|
||||
// match = matchRoute(new RouterSegments(['hola']), null);
|
||||
// expect(match).toBeNull();
|
||||
// });
|
||||
// });
|
||||
66
packages/core/src/components/router/test/parser.spec.tsx
Normal file
66
packages/core/src/components/router/test/parser.spec.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { mockElement } from '@stencil/core/testing';
|
||||
import { flattenRouterTree, readRoutes } from '../utils/parser';
|
||||
import { RouteTree } from '../utils/interfaces';
|
||||
|
||||
describe('readRoutes', () => {
|
||||
it('should read URL', () => {
|
||||
const root = mockElement('div');
|
||||
const r1 = mockRouteElement('/', 'main-page');
|
||||
const r2 = mockRouteElement('/one-page', 'one-page');
|
||||
const r3 = mockRouteElement('secondpage', 'second-page');
|
||||
const r4 = mockRouteElement('/5/hola', '4');
|
||||
const r5 = mockRouteElement('/path/to/five', '5');
|
||||
const r6 = mockRouteElement('/path/to/five2', '6');
|
||||
|
||||
root.appendChild(r1);
|
||||
root.appendChild(r2);
|
||||
root.appendChild(r3);
|
||||
r3.appendChild(r4);
|
||||
r4.appendChild(r5);
|
||||
r4.appendChild(r6);
|
||||
|
||||
const expected: RouteTree = [
|
||||
{ path: [''], id: 'main-page', children: [], props: undefined },
|
||||
{ path: ['one-page'], id: 'one-page', children: [], props: undefined },
|
||||
{ path: ['secondpage'], id: 'second-page', props: undefined, children: [
|
||||
{ path: ['5', 'hola'], id: '4', props: undefined, children: [
|
||||
{ path: ['path', 'to', 'five'], id: '5', children: [], props: undefined },
|
||||
{ path: ['path', 'to', 'five2'], id: '6', children: [], props: undefined }
|
||||
] }
|
||||
] }
|
||||
];
|
||||
expect(readRoutes(root)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flattenRouterTree', () => {
|
||||
it('should process routes', () => {
|
||||
const entries: RouteTree = [
|
||||
{ path: [''], id: 'hola', children: [], props: undefined },
|
||||
{ path: ['one-page'], id: 'one-page', children: [], props: undefined },
|
||||
{ path: ['secondpage'], id: 'second-page', props: undefined, children: [
|
||||
{ path: ['5', 'hola'], id: '4', props: undefined, children: [
|
||||
{ path: ['path', 'to', 'five'], id: '5', children: [], props: undefined },
|
||||
{ path: ['path', 'to', 'five2'], id: '6', children: [], props: undefined }
|
||||
] }
|
||||
] }
|
||||
];
|
||||
const routes = flattenRouterTree(entries);
|
||||
expect(routes).toEqual([
|
||||
[{ path: [''], id: 'hola' }],
|
||||
[{ path: ['one-page'], id: 'one-page' }],
|
||||
[{ path: ['secondpage'], id: 'second-page'}, { path: ['5', 'hola'], id: '4'}, { path: ['path', 'to', 'five'], id: '5'}],
|
||||
[{ path: ['secondpage'], id: 'second-page'}, { path: ['5', 'hola'], id: '4'}, { path: ['path', 'to', 'five2'], id: '6'}],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
export function mockRouteElement(path: string, component: string) {
|
||||
const el = mockElement('ion-route');
|
||||
el.setAttribute('path', path);
|
||||
(el as any).component = component;
|
||||
return el;
|
||||
}
|
||||
55
packages/core/src/components/router/test/path.spec.tsx
Normal file
55
packages/core/src/components/router/test/path.spec.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { generatePath, parsePath } from '../utils/path';
|
||||
|
||||
describe('parseURL', () => {
|
||||
it('should parse empty path', () => {
|
||||
expect(parsePath('')).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse empty path (2)', () => {
|
||||
expect(parsePath(' ')).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse null path', () => {
|
||||
expect(parsePath(null)).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse undefined path', () => {
|
||||
expect(parsePath(undefined)).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse relative path', () => {
|
||||
expect(parsePath('path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
||||
});
|
||||
|
||||
it('should parse absolute path', () => {
|
||||
expect(parsePath('/path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
||||
});
|
||||
it('should parse relative path', () => {
|
||||
expect(parsePath('/PATH///to//file.js//')).toEqual(['PATH', 'to', 'file.js']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('generatePath', () => {
|
||||
it('should generate an empty URL', () => {
|
||||
expect(generatePath([])).toEqual('/');
|
||||
expect(generatePath([{ path: '' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: '/' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: '//' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: ' ' } as any])).toEqual('/');
|
||||
});
|
||||
|
||||
it('should genenerate a basic url', () => {
|
||||
const stack = [
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'path/to',
|
||||
'page',
|
||||
'number-TWO',
|
||||
''
|
||||
];
|
||||
expect(generatePath(stack)).toEqual('/path/to/page/number-TWO');
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,235 +0,0 @@
|
||||
import {
|
||||
RouterEntries, RouterSegments, breadthFirstSearch,
|
||||
generatePath, matchRoute, parsePath
|
||||
} from '../router-utils';
|
||||
|
||||
describe('RouterSegments', () => {
|
||||
it ('should initialize with empty array', () => {
|
||||
const s = new RouterSegments([]);
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('');
|
||||
});
|
||||
|
||||
it ('should initialize with array', () => {
|
||||
const s = new RouterSegments(['', 'path', 'to', 'destination']);
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('path');
|
||||
expect(s.next()).toEqual('to');
|
||||
expect(s.next()).toEqual('destination');
|
||||
expect(s.next()).toEqual('');
|
||||
expect(s.next()).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseURL', () => {
|
||||
it('should parse empty path', () => {
|
||||
expect(parsePath('')).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse empty path (2)', () => {
|
||||
expect(parsePath(' ')).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse null path', () => {
|
||||
expect(parsePath(null)).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse undefined path', () => {
|
||||
expect(parsePath(undefined)).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should parse relative path', () => {
|
||||
expect(parsePath('path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
||||
});
|
||||
|
||||
it('should parse absolute path', () => {
|
||||
expect(parsePath('/path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
||||
});
|
||||
it('should parse relative path', () => {
|
||||
expect(parsePath('/PATH///to//file.js//')).toEqual(['PATH', 'to', 'file.js']);
|
||||
});
|
||||
});
|
||||
|
||||
// describe('readRoutes', () => {
|
||||
// it('should read URL', () => {
|
||||
// const node = (<div>
|
||||
// <ion-route path='/' component='main-page'/>
|
||||
// <ion-route path='/one-page' component='one-page'/>
|
||||
// <ion-route path='secondpage' component='second-page'/>
|
||||
// <ion-route path='/5/hola' component='4'/>
|
||||
// <ion-route path='path/to/five' component='5'/>
|
||||
// </div>) as any;
|
||||
// node.children = node.vchildren;
|
||||
|
||||
// expect(readRoutes(node)).toEqual([
|
||||
// { path: [''], id: 'hola', subroutes: [] },
|
||||
// { path: ['one-page'], id: 'one-page', subroutes: [] },
|
||||
// { path: ['secondpage'], id: 'second-page', subroutes: [] },
|
||||
// { path: ['5', 'hola'], id: '4', subroutes: [] },
|
||||
// { path: ['path', 'to', 'five'], id: '5', subroutes: [] }
|
||||
// ]);
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('matchRoute', () => {
|
||||
it('should match simple route', () => {
|
||||
const seg = new RouterSegments(['path', 'to', 'component']);
|
||||
const routes: RouterEntries = [
|
||||
{ id: 2, path: ['to'], subroutes: [] },
|
||||
{ id: 1, path: ['path'], subroutes: [] },
|
||||
{ id: 3, path: ['segment'], subroutes: [] },
|
||||
{ id: 4, path: [''], subroutes: [] },
|
||||
];
|
||||
const match = matchRoute(seg, routes);
|
||||
expect(match).toEqual({ id: 1, path: ['path'], subroutes: [] });
|
||||
expect(seg.next()).toEqual('to');
|
||||
});
|
||||
|
||||
it('should match default route', () => {
|
||||
const routes: RouterEntries = [
|
||||
{ id: 2, path: ['to'], subroutes: [] },
|
||||
{ id: 1, path: ['path'], subroutes: [] },
|
||||
{ id: 3, path: ['segment'], subroutes: [] },
|
||||
{ id: 4, path: [''], subroutes: [] },
|
||||
];
|
||||
const seg = new RouterSegments(['hola', 'path']);
|
||||
let match = matchRoute(seg, routes);
|
||||
expect(match).toBeNull();
|
||||
|
||||
match = matchRoute(seg, routes);
|
||||
expect(match.id).toEqual(1);
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
match = matchRoute(seg, routes);
|
||||
expect(match.id).toEqual(4);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not match any route', () => {
|
||||
const routes: RouterEntries = [
|
||||
{ id: 2, path: ['to', 'to', 'to'], subroutes: [] },
|
||||
{ id: 1, path: ['adam', 'manu'], subroutes: [] },
|
||||
{ id: 3, path: ['hola', 'adam'], subroutes: [] },
|
||||
{ id: 4, path: [''], subroutes: [] },
|
||||
];
|
||||
const seg = new RouterSegments(['hola', 'manu', 'adam']);
|
||||
const match = matchRoute(seg, routes);
|
||||
expect(match).toBeNull();
|
||||
});
|
||||
|
||||
it('should not match if there are not routes', () => {
|
||||
const routes: RouterEntries = [];
|
||||
const seg = new RouterSegments(['adam']);
|
||||
expect(matchRoute(seg, routes)).toBeNull();
|
||||
expect(matchRoute(seg, routes)).toBeNull();
|
||||
expect(matchRoute(seg, routes)).toBeNull();
|
||||
});
|
||||
|
||||
it('should not match any route (2)', () => {
|
||||
const routes: RouterEntries = [
|
||||
{ id: 1, path: ['adam', 'manu'], subroutes: [] },
|
||||
{ id: 3, path: ['hola', 'adam'], subroutes: [] },
|
||||
];
|
||||
const seg = new RouterSegments(['adam']);
|
||||
expect(matchRoute(seg, routes)).toBeNull();
|
||||
expect(matchRoute(seg, routes)).toBeNull();
|
||||
expect(matchRoute(seg, routes)).toBeNull();
|
||||
});
|
||||
|
||||
it ('should match multiple segments', () => {
|
||||
const routes: RouterEntries = [
|
||||
{ id: 1, path: ['adam', 'manu'], subroutes: [] },
|
||||
{ id: 2, path: ['manu', 'hello'], subroutes: [] },
|
||||
{ id: 3, path: ['hello'], subroutes: [] },
|
||||
{ id: 4, path: [''], subroutes: [] },
|
||||
];
|
||||
const seg = new RouterSegments(['adam', 'manu', 'hello', 'manu', 'hello']);
|
||||
let match = matchRoute(seg, routes);
|
||||
expect(match.id).toEqual(1);
|
||||
|
||||
match = matchRoute(seg, routes);
|
||||
expect(match.id).toEqual(3);
|
||||
|
||||
match = matchRoute(seg, routes);
|
||||
expect(match.id).toEqual(2);
|
||||
|
||||
match = matchRoute(seg, routes);
|
||||
expect(match.id).toEqual(4);
|
||||
|
||||
match = matchRoute(seg, routes);
|
||||
expect(match.id).toEqual(4);
|
||||
});
|
||||
|
||||
it('should match long multi segments', () => {
|
||||
const routes: RouterEntries = [
|
||||
{ id: 1, path: ['adam', 'manu', 'hello', 'menu', 'hello'], subroutes: [] },
|
||||
{ id: 2, path: ['adam', 'manu', 'hello', 'menu'], subroutes: [] },
|
||||
{ id: 3, path: ['adam', 'manu'], subroutes: [] },
|
||||
];
|
||||
const seg = new RouterSegments(['adam', 'manu', 'hello', 'menu', 'hello']);
|
||||
const match = matchRoute(seg, routes);
|
||||
expect(match.id).toEqual(1);
|
||||
expect(matchRoute(seg, routes)).toBeNull();
|
||||
});
|
||||
|
||||
it('should match long multi segments', () => {
|
||||
let match = matchRoute(new RouterSegments(['']), null);
|
||||
expect(match).toBeNull();
|
||||
|
||||
match = matchRoute(new RouterSegments(['hola']), null);
|
||||
expect(match).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('generatePath', () => {
|
||||
it('should generate an empty URL', () => {
|
||||
expect(generatePath([])).toEqual('/');
|
||||
expect(generatePath([{ path: '' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: '/' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: '//' } as any])).toEqual('/');
|
||||
expect(generatePath([{ path: ' ' } as any])).toEqual('/');
|
||||
});
|
||||
|
||||
it('should genenerate a basic url', () => {
|
||||
const stack = [
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'path/to',
|
||||
'page',
|
||||
'number-TWO',
|
||||
''
|
||||
];
|
||||
expect(generatePath(stack)).toEqual('/path/to/page/number-TWO');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('breadthFirstSearch', () => {
|
||||
it('should search in order', () => {
|
||||
const n1 = { tagName: 'ION-TABS', children: [] as any };
|
||||
const n2 = { tagName: 'DIV', children: [n1] };
|
||||
const n3 = { tagName: 'ION-NAV', children: [n2] };
|
||||
const n4 = { tagName: 'ION-TABS', children: [] as any };
|
||||
const n5 = { tagName: 'DIV', children: [n4] };
|
||||
const n6 = { tagName: 'DIV', children: [n5, n3] };
|
||||
const n7 = { tagName: 'DIV', children: [] as any };
|
||||
const n8 = { tagName: 'DIV', children: [n6] };
|
||||
const n9 = { tagName: 'DIV', children: [n8, n7] };
|
||||
|
||||
expect(breadthFirstSearch(n9 as any)).toBe(n3);
|
||||
expect(breadthFirstSearch(n8 as any)).toBe(n3);
|
||||
expect(breadthFirstSearch(n7 as any)).toBe(null);
|
||||
expect(breadthFirstSearch(n6 as any)).toBe(n3);
|
||||
expect(breadthFirstSearch(n5 as any)).toBe(n4);
|
||||
expect(breadthFirstSearch(n4 as any)).toBe(n4);
|
||||
expect(breadthFirstSearch(n3 as any)).toBe(n3);
|
||||
expect(breadthFirstSearch(n2 as any)).toBe(n1);
|
||||
expect(breadthFirstSearch(n1 as any)).toBe(n1);
|
||||
});
|
||||
});
|
||||
|
||||
51
packages/core/src/components/router/utils/common.ts
Normal file
51
packages/core/src/components/router/utils/common.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { NavOutletElement } from './interfaces';
|
||||
|
||||
export class RouterSegments {
|
||||
private path: string[];
|
||||
constructor(path: string[]) {
|
||||
this.path = path.slice();
|
||||
}
|
||||
|
||||
isDefault(): boolean {
|
||||
if (this.path.length > 0) {
|
||||
return this.path[0] === '';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
next(): string {
|
||||
if (this.path.length > 0) {
|
||||
return this.path.shift() as string;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const navs = ['ION-NAV', 'ION-TABS'];
|
||||
export function breadthFirstSearch(root: HTMLElement): NavOutletElement | null {
|
||||
if (!root) {
|
||||
console.error('search root is null');
|
||||
return null;
|
||||
}
|
||||
// we do a Breadth-first search
|
||||
// Breadth-first search (BFS) is an algorithm for traversing or searching tree
|
||||
// or graph data structures.It starts at the tree root(or some arbitrary node of a graph,
|
||||
// sometimes referred to as a 'search key'[1]) and explores the neighbor nodes
|
||||
// first, before moving to the next level neighbours.
|
||||
|
||||
const queue = [root];
|
||||
let node: HTMLElement | undefined;
|
||||
while (node = queue.shift()) {
|
||||
// visit node
|
||||
if (navs.indexOf(node.tagName) >= 0) {
|
||||
return node as NavOutletElement;
|
||||
}
|
||||
|
||||
// queue children
|
||||
const children = node.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
queue.push(children[i] as NavOutletElement);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
50
packages/core/src/components/router/utils/dom.ts
Normal file
50
packages/core/src/components/router/utils/dom.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { breadthFirstSearch } from './common';
|
||||
import { NavOutlet, RouteChain } from './interfaces';
|
||||
|
||||
export function writeNavState(root: HTMLElement, chain: RouteChain, index: number, direction: number): Promise<void> {
|
||||
if (index >= chain.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const route = chain[index];
|
||||
const node = breadthFirstSearch(root);
|
||||
if (!node) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return node.componentOnReady()
|
||||
.then(() => node.setRouteId(route.id, route.props, direction))
|
||||
.then(changed => {
|
||||
if (changed) {
|
||||
direction = 0;
|
||||
}
|
||||
const nextEl = node.getContentElement();
|
||||
if (nextEl) {
|
||||
return writeNavState(nextEl, chain, index + 1, direction)
|
||||
.then(() => node.markVisible());
|
||||
} else {
|
||||
return node.markVisible();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function readNavState(node: HTMLElement) {
|
||||
const stack: string[] = [];
|
||||
let pivot: NavOutlet|null;
|
||||
while (true) {
|
||||
pivot = breadthFirstSearch(node);
|
||||
if (pivot) {
|
||||
const cmp = pivot.getRouteId();
|
||||
if (cmp) {
|
||||
node = pivot.getContentElement();
|
||||
stack.push(cmp.toLowerCase());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
ids: stack,
|
||||
pivot: pivot,
|
||||
};
|
||||
}
|
||||
28
packages/core/src/components/router/utils/interfaces.ts
Normal file
28
packages/core/src/components/router/utils/interfaces.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
export interface NavOutlet {
|
||||
setRouteId(id: any, data: any, direction: number): Promise<boolean>;
|
||||
markVisible(): Promise<void>;
|
||||
getRouteId(): string;
|
||||
|
||||
getContentElement(): HTMLElement | null;
|
||||
}
|
||||
|
||||
export interface RouteMatch {
|
||||
chain: RouteChain;
|
||||
matches: number;
|
||||
}
|
||||
|
||||
export type NavOutletElement = NavOutlet & HTMLStencilElement;
|
||||
|
||||
export interface RouteEntry {
|
||||
id: string;
|
||||
path: string[];
|
||||
props: any|undefined;
|
||||
}
|
||||
|
||||
export interface RouteNode extends RouteEntry {
|
||||
children: RouteTree;
|
||||
}
|
||||
|
||||
export type RouteChain = RouteEntry[];
|
||||
export type RouteTree = RouteNode[];
|
||||
71
packages/core/src/components/router/utils/matching.ts
Normal file
71
packages/core/src/components/router/utils/matching.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { RouterSegments } from './common';
|
||||
import { RouteChain, RouteMatch } from './interfaces';
|
||||
|
||||
export function matchesIDs(ids: string[], chain: RouteChain): number {
|
||||
const len = Math.min(ids.length, chain.length);
|
||||
let i = 0;
|
||||
for (; i < len; i++) {
|
||||
if (ids[i] !== chain[i].id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
export function matchesPath(path: string[], chain: RouteChain): boolean {
|
||||
const segments = new RouterSegments(path);
|
||||
let matchesDefault = false;
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
const route = chain[i];
|
||||
if (route.path[0] !== '') {
|
||||
for (const segment of route.path) {
|
||||
if (segments.next() !== segment) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
matchesDefault = false;
|
||||
} else {
|
||||
matchesDefault = true;
|
||||
}
|
||||
}
|
||||
if (matchesDefault) {
|
||||
return matchesDefault === segments.isDefault();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export function routerIDsToChain(ids: string[], chains: RouteChain[]): RouteMatch {
|
||||
let match: RouteChain|null = null;
|
||||
let maxMatches = 0;
|
||||
for (const chain of chains) {
|
||||
const score = matchesIDs(ids, chain);
|
||||
if (score > maxMatches) {
|
||||
match = chain;
|
||||
maxMatches = score;
|
||||
}
|
||||
}
|
||||
return {
|
||||
chain: match,
|
||||
matches: maxMatches,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function routerPathToChain(path: string[], chains: RouteChain[]): RouteMatch|null {
|
||||
let match: RouteChain = null;
|
||||
let matches = 0;
|
||||
for (const chain of chains) {
|
||||
if (matchesPath(path, chain)) {
|
||||
if (chain.length > matches) {
|
||||
matches = chain.length;
|
||||
match = chain;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
chain: match,
|
||||
matches,
|
||||
};
|
||||
}
|
||||
49
packages/core/src/components/router/utils/parser.ts
Normal file
49
packages/core/src/components/router/utils/parser.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { RouteChain, RouteNode, RouteTree } from './interfaces';
|
||||
import { parsePath } from './path';
|
||||
|
||||
|
||||
export function readRoutes(root: Element): RouteTree {
|
||||
return (Array.from(root.children) as HTMLIonRouteElement[])
|
||||
.filter(el => el.tagName === 'ION-ROUTE')
|
||||
.map(el => ({
|
||||
path: parsePath(readProp(el, 'path')),
|
||||
id: readProp(el, 'component'),
|
||||
props: readProp(el, 'props'),
|
||||
children: readRoutes(el)
|
||||
}));
|
||||
}
|
||||
|
||||
export function readProp(el: HTMLElement, prop: string): string|undefined {
|
||||
if (prop in el) {
|
||||
return (el as any)[prop];
|
||||
}
|
||||
if (el.hasAttribute(prop)) {
|
||||
return el.getAttribute(prop);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function flattenRouterTree(nodes: RouteTree): RouteChain[] {
|
||||
const routes: RouteChain[] = [];
|
||||
for (const node of nodes) {
|
||||
flattenNode([], routes, node);
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
function flattenNode(chain: RouteChain, routes: RouteChain[], node: RouteNode) {
|
||||
const s = chain.slice();
|
||||
s.push({
|
||||
id: node.id,
|
||||
path: node.path,
|
||||
props: node.props
|
||||
});
|
||||
|
||||
if (node.children.length === 0) {
|
||||
routes.push(s);
|
||||
return;
|
||||
}
|
||||
for (const sub of node.children) {
|
||||
flattenNode(s, routes, sub);
|
||||
}
|
||||
}
|
||||
62
packages/core/src/components/router/utils/path.ts
Normal file
62
packages/core/src/components/router/utils/path.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { RouteChain } from './interfaces';
|
||||
|
||||
export function generatePath(segments: string[]): string {
|
||||
const path = segments
|
||||
.filter(s => s.length > 0)
|
||||
.join('/');
|
||||
|
||||
return '/' + path;
|
||||
}
|
||||
|
||||
export function chainToPath(chain: RouteChain): string[] {
|
||||
const path = [];
|
||||
for (const route of chain) {
|
||||
if (route.path[0] !== '') {
|
||||
path.push(...route.path);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function writePath(history: History, base: string, usePath: boolean, path: string[], isPop: boolean, state: number) {
|
||||
path = [base, ...path];
|
||||
let url = generatePath(path);
|
||||
if (usePath) {
|
||||
url = '#' + url;
|
||||
}
|
||||
state++;
|
||||
if (isPop) {
|
||||
history.back();
|
||||
history.replaceState(state, null, url);
|
||||
} else {
|
||||
history.pushState(state, null, url);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export function readPath(loc: Location, base: string, useHash: boolean): string[] | null {
|
||||
const path = useHash
|
||||
? loc.hash.substr(1)
|
||||
: loc.pathname;
|
||||
|
||||
if (path.startsWith(base)) {
|
||||
return parsePath(path.slice(base.length));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parsePath(path: string): string[] {
|
||||
if (path === null || path === undefined) {
|
||||
return [''];
|
||||
}
|
||||
const segments = path.split('/')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
|
||||
if (segments.length === 0) {
|
||||
return [''];
|
||||
} else {
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,4 @@ export class Segment {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,10 +60,6 @@ export class SelectOption {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,9 +39,6 @@ export class ShowWhen implements DisplayWhen {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,4 @@ export class Slide {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,9 +149,6 @@ export class SplitPane {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ ion-tab-button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
ion-tab-button a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tab-cover {
|
||||
@include margin(0);
|
||||
@include padding(0);
|
||||
|
||||
@@ -33,8 +33,11 @@ export class TabButton {
|
||||
|
||||
@Listen('click')
|
||||
protected onClick(ev: UIEvent) {
|
||||
this.ionTabbarClick.emit(this.tab);
|
||||
if (!this.tab.disabled) {
|
||||
this.ionTabbarClick.emit(this.tab);
|
||||
}
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private onKeyUp() {
|
||||
@@ -73,18 +76,19 @@ export class TabButton {
|
||||
|
||||
render() {
|
||||
const tab = this.tab;
|
||||
const href = tab.href || '#';
|
||||
|
||||
return [
|
||||
<button
|
||||
type='button'
|
||||
<a
|
||||
href={href}
|
||||
class='tab-cover'
|
||||
onKeyUp={this.onKeyUp.bind(this)}
|
||||
onBlur={this.onBlur.bind(this)}
|
||||
disabled={tab.disabled}>
|
||||
onBlur={this.onBlur.bind(this)}>
|
||||
{ tab.icon && <ion-icon class='tab-button-icon' name={tab.icon}></ion-icon> }
|
||||
{ tab.title && <span class='tab-button-text'>{tab.title}</span> }
|
||||
{ tab.badge && <ion-badge class='tab-badge' color={tab.badgeStyle}>{tab.badge}</ion-badge> }
|
||||
{ this.mode === 'md' && <ion-ripple-effect/> }
|
||||
</button>
|
||||
</a>
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,13 @@ boolean
|
||||
If true, the user cannot interact with the tab. Defaults to `false`.
|
||||
|
||||
|
||||
#### href
|
||||
|
||||
string
|
||||
|
||||
The URL which will be used as the `href` within this tab's `<ion-tab-button>` anchor.
|
||||
|
||||
|
||||
#### icon
|
||||
|
||||
string
|
||||
@@ -170,6 +177,13 @@ boolean
|
||||
If true, the user cannot interact with the tab. Defaults to `false`.
|
||||
|
||||
|
||||
#### href
|
||||
|
||||
string
|
||||
|
||||
The URL which will be used as the `href` within this tab's `<ion-tab-button>` anchor.
|
||||
|
||||
|
||||
#### icon
|
||||
|
||||
string
|
||||
|
||||
@@ -9,7 +9,7 @@ import { asyncRaf } from '../../utils/helpers';
|
||||
export class Tab {
|
||||
|
||||
private loaded = false;
|
||||
@Element() el: HTMLElement;
|
||||
@Element() el: HTMLIonTabElement;
|
||||
|
||||
@State() init = false;
|
||||
|
||||
@@ -22,6 +22,11 @@ export class Tab {
|
||||
*/
|
||||
@Prop() title: string;
|
||||
|
||||
/**
|
||||
* The URL which will be used as the `href` within this tab's `<ion-tab-button>` anchor.
|
||||
*/
|
||||
@Prop() href: string;
|
||||
|
||||
/**
|
||||
* The icon for the tab.
|
||||
*/
|
||||
@@ -92,7 +97,7 @@ export class Tab {
|
||||
}
|
||||
|
||||
@Method()
|
||||
setActive(): Promise<any> {
|
||||
setActive(): Promise<HTMLIonTabElement> {
|
||||
return this.prepareLazyLoaded().then(() => this.showTab());
|
||||
}
|
||||
|
||||
@@ -104,9 +109,9 @@ export class Tab {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private showTab(): Promise<any|void> {
|
||||
private showTab(): Promise<HTMLIonTabElement> {
|
||||
this.active = true;
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(this.el);
|
||||
}
|
||||
|
||||
hostData() {
|
||||
@@ -120,9 +125,6 @@ export class Tab {
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return <slot/>;
|
||||
}
|
||||
}
|
||||
|
||||
function attachViewToDom(container: HTMLElement, cmp: string): Promise<any> {
|
||||
|
||||
@@ -240,6 +240,9 @@ Emitted when the tab changes.
|
||||
#### getTabs()
|
||||
|
||||
|
||||
#### markVisible()
|
||||
|
||||
|
||||
#### select()
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Config, NavOutlet } from '../../index';
|
||||
export class Tabs implements NavOutlet {
|
||||
private ids = -1;
|
||||
private transitioning = false;
|
||||
private routingView: HTMLIonTabElement;
|
||||
|
||||
private tabsId: number = (++tabIds);
|
||||
|
||||
@Element() el: HTMLElement;
|
||||
@@ -73,8 +75,7 @@ export class Tabs implements NavOutlet {
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
return this.initTabs()
|
||||
.then(() => this.initSelect());
|
||||
return this.initTabs().then(() => this.initSelect());
|
||||
}
|
||||
|
||||
componentDidUnload() {
|
||||
@@ -93,37 +94,8 @@ export class Tabs implements NavOutlet {
|
||||
*/
|
||||
@Method()
|
||||
select(tabOrIndex: number | HTMLIonTabElement): Promise<boolean> {
|
||||
if (this.transitioning) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const selectedTab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex);
|
||||
if (!selectedTab) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// Reset rest of tabs
|
||||
for (const tab of this.tabs) {
|
||||
if (selectedTab !== tab) {
|
||||
tab.selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
const leavingTab = this.selectedTab;
|
||||
this.transitioning = true;
|
||||
return selectedTab.setActive().then(() => {
|
||||
this.transitioning = false;
|
||||
selectedTab.selected = true;
|
||||
if (leavingTab !== selectedTab) {
|
||||
if (leavingTab) {
|
||||
leavingTab.active = false;
|
||||
}
|
||||
this.selectedTab = selectedTab;
|
||||
this.ionChange.emit(selectedTab);
|
||||
this.ionNavChanged.emit({isPop: false});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return this.setActive(tabOrIndex)
|
||||
.then(selectedTab => this.tabSwitch(selectedTab));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,9 +131,18 @@ export class Tabs implements NavOutlet {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const tab = this.tabs.find(t => id === t.getRouteId());
|
||||
return this.select(tab).then(() => true);
|
||||
return this.setActive(tab).then(() => {
|
||||
this.routingView = tab;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Method()
|
||||
markVisible(): Promise<void> {
|
||||
this.tabSwitch(this.routingView);
|
||||
this.routingView = null;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@Method()
|
||||
getRouteId(): string|null {
|
||||
@@ -174,7 +155,7 @@ export class Tabs implements NavOutlet {
|
||||
|
||||
@Method()
|
||||
getContentElement(): HTMLElement {
|
||||
return this.selectedTab;
|
||||
return this.routingView || this.selectedTab;
|
||||
}
|
||||
|
||||
private initTabs() {
|
||||
@@ -189,7 +170,7 @@ export class Tabs implements NavOutlet {
|
||||
return Promise.all(tabPromises);
|
||||
}
|
||||
|
||||
private initSelect() {
|
||||
private initSelect(): Promise<void> {
|
||||
if (document.querySelector('ion-router')) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -203,7 +184,7 @@ export class Tabs implements NavOutlet {
|
||||
tab.selected = false;
|
||||
}
|
||||
}
|
||||
const promise = selectedTab ? selectedTab.setActive() : Promise.resolve();
|
||||
const promise = selectedTab ? selectedTab.setActive() : Promise.resolve(null);
|
||||
return promise.then(() => {
|
||||
this.selectedTab = selectedTab;
|
||||
if (selectedTab) {
|
||||
@@ -220,6 +201,45 @@ export class Tabs implements NavOutlet {
|
||||
}
|
||||
}
|
||||
|
||||
private setActive(tabOrIndex: number | HTMLIonTabElement): Promise<HTMLIonTabElement|null> {
|
||||
if (this.transitioning) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const selectedTab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex);
|
||||
if (!selectedTab) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// Reset rest of tabs
|
||||
for (const tab of this.tabs) {
|
||||
if (selectedTab !== tab) {
|
||||
tab.selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.transitioning = true;
|
||||
return selectedTab.setActive();
|
||||
}
|
||||
|
||||
private tabSwitch(selectedTab: HTMLIonTabElement | null): boolean {
|
||||
this.transitioning = false;
|
||||
if (!selectedTab) {
|
||||
return false;
|
||||
}
|
||||
const leavingTab = this.selectedTab;
|
||||
selectedTab.selected = true;
|
||||
if (leavingTab !== selectedTab) {
|
||||
if (leavingTab) {
|
||||
leavingTab.active = false;
|
||||
}
|
||||
this.selectedTab = selectedTab;
|
||||
this.ionChange.emit(selectedTab);
|
||||
this.ionNavChanged.emit({isPop: false});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const dom = [
|
||||
<div class='tabs-inner'>
|
||||
|
||||
@@ -27,7 +27,4 @@ export class Text {
|
||||
*/
|
||||
@Prop() mode: 'ios' | 'md';
|
||||
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,4 @@ import { Component } from '@stencil/core';
|
||||
theme: 'thumbnail'
|
||||
}
|
||||
})
|
||||
export class Thumbnail {
|
||||
render() {
|
||||
return <slot></slot>;
|
||||
}
|
||||
}
|
||||
export class Thumbnail {}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ToastController implements OverlayController {
|
||||
*/
|
||||
@Method()
|
||||
create(opts?: ToastOptions): Promise<HTMLIonToastElement> {
|
||||
return createOverlay('ion-toast', opts);
|
||||
return createOverlay(document.createElement('ion-toast'), opts);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user