UNPKG

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