react
Advanced tools
Comparing version 0.8.0 to 0.9.0-rc1
module.exports = require('./lib/ReactWithAddons'); | ||
if ('production' !== process.env.NODE_ENV) { | ||
module.exports = require('./ReactJSErrors').wrap(module.exports); | ||
} |
@@ -25,2 +25,3 @@ /** | ||
var ExecutionEnvironment = require("./ExecutionEnvironment"); | ||
var ReactUpdates = require("./ReactUpdates"); | ||
var SyntheticEvent = require("./SyntheticEvent"); | ||
@@ -39,3 +40,13 @@ | ||
captured: keyOf({onChangeCapture: null}) | ||
} | ||
}, | ||
dependencies: [ | ||
topLevelTypes.topBlur, | ||
topLevelTypes.topChange, | ||
topLevelTypes.topClick, | ||
topLevelTypes.topFocus, | ||
topLevelTypes.topInput, | ||
topLevelTypes.topKeyDown, | ||
topLevelTypes.topKeyUp, | ||
topLevelTypes.topSelectionChange | ||
] | ||
} | ||
@@ -78,6 +89,17 @@ }; | ||
// If change bubbled, we'd just bind to it like all the other events | ||
// and have it go through ReactEventTopLevelCallback. Since it doesn't, we | ||
// manually listen for the change event and so we have to enqueue and | ||
// If change and propertychange bubbled, we'd just bind to it like all the | ||
// other events and have it go through ReactEventTopLevelCallback. Since it | ||
// doesn't, we manually listen for the events and so we have to enqueue and | ||
// process the abstract event manually. | ||
// | ||
// Batching is necessary here in order to ensure that all event handlers run | ||
// before the next rerender (including event handlers attached to ancestor | ||
// elements instead of directly on the input). Without this, controlled | ||
// components don't work properly in conjunction with event bubbling because | ||
// the component is rerendered and the value reverted before all the event | ||
// handlers can run. See https://github.com/facebook/react/issues/708. | ||
ReactUpdates.batchedUpdates(runEventInBatch, event); | ||
} | ||
function runEventInBatch(event) { | ||
EventPluginHub.enqueueEvents(event); | ||
@@ -84,0 +106,0 @@ EventPluginHub.processEventQueue(); |
@@ -34,4 +34,18 @@ /** | ||
var useCompositionEvent = ExecutionEnvironment.canUseDOM && | ||
'CompositionEvent' in window; | ||
var useCompositionEvent = ( | ||
ExecutionEnvironment.canUseDOM && | ||
'CompositionEvent' in window | ||
); | ||
// In IE9+, we have access to composition events, but the data supplied | ||
// by the native compositionend event may be incorrect. In Korean, for example, | ||
// the compositionend event contains only one character regardless of | ||
// how many characters have been composed since compositionstart. | ||
// We therefore use the fallback data while still using the native | ||
// events as triggers. | ||
var useFallbackData = ( | ||
!useCompositionEvent || | ||
'documentMode' in document && document.documentMode > 8 | ||
); | ||
var topLevelTypes = EventConstants.topLevelTypes; | ||
@@ -46,3 +60,11 @@ var currentComposition = null; | ||
captured: keyOf({onCompositionEndCapture: null}) | ||
} | ||
}, | ||
dependencies: [ | ||
topLevelTypes.topBlur, | ||
topLevelTypes.topCompositionEnd, | ||
topLevelTypes.topKeyDown, | ||
topLevelTypes.topKeyPress, | ||
topLevelTypes.topKeyUp, | ||
topLevelTypes.topMouseDown | ||
] | ||
}, | ||
@@ -53,3 +75,11 @@ compositionStart: { | ||
captured: keyOf({onCompositionStartCapture: null}) | ||
} | ||
}, | ||
dependencies: [ | ||
topLevelTypes.topBlur, | ||
topLevelTypes.topCompositionStart, | ||
topLevelTypes.topKeyDown, | ||
topLevelTypes.topKeyPress, | ||
topLevelTypes.topKeyUp, | ||
topLevelTypes.topMouseDown | ||
] | ||
}, | ||
@@ -60,3 +90,11 @@ compositionUpdate: { | ||
captured: keyOf({onCompositionUpdateCapture: null}) | ||
} | ||
}, | ||
dependencies: [ | ||
topLevelTypes.topBlur, | ||
topLevelTypes.topCompositionUpdate, | ||
topLevelTypes.topKeyDown, | ||
topLevelTypes.topKeyPress, | ||
topLevelTypes.topKeyUp, | ||
topLevelTypes.topMouseDown | ||
] | ||
} | ||
@@ -190,11 +228,21 @@ }; | ||
if (isFallbackStart(topLevelType, nativeEvent)) { | ||
eventType = eventTypes.start; | ||
currentComposition = new FallbackCompositionState(topLevelTarget); | ||
eventType = eventTypes.compositionStart; | ||
} | ||
} else if (isFallbackEnd(topLevelType, nativeEvent)) { | ||
eventType = eventTypes.compositionEnd; | ||
data = currentComposition.getData(); | ||
currentComposition = null; | ||
} | ||
if (useFallbackData) { | ||
// The current composition is stored statically and must not be | ||
// overwritten while composition continues. | ||
if (!currentComposition && eventType === eventTypes.compositionStart) { | ||
currentComposition = new FallbackCompositionState(topLevelTarget); | ||
} else if (eventType === eventTypes.compositionEnd) { | ||
if (currentComposition) { | ||
data = currentComposition.getData(); | ||
currentComposition = null; | ||
} | ||
} | ||
} | ||
if (eventType) { | ||
@@ -201,0 +249,0 @@ var event = SyntheticCompositionEvent.getPooled( |
@@ -20,7 +20,5 @@ /** | ||
var toArray = require("./toArray"); | ||
/** | ||
* NOTE: if you are a previous user of this function, it has been considered | ||
* unsafe because it's inconsistent across browsers for some inputs. | ||
* Instead use `Array.isArray()`. | ||
* | ||
* Perform a heuristic test to determine if an object is "array-like". | ||
@@ -35,2 +33,4 @@ * | ||
* | ||
* It will return false for other array-like objects like Filelist. | ||
* | ||
* @param {*} obj | ||
@@ -77,4 +77,4 @@ * @return {boolean} | ||
* | ||
* This is also good for converting certain pseudo-arrays, like `arguments` or | ||
* HTMLCollections, into arrays. | ||
* If you need to convert an array-like object, like `arguments`, into an array | ||
* use toArray instead. | ||
* | ||
@@ -87,12 +87,9 @@ * @param {*} obj | ||
return [obj]; | ||
} else if (Array.isArray(obj)) { | ||
return obj.slice(); | ||
} else { | ||
return toArray(obj); | ||
} | ||
if (obj.item) { | ||
// IE does not support Array#slice on HTMLCollections | ||
var l = obj.length, ret = new Array(l); | ||
while (l--) { ret[l] = obj[l]; } | ||
return ret; | ||
} | ||
return Array.prototype.slice.call(obj); | ||
} | ||
module.exports = createArrayFrom; |
@@ -25,3 +25,3 @@ /** | ||
* that should be used when dealing with the display of elements (via their | ||
* CSS classes and visibility on screeni. It is an API focused on mutating the | ||
* CSS classes and visibility on screen. It is an API focused on mutating the | ||
* display and not reading it as no logical state should be encoded in the | ||
@@ -31,20 +31,2 @@ * display of elements. | ||
/** | ||
* Tests whether the element has the class specified. | ||
* | ||
* Note: This function is not exported in CSSCore because CSS classNames should | ||
* not store any logical information about the element. Use DataStore to store | ||
* information on an element. | ||
* | ||
* @param {DOMElement} element the element to set the class on | ||
* @param {string} className the CSS className | ||
* @returns {boolean} true if the element has the class, false if not | ||
*/ | ||
function hasClass(element, className) { | ||
if (element.classList) { | ||
return !!className && element.classList.contains(className); | ||
} | ||
return (' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1; | ||
} | ||
var CSSCore = { | ||
@@ -69,3 +51,3 @@ | ||
element.classList.add(className); | ||
} else if (!hasClass(element, className)) { | ||
} else if (!CSSCore.hasClass(element, className)) { | ||
element.className = element.className + ' ' + className; | ||
@@ -94,3 +76,3 @@ } | ||
element.classList.remove(className); | ||
} else if (hasClass(element, className)) { | ||
} else if (CSSCore.hasClass(element, className)) { | ||
element.className = element.className | ||
@@ -115,5 +97,24 @@ .replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), '$1') | ||
return (bool ? CSSCore.addClass : CSSCore.removeClass)(element, className); | ||
}, | ||
/** | ||
* Tests whether the element has the class specified. | ||
* | ||
* @param {DOMNode|DOMWindow} element the element to set the class on | ||
* @param {string} className the CSS className | ||
* @returns {boolean} true if the element has the class, false if not | ||
*/ | ||
hasClass: function(element, className) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!/\s/.test(className), | ||
'CSS.hasClass takes only a single class name.' | ||
) : invariant(!/\s/.test(className))); | ||
if (element.classList) { | ||
return !!className && element.classList.contains(className); | ||
} | ||
return (' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1; | ||
} | ||
}; | ||
module.exports = CSSCore; |
@@ -25,7 +25,14 @@ /** | ||
var isUnitlessNumber = { | ||
columnCount: true, | ||
fillOpacity: true, | ||
flex: true, | ||
flexGrow: true, | ||
flexShrink: true, | ||
fontWeight: true, | ||
lineClamp: true, | ||
lineHeight: true, | ||
opacity: true, | ||
order: true, | ||
orphans: true, | ||
widows: true, | ||
zIndex: true, | ||
@@ -36,2 +43,26 @@ zoom: true | ||
/** | ||
* @param {string} prefix vendor-specific prefix, eg: Webkit | ||
* @param {string} key style name, eg: transitionDuration | ||
* @return {string} style name prefixed with `prefix`, properly camelCased, eg: | ||
* WebkitTransitionDuration | ||
*/ | ||
function prefixKey(prefix, key) { | ||
return prefix + key.charAt(0).toUpperCase() + key.substring(1); | ||
} | ||
/** | ||
* Support style names that may come passed in prefixed by adding permutations | ||
* of vendor prefixes. | ||
*/ | ||
var prefixes = ['Webkit', 'ms', 'Moz', 'O']; | ||
// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an | ||
// infinite loop, because it iterates over the newly added props too. | ||
Object.keys(isUnitlessNumber).forEach(function(prop) { | ||
prefixes.forEach(function(prefix) { | ||
isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop]; | ||
}); | ||
}); | ||
/** | ||
* Most style properties can be unset by doing .style[prop] = '' but IE8 | ||
@@ -38,0 +69,0 @@ * doesn't like doing that with shorthand properties so for the properties that |
@@ -36,4 +36,4 @@ /** | ||
if (typeof classNames == 'object') { | ||
return Object.keys(classNames).map(function(className) { | ||
return classNames[className] ? className : ''; | ||
return Object.keys(classNames).filter(function(className) { | ||
return classNames[className]; | ||
}).join(' '); | ||
@@ -40,0 +40,0 @@ } else { |
@@ -30,3 +30,2 @@ /** | ||
var invariant = require("./invariant"); | ||
var mutateHTMLNodeWithMarkup = require("./mutateHTMLNodeWithMarkup"); | ||
@@ -175,8 +174,10 @@ var OPEN_TAG_NAME_EXP = /^(<[^ \/>]+)/; | ||
("production" !== process.env.NODE_ENV ? invariant(markup, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.') : invariant(markup)); | ||
// createNodesFromMarkup() won't work if the markup is rooted by <html> | ||
// since it has special semantic meaning. So we use an alternatie strategy. | ||
if (oldChild.tagName.toLowerCase() === 'html') { | ||
mutateHTMLNodeWithMarkup(oldChild, markup); | ||
return; | ||
} | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
oldChild.tagName.toLowerCase() !== 'html', | ||
'dangerouslyReplaceNodeWithMarkup(...): Cannot replace markup of the ' + | ||
'<html> node. This is because browser quirks make this unreliable ' + | ||
'and/or slow. If you want to render to the root you must use ' + | ||
'server rendering. See renderComponentToString().' | ||
) : invariant(oldChild.tagName.toLowerCase() !== 'html')); | ||
var newChild = createNodesFromMarkup(markup, emptyFunction)[0]; | ||
@@ -183,0 +184,0 @@ oldChild.parentNode.replaceChild(newChild, oldChild); |
@@ -48,3 +48,4 @@ /** | ||
autoComplete: null, | ||
autoFocus: HAS_BOOLEAN_VALUE, | ||
// autoFocus is polyfilled/normalized by AutoFocusMixin | ||
// autoFocus: HAS_BOOLEAN_VALUE, | ||
autoPlay: HAS_BOOLEAN_VALUE, | ||
@@ -70,2 +71,3 @@ cellPadding: null, | ||
form: MUST_USE_ATTRIBUTE, | ||
formNoValidate: HAS_BOOLEAN_VALUE, | ||
frameBorder: MUST_USE_ATTRIBUTE, | ||
@@ -89,2 +91,3 @@ height: MUST_USE_ATTRIBUTE, | ||
name: null, | ||
noValidate: HAS_BOOLEAN_VALUE, | ||
pattern: null, | ||
@@ -101,8 +104,13 @@ placeholder: null, | ||
rowSpan: null, | ||
sandbox: null, | ||
scope: null, | ||
scrollLeft: MUST_USE_PROPERTY, | ||
scrollTop: MUST_USE_PROPERTY, | ||
seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, | ||
selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, | ||
size: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE, | ||
span: HAS_POSITIVE_NUMERIC_VALUE, | ||
spellCheck: null, | ||
src: null, | ||
srcDoc: MUST_USE_PROPERTY, | ||
step: null, | ||
@@ -123,2 +131,3 @@ style: null, | ||
autoCorrect: null, // Supported in Mobile Safari for keyboard hints | ||
property: null, // Supports OG in meta tags | ||
@@ -177,3 +186,4 @@ /** | ||
radioGroup: 'radiogroup', | ||
spellCheck: 'spellcheck' | ||
spellCheck: 'spellcheck', | ||
srcDoc: 'srcdoc' | ||
}, | ||
@@ -180,0 +190,0 @@ DOMMutationMethods: { |
@@ -33,3 +33,3 @@ /** | ||
*/ | ||
var textContentAccessor = getTextContentAccessor() || 'NA'; | ||
var textContentAccessor = getTextContentAccessor(); | ||
@@ -62,2 +62,27 @@ /** | ||
/** | ||
* Sets the text content of `node` to `text`. | ||
* | ||
* @param {DOMElement} node Node to change | ||
* @param {string} text New text content | ||
*/ | ||
var updateTextContent; | ||
if (textContentAccessor === 'textContent') { | ||
updateTextContent = function(node, text) { | ||
node.textContent = text; | ||
}; | ||
} else { | ||
updateTextContent = function(node, text) { | ||
// In order to preserve newlines correctly, we can't use .innerText to set | ||
// the contents (see #1080), so we empty the element then append a text node | ||
while (node.firstChild) { | ||
node.removeChild(node.firstChild); | ||
} | ||
if (text) { | ||
var doc = node.ownerDocument || document; | ||
node.appendChild(doc.createTextNode(text)); | ||
} | ||
}; | ||
} | ||
/** | ||
* Operations for updating with DOM children. | ||
@@ -69,2 +94,4 @@ */ | ||
updateTextContent: updateTextContent, | ||
/** | ||
@@ -127,3 +154,6 @@ * Updates a component's children by processing a series of updates. The | ||
case ReactMultiChildUpdateTypes.TEXT_CONTENT: | ||
update.parentNode[textContentAccessor] = update.textContent; | ||
updateTextContent( | ||
update.parentNode, | ||
update.textContent | ||
); | ||
break; | ||
@@ -130,0 +160,0 @@ case ReactMultiChildUpdateTypes.REMOVE_NODE: |
@@ -157,2 +157,4 @@ /** | ||
ID_ATTRIBUTE_NAME: 'data-reactid', | ||
/** | ||
@@ -159,0 +161,0 @@ * Checks whether a property name is a standard property. |
@@ -39,3 +39,2 @@ /** | ||
var reactProps = { | ||
__owner__: true, | ||
children: true, | ||
@@ -77,2 +76,13 @@ dangerouslySetInnerHTML: true, | ||
/** | ||
* Creates markup for the ID property. | ||
* | ||
* @param {string} id Unescaped ID. | ||
* @return {string} Markup string. | ||
*/ | ||
createMarkupForID: function(id) { | ||
return processAttributeNameAndPrefix(DOMProperty.ID_ATTRIBUTE_NAME) + | ||
escapeTextForBrowser(id) + '"'; | ||
}, | ||
/** | ||
* Creates markup for a property. | ||
@@ -90,2 +100,5 @@ * | ||
var attributeName = DOMProperty.getAttributeName[name]; | ||
if (DOMProperty.hasBooleanValue[name]) { | ||
return escapeTextForBrowser(attributeName); | ||
} | ||
return processAttributeNameAndPrefix(attributeName) + | ||
@@ -92,0 +105,0 @@ escapeTextForBrowser(value) + '"'; |
@@ -33,4 +33,16 @@ /** | ||
var eventTypes = { | ||
mouseEnter: {registrationName: keyOf({onMouseEnter: null})}, | ||
mouseLeave: {registrationName: keyOf({onMouseLeave: null})} | ||
mouseEnter: { | ||
registrationName: keyOf({onMouseEnter: null}), | ||
dependencies: [ | ||
topLevelTypes.topMouseOut, | ||
topLevelTypes.topMouseOver | ||
] | ||
}, | ||
mouseLeave: { | ||
registrationName: keyOf({onMouseLeave: null}), | ||
dependencies: [ | ||
topLevelTypes.topMouseOut, | ||
topLevelTypes.topMouseOver | ||
] | ||
} | ||
}; | ||
@@ -73,2 +85,16 @@ | ||
var win; | ||
if (topLevelTarget.window === topLevelTarget) { | ||
// `topLevelTarget` is probably a window object. | ||
win = topLevelTarget; | ||
} else { | ||
// TODO: Figure out why `ownerDocument` is sometimes undefined in IE8. | ||
var doc = topLevelTarget.ownerDocument; | ||
if (doc) { | ||
win = doc.defaultView || doc.parentWindow; | ||
} else { | ||
win = window; | ||
} | ||
} | ||
var from, to; | ||
@@ -79,5 +105,5 @@ if (topLevelType === topLevelTypes.topMouseOut) { | ||
getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement) || | ||
window; | ||
win; | ||
} else { | ||
from = window; | ||
from = win; | ||
to = topLevelTarget; | ||
@@ -99,2 +125,6 @@ } | ||
); | ||
leave.type = 'mouseleave'; | ||
leave.target = from; | ||
leave.relatedTarget = to; | ||
var enter = SyntheticMouseEvent.getPooled( | ||
@@ -105,2 +135,5 @@ eventTypes.mouseEnter, | ||
); | ||
enter.type = 'mouseenter'; | ||
enter.target = to; | ||
enter.relatedTarget = from; | ||
@@ -107,0 +140,0 @@ EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID); |
@@ -47,2 +47,3 @@ /** | ||
topDrop: null, | ||
topError: null, | ||
topFocus: null, | ||
@@ -53,2 +54,3 @@ topInput: null, | ||
topKeyUp: null, | ||
topLoad: null, | ||
topMouseDown: null, | ||
@@ -60,2 +62,3 @@ topMouseMove: null, | ||
topPaste: null, | ||
topReset: null, | ||
topScroll: null, | ||
@@ -62,0 +65,0 @@ topSelectionChange: null, |
/** | ||
* Copyright 2013 Facebook, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* @providesModule EventListener | ||
*/ | ||
var emptyFunction = require("./emptyFunction"); | ||
/** | ||
@@ -25,13 +13,24 @@ * Upstream version of event listener. Does not take into account specific | ||
/** | ||
* Listens to bubbled events on a DOM node. | ||
* Listen to DOM events during the bubble phase. | ||
* | ||
* @param {Element} el DOM element to register listener on. | ||
* @param {string} handlerBaseName 'click'/'mouseover' | ||
* @param {Function!} cb Callback function | ||
* @param {DOMEventTarget} target DOM element to register listener on. | ||
* @param {string} eventType Event type, e.g. 'click' or 'mouseover'. | ||
* @param {function} callback Callback function. | ||
* @return {object} Object with a `remove` method. | ||
*/ | ||
listen: function(el, handlerBaseName, cb) { | ||
if (el.addEventListener) { | ||
el.addEventListener(handlerBaseName, cb, false); | ||
} else if (el.attachEvent) { | ||
el.attachEvent('on' + handlerBaseName, cb); | ||
listen: function(target, eventType, callback) { | ||
if (target.addEventListener) { | ||
target.addEventListener(eventType, callback, false); | ||
return { | ||
remove: function() { | ||
target.removeEventListener(eventType, callback, false); | ||
} | ||
}; | ||
} else if (target.attachEvent) { | ||
target.attachEvent('on' + eventType, callback); | ||
return { | ||
remove: function() { | ||
target.detachEvent(eventType, callback); | ||
} | ||
}; | ||
} | ||
@@ -41,19 +40,28 @@ }, | ||
/** | ||
* Listens to captured events on a DOM node. | ||
* Listen to DOM events during the capture phase. | ||
* | ||
* @see `EventListener.listen` for params. | ||
* @throws Exception if addEventListener is not supported. | ||
* @param {DOMEventTarget} target DOM element to register listener on. | ||
* @param {string} eventType Event type, e.g. 'click' or 'mouseover'. | ||
* @param {function} callback Callback function. | ||
* @return {object} Object with a `remove` method. | ||
*/ | ||
capture: function(el, handlerBaseName, cb) { | ||
if (!el.addEventListener) { | ||
capture: function(target, eventType, callback) { | ||
if (!target.addEventListener) { | ||
if ("production" !== process.env.NODE_ENV) { | ||
console.error( | ||
'You are attempting to use addEventListener ' + | ||
'in a browser that does not support it.' + | ||
'This likely means that you will not receive events that ' + | ||
'your application relies on (such as scroll).'); | ||
'Attempted to listen to events during the capture phase on a ' + | ||
'browser that does not support the capture phase. Your application ' + | ||
'will not receive some events.' | ||
); | ||
} | ||
return; | ||
return { | ||
remove: emptyFunction | ||
}; | ||
} else { | ||
el.addEventListener(handlerBaseName, cb, true); | ||
target.addEventListener(eventType, callback, true); | ||
return { | ||
remove: function() { | ||
target.removeEventListener(eventType, callback, true); | ||
} | ||
}; | ||
} | ||
@@ -60,0 +68,0 @@ } |
@@ -21,6 +21,4 @@ /** | ||
var CallbackRegistry = require("./CallbackRegistry"); | ||
var EventPluginRegistry = require("./EventPluginRegistry"); | ||
var EventPluginUtils = require("./EventPluginUtils"); | ||
var EventPropagators = require("./EventPropagators"); | ||
var ExecutionEnvironment = require("./ExecutionEnvironment"); | ||
@@ -31,4 +29,10 @@ | ||
var invariant = require("./invariant"); | ||
var isEventSupported = require("./isEventSupported"); | ||
/** | ||
* Internal store for event listeners | ||
*/ | ||
var listenerBank = {}; | ||
/** | ||
* Internal queue of events that have accumulated their dispatches and are | ||
@@ -62,2 +66,17 @@ * waiting to have their dispatches executed. | ||
/** | ||
* - `InstanceHandle`: [required] Module that performs logical traversals of DOM | ||
* hierarchy given ids of the logical DOM elements involved. | ||
*/ | ||
var InstanceHandle = null; | ||
function validateInstanceHandle() { | ||
var invalid = !InstanceHandle|| | ||
!InstanceHandle.traverseTwoPhase || | ||
!InstanceHandle.traverseEnterLeave; | ||
if (invalid) { | ||
throw new Error('InstanceHandle not injected before use!'); | ||
} | ||
} | ||
/** | ||
* This is a unified interface for event plugins to be installed and configured. | ||
@@ -92,7 +111,25 @@ * | ||
/** | ||
* @param {object} InjectedMount | ||
* @public | ||
*/ | ||
injectMount: EventPluginUtils.injection.injectMount, | ||
/** | ||
* @param {object} InjectedInstanceHandle | ||
* @public | ||
*/ | ||
injectInstanceHandle: EventPropagators.injection.injectInstanceHandle, | ||
injectInstanceHandle: function(InjectedInstanceHandle) { | ||
InstanceHandle = InjectedInstanceHandle; | ||
if ("production" !== process.env.NODE_ENV) { | ||
validateInstanceHandle(); | ||
} | ||
}, | ||
getInstanceHandle: function() { | ||
if ("production" !== process.env.NODE_ENV) { | ||
validateInstanceHandle(); | ||
} | ||
return InstanceHandle; | ||
}, | ||
/** | ||
@@ -111,13 +148,70 @@ * @param {array} InjectedEventPluginOrder | ||
registrationNames: EventPluginRegistry.registrationNames, | ||
registrationNameModules: EventPluginRegistry.registrationNameModules, | ||
putListener: CallbackRegistry.putListener, | ||
/** | ||
* Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent. | ||
* | ||
* @param {string} id ID of the DOM element. | ||
* @param {string} registrationName Name of listener (e.g. `onClick`). | ||
* @param {?function} listener The callback to store. | ||
*/ | ||
putListener: function(id, registrationName, listener) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
ExecutionEnvironment.canUseDOM, | ||
'Cannot call putListener() in a non-DOM environment.' | ||
) : invariant(ExecutionEnvironment.canUseDOM)); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!listener || typeof listener === 'function', | ||
'Expected %s listener to be a function, instead got type %s', | ||
registrationName, typeof listener | ||
) : invariant(!listener || typeof listener === 'function')); | ||
getListener: CallbackRegistry.getListener, | ||
if ("production" !== process.env.NODE_ENV) { | ||
// IE8 has no API for event capturing and the `onScroll` event doesn't | ||
// bubble. | ||
if (registrationName === 'onScroll' && | ||
!isEventSupported('scroll', true)) { | ||
console.warn('This browser doesn\'t support the `onScroll` event'); | ||
} | ||
} | ||
var bankForRegistrationName = | ||
listenerBank[registrationName] || (listenerBank[registrationName] = {}); | ||
bankForRegistrationName[id] = listener; | ||
}, | ||
deleteListener: CallbackRegistry.deleteListener, | ||
/** | ||
* @param {string} id ID of the DOM element. | ||
* @param {string} registrationName Name of listener (e.g. `onClick`). | ||
* @return {?function} The stored callback. | ||
*/ | ||
getListener: function(id, registrationName) { | ||
var bankForRegistrationName = listenerBank[registrationName]; | ||
return bankForRegistrationName && bankForRegistrationName[id]; | ||
}, | ||
deleteAllListeners: CallbackRegistry.deleteAllListeners, | ||
/** | ||
* Deletes a listener from the registration bank. | ||
* | ||
* @param {string} id ID of the DOM element. | ||
* @param {string} registrationName Name of listener (e.g. `onClick`). | ||
*/ | ||
deleteListener: function(id, registrationName) { | ||
var bankForRegistrationName = listenerBank[registrationName]; | ||
if (bankForRegistrationName) { | ||
delete bankForRegistrationName[id]; | ||
} | ||
}, | ||
/** | ||
* Deletes all listeners for the DOM element with the supplied ID. | ||
* | ||
* @param {string} id ID of the DOM element. | ||
*/ | ||
deleteAllListeners: function(id) { | ||
for (var registrationName in listenerBank) { | ||
delete listenerBank[registrationName][id]; | ||
} | ||
}, | ||
/** | ||
* Allows registered plugins an opportunity to extract events from top-level | ||
@@ -187,2 +281,13 @@ * native browser events. | ||
) : invariant(!eventQueue)); | ||
}, | ||
/** | ||
* These are needed for tests only. Do not use! | ||
*/ | ||
__purge: function() { | ||
listenerBank = {}; | ||
}, | ||
__getListenerBank: function() { | ||
return listenerBank; | ||
} | ||
@@ -192,6 +297,2 @@ | ||
if (ExecutionEnvironment.canUseDOM) { | ||
window.EventPluginHub = EventPluginHub; | ||
} | ||
module.exports = EventPluginHub; |
@@ -66,7 +66,15 @@ /** | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
publishEventForPlugin(publishedEvents[eventName], PluginModule), | ||
publishEventForPlugin( | ||
publishedEvents[eventName], | ||
PluginModule, | ||
eventName | ||
), | ||
'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.', | ||
eventName, | ||
pluginName | ||
) : invariant(publishEventForPlugin(publishedEvents[eventName], PluginModule))); | ||
) : invariant(publishEventForPlugin( | ||
publishedEvents[eventName], | ||
PluginModule, | ||
eventName | ||
))); | ||
} | ||
@@ -84,3 +92,3 @@ } | ||
*/ | ||
function publishEventForPlugin(dispatchConfig, PluginModule) { | ||
function publishEventForPlugin(dispatchConfig, PluginModule, eventName) { | ||
var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames; | ||
@@ -91,3 +99,7 @@ if (phasedRegistrationNames) { | ||
var phasedRegistrationName = phasedRegistrationNames[phaseName]; | ||
publishRegistrationName(phasedRegistrationName, PluginModule); | ||
publishRegistrationName( | ||
phasedRegistrationName, | ||
PluginModule, | ||
eventName | ||
); | ||
} | ||
@@ -97,3 +109,7 @@ } | ||
} else if (dispatchConfig.registrationName) { | ||
publishRegistrationName(dispatchConfig.registrationName, PluginModule); | ||
publishRegistrationName( | ||
dispatchConfig.registrationName, | ||
PluginModule, | ||
eventName | ||
); | ||
return true; | ||
@@ -112,10 +128,12 @@ } | ||
*/ | ||
function publishRegistrationName(registrationName, PluginModule) { | ||
function publishRegistrationName(registrationName, PluginModule, eventName) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!EventPluginRegistry.registrationNames[registrationName], | ||
!EventPluginRegistry.registrationNameModules[registrationName], | ||
'EventPluginHub: More than one plugin attempted to publish the same ' + | ||
'registration name, `%s`.', | ||
registrationName | ||
) : invariant(!EventPluginRegistry.registrationNames[registrationName])); | ||
EventPluginRegistry.registrationNames[registrationName] = PluginModule; | ||
) : invariant(!EventPluginRegistry.registrationNameModules[registrationName])); | ||
EventPluginRegistry.registrationNameModules[registrationName] = PluginModule; | ||
EventPluginRegistry.registrationNameDependencies[registrationName] = | ||
PluginModule.eventTypes[eventName].dependencies; | ||
} | ||
@@ -138,5 +156,10 @@ | ||
*/ | ||
registrationNames: {}, | ||
registrationNameModules: {}, | ||
/** | ||
* Mapping from registration name to event name | ||
*/ | ||
registrationNameDependencies: {}, | ||
/** | ||
* Injects an ordering of plugins (by plugin name). This allows the ordering | ||
@@ -203,3 +226,3 @@ * to be decoupled from injection of the actual plugins so that ordering is | ||
if (dispatchConfig.registrationName) { | ||
return EventPluginRegistry.registrationNames[ | ||
return EventPluginRegistry.registrationNameModules[ | ||
dispatchConfig.registrationName | ||
@@ -212,3 +235,3 @@ ] || null; | ||
} | ||
var PluginModule = EventPluginRegistry.registrationNames[ | ||
var PluginModule = EventPluginRegistry.registrationNameModules[ | ||
dispatchConfig.phasedRegistrationNames[phase] | ||
@@ -235,6 +258,6 @@ ]; | ||
EventPluginRegistry.plugins.length = 0; | ||
var registrationNames = EventPluginRegistry.registrationNames; | ||
for (var registrationName in registrationNames) { | ||
if (registrationNames.hasOwnProperty(registrationName)) { | ||
delete registrationNames[registrationName]; | ||
var registrationNameModules = EventPluginRegistry.registrationNameModules; | ||
for (var registrationName in registrationNameModules) { | ||
if (registrationNameModules.hasOwnProperty(registrationName)) { | ||
delete registrationNameModules[registrationName]; | ||
} | ||
@@ -241,0 +264,0 @@ } |
@@ -25,2 +25,24 @@ /** | ||
/** | ||
* Injected dependencies: | ||
*/ | ||
/** | ||
* - `Mount`: [required] Module that can convert between React dom IDs and | ||
* actual node references. | ||
*/ | ||
var injection = { | ||
Mount: null, | ||
injectMount: function(InjectedMount) { | ||
injection.Mount = InjectedMount; | ||
if ("production" !== process.env.NODE_ENV) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
InjectedMount && InjectedMount.getNode, | ||
'EventPluginUtils.injection.injectMount(...): Injected Mount module ' + | ||
'is missing getNode.' | ||
) : invariant(InjectedMount && InjectedMount.getNode)); | ||
} | ||
} | ||
}; | ||
var topLevelTypes = EventConstants.topLevelTypes; | ||
@@ -43,2 +65,3 @@ | ||
var validateEventDispatches; | ||
@@ -95,3 +118,6 @@ if ("production" !== process.env.NODE_ENV) { | ||
function executeDispatch(event, listener, domID) { | ||
listener(event, domID); | ||
event.currentTarget = injection.Mount.getNode(domID); | ||
var returnValue = listener(event, domID); | ||
event.currentTarget = null; | ||
return returnValue; | ||
} | ||
@@ -181,9 +207,12 @@ | ||
isStartish: isStartish, | ||
executeDirectDispatch: executeDirectDispatch, | ||
executeDispatch: executeDispatch, | ||
executeDispatchesInOrder: executeDispatchesInOrder, | ||
executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue, | ||
executeDirectDispatch: executeDirectDispatch, | ||
hasDispatches: hasDispatches, | ||
executeDispatch: executeDispatch | ||
injection: injection, | ||
useTouchEvents: false | ||
}; | ||
module.exports = EventPluginUtils; |
@@ -21,37 +21,12 @@ /** | ||
var CallbackRegistry = require("./CallbackRegistry"); | ||
var EventConstants = require("./EventConstants"); | ||
var EventPluginHub = require("./EventPluginHub"); | ||
var accumulate = require("./accumulate"); | ||
var forEachAccumulated = require("./forEachAccumulated"); | ||
var getListener = CallbackRegistry.getListener; | ||
var PropagationPhases = EventConstants.PropagationPhases; | ||
var getListener = EventPluginHub.getListener; | ||
/** | ||
* Injected dependencies: | ||
*/ | ||
/** | ||
* - `InstanceHandle`: [required] Module that performs logical traversals of DOM | ||
* hierarchy given ids of the logical DOM elements involved. | ||
*/ | ||
var injection = { | ||
InstanceHandle: null, | ||
injectInstanceHandle: function(InjectedInstanceHandle) { | ||
injection.InstanceHandle = InjectedInstanceHandle; | ||
if ("production" !== process.env.NODE_ENV) { | ||
injection.validate(); | ||
} | ||
}, | ||
validate: function() { | ||
var invalid = !injection.InstanceHandle|| | ||
!injection.InstanceHandle.traverseTwoPhase || | ||
!injection.InstanceHandle.traverseEnterLeave; | ||
if (invalid) { | ||
throw new Error('InstanceHandle not injected before use!'); | ||
} | ||
} | ||
}; | ||
/** | ||
* Some event types have a notion of different registration names for different | ||
@@ -77,3 +52,2 @@ * "phases" of propagation. This finds listeners by a given phase. | ||
} | ||
injection.validate(); | ||
} | ||
@@ -97,3 +71,3 @@ var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured; | ||
if (event && event.dispatchConfig.phasedRegistrationNames) { | ||
injection.InstanceHandle.traverseTwoPhase( | ||
EventPluginHub.injection.getInstanceHandle().traverseTwoPhase( | ||
event.dispatchMarker, | ||
@@ -135,5 +109,2 @@ accumulateDirectionalDispatches, | ||
function accumulateTwoPhaseDispatches(events) { | ||
if ("production" !== process.env.NODE_ENV) { | ||
injection.validate(); | ||
} | ||
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle); | ||
@@ -143,6 +114,3 @@ } | ||
function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) { | ||
if ("production" !== process.env.NODE_ENV) { | ||
injection.validate(); | ||
} | ||
injection.InstanceHandle.traverseEnterLeave( | ||
EventPluginHub.injection.getInstanceHandle().traverseEnterLeave( | ||
fromID, | ||
@@ -158,5 +126,2 @@ toID, | ||
function accumulateDirectDispatches(events) { | ||
if ("production" !== process.env.NODE_ENV) { | ||
injection.validate(); | ||
} | ||
forEachAccumulated(events, accumulateDirectDispatchesSingle); | ||
@@ -181,6 +146,5 @@ } | ||
accumulateDirectDispatches: accumulateDirectDispatches, | ||
accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches, | ||
injection: injection | ||
accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches | ||
}; | ||
module.exports = EventPropagators; |
@@ -37,2 +37,5 @@ /** | ||
canUseEventListeners: | ||
canUseDOM && (window.addEventListener || window.attachEvent), | ||
isInWorker: !canUseDOM // For now, this is true - might change in the future. | ||
@@ -39,0 +42,0 @@ |
@@ -38,7 +38,10 @@ /** | ||
) : invariant(!result.hasOwnProperty(name))); | ||
result[name] = child; | ||
if (child != null) { | ||
result[name] = child; | ||
} | ||
} | ||
/** | ||
* Flattens children that are typically specified as `props.children`. | ||
* Flattens children that are typically specified as `props.children`. Any null | ||
* children will not be included in the resulting object. | ||
* @return {!object} flattened children keyed by name. | ||
@@ -45,0 +48,0 @@ */ |
@@ -23,8 +23,10 @@ /** | ||
* not safe to call document.activeElement if there is nothing focused. | ||
* | ||
* The activeElement will be null only if the document body is not yet defined. | ||
*/ | ||
function getActiveElement() /*?DOMElement*/ { | ||
try { | ||
return document.activeElement; | ||
return document.activeElement || document.body; | ||
} catch (e) { | ||
return null; | ||
return document.body; | ||
} | ||
@@ -34,2 +36,1 @@ } | ||
module.exports = getActiveElement; | ||
@@ -39,7 +39,12 @@ /** | ||
'circle': true, | ||
'defs': true, | ||
'g': true, | ||
'line': true, | ||
'linearGradient': true, | ||
'path': true, | ||
'polygon': true, | ||
'polyline': true, | ||
'radialGradient': true, | ||
'rect': true, | ||
'stop': true, | ||
'text': true | ||
@@ -76,7 +81,12 @@ }; | ||
'circle': svgWrap, | ||
'defs': svgWrap, | ||
'g': svgWrap, | ||
'line': svgWrap, | ||
'linearGradient': svgWrap, | ||
'path': svgWrap, | ||
'polygon': svgWrap, | ||
'polyline': svgWrap, | ||
'radialGradient': svgWrap, | ||
'rect': svgWrap, | ||
'stop': svgWrap, | ||
'text': svgWrap | ||
@@ -83,0 +93,0 @@ }; |
@@ -33,5 +33,7 @@ /** | ||
if (!contentKey && ExecutionEnvironment.canUseDOM) { | ||
contentKey = 'innerText' in document.createElement('div') ? | ||
'innerText' : | ||
'textContent'; | ||
// Prefer textContent to innerText because many browsers support both but | ||
// SVG <text> elements don't support innerText even when <div> does. | ||
contentKey = 'textContent' in document.createElement('div') ? | ||
'textContent' : | ||
'innerText'; | ||
} | ||
@@ -38,0 +40,0 @@ return contentKey; |
@@ -35,4 +35,4 @@ /** | ||
return { | ||
x: document.documentElement.scrollLeft || document.body.scrollLeft, | ||
y: document.documentElement.scrollTop || document.body.scrollTop | ||
x: window.pageXOffset || document.documentElement.scrollLeft, | ||
y: window.pageYOffset || document.documentElement.scrollTop | ||
}; | ||
@@ -39,0 +39,0 @@ } |
@@ -22,4 +22,5 @@ /** | ||
* | ||
* Provide sprintf style format and arguments to provide information about | ||
* what broke and what you were expecting. | ||
* Provide sprintf-style format (only %s is supported) and arguments | ||
* to provide information about what broke and what you were | ||
* expecting. | ||
* | ||
@@ -32,3 +33,8 @@ * The invariant message will be stripped in production, but the invariant | ||
if (!condition) { | ||
throw new Error('Invariant Violation'); | ||
var error = new Error( | ||
'Minified exception occured; use the non-minified dev environment for ' + | ||
'the full error message and additional helpful warnings.' | ||
); | ||
error.framesToPop = 1; | ||
throw error; | ||
} | ||
@@ -48,6 +54,8 @@ } | ||
var argIndex = 0; | ||
throw new Error( | ||
var error = new Error( | ||
'Invariant Violation: ' + | ||
format.replace(/%s/g, function() { return args[argIndex++]; }) | ||
); | ||
error.framesToPop = 1; // we don't care about invariant's own frame | ||
throw error; | ||
} | ||
@@ -54,0 +62,0 @@ }; |
@@ -23,9 +23,9 @@ /** | ||
var testNode, useHasFeature; | ||
var useHasFeature; | ||
if (ExecutionEnvironment.canUseDOM) { | ||
testNode = document.createElement('div'); | ||
useHasFeature = | ||
document.implementation && | ||
document.implementation.hasFeature && | ||
// `hasFeature` always returns true in Firefox 19+. | ||
// always returns true in newer browsers as per the standard. | ||
// @see http://dom.spec.whatwg.org/#dom-domimplementation-hasfeature | ||
document.implementation.hasFeature('', '') !== true; | ||
@@ -49,17 +49,14 @@ } | ||
function isEventSupported(eventNameSuffix, capture) { | ||
if (!testNode || (capture && !testNode.addEventListener)) { | ||
if (!ExecutionEnvironment.canUseDOM || | ||
capture && !('addEventListener' in document)) { | ||
return false; | ||
} | ||
var element = document.createElement('div'); | ||
var eventName = 'on' + eventNameSuffix; | ||
var isSupported = eventName in element; | ||
var isSupported = eventName in document; | ||
if (!isSupported) { | ||
var element = document.createElement('div'); | ||
element.setAttribute(eventName, 'return;'); | ||
isSupported = typeof element[eventName] === 'function'; | ||
if (typeof element[eventName] !== 'undefined') { | ||
element[eventName] = undefined; | ||
} | ||
element.removeAttribute(eventName); | ||
} | ||
@@ -72,3 +69,2 @@ | ||
element = null; | ||
return isSupported; | ||
@@ -75,0 +71,0 @@ } |
@@ -69,5 +69,5 @@ /** | ||
Array.isArray(one) && Array.isArray(two), | ||
'Critical assumptions about the merge functions have been violated. ' + | ||
'This is the fault of the merge functions themselves, not necessarily ' + | ||
'the callers.' | ||
'Tried to merge arrays, instead got %s and %s.', | ||
one, | ||
two | ||
) : invariant(Array.isArray(one) && Array.isArray(two))); | ||
@@ -91,5 +91,4 @@ }, | ||
!isTerminal(arg) && !Array.isArray(arg), | ||
'Critical assumptions about the merge functions have been violated. ' + | ||
'This is the fault of the merge functions themselves, not necessarily ' + | ||
'the callers.' | ||
'Tried to merge an object, instead got %s.', | ||
arg | ||
) : invariant(!isTerminal(arg) && !Array.isArray(arg))); | ||
@@ -96,0 +95,0 @@ }, |
@@ -21,2 +21,4 @@ /** | ||
var invariant = require("./invariant"); | ||
/** | ||
@@ -75,2 +77,6 @@ * Static poolers. Several custom versions for each potential number of | ||
var Klass = this; | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
instance instanceof Klass, | ||
'Trying to release an instance into a pool of a different type.' | ||
) : invariant(instance instanceof Klass)); | ||
if (instance.destructor) { | ||
@@ -77,0 +83,0 @@ instance.destructor(); |
@@ -21,4 +21,8 @@ /** | ||
var DOMPropertyOperations = require("./DOMPropertyOperations"); | ||
var EventPluginUtils = require("./EventPluginUtils"); | ||
var ReactChildren = require("./ReactChildren"); | ||
var ReactComponent = require("./ReactComponent"); | ||
var ReactCompositeComponent = require("./ReactCompositeComponent"); | ||
var ReactContext = require("./ReactContext"); | ||
var ReactCurrentOwner = require("./ReactCurrentOwner"); | ||
@@ -36,9 +40,16 @@ var ReactDOM = require("./ReactDOM"); | ||
var onlyChild = require("./onlyChild"); | ||
ReactDefaultInjection.inject(); | ||
var React = { | ||
Children: { | ||
map: ReactChildren.map, | ||
forEach: ReactChildren.forEach, | ||
only: onlyChild | ||
}, | ||
DOM: ReactDOM, | ||
PropTypes: ReactPropTypes, | ||
initializeTouchEvents: function(shouldUseTouch) { | ||
ReactMount.useTouchEvents = shouldUseTouch; | ||
EventPluginUtils.useTouchEvents = shouldUseTouch; | ||
}, | ||
@@ -55,5 +66,5 @@ createClass: ReactCompositeComponent.createClass, | ||
unmountComponentAtNode: ReactMount.unmountComponentAtNode, | ||
unmountAndReleaseReactRootNode: ReactMount.unmountAndReleaseReactRootNode, | ||
isValidClass: ReactCompositeComponent.isValidClass, | ||
isValidComponent: ReactComponent.isValidComponent, | ||
withContext: ReactContext.withContext, | ||
__internals: { | ||
@@ -63,2 +74,3 @@ Component: ReactComponent, | ||
DOMComponent: ReactDOMComponent, | ||
DOMPropertyOperations: DOMPropertyOperations, | ||
InstanceHandles: ReactInstanceHandles, | ||
@@ -71,6 +83,18 @@ Mount: ReactMount, | ||
if ("production" !== process.env.NODE_ENV) { | ||
var ExecutionEnvironment = require("./ExecutionEnvironment"); | ||
if (ExecutionEnvironment.canUseDOM && | ||
window.top === window.self && | ||
navigator.userAgent.indexOf('Chrome') > -1) { | ||
console.debug( | ||
'Download the React DevTools for a better development experience: ' + | ||
'http://fb.me/react-devtools' | ||
); | ||
} | ||
} | ||
// Version exists only in the open-source version of React, not in Facebook's | ||
// internal version. | ||
React.version = '0.8.0'; | ||
React.version = '0.9.0-rc1'; | ||
module.exports = React; |
@@ -46,12 +46,16 @@ /** | ||
/** | ||
* Warn if there's no key explicitly set on dynamic arrays of children. | ||
* This allows us to keep track of children between updates. | ||
* Warn if there's no key explicitly set on dynamic arrays of children or | ||
* object keys are not valid. This allows us to keep track of children between | ||
* updates. | ||
*/ | ||
var ownerHasWarned = {}; | ||
var ownerHasExplicitKeyWarning = {}; | ||
var ownerHasPropertyWarning = {}; | ||
var NUMERIC_PROPERTY_REGEX = /^\d+$/; | ||
/** | ||
* Warn if the component doesn't have an explicit key assigned to it. | ||
* This component is in an array. The array could grow and shrink or be | ||
* reordered. All children, that hasn't already been validated, are required to | ||
* reordered. All children that haven't already been validated are required to | ||
* have a "key" property assigned to it. | ||
@@ -75,6 +79,6 @@ * | ||
var currentName = ReactCurrentOwner.current.constructor.displayName; | ||
if (ownerHasWarned.hasOwnProperty(currentName)) { | ||
if (ownerHasExplicitKeyWarning.hasOwnProperty(currentName)) { | ||
return; | ||
} | ||
ownerHasWarned[currentName] = true; | ||
ownerHasExplicitKeyWarning[currentName] = true; | ||
@@ -86,4 +90,4 @@ var message = 'Each child in an array should have a unique "key" prop. ' + | ||
var childOwnerName = | ||
component.props.__owner__ && | ||
component.props.__owner__.constructor.displayName; | ||
component._owner && | ||
component._owner.constructor.displayName; | ||
@@ -96,2 +100,3 @@ // Usually the current owner is the offender, but if it accepts | ||
message += ' See http://fb.me/react-warning-keys for more information.'; | ||
console.warn(message); | ||
@@ -101,6 +106,32 @@ } | ||
/** | ||
* Ensure that every component either is passed in a static location or, if | ||
* if it's passed in an array, has an explicit key property defined. | ||
* Warn if the key is being defined as an object property but has an incorrect | ||
* value. | ||
* | ||
* @internal | ||
* @param {string} name Property name of the key. | ||
* @param {ReactComponent} component Component that requires a key. | ||
*/ | ||
function validatePropertyKey(name) { | ||
if (NUMERIC_PROPERTY_REGEX.test(name)) { | ||
// Name of the component whose render method tried to pass children. | ||
var currentName = ReactCurrentOwner.current.constructor.displayName; | ||
if (ownerHasPropertyWarning.hasOwnProperty(currentName)) { | ||
return; | ||
} | ||
ownerHasPropertyWarning[currentName] = true; | ||
console.warn( | ||
'Child objects should have non-numeric keys so ordering is preserved. ' + | ||
'Check the render method of ' + currentName + '. ' + | ||
'See http://fb.me/react-warning-keys for more information.' | ||
); | ||
} | ||
} | ||
/** | ||
* Ensure that every component either is passed in a static location, in an | ||
* array with an explicit keys property defined, or in an object literal | ||
* with valid key property. | ||
* | ||
* @internal | ||
* @param {*} component Statically passed child of any type. | ||
@@ -120,2 +151,6 @@ * @return {boolean} | ||
component.__keyValidated__ = true; | ||
} else if (component && typeof component === 'object') { | ||
for (var name in component) { | ||
validatePropertyKey(name, component); | ||
} | ||
} | ||
@@ -157,6 +192,13 @@ } | ||
isValidComponent: function(object) { | ||
return !!( | ||
object && | ||
typeof object.mountComponentIntoNode === 'function' && | ||
typeof object.receiveComponent === 'function' | ||
if (!object || !object.type || !object.type.prototype) { | ||
return false; | ||
} | ||
// This is the safer way of duck checking the type of instance this is. | ||
// The object can be a generic descriptor but the type property refers to | ||
// the constructor and it's prototype can be used to inspect the type that | ||
// will actually get mounted. | ||
var prototype = object.type.prototype; | ||
return ( | ||
typeof prototype.mountComponentIntoNode === 'function' && | ||
typeof prototype.receiveComponent === 'function' | ||
); | ||
@@ -166,21 +208,4 @@ }, | ||
/** | ||
* Generate a key string that identifies a component within a set. | ||
* | ||
* @param {*} component A component that could contain a manual key. | ||
* @param {number} index Index that is used if a manual key is not provided. | ||
* @return {string} | ||
* @internal | ||
*/ | ||
getKey: function(component, index) { | ||
if (component && component.props && component.props.key != null) { | ||
// Explicit key | ||
return '{' + component.props.key + '}'; | ||
} | ||
// Implicit key determined by the index in the set | ||
return '[' + index + ']'; | ||
}, | ||
/** | ||
* @internal | ||
*/ | ||
LifeCycle: ComponentLifeCycle, | ||
@@ -195,3 +220,3 @@ | ||
*/ | ||
DOMIDOperations: ReactComponentEnvironment.DOMIDOperations, | ||
BackendIDOperations: ReactComponentEnvironment.BackendIDOperations, | ||
@@ -272,13 +297,13 @@ /** | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!this.props.__owner__, | ||
this.isMounted(), | ||
'replaceProps(...): Can only update a mounted component.' | ||
) : invariant(this.isMounted())); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
this._mountDepth === 0, | ||
'replaceProps(...): You called `setProps` or `replaceProps` on a ' + | ||
'component with an owner. This is an anti-pattern since props will ' + | ||
'component with a parent. This is an anti-pattern since props will ' + | ||
'get reactively updated when rendered. Instead, change the owner\'s ' + | ||
'`render` method to pass the correct value as props to the component ' + | ||
'where it is created.' | ||
) : invariant(!this.props.__owner__)); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
this.isMounted(), | ||
'replaceProps(...): Can only update a mounted component.' | ||
) : invariant(this.isMounted())); | ||
) : invariant(this._mountDepth === 0)); | ||
this._pendingProps = props; | ||
@@ -289,3 +314,3 @@ ReactUpdates.enqueueUpdate(this, callback); | ||
/** | ||
* Base constructor for all React component. | ||
* Base constructor for all React components. | ||
* | ||
@@ -302,3 +327,3 @@ * Subclasses that override this method should make sure to invoke | ||
// Record the component responsible for creating this component. | ||
this.props.__owner__ = ReactCurrentOwner.current; | ||
this._owner = ReactCurrentOwner.current; | ||
// All components start unmounted. | ||
@@ -310,2 +335,8 @@ this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; | ||
// Unlike _pendingProps and _pendingCallbacks, we won't use null to | ||
// indicate that nothing is pending because it's possible for a component | ||
// to have a null owner. Instead, an owner change is pending when | ||
// this._owner !== this._pendingOwner. | ||
this._pendingOwner = this._owner; | ||
// Children can be more than one argument | ||
@@ -347,3 +378,5 @@ var childrenLength = arguments.length - 1; | ||
!this.isMounted(), | ||
'mountComponent(%s, ...): Can only mount an unmounted component.', | ||
'mountComponent(%s, ...): Can only mount an unmounted component. ' + | ||
'Make sure to avoid storing components between renders or reusing a ' + | ||
'single component instance in multiple places.', | ||
rootID | ||
@@ -353,3 +386,3 @@ ) : invariant(!this.isMounted())); | ||
if (props.ref != null) { | ||
ReactOwner.addComponentAsRefTo(this, props.ref, props.__owner__); | ||
ReactOwner.addComponentAsRefTo(this, props.ref, this._owner); | ||
} | ||
@@ -379,3 +412,3 @@ this._rootNodeID = rootID; | ||
if (props.ref != null) { | ||
ReactOwner.removeComponentAsRefFrom(this, props.ref, props.__owner__); | ||
ReactOwner.removeComponentAsRefFrom(this, props.ref, this._owner); | ||
} | ||
@@ -403,2 +436,3 @@ ReactComponent.unmountIDFromEnvironment(this._rootNodeID); | ||
) : invariant(this.isMounted())); | ||
this._pendingOwner = nextComponent._owner; | ||
this._pendingProps = nextComponent.props; | ||
@@ -431,5 +465,7 @@ this._performUpdateIfNecessary(transaction); | ||
var prevProps = this.props; | ||
var prevOwner = this._owner; | ||
this.props = this._pendingProps; | ||
this._owner = this._pendingOwner; | ||
this._pendingProps = null; | ||
this.updateComponent(transaction, prevProps); | ||
this.updateComponent(transaction, prevProps, prevOwner); | ||
}, | ||
@@ -444,3 +480,3 @@ | ||
*/ | ||
updateComponent: function(transaction, prevProps) { | ||
updateComponent: function(transaction, prevProps, prevOwner) { | ||
var props = this.props; | ||
@@ -450,7 +486,6 @@ // If either the owner or a `ref` has changed, make sure the newest owner | ||
// has forgotten the reference to `this`. | ||
if (props.__owner__ !== prevProps.__owner__ || | ||
props.ref !== prevProps.ref) { | ||
if (this._owner !== prevOwner || props.ref !== prevProps.ref) { | ||
if (prevProps.ref != null) { | ||
ReactOwner.removeComponentAsRefFrom( | ||
this, prevProps.ref, prevProps.__owner__ | ||
this, prevProps.ref, prevOwner | ||
); | ||
@@ -460,3 +495,3 @@ } | ||
if (props.ref != null) { | ||
ReactOwner.addComponentAsRefTo(this, props.ref, props.__owner__); | ||
ReactOwner.addComponentAsRefTo(this, props.ref, this._owner); | ||
} | ||
@@ -515,3 +550,3 @@ } | ||
isOwnedBy: function(owner) { | ||
return this.props.__owner__ === owner; | ||
return this._owner === owner; | ||
}, | ||
@@ -528,3 +563,3 @@ | ||
getSiblingByRef: function(ref) { | ||
var owner = this.props.__owner__; | ||
var owner = this._owner; | ||
if (!owner || !owner.refs) { | ||
@@ -531,0 +566,0 @@ return null; |
@@ -26,2 +26,3 @@ /** | ||
var ReactMount = require("./ReactMount"); | ||
var ReactPerf = require("./ReactPerf"); | ||
var ReactReconcileTransaction = require("./ReactReconcileTransaction"); | ||
@@ -31,3 +32,2 @@ | ||
var invariant = require("./invariant"); | ||
var mutateHTMLNodeWithMarkup = require("./mutateHTMLNodeWithMarkup"); | ||
@@ -66,3 +66,3 @@ | ||
DOMIDOperations: ReactDOMIDOperations, | ||
BackendIDOperations: ReactDOMIDOperations, | ||
@@ -86,59 +86,77 @@ /** | ||
*/ | ||
mountImageIntoNode: function(markup, container, shouldReuseMarkup) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
container && ( | ||
mountImageIntoNode: ReactPerf.measure( | ||
'ReactComponentBrowserEnvironment', | ||
'mountImageIntoNode', | ||
function(markup, container, shouldReuseMarkup) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
container && ( | ||
container.nodeType === ELEMENT_NODE_TYPE || | ||
container.nodeType === DOC_NODE_TYPE | ||
), | ||
'mountComponentIntoNode(...): Target container is not valid.' | ||
) : invariant(container && ( | ||
container.nodeType === ELEMENT_NODE_TYPE || | ||
container.nodeType === DOC_NODE_TYPE && ReactMount.allowFullPageRender | ||
), | ||
'mountComponentIntoNode(...): Target container is not valid.' | ||
) : invariant(container && ( | ||
container.nodeType === ELEMENT_NODE_TYPE || | ||
container.nodeType === DOC_NODE_TYPE && ReactMount.allowFullPageRender | ||
))); | ||
if (shouldReuseMarkup) { | ||
if (ReactMarkupChecksum.canReuseMarkup( | ||
markup, | ||
getReactRootElementInContainer(container))) { | ||
return; | ||
} else { | ||
if ("production" !== process.env.NODE_ENV) { | ||
console.warn( | ||
'React attempted to use reuse markup in a container but the ' + | ||
'checksum was invalid. This generally means that you are using ' + | ||
'server rendering and the markup generated on the server was ' + | ||
'not what the client was expecting. React injected new markup ' + | ||
'to compensate which works but you have lost many of the ' + | ||
'benefits of server rendering. Instead, figure out why the ' + | ||
'markup being generated is different on the client or server.' | ||
); | ||
container.nodeType === DOC_NODE_TYPE | ||
))); | ||
if (shouldReuseMarkup) { | ||
if (ReactMarkupChecksum.canReuseMarkup( | ||
markup, | ||
getReactRootElementInContainer(container))) { | ||
return; | ||
} else { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
container.nodeType !== DOC_NODE_TYPE, | ||
'You\'re trying to render a component to the document using ' + | ||
'server rendering but the checksum was invalid. This usually ' + | ||
'means you rendered a different component type or props on ' + | ||
'the client from the one on the server, or your render() ' + | ||
'methods are impure. React cannot handle this case due to ' + | ||
'cross-browser quirks by rendering at the document root. You ' + | ||
'should look for environment dependent code in your components ' + | ||
'and ensure the props are the same client and server side.' | ||
) : invariant(container.nodeType !== DOC_NODE_TYPE)); | ||
if ("production" !== process.env.NODE_ENV) { | ||
console.warn( | ||
'React attempted to use reuse markup in a container but the ' + | ||
'checksum was invalid. This generally means that you are ' + | ||
'using server rendering and the markup generated on the ' + | ||
'server was not what the client was expecting. React injected' + | ||
'new markup to compensate which works but you have lost many ' + | ||
'of the benefits of server rendering. Instead, figure out ' + | ||
'why the markup being generated is different on the client ' + | ||
'or server.' | ||
); | ||
} | ||
} | ||
} | ||
} | ||
// You can't naively set the innerHTML of the entire document. You need | ||
// to mutate documentElement which requires doing some crazy tricks. See | ||
// mutateHTMLNodeWithMarkup() | ||
if (container.nodeType === DOC_NODE_TYPE) { | ||
mutateHTMLNodeWithMarkup(container.documentElement, markup); | ||
return; | ||
} | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
container.nodeType !== DOC_NODE_TYPE, | ||
'You\'re trying to render a component to the document but ' + | ||
'you didn\'t use server rendering. We can\'t do this ' + | ||
'without using server rendering due to cross-browser quirks. ' + | ||
'See renderComponentToString() for server rendering.' | ||
) : invariant(container.nodeType !== DOC_NODE_TYPE)); | ||
// Asynchronously inject markup by ensuring that the container is not in | ||
// the document when settings its `innerHTML`. | ||
var parent = container.parentNode; | ||
if (parent) { | ||
var next = container.nextSibling; | ||
parent.removeChild(container); | ||
container.innerHTML = markup; | ||
if (next) { | ||
parent.insertBefore(container, next); | ||
// Asynchronously inject markup by ensuring that the container is not in | ||
// the document when settings its `innerHTML`. | ||
var parent = container.parentNode; | ||
if (parent) { | ||
var next = container.nextSibling; | ||
parent.removeChild(container); | ||
container.innerHTML = markup; | ||
if (next) { | ||
parent.insertBefore(container, next); | ||
} else { | ||
parent.appendChild(container); | ||
} | ||
} else { | ||
parent.appendChild(container); | ||
container.innerHTML = markup; | ||
} | ||
} else { | ||
container.innerHTML = markup; | ||
} | ||
} | ||
) | ||
}; | ||
module.exports = ReactComponentBrowserEnvironment; |
@@ -19,2 +19,4 @@ /** | ||
"use strict"; | ||
var ReactComponentBrowserEnvironment = | ||
@@ -21,0 +23,0 @@ require("./ReactComponentBrowserEnvironment"); |
@@ -22,2 +22,3 @@ /** | ||
var ReactComponent = require("./ReactComponent"); | ||
var ReactContext = require("./ReactContext"); | ||
var ReactCurrentOwner = require("./ReactCurrentOwner"); | ||
@@ -28,2 +29,4 @@ var ReactErrorUtils = require("./ReactErrorUtils"); | ||
var ReactPropTransferer = require("./ReactPropTransferer"); | ||
var ReactPropTypeLocations = require("./ReactPropTypeLocations"); | ||
var ReactPropTypeLocationNames = require("./ReactPropTypeLocationNames"); | ||
var ReactUpdates = require("./ReactUpdates"); | ||
@@ -36,2 +39,3 @@ | ||
var objMap = require("./objMap"); | ||
var shouldUpdateReactComponent = require("./shouldUpdateReactComponent"); | ||
@@ -96,2 +100,11 @@ /** | ||
/** | ||
* An object containing properties and methods that should be defined on | ||
* the component's constructor instead of its prototype (static methods). | ||
* | ||
* @type {object} | ||
* @optional | ||
*/ | ||
statics: SpecPolicy.DEFINE_MANY, | ||
/** | ||
* Definition of prop types for this component. | ||
@@ -102,5 +115,19 @@ * | ||
*/ | ||
propTypes: SpecPolicy.DEFINE_ONCE, | ||
propTypes: SpecPolicy.DEFINE_MANY, | ||
/** | ||
* Definition of context types for this component. | ||
* | ||
* @type {object} | ||
* @optional | ||
*/ | ||
contextTypes: SpecPolicy.DEFINE_MANY, | ||
/** | ||
* Definition of context types this component sets for its children. | ||
* | ||
* @type {object} | ||
* @optional | ||
*/ | ||
childContextTypes: SpecPolicy.DEFINE_MANY, | ||
@@ -138,2 +165,8 @@ // ==== Definition methods ==== | ||
/** | ||
* @return {object} | ||
* @optional | ||
*/ | ||
getChildContext: SpecPolicy.DEFINE_MANY_MERGED, | ||
/** | ||
* Uses props from `this.props` and state from `this.state` to render the | ||
@@ -187,3 +220,3 @@ * structure of the component. | ||
* | ||
* componentWillReceiveProps: function(nextProps) { | ||
* componentWillReceiveProps: function(nextProps, nextContext) { | ||
* this.setState({ | ||
@@ -205,9 +238,12 @@ * likesIncreasing: nextProps.likeCount > this.props.likeCount | ||
* Invoked while deciding if the component should be updated as a result of | ||
* receiving new props and state. | ||
* receiving new props, state and/or context. | ||
* | ||
* Use this as an opportunity to `return false` when you're certain that the | ||
* transition to the new props and state will not require a component update. | ||
* transition to the new props/state/context will not require a component | ||
* update. | ||
* | ||
* shouldComponentUpdate: function(nextProps, nextState) { | ||
* return !equal(nextProps, this.props) || !equal(nextState, this.state); | ||
* shouldComponentUpdate: function(nextProps, nextState, nextContext) { | ||
* return !equal(nextProps, this.props) || | ||
* !equal(nextState, this.state) || | ||
* !equal(nextContext, this.context); | ||
* } | ||
@@ -217,2 +253,3 @@ * | ||
* @param {?object} nextState | ||
* @param {?object} nextContext | ||
* @return {boolean} True if the component should update. | ||
@@ -225,3 +262,4 @@ * @optional | ||
* Invoked when the component is about to update due to a transition from | ||
* `this.props` and `this.state` to `nextProps` and `nextState`. | ||
* `this.props`, `this.state` and `this.context` to `nextProps`, `nextState` | ||
* and `nextContext`. | ||
* | ||
@@ -234,2 +272,3 @@ * Use this as an opportunity to perform preparation before an update occurs. | ||
* @param {?object} nextState | ||
* @param {?object} nextContext | ||
* @param {ReactReconcileTransaction} transaction | ||
@@ -248,2 +287,3 @@ * @optional | ||
* @param {?object} prevState | ||
* @param {?object} prevContext | ||
* @param {DOMElement} rootNode DOM element representing the component. | ||
@@ -288,21 +328,69 @@ * @optional | ||
* | ||
* Although these are declared in the specification when defining classes | ||
* using `React.createClass`, they will not be on the component's prototype. | ||
* Although these are declared like instance properties in the specification | ||
* when defining classes using `React.createClass`, they are actually static | ||
* and are accessible on the constructor instead of the prototype. Despite | ||
* being static, they must be defined outside of the "statics" key under | ||
* which all other static methods are defined. | ||
*/ | ||
var RESERVED_SPEC_KEYS = { | ||
displayName: function(Constructor, displayName) { | ||
Constructor.displayName = displayName; | ||
displayName: function(ConvenienceConstructor, displayName) { | ||
ConvenienceConstructor.componentConstructor.displayName = displayName; | ||
}, | ||
mixins: function(Constructor, mixins) { | ||
mixins: function(ConvenienceConstructor, mixins) { | ||
if (mixins) { | ||
for (var i = 0; i < mixins.length; i++) { | ||
mixSpecIntoComponent(Constructor, mixins[i]); | ||
mixSpecIntoComponent(ConvenienceConstructor, mixins[i]); | ||
} | ||
} | ||
}, | ||
propTypes: function(Constructor, propTypes) { | ||
Constructor.propTypes = propTypes; | ||
childContextTypes: function(ConvenienceConstructor, childContextTypes) { | ||
var Constructor = ConvenienceConstructor.componentConstructor; | ||
validateTypeDef( | ||
Constructor, | ||
childContextTypes, | ||
ReactPropTypeLocations.childContext | ||
); | ||
Constructor.childContextTypes = merge( | ||
Constructor.childContextTypes, | ||
childContextTypes | ||
); | ||
}, | ||
contextTypes: function(ConvenienceConstructor, contextTypes) { | ||
var Constructor = ConvenienceConstructor.componentConstructor; | ||
validateTypeDef( | ||
Constructor, | ||
contextTypes, | ||
ReactPropTypeLocations.context | ||
); | ||
Constructor.contextTypes = merge(Constructor.contextTypes, contextTypes); | ||
}, | ||
propTypes: function(ConvenienceConstructor, propTypes) { | ||
var Constructor = ConvenienceConstructor.componentConstructor; | ||
validateTypeDef( | ||
Constructor, | ||
propTypes, | ||
ReactPropTypeLocations.prop | ||
); | ||
Constructor.propTypes = merge(Constructor.propTypes, propTypes); | ||
}, | ||
statics: function(ConvenienceConstructor, statics) { | ||
mixStaticSpecIntoComponent(ConvenienceConstructor, statics); | ||
} | ||
}; | ||
function validateTypeDef(Constructor, typeDef, location) { | ||
for (var propName in typeDef) { | ||
if (typeDef.hasOwnProperty(propName)) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
typeof typeDef[propName] == 'function', | ||
'%s: %s type `%s` is invalid; it must be a function, usually from ' + | ||
'React.PropTypes.', | ||
Constructor.displayName || 'ReactCompositeComponent', | ||
ReactPropTypeLocationNames[location], | ||
propName | ||
) : invariant(typeof typeDef[propName] == 'function')); | ||
} | ||
} | ||
} | ||
function validateMethodOverride(proto, name) { | ||
@@ -336,3 +424,2 @@ var specPolicy = ReactCompositeComponentInterface[name]; | ||
function validateLifeCycleOnReplaceState(instance) { | ||
@@ -361,13 +448,26 @@ var compositeLifeCycleState = instance._compositeLifeCycleState; | ||
*/ | ||
function mixSpecIntoComponent(Constructor, spec) { | ||
function mixSpecIntoComponent(ConvenienceConstructor, spec) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!isValidClass(spec), | ||
'ReactCompositeComponent: You\'re attempting to ' + | ||
'use a component class as a mixin. Instead, just use a regular object.' | ||
) : invariant(!isValidClass(spec))); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!ReactComponent.isValidComponent(spec), | ||
'ReactCompositeComponent: You\'re attempting to ' + | ||
'use a component as a mixin. Instead, just use a regular object.' | ||
) : invariant(!ReactComponent.isValidComponent(spec))); | ||
var Constructor = ConvenienceConstructor.componentConstructor; | ||
var proto = Constructor.prototype; | ||
for (var name in spec) { | ||
var property = spec[name]; | ||
if (!spec.hasOwnProperty(name) || !property) { | ||
if (!spec.hasOwnProperty(name)) { | ||
continue; | ||
} | ||
validateMethodOverride(proto, name); | ||
if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) { | ||
RESERVED_SPEC_KEYS[name](Constructor, property); | ||
RESERVED_SPEC_KEYS[name](ConvenienceConstructor, property); | ||
} else { | ||
@@ -380,3 +480,3 @@ // Setup methods on prototype: | ||
var isInherited = name in proto; | ||
var markedDontBind = property.__reactDontBind; | ||
var markedDontBind = property && property.__reactDontBind; | ||
var isFunction = typeof property === 'function'; | ||
@@ -413,2 +513,33 @@ var shouldAutoBind = | ||
function mixStaticSpecIntoComponent(ConvenienceConstructor, statics) { | ||
if (!statics) { | ||
return; | ||
} | ||
for (var name in statics) { | ||
var property = statics[name]; | ||
if (!statics.hasOwnProperty(name) || !property) { | ||
return; | ||
} | ||
var isInherited = name in ConvenienceConstructor; | ||
var result = property; | ||
if (isInherited) { | ||
var existingProperty = ConvenienceConstructor[name]; | ||
var existingType = typeof existingProperty; | ||
var propertyType = typeof property; | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
existingType === 'function' && propertyType === 'function', | ||
'ReactCompositeComponent: You are attempting to define ' + | ||
'`%s` on your component more than once, but that is only supported ' + | ||
'for functions, which are chained together. This conflict may be ' + | ||
'due to a mixin.', | ||
name | ||
) : invariant(existingType === 'function' && propertyType === 'function')); | ||
result = createChainedFunction(existingProperty, property); | ||
} | ||
ConvenienceConstructor[name] = result; | ||
ConvenienceConstructor.componentConstructor[name] = result; | ||
} | ||
} | ||
/** | ||
@@ -449,6 +580,10 @@ * Merge two objects, but throw if both contain the same key. | ||
return function mergedResult() { | ||
return mergeObjectsWithNoDuplicateKeys( | ||
one.apply(this, arguments), | ||
two.apply(this, arguments) | ||
); | ||
var a = one.apply(this, arguments); | ||
var b = two.apply(this, arguments); | ||
if (a == null) { | ||
return b; | ||
} else if (b == null) { | ||
return a; | ||
} | ||
return mergeObjectsWithNoDuplicateKeys(a, b); | ||
}; | ||
@@ -472,2 +607,114 @@ } | ||
if ("production" !== process.env.NODE_ENV) { | ||
var unmountedPropertyWhitelist = { | ||
constructor: true, | ||
construct: true, | ||
isOwnedBy: true, // should be deprecated but can have code mod (internal) | ||
mountComponent: true, | ||
mountComponentIntoNode: true, | ||
props: true, | ||
type: true, | ||
_checkPropTypes: true, | ||
_mountComponentIntoNode: true, | ||
_processContext: true | ||
}; | ||
var hasWarnedOnComponentType = {}; | ||
var warnIfUnmounted = function(instance, key) { | ||
if (instance.__hasBeenMounted) { | ||
return; | ||
} | ||
var name = instance.constructor.displayName || 'Unknown'; | ||
var owner = ReactCurrentOwner.current; | ||
var ownerName = (owner && owner.constructor.displayName) || 'Unknown'; | ||
var warningKey = key + '|' + name + '|' + ownerName; | ||
if (hasWarnedOnComponentType.hasOwnProperty(warningKey)) { | ||
// We have already warned for this combination. Skip it this time. | ||
return; | ||
} | ||
hasWarnedOnComponentType[warningKey] = true; | ||
var context = owner ? ' in ' + ownerName + '.' : ' at the top level.'; | ||
var staticMethodExample = '<' + name + ' />.type.' + key + '(...)'; | ||
console.warn( | ||
'Invalid access to component property "' + key + '" on ' + name + | ||
context + ' See http://fb.me/react-warning-descriptors .' + | ||
' Use a static method instead: ' + staticMethodExample | ||
); | ||
}; | ||
var defineMembraneProperty = function(membrane, prototype, key) { | ||
Object.defineProperty(membrane, key, { | ||
configurable: false, | ||
enumerable: true, | ||
get: function() { | ||
if (this !== membrane) { | ||
// When this is accessed through a prototype chain we need to check if | ||
// this component was mounted. | ||
warnIfUnmounted(this, key); | ||
} | ||
return prototype[key]; | ||
}, | ||
set: function(value) { | ||
if (this !== membrane) { | ||
// When this is accessed through a prototype chain, we first check if | ||
// this component was mounted. Then we define a value on "this" | ||
// instance, effectively disabling the membrane on that prototype | ||
// chain. | ||
warnIfUnmounted(this, key); | ||
Object.defineProperty(this, key, { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: value | ||
}); | ||
} else { | ||
// Otherwise, this should modify the prototype | ||
prototype[key] = value; | ||
} | ||
} | ||
}); | ||
}; | ||
/** | ||
* Creates a membrane prototype which wraps the original prototype. If any | ||
* property is accessed in an unmounted state, a warning is issued. | ||
* | ||
* @param {object} prototype Original prototype. | ||
* @return {object} The membrane prototype. | ||
* @private | ||
*/ | ||
var createMountWarningMembrane = function(prototype) { | ||
try { | ||
var membrane = Object.create(prototype); | ||
for (var key in prototype) { | ||
if (unmountedPropertyWhitelist.hasOwnProperty(key)) { | ||
continue; | ||
} | ||
defineMembraneProperty(membrane, prototype, key); | ||
} | ||
membrane.mountComponent = function() { | ||
this.__hasBeenMounted = true; | ||
return prototype.mountComponent.apply(this, arguments); | ||
}; | ||
return membrane; | ||
} catch(x) { | ||
// In IE8 define property will fail on non-DOM objects. If anything in | ||
// the membrane creation fails, we'll bail out and just use the prototype | ||
// without warnings. | ||
return prototype; | ||
} | ||
}; | ||
} | ||
/** | ||
@@ -538,4 +785,10 @@ * `ReactCompositeComponent` maintains an auxiliary life cycle state in | ||
ReactComponent.Mixin.construct.apply(this, arguments); | ||
this.state = null; | ||
this._pendingState = null; | ||
this.context = this._processContext(ReactContext.current); | ||
this._currentContext = ReactContext.current; | ||
this._pendingContext = null; | ||
this._compositeLifeCycleState = null; | ||
@@ -578,3 +831,3 @@ }, | ||
this._defaultProps = this.getDefaultProps ? this.getDefaultProps() : null; | ||
this._processProps(this.props); | ||
this.props = this._processProps(this.props); | ||
@@ -586,2 +839,8 @@ if (this.__reactAutoBindMap) { | ||
this.state = this.getInitialState ? this.getInitialState() : null; | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
typeof this.state === 'object' && !Array.isArray(this.state), | ||
'%s.getInitialState(): must return an object or null', | ||
this.constructor.displayName || 'ReactCompositeComponent' | ||
) : invariant(typeof this.state === 'object' && !Array.isArray(this.state))); | ||
this._pendingState = null; | ||
@@ -631,6 +890,7 @@ this._pendingForceUpdate = false; | ||
ReactComponent.Mixin.unmountComponent.call(this); | ||
this._renderedComponent.unmountComponent(); | ||
this._renderedComponent = null; | ||
ReactComponent.Mixin.unmountComponent.call(this); | ||
if (this.refs) { | ||
@@ -664,2 +924,14 @@ this.refs = null; | ||
setState: function(partialState, callback) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
typeof partialState === 'object' || partialState == null, | ||
'setState(...): takes an object of state variables to update.' | ||
) : invariant(typeof partialState === 'object' || partialState == null)); | ||
if ("production" !== process.env.NODE_ENV) { | ||
if (partialState == null) { | ||
console.warn( | ||
'setState(...): You passed an undefined or null state object; ' + | ||
'instead, use forceUpdate().' | ||
); | ||
} | ||
} | ||
// Merge with `_pendingState` if it exists, otherwise with existing state. | ||
@@ -691,28 +963,106 @@ this.replaceState( | ||
/** | ||
* Filters the context object to only contain keys specified in | ||
* `contextTypes`, and asserts that they are valid. | ||
* | ||
* @param {object} context | ||
* @return {?object} | ||
* @private | ||
*/ | ||
_processContext: function(context) { | ||
var maskedContext = null; | ||
var contextTypes = this.constructor.contextTypes; | ||
if (contextTypes) { | ||
maskedContext = {}; | ||
for (var contextName in contextTypes) { | ||
maskedContext[contextName] = context[contextName]; | ||
} | ||
if ("production" !== process.env.NODE_ENV) { | ||
this._checkPropTypes( | ||
contextTypes, | ||
maskedContext, | ||
ReactPropTypeLocations.context | ||
); | ||
} | ||
} | ||
return maskedContext; | ||
}, | ||
/** | ||
* @param {object} currentContext | ||
* @return {object} | ||
* @private | ||
*/ | ||
_processChildContext: function(currentContext) { | ||
var childContext = this.getChildContext && this.getChildContext(); | ||
var displayName = this.constructor.displayName || 'ReactCompositeComponent'; | ||
if (childContext) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
typeof this.constructor.childContextTypes === 'object', | ||
'%s.getChildContext(): childContextTypes must be defined in order to ' + | ||
'use getChildContext().', | ||
displayName | ||
) : invariant(typeof this.constructor.childContextTypes === 'object')); | ||
if ("production" !== process.env.NODE_ENV) { | ||
this._checkPropTypes( | ||
this.constructor.childContextTypes, | ||
childContext, | ||
ReactPropTypeLocations.childContext | ||
); | ||
} | ||
for (var name in childContext) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
name in this.constructor.childContextTypes, | ||
'%s.getChildContext(): key "%s" is not defined in childContextTypes.', | ||
displayName, | ||
name | ||
) : invariant(name in this.constructor.childContextTypes)); | ||
} | ||
return merge(currentContext, childContext); | ||
} | ||
return currentContext; | ||
}, | ||
/** | ||
* Processes props by setting default values for unspecified props and | ||
* asserting that the props are valid. | ||
* asserting that the props are valid. Does not mutate its argument; returns | ||
* a new props object with defaults merged in. | ||
* | ||
* @param {object} props | ||
* @param {object} newProps | ||
* @return {object} | ||
* @private | ||
*/ | ||
_processProps: function(props) { | ||
var propName; | ||
_processProps: function(newProps) { | ||
var props = merge(newProps); | ||
var defaultProps = this._defaultProps; | ||
for (propName in defaultProps) { | ||
if (!(propName in props)) { | ||
for (var propName in defaultProps) { | ||
if (typeof props[propName] === 'undefined') { | ||
props[propName] = defaultProps[propName]; | ||
} | ||
} | ||
var propTypes = this.constructor.propTypes; | ||
if (propTypes) { | ||
var componentName = this.constructor.displayName; | ||
for (propName in propTypes) { | ||
var checkProp = propTypes[propName]; | ||
if (checkProp) { | ||
checkProp(props, propName, componentName); | ||
} | ||
if ("production" !== process.env.NODE_ENV) { | ||
var propTypes = this.constructor.propTypes; | ||
if (propTypes) { | ||
this._checkPropTypes(propTypes, props, ReactPropTypeLocations.prop); | ||
} | ||
} | ||
return props; | ||
}, | ||
/** | ||
* Assert that the props are valid | ||
* | ||
* @param {object} propTypes Map of prop name to a ReactPropType | ||
* @param {object} props | ||
* @param {string} location e.g. "prop", "context", "child context" | ||
* @private | ||
*/ | ||
_checkPropTypes: function(propTypes, props, location) { | ||
var componentName = this.constructor.displayName; | ||
for (var propName in propTypes) { | ||
if (propTypes.hasOwnProperty(propName)) { | ||
propTypes[propName](props, propName, componentName, location); | ||
} | ||
} | ||
}, | ||
performUpdateIfNecessary: function() { | ||
@@ -739,2 +1089,3 @@ var compositeLifeCycleState = this._compositeLifeCycleState; | ||
this._pendingState == null && | ||
this._pendingContext == null && | ||
!this._pendingForceUpdate) { | ||
@@ -744,6 +1095,9 @@ return; | ||
var nextFullContext = this._pendingContext || this._currentContext; | ||
var nextContext = this._processContext(nextFullContext); | ||
this._pendingContext = null; | ||
var nextProps = this.props; | ||
if (this._pendingProps != null) { | ||
nextProps = this._pendingProps; | ||
this._processProps(nextProps); | ||
nextProps = this._processProps(this._pendingProps); | ||
this._pendingProps = null; | ||
@@ -753,3 +1107,3 @@ | ||
if (this.componentWillReceiveProps) { | ||
this.componentWillReceiveProps(nextProps, transaction); | ||
this.componentWillReceiveProps(nextProps, nextContext); | ||
} | ||
@@ -760,19 +1114,37 @@ } | ||
// Unlike props, state, and context, we specifically don't want to set | ||
// _pendingOwner to null here because it's possible for a component to have | ||
// a null owner, so we instead make `this._owner === this._pendingOwner` | ||
// mean that there's no owner change pending. | ||
var nextOwner = this._pendingOwner; | ||
var nextState = this._pendingState || this.state; | ||
this._pendingState = null; | ||
if (this._pendingForceUpdate || | ||
!this.shouldComponentUpdate || | ||
this.shouldComponentUpdate(nextProps, nextState)) { | ||
this._pendingForceUpdate = false; | ||
// Will set `this.props` and `this.state`. | ||
this._performComponentUpdate(nextProps, nextState, transaction); | ||
} else { | ||
// If it's determined that a component should not update, we still want | ||
// to set props and state. | ||
this.props = nextProps; | ||
this.state = nextState; | ||
try { | ||
if (this._pendingForceUpdate || | ||
!this.shouldComponentUpdate || | ||
this.shouldComponentUpdate(nextProps, nextState, nextContext)) { | ||
this._pendingForceUpdate = false; | ||
// Will set `this.props`, `this.state` and `this.context`. | ||
this._performComponentUpdate( | ||
nextProps, | ||
nextOwner, | ||
nextState, | ||
nextFullContext, | ||
nextContext, | ||
transaction | ||
); | ||
} else { | ||
// If it's determined that a component should not update, we still want | ||
// to set props and state. | ||
this.props = nextProps; | ||
this._owner = nextOwner; | ||
this.state = nextState; | ||
this._currentContext = nextFullContext; | ||
this.context = nextContext; | ||
} | ||
} finally { | ||
this._compositeLifeCycleState = null; | ||
} | ||
this._compositeLifeCycleState = null; | ||
}, | ||
@@ -785,18 +1157,39 @@ | ||
* @param {object} nextProps Next object to set as properties. | ||
* @param {?ReactComponent} nextOwner Next component to set as owner | ||
* @param {?object} nextState Next object to set as state. | ||
* @param {?object} nextFullContext Next object to set as _currentContext. | ||
* @param {?object} nextContext Next object to set as context. | ||
* @param {ReactReconcileTransaction} transaction | ||
* @private | ||
*/ | ||
_performComponentUpdate: function(nextProps, nextState, transaction) { | ||
_performComponentUpdate: function( | ||
nextProps, | ||
nextOwner, | ||
nextState, | ||
nextFullContext, | ||
nextContext, | ||
transaction | ||
) { | ||
var prevProps = this.props; | ||
var prevOwner = this._owner; | ||
var prevState = this.state; | ||
var prevContext = this.context; | ||
if (this.componentWillUpdate) { | ||
this.componentWillUpdate(nextProps, nextState, transaction); | ||
this.componentWillUpdate(nextProps, nextState, nextContext); | ||
} | ||
this.props = nextProps; | ||
this._owner = nextOwner; | ||
this.state = nextState; | ||
this._currentContext = nextFullContext; | ||
this.context = nextContext; | ||
this.updateComponent(transaction, prevProps, prevState); | ||
this.updateComponent( | ||
transaction, | ||
prevProps, | ||
prevOwner, | ||
prevState, | ||
prevContext | ||
); | ||
@@ -806,3 +1199,3 @@ if (this.componentDidUpdate) { | ||
this, | ||
this.componentDidUpdate.bind(this, prevProps, prevState) | ||
this.componentDidUpdate.bind(this, prevProps, prevState, prevContext) | ||
); | ||
@@ -812,2 +1205,18 @@ } | ||
receiveComponent: function(nextComponent, transaction) { | ||
if (nextComponent === this) { | ||
// Since props and context are immutable after the component is | ||
// mounted, we can do a cheap identity compare here to determine | ||
// if this is a superfluous reconcile. | ||
return; | ||
} | ||
this._pendingContext = nextComponent._currentContext; | ||
ReactComponent.Mixin.receiveComponent.call( | ||
this, | ||
nextComponent, | ||
transaction | ||
); | ||
}, | ||
/** | ||
@@ -821,3 +1230,5 @@ * Updates the component's currently mounted DOM representation. | ||
* @param {object} prevProps | ||
* @param {?ReactComponent} prevOwner | ||
* @param {?object} prevState | ||
* @param {?object} prevContext | ||
* @internal | ||
@@ -829,13 +1240,18 @@ * @overridable | ||
'updateComponent', | ||
function(transaction, prevProps, prevState) { | ||
ReactComponent.Mixin.updateComponent.call(this, transaction, prevProps); | ||
var currentComponent = this._renderedComponent; | ||
function(transaction, prevProps, prevOwner, prevState, prevContext) { | ||
ReactComponent.Mixin.updateComponent.call( | ||
this, | ||
transaction, | ||
prevProps, | ||
prevOwner | ||
); | ||
var prevComponent = this._renderedComponent; | ||
var nextComponent = this._renderValidatedComponent(); | ||
if (currentComponent.constructor === nextComponent.constructor) { | ||
currentComponent.receiveComponent(nextComponent, transaction); | ||
if (shouldUpdateReactComponent(prevComponent, nextComponent)) { | ||
prevComponent.receiveComponent(nextComponent, transaction); | ||
} else { | ||
// These two IDs are actually the same! But nothing should rely on that. | ||
var thisID = this._rootNodeID; | ||
var currentComponentID = currentComponent._rootNodeID; | ||
currentComponent.unmountComponent(); | ||
var prevComponentID = prevComponent._rootNodeID; | ||
prevComponent.unmountComponent(); | ||
this._renderedComponent = nextComponent; | ||
@@ -847,4 +1263,4 @@ var nextMarkup = nextComponent.mountComponent( | ||
); | ||
ReactComponent.DOMIDOperations.dangerouslyReplaceNodeWithMarkupByID( | ||
currentComponentID, | ||
ReactComponent.BackendIDOperations.dangerouslyReplaceNodeWithMarkupByID( | ||
prevComponentID, | ||
nextMarkup | ||
@@ -893,21 +1309,25 @@ ); | ||
*/ | ||
_renderValidatedComponent: function() { | ||
var renderedComponent; | ||
ReactCurrentOwner.current = this; | ||
try { | ||
renderedComponent = this.render(); | ||
} catch (error) { | ||
// IE8 requires `catch` in order to use `finally`. | ||
throw error; | ||
} finally { | ||
ReactCurrentOwner.current = null; | ||
_renderValidatedComponent: ReactPerf.measure( | ||
'ReactCompositeComponent', | ||
'_renderValidatedComponent', | ||
function() { | ||
var renderedComponent; | ||
var previousContext = ReactContext.current; | ||
ReactContext.current = this._processChildContext(this._currentContext); | ||
ReactCurrentOwner.current = this; | ||
try { | ||
renderedComponent = this.render(); | ||
} finally { | ||
ReactContext.current = previousContext; | ||
ReactCurrentOwner.current = null; | ||
} | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
ReactComponent.isValidComponent(renderedComponent), | ||
'%s.render(): A valid ReactComponent must be returned. You may have ' + | ||
'returned null, undefined, an array, or some other invalid object.', | ||
this.constructor.displayName || 'ReactCompositeComponent' | ||
) : invariant(ReactComponent.isValidComponent(renderedComponent))); | ||
return renderedComponent; | ||
} | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
ReactComponent.isValidComponent(renderedComponent), | ||
'%s.render(): A valid ReactComponent must be returned. You may have ' + | ||
'returned null, undefined, an array, or some other invalid object.', | ||
this.constructor.displayName || 'ReactCompositeComponent' | ||
) : invariant(ReactComponent.isValidComponent(renderedComponent))); | ||
return renderedComponent; | ||
}, | ||
), | ||
@@ -947,3 +1367,3 @@ /** | ||
var _bind = boundMethod.bind; | ||
boundMethod.bind = function(newThis) { | ||
boundMethod.bind = function(newThis ) {var args=Array.prototype.slice.call(arguments,1); | ||
// User is trying to bind() an autobound method; we effectively will | ||
@@ -957,3 +1377,3 @@ // ignore the value of "this" that the user is trying to use, so | ||
); | ||
} else if (arguments.length === 1) { | ||
} else if (!args.length) { | ||
console.warn( | ||
@@ -969,4 +1389,3 @@ 'bind(): You are binding a component method to the component. ' + | ||
reboundMethod.__reactBoundMethod = method; | ||
reboundMethod.__reactBoundArguments = | ||
Array.prototype.slice.call(arguments, 1); | ||
reboundMethod.__reactBoundArguments = args; | ||
return reboundMethod; | ||
@@ -986,2 +1405,14 @@ }; | ||
/** | ||
* Checks if a value is a valid component constructor. | ||
* | ||
* @param {*} | ||
* @return {boolean} | ||
* @public | ||
*/ | ||
function isValidClass(componentClass) { | ||
return componentClass instanceof Function && | ||
'componentConstructor' in componentClass && | ||
componentClass.componentConstructor instanceof Function; | ||
} | ||
/** | ||
* Module for creating composite components. | ||
@@ -1011,4 +1442,14 @@ * | ||
Constructor.prototype.constructor = Constructor; | ||
mixSpecIntoComponent(Constructor, spec); | ||
var ConvenienceConstructor = function(props, children) { | ||
var instance = new Constructor(); | ||
instance.construct.apply(instance, arguments); | ||
return instance; | ||
}; | ||
ConvenienceConstructor.componentConstructor = Constructor; | ||
Constructor.ConvenienceConstructor = ConvenienceConstructor; | ||
ConvenienceConstructor.originalSpec = spec; | ||
mixSpecIntoComponent(ConvenienceConstructor, spec); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
@@ -1030,2 +1471,10 @@ Constructor.prototype.render, | ||
// Expose the convience constructor on the prototype so that it can be | ||
// easily accessed on descriptors. E.g. <Foo />.type === Foo.type and for | ||
// static methods like <Foo />.type.staticMethod(); | ||
// This should not be named constructor since this may not be the function | ||
// that created the descriptor, and it may not even be a constructor. | ||
ConvenienceConstructor.type = Constructor; | ||
Constructor.prototype.type = Constructor; | ||
// Reduce time spent doing lookups by setting these on the prototype. | ||
@@ -1038,26 +1487,12 @@ for (var methodName in ReactCompositeComponentInterface) { | ||
var ConvenienceConstructor = function(props, children) { | ||
var instance = new Constructor(); | ||
instance.construct.apply(instance, arguments); | ||
return instance; | ||
}; | ||
ConvenienceConstructor.componentConstructor = Constructor; | ||
ConvenienceConstructor.originalSpec = spec; | ||
if ("production" !== process.env.NODE_ENV) { | ||
Constructor.prototype = createMountWarningMembrane(Constructor.prototype); | ||
} | ||
return ConvenienceConstructor; | ||
}, | ||
/** | ||
* Checks if a value is a valid component constructor. | ||
* | ||
* @param {*} | ||
* @return {boolean} | ||
* @public | ||
*/ | ||
isValidClass: function(componentClass) { | ||
return componentClass instanceof Function && | ||
'componentConstructor' in componentClass && | ||
componentClass.componentConstructor instanceof Function; | ||
} | ||
isValidClass: isValidClass | ||
}; | ||
module.exports = ReactCompositeComponent; |
@@ -21,36 +21,44 @@ /** | ||
var ReactDOM = require("./ReactDOM"); | ||
var ReactDOMButton = require("./ReactDOMButton"); | ||
var ReactDOMForm = require("./ReactDOMForm"); | ||
var ReactDOMInput = require("./ReactDOMInput"); | ||
var ReactDOMOption = require("./ReactDOMOption"); | ||
var ReactDOMSelect = require("./ReactDOMSelect"); | ||
var ReactDOMTextarea = require("./ReactDOMTextarea"); | ||
var ReactEventEmitter = require("./ReactEventEmitter"); | ||
var ReactEventTopLevelCallback = require("./ReactEventTopLevelCallback"); | ||
var ReactPerf = require("./ReactPerf"); | ||
var ReactInjection = require("./ReactInjection"); | ||
var ExecutionEnvironment = require("./ExecutionEnvironment"); | ||
var DefaultDOMPropertyConfig = require("./DefaultDOMPropertyConfig"); | ||
var DOMProperty = require("./DOMProperty"); | ||
var ChangeEventPlugin = require("./ChangeEventPlugin"); | ||
var ClientReactRootIndex = require("./ClientReactRootIndex"); | ||
var CompositionEventPlugin = require("./CompositionEventPlugin"); | ||
var DefaultEventPluginOrder = require("./DefaultEventPluginOrder"); | ||
var EnterLeaveEventPlugin = require("./EnterLeaveEventPlugin"); | ||
var EventPluginHub = require("./EventPluginHub"); | ||
var MobileSafariClickEventPlugin = require("./MobileSafariClickEventPlugin"); | ||
var ReactEventTopLevelCallback = require("./ReactEventTopLevelCallback"); | ||
var ReactDOM = require("./ReactDOM"); | ||
var ReactDOMButton = require("./ReactDOMButton"); | ||
var ReactDOMForm = require("./ReactDOMForm"); | ||
var ReactDOMImg = require("./ReactDOMImg"); | ||
var ReactDOMInput = require("./ReactDOMInput"); | ||
var ReactDOMOption = require("./ReactDOMOption"); | ||
var ReactDOMSelect = require("./ReactDOMSelect"); | ||
var ReactDOMTextarea = require("./ReactDOMTextarea"); | ||
var ReactInstanceHandles = require("./ReactInstanceHandles"); | ||
var ReactMount = require("./ReactMount"); | ||
var SelectEventPlugin = require("./SelectEventPlugin"); | ||
var ServerReactRootIndex = require("./ServerReactRootIndex"); | ||
var SimpleEventPlugin = require("./SimpleEventPlugin"); | ||
var ReactDefaultBatchingStrategy = require("./ReactDefaultBatchingStrategy"); | ||
var ReactUpdates = require("./ReactUpdates"); | ||
var createFullPageComponent = require("./createFullPageComponent"); | ||
function inject() { | ||
ReactEventEmitter.TopLevelCallbackCreator = ReactEventTopLevelCallback; | ||
ReactInjection.EventEmitter.injectTopLevelCallbackCreator( | ||
ReactEventTopLevelCallback | ||
); | ||
/** | ||
* Inject module for resolving DOM hierarchy and plugin ordering. | ||
* Inject modules for resolving DOM hierarchy and plugin ordering. | ||
*/ | ||
EventPluginHub.injection.injectEventPluginOrder(DefaultEventPluginOrder); | ||
EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles); | ||
ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder); | ||
ReactInjection.EventPluginHub.injectInstanceHandle(ReactInstanceHandles); | ||
ReactInjection.EventPluginHub.injectMount(ReactMount); | ||
@@ -61,3 +69,3 @@ /** | ||
*/ | ||
EventPluginHub.injection.injectEventPluginsByName({ | ||
ReactInjection.EventPluginHub.injectEventPluginsByName({ | ||
SimpleEventPlugin: SimpleEventPlugin, | ||
@@ -71,20 +79,36 @@ EnterLeaveEventPlugin: EnterLeaveEventPlugin, | ||
ReactDOM.injection.injectComponentClasses({ | ||
ReactInjection.DOM.injectComponentClasses({ | ||
button: ReactDOMButton, | ||
form: ReactDOMForm, | ||
img: ReactDOMImg, | ||
input: ReactDOMInput, | ||
option: ReactDOMOption, | ||
select: ReactDOMSelect, | ||
textarea: ReactDOMTextarea | ||
textarea: ReactDOMTextarea, | ||
html: createFullPageComponent(ReactDOM.html), | ||
head: createFullPageComponent(ReactDOM.head), | ||
title: createFullPageComponent(ReactDOM.title), | ||
body: createFullPageComponent(ReactDOM.body) | ||
}); | ||
DOMProperty.injection.injectDOMPropertyConfig(DefaultDOMPropertyConfig); | ||
ReactInjection.DOMProperty.injectDOMPropertyConfig(DefaultDOMPropertyConfig); | ||
ReactInjection.Updates.injectBatchingStrategy( | ||
ReactDefaultBatchingStrategy | ||
); | ||
ReactInjection.RootIndex.injectCreateReactRootIndex( | ||
ExecutionEnvironment.canUseDOM ? | ||
ClientReactRootIndex.createReactRootIndex : | ||
ServerReactRootIndex.createReactRootIndex | ||
); | ||
if ("production" !== process.env.NODE_ENV) { | ||
ReactPerf.injection.injectMeasure(require("./ReactDefaultPerf").measure); | ||
var url = (ExecutionEnvironment.canUseDOM && window.location.href) || ''; | ||
if ((/[?&]react_perf\b/).test(url)) { | ||
var ReactDefaultPerf = require("./ReactDefaultPerf"); | ||
ReactDefaultPerf.start(); | ||
} | ||
} | ||
ReactUpdates.injection.injectBatchingStrategy( | ||
ReactDefaultBatchingStrategy | ||
); | ||
} | ||
@@ -91,0 +115,0 @@ |
@@ -22,387 +22,224 @@ /** | ||
var DOMProperty = require("./DOMProperty"); | ||
var ReactDefaultPerfAnalysis = require("./ReactDefaultPerfAnalysis"); | ||
var ReactMount = require("./ReactMount"); | ||
var ReactPerf = require("./ReactPerf"); | ||
var performanceNow = require("./performanceNow"); | ||
var ReactDefaultPerf = {}; | ||
function roundFloat(val) { | ||
return Math.floor(val * 100) / 100; | ||
} | ||
if ("production" !== process.env.NODE_ENV) { | ||
ReactDefaultPerf = { | ||
/** | ||
* Gets the stored information for a given object's function. | ||
* | ||
* @param {string} objName | ||
* @param {string} fnName | ||
* @return {?object} | ||
*/ | ||
getInfo: function(objName, fnName) { | ||
if (!this.info[objName] || !this.info[objName][fnName]) { | ||
return null; | ||
} | ||
return this.info[objName][fnName]; | ||
}, | ||
var ReactDefaultPerf = { | ||
_allMeasurements: [], // last item in the list is the current one | ||
_injected: false, | ||
/** | ||
* Gets the logs pertaining to a given object's function. | ||
* | ||
* @param {string} objName | ||
* @param {string} fnName | ||
* @return {?array<object>} | ||
*/ | ||
getLogs: function(objName, fnName) { | ||
if (!this.getInfo(objName, fnName)) { | ||
return null; | ||
} | ||
return this.logs.filter(function(log) { | ||
return log.objName === objName && log.fnName === fnName; | ||
}); | ||
}, | ||
start: function() { | ||
if (!ReactDefaultPerf._injected) { | ||
ReactPerf.injection.injectMeasure(ReactDefaultPerf.measure); | ||
} | ||
/** | ||
* Runs through the logs and builds an array of arrays, where each array | ||
* walks through the mounting/updating of each component underneath. | ||
* | ||
* @param {string} rootID The reactID of the root node, e.g. '.r[2cpyq]' | ||
* @return {array<array>} | ||
*/ | ||
getRawRenderHistory: function(rootID) { | ||
var history = []; | ||
/** | ||
* Since logs are added after the method returns, the logs are in a sense | ||
* upside-down: the inner-most elements from mounting/updating are logged | ||
* first, and the last addition to the log is the top renderComponent. | ||
* Therefore, we flip the logs upside down for ease of processing, and | ||
* reverse the history array at the end so the earliest event has index 0. | ||
*/ | ||
var logs = this.logs.filter(function(log) { | ||
return log.reactID.indexOf(rootID) === 0; | ||
}).reverse(); | ||
ReactDefaultPerf._allMeasurements.length = 0; | ||
ReactPerf.enableMeasure = true; | ||
}, | ||
var subHistory = []; | ||
logs.forEach(function(log, i) { | ||
if (i && log.reactID === rootID && logs[i - 1].reactID !== rootID) { | ||
subHistory.length && history.push(subHistory); | ||
subHistory = []; | ||
} | ||
subHistory.push(log); | ||
}); | ||
if (subHistory.length) { | ||
history.push(subHistory); | ||
} | ||
return history.reverse(); | ||
}, | ||
stop: function() { | ||
ReactPerf.enableMeasure = false; | ||
}, | ||
/** | ||
* Runs through the logs and builds an array of strings, where each string | ||
* is a multiline formatted way of walking through the mounting/updating | ||
* underneath. | ||
* | ||
* @param {string} rootID The reactID of the root node, e.g. '.r[2cpyq]' | ||
* @return {array<string>} | ||
*/ | ||
getRenderHistory: function(rootID) { | ||
var history = this.getRawRenderHistory(rootID); | ||
getLastMeasurements: function() { | ||
return ReactDefaultPerf._allMeasurements; | ||
}, | ||
return history.map(function(subHistory) { | ||
var headerString = ( | ||
'log# Component (execution time) [bloat from logging]\n' + | ||
'================================================================\n' | ||
); | ||
return headerString + subHistory.map(function(log) { | ||
// Add two spaces for every layer in the reactID. | ||
var indents = '\t' + Array(log.reactID.split('.[').length).join(' '); | ||
var delta = _microTime(log.timing.delta); | ||
var bloat = _microTime(log.timing.timeToLog); | ||
printExclusive: function(measurements) { | ||
measurements = measurements || ReactDefaultPerf._allMeasurements; | ||
var summary = ReactDefaultPerfAnalysis.getExclusiveSummary(measurements); | ||
console.table(summary.map(function(item) { | ||
return { | ||
'Component class name': item.componentName, | ||
'Total inclusive time (ms)': roundFloat(item.inclusive), | ||
'Total exclusive time (ms)': roundFloat(item.exclusive), | ||
'Exclusive time per instance (ms)': roundFloat(item.exclusive / item.count), | ||
'Instances': item.count | ||
}; | ||
})); | ||
console.log( | ||
'Total time:', | ||
ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms' | ||
); | ||
}, | ||
return log.index + indents + log.name + ' (' + delta + 'ms)' + | ||
' [' + bloat + 'ms]'; | ||
}).join('\n'); | ||
}); | ||
}, | ||
printInclusive: function(measurements) { | ||
measurements = measurements || ReactDefaultPerf._allMeasurements; | ||
var summary = ReactDefaultPerfAnalysis.getInclusiveSummary(measurements); | ||
console.table(summary.map(function(item) { | ||
return { | ||
'Owner > component': item.componentName, | ||
'Inclusive time (ms)': roundFloat(item.time), | ||
'Instances': item.count | ||
}; | ||
})); | ||
console.log( | ||
'Total time:', | ||
ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms' | ||
); | ||
}, | ||
/** | ||
* Print the render history from `getRenderHistory` using console.log. | ||
* This is currently the best way to display perf data from | ||
* any React component; working on that. | ||
* | ||
* @param {string} rootID The reactID of the root node, e.g. '.r[2cpyq]' | ||
* @param {number} index | ||
*/ | ||
printRenderHistory: function(rootID, index) { | ||
var history = this.getRenderHistory(rootID); | ||
if (!history[index]) { | ||
console.warn( | ||
'Index', index, 'isn\'t available! ' + | ||
'The render history is', history.length, 'long.' | ||
); | ||
return; | ||
} | ||
console.log( | ||
'Loading render history #' + (index + 1) + | ||
' of ' + history.length + ':\n' + history[index] | ||
); | ||
}, | ||
printWasted: function(measurements) { | ||
measurements = measurements || ReactDefaultPerf._allMeasurements; | ||
var summary = ReactDefaultPerfAnalysis.getInclusiveSummary( | ||
measurements, | ||
true | ||
); | ||
console.table(summary.map(function(item) { | ||
return { | ||
'Owner > component': item.componentName, | ||
'Wasted time (ms)': item.time, | ||
'Instances': item.count | ||
}; | ||
})); | ||
console.log( | ||
'Total time:', | ||
ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms' | ||
); | ||
}, | ||
/** | ||
* Prints the heatmap legend to console, showing how the colors correspond | ||
* with render times. This relies on console.log styles. | ||
*/ | ||
printHeatmapLegend: function() { | ||
if (!this.options.heatmap.enabled) { | ||
return; | ||
} | ||
var max = this.info.React | ||
&& this.info.React.renderComponent | ||
&& this.info.React.renderComponent.max; | ||
if (max) { | ||
var logStr = 'Heatmap: '; | ||
for (var ii = 0; ii <= 10 * max; ii += max) { | ||
logStr += '%c ' + (Math.round(ii) / 10) + 'ms '; | ||
} | ||
console.log( | ||
logStr, | ||
'background-color: hsla(100, 100%, 50%, 0.6);', | ||
'background-color: hsla( 90, 100%, 50%, 0.6);', | ||
'background-color: hsla( 80, 100%, 50%, 0.6);', | ||
'background-color: hsla( 70, 100%, 50%, 0.6);', | ||
'background-color: hsla( 60, 100%, 50%, 0.6);', | ||
'background-color: hsla( 50, 100%, 50%, 0.6);', | ||
'background-color: hsla( 40, 100%, 50%, 0.6);', | ||
'background-color: hsla( 30, 100%, 50%, 0.6);', | ||
'background-color: hsla( 20, 100%, 50%, 0.6);', | ||
'background-color: hsla( 10, 100%, 50%, 0.6);', | ||
'background-color: hsla( 0, 100%, 50%, 0.6);' | ||
); | ||
} | ||
}, | ||
printDOM: function(measurements) { | ||
measurements = measurements || ReactDefaultPerf._allMeasurements; | ||
var summary = ReactDefaultPerfAnalysis.getDOMSummary(measurements); | ||
console.table(summary.map(function(item) { | ||
var result = {}; | ||
result[DOMProperty.ID_ATTRIBUTE_NAME] = item.id; | ||
result['type'] = item.type; | ||
result['args'] = JSON.stringify(item.args); | ||
return result; | ||
})); | ||
console.log( | ||
'Total time:', | ||
ReactDefaultPerfAnalysis.getTotalTime(measurements).toFixed(2) + ' ms' | ||
); | ||
}, | ||
/** | ||
* Measure a given function with logging information, and calls a callback | ||
* if there is one. | ||
* | ||
* @param {string} objName | ||
* @param {string} fnName | ||
* @param {function} func | ||
* @return {function} | ||
*/ | ||
measure: function(objName, fnName, func) { | ||
var info = _getNewInfo(objName, fnName); | ||
_recordWrite: function(id, fnName, totalTime, args) { | ||
// TODO: totalTime isn't that useful since it doesn't count paints/reflows | ||
var writes = | ||
ReactDefaultPerf | ||
._allMeasurements[ReactDefaultPerf._allMeasurements.length - 1] | ||
.writes; | ||
writes[id] = writes[id] || []; | ||
writes[id].push({ | ||
type: fnName, | ||
time: totalTime, | ||
args: args | ||
}); | ||
}, | ||
var fnArgs = _getFnArguments(func); | ||
measure: function(moduleName, fnName, func) { | ||
return function() {var args=Array.prototype.slice.call(arguments,0); | ||
var totalTime; | ||
var rv; | ||
var start; | ||
return function() { | ||
var timeBeforeFn = performanceNow(); | ||
var fnReturn = func.apply(this, arguments); | ||
var timeAfterFn = performanceNow(); | ||
if (fnName === '_renderNewRootComponent' || | ||
fnName === 'flushBatchedUpdates') { | ||
// A "measurement" is a set of metrics recorded for each flush. We want | ||
// to group the metrics for a given flush together so we can look at the | ||
// components that rendered and the DOM operations that actually | ||
// happened to determine the amount of "wasted work" performed. | ||
ReactDefaultPerf._allMeasurements.push({ | ||
exclusive: {}, | ||
inclusive: {}, | ||
counts: {}, | ||
writes: {}, | ||
displayNames: {}, | ||
totalTime: 0 | ||
}); | ||
start = performanceNow(); | ||
rv = func.apply(this, args); | ||
ReactDefaultPerf._allMeasurements[ | ||
ReactDefaultPerf._allMeasurements.length - 1 | ||
].totalTime = performanceNow() - start; | ||
return rv; | ||
} else if (moduleName === 'ReactDOMIDOperations' || | ||
moduleName === 'ReactComponentBrowserEnvironment') { | ||
start = performanceNow(); | ||
rv = func.apply(this, args); | ||
totalTime = performanceNow() - start; | ||
/** | ||
* Hold onto arguments in a readable way: args[1] -> args.component. | ||
* args is also passed to the callback, so if you want to save an | ||
* argument in the log, do so in the callback. | ||
*/ | ||
var args = {}; | ||
for (var i = 0; i < arguments.length; i++) { | ||
args[fnArgs[i]] = arguments[i]; | ||
if (fnName === 'mountImageIntoNode') { | ||
var mountID = ReactMount.getID(args[1]); | ||
ReactDefaultPerf._recordWrite(mountID, fnName, totalTime, args[0]); | ||
} else if (fnName === 'dangerouslyProcessChildrenUpdates') { | ||
// special format | ||
args[0].forEach(function(update) { | ||
var writeArgs = {}; | ||
if (update.fromIndex !== null) { | ||
writeArgs.fromIndex = update.fromIndex; | ||
} | ||
if (update.toIndex !== null) { | ||
writeArgs.toIndex = update.toIndex; | ||
} | ||
if (update.textContent !== null) { | ||
writeArgs.textContent = update.textContent; | ||
} | ||
if (update.markupIndex !== null) { | ||
writeArgs.markup = args[1][update.markupIndex]; | ||
} | ||
ReactDefaultPerf._recordWrite( | ||
update.parentID, | ||
update.type, | ||
totalTime, | ||
writeArgs | ||
); | ||
}); | ||
} else { | ||
// basic format | ||
ReactDefaultPerf._recordWrite( | ||
args[0], | ||
fnName, | ||
totalTime, | ||
Array.prototype.slice.call(args, 1) | ||
); | ||
} | ||
return rv; | ||
} else if (moduleName === 'ReactCompositeComponent' && ( | ||
fnName === 'mountComponent' || | ||
fnName === 'updateComponent' || // TODO: receiveComponent()? | ||
fnName === '_renderValidatedComponent')) { | ||
var log = { | ||
index: ReactDefaultPerf.logs.length, | ||
fnName: fnName, | ||
objName: objName, | ||
timing: { | ||
before: timeBeforeFn, | ||
after: timeAfterFn, | ||
delta: timeAfterFn - timeBeforeFn | ||
} | ||
}; | ||
var rootNodeID = fnName === 'mountComponent' ? | ||
args[0] : | ||
this._rootNodeID; | ||
var isRender = fnName === '_renderValidatedComponent'; | ||
var entry = ReactDefaultPerf._allMeasurements[ | ||
ReactDefaultPerf._allMeasurements.length - 1 | ||
]; | ||
ReactDefaultPerf.logs.push(log); | ||
if (isRender) { | ||
entry.counts[rootNodeID] = entry.counts[rootNodeID] || 0; | ||
entry.counts[rootNodeID] += 1; | ||
} | ||
/** | ||
* The callback gets: | ||
* - this (the component) | ||
* - the original method's arguments | ||
* - what the method returned | ||
* - the log object, and | ||
* - the wrapped method's info object. | ||
*/ | ||
var callback = _getCallback(objName, fnName); | ||
callback && callback(this, args, fnReturn, log, info); | ||
start = performanceNow(); | ||
rv = func.apply(this, args); | ||
totalTime = performanceNow() - start; | ||
log.timing.timeToLog = performanceNow() - timeAfterFn; | ||
var typeOfLog = isRender ? entry.exclusive : entry.inclusive; | ||
typeOfLog[rootNodeID] = typeOfLog[rootNodeID] || 0; | ||
typeOfLog[rootNodeID] += totalTime; | ||
return fnReturn; | ||
}; | ||
}, | ||
entry.displayNames[rootNodeID] = { | ||
current: this.constructor.displayName, | ||
owner: this._owner ? this._owner.constructor.displayName : '<root>' | ||
}; | ||
/** | ||
* Holds information on wrapped objects/methods. | ||
* For instance, ReactDefaultPerf.info.React.renderComponent | ||
*/ | ||
info: {}, | ||
/** | ||
* Holds all of the logs. Filter this to pull desired information. | ||
*/ | ||
logs: [], | ||
/** | ||
* Toggle settings for ReactDefaultPerf | ||
*/ | ||
options: { | ||
/** | ||
* The heatmap sets the background color of the React containers | ||
* according to how much total time has been spent rendering them. | ||
* The most temporally expensive component is set as pure red, | ||
* and the others are colored from green to red as a fraction | ||
* of that max component time. | ||
*/ | ||
heatmap: { | ||
enabled: true | ||
return rv; | ||
} else { | ||
return func.apply(this, args); | ||
} | ||
} | ||
}; | ||
/** | ||
* Gets a info area for a given object's function, adding a new one if | ||
* necessary. | ||
* | ||
* @param {string} objName | ||
* @param {string} fnName | ||
* @return {object} | ||
*/ | ||
var _getNewInfo = function(objName, fnName) { | ||
var info = ReactDefaultPerf.getInfo(objName, fnName); | ||
if (info) { | ||
return info; | ||
} | ||
ReactDefaultPerf.info[objName] = ReactDefaultPerf.info[objName] || {}; | ||
return ReactDefaultPerf.info[objName][fnName] = { | ||
getLogs: function() { | ||
return ReactDefaultPerf.getLogs(objName, fnName); | ||
} | ||
}; | ||
}; | ||
} | ||
}; | ||
/** | ||
* Gets a list of the argument names from a function's definition. | ||
* This is useful for storing arguments by their names within wrapFn(). | ||
* | ||
* @param {function} fn | ||
* @return {array<string>} | ||
*/ | ||
var _getFnArguments = function(fn) { | ||
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; | ||
var fnStr = fn.toString().replace(STRIP_COMMENTS, ''); | ||
fnStr = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')); | ||
return fnStr.match(/([^\s,]+)/g); | ||
}; | ||
/** | ||
* Store common callbacks within ReactDefaultPerf. | ||
* | ||
* @param {string} objName | ||
* @param {string} fnName | ||
* @return {?function} | ||
*/ | ||
var _getCallback = function(objName, fnName) { | ||
switch (objName + '.' + fnName) { | ||
case 'React.renderComponent': | ||
return _renderComponentCallback; | ||
case 'ReactDOMComponent.mountComponent': | ||
case 'ReactDOMComponent.updateComponent': | ||
return _nativeComponentCallback; | ||
case 'ReactCompositeComponent.mountComponent': | ||
case 'ReactCompositeComponent.updateComponent': | ||
return _compositeComponentCallback; | ||
default: | ||
return null; | ||
} | ||
}; | ||
/** | ||
* Callback function for React.renderComponent | ||
* | ||
* @param {object} component | ||
* @param {object} args | ||
* @param {?object} fnReturn | ||
* @param {object} log | ||
* @param {object} info | ||
*/ | ||
var _renderComponentCallback = | ||
function(component, args, fnReturn, log, info) { | ||
log.name = args.nextComponent.constructor.displayName || '[unknown]'; | ||
log.reactID = fnReturn._rootNodeID || null; | ||
if (ReactDefaultPerf.options.heatmap.enabled) { | ||
var container = args.container; | ||
if (!container.loggedByReactDefaultPerf) { | ||
container.loggedByReactDefaultPerf = true; | ||
info.components = info.components || []; | ||
info.components.push(container); | ||
} | ||
container.count = container.count || 0; | ||
container.count += log.timing.delta; | ||
info.max = info.max || 0; | ||
if (container.count > info.max) { | ||
info.max = container.count; | ||
info.components.forEach(function(component) { | ||
_setHue(component, 100 - 100 * component.count / info.max); | ||
}); | ||
} else { | ||
_setHue(container, 100 - 100 * container.count / info.max); | ||
} | ||
} | ||
}; | ||
/** | ||
* Callback function for ReactDOMComponent | ||
* | ||
* @param {object} component | ||
* @param {object} args | ||
* @param {?object} fnReturn | ||
* @param {object} log | ||
* @param {object} info | ||
*/ | ||
var _nativeComponentCallback = | ||
function(component, args, fnReturn, log, info) { | ||
log.name = component.tagName || '[unknown]'; | ||
log.reactID = component._rootNodeID; | ||
}; | ||
/** | ||
* Callback function for ReactCompositeComponent | ||
* | ||
* @param {object} component | ||
* @param {object} args | ||
* @param {?object} fnReturn | ||
* @param {object} log | ||
* @param {object} info | ||
*/ | ||
var _compositeComponentCallback = | ||
function(component, args, fnReturn, log, info) { | ||
log.name = component.constructor.displayName || '[unknown]'; | ||
log.reactID = component._rootNodeID; | ||
}; | ||
/** | ||
* Using the hsl() background-color attribute, colors an element. | ||
* | ||
* @param {DOMElement} el | ||
* @param {number} hue [0 for red, 120 for green, 240 for blue] | ||
*/ | ||
var _setHue = function(el, hue) { | ||
el.style.backgroundColor = 'hsla(' + hue + ', 100%, 50%, 0.6)'; | ||
}; | ||
/** | ||
* Round to the thousandth place. | ||
* @param {number} time | ||
* @return {number} | ||
*/ | ||
var _microTime = function(time) { | ||
return Math.round(time * 1000) / 1000; | ||
}; | ||
} | ||
module.exports = ReactDefaultPerf; |
@@ -53,2 +53,10 @@ /** | ||
}; | ||
// Expose the constructor on the ConvenienceConstructor and prototype so that | ||
// it can be easily easily accessed on descriptors. | ||
// E.g. <div />.type === div.type | ||
ConvenienceConstructor.type = Constructor; | ||
Constructor.prototype.type = Constructor; | ||
Constructor.ConvenienceConstructor = ConvenienceConstructor; | ||
ConvenienceConstructor.componentConstructor = Constructor; | ||
@@ -178,7 +186,12 @@ return ConvenienceConstructor; | ||
circle: false, | ||
defs: false, | ||
g: false, | ||
line: false, | ||
linearGradient: false, | ||
path: false, | ||
polygon: false, | ||
polyline: false, | ||
radialGradient: false, | ||
rect: false, | ||
stop: false, | ||
svg: false, | ||
@@ -185,0 +198,0 @@ text: false |
@@ -21,2 +21,3 @@ /** | ||
var AutoFocusMixin = require("./AutoFocusMixin"); | ||
var ReactCompositeComponent = require("./ReactCompositeComponent"); | ||
@@ -48,3 +49,6 @@ var ReactDOM = require("./ReactDOM"); | ||
var ReactDOMButton = ReactCompositeComponent.createClass({ | ||
displayName: 'ReactDOMButton', | ||
mixins: [AutoFocusMixin], | ||
render: function() { | ||
@@ -51,0 +55,0 @@ var props = {}; |
@@ -27,4 +27,4 @@ /** | ||
var ReactEventEmitter = require("./ReactEventEmitter"); | ||
var ReactMount = require("./ReactMount"); | ||
var ReactMultiChild = require("./ReactMultiChild"); | ||
var ReactMount = require("./ReactMount"); | ||
var ReactPerf = require("./ReactPerf"); | ||
@@ -38,5 +38,5 @@ | ||
var putListener = ReactEventEmitter.putListener; | ||
var deleteListener = ReactEventEmitter.deleteListener; | ||
var registrationNames = ReactEventEmitter.registrationNames; | ||
var listenTo = ReactEventEmitter.listenTo; | ||
var registrationNameModules = ReactEventEmitter.registrationNameModules; | ||
@@ -48,2 +48,4 @@ // For quickly matching children type, to test if can be treated as content. | ||
var ELEMENT_NODE_TYPE = 1; | ||
/** | ||
@@ -68,2 +70,18 @@ * @param {?object} props | ||
function putListener(id, registrationName, listener, transaction) { | ||
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 | ||
); | ||
} | ||
/** | ||
@@ -104,3 +122,3 @@ * @constructor ReactDOMComponent | ||
return ( | ||
this._createOpenTagMarkup() + | ||
this._createOpenTagMarkupAndPutListeners(transaction) + | ||
this._createContentMarkup(transaction) + | ||
@@ -121,5 +139,6 @@ this._tagClose | ||
* @private | ||
* @param {ReactReconcileTransaction} transaction | ||
* @return {string} Markup of opening tag. | ||
*/ | ||
_createOpenTagMarkup: function() { | ||
_createOpenTagMarkupAndPutListeners: function(transaction) { | ||
var props = this.props; | ||
@@ -136,4 +155,4 @@ var ret = this._tagOpen; | ||
} | ||
if (registrationNames[propKey]) { | ||
putListener(this._rootNodeID, propKey, propValue); | ||
if (registrationNameModules[propKey]) { | ||
putListener(this._rootNodeID, propKey, propValue, transaction); | ||
} else { | ||
@@ -154,4 +173,4 @@ if (propKey === STYLE) { | ||
var escapedID = escapeTextForBrowser(this._rootNodeID); | ||
return ret + ' ' + ReactMount.ATTR_NAME + '="' + escapedID + '">'; | ||
var idMarkup = DOMPropertyOperations.createMarkupForID(this._rootNodeID); | ||
return ret + ' ' + idMarkup + '>'; | ||
}, | ||
@@ -211,5 +230,10 @@ | ||
'updateComponent', | ||
function(transaction, prevProps) { | ||
ReactComponent.Mixin.updateComponent.call(this, transaction, prevProps); | ||
this._updateDOMProperties(prevProps); | ||
function(transaction, prevProps, prevOwner) { | ||
ReactComponent.Mixin.updateComponent.call( | ||
this, | ||
transaction, | ||
prevProps, | ||
prevOwner | ||
); | ||
this._updateDOMProperties(prevProps, transaction); | ||
this._updateDOMChildren(prevProps, transaction); | ||
@@ -232,4 +256,5 @@ } | ||
* @param {object} lastProps | ||
* @param {ReactReconcileTransaction} transaction | ||
*/ | ||
_updateDOMProperties: function(lastProps) { | ||
_updateDOMProperties: function(lastProps, transaction) { | ||
var nextProps = this.props; | ||
@@ -252,3 +277,3 @@ var propKey; | ||
} | ||
} else if (registrationNames[propKey]) { | ||
} else if (registrationNameModules[propKey]) { | ||
deleteListener(this._rootNodeID, propKey); | ||
@@ -258,3 +283,3 @@ } else if ( | ||
DOMProperty.isCustomAttribute(propKey)) { | ||
ReactComponent.DOMIDOperations.deletePropertyByID( | ||
ReactComponent.BackendIDOperations.deletePropertyByID( | ||
this._rootNodeID, | ||
@@ -296,8 +321,8 @@ propKey | ||
} | ||
} else if (registrationNames[propKey]) { | ||
putListener(this._rootNodeID, propKey, nextProp); | ||
} else if (registrationNameModules[propKey]) { | ||
putListener(this._rootNodeID, propKey, nextProp, transaction); | ||
} else if ( | ||
DOMProperty.isStandardName[propKey] || | ||
DOMProperty.isCustomAttribute(propKey)) { | ||
ReactComponent.DOMIDOperations.updatePropertyByID( | ||
ReactComponent.BackendIDOperations.updatePropertyByID( | ||
this._rootNodeID, | ||
@@ -310,3 +335,3 @@ propKey, | ||
if (styleUpdates) { | ||
ReactComponent.DOMIDOperations.updateStylesByID( | ||
ReactComponent.BackendIDOperations.updateStylesByID( | ||
this._rootNodeID, | ||
@@ -360,3 +385,3 @@ styleUpdates | ||
if (lastHtml !== nextHtml) { | ||
ReactComponent.DOMIDOperations.updateInnerHTMLByID( | ||
ReactComponent.BackendIDOperations.updateInnerHTMLByID( | ||
this._rootNodeID, | ||
@@ -378,5 +403,5 @@ nextHtml | ||
unmountComponent: function() { | ||
this.unmountChildren(); | ||
ReactEventEmitter.deleteAllListeners(this._rootNodeID); | ||
ReactComponent.Mixin.unmountComponent.call(this); | ||
this.unmountChildren(); | ||
} | ||
@@ -383,0 +408,0 @@ |
@@ -36,2 +36,4 @@ /** | ||
var ReactDOMForm = ReactCompositeComponent.createClass({ | ||
displayName: 'ReactDOMForm', | ||
render: function() { | ||
@@ -44,7 +46,12 @@ // TODO: Instead of using `ReactDOM` directly, we should use JSX. However, | ||
componentDidMount: function(node) { | ||
componentDidMount: function() { | ||
ReactEventEmitter.trapBubbledEvent( | ||
EventConstants.topLevelTypes.topReset, | ||
'reset', | ||
this.getDOMNode() | ||
); | ||
ReactEventEmitter.trapBubbledEvent( | ||
EventConstants.topLevelTypes.topSubmit, | ||
'submit', | ||
node | ||
this.getDOMNode() | ||
); | ||
@@ -51,0 +58,0 @@ } |
@@ -28,4 +28,4 @@ /** | ||
var ReactMount = require("./ReactMount"); | ||
var ReactPerf = require("./ReactPerf"); | ||
var getTextContentAccessor = require("./getTextContentAccessor"); | ||
var invariant = require("./invariant"); | ||
@@ -45,15 +45,7 @@ | ||
/** | ||
* The DOM property to use when setting text content. | ||
* | ||
* @type {string} | ||
* @private | ||
*/ | ||
var textContentAccessor = getTextContentAccessor() || 'NA'; | ||
var useWhitespaceWorkaround; | ||
var LEADING_SPACE = /^ /; | ||
/** | ||
* Operations used to process updates to DOM nodes. This is made injectable via | ||
* `ReactComponent.DOMIDOperations`. | ||
* `ReactComponent.BackendIDOperations`. | ||
*/ | ||
@@ -71,19 +63,23 @@ var ReactDOMIDOperations = { | ||
*/ | ||
updatePropertyByID: function(id, name, value) { | ||
var node = ReactMount.getNode(id); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name), | ||
'updatePropertyByID(...): %s', | ||
INVALID_PROPERTY_ERRORS[name] | ||
) : invariant(!INVALID_PROPERTY_ERRORS.hasOwnProperty(name))); | ||
updatePropertyByID: ReactPerf.measure( | ||
'ReactDOMIDOperations', | ||
'updatePropertyByID', | ||
function(id, name, value) { | ||
var node = ReactMount.getNode(id); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name), | ||
'updatePropertyByID(...): %s', | ||
INVALID_PROPERTY_ERRORS[name] | ||
) : invariant(!INVALID_PROPERTY_ERRORS.hasOwnProperty(name))); | ||
// If we're updating to null or undefined, we should remove the property | ||
// from the DOM node instead of inadvertantly setting to a string. This | ||
// brings us in line with the same behavior we have on initial render. | ||
if (value != null) { | ||
DOMPropertyOperations.setValueForProperty(node, name, value); | ||
} else { | ||
DOMPropertyOperations.deleteValueForProperty(node, name); | ||
// If we're updating to null or undefined, we should remove the property | ||
// from the DOM node instead of inadvertantly setting to a string. This | ||
// brings us in line with the same behavior we have on initial render. | ||
if (value != null) { | ||
DOMPropertyOperations.setValueForProperty(node, name, value); | ||
} else { | ||
DOMPropertyOperations.deleteValueForProperty(node, name); | ||
} | ||
} | ||
}, | ||
), | ||
@@ -98,11 +94,15 @@ /** | ||
*/ | ||
deletePropertyByID: function(id, name, value) { | ||
var node = ReactMount.getNode(id); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name), | ||
'updatePropertyByID(...): %s', | ||
INVALID_PROPERTY_ERRORS[name] | ||
) : invariant(!INVALID_PROPERTY_ERRORS.hasOwnProperty(name))); | ||
DOMPropertyOperations.deleteValueForProperty(node, name, value); | ||
}, | ||
deletePropertyByID: ReactPerf.measure( | ||
'ReactDOMIDOperations', | ||
'deletePropertyByID', | ||
function(id, name, value) { | ||
var node = ReactMount.getNode(id); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name), | ||
'updatePropertyByID(...): %s', | ||
INVALID_PROPERTY_ERRORS[name] | ||
) : invariant(!INVALID_PROPERTY_ERRORS.hasOwnProperty(name))); | ||
DOMPropertyOperations.deleteValueForProperty(node, name, value); | ||
} | ||
), | ||
@@ -117,6 +117,10 @@ /** | ||
*/ | ||
updateStylesByID: function(id, styles) { | ||
var node = ReactMount.getNode(id); | ||
CSSPropertyOperations.setValueForStyles(node, styles); | ||
}, | ||
updateStylesByID: ReactPerf.measure( | ||
'ReactDOMIDOperations', | ||
'updateStylesByID', | ||
function(id, styles) { | ||
var node = ReactMount.getNode(id); | ||
CSSPropertyOperations.setValueForStyles(node, styles); | ||
} | ||
), | ||
@@ -130,9 +134,39 @@ /** | ||
*/ | ||
updateInnerHTMLByID: function(id, html) { | ||
var node = ReactMount.getNode(id); | ||
// HACK: IE8- normalize whitespace in innerHTML, removing leading spaces. | ||
// @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html | ||
node.innerHTML = html.replace(LEADING_SPACE, ' '); | ||
}, | ||
updateInnerHTMLByID: ReactPerf.measure( | ||
'ReactDOMIDOperations', | ||
'updateInnerHTMLByID', | ||
function(id, html) { | ||
var node = ReactMount.getNode(id); | ||
// IE8: When updating a just created node with innerHTML only leading | ||
// whitespace is removed. When updating an existing node with innerHTML | ||
// whitespace in root TextNodes is also collapsed. | ||
// @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html | ||
if (useWhitespaceWorkaround === undefined) { | ||
// Feature detection; only IE8 is known to behave improperly like this. | ||
var temp = document.createElement('div'); | ||
temp.innerHTML = ' '; | ||
useWhitespaceWorkaround = temp.innerHTML === ''; | ||
} | ||
if (useWhitespaceWorkaround) { | ||
// Magic theory: IE8 supposedly differentiates between added and updated | ||
// nodes when processing innerHTML, innerHTML on updated nodes suffers | ||
// from worse whitespace behavior. Re-adding a node like this triggers | ||
// the initial and more favorable whitespace behavior. | ||
node.parentNode.replaceChild(node, node); | ||
} | ||
if (useWhitespaceWorkaround && html.match(/^[ \r\n\t\f]/)) { | ||
// Recover leading whitespace by temporarily prepending any character. | ||
// \uFEFF has the potential advantage of being zero-width/invisible. | ||
node.innerHTML = '\uFEFF' + html; | ||
node.firstChild.deleteData(0, 1); | ||
} else { | ||
node.innerHTML = html; | ||
} | ||
} | ||
), | ||
/** | ||
@@ -145,6 +179,10 @@ * Updates a DOM node's text content set by `props.content`. | ||
*/ | ||
updateTextContentByID: function(id, content) { | ||
var node = ReactMount.getNode(id); | ||
node[textContentAccessor] = content; | ||
}, | ||
updateTextContentByID: ReactPerf.measure( | ||
'ReactDOMIDOperations', | ||
'updateTextContentByID', | ||
function(id, content) { | ||
var node = ReactMount.getNode(id); | ||
DOMChildrenOperations.updateTextContent(node, content); | ||
} | ||
), | ||
@@ -159,6 +197,10 @@ /** | ||
*/ | ||
dangerouslyReplaceNodeWithMarkupByID: function(id, markup) { | ||
var node = ReactMount.getNode(id); | ||
DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup); | ||
}, | ||
dangerouslyReplaceNodeWithMarkupByID: ReactPerf.measure( | ||
'ReactDOMIDOperations', | ||
'dangerouslyReplaceNodeWithMarkupByID', | ||
function(id, markup) { | ||
var node = ReactMount.getNode(id); | ||
DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup); | ||
} | ||
), | ||
@@ -172,11 +214,14 @@ /** | ||
*/ | ||
dangerouslyProcessChildrenUpdates: function(updates, markup) { | ||
for (var i = 0; i < updates.length; i++) { | ||
updates[i].parentNode = ReactMount.getNode(updates[i].parentID); | ||
dangerouslyProcessChildrenUpdates: ReactPerf.measure( | ||
'ReactDOMIDOperations', | ||
'dangerouslyProcessChildrenUpdates', | ||
function(updates, markup) { | ||
for (var i = 0; i < updates.length; i++) { | ||
updates[i].parentNode = ReactMount.getNode(updates[i].parentID); | ||
} | ||
DOMChildrenOperations.processUpdates(updates, markup); | ||
} | ||
DOMChildrenOperations.processUpdates(updates, markup); | ||
} | ||
) | ||
}; | ||
module.exports = ReactDOMIDOperations; |
@@ -21,4 +21,5 @@ /** | ||
var AutoFocusMixin = require("./AutoFocusMixin"); | ||
var DOMPropertyOperations = require("./DOMPropertyOperations"); | ||
var LinkedValueMixin = require("./LinkedValueMixin"); | ||
var LinkedValueUtils = require("./LinkedValueUtils"); | ||
var ReactCompositeComponent = require("./ReactCompositeComponent"); | ||
@@ -53,4 +54,6 @@ var ReactDOM = require("./ReactDOM"); | ||
var ReactDOMInput = ReactCompositeComponent.createClass({ | ||
mixins: [LinkedValueMixin], | ||
displayName: 'ReactDOMInput', | ||
mixins: [AutoFocusMixin, LinkedValueUtils.Mixin], | ||
getInitialState: function() { | ||
@@ -75,8 +78,9 @@ var defaultValue = this.props.defaultValue; | ||
props.defaultValue = null; | ||
props.checked = | ||
this.props.checked != null ? this.props.checked : this.state.checked; | ||
var value = this.getValue(); | ||
var value = LinkedValueUtils.getValue(this); | ||
props.value = value != null ? value : this.state.value; | ||
var checked = LinkedValueUtils.getChecked(this); | ||
props.checked = checked != null ? checked : this.state.checked; | ||
props.onChange = this._handleChange; | ||
@@ -87,4 +91,4 @@ | ||
componentDidMount: function(rootNode) { | ||
var id = ReactMount.getID(rootNode); | ||
componentDidMount: function() { | ||
var id = ReactMount.getID(this.getDOMNode()); | ||
instancesByReactID[id] = this; | ||
@@ -99,3 +103,4 @@ }, | ||
componentDidUpdate: function(prevProps, prevState, rootNode) { | ||
componentDidUpdate: function(prevProps, prevState, prevContext) { | ||
var rootNode = this.getDOMNode(); | ||
if (this.props.checked != null) { | ||
@@ -109,3 +114,3 @@ DOMPropertyOperations.setValueForProperty( | ||
var value = this.getValue(); | ||
var value = LinkedValueUtils.getValue(this); | ||
if (value != null) { | ||
@@ -120,6 +125,6 @@ // Cast `value` to a string to ensure the value is set correctly. While | ||
var returnValue; | ||
var onChange = this.getOnChange(); | ||
var onChange = LinkedValueUtils.getOnChange(this); | ||
if (onChange) { | ||
this._isChanging = true; | ||
returnValue = onChange(event); | ||
returnValue = onChange.call(this, event); | ||
this._isChanging = false; | ||
@@ -135,2 +140,8 @@ } | ||
var rootNode = this.getDOMNode(); | ||
var queryRoot = rootNode; | ||
while (queryRoot.parentNode) { | ||
queryRoot = queryRoot.parentNode; | ||
} | ||
// If `rootNode.form` was non-null, then we could try `form.elements`, | ||
@@ -141,8 +152,9 @@ // but that sometimes behaves strangely in IE8. We could also try using | ||
// the input might not even be in a form, let's just use the global | ||
// `getElementsByName` to ensure we don't miss anything. | ||
var group = document.getElementsByName(name); | ||
// `querySelectorAll` to ensure we don't miss anything. | ||
var group = queryRoot.querySelectorAll( | ||
'input[name=' + JSON.stringify('' + name) + '][type="radio"]'); | ||
for (var i = 0, groupLen = group.length; i < groupLen; i++) { | ||
var otherNode = group[i]; | ||
if (otherNode === rootNode || | ||
otherNode.nodeName !== 'INPUT' || otherNode.type !== 'radio' || | ||
otherNode.form !== rootNode.form) { | ||
@@ -149,0 +161,0 @@ continue; |
@@ -31,2 +31,3 @@ /** | ||
var ReactDOMOption = ReactCompositeComponent.createClass({ | ||
displayName: 'ReactDOMOption', | ||
@@ -33,0 +34,0 @@ componentWillMount: function() { |
@@ -21,3 +21,4 @@ /** | ||
var LinkedValueMixin = require("./LinkedValueMixin"); | ||
var AutoFocusMixin = require("./AutoFocusMixin"); | ||
var LinkedValueUtils = require("./LinkedValueUtils"); | ||
var ReactCompositeComponent = require("./ReactCompositeComponent"); | ||
@@ -59,16 +60,25 @@ var ReactDOM = require("./ReactDOM"); | ||
* If `value` is supplied, updates <option> elements on mount and update. | ||
* @param {ReactComponent} component Instance of ReactDOMSelect | ||
* @param {?*} propValue For uncontrolled components, null/undefined. For | ||
* controlled components, a string (or with `multiple`, a list of strings). | ||
* @private | ||
*/ | ||
function updateOptions() { | ||
/*jshint validthis:true */ | ||
var propValue = this.getValue(); | ||
var value = propValue != null ? propValue : this.state.value; | ||
var options = this.getDOMNode().options; | ||
var selectedValue = '' + value; | ||
function updateOptions(component, propValue) { | ||
var multiple = component.props.multiple; | ||
var value = propValue != null ? propValue : component.state.value; | ||
var options = component.getDOMNode().options; | ||
var selectedValue, i, l; | ||
if (multiple) { | ||
selectedValue = {}; | ||
for (i = 0, l = value.length; i < l; ++i) { | ||
selectedValue['' + value[i]] = true; | ||
} | ||
} else { | ||
selectedValue = '' + value; | ||
} | ||
for (i = 0, l = options.length; i < l; i++) { | ||
var selected = multiple ? | ||
selectedValue.hasOwnProperty(options[i].value) : | ||
options[i].value === selectedValue; | ||
for (var i = 0, l = options.length; i < l; i++) { | ||
var selected = this.props.multiple ? | ||
selectedValue.indexOf(options[i].value) >= 0 : | ||
selected = options[i].value === selectedValue; | ||
if (selected !== options[i].selected) { | ||
@@ -96,4 +106,6 @@ options[i].selected = selected; | ||
var ReactDOMSelect = ReactCompositeComponent.createClass({ | ||
mixins: [LinkedValueMixin], | ||
displayName: 'ReactDOMSelect', | ||
mixins: [AutoFocusMixin, LinkedValueUtils.Mixin], | ||
propTypes: { | ||
@@ -131,12 +143,19 @@ defaultValue: selectValueType, | ||
componentDidMount: updateOptions, | ||
componentDidMount: function() { | ||
updateOptions(this, LinkedValueUtils.getValue(this)); | ||
}, | ||
componentDidUpdate: updateOptions, | ||
componentDidUpdate: function() { | ||
var value = LinkedValueUtils.getValue(this); | ||
if (value != null) { | ||
updateOptions(this, value); | ||
} | ||
}, | ||
_handleChange: function(event) { | ||
var returnValue; | ||
var onChange = this.getOnChange(); | ||
var onChange = LinkedValueUtils.getOnChange(this); | ||
if (onChange) { | ||
this._isChanging = true; | ||
returnValue = onChange(event); | ||
returnValue = onChange.call(this, event); | ||
this._isChanging = false; | ||
@@ -143,0 +162,0 @@ } |
@@ -21,4 +21,5 @@ /** | ||
var AutoFocusMixin = require("./AutoFocusMixin"); | ||
var DOMPropertyOperations = require("./DOMPropertyOperations"); | ||
var LinkedValueMixin = require("./LinkedValueMixin"); | ||
var LinkedValueUtils = require("./LinkedValueUtils"); | ||
var ReactCompositeComponent = require("./ReactCompositeComponent"); | ||
@@ -49,4 +50,6 @@ var ReactDOM = require("./ReactDOM"); | ||
var ReactDOMTextarea = ReactCompositeComponent.createClass({ | ||
mixins: [LinkedValueMixin], | ||
displayName: 'ReactDOMTextarea', | ||
mixins: [AutoFocusMixin, LinkedValueUtils.Mixin], | ||
getInitialState: function() { | ||
@@ -80,3 +83,3 @@ var defaultValue = this.props.defaultValue; | ||
} | ||
var value = this.getValue(); | ||
var value = LinkedValueUtils.getValue(this); | ||
return { | ||
@@ -100,3 +103,3 @@ // We save the initial value so that `ReactDOMComponent` doesn't update | ||
var props = merge(this.props); | ||
var value = this.getValue(); | ||
var value = LinkedValueUtils.getValue(this); | ||
@@ -117,5 +120,6 @@ ("production" !== process.env.NODE_ENV ? invariant( | ||
componentDidUpdate: function(prevProps, prevState, rootNode) { | ||
var value = this.getValue(); | ||
componentDidUpdate: function(prevProps, prevState, prevContext) { | ||
var value = LinkedValueUtils.getValue(this); | ||
if (value != null) { | ||
var rootNode = this.getDOMNode(); | ||
// Cast `value` to a string to ensure the value is set correctly. While | ||
@@ -129,6 +133,6 @@ // browsers typically do this as necessary, jsdom doesn't. | ||
var returnValue; | ||
var onChange = this.getOnChange(); | ||
var onChange = LinkedValueUtils.getOnChange(this); | ||
if (onChange) { | ||
this._isChanging = true; | ||
returnValue = onChange(event); | ||
returnValue = onChange.call(this, event); | ||
this._isChanging = false; | ||
@@ -135,0 +139,0 @@ } |
@@ -20,7 +20,9 @@ /** | ||
"use strict"; | ||
var ReactErrorUtils = { | ||
/** | ||
* Creates a guarded version of a function. This is supposed to make debugging | ||
* of event handlers easier. This implementation provides only basic error | ||
* logging and re-throws the error. | ||
* of event handlers easier. To aid debugging with the browser's debugger, | ||
* this currently simply returns the original function. | ||
* | ||
@@ -32,14 +34,3 @@ * @param {function} func Function to be executed | ||
guard: function(func, name) { | ||
if ("production" !== process.env.NODE_ENV) { | ||
return function guarded() { | ||
try { | ||
return func.apply(this, arguments); | ||
} catch(ex) { | ||
console.error(name + ': ' + ex.message); | ||
throw ex; | ||
} | ||
}; | ||
} else { | ||
return func; | ||
} | ||
return func; | ||
} | ||
@@ -46,0 +37,0 @@ }; |
@@ -25,2 +25,3 @@ /** | ||
var EventPluginHub = require("./EventPluginHub"); | ||
var EventPluginRegistry = require("./EventPluginRegistry"); | ||
var ExecutionEnvironment = require("./ExecutionEnvironment"); | ||
@@ -64,12 +65,12 @@ var ReactEventEmitterMixin = require("./ReactEventEmitterMixin"); | ||
* | | . | | +-----------+ | utilities | | ||
* | +-----------.---------+ | +------------+ | ||
* | | | . +----|---------+ | ||
* +-----|------+ . | ^ +-----------+ | ||
* | . | | |Enter/Leave| | ||
* + . | +-------+|Plugin | | ||
* +-------------+ . v +-----------+ | ||
* | application | . +----------+ | ||
* |-------------| . | callback | | ||
* | | . | registry | | ||
* | | . +----------+ | ||
* | +-----------.--->| | +------------+ | ||
* | | | . +--------------+ | ||
* +-----|------+ . ^ +-----------+ | ||
* | . | |Enter/Leave| | ||
* + . +-------+|Plugin | | ||
* +-------------+ . +-----------+ | ||
* | application | . | ||
* |-------------| . | ||
* | | . | ||
* | | . | ||
* +-------------+ . | ||
@@ -80,3 +81,62 @@ * . | ||
var alreadyListeningTo = {}; | ||
var isMonitoringScrollValue = false; | ||
var reactTopListenersCounter = 0; | ||
// For events like 'submit' which don't consistently bubble (which we trap at a | ||
// lower node than `document`), binding at `document` would cause duplicate | ||
// events so we don't include them here | ||
var topEventMapping = { | ||
topBlur: 'blur', | ||
topChange: 'change', | ||
topClick: 'click', | ||
topCompositionEnd: 'compositionend', | ||
topCompositionStart: 'compositionstart', | ||
topCompositionUpdate: 'compositionupdate', | ||
topContextMenu: 'contextmenu', | ||
topCopy: 'copy', | ||
topCut: 'cut', | ||
topDoubleClick: 'dblclick', | ||
topDrag: 'drag', | ||
topDragEnd: 'dragend', | ||
topDragEnter: 'dragenter', | ||
topDragExit: 'dragexit', | ||
topDragLeave: 'dragleave', | ||
topDragOver: 'dragover', | ||
topDragStart: 'dragstart', | ||
topDrop: 'drop', | ||
topFocus: 'focus', | ||
topInput: 'input', | ||
topKeyDown: 'keydown', | ||
topKeyPress: 'keypress', | ||
topKeyUp: 'keyup', | ||
topMouseDown: 'mousedown', | ||
topMouseMove: 'mousemove', | ||
topMouseOut: 'mouseout', | ||
topMouseOver: 'mouseover', | ||
topMouseUp: 'mouseup', | ||
topPaste: 'paste', | ||
topScroll: 'scroll', | ||
topSelectionChange: 'selectionchange', | ||
topTouchCancel: 'touchcancel', | ||
topTouchEnd: 'touchend', | ||
topTouchMove: 'touchmove', | ||
topTouchStart: 'touchstart', | ||
topWheel: 'wheel' | ||
}; | ||
/** | ||
* To ensure no conflicts with other potential React instances on the page | ||
*/ | ||
var topListenersIDKey = "_reactListenersID" + String(Math.random()).slice(2); | ||
function getListeningForDocument(mountAt) { | ||
if (mountAt[topListenersIDKey] == null) { | ||
mountAt[topListenersIDKey] = reactTopListenersCounter++; | ||
alreadyListeningTo[mountAt[topListenersIDKey]] = {}; | ||
} | ||
return alreadyListeningTo[mountAt[topListenersIDKey]]; | ||
} | ||
/** | ||
* Traps top-level events by using event bubbling. | ||
@@ -118,17 +178,2 @@ * | ||
/** | ||
* Listens to window scroll and resize events. We cache scroll values so that | ||
* application code can access them without triggering reflows. | ||
* | ||
* NOTE: Scroll events do not bubble. | ||
* | ||
* @private | ||
* @see http://www.quirksmode.org/dom/events/scroll.html | ||
*/ | ||
function registerScrollValueMonitoring() { | ||
var refresh = ViewportMetrics.refreshScrollValues; | ||
EventListener.listen(window, 'scroll', refresh); | ||
EventListener.listen(window, 'resize', refresh); | ||
} | ||
/** | ||
* `ReactEventEmitter` is used to attach top-level event listeners. For example: | ||
@@ -150,33 +195,9 @@ * | ||
/** | ||
* Ensures that top-level event delegation listeners are installed. | ||
* | ||
* There are issues with listening to both touch events and mouse events on | ||
* the top-level, so we make the caller choose which one to listen to. (If | ||
* there's a touch top-level listeners, anchors don't receive clicks for some | ||
* reason, and only in some cases). | ||
* | ||
* @param {boolean} touchNotMouse Listen to touch events instead of mouse. | ||
* @param {DOMDocument} contentDocument DOM document to listen on | ||
*/ | ||
ensureListening: function(touchNotMouse, contentDocument) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
ExecutionEnvironment.canUseDOM, | ||
'ensureListening(...): Cannot toggle event listening in a Worker ' + | ||
'thread. This is likely a bug in the framework. Please report ' + | ||
'immediately.' | ||
) : invariant(ExecutionEnvironment.canUseDOM)); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
ReactEventEmitter.TopLevelCallbackCreator, | ||
'ensureListening(...): Cannot be called without a top level callback ' + | ||
'creator being injected.' | ||
) : invariant(ReactEventEmitter.TopLevelCallbackCreator)); | ||
// Call out to base implementation. | ||
ReactEventEmitterMixin.ensureListening.call( | ||
ReactEventEmitter, | ||
{ | ||
touchNotMouse: touchNotMouse, | ||
contentDocument: contentDocument | ||
} | ||
); | ||
injection: { | ||
/** | ||
* @param {function} TopLevelCallbackCreator | ||
*/ | ||
injectTopLevelCallbackCreator: function(TopLevelCallbackCreator) { | ||
ReactEventEmitter.TopLevelCallbackCreator = TopLevelCallbackCreator; | ||
} | ||
}, | ||
@@ -228,104 +249,80 @@ | ||
* | ||
* @param {boolean} touchNotMouse Listen to touch events instead of mouse. | ||
* @param {string} registrationName Name of listener (e.g. `onClick`). | ||
* @param {DOMDocument} contentDocument Document which owns the container | ||
* @private | ||
* @see http://www.quirksmode.org/dom/events/keys.html. | ||
*/ | ||
listenAtTopLevel: function(touchNotMouse, contentDocument) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!contentDocument._isListening, | ||
'listenAtTopLevel(...): Cannot setup top-level listener more than once.' | ||
) : invariant(!contentDocument._isListening)); | ||
var topLevelTypes = EventConstants.topLevelTypes; | ||
listenTo: function(registrationName, contentDocument) { | ||
var mountAt = contentDocument; | ||
var isListening = getListeningForDocument(mountAt); | ||
var dependencies = EventPluginRegistry. | ||
registrationNameDependencies[registrationName]; | ||
registerScrollValueMonitoring(); | ||
trapBubbledEvent(topLevelTypes.topMouseOver, 'mouseover', mountAt); | ||
trapBubbledEvent(topLevelTypes.topMouseDown, 'mousedown', mountAt); | ||
trapBubbledEvent(topLevelTypes.topMouseUp, 'mouseup', mountAt); | ||
trapBubbledEvent(topLevelTypes.topMouseMove, 'mousemove', mountAt); | ||
trapBubbledEvent(topLevelTypes.topMouseOut, 'mouseout', mountAt); | ||
trapBubbledEvent(topLevelTypes.topClick, 'click', mountAt); | ||
trapBubbledEvent(topLevelTypes.topDoubleClick, 'dblclick', mountAt); | ||
trapBubbledEvent(topLevelTypes.topContextMenu, 'contextmenu', mountAt); | ||
if (touchNotMouse) { | ||
trapBubbledEvent(topLevelTypes.topTouchStart, 'touchstart', mountAt); | ||
trapBubbledEvent(topLevelTypes.topTouchEnd, 'touchend', mountAt); | ||
trapBubbledEvent(topLevelTypes.topTouchMove, 'touchmove', mountAt); | ||
trapBubbledEvent(topLevelTypes.topTouchCancel, 'touchcancel', mountAt); | ||
} | ||
trapBubbledEvent(topLevelTypes.topKeyUp, 'keyup', mountAt); | ||
trapBubbledEvent(topLevelTypes.topKeyPress, 'keypress', mountAt); | ||
trapBubbledEvent(topLevelTypes.topKeyDown, 'keydown', mountAt); | ||
trapBubbledEvent(topLevelTypes.topInput, 'input', mountAt); | ||
trapBubbledEvent(topLevelTypes.topChange, 'change', mountAt); | ||
trapBubbledEvent( | ||
topLevelTypes.topSelectionChange, | ||
'selectionchange', | ||
mountAt | ||
); | ||
var topLevelTypes = EventConstants.topLevelTypes; | ||
for (var i = 0, l = dependencies.length; i < l; i++) { | ||
var dependency = dependencies[i]; | ||
if (!isListening[dependency]) { | ||
var topLevelType = topLevelTypes[dependency]; | ||
trapBubbledEvent( | ||
topLevelTypes.topCompositionEnd, | ||
'compositionend', | ||
mountAt | ||
); | ||
trapBubbledEvent( | ||
topLevelTypes.topCompositionStart, | ||
'compositionstart', | ||
mountAt | ||
); | ||
trapBubbledEvent( | ||
topLevelTypes.topCompositionUpdate, | ||
'compositionupdate', | ||
mountAt | ||
); | ||
if (topLevelType === topLevelTypes.topWheel) { | ||
if (isEventSupported('wheel')) { | ||
trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt); | ||
} else if (isEventSupported('mousewheel')) { | ||
trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt); | ||
} else { | ||
// Firefox needs to capture a different mouse scroll event. | ||
// @see http://www.quirksmode.org/dom/events/tests/scroll.html | ||
trapBubbledEvent( | ||
topLevelTypes.topWheel, | ||
'DOMMouseScroll', | ||
mountAt); | ||
} | ||
} else if (topLevelType === topLevelTypes.topScroll) { | ||
if (isEventSupported('drag')) { | ||
trapBubbledEvent(topLevelTypes.topDrag, 'drag', mountAt); | ||
trapBubbledEvent(topLevelTypes.topDragEnd, 'dragend', mountAt); | ||
trapBubbledEvent(topLevelTypes.topDragEnter, 'dragenter', mountAt); | ||
trapBubbledEvent(topLevelTypes.topDragExit, 'dragexit', mountAt); | ||
trapBubbledEvent(topLevelTypes.topDragLeave, 'dragleave', mountAt); | ||
trapBubbledEvent(topLevelTypes.topDragOver, 'dragover', mountAt); | ||
trapBubbledEvent(topLevelTypes.topDragStart, 'dragstart', mountAt); | ||
trapBubbledEvent(topLevelTypes.topDrop, 'drop', mountAt); | ||
} | ||
if (isEventSupported('scroll', true)) { | ||
trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt); | ||
} else { | ||
trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window); | ||
} | ||
} else if (topLevelType === topLevelTypes.topFocus || | ||
topLevelType === topLevelTypes.topBlur) { | ||
if (isEventSupported('wheel')) { | ||
trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt); | ||
} else if (isEventSupported('mousewheel')) { | ||
trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt); | ||
} else { | ||
// Firefox needs to capture a different mouse scroll event. | ||
// @see http://www.quirksmode.org/dom/events/tests/scroll.html | ||
trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt); | ||
} | ||
if (isEventSupported('focus', true)) { | ||
trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt); | ||
trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt); | ||
} else if (isEventSupported('focusin')) { | ||
// IE has `focusin` and `focusout` events which bubble. | ||
// @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html | ||
trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt); | ||
trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt); | ||
} | ||
// IE<9 does not support capturing so just trap the bubbled event there. | ||
if (isEventSupported('scroll', true)) { | ||
trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt); | ||
} else { | ||
trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window); | ||
} | ||
// to make sure blur and focus event listeners are only attached once | ||
isListening[topLevelTypes.topBlur] = true; | ||
isListening[topLevelTypes.topFocus] = true; | ||
} else if (topEventMapping[dependency]) { | ||
trapBubbledEvent(topLevelType, topEventMapping[dependency], mountAt); | ||
} | ||
if (isEventSupported('focus', true)) { | ||
trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt); | ||
trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt); | ||
} else if (isEventSupported('focusin')) { | ||
// IE has `focusin` and `focusout` events which bubble. | ||
// @see | ||
// http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html | ||
trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt); | ||
trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt); | ||
isListening[dependency] = true; | ||
} | ||
} | ||
}, | ||
if (isEventSupported('copy')) { | ||
trapBubbledEvent(topLevelTypes.topCopy, 'copy', mountAt); | ||
trapBubbledEvent(topLevelTypes.topCut, 'cut', mountAt); | ||
trapBubbledEvent(topLevelTypes.topPaste, 'paste', mountAt); | ||
/** | ||
* Listens to window scroll and resize events. We cache scroll values so that | ||
* application code can access them without triggering reflows. | ||
* | ||
* NOTE: Scroll events do not bubble. | ||
* | ||
* @see http://www.quirksmode.org/dom/events/scroll.html | ||
*/ | ||
ensureScrollValueMonitoring: function(){ | ||
if (!isMonitoringScrollValue) { | ||
var refresh = ViewportMetrics.refreshScrollValues; | ||
EventListener.listen(window, 'scroll', refresh); | ||
EventListener.listen(window, 'resize', refresh); | ||
isMonitoringScrollValue = true; | ||
} | ||
}, | ||
registrationNames: EventPluginHub.registrationNames, | ||
registrationNameModules: EventPluginHub.registrationNameModules, | ||
@@ -346,3 +343,2 @@ putListener: EventPluginHub.putListener, | ||
module.exports = ReactEventEmitter; |
@@ -30,36 +30,4 @@ /** | ||
var ReactEventEmitterMixin = { | ||
/** | ||
* Whether or not `ensureListening` has been invoked. | ||
* @type {boolean} | ||
* @private | ||
*/ | ||
_isListening: false, | ||
/** | ||
* Function, must be implemented. Listens to events on the top level of the | ||
* application. | ||
* | ||
* @abstract | ||
* | ||
* listenAtTopLevel: null, | ||
*/ | ||
/** | ||
* Ensures that top-level event delegation listeners are installed. | ||
* | ||
* There are issues with listening to both touch events and mouse events on | ||
* the top-level, so we make the caller choose which one to listen to. (If | ||
* there's a touch top-level listeners, anchors don't receive clicks for some | ||
* reason, and only in some cases). | ||
* | ||
* @param {*} config Configuration passed through to `listenAtTopLevel`. | ||
*/ | ||
ensureListening: function(config) { | ||
if (!config.contentDocument._reactIsListening) { | ||
this.listenAtTopLevel(config.touchNotMouse, config.contentDocument); | ||
config.contentDocument._reactIsListening = true; | ||
} | ||
}, | ||
/** | ||
* Streams a fired top-level event to `EventPluginHub` where plugins have the | ||
@@ -66,0 +34,0 @@ * opportunity to create `ReactEvent`s to be dispatched. |
@@ -23,2 +23,3 @@ /** | ||
var ReactEventEmitter = require("./ReactEventEmitter"); | ||
var ReactInstanceHandles = require("./ReactInstanceHandles"); | ||
var ReactMount = require("./ReactMount"); | ||
@@ -35,2 +36,20 @@ | ||
/** | ||
* Finds the parent React component of `node`. | ||
* | ||
* @param {*} node | ||
* @return {?DOMEventTarget} Parent container, or `null` if the specified node | ||
* is not nested. | ||
*/ | ||
function findParent(node) { | ||
// TODO: It may be a good idea to cache this to prevent unnecessary DOM | ||
// traversal, but caching is difficult to do correctly without using a | ||
// mutation observer to listen for all DOM changes. | ||
var nodeID = ReactMount.getID(node); | ||
var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID); | ||
var container = ReactMount.findReactContainerForID(rootID); | ||
var parent = ReactMount.getFirstReactDOM(container); | ||
return parent; | ||
} | ||
/** | ||
* Top-level callback creator used to implement event handling using delegation. | ||
@@ -71,17 +90,18 @@ * This is used via dependency injection. | ||
} | ||
// TODO: Remove when synthetic events are ready, this is for IE<9. | ||
if (nativeEvent.srcElement && | ||
nativeEvent.srcElement !== nativeEvent.target) { | ||
nativeEvent.target = nativeEvent.srcElement; | ||
} | ||
var topLevelTarget = ReactMount.getFirstReactDOM( | ||
getEventTarget(nativeEvent) | ||
) || window; | ||
var topLevelTargetID = ReactMount.getID(topLevelTarget) || ''; | ||
ReactEventEmitter.handleTopLevel( | ||
topLevelType, | ||
topLevelTarget, | ||
topLevelTargetID, | ||
nativeEvent | ||
); | ||
// Loop through the hierarchy, in case there's any nested components. | ||
while (topLevelTarget) { | ||
var topLevelTargetID = ReactMount.getID(topLevelTarget) || ''; | ||
ReactEventEmitter.handleTopLevel( | ||
topLevelType, | ||
topLevelTarget, | ||
topLevelTargetID, | ||
nativeEvent | ||
); | ||
topLevelTarget = findParent(topLevelTarget); | ||
} | ||
}; | ||
@@ -88,0 +108,0 @@ } |
@@ -22,2 +22,4 @@ /** | ||
var ReactRootIndex = require("./ReactRootIndex"); | ||
var invariant = require("./invariant"); | ||
@@ -34,11 +36,2 @@ | ||
/** | ||
* Size of the reactRoot ID space. We generate random numbers for React root | ||
* IDs and if there's a collision the events and DOM update system will | ||
* get confused. If we assume 100 React components per page, and a user | ||
* loads 1 page per minute 24/7 for 50 years, with a mount point space of | ||
* 9,999,999 the likelihood of never having a collision is 99.997%. | ||
*/ | ||
var GLOBAL_MOUNT_POINT_MAX = 9999999; | ||
/** | ||
* Creates a DOM ID prefix to use when mounting React components. | ||
@@ -51,3 +44,3 @@ * | ||
function getReactRootIDString(index) { | ||
return SEPARATOR + 'r[' + index.toString(36) + ']'; | ||
return SEPARATOR + index.toString(36); | ||
} | ||
@@ -181,3 +174,4 @@ | ||
* Traverses the parent path between two IDs (either up or down). The IDs must | ||
* not be the same, and there must exist a parent path between them. | ||
* not be the same, and there must exist a parent path between them. If the | ||
* callback returns `false`, traversal is stopped. | ||
* | ||
@@ -211,6 +205,7 @@ * @param {?string} start ID at which to start traversal. | ||
for (var id = start; /* until break */; id = traverse(id, stop)) { | ||
var ret; | ||
if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) { | ||
cb(id, traverseUp, arg); | ||
ret = cb(id, traverseUp, arg); | ||
} | ||
if (id === stop) { | ||
if (ret === false || id === stop) { | ||
// Only break //after// visiting `stop`. | ||
@@ -237,6 +232,8 @@ break; | ||
/** | ||
* Constructs a React root ID | ||
* @return {string} A React root ID. | ||
*/ | ||
createReactRootID: function() { | ||
return getReactRootIDString( | ||
Math.ceil(Math.random() * GLOBAL_MOUNT_POINT_MAX) | ||
); | ||
return getReactRootIDString(ReactRootIndex.createReactRootIndex()); | ||
}, | ||
@@ -253,3 +250,3 @@ | ||
createReactID: function(rootID, name) { | ||
return rootID + SEPARATOR + name; | ||
return rootID + name; | ||
}, | ||
@@ -266,4 +263,7 @@ | ||
getReactRootIDFromNodeID: function(id) { | ||
var regexResult = /\.r\[[^\]]+\]/.exec(id); | ||
return regexResult && regexResult[0]; | ||
if (id && id.charAt(0) === SEPARATOR && id.length > 1) { | ||
var index = id.indexOf(SEPARATOR, 1); | ||
return index > -1 ? id.substr(0, index) : id; | ||
} | ||
return null; | ||
}, | ||
@@ -313,2 +313,18 @@ | ||
/** | ||
* Traverse a node ID, calling the supplied `cb` for each ancestor ID. For | ||
* example, passing `.0.$row-0.1` would result in `cb` getting called | ||
* with `.0`, `.0.$row-0`, and `.0.$row-0.1`. | ||
* | ||
* NOTE: This traversal happens on IDs without touching the DOM. | ||
* | ||
* @param {string} targetID ID of the target node. | ||
* @param {function} cb Callback to invoke. | ||
* @param {*} arg Argument to invoke the callback with. | ||
* @internal | ||
*/ | ||
traverseAncestors: function(targetID, cb, arg) { | ||
traverseParentPath('', targetID, cb, arg, true, false); | ||
}, | ||
/** | ||
* Exposed for unit testing. | ||
@@ -315,0 +331,0 @@ * @private |
@@ -42,3 +42,3 @@ /** | ||
* We have provided some sugary mixins to make the creation and | ||
* consumption of ReactLink easier; see LinkedValueMixin and LinkedStateMixin. | ||
* consumption of ReactLink easier; see LinkedValueUtils and LinkedStateMixin. | ||
*/ | ||
@@ -45,0 +45,0 @@ |
@@ -21,13 +21,15 @@ /** | ||
var DOMProperty = require("./DOMProperty"); | ||
var ReactEventEmitter = require("./ReactEventEmitter"); | ||
var ReactInstanceHandles = require("./ReactInstanceHandles"); | ||
var ReactPerf = require("./ReactPerf"); | ||
var $ = require("./$"); | ||
var containsNode = require("./containsNode"); | ||
var getReactRootElementInContainer = require("./getReactRootElementInContainer"); | ||
var invariant = require("./invariant"); | ||
var shouldUpdateReactComponent = require("./shouldUpdateReactComponent"); | ||
var SEPARATOR = ReactInstanceHandles.SEPARATOR; | ||
var ATTR_NAME = 'data-reactid'; | ||
var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME; | ||
var nodeCache = {}; | ||
@@ -49,2 +51,5 @@ | ||
// Used to store breadth-first search state in findComponentRoot. | ||
var findComponentRootReusableArray = []; | ||
/** | ||
@@ -163,3 +168,30 @@ * @param {DOMElement} container DOM element that may contain a React component. | ||
var deepestNodeSoFar = null; | ||
function findDeepestCachedAncestorImpl(ancestorID) { | ||
var ancestor = nodeCache[ancestorID]; | ||
if (ancestor && isValid(ancestor, ancestorID)) { | ||
deepestNodeSoFar = ancestor; | ||
} else { | ||
// This node isn't populated in the cache, so presumably none of its | ||
// descendants are. Break out of the loop. | ||
return false; | ||
} | ||
} | ||
/** | ||
* Return the deepest cached node whose ID is a prefix of `targetID`. | ||
*/ | ||
function findDeepestCachedAncestor(targetID) { | ||
deepestNodeSoFar = null; | ||
ReactInstanceHandles.traverseAncestors( | ||
targetID, | ||
findDeepestCachedAncestorImpl | ||
); | ||
var foundNode = deepestNodeSoFar; | ||
deepestNodeSoFar = null; | ||
return foundNode; | ||
} | ||
/** | ||
* Mounting is the process of initializing a React component by creatings its | ||
@@ -169,6 +201,9 @@ * representative DOM elements and inserting them into a supplied `container`. | ||
* | ||
* ReactMount.renderComponent(component, $('container')); | ||
* ReactMount.renderComponent( | ||
* component, | ||
* document.getElementById('container') | ||
* ); | ||
* | ||
* <div id="container"> <-- Supplied `container`. | ||
* <div data-reactid=".r[3]"> <-- Rendered reactRoot of React | ||
* <div data-reactid=".3"> <-- Rendered reactRoot of React | ||
* // ... component. | ||
@@ -181,7 +216,2 @@ * </div> | ||
var ReactMount = { | ||
/** | ||
* Safety guard to prevent accidentally rendering over the entire HTML tree. | ||
*/ | ||
allowFullPageRender: false, | ||
/** Time spent generating markup. */ | ||
@@ -212,26 +242,2 @@ totalInstantiationTime: 0, | ||
/** | ||
* Ensures that the top-level event delegation listener is set up. This will | ||
* be invoked some time before the first time any React component is rendered. | ||
* @param {DOMElement} container container we're rendering into | ||
* | ||
* @private | ||
*/ | ||
prepareEnvironmentForDOM: function(container) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
container && ( | ||
container.nodeType === ELEMENT_NODE_TYPE || | ||
container.nodeType === DOC_NODE_TYPE | ||
), | ||
'prepareEnvironmentForDOM(...): Target container is not a DOM element.' | ||
) : invariant(container && ( | ||
container.nodeType === ELEMENT_NODE_TYPE || | ||
container.nodeType === DOC_NODE_TYPE | ||
))); | ||
var doc = container.nodeType === ELEMENT_NODE_TYPE ? | ||
container.ownerDocument : | ||
container; | ||
ReactEventEmitter.ensureListening(ReactMount.useTouchEvents, doc); | ||
}, | ||
/** | ||
* Take a component that's already mounted into the DOM and replace its props | ||
@@ -263,3 +269,4 @@ * @param {ReactComponent} prevComponent component instance already in the DOM | ||
/** | ||
* Register a component into the instance map and start the events system. | ||
* Register a component into the instance map and starts scroll value | ||
* monitoring | ||
* @param {ReactComponent} nextComponent component instance to render | ||
@@ -270,4 +277,15 @@ * @param {DOMElement} container container to render into | ||
_registerComponent: function(nextComponent, container) { | ||
ReactMount.prepareEnvironmentForDOM(container); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
container && ( | ||
container.nodeType === ELEMENT_NODE_TYPE || | ||
container.nodeType === DOC_NODE_TYPE | ||
), | ||
'_registerComponent(...): Target container is not a DOM element.' | ||
) : invariant(container && ( | ||
container.nodeType === ELEMENT_NODE_TYPE || | ||
container.nodeType === DOC_NODE_TYPE | ||
))); | ||
ReactEventEmitter.ensureScrollValueMonitoring(); | ||
var reactRootID = ReactMount.registerContainer(container); | ||
@@ -285,22 +303,26 @@ instancesByReactRootID[reactRootID] = nextComponent; | ||
*/ | ||
_renderNewRootComponent: function( | ||
nextComponent, | ||
container, | ||
shouldReuseMarkup) { | ||
var reactRootID = ReactMount._registerComponent(nextComponent, container); | ||
nextComponent.mountComponentIntoNode( | ||
reactRootID, | ||
container, | ||
shouldReuseMarkup | ||
); | ||
_renderNewRootComponent: ReactPerf.measure( | ||
'ReactMount', | ||
'_renderNewRootComponent', | ||
function( | ||
nextComponent, | ||
container, | ||
shouldReuseMarkup) { | ||
var reactRootID = ReactMount._registerComponent(nextComponent, container); | ||
nextComponent.mountComponentIntoNode( | ||
reactRootID, | ||
container, | ||
shouldReuseMarkup | ||
); | ||
if ("production" !== process.env.NODE_ENV) { | ||
// Record the root element in case it later gets transplanted. | ||
rootElementsByReactRootID[reactRootID] = | ||
getReactRootElementInContainer(container); | ||
if ("production" !== process.env.NODE_ENV) { | ||
// Record the root element in case it later gets transplanted. | ||
rootElementsByReactRootID[reactRootID] = | ||
getReactRootElementInContainer(container); | ||
} | ||
return nextComponent; | ||
} | ||
), | ||
return nextComponent; | ||
}, | ||
/** | ||
@@ -319,8 +341,8 @@ * Renders a React component into the DOM in the supplied `container`. | ||
renderComponent: function(nextComponent, container, callback) { | ||
var registeredComponent = instancesByReactRootID[getReactRootID(container)]; | ||
var prevComponent = instancesByReactRootID[getReactRootID(container)]; | ||
if (registeredComponent) { | ||
if (registeredComponent.constructor === nextComponent.constructor) { | ||
if (prevComponent) { | ||
if (shouldUpdateReactComponent(prevComponent, nextComponent)) { | ||
return ReactMount._updateRootComponent( | ||
registeredComponent, | ||
prevComponent, | ||
nextComponent, | ||
@@ -339,3 +361,3 @@ container, | ||
var shouldReuseMarkup = containerHasReactMarkup && !registeredComponent; | ||
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent; | ||
@@ -347,3 +369,3 @@ var component = ReactMount._renderNewRootComponent( | ||
); | ||
callback && callback(); | ||
callback && callback.call(component); | ||
return component; | ||
@@ -375,3 +397,9 @@ }, | ||
constructAndRenderComponentByID: function(constructor, props, id) { | ||
return ReactMount.constructAndRenderComponent(constructor, props, $(id)); | ||
var domNode = document.getElementById(id); | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
domNode, | ||
'Tried to get element with id of "%s" but it is not present on the page.', | ||
id | ||
) : invariant(domNode)); | ||
return ReactMount.constructAndRenderComponent(constructor, props, domNode); | ||
}, | ||
@@ -381,3 +409,3 @@ | ||
* Registers a container node into which React components will be rendered. | ||
* This also creates the "reatRoot" ID that will be assigned to the element | ||
* This also creates the "reactRoot" ID that will be assigned to the element | ||
* rendered within. | ||
@@ -425,16 +453,2 @@ * | ||
/** | ||
* @deprecated | ||
*/ | ||
unmountAndReleaseReactRootNode: function() { | ||
if ("production" !== process.env.NODE_ENV) { | ||
console.warn( | ||
'unmountAndReleaseReactRootNode() has been renamed to ' + | ||
'unmountComponentAtNode() and will be removed in the next ' + | ||
'version of React.' | ||
); | ||
} | ||
return ReactMount.unmountComponentAtNode.apply(this, arguments); | ||
}, | ||
/** | ||
* Unmounts a component and removes it from the DOM. | ||
@@ -551,22 +565,35 @@ * | ||
/** | ||
* Finds a node with the supplied `id` inside of the supplied `ancestorNode`. | ||
* Exploits the ID naming scheme to perform the search quickly. | ||
* Finds a node with the supplied `targetID` inside of the supplied | ||
* `ancestorNode`. Exploits the ID naming scheme to perform the search | ||
* quickly. | ||
* | ||
* @param {DOMEventTarget} ancestorNode Search from this root. | ||
* @pararm {string} id ID of the DOM representation of the component. | ||
* @return {DOMEventTarget} DOM node with the supplied `id`. | ||
* @pararm {string} targetID ID of the DOM representation of the component. | ||
* @return {DOMEventTarget} DOM node with the supplied `targetID`. | ||
* @internal | ||
*/ | ||
findComponentRoot: function(ancestorNode, id) { | ||
var firstChildren = [ancestorNode.firstChild]; | ||
findComponentRoot: function(ancestorNode, targetID) { | ||
var firstChildren = findComponentRootReusableArray; | ||
var childIndex = 0; | ||
var deepestAncestor = findDeepestCachedAncestor(targetID) || ancestorNode; | ||
firstChildren[0] = deepestAncestor.firstChild; | ||
firstChildren.length = 1; | ||
while (childIndex < firstChildren.length) { | ||
var child = firstChildren[childIndex++]; | ||
var targetChild; | ||
while (child) { | ||
var childID = ReactMount.getID(child); | ||
if (childID) { | ||
if (id === childID) { | ||
return child; | ||
} else if (ReactInstanceHandles.isAncestorIDOf(childID, id)) { | ||
// Even if we find the node we're looking for, we finish looping | ||
// through its siblings to ensure they're cached so that we don't have | ||
// to revisit this node again. Otherwise, we make n^2 calls to getID | ||
// when visiting the many children of a single node in order. | ||
if (targetID === childID) { | ||
targetChild = child; | ||
} else if (ReactInstanceHandles.isAncestorIDOf(childID, targetID)) { | ||
// If we find a child whose ID is an ancestor of the given ID, | ||
@@ -578,9 +605,4 @@ // then we can be sure that we only want to search the subtree | ||
firstChildren.push(child.firstChild); | ||
break; | ||
} else { | ||
// TODO This should not be necessary if the ID hierarchy is | ||
// correct, but is occasionally necessary if the DOM has been | ||
// modified in unexpected ways. | ||
firstChildren.push(child.firstChild); | ||
} | ||
} else { | ||
@@ -594,18 +616,24 @@ // If this child had no ID, then there's a chance that it was | ||
} | ||
child = child.nextSibling; | ||
} | ||
if (targetChild) { | ||
// Emptying firstChildren/findComponentRootReusableArray is | ||
// not necessary for correctness, but it helps the GC reclaim | ||
// any nodes that were left at the end of the search. | ||
firstChildren.length = 0; | ||
return targetChild; | ||
} | ||
} | ||
if ("production" !== process.env.NODE_ENV) { | ||
console.error( | ||
'Error while invoking `findComponentRoot` with the following ' + | ||
'ancestor node:', | ||
ancestorNode | ||
); | ||
} | ||
firstChildren.length = 0; | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
false, | ||
'findComponentRoot(..., %s): Unable to find element. This probably ' + | ||
'means the DOM was unexpectedly mutated (e.g. by the browser).', | ||
id, | ||
'means the DOM was unexpectedly mutated (e.g., by the browser). ' + | ||
'Try inspecting the child nodes of the element with React ID `%s`.', | ||
targetID, | ||
ReactMount.getID(ancestorNode) | ||
@@ -620,4 +648,2 @@ ) : invariant(false)); | ||
ATTR_NAME: ATTR_NAME, | ||
getReactRootID: getReactRootID, | ||
@@ -631,7 +657,5 @@ | ||
purgeID: purgeID, | ||
injection: {} | ||
purgeID: purgeID | ||
}; | ||
module.exports = ReactMount; |
@@ -69,3 +69,3 @@ /** | ||
var callback = queue[i].callback; | ||
callback.call(component, component.getDOMNode()); | ||
callback.call(component); | ||
} | ||
@@ -72,0 +72,0 @@ queue.length = 0; |
@@ -26,17 +26,5 @@ /** | ||
var flattenChildren = require("./flattenChildren"); | ||
var shouldUpdateReactComponent = require("./shouldUpdateReactComponent"); | ||
/** | ||
* Given a `curChild` and `newChild`, determines if `curChild` should be | ||
* updated as opposed to being destroyed or replaced. | ||
* | ||
* @param {?ReactComponent} curChild | ||
* @param {?ReactComponent} newChild | ||
* @return {boolean} True if `curChild` should be updated with `newChild`. | ||
* @protected | ||
*/ | ||
function shouldUpdateChild(curChild, newChild) { | ||
return curChild && newChild && curChild.constructor === newChild.constructor; | ||
} | ||
/** | ||
* Updating children of a component may trigger recursive updates. The depth is | ||
@@ -157,3 +145,3 @@ * used to batch recursive updates to render markup more efficiently. | ||
if (updateQueue.length) { | ||
ReactComponent.DOMIDOperations.dangerouslyProcessChildrenUpdates( | ||
ReactComponent.BackendIDOperations.dangerouslyProcessChildrenUpdates( | ||
updateQueue, | ||
@@ -208,5 +196,5 @@ markupQueue | ||
var child = children[name]; | ||
if (children.hasOwnProperty(name) && child) { | ||
if (children.hasOwnProperty(name)) { | ||
// Inlined for performance, see `ReactInstanceHandles.createReactID`. | ||
var rootID = this._rootNodeID + '.' + name; | ||
var rootID = this._rootNodeID + name; | ||
var mountImage = child.mountComponent( | ||
@@ -217,3 +205,2 @@ rootID, | ||
); | ||
child._mountImage = mountImage; | ||
child._mountIndex = index; | ||
@@ -235,2 +222,3 @@ mountImages.push(mountImage); | ||
updateDepth++; | ||
var errorThrown = true; | ||
try { | ||
@@ -240,4 +228,3 @@ var prevChildren = this._renderedChildren; | ||
for (var name in prevChildren) { | ||
if (prevChildren.hasOwnProperty(name) && | ||
prevChildren[name]) { | ||
if (prevChildren.hasOwnProperty(name)) { | ||
this._unmountChildByName(prevChildren[name], name); | ||
@@ -248,9 +235,9 @@ } | ||
this.setTextContent(nextContent); | ||
} catch (error) { | ||
errorThrown = false; | ||
} finally { | ||
updateDepth--; | ||
updateDepth || clearQueue(); | ||
throw error; | ||
if (!updateDepth) { | ||
errorThrown ? clearQueue() : processQueue(); | ||
} | ||
} | ||
updateDepth--; | ||
updateDepth || processQueue(); | ||
}, | ||
@@ -267,11 +254,12 @@ | ||
updateDepth++; | ||
var errorThrown = true; | ||
try { | ||
this._updateChildren(nextNestedChildren, transaction); | ||
} catch (error) { | ||
errorThrown = false; | ||
} finally { | ||
updateDepth--; | ||
updateDepth || clearQueue(); | ||
throw error; | ||
if (!updateDepth) { | ||
errorThrown ? clearQueue() : processQueue(); | ||
} | ||
} | ||
updateDepth--; | ||
updateDepth || processQueue(); | ||
}, | ||
@@ -305,3 +293,3 @@ | ||
var nextChild = nextChildren[name]; | ||
if (shouldUpdateChild(prevChild, nextChild)) { | ||
if (shouldUpdateReactComponent(prevChild, nextChild)) { | ||
this.moveChild(prevChild, nextIndex, lastIndex); | ||
@@ -317,11 +305,7 @@ lastIndex = Math.max(prevChild._mountIndex, lastIndex); | ||
} | ||
if (nextChild) { | ||
this._mountChildByNameAtIndex( | ||
nextChild, name, nextIndex, transaction | ||
); | ||
} | ||
this._mountChildByNameAtIndex( | ||
nextChild, name, nextIndex, transaction | ||
); | ||
} | ||
if (nextChild) { | ||
nextIndex++; | ||
} | ||
nextIndex++; | ||
} | ||
@@ -331,3 +315,2 @@ // Remove children that are no longer present. | ||
if (prevChildren.hasOwnProperty(name) && | ||
prevChildren[name] && | ||
!(nextChildren && nextChildren[name])) { | ||
@@ -349,3 +332,4 @@ this._unmountChildByName(prevChildren[name], name); | ||
var renderedChild = renderedChildren[name]; | ||
if (renderedChild && renderedChild.unmountComponent) { | ||
// TODO: When is this not true? | ||
if (renderedChild.unmountComponent) { | ||
renderedChild.unmountComponent(); | ||
@@ -378,6 +362,7 @@ } | ||
* @param {ReactComponent} child Component to create. | ||
* @param {string} mountImage Markup to insert. | ||
* @protected | ||
*/ | ||
createChild: function(child) { | ||
enqueueMarkup(this._rootNodeID, child._mountImage, child._mountIndex); | ||
createChild: function(child, mountImage) { | ||
enqueueMarkup(this._rootNodeID, mountImage, child._mountIndex); | ||
}, | ||
@@ -418,3 +403,3 @@ | ||
// Inlined for performance, see `ReactInstanceHandles.createReactID`. | ||
var rootID = this._rootNodeID + '.' + name; | ||
var rootID = this._rootNodeID + name; | ||
var mountImage = child.mountComponent( | ||
@@ -425,5 +410,4 @@ rootID, | ||
); | ||
child._mountImage = mountImage; | ||
child._mountIndex = index; | ||
this.createChild(child); | ||
this.createChild(child, mountImage); | ||
this._renderedChildren = this._renderedChildren || {}; | ||
@@ -443,5 +427,5 @@ this._renderedChildren[name] = child; | ||
_unmountChildByName: function(child, name) { | ||
// TODO: When is this not true? | ||
if (ReactComponent.isValidComponent(child)) { | ||
this.removeChild(child); | ||
child._mountImage = null; | ||
child._mountIndex = null; | ||
@@ -448,0 +432,0 @@ child.unmountComponent(); |
@@ -19,2 +19,4 @@ /** | ||
"use strict"; | ||
var keyMirror = require("./keyMirror"); | ||
@@ -21,0 +23,0 @@ |
@@ -80,3 +80,7 @@ /** | ||
ReactOwner.isValidOwner(owner), | ||
'addComponentAsRefTo(...): Only a ReactOwner can have refs.' | ||
'addComponentAsRefTo(...): Only a ReactOwner can have refs. This ' + | ||
'usually means that you\'re trying to add a ref to a component that ' + | ||
'doesn\'t have an owner (that is, was not created inside of another ' + | ||
'component\'s `render` method). Try rendering this component inside of ' + | ||
'a new top-level component which will hold the ref.' | ||
) : invariant(ReactOwner.isValidOwner(owner))); | ||
@@ -98,3 +102,7 @@ owner.attachRef(ref, component); | ||
ReactOwner.isValidOwner(owner), | ||
'removeComponentAsRefFrom(...): Only a ReactOwner can have refs.' | ||
'removeComponentAsRefFrom(...): Only a ReactOwner can have refs. This ' + | ||
'usually means that you\'re trying to remove a ref to a component that ' + | ||
'doesn\'t have an owner (that is, was not created inside of another ' + | ||
'component\'s `render` method). Try rendering this component inside of ' + | ||
'a new top-level component which will hold the ref.' | ||
) : invariant(ReactOwner.isValidOwner(owner))); | ||
@@ -101,0 +109,0 @@ // Check that `component` is still the current ref because we do not want to |
@@ -22,2 +22,6 @@ /** | ||
/** | ||
* ReactPerf is a general AOP system designed to measure performance. This | ||
* module only has the hooks: see ReactDefaultPerf for the analysis tool. | ||
*/ | ||
var ReactPerf = { | ||
@@ -37,3 +41,3 @@ /** | ||
/** | ||
* Use this to wrap methods you want to measure. | ||
* Use this to wrap methods you want to measure. Zero overhead in production. | ||
* | ||
@@ -71,9 +75,2 @@ * @param {string} objName | ||
if ("production" !== process.env.NODE_ENV) { | ||
var ExecutionEnvironment = require("./ExecutionEnvironment"); | ||
var url = (ExecutionEnvironment.canUseDOM && window.location.href) || ''; | ||
ReactPerf.enableMeasure = ReactPerf.enableMeasure || | ||
(/[?&]react_perf\b/).test(url); | ||
} | ||
/** | ||
@@ -80,0 +77,0 @@ * Simply passes through the measured function, without measuring it. |
@@ -45,2 +45,4 @@ /** | ||
* Transfer strategies dictate how props are transferred by `transferPropsTo`. | ||
* NOTE: if you add any more exceptions to this list you should be sure to | ||
* update `cloneWithProps()` accordingly. | ||
*/ | ||
@@ -57,2 +59,6 @@ var TransferStrategies = { | ||
/** | ||
* Never transfer the `key` prop. | ||
*/ | ||
key: emptyFunction, | ||
/** | ||
* Never transfer the `ref` prop. | ||
@@ -78,2 +84,29 @@ */ | ||
/** | ||
* Merge two props objects using TransferStrategies. | ||
* | ||
* @param {object} oldProps original props (they take precedence) | ||
* @param {object} newProps new props to merge in | ||
* @return {object} a new object containing both sets of props merged. | ||
*/ | ||
mergeProps: function(oldProps, newProps) { | ||
var props = merge(oldProps); | ||
for (var thisKey in newProps) { | ||
if (!newProps.hasOwnProperty(thisKey)) { | ||
continue; | ||
} | ||
var transferStrategy = TransferStrategies[thisKey]; | ||
if (transferStrategy) { | ||
transferStrategy(props, thisKey, newProps[thisKey]); | ||
} else if (!props.hasOwnProperty(thisKey)) { | ||
props[thisKey] = newProps[thisKey]; | ||
} | ||
} | ||
return props; | ||
}, | ||
/** | ||
* @lends {ReactPropTransferer.prototype} | ||
@@ -98,3 +131,3 @@ */ | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
component.props.__owner__ === this, | ||
component._owner === this, | ||
'%s: You can\'t call transferPropsTo() on a component that you ' + | ||
@@ -105,22 +138,9 @@ 'don\'t own, %s. This usually means you are calling ' + | ||
component.constructor.displayName | ||
) : invariant(component.props.__owner__ === this)); | ||
) : invariant(component._owner === this)); | ||
var props = {}; | ||
for (var thatKey in component.props) { | ||
if (component.props.hasOwnProperty(thatKey)) { | ||
props[thatKey] = component.props[thatKey]; | ||
} | ||
} | ||
for (var thisKey in this.props) { | ||
if (!this.props.hasOwnProperty(thisKey)) { | ||
continue; | ||
} | ||
var transferStrategy = TransferStrategies[thisKey]; | ||
if (transferStrategy) { | ||
transferStrategy(props, thisKey, this.props[thisKey]); | ||
} else if (!props.hasOwnProperty(thisKey)) { | ||
props[thisKey] = this.props[thisKey]; | ||
} | ||
} | ||
component.props = props; | ||
component.props = ReactPropTransferer.mergeProps( | ||
component.props, | ||
this.props | ||
); | ||
return component; | ||
@@ -130,5 +150,4 @@ } | ||
} | ||
}; | ||
module.exports = ReactPropTransferer; |
@@ -21,4 +21,7 @@ /** | ||
var ReactComponent = require("./ReactComponent"); | ||
var ReactPropTypeLocationNames = require("./ReactPropTypeLocationNames"); | ||
var warning = require("./warning"); | ||
var createObjectFrom = require("./createObjectFrom"); | ||
var invariant = require("./invariant"); | ||
@@ -58,3 +61,3 @@ /** | ||
* var propValue = props[propName]; | ||
* invariant( | ||
* warning( | ||
* propValue == null || | ||
@@ -83,6 +86,14 @@ * typeof propValue === 'string' || | ||
shape: createShapeTypeChecker, | ||
oneOf: createEnumTypeChecker, | ||
oneOfType: createUnionTypeChecker, | ||
arrayOf: createArrayOfTypeChecker, | ||
instanceOf: createInstanceTypeChecker | ||
instanceOf: createInstanceTypeChecker, | ||
renderable: createRenderableTypeChecker(), | ||
component: createComponentTypeChecker(), | ||
any: createAnyTypeChecker() | ||
}; | ||
@@ -92,16 +103,61 @@ | ||
function isRenderable(propValue) { | ||
switch(typeof propValue) { | ||
case 'number': | ||
case 'string': | ||
return true; | ||
case 'object': | ||
if (Array.isArray(propValue)) { | ||
return propValue.every(isRenderable); | ||
} | ||
if (ReactComponent.isValidComponent(propValue)) { | ||
return true; | ||
} | ||
for (var k in propValue) { | ||
if (!isRenderable(propValue[k])) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
default: | ||
return false; | ||
} | ||
} | ||
// Equivalent of typeof but with special handling for arrays | ||
function getPropType(propValue) { | ||
var propType = typeof propValue; | ||
if (propType === 'object' && Array.isArray(propValue)) { | ||
return 'array'; | ||
} | ||
return propType; | ||
} | ||
function createAnyTypeChecker() { | ||
function validateAnyType( | ||
shouldWarn, propValue, propName, componentName, location | ||
) { | ||
return true; // is always valid | ||
} | ||
return createChainableTypeChecker(validateAnyType); | ||
} | ||
function createPrimitiveTypeChecker(expectedType) { | ||
function validatePrimitiveType(propValue, propName, componentName) { | ||
var propType = typeof propValue; | ||
if (propType === 'object' && Array.isArray(propValue)) { | ||
propType = 'array'; | ||
function validatePrimitiveType( | ||
shouldWarn, propValue, propName, componentName, location | ||
) { | ||
var propType = getPropType(propValue); | ||
var isValid = propType === expectedType; | ||
if (shouldWarn) { | ||
warning( | ||
isValid, | ||
'Invalid %s `%s` of type `%s` supplied to `%s`, expected `%s`.', | ||
ReactPropTypeLocationNames[location], | ||
propName, | ||
propType, | ||
componentName, | ||
expectedType | ||
); | ||
} | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
propType === expectedType, | ||
'Invalid prop `%s` of type `%s` supplied to `%s`, expected `%s`.', | ||
propName, | ||
propType, | ||
componentName, | ||
expectedType | ||
) : invariant(propType === expectedType)); | ||
return isValid; | ||
} | ||
@@ -113,10 +169,17 @@ return createChainableTypeChecker(validatePrimitiveType); | ||
var expectedEnum = createObjectFrom(expectedValues); | ||
function validateEnumType(propValue, propName, componentName) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
expectedEnum[propValue], | ||
'Invalid prop `%s` supplied to `%s`, expected one of %s.', | ||
propName, | ||
componentName, | ||
JSON.stringify(Object.keys(expectedEnum)) | ||
) : invariant(expectedEnum[propValue])); | ||
function validateEnumType( | ||
shouldWarn, propValue, propName, componentName, location | ||
) { | ||
var isValid = expectedEnum[propValue]; | ||
if (shouldWarn) { | ||
warning( | ||
isValid, | ||
'Invalid %s `%s` supplied to `%s`, expected one of %s.', | ||
ReactPropTypeLocationNames[location], | ||
propName, | ||
componentName, | ||
JSON.stringify(Object.keys(expectedEnum)) | ||
); | ||
} | ||
return isValid; | ||
} | ||
@@ -126,11 +189,47 @@ return createChainableTypeChecker(validateEnumType); | ||
function createShapeTypeChecker(shapeTypes) { | ||
function validateShapeType( | ||
shouldWarn, propValue, propName, componentName, location | ||
) { | ||
var propType = getPropType(propValue); | ||
var isValid = propType === 'object'; | ||
if (isValid) { | ||
for (var key in shapeTypes) { | ||
var checker = shapeTypes[key]; | ||
if (checker && !checker(propValue, key, componentName, location)) { | ||
return false; | ||
} | ||
} | ||
} | ||
if (shouldWarn) { | ||
warning( | ||
isValid, | ||
'Invalid %s `%s` of type `%s` supplied to `%s`, expected `object`.', | ||
ReactPropTypeLocationNames[location], | ||
propName, | ||
propType, | ||
componentName | ||
); | ||
} | ||
return isValid; | ||
} | ||
return createChainableTypeChecker(validateShapeType); | ||
} | ||
function createInstanceTypeChecker(expectedClass) { | ||
function validateInstanceType(propValue, propName, componentName) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
propValue instanceof expectedClass, | ||
'Invalid prop `%s` supplied to `%s`, expected instance of `%s`.', | ||
propName, | ||
componentName, | ||
expectedClass.name || ANONYMOUS | ||
) : invariant(propValue instanceof expectedClass)); | ||
function validateInstanceType( | ||
shouldWarn, propValue, propName, componentName, location | ||
) { | ||
var isValid = propValue instanceof expectedClass; | ||
if (shouldWarn) { | ||
warning( | ||
isValid, | ||
'Invalid %s `%s` supplied to `%s`, expected instance of `%s`.', | ||
ReactPropTypeLocationNames[location], | ||
propName, | ||
componentName, | ||
expectedClass.name || ANONYMOUS | ||
); | ||
} | ||
return isValid; | ||
} | ||
@@ -140,26 +239,128 @@ return createChainableTypeChecker(validateInstanceType); | ||
function createArrayOfTypeChecker(propTypeChecker) { | ||
function validateArrayType( | ||
shouldWarn, propValue, propName, componentName, location | ||
) { | ||
var isValid = Array.isArray(propValue); | ||
if (isValid) { | ||
for (var i = 0; i < propValue.length; i++) { | ||
if (!propTypeChecker(propValue, i, componentName, location)) { | ||
return false; | ||
} | ||
} | ||
} | ||
if (shouldWarn) { | ||
warning( | ||
isValid, | ||
'Invalid %s `%s` supplied to `%s`, expected an array.', | ||
ReactPropTypeLocationNames[location], | ||
propName, | ||
componentName | ||
); | ||
} | ||
return isValid; | ||
} | ||
return createChainableTypeChecker(validateArrayType); | ||
} | ||
function createRenderableTypeChecker() { | ||
function validateRenderableType( | ||
shouldWarn, propValue, propName, componentName, location | ||
) { | ||
var isValid = isRenderable(propValue); | ||
if (shouldWarn) { | ||
warning( | ||
isValid, | ||
'Invalid %s `%s` supplied to `%s`, expected a renderable prop.', | ||
ReactPropTypeLocationNames[location], | ||
propName, | ||
componentName | ||
); | ||
} | ||
return isValid; | ||
} | ||
return createChainableTypeChecker(validateRenderableType); | ||
} | ||
function createComponentTypeChecker() { | ||
function validateComponentType( | ||
shouldWarn, propValue, propName, componentName, location | ||
) { | ||
var isValid = ReactComponent.isValidComponent(propValue); | ||
if (shouldWarn) { | ||
warning( | ||
isValid, | ||
'Invalid %s `%s` supplied to `%s`, expected a React component.', | ||
ReactPropTypeLocationNames[location], | ||
propName, | ||
componentName | ||
); | ||
} | ||
return isValid; | ||
} | ||
return createChainableTypeChecker(validateComponentType); | ||
} | ||
function createUnionTypeChecker(arrayOfValidators) { | ||
return function(props, propName, componentName, location) { | ||
var isValid = false; | ||
for (var ii = 0; ii < arrayOfValidators.length; ii++) { | ||
var validate = arrayOfValidators[ii]; | ||
if (typeof validate.weak === 'function') { | ||
validate = validate.weak; | ||
} | ||
if (validate(props, propName, componentName, location)) { | ||
isValid = true; | ||
break; | ||
} | ||
} | ||
warning( | ||
isValid, | ||
'Invalid %s `%s` supplied to `%s`.', | ||
ReactPropTypeLocationNames[location], | ||
propName, | ||
componentName || ANONYMOUS | ||
); | ||
return isValid; | ||
}; | ||
} | ||
function createChainableTypeChecker(validate) { | ||
function createTypeChecker(isRequired) { | ||
function checkType(props, propName, componentName) { | ||
var propValue = props[propName]; | ||
if (propValue != null) { | ||
// Only validate if there is a value to check. | ||
validate(propValue, propName, componentName || ANONYMOUS); | ||
} else { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
!isRequired, | ||
'Required prop `%s` was not specified in `%s`.', | ||
function checkType( | ||
isRequired, shouldWarn, props, propName, componentName, location | ||
) { | ||
var propValue = props[propName]; | ||
if (propValue != null) { | ||
// Only validate if there is a value to check. | ||
return validate( | ||
shouldWarn, | ||
propValue, | ||
propName, | ||
componentName || ANONYMOUS, | ||
location | ||
); | ||
} else { | ||
var isValid = !isRequired; | ||
if (shouldWarn) { | ||
warning( | ||
isValid, | ||
'Required %s `%s` was not specified in `%s`.', | ||
ReactPropTypeLocationNames[location], | ||
propName, | ||
componentName || ANONYMOUS | ||
) : invariant(!isRequired)); | ||
); | ||
} | ||
return isValid; | ||
} | ||
if (!isRequired) { | ||
checkType.isRequired = createTypeChecker(true); | ||
} | ||
return checkType; | ||
} | ||
return createTypeChecker(false); | ||
var checker = checkType.bind(null, false, true); | ||
checker.weak = checkType.bind(null, false, false); | ||
checker.isRequired = checkType.bind(null, true, true); | ||
checker.weak.isRequired = checkType.bind(null, true, false); | ||
checker.isRequired.weak = checker.weak.isRequired; | ||
return checker; | ||
} | ||
module.exports = Props; |
@@ -27,2 +27,3 @@ /** | ||
var ReactMountReady = require("./ReactMountReady"); | ||
var ReactPutListenerQueue = require("./ReactPutListenerQueue"); | ||
var Transaction = require("./Transaction"); | ||
@@ -92,2 +93,12 @@ | ||
var PUT_LISTENER_QUEUEING = { | ||
initialize: function() { | ||
this.putListenerQueue.reset(); | ||
}, | ||
close: function() { | ||
this.putListenerQueue.putListeners(); | ||
} | ||
}; | ||
/** | ||
@@ -99,2 +110,3 @@ * Executed within the scope of the `Transaction` instance. Consider these as | ||
var TRANSACTION_WRAPPERS = [ | ||
PUT_LISTENER_QUEUEING, | ||
SELECTION_RESTORATION, | ||
@@ -122,2 +134,3 @@ EVENT_SUPPRESSION, | ||
this.reactMountReady = ReactMountReady.getPooled(null); | ||
this.putListenerQueue = ReactPutListenerQueue.getPooled(); | ||
} | ||
@@ -149,2 +162,6 @@ | ||
getPutListenerQueue: function() { | ||
return this.putListenerQueue; | ||
}, | ||
/** | ||
@@ -157,2 +174,5 @@ * `PooledClass` looks for this, and will invoke this before allowing this | ||
this.reactMountReady = null; | ||
ReactPutListenerQueue.release(this.putListenerQueue); | ||
this.putListenerQueue = null; | ||
} | ||
@@ -159,0 +179,0 @@ }; |
@@ -30,8 +30,5 @@ /** | ||
* @param {ReactComponent} component | ||
* @param {function} callback | ||
* @return {string} the markup | ||
*/ | ||
function renderComponentToString(component, callback) { | ||
// We use a callback API to keep the API async in case in the future we ever | ||
// need it, but in reality this is a synchronous operation. | ||
function renderComponentToString(component) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
@@ -43,5 +40,6 @@ ReactComponent.isValidComponent(component), | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
typeof callback === 'function', | ||
'renderComponentToString(): You must pass a function as a callback.' | ||
) : invariant(typeof callback === 'function')); | ||
!(arguments.length === 2 && typeof arguments[1] === 'function'), | ||
'renderComponentToString(): This function became synchronous and now ' + | ||
'returns the generated markup. Please remove the second parameter.' | ||
) : invariant(!(arguments.length === 2 && typeof arguments[1] === 'function'))); | ||
@@ -52,6 +50,5 @@ var id = ReactInstanceHandles.createReactRootID(); | ||
try { | ||
transaction.perform(function() { | ||
return transaction.perform(function() { | ||
var markup = component.mountComponent(id, transaction, 0); | ||
markup = ReactMarkupChecksum.addChecksumToMarkup(markup); | ||
callback(markup); | ||
return ReactMarkupChecksum.addChecksumToMarkup(markup); | ||
}, null); | ||
@@ -58,0 +55,0 @@ } finally { |
@@ -22,4 +22,4 @@ /** | ||
var DOMPropertyOperations = require("./DOMPropertyOperations"); | ||
var ReactComponent = require("./ReactComponent"); | ||
var ReactMount = require("./ReactMount"); | ||
@@ -69,3 +69,3 @@ var escapeTextForBrowser = require("./escapeTextForBrowser"); | ||
return ( | ||
'<span ' + ReactMount.ATTR_NAME + '="' + escapeTextForBrowser(rootID) + '">' + | ||
'<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' + | ||
escapeTextForBrowser(this.props.text) + | ||
@@ -87,3 +87,3 @@ '</span>' | ||
this.props.text = nextProps.text; | ||
ReactComponent.DOMIDOperations.updateTextContentByID( | ||
ReactComponent.BackendIDOperations.updateTextContentByID( | ||
this._rootNodeID, | ||
@@ -97,2 +97,7 @@ nextProps.text | ||
// Expose the constructor on itself and the prototype for consistency with other | ||
// descriptors. | ||
ReactTextComponent.type = ReactTextComponent; | ||
ReactTextComponent.prototype.type = ReactTextComponent; | ||
module.exports = ReactTextComponent; |
@@ -22,13 +22,13 @@ /** | ||
var React = require("./React"); | ||
var ReactTransitionableChild = require("./ReactTransitionableChild"); | ||
var ReactTransitionKeySet = require("./ReactTransitionKeySet"); | ||
var ReactTransitionChildMapping = require("./ReactTransitionChildMapping"); | ||
var cloneWithProps = require("./cloneWithProps"); | ||
var emptyFunction = require("./emptyFunction"); | ||
var merge = require("./merge"); | ||
var ReactTransitionGroup = React.createClass({ | ||
propTypes: { | ||
transitionName: React.PropTypes.string.isRequired, | ||
transitionEnter: React.PropTypes.bool, | ||
transitionLeave: React.PropTypes.bool, | ||
onTransition: React.PropTypes.func, | ||
component: React.PropTypes.func | ||
component: React.PropTypes.func, | ||
childFactory: React.PropTypes.func | ||
}, | ||
@@ -38,74 +38,149 @@ | ||
return { | ||
transitionEnter: true, | ||
transitionLeave: true, | ||
component: React.DOM.span | ||
component: React.DOM.span, | ||
childFactory: emptyFunction.thatReturnsArgument | ||
}; | ||
}, | ||
getInitialState: function() { | ||
return { | ||
children: ReactTransitionChildMapping.getChildMapping(this.props.children) | ||
}; | ||
}, | ||
componentWillReceiveProps: function(nextProps) { | ||
var nextChildMapping = ReactTransitionChildMapping.getChildMapping( | ||
nextProps.children | ||
); | ||
var prevChildMapping = this.state.children; | ||
this.setState({ | ||
children: ReactTransitionChildMapping.mergeChildMappings( | ||
prevChildMapping, | ||
nextChildMapping | ||
) | ||
}); | ||
var key; | ||
for (key in nextChildMapping) { | ||
if (!prevChildMapping.hasOwnProperty(key) && | ||
!this.currentlyTransitioningKeys[key]) { | ||
this.keysToEnter.push(key); | ||
} | ||
} | ||
for (key in prevChildMapping) { | ||
if (!nextChildMapping.hasOwnProperty(key) && | ||
!this.currentlyTransitioningKeys[key]) { | ||
this.keysToLeave.push(key); | ||
} | ||
} | ||
// If we want to someday check for reordering, we could do it here. | ||
}, | ||
componentWillMount: function() { | ||
// _transitionGroupCurrentKeys stores the union of previous *and* next keys. | ||
// If this were a component we'd store it as state, however, since this must | ||
// be a mixin, we need to keep the result of the union of keys in each | ||
// call to animateChildren() which happens in render(), so we can't | ||
// call setState() in there. | ||
this._transitionGroupCurrentKeys = {}; | ||
this.currentlyTransitioningKeys = {}; | ||
this.keysToEnter = []; | ||
this.keysToLeave = []; | ||
}, | ||
componentDidUpdate: function() { | ||
if (this.props.onTransition) { | ||
this.props.onTransition(); | ||
var keysToEnter = this.keysToEnter; | ||
this.keysToEnter = []; | ||
keysToEnter.forEach(this.performEnter); | ||
var keysToLeave = this.keysToLeave; | ||
this.keysToLeave = []; | ||
keysToLeave.forEach(this.performLeave); | ||
}, | ||
performEnter: function(key) { | ||
this.currentlyTransitioningKeys[key] = true; | ||
var component = this.refs[key]; | ||
if (component.componentWillEnter) { | ||
component.componentWillEnter( | ||
this._handleDoneEntering.bind(this, key) | ||
); | ||
} else { | ||
this._handleDoneEntering(key); | ||
} | ||
}, | ||
/** | ||
* Render some children in a transitionable way. | ||
*/ | ||
renderTransitionableChildren: function(sourceChildren) { | ||
var children = {}; | ||
var childMapping = ReactTransitionKeySet.getChildMapping(sourceChildren); | ||
_handleDoneEntering: function(key) { | ||
var component = this.refs[key]; | ||
if (component.componentDidEnter) { | ||
component.componentDidEnter(); | ||
} | ||
var currentKeys = ReactTransitionKeySet.mergeKeySets( | ||
this._transitionGroupCurrentKeys, | ||
ReactTransitionKeySet.getKeySet(sourceChildren) | ||
delete this.currentlyTransitioningKeys[key]; | ||
var currentChildMapping = ReactTransitionChildMapping.getChildMapping( | ||
this.props.children | ||
); | ||
for (var key in currentKeys) { | ||
// Here is how we keep the nodes in the DOM. ReactTransitionableChild | ||
// knows how to hold onto its child if it changes to undefined. Here, we | ||
// may look up an old key in the new children, and it may switch to | ||
// undefined. React's reconciler will keep the ReactTransitionableChild | ||
// instance alive such that we can animate it. | ||
if (childMapping[key] || this.props.transitionLeave) { | ||
children[key] = ReactTransitionableChild({ | ||
name: this.props.transitionName, | ||
enter: this.props.transitionEnter, | ||
onDoneLeaving: this._handleDoneLeaving.bind(this, key) | ||
}, childMapping[key]); | ||
} | ||
if (!currentChildMapping.hasOwnProperty(key)) { | ||
// This was removed before it had fully entered. Remove it. | ||
this.performLeave(key); | ||
} | ||
}, | ||
this._transitionGroupCurrentKeys = currentKeys; | ||
performLeave: function(key) { | ||
this.currentlyTransitioningKeys[key] = true; | ||
return children; | ||
var component = this.refs[key]; | ||
if (component.componentWillLeave) { | ||
component.componentWillLeave(this._handleDoneLeaving.bind(this, key)); | ||
} else { | ||
// Note that this is somewhat dangerous b/c it calls setState() | ||
// again, effectively mutating the component before all the work | ||
// is done. | ||
this._handleDoneLeaving(key); | ||
} | ||
}, | ||
_handleDoneLeaving: function(key) { | ||
// When the leave animation finishes, we should blow away the actual DOM | ||
// node. | ||
delete this._transitionGroupCurrentKeys[key]; | ||
this.forceUpdate(); | ||
var component = this.refs[key]; | ||
if (component.componentDidLeave) { | ||
component.componentDidLeave(); | ||
} | ||
delete this.currentlyTransitioningKeys[key]; | ||
var currentChildMapping = ReactTransitionChildMapping.getChildMapping( | ||
this.props.children | ||
); | ||
if (currentChildMapping.hasOwnProperty(key)) { | ||
// This entered again before it fully left. Add it again. | ||
this.performEnter(key); | ||
} else { | ||
var newChildren = merge(this.state.children); | ||
delete newChildren[key]; | ||
this.setState({children: newChildren}); | ||
} | ||
}, | ||
render: function() { | ||
return this.transferPropsTo( | ||
this.props.component( | ||
{ | ||
transitionName: null, | ||
transitionEnter: null, | ||
transitionLeave: null, | ||
component: null | ||
}, | ||
this.renderTransitionableChildren(this.props.children) | ||
) | ||
); | ||
// TODO: we could get rid of the need for the wrapper node | ||
// by cloning a single child | ||
var childrenToRender = {}; | ||
for (var key in this.state.children) { | ||
var child = this.state.children[key]; | ||
if (child) { | ||
// You may need to apply reactive updates to a child as it is leaving. | ||
// The normal React way to do it won't work since the child will have | ||
// already been removed. In case you need this behavior you can provide | ||
// a childFactory function to wrap every child, even the ones that are | ||
// leaving. | ||
childrenToRender[key] = cloneWithProps( | ||
this.props.childFactory(child), | ||
{ref: key} | ||
); | ||
} | ||
} | ||
return this.transferPropsTo(this.props.component(null, childrenToRender)); | ||
} | ||
@@ -112,0 +187,0 @@ }); |
@@ -21,2 +21,4 @@ /** | ||
var ReactPerf = require("./ReactPerf"); | ||
var invariant = require("./invariant"); | ||
@@ -79,13 +81,14 @@ | ||
function flushBatchedUpdates() { | ||
// Run these in separate functions so the JIT can optimize | ||
try { | ||
runBatchedUpdates(); | ||
} catch (e) { | ||
// IE 8 requires catch to use finally. | ||
throw e; | ||
} finally { | ||
clearDirtyComponents(); | ||
var flushBatchedUpdates = ReactPerf.measure( | ||
'ReactUpdates', | ||
'flushBatchedUpdates', | ||
function() { | ||
// Run these in separate functions so the JIT can optimize | ||
try { | ||
runBatchedUpdates(); | ||
} finally { | ||
clearDirtyComponents(); | ||
} | ||
} | ||
} | ||
); | ||
@@ -107,3 +110,3 @@ /** | ||
component.performUpdateIfNecessary(); | ||
callback && callback(); | ||
callback && callback.call(component); | ||
return; | ||
@@ -110,0 +113,0 @@ } |
@@ -30,10 +30,15 @@ /** | ||
var React = require("./React"); | ||
var ReactCSSTransitionGroup = require("./ReactCSSTransitionGroup"); | ||
var ReactTransitionGroup = require("./ReactTransitionGroup"); | ||
var cx = require("./cx"); | ||
var cloneWithProps = require("./cloneWithProps"); | ||
React.addons = { | ||
LinkedStateMixin: LinkedStateMixin, | ||
CSSTransitionGroup: ReactCSSTransitionGroup, | ||
TransitionGroup: ReactTransitionGroup, | ||
classSet: cx, | ||
LinkedStateMixin: LinkedStateMixin, | ||
TransitionGroup: ReactTransitionGroup | ||
cloneWithProps: cloneWithProps | ||
}; | ||
@@ -40,0 +45,0 @@ |
@@ -22,5 +22,3 @@ /** | ||
var EventConstants = require("./EventConstants"); | ||
var EventPluginHub = require("./EventPluginHub"); | ||
var EventPropagators = require("./EventPropagators"); | ||
var ExecutionEnvironment = require("./ExecutionEnvironment"); | ||
var ReactInputSelection = require("./ReactInputSelection"); | ||
@@ -41,15 +39,17 @@ var SyntheticEvent = require("./SyntheticEvent"); | ||
captured: keyOf({onSelectCapture: null}) | ||
} | ||
}, | ||
dependencies: [ | ||
topLevelTypes.topBlur, | ||
topLevelTypes.topContextMenu, | ||
topLevelTypes.topFocus, | ||
topLevelTypes.topKeyDown, | ||
topLevelTypes.topMouseDown, | ||
topLevelTypes.topMouseUp, | ||
topLevelTypes.topSelectionChange | ||
] | ||
} | ||
}; | ||
var useSelectionChange = false; | ||
if (ExecutionEnvironment.canUseDOM) { | ||
useSelectionChange = 'onselectionchange' in document; | ||
} | ||
var activeElement = null; | ||
var activeElementID = null; | ||
var activeNativeEvent = null; | ||
var lastSelection = null; | ||
@@ -101,4 +101,8 @@ var mouseDown = false; | ||
// Ensure we have the right element, and that the user is not dragging a | ||
// selection (this matches native `select` event behavior). | ||
if (mouseDown || activeElement != getActiveElement()) { | ||
// selection (this matches native `select` event behavior). In HTML5, select | ||
// fires only on input and textarea thus if there's no focused element we | ||
// won't dispatch. | ||
if (mouseDown || | ||
activeElement == null || | ||
activeElement != getActiveElement()) { | ||
return; | ||
@@ -128,20 +132,2 @@ } | ||
/** | ||
* Handle deferred event. And manually dispatch synthetic events. | ||
*/ | ||
function dispatchDeferredSelectEvent() { | ||
if (!activeNativeEvent) { | ||
return; | ||
} | ||
var syntheticEvent = constructSelectEvent(activeNativeEvent); | ||
activeNativeEvent = null; | ||
// Enqueue and process the abstract event manually. | ||
if (syntheticEvent) { | ||
EventPluginHub.enqueueEvents(syntheticEvent); | ||
EventPluginHub.processEventQueue(); | ||
} | ||
} | ||
/** | ||
* This plugin creates an `onSelect` event that normalizes select events | ||
@@ -206,13 +192,10 @@ * across form elements. | ||
// sometimes when it hasn't). | ||
// Firefox doesn't support selectionchange, so check selection status | ||
// after each key entry. The selection changes after keydown and before | ||
// keyup, but we check on keydown as well in the case of holding down a | ||
// key, when multiple keydown events are fired but only one keyup is. | ||
case topLevelTypes.topSelectionChange: | ||
case topLevelTypes.topKeyDown: | ||
case topLevelTypes.topKeyUp: | ||
return constructSelectEvent(nativeEvent); | ||
// Firefox doesn't support selectionchange, so check selection status | ||
// after each key entry. | ||
case topLevelTypes.topKeyDown: | ||
if (!useSelectionChange) { | ||
activeNativeEvent = nativeEvent; | ||
setTimeout(dispatchDeferredSelectEvent, 0); | ||
} | ||
break; | ||
} | ||
@@ -219,0 +202,0 @@ } |
@@ -22,2 +22,3 @@ /** | ||
var EventConstants = require("./EventConstants"); | ||
var EventPluginUtils = require("./EventPluginUtils"); | ||
var EventPropagators = require("./EventPropagators"); | ||
@@ -29,2 +30,3 @@ var SyntheticClipboardEvent = require("./SyntheticClipboardEvent"); | ||
var SyntheticMouseEvent = require("./SyntheticMouseEvent"); | ||
var SyntheticDragEvent = require("./SyntheticDragEvent"); | ||
var SyntheticTouchEvent = require("./SyntheticTouchEvent"); | ||
@@ -154,2 +156,14 @@ var SyntheticUIEvent = require("./SyntheticUIEvent"); | ||
}, | ||
load: { | ||
phasedRegistrationNames: { | ||
bubbled: keyOf({onLoad: true}), | ||
captured: keyOf({onLoadCapture: true}) | ||
} | ||
}, | ||
error: { | ||
phasedRegistrationNames: { | ||
bubbled: keyOf({onError: true}), | ||
captured: keyOf({onErrorCapture: true}) | ||
} | ||
}, | ||
// Note: We do not allow listening to mouseOver events. Instead, use the | ||
@@ -169,2 +183,14 @@ // onMouseEnter/onMouseLeave created by `EnterLeaveEventPlugin`. | ||
}, | ||
mouseOut: { | ||
phasedRegistrationNames: { | ||
bubbled: keyOf({onMouseOut: true}), | ||
captured: keyOf({onMouseOutCapture: true}) | ||
} | ||
}, | ||
mouseOver: { | ||
phasedRegistrationNames: { | ||
bubbled: keyOf({onMouseOver: true}), | ||
captured: keyOf({onMouseOverCapture: true}) | ||
} | ||
}, | ||
mouseUp: { | ||
@@ -182,2 +208,8 @@ phasedRegistrationNames: { | ||
}, | ||
reset: { | ||
phasedRegistrationNames: { | ||
bubbled: keyOf({onReset: true}), | ||
captured: keyOf({onResetCapture: true}) | ||
} | ||
}, | ||
scroll: { | ||
@@ -242,2 +274,3 @@ phasedRegistrationNames: { | ||
topDrop: eventTypes.drop, | ||
topError: eventTypes.error, | ||
topFocus: eventTypes.focus, | ||
@@ -248,6 +281,10 @@ topInput: eventTypes.input, | ||
topKeyUp: eventTypes.keyUp, | ||
topLoad: eventTypes.load, | ||
topMouseDown: eventTypes.mouseDown, | ||
topMouseMove: eventTypes.mouseMove, | ||
topMouseOut: eventTypes.mouseOut, | ||
topMouseOver: eventTypes.mouseOver, | ||
topMouseUp: eventTypes.mouseUp, | ||
topPaste: eventTypes.paste, | ||
topReset: eventTypes.reset, | ||
topScroll: eventTypes.scroll, | ||
@@ -262,2 +299,6 @@ topSubmit: eventTypes.submit, | ||
for (var topLevelType in topLevelEventsToDispatchConfig) { | ||
topLevelEventsToDispatchConfig[topLevelType].dependencies = [topLevelType]; | ||
} | ||
var SimpleEventPlugin = { | ||
@@ -276,3 +317,3 @@ | ||
executeDispatch: function(event, listener, domID) { | ||
var returnValue = listener(event, domID); | ||
var returnValue = EventPluginUtils.executeDispatch(event, listener, domID); | ||
if (returnValue === false) { | ||
@@ -302,4 +343,7 @@ event.stopPropagation(); | ||
var EventConstructor; | ||
switch(topLevelType) { | ||
switch (topLevelType) { | ||
case topLevelTypes.topInput: | ||
case topLevelTypes.topLoad: | ||
case topLevelTypes.topError: | ||
case topLevelTypes.topReset: | ||
case topLevelTypes.topSubmit: | ||
@@ -328,2 +372,9 @@ // HTML Events | ||
case topLevelTypes.topDoubleClick: | ||
case topLevelTypes.topMouseDown: | ||
case topLevelTypes.topMouseMove: | ||
case topLevelTypes.topMouseOut: | ||
case topLevelTypes.topMouseOver: | ||
case topLevelTypes.topMouseUp: | ||
EventConstructor = SyntheticMouseEvent; | ||
break; | ||
case topLevelTypes.topDrag: | ||
@@ -337,6 +388,3 @@ case topLevelTypes.topDragEnd: | ||
case topLevelTypes.topDrop: | ||
case topLevelTypes.topMouseDown: | ||
case topLevelTypes.topMouseMove: | ||
case topLevelTypes.topMouseUp: | ||
EventConstructor = SyntheticMouseEvent; | ||
EventConstructor = SyntheticDragEvent; | ||
break; | ||
@@ -343,0 +391,0 @@ case topLevelTypes.topTouchCancel: |
@@ -29,3 +29,9 @@ /** | ||
var ClipboardEventInterface = { | ||
clipboardData: null | ||
clipboardData: function(event) { | ||
return ( | ||
'clipboardData' in event ? | ||
event.clipboardData : | ||
window.clipboardData | ||
); | ||
} | ||
}; | ||
@@ -32,0 +38,0 @@ |
@@ -36,3 +36,4 @@ /** | ||
target: getEventTarget, | ||
currentTarget: null, | ||
// currentTarget is set when dispatching; no use in copying it here | ||
currentTarget: emptyFunction.thatReturnsNull, | ||
eventPhase: null, | ||
@@ -39,0 +40,0 @@ bubbles: null, |
@@ -24,2 +24,4 @@ /** | ||
var getEventKey = require("./getEventKey"); | ||
/** | ||
@@ -30,4 +32,3 @@ * @interface KeyboardEvent | ||
var KeyboardEventInterface = { | ||
'char': null, | ||
key: null, | ||
key: getEventKey, | ||
location: null, | ||
@@ -41,2 +42,3 @@ ctrlKey: null, | ||
// Legacy Interface | ||
'char': null, | ||
charCode: null, | ||
@@ -43,0 +45,0 @@ keyCode: null, |
@@ -30,3 +30,2 @@ /** | ||
deltaX: function(event) { | ||
// NOTE: IE<9 does not support x-axis delta. | ||
return ( | ||
@@ -40,11 +39,15 @@ 'deltaX' in event ? event.deltaX : | ||
return ( | ||
// Normalize (up is positive). | ||
'deltaY' in event ? -event.deltaY : | ||
// Fallback to `wheelDeltaY` for Webkit. | ||
'wheelDeltaY' in event ? event.wheelDeltaY : | ||
// Fallback to `wheelDelta` for IE<9. | ||
'wheelDelta' in event ? event.wheelDelta : 0 | ||
'deltaY' in event ? event.deltaY : | ||
// Fallback to `wheelDeltaY` for Webkit and normalize (down is positive). | ||
'wheelDeltaY' in event ? -event.wheelDeltaY : | ||
// Fallback to `wheelDelta` for IE<9 and normalize (down is positive). | ||
'wheelDelta' in event ? -event.wheelDelta : 0 | ||
); | ||
}, | ||
deltaZ: null, | ||
// Browsers without "deltaMode" is reporting in raw wheel delta where one | ||
// notch on the scroll is always +/- 120, roughly equivalent to pixels. | ||
// A good approximation of DOM_DELTA_LINE (1) is 5% of viewport size or | ||
// ~40 pixels, for DOM_DELTA_SCREEN (2) it is 87.5% of viewport size. | ||
deltaMode: null | ||
@@ -51,0 +54,0 @@ }; |
@@ -30,3 +30,3 @@ /** | ||
* automatic invariant for you - the invariant that any transaction instance | ||
* should not be ran while it is already being ran. You would typically create a | ||
* should not be run while it is already being run. You would typically create a | ||
* single instance of a `Transaction` for reuse multiple times, that potentially | ||
@@ -71,3 +71,3 @@ * is used to wrap several different methods. Wrappers are extremely simple - | ||
* reconciliation takes place in a worker thread. | ||
* - Invoking any collected `componentDidRender` callbacks after rendering new | ||
* - Invoking any collected `componentDidUpdate` callbacks after rendering new | ||
* content. | ||
@@ -151,10 +151,14 @@ * - (Future use case): Wrapping particular flushes of the `ReactWorker` queue | ||
var memberStart = Date.now(); | ||
var errorToThrow = null; | ||
var errorThrown; | ||
var ret; | ||
try { | ||
this.initializeAll(); | ||
this._isInTransaction = true; | ||
// Catching errors makes debugging more difficult, so we start with | ||
// errorThrown set to true before setting it to false after calling | ||
// close -- if it's still set to true in the finally block, it means | ||
// one of these calls threw. | ||
errorThrown = true; | ||
this.initializeAll(0); | ||
ret = method.call(scope, a, b, c, d, e, f); | ||
} catch (error) { | ||
// IE8 requires `catch` in order to use `finally`. | ||
errorToThrow = error; | ||
errorThrown = false; | ||
} finally { | ||
@@ -164,31 +168,36 @@ var memberEnd = Date.now(); | ||
try { | ||
this.closeAll(); | ||
} catch (closeError) { | ||
// If `method` throws, prefer to show that stack trace over any thrown | ||
// by invoking `closeAll`. | ||
errorToThrow = errorToThrow || closeError; | ||
if (errorThrown) { | ||
// If `method` throws, prefer to show that stack trace over any thrown | ||
// by invoking `closeAll`. | ||
try { | ||
this.closeAll(0); | ||
} catch (err) { | ||
} | ||
} else { | ||
// Since `method` didn't throw, we don't want to silence the exception | ||
// here. | ||
this.closeAll(0); | ||
} | ||
} finally { | ||
this._isInTransaction = false; | ||
} | ||
} | ||
if (errorToThrow) { | ||
throw errorToThrow; | ||
} | ||
return ret; | ||
}, | ||
initializeAll: function() { | ||
this._isInTransaction = true; | ||
initializeAll: function(startIndex) { | ||
var transactionWrappers = this.transactionWrappers; | ||
var wrapperInitTimes = this.timingMetrics.wrapperInitTimes; | ||
var errorToThrow = null; | ||
for (var i = 0; i < transactionWrappers.length; i++) { | ||
for (var i = startIndex; i < transactionWrappers.length; i++) { | ||
var initStart = Date.now(); | ||
var wrapper = transactionWrappers[i]; | ||
try { | ||
// Catching errors makes debugging more difficult, so we start with the | ||
// OBSERVED_ERROR state before overwriting it with the real return value | ||
// of initialize -- if it's still set to OBSERVED_ERROR in the finally | ||
// block, it means wrapper.initialize threw. | ||
this.wrapperInitData[i] = Transaction.OBSERVED_ERROR; | ||
this.wrapperInitData[i] = wrapper.initialize ? | ||
wrapper.initialize.call(this) : | ||
null; | ||
} catch (initError) { | ||
// Prefer to show the stack trace of the first error. | ||
errorToThrow = errorToThrow || initError; | ||
this.wrapperInitData[i] = Transaction.OBSERVED_ERROR; | ||
} finally { | ||
@@ -198,7 +207,14 @@ var curInitTime = wrapperInitTimes[i]; | ||
wrapperInitTimes[i] = (curInitTime || 0) + (initEnd - initStart); | ||
if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) { | ||
// The initializer for wrapper i threw an error; initialize the | ||
// remaining wrappers but silence any exceptions from them to ensure | ||
// that the first error is the one to bubble up. | ||
try { | ||
this.initializeAll(i + 1); | ||
} catch (err) { | ||
} | ||
} | ||
} | ||
} | ||
if (errorToThrow) { | ||
throw errorToThrow; | ||
} | ||
}, | ||
@@ -212,3 +228,3 @@ | ||
*/ | ||
closeAll: function() { | ||
closeAll: function(startIndex) { | ||
("production" !== process.env.NODE_ENV ? invariant( | ||
@@ -220,14 +236,17 @@ this.isInTransaction(), | ||
var wrapperCloseTimes = this.timingMetrics.wrapperCloseTimes; | ||
var errorToThrow = null; | ||
for (var i = 0; i < transactionWrappers.length; i++) { | ||
for (var i = startIndex; i < transactionWrappers.length; i++) { | ||
var wrapper = transactionWrappers[i]; | ||
var closeStart = Date.now(); | ||
var initData = this.wrapperInitData[i]; | ||
var errorThrown; | ||
try { | ||
// Catching errors makes debugging more difficult, so we start with | ||
// errorThrown set to true before setting it to false after calling | ||
// close -- if it's still set to true in the finally block, it means | ||
// wrapper.close threw. | ||
errorThrown = true; | ||
if (initData !== Transaction.OBSERVED_ERROR) { | ||
wrapper.close && wrapper.close.call(this, initData); | ||
} | ||
} catch (closeError) { | ||
// Prefer to show the stack trace of the first error. | ||
errorToThrow = errorToThrow || closeError; | ||
errorThrown = false; | ||
} finally { | ||
@@ -237,9 +256,15 @@ var closeEnd = Date.now(); | ||
wrapperCloseTimes[i] = (curCloseTime || 0) + (closeEnd - closeStart); | ||
if (errorThrown) { | ||
// The closer for wrapper i threw an error; close the remaining | ||
// wrappers but silence any exceptions from them to ensure that the | ||
// first error is the one to bubble up. | ||
try { | ||
this.closeAll(i + 1); | ||
} catch (e) { | ||
} | ||
} | ||
} | ||
} | ||
this.wrapperInitData.length = 0; | ||
this._isInTransaction = false; | ||
if (errorToThrow) { | ||
throw errorToThrow; | ||
} | ||
} | ||
@@ -246,0 +271,0 @@ }; |
@@ -21,3 +21,3 @@ /** | ||
var ReactComponent = require("./ReactComponent"); | ||
var ReactInstanceHandles = require("./ReactInstanceHandles"); | ||
var ReactTextComponent = require("./ReactTextComponent"); | ||
@@ -27,2 +27,5 @@ | ||
var SEPARATOR = ReactInstanceHandles.SEPARATOR; | ||
var SUBSEPARATOR = ':'; | ||
/** | ||
@@ -36,3 +39,55 @@ * TODO: Test that: | ||
var userProvidedKeyEscaperLookup = { | ||
'=': '=0', | ||
'.': '=1', | ||
':': '=2' | ||
}; | ||
var userProvidedKeyEscapeRegex = /[=.:]/g; | ||
function userProvidedKeyEscaper(match) { | ||
return userProvidedKeyEscaperLookup[match]; | ||
} | ||
/** | ||
* Generate a key string that identifies a component within a set. | ||
* | ||
* @param {*} component A component that could contain a manual key. | ||
* @param {number} index Index that is used if a manual key is not provided. | ||
* @return {string} | ||
*/ | ||
function getComponentKey(component, index) { | ||
if (component && component.props && component.props.key != null) { | ||
// Explicit key | ||
return wrapUserProvidedKey(component.props.key); | ||
} | ||
// Implicit key determined by the index in the set | ||
return index.toString(36); | ||
} | ||
/** | ||
* Escape a component key so that it is safe to use in a reactid. | ||
* | ||
* @param {*} key Component key to be escaped. | ||
* @return {string} An escaped string. | ||
*/ | ||
function escapeUserProvidedKey(text) { | ||
return ('' + text).replace( | ||
userProvidedKeyEscapeRegex, | ||
userProvidedKeyEscaper | ||
); | ||
} | ||
/** | ||
* Wrap a `key` value explicitly provided by the user to distinguish it from | ||
* implicitly-generated keys generated by a component's index in its parent. | ||
* | ||
* @param {string} key Value of a user-provided `key` attribute | ||
* @return {string} | ||
*/ | ||
function wrapUserProvidedKey(key) { | ||
return '$' + escapeUserProvidedKey(key); | ||
} | ||
/** | ||
* @param {?*} children Children tree container. | ||
@@ -52,3 +107,7 @@ * @param {!string} nameSoFar Name of the key path so far. | ||
var child = children[i]; | ||
var nextName = nameSoFar + ReactComponent.getKey(child, i); | ||
var nextName = ( | ||
nameSoFar + | ||
(nameSoFar ? SUBSEPARATOR : SEPARATOR) + | ||
getComponentKey(child, i) | ||
); | ||
var nextIndex = indexSoFar + subtreeCount; | ||
@@ -68,6 +127,5 @@ subtreeCount += traverseAllChildrenImpl( | ||
// so that it's consistent if the number of children grows | ||
var storageName = isOnlyChild ? | ||
ReactComponent.getKey(children, 0): | ||
nameSoFar; | ||
if (children === null || children === undefined || type === 'boolean') { | ||
var storageName = | ||
isOnlyChild ? SEPARATOR + getComponentKey(children, 0) : nameSoFar; | ||
if (children == null || type === 'boolean') { | ||
// All of the above are perceived as null. | ||
@@ -90,3 +148,7 @@ callback(traverseContext, null, storageName, indexSoFar); | ||
children[key], | ||
nameSoFar + '{' + key + '}', | ||
( | ||
nameSoFar + (nameSoFar ? SUBSEPARATOR : SEPARATOR) + | ||
wrapUserProvidedKey(key) + SUBSEPARATOR + | ||
getComponentKey(children[key], 0) | ||
), | ||
indexSoFar + subtreeCount, | ||
@@ -93,0 +155,0 @@ callback, |
{ | ||
"name": "react", | ||
"version": "0.8.0", | ||
"version": "0.9.0-rc1", | ||
"keywords": [ | ||
@@ -19,3 +19,2 @@ "react" | ||
"react.js", | ||
"ReactJSErrors.js", | ||
"lib/" | ||
@@ -32,3 +31,3 @@ ], | ||
"peerDependencies": { | ||
"envify": "~0.2.0" | ||
"envify": "~1.0.1" | ||
}, | ||
@@ -35,0 +34,0 @@ "browserify": { |
module.exports = require('./lib/React'); | ||
if ('production' !== process.env.NODE_ENV) { | ||
module.exports = require('./ReactJSErrors').wrap(module.exports); | ||
} |
@@ -8,14 +8,6 @@ # react | ||
## The `react` npm package has recently changed! | ||
If you're looking for jeffbski's [React.js](https://github.com/jeffbski/react) project, it's now in `npm` as `autoflow` rather than `react`. | ||
## Example Usage | ||
```js | ||
// Previously, you might access React with react-tools. | ||
var React = require('react-tools').React; | ||
// Now you can access React directly with react-core. | ||
var React = require('react'); | ||
@@ -22,0 +14,0 @@ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
546201
145
15649
0
18
157