react-dom
Version:
React package for working with the DOM.
568 lines (527 loc) • 23.7 kB
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.
*
*
*/
/* global hasOwnProperty:true */
'use strict';
var _extends = _assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _prodInvariant = require('./reactProdInvariant'),
_assign = require('object-assign');
var CSSPropertyOperations = require('./CSSPropertyOperations');
var DOMNamespaces = require('./DOMNamespaces');
var DOMProperty = require('./DOMProperty');
var DOMPropertyOperations = require('./DOMPropertyOperations');
var EventPluginRegistry = require('./EventPluginRegistry');
var ReactBrowserEventEmitter = require('./ReactBrowserEventEmitter');
var ReactDOMFiberInput = require('./ReactDOMFiberInput');
var ReactDOMFiberOption = require('./ReactDOMFiberOption');
var ReactDOMFiberSelect = require('./ReactDOMFiberSelect');
var ReactDOMFiberTextarea = require('./ReactDOMFiberTextarea');
var _require = require('./ReactDebugCurrentFiber'),
getCurrentFiberOwnerName = _require.getCurrentFiberOwnerName;
var emptyFunction = require('fbjs/lib/emptyFunction');
var invariant = require('fbjs/lib/invariant');
var isEventSupported = require('./isEventSupported');
var setInnerHTML = require('./setInnerHTML');
var setTextContent = require('./setTextContent');
var inputValueTracking = require('./inputValueTracking');
var warning = require('fbjs/lib/warning');
if (process.env.NODE_ENV !== 'production') {
var ReactDOMInvalidARIAHook = require('./ReactDOMInvalidARIAHook');
var ReactDOMNullInputValuePropHook = require('./ReactDOMNullInputValuePropHook');
var ReactDOMUnknownPropertyHook = require('./ReactDOMUnknownPropertyHook');
var validateARIAProperties = ReactDOMInvalidARIAHook.validateProperties;
var validateInputPropertes = ReactDOMNullInputValuePropHook.validateProperties;
var validateUnknownPropertes = ReactDOMUnknownPropertyHook.validateProperties;
}
var didWarnShadyDOM = false;
var listenTo = ReactBrowserEventEmitter.listenTo;
var registrationNameModules = EventPluginRegistry.registrationNameModules;
var DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML';
var SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning';
var CHILDREN = 'children';
var STYLE = 'style';
var HTML = '__html';
var HTML_NAMESPACE = DOMNamespaces.html,
SVG_NAMESPACE = DOMNamespaces.svg,
MATH_NAMESPACE = DOMNamespaces.mathml;
// Node type for document fragments (Node.DOCUMENT_FRAGMENT_NODE).
var DOC_FRAGMENT_TYPE = 11;
function getDeclarationErrorAddendum() {
var ownerName = getCurrentFiberOwnerName();
if (ownerName) {
// TODO: also report the stack.
return ' This DOM node was rendered by `' + ownerName + '`.';
}
return '';
}
function assertValidProps(tag, props) {
if (!props) {
return;
}
// Note the use of `==` which checks for null or undefined.
if (voidElementTags[tag]) {
!(props.children == null && props.dangerouslySetInnerHTML == null) ? process.env.NODE_ENV !== 'production' ? invariant(false, '%s is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`.%s', tag, getDeclarationErrorAddendum()) : _prodInvariant('137', tag, getDeclarationErrorAddendum()) : 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`.') : _prodInvariant('60') : 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.') : _prodInvariant('61') : 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()) : _prodInvariant('62', getDeclarationErrorAddendum()) : void 0;
}
if (process.env.NODE_ENV !== 'production') {
var validatePropertiesInDevelopment = function (type, props) {
validateARIAProperties(type, props);
validateInputPropertes(type, props);
validateUnknownPropertes(type, props);
};
}
function ensureListeningTo(rootContainerElement, registrationName) {
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 isDocumentFragment = rootContainerElement.nodeType === DOC_FRAGMENT_TYPE;
var doc = isDocumentFragment ? rootContainerElement : rootContainerElement.ownerDocument;
listenTo(registrationName, doc);
}
// 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 trapClickOnNonInteractiveElement(node) {
// Mobile Safari does not fire properly bubble click events on
// non-interactive elements, which means delegated click listeners do not
// fire. The workaround for this bug involves attaching an empty click
// listener on the target node.
// http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
// Just set it using the onclick property so that we don't have to manage any
// bookkeeping for it. Not sure if we need to clear it when the listener is
// removed.
// TODO: Only do this for the relevant Safaris maybe?
node.onclick = emptyFunction;
}
function trapBubbledEventsLocal(node, tag) {
// 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.
// TODO: Make sure that we check isMounted before firing any of these events.
// TODO: Inline these below since we're calling this from an equivalent
// switch statement.
switch (tag) {
case 'iframe':
case 'object':
ReactBrowserEventEmitter.trapBubbledEvent('topLoad', 'load', node);
break;
case 'video':
case 'audio':
// Create listener for each media event
for (var event in mediaEvents) {
if (mediaEvents.hasOwnProperty(event)) {
ReactBrowserEventEmitter.trapBubbledEvent(event, mediaEvents[event], node);
}
}
break;
case 'source':
ReactBrowserEventEmitter.trapBubbledEvent('topError', 'error', node);
break;
case 'img':
ReactBrowserEventEmitter.trapBubbledEvent('topError', 'error', node);
ReactBrowserEventEmitter.trapBubbledEvent('topLoad', 'load', node);
break;
case 'form':
ReactBrowserEventEmitter.trapBubbledEvent('topReset', 'reset', node);
ReactBrowserEventEmitter.trapBubbledEvent('topSubmit', 'submit', node);
break;
case 'input':
case 'select':
case 'textarea':
ReactBrowserEventEmitter.trapBubbledEvent('topInvalid', 'invalid', node);
break;
}
}
// 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
};
// 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 = _extends({
'menuitem': true
}, omittedCloseTags);
function isCustomComponent(tagName, props) {
return tagName.indexOf('-') >= 0 || props.is != null;
}
/**
* 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.
*/
function updateDOMProperties(domElement, rootContainerElement, lastProps, nextProps, wasCustomComponentTag, isCustomComponentTag) {
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 = lastProps[propKey];
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
// TODO: Clear innerHTML. This is currently broken in Fiber because we are
// too late to clear everything at this point because new children have
// already been inserted.
} else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) {
// Noop
} else if (registrationNameModules.hasOwnProperty(propKey)) {
// Do nothing for deleted listeners.
} else if (wasCustomComponentTag) {
DOMPropertyOperations.deleteValueForAttribute(domElement, propKey);
} else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
DOMPropertyOperations.deleteValueForProperty(domElement, propKey);
}
}
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp = lastProps != null ? lastProps[propKey] : undefined;
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) {
continue;
}
if (propKey === STYLE) {
if (process.env.NODE_ENV !== 'production') {
if (nextProp) {
// Freeze the next style object so that we can assume it won't be
// mutated. We have already warned for this in the past.
Object.freeze(nextProp);
}
}
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 (propKey === DANGEROUSLY_SET_INNER_HTML) {
var nextHtml = nextProp ? nextProp[HTML] : undefined;
var lastHtml = lastProp ? lastProp[HTML] : undefined;
if (nextHtml) {
if (lastHtml) {
if (lastHtml !== nextHtml) {
setInnerHTML(domElement, '' + nextHtml);
}
} else {
setInnerHTML(domElement, nextHtml);
}
} else {
// TODO: It might be too late to clear this if we have children
// inserted already.
}
} else if (propKey === CHILDREN) {
if (typeof nextProp === 'string') {
setTextContent(domElement, nextProp);
} else if (typeof nextProp === 'number') {
setTextContent(domElement, '' + nextProp);
}
} else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) {
// Noop
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
ensureListeningTo(rootContainerElement, propKey);
}
} else if (isCustomComponentTag) {
DOMPropertyOperations.setValueForAttribute(domElement, propKey, nextProp);
} else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
// 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(domElement, propKey, nextProp);
} else {
DOMPropertyOperations.deleteValueForProperty(domElement, propKey);
}
}
}
if (styleUpdates) {
// TODO: call ReactInstrumentation.debugTool.onHostOperation in DEV.
CSSPropertyOperations.setValueForStyles(domElement, styleUpdates);
}
}
// Assumes there is no parent namespace.
function getIntrinsicNamespace(type) {
switch (type) {
case 'svg':
return SVG_NAMESPACE;
case 'math':
return MATH_NAMESPACE;
default:
return HTML_NAMESPACE;
}
}
var ReactDOMFiberComponent = {
getChildNamespace: function (parentNamespace, type) {
if (parentNamespace == null || parentNamespace === HTML_NAMESPACE) {
// No (or default) parent namespace: potential entry point.
return getIntrinsicNamespace(type);
}
if (parentNamespace === SVG_NAMESPACE && type === 'foreignObject') {
// We're leaving SVG.
return HTML_NAMESPACE;
}
// By default, pass namespace below.
return parentNamespace;
},
createElement: function (type, props, rootContainerElement, parentNamespace) {
// We create tags in the namespace of their parent container, except HTML
// tags get no namespace.
var ownerDocument = rootContainerElement.ownerDocument;
var domElement;
var namespaceURI = parentNamespace;
if (namespaceURI === HTML_NAMESPACE) {
namespaceURI = getIntrinsicNamespace(type);
}
if (namespaceURI === HTML_NAMESPACE) {
if (process.env.NODE_ENV !== 'production') {
process.env.NODE_ENV !== 'production' ? warning(type === type.toLowerCase() || isCustomComponent(type, props), '<%s /> is using uppercase HTML. Always use lowercase HTML tags ' + 'in React.', type) : void 0;
}
if (type === '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');
div.innerHTML = '<script></script>';
// This is guaranteed to yield a script element.
var firstChild = div.firstChild;
domElement = div.removeChild(firstChild);
} else if (props.is) {
domElement = ownerDocument.createElement(type, props.is);
} else {
// Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug.
// See discussion in https://github.com/facebook/react/pull/6896
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
domElement = ownerDocument.createElement(type);
}
} else {
domElement = ownerDocument.createElementNS(namespaceURI, type);
}
return domElement;
},
setInitialProperties: function (domElement, tag, rawProps, rootContainerElement) {
var isCustomComponentTag = isCustomComponent(tag, rawProps);
if (process.env.NODE_ENV !== 'production') {
validatePropertiesInDevelopment(tag, rawProps);
if (isCustomComponentTag && !didWarnShadyDOM && domElement.shadyRoot) {
process.env.NODE_ENV !== 'production' ? warning(false, '%s is using shady DOM. Using shady DOM with React can ' + 'cause things to break subtly.', getCurrentFiberOwnerName() || 'A component') : void 0;
didWarnShadyDOM = true;
}
}
var props;
switch (tag) {
case 'audio':
case 'form':
case 'iframe':
case 'img':
case 'link':
case 'object':
case 'source':
case 'video':
trapBubbledEventsLocal(domElement, tag);
props = rawProps;
break;
case 'input':
ReactDOMFiberInput.mountWrapper(domElement, rawProps);
props = ReactDOMFiberInput.getHostProps(domElement, rawProps);
trapBubbledEventsLocal(domElement, tag);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'option':
ReactDOMFiberOption.mountWrapper(domElement, rawProps);
props = ReactDOMFiberOption.getHostProps(domElement, rawProps);
break;
case 'select':
ReactDOMFiberSelect.mountWrapper(domElement, rawProps);
props = ReactDOMFiberSelect.getHostProps(domElement, rawProps);
trapBubbledEventsLocal(domElement, tag);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMFiberTextarea.mountWrapper(domElement, rawProps);
props = ReactDOMFiberTextarea.getHostProps(domElement, rawProps);
trapBubbledEventsLocal(domElement, tag);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
default:
props = rawProps;
}
assertValidProps(tag, props);
updateDOMProperties(domElement, rootContainerElement, null, props, false, isCustomComponentTag);
switch (tag) {
case 'input':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
inputValueTracking.trackNode(domElement);
ReactDOMFiberInput.postMountWrapper(domElement, rawProps);
break;
case 'textarea':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
inputValueTracking.trackNode(domElement);
ReactDOMFiberTextarea.postMountWrapper(domElement, rawProps);
break;
case 'option':
ReactDOMFiberOption.postMountWrapper(domElement, rawProps);
break;
default:
if (typeof props.onClick === 'function') {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(domElement);
}
break;
}
},
updateProperties: function (domElement, tag, lastRawProps, nextRawProps, rootContainerElement) {
if (process.env.NODE_ENV !== 'production') {
validatePropertiesInDevelopment(tag, nextRawProps);
}
var lastProps;
var nextProps;
switch (tag) {
case 'input':
lastProps = ReactDOMFiberInput.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMFiberInput.getHostProps(domElement, nextRawProps);
break;
case 'option':
lastProps = ReactDOMFiberOption.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMFiberOption.getHostProps(domElement, nextRawProps);
break;
case 'select':
lastProps = ReactDOMFiberSelect.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMFiberSelect.getHostProps(domElement, nextRawProps);
break;
case 'textarea':
lastProps = ReactDOMFiberTextarea.getHostProps(domElement, lastRawProps);
nextProps = ReactDOMFiberTextarea.getHostProps(domElement, nextRawProps);
break;
default:
lastProps = lastRawProps;
nextProps = nextRawProps;
if (typeof lastProps.onClick !== 'function' && typeof nextProps.onClick === 'function') {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(domElement);
}
break;
}
assertValidProps(tag, nextProps);
var wasCustomComponentTag = isCustomComponent(tag, lastProps);
var isCustomComponentTag = isCustomComponent(tag, nextProps);
updateDOMProperties(domElement, rootContainerElement, lastProps, nextProps, wasCustomComponentTag, isCustomComponentTag);
switch (tag) {
case 'input':
// Update the wrapper around inputs *after* updating props. This has to
// happen after `updateDOMProperties`. Otherwise HTML5 input validations
// raise warnings and prevent the new value from being assigned.
ReactDOMFiberInput.updateWrapper(domElement, nextRawProps);
break;
case 'textarea':
ReactDOMFiberTextarea.updateWrapper(domElement, nextRawProps);
break;
case 'select':
// <select> value update needs to occur after <option> children
// reconciliation
ReactDOMFiberSelect.postUpdateWrapper(domElement, nextRawProps);
break;
}
},
restoreControlledState: function (domElement, tag, props) {
switch (tag) {
case 'input':
ReactDOMFiberInput.restoreControlledState(domElement, props);
return;
case 'textarea':
ReactDOMFiberTextarea.restoreControlledState(domElement, props);
return;
case 'select':
ReactDOMFiberSelect.restoreControlledState(domElement, props);
return;
}
}
};
module.exports = ReactDOMFiberComponent;