/** * Copyright 2013-2014 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @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) { invariant( isValidID(ancestorID) && isValidID(destinationID), 'getNextDescendantID(%s, %s): Received an invalid React DOM ID.', ancestorID, destinationID ); 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 ); 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; for (var 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); invariant( isValidID(longestCommonID), 'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s', oneID, twoID, 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 || ''; invariant( start !== stop, 'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.', start ); var traverseUp = isAncestorIDOf(stop, start); invariant( traverseUp || isAncestorIDOf(start, stop), 'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' + 'not have a parent path.', 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; } 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 ); } } /** * 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); } }, /** * Same as `traverseTwoPhase` but skips the `targetID`. */ traverseTwoPhaseSkipTarget: function(targetID, cb, arg) { if (targetID) { traverseParentPath('', targetID, cb, arg, true, true); traverseParentPath(targetID, '', cb, arg, true, 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;