Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| /** | |
| * 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 AutoFocusUtils = require('AutoFocusUtils'); | |
| var CSSPropertyOperations = require('CSSPropertyOperations'); | |
| var DOMLazyTree = require('DOMLazyTree'); | |
| var DOMNamespaces = require('DOMNamespaces'); | |
| var DOMProperty = require('DOMProperty'); | |
| var DOMPropertyOperations = require('DOMPropertyOperations'); | |
| var EventPluginHub = require('EventPluginHub'); | |
| var EventPluginRegistry = require('EventPluginRegistry'); | |
| var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); | |
| 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('emptyFunction'); | |
| var escapeTextContentForBrowser = require('escapeTextContentForBrowser'); | |
| var invariant = require('invariant'); | |
| var isEventSupported = require('isEventSupported'); | |
| var shallowEqual = require('shallowEqual'); | |
| var inputValueTracking = require('inputValueTracking'); | |
| var validateDOMNesting = require('validateDOMNesting'); | |
| var warning = require('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 = 'style'; | |
| var HTML = '__html'; | |
| 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; | |
| 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) | |
| ); | |
| } | |
| /** | |
| * @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]) { | |
| invariant( | |
| props.children == null && props.dangerouslySetInnerHTML == null, | |
| '%s is a void element tag and must neither have `children` nor ' + | |
| 'use `dangerouslySetInnerHTML`.%s', | |
| component._tag, | |
| component._currentElement._owner ? | |
| ' Check the render method of ' + | |
| component._currentElement._owner.getName() + '.' : | |
| '' | |
| ); | |
| } | |
| if (props.dangerouslySetInnerHTML != null) { | |
| invariant( | |
| props.children == null, | |
| 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.' | |
| ); | |
| invariant( | |
| typeof props.dangerouslySetInnerHTML === 'object' && | |
| HTML in props.dangerouslySetInnerHTML, | |
| '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + | |
| 'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' + | |
| 'for more information.' | |
| ); | |
| } | |
| if (__DEV__) { | |
| warning( | |
| props.innerHTML == null, | |
| 'Directly setting property `innerHTML` is not permitted. ' + | |
| 'For more information, lookup documentation on `dangerouslySetInnerHTML`.' | |
| ); | |
| 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.' | |
| ); | |
| 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.' | |
| ); | |
| } | |
| 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.%s', | |
| getDeclarationErrorAddendum(component) | |
| ); | |
| } | |
| function enqueuePutListener(inst, registrationName, listener, transaction) { | |
| if (transaction instanceof ReactServerRenderingTransaction) { | |
| return; | |
| } | |
| if (__DEV__) { | |
| // IE8 has no API for event capturing and the `onScroll` event doesn't | |
| // bubble. | |
| warning( | |
| registrationName !== 'onScroll' || isEventSupported('scroll', true), | |
| 'This browser doesn\'t support the `onScroll` event' | |
| ); | |
| } | |
| var containerInfo = inst._hostContainerInfo; | |
| 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 inputPostMount() { | |
| var inst = this; | |
| ReactDOMInput.postMountWrapper(inst); | |
| } | |
| function textareaPostMount() { | |
| var inst = this; | |
| ReactDOMTextarea.postMountWrapper(inst); | |
| } | |
| function optionPostMount() { | |
| var inst = this; | |
| ReactDOMOption.postMountWrapper(inst); | |
| } | |
| var setAndValidateContentChildDev = emptyFunction; | |
| if (__DEV__) { | |
| setAndValidateContentChildDev = function(content) { | |
| var hasExistingContent = this._contentDebugID != null; | |
| var debugID = this._debugID; | |
| // This ID represents the inlined child that has no backing instance: | |
| var contentDebugID = -debugID; | |
| if (content == null) { | |
| if (hasExistingContent) { | |
| ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID); | |
| } | |
| this._contentDebugID = null; | |
| return; | |
| } | |
| validateDOMNesting(null, String(content), this, this._ancestorInfo); | |
| this._contentDebugID = contentDebugID; | |
| if (hasExistingContent) { | |
| ReactInstrumentation.debugTool.onBeforeUpdateComponent(contentDebugID, content); | |
| ReactInstrumentation.debugTool.onUpdateComponent(contentDebugID); | |
| } else { | |
| ReactInstrumentation.debugTool.onBeforeMountComponent(contentDebugID, content, debugID); | |
| 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 trackInputValue() { | |
| inputValueTracking.track(this); | |
| } | |
| 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. | |
| invariant(inst._rootNodeID, 'Must be mounted to trap events'); | |
| var node = getNode(inst); | |
| invariant( | |
| node, | |
| 'trapBubbledEvent(...): Requires node to be rendered.' | |
| ); | |
| switch (inst._tag) { | |
| case 'iframe': | |
| case 'object': | |
| inst._wrapperState.listeners = [ | |
| ReactBrowserEventEmitter.trapBubbledEvent( | |
| '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( | |
| event, | |
| mediaEvents[event], | |
| node | |
| ) | |
| ); | |
| } | |
| } | |
| break; | |
| case 'source': | |
| inst._wrapperState.listeners = [ | |
| ReactBrowserEventEmitter.trapBubbledEvent( | |
| 'topError', | |
| 'error', | |
| node | |
| ), | |
| ]; | |
| break; | |
| case 'img': | |
| inst._wrapperState.listeners = [ | |
| ReactBrowserEventEmitter.trapBubbledEvent( | |
| 'topError', | |
| 'error', | |
| node | |
| ), | |
| ReactBrowserEventEmitter.trapBubbledEvent( | |
| 'topLoad', | |
| 'load', | |
| node | |
| ), | |
| ]; | |
| break; | |
| case 'form': | |
| inst._wrapperState.listeners = [ | |
| ReactBrowserEventEmitter.trapBubbledEvent( | |
| 'topReset', | |
| 'reset', | |
| node | |
| ), | |
| ReactBrowserEventEmitter.trapBubbledEvent( | |
| 'topSubmit', | |
| 'submit', | |
| node | |
| ), | |
| ]; | |
| break; | |
| case 'input': | |
| case 'select': | |
| case 'textarea': | |
| inst._wrapperState.listeners = [ | |
| ReactBrowserEventEmitter.trapBubbledEvent( | |
| '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 = Object.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)) { | |
| invariant(VALID_TAG_REGEX.test(tag), 'Invalid tag: %s', tag); | |
| 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._hostNode = null; | |
| this._hostParent = null; | |
| this._rootNodeID = 0; | |
| this._domID = 0; | |
| this._hostContainerInfo = null; | |
| this._wrapperState = null; | |
| this._topLevelWrapper = null; | |
| this._flags = 0; | |
| if (__DEV__) { | |
| this._ancestorInfo = null; | |
| setAndValidateContentChildDev.call(this, 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 parent component instance | |
| * @param {?object} info about the host container | |
| * @param {object} context | |
| * @return {string} The computed markup. | |
| */ | |
| mountComponent: function( | |
| transaction, | |
| hostParent, | |
| hostContainerInfo, | |
| context | |
| ) { | |
| this._rootNodeID = globalIdCounter++; | |
| this._domID = hostContainerInfo._idCounter++; | |
| this._hostParent = hostParent; | |
| this._hostContainerInfo = hostContainerInfo; | |
| var props = this._currentElement.props; | |
| switch (this._tag) { | |
| case 'audio': | |
| case 'form': | |
| case 'iframe': | |
| case 'img': | |
| case 'link': | |
| case 'object': | |
| case 'source': | |
| case 'video': | |
| this._wrapperState = { | |
| listeners: null, | |
| }; | |
| transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this); | |
| break; | |
| case 'input': | |
| ReactDOMInput.mountWrapper(this, props, hostParent); | |
| props = ReactDOMInput.getHostProps(this, props); | |
| transaction.getReactMountReady().enqueue(trackInputValue, this); | |
| transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this); | |
| break; | |
| case 'option': | |
| ReactDOMOption.mountWrapper(this, props, hostParent); | |
| props = ReactDOMOption.getHostProps(this, props); | |
| break; | |
| case 'select': | |
| ReactDOMSelect.mountWrapper(this, props, hostParent); | |
| props = ReactDOMSelect.getHostProps(this, props); | |
| transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this); | |
| break; | |
| case 'textarea': | |
| ReactDOMTextarea.mountWrapper(this, props, hostParent); | |
| props = ReactDOMTextarea.getHostProps(this, props); | |
| transaction.getReactMountReady().enqueue(trackInputValue, this); | |
| 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 (hostParent != null) { | |
| namespaceURI = hostParent._namespaceURI; | |
| parentTag = hostParent._tag; | |
| } else if (hostContainerInfo._tag) { | |
| namespaceURI = hostContainerInfo._namespaceURI; | |
| parentTag = hostContainerInfo._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 (__DEV__) { | |
| var parentInfo; | |
| if (hostParent != null) { | |
| parentInfo = hostParent._ancestorInfo; | |
| } else if (hostContainerInfo._tag) { | |
| parentInfo = hostContainerInfo._ancestorInfo; | |
| } | |
| if (parentInfo) { | |
| // parentInfo should always be present except for the top-level | |
| // component when server rendering | |
| validateDOMNesting(this._tag, null, this, parentInfo); | |
| } | |
| this._ancestorInfo = | |
| validateDOMNesting.updatedAncestorInfo(parentInfo, this._tag, this); | |
| } | |
| var mountImage; | |
| if (transaction.useCreateElement) { | |
| var ownerDocument = hostContainerInfo._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 if (props.is) { | |
| el = ownerDocument.createElement(this._currentElement.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 | |
| el = ownerDocument.createElement(this._currentElement.type); | |
| } | |
| } else { | |
| el = ownerDocument.createElementNS( | |
| namespaceURI, | |
| this._currentElement.type | |
| ); | |
| } | |
| ReactDOMComponentTree.precacheNode(this, el); | |
| this._flags |= Flags.hasCachedChildNodes; | |
| if (!this._hostParent) { | |
| 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 'input': | |
| transaction.getReactMountReady().enqueue( | |
| inputPostMount, | |
| this | |
| ); | |
| if (props.autoFocus) { | |
| transaction.getReactMountReady().enqueue( | |
| AutoFocusUtils.focusDOMComponent, | |
| this | |
| ); | |
| } | |
| break; | |
| case 'textarea': | |
| transaction.getReactMountReady().enqueue( | |
| textareaPostMount, | |
| this | |
| ); | |
| if (props.autoFocus) { | |
| transaction.getReactMountReady().enqueue( | |
| AutoFocusUtils.focusDOMComponent, | |
| this | |
| ); | |
| } | |
| break; | |
| case 'select': | |
| if (props.autoFocus) { | |
| transaction.getReactMountReady().enqueue( | |
| AutoFocusUtils.focusDOMComponent, | |
| this | |
| ); | |
| } | |
| break; | |
| case 'button': | |
| if (props.autoFocus) { | |
| transaction.getReactMountReady().enqueue( | |
| AutoFocusUtils.focusDOMComponent, | |
| this | |
| ); | |
| } | |
| break; | |
| case 'option': | |
| transaction.getReactMountReady().enqueue( | |
| optionPostMount, | |
| this | |
| ); | |
| break; | |
| } | |
| 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 (__DEV__) { | |
| // See `_updateDOMProperties`. style block | |
| this._previousStyle = propValue; | |
| } | |
| propValue = this._previousStyleCopy = Object.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._hostParent) { | |
| 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 (__DEV__) { | |
| setAndValidateContentChildDev.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 (__DEV__) { | |
| setAndValidateContentChildDev.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 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 'input': | |
| lastProps = ReactDOMInput.getHostProps(this, lastProps); | |
| nextProps = ReactDOMInput.getHostProps(this, nextProps); | |
| break; | |
| case 'option': | |
| lastProps = ReactDOMOption.getHostProps(this, lastProps); | |
| nextProps = ReactDOMOption.getHostProps(this, nextProps); | |
| break; | |
| case 'select': | |
| lastProps = ReactDOMSelect.getHostProps(this, lastProps); | |
| nextProps = ReactDOMSelect.getHostProps(this, nextProps); | |
| break; | |
| case 'textarea': | |
| lastProps = ReactDOMTextarea.getHostProps(this, lastProps); | |
| nextProps = ReactDOMTextarea.getHostProps(this, nextProps); | |
| break; | |
| } | |
| assertValidProps(this, nextProps); | |
| this._updateDOMProperties(lastProps, nextProps, transaction); | |
| this._updateDOMChildren( | |
| lastProps, | |
| nextProps, | |
| transaction, | |
| context | |
| ); | |
| switch (this._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. | |
| ReactDOMInput.updateWrapper(this); | |
| break; | |
| case 'textarea': | |
| ReactDOMTextarea.updateWrapper(this); | |
| break; | |
| case 'select': | |
| // <select> value update needs to occur after <option> children | |
| // reconciliation | |
| transaction.getReactMountReady().enqueue(postUpdateSelectWrapper, this); | |
| break; | |
| } | |
| }, | |
| /** | |
| * 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 (isCustomComponent(this._tag, lastProps)) { | |
| if (!RESERVED_PROPS.hasOwnProperty(propKey)) { | |
| DOMPropertyOperations.deleteValueForAttribute( | |
| getNode(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 (__DEV__) { | |
| checkAndWarnForMutatedStyle( | |
| this._previousStyleCopy, | |
| this._previousStyle, | |
| this | |
| ); | |
| this._previousStyle = nextProp; | |
| } | |
| nextProp = this._previousStyleCopy = Object.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 (__DEV__) { | |
| ReactInstrumentation.debugTool.onSetChildren(this._debugID, []); | |
| } | |
| } | |
| if (nextContent != null) { | |
| if (lastContent !== nextContent) { | |
| this.updateTextContent('' + nextContent); | |
| if (__DEV__) { | |
| setAndValidateContentChildDev.call(this, nextContent); | |
| } | |
| } | |
| } else if (nextHtml != null) { | |
| if (lastHtml !== nextHtml) { | |
| this.updateMarkup('' + nextHtml); | |
| } | |
| if (__DEV__) { | |
| ReactInstrumentation.debugTool.onSetChildren(this._debugID, []); | |
| } | |
| } else if (nextChildren != null) { | |
| if (__DEV__) { | |
| setAndValidateContentChildDev.call(this, null); | |
| } | |
| this.updateChildren(nextChildren, transaction, context); | |
| } | |
| }, | |
| getHostNode: 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 'audio': | |
| case 'form': | |
| case 'iframe': | |
| case 'img': | |
| case 'link': | |
| case 'object': | |
| case 'source': | |
| case 'video': | |
| var listeners = this._wrapperState.listeners; | |
| if (listeners) { | |
| for (var i = 0; i < listeners.length; i++) { | |
| listeners[i].remove(); | |
| } | |
| } | |
| break; | |
| case 'input': | |
| case 'textarea': | |
| inputValueTracking.stopTracking(this); | |
| 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. | |
| */ | |
| 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 | |
| ); | |
| break; | |
| } | |
| this.unmountChildren(safely); | |
| ReactDOMComponentTree.uncacheNode(this); | |
| EventPluginHub.deleteAllListeners(this); | |
| this._rootNodeID = 0; | |
| this._domID = 0; | |
| this._wrapperState = null; | |
| if (__DEV__) { | |
| setAndValidateContentChildDev.call(this, null); | |
| } | |
| }, | |
| getPublicInstance: function() { | |
| return getNode(this); | |
| }, | |
| }; | |
| Object.assign( | |
| ReactDOMComponent.prototype, | |
| ReactDOMComponent.Mixin, | |
| ReactMultiChild | |
| ); | |
| module.exports = ReactDOMComponent; |