Compare commits

..

11 Commits

Author SHA1 Message Date
Brandy Carney
c36b1fd2dc @ionic/core 0.1.4-1 2018-03-07 17:59:13 -05:00
Brandy Carney
cc365f829d refactor(fab): combine boolean position attributes to string props
fixes #13596
2018-03-07 17:31:05 -05:00
Manu Mtz.-Almeida
dc8b363ea8 feat(virtual-scroll): adds JSX support 2018-03-07 18:04:14 +01:00
Adam Bradley
a8b90a3eb1 refactor(slot): remove render() functions w/ only <slot/> 2018-03-06 20:19:56 -06:00
Adam Bradley
d313e69013 refactor(tab-button): use <a href> instead of <button> 2018-03-06 19:59:08 -06:00
Adam Bradley
851aa838fa refactor(router): init ng router refactor 2018-03-06 19:57:45 -06:00
Adam Bradley
a1f942d45e chore(cleanup): remove unused code 2018-03-06 19:56:53 -06:00
Manu Mtz.-Almeida
7b264f983b fix(routing): flickering (part 1) 2018-03-07 00:14:05 +01:00
Manu Mtz.-Almeida
9650bec06a fix(overlays): bundling of overlays 2018-03-06 23:16:58 +01:00
Manu Mtz.-Almeida
174d9b5a41 fix(tabs): do not select when using router 2018-03-06 23:02:43 +01:00
Manu Mtz.-Almeida
c8a27b7a19 fix(ion-router): fixes routing algorithm 2018-03-06 22:35:47 +01:00
105 changed files with 1611 additions and 2078 deletions

View File

@@ -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>

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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';

View File

@@ -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,

View 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);
}
});
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}
}

View 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;

View 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);
});
}

View File

@@ -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();
}
}

View File

@@ -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);
});
}

View File

@@ -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) {

View File

@@ -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 [];
}
}

View File

@@ -1,10 +0,0 @@
export class NavParams {
constructor(public data: any = {}) {
}
get(param: string): any {
return this.data[param];
}
}

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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';

View File

@@ -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);
}
}

View File

@@ -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 || {});
});
});
}

View File

@@ -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>;
}

View File

@@ -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);
});
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -1,3 +1,6 @@
{
"extends": "tslint-ionic-rules"
"extends": "tslint-ionic-rules",
"rules": {
"no-non-null-assertion": false
}
}

View File

@@ -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)

View File

@@ -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')`.

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "0.1.4-0",
"version": "0.1.4-1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "0.1.4-0",
"version": "0.1.4-1",
"description": "Base components for Ionic",
"keywords": [
"ionic",

View File

@@ -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;
}
}
}

View File

@@ -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);
}
/*

View File

@@ -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);
}
/*

View File

@@ -11,8 +11,4 @@ import { Component } from '@stencil/core';
theme: 'avatar'
}
})
export class Avatar {
render() {
return <slot></slot>;
}
}
export class Avatar {}

View File

@@ -27,7 +27,4 @@ export class Badge {
*/
@Prop() mode: 'ios' | 'md';
render() {
return <slot></slot>;
}
}

View File

@@ -7,8 +7,4 @@ import { Component } from '@stencil/core';
theme: 'bar-buttons'
}
})
export class Buttons {
render() {
return <slot></slot>;
}
}
export class Buttons {}

View File

@@ -23,7 +23,4 @@ export class CardContent {
*/
@Prop() mode: 'ios' | 'md';
render() {
return <slot></slot>;
}
}

View File

@@ -42,7 +42,4 @@ export class CardHeader {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -31,7 +31,4 @@ export class CardSubtitle {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -32,7 +32,4 @@ export class CardTitle {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -24,7 +24,4 @@ export class Card {
*/
@Prop() mode: 'ios' | 'md';
render() {
return <slot></slot>;
}
}

View File

@@ -24,7 +24,4 @@ export class Chip {
*/
@Prop() mode: 'ios' | 'md';
render() {
return <slot></slot>;
}
}

View File

@@ -30,9 +30,4 @@ export class FabList {
};
}
render() {
return (
<slot></slot>
);
}
}

View File

@@ -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%;
}

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -36,7 +36,4 @@ export class Footer {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -36,7 +36,4 @@ export class Header {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -39,9 +39,6 @@ export class HideWhen implements DisplayWhen {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -233,8 +233,4 @@ export class InfiniteScroll {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -11,8 +11,4 @@ import { Component } from '@stencil/core';
theme: 'item-group'
}
})
export class ItemGroup {
render() {
return <slot></slot>;
}
}
export class ItemGroup {}

View File

@@ -47,7 +47,4 @@ export class ItemOptions {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -73,7 +73,4 @@ export class Label {
});
}
render() {
return <slot></slot>;
}
}

View File

@@ -27,7 +27,4 @@ export class ListHeader {
*/
@Prop() mode: 'ios' | 'md';
render() {
return <slot></slot>;
}
}

View File

@@ -39,7 +39,4 @@ export class List {
return false;
}
render() {
return <slot></slot>;
}
}

View File

@@ -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);
}
/*

View File

@@ -60,9 +60,6 @@ export class MenuToggle {
};
}
render() {
return <slot></slot>;
}
}
function getMenuController(): Promise<HTMLIonMenuControllerElement> {

View File

@@ -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);
}
/*

View File

@@ -16,8 +16,4 @@ export class NavPop {
return Promise.resolve(null);
}
render() {
return <slot></slot>;
}
}

View File

@@ -21,8 +21,4 @@ export class NavPush {
return Promise.resolve(null);
}
render() {
return <slot></slot>;
}
}

View File

@@ -20,8 +20,4 @@ export class NavSetRoot {
return Promise.resolve(null);
}
render() {
return <slot></slot>;
}
}

View File

@@ -223,6 +223,11 @@ export class NavControllerBase implements NavOutlet {
return null;
}
@Method()
markVisible() {
return Promise.resolve();
}
@Method()
getContentElement(): HTMLElement {
const active = this.getActive();

View File

@@ -71,6 +71,9 @@ Return a view controller
#### insertPages()
#### markVisible()
#### pop()

View File

@@ -27,7 +27,4 @@ export class Note {
*/
@Prop() mode: 'ios' | 'md';
render() {
return <slot></slot>;
}
}

View File

@@ -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);
}
/*

View File

@@ -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);
}
/*

View File

@@ -175,9 +175,6 @@ export class RadioGroup implements ComponentDidLoad, RadioGroupInput {
return hostAttrs;
}
render() {
return <slot></slot>;
}
}
let radioGroupIds = 0;

View File

@@ -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;
}

View File

@@ -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);
}

View 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);
});
});

View 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));
}

View 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();
// });
// });

View 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;
}

View 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');
});
});

View File

@@ -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);
});
});

View 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;
}

View 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,
};
}

View 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[];

View 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,
};
}

View 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);
}
}

View 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;
}
}

View File

@@ -83,7 +83,4 @@ export class Segment {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -60,10 +60,6 @@ export class SelectOption {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -39,9 +39,6 @@ export class ShowWhen implements DisplayWhen {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -16,7 +16,4 @@ export class Slide {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -149,9 +149,6 @@ export class SplitPane {
};
}
render() {
return <slot></slot>;
}
}

View File

@@ -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);

View File

@@ -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>
];
}
}

View File

@@ -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

View File

@@ -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> {

View File

@@ -240,6 +240,9 @@ Emitted when the tab changes.
#### getTabs()
#### markVisible()
#### select()

View File

@@ -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'>

View File

@@ -27,7 +27,4 @@ export class Text {
*/
@Prop() mode: 'ios' | 'md';
render() {
return <slot></slot>;
}
}

View File

@@ -11,8 +11,4 @@ import { Component } from '@stencil/core';
theme: 'thumbnail'
}
})
export class Thumbnail {
render() {
return <slot></slot>;
}
}
export class Thumbnail {}

View File

@@ -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