UNPKG

closeheat-builder

Version:
507 lines (463 loc) 17 kB
/** * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactDOMComponent * @typechecks static-only */ /* global hasOwnProperty:true */ 'use strict'; var CSSPropertyOperations = require("./CSSPropertyOperations"); var DOMProperty = require("./DOMProperty"); var DOMPropertyOperations = require("./DOMPropertyOperations"); var ReactBrowserEventEmitter = require("./ReactBrowserEventEmitter"); var ReactComponentBrowserEnvironment = require("./ReactComponentBrowserEnvironment"); var ReactMount = require("./ReactMount"); var ReactMultiChild = require("./ReactMultiChild"); var ReactPerf = require("./ReactPerf"); var assign = require("./Object.assign"); var escapeTextContentForBrowser = require("./escapeTextContentForBrowser"); var invariant = require("./invariant"); var isEventSupported = require("./isEventSupported"); var keyOf = require("./keyOf"); var warning = require("./warning"); var deleteListener = ReactBrowserEventEmitter.deleteListener; var listenTo = ReactBrowserEventEmitter.listenTo; var registrationNameModules = ReactBrowserEventEmitter.registrationNameModules; // For quickly matching children type, to test if can be treated as content. var CONTENT_TYPES = {'string': true, 'number': true}; var STYLE = keyOf({style: null}); var ELEMENT_NODE_TYPE = 1; /** * Optionally injectable operations for mutating the DOM */ var BackendIDOperations = null; /** * @param {?object} props */ function assertValidProps(props) { if (!props) { return; } // Note the use of `==` which checks for null or undefined. if (props.dangerouslySetInnerHTML != null) { ("production" !== process.env.NODE_ENV ? invariant( props.children == null, 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.' ) : invariant(props.children == null)); ("production" !== process.env.NODE_ENV ? invariant( 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.' ) : invariant(typeof props.dangerouslySetInnerHTML === 'object' && '__html' in props.dangerouslySetInnerHTML)); } if ("production" !== process.env.NODE_ENV) { ("production" !== process.env.NODE_ENV ? warning( props.innerHTML == null, 'Directly setting property `innerHTML` is not permitted. ' + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.' ) : null); ("production" !== process.env.NODE_ENV ? warning( !props.contentEditable || props.children == null, 'A component is `contentEditable` and contains `children` managed by ' + 'React. It is now your responsibility to guarantee that none of ' + 'those nodes are unexpectedly modified or duplicated. This is ' + 'probably not intentional.' ) : null); } ("production" !== process.env.NODE_ENV ? invariant( props.style == null || typeof props.style === 'object', 'The `style` prop expects a mapping from style properties to values, ' + 'not a string. For example, style={{marginRight: spacing + \'em\'}} when ' + 'using JSX.' ) : invariant(props.style == null || typeof props.style === 'object')); } function putListener(id, registrationName, listener, transaction) { if ("production" !== process.env.NODE_ENV) { // IE8 has no API for event capturing and the `onScroll` event doesn't // bubble. ("production" !== process.env.NODE_ENV ? warning( registrationName !== 'onScroll' || isEventSupported('scroll', true), 'This browser doesn\'t support the `onScroll` event' ) : null); } var container = ReactMount.findReactContainerForID(id); if (container) { var doc = container.nodeType === ELEMENT_NODE_TYPE ? container.ownerDocument : container; listenTo(registrationName, doc); } transaction.getPutListenerQueue().enqueuePutListener( id, registrationName, listener ); } // For HTML, certain tags should omit their close tag. We keep a whitelist for // those special cased tags. var omittedCloseTags = { 'area': true, 'base': true, 'br': true, 'col': true, 'embed': true, 'hr': true, 'img': true, 'input': true, 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, 'track': true, 'wbr': true // NOTE: menuitem's close tag should be omitted, but that causes problems. }; // We accept any tag to be rendered but since this gets injected into abitrary // HTML, we want to make sure that it's a safe tag. // http://www.w3.org/TR/REC-xml/#NT-Name var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset var validatedTagCache = {}; var hasOwnProperty = {}.hasOwnProperty; function validateDangerousTag(tag) { if (!hasOwnProperty.call(validatedTagCache, tag)) { ("production" !== process.env.NODE_ENV ? invariant(VALID_TAG_REGEX.test(tag), 'Invalid tag: %s', tag) : invariant(VALID_TAG_REGEX.test(tag))); validatedTagCache[tag] = true; } } /** * Creates a new React class that is idempotent and capable of containing other * React components. It accepts event listeners and DOM properties that are * valid according to `DOMProperty`. * * - Event listeners: `onClick`, `onMouseDown`, etc. * - DOM properties: `className`, `name`, `title`, etc. * * The `style` property functions differently from the DOM API. It accepts an * object mapping of style properties to values. * * @constructor ReactDOMComponent * @extends ReactMultiChild */ function ReactDOMComponent(tag) { validateDangerousTag(tag); this._tag = tag; this._renderedChildren = null; this._previousStyleCopy = null; this._rootNodeID = null; } ReactDOMComponent.displayName = 'ReactDOMComponent'; ReactDOMComponent.Mixin = { construct: function(element) { this._currentElement = element; }, /** * Generates root tag markup then recurses. This method has side effects and * is not idempotent. * * @internal * @param {string} rootID The root DOM ID for this node. * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction * @return {string} The computed markup. */ mountComponent: function(rootID, transaction, context) { this._rootNodeID = rootID; assertValidProps(this._currentElement.props); var closeTag = omittedCloseTags[this._tag] ? '' : '</' + this._tag + '>'; return ( this._createOpenTagMarkupAndPutListeners(transaction) + this._createContentMarkup(transaction, context) + closeTag ); }, /** * Creates markup for the open tag and all attributes. * * This method has side effects because events get registered. * * Iterating over object properties is faster than iterating over arrays. * @see http://jsperf.com/obj-vs-arr-iteration * * @private * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction * @return {string} Markup of opening tag. */ _createOpenTagMarkupAndPutListeners: function(transaction) { var props = this._currentElement.props; var ret = '<' + this._tag; for (var propKey in props) { if (!props.hasOwnProperty(propKey)) { continue; } var propValue = props[propKey]; if (propValue == null) { continue; } if (registrationNameModules.hasOwnProperty(propKey)) { putListener(this._rootNodeID, propKey, propValue, transaction); } else { if (propKey === STYLE) { if (propValue) { propValue = this._previousStyleCopy = assign({}, props.style); } propValue = CSSPropertyOperations.createMarkupForStyles(propValue); } var markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue); if (markup) { ret += ' ' + markup; } } } // For static pages, no need to put React ID and checksum. Saves lots of // bytes. if (transaction.renderToStaticMarkup) { return ret + '>'; } var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID); return ret + ' ' + markupForID + '>'; }, /** * Creates markup for the content between the tags. * * @private * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction * @param {object} context * @return {string} Content markup. */ _createContentMarkup: function(transaction, context) { var prefix = ''; if (this._tag === 'listing' || this._tag === 'pre' || this._tag === 'textarea') { // Add an initial newline because browsers ignore the first newline in // a <listing>, <pre>, or <textarea> as an "authoring convenience" -- see // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody. prefix = '\n'; } var props = this._currentElement.props; // Intentional use of != to avoid catching zero/false. var innerHTML = props.dangerouslySetInnerHTML; if (innerHTML != null) { if (innerHTML.__html != null) { return prefix + innerHTML.__html; } } else { var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null; var childrenToUse = contentToUse != null ? null : props.children; if (contentToUse != null) { return prefix + escapeTextContentForBrowser(contentToUse); } else if (childrenToUse != null) { var mountImages = this.mountChildren( childrenToUse, transaction, context ); return prefix + mountImages.join(''); } } return prefix; }, receiveComponent: function(nextElement, transaction, context) { var prevElement = this._currentElement; this._currentElement = nextElement; this.updateComponent(transaction, prevElement, nextElement, context); }, /** * Updates a native DOM component after it has already been allocated and * attached to the DOM. Reconciles the root DOM node, then recurses. * * @param {ReactReconcileTransaction} transaction * @param {ReactElement} prevElement * @param {ReactElement} nextElement * @internal * @overridable */ updateComponent: function(transaction, prevElement, nextElement, context) { assertValidProps(this._currentElement.props); this._updateDOMProperties(prevElement.props, transaction); this._updateDOMChildren(prevElement.props, transaction, context); }, /** * Reconciles the properties by detecting differences in property values and * updating the DOM as necessary. This function is probably the single most * critical path for performance optimization. * * TODO: Benchmark whether checking for changed values in memory actually * improves performance (especially statically positioned elements). * TODO: Benchmark the effects of putting this at the top since 99% of props * do not change for a given reconciliation. * TODO: Benchmark areas that can be improved with caching. * * @private * @param {object} lastProps * @param {ReactReconcileTransaction} transaction */ _updateDOMProperties: function(lastProps, transaction) { var nextProps = this._currentElement.props; var propKey; var styleName; var styleUpdates; for (propKey in lastProps) { if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) { continue; } if (propKey === STYLE) { var lastStyle = this._previousStyleCopy; for (styleName in lastStyle) { if (lastStyle.hasOwnProperty(styleName)) { styleUpdates = styleUpdates || {}; styleUpdates[styleName] = ''; } } this._previousStyleCopy = null; } else if (registrationNameModules.hasOwnProperty(propKey)) { deleteListener(this._rootNodeID, propKey); } else if ( DOMProperty.isStandardName[propKey] || DOMProperty.isCustomAttribute(propKey)) { BackendIDOperations.deletePropertyByID( this._rootNodeID, propKey ); } } for (propKey in nextProps) { var nextProp = nextProps[propKey]; var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps[propKey]; if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) { continue; } if (propKey === STYLE) { if (nextProp) { nextProp = this._previousStyleCopy = assign({}, nextProp); } else { this._previousStyleCopy = null; } if (lastProp) { // Unset styles on `lastProp` but not on `nextProp`. for (styleName in lastProp) { if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) { styleUpdates = styleUpdates || {}; styleUpdates[styleName] = ''; } } // Update styles that changed since `lastProp`. for (styleName in nextProp) { if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) { styleUpdates = styleUpdates || {}; styleUpdates[styleName] = nextProp[styleName]; } } } else { // Relies on `updateStylesByID` not mutating `styleUpdates`. styleUpdates = nextProp; } } else if (registrationNameModules.hasOwnProperty(propKey)) { putListener(this._rootNodeID, propKey, nextProp, transaction); } else if ( DOMProperty.isStandardName[propKey] || DOMProperty.isCustomAttribute(propKey)) { BackendIDOperations.updatePropertyByID( this._rootNodeID, propKey, nextProp ); } } if (styleUpdates) { BackendIDOperations.updateStylesByID( this._rootNodeID, styleUpdates ); } }, /** * Reconciles the children with the various properties that affect the * children content. * * @param {object} lastProps * @param {ReactReconcileTransaction} transaction */ _updateDOMChildren: function(lastProps, transaction, context) { var nextProps = this._currentElement.props; var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null; var nextContent = CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null; var lastHtml = lastProps.dangerouslySetInnerHTML && lastProps.dangerouslySetInnerHTML.__html; var nextHtml = nextProps.dangerouslySetInnerHTML && nextProps.dangerouslySetInnerHTML.__html; // Note the use of `!=` which checks for null or undefined. var lastChildren = lastContent != null ? null : lastProps.children; var nextChildren = nextContent != null ? null : nextProps.children; // If we're switching from children to content/html or vice versa, remove // the old content var lastHasContentOrHtml = lastContent != null || lastHtml != null; var nextHasContentOrHtml = nextContent != null || nextHtml != null; if (lastChildren != null && nextChildren == null) { this.updateChildren(null, transaction, context); } else if (lastHasContentOrHtml && !nextHasContentOrHtml) { this.updateTextContent(''); } if (nextContent != null) { if (lastContent !== nextContent) { this.updateTextContent('' + nextContent); } } else if (nextHtml != null) { if (lastHtml !== nextHtml) { BackendIDOperations.updateInnerHTMLByID( this._rootNodeID, nextHtml ); } } else if (nextChildren != null) { this.updateChildren(nextChildren, transaction, context); } }, /** * Destroys all event registrations for this instance. Does not remove from * the DOM. That must be done by the parent. * * @internal */ unmountComponent: function() { this.unmountChildren(); ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID); ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID); this._rootNodeID = null; } }; ReactPerf.measureMethods(ReactDOMComponent, 'ReactDOMComponent', { mountComponent: 'mountComponent', updateComponent: 'updateComponent' }); assign( ReactDOMComponent.prototype, ReactDOMComponent.Mixin, ReactMultiChild.Mixin ); ReactDOMComponent.injection = { injectIDOperations: function(IDOperations) { ReactDOMComponent.BackendIDOperations = BackendIDOperations = IDOperations; } }; module.exports = ReactDOMComponent;