diff --git a/Gruntfile.js b/Gruntfile.js index de406e42ca4e1..cf1fb14bdbfc8 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 0000000000000..4a9d53c266c91 --- /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 0000000000000..680d68d33a905 --- /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 61481e4353414..5733e06859d56 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 acafefb4af4be..d66230cebf398 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 0000000000000..5bb2ea6b8206c --- /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 0000000000000..3d95de7bcbc49 --- /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 0000000000000..ce4c888deca15 --- /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 5f1956d7d9db4..1eef2d4177d57 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 e81966e261817..230b2891c9379 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 0000000000000..6dfb2e302cc69 --- /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 0000000000000..4a3d6ab71df50 --- /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 0000000000000..83a2af6938857 --- /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 da0fadb6e20ee..47984d79bd769 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 2aeaecbedfb19..5c169a9bb5687 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 6dbfaa4399566..7aaef18f6a82f 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 dd61e4b1ade71..693a29a9272c0 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 37bc41b50f72d..dd77d3ea1f489 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 895ce51238e81..c75cf1d25f456 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 02537f3af3b08..5657c9b22d40f 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 efb81599918ef..b64eb2d4aa177 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 a04bb79067170..e0b2b0abbd110 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 e8ecbbf230276..ca43ae0d19cee 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 ea206cd065b90..f275774ee433b 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 0000000000000..168f48e4c605c --- /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 a981a93f9b90d..4734943d5afb2 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 0000000000000..68faf57374271 --- /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 aef2eef09fa62..0000000000000 --- 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 c51a2128003cc..0000000000000 --- 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 e898b5899488f..22ea3e0a44187 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 0000000000000..e29cde105c22d --- /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 ccc279f2e29e4..78d42b176ee89 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 a1b93f04ec3aa..e21798598518f 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 d077a670628d5..d6dd91a8a9a99 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 c73ddb461f611..c2796c313a569 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 8a545252eded6..bf9e8202afec7 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 e2e3de2005a7d..0e602dcf27d2d 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 621a517220bf6..375917c289288 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 b8696be86085b..45429fb2344fd 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 46b154f69d6dd..663196614f603 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 0437f60513c07..8ffec61ed7c9f 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 3ecf18194dcad..10c0c228402a3 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 0000000000000..51a2591376892 --- /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 0000000000000..16b075c5ccb15 --- /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 0000000000000..823cee57125e8 --- /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 0000000000000..fcab5ddae423d --- /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 0000000000000..4b974a628d882 --- /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 0000000000000..545571b4008da --- /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 0000000000000..57d40b3f45752 --- /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 608fe62d8461a..8aa337cde8200 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 18cfd065e8490..7667346300cc4 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 71067e15eb4dd..929ba0a0c8c9e 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 e3a9d2af521a8..4649133db2ed3 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 b07b2adcac3dc..4f6743b4e3732 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 4a5fe2243ee9e..b5b87b74d7b4c 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 a560ac13d2741..06066608faac9 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 a302a65f06351..27df544c894ef 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 e497abc99086a..0d1f93adaf49e 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 cac11e9bbae7f..c372eaf25e3ab 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 ab80af71939e7..c2c5cd14aadf0 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 059eea4a97782..25bbde55993ac 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 };