react-native/Libraries/CustomComponents/NavigationExperimental/NavigationLegacyNavigatorRo...

294 lines
7.1 KiB
JavaScript

/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule NavigationLegacyNavigatorRouteStack
* @flow
*/
'use strict';
const invariant = require('fbjs/lib/invariant');
import type {
NavigationState,
NavigationParentState,
} from 'NavigationStateUtils';
type IterationCallback = (route: any, index: number, key: string) => void;
function isRouteEmpty(route: any): boolean {
return (route === undefined || route === null || route === '') || false;
}
function areRouteNodesEqual(
one: Array<RouteNode>,
two: Array<RouteNode>,
): boolean {
if (one === two) {
return true;
}
if (one.length !== two.length) {
return false;
}
for (let ii = 0, jj = one.length; ii < jj; ii++) {
if (one[ii] !== two[ii]) {
return false;
}
}
return true;
}
let _nextRouteNodeID = 0;
/**
* Private struct class that holds the key for a route.
*/
class RouteNode {
key: string;
route: any;
constructor(route: any) {
// Key value gets bigger incrementally. Developer can compare the
// keys of two routes then know which route is added to the stack
// earlier.
const key = String(_nextRouteNodeID++);
if (__DEV__ ) {
// Ensure the immutability of the node.
Object.defineProperty(this, 'key' , {
enumerable: true,
configurable: false,
writable: false,
value: key,
});
Object.defineProperty(this, 'route' , {
enumerable: true,
configurable: false,
writable: false,
value: route,
});
} else {
this.key = key;
this.route = route;
}
}
toNavigationState(): NavigationState {
const state: NavigationState = this;
return state;
}
}
let _nextRouteStackID = 0;
/**
* The data structure that holds a list of routes and the focused index
* of the routes. This data structure is implemented as immutable data
* and mutation (e.g. push, pop...etc) will yields a new instance.
*/
class RouteStack {
_index: number;
_key: string;
_routeNodes: Array<RouteNode>;
constructor(index: number, routes: Array<any>) {
invariant(
routes.length > 0,
'routes must not be an empty array'
);
invariant(
index > -1 && index <= routes.length - 1,
'index out of bound'
);
let routeNodes;
if (routes[0] instanceof RouteNode) {
// The array is already an array of <RouteNode>.
routeNodes = routes;
} else {
// Wrap the route with <RouteNode>.
routeNodes = routes.map((route) => {
invariant(!isRouteEmpty(route), 'route must not be mepty');
return new RouteNode(route);
});
}
this._routeNodes = routeNodes;
this._index = index;
this._key = String(_nextRouteStackID++);
}
/* $FlowFixMe - get/set properties not yet supported */
get size(): number {
return this._routeNodes.length;
}
/* $FlowFixMe - get/set properties not yet supported */
get index(): number {
return this._index;
}
// Export as...
toArray(): Array<any> {
return this._routeNodes.map(node => node.route);
}
toNavigationState(): NavigationParentState {
return {
index: this._index,
key: this._key,
children: this._routeNodes.map(node => node.toNavigationState()),
};
}
get(index: number): any {
if (index < 0 || index > this._routeNodes.length - 1) {
return null;
}
return this._routeNodes[index].route;
}
/**
* Returns the key associated with the route.
* When a route is added to a stack, the stack creates a key for this route.
* The key will persist until the initial stack and its derived stack
* no longer contains this route.
*/
keyOf(route: any): ?string {
if (isRouteEmpty(route)) {
return null;
}
const index = this.indexOf(route);
return index > -1 ?
this._routeNodes[index].key :
null;
}
indexOf(route: any): number {
if (isRouteEmpty(route)) {
return -1;
}
for (let ii = 0, jj = this._routeNodes.length; ii < jj; ii++) {
let node = this._routeNodes[ii];
if (node.route === route) {
return ii;
}
}
return -1;
}
slice(begin: ?number, end: ?number): RouteStack {
// check `begin` and `end` first to keep @flow happy.
const routeNodes = (end === undefined || end === null) ?
this._routeNodes.slice(begin || 0) :
this._routeNodes.slice(begin || 0, end || 0);
const index = Math.min(this._index, routeNodes.length - 1);
return this._update(index, routeNodes);
}
/**
* Returns a new stack with the provided route appended,
* starting at this stack size.
*/
push(route: any): RouteStack {
invariant(
!isRouteEmpty(route),
'Must supply route to push'
);
invariant(this.indexOf(route) === -1, 'route must be unique');
// When pushing, removes the rest of the routes past the current index.
const routeNodes = this._routeNodes.slice(0, this._index + 1);
routeNodes.push(new RouteNode(route));
return this._update(routeNodes.length - 1, routeNodes);
}
/**
* Returns a new stack a size ones less than this stack,
* excluding the last index in this stack.
*/
pop(): RouteStack {
invariant(
this._routeNodes.length > 1,
'should not pop routeNodes stack to empty'
);
// When popping, removes the rest of the routes past the current index.
const routeNodes = this._routeNodes.slice(0, this._index);
return this._update(routeNodes.length - 1, routeNodes);
}
jumpToIndex(index: number): RouteStack {
invariant(
index > -1 && index < this._routeNodes.length,
'jumpToIndex: index out of bound'
);
return this._update(index, this._routeNodes);
}
/**
* Replace a route in the navigation stack.
*
* `index` specifies the route in the stack that should be replaced.
* If it's negative, it counts from the back.
*/
replaceAtIndex(index: number, route: any): RouteStack {
invariant(
!isRouteEmpty(route),
'Must supply route to replace'
);
if (this.get(index) === route) {
return this;
}
invariant(this.indexOf(route) === -1, 'route must be unique');
if (index < 0) {
index += this._routeNodes.length;
}
invariant(
index > -1 && index < this._routeNodes.length,
'replaceAtIndex: index out of bound'
);
const routeNodes = this._routeNodes.slice(0);
routeNodes[index] = new RouteNode(route);
return this._update(index, routeNodes);
}
// Iterations
forEach(callback: IterationCallback, context: ?Object): void {
this._routeNodes.forEach((node, index) => {
callback.call(context, node.route, index, node.key);
});
}
mapToArray(callback: IterationCallback, context: ?Object): Array<any> {
return this._routeNodes.map((node, index) => {
return callback.call(context, node.route, index, node.key);
});
}
_update(index: number, routeNodes: Array<RouteNode>): RouteStack {
if (
this._index === index &&
areRouteNodesEqual(this._routeNodes, routeNodes)
) {
return this;
}
return new RouteStack(index, routeNodes);
}
}
module.exports = RouteStack;