UNPKG

react-onsenui

Version:

Onsen UI - React Components for Hybrid Cordova/PhoneGap Apps with Material Design and iOS UI components

398 lines (345 loc) 10.8 kB
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 RouterNavigatorClass extends React.Component { constructor(...args) { super(...args); this.cancelUpdate = false; const callback = (name, event) => { if (this.props[name]) { return this.props[name](event); } }; this.onPrePush = callback.bind(this, 'onPrePush'); this.onPostPush = callback.bind(this, 'onPostPush'); this.onPrePop = callback.bind(this, 'onPrePop'); this.onPostPop = callback.bind(this, 'onPostPop'); this.ref = React.createRef(); this.state = { internalStack: [] }; } update(cb) { if (!this.cancelUpdate) { this.setState({}, cb); } } /** * @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; } const update = () => { return new Promise(resolve => { this.setState({internalStack: [...this.state.internalStack, routes[routes.length - 1]]}, resolve); }); }; return this.ref.current._pushPage(options, update) .then(() => { this.setState({internalStack: [...routes]}); }); } /** * @method pushPage * @signature pushPage(route, options = {}) * @param {Array} [routes] * [en] The routes 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; } const update = () => { return new Promise(resolve => { this.setState({internalStack: [...this.state.internalStack, route]}, resolve); }); }; return this.ref.current._pushPage(options, update); } isRunning() { return this.ref.current._isRunning; } /* * @method replacePage * @signature replacePage(page, [options]) * @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; } const update = () => { return new Promise(resolve => { this.setState({internalStack: [...this.state.internalStack, route]}, resolve); }); }; return this.ref.current._pushPage(options, update) .then(() => { this.setState({internalStack: [...this.state.internalStack.slice(0, -2), route]}); }); } /** * @method popPage * @signature popPage(route, 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; } const update = () => { return new Promise(resolve => { ReactDOM.flushSync(() => { // prevents flickering caused by React 18 batching this.setState({internalStack: this.state.internalStack.slice(0, -1)}, resolve); }); }); }; return this.ref.current._popPage(options, update); } _onDeviceBackButton(event) { if (this.props.routeConfig.routeStack.length > 1) { this.popPage(); } else { event.callParentHandler(); } } componentDidMount() { const node = this.ref.current; this.cancelUpdate = false; node.addEventListener('prepush', this.onPrePush); node.addEventListener('postpush', this.onPostPush); node.addEventListener('prepop', this.onPrePop); node.addEventListener('postpop', this.onPostPop); if (!this.props.routeConfig) { throw new Error('In RouterNavigator the property routeConfig needs to be set'); } node.swipeMax = this.props.swipePop; node.onDeviceBackButton = this.props.onDeviceBackButton || this._onDeviceBackButton.bind(this); this.setState({internalStack: this.props.routeConfig.routeStack}); } componentWillUnmount() { const node = this.ref.current; node.removeEventListener('prepush', this.onPrePush); node.removeEventListener('postpush', this.onPostPush); node.removeEventListener('prepop', this.onPrePop); node.removeEventListener('postpop', this.onPostPop); this.cancelUpdate = true; } componentDidUpdate(prevProps) { if (this.props.onDeviceBackButton !== undefined) { this.ref.current.onDeviceBackButton = this.props.onDeviceBackButton; } const processStack = [...this.props.routeConfig.processStack]; /** * Fix for Redux Timetravel. */ if (prevProps.routeConfig.processStack.length < this.props.routeConfig.processStack.length && prevProps.routeConfig.routeStack.length > this.props.routeConfig.routeStack.length) { return; } if (processStack.length > 0) { const {type, route, options} = processStack[0]; switch (type) { case 'push': this.pushPage(route, options); break; case 'pop': this.popPage(options); break; case 'reset': if (Array.isArray(route)) { this.resetPageStack(route, options); } else { this.resetPageStack([route], options); } break; case 'replace': this.replacePage(route, options); break; default: throw new Error(`Unknown type ${type} in processStack`); } } } render() { const { innerRef, renderPage, // these props should not be passed down onPrePush, onPostPush, onPrePop, onPostPop, swipePop, onDeviceBackButton, ...rest } = this.props; const pagesToRender = this.state.internalStack.map(route => renderPage(route)); if (innerRef && innerRef !== this.ref) { this.ref = innerRef; } return ( <Element {...rest} ref={this.ref}> {pagesToRender} </Element> ); } } /** * @original ons-navigator * @category navigation * @tutorial react/Reference/navigator * @description * [en] This component is a variant of the Navigator with a declarative API. 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] */ const RouterNavigator = React.forwardRef((props, ref) => ( <RouterNavigatorClass innerRef={ref} {...props}>{props.children}</RouterNavigatorClass> )); RouterNavigator.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 componen.[/en] * [ja][/ja] */ renderPage: PropTypes.func.isRequired, /** * @name routeConfig * @type object * @required true * @defaultValue null * @description * [en] This object must contain two properties: * `routeStack`: An array of route objects, * `processStack`: An array of process objects `{ type: push | pop | reset, route: userRoute }` that * describe the transition from the current state to the next state. * Make sure that the route stack is not emptied before the animations for the `processStack` have completed. * It is recommended to update the `routeStack` and empty the `processStack` in the 'onPostPop' callback. * [/en] * [ja][/ja] */ routeConfig: PropTypes.shape({ routeStack: PropTypes.arrayOf(PropTypes.object), processStack: PropTypes.arrayOf(PropTypes.object) }), /** * @name onPrePush * @type function * @required false * @description * [en]Called just before a page is pushed.[/en] */ onPrePush: PropTypes.func, /** * @name onPostPush * @type function * @required false * @description * [en]Called just after a page is pushed.[/en] */ onPostPush: PropTypes.func, /** * @name onPrePop * @type function * @required false * @description * [en]Called just before a page is popped.[/en] */ onPrePop: PropTypes.func, /** * @name onPostPop * @type function * @required false * @description * [en]Called just after a page is popped.[/en] */ onPostPop: PropTypes.func, /** * @property 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] */ 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] * Function called on swipe-to-pop. 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 }; export default RouterNavigator;