mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 11:41:20 +08:00
router updates
This commit is contained in:
@ -26,7 +26,11 @@ export class IonicApp {
|
|||||||
|
|
||||||
load(appRef) {
|
load(appRef) {
|
||||||
this.ref(appRef);
|
this.ref(appRef);
|
||||||
this.zone(this.injector().get(NgZone));
|
this._zone = this.injector().get(NgZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
title(val) {
|
||||||
|
document.title = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref(val) {
|
ref(val) {
|
||||||
@ -40,11 +44,8 @@ export class IonicApp {
|
|||||||
return this._ref.injector;
|
return this._ref.injector;
|
||||||
}
|
}
|
||||||
|
|
||||||
zone(val) {
|
zoneRun(fn) {
|
||||||
if (arguments.length) {
|
this._zone.run(fn);
|
||||||
this._zone = val;
|
|
||||||
}
|
|
||||||
return this._zone;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stateChange(type, activeView) {
|
stateChange(type, activeView) {
|
||||||
@ -74,10 +75,10 @@ export class IonicApp {
|
|||||||
* Create and append the given component into the root
|
* Create and append the given component into the root
|
||||||
* element of the app.
|
* element of the app.
|
||||||
*
|
*
|
||||||
* @param Component the cls to create and insert
|
* @param Component the component to create and insert
|
||||||
* @return Promise that resolves with the ContainerRef created
|
* @return Promise that resolves with the ContainerRef created
|
||||||
*/
|
*/
|
||||||
appendComponent(cls: Type, context=null) {
|
appendComponent(component: Type, context=null) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let injector = this.injector();
|
let injector = this.injector();
|
||||||
let compiler = injector.get(Compiler);
|
let compiler = injector.get(Compiler);
|
||||||
@ -85,7 +86,7 @@ export class IonicApp {
|
|||||||
let rootComponentRef = this._ref._hostComponent;
|
let rootComponentRef = this._ref._hostComponent;
|
||||||
let viewContainerLocation = rootComponentRef.location;
|
let viewContainerLocation = rootComponentRef.location;
|
||||||
|
|
||||||
compiler.compileInHost(cls).then(protoViewRef => {
|
compiler.compileInHost(component).then(protoViewRef => {
|
||||||
let atIndex = 0;
|
let atIndex = 0;
|
||||||
|
|
||||||
let hostViewRef = viewMngr.createViewInContainer(
|
let hostViewRef = viewMngr.createViewInContainer(
|
||||||
@ -158,8 +159,8 @@ function initApp(window, document, config) {
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ionicBootstrap(cls, config, router) {
|
export function ionicBootstrap(component, config, router) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => {
|
||||||
try {
|
try {
|
||||||
// get the user config, or create one if wasn't passed in
|
// get the user config, or create one if wasn't passed in
|
||||||
if (typeof config !== IonicConfig) {
|
if (typeof config !== IonicConfig) {
|
||||||
@ -202,7 +203,7 @@ export function ionicBootstrap(cls, config, router) {
|
|||||||
bind(Modal).toValue(modal)
|
bind(Modal).toValue(modal)
|
||||||
];
|
];
|
||||||
|
|
||||||
bootstrap(cls, injectableBindings).then(appRef => {
|
bootstrap(component, injectableBindings).then(appRef => {
|
||||||
app.load(appRef);
|
app.load(appRef);
|
||||||
|
|
||||||
router.load(window, app, config).then(() => {
|
router.load(window, app, config).then(() => {
|
||||||
@ -212,12 +213,10 @@ export function ionicBootstrap(cls, config, router) {
|
|||||||
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('ionicBootstrap', err);
|
console.error('ionicBootstrap', err);
|
||||||
reject(err);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('ionicBootstrap', err);
|
console.error(err);
|
||||||
reject(err);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import {ProtoViewRef} from 'angular2/src/core/compiler/view_ref';
|
|||||||
import {Ion} from '../ion';
|
import {Ion} from '../ion';
|
||||||
import {IonicConfig} from '../../config/config';
|
import {IonicConfig} from '../../config/config';
|
||||||
import {IonicComponent} from '../../config/annotations';
|
import {IonicComponent} from '../../config/annotations';
|
||||||
|
import {IonicApp} from '../app/app';
|
||||||
import {ViewItem} from '../view/view-item';
|
import {ViewItem} from '../view/view-item';
|
||||||
import * as dom from '../../util/dom';
|
import * as dom from '../../util/dom';
|
||||||
|
|
||||||
@ -42,9 +43,10 @@ import * as dom from '../../util/dom';
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class Navbar extends Ion {
|
export class Navbar extends Ion {
|
||||||
constructor(item: ViewItem, elementRef: ElementRef, ionicConfig: IonicConfig) {
|
constructor(item: ViewItem, elementRef: ElementRef, ionicConfig: IonicConfig, app: IonicApp) {
|
||||||
super(elementRef, ionicConfig);
|
super(elementRef, ionicConfig);
|
||||||
|
|
||||||
|
this.app = app;
|
||||||
this.eleRef = elementRef;
|
this.eleRef = elementRef;
|
||||||
this.itemEles = [];
|
this.itemEles = [];
|
||||||
item.navbarView(this);
|
item.navbarView(this);
|
||||||
@ -130,8 +132,10 @@ export class Navbar extends Ion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
didEnter() {
|
didEnter() {
|
||||||
setTimeout(() => {
|
|
||||||
const titleEle = this._ttEle || (this._ttEle = this.eleRef.nativeElement.querySelector('ion-title'));
|
const titleEle = this._ttEle || (this._ttEle = this.eleRef.nativeElement.querySelector('ion-title'));
|
||||||
|
this.app.title(titleEle.textContent);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
//this.titleText((titleEle && titleEle.textContent) || '');
|
//this.titleText((titleEle && titleEle.textContent) || '');
|
||||||
}, 32);
|
}, 32);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,25 @@
|
|||||||
import {App} from 'ionic/ionic';
|
import {App} from 'ionic/ionic';
|
||||||
|
|
||||||
|
import {FirstPage} from './pages/first-page';
|
||||||
|
import {SecondPage} from './pages/second-page';
|
||||||
|
import {ThirdPage} from './pages/third-page';
|
||||||
|
|
||||||
|
|
||||||
@App({
|
@App({
|
||||||
routes: {
|
routes: [
|
||||||
'FirstPage': {
|
{
|
||||||
'path': '/firstpage',
|
path: '/firstpage',
|
||||||
'module': 'dist/examples/nav/basic/pages/first-page',
|
component: FirstPage,
|
||||||
'root': true
|
root: true
|
||||||
},
|
},
|
||||||
'SecondPage': {
|
{
|
||||||
'path': '/secondpage',
|
path: '/secondpage',
|
||||||
'module': 'dist/examples/nav/basic/pages/second-page',
|
component: SecondPage,
|
||||||
},
|
|
||||||
'ThirdPage': {
|
|
||||||
'path': '/thirdpage',
|
|
||||||
'module': 'dist/examples/nav/basic/pages/third-page',
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/thirdpage',
|
||||||
|
component: ThirdPage,
|
||||||
}
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
class MyApp {}
|
class MyApp {}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {IonicView, IonicConfig, IonicApp} from 'ionic/ionic';
|
import {IonicView, IonicConfig, IonicApp} from 'ionic/ionic';
|
||||||
import {NavParams, Routable, NavController} from 'ionic/ionic';
|
import {NavParams, NavController} from 'ionic/ionic';
|
||||||
|
|
||||||
import {SecondPage} from './second-page';
|
import {SecondPage} from './second-page';
|
||||||
import {ThirdPage} from './third-page';
|
import {ThirdPage} from './third-page';
|
||||||
@ -52,39 +52,35 @@ export class FirstPage {
|
|||||||
this.nav.setItems(items);
|
this.nav.setItems(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewLoaded() {
|
// viewLoaded() {
|
||||||
console.log('viewLoaded first page');
|
// console.log('viewLoaded first page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewWillEnter() {
|
// viewWillEnter() {
|
||||||
console.log('viewWillEnter first page');
|
// console.log('viewWillEnter first page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewDidEnter() {
|
// viewDidEnter() {
|
||||||
console.log('viewDidEnter first page');
|
// console.log('viewDidEnter first page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewWillLeave() {
|
// viewWillLeave() {
|
||||||
console.log('viewWillLeave first page');
|
// console.log('viewWillLeave first page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewDidLeave() {
|
// viewDidLeave() {
|
||||||
console.log('viewDidLeave first page');
|
// console.log('viewDidLeave first page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewWillUnload() {
|
// viewWillUnload() {
|
||||||
console.log('viewWillUnload first page');
|
// console.log('viewWillUnload first page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewDidUnload() {
|
// viewDidUnload() {
|
||||||
console.log('viewDidUnload first page');
|
// console.log('viewDidUnload first page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
push() {
|
push() {
|
||||||
this.nav.push(SecondPage, { id: 8675309, myData: [1,2,3,4] }, { animation: 'ios' });
|
this.nav.push(SecondPage, { id: 8675309, myData: [1,2,3,4] }, { animation: 'ios' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new Routable(FirstPage, {
|
|
||||||
path: '/firstpage'
|
|
||||||
});
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {IonicView, Routable, NavController, NavParams} from 'ionic/ionic';
|
import {IonicView, NavController, NavParams} from 'ionic/ionic';
|
||||||
import {ThirdPage} from './third-page';
|
import {ThirdPage} from './third-page';
|
||||||
import {FirstPage} from './first-page';
|
import {FirstPage} from './first-page';
|
||||||
|
|
||||||
@ -45,36 +45,32 @@ export class SecondPage {
|
|||||||
this.nav.push(ThirdPage);
|
this.nav.push(ThirdPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewLoaded() {
|
// viewLoaded() {
|
||||||
console.log('viewLoaded second page');
|
// console.log('viewLoaded second page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewWillEnter() {
|
// viewWillEnter() {
|
||||||
console.log('viewWillEnter second page');
|
// console.log('viewWillEnter second page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewDidEnter() {
|
// viewDidEnter() {
|
||||||
console.log('viewDidEnter second page');
|
// console.log('viewDidEnter second page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewWillLeave() {
|
// viewWillLeave() {
|
||||||
console.log('viewWillLeave second page');
|
// console.log('viewWillLeave second page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewDidLeave() {
|
// viewDidLeave() {
|
||||||
console.log('viewDidLeave second page');
|
// console.log('viewDidLeave second page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewWillUnload() {
|
// viewWillUnload() {
|
||||||
console.log('viewWillUnload second page');
|
// console.log('viewWillUnload second page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewDidUnload() {
|
// viewDidUnload() {
|
||||||
console.log('viewDidUnload second page');
|
// console.log('viewDidUnload second page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new Routable(SecondPage, {
|
|
||||||
path: '/secondpage'
|
|
||||||
});
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {IonicView, Routable, NavController} from 'ionic/ionic';
|
import {IonicView, NavController} from 'ionic/ionic';
|
||||||
|
|
||||||
|
|
||||||
@IonicView({
|
@IonicView({
|
||||||
@ -23,36 +23,32 @@ export class ThirdPage {
|
|||||||
this.nav.pop()
|
this.nav.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewLoaded() {
|
// viewLoaded() {
|
||||||
console.log('viewLoaded third page');
|
// console.log('viewLoaded third page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewWillEnter() {
|
// viewWillEnter() {
|
||||||
console.log('viewWillEnter third page');
|
// console.log('viewWillEnter third page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewDidEnter() {
|
// viewDidEnter() {
|
||||||
console.log('viewDidEnter third page');
|
// console.log('viewDidEnter third page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewWillLeave() {
|
// viewWillLeave() {
|
||||||
console.log('viewWillLeave third page');
|
// console.log('viewWillLeave third page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewDidLeave() {
|
// viewDidLeave() {
|
||||||
console.log('viewDidLeave third page');
|
// console.log('viewDidLeave third page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewWillUnload() {
|
// viewWillUnload() {
|
||||||
console.log('viewWillUnload third page');
|
// console.log('viewWillUnload third page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewDidUnload() {
|
// viewDidUnload() {
|
||||||
console.log('viewDidUnload third page');
|
// console.log('viewDidUnload third page');
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new Routable(ThirdPage, {
|
|
||||||
path: '/thirdpage'
|
|
||||||
});
|
|
||||||
|
@ -49,8 +49,8 @@ export class ViewController extends Ion {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
push(ComponentType, params = {}, opts = {}) {
|
push(component, params = {}, opts = {}) {
|
||||||
if (!ComponentType || this.isTransitioning()) {
|
if (!component || this.isTransitioning()) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ export class ViewController extends Ion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create a new ViewItem
|
// create a new ViewItem
|
||||||
let enteringItem = new ViewItem(this, ComponentType, params);
|
let enteringItem = new ViewItem(this, component, params);
|
||||||
|
|
||||||
// add the item to the stack
|
// add the item to the stack
|
||||||
this.add(enteringItem);
|
this.add(enteringItem);
|
||||||
@ -136,6 +136,10 @@ export class ViewController extends Ion {
|
|||||||
* Set the item stack to reflect the given component classes.
|
* Set the item stack to reflect the given component classes.
|
||||||
*/
|
*/
|
||||||
setItems(components, opts = {}) {
|
setItems(components, opts = {}) {
|
||||||
|
if (!components || !components.length) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
// if animate has not been set then default to false
|
// if animate has not been set then default to false
|
||||||
opts.animate = opts.animate || false;
|
opts.animate = opts.animate || false;
|
||||||
|
|
||||||
@ -162,6 +166,7 @@ export class ViewController extends Ion {
|
|||||||
let newBeforeItems = components.slice(0, components.length - 1);
|
let newBeforeItems = components.slice(0, components.length - 1);
|
||||||
for (let j = 0; j < newBeforeItems.length; j++) {
|
for (let j = 0; j < newBeforeItems.length; j++) {
|
||||||
component = newBeforeItems[j];
|
component = newBeforeItems[j];
|
||||||
|
if (component) {
|
||||||
viewItem = new ViewItem(this, component.component || component, component.params);
|
viewItem = new ViewItem(this, component.component || component, component.params);
|
||||||
viewItem.state = CACHED_STATE;
|
viewItem.state = CACHED_STATE;
|
||||||
viewItem.shouldDestroy = false;
|
viewItem.shouldDestroy = false;
|
||||||
@ -171,19 +176,20 @@ export class ViewController extends Ion {
|
|||||||
this.add(viewItem);
|
this.add(viewItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// get the component that will become the active item
|
// get the component that will become the active item
|
||||||
// it'll be the last one in the given components array
|
// it'll be the last one in the given components array
|
||||||
component = components[ components.length - 1 ];
|
component = components[ components.length - 1 ];
|
||||||
|
|
||||||
// transition the leaving and entering
|
// transition the leaving and entering
|
||||||
return this.push(component.component || component, component.params, opts);
|
return this.push((component && component.component) || component, (component && component.params), opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
setRoot(ComponentType, params = {}, opts = {}) {
|
setRoot(component, params = {}, opts = {}) {
|
||||||
return this.setItems([{
|
return this.setItems([{
|
||||||
component: ComponentType,
|
component,
|
||||||
params: params
|
params
|
||||||
}], opts);
|
}], opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,7 +491,7 @@ export class ViewController extends Ion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
add(item) {
|
add(item) {
|
||||||
item.id = this.id + '' + (++this._ids);
|
item.id = this.id + '-' + (++this._ids);
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,9 +6,9 @@ import {NavParams} from '../nav/nav-controller';
|
|||||||
|
|
||||||
export class ViewItem {
|
export class ViewItem {
|
||||||
|
|
||||||
constructor(viewCtrl, cls, params = {}) {
|
constructor(viewCtrl, component, params = {}) {
|
||||||
this.viewCtrl = viewCtrl;
|
this.viewCtrl = viewCtrl;
|
||||||
this.cls = cls;
|
this.component = component;
|
||||||
this.params = new NavParams(params);
|
this.params = new NavParams(params);
|
||||||
this.instance = null;
|
this.instance = null;
|
||||||
this.state = 0;
|
this.state = 0;
|
||||||
@ -37,7 +37,7 @@ export class ViewItem {
|
|||||||
'class': 'nav-item'
|
'class': 'nav-item'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let ionViewComponent = DirectiveBinding.createFromType(this.cls, annotation);
|
let ionViewComponent = DirectiveBinding.createFromType(this.component, annotation);
|
||||||
|
|
||||||
// compile the Component
|
// compile the Component
|
||||||
viewCtrl.compiler.compileInHost(ionViewComponent).then(componentProtoViewRef => {
|
viewCtrl.compiler.compileInHost(ionViewComponent).then(componentProtoViewRef => {
|
||||||
|
@ -12,7 +12,7 @@ export * from 'ionic/platform/platform'
|
|||||||
export * from 'ionic/platform/registry'
|
export * from 'ionic/platform/registry'
|
||||||
|
|
||||||
export * from 'ionic/routing/router'
|
export * from 'ionic/routing/router'
|
||||||
export * from 'ionic/routing/hash-url-state'
|
export * from 'ionic/routing/url-state'
|
||||||
|
|
||||||
export * from 'ionic/util/click-block'
|
export * from 'ionic/util/click-block'
|
||||||
export * from 'ionic/util/focus'
|
export * from 'ionic/util/focus'
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
import {IonicRouter} from './router';
|
|
||||||
import * as util from '../util/util';
|
|
||||||
|
|
||||||
|
|
||||||
class HashUrlStateManager {
|
|
||||||
|
|
||||||
constructor(window, router) {
|
|
||||||
this.location = window.location;
|
|
||||||
this.history = window.history;
|
|
||||||
this.router = router;
|
|
||||||
|
|
||||||
window.addEventListener('popstate', ev => {
|
|
||||||
this.onPopState(ev);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
stateChange(path, type, activeView) {
|
|
||||||
if (type == 'pop') {
|
|
||||||
// if the popstate came from the browser's back button (and not Ionic)
|
|
||||||
// then we shouldn't force another browser history.back()
|
|
||||||
// only do a history.back() if the URL hasn't been updated yet
|
|
||||||
if (this.isDifferentPath(path)) {
|
|
||||||
this.history.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// push state change
|
|
||||||
let enteringState = {
|
|
||||||
path: path,
|
|
||||||
backPath: this.router.lastPath(),
|
|
||||||
forwardPath: null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this._hasInit) {
|
|
||||||
// update the leaving state to know what it's forward state will be
|
|
||||||
let leavingState = util.extend(this.history.state, {
|
|
||||||
forwardPath: enteringState.path
|
|
||||||
});
|
|
||||||
if (leavingState.path !== enteringState.path) {
|
|
||||||
this.history.replaceState(leavingState, '', '#' + leavingState.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isDifferentPath(path)) {
|
|
||||||
// push the new state to the history stack since the path
|
|
||||||
// isn't already in the location hash
|
|
||||||
this.history.pushState(enteringState, '', '#' + enteringState.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// replace the very first load with the correct entering state info
|
|
||||||
this.history.replaceState(enteringState, '', '#' + enteringState.path);
|
|
||||||
this._hasInit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPopState(ev) {
|
|
||||||
let newState = ev.state || {};
|
|
||||||
let newStatePath = newState.path;
|
|
||||||
let newStateBackPath = newState.backPath;
|
|
||||||
let newStateForwardPath = newState.forwardPath;
|
|
||||||
let lastLoadedStatePath = this.router.lastPath();
|
|
||||||
|
|
||||||
if (newStatePath === lastLoadedStatePath) {
|
|
||||||
// do nothing if the last path is the same
|
|
||||||
// as the "new" current state
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let activeViewCtrl = this.router.activeViewController();
|
|
||||||
if (activeViewCtrl) {
|
|
||||||
|
|
||||||
if (newStateForwardPath === lastLoadedStatePath) {
|
|
||||||
// if the last loaded state path is the same as the new
|
|
||||||
// state's forward path then the user is moving back
|
|
||||||
activeViewCtrl.pop();
|
|
||||||
|
|
||||||
} else if (newStateBackPath === lastLoadedStatePath) {
|
|
||||||
// if the last loaded state path is the new state's
|
|
||||||
// back path, then the user is moving forward
|
|
||||||
this.router.loadByPath(newStatePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentPath() {
|
|
||||||
// Grab the path without the leading hash
|
|
||||||
return new Promise(resolve => {
|
|
||||||
resolve({
|
|
||||||
path: this.location.hash.slice(1),
|
|
||||||
priority: 0
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isDifferentPath(path) {
|
|
||||||
// check if the given path is different than the current location
|
|
||||||
return (this.location.hash !== ('#' + path));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
IonicRouter.registerStateManager('hashurl', HashUrlStateManager);
|
|
@ -30,8 +30,8 @@ class StaticSegment {
|
|||||||
this.regex = escapeRegex(string);
|
this.regex = escapeRegex(string);
|
||||||
}
|
}
|
||||||
|
|
||||||
generate(params) {
|
generate() {
|
||||||
return this.string;
|
return this.regex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,6 @@ class DynamicSegment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class StarSegment {
|
class StarSegment {
|
||||||
constructor(name) {
|
constructor(name) {
|
||||||
this.regex = "(.+)";
|
this.regex = "(.+)";
|
||||||
@ -124,8 +123,6 @@ export class PathRecognizer {
|
|||||||
constructor(path) {
|
constructor(path) {
|
||||||
this.segments = [];
|
this.segments = [];
|
||||||
|
|
||||||
// TODO: use destructuring assignment
|
|
||||||
// see https://github.com/angular/ts2dart/issues/158
|
|
||||||
var parsed = parsePathString(path);
|
var parsed = parsePathString(path);
|
||||||
var specificity = parsed['specificity'];
|
var specificity = parsed['specificity'];
|
||||||
var segments = parsed['segments'];
|
var segments = parsed['segments'];
|
||||||
|
@ -6,165 +6,189 @@ import {PathRecognizer} from './path-recognizer';
|
|||||||
|
|
||||||
export class IonicRouter {
|
export class IonicRouter {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this._routes = {};
|
this._routes = [];
|
||||||
this._viewCtrls = [];
|
this._viewCtrls = [];
|
||||||
this.config(config);
|
this.config(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
app(app) {
|
app(app) {
|
||||||
this._app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
config(config) {
|
config(config) {
|
||||||
if (config) {
|
if (config) {
|
||||||
for (let routeName in config) {
|
for (let i = 0; i < config.length; i++) {
|
||||||
this.addRoute(routeName, config[routeName]);
|
this.addRoute(config[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addRoute(routeName, routeConfig) {
|
addRoute(routeConfig) {
|
||||||
if (routeName && routeConfig && routeConfig.path) {
|
if (routeConfig && routeConfig.path && routeConfig.component) {
|
||||||
this._routes[routeName] = new Route(routeName, routeConfig);
|
let route = new Route(routeConfig);
|
||||||
if (routeConfig.root) {
|
if (routeConfig.root) {
|
||||||
this.otherwise(routeName);
|
this.otherwise(route);
|
||||||
}
|
}
|
||||||
|
this._routes.push(route);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
load(window, ionicApp, ionicConfig) {
|
|
||||||
// create each of the state manager classes
|
|
||||||
for (let name in stateManagerClasses) {
|
|
||||||
stateManagers[name] = new stateManagerClasses[name](window, this, ionicApp, ionicConfig);
|
|
||||||
}
|
|
||||||
stateManagerClasses = {};
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.getCurrentPath().then(path => {
|
|
||||||
this.loadByPath(path, this.otherwise()).then(resolve);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadByPath(path, fallbackRoute) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
let self = this;
|
|
||||||
let activeViewCtrl = self.activeViewController();
|
|
||||||
let matchedRoute = self.match(path) || fallbackRoute;
|
|
||||||
|
|
||||||
function zoneLoad() {
|
|
||||||
self._app.zone().run(() => {
|
|
||||||
activeViewCtrl.push(matchedRoute.cls);
|
|
||||||
self.lastPath(matchedRoute.path);
|
|
||||||
resolve();
|
|
||||||
}, err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeViewCtrl && matchedRoute) {
|
|
||||||
|
|
||||||
if (matchedRoute.cls) {
|
|
||||||
zoneLoad();
|
|
||||||
|
|
||||||
} else if (matchedRoute.module) {
|
|
||||||
System.import(matchedRoute.module).then(m => {
|
|
||||||
if (m) {
|
|
||||||
matchedRoute.cls = m[matchedRoute.name];
|
|
||||||
zoneLoad();
|
|
||||||
}
|
|
||||||
}, err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentPath() {
|
|
||||||
// check each of the state managers and the one with the
|
|
||||||
// highest priority wins of knowing what path we are currently at
|
|
||||||
return new Promise(resolve => {
|
|
||||||
|
|
||||||
let promises = [];
|
|
||||||
for (let name in stateManagers) {
|
|
||||||
promises.push(stateManagers[name].getCurrentPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
// when all the promises have resolved then see which one wins
|
|
||||||
Promise.all(promises).then(results => {
|
|
||||||
let rtnPath = null;
|
|
||||||
let highestPriority = -1;
|
|
||||||
let state = null;
|
|
||||||
|
|
||||||
for (let i = 0; i < results.length; i++) {
|
|
||||||
state = results[i];
|
|
||||||
if (state.path && state.priority > highestPriority) {
|
|
||||||
rtnPath = state.path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(rtnPath);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
stateChange(type, activeView) {
|
stateChange(type, activeView) {
|
||||||
if (activeView && activeView.cls) {
|
// this fires when the app's state has changed stateChange will
|
||||||
|
// tell each of the state managers that the state has changed, and
|
||||||
let routeConfig = activeView.cls.route;
|
// each state manager will decide what to do with this info
|
||||||
if (routeConfig) {
|
// (the url state manager updates the url bar if a route was setup)
|
||||||
let matchedRoute = this.match(routeConfig.path);
|
if (activeView && activeView.component) {
|
||||||
|
|
||||||
if (matchedRoute) {
|
|
||||||
|
|
||||||
|
let componentRoute = activeView.component.route;
|
||||||
|
if (componentRoute) {
|
||||||
|
let path = componentRoute.generate(activeView.params);
|
||||||
|
if (path) {
|
||||||
for (let name in stateManagers) {
|
for (let name in stateManagers) {
|
||||||
stateManagers[name].stateChange(matchedRoute.path, type, activeView);
|
stateManagers[name].stateChange(path, type, activeView);
|
||||||
}
|
|
||||||
|
|
||||||
this.lastPath(matchedRoute.path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lastPath(val) {
|
|
||||||
if (arguments.length) {
|
|
||||||
this._lastPath = val;
|
|
||||||
}
|
}
|
||||||
return this._lastPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match(path) {
|
matchPaths(paths) {
|
||||||
|
// load each of paths to a component
|
||||||
|
let components = [];
|
||||||
|
let route;
|
||||||
|
|
||||||
|
if (paths) {
|
||||||
|
for (let i = 0; i < paths.length; i++) {
|
||||||
|
route = this.matchPath(paths[i]);
|
||||||
|
if (route && route.component) {
|
||||||
|
components.push(route.component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchPath(path) {
|
||||||
|
// takes a string path and loops through each of the setup
|
||||||
|
// routes to see if the path matches any of the routes
|
||||||
|
// the matched path with the highest specifity wins
|
||||||
let matchedRoute = null;
|
let matchedRoute = null;
|
||||||
|
let route = null;
|
||||||
let routeMatch = null;
|
let routeMatch = null;
|
||||||
let highestSpecifity = 0;
|
|
||||||
|
|
||||||
for (let routeName in this._routes) {
|
for (let i = 0; i < this._routes.length; i++) {
|
||||||
routeMatch = this._routes[routeName].match(path);
|
route = this._routes[i];
|
||||||
|
routeMatch = route.match(path);
|
||||||
|
|
||||||
if (routeMatch.match && (!matchedRoute || routeMatch.specificity > highestSpecifity)) {
|
if (routeMatch && (!matchedRoute || route.specificity > matchedRoute.specificity)) {
|
||||||
matchedRoute = this._routes[routeName];
|
matchedRoute = route;
|
||||||
highestSpecifity = routeMatch.specificity;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return matchedRoute;
|
return matchedRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
load(window, ionicApp, ionicConfig) {
|
||||||
|
// load is called when the app has finished loading each state
|
||||||
|
// manager gets a chance to say what path the app should be at
|
||||||
|
let viewCtrl = this.viewController();
|
||||||
|
if (!viewCtrl || !this._routes.length) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolve;
|
||||||
|
let promise = new Promise(res => { resolve = res; });
|
||||||
|
|
||||||
|
// get the initial load paths from the state manager with the highest priorty
|
||||||
|
this.getManagerPaths(window, ionicApp, ionicConfig).then(paths => {
|
||||||
|
|
||||||
|
// load all of the paths the highest priority state manager has given
|
||||||
|
let components = this.matchPaths(paths);
|
||||||
|
|
||||||
|
if (!components.length && this.otherwise()) {
|
||||||
|
// the state manager did not find and loaded components
|
||||||
|
// use the "otherwise" path
|
||||||
|
components = [this.otherwise().component];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.app.zoneRun(() => {
|
||||||
|
viewCtrl.setItems(components).then(resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
getManagerPaths(window, ionicApp, ionicConfig) {
|
||||||
|
// loop through all of the state managers and load their paths
|
||||||
|
// the state manager with valid paths and highest priority wins
|
||||||
|
let resolve;
|
||||||
|
let promise = new Promise(res => { resolve = res; });
|
||||||
|
|
||||||
|
// load each of the state managers
|
||||||
|
let stateManagerPromises = [];
|
||||||
|
for (let name in stateManagerClasses) {
|
||||||
|
stateManagers[name] = new stateManagerClasses[name](window, this, ionicApp, ionicConfig);
|
||||||
|
stateManagerPromises.push( stateManagers[name].load() );
|
||||||
|
}
|
||||||
|
|
||||||
|
// when all the state manager loads have resolved then see which one wins
|
||||||
|
Promise.all(stateManagerPromises).then(stateManagerLoadResults => {
|
||||||
|
|
||||||
|
// now that all the state managers are loaded
|
||||||
|
// get the highest priority state manager's paths
|
||||||
|
let stateLoadResult = null;
|
||||||
|
let paths = null;
|
||||||
|
let highestPriority = -1;
|
||||||
|
|
||||||
|
for (let i = 0; i < stateManagerLoadResults.length; i++) {
|
||||||
|
stateLoadResult = stateManagerLoadResults[i];
|
||||||
|
if (stateLoadResult && stateLoadResult.paths.length && stateLoadResult.priority > highestPriority) {
|
||||||
|
paths = stateLoadResult.paths;
|
||||||
|
highestPriority = stateLoadResult.priority;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(paths);
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
push(path) {
|
||||||
|
let viewCtrl = this.viewController();
|
||||||
|
if (viewCtrl) {
|
||||||
|
let matchedRoute = this.matchPath(path);
|
||||||
|
if (matchedRoute && matchedRoute.component) {
|
||||||
|
this.app.zoneRun(() => {
|
||||||
|
viewCtrl.push(matchedRoute.component, matchedRoute.params, {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pop() {
|
||||||
|
let viewCtrl = this.viewController();
|
||||||
|
if (viewCtrl) {
|
||||||
|
this.app.zoneRun(() => {
|
||||||
|
viewCtrl.pop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
otherwise(val) {
|
otherwise(val) {
|
||||||
if (arguments.length) {
|
if (arguments.length) {
|
||||||
this._otherwise = val;
|
this._otherwise = val;
|
||||||
|
|
||||||
} else if (this._otherwise) {
|
|
||||||
return this._routes[this._otherwise];
|
|
||||||
}
|
}
|
||||||
|
return this._otherwise
|
||||||
}
|
}
|
||||||
|
|
||||||
addViewController(viewCtrl) {
|
addViewController(viewCtrl) {
|
||||||
this._viewCtrls.push(viewCtrl);
|
this._viewCtrls.push(viewCtrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
activeViewController() {
|
viewController() {
|
||||||
if (this._viewCtrls.length) {
|
if (this._viewCtrls.length) {
|
||||||
return this._viewCtrls[ this._viewCtrls.length - 1 ];
|
return this._viewCtrls[ this._viewCtrls.length - 1 ];
|
||||||
}
|
}
|
||||||
@ -185,35 +209,21 @@ let stateManagerClasses = {};
|
|||||||
let stateManagers = {};
|
let stateManagers = {};
|
||||||
|
|
||||||
|
|
||||||
export class Routable {
|
|
||||||
constructor(cls, routeConfig) {
|
|
||||||
cls.route = routeConfig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Route {
|
class Route {
|
||||||
constructor(name, routeConfig) {
|
constructor(routeConfig) {
|
||||||
this.name = name;
|
|
||||||
this.cls = null;
|
|
||||||
util.extend(this, routeConfig);
|
util.extend(this, routeConfig);
|
||||||
this.recognizer = new PathRecognizer(this.path);
|
this.recognizer = new PathRecognizer(this.path);
|
||||||
|
this.specificity = this.recognizer.specificity;
|
||||||
|
|
||||||
|
this.component.route = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
match(matchPath) {
|
match(path) {
|
||||||
let routeMatch = new RouteMatch(this, matchPath);
|
return RegExpWrapper.firstMatch(this.recognizer.regex, path);
|
||||||
if (routeMatch) {
|
|
||||||
return routeMatch;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
generate(params) {
|
||||||
|
return this.recognizer.generate(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RouteMatch {
|
|
||||||
constructor(route, matchPath) {
|
|
||||||
this.route = route;
|
|
||||||
this.specificity = route.recognizer.specificity;
|
|
||||||
this.match = RegExpWrapper.firstMatch(route.recognizer.regex, matchPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
173
ionic/routing/url-state.ts
Normal file
173
ionic/routing/url-state.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import {IonicRouter} from './router';
|
||||||
|
import * as util from '../util/util';
|
||||||
|
|
||||||
|
|
||||||
|
class UrlStateManager {
|
||||||
|
|
||||||
|
constructor(window, router) {
|
||||||
|
this.location = window.location;
|
||||||
|
this.history = window.history;
|
||||||
|
this.ls = window.localStorage;
|
||||||
|
this.router = router;
|
||||||
|
|
||||||
|
// overkill for location change listeners, but ensures we
|
||||||
|
// know when the location has changed. Only 1 of the listeners
|
||||||
|
// will actually do the work, the other will be skipped.
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
this.onLocationChange();
|
||||||
|
});
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
this.onLocationChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
let paths = [this.getCurrentPath()];
|
||||||
|
let savedPaths = this.paths();
|
||||||
|
|
||||||
|
if (savedPaths[savedPaths.length - 1] == paths[0]) {
|
||||||
|
// the last path in the saved paths is the same as the
|
||||||
|
// current path, so use the saved paths to rebuild the history
|
||||||
|
paths = savedPaths;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// the current path is not the same as the last path in the
|
||||||
|
// saved history, so the saved history is no good, erase it
|
||||||
|
this.paths([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
paths: paths,
|
||||||
|
priority: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stateChange(path, type, activeView) {
|
||||||
|
let savedPaths = this.paths();
|
||||||
|
|
||||||
|
// check if the given path is different than the current location
|
||||||
|
let isDifferentPath = (this.getCurrentPath() !== path);
|
||||||
|
|
||||||
|
if (type == 'pop') {
|
||||||
|
// if the popstate came from the browser's back button (and not Ionic)
|
||||||
|
// then we shouldn't force another browser history.back()
|
||||||
|
// only do a history.back() if the URL hasn't been updated yet
|
||||||
|
if (isDifferentPath) {
|
||||||
|
this.history.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedPaths.length && savedPaths[savedPaths.length - 1] != path) {
|
||||||
|
// only if the last item in the saved paths
|
||||||
|
// equals this path then it can be removed
|
||||||
|
savedPaths.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (this._hasInit) {
|
||||||
|
if (isDifferentPath) {
|
||||||
|
// push the new state to the history stack since the path
|
||||||
|
// isn't already in the location hash
|
||||||
|
this.history.pushState(path, '', '#' + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// replace the very first load with the correct entering state info
|
||||||
|
this.history.replaceState(path, '', '#' + path);
|
||||||
|
this._hasInit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedPaths[savedPaths.length - 1] != path) {
|
||||||
|
// only if the last item in the saved paths does
|
||||||
|
// not equal this path then it can be added
|
||||||
|
savedPaths.push(path);
|
||||||
|
|
||||||
|
// don't allow the history to grow too large
|
||||||
|
if (savedPaths.length > MAX_PATH_STORE) {
|
||||||
|
savedPaths = savedPaths.slice( savedPaths.length - MAX_PATH_STORE );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the new path data
|
||||||
|
this.paths(savedPaths);
|
||||||
|
|
||||||
|
// ensure this resets
|
||||||
|
this._currentPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onLocationChange() {
|
||||||
|
let currentPath = this.getCurrentPath();
|
||||||
|
|
||||||
|
if (currentPath == this._currentPath) {
|
||||||
|
// absolutely no change since last onLocationChange
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep in-memory the current path to quickly tell if things have changed
|
||||||
|
this._currentPath = currentPath;
|
||||||
|
|
||||||
|
// load up the saved paths
|
||||||
|
let savedPaths = this.paths();
|
||||||
|
|
||||||
|
if (currentPath === savedPaths[savedPaths.length - 1]) {
|
||||||
|
// do nothing if the last saved path is
|
||||||
|
// the same as the current path
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPath === savedPaths[savedPaths.length - 2]) {
|
||||||
|
// the user is moving back
|
||||||
|
this.router.pop();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// the user is moving forward
|
||||||
|
this.router.push(currentPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paths(val) {
|
||||||
|
if (arguments.length) {
|
||||||
|
// set in-memory data
|
||||||
|
this._paths = val;
|
||||||
|
|
||||||
|
// set localStorage data
|
||||||
|
try {
|
||||||
|
this.ls.setItem(PATH_STORE_KEY, JSON.stringify(val));
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (!this._paths) {
|
||||||
|
// we don't already have data in-memory
|
||||||
|
|
||||||
|
// see if we have data in localStorage
|
||||||
|
try {
|
||||||
|
let strData = this.ls.getItem(PATH_STORE_KEY);
|
||||||
|
if (strData) {
|
||||||
|
this._paths = JSON.parse(strData);
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
// if not in localStorage yet then create new path data
|
||||||
|
if (!this._paths) {
|
||||||
|
this._paths = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the in-memory data
|
||||||
|
return this._paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPath() {
|
||||||
|
// remove leading # to get the path
|
||||||
|
return this.location.hash.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const PATH_STORE_KEY = 'ionic:history';
|
||||||
|
const MAX_PATH_STORE = 20;
|
||||||
|
|
||||||
|
IonicRouter.registerStateManager('url', UrlStateManager);
|
Reference in New Issue
Block a user