You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
332 lines
10 KiB
332 lines
10 KiB
/** |
|
* Copyright 2013-2015, Facebook, Inc. |
|
* All rights reserved. |
|
* |
|
* This source code is licensed under the BSD-style license found in the |
|
* LICENSE file in the root directory of this source tree. An additional grant |
|
* of patent rights can be found in the PATENTS file in the same directory. |
|
* |
|
* @providesModule ReactInstanceHandles |
|
* @typechecks static-only |
|
*/ |
|
|
|
'use strict'; |
|
|
|
var ReactRootIndex = require("./ReactRootIndex"); |
|
|
|
var invariant = require("./invariant"); |
|
|
|
var SEPARATOR = '.'; |
|
var SEPARATOR_LENGTH = SEPARATOR.length; |
|
|
|
/** |
|
* Maximum depth of traversals before we consider the possibility of a bad ID. |
|
*/ |
|
var MAX_TREE_DEPTH = 100; |
|
|
|
/** |
|
* Creates a DOM ID prefix to use when mounting React components. |
|
* |
|
* @param {number} index A unique integer |
|
* @return {string} React root ID. |
|
* @internal |
|
*/ |
|
function getReactRootIDString(index) { |
|
return SEPARATOR + index.toString(36); |
|
} |
|
|
|
/** |
|
* Checks if a character in the supplied ID is a separator or the end. |
|
* |
|
* @param {string} id A React DOM ID. |
|
* @param {number} index Index of the character to check. |
|
* @return {boolean} True if the character is a separator or end of the ID. |
|
* @private |
|
*/ |
|
function isBoundary(id, index) { |
|
return id.charAt(index) === SEPARATOR || index === id.length; |
|
} |
|
|
|
/** |
|
* Checks if the supplied string is a valid React DOM ID. |
|
* |
|
* @param {string} id A React DOM ID, maybe. |
|
* @return {boolean} True if the string is a valid React DOM ID. |
|
* @private |
|
*/ |
|
function isValidID(id) { |
|
return id === '' || ( |
|
id.charAt(0) === SEPARATOR && id.charAt(id.length - 1) !== SEPARATOR |
|
); |
|
} |
|
|
|
/** |
|
* Checks if the first ID is an ancestor of or equal to the second ID. |
|
* |
|
* @param {string} ancestorID |
|
* @param {string} descendantID |
|
* @return {boolean} True if `ancestorID` is an ancestor of `descendantID`. |
|
* @internal |
|
*/ |
|
function isAncestorIDOf(ancestorID, descendantID) { |
|
return ( |
|
descendantID.indexOf(ancestorID) === 0 && |
|
isBoundary(descendantID, ancestorID.length) |
|
); |
|
} |
|
|
|
/** |
|
* Gets the parent ID of the supplied React DOM ID, `id`. |
|
* |
|
* @param {string} id ID of a component. |
|
* @return {string} ID of the parent, or an empty string. |
|
* @private |
|
*/ |
|
function getParentID(id) { |
|
return id ? id.substr(0, id.lastIndexOf(SEPARATOR)) : ''; |
|
} |
|
|
|
/** |
|
* Gets the next DOM ID on the tree path from the supplied `ancestorID` to the |
|
* supplied `destinationID`. If they are equal, the ID is returned. |
|
* |
|
* @param {string} ancestorID ID of an ancestor node of `destinationID`. |
|
* @param {string} destinationID ID of the destination node. |
|
* @return {string} Next ID on the path from `ancestorID` to `destinationID`. |
|
* @private |
|
*/ |
|
function getNextDescendantID(ancestorID, destinationID) { |
|
("production" !== process.env.NODE_ENV ? invariant( |
|
isValidID(ancestorID) && isValidID(destinationID), |
|
'getNextDescendantID(%s, %s): Received an invalid React DOM ID.', |
|
ancestorID, |
|
destinationID |
|
) : invariant(isValidID(ancestorID) && isValidID(destinationID))); |
|
("production" !== process.env.NODE_ENV ? invariant( |
|
isAncestorIDOf(ancestorID, destinationID), |
|
'getNextDescendantID(...): React has made an invalid assumption about ' + |
|
'the DOM hierarchy. Expected `%s` to be an ancestor of `%s`.', |
|
ancestorID, |
|
destinationID |
|
) : invariant(isAncestorIDOf(ancestorID, destinationID))); |
|
if (ancestorID === destinationID) { |
|
return ancestorID; |
|
} |
|
// Skip over the ancestor and the immediate separator. Traverse until we hit |
|
// another separator or we reach the end of `destinationID`. |
|
var start = ancestorID.length + SEPARATOR_LENGTH; |
|
var i; |
|
for (i = start; i < destinationID.length; i++) { |
|
if (isBoundary(destinationID, i)) { |
|
break; |
|
} |
|
} |
|
return destinationID.substr(0, i); |
|
} |
|
|
|
/** |
|
* Gets the nearest common ancestor ID of two IDs. |
|
* |
|
* Using this ID scheme, the nearest common ancestor ID is the longest common |
|
* prefix of the two IDs that immediately preceded a "marker" in both strings. |
|
* |
|
* @param {string} oneID |
|
* @param {string} twoID |
|
* @return {string} Nearest common ancestor ID, or the empty string if none. |
|
* @private |
|
*/ |
|
function getFirstCommonAncestorID(oneID, twoID) { |
|
var minLength = Math.min(oneID.length, twoID.length); |
|
if (minLength === 0) { |
|
return ''; |
|
} |
|
var lastCommonMarkerIndex = 0; |
|
// Use `<=` to traverse until the "EOL" of the shorter string. |
|
for (var i = 0; i <= minLength; i++) { |
|
if (isBoundary(oneID, i) && isBoundary(twoID, i)) { |
|
lastCommonMarkerIndex = i; |
|
} else if (oneID.charAt(i) !== twoID.charAt(i)) { |
|
break; |
|
} |
|
} |
|
var longestCommonID = oneID.substr(0, lastCommonMarkerIndex); |
|
("production" !== process.env.NODE_ENV ? invariant( |
|
isValidID(longestCommonID), |
|
'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s', |
|
oneID, |
|
twoID, |
|
longestCommonID |
|
) : invariant(isValidID(longestCommonID))); |
|
return longestCommonID; |
|
} |
|
|
|
/** |
|
* Traverses the parent path between two IDs (either up or down). The IDs must |
|
* not be the same, and there must exist a parent path between them. If the |
|
* callback returns `false`, traversal is stopped. |
|
* |
|
* @param {?string} start ID at which to start traversal. |
|
* @param {?string} stop ID at which to end traversal. |
|
* @param {function} cb Callback to invoke each ID with. |
|
* @param {?boolean} skipFirst Whether or not to skip the first node. |
|
* @param {?boolean} skipLast Whether or not to skip the last node. |
|
* @private |
|
*/ |
|
function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) { |
|
start = start || ''; |
|
stop = stop || ''; |
|
("production" !== process.env.NODE_ENV ? invariant( |
|
start !== stop, |
|
'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.', |
|
start |
|
) : invariant(start !== stop)); |
|
var traverseUp = isAncestorIDOf(stop, start); |
|
("production" !== process.env.NODE_ENV ? invariant( |
|
traverseUp || isAncestorIDOf(start, stop), |
|
'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' + |
|
'not have a parent path.', |
|
start, |
|
stop |
|
) : invariant(traverseUp || isAncestorIDOf(start, stop))); |
|
// Traverse from `start` to `stop` one depth at a time. |
|
var depth = 0; |
|
var traverse = traverseUp ? getParentID : getNextDescendantID; |
|
for (var id = start; /* until break */; id = traverse(id, stop)) { |
|
var ret; |
|
if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) { |
|
ret = cb(id, traverseUp, arg); |
|
} |
|
if (ret === false || id === stop) { |
|
// Only break //after// visiting `stop`. |
|
break; |
|
} |
|
("production" !== process.env.NODE_ENV ? invariant( |
|
depth++ < MAX_TREE_DEPTH, |
|
'traverseParentPath(%s, %s, ...): Detected an infinite loop while ' + |
|
'traversing the React DOM ID tree. This may be due to malformed IDs: %s', |
|
start, stop |
|
) : invariant(depth++ < MAX_TREE_DEPTH)); |
|
} |
|
} |
|
|
|
/** |
|
* Manages the IDs assigned to DOM representations of React components. This |
|
* uses a specific scheme in order to traverse the DOM efficiently (e.g. in |
|
* order to simulate events). |
|
* |
|
* @internal |
|
*/ |
|
var ReactInstanceHandles = { |
|
|
|
/** |
|
* Constructs a React root ID |
|
* @return {string} A React root ID. |
|
*/ |
|
createReactRootID: function() { |
|
return getReactRootIDString(ReactRootIndex.createReactRootIndex()); |
|
}, |
|
|
|
/** |
|
* Constructs a React ID by joining a root ID with a name. |
|
* |
|
* @param {string} rootID Root ID of a parent component. |
|
* @param {string} name A component's name (as flattened children). |
|
* @return {string} A React ID. |
|
* @internal |
|
*/ |
|
createReactID: function(rootID, name) { |
|
return rootID + name; |
|
}, |
|
|
|
/** |
|
* Gets the DOM ID of the React component that is the root of the tree that |
|
* contains the React component with the supplied DOM ID. |
|
* |
|
* @param {string} id DOM ID of a React component. |
|
* @return {?string} DOM ID of the React component that is the root. |
|
* @internal |
|
*/ |
|
getReactRootIDFromNodeID: function(id) { |
|
if (id && id.charAt(0) === SEPARATOR && id.length > 1) { |
|
var index = id.indexOf(SEPARATOR, 1); |
|
return index > -1 ? id.substr(0, index) : id; |
|
} |
|
return null; |
|
}, |
|
|
|
/** |
|
* Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that |
|
* should would receive a `mouseEnter` or `mouseLeave` event. |
|
* |
|
* NOTE: Does not invoke the callback on the nearest common ancestor because |
|
* nothing "entered" or "left" that element. |
|
* |
|
* @param {string} leaveID ID being left. |
|
* @param {string} enterID ID being entered. |
|
* @param {function} cb Callback to invoke on each entered/left ID. |
|
* @param {*} upArg Argument to invoke the callback with on left IDs. |
|
* @param {*} downArg Argument to invoke the callback with on entered IDs. |
|
* @internal |
|
*/ |
|
traverseEnterLeave: function(leaveID, enterID, cb, upArg, downArg) { |
|
var ancestorID = getFirstCommonAncestorID(leaveID, enterID); |
|
if (ancestorID !== leaveID) { |
|
traverseParentPath(leaveID, ancestorID, cb, upArg, false, true); |
|
} |
|
if (ancestorID !== enterID) { |
|
traverseParentPath(ancestorID, enterID, cb, downArg, true, false); |
|
} |
|
}, |
|
|
|
/** |
|
* Simulates the traversal of a two-phase, capture/bubble event dispatch. |
|
* |
|
* NOTE: This traversal happens on IDs without touching the DOM. |
|
* |
|
* @param {string} targetID ID of the target node. |
|
* @param {function} cb Callback to invoke. |
|
* @param {*} arg Argument to invoke the callback with. |
|
* @internal |
|
*/ |
|
traverseTwoPhase: function(targetID, cb, arg) { |
|
if (targetID) { |
|
traverseParentPath('', targetID, cb, arg, true, false); |
|
traverseParentPath(targetID, '', cb, arg, false, true); |
|
} |
|
}, |
|
|
|
/** |
|
* Traverse a node ID, calling the supplied `cb` for each ancestor ID. For |
|
* example, passing `.0.$row-0.1` would result in `cb` getting called |
|
* with `.0`, `.0.$row-0`, and `.0.$row-0.1`. |
|
* |
|
* NOTE: This traversal happens on IDs without touching the DOM. |
|
* |
|
* @param {string} targetID ID of the target node. |
|
* @param {function} cb Callback to invoke. |
|
* @param {*} arg Argument to invoke the callback with. |
|
* @internal |
|
*/ |
|
traverseAncestors: function(targetID, cb, arg) { |
|
traverseParentPath('', targetID, cb, arg, true, false); |
|
}, |
|
|
|
/** |
|
* Exposed for unit testing. |
|
* @private |
|
*/ |
|
_getFirstCommonAncestorID: getFirstCommonAncestorID, |
|
|
|
/** |
|
* Exposed for unit testing. |
|
* @private |
|
*/ |
|
_getNextDescendantID: getNextDescendantID, |
|
|
|
isAncestorIDOf: isAncestorIDOf, |
|
|
|
SEPARATOR: SEPARATOR |
|
|
|
}; |
|
|
|
module.exports = ReactInstanceHandles;
|
|
|