From aec6516eaa66f75d058871a7196ad71974325eff Mon Sep 17 00:00:00 2001 From: petehunt Date: Mon, 3 Mar 2014 23:09:39 -0800 Subject: [PATCH] almost there working make it more message passing-y make triggerEvent public fix reacttestutils Tests passing fix where files live, remove old toplevel callback fix cloneWithProps tst Use opaque DOM node handles move mountimage stuff to use handles fix almost all of the tests all tests fixed add the plan fix bad rebase mostly fix tests...mostly completly unfuck rebase make example look better okay, apparently the test app works fuck the tests add reactworkermount break events revert other changes revert reactjs changes maybe fix things fix server rendering okay now all tests pass begin cutting over to workermount combine resolveDOMNodeHandle() and ReactDOMNodeMapping move more stuff to dom node handles sample apps work whatttttt change container handles to use opaque ids rename markup to image fix todo add helpful comment update make tests run again Begin work on ReactRemote bridge worker to dom remove another reference to innerHTML Add bootstrapping abilitiy Rename ReactRemoteModule to RemoteModule Bring ReactWorkerMount into the React global reactworker is running start adding injections set up bidirectional comms ReactWorkerReconcileTransaction move some things around move back containersByReactRootID begin to break more dependencies It finally works serialize events over the bridge remove some references to window in the event system ok it works entirely now make a new example for web workers Clean up ReactWorker API ReactWithWorkers entrypoint reset React.js Kill ReactInjection Remove todo remove some more todos fix unmounting reset basic example @vjeux comments, fix typehints fix typehints in ReactEventListener --- Gruntfile.js | 2 + examples/webworker/example.js | 39 ++ examples/webworker/index.html | 30 + grunt/config/browserify.js | 13 + grunt/config/jsx.js | 3 +- src/browser/ReactDOMNodeHandle.js | 53 ++ src/browser/ReactDOMNodeHandleMapping.js | 113 ++++ src/browser/ReactDOMNodeHandleTypes.js | 29 + src/browser/ReactEventEmitter.js | 111 ++-- src/browser/ReactPutListenerQueue.js | 7 +- src/browser/ReactWithWorkers.js | 106 ++++ src/browser/RemoteModule.js | 38 ++ src/browser/RemoteModuleServer.js | 67 ++ src/browser/__tests__/ReactDOM-test.js | 8 +- .../__tests__/ReactEventEmitter-test.js | 32 +- .../eventPlugins/EnterLeaveEventPlugin.js | 8 +- .../__tests__/EnterLeaveEventPlugin-test.js | 6 +- src/browser/{ui/dom => }/getEventTarget.js | 4 +- .../__tests__/ReactServerRendering-test.js | 7 +- .../syntheticEvents/SyntheticUIEvent.js | 3 +- src/browser/ui/ReactBrowserComponentMixin.js | 5 +- .../ui/ReactComponentBrowserEnvironment.js | 14 +- src/browser/ui/ReactDOMComponent.js | 16 +- src/browser/ui/ReactDOMIDOperations.js | 22 +- src/browser/ui/ReactDOMNodeMapping.js | 566 +++++++++++++++++ src/browser/ui/ReactDefaultInjection.js | 48 +- src/browser/ui/ReactEventListener.js | 183 ++++++ src/browser/ui/ReactEventTopLevelCallback.js | 160 ----- src/browser/ui/ReactInjection.js | 45 -- src/browser/ui/ReactMount.js | 583 ++---------------- .../{ => ui}/ReactReconcileTransaction.js | 0 src/browser/ui/ReactWorker.js | 79 +++ .../ui/__tests__/ReactDOMComponent-test.js | 4 +- .../ui/__tests__/ReactDOMIDOperations-test.js | 12 +- ...ack-test.js => ReactEventListener-test.js} | 39 +- .../__tests__/ReactMountDestruction-test.js | 2 +- .../ui/__tests__/ReactRenderDocument-test.js | 6 +- src/browser/ui/dom/DOMChildrenOperations.js | 4 +- src/browser/ui/dom/ViewportMetrics.js | 5 +- src/browser/ui/dom/components/ReactDOMForm.js | 5 +- src/browser/ui/dom/components/ReactDOMImg.js | 5 +- .../ui/dom/components/ReactDOMInput.js | 8 +- ...ReactComponentBrowserEnvironmentRemote.js} | 29 +- .../worker/ReactComponentWorkerEnvironment.js | 39 ++ .../worker/ReactDOMIDOperationsRemote.js | 40 ++ .../worker/ReactDOMNodeMappingRemote.js | 39 ++ .../worker/ReactEventListenerRemote.js | 57 ++ src/browser/worker/ReactWorkerInjection.js | 144 +++++ src/browser/worker/ReactWorkerMount.js | 139 +++++ .../worker/ReactWorkerReconcileTransaction.js | 164 +++++ src/core/ReactMultiChild.js | 32 +- src/core/ReactMultiChildUpdateTypes.js | 2 +- src/core/__tests__/ReactComponent-test.js | 4 +- .../__tests__/ReactCompositeComponent-test.js | 14 +- src/core/__tests__/ReactIdentity-test.js | 10 +- .../__tests__/ReactInstanceHandles-test.js | 34 +- .../ReactMultiChildReconcile-test.js | 4 +- src/test/ReactDefaultPerf.js | 4 +- src/test/ReactDefaultPerfAnalysis.js | 2 +- src/test/ReactTestUtils.js | 13 +- src/utils/__tests__/cloneWithProps-test.js | 2 + src/vendor/core/ExecutionEnvironment.js | 18 +- 62 files changed, 2275 insertions(+), 1005 deletions(-) create mode 100644 examples/webworker/example.js create mode 100644 examples/webworker/index.html create mode 100644 src/browser/ReactDOMNodeHandle.js create mode 100644 src/browser/ReactDOMNodeHandleMapping.js create mode 100644 src/browser/ReactDOMNodeHandleTypes.js create mode 100644 src/browser/ReactWithWorkers.js create mode 100644 src/browser/RemoteModule.js create mode 100644 src/browser/RemoteModuleServer.js rename src/browser/{ui/dom => }/getEventTarget.js (88%) create mode 100644 src/browser/ui/ReactDOMNodeMapping.js create mode 100644 src/browser/ui/ReactEventListener.js delete mode 100644 src/browser/ui/ReactEventTopLevelCallback.js delete mode 100644 src/browser/ui/ReactInjection.js rename src/browser/{ => ui}/ReactReconcileTransaction.js (100%) create mode 100644 src/browser/ui/ReactWorker.js rename src/browser/ui/__tests__/{ReactEventTopLevelCallback-test.js => ReactEventListener-test.js} (83%) rename src/browser/{ui/getReactRootElementInContainer.js => worker/ReactComponentBrowserEnvironmentRemote.js} (52%) create mode 100644 src/browser/worker/ReactComponentWorkerEnvironment.js create mode 100644 src/browser/worker/ReactDOMIDOperationsRemote.js create mode 100644 src/browser/worker/ReactDOMNodeMappingRemote.js create mode 100644 src/browser/worker/ReactEventListenerRemote.js create mode 100644 src/browser/worker/ReactWorkerInjection.js create mode 100644 src/browser/worker/ReactWorkerMount.js create mode 100644 src/browser/worker/ReactWorkerReconcileTransaction.js diff --git a/Gruntfile.js b/Gruntfile.js index de406e42ca4..cf1fb14bdbf 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -75,6 +75,7 @@ module.exports = function(grunt) { grunt.registerTask('build:transformer', ['jsx:normal', 'browserify:transformer']); grunt.registerTask('build:min', ['jsx:normal', 'version-check', 'browserify:min']); grunt.registerTask('build:addons-min', ['jsx:normal', 'browserify:addonsMin']); + grunt.registerTask('build:workers', ['jsx:normal', 'browserify:workers']); grunt.registerTask('build:withCodeCoverageLogging', [ 'jsx:normal', 'version-check', @@ -217,6 +218,7 @@ module.exports = function(grunt) { 'browserify:addons', 'browserify:min', 'browserify:addonsMin', + 'browserify:workers', 'npm-react:release', 'npm-react:pack', 'npm-react-tools:pack', diff --git a/examples/webworker/example.js b/examples/webworker/example.js new file mode 100644 index 00000000000..4a9d53c266c --- /dev/null +++ b/examples/webworker/example.js @@ -0,0 +1,39 @@ +/** + * @jsx React.DOM + */ + +if (typeof React === 'undefined') { + importScripts('../../build/react-with-workers.js'); +} + +React.Worker.run('./example.js', [], function() { + var ExampleApplication = React.createClass({ + getInitialState: function() { + return {red: false}; + }, + toggle: function() { + this.setState({red: !this.state.red}); + }, + render: function() { + var elapsed = Math.round(this.props.elapsed / 100); + var seconds = elapsed / 10 + (elapsed % 10 ? '' : '.0' ); + var message = + 'React has been successfully running for ' + seconds + ' seconds.'; + + return React.DOM.p({onClick: this.toggle, style: {color: this.state.red ? 'red' : 'blue'}}, message); + } + }); + + var start = new Date().getTime(); + + setInterval(function() { + try { + React.renderComponent( + ExampleApplication({elapsed: new Date().getTime() - start}), + 'container' + ); + } catch (e) { + console.log(e.stack); + } + }, 50); +}); diff --git a/examples/webworker/index.html b/examples/webworker/index.html new file mode 100644 index 00000000000..680d68d33a9 --- /dev/null +++ b/examples/webworker/index.html @@ -0,0 +1,30 @@ + + + + + Basic Example with Precompiled JSX + + + +

Basic Example with Precompiled JSX

+
+

+ If you can see this, React is not running. Try running: +

+
npm install -g react-tools
+cd examples/basic-jsx-precompile/
+jsx . build/
+
+

Example Details

+

This is written with JSX in a separate file and precompiled to vanilla JS by running:

+
npm install -g react-tools
+cd examples/basic-jsx-precompile/
+jsx . build/
+

+ Learn more about React at + facebook.github.io/react. +

+ + + + diff --git a/grunt/config/browserify.js b/grunt/config/browserify.js index 61481e43534..5733e06859d 100644 --- a/grunt/config/browserify.js +++ b/grunt/config/browserify.js @@ -103,6 +103,18 @@ var addonsMin = _.merge({}, addons, { after: [minify, bannerify] }); +var workers = { + entries: [ + './build/modules/ReactWithWorkers.js' + ], + outfile: './build/react-with-workers.js', + debug: false, + standalone: 'React', + transforms: [envify({NODE_ENV: 'development'})], + packageName: 'React (web workers)', + after: [es3ify.transform, simpleBannerify] +}; + var withCodeCoverageLogging = { entries: [ './build/modules/React.js' @@ -122,5 +134,6 @@ module.exports = { transformer: transformer, addons: addons, addonsMin: addonsMin, + workers: workers, withCodeCoverageLogging: withCodeCoverageLogging }; diff --git a/grunt/config/jsx.js b/grunt/config/jsx.js index acafefb4af4..d66230cebf3 100644 --- a/grunt/config/jsx.js +++ b/grunt/config/jsx.js @@ -5,7 +5,8 @@ var _ = require('lodash'); var rootIDs = [ "React", - "ReactWithAddons" + "ReactWithAddons", + "ReactWithWorkers" ]; diff --git a/src/browser/ReactDOMNodeHandle.js b/src/browser/ReactDOMNodeHandle.js new file mode 100644 index 00000000000..5bb2ea6b820 --- /dev/null +++ b/src/browser/ReactDOMNodeHandle.js @@ -0,0 +1,53 @@ +/** + * 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 ReactDOMNodeHandle + */ + +"use strict"; + +var ReactDOMNodeHandleTypes = require('ReactDOMNodeHandleTypes'); + +var ReactDOMNodeHandle = { + getKey: function(handle) { + return handle.key; + }, + + getHandleForReactID: function(reactID) { + return { + key: 'reactid:' + reactID, + type: ReactDOMNodeHandleTypes.REACT_ID, + reactID: reactID + }; + }, + + getHandleForReactIDTopLevel: function(reactID) { + return { + key: 'toplevel:' + reactID, + type: ReactDOMNodeHandleTypes.REACT_ID_TOP_LEVEL, + reactID: reactID + }; + }, + + getHandleForContainerID: function(id) { + return { + key: 'containerID:' + id, + type: ReactDOMNodeHandleTypes.CONTAINER, + id: id + }; + } +}; + +module.exports = ReactDOMNodeHandle; diff --git a/src/browser/ReactDOMNodeHandleMapping.js b/src/browser/ReactDOMNodeHandleMapping.js new file mode 100644 index 00000000000..3d95de7bcbc --- /dev/null +++ b/src/browser/ReactDOMNodeHandleMapping.js @@ -0,0 +1,113 @@ +/** + * 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 ReactDOMNodeHandleMapping + * @typechecks static-only + */ + +"use strict"; + +var ReactDOMNodeHandle = require('ReactDOMNodeHandle'); +var ReactInstanceHandles = require('ReactInstanceHandles'); + +/** Mapping from reactRootID to React component instance. */ +var instancesByReactRootID = {}; + +/** inverse of above */ +var reactRootIDByContainerKey = {}; + +/** + * @param {object} container DOM node handle that may contain a React component. + * @return {?string} A "reactRoot" ID, if a React component is rendered. + */ +function getReactRootID(containerHandle) { + return reactRootIDByContainerKey[ + ReactDOMNodeHandle.getKey(containerHandle) + ]; +} + +var ReactDOMNodeHandleMapping = { + /** Exposed for debugging purposes **/ + _instancesByReactRootID: instancesByReactRootID, + + getReactRootID: getReactRootID, + + /** + * Register a component into the instance map and starts scroll value + * monitoring + * @param {ReactComponent} nextComponent component instance to render + * @param {object} containerHandle container to render into + * @param {string} forceReactRootID reactRootID to use (rather than generating one) + * @return {string} reactRoot ID prefix + */ + registerComponent: function(nextComponent, containerHandle, forceReactRootID) { + var reactRootID = ReactDOMNodeHandleMapping.registerContainer( + containerHandle, + forceReactRootID + ); + instancesByReactRootID[reactRootID] = nextComponent; + return reactRootID; + }, + + /** + * Registers a container node into which React components will be rendered. + * This also creates the "reactRoot" ID that will be assigned to the element + * rendered within. + * + * @param {object} containerHandle DOM node handle to register as a container. + * @param {string} forceReactRootID reactRootID to use (rather than generating one) + * @return {string} The "reactRoot" ID of elements rendered within. + */ + registerContainer: function(containerHandle, forceReactRootID) { + var reactRootID = forceReactRootID || getReactRootID(containerHandle); + if (reactRootID) { + // If one exists, make sure it is a valid "reactRoot" ID. + reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID); + } + if (!reactRootID) { + // No valid "reactRoot" ID found, create one. + reactRootID = ReactInstanceHandles.createReactRootID(); + } + reactRootIDByContainerKey[ + ReactDOMNodeHandle.getKey(containerHandle) + ] = reactRootID; + return reactRootID; + }, + + /** + * Unmounts and destroys the React component rendered in the `container`. + * + * @param {object} containerHandle DOM node handle containing a React component. + * @return {?string} reactRootID that was just unmounted or null if no component is there. + */ + unmountComponentAtNode: function(containerHandle) { + var reactRootID = getReactRootID(containerHandle); + var component = instancesByReactRootID[reactRootID]; + + if (!component) { + return null; + } + delete instancesByReactRootID[reactRootID]; + + component.unmountComponent(); + return reactRootID; + }, + + getInstanceFromContainer: function(containerHandle) { + return instancesByReactRootID[getReactRootID(containerHandle)]; + } +}; + +module.exports = ReactDOMNodeHandleMapping; diff --git a/src/browser/ReactDOMNodeHandleTypes.js b/src/browser/ReactDOMNodeHandleTypes.js new file mode 100644 index 00000000000..ce4c888deca --- /dev/null +++ b/src/browser/ReactDOMNodeHandleTypes.js @@ -0,0 +1,29 @@ +/** + * 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 ReactDOMNodeHandleTypes + */ + +"use strict"; + +var keyMirror = require('keyMirror'); + +var ReactDOMNodeHandleTypes = keyMirror({ + REACT_ID: null, + REACT_ID_TOP_LEVEL: null, + CONTAINER: null +}); + +module.exports = ReactDOMNodeHandleTypes; diff --git a/src/browser/ReactEventEmitter.js b/src/browser/ReactEventEmitter.js index 5f1956d7d9d..1eef2d4177d 100644 --- a/src/browser/ReactEventEmitter.js +++ b/src/browser/ReactEventEmitter.js @@ -20,14 +20,12 @@ "use strict"; var EventConstants = require('EventConstants'); -var EventListener = require('EventListener'); var EventPluginHub = require('EventPluginHub'); var EventPluginRegistry = require('EventPluginRegistry'); var ExecutionEnvironment = require('ExecutionEnvironment'); var ReactEventEmitterMixin = require('ReactEventEmitterMixin'); var ViewportMetrics = require('ViewportMetrics'); -var invariant = require('invariant'); var isEventSupported = require('isEventSupported'); var merge = require('merge'); @@ -138,65 +136,31 @@ function getListeningForDocument(mountAt) { return alreadyListeningTo[mountAt[topListenersIDKey]]; } -/** - * Traps top-level events by using event bubbling. - * - * @param {string} topLevelType Record from `EventConstants`. - * @param {string} handlerBaseName Event name (e.g. "click"). - * @param {DOMEventTarget} element Element on which to attach listener. - * @internal - */ -function trapBubbledEvent(topLevelType, handlerBaseName, element) { - EventListener.listen( - element, - handlerBaseName, - ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( - topLevelType - ) - ); -} - -/** - * Traps a top-level event by using event capturing. - * - * @param {string} topLevelType Record from `EventConstants`. - * @param {string} handlerBaseName Event name (e.g. "click"). - * @param {DOMEventTarget} element Element on which to attach listener. - * @internal - */ -function trapCapturedEvent(topLevelType, handlerBaseName, element) { - EventListener.capture( - element, - handlerBaseName, - ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( - topLevelType - ) - ); -} - /** * `ReactEventEmitter` is used to attach top-level event listeners. For example: * * ReactEventEmitter.putListener('myID', 'onClick', myFunction); * - * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'. + * ReactEventEmitter would allocate a "registration" of `('onClick', myFunction)` on 'myID'. * * @internal */ var ReactEventEmitter = merge(ReactEventEmitterMixin, { /** - * React references `ReactEventTopLevelCallback` using this property in order - * to allow dependency injection. + * Injectable event backend */ - TopLevelCallbackCreator: null, + ReactEventListener: null, injection: { /** * @param {function} TopLevelCallbackCreator */ - injectTopLevelCallbackCreator: function(TopLevelCallbackCreator) { - ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator; + injectReactEventListener: function(ReactEventListener) { + ReactEventListener.setHandleTopLevel( + ReactEventEmitter.handleTopLevel + ); + ReactEventEmitter.ReactEventListener = ReactEventListener; } }, @@ -206,13 +170,8 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, { * @param {boolean} enabled True if callbacks should be enabled. */ setEnabled: function(enabled) { - invariant( - ExecutionEnvironment.canUseDOM, - 'setEnabled(...): Cannot toggle event listening in a Worker thread. ' + - 'This is likely a bug in the framework. Please report immediately.' - ); - if (ReactEventEmitter.TopLevelCallbackCreator) { - ReactEventEmitter.TopLevelCallbackCreator.setEnabled(enabled); + if (ReactEventEmitter.ReactEventListener) { + ReactEventEmitter.ReactEventListener.setEnabled(enabled); } }, @@ -221,8 +180,8 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, { */ isEnabled: function() { return !!( - ReactEventEmitter.TopLevelCallbackCreator && - ReactEventEmitter.TopLevelCallbackCreator.isEnabled() + ReactEventEmitter.ReactEventListener && + ReactEventEmitter.ReactEventListener.isEnabled() ); }, @@ -264,13 +223,13 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, { if (topLevelType === topLevelTypes.topWheel) { if (isEventSupported('wheel')) { - trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt); + ReactEventEmitter.trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt); } else if (isEventSupported('mousewheel')) { - trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt); + ReactEventEmitter.trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt); } else { // Firefox needs to capture a different mouse scroll event. // @see http://www.quirksmode.org/dom/events/tests/scroll.html - trapBubbledEvent( + ReactEventEmitter.trapBubbledEvent( topLevelTypes.topWheel, 'DOMMouseScroll', mountAt); @@ -278,28 +237,28 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, { } else if (topLevelType === topLevelTypes.topScroll) { if (isEventSupported('scroll', true)) { - trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt); + ReactEventEmitter.trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt); } else { - trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window); + ReactEventEmitter.trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window); } } else if (topLevelType === topLevelTypes.topFocus || topLevelType === topLevelTypes.topBlur) { if (isEventSupported('focus', true)) { - trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt); - trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt); + ReactEventEmitter.trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt); + ReactEventEmitter.trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt); } else if (isEventSupported('focusin')) { // IE has `focusin` and `focusout` events which bubble. // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html - trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt); - trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt); + ReactEventEmitter.trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt); + ReactEventEmitter.trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt); } // to make sure blur and focus event listeners are only attached once isListening[topLevelTypes.topBlur] = true; isListening[topLevelTypes.topFocus] = true; } else if (topEventMapping.hasOwnProperty(dependency)) { - trapBubbledEvent(topLevelType, topEventMapping[dependency], mountAt); + ReactEventEmitter.trapBubbledEvent(topLevelType, topEventMapping[dependency], mountAt); } isListening[dependency] = true; @@ -307,6 +266,22 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, { } }, + trapBubbledEvent: function(topLevelType, handlerBaseName, element) { + ReactEventEmitter.ReactEventListener.trapBubbledEvent( + topLevelType, + handlerBaseName, + element + ); + }, + + trapCapturedEvent: function(topLevelType, handlerBaseName, element) { + ReactEventEmitter.ReactEventListener.trapCapturedEvent( + topLevelType, + handlerBaseName, + element + ); + }, + /** * Listens to window scroll and resize events. We cache scroll values so that * application code can access them without triggering reflows. @@ -318,8 +293,7 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, { ensureScrollValueMonitoring: function(){ if (!isMonitoringScrollValue) { var refresh = ViewportMetrics.refreshScrollValues; - EventListener.listen(window, 'scroll', refresh); - EventListener.listen(window, 'resize', refresh); + ReactEventEmitter.ReactEventListener.monitorScrollValue(refresh); isMonitoringScrollValue = true; } }, @@ -334,12 +308,7 @@ var ReactEventEmitter = merge(ReactEventEmitterMixin, { deleteListener: EventPluginHub.deleteListener, - deleteAllListeners: EventPluginHub.deleteAllListeners, - - trapBubbledEvent: trapBubbledEvent, - - trapCapturedEvent: trapCapturedEvent - + deleteAllListeners: EventPluginHub.deleteAllListeners }); module.exports = ReactEventEmitter; diff --git a/src/browser/ReactPutListenerQueue.js b/src/browser/ReactPutListenerQueue.js index e81966e2618..230b2891c93 100644 --- a/src/browser/ReactPutListenerQueue.js +++ b/src/browser/ReactPutListenerQueue.js @@ -28,8 +28,9 @@ function ReactPutListenerQueue() { } mixInto(ReactPutListenerQueue, { - enqueuePutListener: function(rootNodeID, propKey, propValue) { + enqueuePutListener: function(handle, rootNodeID, propKey, propValue) { this.listenersToPut.push({ + handle: handle, rootNodeID: rootNodeID, propKey: propKey, propValue: propValue @@ -39,6 +40,10 @@ mixInto(ReactPutListenerQueue, { putListeners: function() { for (var i = 0; i < this.listenersToPut.length; i++) { var listenerToPut = this.listenersToPut[i]; + ReactEventEmitter.listenTo( + listenerToPut.propKey, + listenerToPut.handle + ); ReactEventEmitter.putListener( listenerToPut.rootNodeID, listenerToPut.propKey, diff --git a/src/browser/ReactWithWorkers.js b/src/browser/ReactWithWorkers.js new file mode 100644 index 00000000000..6dfb2e302cc --- /dev/null +++ b/src/browser/ReactWithWorkers.js @@ -0,0 +1,106 @@ +/** + * 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 ReactWithWorkers + */ + +"use strict"; + +var ExecutionEnvironment = require('ExecutionEnvironment'); +var DOMPropertyOperations = require('DOMPropertyOperations'); +var EventPluginUtils = require('EventPluginUtils'); +var ReactChildren = require('ReactChildren'); +var ReactComponent = require('ReactComponent'); +var ReactCompositeComponent = require('ReactCompositeComponent'); +var ReactContext = require('ReactContext'); +var ReactCurrentOwner = require('ReactCurrentOwner'); +var ReactDefaultInjection = require('ReactDefaultInjection'); +var ReactDescriptor = require('ReactDescriptor'); +var ReactDOM = require('ReactDOM'); +var ReactDOMComponent = require('ReactDOMComponent'); +var ReactWorkerInjection = require('ReactWorkerInjection'); +var ReactInstanceHandles = require('ReactInstanceHandles'); +var ReactMount = require('ReactMount'); +var ReactMultiChild = require('ReactMultiChild'); +var ReactPerf = require('ReactPerf'); +var ReactPropTypes = require('ReactPropTypes'); +var ReactServerRendering = require('ReactServerRendering'); +var ReactTextComponent = require('ReactTextComponent'); +var ReactWorker = require('ReactWorker'); +var ReactWorkerMount = require('ReactWorkerMount'); + +var onlyChild = require('onlyChild'); + +if (ExecutionEnvironment.canUseDOM) { + ReactDefaultInjection.inject(); +} else { + ReactWorkerInjection.inject(); +} + +var React = { + Children: { + map: ReactChildren.map, + forEach: ReactChildren.forEach, + only: onlyChild + }, + DOM: ReactDOM, + PropTypes: ReactPropTypes, + initializeTouchEvents: function(shouldUseTouch) { + EventPluginUtils.useTouchEvents = shouldUseTouch; + }, + Worker: ReactWorker, + createClass: ReactCompositeComponent.createClass, + constructAndRenderComponent: ReactMount.constructAndRenderComponent, + constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID, + renderComponent: ReactPerf.measure( + 'React', + 'renderComponent', + ReactWorkerMount.renderComponent + ), + renderComponentToString: ReactServerRendering.renderComponentToString, + renderComponentToStaticMarkup: + ReactServerRendering.renderComponentToStaticMarkup, + unmountComponentAtNode: ReactMount.unmountComponentAtNode, + isValidClass: ReactDescriptor.isValidFactory, + isValidComponent: ReactDescriptor.isValidDescriptor, + withContext: ReactContext.withContext, + __internals: { + Component: ReactComponent, + CurrentOwner: ReactCurrentOwner, + DOMComponent: ReactDOMComponent, + DOMPropertyOperations: DOMPropertyOperations, + InstanceHandles: ReactInstanceHandles, + Mount: ExecutionEnvironment.canUseDOM ? ReactMount : ReactWorkerMount, + MultiChild: ReactMultiChild, + TextComponent: ReactTextComponent + } +}; + +if (__DEV__) { + if (ExecutionEnvironment.canUseDOM && + window.top === window.self && + navigator.userAgent.indexOf('Chrome') > -1) { + console.debug( + 'Download the React DevTools for a better development experience: ' + + 'http://fb.me/react-devtools' + ); + } +} + +// Version exists only in the open-source version of React, not in Facebook's +// internal version. +React.version = '0.11.0-alpha'; + +module.exports = React; diff --git a/src/browser/RemoteModule.js b/src/browser/RemoteModule.js new file mode 100644 index 00000000000..4a3d6ab71df --- /dev/null +++ b/src/browser/RemoteModule.js @@ -0,0 +1,38 @@ +/** + * 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 RemoteModule + * @typechecks static-only + */ + +// TODO: use a better bridging system that doesn't marshal strings all +// the time. +class RemoteModule { + constructor(target, name, methods) { + this.target = target; + this.name = name; + for (var method in methods) { + this[method] = this.invoke.bind(this, method); + } + } + + invoke(name) { + // No return values allowed! + var args = Array.prototype.slice.call(arguments, 1); + this.target.postMessage([this.name, name, args]); + } +} + +module.exports = RemoteModule; diff --git a/src/browser/RemoteModuleServer.js b/src/browser/RemoteModuleServer.js new file mode 100644 index 00000000000..83a2af69388 --- /dev/null +++ b/src/browser/RemoteModuleServer.js @@ -0,0 +1,67 @@ +/** + * 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 RemoteModuleServer + * @typechecks static-only + */ + +var invariant = require('invariant'); + +class RemoteModuleServer { + /** + * @param {object} target web worker receiving messages for this server. + * @param {object} modules mapping of module name to module instance + */ + constructor(target, modules) { + invariant(!target.onmessage, 'target already has an onmessage handler'); + this.target = target; + this.modules = modules; + + this.target.onmessage = this.handleMessage.bind(this); + } + + handleMessage(event) { + var moduleName = event.data[0]; + var methodName = event.data[1]; + var args = event.data[2]; + + invariant( + this.modules[moduleName], + 'Module name %s not found', + moduleName + ); + invariant( + this.modules[moduleName][methodName].apply, + 'Method %s.%s not found', + moduleName, + methodName + ); + + try { + this.modules[moduleName][methodName].apply( + this.modules[moduleName], + args + ); + } catch (e) { + console.log(e.stack); + } + } + + destroy() { + this.target.onmessage = null; + } +} + +module.exports = RemoteModuleServer; diff --git a/src/browser/__tests__/ReactDOM-test.js b/src/browser/__tests__/ReactDOM-test.js index da0fadb6e20..47984d79bd7 100644 --- a/src/browser/__tests__/ReactDOM-test.js +++ b/src/browser/__tests__/ReactDOM-test.js @@ -23,7 +23,7 @@ var React = require('React'); var ReactDOM = require('ReactDOM'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var ReactTestUtils = require('ReactTestUtils'); describe('ReactDOM', function() { @@ -60,7 +60,7 @@ describe('ReactDOM', function() { var argDiv = ReactTestUtils.renderIntoDocument( ReactDOM.div(null, 'child') ); - var argNode = ReactMount.getNode(argDiv._rootNodeID); + var argNode = ReactDOMNodeMapping.getNode(argDiv._rootNodeID); expect(argNode.innerHTML).toBe('child'); }); @@ -68,7 +68,7 @@ describe('ReactDOM', function() { var conflictDiv = ReactTestUtils.renderIntoDocument( ReactDOM.div({children: 'fakechild'}, 'child') ); - var conflictNode = ReactMount.getNode(conflictDiv._rootNodeID); + var conflictNode = ReactDOMNodeMapping.getNode(conflictDiv._rootNodeID); expect(conflictNode.innerHTML).toBe('child'); }); @@ -110,7 +110,7 @@ describe('ReactDOM', function() { theBird:
} }); - var root = ReactMount.getNode(myDiv._rootNodeID); + var root = ReactDOMNodeMapping.getNode(myDiv._rootNodeID); var dog = root.childNodes[0]; expect(dog.className).toBe('bigdog'); }); diff --git a/src/browser/__tests__/ReactEventEmitter-test.js b/src/browser/__tests__/ReactEventEmitter-test.js index 2aeaecbedfb..5c169a9bb56 100644 --- a/src/browser/__tests__/ReactEventEmitter-test.js +++ b/src/browser/__tests__/ReactEventEmitter-test.js @@ -21,7 +21,7 @@ require('mock-modules') .dontMock('BrowserScroll') .dontMock('EventPluginHub') - .dontMock('ReactMount') + .dontMock('ReactDOMNodeMapping') .dontMock('ReactEventEmitter') .dontMock('ReactInstanceHandles') .dontMock('EventPluginHub') @@ -33,14 +33,14 @@ require('mock-modules') var keyOf = require('keyOf'); var mocks = require('mocks'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var idToNode = {}; -var getID = ReactMount.getID; +var getID = ReactDOMNodeMapping.getID; var setID = function(el, id) { - ReactMount.setID(el, id); + ReactDOMNodeMapping.setID(el, id); idToNode[id] = el; }; -var oldGetNode = ReactMount.getNode; +var oldGetNode = ReactDOMNodeMapping.getNode; var EventPluginHub; var ReactEventEmitter; @@ -82,6 +82,8 @@ setID(CHILD, '.0.0.0.0'); setID(PARENT, '.0.0.0'); setID(GRANDPARENT, '.0.0'); +var renderedHandle; + function registerSimpleTestHandler() { ReactEventEmitter.putListener(getID(CHILD), ON_CLICK_KEY, LISTENER); var listener = ReactEventEmitter.getListener(getID(CHILD), ON_CLICK_KEY); @@ -96,11 +98,11 @@ describe('ReactEventEmitter', function() { LISTENER.mockClear(); EventPluginHub = require('EventPluginHub'); TapEventPlugin = require('TapEventPlugin'); - ReactMount = require('ReactMount'); + ReactDOMNodeMapping = require('ReactDOMNodeMapping'); EventListener = require('EventListener'); ReactEventEmitter = require('ReactEventEmitter'); ReactTestUtils = require('ReactTestUtils'); - ReactMount.getNode = function(id) { + ReactDOMNodeMapping.getNode = function(id) { return idToNode[id]; }; idCallOrder = []; @@ -108,10 +110,16 @@ describe('ReactEventEmitter', function() { EventPluginHub.injection.injectEventPluginsByName({ TapEventPlugin: TapEventPlugin }); + + var ReactDOM = require('ReactDOM'); + var ReactDOMNodeHandle = require('ReactDOMNodeHandle'); + renderedHandle = ReactDOMNodeHandle.getHandleForReactIDTopLevel( + ReactTestUtils.renderIntoDocument(ReactDOM.div())._rootNodeID + ); }); afterEach(function() { - ReactMount.getNode = oldGetNode; + ReactDOMNodeMapping.getNode = oldGetNode; }); it('should store a listener correctly', function() { @@ -361,15 +369,15 @@ describe('ReactEventEmitter', function() { it('should listen to events only once', function() { spyOn(EventListener, 'listen'); - ReactEventEmitter.listenTo(ON_CLICK_KEY, document); - ReactEventEmitter.listenTo(ON_CLICK_KEY, document); + ReactEventEmitter.listenTo(ON_CLICK_KEY, renderedHandle); + ReactEventEmitter.listenTo(ON_CLICK_KEY, renderedHandle); expect(EventListener.listen.callCount).toBe(1); }); it('should work with event plugins without dependencies', function() { spyOn(EventListener, 'listen'); - ReactEventEmitter.listenTo(ON_CLICK_KEY, document); + ReactEventEmitter.listenTo(ON_CLICK_KEY, renderedHandle); expect(EventListener.listen.argsForCall[0][1]).toBe('click'); }); @@ -378,7 +386,7 @@ describe('ReactEventEmitter', function() { spyOn(EventListener, 'listen'); spyOn(EventListener, 'capture'); - ReactEventEmitter.listenTo(ON_CHANGE_KEY, document); + ReactEventEmitter.listenTo(ON_CHANGE_KEY, renderedHandle); var setEventListeners = []; var listenCalls = EventListener.listen.argsForCall; diff --git a/src/browser/eventPlugins/EnterLeaveEventPlugin.js b/src/browser/eventPlugins/EnterLeaveEventPlugin.js index 6dbfaa43995..7aaef18f6a8 100644 --- a/src/browser/eventPlugins/EnterLeaveEventPlugin.js +++ b/src/browser/eventPlugins/EnterLeaveEventPlugin.js @@ -23,11 +23,11 @@ var EventConstants = require('EventConstants'); var EventPropagators = require('EventPropagators'); var SyntheticMouseEvent = require('SyntheticMouseEvent'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var keyOf = require('keyOf'); var topLevelTypes = EventConstants.topLevelTypes; -var getFirstReactDOM = ReactMount.getFirstReactDOM; +var getFirstReactDOM = ReactDOMNodeMapping.getFirstReactDOM; var eventTypes = { mouseEnter: { @@ -111,8 +111,8 @@ var EnterLeaveEventPlugin = { return null; } - var fromID = from ? ReactMount.getID(from) : ''; - var toID = to ? ReactMount.getID(to) : ''; + var fromID = from ? ReactDOMNodeMapping.getID(from) : ''; + var toID = to ? ReactDOMNodeMapping.getID(to) : ''; var leave = SyntheticMouseEvent.getPooled( eventTypes.mouseLeave, diff --git a/src/browser/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js b/src/browser/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js index dd61e4b1ade..693a29a9272 100644 --- a/src/browser/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js +++ b/src/browser/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js @@ -24,7 +24,7 @@ var EnterLeaveEventPlugin; var EventConstants; var React; -var ReactMount; +var ReactDOMNodeMapping; var topLevelTypes; @@ -35,7 +35,7 @@ describe('EnterLeaveEventPlugin', function() { EnterLeaveEventPlugin = require('EnterLeaveEventPlugin'); EventConstants = require('EventConstants'); React = require('React'); - ReactMount = require('ReactMount'); + ReactDOMNodeMapping = require('ReactDOMNodeMapping'); topLevelTypes = EventConstants.topLevelTypes; }); @@ -56,7 +56,7 @@ describe('EnterLeaveEventPlugin', function() { var extracted = EnterLeaveEventPlugin.extractEvents( topLevelTypes.topMouseOver, div, - ReactMount.getID(div), + ReactDOMNodeMapping.getID(div), {target: div} ); expect(extracted.length).toBe(2); diff --git a/src/browser/ui/dom/getEventTarget.js b/src/browser/getEventTarget.js similarity index 88% rename from src/browser/ui/dom/getEventTarget.js rename to src/browser/getEventTarget.js index 37bc41b50f7..dd77d3ea1f4 100644 --- a/src/browser/ui/dom/getEventTarget.js +++ b/src/browser/getEventTarget.js @@ -19,6 +19,8 @@ "use strict"; +var ExecutionEnvironment = require('ExecutionEnvironment'); + /** * Gets the target node from a native browser event by accounting for * inconsistencies in browser DOM APIs. @@ -27,7 +29,7 @@ * @return {DOMEventTarget} Target node. */ function getEventTarget(nativeEvent) { - var target = nativeEvent.target || nativeEvent.srcElement || window; + var target = nativeEvent.target || nativeEvent.srcElement || ExecutionEnvironment.global; // Safari may fire events on text nodes (Node.TEXT_NODE is 3). // @see http://www.quirksmode.org/js/events_properties.html return target.nodeType === 3 ? target.parentNode : target; diff --git a/src/browser/server/__tests__/ReactServerRendering-test.js b/src/browser/server/__tests__/ReactServerRendering-test.js index 895ce51238e..c75cf1d25f4 100644 --- a/src/browser/server/__tests__/ReactServerRendering-test.js +++ b/src/browser/server/__tests__/ReactServerRendering-test.js @@ -24,7 +24,7 @@ require('mock-modules') .dontMock('ExecutionEnvironment') .dontMock('React') - .dontMock('ReactMount') + .dontMock('ReactDOMNodeMapping') .dontMock('ReactServerRendering') .dontMock('ReactTestUtils') .dontMock('ReactMarkupChecksum'); @@ -32,7 +32,7 @@ require('mock-modules') var mocks = require('mocks'); var React; -var ReactMount; +var ReactDOMNodeMapping; var ReactTestUtils; var ReactServerRendering; var ReactMarkupChecksum; @@ -44,7 +44,7 @@ describe('ReactServerRendering', function() { beforeEach(function() { require('mock-modules').dumpCache(); React = require('React'); - ReactMount = require('ReactMount'); + ReactDOMNodeMapping = require('ReactDOMNodeMapping'); ReactTestUtils = require('ReactTestUtils'); ExecutionEnvironment = require('ExecutionEnvironment'); ExecutionEnvironment.canUseDOM = false; @@ -211,7 +211,6 @@ describe('ReactServerRendering', function() { ); ExecutionEnvironment.canUseDOM = true; element.innerHTML = lastMarkup + ' __sentinel__'; - React.renderComponent(, element); expect(mountCount).toEqual(3); expect(element.innerHTML.indexOf('__sentinel__') > -1).toBe(true); diff --git a/src/browser/syntheticEvents/SyntheticUIEvent.js b/src/browser/syntheticEvents/SyntheticUIEvent.js index 02537f3af3b..5657c9b22d4 100644 --- a/src/browser/syntheticEvents/SyntheticUIEvent.js +++ b/src/browser/syntheticEvents/SyntheticUIEvent.js @@ -19,6 +19,7 @@ "use strict"; +var ExecutionEnvironment = require('ExecutionEnvironment'); var SyntheticEvent = require('SyntheticEvent'); var getEventTarget = require('getEventTarget'); @@ -44,7 +45,7 @@ var UIEventInterface = { if (doc) { return doc.defaultView || doc.parentWindow; } else { - return window; + return ExecutionEnvironment.global; } }, detail: function(event) { diff --git a/src/browser/ui/ReactBrowserComponentMixin.js b/src/browser/ui/ReactBrowserComponentMixin.js index efb81599918..b64eb2d4aa1 100644 --- a/src/browser/ui/ReactBrowserComponentMixin.js +++ b/src/browser/ui/ReactBrowserComponentMixin.js @@ -19,7 +19,8 @@ "use strict"; var ReactEmptyComponent = require('ReactEmptyComponent'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); + var invariant = require('invariant'); @@ -39,7 +40,7 @@ var ReactBrowserComponentMixin = { if (ReactEmptyComponent.isNullComponentID(this._rootNodeID)) { return null; } - return ReactMount.getNode(this._rootNodeID); + return ReactDOMNodeMapping.getNode(this._rootNodeID); } }; diff --git a/src/browser/ui/ReactComponentBrowserEnvironment.js b/src/browser/ui/ReactComponentBrowserEnvironment.js index a04bb790671..e0b2b0abbd1 100644 --- a/src/browser/ui/ReactComponentBrowserEnvironment.js +++ b/src/browser/ui/ReactComponentBrowserEnvironment.js @@ -22,15 +22,13 @@ var ReactDOMIDOperations = require('ReactDOMIDOperations'); var ReactMarkupChecksum = require('ReactMarkupChecksum'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var ReactPerf = require('ReactPerf'); var ReactReconcileTransaction = require('ReactReconcileTransaction'); -var getReactRootElementInContainer = require('getReactRootElementInContainer'); var invariant = require('invariant'); var setInnerHTML = require('setInnerHTML'); - var ELEMENT_NODE_TYPE = 1; var DOC_NODE_TYPE = 9; @@ -52,19 +50,21 @@ var ReactComponentBrowserEnvironment = { * @private */ unmountIDFromEnvironment: function(rootNodeID) { - ReactMount.purgeID(rootNodeID); + ReactDOMNodeMapping.purgeID(rootNodeID); }, /** * @param {string} markup Markup string to place into the DOM Element. - * @param {DOMElement} container DOM Element to insert markup into. + * @param {object} handle DOM node handle to insert markup into. * @param {boolean} shouldReuseMarkup Should reuse the existing markup in the * container if possible. */ mountImageIntoNode: ReactPerf.measure( 'ReactComponentBrowserEnvironment', 'mountImageIntoNode', - function(markup, container, shouldReuseMarkup) { + function(markup, handle, shouldReuseMarkup) { + var container = ReactDOMNodeMapping.resolveDOMNodeHandle(handle); + invariant( container && ( container.nodeType === ELEMENT_NODE_TYPE || @@ -76,7 +76,7 @@ var ReactComponentBrowserEnvironment = { if (shouldReuseMarkup) { if (ReactMarkupChecksum.canReuseMarkup( markup, - getReactRootElementInContainer(container))) { + ReactDOMNodeMapping.getReactRootElementInContainer(container))) { return; } else { invariant( diff --git a/src/browser/ui/ReactDOMComponent.js b/src/browser/ui/ReactDOMComponent.js index e8ecbbf2302..ca43ae0d19c 100644 --- a/src/browser/ui/ReactDOMComponent.js +++ b/src/browser/ui/ReactDOMComponent.js @@ -24,8 +24,8 @@ var DOMProperty = require('DOMProperty'); var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); var ReactComponent = require('ReactComponent'); +var ReactDOMNodeHandle = require('ReactDOMNodeHandle'); var ReactEventEmitter = require('ReactEventEmitter'); -var ReactMount = require('ReactMount'); var ReactMultiChild = require('ReactMultiChild'); var ReactPerf = require('ReactPerf'); @@ -36,7 +36,6 @@ var merge = require('merge'); var mixInto = require('mixInto'); var deleteListener = ReactEventEmitter.deleteListener; -var listenTo = ReactEventEmitter.listenTo; var registrationNameModules = ReactEventEmitter.registrationNameModules; // For quickly matching children type, to test if can be treated as content. @@ -44,8 +43,6 @@ var CONTENT_TYPES = {'string': true, 'number': true}; var STYLE = keyOf({style: null}); -var ELEMENT_NODE_TYPE = 1; - /** * @param {?object} props */ @@ -66,14 +63,9 @@ function assertValidProps(props) { } function putListener(id, registrationName, listener, transaction) { - var container = ReactMount.findReactContainerForID(id); - if (container) { - var doc = container.nodeType === ELEMENT_NODE_TYPE ? - container.ownerDocument : - container; - listenTo(registrationName, doc); - } + var handle = ReactDOMNodeHandle.getHandleForReactIDTopLevel(id); transaction.getPutListenerQueue().enqueuePutListener( + handle, id, registrationName, listener @@ -386,7 +378,7 @@ ReactDOMComponent.Mixin = { } } else if (nextHtml != null) { if (lastHtml !== nextHtml) { - ReactComponent.BackendIDOperations.updateInnerHTMLByID( + ReactComponent.BackendIDOperations.updateImageByID( this._rootNodeID, nextHtml ); diff --git a/src/browser/ui/ReactDOMIDOperations.js b/src/browser/ui/ReactDOMIDOperations.js index ea206cd065b..f275774ee43 100644 --- a/src/browser/ui/ReactDOMIDOperations.js +++ b/src/browser/ui/ReactDOMIDOperations.js @@ -24,7 +24,7 @@ var CSSPropertyOperations = require('CSSPropertyOperations'); var DOMChildrenOperations = require('DOMChildrenOperations'); var DOMPropertyOperations = require('DOMPropertyOperations'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var ReactPerf = require('ReactPerf'); var invariant = require('invariant'); @@ -38,7 +38,7 @@ var setInnerHTML = require('setInnerHTML'); */ var INVALID_PROPERTY_ERRORS = { dangerouslySetInnerHTML: - '`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.', + '`dangerouslySetInnerHTML` must be set using `updateImageByID()`.', style: '`style` must be set using `updateStylesByID()`.' }; @@ -61,7 +61,7 @@ var ReactDOMIDOperations = { 'ReactDOMIDOperations', 'updatePropertyByID', function(id, name, value) { - var node = ReactMount.getNode(id); + var node = ReactDOMNodeMapping.getNode(id); invariant( !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), 'updatePropertyByID(...): %s', @@ -91,7 +91,7 @@ var ReactDOMIDOperations = { 'ReactDOMIDOperations', 'deletePropertyByID', function(id, name, value) { - var node = ReactMount.getNode(id); + var node = ReactDOMNodeMapping.getNode(id); invariant( !INVALID_PROPERTY_ERRORS.hasOwnProperty(name), 'updatePropertyByID(...): %s', @@ -113,7 +113,7 @@ var ReactDOMIDOperations = { 'ReactDOMIDOperations', 'updateStylesByID', function(id, styles) { - var node = ReactMount.getNode(id); + var node = ReactDOMNodeMapping.getNode(id); CSSPropertyOperations.setValueForStyles(node, styles); } ), @@ -125,11 +125,11 @@ var ReactDOMIDOperations = { * @param {string} html An HTML string. * @internal */ - updateInnerHTMLByID: ReactPerf.measure( + updateImageByID: ReactPerf.measure( 'ReactDOMIDOperations', - 'updateInnerHTMLByID', + 'updateImageByID', function(id, html) { - var node = ReactMount.getNode(id); + var node = ReactDOMNodeMapping.getNode(id); setInnerHTML(node, html); } ), @@ -145,7 +145,7 @@ var ReactDOMIDOperations = { 'ReactDOMIDOperations', 'updateTextContentByID', function(id, content) { - var node = ReactMount.getNode(id); + var node = ReactDOMNodeMapping.getNode(id); DOMChildrenOperations.updateTextContent(node, content); } ), @@ -162,7 +162,7 @@ var ReactDOMIDOperations = { 'ReactDOMIDOperations', 'dangerouslyReplaceNodeWithMarkupByID', function(id, markup) { - var node = ReactMount.getNode(id); + var node = ReactDOMNodeMapping.getNode(id); DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup); } ), @@ -179,7 +179,7 @@ var ReactDOMIDOperations = { 'dangerouslyProcessChildrenUpdates', function(updates, markup) { for (var i = 0; i < updates.length; i++) { - updates[i].parentNode = ReactMount.getNode(updates[i].parentID); + updates[i].parentNode = ReactDOMNodeMapping.getNode(updates[i].parentID); } DOMChildrenOperations.processUpdates(updates, markup); } diff --git a/src/browser/ui/ReactDOMNodeMapping.js b/src/browser/ui/ReactDOMNodeMapping.js new file mode 100644 index 00000000000..168f48e4c60 --- /dev/null +++ b/src/browser/ui/ReactDOMNodeMapping.js @@ -0,0 +1,566 @@ +/** + * 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 ReactDOMNodeMapping + */ + +"use strict"; + +var DOMProperty = require('DOMProperty'); +var ExecutionEnvironment = require('ExecutionEnvironment'); +var ReactDOMNodeHandle = require('ReactDOMNodeHandle'); + +var ReactDOMNodeHandleMapping = require('ReactDOMNodeHandleMapping'); +var ReactDOMNodeHandleTypes = require('ReactDOMNodeHandleTypes'); +var ReactEventEmitter = require('ReactEventEmitter'); +var ReactInstanceHandles = require('ReactInstanceHandles'); + +var containsNode = require('containsNode'); +var invariant = require('invariant'); + +var SEPARATOR = ReactInstanceHandles.SEPARATOR; + +var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME; +var nodeCache = {}; + +var ELEMENT_NODE_TYPE = 1; +var DOC_NODE_TYPE = 9; + +var idSeed = 0; +// Like getElementById(), except supports elements not in the document. +var reactContainers = {}; + +/** Mapping from reactRootID to `container` nodes. */ +var containersByReactRootID = {}; + +function registerContainerDOMNode(domNode) { + reactContainers[domNode.id] = domNode; +} + +// Used to store breadth-first search state in findComponentRoot. +var findComponentRootReusableArray = []; + +if (__DEV__) { + /** __DEV__-only mapping from reactRootID to root elements. */ + var rootElementsByReactRootID = {}; +} + +function getReactRootID(container) { + return getID(container.firstChild); +} + +/** + * Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form + * element can return its control whose name or ID equals ATTR_NAME. All + * DOM nodes support `getAttributeNode` but this can also get called on + * other objects so just return '' if we're given something other than a + * DOM node (such as window). + * + * @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node. + * @return {string} ID of the supplied `domNode`. + */ +function getID(node) { + var id = internalGetID(node); + if (id) { + if (nodeCache.hasOwnProperty(id)) { + var cached = nodeCache[id]; + if (cached !== node) { + invariant( + !isValid(cached, id), + 'ReactDOMNodeMapping: Two valid but unequal nodes with the same `%s`: %s', + ATTR_NAME, id + ); + + nodeCache[id] = node; + } + } else { + nodeCache[id] = node; + } + } + + return id; +} + +function internalGetID(node) { + // If node is something like a window, document, or text node, none of + // which support attributes or a .getAttribute method, gracefully return + // the empty string, as if the attribute were missing. + return node && node.getAttribute && node.getAttribute(ATTR_NAME) || ''; +} + +/** + * Sets the React-specific ID of the given node. + * + * @param {DOMElement} node The DOM node whose ID will be set. + * @param {string} id The value of the ID attribute. + */ +function setID(node, id) { + var oldID = internalGetID(node); + if (oldID !== id) { + delete nodeCache[oldID]; + } + node.setAttribute(ATTR_NAME, id); + nodeCache[id] = node; +} + +/** + * Finds the node with the supplied React-generated DOM ID. + * + * @param {string} id A React-generated DOM ID. + * @return {DOMElement} DOM node with the suppled `id`. + * @internal + */ +function getNode(id) { + if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) { + nodeCache[id] = ReactDOMNodeMapping.findReactNodeByID(id); + } + return nodeCache[id]; +} + +/** + * A node is "valid" if it is contained by a currently mounted container. + * + * This means that the node does not have to be contained by a document in + * order to be considered valid. + * + * @param {?DOMElement} node The candidate DOM node. + * @param {string} id The expected ID of the node. + * @return {boolean} Whether the node is contained by a mounted container. + */ +function isValid(node, id) { + if (node) { + invariant( + internalGetID(node) === id, + 'ReactDOMNodeMapping: Unexpected modification of `%s`', + ATTR_NAME + ); + + var container = ReactDOMNodeMapping.findReactContainerForID(id); + if (container && containsNode(container, node)) { + return true; + } + } + + return false; +} + +/** + * Causes the cache to forget about one React-specific ID. + * + * @param {string} id The ID to forget. + */ +function purgeID(id) { + delete nodeCache[id]; +} + +var deepestNodeSoFar = null; +function findDeepestCachedAncestorImpl(ancestorID) { + var ancestor = nodeCache[ancestorID]; + if (ancestor && isValid(ancestor, ancestorID)) { + deepestNodeSoFar = ancestor; + } else { + // This node isn't populated in the cache, so presumably none of its + // descendants are. Break out of the loop. + return false; + } +} + +/** + * Return the deepest cached node whose ID is a prefix of `targetID`. + */ +function findDeepestCachedAncestor(targetID) { + deepestNodeSoFar = null; + ReactInstanceHandles.traverseAncestors( + targetID, + findDeepestCachedAncestorImpl + ); + + var foundNode = deepestNodeSoFar; + deepestNodeSoFar = null; + return foundNode; +} + +/** + * Mounting is the process of initializing a React component by creatings its + * representative DOM elements and inserting them into a supplied `container`. + * Any prior content inside `container` is destroyed in the process. + * + * ReactMount.renderComponent( + * component, + * document.getElementById('container') + * ); + * + *
<-- Supplied `container`. + *
<-- Rendered reactRoot of React + * // ... component. + *
+ *
+ * + * Inside of `container`, the first element rendered is the "reactRoot". + */ +var ReactDOMNodeMapping = { + /** Time spent generating markup. */ + totalInstantiationTime: 0, + + /** Time spent inserting markup into the DOM. */ + totalInjectionTime: 0, + + /** Whether support for touch events should be initialized. */ + useTouchEvents: false, + + getReactRootElementInContainer: function(container) { + if (!container) { + return null; + } + + if (container.nodeType === DOC_NODE_TYPE) { + return container.documentElement; + } else { + return container.firstChild; + } + }, + + /** + * Register a component into the instance map and starts scroll value + * monitoring + * @param {ReactComponent} nextComponent component instance to render + * @param {DOMElement} container container to render into + * @return {string} reactRoot ID prefix + */ + registerComponentInContainer: function(reactRootID, containerHandle) { + var container = ReactDOMNodeMapping.resolveDOMNodeHandle(containerHandle); + + invariant( + container && ( + container.nodeType === ELEMENT_NODE_TYPE || + container.nodeType === DOC_NODE_TYPE + ), + '_registerComponent(...): Target container is not a DOM element.' + ); + + ReactEventEmitter.ensureScrollValueMonitoring(); + + containersByReactRootID[reactRootID] = container; + return reactRootID; + }, + + /** + * Finds the container DOM element that contains React component to which the + * supplied DOM `id` belongs. + * + * @param {string} id The ID of an element rendered by a React component. + * @return {?DOMElement} DOM element that contains the `id`. + */ + findReactContainerForID: function(id) { + var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id); + var container = containersByReactRootID[reactRootID]; + + if (__DEV__) { + var rootElement = rootElementsByReactRootID[reactRootID]; + if (rootElement && rootElement.parentNode !== container) { + invariant( + // Call internalGetID here because getID calls isValid which calls + // findReactContainerForID (this function). + internalGetID(rootElement) === reactRootID, + 'ReactDOMNodeMapping: Root element ID differed from reactRootID.' + ); + + var containerChild = container.firstChild; + if (containerChild && + reactRootID === internalGetID(containerChild)) { + // If the container has a new child with the same ID as the old + // root element, then rootElementsByReactRootID[reactRootID] is + // just stale and needs to be updated. The case that deserves a + // warning is when the container is empty. + rootElementsByReactRootID[reactRootID] = containerChild; + } else { + console.warn( + 'ReactDOMNodeMapping: Root element has been removed from its original ' + + 'container. New container:', rootElement.parentNode + ); + } + } + } + + return container; + }, + + /** + * Finds an element rendered by React with the supplied ID. + * + * @param {string} id ID of a DOM node in the React component. + * @return {DOMElement} Root DOM node of the React component. + */ + findReactNodeByID: function(id) { + var reactRoot = ReactDOMNodeMapping.findReactContainerForID(id); + return ReactDOMNodeMapping.findComponentRoot(reactRoot, id); + }, + + /** + * True if the supplied `node` is rendered by React. + * + * @param {*} node DOM Element to check. + * @return {boolean} True if the DOM Element appears to be rendered by React. + * @internal + */ + isRenderedByReact: function(node) { + if (node.nodeType !== 1) { + // Not a DOMElement, therefore not a React component + return false; + } + var id = ReactDOMNodeMapping.getID(node); + return id ? id.charAt(0) === SEPARATOR : false; + }, + + /** + * Traverses up the ancestors of the supplied node to find a node that is a + * DOM representation of a React component. + * + * @param {*} node + * @return {?DOMEventTarget} + * @internal + */ + getFirstReactDOM: function(node) { + var current = node; + while (current && current.parentNode !== current) { + if (ReactDOMNodeMapping.isRenderedByReact(current)) { + return current; + } + current = current.parentNode; + } + return null; + }, + + /** + * Finds a node with the supplied `targetID` inside of the supplied + * `ancestorNode`. Exploits the ID naming scheme to perform the search + * quickly. + * + * @param {DOMEventTarget} ancestorNode Search from this root. + * @pararm {string} targetID ID of the DOM representation of the component. + * @return {DOMEventTarget} DOM node with the supplied `targetID`. + * @internal + */ + findComponentRoot: function(ancestorNode, targetID) { + var firstChildren = findComponentRootReusableArray; + var childIndex = 0; + + var deepestAncestor = findDeepestCachedAncestor(targetID) || ancestorNode; + + firstChildren[0] = deepestAncestor.firstChild; + firstChildren.length = 1; + + while (childIndex < firstChildren.length) { + var child = firstChildren[childIndex++]; + var targetChild; + + while (child) { + var childID = ReactDOMNodeMapping.getID(child); + if (childID) { + // Even if we find the node we're looking for, we finish looping + // through its siblings to ensure they're cached so that we don't have + // to revisit this node again. Otherwise, we make n^2 calls to getID + // when visiting the many children of a single node in order. + + if (targetID === childID) { + targetChild = child; + } else if (ReactInstanceHandles.isAncestorIDOf(childID, targetID)) { + // If we find a child whose ID is an ancestor of the given ID, + // then we can be sure that we only want to search the subtree + // rooted at this child, so we can throw out the rest of the + // search state. + firstChildren.length = childIndex = 0; + firstChildren.push(child.firstChild); + } + + } else { + // If this child had no ID, then there's a chance that it was + // injected automatically by the browser, as when a `` + // element sprouts an extra `` child as a side effect of + // `.innerHTML` parsing. Optimistically continue down this + // branch, but not before examining the other siblings. + firstChildren.push(child.firstChild); + } + + child = child.nextSibling; + } + + if (targetChild) { + // Emptying firstChildren/findComponentRootReusableArray is + // not necessary for correctness, but it helps the GC reclaim + // any nodes that were left at the end of the search. + firstChildren.length = 0; + + return targetChild; + } + } + + firstChildren.length = 0; + + invariant( + false, + 'findComponentRoot(..., %s): Unable to find element. This probably ' + + 'means the DOM was unexpectedly mutated (e.g., by the browser), ' + + 'usually due to forgetting a when using tables or nesting

' + + 'or tags. Try inspecting the child nodes of the element with React ' + + 'ID `%s`.', + targetID, + ReactDOMNodeMapping.getID(ancestorNode) + ); + }, + + // Called remotely + unmountComponentAtHandle: function(containerHandle) { + ReactDOMNodeMapping.unmountComponentAtNode( + ReactDOMNodeMapping.resolveDOMNodeHandle(containerHandle) + ); + }, + + /** + * Unmounts and destroys the React component rendered in the `container`. + * + * @param {DOMElement} container DOM element containing a React component. + * @return {boolean} True if a component was found in and unmounted from + * `container` + */ + unmountComponentAtNode: function(container) { + invariant( + container && ( + container.nodeType === ELEMENT_NODE_TYPE || + container.nodeType === DOC_NODE_TYPE + ), + 'unmountComponentAtNode(...): Target container is not a DOM element.' + ); + + var containerHandle = ReactDOMNodeMapping.getHandleForContainer(container); + delete reactContainers[containerHandle.id]; + + var reactRootID = ReactDOMNodeHandleMapping.unmountComponentAtNode(containerHandle); + if (__DEV__) { + delete rootElementsByReactRootID[reactRootID]; + } + if (reactRootID) { + delete containersByReactRootID[reactRootID] + ReactDOMNodeMapping._unmountNode(container); + } + }, + + /** + * Unmounts a component and removes it from the DOM. + * + * @param {ReactComponent} instance React component instance. + * @param {DOMElement} container DOM element to unmount from. + * @final + * @internal + * @see {ReactDOMNodeMapping.unmountComponentAtNode} + */ + _unmountNode: function(container) { + if (container.nodeType === DOC_NODE_TYPE) { + container = container.documentElement; + } + + // http://jsperf.com/emptying-a-node + while (container.lastChild) { + container.removeChild(container.lastChild); + } + }, + + getInstanceFromContainer: function(container) { + invariant( + container && ( + container.nodeType === ELEMENT_NODE_TYPE || + container.nodeType === DOC_NODE_TYPE + ), + 'getInstanceFromContainer(...): Target container is not a DOM element.' + ); + + return ReactDOMNodeHandleMapping.getInstanceFromContainer( + ReactDOMNodeMapping.getHandleForContainer(container) + ); + }, + + recordRootElementForTransplantWarning: function(container) { + if (__DEV__) { + // Record the root element in case it later gets transplanted. + rootElementsByReactRootID[getReactRootID(container)] = + ReactDOMNodeMapping.getReactRootElementInContainer(container); + } + }, + + resolveDOMNodeHandle: function(handle) { + invariant( + ExecutionEnvironment.canUseDOM, + 'Cannot resolveHandle() in a worker!' + ); + + + if (handle.type === ReactDOMNodeHandleTypes.REACT_ID_TOP_LEVEL) { + var container = ReactDOMNodeMapping.findReactContainerForID(handle.reactID); + if (container) { + return container.nodeType === ELEMENT_NODE_TYPE ? + container.ownerDocument : + container; + } + return null; + } else if (handle.type === ReactDOMNodeHandleTypes.REACT_ID) { + return ReactDOMNodeMapping.getNode(handle.reactID); + } else { + invariant( + handle.type === ReactDOMNodeHandleTypes.CONTAINER, + 'Invalid handle type: %s', + handle.type + ); + var container = reactContainers[handle.id]; + if (!container) { + container = reactContainers[handle.id] = document.getElementById( + handle.id + ); + } + return container; + } + }, + + getHandleForContainer: function(domNode) { + if (!domNode.id) { + domNode.id = '.rC_' + (idSeed++); + } + + registerContainerDOMNode(domNode); + + return ReactDOMNodeHandle.getHandleForContainerID(domNode.id); + }, + + // Called remotely; see ReactDOMNodeMappingRemote + registerContainerHandle: function(handle) { + var domNode = ReactDOMNodeMapping.resolveDOMNodeHandle(handle); + registerContainerDOMNode(domNode); + }, + + /** + * React ID utilities. + */ + + getReactRootID: getReactRootID, + + getID: getID, + + setID: setID, + + getNode: getNode, + + purgeID: purgeID +}; + +module.exports = ReactDOMNodeMapping; diff --git a/src/browser/ui/ReactDefaultInjection.js b/src/browser/ui/ReactDefaultInjection.js index a981a93f9b9..4734943d5af 100644 --- a/src/browser/ui/ReactDefaultInjection.js +++ b/src/browser/ui/ReactDefaultInjection.js @@ -18,6 +18,19 @@ "use strict"; +var DOMProperty = require('DOMProperty'); +var EventPluginHub = require('EventPluginHub'); +var ReactComponent = require('ReactComponent'); +var ReactCompositeComponent = require('ReactCompositeComponent'); +var ReactDOM = require('ReactDOM'); +var ReactEmptyComponent = require('ReactEmptyComponent'); +var ReactEventEmitter = require('ReactEventEmitter'); +var ReactPerf = require('ReactPerf'); +var ReactRootIndex = require('ReactRootIndex'); +var ReactUpdates = require('ReactUpdates'); + +var ExecutionEnvironment = require('ExecutionEnvironment'); + var BeforeInputEventPlugin = require('BeforeInputEventPlugin'); var ChangeEventPlugin = require('ChangeEventPlugin'); var ClientReactRootIndex = require('ClientReactRootIndex'); @@ -31,6 +44,7 @@ var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); var ReactComponentBrowserEnvironment = require('ReactComponentBrowserEnvironment'); var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); +var ReactEventListener = require('ReactEventListener'); var ReactDOM = require('ReactDOM'); var ReactDOMButton = require('ReactDOMButton'); var ReactDOMForm = require('ReactDOMForm'); @@ -39,10 +53,8 @@ var ReactDOMInput = require('ReactDOMInput'); var ReactDOMOption = require('ReactDOMOption'); var ReactDOMSelect = require('ReactDOMSelect'); var ReactDOMTextarea = require('ReactDOMTextarea'); -var ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); -var ReactInjection = require('ReactInjection'); var ReactInstanceHandles = require('ReactInstanceHandles'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var SelectEventPlugin = require('SelectEventPlugin'); var ServerReactRootIndex = require('ServerReactRootIndex'); var SimpleEventPlugin = require('SimpleEventPlugin'); @@ -51,22 +63,22 @@ var SVGDOMPropertyConfig = require('SVGDOMPropertyConfig'); var createFullPageComponent = require('createFullPageComponent'); function inject() { - ReactInjection.EventEmitter.injectTopLevelCallbackCreator( - ReactEventTopLevelCallback + ReactEventEmitter.injection.injectReactEventListener( + ReactEventListener ); /** * Inject modules for resolving DOM hierarchy and plugin ordering. */ - ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder); - ReactInjection.EventPluginHub.injectInstanceHandle(ReactInstanceHandles); - ReactInjection.EventPluginHub.injectMount(ReactMount); + EventPluginHub.injection.injectEventPluginOrder(DefaultEventPluginOrder); + EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles); + EventPluginHub.injection.injectMount(ReactDOMNodeMapping); /** * Some important event plugins included by default (without having to require * them). */ - ReactInjection.EventPluginHub.injectEventPluginsByName({ + EventPluginHub.injection.injectEventPluginsByName({ SimpleEventPlugin: SimpleEventPlugin, EnterLeaveEventPlugin: EnterLeaveEventPlugin, ChangeEventPlugin: ChangeEventPlugin, @@ -76,7 +88,7 @@ function inject() { BeforeInputEventPlugin: BeforeInputEventPlugin }); - ReactInjection.DOM.injectComponentClasses({ + ReactDOM.injection.injectComponentClasses({ button: ReactDOMButton, form: ReactDOMForm, img: ReactDOMImg, @@ -93,27 +105,27 @@ function inject() { // This needs to happen after createFullPageComponent() otherwise the mixin // gets double injected. - ReactInjection.CompositeComponent.injectMixin(ReactBrowserComponentMixin); + ReactCompositeComponent.injection.injectMixin(ReactBrowserComponentMixin); - ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig); - ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig); + DOMProperty.injection.injectDOMPropertyConfig(HTMLDOMPropertyConfig); + DOMProperty.injection.injectDOMPropertyConfig(SVGDOMPropertyConfig); - ReactInjection.EmptyComponent.injectEmptyComponent(ReactDOM.script); + ReactEmptyComponent.injection.injectEmptyComponent(ReactDOM.script); - ReactInjection.Updates.injectReconcileTransaction( + ReactUpdates.injection.injectReconcileTransaction( ReactComponentBrowserEnvironment.ReactReconcileTransaction ); - ReactInjection.Updates.injectBatchingStrategy( + ReactUpdates.injection.injectBatchingStrategy( ReactDefaultBatchingStrategy ); - ReactInjection.RootIndex.injectCreateReactRootIndex( + ReactRootIndex.injection.injectCreateReactRootIndex( ExecutionEnvironment.canUseDOM ? ClientReactRootIndex.createReactRootIndex : ServerReactRootIndex.createReactRootIndex ); - ReactInjection.Component.injectEnvironment(ReactComponentBrowserEnvironment); + ReactComponent.injection.injectEnvironment(ReactComponentBrowserEnvironment); if (__DEV__) { var url = (ExecutionEnvironment.canUseDOM && window.location.href) || ''; diff --git a/src/browser/ui/ReactEventListener.js b/src/browser/ui/ReactEventListener.js new file mode 100644 index 00000000000..68faf573742 --- /dev/null +++ b/src/browser/ui/ReactEventListener.js @@ -0,0 +1,183 @@ +/** + * 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 ReactEventListener + * @typechecks static-only + */ + +"use strict"; + +var EventListener = require('EventListener'); +var PooledClass = require('PooledClass'); +var ReactInstanceHandles = require('ReactInstanceHandles'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); +var ReactUpdates = require('ReactUpdates'); + +var getEventTarget = require('getEventTarget'); +var getUnboundedScrollPosition = require('getUnboundedScrollPosition'); +var invariant = require('invariant'); +var mixInto = require('mixInto'); + +/** + * Finds the parent React component of `node`. + * + * @param {*} node + * @return {?DOMEventTarget} Parent container, or `null` if the specified node + * is not nested. + */ +function findParent(node) { + // TODO: It may be a good idea to cache this to prevent unnecessary DOM + // traversal, but caching is difficult to do correctly without using a + // mutation observer to listen for all DOM changes. + var nodeID = ReactDOMNodeMapping.getID(node); + var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID); + var container = ReactDOMNodeMapping.findReactContainerForID(rootID); + var parent = ReactDOMNodeMapping.getFirstReactDOM(container); + return parent; +} + +// Used to store ancestor hierarchy in top level callback +function TopLevelCallbackBookKeeping(topLevelType, nativeEvent) { + this.topLevelType = topLevelType; + this.nativeEvent = nativeEvent; + this.ancestors = []; +} +mixInto(TopLevelCallbackBookKeeping, { + destructor: function() { + this.topLevelType = null; + this.nativeEvent = null; + this.ancestors.length = 0; + } +}); +PooledClass.addPoolingTo( + TopLevelCallbackBookKeeping, + PooledClass.twoArgumentPooler +); + +function handleTopLevelImpl(bookKeeping) { + var topLevelTarget = ReactDOMNodeMapping.getFirstReactDOM( + getEventTarget(bookKeeping.nativeEvent) + ) || window; + + // Loop through the hierarchy, in case there's any nested components. + // It's important that we build the array of ancestors before calling any + // event handlers, because event handlers can modify the DOM, leading to + // inconsistencies with ReactDOMNodeMapping's node cache. See #1105. + var ancestor = topLevelTarget; + while (ancestor) { + bookKeeping.ancestors.push(ancestor); + ancestor = findParent(ancestor); + } + + for (var i = 0, l = bookKeeping.ancestors.length; i < l; i++) { + topLevelTarget = bookKeeping.ancestors[i]; + var topLevelTargetID = ReactDOMNodeMapping.getID(topLevelTarget) || ''; + ReactEventListener._handleTopLevel( + bookKeeping.topLevelType, + topLevelTarget, + topLevelTargetID, + bookKeeping.nativeEvent + ); + } +} + +function scrollValueMonitor(cb) { + var scrollPosition = getUnboundedScrollPosition(window); + cb(scrollPosition); +} + +var ReactEventListener = { + _enabled: true, + _handleTopLevel: null, + + setHandleTopLevel: function(handleTopLevel) { + ReactEventListener._handleTopLevel = handleTopLevel; + }, + + setEnabled: function(enabled) { + ReactEventListener._enabled = !!enabled; + }, + + isEnabled: function() { + return ReactEventListener._enabled; + }, + + + /** + * Traps top-level events by using event bubbling. + * + * @param {string} topLevelType Record from `EventConstants`. + * @param {string} handlerBaseName Event name (e.g. "click"). + * @param {object} handle Element on which to attach listener. + * @internal + */ + trapBubbledEvent: function(topLevelType, handlerBaseName, handle) { + var element = ReactDOMNodeMapping.resolveDOMNodeHandle(handle); + if (!element) { + return; + } + EventListener.listen( + element, + handlerBaseName, + ReactEventListener.dispatchEvent.bind(null, topLevelType) + ); + }, + + /** + * Traps a top-level event by using event capturing. + * + * @param {string} topLevelType Record from `EventConstants`. + * @param {string} handlerBaseName Event name (e.g. "click"). + * @param {object} handle Element on which to attach listener. + * @internal + */ + trapCapturedEvent: function(topLevelType, handlerBaseName, handle) { + var element = ReactDOMNodeMapping.resolveDOMNodeHandle(handle); + if (!element) { + return; + } + EventListener.capture( + element, + handlerBaseName, + ReactEventListener.dispatchEvent.bind(null, topLevelType) + ); + }, + + monitorScrollValue: function(refresh) { + var callback = scrollValueMonitor.bind(null, refresh); + EventListener.listen(window, 'scroll', callback); + EventListener.listen(window, 'resize', callback); + }, + + dispatchEvent: function(topLevelType, nativeEvent) { + if (!ReactEventListener._enabled) { + return; + } + + var bookKeeping = TopLevelCallbackBookKeeping.getPooled( + topLevelType, + nativeEvent + ); + try { + // Event queue being processed in the same cycle allows + // `preventDefault`. + ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); + } finally { + TopLevelCallbackBookKeeping.release(bookKeeping); + } + } +}; + +module.exports = ReactEventListener; diff --git a/src/browser/ui/ReactEventTopLevelCallback.js b/src/browser/ui/ReactEventTopLevelCallback.js deleted file mode 100644 index aef2eef09fa..00000000000 --- a/src/browser/ui/ReactEventTopLevelCallback.js +++ /dev/null @@ -1,160 +0,0 @@ -/** - * 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 ReactEventTopLevelCallback - * @typechecks static-only - */ - -"use strict"; - -var PooledClass = require('PooledClass'); -var ReactEventEmitter = require('ReactEventEmitter'); -var ReactInstanceHandles = require('ReactInstanceHandles'); -var ReactMount = require('ReactMount'); -var ReactUpdates = require('ReactUpdates'); - -var getEventTarget = require('getEventTarget'); -var mixInto = require('mixInto'); - -/** - * @type {boolean} - * @private - */ -var _topLevelListenersEnabled = true; - -/** - * Finds the parent React component of `node`. - * - * @param {*} node - * @return {?DOMEventTarget} Parent container, or `null` if the specified node - * is not nested. - */ -function findParent(node) { - // TODO: It may be a good idea to cache this to prevent unnecessary DOM - // traversal, but caching is difficult to do correctly without using a - // mutation observer to listen for all DOM changes. - var nodeID = ReactMount.getID(node); - var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID); - var container = ReactMount.findReactContainerForID(rootID); - var parent = ReactMount.getFirstReactDOM(container); - return parent; -} - -/** - * Calls ReactEventEmitter.handleTopLevel for each node stored in bookKeeping's - * ancestor list. Separated from createTopLevelCallback to avoid try/finally - * deoptimization. - * - * @param {TopLevelCallbackBookKeeping} bookKeeping - */ -function handleTopLevelImpl(bookKeeping) { - var topLevelTarget = ReactMount.getFirstReactDOM( - getEventTarget(bookKeeping.nativeEvent) - ) || window; - - // Loop through the hierarchy, in case there's any nested components. - // It's important that we build the array of ancestors before calling any - // event handlers, because event handlers can modify the DOM, leading to - // inconsistencies with ReactMount's node cache. See #1105. - var ancestor = topLevelTarget; - while (ancestor) { - bookKeeping.ancestors.push(ancestor); - ancestor = findParent(ancestor); - } - - for (var i = 0, l = bookKeeping.ancestors.length; i < l; i++) { - topLevelTarget = bookKeeping.ancestors[i]; - var topLevelTargetID = ReactMount.getID(topLevelTarget) || ''; - ReactEventEmitter.handleTopLevel( - bookKeeping.topLevelType, - topLevelTarget, - topLevelTargetID, - bookKeeping.nativeEvent - ); - } -} - -// Used to store ancestor hierarchy in top level callback -function TopLevelCallbackBookKeeping(topLevelType, nativeEvent) { - this.topLevelType = topLevelType; - this.nativeEvent = nativeEvent; - this.ancestors = []; -} -mixInto(TopLevelCallbackBookKeeping, { - destructor: function() { - this.topLevelType = null; - this.nativeEvent = null; - this.ancestors.length = 0; - } -}); -PooledClass.addPoolingTo( - TopLevelCallbackBookKeeping, - PooledClass.twoArgumentPooler -); - -/** - * Top-level callback creator used to implement event handling using delegation. - * This is used via dependency injection. - */ -var ReactEventTopLevelCallback = { - - /** - * Sets whether or not any created callbacks should be enabled. - * - * @param {boolean} enabled True if callbacks should be enabled. - */ - setEnabled: function(enabled) { - _topLevelListenersEnabled = !!enabled; - }, - - /** - * @return {boolean} True if callbacks are enabled. - */ - isEnabled: function() { - return _topLevelListenersEnabled; - }, - - /** - * Creates a callback for the supplied `topLevelType` that could be added as - * a listener to the document. The callback computes a `topLevelTarget` which - * should be the root node of a mounted React component where the listener - * is attached. - * - * @param {string} topLevelType Record from `EventConstants`. - * @return {function} Callback for handling top-level events. - */ - createTopLevelCallback: function(topLevelType) { - return function(nativeEvent) { - if (!_topLevelListenersEnabled) { - return; - } - - var bookKeeping = TopLevelCallbackBookKeeping.getPooled( - topLevelType, - nativeEvent - ); - try { - // Event queue being processed in the same cycle allows - // `preventDefault`. - ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); - } finally { - TopLevelCallbackBookKeeping.release(bookKeeping); - } - }; - } - -}; - -module.exports = ReactEventTopLevelCallback; diff --git a/src/browser/ui/ReactInjection.js b/src/browser/ui/ReactInjection.js deleted file mode 100644 index c51a2128003..00000000000 --- a/src/browser/ui/ReactInjection.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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 ReactInjection - */ - -"use strict"; - -var DOMProperty = require('DOMProperty'); -var EventPluginHub = require('EventPluginHub'); -var ReactComponent = require('ReactComponent'); -var ReactCompositeComponent = require('ReactCompositeComponent'); -var ReactDOM = require('ReactDOM'); -var ReactEmptyComponent = require('ReactEmptyComponent'); -var ReactEventEmitter = require('ReactEventEmitter'); -var ReactPerf = require('ReactPerf'); -var ReactRootIndex = require('ReactRootIndex'); -var ReactUpdates = require('ReactUpdates'); - -var ReactInjection = { - Component: ReactComponent.injection, - CompositeComponent: ReactCompositeComponent.injection, - DOMProperty: DOMProperty.injection, - EmptyComponent: ReactEmptyComponent.injection, - EventPluginHub: EventPluginHub.injection, - DOM: ReactDOM.injection, - EventEmitter: ReactEventEmitter.injection, - Perf: ReactPerf.injection, - RootIndex: ReactRootIndex.injection, - Updates: ReactUpdates.injection -}; - -module.exports = ReactInjection; diff --git a/src/browser/ui/ReactMount.js b/src/browser/ui/ReactMount.js index e898b589948..22ea3e0a441 100644 --- a/src/browser/ui/ReactMount.js +++ b/src/browser/ui/ReactMount.js @@ -18,271 +18,47 @@ "use strict"; -var DOMProperty = require('DOMProperty'); var ReactCurrentOwner = require('ReactCurrentOwner'); -var ReactEventEmitter = require('ReactEventEmitter'); -var ReactInstanceHandles = require('ReactInstanceHandles'); +var ReactDOMNodeHandleMapping = require('ReactDOMNodeHandleMapping'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var ReactPerf = require('ReactPerf'); -var containsNode = require('containsNode'); -var getReactRootElementInContainer = require('getReactRootElementInContainer'); var instantiateReactComponent = require('instantiateReactComponent'); var invariant = require('invariant'); var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); var warning = require('warning'); -var SEPARATOR = ReactInstanceHandles.SEPARATOR; - -var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME; -var nodeCache = {}; - -var ELEMENT_NODE_TYPE = 1; -var DOC_NODE_TYPE = 9; - -/** Mapping from reactRootID to React component instance. */ -var instancesByReactRootID = {}; - -/** Mapping from reactRootID to `container` nodes. */ -var containersByReactRootID = {}; - -if (__DEV__) { - /** __DEV__-only mapping from reactRootID to root elements. */ - var rootElementsByReactRootID = {}; -} - -// Used to store breadth-first search state in findComponentRoot. -var findComponentRootReusableArray = []; - -/** - * @param {DOMElement} container DOM element that may contain a React component. - * @return {?string} A "reactRoot" ID, if a React component is rendered. - */ -function getReactRootID(container) { - var rootElement = getReactRootElementInContainer(container); - return rootElement && ReactMount.getID(rootElement); -} - -/** - * Accessing node[ATTR_NAME] or calling getAttribute(ATTR_NAME) on a form - * element can return its control whose name or ID equals ATTR_NAME. All - * DOM nodes support `getAttributeNode` but this can also get called on - * other objects so just return '' if we're given something other than a - * DOM node (such as window). - * - * @param {?DOMElement|DOMWindow|DOMDocument|DOMTextNode} node DOM node. - * @return {string} ID of the supplied `domNode`. - */ -function getID(node) { - var id = internalGetID(node); - if (id) { - if (nodeCache.hasOwnProperty(id)) { - var cached = nodeCache[id]; - if (cached !== node) { - invariant( - !isValid(cached, id), - 'ReactMount: Two valid but unequal nodes with the same `%s`: %s', - ATTR_NAME, id - ); - - nodeCache[id] = node; - } - } else { - nodeCache[id] = node; - } - } - - return id; -} - -function internalGetID(node) { - // If node is something like a window, document, or text node, none of - // which support attributes or a .getAttribute method, gracefully return - // the empty string, as if the attribute were missing. - return node && node.getAttribute && node.getAttribute(ATTR_NAME) || ''; -} - -/** - * Sets the React-specific ID of the given node. - * - * @param {DOMElement} node The DOM node whose ID will be set. - * @param {string} id The value of the ID attribute. - */ -function setID(node, id) { - var oldID = internalGetID(node); - if (oldID !== id) { - delete nodeCache[oldID]; - } - node.setAttribute(ATTR_NAME, id); - nodeCache[id] = node; -} - -/** - * Finds the node with the supplied React-generated DOM ID. - * - * @param {string} id A React-generated DOM ID. - * @return {DOMElement} DOM node with the suppled `id`. - * @internal - */ -function getNode(id) { - if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) { - nodeCache[id] = ReactMount.findReactNodeByID(id); - } - return nodeCache[id]; -} - -/** - * A node is "valid" if it is contained by a currently mounted container. - * - * This means that the node does not have to be contained by a document in - * order to be considered valid. - * - * @param {?DOMElement} node The candidate DOM node. - * @param {string} id The expected ID of the node. - * @return {boolean} Whether the node is contained by a mounted container. - */ -function isValid(node, id) { - if (node) { - invariant( - internalGetID(node) === id, - 'ReactMount: Unexpected modification of `%s`', - ATTR_NAME - ); - - var container = ReactMount.findReactContainerForID(id); - if (container && containsNode(container, node)) { - return true; - } - } - - return false; -} - -/** - * Causes the cache to forget about one React-specific ID. - * - * @param {string} id The ID to forget. - */ -function purgeID(id) { - delete nodeCache[id]; -} - -var deepestNodeSoFar = null; -function findDeepestCachedAncestorImpl(ancestorID) { - var ancestor = nodeCache[ancestorID]; - if (ancestor && isValid(ancestor, ancestorID)) { - deepestNodeSoFar = ancestor; - } else { - // This node isn't populated in the cache, so presumably none of its - // descendants are. Break out of the loop. - return false; - } -} - -/** - * Return the deepest cached node whose ID is a prefix of `targetID`. - */ -function findDeepestCachedAncestor(targetID) { - deepestNodeSoFar = null; - ReactInstanceHandles.traverseAncestors( - targetID, - findDeepestCachedAncestorImpl - ); - - var foundNode = deepestNodeSoFar; - deepestNodeSoFar = null; - return foundNode; -} - -/** - * Mounting is the process of initializing a React component by creatings its - * representative DOM elements and inserting them into a supplied `container`. - * Any prior content inside `container` is destroyed in the process. - * - * ReactMount.renderComponent( - * component, - * document.getElementById('container') - * ); - * - *

- * - * Inside of `container`, the first element rendered is the "reactRoot". - */ var ReactMount = { - /** Time spent generating markup. */ - totalInstantiationTime: 0, - - /** Time spent inserting markup into the DOM. */ - totalInjectionTime: 0, - - /** Whether support for touch events should be initialized. */ - useTouchEvents: false, - - /** Exposed for debugging purposes **/ - _instancesByReactRootID: instancesByReactRootID, - /** - * This is a hook provided to support rendering React components while - * ensuring that the apparent scroll position of its `container` does not - * change. + * Constructs a component instance of `constructor` with `initialProps` and + * renders it into the supplied `container`. * - * @param {DOMElement} container The `container` being rendered into. - * @param {function} renderCallback This must be called once to do the render. - */ - scrollMonitor: function(container, renderCallback) { - renderCallback(); - }, - - /** - * Take a component that's already mounted into the DOM and replace its props - * @param {ReactComponent} prevComponent component instance already in the DOM - * @param {ReactComponent} nextComponent component instance to render - * @param {DOMElement} container container to render into - * @param {?function} callback function triggered on completion + * @param {function} constructor React component constructor. + * @param {?object} props Initial props of the component instance. + * @param {DOMElement} container DOM element to render into. + * @return {ReactComponent} Component instance rendered in `container`. */ - _updateRootComponent: function( - prevComponent, - nextComponent, - container, - callback) { - var nextProps = nextComponent.props; - ReactMount.scrollMonitor(container, function() { - prevComponent.replaceProps(nextProps, callback); - }); - - if (__DEV__) { - // Record the root element in case it later gets transplanted. - rootElementsByReactRootID[getReactRootID(container)] = - getReactRootElementInContainer(container); - } - - return prevComponent; + constructAndRenderComponent: function(constructor, props, container) { + return ReactMount.renderComponent(constructor(props), container); }, /** - * Register a component into the instance map and starts scroll value - * monitoring - * @param {ReactComponent} nextComponent component instance to render - * @param {DOMElement} container container to render into - * @return {string} reactRoot ID prefix + * Constructs a component instance of `constructor` with `initialProps` and + * renders it into a container node identified by supplied `id`. + * + * @param {function} componentConstructor React component constructor + * @param {?object} props Initial props of the component instance. + * @param {string} id ID of the DOM element to render into. + * @return {ReactComponent} Component instance rendered in the container node. */ - _registerComponent: function(nextComponent, container) { + constructAndRenderComponentByID: function(constructor, props, id) { + var domNode = document.getElementById(id); invariant( - container && ( - container.nodeType === ELEMENT_NODE_TYPE || - container.nodeType === DOC_NODE_TYPE - ), - '_registerComponent(...): Target container is not a DOM element.' + domNode, + 'Tried to get element with id of "%s" but it is not present on the page.', + id ); - - ReactEventEmitter.ensureScrollValueMonitoring(); - - var reactRootID = ReactMount.registerContainer(container); - instancesByReactRootID[reactRootID] = nextComponent; - return reactRootID; + return ReactMount.constructAndRenderComponent(constructor, props, domNode); }, /** @@ -311,21 +87,23 @@ var ReactMount = { ); var componentInstance = instantiateReactComponent(nextComponent); - var reactRootID = ReactMount._registerComponent( + var containerHandle = ReactDOMNodeMapping.getHandleForContainer(container); + var reactRootID = ReactDOMNodeHandleMapping.registerComponent( componentInstance, - container + containerHandle, + ReactDOMNodeMapping.getReactRootID(container) + ); + ReactDOMNodeMapping.registerComponentInContainer( + reactRootID, + containerHandle ); componentInstance.mountComponentIntoNode( reactRootID, - container, + ReactDOMNodeMapping.getHandleForContainer(container), shouldReuseMarkup ); - if (__DEV__) { - // Record the root element in case it later gets transplanted. - rootElementsByReactRootID[reactRootID] = - getReactRootElementInContainer(container); - } + ReactDOMNodeMapping.recordRootElementForTransplantWarning(container); return componentInstance; } @@ -344,7 +122,7 @@ var ReactMount = { * @return {ReactComponent} Component instance rendered in `container`. */ renderComponent: function(nextDescriptor, container, callback) { - var prevComponent = instancesByReactRootID[getReactRootID(container)]; + var prevComponent = ReactDOMNodeMapping.getInstanceFromContainer(container); if (prevComponent) { var prevDescriptor = prevComponent._descriptor; @@ -360,9 +138,9 @@ var ReactMount = { } } - var reactRootElement = getReactRootElementInContainer(container); + var reactRootElement = ReactDOMNodeMapping.getReactRootElementInContainer(container); var containerHasReactMarkup = - reactRootElement && ReactMount.isRenderedByReact(reactRootElement); + reactRootElement && ReactDOMNodeMapping.isRenderedByReact(reactRootElement); var shouldReuseMarkup = containerHasReactMarkup && !prevComponent; @@ -375,68 +153,22 @@ var ReactMount = { return component; }, - /** - * Constructs a component instance of `constructor` with `initialProps` and - * renders it into the supplied `container`. - * - * @param {function} constructor React component constructor. - * @param {?object} props Initial props of the component instance. - * @param {DOMElement} container DOM element to render into. - * @return {ReactComponent} Component instance rendered in `container`. - */ - constructAndRenderComponent: function(constructor, props, container) { - return ReactMount.renderComponent(constructor(props), container); - }, + _updateRootComponent: function( + prevComponent, + nextComponent, + container, + callback) { + var nextProps = nextComponent.props; + ReactMount.scrollMonitor(container, function() { + prevComponent.replaceProps(nextProps, callback); + }); - /** - * Constructs a component instance of `constructor` with `initialProps` and - * renders it into a container node identified by supplied `id`. - * - * @param {function} componentConstructor React component constructor - * @param {?object} props Initial props of the component instance. - * @param {string} id ID of the DOM element to render into. - * @return {ReactComponent} Component instance rendered in the container node. - */ - constructAndRenderComponentByID: function(constructor, props, id) { - var domNode = document.getElementById(id); - invariant( - domNode, - 'Tried to get element with id of "%s" but it is not present on the page.', - id - ); - return ReactMount.constructAndRenderComponent(constructor, props, domNode); - }, + ReactDOMNodeMapping.recordRootElementForTransplantWarning(container); - /** - * Registers a container node into which React components will be rendered. - * This also creates the "reactRoot" ID that will be assigned to the element - * rendered within. - * - * @param {DOMElement} container DOM element to register as a container. - * @return {string} The "reactRoot" ID of elements rendered within. - */ - registerContainer: function(container) { - var reactRootID = getReactRootID(container); - if (reactRootID) { - // If one exists, make sure it is a valid "reactRoot" ID. - reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID); - } - if (!reactRootID) { - // No valid "reactRoot" ID found, create one. - reactRootID = ReactInstanceHandles.createReactRootID(); - } - containersByReactRootID[reactRootID] = container; - return reactRootID; + return prevComponent; }, - /** - * Unmounts and destroys the React component rendered in the `container`. - * - * @param {DOMElement} container DOM element containing a React component. - * @return {boolean} True if a component was found in and unmounted from - * `container` - */ - unmountComponentAtNode: function(container) { + unmountComponentAtNode: function() { // Various parts of our code (such as ReactCompositeComponent's // _renderValidatedComponent) assume that calls to render aren't nested; // verify that that's the case. (Strictly speaking, unmounting won't cause a @@ -449,221 +181,20 @@ var ReactMount = { 'componentDidUpdate.' ); - var reactRootID = getReactRootID(container); - var component = instancesByReactRootID[reactRootID]; - if (!component) { - return false; - } - ReactMount.unmountComponentFromNode(component, container); - delete instancesByReactRootID[reactRootID]; - delete containersByReactRootID[reactRootID]; - if (__DEV__) { - delete rootElementsByReactRootID[reactRootID]; - } - return true; + return ReactDOMNodeMapping.unmountComponentAtNode.apply(ReactDOMNodeMapping, arguments); }, /** - * Unmounts a component and removes it from the DOM. - * - * @param {ReactComponent} instance React component instance. - * @param {DOMElement} container DOM element to unmount from. - * @final - * @internal - * @see {ReactMount.unmountComponentAtNode} - */ - unmountComponentFromNode: function(instance, container) { - instance.unmountComponent(); - - if (container.nodeType === DOC_NODE_TYPE) { - container = container.documentElement; - } - - // http://jsperf.com/emptying-a-node - while (container.lastChild) { - container.removeChild(container.lastChild); - } - }, - - /** - * Finds the container DOM element that contains React component to which the - * supplied DOM `id` belongs. - * - * @param {string} id The ID of an element rendered by a React component. - * @return {?DOMElement} DOM element that contains the `id`. - */ - findReactContainerForID: function(id) { - var reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id); - var container = containersByReactRootID[reactRootID]; - - if (__DEV__) { - var rootElement = rootElementsByReactRootID[reactRootID]; - if (rootElement && rootElement.parentNode !== container) { - invariant( - // Call internalGetID here because getID calls isValid which calls - // findReactContainerForID (this function). - internalGetID(rootElement) === reactRootID, - 'ReactMount: Root element ID differed from reactRootID.' - ); - - var containerChild = container.firstChild; - if (containerChild && - reactRootID === internalGetID(containerChild)) { - // If the container has a new child with the same ID as the old - // root element, then rootElementsByReactRootID[reactRootID] is - // just stale and needs to be updated. The case that deserves a - // warning is when the container is empty. - rootElementsByReactRootID[reactRootID] = containerChild; - } else { - console.warn( - 'ReactMount: Root element has been removed from its original ' + - 'container. New container:', rootElement.parentNode - ); - } - } - } - - return container; - }, - - /** - * Finds an element rendered by React with the supplied ID. - * - * @param {string} id ID of a DOM node in the React component. - * @return {DOMElement} Root DOM node of the React component. - */ - findReactNodeByID: function(id) { - var reactRoot = ReactMount.findReactContainerForID(id); - return ReactMount.findComponentRoot(reactRoot, id); - }, - - /** - * True if the supplied `node` is rendered by React. - * - * @param {*} node DOM Element to check. - * @return {boolean} True if the DOM Element appears to be rendered by React. - * @internal - */ - isRenderedByReact: function(node) { - if (node.nodeType !== 1) { - // Not a DOMElement, therefore not a React component - return false; - } - var id = ReactMount.getID(node); - return id ? id.charAt(0) === SEPARATOR : false; - }, - - /** - * Traverses up the ancestors of the supplied node to find a node that is a - * DOM representation of a React component. - * - * @param {*} node - * @return {?DOMEventTarget} - * @internal - */ - getFirstReactDOM: function(node) { - var current = node; - while (current && current.parentNode !== current) { - if (ReactMount.isRenderedByReact(current)) { - return current; - } - current = current.parentNode; - } - return null; - }, - - /** - * Finds a node with the supplied `targetID` inside of the supplied - * `ancestorNode`. Exploits the ID naming scheme to perform the search - * quickly. + * This is a hook provided to support rendering React components while + * ensuring that the apparent scroll position of its `container` does not + * change. * - * @param {DOMEventTarget} ancestorNode Search from this root. - * @pararm {string} targetID ID of the DOM representation of the component. - * @return {DOMEventTarget} DOM node with the supplied `targetID`. - * @internal - */ - findComponentRoot: function(ancestorNode, targetID) { - var firstChildren = findComponentRootReusableArray; - var childIndex = 0; - - var deepestAncestor = findDeepestCachedAncestor(targetID) || ancestorNode; - - firstChildren[0] = deepestAncestor.firstChild; - firstChildren.length = 1; - - while (childIndex < firstChildren.length) { - var child = firstChildren[childIndex++]; - var targetChild; - - while (child) { - var childID = ReactMount.getID(child); - if (childID) { - // Even if we find the node we're looking for, we finish looping - // through its siblings to ensure they're cached so that we don't have - // to revisit this node again. Otherwise, we make n^2 calls to getID - // when visiting the many children of a single node in order. - - if (targetID === childID) { - targetChild = child; - } else if (ReactInstanceHandles.isAncestorIDOf(childID, targetID)) { - // If we find a child whose ID is an ancestor of the given ID, - // then we can be sure that we only want to search the subtree - // rooted at this child, so we can throw out the rest of the - // search state. - firstChildren.length = childIndex = 0; - firstChildren.push(child.firstChild); - } - - } else { - // If this child had no ID, then there's a chance that it was - // injected automatically by the browser, as when a `
` - // element sprouts an extra `` child as a side effect of - // `.innerHTML` parsing. Optimistically continue down this - // branch, but not before examining the other siblings. - firstChildren.push(child.firstChild); - } - - child = child.nextSibling; - } - - if (targetChild) { - // Emptying firstChildren/findComponentRootReusableArray is - // not necessary for correctness, but it helps the GC reclaim - // any nodes that were left at the end of the search. - firstChildren.length = 0; - - return targetChild; - } - } - - firstChildren.length = 0; - - invariant( - false, - 'findComponentRoot(..., %s): Unable to find element. This probably ' + - 'means the DOM was unexpectedly mutated (e.g., by the browser), ' + - 'usually due to forgetting a when using tables or nesting

' + - 'or tags. Try inspecting the child nodes of the element with React ' + - 'ID `%s`.', - targetID, - ReactMount.getID(ancestorNode) - ); - }, - - - /** - * React ID utilities. + * @param {DOMElement} container The `container` being rendered into. + * @param {function} renderCallback This must be called once to do the render. */ - - getReactRootID: getReactRootID, - - getID: getID, - - setID: setID, - - getNode: getNode, - - purgeID: purgeID + scrollMonitor: function(container, renderCallback) { + renderCallback(); + } }; module.exports = ReactMount; diff --git a/src/browser/ReactReconcileTransaction.js b/src/browser/ui/ReactReconcileTransaction.js similarity index 100% rename from src/browser/ReactReconcileTransaction.js rename to src/browser/ui/ReactReconcileTransaction.js diff --git a/src/browser/ui/ReactWorker.js b/src/browser/ui/ReactWorker.js new file mode 100644 index 00000000000..e29cde105c2 --- /dev/null +++ b/src/browser/ui/ReactWorker.js @@ -0,0 +1,79 @@ +/** + * 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 ReactWorker + */ + +"use strict"; + +var ExecutionEnvironment = require('ExecutionEnvironment'); +var ReactComponentBrowserEnvironment = + require('ReactComponentBrowserEnvironment'); +var ReactDOMIDOperations = require('ReactDOMIDOperations'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); +var ReactEventListener = require('ReactEventListener'); +var RemoteModule = require('RemoteModule'); +var RemoteModuleServer = require('RemoteModuleServer'); + +var keyOf = require('keyOf'); + +// The UI thread uses this to kick off the worker. +class ReactWorker { + constructor(scriptURI) { + this.worker = new Worker(scriptURI); + this.server = new RemoteModuleServer(this.worker, { + ReactComponentBrowserEnvironment: ReactComponentBrowserEnvironment, + ReactDOMIDOperations: ReactDOMIDOperations, + ReactDOMNodeMapping: ReactDOMNodeMapping, + ReactEventListener: ReactEventListener + }); + + var ReactEventEmitterRemote = new RemoteModule( + this.worker, + keyOf({ReactEventEmitter: null}), + {handleTopLevel: null} + ); + + ReactEventListener.setHandleTopLevel( + function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { + ReactEventEmitterRemote.handleTopLevel( + topLevelType, + {}, + topLevelTargetID, + {target: {}} + ); + } + ); + } + + terminate() { + this.server.destroy(); + this.worker.terminate(); + } +} + +ReactWorker.run = function(script, dependencies, main) { + if (ExecutionEnvironment.canUseDOM) { + return new ReactWorker(script); + } else { + if (dependencies.length > 0) { + importScripts.apply(null, dependencies); + } + main(); + return self; + } +}; + +module.exports = ReactWorker; diff --git a/src/browser/ui/__tests__/ReactDOMComponent-test.js b/src/browser/ui/__tests__/ReactDOMComponent-test.js index ccc279f2e29..78d42b176ee 100644 --- a/src/browser/ui/__tests__/ReactDOMComponent-test.js +++ b/src/browser/ui/__tests__/ReactDOMComponent-test.js @@ -384,7 +384,7 @@ describe('ReactDOMComponent', function() { it("should clean up listeners", function() { var React = require('React'); var ReactEventEmitter = require('ReactEventEmitter'); - var ReactMount = require('ReactMount'); + var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var container = document.createElement('div'); document.documentElement.appendChild(container); @@ -394,7 +394,7 @@ describe('ReactDOMComponent', function() { instance = React.renderComponent(instance, container); var rootNode = instance.getDOMNode(); - var rootNodeID = ReactMount.getID(rootNode); + var rootNodeID = ReactDOMNodeMapping.getID(rootNode); expect( ReactEventEmitter.getListener(rootNodeID, 'onClick') ).toBe(callback); diff --git a/src/browser/ui/__tests__/ReactDOMIDOperations-test.js b/src/browser/ui/__tests__/ReactDOMIDOperations-test.js index a1b93f04ec3..e2179859851 100644 --- a/src/browser/ui/__tests__/ReactDOMIDOperations-test.js +++ b/src/browser/ui/__tests__/ReactDOMIDOperations-test.js @@ -23,11 +23,11 @@ describe('ReactDOMIDOperations', function() { var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); - var ReactMount = require('ReactMount'); + var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var keyOf = require('keyOf'); it('should disallow updating special properties', function() { - spyOn(ReactMount, "getNode"); + spyOn(ReactDOMNodeMapping, "getNode"); spyOn(DOMPropertyOperations, "setValueForProperty"); expect(function() { @@ -39,7 +39,7 @@ describe('ReactDOMIDOperations', function() { }).toThrow(); expect( - ReactMount.getNode.argsForCall[0][0] + ReactDOMNodeMapping.getNode.argsForCall[0][0] ).toBe('testID'); expect( @@ -49,17 +49,17 @@ describe('ReactDOMIDOperations', function() { it('should update innerHTML and preserve whitespace', function() { var stubNode = document.createElement('div'); - spyOn(ReactMount, "getNode").andReturn(stubNode); + spyOn(ReactDOMNodeMapping, "getNode").andReturn(stubNode); var html = '\n \t \n testContent \t \n \t'; - ReactDOMIDOperations.updateInnerHTMLByID( + ReactDOMIDOperations.updateImageByID( 'testID', html ); expect( - ReactMount.getNode.argsForCall[0][0] + ReactDOMNodeMapping.getNode.argsForCall[0][0] ).toBe('testID'); expect(stubNode.innerHTML).toBe(html); diff --git a/src/browser/ui/__tests__/ReactEventTopLevelCallback-test.js b/src/browser/ui/__tests__/ReactEventListener-test.js similarity index 83% rename from src/browser/ui/__tests__/ReactEventTopLevelCallback-test.js rename to src/browser/ui/__tests__/ReactEventListener-test.js index d077a670628..d6dd91a8a9a 100644 --- a/src/browser/ui/__tests__/ReactEventTopLevelCallback-test.js +++ b/src/browser/ui/__tests__/ReactEventListener-test.js @@ -19,23 +19,24 @@ 'use strict'; -require('mock-modules') - .mock('ReactEventEmitter'); +var mocks = require('mocks'); var EVENT_TARGET_PARAM = 1; -describe('ReactEventTopLevelCallback', function() { +describe('ReactEventListener', function() { var React; - var ReactEventTopLevelCallback; var ReactMount; - var ReactEventEmitter; // mocked + var ReactEventListener; + var handleTopLevel; beforeEach(function() { require('mock-modules').dumpCache(); React = require('React'); - ReactEventTopLevelCallback = require('ReactEventTopLevelCallback'); ReactMount = require('ReactMount'); - ReactEventEmitter = require('ReactEventEmitter'); // mocked + ReactEventListener = require('ReactEventListener'); + + handleTopLevel = mocks.getMockFunction(); + ReactEventListener._handleTopLevel = handleTopLevel; }); describe('Propagation', function() { @@ -48,12 +49,12 @@ describe('ReactEventTopLevelCallback', function() { parentControl = ReactMount.renderComponent(parentControl, parentContainer); parentControl.getDOMNode().appendChild(childContainer); - var callback = ReactEventTopLevelCallback.createTopLevelCallback('test'); + var callback = ReactEventListener.dispatchEvent.bind(null, 'test'); callback({ target: childControl.getDOMNode() }); - var calls = ReactEventEmitter.handleTopLevel.mock.calls; + var calls = handleTopLevel.mock.calls; expect(calls.length).toBe(2); expect(calls[0][EVENT_TARGET_PARAM]).toBe(childControl.getDOMNode()); expect(calls[1][EVENT_TARGET_PARAM]).toBe(parentControl.getDOMNode()); @@ -72,12 +73,12 @@ describe('ReactEventTopLevelCallback', function() { parentControl.getDOMNode().appendChild(childContainer); grandParentControl.getDOMNode().appendChild(parentContainer); - var callback = ReactEventTopLevelCallback.createTopLevelCallback('test'); + var callback = ReactEventListener.dispatchEvent.bind(null, 'test'); callback({ target: childControl.getDOMNode() }); - var calls = ReactEventEmitter.handleTopLevel.mock.calls; + var calls = handleTopLevel.mock.calls; expect(calls.length).toBe(3); expect(calls[0][EVENT_TARGET_PARAM]).toBe(childControl.getDOMNode()); expect(calls[1][EVENT_TARGET_PARAM]).toBe(parentControl.getDOMNode()); @@ -99,7 +100,7 @@ describe('ReactEventTopLevelCallback', function() { // handlers are called; we'll still expect to receive a second call for // the parent control. var childNode = childControl.getDOMNode(); - ReactEventEmitter.handleTopLevel.mockImplementation( + handleTopLevel.mockImplementation( function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { if (topLevelTarget === childNode) { ReactMount.unmountComponentAtNode(childContainer); @@ -107,12 +108,12 @@ describe('ReactEventTopLevelCallback', function() { } ); - var callback = ReactEventTopLevelCallback.createTopLevelCallback('test'); + var callback = ReactEventListener.dispatchEvent.bind(null, 'test'); callback({ target: childNode }); - var calls = ReactEventEmitter.handleTopLevel.mock.calls; + var calls = handleTopLevel.mock.calls; expect(calls.length).toBe(2); expect(calls[0][EVENT_TARGET_PARAM]).toBe(childNode); expect(calls[1][EVENT_TARGET_PARAM]).toBe(parentControl.getDOMNode()); @@ -134,7 +135,7 @@ describe('ReactEventTopLevelCallback', function() { // Suppose an event handler in each root enqueues an update to the // childControl element -- the two updates should get batched together. var childNode = childControl.getDOMNode(); - ReactEventEmitter.handleTopLevel.mockImplementation( + handleTopLevel.mockImplementation( function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) { ReactMount.renderComponent(

{topLevelTarget === childNode ? '1' : '2'}
, @@ -145,12 +146,12 @@ describe('ReactEventTopLevelCallback', function() { } ); - var callback = ReactEventTopLevelCallback.createTopLevelCallback('test'); + var callback = ReactEventListener.dispatchEvent.bind(ReactEventListener, 'test'); callback({ target: childNode }); - var calls = ReactEventEmitter.handleTopLevel.mock.calls; + var calls = handleTopLevel.mock.calls; expect(calls.length).toBe(2); expect(childNode.textContent).toBe('2'); }); @@ -173,12 +174,12 @@ describe('ReactEventTopLevelCallback', function() { var instance = ReactMount.renderComponent(, container); - var callback = ReactEventTopLevelCallback.createTopLevelCallback('test'); + var callback = ReactEventListener.dispatchEvent.bind(null, 'test'); callback({ target: instance.getInner().getDOMNode() }); - var calls = ReactEventEmitter.handleTopLevel.mock.calls; + var calls = handleTopLevel.mock.calls; expect(calls.length).toBe(1); expect(calls[0][EVENT_TARGET_PARAM]).toBe(instance.getInner().getDOMNode()); }); diff --git a/src/browser/ui/__tests__/ReactMountDestruction-test.js b/src/browser/ui/__tests__/ReactMountDestruction-test.js index c73ddb461f6..c2796c313a5 100644 --- a/src/browser/ui/__tests__/ReactMountDestruction-test.js +++ b/src/browser/ui/__tests__/ReactMountDestruction-test.js @@ -21,7 +21,7 @@ var React = require('React'); -describe('ReactMount', function() { +describe('ReactDOMNodeMapping', function() { it("should destroy a react root upon request", function() { var mainContainerDiv = document.createElement('div'); document.documentElement.appendChild(mainContainerDiv); diff --git a/src/browser/ui/__tests__/ReactRenderDocument-test.js b/src/browser/ui/__tests__/ReactRenderDocument-test.js index 8a545252ede..bf9e8202afe 100644 --- a/src/browser/ui/__tests__/ReactRenderDocument-test.js +++ b/src/browser/ui/__tests__/ReactRenderDocument-test.js @@ -22,7 +22,7 @@ "use strict"; var React; -var ReactMount; +var ReactDOMNodeMapping; var getTestDocument; @@ -40,7 +40,7 @@ describe('rendering React components at document', function() { require('mock-modules').dumpCache(); React = require('React'); - ReactMount = require('ReactMount'); + ReactDOMNodeMapping = require('ReactDOMNodeMapping'); getTestDocument = require('getTestDocument'); testDocument = getTestDocument(); @@ -69,7 +69,7 @@ describe('rendering React components at document', function() { var component = React.renderComponent(, testDocument); expect(testDocument.body.innerHTML).toBe('Hello world'); - var componentID = ReactMount.getReactRootID(testDocument); + var componentID = ReactDOMNodeMapping.getReactRootID(testDocument); expect(componentID).toBe(component._rootNodeID); }); diff --git a/src/browser/ui/dom/DOMChildrenOperations.js b/src/browser/ui/dom/DOMChildrenOperations.js index e2e3de2005a..0e602dcf27d 100644 --- a/src/browser/ui/dom/DOMChildrenOperations.js +++ b/src/browser/ui/dom/DOMChildrenOperations.js @@ -145,10 +145,10 @@ var DOMChildrenOperations = { for (var k = 0; update = updates[k]; k++) { switch (update.type) { - case ReactMultiChildUpdateTypes.INSERT_MARKUP: + case ReactMultiChildUpdateTypes.INSERT_IMAGE: insertChildAt( update.parentNode, - renderedMarkup[update.markupIndex], + renderedMarkup[update.imageIndex], update.toIndex ); break; diff --git a/src/browser/ui/dom/ViewportMetrics.js b/src/browser/ui/dom/ViewportMetrics.js index 621a517220b..375917c2892 100644 --- a/src/browser/ui/dom/ViewportMetrics.js +++ b/src/browser/ui/dom/ViewportMetrics.js @@ -18,16 +18,13 @@ "use strict"; -var getUnboundedScrollPosition = require('getUnboundedScrollPosition'); - var ViewportMetrics = { currentScrollLeft: 0, currentScrollTop: 0, - refreshScrollValues: function() { - var scrollPosition = getUnboundedScrollPosition(window); + refreshScrollValues: function(scrollPosition) { ViewportMetrics.currentScrollLeft = scrollPosition.x; ViewportMetrics.currentScrollTop = scrollPosition.y; } diff --git a/src/browser/ui/dom/components/ReactDOMForm.js b/src/browser/ui/dom/components/ReactDOMForm.js index b8696be8608..45429fb2344 100644 --- a/src/browser/ui/dom/components/ReactDOMForm.js +++ b/src/browser/ui/dom/components/ReactDOMForm.js @@ -21,6 +21,7 @@ var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); var ReactCompositeComponent = require('ReactCompositeComponent'); var ReactDOM = require('ReactDOM'); +var ReactDOMNodeHandle = require('ReactDOMNodeHandle'); var ReactEventEmitter = require('ReactEventEmitter'); var EventConstants = require('EventConstants'); @@ -49,12 +50,12 @@ var ReactDOMForm = ReactCompositeComponent.createClass({ ReactEventEmitter.trapBubbledEvent( EventConstants.topLevelTypes.topReset, 'reset', - this.getDOMNode() + ReactDOMNodeHandle.getHandleForReactID(this._rootNodeID) ); ReactEventEmitter.trapBubbledEvent( EventConstants.topLevelTypes.topSubmit, 'submit', - this.getDOMNode() + ReactDOMNodeHandle.getHandleForReactID(this._rootNodeID) ); } }); diff --git a/src/browser/ui/dom/components/ReactDOMImg.js b/src/browser/ui/dom/components/ReactDOMImg.js index 46b154f69d6..663196614f6 100644 --- a/src/browser/ui/dom/components/ReactDOMImg.js +++ b/src/browser/ui/dom/components/ReactDOMImg.js @@ -21,6 +21,7 @@ var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); var ReactCompositeComponent = require('ReactCompositeComponent'); var ReactDOM = require('ReactDOM'); +var ReactDOMNodeHandle = require('ReactDOMNodeHandle'); var ReactEventEmitter = require('ReactEventEmitter'); var EventConstants = require('EventConstants'); @@ -48,12 +49,12 @@ var ReactDOMImg = ReactCompositeComponent.createClass({ ReactEventEmitter.trapBubbledEvent( EventConstants.topLevelTypes.topLoad, 'load', - node + ReactDOMNodeHandle.getHandleForReactID(this._rootNodeID) ); ReactEventEmitter.trapBubbledEvent( EventConstants.topLevelTypes.topError, 'error', - node + ReactDOMNodeHandle.getHandleForReactID(this._rootNodeID) ); } }); diff --git a/src/browser/ui/dom/components/ReactDOMInput.js b/src/browser/ui/dom/components/ReactDOMInput.js index 0437f60513c..8ffec61ed7c 100644 --- a/src/browser/ui/dom/components/ReactDOMInput.js +++ b/src/browser/ui/dom/components/ReactDOMInput.js @@ -24,7 +24,7 @@ var LinkedValueUtils = require('LinkedValueUtils'); var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); var ReactCompositeComponent = require('ReactCompositeComponent'); var ReactDOM = require('ReactDOM'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var invariant = require('invariant'); var merge = require('merge'); @@ -87,13 +87,13 @@ var ReactDOMInput = ReactCompositeComponent.createClass({ }, componentDidMount: function() { - var id = ReactMount.getID(this.getDOMNode()); + var id = ReactDOMNodeMapping.getID(this.getDOMNode()); instancesByReactID[id] = this; }, componentWillUnmount: function() { var rootNode = this.getDOMNode(); - var id = ReactMount.getID(rootNode); + var id = ReactDOMNodeMapping.getID(rootNode); delete instancesByReactID[id]; }, @@ -152,7 +152,7 @@ var ReactDOMInput = ReactCompositeComponent.createClass({ otherNode.form !== rootNode.form) { continue; } - var otherID = ReactMount.getID(otherNode); + var otherID = ReactDOMNodeMapping.getID(otherNode); invariant( otherID, 'ReactDOMInput: Mixing React and non-React radio inputs with the ' + diff --git a/src/browser/ui/getReactRootElementInContainer.js b/src/browser/worker/ReactComponentBrowserEnvironmentRemote.js similarity index 52% rename from src/browser/ui/getReactRootElementInContainer.js rename to src/browser/worker/ReactComponentBrowserEnvironmentRemote.js index 3ecf18194dc..10c0c228402 100644 --- a/src/browser/ui/getReactRootElementInContainer.js +++ b/src/browser/worker/ReactComponentBrowserEnvironmentRemote.js @@ -13,28 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. * - * @providesModule getReactRootElementInContainer + * @providesModule ReactComponentBrowserEnvironmentRemote */ "use strict"; -var DOC_NODE_TYPE = 9; +var ExecutionEnvironment = require('ExecutionEnvironment'); +var RemoteModule = require('RemoteModule'); -/** - * @param {DOMElement|DOMDocument} container DOM element that may contain - * a React component - * @return {?*} DOM element that may have the reactRoot ID, or null. - */ -function getReactRootElementInContainer(container) { - if (!container) { - return null; - } +var keyOf = require('keyOf'); - if (container.nodeType === DOC_NODE_TYPE) { - return container.documentElement; - } else { - return container.firstChild; +var ReactComponentBrowserEnvironmentRemote = new RemoteModule( + ExecutionEnvironment.global, + keyOf({ReactComponentBrowserEnvironment: null}), + { + mountImageIntoNode: null, + unmountIDFromEnvironment: null } -} +); -module.exports = getReactRootElementInContainer; +module.exports = ReactComponentBrowserEnvironmentRemote; diff --git a/src/browser/worker/ReactComponentWorkerEnvironment.js b/src/browser/worker/ReactComponentWorkerEnvironment.js new file mode 100644 index 00000000000..51a25913768 --- /dev/null +++ b/src/browser/worker/ReactComponentWorkerEnvironment.js @@ -0,0 +1,39 @@ +/** + * 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 ReactComponentWorkerEnvironment + */ + +"use strict"; + +var ExecutionEnvironment = require('ExecutionEnvironment'); +var ReactComponentBrowserEnvironmentRemote = + require('ReactComponentBrowserEnvironmentRemote'); +var ReactDOMIDOperationsRemote = require('ReactDOMIDOperationsRemote'); +var ReactWorkerReconcileTransaction = require('ReactWorkerReconcileTransaction'); + +var ReactComponentWorkerEnvironment = { + ReactReconcileTransaction: ReactWorkerReconcileTransaction, + + BackendIDOperations: ReactDOMIDOperationsRemote, + + unmountIDFromEnvironment: + ReactComponentBrowserEnvironmentRemote.unmountIDFromEnvironment, + + mountImageIntoNode: + ReactComponentBrowserEnvironmentRemote.mountImageIntoNode, +}; + +module.exports = ReactComponentWorkerEnvironment; diff --git a/src/browser/worker/ReactDOMIDOperationsRemote.js b/src/browser/worker/ReactDOMIDOperationsRemote.js new file mode 100644 index 00000000000..16b075c5ccb --- /dev/null +++ b/src/browser/worker/ReactDOMIDOperationsRemote.js @@ -0,0 +1,40 @@ +/** + * 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 ReactDOMIDOperationsRemote + */ + +"use strict"; + +var ExecutionEnvironment = require('ExecutionEnvironment'); +var RemoteModule = require('RemoteModule'); + +var keyOf = require('keyOf'); + +var ReactDOMIDOperationsRemote = new RemoteModule( + ExecutionEnvironment.global, + keyOf({ReactDOMIDOperations: null}), + { + updatePropertyByID: null, + deletePropertyByID: null, + updateStylesByID: null, + updateImageByID: null, + updateTextContentByID: null, + dangerouslyReplaceNodeWithMarkupByID: null, + dangerouslyProcessChildrenUpdates: null + } +); + +module.exports = ReactDOMIDOperationsRemote; diff --git a/src/browser/worker/ReactDOMNodeMappingRemote.js b/src/browser/worker/ReactDOMNodeMappingRemote.js new file mode 100644 index 00000000000..823cee57125 --- /dev/null +++ b/src/browser/worker/ReactDOMNodeMappingRemote.js @@ -0,0 +1,39 @@ +/** + * 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 ReactDOMNodeMappingRemote + */ + +"use strict"; + +var ExecutionEnvironment = require('ExecutionEnvironment'); +var RemoteModule = require('RemoteModule'); + +var keyOf = require('keyOf'); + +var ReactDOMNodeMappingRemote = new RemoteModule( + ExecutionEnvironment.global, + keyOf({ReactDOMNodeMapping: null}), + // TODO: we should codegen this when we move to a better bridge + // so we can get type checking. + // TODO: should we move this out of ReactDOMNodeMapping? + { + registerContainerHandle: null, + unmountComponentAtHandle: null, + registerComponentInContainer: null + } +); + +module.exports = ReactDOMNodeMappingRemote; diff --git a/src/browser/worker/ReactEventListenerRemote.js b/src/browser/worker/ReactEventListenerRemote.js new file mode 100644 index 00000000000..fcab5ddae42 --- /dev/null +++ b/src/browser/worker/ReactEventListenerRemote.js @@ -0,0 +1,57 @@ +/** + * 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 ReactEventListenerRemote + */ + +"use strict"; + +var ExecutionEnvironment = require('ExecutionEnvironment'); +var RemoteModule = require('RemoteModule'); + +var copyProperties = require('copyProperties'); +var emptyFunction = require('emptyFunction'); +var keyOf = require('keyOf'); + +var enabled = true; + +var ReactEventListenerRemote = new RemoteModule( + ExecutionEnvironment.global, + keyOf({ReactEventListener: null}), + { + monitorScrollValue: null, + setEnabled: null, + trapBubbledEvent: null, + trapCapturedEvent: null + } +); + +var _setEnabled = ReactEventListenerRemote.setEnabled; + +copyProperties(ReactEventListenerRemote, { + setEnabled: function(value) { + enabled = value; + _setEnabled(value); + }, + + isEnabled: function() { + return enabled; + }, + + // This is handled by RemoteModuleServer + setHandleTopLevel: emptyFunction +}); + +module.exports = ReactEventListenerRemote; diff --git a/src/browser/worker/ReactWorkerInjection.js b/src/browser/worker/ReactWorkerInjection.js new file mode 100644 index 00000000000..4b974a628d8 --- /dev/null +++ b/src/browser/worker/ReactWorkerInjection.js @@ -0,0 +1,144 @@ +/** + * 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 ReactWorkerInjection + */ + +"use strict"; + +var DOMProperty = require('DOMProperty'); +var EventPluginHub = require('EventPluginHub'); +var ReactComponent = require('ReactComponent'); +var ReactCompositeComponent = require('ReactCompositeComponent'); +var ReactDOM = require('ReactDOM'); +var ReactEmptyComponent = require('ReactEmptyComponent'); +var ReactEventEmitter = require('ReactEventEmitter'); +var ReactPerf = require('ReactPerf'); +var ReactRootIndex = require('ReactRootIndex'); +var ReactUpdates = require('ReactUpdates'); + +var ExecutionEnvironment = require('ExecutionEnvironment'); + +var ChangeEventPlugin = require('ChangeEventPlugin'); +var ClientReactRootIndex = require('ClientReactRootIndex'); +var CompositionEventPlugin = require('CompositionEventPlugin'); +var DefaultEventPluginOrder = require('DefaultEventPluginOrder'); +var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin'); +var HTMLDOMPropertyConfig = require('HTMLDOMPropertyConfig'); +var MobileSafariClickEventPlugin = require('MobileSafariClickEventPlugin'); +var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin'); +var ReactComponentWorkerEnvironment = + require('ReactComponentWorkerEnvironment'); +var ReactEventListenerRemote = require('ReactEventListenerRemote'); +var ReactDOM = require('ReactDOM'); +var ReactDOMButton = require('ReactDOMButton'); +var ReactDOMForm = require('ReactDOMForm'); +var ReactDOMImg = require('ReactDOMImg'); +var ReactDOMInput = require('ReactDOMInput'); +var ReactDOMOption = require('ReactDOMOption'); +var ReactDOMSelect = require('ReactDOMSelect'); +var ReactDOMTextarea = require('ReactDOMTextarea'); +var ReactEventEmitter = require('ReactEventEmitter'); +var ReactInstanceHandles = require('ReactInstanceHandles'); +var SelectEventPlugin = require('SelectEventPlugin'); +var SimpleEventPlugin = require('SimpleEventPlugin'); +var SVGDOMPropertyConfig = require('SVGDOMPropertyConfig'); +var BeforeInputEventPlugin = require('BeforeInputEventPlugin'); + +var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); +var RemoteModuleServer = require('RemoteModuleServer'); + +var createFullPageComponent = require('createFullPageComponent'); + +var server; + +function inject() { + server = new RemoteModuleServer(ExecutionEnvironment.global, { + ReactEventEmitter: ReactEventEmitter + }); + + ReactEventEmitter.injection.injectReactEventListener( + ReactEventListenerRemote + ); + + /** + * Inject modules for resolving DOM hierarchy and plugin ordering. + */ + EventPluginHub.injection.injectEventPluginOrder(DefaultEventPluginOrder); + EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles); + EventPluginHub.injection.injectMount({getNode: function() {}}); + + /** + * Some important event plugins included by default (without having to require + * them). + */ + + EventPluginHub.injection.injectEventPluginsByName({ + SimpleEventPlugin: SimpleEventPlugin, + EnterLeaveEventPlugin: EnterLeaveEventPlugin, + ChangeEventPlugin: ChangeEventPlugin, + CompositionEventPlugin: CompositionEventPlugin, + MobileSafariClickEventPlugin: MobileSafariClickEventPlugin, + SelectEventPlugin: SelectEventPlugin, + BeforeInputEventPlugin: BeforeInputEventPlugin + }); + + ReactDOM.injection.injectComponentClasses({ + button: ReactDOMButton, + form: ReactDOMForm, + img: ReactDOMImg, + input: ReactDOMInput, + option: ReactDOMOption, + select: ReactDOMSelect, + textarea: ReactDOMTextarea, + + html: createFullPageComponent(ReactDOM.html), + head: createFullPageComponent(ReactDOM.head), + title: createFullPageComponent(ReactDOM.title), + body: createFullPageComponent(ReactDOM.body) + }); + + + // This needs to happen after createFullPageComponent() otherwise the mixin + // gets double injected. + ReactCompositeComponent.injection.injectMixin(ReactBrowserComponentMixin); + + DOMProperty.injection.injectDOMPropertyConfig(HTMLDOMPropertyConfig); + DOMProperty.injection.injectDOMPropertyConfig(SVGDOMPropertyConfig); + + ReactEmptyComponent.injection.injectEmptyComponent(ReactDOM.script); + + ReactUpdates.injection.injectBatchingStrategy( + ReactDefaultBatchingStrategy + ); + + ReactRootIndex.injection.injectCreateReactRootIndex( + ClientReactRootIndex.createReactRootIndex + ); + + ReactComponent.injection.injectEnvironment(ReactComponentWorkerEnvironment); + + if (__DEV__) { + var url = (ExecutionEnvironment.canUseDOM && window.location.href) || ''; + if ((/[?&]react_perf\b/).test(url)) { + var ReactDefaultPerf = require('ReactDefaultPerf'); + ReactDefaultPerf.start(); + } + } +} + +module.exports = { + inject: inject +}; diff --git a/src/browser/worker/ReactWorkerMount.js b/src/browser/worker/ReactWorkerMount.js new file mode 100644 index 00000000000..545571b4008 --- /dev/null +++ b/src/browser/worker/ReactWorkerMount.js @@ -0,0 +1,139 @@ +/** + * 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 ReactWorkerMount + */ + +"use strict"; + +var ReactDOMNodeHandle = require('ReactDOMNodeHandle'); +var ReactDOMNodeHandleMapping = require('ReactDOMNodeHandleMapping'); +var ReactDOMNodeMappingRemote = require('ReactDOMNodeMappingRemote'); +var ReactPerf = require('ReactPerf'); + +var instantiateReactComponent = require('instantiateReactComponent'); +var invariant = require('invariant'); +var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); + +var ReactWorkerMount = { + /** + * Render a new component into the DOM. + * @param {ReactComponent} nextComponent component instance to render + * @param {DOMElement} container container to render into + * @param {boolean} shouldReuseMarkup if we should skip the markup insertion + * @return {ReactComponent} nextComponent + */ + _renderNewRootComponent: ReactPerf.measure( + 'ReactMount', + '_renderNewRootComponent', + function( + nextComponent, + containerHandle, + shouldReuseMarkup) { + var componentInstance = instantiateReactComponent(nextComponent); + + ReactDOMNodeMappingRemote.registerContainerHandle(containerHandle); + + var reactRootID = ReactDOMNodeHandleMapping.registerComponent( + componentInstance, + containerHandle + ); + + ReactDOMNodeMappingRemote.registerComponentInContainer( + reactRootID, + containerHandle + ); + + componentInstance.mountComponentIntoNode( + reactRootID, + containerHandle, + shouldReuseMarkup + ); + + return componentInstance; + } + ), + + /** + * Renders a React component into the DOM in the supplied `container`. + * + * If the React component was previously rendered into `container`, this will + * perform an update on it and only mutate the DOM as necessary to reflect the + * latest React component. + * + * @param {ReactDescriptor} nextDescriptor Component descriptor to render. + * @param {DOMElement} container DOM element to render into. + * @param {?function} callback function triggered on completion + * @return {ReactComponent} Component instance rendered in `container`. + */ + renderComponent: function(nextDescriptor, containerID, callback) { + var containerHandle = ReactDOMNodeHandle.getHandleForContainerID( + containerID + ); + + var prevComponent = ReactDOMNodeHandleMapping.getInstanceFromContainer(containerHandle); + + if (prevComponent) { + var prevDescriptor = prevComponent._descriptor; + if (shouldUpdateReactComponent(prevDescriptor, nextDescriptor)) { + return ReactWorkerMount._updateRootComponent( + prevComponent, + nextDescriptor, + containerHandle, + callback + ); + } else { + ReactWorkerMount.unmountComponentAtHandle(containerHandle); + } + } + + var component = ReactWorkerMount._renderNewRootComponent( + nextDescriptor, + containerHandle, + false // TODO: figure out hwo to reuse markup from a worker + ); + callback && callback.call(component); + return component; + }, + + _updateRootComponent: function( + prevComponent, + nextComponent, + containerHandle, + callback) { + var nextProps = nextComponent.props; + prevComponent.replaceProps(nextProps, callback); + + return prevComponent; + }, + + unmountComponentAtHandle: function(handle) { + ReactDOMNodeMappingRemote.unmountComponentAtHandle(handle); + }, + + /** + * This is a hook provided to support rendering React components while + * ensuring that the apparent scroll position of its `container` does not + * change. + * + * @param {DOMElement} container The `container` being rendered into. + * @param {function} renderCallback This must be called once to do the render. + */ + scrollMonitor: function(containerHandle, renderCallback) { + renderCallback(); + } +}; + +module.exports = ReactWorkerMount; diff --git a/src/browser/worker/ReactWorkerReconcileTransaction.js b/src/browser/worker/ReactWorkerReconcileTransaction.js new file mode 100644 index 00000000000..57d40b3f457 --- /dev/null +++ b/src/browser/worker/ReactWorkerReconcileTransaction.js @@ -0,0 +1,164 @@ +/** + * 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 ReactWorkerReconcileTransaction + * @typechecks static-only + */ + +"use strict"; + +var CallbackQueue = require('CallbackQueue'); +var PooledClass = require('PooledClass'); +var ReactEventEmitter = require('ReactEventEmitter'); +var ReactPutListenerQueue = require('ReactPutListenerQueue'); +var Transaction = require('Transaction'); + +var mixInto = require('mixInto'); + +/** + * Suppresses events (blur/focus) that could be inadvertently dispatched due to + * high level DOM manipulations (like temporarily removing a text input from the + * DOM). + */ +var EVENT_SUPPRESSION = { + /** + * @return {boolean} The enabled status of `ReactEventEmitter` before the + * reconciliation. + */ + initialize: function() { + var currentlyEnabled = ReactEventEmitter.isEnabled(); + ReactEventEmitter.setEnabled(false); + return currentlyEnabled; + }, + + /** + * @param {boolean} previouslyEnabled Enabled status of `ReactEventEmitter` + * before the reconciliation occured. `close` restores the previous value. + */ + close: function(previouslyEnabled) { + ReactEventEmitter.setEnabled(previouslyEnabled); + } +}; + +/** + * Provides a queue for collecting `componentDidMount` and + * `componentDidUpdate` callbacks during the the transaction. + */ +var ON_DOM_READY_QUEUEING = { + /** + * Initializes the internal `onDOMReady` queue. + */ + initialize: function() { + this.reactMountReady.reset(); + }, + + /** + * After DOM is flushed, invoke all registered `onDOMReady` callbacks. + */ + close: function() { + this.reactMountReady.notifyAll(); + } +}; + +var PUT_LISTENER_QUEUEING = { + initialize: function() { + this.putListenerQueue.reset(); + }, + + close: function() { + this.putListenerQueue.putListeners(); + } +}; + +/** + * Executed within the scope of the `Transaction` instance. Consider these as + * being member methods, but with an implied ordering while being isolated from + * each other. + */ +var TRANSACTION_WRAPPERS = [ + PUT_LISTENER_QUEUEING, + EVENT_SUPPRESSION, + ON_DOM_READY_QUEUEING +]; + +/** + * Currently: + * - The order that these are listed in the transaction is critical: + * - Suppresses events. + * - Restores selection range. + * + * Future: + * - Restore document/overflow scroll positions that were unintentionally + * modified via DOM insertions above the top viewport boundary. + * - Implement/integrate with customized constraint based layout system and keep + * track of which dimensions must be remeasured. + * + * @class ReactWorkerReconcileTransaction + */ +function ReactWorkerReconcileTransaction() { + this.reinitializeTransaction(); + // Only server-side rendering really needs this option (see + // `ReactServerRendering`), but server-side uses + // `ReactServerRenderingTransaction` instead. This option is here so that it's + // accessible and defaults to false when `ReactDOMComponent` and + // `ReactTextComponent` checks it in `mountComponent`.` + this.renderToStaticMarkup = false; + this.reactMountReady = CallbackQueue.getPooled(null); + this.putListenerQueue = ReactPutListenerQueue.getPooled(); +} + +var Mixin = { + /** + * @see Transaction + * @abstract + * @final + * @return {array} List of operation wrap proceedures. + * TODO: convert to array + */ + getTransactionWrappers: function() { + return TRANSACTION_WRAPPERS; + }, + + /** + * @return {object} The queue to collect `onDOMReady` callbacks with. + */ + getReactMountReady: function() { + return this.reactMountReady; + }, + + getPutListenerQueue: function() { + return this.putListenerQueue; + }, + + /** + * `PooledClass` looks for this, and will invoke this before allowing this + * instance to be resused. + */ + destructor: function() { + CallbackQueue.release(this.reactMountReady); + this.reactMountReady = null; + + ReactPutListenerQueue.release(this.putListenerQueue); + this.putListenerQueue = null; + } +}; + + +mixInto(ReactWorkerReconcileTransaction, Transaction.Mixin); +mixInto(ReactWorkerReconcileTransaction, Mixin); + +PooledClass.addPoolingTo(ReactWorkerReconcileTransaction); + +module.exports = ReactWorkerReconcileTransaction; diff --git a/src/core/ReactMultiChild.js b/src/core/ReactMultiChild.js index 608fe62d846..8aa337cde82 100644 --- a/src/core/ReactMultiChild.js +++ b/src/core/ReactMultiChild.js @@ -28,7 +28,7 @@ var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); /** * Updating children of a component may trigger recursive updates. The depth is - * used to batch recursive updates to render markup more efficiently. + * used to batch recursive updates to render image more efficiently. * * @type {number} * @private @@ -46,28 +46,28 @@ var updateDepth = 0; var updateQueue = []; /** - * Queue of markup to be rendered. + * Queue of image to be rendered. * * @type {array} * @private */ -var markupQueue = []; +var imageQueue = []; /** - * Enqueues markup to be rendered and inserted at a supplied index. + * Enqueues image to be rendered and inserted at a supplied index. * * @param {string} parentID ID of the parent component. - * @param {string} markup Markup that renders into an element. + * @param {string} image Image that renders into an element. * @param {number} toIndex Destination index. * @private */ -function enqueueMarkup(parentID, markup, toIndex) { +function enqueueImage(parentID, image, toIndex) { // NOTE: Null values reduce hidden classes. updateQueue.push({ parentID: parentID, parentNode: null, - type: ReactMultiChildUpdateTypes.INSERT_MARKUP, - markupIndex: markupQueue.push(markup) - 1, + type: ReactMultiChildUpdateTypes.INSERT_IMAGE, + imageIndex: imageQueue.push(image) - 1, textContent: null, fromIndex: null, toIndex: toIndex @@ -88,7 +88,7 @@ function enqueueMove(parentID, fromIndex, toIndex) { parentID: parentID, parentNode: null, type: ReactMultiChildUpdateTypes.MOVE_EXISTING, - markupIndex: null, + imageIndex: null, textContent: null, fromIndex: fromIndex, toIndex: toIndex @@ -108,7 +108,7 @@ function enqueueRemove(parentID, fromIndex) { parentID: parentID, parentNode: null, type: ReactMultiChildUpdateTypes.REMOVE_NODE, - markupIndex: null, + imageIndex: null, textContent: null, fromIndex: fromIndex, toIndex: null @@ -128,7 +128,7 @@ function enqueueTextContent(parentID, textContent) { parentID: parentID, parentNode: null, type: ReactMultiChildUpdateTypes.TEXT_CONTENT, - markupIndex: null, + imageIndex: null, textContent: textContent, fromIndex: null, toIndex: null @@ -144,7 +144,7 @@ function processQueue() { if (updateQueue.length) { ReactComponent.BackendIDOperations.dangerouslyProcessChildrenUpdates( updateQueue, - markupQueue + imageQueue ); clearQueue(); } @@ -157,7 +157,7 @@ function processQueue() { */ function clearQueue() { updateQueue.length = 0; - markupQueue.length = 0; + imageQueue.length = 0; } /** @@ -179,7 +179,7 @@ var ReactMultiChild = { /** * Generates a "mount image" for each of the supplied children. In the case - * of `ReactDOMComponent`, a mount image is a string of markup. + * of `ReactDOMComponent`, a mount image is a string of image. * * @param {?object} nestedChildren Nested child maps. * @return {array} An array of mounted representations. @@ -355,11 +355,11 @@ var ReactMultiChild = { * Creates a child component. * * @param {ReactComponent} child Component to create. - * @param {string} mountImage Markup to insert. + * @param {string} mountImage Image to insert. * @protected */ createChild: function(child, mountImage) { - enqueueMarkup(this._rootNodeID, mountImage, child._mountIndex); + enqueueImage(this._rootNodeID, mountImage, child._mountIndex); }, /** diff --git a/src/core/ReactMultiChildUpdateTypes.js b/src/core/ReactMultiChildUpdateTypes.js index 18cfd065e84..7667346300c 100644 --- a/src/core/ReactMultiChildUpdateTypes.js +++ b/src/core/ReactMultiChildUpdateTypes.js @@ -29,7 +29,7 @@ var keyMirror = require('keyMirror'); * @internal */ var ReactMultiChildUpdateTypes = keyMirror({ - INSERT_MARKUP: null, + INSERT_IMAGE: null, MOVE_EXISTING: null, REMOVE_NODE: null, TEXT_CONTENT: null diff --git a/src/core/__tests__/ReactComponent-test.js b/src/core/__tests__/ReactComponent-test.js index 71067e15eb4..929ba0a0c8c 100644 --- a/src/core/__tests__/ReactComponent-test.js +++ b/src/core/__tests__/ReactComponent-test.js @@ -37,14 +37,14 @@ describe('ReactComponent', function() { expect(function() { React.renderComponent(
, [container]); }).toThrow( - 'Invariant Violation: _registerComponent(...): Target container ' + + 'Invariant Violation: getInstanceFromContainer(...): Target container ' + 'is not a DOM element.' ); expect(function() { React.renderComponent(
, null); }).toThrow( - 'Invariant Violation: _registerComponent(...): Target container ' + + 'Invariant Violation: getInstanceFromContainer(...): Target container ' + 'is not a DOM element.' ); }); diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index e3a9d2af521..4649133db2e 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -30,6 +30,8 @@ var ReactPropTypes; var ReactServerRendering; var ReactTestUtils; var TogglingComponent; +var ReactDOMNodeMapping; +var ReactDoNotBindDeprecated; var cx; var reactComponentExpect; @@ -50,8 +52,8 @@ describe('ReactCompositeComponent', function() { ReactDoNotBindDeprecated = require('ReactDoNotBindDeprecated'); ReactPropTypes = require('ReactPropTypes'); ReactTestUtils = require('ReactTestUtils'); - ReactMount = require('ReactMount'); ReactServerRendering = require('ReactServerRendering'); + ReactDOMNodeMapping = require('ReactDOMNodeMapping'); MorphingComponent = React.createClass({ getInitialState: function() { @@ -316,7 +318,7 @@ describe('ReactCompositeComponent', function() { // rerender instance.setProps({renderAnchor: true, anchorClassOn: false}); var anchorID = instance.getAnchorID(); - var actualDOMAnchorNode = ReactMount.getNode(anchorID); + var actualDOMAnchorNode = ReactDOMNodeMapping.getNode(anchorID); expect(actualDOMAnchorNode.className).toBe(''); }); @@ -812,7 +814,7 @@ describe('ReactCompositeComponent', function() { var container = document.createElement('div'); var innerUnmounted = false; - spyOn(ReactMount, 'purgeID').andCallThrough(); + spyOn(ReactDOMNodeMapping, 'purgeID').andCallThrough(); var Component = React.createClass({ render: function() { @@ -823,11 +825,11 @@ describe('ReactCompositeComponent', function() { }); var Inner = React.createClass({ componentWillUnmount: function() { - // It's important that ReactMount.purgeID be called after any component + // It's important that ReactDOMNodeMapping.purgeID be called after any component // lifecycle methods, because a componentWillMount implementation is // likely call this.getDOMNode(), which will repopulate the node cache // after it's been cleared, causing a memory leak. - expect(ReactMount.purgeID.callCount).toBe(0); + expect(ReactDOMNodeMapping.purgeID.callCount).toBe(0); innerUnmounted = true; }, render: function() { @@ -841,7 +843,7 @@ describe('ReactCompositeComponent', function() { // , , and both
elements each call // unmountIDFromEnvironment which calls purgeID, for a total of 4. - expect(ReactMount.purgeID.callCount).toBe(4); + expect(ReactDOMNodeMapping.purgeID.callCount).toBe(4); }); it('should detect valid CompositeComponent classes', function() { diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js index b07b2adcac3..4f6743b4e37 100644 --- a/src/core/__tests__/ReactIdentity-test.js +++ b/src/core/__tests__/ReactIdentity-test.js @@ -22,7 +22,7 @@ var React; var ReactTestUtils; var reactComponentExpect; -var ReactMount; +var ReactDOMNodeMapping; describe('ReactIdentity', function() { @@ -31,12 +31,12 @@ describe('ReactIdentity', function() { React = require('React'); ReactTestUtils = require('ReactTestUtils'); reactComponentExpect = require('reactComponentExpect'); - ReactMount = require('ReactMount'); + ReactDOMNodeMapping = require('ReactDOMNodeMapping'); }); var idExp = /^\.[^.]+(.*)$/; function checkId(child, expectedId) { - var actual = idExp.exec(ReactMount.getID(child)); + var actual = idExp.exec(ReactDOMNodeMapping.getID(child)); var expected = idExp.exec(expectedId); expect(actual).toBeTruthy(); expect(expected).toBeTruthy(); @@ -294,11 +294,11 @@ describe('ReactIdentity', function() { wrapped = React.renderComponent(wrapped, document.createElement('div')); - var beforeID = ReactMount.getID(wrapped.getDOMNode().firstChild); + var beforeID = ReactDOMNodeMapping.getID(wrapped.getDOMNode().firstChild); wrapped.swap(); - var afterID = ReactMount.getID(wrapped.getDOMNode().firstChild); + var afterID = ReactDOMNodeMapping.getID(wrapped.getDOMNode().firstChild); expect(beforeID).not.toEqual(afterID); diff --git a/src/core/__tests__/ReactInstanceHandles-test.js b/src/core/__tests__/ReactInstanceHandles-test.js index 4a5fe2243ee..b5b87b74d7b 100644 --- a/src/core/__tests__/ReactInstanceHandles-test.js +++ b/src/core/__tests__/ReactInstanceHandles-test.js @@ -21,7 +21,7 @@ var React = require('React'); var ReactTestUtils = require('ReactTestUtils'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); /** * Ensure that all callbacks are invoked, passing this unique argument. @@ -78,7 +78,7 @@ describe('ReactInstanceHandles', function() { describe('isRenderedByReact', function() { it('should not crash on text nodes', function() { expect(function() { - ReactMount.isRenderedByReact(document.createTextNode('yolo')); + ReactDOMNodeMapping.isRenderedByReact(document.createTextNode('yolo')); }).not.toThrow(); }); }); @@ -91,14 +91,14 @@ describe('ReactInstanceHandles', function() { parentNode.appendChild(childNodeA); parentNode.appendChild(childNodeB); - ReactMount.setID(parentNode, '.0'); - ReactMount.setID(childNodeA, '.0.0'); - ReactMount.setID(childNodeB, '.0.0:1'); + ReactDOMNodeMapping.setID(parentNode, '.0'); + ReactDOMNodeMapping.setID(childNodeA, '.0.0'); + ReactDOMNodeMapping.setID(childNodeB, '.0.0:1'); expect( - ReactMount.findComponentRoot( + ReactDOMNodeMapping.findComponentRoot( parentNode, - ReactMount.getID(childNodeB) + ReactDOMNodeMapping.getID(childNodeB) ) ).toBe(childNodeB); }); @@ -110,14 +110,14 @@ describe('ReactInstanceHandles', function() { parentNode.appendChild(childNodeA); parentNode.appendChild(childNodeB); - ReactMount.setID(parentNode, '.0'); + ReactDOMNodeMapping.setID(parentNode, '.0'); // No ID on `childNodeA`. - ReactMount.setID(childNodeB, '.0.0:1'); + ReactDOMNodeMapping.setID(childNodeB, '.0.0:1'); expect( - ReactMount.findComponentRoot( + ReactDOMNodeMapping.findComponentRoot( parentNode, - ReactMount.getID(childNodeB) + ReactDOMNodeMapping.getID(childNodeB) ) ).toBe(childNodeB); }); @@ -129,19 +129,19 @@ describe('ReactInstanceHandles', function() { parentNode.appendChild(childNodeA); childNodeA.appendChild(childNodeB); - ReactMount.setID(parentNode, '.0'); + ReactDOMNodeMapping.setID(parentNode, '.0'); // No ID on `childNodeA`, it was "rendered by the browser". - ReactMount.setID(childNodeB, '.0.1:0'); + ReactDOMNodeMapping.setID(childNodeB, '.0.1:0'); - expect(ReactMount.findComponentRoot( + expect(ReactDOMNodeMapping.findComponentRoot( parentNode, - ReactMount.getID(childNodeB) + ReactDOMNodeMapping.getID(childNodeB) )).toBe(childNodeB); expect(function() { - ReactMount.findComponentRoot( + ReactDOMNodeMapping.findComponentRoot( parentNode, - ReactMount.getID(childNodeB) + ":junk" + ReactDOMNodeMapping.getID(childNodeB) + ":junk" ); }).toThrow( 'Invariant Violation: findComponentRoot(..., .0.1:0:junk): ' + diff --git a/src/core/__tests__/ReactMultiChildReconcile-test.js b/src/core/__tests__/ReactMultiChildReconcile-test.js index a560ac13d27..06066608faa 100644 --- a/src/core/__tests__/ReactMultiChildReconcile-test.js +++ b/src/core/__tests__/ReactMultiChildReconcile-test.js @@ -23,7 +23,7 @@ require('mock-modules'); var React = require('React'); var ReactTestUtils = require('ReactTestUtils'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var mapObject = require('mapObject'); @@ -193,7 +193,7 @@ function verifyDomOrderingAccurate(parentInstance, statusDisplays) { var i; var orderedDomIds = []; for (i=0; i < statusDisplayNodes.length; i++) { - orderedDomIds.push(ReactMount.getID(statusDisplayNodes[i])); + orderedDomIds.push(ReactDOMNodeMapping.getID(statusDisplayNodes[i])); } var orderedLogicalIds = []; diff --git a/src/test/ReactDefaultPerf.js b/src/test/ReactDefaultPerf.js index a302a65f063..27df544c894 100644 --- a/src/test/ReactDefaultPerf.js +++ b/src/test/ReactDefaultPerf.js @@ -21,7 +21,7 @@ var DOMProperty = require('DOMProperty'); var ReactDefaultPerfAnalysis = require('ReactDefaultPerfAnalysis'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var ReactPerf = require('ReactPerf'); var performanceNow = require('performanceNow'); @@ -175,7 +175,7 @@ var ReactDefaultPerf = { totalTime = performanceNow() - start; if (fnName === 'mountImageIntoNode') { - var mountID = ReactMount.getID(args[1]); + var mountID = ReactDOMNodeMapping.getID(args[1]); ReactDefaultPerf._recordWrite(mountID, fnName, totalTime, args[0]); } else if (fnName === 'dangerouslyProcessChildrenUpdates') { // special format diff --git a/src/test/ReactDefaultPerfAnalysis.js b/src/test/ReactDefaultPerfAnalysis.js index e497abc9908..0d1f93adaf4 100644 --- a/src/test/ReactDefaultPerfAnalysis.js +++ b/src/test/ReactDefaultPerfAnalysis.js @@ -29,7 +29,7 @@ var DOM_OPERATION_TYPES = { 'updatePropertyByID': 'update attribute', 'deletePropertyByID': 'delete attribute', 'updateStylesByID': 'update styles', - 'updateInnerHTMLByID': 'set innerHTML', + 'updateImageByID': 'set innerHTML', 'dangerouslyReplaceNodeWithMarkupByID': 'replace' }; diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index cac11e9bbae..c372eaf25e3 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -25,7 +25,7 @@ var React = require('React'); var ReactDescriptor = require('ReactDescriptor'); var ReactDOM = require('ReactDOM'); var ReactEventEmitter = require('ReactEventEmitter'); -var ReactMount = require('ReactMount'); +var ReactDOMNodeMapping = require('ReactDOMNodeMapping'); var ReactTextComponent = require('ReactTextComponent'); var ReactUpdates = require('ReactUpdates'); var SyntheticEvent = require('SyntheticEvent'); @@ -259,12 +259,11 @@ var ReactTestUtils = { * @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent. */ simulateNativeEventOnNode: function(topLevelType, node, fakeNativeEvent) { - var virtualHandler = - ReactEventEmitter.TopLevelCallbackCreator.createTopLevelCallback( - topLevelType - ); fakeNativeEvent.target = node; - virtualHandler(fakeNativeEvent); + ReactEventEmitter.ReactEventListener.dispatchEvent( + topLevelType, + fakeNativeEvent + ); }, /** @@ -320,7 +319,7 @@ function makeSimulator(eventType) { // properly destroying any properties assigned from `eventData` upon release var event = new SyntheticEvent( ReactEventEmitter.eventNameDispatchConfigs[eventType], - ReactMount.getID(node), + ReactDOMNodeMapping.getID(node), fakeNativeEvent ); mergeInto(event, eventData); diff --git a/src/utils/__tests__/cloneWithProps-test.js b/src/utils/__tests__/cloneWithProps-test.js index ab80af71939..c2c5cd14aad 100644 --- a/src/utils/__tests__/cloneWithProps-test.js +++ b/src/utils/__tests__/cloneWithProps-test.js @@ -28,6 +28,7 @@ var mocks = require('mocks'); var React; var ReactTestUtils; +var emptyObject; var onlyChild; var cloneWithProps; var emptyObject; @@ -37,6 +38,7 @@ describe('cloneWithProps', function() { beforeEach(function() { React = require('React'); ReactTestUtils = require('ReactTestUtils'); + emptyObject = require('emptyObject'); onlyChild = require('onlyChild'); cloneWithProps = require('cloneWithProps'); emptyObject = require('emptyObject'); diff --git a/src/vendor/core/ExecutionEnvironment.js b/src/vendor/core/ExecutionEnvironment.js index 059eea4a977..25bbde55993 100644 --- a/src/vendor/core/ExecutionEnvironment.js +++ b/src/vendor/core/ExecutionEnvironment.js @@ -20,8 +20,22 @@ "use strict"; +var invariant = require('invariant'); + var canUseDOM = typeof window !== 'undefined'; +var globalObj; + +if (typeof window !== 'undefined') { + globalObj = window; +} else if (typeof self !== 'undefined') { + globalObj = self; +} else if (typeof global !== 'undefined') { + globalObj = global; +} + +invariant(globalObj, 'ExecutionEnvironment: could not find global object'); + /** * Simple, lightweight module assisting with the detection and context of * Worker. Helps avoid circular dependencies and allows code to reason about @@ -37,7 +51,9 @@ var ExecutionEnvironment = { canUseEventListeners: canUseDOM && (window.addEventListener || window.attachEvent), - isInWorker: !canUseDOM // For now, this is true - might change in the future. + isInWorker: !canUseDOM, // For now, this is true - might change in the future. + + global: globalObj };