diff --git a/src/renderers/native/ReactIOS/IOSNativeBridgeEventPlugin.js b/src/renderers/native/ReactIOS/IOSNativeBridgeEventPlugin.js index 6c1d887e55..844e47536b 100644 --- a/src/renderers/native/ReactIOS/IOSNativeBridgeEventPlugin.js +++ b/src/renderers/native/ReactIOS/IOSNativeBridgeEventPlugin.js @@ -41,25 +41,21 @@ var IOSNativeBridgeEventPlugin = { eventTypes: merge(customBubblingEventTypes, customDirectEventTypes), /** - * @param {string} topLevelType Record from `EventConstants`. - * @param {DOMEventTarget} topLevelTarget The listening component root node. - * @param {string} topLevelTargetID ID of `topLevelTarget`. - * @param {object} nativeEvent Native browser event. - * @return {*} An accumulation of synthetic events. * @see {EventPluginHub.extractEvents} */ extractEvents: function( - topLevelType: string, - topLevelTarget: EventTarget, - topLevelTargetID: string, - nativeEvent: Event + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget ): ?Object { var bubbleDispatchConfig = customBubblingEventTypes[topLevelType]; var directDispatchConfig = customDirectEventTypes[topLevelType]; var event = SyntheticEvent.getPooled( bubbleDispatchConfig || directDispatchConfig, - topLevelTargetID, - nativeEvent + targetInst, + nativeEvent, + nativeEventTarget ); if (bubbleDispatchConfig) { EventPropagators.accumulateTwoPhaseDispatches(event); diff --git a/src/renderers/native/ReactNative/ReactNativeBaseComponent.js b/src/renderers/native/ReactNative/ReactNativeBaseComponent.js index f16aff4f7c..ed21216ce6 100644 --- a/src/renderers/native/ReactNative/ReactNativeBaseComponent.js +++ b/src/renderers/native/ReactNative/ReactNativeBaseComponent.js @@ -13,6 +13,7 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var ReactNativeAttributePayload = require('ReactNativeAttributePayload'); +var ReactNativeComponentTree = require('ReactNativeComponentTree'); var ReactNativeEventEmitter = require('ReactNativeEventEmitter'); var ReactNativeTagHandles = require('ReactNativeTagHandles'); var ReactMultiChild = require('ReactMultiChild'); @@ -57,6 +58,7 @@ ReactNativeBaseComponent.Mixin = { }, unmountComponent: function() { + ReactNativeComponentTree.uncacheNode(this); deleteAllListeners(this._rootNodeID); this.unmountChildren(); this._rootNodeID = null; @@ -196,7 +198,6 @@ ReactNativeBaseComponent.Mixin = { ); var nativeTopRootTag = nativeContainerInfo._tag; - console.log('mountInCmp', nativeContainerInfo, nativeTopRootTag); UIManager.createView( tag, this.viewConfig.uiViewClassName, @@ -204,6 +205,8 @@ ReactNativeBaseComponent.Mixin = { updatePayload ); + ReactNativeComponentTree.precacheNode(this, tag); + this._registerListenersUponCreation(this._currentElement.props); this.initializeChildren( this._currentElement.props.children, diff --git a/src/renderers/native/ReactNative/ReactNativeComponentTree.js b/src/renderers/native/ReactNative/ReactNativeComponentTree.js new file mode 100644 index 0000000000..82963ad4b1 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeComponentTree.js @@ -0,0 +1,66 @@ +/** + * Copyright 2013-present, 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 ReactNativeComponentTree + */ + +'use strict'; + +var invariant = require('invariant'); + +var instanceCache = {}; + +/** + * Drill down (through composites and empty components) until we get a native or + * native text component. + * + * This is pretty polymorphic but unavoidable with the current structure we have + * for `_renderedChildren`. + */ +function getRenderedNativeOrTextFromComponent(component) { + var rendered; + while ((rendered = component._renderedComponent)) { + component = rendered; + } + return component; +} + +/** + * Populate `_nativeNode` on the rendered native/text component with the given + * DOM node. The passed `inst` can be a composite. + */ +function precacheNode(inst, tag) { + var nativeInst = getRenderedNativeOrTextFromComponent(inst); + instanceCache[tag] = nativeInst; +} + +function uncacheNode(inst) { + var tag = inst._rootNodeID; + if (tag) { + delete instanceCache[tag]; + } +} + +function getInstanceFromTag(tag) { + return instanceCache[tag] || null; +} + +function getTagFromInstance(inst) { + invariant(inst._rootNodeID, 'All native instances should have a tag.'); + return inst._rootNodeID; +} + +var ReactNativeComponentTree = { + getClosestInstanceFromNode: getInstanceFromTag, + getInstanceFromNode: getInstanceFromTag, + getNodeFromInstance: getTagFromInstance, + precacheNode: precacheNode, + uncacheNode: uncacheNode, +}; + +module.exports = ReactNativeComponentTree; diff --git a/src/renderers/native/ReactNative/ReactNativeDefaultInjection.js b/src/renderers/native/ReactNative/ReactNativeDefaultInjection.js index 18ddc86f97..c95e300be2 100644 --- a/src/renderers/native/ReactNative/ReactNativeDefaultInjection.js +++ b/src/renderers/native/ReactNative/ReactNativeDefaultInjection.js @@ -30,7 +30,9 @@ var ReactNativeComponentEnvironment = require('ReactNativeComponentEnvironment') var ReactNativeGlobalInteractionHandler = require('ReactNativeGlobalInteractionHandler'); var ReactNativeGlobalResponderHandler = require('ReactNativeGlobalResponderHandler'); var ReactNativeTextComponent = require('ReactNativeTextComponent'); +var ReactNativeTreeTraversal = require('ReactNativeTreeTraversal'); var ReactNativeComponent = require('ReactNativeComponent'); +var ReactNativeComponentTree = require('ReactNativeComponentTree'); var ReactSimpleEmptyComponent = require('ReactSimpleEmptyComponent'); var ReactUpdates = require('ReactUpdates'); var ResponderEventPlugin = require('ResponderEventPlugin'); @@ -47,6 +49,8 @@ function inject() { * Inject module for resolving DOM hierarchy and plugin ordering. */ EventPluginHub.injection.injectEventPluginOrder(IOSDefaultEventPluginOrder); + EventPluginUtils.injection.injectComponentTree(ReactNativeComponentTree); + EventPluginUtils.injection.injectTreeTraversal(ReactNativeTreeTraversal); ResponderEventPlugin.injection.injectGlobalResponderHandler( ReactNativeGlobalResponderHandler diff --git a/src/renderers/native/ReactNative/ReactNativeEventEmitter.js b/src/renderers/native/ReactNative/ReactNativeEventEmitter.js index eeea701f6e..67d18d11cb 100644 --- a/src/renderers/native/ReactNative/ReactNativeEventEmitter.js +++ b/src/renderers/native/ReactNative/ReactNativeEventEmitter.js @@ -14,6 +14,7 @@ var EventPluginHub = require('EventPluginHub'); var EventPluginRegistry = require('EventPluginRegistry'); var ReactEventEmitterMixin = require('ReactEventEmitterMixin'); +var ReactNativeComponentTree = require('ReactNativeComponentTree'); var ReactNativeTagHandles = require('ReactNativeTagHandles'); var EventConstants = require('EventConstants'); @@ -112,15 +113,15 @@ var ReactNativeEventEmitter = merge(ReactEventEmitterMixin, { * @param {object} nativeEventParam Object passed from native. */ _receiveRootNodeIDEvent: function( - rootNodeID: ?string, + rootNodeID: number, topLevelType: string, nativeEventParam: Object ) { var nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT; + var inst = ReactNativeComponentTree.getInstanceFromNode(rootNodeID); ReactNativeEventEmitter.handleTopLevel( topLevelType, - rootNodeID, - rootNodeID, + inst, nativeEvent, nativeEvent.target ); diff --git a/src/renderers/native/ReactNative/ReactNativeTextComponent.js b/src/renderers/native/ReactNative/ReactNativeTextComponent.js index 534828e161..434e4b8885 100644 --- a/src/renderers/native/ReactNative/ReactNativeTextComponent.js +++ b/src/renderers/native/ReactNative/ReactNativeTextComponent.js @@ -11,6 +11,7 @@ 'use strict'; +var ReactNativeComponentTree = require('ReactNativeComponentTree'); var ReactNativeTagHandles = require('ReactNativeTagHandles'); var UIManager = require('UIManager'); @@ -43,6 +44,9 @@ Object.assign(ReactNativeTextComponent.prototype, { nativeTopRootTag, {text: this._stringText} ); + + ReactNativeComponentTree.precacheNode(this, tag); + return tag; }, @@ -56,7 +60,6 @@ Object.assign(ReactNativeTextComponent.prototype, { var nextStringText = '' + nextText; if (nextStringText !== this._stringText) { this._stringText = nextStringText; - console.log('receiveComponent', this, this._rootNodeID); UIManager.updateView( this._rootNodeID, 'RCTRawText', @@ -67,6 +70,7 @@ Object.assign(ReactNativeTextComponent.prototype, { }, unmountComponent: function() { + ReactNativeComponentTree.uncacheNode(this); this._currentElement = null; this._stringText = null; this._rootNodeID = null; diff --git a/src/renderers/native/ReactNative/ReactNativeTreeTraversal.js b/src/renderers/native/ReactNative/ReactNativeTreeTraversal.js new file mode 100644 index 0000000000..58a3a9e479 --- /dev/null +++ b/src/renderers/native/ReactNative/ReactNativeTreeTraversal.js @@ -0,0 +1,126 @@ +/** + * Copyright 2015-present, 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 ReactNativeTreeTraversal + */ + +'use strict'; + +// Same as ReactDOMTreeTraversal without the invariants. + +/** + * Return the lowest common ancestor of A and B, or null if they are in + * different trees. + */ +function getLowestCommonAncestor(instA, instB) { + var depthA = 0; + for (var tempA = instA; tempA; tempA = tempA._nativeParent) { + depthA++; + } + var depthB = 0; + for (var tempB = instB; tempB; tempB = tempB._nativeParent) { + depthB++; + } + + // If A is deeper, crawl up. + while (depthA - depthB > 0) { + instA = instA._nativeParent; + depthA--; + } + + // If B is deeper, crawl up. + while (depthB - depthA > 0) { + instB = instB._nativeParent; + depthB--; + } + + // Walk in lockstep until we find a match. + var depth = depthA; + while (depth--) { + if (instA === instB) { + return instA; + } + instA = instA._nativeParent; + instB = instB._nativeParent; + } + return null; +} + +/** + * Return if A is an ancestor of B. + */ +function isAncestor(instA, instB) { + while (instB) { + if (instB === instA) { + return true; + } + instB = instB._nativeParent; + } + return false; +} + +/** + * Return the parent instance of the passed-in instance. + */ +function getParentInstance(inst) { + return inst._nativeParent; +} + +/** + * Simulates the traversal of a two-phase, capture/bubble event dispatch. + */ +function traverseTwoPhase(inst, fn, arg) { + var path = []; + while (inst) { + path.push(inst); + inst = inst._nativeParent; + } + var i; + for (i = path.length; i-- > 0;) { + fn(path[i], false, arg); + } + for (i = 0; i < path.length; i++) { + fn(path[i], true, arg); + } +} + +/** + * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that + * should would receive a `mouseEnter` or `mouseLeave` event. + * + * Does not invoke the callback on the nearest common ancestor because nothing + * "entered" or "left" that element. + */ +function traverseEnterLeave(from, to, fn, argFrom, argTo) { + var common = from && to ? getLowestCommonAncestor(from, to) : null; + var pathFrom = []; + while (from && from !== common) { + pathFrom.push(from); + from = from._nativeParent; + } + var pathTo = []; + while (to && to !== common) { + pathTo.push(to); + to = to._nativeParent; + } + var i; + for (i = 0; i < pathFrom.length; i++) { + fn(pathFrom[i], true, argFrom); + } + for (i = pathTo.length; i-- > 0;) { + fn(pathTo[i], false, argTo); + } +} + +module.exports = { + isAncestor: isAncestor, + getLowestCommonAncestor: getLowestCommonAncestor, + getParentInstance: getParentInstance, + traverseTwoPhase: traverseTwoPhase, + traverseEnterLeave: traverseEnterLeave, +}; diff --git a/src/renderers/native/ReactNative/findNodeHandle.js b/src/renderers/native/ReactNative/findNodeHandle.js index 1c7636282e..15f709e7e9 100644 --- a/src/renderers/native/ReactNative/findNodeHandle.js +++ b/src/renderers/native/ReactNative/findNodeHandle.js @@ -79,7 +79,7 @@ function findNodeHandle(componentOrHandle: any): ?number { // ReactInstanceMap.get here will always succeed for mounted components var internalInstance = ReactInstanceMap.get(component); if (internalInstance) { - return internalInstance._rootNodeID; + return internalInstance.getNativeNode(); } else { var rootNodeID = component._rootNodeID; if (rootNodeID) {