Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
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; |