392 lines
11 KiB
JavaScript
392 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @providesModule ReactChildren
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var ReactElement = require('ReactElement');
|
|
|
|
var emptyFunction = require('fbjs/lib/emptyFunction');
|
|
var invariant = require('fbjs/lib/invariant');
|
|
|
|
if (__DEV__) {
|
|
var warning = require('fbjs/lib/warning');
|
|
var {getStackAddendum} = require('ReactDebugCurrentFrame');
|
|
}
|
|
|
|
var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
|
|
var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
|
|
// The Symbol used to tag the ReactElement type. If there is no native Symbol
|
|
// nor polyfill, then a plain number is used for performance.
|
|
var REACT_ELEMENT_TYPE =
|
|
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
|
|
0xeac7;
|
|
|
|
var SEPARATOR = '.';
|
|
var SUBSEPARATOR = ':';
|
|
|
|
/**
|
|
* Escape and wrap key so it is safe to use as a reactid
|
|
*
|
|
* @param {string} key to be escaped.
|
|
* @return {string} the escaped key.
|
|
*/
|
|
function escape(key) {
|
|
var escapeRegex = /[=:]/g;
|
|
var escaperLookup = {
|
|
'=': '=0',
|
|
':': '=2',
|
|
};
|
|
var escapedString = ('' + key).replace(escapeRegex, function(match) {
|
|
return escaperLookup[match];
|
|
});
|
|
|
|
return '$' + escapedString;
|
|
}
|
|
|
|
/**
|
|
* TODO: Test that a single child and an array with one item have the same key
|
|
* pattern.
|
|
*/
|
|
|
|
var didWarnAboutMaps = false;
|
|
|
|
var userProvidedKeyEscapeRegex = /\/+/g;
|
|
function escapeUserProvidedKey(text) {
|
|
return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
|
|
}
|
|
|
|
var POOL_SIZE = 10;
|
|
var traverseContextPool = [];
|
|
function getPooledTraverseContext(
|
|
mapResult,
|
|
keyPrefix,
|
|
mapFunction,
|
|
mapContext,
|
|
) {
|
|
if (traverseContextPool.length) {
|
|
var traverseContext = traverseContextPool.pop();
|
|
traverseContext.result = mapResult;
|
|
traverseContext.keyPrefix = keyPrefix;
|
|
traverseContext.func = mapFunction;
|
|
traverseContext.context = mapContext;
|
|
traverseContext.count = 0;
|
|
return traverseContext;
|
|
} else {
|
|
return {
|
|
result: mapResult,
|
|
keyPrefix: keyPrefix,
|
|
func: mapFunction,
|
|
context: mapContext,
|
|
count: 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
function releaseTraverseContext(traverseContext) {
|
|
traverseContext.result = null;
|
|
traverseContext.keyPrefix = null;
|
|
traverseContext.func = null;
|
|
traverseContext.context = null;
|
|
traverseContext.count = 0;
|
|
if (traverseContextPool.length < POOL_SIZE) {
|
|
traverseContextPool.push(traverseContext);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {?*} children Children tree container.
|
|
* @param {!string} nameSoFar Name of the key path so far.
|
|
* @param {!function} callback Callback to invoke with each child found.
|
|
* @param {?*} traverseContext Used to pass information throughout the traversal
|
|
* process.
|
|
* @return {!number} The number of children in this subtree.
|
|
*/
|
|
function traverseAllChildrenImpl(
|
|
children,
|
|
nameSoFar,
|
|
callback,
|
|
traverseContext,
|
|
) {
|
|
var type = typeof children;
|
|
|
|
if (type === 'undefined' || type === 'boolean') {
|
|
// All of the above are perceived as null.
|
|
children = null;
|
|
}
|
|
|
|
if (
|
|
children === null ||
|
|
type === 'string' ||
|
|
type === 'number' ||
|
|
// The following is inlined from ReactElement. This means we can optimize
|
|
// some checks. React Fiber also inlines this logic for similar purposes.
|
|
(type === 'object' && children.$$typeof === REACT_ELEMENT_TYPE)
|
|
) {
|
|
callback(
|
|
traverseContext,
|
|
children,
|
|
// If it's the only child, treat the name as if it was wrapped in an array
|
|
// so that it's consistent if the number of children grows.
|
|
nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
|
|
);
|
|
return 1;
|
|
}
|
|
|
|
var child;
|
|
var nextName;
|
|
var subtreeCount = 0; // Count of children found in the current subtree.
|
|
var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
|
|
|
|
if (Array.isArray(children)) {
|
|
for (var i = 0; i < children.length; i++) {
|
|
child = children[i];
|
|
nextName = nextNamePrefix + getComponentKey(child, i);
|
|
subtreeCount += traverseAllChildrenImpl(
|
|
child,
|
|
nextName,
|
|
callback,
|
|
traverseContext,
|
|
);
|
|
}
|
|
} else {
|
|
var iteratorFn =
|
|
(ITERATOR_SYMBOL && children[ITERATOR_SYMBOL]) ||
|
|
children[FAUX_ITERATOR_SYMBOL];
|
|
if (typeof iteratorFn === 'function') {
|
|
if (__DEV__) {
|
|
// Warn about using Maps as children
|
|
if (iteratorFn === children.entries) {
|
|
warning(
|
|
didWarnAboutMaps,
|
|
'Using Maps as children is unsupported and will likely yield ' +
|
|
'unexpected results. Convert it to a sequence/iterable of keyed ' +
|
|
'ReactElements instead.%s',
|
|
getStackAddendum(),
|
|
);
|
|
didWarnAboutMaps = true;
|
|
}
|
|
}
|
|
|
|
var iterator = iteratorFn.call(children);
|
|
var step;
|
|
var ii = 0;
|
|
while (!(step = iterator.next()).done) {
|
|
child = step.value;
|
|
nextName = nextNamePrefix + getComponentKey(child, ii++);
|
|
subtreeCount += traverseAllChildrenImpl(
|
|
child,
|
|
nextName,
|
|
callback,
|
|
traverseContext,
|
|
);
|
|
}
|
|
} else if (type === 'object') {
|
|
var addendum = '';
|
|
if (__DEV__) {
|
|
addendum =
|
|
' If you meant to render a collection of children, use an array ' +
|
|
'instead.' +
|
|
getStackAddendum();
|
|
}
|
|
var childrenString = '' + children;
|
|
invariant(
|
|
false,
|
|
'Objects are not valid as a React child (found: %s).%s',
|
|
childrenString === '[object Object]'
|
|
? 'object with keys {' + Object.keys(children).join(', ') + '}'
|
|
: childrenString,
|
|
addendum,
|
|
);
|
|
}
|
|
}
|
|
|
|
return subtreeCount;
|
|
}
|
|
|
|
/**
|
|
* Traverses children that are typically specified as `props.children`, but
|
|
* might also be specified through attributes:
|
|
*
|
|
* - `traverseAllChildren(this.props.children, ...)`
|
|
* - `traverseAllChildren(this.props.leftPanelChildren, ...)`
|
|
*
|
|
* The `traverseContext` is an optional argument that is passed through the
|
|
* entire traversal. It can be used to store accumulations or anything else that
|
|
* the callback might find relevant.
|
|
*
|
|
* @param {?*} children Children tree object.
|
|
* @param {!function} callback To invoke upon traversing each child.
|
|
* @param {?*} traverseContext Context for traversal.
|
|
* @return {!number} The number of children in this subtree.
|
|
*/
|
|
function traverseAllChildren(children, callback, traverseContext) {
|
|
if (children == null) {
|
|
return 0;
|
|
}
|
|
|
|
return traverseAllChildrenImpl(children, '', callback, traverseContext);
|
|
}
|
|
|
|
/**
|
|
* Generate a key string that identifies a component within a set.
|
|
*
|
|
* @param {*} component A component that could contain a manual key.
|
|
* @param {number} index Index that is used if a manual key is not provided.
|
|
* @return {string}
|
|
*/
|
|
function getComponentKey(component, index) {
|
|
// Do some typechecking here since we call this blindly. We want to ensure
|
|
// that we don't block potential future ES APIs.
|
|
if (
|
|
typeof component === 'object' &&
|
|
component !== null &&
|
|
component.key != null
|
|
) {
|
|
// Explicit key
|
|
return escape(component.key);
|
|
}
|
|
// Implicit key determined by the index in the set
|
|
return index.toString(36);
|
|
}
|
|
|
|
function forEachSingleChild(bookKeeping, child, name) {
|
|
var {func, context} = bookKeeping;
|
|
func.call(context, child, bookKeeping.count++);
|
|
}
|
|
|
|
/**
|
|
* Iterates through children that are typically specified as `props.children`.
|
|
*
|
|
* See https://reactjs.org/docs/react-api.html#react.children.foreach
|
|
*
|
|
* The provided forEachFunc(child, index) will be called for each
|
|
* leaf child.
|
|
*
|
|
* @param {?*} children Children tree container.
|
|
* @param {function(*, int)} forEachFunc
|
|
* @param {*} forEachContext Context for forEachContext.
|
|
*/
|
|
function forEachChildren(children, forEachFunc, forEachContext) {
|
|
if (children == null) {
|
|
return children;
|
|
}
|
|
var traverseContext = getPooledTraverseContext(
|
|
null,
|
|
null,
|
|
forEachFunc,
|
|
forEachContext,
|
|
);
|
|
traverseAllChildren(children, forEachSingleChild, traverseContext);
|
|
releaseTraverseContext(traverseContext);
|
|
}
|
|
|
|
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
|
|
var {result, keyPrefix, func, context} = bookKeeping;
|
|
|
|
var mappedChild = func.call(context, child, bookKeeping.count++);
|
|
if (Array.isArray(mappedChild)) {
|
|
mapIntoWithKeyPrefixInternal(
|
|
mappedChild,
|
|
result,
|
|
childKey,
|
|
emptyFunction.thatReturnsArgument,
|
|
);
|
|
} else if (mappedChild != null) {
|
|
if (ReactElement.isValidElement(mappedChild)) {
|
|
mappedChild = ReactElement.cloneAndReplaceKey(
|
|
mappedChild,
|
|
// Keep both the (mapped) and old keys if they differ, just as
|
|
// traverseAllChildren used to do for objects as children
|
|
keyPrefix +
|
|
(mappedChild.key && (!child || child.key !== mappedChild.key)
|
|
? escapeUserProvidedKey(mappedChild.key) + '/'
|
|
: '') +
|
|
childKey,
|
|
);
|
|
}
|
|
result.push(mappedChild);
|
|
}
|
|
}
|
|
|
|
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
|
|
var escapedPrefix = '';
|
|
if (prefix != null) {
|
|
escapedPrefix = escapeUserProvidedKey(prefix) + '/';
|
|
}
|
|
var traverseContext = getPooledTraverseContext(
|
|
array,
|
|
escapedPrefix,
|
|
func,
|
|
context,
|
|
);
|
|
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
|
|
releaseTraverseContext(traverseContext);
|
|
}
|
|
|
|
/**
|
|
* Maps children that are typically specified as `props.children`.
|
|
*
|
|
* See https://reactjs.org/docs/react-api.html#react.children.map
|
|
*
|
|
* The provided mapFunction(child, key, index) will be called for each
|
|
* leaf child.
|
|
*
|
|
* @param {?*} children Children tree container.
|
|
* @param {function(*, int)} func The map function.
|
|
* @param {*} context Context for mapFunction.
|
|
* @return {object} Object containing the ordered map of results.
|
|
*/
|
|
function mapChildren(children, func, context) {
|
|
if (children == null) {
|
|
return children;
|
|
}
|
|
var result = [];
|
|
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Count the number of children that are typically specified as
|
|
* `props.children`.
|
|
*
|
|
* See https://reactjs.org/docs/react-api.html#react.children.count
|
|
*
|
|
* @param {?*} children Children tree container.
|
|
* @return {number} The number of children.
|
|
*/
|
|
function countChildren(children, context) {
|
|
return traverseAllChildren(children, emptyFunction.thatReturnsNull, null);
|
|
}
|
|
|
|
/**
|
|
* Flatten a children object (typically specified as `props.children`) and
|
|
* return an array with appropriately re-keyed children.
|
|
*
|
|
* See https://reactjs.org/docs/react-api.html#react.children.toarray
|
|
*/
|
|
function toArray(children) {
|
|
var result = [];
|
|
mapIntoWithKeyPrefixInternal(
|
|
children,
|
|
result,
|
|
null,
|
|
emptyFunction.thatReturnsArgument,
|
|
);
|
|
return result;
|
|
}
|
|
|
|
var ReactChildren = {
|
|
forEach: forEachChildren,
|
|
map: mapChildren,
|
|
count: countChildren,
|
|
toArray: toArray,
|
|
};
|
|
|
|
module.exports = ReactChildren;
|