react-onsenui
Version:
Onsen UI - React Components for Hybrid Cordova/PhoneGap Apps with Material Design and iOS UI components
509 lines (443 loc) • 13.6 kB
JSX
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import 'onsenui/esm/elements/ons-navigator';
import onsCustomElement from '../onsCustomElement';
const Element = onsCustomElement('ons-navigator');
class NavigatorClass extends React.Component {
constructor(...args) {
super(...args);
this.ref = React.createRef();
this.pages = [];
this.state = { };
this._prePush = this._prePush.bind(this);
this._postPush = this._postPush.bind(this);
this._prePop = this._prePop.bind(this);
this._postPop = this._postPop.bind(this);
}
update(pages, obj) {
this.pages = pages || [];
return new Promise((resolve) => {
this.forceUpdate(resolve);
});
}
/**
* @method resetPage
* @signature resetPage(route, options = {})
* @param {Object} route
* [en] The route that the page should be reset to.[/en]
* [ja][/ja]
* @return {Promise}
* [en]Promise which resolves to the revealed page.[/en]
* [ja]明らかにしたページを解決するPromiseを返します。[/ja]
* @description
* [en]Resets the current page[/en]
* [ja][/ja]
*/
resetPage(route, options = {}) {
return this.resetPageStack([route], options);
}
/**
* @method resetPageStack
* @signature resetPageStack(route, options = {})
* @param {Array} routes
* [en] The routes that the navigator should be reset to.[/en]
* [ja][/ja]
* @return {Promise}
* [en]Promise which resolves to the revealed page.[/en]
* [ja]明らかにしたページを解決するPromiseを返します。[/ja]
* @description
* [en] Resets the navigator to the current page stack[/en]
* [ja][/ja]
*/
resetPageStack(routes, options = {}) {
if (this.isRunning()) {
return Promise.reject('Navigator is already running animation.');
}
const hidePages = () => {
const pageElements = this.ref.current.pages;
for (let i = pageElements.length - 2; i >= 0; i--) {
pageElements[i].style.display = 'none';
}
};
if (options.pop) {
this.routesBeforePop = this.routes.slice();
this.routesAfterPop = routes;
this.routes = routes.concat([this.routes[this.routes.length - 1]]);
const update = () => {
this.pages.pop();
this.routes = routes;
return new Promise((resolve) => this.forceUpdate(resolve));
};
return this.update(this.pages)
.then(() => this.ref.current._popPage(options, update))
.then(() => hidePages());
}
const lastRoute = routes[routes.length - 1];
const newPage = this.props.renderPage(lastRoute, this);
this.routes.push(lastRoute);
const update = () => {
this.pages.push(newPage);
return new Promise((resolve) => this.forceUpdate(resolve));
};
return this.ref.current._pushPage(options, update).then(() => {
this.routes = routes;
this.pages = routes.map(route => this.props.renderPage(route, this));
return this.update(this.pages).then(() => hidePages());
});
}
/**
* @method pushPage
* @signature pushPage(route, options = {})
* @param {Object} route
* [en] The route that the navigator should push to.[/en]
* [ja][/ja]
* @return {Promise}
* [en] Promise which resolves to the revealed page.[/en]
* [ja]明らかにしたページを解決するPromiseを返します。[/ja]
* @description
* [en] Pushes a page to the page stack[/en]
* [ja][/ja]
*/
pushPage(route, options = {}) {
if (this.isRunning()) {
return Promise.reject('Navigator is already running animation.');
}
return new Promise((resolve) => {
const update = () => {
return new Promise((resolve) => {
this.pages.push(this.props.renderPage(route, this));
this.forceUpdate(resolve);
});
};
this.routes.push(route);
this.ref.current
._pushPage(
options,
update
)
.then(resolve)
.catch((error) => {
this.routes.pop();
this.pages.pop();
throw error;
});
});
}
isRunning() {
return this.ref.current._isRunning;
}
/*
* @method replacePage
* @signature replacePage(route, [options])
* @param {Object} route
* [en] The route that the navigator should replace the top page with.[/en]
* [ja][/ja]
* @return {Promise}
* [en]Promise which resolves to the new page.[/en]
* [ja]新しいページを解決するPromiseを返します。[/ja]
* @description
* [en]Replaces the current top page with the specified one. Extends `pushPage()` parameters.[/en]
* [ja]現在表示中のページをを指定したページに置き換えます。[/ja]
*/
replacePage(route, options = {}) {
if (this.isRunning()) {
return Promise.reject('Navigator is already running animation.');
}
return this.pushPage(route, options).then(() => {
const pos = this.pages.length - 2;
this.pages.splice(pos, 1);
this.routes.splice(pos, 1);
this.ref.current.topPage.updateBackButton(this.pages.length > 1);
this.forceUpdate();
});
}
/**
* @method popPage
* @signature popPage(options = {})
* @return {Promise}
* [en] Promise which resolves to the revealed page.[/en]
* [ja]明らかにしたページを解決するPromiseを返します。[/ja]
* @description
* [en] Pops a page out of the page stack[/en]
* [ja][/ja]
*/
popPage(options = {}) {
if (this.isRunning()) {
return Promise.reject('Navigator is already running animation.');
}
this.routesBeforePop = this.routes.slice();
this.routesAfterPop = this.routesBeforePop.slice(0, this.routesBeforePop.length - 1);
const update = () => {
return new Promise((resolve) => {
this.pages.pop();
this.routes.pop();
ReactDOM.flushSync(() => { // prevents flickering caused by React 18 batching
this.forceUpdate(resolve);
});
});
};
return this.ref.current._popPage(options, update);
}
_onDeviceBackButton(event) {
if (this.pages.length > 1) {
this.popPage();
} else {
event.callParentHandler();
}
}
_prePop(event) {
if (event.target !== this.ref.current) {
return;
}
event.routes = {
poppingRoute: this.routesBeforePop[this.routesBeforePop.length - 1],
routes: this.routesBeforePop
};
this.props.onPrePop(event);
}
_postPop(event) {
if (event.target !== this.ref.current) {
return;
}
event.routes = {
poppedRoute: this.routesBeforePop[this.routesBeforePop.length - 1],
routes: this.routesAfterPop
};
this.props.onPostPop(event);
}
_prePush(event) {
if (event.target !== this.ref.current) {
return;
}
event.routes = {
pushingRoute: this.routes[this.routes.length - 1],
routes: this.routes.slice(0, this.routes.length - 1)
};
this.props.onPrePush(event);
}
_postPush(event) {
if (event.target !== this.ref.current) {
return;
}
event.routes = {
pushedRoute: this.routes[this.routes.length - 1],
routes: this.routes
};
this.props.onPostPush(event);
}
componentDidMount() {
const node = this.ref.current;
node.popPage = this.popPage.bind(this);
node.addEventListener('prepush', this._prePush);
node.addEventListener('postpush', this._postPush);
node.addEventListener('prepop', this._prePop);
node.addEventListener('postpop', this._postPop);
node.swipeMax = this.props.swipePop;
node.onDeviceBackButton = this.props.onDeviceBackButton || this._onDeviceBackButton.bind(this);
if (this.props.initialRoute && this.props.initialRouteStack) {
throw new Error('In Navigator either initalRoute or initalRoutes can be set');
}
if (this.props.initialRoute) {
this.routes = [this.props.initialRoute];
} else if (this.props.initialRouteStack) {
this.routes = this.props.initialRouteStack;
} else {
this.routes = [];
}
this.pages = this.routes.map(
(route) => this.props.renderPage(route, this)
);
this.forceUpdate();
}
componentDidUpdate() {
if (this.props.onDeviceBackButton !== undefined) {
this.ref.current.onDeviceBackButton = this.props.onDeviceBackButton;
}
}
componentWillUnmount() {
const node = this.ref.current;
node.removeEventListener('prepush', this.props.onPrePush);
node.removeEventListener('postpush', this.props.onPostPush);
node.removeEventListener('prepop', this.props.onPrePop);
node.removeEventListener('postpop', this.props.onPostPop);
}
render() {
const {
innerRef,
renderPage,
// these props should not be passed down
initialRouteStack,
initialRoute,
onPrePush,
onPostPush,
onPrePop,
onPostPop,
swipePop,
onDeviceBackButton,
...rest
} = this.props;
const pages = this.routes ? this.routes.map((route) => renderPage(route, this)) : null;
if (innerRef && innerRef !== this.ref) {
this.ref = innerRef;
}
return (
<Element
ref={this.ref}
{...rest}
>
{pages}
</Element>
);
}
}
/**
* @original ons-navigator
* @category navigation
* @tutorial react/Reference/navigator
* @description
* [en] This component is responsible for page transitioning and managing the pages of your OnsenUI application. In order to manage to display the pages, the navigator needs to define the `renderPage` method, that takes an route and a navigator and converts it to an page. [/en]
* [ja][/ja]
* @example
<Navigator
renderPage={(route, navigator) =>
<MyPage
title={route.title}
onPop={() => navigator.popPage()}
/>
}
initialRoute={{
title: 'First Page'
}} />
}
}
*/
const Navigator = React.forwardRef((props, ref) => (
<NavigatorClass innerRef={ref} {...props}>{props.children}</NavigatorClass>
));
Navigator.propTypes = {
/**
* @name renderPage
* @type function
* @required true
* @defaultValue null
* @description
* [en] This function takes the current route object as a parameter and returns a React component.[/en]
* [ja][/ja]
*/
renderPage: PropTypes.func.isRequired,
/**
* @name initialRouteStack
* @type array
* @required false
* @defaultValue null
* @description
* [en] This array contains the initial routes from the Navigator,
* which will be used to render the initial pages in the `renderPage` method.
* [/en]
* [ja][/ja]
*/
initialRouteStack: PropTypes.array,
/**
* @name initialRoute
* @type object
* @required false
* @defaultValue null
* @description
* [en] This array contains the initial route of the navigator,
* which will be used to render the initial pages in the
* renderPage method.
* [/en]
* [ja][/ja]
*/
initialRoute: PropTypes.object,
/**
* @name onPrePush
* @type function
* @required false
* @description
* [en]Called just before a page is pushed. It gets an event object with route information.[/en]
* [ja][/ja]
*/
onPrePush: PropTypes.func,
/**
* @name onPostPush
* @type function
* @required false
* @description
* [en]Called just after a page is pushed. It gets an event object with route information.[/en]
* [ja][/ja]
*/
onPostPush: PropTypes.func,
/**
* @name onPrePop
* @type function
* @required false
* @description
* [en]Called just before a page is popped. It gets an event object with route information.[/en]
*/
onPrePop: PropTypes.func,
/**
* @name onPostPop
* @type function
* @required false
* @description
* [en]Called just after a page is popped. It gets an event object with route information.[/en]
* [ja][/ja]
*/
onPostPop: PropTypes.func,
/**
* @name animation
* @type {String}
* @description
* [en]
* Animation name. Available animations are `"slide"`, `"lift"`, `"fade"` and `"none"`.
* These are platform based animations. For fixed animations, add `"-ios"` or `"-md"` suffix to the animation name. E.g. `"lift-ios"`, `"lift-md"`. Defaults values are `"slide-ios"` and `"fade-md"`.
* [/en]
* [ja][/ja]
*/
animation: PropTypes.string,
/**
* @name animationOptions
* @type object
* @description
* [en]Specify the animation's duration, delay and timing. E.g. `{duration: 0.2, delay: 0.4, timing: 'ease-in'}`.[/en]
* [ja][/ja]
*/
animationOptions: PropTypes.object,
/**
* @name swipeable
* @type bool|string
* @required false
* @description
* [en]Enables swipe-to-pop functionality for iOS.[/en]
* [ja][/ja]
*/
swipeable: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
/**
* @name swipePop
* @type function
* @required false
* @description
* [en]Optional function called on swipe-to-pop. If provided, must perform a popPage with the given options object.[/en]
* [ja][/ja]
*/
swipePop: PropTypes.func,
/**
* @name onDeviceBackButton
* @type function
* @required false
* @description
* [en]Custom handler for device back button.[/en]
* [ja][/ja]
*/
onDeviceBackButton: PropTypes.func
};
const NOOP = () => null;
Navigator.defaultProps = {
onPostPush: NOOP,
onPrePush: NOOP,
onPrePop: NOOP,
onPostPop: NOOP
};
export default Navigator;