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.
505 lines
16 KiB
505 lines
16 KiB
10 years ago
|
/**
|
||
|
* 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;
|