You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
504 lines
16 KiB
504 lines
16 KiB
/** |
|
* Copyright 2013-2015, Facebook, Inc. |
|
* All rights reserved. |
|
* |
|
* This source code is licensed under the BSD-style license found in the |
|
* LICENSE file in the root directory of this source tree. An additional grant |
|
* of patent rights can be found in the PATENTS file in the same directory. |
|
* |
|
* @providesModule ReactDOMComponent |
|
* @typechecks static-only |
|
*/ |
|
|
|
/* global hasOwnProperty:true */ |
|
|
|
'use strict'; |
|
|
|
var CSSPropertyOperations = require("./CSSPropertyOperations"); |
|
var DOMProperty = require("./DOMProperty"); |
|
var DOMPropertyOperations = require("./DOMPropertyOperations"); |
|
var ReactBrowserEventEmitter = require("./ReactBrowserEventEmitter"); |
|
var ReactComponentBrowserEnvironment = |
|
require("./ReactComponentBrowserEnvironment"); |
|
var ReactMount = require("./ReactMount"); |
|
var ReactMultiChild = require("./ReactMultiChild"); |
|
var ReactPerf = require("./ReactPerf"); |
|
|
|
var assign = require("./Object.assign"); |
|
var escapeTextContentForBrowser = require("./escapeTextContentForBrowser"); |
|
var invariant = require("./invariant"); |
|
var isEventSupported = require("./isEventSupported"); |
|
var keyOf = require("./keyOf"); |
|
var warning = require("./warning"); |
|
|
|
var deleteListener = ReactBrowserEventEmitter.deleteListener; |
|
var listenTo = ReactBrowserEventEmitter.listenTo; |
|
var registrationNameModules = ReactBrowserEventEmitter.registrationNameModules; |
|
|
|
// For quickly matching children type, to test if can be treated as content. |
|
var CONTENT_TYPES = {'string': true, 'number': true}; |
|
|
|
var STYLE = keyOf({style: null}); |
|
|
|
var ELEMENT_NODE_TYPE = 1; |
|
|
|
/** |
|
* Optionally injectable operations for mutating the DOM |
|
*/ |
|
var BackendIDOperations = null; |
|
|
|
/** |
|
* @param {?object} props |
|
*/ |
|
function assertValidProps(props) { |
|
if (!props) { |
|
return; |
|
} |
|
// Note the use of `==` which checks for null or undefined. |
|
if (props.dangerouslySetInnerHTML != null) { |
|
("production" !== process.env.NODE_ENV ? invariant( |
|
props.children == null, |
|
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.' |
|
) : invariant(props.children == null)); |
|
("production" !== process.env.NODE_ENV ? invariant( |
|
props.dangerouslySetInnerHTML.__html != null, |
|
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + |
|
'Please visit http://fb.me/react-invariant-dangerously-set-inner-html ' + |
|
'for more information.' |
|
) : invariant(props.dangerouslySetInnerHTML.__html != null)); |
|
} |
|
if ("production" !== process.env.NODE_ENV) { |
|
("production" !== process.env.NODE_ENV ? warning( |
|
props.innerHTML == null, |
|
'Directly setting property `innerHTML` is not permitted. ' + |
|
'For more information, lookup documentation on `dangerouslySetInnerHTML`.' |
|
) : null); |
|
("production" !== process.env.NODE_ENV ? warning( |
|
!props.contentEditable || props.children == null, |
|
'A component is `contentEditable` and contains `children` managed by ' + |
|
'React. It is now your responsibility to guarantee that none of ' + |
|
'those nodes are unexpectedly modified or duplicated. This is ' + |
|
'probably not intentional.' |
|
) : null); |
|
} |
|
("production" !== process.env.NODE_ENV ? invariant( |
|
props.style == null || typeof props.style === 'object', |
|
'The `style` prop expects a mapping from style properties to values, ' + |
|
'not a string. For example, style={{marginRight: spacing + \'em\'}} when ' + |
|
'using JSX.' |
|
) : invariant(props.style == null || typeof props.style === 'object')); |
|
} |
|
|
|
function putListener(id, registrationName, listener, transaction) { |
|
if ("production" !== process.env.NODE_ENV) { |
|
// IE8 has no API for event capturing and the `onScroll` event doesn't |
|
// bubble. |
|
("production" !== process.env.NODE_ENV ? warning( |
|
registrationName !== 'onScroll' || isEventSupported('scroll', true), |
|
'This browser doesn\'t support the `onScroll` event' |
|
) : null); |
|
} |
|
var container = ReactMount.findReactContainerForID(id); |
|
if (container) { |
|
var doc = container.nodeType === ELEMENT_NODE_TYPE ? |
|
container.ownerDocument : |
|
container; |
|
listenTo(registrationName, doc); |
|
} |
|
transaction.getPutListenerQueue().enqueuePutListener( |
|
id, |
|
registrationName, |
|
listener |
|
); |
|
} |
|
|
|
// For HTML, certain tags should omit their close tag. We keep a whitelist for |
|
// those special cased tags. |
|
|
|
var omittedCloseTags = { |
|
'area': true, |
|
'base': true, |
|
'br': true, |
|
'col': true, |
|
'embed': true, |
|
'hr': true, |
|
'img': true, |
|
'input': true, |
|
'keygen': true, |
|
'link': true, |
|
'meta': true, |
|
'param': true, |
|
'source': true, |
|
'track': true, |
|
'wbr': true |
|
// NOTE: menuitem's close tag should be omitted, but that causes problems. |
|
}; |
|
|
|
// We accept any tag to be rendered but since this gets injected into abitrary |
|
// HTML, we want to make sure that it's a safe tag. |
|
// http://www.w3.org/TR/REC-xml/#NT-Name |
|
|
|
var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset |
|
var validatedTagCache = {}; |
|
var hasOwnProperty = {}.hasOwnProperty; |
|
|
|
function validateDangerousTag(tag) { |
|
if (!hasOwnProperty.call(validatedTagCache, tag)) { |
|
("production" !== process.env.NODE_ENV ? invariant(VALID_TAG_REGEX.test(tag), 'Invalid tag: %s', tag) : invariant(VALID_TAG_REGEX.test(tag))); |
|
validatedTagCache[tag] = true; |
|
} |
|
} |
|
|
|
/** |
|
* Creates a new React class that is idempotent and capable of containing other |
|
* React components. It accepts event listeners and DOM properties that are |
|
* valid according to `DOMProperty`. |
|
* |
|
* - Event listeners: `onClick`, `onMouseDown`, etc. |
|
* - DOM properties: `className`, `name`, `title`, etc. |
|
* |
|
* The `style` property functions differently from the DOM API. It accepts an |
|
* object mapping of style properties to values. |
|
* |
|
* @constructor ReactDOMComponent |
|
* @extends ReactMultiChild |
|
*/ |
|
function ReactDOMComponent(tag) { |
|
validateDangerousTag(tag); |
|
this._tag = tag; |
|
this._renderedChildren = null; |
|
this._previousStyleCopy = null; |
|
this._rootNodeID = null; |
|
} |
|
|
|
ReactDOMComponent.displayName = 'ReactDOMComponent'; |
|
|
|
ReactDOMComponent.Mixin = { |
|
|
|
construct: function(element) { |
|
this._currentElement = element; |
|
}, |
|
|
|
/** |
|
* Generates root tag markup then recurses. This method has side effects and |
|
* is not idempotent. |
|
* |
|
* @internal |
|
* @param {string} rootID The root DOM ID for this node. |
|
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction |
|
* @return {string} The computed markup. |
|
*/ |
|
mountComponent: function(rootID, transaction, context) { |
|
this._rootNodeID = rootID; |
|
assertValidProps(this._currentElement.props); |
|
var closeTag = omittedCloseTags[this._tag] ? '' : '</' + this._tag + '>'; |
|
return ( |
|
this._createOpenTagMarkupAndPutListeners(transaction) + |
|
this._createContentMarkup(transaction, context) + |
|
closeTag |
|
); |
|
}, |
|
|
|
/** |
|
* Creates markup for the open tag and all attributes. |
|
* |
|
* This method has side effects because events get registered. |
|
* |
|
* Iterating over object properties is faster than iterating over arrays. |
|
* @see http://jsperf.com/obj-vs-arr-iteration |
|
* |
|
* @private |
|
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction |
|
* @return {string} Markup of opening tag. |
|
*/ |
|
_createOpenTagMarkupAndPutListeners: function(transaction) { |
|
var props = this._currentElement.props; |
|
var ret = '<' + this._tag; |
|
|
|
for (var propKey in props) { |
|
if (!props.hasOwnProperty(propKey)) { |
|
continue; |
|
} |
|
var propValue = props[propKey]; |
|
if (propValue == null) { |
|
continue; |
|
} |
|
if (registrationNameModules.hasOwnProperty(propKey)) { |
|
putListener(this._rootNodeID, propKey, propValue, transaction); |
|
} else { |
|
if (propKey === STYLE) { |
|
if (propValue) { |
|
propValue = this._previousStyleCopy = assign({}, props.style); |
|
} |
|
propValue = CSSPropertyOperations.createMarkupForStyles(propValue); |
|
} |
|
var markup = |
|
DOMPropertyOperations.createMarkupForProperty(propKey, propValue); |
|
if (markup) { |
|
ret += ' ' + markup; |
|
} |
|
} |
|
} |
|
|
|
// For static pages, no need to put React ID and checksum. Saves lots of |
|
// bytes. |
|
if (transaction.renderToStaticMarkup) { |
|
return ret + '>'; |
|
} |
|
|
|
var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID); |
|
return ret + ' ' + markupForID + '>'; |
|
}, |
|
|
|
/** |
|
* Creates markup for the content between the tags. |
|
* |
|
* @private |
|
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction |
|
* @param {object} context |
|
* @return {string} Content markup. |
|
*/ |
|
_createContentMarkup: function(transaction, context) { |
|
var prefix = ''; |
|
if (this._tag === 'listing' || |
|
this._tag === 'pre' || |
|
this._tag === 'textarea') { |
|
// Add an initial newline because browsers ignore the first newline in |
|
// a <listing>, <pre>, or <textarea> as an "authoring convenience" -- see |
|
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody. |
|
prefix = '\n'; |
|
} |
|
|
|
var props = this._currentElement.props; |
|
|
|
// Intentional use of != to avoid catching zero/false. |
|
var innerHTML = props.dangerouslySetInnerHTML; |
|
if (innerHTML != null) { |
|
if (innerHTML.__html != null) { |
|
return prefix + innerHTML.__html; |
|
} |
|
} else { |
|
var contentToUse = |
|
CONTENT_TYPES[typeof props.children] ? props.children : null; |
|
var childrenToUse = contentToUse != null ? null : props.children; |
|
if (contentToUse != null) { |
|
return prefix + escapeTextContentForBrowser(contentToUse); |
|
} else if (childrenToUse != null) { |
|
var mountImages = this.mountChildren( |
|
childrenToUse, |
|
transaction, |
|
context |
|
); |
|
return prefix + mountImages.join(''); |
|
} |
|
} |
|
return prefix; |
|
}, |
|
|
|
receiveComponent: function(nextElement, transaction, context) { |
|
var prevElement = this._currentElement; |
|
this._currentElement = nextElement; |
|
this.updateComponent(transaction, prevElement, nextElement, context); |
|
}, |
|
|
|
/** |
|
* Updates a native DOM component after it has already been allocated and |
|
* attached to the DOM. Reconciles the root DOM node, then recurses. |
|
* |
|
* @param {ReactReconcileTransaction} transaction |
|
* @param {ReactElement} prevElement |
|
* @param {ReactElement} nextElement |
|
* @internal |
|
* @overridable |
|
*/ |
|
updateComponent: function(transaction, prevElement, nextElement, context) { |
|
assertValidProps(this._currentElement.props); |
|
this._updateDOMProperties(prevElement.props, transaction); |
|
this._updateDOMChildren(prevElement.props, transaction, context); |
|
}, |
|
|
|
/** |
|
* Reconciles the properties by detecting differences in property values and |
|
* updating the DOM as necessary. This function is probably the single most |
|
* critical path for performance optimization. |
|
* |
|
* TODO: Benchmark whether checking for changed values in memory actually |
|
* improves performance (especially statically positioned elements). |
|
* TODO: Benchmark the effects of putting this at the top since 99% of props |
|
* do not change for a given reconciliation. |
|
* TODO: Benchmark areas that can be improved with caching. |
|
* |
|
* @private |
|
* @param {object} lastProps |
|
* @param {ReactReconcileTransaction} transaction |
|
*/ |
|
_updateDOMProperties: function(lastProps, transaction) { |
|
var nextProps = this._currentElement.props; |
|
var propKey; |
|
var styleName; |
|
var styleUpdates; |
|
for (propKey in lastProps) { |
|
if (nextProps.hasOwnProperty(propKey) || |
|
!lastProps.hasOwnProperty(propKey)) { |
|
continue; |
|
} |
|
if (propKey === STYLE) { |
|
var lastStyle = this._previousStyleCopy; |
|
for (styleName in lastStyle) { |
|
if (lastStyle.hasOwnProperty(styleName)) { |
|
styleUpdates = styleUpdates || {}; |
|
styleUpdates[styleName] = ''; |
|
} |
|
} |
|
this._previousStyleCopy = null; |
|
} else if (registrationNameModules.hasOwnProperty(propKey)) { |
|
deleteListener(this._rootNodeID, propKey); |
|
} else if ( |
|
DOMProperty.isStandardName[propKey] || |
|
DOMProperty.isCustomAttribute(propKey)) { |
|
BackendIDOperations.deletePropertyByID( |
|
this._rootNodeID, |
|
propKey |
|
); |
|
} |
|
} |
|
for (propKey in nextProps) { |
|
var nextProp = nextProps[propKey]; |
|
var lastProp = propKey === STYLE ? |
|
this._previousStyleCopy : |
|
lastProps[propKey]; |
|
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) { |
|
continue; |
|
} |
|
if (propKey === STYLE) { |
|
if (nextProp) { |
|
nextProp = this._previousStyleCopy = assign({}, nextProp); |
|
} else { |
|
this._previousStyleCopy = null; |
|
} |
|
if (lastProp) { |
|
// Unset styles on `lastProp` but not on `nextProp`. |
|
for (styleName in lastProp) { |
|
if (lastProp.hasOwnProperty(styleName) && |
|
(!nextProp || !nextProp.hasOwnProperty(styleName))) { |
|
styleUpdates = styleUpdates || {}; |
|
styleUpdates[styleName] = ''; |
|
} |
|
} |
|
// Update styles that changed since `lastProp`. |
|
for (styleName in nextProp) { |
|
if (nextProp.hasOwnProperty(styleName) && |
|
lastProp[styleName] !== nextProp[styleName]) { |
|
styleUpdates = styleUpdates || {}; |
|
styleUpdates[styleName] = nextProp[styleName]; |
|
} |
|
} |
|
} else { |
|
// Relies on `updateStylesByID` not mutating `styleUpdates`. |
|
styleUpdates = nextProp; |
|
} |
|
} else if (registrationNameModules.hasOwnProperty(propKey)) { |
|
putListener(this._rootNodeID, propKey, nextProp, transaction); |
|
} else if ( |
|
DOMProperty.isStandardName[propKey] || |
|
DOMProperty.isCustomAttribute(propKey)) { |
|
BackendIDOperations.updatePropertyByID( |
|
this._rootNodeID, |
|
propKey, |
|
nextProp |
|
); |
|
} |
|
} |
|
if (styleUpdates) { |
|
BackendIDOperations.updateStylesByID( |
|
this._rootNodeID, |
|
styleUpdates |
|
); |
|
} |
|
}, |
|
|
|
/** |
|
* Reconciles the children with the various properties that affect the |
|
* children content. |
|
* |
|
* @param {object} lastProps |
|
* @param {ReactReconcileTransaction} transaction |
|
*/ |
|
_updateDOMChildren: function(lastProps, transaction, context) { |
|
var nextProps = this._currentElement.props; |
|
|
|
var lastContent = |
|
CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null; |
|
var nextContent = |
|
CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null; |
|
|
|
var lastHtml = |
|
lastProps.dangerouslySetInnerHTML && |
|
lastProps.dangerouslySetInnerHTML.__html; |
|
var nextHtml = |
|
nextProps.dangerouslySetInnerHTML && |
|
nextProps.dangerouslySetInnerHTML.__html; |
|
|
|
// Note the use of `!=` which checks for null or undefined. |
|
var lastChildren = lastContent != null ? null : lastProps.children; |
|
var nextChildren = nextContent != null ? null : nextProps.children; |
|
|
|
// If we're switching from children to content/html or vice versa, remove |
|
// the old content |
|
var lastHasContentOrHtml = lastContent != null || lastHtml != null; |
|
var nextHasContentOrHtml = nextContent != null || nextHtml != null; |
|
if (lastChildren != null && nextChildren == null) { |
|
this.updateChildren(null, transaction, context); |
|
} else if (lastHasContentOrHtml && !nextHasContentOrHtml) { |
|
this.updateTextContent(''); |
|
} |
|
|
|
if (nextContent != null) { |
|
if (lastContent !== nextContent) { |
|
this.updateTextContent('' + nextContent); |
|
} |
|
} else if (nextHtml != null) { |
|
if (lastHtml !== nextHtml) { |
|
BackendIDOperations.updateInnerHTMLByID( |
|
this._rootNodeID, |
|
nextHtml |
|
); |
|
} |
|
} else if (nextChildren != null) { |
|
this.updateChildren(nextChildren, transaction, context); |
|
} |
|
}, |
|
|
|
/** |
|
* Destroys all event registrations for this instance. Does not remove from |
|
* the DOM. That must be done by the parent. |
|
* |
|
* @internal |
|
*/ |
|
unmountComponent: function() { |
|
this.unmountChildren(); |
|
ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID); |
|
ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID); |
|
this._rootNodeID = null; |
|
} |
|
|
|
}; |
|
|
|
ReactPerf.measureMethods(ReactDOMComponent, 'ReactDOMComponent', { |
|
mountComponent: 'mountComponent', |
|
updateComponent: 'updateComponent' |
|
}); |
|
|
|
assign( |
|
ReactDOMComponent.prototype, |
|
ReactDOMComponent.Mixin, |
|
ReactMultiChild.Mixin |
|
); |
|
|
|
ReactDOMComponent.injection = { |
|
injectIDOperations: function(IDOperations) { |
|
ReactDOMComponent.BackendIDOperations = BackendIDOperations = IDOperations; |
|
} |
|
}; |
|
|
|
module.exports = ReactDOMComponent;
|
|
|