949 lines
36 KiB
JavaScript
949 lines
36 KiB
JavaScript
/**
|
|
* Copyright 2013-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*
|
|
* @providesModule ReactDOMComponent
|
|
*/
|
|
|
|
/* global hasOwnProperty:true */
|
|
|
|
'use strict';
|
|
|
|
var _assign = require('object-assign');
|
|
|
|
var AutoFocusUtils = require('./AutoFocusUtils');
|
|
var CSSPropertyOperations = require('./CSSPropertyOperations');
|
|
var DOMLazyTree = require('./DOMLazyTree');
|
|
var DOMNamespaces = require('./DOMNamespaces');
|
|
var DOMProperty = require('./DOMProperty');
|
|
var DOMPropertyOperations = require('./DOMPropertyOperations');
|
|
var EventConstants = require('./EventConstants');
|
|
var EventPluginHub = require('./EventPluginHub');
|
|
var EventPluginRegistry = require('./EventPluginRegistry');
|
|
var ReactBrowserEventEmitter = require('./ReactBrowserEventEmitter');
|
|
var ReactComponentBrowserEnvironment = require('./ReactComponentBrowserEnvironment');
|
|
var ReactDOMButton = require('./ReactDOMButton');
|
|
var ReactDOMComponentFlags = require('./ReactDOMComponentFlags');
|
|
var ReactDOMComponentTree = require('./ReactDOMComponentTree');
|
|
var ReactDOMInput = require('./ReactDOMInput');
|
|
var ReactDOMOption = require('./ReactDOMOption');
|
|
var ReactDOMSelect = require('./ReactDOMSelect');
|
|
var ReactDOMTextarea = require('./ReactDOMTextarea');
|
|
var ReactInstrumentation = require('./ReactInstrumentation');
|
|
var ReactMultiChild = require('./ReactMultiChild');
|
|
var ReactServerRenderingTransaction = require('./ReactServerRenderingTransaction');
|
|
|
|
var emptyFunction = require('fbjs/lib/emptyFunction');
|
|
var escapeTextContentForBrowser = require('./escapeTextContentForBrowser');
|
|
var invariant = require('fbjs/lib/invariant');
|
|
var isEventSupported = require('./isEventSupported');
|
|
var keyOf = require('fbjs/lib/keyOf');
|
|
var shallowEqual = require('fbjs/lib/shallowEqual');
|
|
var validateDOMNesting = require('./validateDOMNesting');
|
|
var warning = require('fbjs/lib/warning');
|
|
|
|
var Flags = ReactDOMComponentFlags;
|
|
var deleteListener = EventPluginHub.deleteListener;
|
|
var getNode = ReactDOMComponentTree.getNodeFromInstance;
|
|
var listenTo = ReactBrowserEventEmitter.listenTo;
|
|
var registrationNameModules = EventPluginRegistry.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 HTML = keyOf({ __html: null });
|
|
var RESERVED_PROPS = {
|
|
children: null,
|
|
dangerouslySetInnerHTML: null,
|
|
suppressContentEditableWarning: null
|
|
};
|
|
|
|
// Node type for document fragments (Node.DOCUMENT_FRAGMENT_NODE).
|
|
var DOC_FRAGMENT_TYPE = 11;
|
|
|
|
function getDeclarationErrorAddendum(internalInstance) {
|
|
if (internalInstance) {
|
|
var owner = internalInstance._currentElement._owner || null;
|
|
if (owner) {
|
|
var name = owner.getName();
|
|
if (name) {
|
|
return ' This DOM node was rendered by `' + name + '`.';
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function friendlyStringify(obj) {
|
|
if (typeof obj === 'object') {
|
|
if (Array.isArray(obj)) {
|
|
return '[' + obj.map(friendlyStringify).join(', ') + ']';
|
|
} else {
|
|
var pairs = [];
|
|
for (var key in obj) {
|
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
var keyEscaped = /^[a-z$_][\w$_]*$/i.test(key) ? key : JSON.stringify(key);
|
|
pairs.push(keyEscaped + ': ' + friendlyStringify(obj[key]));
|
|
}
|
|
}
|
|
return '{' + pairs.join(', ') + '}';
|
|
}
|
|
} else if (typeof obj === 'string') {
|
|
return JSON.stringify(obj);
|
|
} else if (typeof obj === 'function') {
|
|
return '[function object]';
|
|
}
|
|
// Differs from JSON.stringify in that undefined because undefined and that
|
|
// inf and nan don't become null
|
|
return String(obj);
|
|
}
|
|
|
|
var styleMutationWarning = {};
|
|
|
|
function checkAndWarnForMutatedStyle(style1, style2, component) {
|
|
if (style1 == null || style2 == null) {
|
|
return;
|
|
}
|
|
if (shallowEqual(style1, style2)) {
|
|
return;
|
|
}
|
|
|
|
var componentName = component._tag;
|
|
var owner = component._currentElement._owner;
|
|
var ownerName;
|
|
if (owner) {
|
|
ownerName = owner.getName();
|
|
}
|
|
|
|
var hash = ownerName + '|' + componentName;
|
|
|
|
if (styleMutationWarning.hasOwnProperty(hash)) {
|
|
return;
|
|
}
|
|
|
|
styleMutationWarning[hash] = true;
|
|
|
|
process.env.NODE_ENV !== 'production' ? warning(false, '`%s` was passed a style object that has previously been mutated. ' + 'Mutating `style` is deprecated. Consider cloning it beforehand. Check ' + 'the `render` %s. Previous style: %s. Mutated style: %s.', componentName, owner ? 'of `' + ownerName + '`' : 'using <' + componentName + '>', friendlyStringify(style1), friendlyStringify(style2)) : void 0;
|
|
}
|
|
|
|
/**
|
|
* @param {object} component
|
|
* @param {?object} props
|
|
*/
|
|
function assertValidProps(component, props) {
|
|
if (!props) {
|
|
return;
|
|
}
|
|
// Note the use of `==` which checks for null or undefined.
|
|
if (voidElementTags[component._tag]) {
|
|
!(props.children == null && props.dangerouslySetInnerHTML == null) ? process.env.NODE_ENV !== 'production' ? invariant(false, '%s is a void element tag and must not have `children` or ' + 'use `props.dangerouslySetInnerHTML`.%s', component._tag, component._currentElement._owner ? ' Check the render method of ' + component._currentElement._owner.getName() + '.' : '') : invariant(false) : void 0;
|
|
}
|
|
if (props.dangerouslySetInnerHTML != null) {
|
|
!(props.children == null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.') : invariant(false) : void 0;
|
|
!(typeof props.dangerouslySetInnerHTML === 'object' && HTML in props.dangerouslySetInnerHTML) ? process.env.NODE_ENV !== 'production' ? invariant(false, '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + 'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' + 'for more information.') : invariant(false) : void 0;
|
|
}
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
process.env.NODE_ENV !== 'production' ? warning(props.innerHTML == null, 'Directly setting property `innerHTML` is not permitted. ' + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.') : void 0;
|
|
process.env.NODE_ENV !== 'production' ? warning(props.suppressContentEditableWarning || !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.') : void 0;
|
|
process.env.NODE_ENV !== 'production' ? warning(props.onFocusIn == null && props.onFocusOut == null, 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' + 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' + 'are not needed/supported by React.') : void 0;
|
|
}
|
|
!(props.style == null || typeof props.style === 'object') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'The `style` prop expects a mapping from style properties to values, ' + 'not a string. For example, style={{marginRight: spacing + \'em\'}} when ' + 'using JSX.%s', getDeclarationErrorAddendum(component)) : invariant(false) : void 0;
|
|
}
|
|
|
|
function enqueuePutListener(inst, registrationName, listener, transaction) {
|
|
if (transaction instanceof ReactServerRenderingTransaction) {
|
|
return;
|
|
}
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
// IE8 has no API for event capturing and the `onScroll` event doesn't
|
|
// bubble.
|
|
process.env.NODE_ENV !== 'production' ? warning(registrationName !== 'onScroll' || isEventSupported('scroll', true), 'This browser doesn\'t support the `onScroll` event') : void 0;
|
|
}
|
|
var containerInfo = inst._nativeContainerInfo;
|
|
var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
|
|
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
|
|
listenTo(registrationName, doc);
|
|
transaction.getReactMountReady().enqueue(putListener, {
|
|
inst: inst,
|
|
registrationName: registrationName,
|
|
listener: listener
|
|
});
|
|
}
|
|
|
|
function putListener() {
|
|
var listenerToPut = this;
|
|
EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
|
|
}
|
|
|
|
function optionPostMount() {
|
|
var inst = this;
|
|
ReactDOMOption.postMountWrapper(inst);
|
|
}
|
|
|
|
var setContentChildForInstrumentation = emptyFunction;
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
setContentChildForInstrumentation = function (contentToUse) {
|
|
var debugID = this._debugID;
|
|
var contentDebugID = debugID + '#text';
|
|
this._contentDebugID = contentDebugID;
|
|
ReactInstrumentation.debugTool.onSetDisplayName(contentDebugID, '#text');
|
|
ReactInstrumentation.debugTool.onSetText(contentDebugID, '' + contentToUse);
|
|
ReactInstrumentation.debugTool.onMountComponent(contentDebugID);
|
|
ReactInstrumentation.debugTool.onSetChildren(debugID, [contentDebugID]);
|
|
};
|
|
}
|
|
|
|
// There are so many media events, it makes sense to just
|
|
// maintain a list rather than create a `trapBubbledEvent` for each
|
|
var mediaEvents = {
|
|
topAbort: 'abort',
|
|
topCanPlay: 'canplay',
|
|
topCanPlayThrough: 'canplaythrough',
|
|
topDurationChange: 'durationchange',
|
|
topEmptied: 'emptied',
|
|
topEncrypted: 'encrypted',
|
|
topEnded: 'ended',
|
|
topError: 'error',
|
|
topLoadedData: 'loadeddata',
|
|
topLoadedMetadata: 'loadedmetadata',
|
|
topLoadStart: 'loadstart',
|
|
topPause: 'pause',
|
|
topPlay: 'play',
|
|
topPlaying: 'playing',
|
|
topProgress: 'progress',
|
|
topRateChange: 'ratechange',
|
|
topSeeked: 'seeked',
|
|
topSeeking: 'seeking',
|
|
topStalled: 'stalled',
|
|
topSuspend: 'suspend',
|
|
topTimeUpdate: 'timeupdate',
|
|
topVolumeChange: 'volumechange',
|
|
topWaiting: 'waiting'
|
|
};
|
|
|
|
function trapBubbledEventsLocal() {
|
|
var inst = this;
|
|
// If a component renders to null or if another component fatals and causes
|
|
// the state of the tree to be corrupted, `node` here can be null.
|
|
!inst._rootNodeID ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Must be mounted to trap events') : invariant(false) : void 0;
|
|
var node = getNode(inst);
|
|
!node ? process.env.NODE_ENV !== 'production' ? invariant(false, 'trapBubbledEvent(...): Requires node to be rendered.') : invariant(false) : void 0;
|
|
|
|
switch (inst._tag) {
|
|
case 'iframe':
|
|
case 'object':
|
|
inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load', node)];
|
|
break;
|
|
case 'video':
|
|
case 'audio':
|
|
|
|
inst._wrapperState.listeners = [];
|
|
// Create listener for each media event
|
|
for (var event in mediaEvents) {
|
|
if (mediaEvents.hasOwnProperty(event)) {
|
|
inst._wrapperState.listeners.push(ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes[event], mediaEvents[event], node));
|
|
}
|
|
}
|
|
|
|
break;
|
|
case 'img':
|
|
inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topError, 'error', node), ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topLoad, 'load', node)];
|
|
break;
|
|
case 'form':
|
|
inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topReset, 'reset', node), ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topSubmit, 'submit', node)];
|
|
break;
|
|
case 'input':
|
|
case 'select':
|
|
case 'textarea':
|
|
inst._wrapperState.listeners = [ReactBrowserEventEmitter.trapBubbledEvent(EventConstants.topLevelTypes.topInvalid, 'invalid', node)];
|
|
break;
|
|
}
|
|
}
|
|
|
|
function postUpdateSelectWrapper() {
|
|
ReactDOMSelect.postUpdateWrapper(this);
|
|
}
|
|
|
|
// For HTML, certain tags should omit their close tag. We keep a whitelist for
|
|
// those special-case 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.
|
|
var newlineEatingTags = {
|
|
'listing': true,
|
|
'pre': true,
|
|
'textarea': true
|
|
};
|
|
|
|
// For HTML, certain tags cannot have children. This has the same purpose as
|
|
// `omittedCloseTags` except that `menuitem` should still have its closing tag.
|
|
|
|
var voidElementTags = _assign({
|
|
'menuitem': true
|
|
}, omittedCloseTags);
|
|
|
|
// We accept any tag to be rendered but since this gets injected into arbitrary
|
|
// 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)) {
|
|
!VALID_TAG_REGEX.test(tag) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Invalid tag: %s', tag) : invariant(false) : void 0;
|
|
validatedTagCache[tag] = true;
|
|
}
|
|
}
|
|
|
|
function isCustomComponent(tagName, props) {
|
|
return tagName.indexOf('-') >= 0 || props.is != null;
|
|
}
|
|
|
|
var globalIdCounter = 1;
|
|
|
|
/**
|
|
* 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(element) {
|
|
var tag = element.type;
|
|
validateDangerousTag(tag);
|
|
this._currentElement = element;
|
|
this._tag = tag.toLowerCase();
|
|
this._namespaceURI = null;
|
|
this._renderedChildren = null;
|
|
this._previousStyle = null;
|
|
this._previousStyleCopy = null;
|
|
this._nativeNode = null;
|
|
this._nativeParent = null;
|
|
this._rootNodeID = null;
|
|
this._domID = null;
|
|
this._nativeContainerInfo = null;
|
|
this._wrapperState = null;
|
|
this._topLevelWrapper = null;
|
|
this._flags = 0;
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
this._ancestorInfo = null;
|
|
this._contentDebugID = null;
|
|
}
|
|
}
|
|
|
|
ReactDOMComponent.displayName = 'ReactDOMComponent';
|
|
|
|
ReactDOMComponent.Mixin = {
|
|
|
|
/**
|
|
* Generates root tag markup then recurses. This method has side effects and
|
|
* is not idempotent.
|
|
*
|
|
* @internal
|
|
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
|
|
* @param {?ReactDOMComponent} the containing DOM component instance
|
|
* @param {?object} info about the native container
|
|
* @param {object} context
|
|
* @return {string} The computed markup.
|
|
*/
|
|
mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {
|
|
this._rootNodeID = globalIdCounter++;
|
|
this._domID = nativeContainerInfo._idCounter++;
|
|
this._nativeParent = nativeParent;
|
|
this._nativeContainerInfo = nativeContainerInfo;
|
|
|
|
var props = this._currentElement.props;
|
|
|
|
switch (this._tag) {
|
|
case 'iframe':
|
|
case 'object':
|
|
case 'img':
|
|
case 'form':
|
|
case 'video':
|
|
case 'audio':
|
|
this._wrapperState = {
|
|
listeners: null
|
|
};
|
|
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
|
|
break;
|
|
case 'button':
|
|
props = ReactDOMButton.getNativeProps(this, props, nativeParent);
|
|
break;
|
|
case 'input':
|
|
ReactDOMInput.mountWrapper(this, props, nativeParent);
|
|
props = ReactDOMInput.getNativeProps(this, props);
|
|
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
|
|
break;
|
|
case 'option':
|
|
ReactDOMOption.mountWrapper(this, props, nativeParent);
|
|
props = ReactDOMOption.getNativeProps(this, props);
|
|
break;
|
|
case 'select':
|
|
ReactDOMSelect.mountWrapper(this, props, nativeParent);
|
|
props = ReactDOMSelect.getNativeProps(this, props);
|
|
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
|
|
break;
|
|
case 'textarea':
|
|
ReactDOMTextarea.mountWrapper(this, props, nativeParent);
|
|
props = ReactDOMTextarea.getNativeProps(this, props);
|
|
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
|
|
break;
|
|
}
|
|
|
|
assertValidProps(this, props);
|
|
|
|
// We create tags in the namespace of their parent container, except HTML
|
|
// tags get no namespace.
|
|
var namespaceURI;
|
|
var parentTag;
|
|
if (nativeParent != null) {
|
|
namespaceURI = nativeParent._namespaceURI;
|
|
parentTag = nativeParent._tag;
|
|
} else if (nativeContainerInfo._tag) {
|
|
namespaceURI = nativeContainerInfo._namespaceURI;
|
|
parentTag = nativeContainerInfo._tag;
|
|
}
|
|
if (namespaceURI == null || namespaceURI === DOMNamespaces.svg && parentTag === 'foreignobject') {
|
|
namespaceURI = DOMNamespaces.html;
|
|
}
|
|
if (namespaceURI === DOMNamespaces.html) {
|
|
if (this._tag === 'svg') {
|
|
namespaceURI = DOMNamespaces.svg;
|
|
} else if (this._tag === 'math') {
|
|
namespaceURI = DOMNamespaces.mathml;
|
|
}
|
|
}
|
|
this._namespaceURI = namespaceURI;
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
var parentInfo;
|
|
if (nativeParent != null) {
|
|
parentInfo = nativeParent._ancestorInfo;
|
|
} else if (nativeContainerInfo._tag) {
|
|
parentInfo = nativeContainerInfo._ancestorInfo;
|
|
}
|
|
if (parentInfo) {
|
|
// parentInfo should always be present except for the top-level
|
|
// component when server rendering
|
|
validateDOMNesting(this._tag, this, parentInfo);
|
|
}
|
|
this._ancestorInfo = validateDOMNesting.updatedAncestorInfo(parentInfo, this._tag, this);
|
|
}
|
|
|
|
var mountImage;
|
|
if (transaction.useCreateElement) {
|
|
var ownerDocument = nativeContainerInfo._ownerDocument;
|
|
var el;
|
|
if (namespaceURI === DOMNamespaces.html) {
|
|
if (this._tag === 'script') {
|
|
// Create the script via .innerHTML so its "parser-inserted" flag is
|
|
// set to true and it does not execute
|
|
var div = ownerDocument.createElement('div');
|
|
var type = this._currentElement.type;
|
|
div.innerHTML = '<' + type + '></' + type + '>';
|
|
el = div.removeChild(div.firstChild);
|
|
} else {
|
|
el = ownerDocument.createElement(this._currentElement.type, props.is || null);
|
|
}
|
|
} else {
|
|
el = ownerDocument.createElementNS(namespaceURI, this._currentElement.type);
|
|
}
|
|
ReactDOMComponentTree.precacheNode(this, el);
|
|
this._flags |= Flags.hasCachedChildNodes;
|
|
if (!this._nativeParent) {
|
|
DOMPropertyOperations.setAttributeForRoot(el);
|
|
}
|
|
this._updateDOMProperties(null, props, transaction);
|
|
var lazyTree = DOMLazyTree(el);
|
|
this._createInitialChildren(transaction, props, context, lazyTree);
|
|
mountImage = lazyTree;
|
|
} else {
|
|
var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
|
|
var tagContent = this._createContentMarkup(transaction, props, context);
|
|
if (!tagContent && omittedCloseTags[this._tag]) {
|
|
mountImage = tagOpen + '/>';
|
|
} else {
|
|
mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
|
|
}
|
|
}
|
|
|
|
switch (this._tag) {
|
|
case 'button':
|
|
case 'input':
|
|
case 'select':
|
|
case 'textarea':
|
|
if (props.autoFocus) {
|
|
transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
|
|
}
|
|
break;
|
|
case 'option':
|
|
transaction.getReactMountReady().enqueue(optionPostMount, this);
|
|
}
|
|
|
|
return mountImage;
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
* @param {object} props
|
|
* @return {string} Markup of opening tag.
|
|
*/
|
|
_createOpenTagMarkupAndPutListeners: function (transaction, props) {
|
|
var ret = '<' + this._currentElement.type;
|
|
|
|
for (var propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
var propValue = props[propKey];
|
|
if (propValue == null) {
|
|
continue;
|
|
}
|
|
if (registrationNameModules.hasOwnProperty(propKey)) {
|
|
if (propValue) {
|
|
enqueuePutListener(this, propKey, propValue, transaction);
|
|
}
|
|
} else {
|
|
if (propKey === STYLE) {
|
|
if (propValue) {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
// See `_updateDOMProperties`. style block
|
|
this._previousStyle = propValue;
|
|
}
|
|
propValue = this._previousStyleCopy = _assign({}, props.style);
|
|
}
|
|
propValue = CSSPropertyOperations.createMarkupForStyles(propValue, this);
|
|
}
|
|
var markup = null;
|
|
if (this._tag != null && isCustomComponent(this._tag, props)) {
|
|
if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
|
|
markup = DOMPropertyOperations.createMarkupForCustomAttribute(propKey, propValue);
|
|
}
|
|
} else {
|
|
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;
|
|
}
|
|
|
|
if (!this._nativeParent) {
|
|
ret += ' ' + DOMPropertyOperations.createMarkupForRoot();
|
|
}
|
|
ret += ' ' + DOMPropertyOperations.createMarkupForID(this._domID);
|
|
return ret;
|
|
},
|
|
|
|
/**
|
|
* Creates markup for the content between the tags.
|
|
*
|
|
* @private
|
|
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
|
|
* @param {object} props
|
|
* @param {object} context
|
|
* @return {string} Content markup.
|
|
*/
|
|
_createContentMarkup: function (transaction, props, context) {
|
|
var ret = '';
|
|
|
|
// Intentional use of != to avoid catching zero/false.
|
|
var innerHTML = props.dangerouslySetInnerHTML;
|
|
if (innerHTML != null) {
|
|
if (innerHTML.__html != null) {
|
|
ret = innerHTML.__html;
|
|
}
|
|
} else {
|
|
var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
|
|
var childrenToUse = contentToUse != null ? null : props.children;
|
|
if (contentToUse != null) {
|
|
// TODO: Validate that text is allowed as a child of this node
|
|
ret = escapeTextContentForBrowser(contentToUse);
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
setContentChildForInstrumentation.call(this, contentToUse);
|
|
}
|
|
} else if (childrenToUse != null) {
|
|
var mountImages = this.mountChildren(childrenToUse, transaction, context);
|
|
ret = mountImages.join('');
|
|
}
|
|
}
|
|
if (newlineEatingTags[this._tag] && ret.charAt(0) === '\n') {
|
|
// text/html ignores the first character in these tags if it's a newline
|
|
// Prefer to break application/xml over text/html (for now) by adding
|
|
// a newline specifically to get eaten by the parser. (Alternately for
|
|
// textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
|
|
// \r is normalized out by HTMLTextAreaElement#value.)
|
|
// See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
|
|
// See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
|
|
// See: <http://www.w3.org/TR/html5/syntax.html#newlines>
|
|
// See: Parsing of "textarea" "listing" and "pre" elements
|
|
// from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
|
|
return '\n' + ret;
|
|
} else {
|
|
return ret;
|
|
}
|
|
},
|
|
|
|
_createInitialChildren: function (transaction, props, context, lazyTree) {
|
|
// Intentional use of != to avoid catching zero/false.
|
|
var innerHTML = props.dangerouslySetInnerHTML;
|
|
if (innerHTML != null) {
|
|
if (innerHTML.__html != null) {
|
|
DOMLazyTree.queueHTML(lazyTree, innerHTML.__html);
|
|
}
|
|
} else {
|
|
var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
|
|
var childrenToUse = contentToUse != null ? null : props.children;
|
|
if (contentToUse != null) {
|
|
// TODO: Validate that text is allowed as a child of this node
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
setContentChildForInstrumentation.call(this, contentToUse);
|
|
}
|
|
DOMLazyTree.queueText(lazyTree, contentToUse);
|
|
} else if (childrenToUse != null) {
|
|
var mountImages = this.mountChildren(childrenToUse, transaction, context);
|
|
for (var i = 0; i < mountImages.length; i++) {
|
|
DOMLazyTree.queueChild(lazyTree, mountImages[i]);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Receives a next element and updates the component.
|
|
*
|
|
* @internal
|
|
* @param {ReactElement} nextElement
|
|
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
|
|
* @param {object} context
|
|
*/
|
|
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) {
|
|
var lastProps = prevElement.props;
|
|
var nextProps = this._currentElement.props;
|
|
|
|
switch (this._tag) {
|
|
case 'button':
|
|
lastProps = ReactDOMButton.getNativeProps(this, lastProps);
|
|
nextProps = ReactDOMButton.getNativeProps(this, nextProps);
|
|
break;
|
|
case 'input':
|
|
ReactDOMInput.updateWrapper(this);
|
|
lastProps = ReactDOMInput.getNativeProps(this, lastProps);
|
|
nextProps = ReactDOMInput.getNativeProps(this, nextProps);
|
|
break;
|
|
case 'option':
|
|
lastProps = ReactDOMOption.getNativeProps(this, lastProps);
|
|
nextProps = ReactDOMOption.getNativeProps(this, nextProps);
|
|
break;
|
|
case 'select':
|
|
lastProps = ReactDOMSelect.getNativeProps(this, lastProps);
|
|
nextProps = ReactDOMSelect.getNativeProps(this, nextProps);
|
|
break;
|
|
case 'textarea':
|
|
ReactDOMTextarea.updateWrapper(this);
|
|
lastProps = ReactDOMTextarea.getNativeProps(this, lastProps);
|
|
nextProps = ReactDOMTextarea.getNativeProps(this, nextProps);
|
|
break;
|
|
}
|
|
|
|
assertValidProps(this, nextProps);
|
|
this._updateDOMProperties(lastProps, nextProps, transaction);
|
|
this._updateDOMChildren(lastProps, nextProps, transaction, context);
|
|
|
|
if (this._tag === 'select') {
|
|
// <select> value update needs to occur after <option> children
|
|
// reconciliation
|
|
transaction.getReactMountReady().enqueue(postUpdateSelectWrapper, this);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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 {object} nextProps
|
|
* @param {?DOMElement} node
|
|
*/
|
|
_updateDOMProperties: function (lastProps, nextProps, transaction) {
|
|
var propKey;
|
|
var styleName;
|
|
var styleUpdates;
|
|
for (propKey in lastProps) {
|
|
if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) {
|
|
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)) {
|
|
if (lastProps[propKey]) {
|
|
// Only call deleteListener if there was a listener previously or
|
|
// else willDeleteListener gets called when there wasn't actually a
|
|
// listener (e.g., onClick={null})
|
|
deleteListener(this, propKey);
|
|
}
|
|
} else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
|
|
DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey);
|
|
}
|
|
}
|
|
for (propKey in nextProps) {
|
|
var nextProp = nextProps[propKey];
|
|
var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps != null ? lastProps[propKey] : undefined;
|
|
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) {
|
|
continue;
|
|
}
|
|
if (propKey === STYLE) {
|
|
if (nextProp) {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
checkAndWarnForMutatedStyle(this._previousStyleCopy, this._previousStyle, this);
|
|
this._previousStyle = 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)) {
|
|
if (nextProp) {
|
|
enqueuePutListener(this, propKey, nextProp, transaction);
|
|
} else if (lastProp) {
|
|
deleteListener(this, propKey);
|
|
}
|
|
} else if (isCustomComponent(this._tag, nextProps)) {
|
|
if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
|
|
DOMPropertyOperations.setValueForAttribute(getNode(this), propKey, nextProp);
|
|
}
|
|
} else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
|
|
var node = getNode(this);
|
|
// If we're updating to null or undefined, we should remove the property
|
|
// from the DOM node instead of inadvertently setting to a string. This
|
|
// brings us in line with the same behavior we have on initial render.
|
|
if (nextProp != null) {
|
|
DOMPropertyOperations.setValueForProperty(node, propKey, nextProp);
|
|
} else {
|
|
DOMPropertyOperations.deleteValueForProperty(node, propKey);
|
|
}
|
|
}
|
|
}
|
|
if (styleUpdates) {
|
|
CSSPropertyOperations.setValueForStyles(getNode(this), styleUpdates, this);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Reconciles the children with the various properties that affect the
|
|
* children content.
|
|
*
|
|
* @param {object} lastProps
|
|
* @param {object} nextProps
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @param {object} context
|
|
*/
|
|
_updateDOMChildren: function (lastProps, nextProps, transaction, context) {
|
|
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 (process.env.NODE_ENV !== 'production') {
|
|
ReactInstrumentation.debugTool.onSetChildren(this._debugID, []);
|
|
}
|
|
}
|
|
|
|
if (nextContent != null) {
|
|
if (lastContent !== nextContent) {
|
|
this.updateTextContent('' + nextContent);
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
this._contentDebugID = this._debugID + '#text';
|
|
setContentChildForInstrumentation.call(this, nextContent);
|
|
}
|
|
}
|
|
} else if (nextHtml != null) {
|
|
if (lastHtml !== nextHtml) {
|
|
this.updateMarkup('' + nextHtml);
|
|
}
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
ReactInstrumentation.debugTool.onSetChildren(this._debugID, []);
|
|
}
|
|
} else if (nextChildren != null) {
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
if (this._contentDebugID) {
|
|
ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID);
|
|
this._contentDebugID = null;
|
|
}
|
|
}
|
|
|
|
this.updateChildren(nextChildren, transaction, context);
|
|
}
|
|
},
|
|
|
|
getNativeNode: function () {
|
|
return getNode(this);
|
|
},
|
|
|
|
/**
|
|
* Destroys all event registrations for this instance. Does not remove from
|
|
* the DOM. That must be done by the parent.
|
|
*
|
|
* @internal
|
|
*/
|
|
unmountComponent: function (safely) {
|
|
switch (this._tag) {
|
|
case 'iframe':
|
|
case 'object':
|
|
case 'img':
|
|
case 'form':
|
|
case 'video':
|
|
case 'audio':
|
|
var listeners = this._wrapperState.listeners;
|
|
if (listeners) {
|
|
for (var i = 0; i < listeners.length; i++) {
|
|
listeners[i].remove();
|
|
}
|
|
}
|
|
break;
|
|
case 'html':
|
|
case 'head':
|
|
case 'body':
|
|
/**
|
|
* Components like <html> <head> and <body> can't be removed or added
|
|
* easily in a cross-browser way, however it's valuable to be able to
|
|
* take advantage of React's reconciliation for styling and <title>
|
|
* management. So we just document it and throw in dangerous cases.
|
|
*/
|
|
!false ? process.env.NODE_ENV !== 'production' ? invariant(false, '<%s> tried to unmount. Because of cross-browser quirks it is ' + 'impossible to unmount some top-level components (eg <html>, ' + '<head>, and <body>) reliably and efficiently. To fix this, have a ' + 'single top-level component that never unmounts render these ' + 'elements.', this._tag) : invariant(false) : void 0;
|
|
break;
|
|
}
|
|
|
|
this.unmountChildren(safely);
|
|
ReactDOMComponentTree.uncacheNode(this);
|
|
EventPluginHub.deleteAllListeners(this);
|
|
ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
|
|
this._rootNodeID = null;
|
|
this._domID = null;
|
|
this._wrapperState = null;
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
if (this._contentDebugID) {
|
|
ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID);
|
|
this._contentDebugID = null;
|
|
}
|
|
}
|
|
},
|
|
|
|
getPublicInstance: function () {
|
|
return getNode(this);
|
|
}
|
|
|
|
};
|
|
|
|
_assign(ReactDOMComponent.prototype, ReactDOMComponent.Mixin, ReactMultiChild.Mixin);
|
|
|
|
module.exports = ReactDOMComponent; |