mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge branch 'textarea-update-value' of git://github.com/spicyj/react
Conflicts: src/core/ReactDefaultInjection.js
This commit is contained in:
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 $
|
||||
*/
|
||||
|
||||
var ge = require("./ge");
|
||||
|
||||
/**
|
||||
* Find a node by ID.
|
||||
*
|
||||
* If your application code depends on the existence of the element, use $,
|
||||
* which will throw if the element doesn't exist.
|
||||
*
|
||||
* If you're not sure whether or not the element exists, use ge instead, and
|
||||
* manually check for the element's existence in your application code.
|
||||
*/
|
||||
function $(arg) {
|
||||
var element = ge(arg);
|
||||
if (!element) {
|
||||
if (typeof arg == 'undefined') {
|
||||
arg = 'undefined';
|
||||
} else if (arg === null) {
|
||||
arg = 'null';
|
||||
}
|
||||
throw new Error(
|
||||
'Tried to get element "' + arg.toString() + '" but it is not present ' +
|
||||
'on the page.'
|
||||
);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
module.exports = $;
|
||||
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* 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 AbstractEvent
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var BrowserEnv = require("./BrowserEnv");
|
||||
var PooledClass = require("./PooledClass");
|
||||
var TouchEventUtils = require("./TouchEventUtils");
|
||||
|
||||
var throwIf = require("./throwIf");
|
||||
|
||||
|
||||
// Only accessed in __DEV__
|
||||
var CLONE_TYPE_ERR;
|
||||
if (true) {
|
||||
CLONE_TYPE_ERR =
|
||||
'You may only clone instances of AbstractEvent for ' +
|
||||
'persistent references. Check yourself.';
|
||||
}
|
||||
var MAX_POOL_SIZE = 20;
|
||||
|
||||
/**
|
||||
* AbstractEvent copy constructor. @see `PooledClass`. Provides a single place
|
||||
* to define all cross browser normalization of DOM events. Does not attempt to
|
||||
* extend a native event, rather creates a completely new object that has a
|
||||
* reference to the nativeEvent through .nativeEvent member. The property .data
|
||||
* should hold all data that is extracted from the event in a cross browser
|
||||
* manner. Application code should use the data field when possible, not the
|
||||
* unreliable native event.
|
||||
*/
|
||||
function AbstractEvent(
|
||||
abstractEventType,
|
||||
abstractTargetID, // Allows the abstract target to differ from native.
|
||||
originatingTopLevelEventType,
|
||||
nativeEvent,
|
||||
data) {
|
||||
this.type = abstractEventType;
|
||||
this.abstractTargetID = abstractTargetID || '';
|
||||
this.originatingTopLevelEventType = originatingTopLevelEventType;
|
||||
this.nativeEvent = nativeEvent;
|
||||
this.data = data;
|
||||
// TODO: Deprecate storing target - doesn't always make sense for some types
|
||||
this.target = nativeEvent && nativeEvent.target;
|
||||
|
||||
/**
|
||||
* As a performance optimization, we tag the existing event with the listeners
|
||||
* (or listener [singular] if only one). This avoids having to package up an
|
||||
* abstract event along with the set of listeners into a wrapping "dispatch"
|
||||
* object. No one should ever read this property except event system and
|
||||
* plugin/dispatcher code. We also tag the abstract event with a parallel
|
||||
* ID array. _dispatchListeners[i] is being dispatched to a DOM node at ID
|
||||
* _dispatchIDs[i]. The lengths should never, ever, ever be different.
|
||||
*/
|
||||
this._dispatchListeners = null;
|
||||
this._dispatchIDs = null;
|
||||
|
||||
this.isPropagationStopped = false;
|
||||
}
|
||||
|
||||
/** `PooledClass` looks for this. */
|
||||
AbstractEvent.poolSize = MAX_POOL_SIZE;
|
||||
|
||||
/**
|
||||
* `PooledClass` looks for `destructor` on each instance it releases. We need to
|
||||
* ensure that we remove all references to listeners which could trap large
|
||||
* amounts of memory in their closures.
|
||||
*/
|
||||
AbstractEvent.prototype.destructor = function() {
|
||||
this.target = null;
|
||||
this._dispatchListeners = null;
|
||||
this._dispatchIDs = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Enhance the `AbstractEvent` class to have pooling abilities. We instruct
|
||||
* `PooledClass` that our copy constructor accepts five arguments (this is just
|
||||
* a performance optimization). These objects are instantiated frequently.
|
||||
*/
|
||||
PooledClass.addPoolingTo(AbstractEvent, PooledClass.fiveArgumentPooler);
|
||||
|
||||
AbstractEvent.prototype.stopPropagation = function() {
|
||||
this.isPropagationStopped = true;
|
||||
if (this.nativeEvent.stopPropagation) {
|
||||
this.nativeEvent.stopPropagation();
|
||||
}
|
||||
// IE8 only understands cancelBubble, not stopPropagation().
|
||||
this.nativeEvent.cancelBubble = true;
|
||||
};
|
||||
|
||||
AbstractEvent.prototype.preventDefault = function() {
|
||||
AbstractEvent.preventDefaultOnNativeEvent(this.nativeEvent);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function for preventing default in cross browser manner.
|
||||
*/
|
||||
AbstractEvent.preventDefaultOnNativeEvent = function(nativeEvent) {
|
||||
if (nativeEvent.preventDefault) {
|
||||
nativeEvent.preventDefault();
|
||||
} else {
|
||||
nativeEvent.returnValue = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Element} target The target element.
|
||||
*/
|
||||
AbstractEvent.normalizeScrollDataFromTarget = function(target) {
|
||||
return {
|
||||
scrollTop: target.scrollTop,
|
||||
scrollLeft: target.scrollLeft,
|
||||
clientWidth: target.clientWidth,
|
||||
clientHeight: target.clientHeight,
|
||||
scrollHeight: target.scrollHeight,
|
||||
scrollWidth: target.scrollWidth
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* There are some normalizations that need to happen for various browsers. In
|
||||
* addition to replacing the general event fixing with a framework such as
|
||||
* jquery, we need to normalize mouse events here. Code below is mostly borrowed
|
||||
* from: jScrollPane/script/jquery.mousewheel.js
|
||||
*/
|
||||
AbstractEvent.normalizeMouseWheelData = function(nativeEvent) {
|
||||
var delta = 0;
|
||||
var deltaX = 0;
|
||||
var deltaY = 0;
|
||||
|
||||
/* traditional scroll wheel data */
|
||||
if ( nativeEvent.wheelDelta ) { delta = nativeEvent.wheelDelta/120; }
|
||||
if ( nativeEvent.detail ) { delta = -nativeEvent.detail/3; }
|
||||
|
||||
/* Multidimensional scroll (touchpads) with deltas */
|
||||
deltaY = delta;
|
||||
|
||||
/* Gecko based browsers */
|
||||
if (nativeEvent.axis !== undefined &&
|
||||
nativeEvent.axis === nativeEvent.HORIZONTAL_AXIS ) {
|
||||
deltaY = 0;
|
||||
deltaX = -delta;
|
||||
}
|
||||
|
||||
/* Webkit based browsers */
|
||||
if (nativeEvent.wheelDeltaY !== undefined ) {
|
||||
deltaY = nativeEvent.wheelDeltaY/120;
|
||||
}
|
||||
if (nativeEvent.wheelDeltaX !== undefined ) {
|
||||
deltaX = -nativeEvent.wheelDeltaX/120;
|
||||
}
|
||||
|
||||
return { delta: delta, deltaX: deltaX, deltaY: deltaY };
|
||||
};
|
||||
|
||||
/**
|
||||
* I <3 Quirksmode.org:
|
||||
* http://www.quirksmode.org/js/events_properties.html
|
||||
*/
|
||||
AbstractEvent.isNativeClickEventRightClick = function(nativeEvent) {
|
||||
return nativeEvent.which ? nativeEvent.which === 3 :
|
||||
nativeEvent.button ? nativeEvent.button === 2 :
|
||||
false;
|
||||
};
|
||||
|
||||
AbstractEvent.normalizePointerData = function(nativeEvent) {
|
||||
return {
|
||||
globalX: AbstractEvent.eventPageX(nativeEvent),
|
||||
globalY: AbstractEvent.eventPageY(nativeEvent),
|
||||
rightMouseButton:
|
||||
AbstractEvent.isNativeClickEventRightClick(nativeEvent)
|
||||
};
|
||||
};
|
||||
|
||||
AbstractEvent.normalizeDragEventData =
|
||||
function(nativeEvent, globalX, globalY, startX, startY) {
|
||||
return {
|
||||
globalX: globalX,
|
||||
globalY: globalY,
|
||||
startX: startX,
|
||||
startY: startY
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Warning: It is possible to move your finger on a touch surface, yet not
|
||||
* effect the `eventPageX/Y` because the touch had caused a scroll that
|
||||
* compensated for your movement. To track movements across the page, prevent
|
||||
* default to avoid scrolling, and control scrolling in javascript.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets the exact position of a touch/mouse event on the page with respect to
|
||||
* the document body. The only reason why this method is needed instead of using
|
||||
* `TouchEventUtils.extractSingleTouch` is to support IE8-. Mouse events in all
|
||||
* browsers except IE8- contain a pageY. IE8 and below require clientY
|
||||
* computation:
|
||||
*
|
||||
* @param {Event} nativeEvent Native event, possibly touch or mouse.
|
||||
* @return {number} Coordinate with respect to document body.
|
||||
*/
|
||||
AbstractEvent.eventPageY = function(nativeEvent) {
|
||||
var singleTouch = TouchEventUtils.extractSingleTouch(nativeEvent);
|
||||
if (singleTouch) {
|
||||
return singleTouch.pageY;
|
||||
} else if (typeof nativeEvent.pageY !== 'undefined') {
|
||||
return nativeEvent.pageY;
|
||||
} else {
|
||||
return nativeEvent.clientY + BrowserEnv.currentPageScrollTop;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @see `AbstractEvent.eventPageY`.
|
||||
*
|
||||
* @param {Event} nativeEvent Native event, possibly touch or mouse.
|
||||
* @return {number} Coordinate with respect to document body.
|
||||
*/
|
||||
AbstractEvent.eventPageX = function(nativeEvent) {
|
||||
var singleTouch = TouchEventUtils.extractSingleTouch(nativeEvent);
|
||||
if (singleTouch) {
|
||||
return singleTouch.pageX;
|
||||
} else if (typeof nativeEvent.pageX !== 'undefined') {
|
||||
return nativeEvent.pageX;
|
||||
} else {
|
||||
return nativeEvent.clientX + BrowserEnv.currentPageScrollLeft;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A semantic API around cloning an event for use in another event loop. We
|
||||
* clear out all dispatched `AbstractEvent`s after each event loop, adding them
|
||||
* back into the pool. This allows a way to hold onto a reference that won't be
|
||||
* added back into the pool. Please note that `AbstractEvent.nativeEvent` is
|
||||
* *not* cloned and you will run into problems in IE if you assume that it will
|
||||
* be! The moral of that story is to always normalize any data you need into the
|
||||
* `.data` field. The data field is not cloned either, but there won't be any
|
||||
* issues related to use of `.data` in a future event cycle so long as no part
|
||||
* of your application mutates it. We don't clone the private fields because
|
||||
* your application should never be accessing them.
|
||||
*
|
||||
* - TODO: In __DEV__ when "releasing" events, don't put them back into the
|
||||
* pool. Instead add ES5 getters on all their fields that throw errors so you
|
||||
* can detect any application that's hanging onto events and reusing them.
|
||||
* In prod - we can put them back into the pool for reuse.
|
||||
*/
|
||||
AbstractEvent.persistentCloneOf = function(abstractEvent) {
|
||||
if (true) {
|
||||
throwIf(!(abstractEvent instanceof AbstractEvent), CLONE_TYPE_ERR);
|
||||
}
|
||||
return new AbstractEvent(
|
||||
abstractEvent.type,
|
||||
abstractEvent.abstractTargetID,
|
||||
abstractEvent.originatingTopLevelEventType,
|
||||
abstractEvent.nativeEvent,
|
||||
abstractEvent.data,
|
||||
abstractEvent.target
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = AbstractEvent;
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 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 AnalyticsEventPluginFactory
|
||||
*
|
||||
* This module provides a factory method to create the AnalyticsEventPlugin that
|
||||
* can be used to track the usage of React components that are of interest to
|
||||
* the user.
|
||||
*
|
||||
* In order to enable a component for analytics tracking, you need to specify
|
||||
* two additional attributes to the component when you describe the structure of
|
||||
* your component in the render() method:
|
||||
*
|
||||
* 1. 'data-analytics-id': This represents a unique ID that the analytics module
|
||||
* will use to identify this component in all the analytics data. Note that
|
||||
* this is independent of the ref or the DOM id of the element. Over the
|
||||
* lifetime of the product, even if the component id or ref needs to be
|
||||
* changed, as long as you can ensure the analytics ID doesnt change, the
|
||||
* historical data can be correlated. Also note that React does NOT do
|
||||
* anything to guarantee or enforce uniqueness of this ID. If its not unique
|
||||
* the analytics data reported will be incorrect.
|
||||
*
|
||||
* 2. 'data-analytics-events': This is a comma separated list of DOM events that
|
||||
* you want analytics on. React currently supports tracking only on a
|
||||
* distinct set of events (See topLevelTypesToAnalyticsEvent).
|
||||
* If the list contains an event that React does not recognize for analytics
|
||||
* tracking, in __DEV__, an error will be thrown. Note that it is case
|
||||
* sensitive and space sensitive.
|
||||
*
|
||||
* By default the AnalyticsEventPlugin is NOT enabled in React. To use it, you
|
||||
* need to create the plugin using the factory method and add it to the list of
|
||||
* Plugins maintained in the EventPluginHub before your component is rendered.
|
||||
* As creation parameters you can specify two arguments:
|
||||
*
|
||||
* 1. callback: This is a required parameter. In __DEV__, an error will be
|
||||
* thrown if this param is missing. The callback will be called with the
|
||||
* analyticsData as an argument. The analyticsData will contain one property
|
||||
* per every React component, identified by its data-analytics-id. The value
|
||||
* of this property will be an object containing properties corresponding to
|
||||
* each of the comma separated events specified in data-analytics-events.
|
||||
*
|
||||
* For example, if you have:
|
||||
* <Button ...
|
||||
* data-analytics-id="createButton"
|
||||
* data-analytics-events="click"
|
||||
* />
|
||||
* and
|
||||
* <TextBox ...
|
||||
* data-analytics-id="disclaimerBox"
|
||||
* data-analytics-events="focus,scroll"
|
||||
* />
|
||||
* analyticsData will be something like:
|
||||
* '{"createButton":{"click":50}, "disclaimerBox":{"focus":15, "scroll":5}}'
|
||||
*
|
||||
* DO NOT mutate the data that you get in the callback. Mutating it will lead
|
||||
* to errors and unstable behavior.
|
||||
*
|
||||
* The React component will be included for analytics as long as some user
|
||||
* interaction has happened with that component. If no user interaction has
|
||||
* happened with any of the components tracked for analytics, the callback
|
||||
* will not be called.
|
||||
*
|
||||
* 2. interval (in milliseconds): This is an optional parameter to specify the
|
||||
* interval at which the callback needs to be called. It needs to be greater
|
||||
* than the 2 minutes (which is the default value if this parameter is not
|
||||
* specified or a value less than 2 minutes is specified)
|
||||
*
|
||||
* Please refer to the unit tests AnalyticsEventPlugin-test.js for details on
|
||||
* usage.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ExecutionEnvironment = require("./ExecutionEnvironment");
|
||||
|
||||
var emptyFunction = require("./emptyFunction");
|
||||
var invariant = require("./invariant");
|
||||
var topLevelTypes = require("./EventConstants").topLevelTypes;
|
||||
|
||||
var ANALYTICS_ID = 'data-analytics-id';
|
||||
var ANALYTICS_EVENTS = 'data-analytics-events';
|
||||
var DEFAULT_INTERVAL_MS = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
var analyticsData = {};
|
||||
|
||||
// List of topLevel event types that React supports for analytics tracking
|
||||
var topLevelTypesToAnalyticsEvent = {
|
||||
topClick: 'click',
|
||||
topDoubleClick: 'doubleClick',
|
||||
topMouseWheel: 'mouseWheel',
|
||||
topTouchStart: 'touchStart',
|
||||
topTouchEnd: 'touchEnd',
|
||||
topTouchMove: 'touchMove',
|
||||
topTouchCancel: 'touchCancel',
|
||||
topKeyUp: 'keyUp',
|
||||
topKeyPress: 'keyPress',
|
||||
topKeyDown: 'keyDown',
|
||||
topFocus: 'focus',
|
||||
topBlur: 'blur',
|
||||
topScroll: 'scroll',
|
||||
topChange: 'change'
|
||||
};
|
||||
|
||||
if (true) {
|
||||
var analyticsEventNameToTopLevelType = {
|
||||
'click': topLevelTypes.topClick,
|
||||
'doubleClick': topLevelTypes.topDoubleClick,
|
||||
'mouseWheel': topLevelTypes.topMouseWheel,
|
||||
'touchStart': topLevelTypes.topTouchStart,
|
||||
'touchEnd': topLevelTypes.topTouchEnd,
|
||||
'touchMove': topLevelTypes.topTouchMove,
|
||||
'touchCancel': topLevelTypes.topTouchCancel,
|
||||
'keyUp': topLevelTypes.topKeyUp,
|
||||
'keyPress': topLevelTypes.topKeyPress,
|
||||
'keyDown': topLevelTypes.topKeyDown,
|
||||
'focus': topLevelTypes.topFocus,
|
||||
'blur': topLevelTypes.topBlur,
|
||||
'scroll': topLevelTypes.topScroll,
|
||||
'change': topLevelTypes.topChange
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This plugin does not really extract any abstract events. Rather it just looks
|
||||
* at the top level event and bumps up counters as appropriate
|
||||
*
|
||||
* @see EventPluginHub.extractAbstractEvents
|
||||
*/
|
||||
function extractAbstractEvents(
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
renderedTargetID,
|
||||
renderedTarget) {
|
||||
var currentEvent = topLevelTypesToAnalyticsEvent[topLevelType];
|
||||
if (!currentEvent || !renderedTarget || !renderedTarget.attributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var analyticsIDAttribute = renderedTarget.attributes[ANALYTICS_ID];
|
||||
var analyticsEventsAttribute = renderedTarget.attributes[ANALYTICS_EVENTS];
|
||||
if(!analyticsIDAttribute || !analyticsEventsAttribute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var analyticsEventsArr = analyticsEventsAttribute.value.split(",");
|
||||
var analyticsID = analyticsIDAttribute.value;
|
||||
if (!analyticsData[analyticsID]) {
|
||||
initAnalyticsDataForID(analyticsID, analyticsEventsArr);
|
||||
}
|
||||
|
||||
if (analyticsEventsArr.indexOf(currentEvent) !== -1) {
|
||||
analyticsData[analyticsID][currentEvent]++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the analytics data for a specific element identified by the
|
||||
* analyticsID - Create an entry in the analyticsData object for the element and
|
||||
* initialize all counters for that element to 0.
|
||||
*/
|
||||
function initAnalyticsDataForID(analyticsID, analyticsEventsArr) {
|
||||
analyticsData[analyticsID] = {};
|
||||
analyticsEventsArr.forEach(function(analyticsEvent) {
|
||||
if (true) {
|
||||
invariant(
|
||||
analyticsEventNameToTopLevelType[analyticsEvent],
|
||||
'Invalid analyticsEvent:%s for analyticsID:%s',
|
||||
analyticsEvent,
|
||||
analyticsID
|
||||
);
|
||||
}
|
||||
analyticsData[analyticsID][analyticsEvent] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the analytics event plugin given the callback that needs to be
|
||||
* invoked for reporting analytics and the interval at which the callback needs
|
||||
* to be invoked. This interval has to be atleast DEFAULT_INTERVAL_MS.
|
||||
*/
|
||||
var createAnalyticsPlugin = function(cb, interval) {
|
||||
invariant(
|
||||
ExecutionEnvironment.canUseDOM,
|
||||
'createAnalyticsPlugin(...): The DOM is not supported in the execution ' +
|
||||
'environment.'
|
||||
);
|
||||
|
||||
if (true) {
|
||||
invariant(cb, 'createAnalyticsPlugin(...): You must provide a callback.');
|
||||
}
|
||||
cb = cb || emptyFunction;
|
||||
|
||||
setInterval(
|
||||
function() {
|
||||
if (Object.keys(analyticsData).length) {
|
||||
// Invoke the callback with a clone of analyticsData, otherwise our
|
||||
// analyticsData will be dirtied by user changes
|
||||
cb(analyticsData);
|
||||
}
|
||||
},
|
||||
interval > DEFAULT_INTERVAL_MS ? interval : DEFAULT_INTERVAL_MS
|
||||
);
|
||||
|
||||
return {extractAbstractEvents: extractAbstractEvents};
|
||||
};
|
||||
|
||||
var AnalyticsEventPluginFactory = {
|
||||
createAnalyticsPlugin: createAnalyticsPlugin
|
||||
};
|
||||
|
||||
module.exports = AnalyticsEventPluginFactory;
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 BrowserEnv
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* A place to share/cache browser/chrome level computations.
|
||||
*/
|
||||
var BrowserEnv = {
|
||||
currentScrollLeft: 0,
|
||||
currentScrollTop: 0,
|
||||
browserInfo: null,
|
||||
refreshAuthoritativeScrollValues: function() {
|
||||
BrowserEnv.currentScrollLeft =
|
||||
document.body.scrollLeft + document.documentElement.scrollLeft;
|
||||
BrowserEnv.currentScrollTop =
|
||||
document.body.scrollTop + document.documentElement.scrollTop;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = BrowserEnv;
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 CSSProperty
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* CSS properties for which we do not append "px".
|
||||
*/
|
||||
var isNumber = {
|
||||
fillOpacity: true,
|
||||
fontWeight: true,
|
||||
opacity: true,
|
||||
orphans: true,
|
||||
textDecoration: true,
|
||||
zIndex: true,
|
||||
zoom: true
|
||||
};
|
||||
|
||||
var CSSProperty = {
|
||||
isNumber: isNumber
|
||||
};
|
||||
|
||||
module.exports = CSSProperty;
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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 CSSPropertyOperations
|
||||
* @typechecks
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var dangerousStyleValue = require("./dangerousStyleValue");
|
||||
var escapeTextForBrowser = require("./escapeTextForBrowser");
|
||||
var hyphenate = require("./hyphenate");
|
||||
var memoizeStringOnly = require("./memoizeStringOnly");
|
||||
|
||||
var processStyleName = memoizeStringOnly(function(styleName) {
|
||||
return escapeTextForBrowser(hyphenate(styleName));
|
||||
});
|
||||
|
||||
/**
|
||||
* Operations for dealing with CSS properties.
|
||||
*/
|
||||
var CSSPropertyOperations = {
|
||||
|
||||
/**
|
||||
* Serializes a mapping of style properties for use as inline styles:
|
||||
*
|
||||
* > createMarkupForStyles({width: '200px', height: 0})
|
||||
* "width:200px;height:0;"
|
||||
*
|
||||
* Undefined values are ignored so that declarative programming is easier.
|
||||
*
|
||||
* @param {object} styles
|
||||
* @return {string}
|
||||
*/
|
||||
createMarkupForStyles: function(styles) {
|
||||
var serialized = '';
|
||||
for (var styleName in styles) {
|
||||
if (!styles.hasOwnProperty(styleName)) {
|
||||
continue;
|
||||
}
|
||||
var styleValue = styles[styleName];
|
||||
if (typeof styleValue !== 'undefined') {
|
||||
serialized += processStyleName(styleName) + ':';
|
||||
serialized += dangerousStyleValue(styleName, styleValue) + ';';
|
||||
}
|
||||
}
|
||||
return serialized;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the value for multiple styles on a node.
|
||||
*
|
||||
* @param {DOMElement} node
|
||||
* @param {object} styles
|
||||
*/
|
||||
setValueForStyles: function(node, styles) {
|
||||
var style = node.style;
|
||||
for (var styleName in styles) {
|
||||
if (!styles.hasOwnProperty(styleName)) {
|
||||
continue;
|
||||
}
|
||||
var styleValue = styles[styleName];
|
||||
style[styleName] = dangerousStyleValue(styleName, styleValue);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = CSSPropertyOperations;
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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 CallbackRegistry
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var listenerBank = {};
|
||||
|
||||
/**
|
||||
* Stores "listeners" by `registrationName`/`id`. There should be at most one
|
||||
* "listener" per `registrationName/id` in the `listenerBank`.
|
||||
* Access listeners via `listenerBank[registrationName][id]`
|
||||
*
|
||||
* @constructor CallbackRegistry
|
||||
*/
|
||||
var CallbackRegistry = {
|
||||
|
||||
/**
|
||||
* Stores `listener` at `listenerBank[registrationName][id]. Is idempotent.
|
||||
* @param {string} domID The id of the DOM node.
|
||||
* @param {string} registrationName The name of listener (`onClick` etc).
|
||||
* @param {Function} listener The callback to to store.
|
||||
*/
|
||||
putListener: function(id, registrationName, listener) {
|
||||
var bankForRegistrationName =
|
||||
listenerBank[registrationName] || (listenerBank[registrationName] = {});
|
||||
bankForRegistrationName[id] = listener;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} id.
|
||||
* @param {string} registrationName Name of registration (`onClick` etc).
|
||||
* @return {Function?} The Listener
|
||||
*/
|
||||
getListener: function(id, registrationName) {
|
||||
var bankForRegistrationName = listenerBank[registrationName];
|
||||
return bankForRegistrationName && bankForRegistrationName[id];
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes the listener from the registration bank.
|
||||
* @param {string} id
|
||||
* @param {string} registrationName (`onClick` etc).
|
||||
*/
|
||||
deleteListener: function(id, registrationName) {
|
||||
var bankForRegistrationName = listenerBank[registrationName];
|
||||
if (bankForRegistrationName) {
|
||||
delete bankForRegistrationName[id];
|
||||
}
|
||||
},
|
||||
|
||||
// This is needed for tests only. Do not use in real life
|
||||
__purge: function() {
|
||||
listenerBank = {};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = CallbackRegistry;
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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 DOMChildrenOperations
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var Danger = require("./Danger");
|
||||
|
||||
var insertNodeAt = require("./insertNodeAt");
|
||||
var keyOf = require("./keyOf");
|
||||
var throwIf = require("./throwIf");
|
||||
|
||||
var NON_INCREASING_OPERATIONS;
|
||||
if (true) {
|
||||
NON_INCREASING_OPERATIONS =
|
||||
'DOM child management operations must be provided in order ' +
|
||||
'of increasing destination index. This is likely an issue with ' +
|
||||
'the core framework.';
|
||||
}
|
||||
|
||||
var MOVE_NODE_AT_ORIG_INDEX = keyOf({moveFrom: null});
|
||||
var INSERT_MARKUP = keyOf({insertMarkup: null});
|
||||
var REMOVE_AT = keyOf({removeAt: null});
|
||||
|
||||
/**
|
||||
* In order to carry out movement of DOM nodes without knowing their IDs, we
|
||||
* have to first store knowledge about nodes' original indices before beginning
|
||||
* to carry out the sequence of operations. Once we begin the sequence, the DOM
|
||||
* indices in future instructions are no longer valid.
|
||||
*
|
||||
* @param {Element} parent Parent DOM node.
|
||||
* @param {Object} childOperations Description of child operations.
|
||||
* @returns {Array?} Sparse array containing elements by their current index in
|
||||
* the DOM.
|
||||
*/
|
||||
var _getNodesByOriginalIndex = function(parent, childOperations) {
|
||||
var nodesByOriginalIndex; // Sparse array.
|
||||
var childOperation;
|
||||
var origIndex;
|
||||
for (var i = 0; i < childOperations.length; i++) {
|
||||
childOperation = childOperations[i];
|
||||
if (MOVE_NODE_AT_ORIG_INDEX in childOperation) {
|
||||
nodesByOriginalIndex = nodesByOriginalIndex || [];
|
||||
origIndex = childOperation.moveFrom;
|
||||
nodesByOriginalIndex[origIndex] = parent.childNodes[origIndex];
|
||||
} else if (REMOVE_AT in childOperation) {
|
||||
nodesByOriginalIndex = nodesByOriginalIndex || [];
|
||||
origIndex = childOperation.removeAt;
|
||||
nodesByOriginalIndex[origIndex] = parent.childNodes[origIndex];
|
||||
}
|
||||
}
|
||||
return nodesByOriginalIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes DOM elements from their parent, or moved.
|
||||
* @param {Element} parent Parent DOM node.
|
||||
* @param {Array} nodesByOriginalIndex Child nodes by their original index
|
||||
* (potentially sparse.)
|
||||
*/
|
||||
var _removeChildrenByOriginalIndex = function(parent, nodesByOriginalIndex) {
|
||||
for (var j = 0; j < nodesByOriginalIndex.length; j++) {
|
||||
var nodeToRemove = nodesByOriginalIndex[j];
|
||||
if (nodeToRemove) { // We used a sparse array.
|
||||
parent.removeChild(nodesByOriginalIndex[j]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Once all nodes that will be removed or moved - are removed from the parent
|
||||
* node, we can begin the process of placing nodes into their final locations.
|
||||
* We must perform all operations in the order of the final DOM index -
|
||||
* otherwise, we couldn't count on the fact that an insertion at index X, will
|
||||
* remain at index X. This will iterate through the child operations, adding
|
||||
* content where needed, skip over removals (they've already been removed) and
|
||||
* insert "moved" Elements that were previously removed. The "moved" elements
|
||||
* are only temporarily removed from the parent, so that index calculations can
|
||||
* be manageable and perform well in the cases that matter.
|
||||
*/
|
||||
var _placeNodesAtDestination =
|
||||
function(parent, childOperations, nodesByOriginalIndex) {
|
||||
var origNode;
|
||||
var finalIndex;
|
||||
var lastFinalIndex = -1;
|
||||
var childOperation;
|
||||
for (var k = 0; k < childOperations.length; k++) {
|
||||
childOperation = childOperations[k];
|
||||
if (MOVE_NODE_AT_ORIG_INDEX in childOperation) {
|
||||
origNode = nodesByOriginalIndex[childOperation.moveFrom];
|
||||
finalIndex = childOperation.finalIndex;
|
||||
insertNodeAt(parent, origNode, finalIndex);
|
||||
if (true) {
|
||||
throwIf(finalIndex <= lastFinalIndex, NON_INCREASING_OPERATIONS);
|
||||
lastFinalIndex = finalIndex;
|
||||
}
|
||||
} else if (REMOVE_AT in childOperation) {
|
||||
} else if (INSERT_MARKUP in childOperation) {
|
||||
finalIndex = childOperation.finalIndex;
|
||||
var markup = childOperation.insertMarkup;
|
||||
Danger.dangerouslyInsertMarkupAt(parent, markup, finalIndex);
|
||||
if (true) {
|
||||
throwIf(finalIndex <= lastFinalIndex, NON_INCREASING_OPERATIONS);
|
||||
lastFinalIndex = finalIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var manageChildren = function(parent, childOperations) {
|
||||
var nodesByOriginalIndex = _getNodesByOriginalIndex(parent, childOperations);
|
||||
if (nodesByOriginalIndex) {
|
||||
_removeChildrenByOriginalIndex(parent, nodesByOriginalIndex);
|
||||
}
|
||||
_placeNodesAtDestination(parent, childOperations, nodesByOriginalIndex);
|
||||
};
|
||||
|
||||
var setTextNodeValueAtIndex = function(parent, index, val) {
|
||||
parent.childNodes[index].nodeValue = val;
|
||||
};
|
||||
|
||||
/**
|
||||
* Also reexport all of the dangerous functions. It helps to have all dangerous
|
||||
* functions located in a single module `Danger`.
|
||||
*/
|
||||
var DOMChildrenOperations = {
|
||||
dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup,
|
||||
manageChildren: manageChildren,
|
||||
setTextNodeValueAtIndex: setTextNodeValueAtIndex
|
||||
};
|
||||
|
||||
module.exports = DOMChildrenOperations;
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* 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 DOMProperty
|
||||
* @typechecks
|
||||
*/
|
||||
|
||||
/*jslint bitwise: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var invariant = require("./invariant");
|
||||
|
||||
/**
|
||||
* DOMProperty exports lookup objects that can be used like functions:
|
||||
*
|
||||
* > DOMProperty.isValid['id']
|
||||
* true
|
||||
* > DOMProperty.isValid['foobar']
|
||||
* undefined
|
||||
*
|
||||
* Although this may be confusing, it performs better in general.
|
||||
*
|
||||
* @see http://jsperf.com/key-exists
|
||||
* @see http://jsperf.com/key-missing
|
||||
*/
|
||||
var DOMProperty = {
|
||||
|
||||
/**
|
||||
* Checks whether a property name is a standard property.
|
||||
* @type {Object}
|
||||
*/
|
||||
isStandardName: {},
|
||||
|
||||
/**
|
||||
* Mapping from normalized names to attribute names that differ. Attribute
|
||||
* names are used when rendering markup or with `*Attribute()`.
|
||||
* @type {Object}
|
||||
*/
|
||||
getAttributeName: {},
|
||||
|
||||
/**
|
||||
* Mapping from normalized names to properties on DOM node instances.
|
||||
* (This includes properties that mutate due to external factors.)
|
||||
* @type {Object}
|
||||
*/
|
||||
getPropertyName: {},
|
||||
|
||||
/**
|
||||
* Mapping from normalized names to mutation methods. This will only exist if
|
||||
* mutation cannot be set simply by the property or `setAttribute()`.
|
||||
* @type {Object}
|
||||
*/
|
||||
getMutationMethod: {},
|
||||
|
||||
/**
|
||||
* Whether the property must be accessed and mutated as an object property.
|
||||
* @type {Object}
|
||||
*/
|
||||
mustUseAttribute: {},
|
||||
|
||||
/**
|
||||
* Whether the property must be accessed and mutated using `*Attribute()`.
|
||||
* (This includes anything that fails `<propName> in <element>`.)
|
||||
* @type {Object}
|
||||
*/
|
||||
mustUseProperty: {},
|
||||
|
||||
/**
|
||||
* Whether the property should be removed when set to a falsey value.
|
||||
* @type {Object}
|
||||
*/
|
||||
hasBooleanValue: {},
|
||||
|
||||
/**
|
||||
* Whether or not setting a value causes side effects such as triggering
|
||||
* resources to be loaded or text selection changes. We must ensure that
|
||||
* the value is only set if it has changed.
|
||||
* @type {Object}
|
||||
*/
|
||||
hasSideEffects: {},
|
||||
|
||||
/**
|
||||
* Checks whether a property name is a custom attribute.
|
||||
* @method
|
||||
*/
|
||||
isCustomAttribute: RegExp.prototype.test.bind(
|
||||
/^(data|aria)-[a-z_][a-z\d_.\-]*$/
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping from normalized, camelcased property names to a configuration that
|
||||
* specifies how the associated DOM property should be accessed or rendered.
|
||||
*/
|
||||
var MustUseAttribute = 0x1;
|
||||
var MustUseProperty = 0x2;
|
||||
var HasBooleanValue = 0x4;
|
||||
var HasSideEffects = 0x8;
|
||||
|
||||
var Properties = {
|
||||
/**
|
||||
* Standard Properties
|
||||
*/
|
||||
accept: null,
|
||||
action: null,
|
||||
ajaxify: MustUseAttribute,
|
||||
allowFullScreen: MustUseAttribute | HasBooleanValue,
|
||||
alt: null,
|
||||
autoComplete: null,
|
||||
autoplay: HasBooleanValue,
|
||||
cellPadding: null,
|
||||
cellSpacing: null,
|
||||
checked: MustUseProperty | HasBooleanValue,
|
||||
className: MustUseProperty,
|
||||
colSpan: null,
|
||||
contentEditable: null,
|
||||
controls: MustUseProperty | HasBooleanValue,
|
||||
data: null, // For `<object />` acts as `src`.
|
||||
dir: null,
|
||||
disabled: MustUseProperty | HasBooleanValue,
|
||||
enctype: null,
|
||||
height: null,
|
||||
href: null,
|
||||
htmlFor: null,
|
||||
max: null,
|
||||
method: null,
|
||||
min: null,
|
||||
multiple: MustUseProperty | HasBooleanValue,
|
||||
name: null,
|
||||
poster: null,
|
||||
preload: null,
|
||||
placeholder: null,
|
||||
rel: null,
|
||||
required: HasBooleanValue,
|
||||
role: MustUseAttribute,
|
||||
scrollLeft: MustUseProperty,
|
||||
scrollTop: MustUseProperty,
|
||||
selected: MustUseProperty | HasBooleanValue,
|
||||
spellCheck: null,
|
||||
src: null,
|
||||
step: null,
|
||||
style: null,
|
||||
tabIndex: null,
|
||||
target: null,
|
||||
title: null,
|
||||
type: null,
|
||||
value: MustUseProperty | HasSideEffects,
|
||||
width: null,
|
||||
wmode: MustUseAttribute,
|
||||
/**
|
||||
* SVG Properties
|
||||
*/
|
||||
cx: MustUseProperty,
|
||||
cy: MustUseProperty,
|
||||
d: MustUseProperty,
|
||||
fill: MustUseProperty,
|
||||
fx: MustUseProperty,
|
||||
fy: MustUseProperty,
|
||||
points: MustUseProperty,
|
||||
r: MustUseProperty,
|
||||
stroke: MustUseProperty,
|
||||
strokeLinecap: MustUseProperty,
|
||||
strokeWidth: MustUseProperty,
|
||||
transform: MustUseProperty,
|
||||
x: MustUseProperty,
|
||||
x1: MustUseProperty,
|
||||
x2: MustUseProperty,
|
||||
version: MustUseProperty,
|
||||
viewBox: MustUseProperty,
|
||||
y: MustUseProperty,
|
||||
y1: MustUseProperty,
|
||||
y2: MustUseProperty,
|
||||
spreadMethod: MustUseProperty,
|
||||
offset: MustUseProperty,
|
||||
stopColor: MustUseProperty,
|
||||
stopOpacity: MustUseProperty,
|
||||
gradientUnits: MustUseProperty,
|
||||
gradientTransform: MustUseProperty
|
||||
};
|
||||
|
||||
/**
|
||||
* Attribute names not specified use the **lowercase** normalized name.
|
||||
*/
|
||||
var DOMAttributeNames = {
|
||||
className: 'class',
|
||||
htmlFor: 'for',
|
||||
strokeLinecap: 'stroke-linecap',
|
||||
strokeWidth: 'stroke-width',
|
||||
stopColor: 'stop-color',
|
||||
stopOpacity: 'stop-opacity'
|
||||
};
|
||||
|
||||
/**
|
||||
* Property names not specified use the normalized name.
|
||||
*/
|
||||
var DOMPropertyNames = {
|
||||
autoComplete: 'autocomplete',
|
||||
spellCheck: 'spellcheck'
|
||||
};
|
||||
|
||||
/**
|
||||
* Properties that require special mutation methods.
|
||||
*/
|
||||
var DOMMutationMethods = {
|
||||
/**
|
||||
* Setting `className` to null may cause it to be set to the string "null".
|
||||
*
|
||||
* @param {DOMElement} node
|
||||
* @param {*} value
|
||||
*/
|
||||
className: function(node, value) {
|
||||
node.className = value || '';
|
||||
}
|
||||
};
|
||||
|
||||
for (var propName in Properties) {
|
||||
DOMProperty.isStandardName[propName] = true;
|
||||
|
||||
DOMProperty.getAttributeName[propName] =
|
||||
DOMAttributeNames[propName] || propName.toLowerCase();
|
||||
|
||||
DOMProperty.getPropertyName[propName] =
|
||||
DOMPropertyNames[propName] || propName;
|
||||
|
||||
var mutationMethod = DOMMutationMethods[propName];
|
||||
if (mutationMethod) {
|
||||
DOMProperty.getMutationMethod[propName] = mutationMethod;
|
||||
}
|
||||
|
||||
var propConfig = Properties[propName];
|
||||
DOMProperty.mustUseAttribute[propName] = propConfig & MustUseAttribute;
|
||||
DOMProperty.mustUseProperty[propName] = propConfig & MustUseProperty;
|
||||
DOMProperty.hasBooleanValue[propName] = propConfig & HasBooleanValue;
|
||||
DOMProperty.hasSideEffects[propName] = propConfig & HasSideEffects;
|
||||
|
||||
invariant(
|
||||
!DOMProperty.mustUseAttribute[propName] ||
|
||||
!DOMProperty.mustUseProperty[propName],
|
||||
'DOMProperty: Cannot use require using both attribute and property: %s',
|
||||
propName
|
||||
);
|
||||
invariant(
|
||||
DOMProperty.mustUseProperty[propName] ||
|
||||
!DOMProperty.hasSideEffects[propName],
|
||||
'DOMProperty: Properties that have side effects must use property: %s',
|
||||
propName
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = DOMProperty;
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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 DOMPropertyOperations
|
||||
* @typechecks
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var DOMProperty = require("./DOMProperty");
|
||||
|
||||
var escapeTextForBrowser = require("./escapeTextForBrowser");
|
||||
var memoizeStringOnly = require("./memoizeStringOnly");
|
||||
|
||||
var processAttributeNameAndPrefix = memoizeStringOnly(function(name) {
|
||||
return escapeTextForBrowser(name) + '="';
|
||||
});
|
||||
|
||||
/**
|
||||
* Operations for dealing with DOM properties.
|
||||
*/
|
||||
var DOMPropertyOperations = {
|
||||
|
||||
/**
|
||||
* Creates markup for a property.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {*} value
|
||||
* @return {?string} Markup string, or null if the property was invalid.
|
||||
*/
|
||||
createMarkupForProperty: function(name, value) {
|
||||
if (DOMProperty.isStandardName[name]) {
|
||||
if (value == null || DOMProperty.hasBooleanValue[name] && !value) {
|
||||
return '';
|
||||
}
|
||||
var attributeName = DOMProperty.getAttributeName[name];
|
||||
return processAttributeNameAndPrefix(attributeName) +
|
||||
escapeTextForBrowser(value) + '"';
|
||||
} else if (DOMProperty.isCustomAttribute(name)) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
return processAttributeNameAndPrefix(name) +
|
||||
escapeTextForBrowser(value) + '"';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the value for a property on a node.
|
||||
*
|
||||
* @param {DOMElement} node
|
||||
* @param {string} name
|
||||
* @param {*} value
|
||||
*/
|
||||
setValueForProperty: function(node, name, value) {
|
||||
if (DOMProperty.isStandardName[name]) {
|
||||
var mutationMethod = DOMProperty.getMutationMethod[name];
|
||||
if (mutationMethod) {
|
||||
mutationMethod(node, value);
|
||||
} else if (DOMProperty.mustUseAttribute[name]) {
|
||||
if (DOMProperty.hasBooleanValue[name] && !value) {
|
||||
node.removeAttribute(DOMProperty.getAttributeName[name]);
|
||||
} else {
|
||||
node.setAttribute(DOMProperty.getAttributeName[name], value);
|
||||
}
|
||||
} else {
|
||||
var propName = DOMProperty.getPropertyName[name];
|
||||
if (!DOMProperty.hasSideEffects[name] || node[propName] !== value) {
|
||||
node[propName] = value;
|
||||
}
|
||||
}
|
||||
} else if (DOMProperty.isCustomAttribute(name)) {
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = DOMPropertyOperations;
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 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 Danger
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ExecutionEnvironment = require("./ExecutionEnvironment");
|
||||
|
||||
var throwIf = require("./throwIf");
|
||||
|
||||
var DOM_UNSUPPORTED;
|
||||
var NO_MARKUP_PARENT;
|
||||
var NO_MULTI_MARKUP;
|
||||
if (true) {
|
||||
DOM_UNSUPPORTED =
|
||||
'You may not insert markup into the document while you are in a worker ' +
|
||||
'thread. It\'s not you, it\'s me. This is likely the fault of the ' +
|
||||
'framework. Please report this immediately.';
|
||||
NO_MARKUP_PARENT =
|
||||
'You have attempted to inject markup without a suitable parent. This is ' +
|
||||
'likely the fault of the framework - please report immediately.';
|
||||
NO_MULTI_MARKUP =
|
||||
'The framework has attempted to either insert zero or multiple markup ' +
|
||||
'roots into a single location when it should not. This is a serious ' +
|
||||
'error - a fault of the framework - please report immediately.';
|
||||
}
|
||||
|
||||
var validateMarkupParams;
|
||||
if (true) {
|
||||
validateMarkupParams = function(parentNode, markup) {
|
||||
throwIf(!ExecutionEnvironment.canUseDOM, DOM_UNSUPPORTED);
|
||||
throwIf(!parentNode || !parentNode.tagName, NO_MARKUP_PARENT);
|
||||
throwIf(!markup, NO_MULTI_MARKUP);
|
||||
};
|
||||
}
|
||||
|
||||
var dummies = {};
|
||||
|
||||
function getParentDummy(parent) {
|
||||
var parentTag = parent.tagName;
|
||||
return dummies[parentTag] ||
|
||||
(dummies[parentTag] = document.createElement(parentTag));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts node after 'after'. If 'after' is null, inserts it after nothing,
|
||||
* which is inserting it at the beginning.
|
||||
*
|
||||
* @param {Element} elem Parent element.
|
||||
* @param {Element} insert Element to insert.
|
||||
* @param {Element} after Element to insert after.
|
||||
* @returns {Element} Element that was inserted.
|
||||
*/
|
||||
function insertNodeAfterNode(elem, insert, after) {
|
||||
if (true) {
|
||||
throwIf(!ExecutionEnvironment.canUseDOM, DOM_UNSUPPORTED);
|
||||
}
|
||||
if (after) {
|
||||
if (after.nextSibling) {
|
||||
return elem.insertBefore(insert, after.nextSibling);
|
||||
} else {
|
||||
return elem.appendChild(insert);
|
||||
}
|
||||
} else {
|
||||
return elem.insertBefore(insert, elem.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slow: Should only be used when it is known there are a few (or one) element
|
||||
* in the node list.
|
||||
* @param {Element} parentRootDomNode Parent element.
|
||||
* @param {HTMLCollection} htmlCollection HTMLCollection to insert.
|
||||
* @param {Element} after Element to insert the node list after.
|
||||
*/
|
||||
function inefficientlyInsertHTMLCollectionAfter(
|
||||
parentRootDomNode,
|
||||
htmlCollection,
|
||||
after) {
|
||||
|
||||
if (true) {
|
||||
throwIf(!ExecutionEnvironment.canUseDOM, DOM_UNSUPPORTED);
|
||||
}
|
||||
var ret;
|
||||
var originalLength = htmlCollection.length;
|
||||
// Access htmlCollection[0] because htmlCollection shrinks as we remove items.
|
||||
// `insertNodeAfterNode` will remove items from the htmlCollection.
|
||||
for (var i = 0; i < originalLength; i++) {
|
||||
ret =
|
||||
insertNodeAfterNode(parentRootDomNode, htmlCollection[0], ret || after);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Super-dangerously inserts markup into existing DOM structure. Seriously, you
|
||||
* don't want to use this module unless you are building a framework. This
|
||||
* requires that the markup that you are inserting represents the root of a
|
||||
* tree. We do not support the case where there `markup` represents several
|
||||
* roots.
|
||||
*
|
||||
* @param {Element} parentNode Parent DOM element.
|
||||
* @param {string} markup Markup to dangerously insert.
|
||||
* @param {number} index Position to insert markup at.
|
||||
*/
|
||||
function dangerouslyInsertMarkupAt(parentNode, markup, index) {
|
||||
if (true) {
|
||||
validateMarkupParams(parentNode, markup);
|
||||
}
|
||||
var parentDummy = getParentDummy(parentNode);
|
||||
parentDummy.innerHTML = markup;
|
||||
var htmlCollection = parentDummy.childNodes;
|
||||
var afterNode = index ? parentNode.childNodes[index - 1] : null;
|
||||
inefficientlyInsertHTMLCollectionAfter(parentNode, htmlCollection, afterNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a node with a string of markup at its current position within its
|
||||
* parent. `childNode` must be in the document (or at least within a parent
|
||||
* node). The string of markup must represent a tree of markup with a single
|
||||
* root.
|
||||
*
|
||||
* @param {Element} childNode Child node to replace.
|
||||
* @param {string} markup Markup to dangerously replace child with.
|
||||
*/
|
||||
function dangerouslyReplaceNodeWithMarkup(childNode, markup) {
|
||||
var parentNode = childNode.parentNode;
|
||||
if (true) {
|
||||
validateMarkupParams(parentNode, markup);
|
||||
}
|
||||
var parentDummy = getParentDummy(parentNode);
|
||||
parentDummy.innerHTML = markup;
|
||||
var htmlCollection = parentDummy.childNodes;
|
||||
if (true) {
|
||||
throwIf(htmlCollection.length !== 1, NO_MULTI_MARKUP);
|
||||
}
|
||||
parentNode.replaceChild(htmlCollection[0], childNode);
|
||||
}
|
||||
|
||||
var Danger = {
|
||||
dangerouslyInsertMarkupAt: dangerouslyInsertMarkupAt,
|
||||
dangerouslyReplaceNodeWithMarkup: dangerouslyReplaceNodeWithMarkup
|
||||
};
|
||||
|
||||
module.exports = Danger;
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 DefaultEventPluginOrder
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var keyOf = require("./keyOf");
|
||||
|
||||
/**
|
||||
* Module that is injectable into `EventPluginHub`, that specifies a
|
||||
* deterministic ordering of `EventPlugin`s. A convenient way to reason about
|
||||
* plugins, without having to package every one of them. This is better than
|
||||
* having plugins be ordered in the same order that they are injected because
|
||||
* that ordering would be influenced by the packaging order.
|
||||
* `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that
|
||||
* preventing default on events is convenient in `SimpleEventPlugin` handlers.
|
||||
*/
|
||||
var DefaultEventPluginOrder = [
|
||||
keyOf({ResponderEventPlugin: null}),
|
||||
keyOf({SimpleEventPlugin: null}),
|
||||
keyOf({TapEventPlugin: null}),
|
||||
keyOf({EnterLeaveEventPlugin: null}),
|
||||
keyOf({AnalyticsEventPlugin: null})
|
||||
];
|
||||
|
||||
module.exports = DefaultEventPluginOrder;
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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 EnterLeaveEventPlugin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var EventPropagators = require("./EventPropagators");
|
||||
var ExecutionEnvironment = require("./ExecutionEnvironment");
|
||||
var AbstractEvent = require("./AbstractEvent");
|
||||
var EventConstants = require("./EventConstants");
|
||||
var ReactInstanceHandles = require("./ReactInstanceHandles");
|
||||
|
||||
var getDOMNodeID = require("./getDOMNodeID");
|
||||
var keyOf = require("./keyOf");
|
||||
|
||||
var topLevelTypes = EventConstants.topLevelTypes;
|
||||
var getFirstReactDOM = ReactInstanceHandles.getFirstReactDOM;
|
||||
|
||||
var abstractEventTypes = {
|
||||
mouseEnter: {registrationName: keyOf({onMouseEnter: null})},
|
||||
mouseLeave: {registrationName: keyOf({onMouseLeave: null})}
|
||||
};
|
||||
|
||||
/**
|
||||
* For almost every interaction we care about, there will be a top level
|
||||
* `mouseOver` and `mouseOut` event that occur so we can usually only pay
|
||||
* attention to one of the two (we'll pay attention to the `mouseOut` event) to
|
||||
* avoid extracting a duplicate event. However, there's one interaction where
|
||||
* there will be no `mouseOut` event to rely on - mousing from outside the
|
||||
* browser *into* the chrome. We detect this scenario and only in that case, we
|
||||
* use the `mouseOver` event.
|
||||
*
|
||||
* @see EventPluginHub.extractAbstractEvents
|
||||
*/
|
||||
var extractAbstractEvents = function(
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
renderedTargetID,
|
||||
renderedTarget) {
|
||||
|
||||
if (topLevelType === topLevelTypes.topMouseOver &&
|
||||
(nativeEvent.relatedTarget || nativeEvent.fromElement)) {
|
||||
return;
|
||||
}
|
||||
if (topLevelType !== topLevelTypes.topMouseOut &&
|
||||
topLevelType !== topLevelTypes.topMouseOver){
|
||||
return null; // Must not be a mouse in or mouse out - ignoring.
|
||||
}
|
||||
|
||||
var to, from;
|
||||
if (topLevelType === topLevelTypes.topMouseOut) {
|
||||
to = getFirstReactDOM(nativeEvent.relatedTarget || nativeEvent.toElement) ||
|
||||
ExecutionEnvironment.global;
|
||||
from = renderedTarget;
|
||||
} else {
|
||||
to = renderedTarget;
|
||||
from = ExecutionEnvironment.global;
|
||||
}
|
||||
|
||||
// Nothing pertains to our managed components.
|
||||
if (from === to ) {
|
||||
return;
|
||||
}
|
||||
|
||||
var fromID = from ? getDOMNodeID(from) : '';
|
||||
var toID = to ? getDOMNodeID(to) : '';
|
||||
var leave = AbstractEvent.getPooled(
|
||||
abstractEventTypes.mouseLeave,
|
||||
fromID,
|
||||
topLevelType,
|
||||
nativeEvent
|
||||
);
|
||||
var enter = AbstractEvent.getPooled(
|
||||
abstractEventTypes.mouseEnter,
|
||||
toID,
|
||||
topLevelType,
|
||||
nativeEvent
|
||||
);
|
||||
EventPropagators.accumulateEnterLeaveDispatches(leave, enter, fromID, toID);
|
||||
return [leave, enter];
|
||||
};
|
||||
|
||||
var EnterLeaveEventPlugin = {
|
||||
abstractEventTypes: abstractEventTypes,
|
||||
extractAbstractEvents: extractAbstractEvents
|
||||
};
|
||||
|
||||
module.exports = EnterLeaveEventPlugin;
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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 EventConstants
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var keyMirror = require("./keyMirror");
|
||||
|
||||
var PropagationPhases = keyMirror({bubbled: null, captured: null});
|
||||
|
||||
/**
|
||||
* Types of raw signals from the browser caught at the top level.
|
||||
*/
|
||||
var topLevelTypes = keyMirror({
|
||||
topBlur: null,
|
||||
topChange: null,
|
||||
topClick: null,
|
||||
topDOMCharacterDataModified: null,
|
||||
topDoubleClick: null,
|
||||
topFocus: null,
|
||||
topKeyDown: null,
|
||||
topKeyPress: null,
|
||||
topKeyUp: null,
|
||||
topMouseDown: null,
|
||||
topMouseMove: null,
|
||||
topMouseOut: null,
|
||||
topMouseOver: null,
|
||||
topMouseUp: null,
|
||||
topMouseWheel: null,
|
||||
topScroll: null,
|
||||
topSubmit: null,
|
||||
topTouchCancel: null,
|
||||
topTouchEnd: null,
|
||||
topTouchMove: null,
|
||||
topTouchStart: null
|
||||
});
|
||||
|
||||
var EventConstants = {
|
||||
topLevelTypes: topLevelTypes,
|
||||
PropagationPhases: PropagationPhases
|
||||
};
|
||||
|
||||
module.exports = EventConstants;
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Upstream version of event listener. Does not take into account specific
|
||||
* nature of platform.
|
||||
*/
|
||||
var EventListener = {
|
||||
/**
|
||||
* Listens to bubbled events on a DOM node.
|
||||
*
|
||||
* @param {Element} el DOM element to register listener on.
|
||||
* @param {string} handlerBaseName 'click'/'mouseover'
|
||||
* @param {Function!} cb Callback function
|
||||
*/
|
||||
listen: function(el, handlerBaseName, cb) {
|
||||
if (el.addEventListener) {
|
||||
el.addEventListener(handlerBaseName, cb, false);
|
||||
} else if (el.attachEvent) {
|
||||
el.attachEvent('on' + handlerBaseName, cb);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Listens to captured events on a DOM node.
|
||||
*
|
||||
* @see `EventListener.listen` for params.
|
||||
* @throws Exception if addEventListener is not supported.
|
||||
*/
|
||||
capture: function(el, handlerBaseName, cb) {
|
||||
if (!el.addEventListener) {
|
||||
console.error(
|
||||
'You are attempting to use addEventlistener ' +
|
||||
'in a browser that does not support it support it.' +
|
||||
'This likely means that you will not receive events that ' +
|
||||
'your application relies on (such as scroll).');
|
||||
return;
|
||||
} else {
|
||||
el.addEventListener(handlerBaseName, cb, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = EventListener;
|
||||
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* 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 EventPluginHub
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var AbstractEvent = require("./AbstractEvent");
|
||||
var CallbackRegistry = require("./CallbackRegistry");
|
||||
var EventPluginUtils = require("./EventPluginUtils");
|
||||
var EventPropagators = require("./EventPropagators");
|
||||
var ExecutionEnvironment = require("./ExecutionEnvironment");
|
||||
|
||||
var accumulate = require("./accumulate");
|
||||
var forEachAccumulated = require("./forEachAccumulated");
|
||||
var keyMirror = require("./keyMirror");
|
||||
var merge = require("./merge");
|
||||
var throwIf = require("./throwIf");
|
||||
|
||||
var deleteListener = CallbackRegistry.deleteListener;
|
||||
|
||||
var ERRORS = keyMirror({
|
||||
DOUBLE_REGISTER: null,
|
||||
DOUBLE_ENQUEUE: null,
|
||||
DEPENDENCY_ERROR: null
|
||||
});
|
||||
|
||||
if (true) {
|
||||
ERRORS.DOUBLE_REGISTER =
|
||||
'You\'ve included an event plugin that extracts an ' +
|
||||
'event type with the exact same or identity as an event that ' +
|
||||
'had previously been injected - or, one of the registration names ' +
|
||||
'used by an plugin has already been used.';
|
||||
ERRORS.DOUBLE_ENQUEUE =
|
||||
'During the processing of events, more events were enqueued. This ' +
|
||||
'is strange and should not happen. Please report immediately. ';
|
||||
ERRORS.DEPENDENCY_ERROR =
|
||||
'You have either attempted to load an event plugin that has no ' +
|
||||
'entry in EventPluginOrder, or have attempted to extract events ' +
|
||||
'when some critical dependencies have not yet been injected.';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* EventPluginHub: To see a diagram and explanation of the overall architecture
|
||||
* of the plugin hub system, @see ReactEvents.js
|
||||
*/
|
||||
|
||||
/**
|
||||
* Injected Dependencies:
|
||||
*/
|
||||
var injection = {
|
||||
/**
|
||||
* [required] Dependency of `EventPropagators`.
|
||||
*/
|
||||
injectInstanceHandle: function(InjectedInstanceHandle) {
|
||||
EventPropagators.injection.injectInstanceHandle(InjectedInstanceHandle);
|
||||
},
|
||||
|
||||
/**
|
||||
* `EventPluginOrder`: [optional] Provides deterministic ordering of
|
||||
* `EventPlugin`s. Ordering decoupled from the injection of actual
|
||||
* plugins so that there is always a deterministic and permanent ordering
|
||||
* regardless of the plugins that happen to become packaged, or applications
|
||||
* that happen to inject on-the-fly event plugins.
|
||||
*/
|
||||
EventPluginOrder: null,
|
||||
injectEventPluginOrder: function(InjectedEventPluginOrder) {
|
||||
injection.EventPluginOrder = InjectedEventPluginOrder;
|
||||
injection._recomputePluginsList();
|
||||
},
|
||||
|
||||
/**
|
||||
* `EventPlugins`: [optional][lazy-loadable] Plugins that must be listed in
|
||||
* the `EventPluginOrder` injected dependency. The list of plugins may grow as
|
||||
* custom plugins are injected into the system at page as part of the
|
||||
* initialization process, or even after the initialization process (on-the-
|
||||
* fly). Plugins injected into the hub have an opportunity to infer abstract
|
||||
* events when top level events are streamed through the `EventPluginHub`.
|
||||
*/
|
||||
plugins: [],
|
||||
injectEventPluginsByName: function(pluginsByName) {
|
||||
injection.pluginsByName = merge(injection.pluginsByName, pluginsByName);
|
||||
injection._recomputePluginsList();
|
||||
},
|
||||
|
||||
/**
|
||||
* A reference of all injected plugins by their name. Both plugins and
|
||||
* `EventPluginOrder` can be injected on-the-fly. Any time either dependency
|
||||
* is (re)injected, the resulting list of plugins in correct order is
|
||||
* recomputed.
|
||||
*/
|
||||
pluginsByName: {},
|
||||
_recomputePluginsList: function() {
|
||||
var injectPluginByName = function(name, PluginModule) {
|
||||
var pluginIndex = injection.EventPluginOrder.indexOf(name);
|
||||
throwIf(pluginIndex === -1, ERRORS.DEPENDENCY_ERROR + name);
|
||||
if (!injection.plugins[pluginIndex]) {
|
||||
injection.plugins[pluginIndex] = PluginModule;
|
||||
for (var eventName in PluginModule.abstractEventTypes) {
|
||||
var eventType = PluginModule.abstractEventTypes[eventName];
|
||||
recordAllRegistrationNames(eventType, PluginModule);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (injection.EventPluginOrder) { // Else, do when plugin order injected
|
||||
var injectedPluginsByName = injection.pluginsByName;
|
||||
for (var name in injectedPluginsByName) {
|
||||
injectPluginByName(name, injectedPluginsByName[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* `renderedTarget`: We'll refer to the concept of a "rendered target". Any
|
||||
* framework code that uses the `EventPluginHub` will stream events to the
|
||||
* `EventPluginHub`. In order for registered plugins to make sense of the native
|
||||
* events, it helps to allow plugins to not only have access to the native
|
||||
* `Event` object but also the notion of a "rendered target", which is the
|
||||
* conceptual target `Element` of the `Event` that the framework (user of
|
||||
* `EventPluginHub`) deems as being the "important" target.
|
||||
*/
|
||||
/**
|
||||
* Keeps track of all valid "registration names" (`onClick` etc). We expose
|
||||
* these structures to other modules, who will always see updates that we apply
|
||||
* to these structures. They should never have a desire to mutate these.
|
||||
*/
|
||||
var registrationNames = {};
|
||||
|
||||
/**
|
||||
* This prevents the need for clients to call `Object.keys(registrationNames)`
|
||||
* every time they want to loop through the possible registration names.
|
||||
*/
|
||||
var registrationNamesArr = [];
|
||||
|
||||
/**
|
||||
* Internal queue of events that have accumulated their dispatches and are
|
||||
* waiting to have their dispatches executed.
|
||||
*/
|
||||
var abstractEventQueue = [];
|
||||
|
||||
/**
|
||||
* Records all the registration names that the event plugin makes available
|
||||
* to the general event system. These are things like
|
||||
* `onClick`/`onClickCapture`.
|
||||
*/
|
||||
function recordAllRegistrationNames(eventType, PluginModule) {
|
||||
var phaseName;
|
||||
var phasedRegistrationNames = eventType.phasedRegistrationNames;
|
||||
if (phasedRegistrationNames) {
|
||||
for (phaseName in phasedRegistrationNames) {
|
||||
if (!phasedRegistrationNames.hasOwnProperty(phaseName)) {
|
||||
continue;
|
||||
}
|
||||
if (true) {
|
||||
throwIf(
|
||||
registrationNames[phasedRegistrationNames[phaseName]],
|
||||
ERRORS.DOUBLE_REGISTER
|
||||
);
|
||||
}
|
||||
registrationNames[phasedRegistrationNames[phaseName]] = PluginModule;
|
||||
registrationNamesArr.push(phasedRegistrationNames[phaseName]);
|
||||
}
|
||||
} else if (eventType.registrationName) {
|
||||
if (true) {
|
||||
throwIf(
|
||||
registrationNames[eventType.registrationName],
|
||||
ERRORS.DOUBLE_REGISTER
|
||||
);
|
||||
}
|
||||
registrationNames[eventType.registrationName] = PluginModule;
|
||||
registrationNamesArr.push(eventType.registrationName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A hacky way to reverse engineer which event plugin module created an
|
||||
* AbstractEvent.
|
||||
* @param {AbstractEvent} abstractEvent to look at
|
||||
*/
|
||||
function getPluginModuleForAbstractEvent(abstractEvent) {
|
||||
if (abstractEvent.type.registrationName) {
|
||||
return registrationNames[abstractEvent.type.registrationName];
|
||||
} else {
|
||||
for (var phase in abstractEvent.type.phasedRegistrationNames) {
|
||||
if (!abstractEvent.type.phasedRegistrationNames.hasOwnProperty(phase)) {
|
||||
continue;
|
||||
}
|
||||
var PluginModule = registrationNames[
|
||||
abstractEvent.type.phasedRegistrationNames[phase]
|
||||
];
|
||||
if (PluginModule) {
|
||||
return PluginModule;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var deleteAllListeners = function(domID) {
|
||||
var ii;
|
||||
for (ii = 0; ii < registrationNamesArr.length; ii++) {
|
||||
deleteListener(domID, registrationNamesArr[ii]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Accepts the stream of top level native events, and gives every registered
|
||||
* plugin an opportunity to extract `AbstractEvent`s with annotated dispatches.
|
||||
*
|
||||
* @param {Enum} topLevelType Record from `EventConstants`.
|
||||
* @param {Event} nativeEvent A Standard Event with fixed `target` property.
|
||||
* @param {Element} renderedTarget Element of interest to the framework, usually
|
||||
* the same as `nativeEvent.target` but occasionally an element immediately
|
||||
* above `nativeEvent.target` (the first DOM node recognized as one "rendered"
|
||||
* by the framework at hand.)
|
||||
* @param {string} renderedTargetID string ID of `renderedTarget`.
|
||||
*/
|
||||
var extractAbstractEvents =
|
||||
function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) {
|
||||
var abstractEvents;
|
||||
var plugins = injection.plugins;
|
||||
var len = plugins.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
// Not every plugin in the ordering may be loaded at runtime.
|
||||
var possiblePlugin = plugins[i];
|
||||
var extractedAbstractEvents =
|
||||
possiblePlugin &&
|
||||
possiblePlugin.extractAbstractEvents(
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
renderedTargetID,
|
||||
renderedTarget
|
||||
);
|
||||
if (extractedAbstractEvents) {
|
||||
abstractEvents = accumulate(abstractEvents, extractedAbstractEvents);
|
||||
}
|
||||
}
|
||||
return abstractEvents;
|
||||
};
|
||||
|
||||
var enqueueAbstractEvents = function(abstractEvents) {
|
||||
if (abstractEvents) {
|
||||
abstractEventQueue = accumulate(abstractEventQueue, abstractEvents);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a single abstract event dispatch. Returns a value, but this return
|
||||
* value doesn't make much sense when executing dispatches for a list of a
|
||||
* events. However, if a plugin executes a single dispatch, mostly bypassing
|
||||
* `EventPluginHub`, it can execute dispatches directly and assign special
|
||||
* meaning to the return value. So this will return the result of executing the
|
||||
* dispatch, though for most use cases, it gets dropped.
|
||||
*/
|
||||
var executeDispatchesAndRelease = function(abstractEvent) {
|
||||
if (abstractEvent) {
|
||||
var PluginModule = getPluginModuleForAbstractEvent(abstractEvent);
|
||||
var pluginExecuteDispatch = PluginModule && PluginModule.executeDispatch;
|
||||
EventPluginUtils.executeDispatchesInOrder(
|
||||
abstractEvent,
|
||||
pluginExecuteDispatch || EventPluginUtils.executeDispatch
|
||||
);
|
||||
AbstractEvent.release(abstractEvent);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets `abstractEventQueue` to null before processing it, so that we can tell
|
||||
* if in the process of processing events, more were enqueued. We throw if we
|
||||
* find that any were enqueued though this use case could be supported in the
|
||||
* future. For now, throwing an error as it's something we don't expect to
|
||||
* occur.
|
||||
*/
|
||||
var processAbstractEventQueue = function() {
|
||||
var processingAbstractEventQueue = abstractEventQueue;
|
||||
abstractEventQueue = null;
|
||||
forEachAccumulated(processingAbstractEventQueue, executeDispatchesAndRelease);
|
||||
if (true) {
|
||||
throwIf(abstractEventQueue, ERRORS.DOUBLE_ENQUEUE);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides a unified interface for an arbitrary and dynamic set of event
|
||||
* plugins. Loosely, a hub, where several "event plugins" may act as a single
|
||||
* event plugin behind the facade of `EventPluginHub`. Each event plugin injects
|
||||
* themselves into the HUB, and will immediately become operable upon injection.
|
||||
*
|
||||
* @constructor EventPluginHub
|
||||
*/
|
||||
var EventPluginHub = {
|
||||
registrationNames: registrationNames,
|
||||
registrationNamesArr: registrationNamesArr,
|
||||
putListener: CallbackRegistry.putListener,
|
||||
getListener: CallbackRegistry.getListener,
|
||||
deleteAllListeners: deleteAllListeners,
|
||||
extractAbstractEvents: extractAbstractEvents,
|
||||
enqueueAbstractEvents: enqueueAbstractEvents,
|
||||
processAbstractEventQueue: processAbstractEventQueue,
|
||||
injection: injection
|
||||
};
|
||||
|
||||
|
||||
if (ExecutionEnvironment.canUseDOM) {
|
||||
window.EventPluginHub = EventPluginHub;
|
||||
}
|
||||
|
||||
module.exports = EventPluginHub;
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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 EventPluginUtils
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var EventConstants = require("./EventConstants");
|
||||
var AbstractEvent = require("./AbstractEvent");
|
||||
|
||||
var invariant = require("./invariant");
|
||||
|
||||
var topLevelTypes = EventConstants.topLevelTypes;
|
||||
|
||||
function isEndish(topLevelType) {
|
||||
return topLevelType === topLevelTypes.topMouseUp ||
|
||||
topLevelType === topLevelTypes.topTouchEnd ||
|
||||
topLevelType === topLevelTypes.topTouchCancel;
|
||||
}
|
||||
|
||||
function isMoveish(topLevelType) {
|
||||
return topLevelType === topLevelTypes.topMouseMove ||
|
||||
topLevelType === topLevelTypes.topTouchMove;
|
||||
}
|
||||
function isStartish(topLevelType) {
|
||||
return topLevelType === topLevelTypes.topMouseDown ||
|
||||
topLevelType === topLevelTypes.topTouchStart;
|
||||
}
|
||||
|
||||
function storePageCoordsIn(obj, nativeEvent) {
|
||||
var pageX = AbstractEvent.eventPageX(nativeEvent);
|
||||
var pageY = AbstractEvent.eventPageY(nativeEvent);
|
||||
obj.pageX = pageX;
|
||||
obj.pageY = pageY;
|
||||
}
|
||||
|
||||
function eventDistance(coords, nativeEvent) {
|
||||
var pageX = AbstractEvent.eventPageX(nativeEvent);
|
||||
var pageY = AbstractEvent.eventPageY(nativeEvent);
|
||||
return Math.pow(
|
||||
Math.pow(pageX - coords.pageX, 2) + Math.pow(pageY - coords.pageY, 2),
|
||||
0.5
|
||||
);
|
||||
}
|
||||
|
||||
var validateEventDispatches;
|
||||
if (true) {
|
||||
validateEventDispatches = function(abstractEvent) {
|
||||
var dispatchListeners = abstractEvent._dispatchListeners;
|
||||
var dispatchIDs = abstractEvent._dispatchIDs;
|
||||
|
||||
var listenersIsArr = Array.isArray(dispatchListeners);
|
||||
var idsIsArr = Array.isArray(dispatchIDs);
|
||||
var IDsLen = idsIsArr ? dispatchIDs.length : dispatchIDs ? 1 : 0;
|
||||
var listenersLen = listenersIsArr ?
|
||||
dispatchListeners.length :
|
||||
dispatchListeners ? 1 : 0;
|
||||
|
||||
invariant(
|
||||
idsIsArr === listenersIsArr && IDsLen === listenersLen,
|
||||
'EventPluginUtils: Invalid `abstractEvent`.'
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes `cb(abstractEvent, listener, id)`. Avoids using call if no scope is
|
||||
* provided. The `(listener,id)` pair effectively forms the "dispatch" but are
|
||||
* kept separate to conserve memory.
|
||||
*/
|
||||
function forEachEventDispatch(abstractEvent, cb) {
|
||||
var dispatchListeners = abstractEvent._dispatchListeners;
|
||||
var dispatchIDs = abstractEvent._dispatchIDs;
|
||||
if (true) {
|
||||
validateEventDispatches(abstractEvent);
|
||||
}
|
||||
if (Array.isArray(dispatchListeners)) {
|
||||
var i;
|
||||
for (
|
||||
i = 0;
|
||||
i < dispatchListeners.length && !abstractEvent.isPropagationStopped;
|
||||
i++) {
|
||||
// Listeners and IDs are two parallel arrays that are always in sync.
|
||||
cb(abstractEvent, dispatchListeners[i], dispatchIDs[i]);
|
||||
}
|
||||
} else if (dispatchListeners) {
|
||||
cb(abstractEvent, dispatchListeners, dispatchIDs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of PluginModule.executeDispatch().
|
||||
* @param {AbstractEvent} AbstractEvent to handle
|
||||
* @param {function} Application-level callback
|
||||
* @param {string} domID DOM id to pass to the callback.
|
||||
*/
|
||||
function executeDispatch(abstractEvent, listener, domID) {
|
||||
listener(abstractEvent, domID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard/simple iteration through an event's collected dispatches.
|
||||
*/
|
||||
function executeDispatchesInOrder(abstractEvent, executeDispatch) {
|
||||
forEachEventDispatch(abstractEvent, executeDispatch);
|
||||
abstractEvent._dispatchListeners = null;
|
||||
abstractEvent._dispatchIDs = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard/simple iteration through an event's collected dispatches, but stops
|
||||
* at the first dispatch execution returning true, and returns that id.
|
||||
*
|
||||
* @returns id of the first dispatch execution who's listener returns true, or
|
||||
* null if no listener returned true.
|
||||
*/
|
||||
function executeDispatchesInOrderStopAtTrue(abstractEvent) {
|
||||
var dispatchListeners = abstractEvent._dispatchListeners;
|
||||
var dispatchIDs = abstractEvent._dispatchIDs;
|
||||
if (true) {
|
||||
validateEventDispatches(abstractEvent);
|
||||
}
|
||||
if (Array.isArray(dispatchListeners)) {
|
||||
var i;
|
||||
for (
|
||||
i = 0;
|
||||
i < dispatchListeners.length && !abstractEvent.isPropagationStopped;
|
||||
i++) {
|
||||
// Listeners and IDs are two parallel arrays that are always in sync.
|
||||
if (dispatchListeners[i](abstractEvent, dispatchIDs[i])) {
|
||||
return dispatchIDs[i];
|
||||
}
|
||||
}
|
||||
} else if (dispatchListeners) {
|
||||
if (dispatchListeners(abstractEvent, dispatchIDs)) {
|
||||
return dispatchIDs;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution of a "direct" dispatch - there must be at most one dispatch
|
||||
* accumulated on the event or it is considered an error. It doesn't really make
|
||||
* sense for an event with multiple dispatches (bubbled) to keep track of the
|
||||
* return values at each dispatch execution, but it does tend to make sense when
|
||||
* dealing with "direct" dispatches.
|
||||
*
|
||||
* @returns The return value of executing the single dispatch.
|
||||
*/
|
||||
function executeDirectDispatch(abstractEvent) {
|
||||
if (true) {
|
||||
validateEventDispatches(abstractEvent);
|
||||
}
|
||||
var dispatchListener = abstractEvent._dispatchListeners;
|
||||
var dispatchID = abstractEvent._dispatchIDs;
|
||||
invariant(
|
||||
!Array.isArray(dispatchListener),
|
||||
'executeDirectDispatch(...): Invalid `abstractEvent`.'
|
||||
);
|
||||
var res = dispatchListener ?
|
||||
dispatchListener(abstractEvent, dispatchID) :
|
||||
null;
|
||||
abstractEvent._dispatchListeners = null;
|
||||
abstractEvent._dispatchIDs = null;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AbstractEvent} abstractEvent
|
||||
* @returns {bool} True iff number of dispatches accumulated is greater than 0.
|
||||
*/
|
||||
function hasDispatches(abstractEvent) {
|
||||
return !!abstractEvent._dispatchListeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* General utilities that are useful in creating custom Event Plugins.
|
||||
*/
|
||||
var EventPluginUtils = {
|
||||
isEndish: isEndish,
|
||||
isMoveish: isMoveish,
|
||||
isStartish: isStartish,
|
||||
storePageCoordsIn: storePageCoordsIn,
|
||||
eventDistance: eventDistance,
|
||||
executeDispatchesInOrder: executeDispatchesInOrder,
|
||||
executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue,
|
||||
executeDirectDispatch: executeDirectDispatch,
|
||||
hasDispatches: hasDispatches,
|
||||
executeDispatch: executeDispatch
|
||||
};
|
||||
|
||||
module.exports = EventPluginUtils;
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 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 EventPropagators
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var CallbackRegistry = require("./CallbackRegistry");
|
||||
var EventConstants = require("./EventConstants");
|
||||
|
||||
var accumulate = require("./accumulate");
|
||||
var forEachAccumulated = require("./forEachAccumulated");
|
||||
var getListener = CallbackRegistry.getListener;
|
||||
var PropagationPhases = EventConstants.PropagationPhases;
|
||||
|
||||
/**
|
||||
* 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 (true) {
|
||||
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
|
||||
* "phases" of propagation. This finds listeners by a given phase.
|
||||
*/
|
||||
function listenerAtPhase(id, abstractEvent, propagationPhase) {
|
||||
var registrationName =
|
||||
abstractEvent.type.phasedRegistrationNames[propagationPhase];
|
||||
return getListener(id, registrationName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tags an `AbstractEvent` with dispatched listeners. Creating this function
|
||||
* here, allows us to not have to bind or create functions for each event.
|
||||
* Mutating the event's members allows us to not have to create a wrapping
|
||||
* "dispatch" object that pairs the event with the listener.
|
||||
*/
|
||||
function accumulateDirectionalDispatches(domID, upwards, abstractEvent) {
|
||||
if (true) {
|
||||
if (!domID) {
|
||||
throw new Error('Dispatching id must not be null');
|
||||
}
|
||||
injection.validate();
|
||||
}
|
||||
var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
|
||||
var listener = listenerAtPhase(domID, abstractEvent, phase);
|
||||
if (listener) {
|
||||
abstractEvent._dispatchListeners =
|
||||
accumulate(abstractEvent._dispatchListeners, listener);
|
||||
abstractEvent._dispatchIDs = accumulate(abstractEvent._dispatchIDs, domID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect dispatches (must be entirely collected before dispatching - see unit
|
||||
* tests). Lazily allocate the array to conserve memory. We must loop through
|
||||
* each event and perform the traversal for each one. We can not perform a
|
||||
* single traversal for the entire collection of events because each event may
|
||||
* have a different target.
|
||||
*/
|
||||
function accumulateTwoPhaseDispatchesSingle(abstractEvent) {
|
||||
if (abstractEvent && abstractEvent.type.phasedRegistrationNames) {
|
||||
injection.InstanceHandle.traverseTwoPhase(
|
||||
abstractEvent.abstractTargetID,
|
||||
accumulateDirectionalDispatches,
|
||||
abstractEvent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Accumulates without regard to direction, does not look for phased
|
||||
* registration names. Same as `accumulateDirectDispatchesSingle` but without
|
||||
* requiring that the `abstractTargetID` be the same as the dispatched ID.
|
||||
*/
|
||||
function accumulateDispatches(id, ignoredDirection, abstractEvent) {
|
||||
if (abstractEvent && abstractEvent.type.registrationName) {
|
||||
var listener = getListener(id, abstractEvent.type.registrationName);
|
||||
if (listener) {
|
||||
abstractEvent._dispatchListeners =
|
||||
accumulate(abstractEvent._dispatchListeners, listener);
|
||||
abstractEvent._dispatchIDs = accumulate(abstractEvent._dispatchIDs, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accumulates dispatches on an `AbstractEvent`, but only for the
|
||||
* `abstractTargetID`.
|
||||
* @param {AbstractEvent} abstractEvent
|
||||
*/
|
||||
function accumulateDirectDispatchesSingle(abstractEvent) {
|
||||
if (abstractEvent && abstractEvent.type.registrationName) {
|
||||
accumulateDispatches(abstractEvent.abstractTargetID, null, abstractEvent);
|
||||
}
|
||||
}
|
||||
|
||||
function accumulateTwoPhaseDispatches(abstractEvents) {
|
||||
if (true) {
|
||||
injection.validate();
|
||||
}
|
||||
forEachAccumulated(abstractEvents, accumulateTwoPhaseDispatchesSingle);
|
||||
}
|
||||
|
||||
function accumulateEnterLeaveDispatches(leave, enter, fromID, toID) {
|
||||
if (true) {
|
||||
injection.validate();
|
||||
}
|
||||
injection.InstanceHandle.traverseEnterLeave(
|
||||
fromID,
|
||||
toID,
|
||||
accumulateDispatches,
|
||||
leave,
|
||||
enter
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function accumulateDirectDispatches(abstractEvents) {
|
||||
if (true) {
|
||||
injection.validate();
|
||||
}
|
||||
forEachAccumulated(abstractEvents, accumulateDirectDispatchesSingle);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A small set of propagation patterns, each of which will accept a small amount
|
||||
* of information, and generate a set of "dispatch ready event objects" - which
|
||||
* are sets of events that have already been annotated with a set of dispatched
|
||||
* listener functions/ids. The API is designed this way to discourage these
|
||||
* propagation strategies from actually executing the dispatches, since we
|
||||
* always want to collect the entire set of dispatches before executing event a
|
||||
* single one.
|
||||
*
|
||||
* @constructor EventPropagators
|
||||
*/
|
||||
var EventPropagators = {
|
||||
accumulateTwoPhaseDispatches: accumulateTwoPhaseDispatches,
|
||||
accumulateDirectDispatches: accumulateDirectDispatches,
|
||||
accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches,
|
||||
injection: injection
|
||||
};
|
||||
|
||||
module.exports = EventPropagators;
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 ExecutionEnvironment
|
||||
*/
|
||||
|
||||
/*jslint evil: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var canUseDOM = typeof window !== 'undefined';
|
||||
|
||||
/**
|
||||
* Simple, lightweight module assisting with the detection and context of
|
||||
* Worker. Helps avoid circular dependencies and allows code to reason about
|
||||
* whether or not they are in a Worker, even if they never include the main
|
||||
* `ReactWorker` dependency.
|
||||
*/
|
||||
var ExecutionEnvironment = {
|
||||
|
||||
canUseDOM: canUseDOM,
|
||||
|
||||
canUseWorkers: typeof Worker !== 'undefined',
|
||||
|
||||
isInWorker: !canUseDOM, // For now, this is true - might change in the future.
|
||||
|
||||
global: new Function('return this;')()
|
||||
|
||||
};
|
||||
|
||||
module.exports = ExecutionEnvironment;
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* 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 ImmutableObject
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var keyMirror = require("./keyMirror");
|
||||
var merge = require("./merge");
|
||||
var mergeInto = require("./mergeInto");
|
||||
var mergeHelpers = require("./mergeHelpers");
|
||||
var throwIf = require("./throwIf");
|
||||
|
||||
var checkMergeObjectArgs = mergeHelpers.checkMergeObjectArgs;
|
||||
var isTerminal = mergeHelpers.isTerminal;
|
||||
|
||||
/**
|
||||
* Simple wrapper around javascript key/value objects that provide a dev time
|
||||
* guarantee of immutability (assuming all modules that may potentially mutate
|
||||
* include a "use strict" declaration). Retaining immutability requires CPU
|
||||
* cycles (in order to perform the freeze), but this computation can be avoided
|
||||
* in production. The fact that mutation attempts in __DEV__ will be caught,
|
||||
* allows us to reasonably assume that mutation on those objects won't even be
|
||||
* attempted in production. This means that an object being an instanceof
|
||||
* `ImmutableObject` implies that the object may never change.
|
||||
*
|
||||
* TODO: Require strict mode in source files that use ImmutableObject (lint
|
||||
* rule).
|
||||
*
|
||||
* @class ImmutableObject
|
||||
*/
|
||||
|
||||
var ERRORS;
|
||||
var ImmutableObject;
|
||||
|
||||
if (true) {
|
||||
ERRORS = {
|
||||
INVALID_MAP_SET_ARG: 'You have attempted to set fields on an object that ' +
|
||||
'is not an instance of ImmutableObject'
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructs an instance of `ImmutableObject`.
|
||||
*
|
||||
* @param {!Object} initMap The initial set of properties.
|
||||
* @constructor
|
||||
*/
|
||||
ImmutableObject = function(initMap) {
|
||||
mergeInto(this, initMap);
|
||||
deepFreeze(this, initMap);
|
||||
};
|
||||
|
||||
/**
|
||||
* Objects that are instances of `ImmutableObject` are assumed to be deep
|
||||
* frozen.
|
||||
* @param {!Object} o The object to deep freeze.
|
||||
* @return {!boolean} Whether or not deep freeze is needed.
|
||||
*/
|
||||
var shouldRecurseFreeze = function(o) {
|
||||
return (typeof o) === 'object' &&
|
||||
!(o instanceof ImmutableObject) && o !== null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Freezes an object `o` deeply. Invokes `shouldRecurseFreeze` to determine if
|
||||
* further freezing is needed.
|
||||
*
|
||||
* @param {!Object} o The object to freeze.
|
||||
*/
|
||||
var deepFreeze = function(o) {
|
||||
var prop;
|
||||
Object.freeze(o); // First freeze the object.
|
||||
for (prop in o) {
|
||||
var field = o[prop];
|
||||
if (o.hasOwnProperty(prop) && shouldRecurseFreeze(field)) {
|
||||
deepFreeze(field);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a new ImmutableObject that is that is the same as the parameter
|
||||
* `immutableObject` but with the differences specified in `put`.
|
||||
* @param {!ImmutableObject} ImmutableObject The ImmutableObject object to set
|
||||
* fields on.
|
||||
* @param {!Object} put Subset of fields to merge into the returned result.
|
||||
* @return {!ImmutableObject} The result of merging in `put` fields.
|
||||
*/
|
||||
ImmutableObject.set = function(immutableObject, put) {
|
||||
throwIf(
|
||||
!(immutableObject instanceof ImmutableObject),
|
||||
ERRORS.INVALID_MAP_SET_ARG
|
||||
);
|
||||
var totalNewFields = merge(immutableObject, put);
|
||||
return new ImmutableObject(totalNewFields);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sugar for `ImmutableObject.set(ImmutableObject, {fieldName: putField})`
|
||||
* @see ImmutableObject.set
|
||||
*/
|
||||
ImmutableObject.setField = function(immutableObject, fieldName, putField) {
|
||||
var put = {};
|
||||
put[fieldName] = putField;
|
||||
return ImmutableObject.set(immutableObject, put);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a new ImmutableObject that is that is the same as the parameter
|
||||
* `immutableObject` but with the differences specified in `put` recursively
|
||||
* applied.
|
||||
*/
|
||||
ImmutableObject.setDeep = function(immutableObject, put) {
|
||||
throwIf(
|
||||
!(immutableObject instanceof ImmutableObject),
|
||||
ERRORS.INVALID_MAP_SET_ARG
|
||||
);
|
||||
return _setDeep(immutableObject, put);
|
||||
};
|
||||
|
||||
function _setDeep(object, put) {
|
||||
checkMergeObjectArgs(object, put);
|
||||
var totalNewFields = {};
|
||||
|
||||
// To maintain the order of the keys, copy the base object's entries first.
|
||||
var keys = Object.keys(object);
|
||||
for (var ii = 0; ii < keys.length; ii++) {
|
||||
var key = keys[ii];
|
||||
if (!put.hasOwnProperty(key)) {
|
||||
totalNewFields[key] = object[key];
|
||||
} else if (isTerminal(object[key]) || isTerminal(put[key])) {
|
||||
totalNewFields[key] = put[key];
|
||||
} else {
|
||||
totalNewFields[key] = _setDeep(object[key], put[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply any new keys that the base object didn't have.
|
||||
var newKeys = Object.keys(put);
|
||||
for (ii = 0; ii < newKeys.length; ii++) {
|
||||
var newKey = newKeys[ii];
|
||||
if (object.hasOwnProperty(newKey)) {
|
||||
continue;
|
||||
}
|
||||
totalNewFields[newKey] = put[newKey];
|
||||
}
|
||||
|
||||
return (object instanceof ImmutableObject || put instanceof ImmutableObject) ?
|
||||
new ImmutableObject(totalNewFields) :
|
||||
totalNewFields;
|
||||
}
|
||||
|
||||
module.exports = ImmutableObject;
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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 NormalizedEventListener
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var EventListener = require("./EventListener");
|
||||
|
||||
/**
|
||||
* @param {?Event} eventParam Event parameter from an attached listener.
|
||||
* @return {Event} Normalized event object.
|
||||
* @private
|
||||
*/
|
||||
function normalizeEvent(eventParam) {
|
||||
var normalized = eventParam || window.event;
|
||||
// In some browsers (OLD FF), setting the target throws an error. A good way
|
||||
// to tell if setting the target will throw an error, is to check if the event
|
||||
// has a `target` property. Safari events have a `target` but it's not always
|
||||
// normalized. Even if a `target` property exists, it's good to only set the
|
||||
// target property if we realize that a change will actually take place.
|
||||
var hasTargetProperty = 'target' in normalized;
|
||||
var eventTarget = normalized.target || normalized.srcElement || window;
|
||||
// Safari may fire events on text nodes (Node.TEXT_NODE is 3)
|
||||
// @see http://www.quirksmode.org/js/events_properties.html
|
||||
var textNodeNormalizedTarget =
|
||||
(eventTarget.nodeType === 3) ? eventTarget.parentNode : eventTarget;
|
||||
if (!hasTargetProperty || normalized.target !== textNodeNormalizedTarget) {
|
||||
// Create an object that inherits from the native event so that we can set
|
||||
// `target` on it. (It is read-only and setting it throws in strict mode).
|
||||
normalized = Object.create(normalized);
|
||||
normalized.target = textNodeNormalizedTarget;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function createNormalizedCallback(cb) {
|
||||
return function(unfixedNativeEvent) {
|
||||
cb(normalizeEvent(unfixedNativeEvent));
|
||||
};
|
||||
}
|
||||
|
||||
var NormalizedEventListener = {
|
||||
|
||||
/**
|
||||
* Listens to bubbled events on a DOM node.
|
||||
*
|
||||
* NOTE: The listener will be invoked with a normalized event object.
|
||||
*
|
||||
* @param {DOMElement} el DOM element to register listener on.
|
||||
* @param {string} handlerBaseName Event name, e.g. "click".
|
||||
* @param {function} cb Callback function.
|
||||
* @public
|
||||
*/
|
||||
listen: function(el, handlerBaseName, cb) {
|
||||
EventListener.listen(el, handlerBaseName, createNormalizedCallback(cb));
|
||||
},
|
||||
|
||||
/**
|
||||
* Listens to captured events on a DOM node.
|
||||
*
|
||||
* NOTE: The listener will be invoked with a normalized event object.
|
||||
*
|
||||
* @param {DOMElement} el DOM element to register listener on.
|
||||
* @param {string} handlerBaseName Event name, e.g. "click".
|
||||
* @param {function} cb Callback function.
|
||||
* @public
|
||||
*/
|
||||
capture: function(el, handlerBaseName, cb) {
|
||||
EventListener.capture(el, handlerBaseName, createNormalizedCallback(cb));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = NormalizedEventListener;
|
||||
@@ -0,0 +1,514 @@
|
||||
/**
|
||||
* 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 OrderedMap
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var mixInto = require("./mixInto");
|
||||
var throwIf = require("./throwIf");
|
||||
|
||||
var PREFIX = 'key:';
|
||||
|
||||
var ARRAY_MUST_CB = 'ARRAY_MUST_CB';
|
||||
var INVALID_KEY = 'INVALID_KEY';
|
||||
var DUPLICATE_KEY = 'DUPLICATE_KEY';
|
||||
var OPERATION_ARGS = 'OPERATION_ARGS';
|
||||
var RANGE_ARGS = 'RANGE_ARGS';
|
||||
var NO_EXIST = 'NO_EXIST';
|
||||
|
||||
if (true) {
|
||||
ARRAY_MUST_CB =
|
||||
'If providing an array to an OrderedMap constructor, you must provide ' +
|
||||
'a callback that may determine the uniqueue key for each entry. ' +
|
||||
'The key that you return from the callback should uniquely define that ' +
|
||||
'entity. What is returned from that function must answer the question: ' +
|
||||
'"If you were to shuffle the array of entities, how would you be able to ' +
|
||||
'determine the identity of the Array entry?"';
|
||||
INVALID_KEY =
|
||||
'Key must be non-empty, non-null, string or number';
|
||||
DUPLICATE_KEY =
|
||||
'At creation time, you must extract out IDs that are unique.';
|
||||
OPERATION_ARGS =
|
||||
'Invalid argument type for construction or operation on OrderedMap. ' +
|
||||
'from/merge accepts another OrderedMap. fromArray accepts an Array ' +
|
||||
'and a callback to extract a unique ID.';
|
||||
RANGE_ARGS =
|
||||
'OrderedMap.mapRange requires end of range to be >= start, start to be ' +
|
||||
'>= 0 and end < length.';
|
||||
NO_EXIST =
|
||||
'The requested key does not exist in the OrderedMap';
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to extract a backing object from an initialization `Array`, allowing
|
||||
* the caller to assist in resolving the unique ID for each entry via the
|
||||
* `keyExtractor` callback. The `keyExtractor` must extract non-empty strings or
|
||||
* numbers.
|
||||
* @param {Array<Object!>} arr Array of items.
|
||||
* @param {function} keyExtractor Extracts a unique key from each item.
|
||||
* @return {Object} Map from unique key to originating value that the key was
|
||||
* extracted from.
|
||||
* @throws Exception if the initialization array has duplicate extracted keys.
|
||||
*/
|
||||
function extractObjectFromArray(arr, keyExtractor) {
|
||||
var normalizedObj = {};
|
||||
for (var i=0; i < arr.length; i++) {
|
||||
var item = arr[i];
|
||||
var key = keyExtractor(item);
|
||||
validatePublicKey(key);
|
||||
var normalizedKey = PREFIX + key;
|
||||
throwIf(normalizedKey in normalizedObj, DUPLICATE_KEY);
|
||||
normalizedObj[normalizedKey] = item;
|
||||
}
|
||||
return normalizedObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for mappings with ordering. This class is to be used in an
|
||||
* immutable manner. A `OrderedMap` is very much like the native JavaScript
|
||||
* object, where keys map to values via the `get()` function. Also, like the
|
||||
* native JavaScript object, there is an ordering associated with the mapping.
|
||||
* This class is helpful because it eliminates many of the pitfalls that come
|
||||
* with the native JavaScript ordered mappings. Specifically, there are
|
||||
* inconsistencies with numeric keys in some JavaScript implementations
|
||||
* (enumeration ordering). This class protects against those pitfalls and
|
||||
* provides functional utilities for dealing with these `OrderedMap`s.
|
||||
*
|
||||
* - TODO:
|
||||
* - orderedMergeExclusive: Merges mutually exclusive `OrderedMap`s.
|
||||
* - mapReverse().
|
||||
*
|
||||
* @class {OrderedMap}
|
||||
* @constructor {OrderedMap}
|
||||
* @param {Object} normalizedObj Object that is known to be a defensive copy of
|
||||
* caller supplied data. We require a defensive copy to guard against callers
|
||||
* mutating. It is also assumed that the keys of `normalizedObj` have been
|
||||
* normalized and do not contain any numeric-appearing strings.
|
||||
* @param {number} computedLength The precomputed length of `_normalizedObj`
|
||||
* keys.
|
||||
* @private
|
||||
*/
|
||||
function OrderedMapImpl(normalizedObj, computedLength) {
|
||||
this._normalizedObj = normalizedObj;
|
||||
this._computedPositions = null;
|
||||
this.length = computedLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a "public" key - that is, one that the public facing API supplies.
|
||||
* The key is then normalized for internal storage. In order to be considered
|
||||
* valid, all keys must be non-empty, defined, non-null strings or numbers.
|
||||
* Since this already costs a function invocation, will avoid additional call to
|
||||
* `throwIf`.
|
||||
*
|
||||
* @param {string?} key Validates that the key is suitable for use in a
|
||||
* `OrderedMap`.
|
||||
* @throws Error if key is not appropriate for use in `OrderedMap`.
|
||||
*/
|
||||
function validatePublicKey(key) {
|
||||
var isEmpty = key === '';
|
||||
var correctKeyType = typeof key === 'string' || typeof key === 'number';
|
||||
if (isEmpty || !correctKeyType) {
|
||||
throw new Error(INVALID_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that arguments to range operations are within the correct limits.
|
||||
*
|
||||
* @param {number} start Start of range.
|
||||
* @param {number} length Length of range.
|
||||
* @param {number} actualLen Actual length of range that should not be
|
||||
* exceeded.
|
||||
* @return {void} description
|
||||
*/
|
||||
function validateRangeIndices(start, length, actualLen) {
|
||||
var startType = typeof start;
|
||||
var lengthType = typeof length;
|
||||
var invalid =
|
||||
startType !== 'number' || lengthType !== 'number' ||
|
||||
length < 0 || start < 0 || start + length > actualLen;
|
||||
if (invalid) {
|
||||
throw new Error(RANGE_ARGS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two "normalized" objects (objects who's key have been normalized) into
|
||||
* a `OrderedMap`.
|
||||
*
|
||||
* @param {Object} a Object of key value pairs.
|
||||
* @param {Object} b Object of key value pairs.
|
||||
* @return {OrderedMap} new `OrderedMap` that results in merging `a` and `b`.
|
||||
*/
|
||||
function _fromNormalizedObjects(a, b) {
|
||||
// Second optional, both must be a Plain Old JavaScript Object.
|
||||
var invalidArgs =
|
||||
!a || a.constructor !== Object || (b && b.constructor !== Object);
|
||||
throwIf(invalidArgs, OPERATION_ARGS);
|
||||
var newSet = {};
|
||||
var length = 0;
|
||||
var key;
|
||||
for (key in a) {
|
||||
if (a.hasOwnProperty(key)) {
|
||||
newSet[key] = a[key];
|
||||
length++;
|
||||
}
|
||||
}
|
||||
|
||||
for (key in b) {
|
||||
if (b.hasOwnProperty(key)) {
|
||||
// Increment length if not already added via first object (a)
|
||||
if (!(key in newSet)) {
|
||||
length++;
|
||||
}
|
||||
newSet[key] = b[key];
|
||||
}
|
||||
}
|
||||
return new OrderedMapImpl(newSet, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods for `OrderedMap` instances.
|
||||
*
|
||||
* @lends OrderedMap.prototype
|
||||
* TODO: Make this data structure lazy, unify with LazyArray.
|
||||
* TODO: Unify this with ImmutableObject - it is to be used immutably.
|
||||
* TODO: If so, consider providing `fromObject` API.
|
||||
* TODO: Create faster implementation of merging/mapping from original Array,
|
||||
* without having to first create an object - simply for the sake of merging.
|
||||
*/
|
||||
var OrderedMapMethods = {
|
||||
|
||||
/**
|
||||
* Returns whether or not a given key is present in the map.
|
||||
*
|
||||
* @param {string} key Valid string key to lookup membership for.
|
||||
* @return {boolean} Whether or not `key` is a member of the map.
|
||||
* @throws Error if provided known invalid key.
|
||||
*/
|
||||
has: function(key) {
|
||||
validatePublicKey(key);
|
||||
var normalizedKey = PREFIX + key;
|
||||
return normalizedKey in this._normalizedObj;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the object for a given key, or `undefined` if not present. To
|
||||
* distinguish a undefined entry vs not being in the set, use `has()`.
|
||||
*
|
||||
* @param {string} key String key to lookup the value for.
|
||||
* @return {Object?} Object at key `key`, or undefined if not in map.
|
||||
* @throws Error if provided known invalid key.
|
||||
*/
|
||||
get: function(key) {
|
||||
validatePublicKey(key);
|
||||
var normalizedKey = PREFIX + key;
|
||||
return this.has(key) ? this._normalizedObj[normalizedKey] : undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Merges, appending new keys to the end of the ordering. Keys in `orderedMap`
|
||||
* that are redundant with `this`, maintain the same ordering index that they
|
||||
* had in `this`. This is how standard JavaScript object merging would work.
|
||||
* If you wish to prepend a `OrderedMap` to the beginning of another
|
||||
* `OrderedMap` then simply reverse the order of operation. This is the analog
|
||||
* to `merge(x, y)`.
|
||||
*
|
||||
* @param {OrderedMap} orderedMap OrderedMap to merge onto the end.
|
||||
* @return {OrderedMap} New OrderedMap that represents the result of the
|
||||
* merge.
|
||||
*/
|
||||
merge: function(orderedMap) {
|
||||
throwIf(!(orderedMap instanceof OrderedMapImpl), OPERATION_ARGS);
|
||||
return _fromNormalizedObjects(
|
||||
this._normalizedObj,
|
||||
orderedMap._normalizedObj
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Functional map API. Returns a new `OrderedMap`.
|
||||
*
|
||||
* @param {Function} cb Callback to invoke for each item.
|
||||
* @param {Object?=} context Context to invoke callback from.
|
||||
* @returns {OrderedMap} OrderedMap that results from mapping.
|
||||
*/
|
||||
map: function(cb, context) {
|
||||
return this.mapRange(cb, 0, this.length, context);
|
||||
},
|
||||
|
||||
/**
|
||||
* The callback `cb` is invoked with the arguments (item, key,
|
||||
* indexInOriginal).
|
||||
*
|
||||
* @param {Function} cb Determines result for each item.
|
||||
* @param {number} start Start index of map range.
|
||||
* @param {end} end End index of map range.
|
||||
* @param {*!?} context Context of callback invocation.
|
||||
* @return {OrderedMap} OrderedMap resulting from mapping the range.
|
||||
*/
|
||||
mapRange: function(cb, start, length, context) {
|
||||
var thisSet = this._normalizedObj;
|
||||
var newSet = {};
|
||||
var i = 0;
|
||||
validateRangeIndices(start, length, this.length);
|
||||
var end = start + length - 1;
|
||||
for (var key in thisSet) {
|
||||
if (thisSet.hasOwnProperty(key)) {
|
||||
if (i >= start) {
|
||||
if (i > end) {
|
||||
break;
|
||||
}
|
||||
var item = thisSet[key];
|
||||
newSet[key] = cb.call(context, item, key.substr(PREFIX.length), i);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return new OrderedMapImpl(newSet, length);
|
||||
},
|
||||
|
||||
/**
|
||||
* Function filter API. Returns new `OrderedMap`.
|
||||
*
|
||||
* @param {Function} cb Callback to invoke for each item.
|
||||
* @param {Object?=} context Context to invoke callback from.
|
||||
* @returns {OrderedMap} OrderedMap that results from filtering.
|
||||
*/
|
||||
filter: function(cb, context) {
|
||||
return this.filterRange(cb, 0, this.length, context);
|
||||
},
|
||||
|
||||
/**
|
||||
* The callback `cb` is invoked with the arguments (item, key,
|
||||
* indexInOriginal).
|
||||
*
|
||||
* @param {Function} cb Returns true if item should be in result.
|
||||
* @param {number} start Start index of filter range.
|
||||
* @param {number} end End index of map range.
|
||||
* @param {*!?} context Context of callback invocation.
|
||||
* @return {OrderedMap} OrderedMap resulting from filtering the range.
|
||||
*/
|
||||
filterRange: function(cb, start, length, context) {
|
||||
var newSet = {};
|
||||
var newSetLength = 0;
|
||||
this.forEachRange(function(item, key, originalIndex) {
|
||||
if (cb.call(context, item, key, originalIndex)) {
|
||||
var normalizedKey = PREFIX + key;
|
||||
newSet[normalizedKey] = item;
|
||||
newSetLength++;
|
||||
}
|
||||
}, start, length);
|
||||
return new OrderedMapImpl(newSet, newSetLength);
|
||||
},
|
||||
|
||||
forEach: function(cb, context) {
|
||||
this.forEachRange(cb, 0, this.length, context);
|
||||
},
|
||||
|
||||
forEachRange: function(cb, start, length, context) {
|
||||
validateRangeIndices(start, length, this.length);
|
||||
var thisSet = this._normalizedObj;
|
||||
var i = 0;
|
||||
var end = start + length - 1;
|
||||
for (var key in thisSet) {
|
||||
if (thisSet.hasOwnProperty(key)) {
|
||||
if (i >= start) {
|
||||
if (i > end) {
|
||||
break;
|
||||
}
|
||||
var item = thisSet[key];
|
||||
cb.call(context, item, key.substr(PREFIX.length), i);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Even though `mapRange`/`forEachKeyRange` allow zero length mappings, we'll
|
||||
* impose an additional restriction here that the length of mapping be greater
|
||||
* than zero - the only reason is that there are many ways to express length
|
||||
* zero in terms of two keys and that is confusing.
|
||||
*/
|
||||
mapKeyRange: function(cb, startKey, endKey, context) {
|
||||
var startIndex = this.indexOfKey(startKey);
|
||||
var endIndex = this.indexOfKey(endKey);
|
||||
if (endIndex < startIndex) {
|
||||
throw new Error(RANGE_ARGS);
|
||||
}
|
||||
return this.mapRange(cb, startIndex, (endIndex - startIndex) + 1, context);
|
||||
},
|
||||
|
||||
forEachKeyRange: function(cb, startKey, endKey, context) {
|
||||
var startIndex = this.indexOfKey(startKey);
|
||||
var endIndex = this.indexOfKey(endKey);
|
||||
if (endIndex < startIndex) {
|
||||
throw new Error(RANGE_ARGS);
|
||||
}
|
||||
this.forEachRange(cb, startIndex, (endIndex - startIndex) + 1, context);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} pos Index to search for key at.
|
||||
* @return {string|undefined} Either the key at index `pos` or undefined if
|
||||
* not in map.
|
||||
*/
|
||||
keyAtIndex: function(pos) {
|
||||
var computedPositions = this._getOrComputePositions();
|
||||
var keyAtPos = computedPositions.keyByIndex[pos];
|
||||
return keyAtPos ? keyAtPos.substr(PREFIX.length) : undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} key String key from which to find the next key.
|
||||
* @return {string|undefined} Either the next key, or undefined if there is no
|
||||
* next key.
|
||||
* @throws Error if `key` is not in this `OrderedMap`.
|
||||
*/
|
||||
keyAfter: function(key) {
|
||||
return this.nthKeyAfter(key, 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} key String key from which to find the preceding key.
|
||||
* @return {string|undefined} Either the preceding key, or undefined if there
|
||||
* is no preceding.key.
|
||||
* @throws Error if `key` is not in this `OrderedMap`.
|
||||
*/
|
||||
keyBefore: function(key) {
|
||||
return this.nthKeyBefore(key, 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} key String key from which to find a following key.
|
||||
* @param {number} n Distance to scan forward after `key`.
|
||||
* @return {string|undefined} Either the nth key after `key`, or undefined if
|
||||
* there is no next key.
|
||||
* @throws Error if `key` is not in this `OrderedMap`.
|
||||
*/
|
||||
nthKeyAfter: function(key, n) {
|
||||
var curIndex = this.indexOfKey(key);
|
||||
throwIf(curIndex === undefined, NO_EXIST);
|
||||
return this.keyAtIndex(curIndex + n);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} key String key from which to find a preceding key.
|
||||
* @param {number} n Distance to scan backwards before `key`.
|
||||
* @return {string|undefined} Either the nth key before `key`, or undefined if
|
||||
* there is no previous key.
|
||||
* @throws Error if `key` is not in this `OrderedMap`.
|
||||
*/
|
||||
nthKeyBefore: function(key, n) {
|
||||
return this.nthKeyAfter(key, -n);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} key Key to find the index of.
|
||||
* @return {number|undefined} Index of the provided key, or `undefined` if the
|
||||
* key is not found.
|
||||
*/
|
||||
indexOfKey: function(key) {
|
||||
validatePublicKey(key);
|
||||
var normalizedKey = PREFIX + key;
|
||||
var computedPositions = this._getOrComputePositions();
|
||||
var computedPosition = computedPositions.indexByKey[normalizedKey];
|
||||
// Just writing it this way to make it clear this is intentional.
|
||||
return computedPosition === undefined ? undefined : computedPosition;
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Array} An ordered array of this object's values.
|
||||
*/
|
||||
toArray: function() {
|
||||
var result = [];
|
||||
var thisSet = this._normalizedObj;
|
||||
for (var key in thisSet) {
|
||||
if (thisSet.hasOwnProperty(key)) {
|
||||
result.push(thisSet[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds the key at a given position, or indicates via `undefined` that that
|
||||
* position does not exist in the `OrderedMap`. It is appropriate to return
|
||||
* undefined, indicating that the key doesn't exist in the `OrderedMap`
|
||||
* because `undefined` is not ever a valid `OrderedMap` key.
|
||||
*
|
||||
* @private
|
||||
* @param {number} pos Position for which we're querying the name.
|
||||
* @return {string?} Name of the item at position `pos`, or `undefined` if
|
||||
* there is no item at that position.
|
||||
*/
|
||||
_getOrComputePositions: function() {
|
||||
// TODO: Entertain computing this at construction time in some less
|
||||
// performance critical paths.
|
||||
var computedPositions = this._computedPositions;
|
||||
if (!computedPositions) {
|
||||
this._computePositions();
|
||||
}
|
||||
return this._computedPositions;
|
||||
},
|
||||
|
||||
/**
|
||||
* Precomputes the index/key mapping for future lookup. Since `OrderedMap`s
|
||||
* are immutable, there is only ever a need to perform this once.
|
||||
* @private
|
||||
*/
|
||||
_computePositions: function() {
|
||||
this._computedPositions = {
|
||||
keyByIndex: {},
|
||||
indexByKey: {}
|
||||
};
|
||||
var keyByIndex = this._computedPositions.keyByIndex;
|
||||
var indexByKey = this._computedPositions.indexByKey;
|
||||
var index = 0;
|
||||
var thisSet = this._normalizedObj;
|
||||
for (var key in thisSet) {
|
||||
if (thisSet.hasOwnProperty(key)) {
|
||||
keyByIndex[index] = key;
|
||||
indexByKey[key] = index;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mixInto(OrderedMapImpl, OrderedMapMethods);
|
||||
|
||||
var OrderedMap = {
|
||||
from: function(orderedMap) {
|
||||
var invalidArg = !(orderedMap instanceof OrderedMapImpl);
|
||||
throwIf(invalidArg, OPERATION_ARGS);
|
||||
return _fromNormalizedObjects(orderedMap._normalizedObj, null);
|
||||
},
|
||||
|
||||
fromArray: function(arr, keyExtractor) {
|
||||
throwIf(!Array.isArray(arr), OPERATION_ARGS);
|
||||
throwIf(!keyExtractor, ARRAY_MUST_CB);
|
||||
return new OrderedMapImpl(
|
||||
extractObjectFromArray(arr, keyExtractor),
|
||||
arr.length
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = OrderedMap;
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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 PooledClass
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Static poolers. Several custom versions for each potential number of
|
||||
* arguments. A completely generic pooler is easy to implement, but would
|
||||
* require accessing the `arguments` object. In each of these, `this` refers to
|
||||
* the Class itself, not an instance. If any others are needed, simply add them
|
||||
* here, or in their own files.
|
||||
*/
|
||||
var oneArgumentPooler = function(copyFieldsFrom) {
|
||||
var Klass = this;
|
||||
if (Klass.instancePool.length) {
|
||||
var instance = Klass.instancePool.pop();
|
||||
Klass.call(instance, copyFieldsFrom);
|
||||
return instance;
|
||||
} else {
|
||||
return new Klass(copyFieldsFrom);
|
||||
}
|
||||
};
|
||||
|
||||
var twoArgumentPooler = function(a1, a2) {
|
||||
var Klass = this;
|
||||
if (Klass.instancePool.length) {
|
||||
var instance = Klass.instancePool.pop();
|
||||
Klass.call(instance, a1, a2);
|
||||
return instance;
|
||||
} else {
|
||||
return new Klass(a1, a2);
|
||||
}
|
||||
};
|
||||
|
||||
var fiveArgumentPooler = function(a1, a2, a3, a4, a5) {
|
||||
var Klass = this;
|
||||
if (Klass.instancePool.length) {
|
||||
var instance = Klass.instancePool.pop();
|
||||
Klass.call(instance, a1, a2, a3, a4, a5);
|
||||
return instance;
|
||||
} else {
|
||||
return new Klass(a1, a2, a3, a4, a5);
|
||||
}
|
||||
};
|
||||
|
||||
var standardReleaser = function(instance) {
|
||||
var Klass = this;
|
||||
if (instance.destructor) {
|
||||
instance.destructor();
|
||||
}
|
||||
if (Klass.instancePool.length < Klass.poolSize) {
|
||||
Klass.instancePool.push(instance);
|
||||
}
|
||||
};
|
||||
|
||||
var DEFAULT_POOL_SIZE = 10;
|
||||
var DEFAULT_POOLER = oneArgumentPooler;
|
||||
|
||||
/**
|
||||
* Augments `CopyConstructor` to be a poolable class, augmenting only the class
|
||||
* itself (statically) not adding any prototypical fields. Any CopyConstructor
|
||||
* you give this may have a `poolSize` property, and will look for a
|
||||
* prototypical `destructor` on instances (optional).
|
||||
*
|
||||
* @param {Function} CopyConstructor Constructor that can be used to reset.
|
||||
* @param {Function} pooler Customizable pooler.
|
||||
*/
|
||||
var addPoolingTo = function(CopyConstructor, pooler) {
|
||||
var NewKlass = CopyConstructor;
|
||||
NewKlass.instancePool = [];
|
||||
NewKlass.getPooled = pooler || DEFAULT_POOLER;
|
||||
if (!NewKlass.poolSize) {
|
||||
NewKlass.poolSize = DEFAULT_POOL_SIZE;
|
||||
}
|
||||
NewKlass.release = standardReleaser;
|
||||
return NewKlass;
|
||||
};
|
||||
|
||||
var PooledClass = {
|
||||
addPoolingTo: addPoolingTo,
|
||||
oneArgumentPooler: oneArgumentPooler,
|
||||
twoArgumentPooler: twoArgumentPooler,
|
||||
fiveArgumentPooler: fiveArgumentPooler
|
||||
};
|
||||
|
||||
module.exports = PooledClass;
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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 React
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactCompositeComponent = require("./ReactCompositeComponent");
|
||||
var ReactComponent = require("./ReactComponent");
|
||||
var ReactDOM = require("./ReactDOM");
|
||||
var ReactMount = require("./ReactMount");
|
||||
|
||||
var ReactDefaultInjection = require("./ReactDefaultInjection");
|
||||
|
||||
ReactDefaultInjection.inject();
|
||||
|
||||
var React = {
|
||||
DOM: ReactDOM,
|
||||
initializeTouchEvents: function(shouldUseTouch) {
|
||||
ReactMount.useTouchEvents = shouldUseTouch;
|
||||
},
|
||||
autoBind: ReactCompositeComponent.autoBind,
|
||||
createClass: ReactCompositeComponent.createClass,
|
||||
createComponentRenderer: ReactMount.createComponentRenderer,
|
||||
constructAndRenderComponent: ReactMount.constructAndRenderComponent,
|
||||
constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID,
|
||||
renderComponent: ReactMount.renderComponent,
|
||||
unmountAndReleaseReactRootNode: ReactMount.unmountAndReleaseReactRootNode,
|
||||
isValidComponent: ReactComponent.isValidComponent
|
||||
};
|
||||
|
||||
module.exports = React;
|
||||
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* 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 ReactComponent
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ExecutionEnvironment = require("./ExecutionEnvironment");
|
||||
var ReactCurrentOwner = require("./ReactCurrentOwner");
|
||||
var ReactDOMIDOperations = require("./ReactDOMIDOperations");
|
||||
var ReactMount = require("./ReactMount");
|
||||
var ReactOwner = require("./ReactOwner");
|
||||
var ReactReconcileTransaction = require("./ReactReconcileTransaction");
|
||||
|
||||
var invariant = require("./invariant");
|
||||
var keyMirror = require("./keyMirror");
|
||||
var merge = require("./merge");
|
||||
|
||||
/**
|
||||
* Prop key that references a component's owner.
|
||||
* @private
|
||||
*/
|
||||
var OWNER = '{owner}';
|
||||
|
||||
/**
|
||||
* Every React component is in one of these life cycles.
|
||||
*/
|
||||
var ComponentLifeCycle = keyMirror({
|
||||
/**
|
||||
* Mounted components have a DOM node representation and are capable of
|
||||
* receiving new props.
|
||||
*/
|
||||
MOUNTED: null,
|
||||
/**
|
||||
* Unmounted components are inactive and cannot receive new props.
|
||||
*/
|
||||
UNMOUNTED: null
|
||||
});
|
||||
|
||||
/**
|
||||
* Components are the basic units of composition in React.
|
||||
*
|
||||
* Every component accepts a set of keyed input parameters known as "props" that
|
||||
* are initialized by the constructor. Once a component is mounted, the props
|
||||
* can be mutated using `setProps` or `replaceProps`.
|
||||
*
|
||||
* Every component is capable of the following operations:
|
||||
*
|
||||
* `mountComponent`
|
||||
* Initializes the component, renders markup, and registers event listeners.
|
||||
*
|
||||
* `receiveProps`
|
||||
* Updates the rendered DOM nodes given a new set of props.
|
||||
*
|
||||
* `unmountComponent`
|
||||
* Releases any resources allocated by this component.
|
||||
*
|
||||
* Components can also be "owned" by other components. Being owned by another
|
||||
* component means being constructed by that component. This is different from
|
||||
* being the child of a component, which means having a DOM representation that
|
||||
* is a child of the DOM representation of that component.
|
||||
*
|
||||
* @class ReactComponent
|
||||
*/
|
||||
var ReactComponent = {
|
||||
|
||||
/**
|
||||
* @param {?object} object
|
||||
* @return {boolean} True if `object` is a valid component.
|
||||
* @final
|
||||
*/
|
||||
isValidComponent: function(object) {
|
||||
return !!(
|
||||
object &&
|
||||
typeof object.mountComponentIntoNode === 'function' &&
|
||||
typeof object.receiveProps === 'function'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
LifeCycle: ComponentLifeCycle,
|
||||
|
||||
/**
|
||||
* React references `ReactDOMIDOperations` using this property in order to
|
||||
* allow dependency injection.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
DOMIDOperations: ReactDOMIDOperations,
|
||||
|
||||
/**
|
||||
* React references `ReactReconcileTransaction` using this property in order
|
||||
* to allow dependency injection.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
ReactReconcileTransaction: ReactReconcileTransaction,
|
||||
|
||||
/**
|
||||
* @param {object} DOMIDOperations
|
||||
* @final
|
||||
*/
|
||||
setDOMOperations: function(DOMIDOperations) {
|
||||
ReactComponent.DOMIDOperations = DOMIDOperations;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Transaction} ReactReconcileTransaction
|
||||
* @final
|
||||
*/
|
||||
setReactReconcileTransaction: function(ReactReconcileTransaction) {
|
||||
ReactComponent.ReactReconcileTransaction = ReactReconcileTransaction;
|
||||
},
|
||||
|
||||
/**
|
||||
* Base functionality for every ReactComponent constructor.
|
||||
*
|
||||
* @lends {ReactComponent.prototype}
|
||||
*/
|
||||
Mixin: {
|
||||
|
||||
/**
|
||||
* Returns the DOM node rendered by this component.
|
||||
*
|
||||
* @return {?DOMElement} The root node of this component.
|
||||
* @final
|
||||
* @protected
|
||||
*/
|
||||
getDOMNode: function() {
|
||||
invariant(
|
||||
ExecutionEnvironment.canUseDOM,
|
||||
'getDOMNode(): The DOM is not supported in the current environment.'
|
||||
);
|
||||
invariant(
|
||||
this._lifeCycleState === ComponentLifeCycle.MOUNTED,
|
||||
'getDOMNode(): A component must be mounted to have a DOM node.'
|
||||
);
|
||||
var rootNode = this._rootNode;
|
||||
if (!rootNode) {
|
||||
rootNode = document.getElementById(this._rootNodeID);
|
||||
if (!rootNode) {
|
||||
// TODO: Log the frequency that we reach this path.
|
||||
rootNode = ReactMount.findReactRenderedDOMNodeSlow(this._rootNodeID);
|
||||
}
|
||||
this._rootNode = rootNode;
|
||||
}
|
||||
return rootNode;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a subset of the props.
|
||||
*
|
||||
* @param {object} partialProps Subset of the next props.
|
||||
* @final
|
||||
* @public
|
||||
*/
|
||||
setProps: function(partialProps) {
|
||||
this.replaceProps(merge(this.props, partialProps));
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces all of the props.
|
||||
*
|
||||
* @param {object} props New props.
|
||||
* @final
|
||||
* @public
|
||||
*/
|
||||
replaceProps: function(props) {
|
||||
invariant(
|
||||
!this.props[OWNER],
|
||||
'replaceProps(...): You called `setProps` or `replaceProps` on a ' +
|
||||
'component with an owner. 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.'
|
||||
);
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(this.receiveProps, this, props, transaction);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
},
|
||||
|
||||
/**
|
||||
* Base constructor for all React component.
|
||||
*
|
||||
* Subclasses that override this method should make sure to invoke
|
||||
* `ReactComponent.Mixin.construct.call(this, ...)`.
|
||||
*
|
||||
* @param {?object} initialProps
|
||||
* @param {*} children
|
||||
* @internal
|
||||
*/
|
||||
construct: function(initialProps, children) {
|
||||
this.props = initialProps || {};
|
||||
if (typeof children !== 'undefined') {
|
||||
this.props.children = children;
|
||||
}
|
||||
// Record the component responsible for creating this component.
|
||||
this.props[OWNER] = ReactCurrentOwner.current;
|
||||
// All components start unmounted.
|
||||
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes the component, renders markup, and registers event listeners.
|
||||
*
|
||||
* NOTE: This does not insert any nodes into the DOM.
|
||||
*
|
||||
* Subclasses that override this method should make sure to invoke
|
||||
* `ReactComponent.Mixin.mountComponent.call(this, ...)`.
|
||||
*
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @return {?string} Rendered markup to be inserted into the DOM.
|
||||
* @internal
|
||||
*/
|
||||
mountComponent: function(rootID, transaction) {
|
||||
invariant(
|
||||
this._lifeCycleState === ComponentLifeCycle.UNMOUNTED,
|
||||
'mountComponent(%s, ...): Can only mount an unmounted component.',
|
||||
rootID
|
||||
);
|
||||
var props = this.props;
|
||||
if (props.ref != null) {
|
||||
ReactOwner.addComponentAsRefTo(this, props.ref, props[OWNER]);
|
||||
}
|
||||
this._rootNodeID = rootID;
|
||||
this._lifeCycleState = ComponentLifeCycle.MOUNTED;
|
||||
// Effectively: return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Releases any resources allocated by `mountComponent`.
|
||||
*
|
||||
* NOTE: This does not remove any nodes from the DOM.
|
||||
*
|
||||
* Subclasses that override this method should make sure to invoke
|
||||
* `ReactComponent.Mixin.unmountComponent.call(this)`.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
unmountComponent: function() {
|
||||
invariant(
|
||||
this._lifeCycleState === ComponentLifeCycle.MOUNTED,
|
||||
'unmountComponent(): Can only unmount a mounted component.'
|
||||
);
|
||||
var props = this.props;
|
||||
if (props.ref != null) {
|
||||
ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]);
|
||||
}
|
||||
this._rootNode = null;
|
||||
this._rootNodeID = null;
|
||||
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the rendered DOM nodes given a new set of props.
|
||||
*
|
||||
* Subclasses that override this method should make sure to invoke
|
||||
* `ReactComponent.Mixin.receiveProps.call(this, ...)`.
|
||||
*
|
||||
* @param {object} nextProps Next set of properties.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @internal
|
||||
*/
|
||||
receiveProps: function(nextProps, transaction) {
|
||||
invariant(
|
||||
this._lifeCycleState === ComponentLifeCycle.MOUNTED,
|
||||
'receiveProps(...): Can only update a mounted component.'
|
||||
);
|
||||
var props = this.props;
|
||||
// If either the owner or a `ref` has changed, make sure the newest owner
|
||||
// has stored a reference to `this`, and the previous owner (if different)
|
||||
// has forgotten the reference to `this`.
|
||||
if (nextProps[OWNER] !== props[OWNER] || nextProps.ref !== props.ref) {
|
||||
if (props.ref != null) {
|
||||
ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]);
|
||||
}
|
||||
// Correct, even if the owner is the same, and only the ref has changed.
|
||||
if (nextProps.ref != null) {
|
||||
ReactOwner.addComponentAsRefTo(this, nextProps.ref, nextProps[OWNER]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Mounts this component and inserts it into the DOM.
|
||||
*
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @param {DOMElement} container DOM element to mount into.
|
||||
* @final
|
||||
* @internal
|
||||
* @see {ReactMount.renderComponent}
|
||||
*/
|
||||
mountComponentIntoNode: function(rootID, container) {
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(
|
||||
this._mountComponentIntoNode,
|
||||
this,
|
||||
rootID,
|
||||
container,
|
||||
transaction
|
||||
);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @param {DOMElement} container DOM element to mount into.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @final
|
||||
* @private
|
||||
*/
|
||||
_mountComponentIntoNode: function(rootID, container, transaction) {
|
||||
var renderStart = Date.now();
|
||||
var markup = this.mountComponent(rootID, transaction);
|
||||
ReactMount.totalInstantiationTime += (Date.now() - renderStart);
|
||||
|
||||
var injectionStart = Date.now();
|
||||
// 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 {
|
||||
container.innerHTML = markup;
|
||||
}
|
||||
ReactMount.totalInjectionTime += (Date.now() - injectionStart);
|
||||
},
|
||||
|
||||
/**
|
||||
* Unmounts this component and removes it from the DOM.
|
||||
*
|
||||
* @param {DOMElement} container DOM element to unmount from.
|
||||
* @final
|
||||
* @internal
|
||||
* @see {ReactMount.unmountAndReleaseReactRootNode}
|
||||
*/
|
||||
unmountComponentFromNode: function(container) {
|
||||
this.unmountComponent();
|
||||
// http://jsperf.com/emptying-a-node
|
||||
while (container.lastChild) {
|
||||
container.removeChild(container.lastChild);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if this component is owned by the supplied `owner` component.
|
||||
*
|
||||
* @param {ReactComponent} owner Component to check.
|
||||
* @return {boolean} True if `owners` owns this component.
|
||||
* @final
|
||||
* @internal
|
||||
*/
|
||||
isOwnedBy: function(owner) {
|
||||
return this.props[OWNER] === owner;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function logDeprecated(msg) {
|
||||
if (true) {
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
ReactComponent.Mixin.update = function(props) {
|
||||
logDeprecated('this.update() is deprecated. Use this.setProps()');
|
||||
this.setProps(props);
|
||||
};
|
||||
ReactComponent.Mixin.updateAll = function(props) {
|
||||
logDeprecated('this.updateAll() is deprecated. Use this.replaceProps()');
|
||||
this.replaceProps(props);
|
||||
};
|
||||
|
||||
module.exports = ReactComponent;
|
||||
@@ -0,0 +1,836 @@
|
||||
/**
|
||||
* 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 ReactCompositeComponent
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactComponent = require("./ReactComponent");
|
||||
var ReactCurrentOwner = require("./ReactCurrentOwner");
|
||||
var ReactOwner = require("./ReactOwner");
|
||||
var ReactPropTransferer = require("./ReactPropTransferer");
|
||||
|
||||
var invariant = require("./invariant");
|
||||
var keyMirror = require("./keyMirror");
|
||||
var merge = require("./merge");
|
||||
var mixInto = require("./mixInto");
|
||||
|
||||
/**
|
||||
* Policies that describe methods in `ReactCompositeComponentInterface`.
|
||||
*/
|
||||
var SpecPolicy = keyMirror({
|
||||
/**
|
||||
* These methods may be defined only once by the class specification or mixin.
|
||||
*/
|
||||
DEFINE_ONCE: null,
|
||||
/**
|
||||
* These methods may be defined by both the class specification and mixins.
|
||||
* Subsequent definitions will be chained. These methods must return void.
|
||||
*/
|
||||
DEFINE_MANY: null,
|
||||
/**
|
||||
* These methods are overriding the base ReactCompositeComponent class.
|
||||
*/
|
||||
OVERRIDE_BASE: null
|
||||
});
|
||||
|
||||
/**
|
||||
* Composite components are higher-level components that compose other composite
|
||||
* or native components.
|
||||
*
|
||||
* To create a new type of `ReactCompositeComponent`, pass a specification of
|
||||
* your new class to `React.createClass`. The only requirement of your class
|
||||
* specification is that you implement a `render` method.
|
||||
*
|
||||
* var MyComponent = React.createClass({
|
||||
* render: function() {
|
||||
* return <div>Hello World</div>;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* The class specification supports a specific protocol of methods that have
|
||||
* special meaning (e.g. `render`). See `ReactCompositeComponentInterface` for
|
||||
* more the comprehensive protocol. Any other properties and methods in the
|
||||
* class specification will available on the prototype.
|
||||
*
|
||||
* @interface ReactCompositeComponentInterface
|
||||
* @internal
|
||||
*/
|
||||
var ReactCompositeComponentInterface = {
|
||||
|
||||
/**
|
||||
* An array of Mixin objects to include when defining your component.
|
||||
*
|
||||
* @type {array}
|
||||
* @optional
|
||||
*/
|
||||
mixins: SpecPolicy.DEFINE_MANY,
|
||||
|
||||
/**
|
||||
* Definition of props for this component.
|
||||
*
|
||||
* @type {array}
|
||||
* @optional
|
||||
*/
|
||||
props: SpecPolicy.DEFINE_ONCE,
|
||||
|
||||
|
||||
|
||||
// ==== Definition methods ====
|
||||
|
||||
/**
|
||||
* Invoked once before the component is mounted. The return value will be used
|
||||
* as the initial value of `this.state`.
|
||||
*
|
||||
* getInitialState: function() {
|
||||
* return {
|
||||
* isOn: false,
|
||||
* fooBaz: new BazFoo()
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @return {object}
|
||||
* @optional
|
||||
*/
|
||||
getInitialState: SpecPolicy.DEFINE_ONCE,
|
||||
|
||||
/**
|
||||
* Uses props from `this.props` and state from `this.state` to render the
|
||||
* structure of the component.
|
||||
*
|
||||
* No guarantees are made about when or how often this method is invoked, so
|
||||
* it must not have side effects.
|
||||
*
|
||||
* render: function() {
|
||||
* var name = this.props.name;
|
||||
* return <div>Hello, {name}!</div>;
|
||||
* }
|
||||
*
|
||||
* @return {ReactComponent}
|
||||
* @nosideeffects
|
||||
* @required
|
||||
*/
|
||||
render: SpecPolicy.DEFINE_ONCE,
|
||||
|
||||
|
||||
|
||||
// ==== Delegate methods ====
|
||||
|
||||
/**
|
||||
* Invoked when the component is initially created and about to be mounted.
|
||||
* This may have side effects, but any external subscriptions or data created
|
||||
* by this method must be cleaned up in `componentWillUnmount`.
|
||||
*
|
||||
* @optional
|
||||
*/
|
||||
componentWillMount: SpecPolicy.DEFINE_MANY,
|
||||
|
||||
/**
|
||||
* Invoked when the component has been mounted and has a DOM representation.
|
||||
* However, there is no guarantee that the DOM node is in the document.
|
||||
*
|
||||
* Use this as an opportunity to operate on the DOM when the component has
|
||||
* been mounted (initialized and rendered) for the first time.
|
||||
*
|
||||
* @param {DOMElement} rootNode DOM element representing the component.
|
||||
* @optional
|
||||
*/
|
||||
componentDidMount: SpecPolicy.DEFINE_MANY,
|
||||
|
||||
/**
|
||||
* Invoked before the component receives new props.
|
||||
*
|
||||
* Use this as an opportunity to react to a prop transition by updating the
|
||||
* state using `this.setState`. Current props are accessed via `this.props`.
|
||||
*
|
||||
* componentWillReceiveProps: function(nextProps) {
|
||||
* this.setState({
|
||||
* likesIncreasing: nextProps.likeCount > this.props.likeCount
|
||||
* });
|
||||
* }
|
||||
*
|
||||
* NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop
|
||||
* transition may cause a state change, but the opposite is not true. If you
|
||||
* need it, you are probably looking for `componentWillUpdate`.
|
||||
*
|
||||
* @param {object} nextProps
|
||||
* @optional
|
||||
*/
|
||||
componentWillReceiveProps: SpecPolicy.DEFINE_MANY,
|
||||
|
||||
/**
|
||||
* Invoked while deciding if the component should be updated as a result of
|
||||
* receiving new props and state.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* shouldComponentUpdate: function(nextProps, nextState) {
|
||||
* return !equal(nextProps, this.props) || !equal(nextState, this.state);
|
||||
* }
|
||||
*
|
||||
* @param {object} nextProps
|
||||
* @param {?object} nextState
|
||||
* @return {boolean} True if the component should update.
|
||||
* @optional
|
||||
*/
|
||||
shouldComponentUpdate: SpecPolicy.DEFINE_ONCE,
|
||||
|
||||
/**
|
||||
* Invoked when the component is about to update due to a transition from
|
||||
* `this.props` and `this.state` to `nextProps` and `nextState`.
|
||||
*
|
||||
* Use this as an opportunity to perform preparation before an update occurs.
|
||||
*
|
||||
* NOTE: You **cannot** use `this.setState()` in this method.
|
||||
*
|
||||
* @param {object} nextProps
|
||||
* @param {?object} nextState
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @optional
|
||||
*/
|
||||
componentWillUpdate: SpecPolicy.DEFINE_MANY,
|
||||
|
||||
/**
|
||||
* Invoked when the component's DOM representation has been updated.
|
||||
*
|
||||
* Use this as an opportunity to operate on the DOM when the component has
|
||||
* been updated.
|
||||
*
|
||||
* @param {object} prevProps
|
||||
* @param {?object} prevState
|
||||
* @param {DOMElement} rootNode DOM element representing the component.
|
||||
* @optional
|
||||
*/
|
||||
componentDidUpdate: SpecPolicy.DEFINE_MANY,
|
||||
|
||||
/**
|
||||
* Invoked when the component is about to be removed from its parent and have
|
||||
* its DOM representation destroyed.
|
||||
*
|
||||
* Use this as an opportunity to deallocate any external resources.
|
||||
*
|
||||
* NOTE: There is no `componentDidUnmount` since your component will have been
|
||||
* destroyed by that point.
|
||||
*
|
||||
* @optional
|
||||
*/
|
||||
componentWillUnmount: SpecPolicy.DEFINE_MANY,
|
||||
|
||||
|
||||
|
||||
// ==== Advanced methods ====
|
||||
|
||||
/**
|
||||
* Updates the component's currently mounted DOM representation.
|
||||
*
|
||||
* By default, this implements React's rendering and reconciliation algorithm.
|
||||
* Sophisticated clients may wish to override this.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @internal
|
||||
* @overridable
|
||||
*/
|
||||
updateComponent: SpecPolicy.OVERRIDE_BASE
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping from class specification keys to special processing functions.
|
||||
*
|
||||
* Although these are declared in the specification when defining classes
|
||||
* using `React.createClass`, they will not be on the component's prototype.
|
||||
*/
|
||||
var RESERVED_SPEC_KEYS = {
|
||||
displayName: function(Constructor, displayName) {
|
||||
Constructor.displayName = displayName;
|
||||
},
|
||||
mixins: function(Constructor, mixins) {
|
||||
if (mixins) {
|
||||
for (var i = 0; i < mixins.length; i++) {
|
||||
mixSpecIntoComponent(Constructor, mixins[i]);
|
||||
}
|
||||
}
|
||||
},
|
||||
props: function(Constructor, props) {
|
||||
Constructor.propDeclarations = props;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom version of `mixInto` which handles policy validation and reserved
|
||||
* specification keys when building `ReactCompositeComponent` classses.
|
||||
*/
|
||||
function mixSpecIntoComponent(Constructor, spec) {
|
||||
var proto = Constructor.prototype;
|
||||
for (var name in spec) {
|
||||
if (!spec.hasOwnProperty(name)) {
|
||||
continue;
|
||||
}
|
||||
var property = spec[name];
|
||||
var specPolicy = ReactCompositeComponentInterface[name];
|
||||
|
||||
// Disallow overriding of base class methods unless explicitly allowed.
|
||||
if (ReactCompositeComponentMixin.hasOwnProperty(name)) {
|
||||
invariant(
|
||||
specPolicy === SpecPolicy.OVERRIDE_BASE,
|
||||
'ReactCompositeComponentInterface: You are attempting to override ' +
|
||||
'`%s` from your class specification. Ensure that your method names ' +
|
||||
'do not overlap with React methods.',
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
// Disallow using `React.autoBind` on internal methods.
|
||||
if (specPolicy != null) {
|
||||
invariant(
|
||||
!property || !property.__reactAutoBind,
|
||||
'ReactCompositeComponentInterface: You are attempting to use ' +
|
||||
'`React.autoBind` on `%s`, a method that is internal to React.' +
|
||||
'Internal methods are called with the component as the context.',
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
// Disallow defining methods more than once unless explicitly allowed.
|
||||
if (proto.hasOwnProperty(name)) {
|
||||
invariant(
|
||||
specPolicy === SpecPolicy.DEFINE_MANY,
|
||||
'ReactCompositeComponentInterface: You are attempting to define ' +
|
||||
'`%s` on your component more than once. This conflict may be due ' +
|
||||
'to a mixin.',
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) {
|
||||
RESERVED_SPEC_KEYS[name](Constructor, property);
|
||||
} else if (property && property.__reactAutoBind) {
|
||||
if (!proto.__reactAutoBindMap) {
|
||||
proto.__reactAutoBindMap = {};
|
||||
}
|
||||
proto.__reactAutoBindMap[name] = property.__reactAutoBind;
|
||||
} else if (proto.hasOwnProperty(name)) {
|
||||
// For methods which are defined more than once, call the existing methods
|
||||
// before calling the new property.
|
||||
proto[name] = createChainedFunction(proto[name], property);
|
||||
} else {
|
||||
proto[name] = property;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function that invokes two functions and ignores their return vales.
|
||||
*
|
||||
* @param {function} one Function to invoke first.
|
||||
* @param {function} two Function to invoke second.
|
||||
* @return {function} Function that invokes the two argument functions.
|
||||
* @private
|
||||
*/
|
||||
function createChainedFunction(one, two) {
|
||||
return function chainedFunction(a, b, c, d, e, tooMany) {
|
||||
invariant(
|
||||
typeof tooMany === 'undefined',
|
||||
'Chained function can only take a maximum of 5 arguments.'
|
||||
);
|
||||
one.call(this, a, b, c, d, e);
|
||||
two.call(this, a, b, c, d, e);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `ReactCompositeComponent` maintains an auxiliary life cycle state in
|
||||
* `this._compositeLifeCycleState` (which can be null).
|
||||
*
|
||||
* This is different from the life cycle state maintained by `ReactComponent` in
|
||||
* `this._lifeCycleState`.
|
||||
*/
|
||||
var CompositeLifeCycle = keyMirror({
|
||||
/**
|
||||
* Components in the process of being mounted respond to state changes
|
||||
* differently.
|
||||
*/
|
||||
MOUNTING: null,
|
||||
/**
|
||||
* Components in the process of being unmounted are guarded against state
|
||||
* changes.
|
||||
*/
|
||||
UNMOUNTING: null,
|
||||
/**
|
||||
* Components that are mounted and receiving new props respond to state
|
||||
* changes differently.
|
||||
*/
|
||||
RECEIVING_PROPS: null,
|
||||
/**
|
||||
* Components that are mounted and receiving new state are guarded against
|
||||
* additional state changes.
|
||||
*/
|
||||
RECEIVING_STATE: null
|
||||
});
|
||||
|
||||
/**
|
||||
* @lends {ReactCompositeComponent.prototype}
|
||||
*/
|
||||
var ReactCompositeComponentMixin = {
|
||||
|
||||
/**
|
||||
* Base constructor for all composite component.
|
||||
*
|
||||
* @param {?object} initialProps
|
||||
* @param {*} children
|
||||
* @final
|
||||
* @internal
|
||||
*/
|
||||
construct: function(initialProps, children) {
|
||||
ReactComponent.Mixin.construct.call(this, initialProps, children);
|
||||
this.state = null;
|
||||
this._pendingState = null;
|
||||
this._compositeLifeCycleState = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes the component, renders markup, and registers event listeners.
|
||||
*
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @return {?string} Rendered markup to be inserted into the DOM.
|
||||
* @final
|
||||
* @internal
|
||||
*/
|
||||
mountComponent: function(rootID, transaction) {
|
||||
ReactComponent.Mixin.mountComponent.call(this, rootID, transaction);
|
||||
|
||||
// Unset `this._lifeCycleState` until after this method is finished.
|
||||
this._lifeCycleState = ReactComponent.LifeCycle.UNMOUNTED;
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
|
||||
|
||||
if (this.constructor.propDeclarations) {
|
||||
this._assertValidProps(this.props);
|
||||
}
|
||||
|
||||
if (this.__reactAutoBindMap) {
|
||||
this._bindAutoBindMethods();
|
||||
}
|
||||
|
||||
this.state = this.getInitialState ? this.getInitialState() : null;
|
||||
this._pendingState = null;
|
||||
|
||||
if (this.componentWillMount) {
|
||||
this.componentWillMount();
|
||||
// When mounting, calls to `setState` by `componentWillMount` will set
|
||||
// `this._pendingState` without triggering a re-render.
|
||||
if (this._pendingState) {
|
||||
this.state = this._pendingState;
|
||||
this._pendingState = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.componentDidMount) {
|
||||
transaction.getReactOnDOMReady().enqueue(this, this.componentDidMount);
|
||||
}
|
||||
|
||||
this._renderedComponent = this._renderValidatedComponent();
|
||||
|
||||
// Done with mounting, `setState` will now trigger UI changes.
|
||||
this._compositeLifeCycleState = null;
|
||||
this._lifeCycleState = ReactComponent.LifeCycle.MOUNTED;
|
||||
|
||||
return this._renderedComponent.mountComponent(rootID, transaction);
|
||||
},
|
||||
|
||||
/**
|
||||
* Releases any resources allocated by `mountComponent`.
|
||||
*
|
||||
* @final
|
||||
* @internal
|
||||
*/
|
||||
unmountComponent: function() {
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.UNMOUNTING;
|
||||
if (this.componentWillUnmount) {
|
||||
this.componentWillUnmount();
|
||||
}
|
||||
this._compositeLifeCycleState = null;
|
||||
|
||||
ReactComponent.Mixin.unmountComponent.call(this);
|
||||
this._renderedComponent.unmountComponent();
|
||||
this._renderedComponent = null;
|
||||
|
||||
if (this.refs) {
|
||||
this.refs = null;
|
||||
}
|
||||
|
||||
// Some existing components rely on this.props even after they've been
|
||||
// destroyed (in event handlers).
|
||||
// TODO: this.props = null;
|
||||
// TODO: this.state = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the rendered DOM nodes given a new set of props.
|
||||
*
|
||||
* @param {object} nextProps Next set of properties.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @final
|
||||
* @internal
|
||||
*/
|
||||
receiveProps: function(nextProps, transaction) {
|
||||
if (this.constructor.propDeclarations) {
|
||||
this._assertValidProps(nextProps);
|
||||
}
|
||||
ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
|
||||
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
|
||||
if (this.componentWillReceiveProps) {
|
||||
this.componentWillReceiveProps(nextProps, transaction);
|
||||
}
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
|
||||
// When receiving props, calls to `setState` by `componentWillReceiveProps`
|
||||
// will set `this._pendingState` without triggering a re-render.
|
||||
var nextState = this._pendingState || this.state;
|
||||
this._pendingState = null;
|
||||
this._receivePropsAndState(nextProps, nextState, transaction);
|
||||
this._compositeLifeCycleState = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a subset of the state. Always use this or `replaceState` to mutate
|
||||
* state. You should treat `this.state` as immutable.
|
||||
*
|
||||
* There is no guarantee that `this.state` will be immediately updated, so
|
||||
* accessing `this.state` after calling this method may return the old value.
|
||||
*
|
||||
* @param {object} partialState Next partial state to be merged with state.
|
||||
* @final
|
||||
* @protected
|
||||
*/
|
||||
setState: function(partialState) {
|
||||
// Merge with `_pendingState` if it exists, otherwise with existing state.
|
||||
this.replaceState(merge(this._pendingState || this.state, partialState));
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces all of the state. Always use this or `setState` to mutate state.
|
||||
* You should treat `this.state` as immutable.
|
||||
*
|
||||
* There is no guarantee that `this.state` will be immediately updated, so
|
||||
* accessing `this.state` after calling this method may return the old value.
|
||||
*
|
||||
* @param {object} completeState Next state.
|
||||
* @final
|
||||
* @protected
|
||||
*/
|
||||
replaceState: function(completeState) {
|
||||
var compositeLifeCycleState = this._compositeLifeCycleState;
|
||||
invariant(
|
||||
this._lifeCycleState === ReactComponent.LifeCycle.MOUNTED ||
|
||||
compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
|
||||
'replaceState(...): Can only update a mounted (or mounting) component.'
|
||||
);
|
||||
invariant(
|
||||
compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
|
||||
compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
|
||||
'replaceState(...): Cannot update while unmounting component or during ' +
|
||||
'an existing state transition (such as within `render`).'
|
||||
);
|
||||
|
||||
this._pendingState = completeState;
|
||||
|
||||
// Do not trigger a state transition if we are in the middle of mounting or
|
||||
// receiving props because both of those will already be doing this.
|
||||
if (compositeLifeCycleState !== CompositeLifeCycle.MOUNTING &&
|
||||
compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_PROPS) {
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
|
||||
|
||||
var nextState = this._pendingState;
|
||||
this._pendingState = null;
|
||||
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(
|
||||
this._receivePropsAndState,
|
||||
this,
|
||||
this.props,
|
||||
nextState,
|
||||
transaction
|
||||
);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
|
||||
this._compositeLifeCycleState = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Receives next props and next state, and negotiates whether or not the
|
||||
* component should update as a result.
|
||||
*
|
||||
* @param {object} nextProps Next object to set as props.
|
||||
* @param {?object} nextState Next object to set as state.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @private
|
||||
*/
|
||||
_receivePropsAndState: function(nextProps, nextState, transaction) {
|
||||
if (!this.shouldComponentUpdate ||
|
||||
this.shouldComponentUpdate(nextProps, nextState)) {
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Merges new props and state, notifies delegate methods of update and
|
||||
* performs update.
|
||||
*
|
||||
* @param {object} nextProps Next object to set as properties.
|
||||
* @param {?object} nextState Next object to set as state.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @private
|
||||
*/
|
||||
_performComponentUpdate: function(nextProps, nextState, transaction) {
|
||||
var prevProps = this.props;
|
||||
var prevState = this.state;
|
||||
|
||||
if (this.componentWillUpdate) {
|
||||
this.componentWillUpdate(nextProps, nextState, transaction);
|
||||
}
|
||||
|
||||
this.props = nextProps;
|
||||
this.state = nextState;
|
||||
|
||||
this.updateComponent(transaction);
|
||||
|
||||
if (this.componentDidUpdate) {
|
||||
transaction.getReactOnDOMReady().enqueue(
|
||||
this,
|
||||
this.componentDidUpdate.bind(this, prevProps, prevState)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the component's currently mounted DOM representation.
|
||||
*
|
||||
* By default, this implements React's rendering and reconciliation algorithm.
|
||||
* Sophisticated clients may wish to override this.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @internal
|
||||
* @overridable
|
||||
*/
|
||||
updateComponent: function(transaction) {
|
||||
var currentComponent = this._renderedComponent;
|
||||
var nextComponent = this._renderValidatedComponent();
|
||||
if (currentComponent.constructor === nextComponent.constructor) {
|
||||
if (!nextComponent.props.isStatic) {
|
||||
currentComponent.receiveProps(nextComponent.props, 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 nextMarkup = nextComponent.mountComponent(thisID, transaction);
|
||||
ReactComponent.DOMIDOperations.dangerouslyReplaceNodeWithMarkupByID(
|
||||
currentComponentID,
|
||||
nextMarkup
|
||||
);
|
||||
this._renderedComponent = nextComponent;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Forces an update. This should only be invoked when it is known with
|
||||
* certainty that we are **not** in a DOM transaction.
|
||||
*
|
||||
* You may want to call this when you know that some deeper aspect of the
|
||||
* component's state has changed but `setState` was not called.
|
||||
*
|
||||
* This will not invoke `shouldUpdateComponent`, but it will invoke
|
||||
* `componentWillUpdate` and `componentDidUpdate`.
|
||||
*
|
||||
* @final
|
||||
* @protected
|
||||
*/
|
||||
forceUpdate: function() {
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(
|
||||
this._performComponentUpdate,
|
||||
this,
|
||||
this.props,
|
||||
this.state,
|
||||
transaction
|
||||
);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderValidatedComponent: function() {
|
||||
ReactCurrentOwner.current = this;
|
||||
var renderedComponent = this.render();
|
||||
ReactCurrentOwner.current = null;
|
||||
invariant(
|
||||
ReactComponent.isValidComponent(renderedComponent),
|
||||
'%s.render(): A valid ReactComponent must be returned.',
|
||||
this.constructor.displayName || 'ReactCompositeComponent'
|
||||
);
|
||||
return renderedComponent;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} props
|
||||
* @private
|
||||
*/
|
||||
_assertValidProps: function(props) {
|
||||
var propDeclarations = this.constructor.propDeclarations;
|
||||
var componentName = this.constructor.displayName;
|
||||
for (var propName in propDeclarations) {
|
||||
var checkProp = propDeclarations[propName];
|
||||
if (checkProp) {
|
||||
checkProp(props, propName, componentName);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_bindAutoBindMethods: function() {
|
||||
for (var autoBindKey in this.__reactAutoBindMap) {
|
||||
if (!this.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
|
||||
continue;
|
||||
}
|
||||
var method = this.__reactAutoBindMap[autoBindKey];
|
||||
this[autoBindKey] = this._bindAutoBindMethod(method);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Binds a method to the component.
|
||||
*
|
||||
* @param {function} method Method to be bound.
|
||||
* @private
|
||||
*/
|
||||
_bindAutoBindMethod: function(method) {
|
||||
var component = this;
|
||||
var hasWarned = false;
|
||||
function autoBound(a, b, c, d, e, tooMany) {
|
||||
invariant(
|
||||
typeof tooMany === 'undefined',
|
||||
'React.autoBind(...): Methods can only take a maximum of 5 arguments.'
|
||||
);
|
||||
if (component._lifeCycleState === ReactComponent.LifeCycle.MOUNTED) {
|
||||
return method.call(component, a, b, c, d, e);
|
||||
} else if (!hasWarned) {
|
||||
hasWarned = true;
|
||||
if (true) {
|
||||
console.warn(
|
||||
'React.autoBind(...): Attempted to invoke an auto-bound method ' +
|
||||
'on an unmounted instance of `%s`. You either have a memory leak ' +
|
||||
'or an event handler that is being run after unmounting.',
|
||||
component.constructor.displayName || 'ReactCompositeComponent'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return autoBound;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var ReactCompositeComponentBase = function() {};
|
||||
mixInto(ReactCompositeComponentBase, ReactComponent.Mixin);
|
||||
mixInto(ReactCompositeComponentBase, ReactOwner.Mixin);
|
||||
mixInto(ReactCompositeComponentBase, ReactPropTransferer.Mixin);
|
||||
mixInto(ReactCompositeComponentBase, ReactCompositeComponentMixin);
|
||||
|
||||
/**
|
||||
* Module for creating composite components.
|
||||
*
|
||||
* @class ReactCompositeComponent
|
||||
* @extends ReactComponent
|
||||
* @extends ReactOwner
|
||||
* @extends ReactPropTransferer
|
||||
*/
|
||||
var ReactCompositeComponent = {
|
||||
|
||||
LifeCycle: CompositeLifeCycle,
|
||||
|
||||
Base: ReactCompositeComponentBase,
|
||||
|
||||
/**
|
||||
* Creates a composite component class given a class specification.
|
||||
*
|
||||
* @param {object} spec Class specification (which must define `render`).
|
||||
* @return {function} Component constructor function.
|
||||
* @public
|
||||
*/
|
||||
createClass: function(spec) {
|
||||
var Constructor = function(initialProps, children) {
|
||||
this.construct(initialProps, children);
|
||||
};
|
||||
Constructor.prototype = new ReactCompositeComponentBase();
|
||||
Constructor.prototype.constructor = Constructor;
|
||||
mixSpecIntoComponent(Constructor, spec);
|
||||
invariant(
|
||||
Constructor.prototype.render,
|
||||
'createClass(...): Class specification must implement a `render` method.'
|
||||
);
|
||||
|
||||
var ConvenienceConstructor = function(props, children) {
|
||||
return new Constructor(props, children);
|
||||
};
|
||||
ConvenienceConstructor.componentConstructor = Constructor;
|
||||
ConvenienceConstructor.originalSpec = spec;
|
||||
return ConvenienceConstructor;
|
||||
},
|
||||
|
||||
/**
|
||||
* Marks the provided method to be automatically bound to the component.
|
||||
* This means the method's context will always be the component.
|
||||
*
|
||||
* React.createClass({
|
||||
* handleClick: React.autoBind(function() {
|
||||
* this.setState({jumping: true});
|
||||
* }),
|
||||
* render: function() {
|
||||
* return <a onClick={this.handleClick}>Jump</a>;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* @param {function} method Method to be bound.
|
||||
* @public
|
||||
*/
|
||||
autoBind: function(method) {
|
||||
function unbound() {
|
||||
invariant(
|
||||
false,
|
||||
'React.autoBind(...): Attempted to invoke an auto-bound method that ' +
|
||||
'was not correctly defined on the class specification.'
|
||||
);
|
||||
}
|
||||
unbound.__reactAutoBind = method;
|
||||
return unbound;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactCompositeComponent;
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 ReactCurrentOwner
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Keeps track of the current owner.
|
||||
*
|
||||
* The current owner is the component who should own any components that are
|
||||
* currently being constructed.
|
||||
*/
|
||||
var ReactCurrentOwner = {
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @type {ReactComponent}
|
||||
*/
|
||||
current: null
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactCurrentOwner;
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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 ReactDOM
|
||||
* @typechecks
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactNativeComponent = require("./ReactNativeComponent");
|
||||
|
||||
var mergeInto = require("./mergeInto");
|
||||
var objMapKeyVal = require("./objMapKeyVal");
|
||||
|
||||
/**
|
||||
* Creates a new React class that is idempotent and capable of containing other
|
||||
* React components. It accepts event listeners and DOM properties that are
|
||||
* valid according to `DOMProperty`.
|
||||
*
|
||||
* - Event listeners: `onClick`, `onMouseDown`, etc.
|
||||
* - DOM properties: `className`, `name`, `title`, etc.
|
||||
*
|
||||
* The `style` property functions differently from the DOM API. It accepts an
|
||||
* object mapping of style properties to values.
|
||||
*
|
||||
* @param {string} tag Tag name (e.g. `div`).
|
||||
* @param {boolean} omitClose True if the close tag should be omitted.
|
||||
* @private
|
||||
*/
|
||||
function createDOMComponentClass(tag, omitClose) {
|
||||
var Constructor = function(initialProps, children) {
|
||||
this.construct(initialProps, children);
|
||||
};
|
||||
|
||||
Constructor.prototype = new ReactNativeComponent(tag, omitClose);
|
||||
Constructor.prototype.constructor = Constructor;
|
||||
|
||||
return function(props, children) {
|
||||
return new Constructor(props, children);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mapping from supported HTML tags to `ReactNativeComponent` classes.
|
||||
* This is also accessible via `React.DOM`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
var ReactDOM = objMapKeyVal({
|
||||
a: false,
|
||||
abbr: false,
|
||||
address: false,
|
||||
audio: false,
|
||||
b: false,
|
||||
body: false,
|
||||
br: true,
|
||||
button: false,
|
||||
code: false,
|
||||
col: true,
|
||||
colgroup: false,
|
||||
dd: false,
|
||||
div: false,
|
||||
section: false,
|
||||
dl: false,
|
||||
dt: false,
|
||||
em: false,
|
||||
embed: true,
|
||||
fieldset: false,
|
||||
footer: false,
|
||||
// Danger: this gets monkeypatched! See ReactDOMForm for more info.
|
||||
form: false,
|
||||
h1: false,
|
||||
h2: false,
|
||||
h3: false,
|
||||
h4: false,
|
||||
h5: false,
|
||||
h6: false,
|
||||
header: false,
|
||||
hr: true,
|
||||
i: false,
|
||||
iframe: false,
|
||||
img: true,
|
||||
input: true,
|
||||
label: false,
|
||||
legend: false,
|
||||
li: false,
|
||||
line: false,
|
||||
nav: false,
|
||||
object: false,
|
||||
ol: false,
|
||||
optgroup: false,
|
||||
option: false,
|
||||
p: false,
|
||||
param: true,
|
||||
pre: false,
|
||||
select: false,
|
||||
small: false,
|
||||
source: false,
|
||||
span: false,
|
||||
sub: false,
|
||||
sup: false,
|
||||
strong: false,
|
||||
table: false,
|
||||
tbody: false,
|
||||
td: false,
|
||||
textarea: false,
|
||||
tfoot: false,
|
||||
th: false,
|
||||
thead: false,
|
||||
time: false,
|
||||
title: false,
|
||||
tr: false,
|
||||
u: false,
|
||||
ul: false,
|
||||
video: false,
|
||||
wbr: false,
|
||||
|
||||
// SVG
|
||||
circle: false,
|
||||
g: false,
|
||||
path: false,
|
||||
polyline: false,
|
||||
rect: false,
|
||||
svg: false,
|
||||
text: false
|
||||
}, createDOMComponentClass);
|
||||
|
||||
var injection = {
|
||||
injectComponentClasses: function(componentClasses) {
|
||||
mergeInto(ReactDOM, componentClasses);
|
||||
}
|
||||
};
|
||||
|
||||
ReactDOM.injection = injection;
|
||||
|
||||
module.exports = ReactDOM;
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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 ReactDOMForm
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactCompositeComponent = require("./ReactCompositeComponent");
|
||||
var ReactDOM = require("./ReactDOM");
|
||||
var ReactEvent = require("./ReactEvent");
|
||||
var EventConstants = require("./EventConstants");
|
||||
|
||||
// Store a reference to the <form> `ReactNativeComponent`.
|
||||
var form = ReactDOM.form;
|
||||
|
||||
/**
|
||||
* Since onSubmit doesn't bubble OR capture on the top level in IE8, we need
|
||||
* to capture it on the <form> element itself. There are lots of hacks we could
|
||||
* do to accomplish this, but the most reliable is to make <form> a
|
||||
* composite component and use `componentDidMount` to attach the event handlers.
|
||||
*/
|
||||
var ReactDOMForm = ReactCompositeComponent.createClass({
|
||||
render: function() {
|
||||
// TODO: Instead of using `ReactDOM` directly, we should use JSX. However,
|
||||
// `jshint` fails to parse JSX so in order for linting to work in the open
|
||||
// source repo, we need to just use `ReactDOM.form`.
|
||||
return this.transferPropsTo(form(null, this.props.children));
|
||||
},
|
||||
|
||||
componentDidMount: function(node) {
|
||||
ReactEvent.trapBubbledEvent(
|
||||
EventConstants.topLevelTypes.topSubmit,
|
||||
'submit',
|
||||
node
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ReactDOMForm;
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* 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 ReactDOMIDOperations
|
||||
* @typechecks
|
||||
*/
|
||||
|
||||
/*jslint evil: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
var CSSPropertyOperations = require("./CSSPropertyOperations");
|
||||
var DOMChildrenOperations = require("./DOMChildrenOperations");
|
||||
var DOMPropertyOperations = require("./DOMPropertyOperations");
|
||||
var ReactDOMNodeCache = require("./ReactDOMNodeCache");
|
||||
|
||||
var getTextContentAccessor = require("./getTextContentAccessor");
|
||||
var invariant = require("./invariant");
|
||||
|
||||
/**
|
||||
* Errors for properties that should not be updated with `updatePropertyById()`.
|
||||
*
|
||||
* @type {object}
|
||||
* @private
|
||||
*/
|
||||
var INVALID_PROPERTY_ERRORS = {
|
||||
content: '`content` must be set using `updateTextContentByID()`.',
|
||||
dangerouslySetInnerHTML:
|
||||
'`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.',
|
||||
style: '`style` must be set using `updateStylesByID()`.'
|
||||
};
|
||||
|
||||
/**
|
||||
* The DOM property to use when setting text content.
|
||||
*
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
var textContentAccessor = getTextContentAccessor() || 'NA';
|
||||
|
||||
/**
|
||||
* Operations used to process updates to DOM nodes. This is made injectable via
|
||||
* `ReactComponent.DOMIDOperations`.
|
||||
*/
|
||||
var ReactDOMIDOperations = {
|
||||
|
||||
/**
|
||||
* Updates a DOM node with new property values. This should only be used to
|
||||
* update DOM properties in `DOMProperty`.
|
||||
*
|
||||
* @param {string} id ID of the node to update.
|
||||
* @param {string} name A valid property name, see `DOMProperty`.
|
||||
* @param {*} value New value of the property.
|
||||
* @internal
|
||||
*/
|
||||
updatePropertyByID: function(id, name, value) {
|
||||
var node = ReactDOMNodeCache.getCachedNodeByID(id);
|
||||
invariant(
|
||||
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name),
|
||||
'updatePropertyByID(...): %s',
|
||||
INVALID_PROPERTY_ERRORS[name]
|
||||
);
|
||||
DOMPropertyOperations.setValueForProperty(node, name, value);
|
||||
},
|
||||
|
||||
/**
|
||||
* This should almost never be used instead of `updatePropertyByID()` due to
|
||||
* the extra object allocation required by the API. That said, this is useful
|
||||
* for batching up several operations across worker thread boundaries.
|
||||
*
|
||||
* @param {string} id ID of the node to update.
|
||||
* @param {object} properties A mapping of valid property names.
|
||||
* @internal
|
||||
* @see {ReactDOMIDOperations.updatePropertyByID}
|
||||
*/
|
||||
updatePropertiesByID: function(id, properties) {
|
||||
for (var name in properties) {
|
||||
if (!properties.hasOwnProperty(name)) {
|
||||
continue;
|
||||
}
|
||||
ReactDOMIDOperations.updatePropertiesByID(id, name, properties[name]);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a DOM node with new style values.
|
||||
*
|
||||
* @param {string} id ID of the node to update.
|
||||
* @param {object} styles Mapping from styles to values.
|
||||
* @internal
|
||||
*/
|
||||
updateStylesByID: function(id, styles) {
|
||||
var node = ReactDOMNodeCache.getCachedNodeByID(id);
|
||||
CSSPropertyOperations.setValueForStyles(node, styles);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a DOM node's innerHTML set by `props.dangerouslySetInnerHTML`.
|
||||
*
|
||||
* @param {string} id ID of the node to update.
|
||||
* @param {object} html An HTML object with the `__html` property.
|
||||
* @internal
|
||||
*/
|
||||
updateInnerHTMLByID: function(id, html) {
|
||||
var node = ReactDOMNodeCache.getCachedNodeByID(id);
|
||||
// HACK: IE8- normalize whitespace in innerHTML, removing leading spaces.
|
||||
// @see quirksmode.org/bugreports/archives/2004/11/innerhtml_and_t.html
|
||||
node.innerHTML = (html && html.__html || '').replace(/^ /g, ' ');
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a DOM node's text content set by `props.content`.
|
||||
*
|
||||
* @param {string} id ID of the node to update.
|
||||
* @param {string} content Text content.
|
||||
* @internal
|
||||
*/
|
||||
updateTextContentByID: function(id, content) {
|
||||
var node = ReactDOMNodeCache.getCachedNodeByID(id);
|
||||
node[textContentAccessor] = content;
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces a DOM node that exists in the document with markup.
|
||||
*
|
||||
* @param {string} id ID of child to be replaced.
|
||||
* @param {string} markup Dangerous markup to inject in place of child.
|
||||
* @internal
|
||||
* @see {Danger.dangerouslyReplaceNodeWithMarkup}
|
||||
*/
|
||||
dangerouslyReplaceNodeWithMarkupByID: function(id, markup) {
|
||||
var node = ReactDOMNodeCache.getCachedNodeByID(id);
|
||||
DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup(node, markup);
|
||||
ReactDOMNodeCache.purgeEntireCache();
|
||||
},
|
||||
|
||||
/**
|
||||
* TODO: We only actually *need* to purge the cache when we remove elements.
|
||||
* Detect if any elements were removed instead of blindly purging.
|
||||
*/
|
||||
manageChildrenByParentID: function(parentID, domOperations) {
|
||||
var parent = ReactDOMNodeCache.getCachedNodeByID(parentID);
|
||||
DOMChildrenOperations.manageChildren(parent, domOperations);
|
||||
ReactDOMNodeCache.purgeEntireCache();
|
||||
},
|
||||
|
||||
setTextNodeValueAtIndexByParentID: function(parentID, index, value) {
|
||||
var parent = ReactDOMNodeCache.getCachedNodeByID(parentID);
|
||||
DOMChildrenOperations.setTextNodeValueAtIndex(parent, index, value);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactDOMIDOperations;
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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 ReactDOMNodeCache
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactMount = require("./ReactMount");
|
||||
|
||||
var nodeCache = {};
|
||||
|
||||
/**
|
||||
* DOM node cache only intended for use by React. Placed into a shared module so
|
||||
* that both read and write utilities may benefit from a shared cache.
|
||||
*/
|
||||
var ReactDOMNodeCache = {
|
||||
/**
|
||||
* Releases fast id lookups (node/style cache). This implementation is
|
||||
* aggressive with purging because the bookkeeping associated with doing fine
|
||||
* grained deleted from the cache may outweight the benefits of the cache. The
|
||||
* heuristic that should be used to purge is 'any time anything is deleted'.
|
||||
* Typically this means that a large amount of content is being replaced and
|
||||
* several elements would need purging regardless. It's also a time when an
|
||||
* application is likely not in the middle of a "smooth operation" (such as
|
||||
* animating/scrolling).
|
||||
*/
|
||||
purgeEntireCache: function() {
|
||||
nodeCache = {};
|
||||
return nodeCache;
|
||||
},
|
||||
getCachedNodeByID: function(id) {
|
||||
return nodeCache[id] ||
|
||||
(nodeCache[id] =
|
||||
document.getElementById(id) ||
|
||||
ReactMount.findReactRenderedDOMNodeSlow(id));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ReactDOMNodeCache;
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 ReactDefaultInjection
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactDOM = require("./ReactDOM");
|
||||
var ReactDOMForm = require("./ReactDOMForm");
|
||||
|
||||
var DefaultEventPluginOrder = require("./DefaultEventPluginOrder");
|
||||
var EnterLeaveEventPlugin = require("./EnterLeaveEventPlugin");
|
||||
var EventPluginHub = require("./EventPluginHub");
|
||||
var ReactInstanceHandles = require("./ReactInstanceHandles");
|
||||
var SimpleEventPlugin = require("./SimpleEventPlugin");
|
||||
|
||||
function inject() {
|
||||
/**
|
||||
* Inject module for resolving DOM hierarchy and plugin ordering.
|
||||
*/
|
||||
EventPluginHub.injection.injectEventPluginOrder(DefaultEventPluginOrder);
|
||||
EventPluginHub.injection.injectInstanceHandle(ReactInstanceHandles);
|
||||
|
||||
/**
|
||||
* Two important event plugins included by default (without having to require
|
||||
* them).
|
||||
*/
|
||||
EventPluginHub.injection.injectEventPluginsByName({
|
||||
'SimpleEventPlugin': SimpleEventPlugin,
|
||||
'EnterLeaveEventPlugin': EnterLeaveEventPlugin
|
||||
});
|
||||
|
||||
/*
|
||||
* This is a bit of a hack. We need to override the <form> element
|
||||
* to be a composite component because IE8 does not bubble or capture
|
||||
* submit to the top level. In order to make this work with our
|
||||
* dependency graph we need to inject it here.
|
||||
*/
|
||||
ReactDOM.injection.injectComponentClasses({
|
||||
form: ReactDOMForm
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
inject: inject
|
||||
};
|
||||
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* 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 ReactEvent
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var BrowserEnv = require("./BrowserEnv");
|
||||
var EventConstants = require("./EventConstants");
|
||||
var EventPluginHub = require("./EventPluginHub");
|
||||
var ExecutionEnvironment = require("./ExecutionEnvironment");
|
||||
var NormalizedEventListener = require("./NormalizedEventListener");
|
||||
|
||||
var invariant = require("./invariant");
|
||||
var isEventSupported = require("./isEventSupported");
|
||||
|
||||
var registrationNames = EventPluginHub.registrationNames;
|
||||
var topLevelTypes = EventConstants.topLevelTypes;
|
||||
var listen = NormalizedEventListener.listen;
|
||||
var capture = NormalizedEventListener.capture;
|
||||
|
||||
/**
|
||||
* `ReactEvent` is used to attach top-level event listeners. For example:
|
||||
*
|
||||
* ReactEvent.putListener('myID', 'onClick', myFunction);
|
||||
*
|
||||
* This would allocate a "registration" of `('onClick', myFunction)` on 'myID'.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Overview of React and the event system:
|
||||
*
|
||||
* .
|
||||
* +-------------+ .
|
||||
* | DOM | .
|
||||
* +-------------+ . +-----------+
|
||||
* + . +--------+|SimpleEvent|
|
||||
* | . | |Plugin |
|
||||
* +-----|-------+ . v +-----------+
|
||||
* | | | . +--------------+ +------------+
|
||||
* | +------------.---->|EventPluginHub| | Event |
|
||||
* | | . | | +-----------+ | Propagators|
|
||||
* | ReactEvent | . | | |TapEvent | |------------|
|
||||
* | | . | |<---+|Plugin | |other plugin|
|
||||
* | +------------.---------+ | +-----------+ | utilities |
|
||||
* | | | . | | | +------------+
|
||||
* | | | . +---|----------+
|
||||
* | | | . | ^ +-----------+
|
||||
* | | | . | | |Enter/Leave|
|
||||
* +-----| ------+ . | +-------+|Plugin |
|
||||
* | . v +-----------+
|
||||
* + . +--------+
|
||||
* +-------------+ . |callback|
|
||||
* | application | . |registry|
|
||||
* |-------------| . +--------+
|
||||
* | | .
|
||||
* | | .
|
||||
* | | .
|
||||
* | | .
|
||||
* +-------------+ .
|
||||
* .
|
||||
* React Core . General Purpose Event Plugin System
|
||||
*/
|
||||
|
||||
/**
|
||||
* We listen for bubbled touch events on the document object.
|
||||
*
|
||||
* Firefox v8.01 (and possibly others) exhibited strange behavior when mounting
|
||||
* `onmousemove` events at some node that was not the document element. The
|
||||
* symptoms were that if your mouse is not moving over something contained
|
||||
* within that mount point (for example on the background) the top-level
|
||||
* listeners for `onmousemove` won't be called. However, if you register the
|
||||
* `mousemove` on the document object, then it will of course catch all
|
||||
* `mousemove`s. This along with iOS quirks, justifies restricting top-level
|
||||
* listeners to the document object only, at least for these movement types of
|
||||
* events and possibly all events.
|
||||
*
|
||||
* @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
|
||||
*
|
||||
* Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
|
||||
* they bubble to document.
|
||||
*
|
||||
* @see http://www.quirksmode.org/dom/events/keys.html.
|
||||
*/
|
||||
|
||||
var _isListening = false;
|
||||
|
||||
var EVENT_LISTEN_MISUSE;
|
||||
var WORKER_DISABLE;
|
||||
|
||||
if (true) {
|
||||
EVENT_LISTEN_MISUSE =
|
||||
'You must register listeners at the top of the document, only once - ' +
|
||||
'and only in the main UI thread of a browser - if you are attempting ' +
|
||||
'listen in a worker, the framework is probably doing something wrong ' +
|
||||
'and you should report this immediately.';
|
||||
WORKER_DISABLE =
|
||||
'Cannot disable event listening in Worker thread. This is likely a ' +
|
||||
'bug in the framework. Please report immediately.';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Traps top-level events that bubble. Delegates to the main dispatcher
|
||||
* `handleTopLevel` after performing some basic normalization via
|
||||
* `TopLevelCallbackCreator.createTopLevelCallback`.
|
||||
*/
|
||||
function trapBubbledEvent(topLevelType, handlerBaseName, onWhat) {
|
||||
listen(
|
||||
onWhat,
|
||||
handlerBaseName,
|
||||
ReactEvent.TopLevelCallbackCreator.createTopLevelCallback(topLevelType)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traps a top-level event by using event capturing.
|
||||
*/
|
||||
function trapCapturedEvent(topLevelType, handlerBaseName, onWhat) {
|
||||
capture(
|
||||
onWhat,
|
||||
handlerBaseName,
|
||||
ReactEvent.TopLevelCallbackCreator.createTopLevelCallback(topLevelType)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to document scroll and window resize events that may change the
|
||||
* document scroll values. We store those results so as to discourage
|
||||
* application code from asking the DOM itself which could trigger additional
|
||||
* reflows.
|
||||
*/
|
||||
function registerDocumentScrollListener() {
|
||||
listen(window, 'scroll', function(nativeEvent) {
|
||||
if (nativeEvent.target === window) {
|
||||
BrowserEnv.refreshAuthoritativeScrollValues();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerDocumentResizeListener() {
|
||||
listen(window, 'resize', function(nativeEvent) {
|
||||
if (nativeEvent.target === window) {
|
||||
BrowserEnv.refreshAuthoritativeScrollValues();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary of `ReactEvent` event handling:
|
||||
*
|
||||
* - We trap low level 'top-level' events.
|
||||
*
|
||||
* - We dedupe cross-browser event names into these 'top-level types' so that
|
||||
* `DOMMouseScroll` or `mouseWheel` both become `topMouseWheel`.
|
||||
*
|
||||
* - At this point we have native browser events with the top-level type that
|
||||
* was used to catch it at the top-level.
|
||||
*
|
||||
* - We continuously stream these native events (and their respective top-level
|
||||
* types) to the event plugin system `EventPluginHub` and ask the plugin
|
||||
* system if it was able to extract `AbstractEvent` objects. `AbstractEvent`
|
||||
* objects are the events that applications actually deal with - they are not
|
||||
* native browser events but cross-browser wrappers.
|
||||
*
|
||||
* - When returning the `AbstractEvent` objects, `EventPluginHub` will make
|
||||
* sure each abstract event is annotated with "dispatches", which are the
|
||||
* sequence of listeners (and IDs) that care about the event.
|
||||
*
|
||||
* - These `AbstractEvent` objects are fed back into the event plugin system,
|
||||
* which in turn executes these dispatches.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function listenAtTopLevel(touchNotMouse) {
|
||||
invariant(
|
||||
!_isListening,
|
||||
'listenAtTopLevel(...): Cannot setup top-level listener more than once.'
|
||||
);
|
||||
var mountAt = document;
|
||||
|
||||
registerDocumentScrollListener();
|
||||
registerDocumentResizeListener();
|
||||
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.topMouseWheel, 'mousewheel', 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.topChange, 'change', mountAt);
|
||||
trapBubbledEvent(
|
||||
topLevelTypes.topDOMCharacterDataModified,
|
||||
'DOMCharacterDataModified',
|
||||
mountAt
|
||||
);
|
||||
|
||||
// Firefox needs to capture a different mouse scroll event.
|
||||
// @see http://www.quirksmode.org/dom/events/tests/scroll.html
|
||||
trapBubbledEvent(topLevelTypes.topMouseWheel, 'DOMMouseScroll', mountAt);
|
||||
// IE < 9 doesn't support capturing so just trap the bubbled event there.
|
||||
if (isEventSupported('scroll', true)) {
|
||||
trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
|
||||
} else {
|
||||
trapBubbledEvent(topLevelTypes.topScroll, 'scroll', window);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the heart of `ReactEvent`. It simply streams the top-level native
|
||||
* events to `EventPluginHub`.
|
||||
*
|
||||
* @param {object} topLevelType Record from `EventConstants`.
|
||||
* @param {Event} nativeEvent A Standard Event with fixed `target` property.
|
||||
* @param {DOMElement} renderedTarget Element of interest to the framework.
|
||||
* @param {string} renderedTargetID string ID of `renderedTarget`.
|
||||
* @internal
|
||||
*/
|
||||
function handleTopLevel(
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
renderedTargetID,
|
||||
renderedTarget) {
|
||||
var abstractEvents = EventPluginHub.extractAbstractEvents(
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
renderedTargetID,
|
||||
renderedTarget
|
||||
);
|
||||
|
||||
// The event queue being processed in the same cycle allows preventDefault.
|
||||
EventPluginHub.enqueueAbstractEvents(abstractEvents);
|
||||
EventPluginHub.processAbstractEventQueue();
|
||||
}
|
||||
|
||||
function setEnabled(enabled) {
|
||||
invariant(
|
||||
ExecutionEnvironment.canUseDOM,
|
||||
'setEnabled(...): Cannot toggle event listening in a Worker thread. This ' +
|
||||
'is likely a bug in the framework. Please report immediately.'
|
||||
);
|
||||
ReactEvent.TopLevelCallbackCreator.setEnabled(enabled);
|
||||
}
|
||||
|
||||
function isEnabled() {
|
||||
return ReactEvent.TopLevelCallbackCreator.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that top-level event delegation listeners are listening at `mountAt`.
|
||||
* 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 {object} TopLevelCallbackCreator Module that can create top-level
|
||||
* callback handlers.
|
||||
* @internal
|
||||
*/
|
||||
function ensureListening(touchNotMouse, TopLevelCallbackCreator) {
|
||||
invariant(
|
||||
ExecutionEnvironment.canUseDOM,
|
||||
'ensureListening(...): Cannot toggle event listening in a Worker thread. ' +
|
||||
'This is likely a bug in the framework. Please report immediately.'
|
||||
);
|
||||
if (!_isListening) {
|
||||
ReactEvent.TopLevelCallbackCreator = TopLevelCallbackCreator;
|
||||
listenAtTopLevel(touchNotMouse);
|
||||
_isListening = true;
|
||||
}
|
||||
}
|
||||
|
||||
var ReactEvent = {
|
||||
TopLevelCallbackCreator: null, // Injectable callback creator.
|
||||
handleTopLevel: handleTopLevel,
|
||||
setEnabled: setEnabled,
|
||||
isEnabled: isEnabled,
|
||||
ensureListening: ensureListening,
|
||||
registrationNames: registrationNames,
|
||||
putListener: EventPluginHub.putListener,
|
||||
getListener: EventPluginHub.getListener,
|
||||
deleteAllListeners: EventPluginHub.deleteAllListeners,
|
||||
trapBubbledEvent: trapBubbledEvent,
|
||||
trapCapturedEvent: trapCapturedEvent
|
||||
};
|
||||
|
||||
module.exports = ReactEvent;
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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 ReactEventTopLevelCallback
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ExecutionEnvironment = require("./ExecutionEnvironment");
|
||||
var ReactEvent = require("./ReactEvent");
|
||||
var ReactInstanceHandles = require("./ReactInstanceHandles");
|
||||
|
||||
var getDOMNodeID = require("./getDOMNodeID");
|
||||
|
||||
var _topLevelListenersEnabled = true;
|
||||
|
||||
var ReactEventTopLevelCallback = {
|
||||
|
||||
/**
|
||||
* @param {boolean} enabled Whether or not all callbacks that have ever been
|
||||
* created with this module should be enabled.
|
||||
*/
|
||||
setEnabled: function(enabled) {
|
||||
_topLevelListenersEnabled = !!enabled;
|
||||
},
|
||||
|
||||
isEnabled: function() {
|
||||
return _topLevelListenersEnabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* For a given `topLevelType`, creates a callback that could be added as a
|
||||
* listener to the document. That top level callback will simply fix the
|
||||
* native events before invoking `handleTopLevel`.
|
||||
*
|
||||
* - Raw native events cannot be trusted to describe their targets correctly
|
||||
* so we expect that the argument to the nested function has already been
|
||||
* fixed. But the `target` property may not be something of interest to
|
||||
* React, so we find the most suitable target. But even at that point, DOM
|
||||
* Elements (the target ) can't be trusted to describe their IDs correctly
|
||||
* so we obtain the ID in a reliable manner and pass it to
|
||||
* `handleTopLevel`. The target/id that we found to be relevant to our
|
||||
* framework are called `renderedTarget`/`renderedTargetID` respectively.
|
||||
*/
|
||||
createTopLevelCallback: function(topLevelType) {
|
||||
return function(fixedNativeEvent) {
|
||||
if (!_topLevelListenersEnabled) {
|
||||
return;
|
||||
}
|
||||
var renderedTarget = ReactInstanceHandles.getFirstReactDOM(
|
||||
fixedNativeEvent.target
|
||||
) || ExecutionEnvironment.global;
|
||||
var renderedTargetID = getDOMNodeID(renderedTarget);
|
||||
var event = fixedNativeEvent;
|
||||
var target = renderedTarget;
|
||||
ReactEvent.handleTopLevel(topLevelType, event, renderedTargetID, target);
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactEventTopLevelCallback;
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 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 ReactInputSelection
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// It is not safe to read the document.activeElement property in IE if there's
|
||||
// nothing focused.
|
||||
function getActiveElement() {
|
||||
try {
|
||||
return document.activeElement;
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ReactInputSelection: React input selection module. Based on Selection.js,
|
||||
* but modified to be suitable for react and has a couple of bug fixes (doesn't
|
||||
* assume buttons have range selections allowed).
|
||||
* Input selection module for React.
|
||||
*/
|
||||
var ReactInputSelection = {
|
||||
|
||||
hasSelectionCapabilities: function(elem) {
|
||||
return elem && (
|
||||
(elem.nodeName === 'INPUT' && elem.type === 'text') ||
|
||||
elem.nodeName === 'TEXTAREA' ||
|
||||
elem.contentEditable === 'true'
|
||||
);
|
||||
},
|
||||
|
||||
getSelectionInformation: function() {
|
||||
var focusedElem = getActiveElement();
|
||||
return {
|
||||
focusedElem: focusedElem,
|
||||
selectionRange:
|
||||
ReactInputSelection.hasSelectionCapabilities(focusedElem) ?
|
||||
ReactInputSelection.getSelection(focusedElem) :
|
||||
null
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @restoreSelection: If any selection information was potentially lost,
|
||||
* restore it. This is useful when performing operations that could remove dom
|
||||
* nodes and place them back in, resulting in focus being lost.
|
||||
*/
|
||||
restoreSelection: function(priorSelectionInformation) {
|
||||
var curFocusedElem = getActiveElement();
|
||||
var priorFocusedElem = priorSelectionInformation.focusedElem;
|
||||
var priorSelectionRange = priorSelectionInformation.selectionRange;
|
||||
if (curFocusedElem !== priorFocusedElem &&
|
||||
document.getElementById(priorFocusedElem.id)) {
|
||||
if (ReactInputSelection.hasSelectionCapabilities(priorFocusedElem)) {
|
||||
ReactInputSelection.setSelection(
|
||||
priorFocusedElem,
|
||||
priorSelectionRange
|
||||
);
|
||||
}
|
||||
priorFocusedElem.focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @getSelection: Gets the selection bounds of a textarea or input.
|
||||
* -@input: Look up selection bounds of this input or textarea
|
||||
* -@return {start: selectionStart, end: selectionEnd}
|
||||
*/
|
||||
getSelection: function(input) {
|
||||
var range;
|
||||
if (input.contentEditable === 'true' && window.getSelection) {
|
||||
range = window.getSelection().getRangeAt(0);
|
||||
var commonAncestor = range.commonAncestorContainer;
|
||||
if (commonAncestor && commonAncestor.nodeType === 3) {
|
||||
commonAncestor = commonAncestor.parentNode;
|
||||
}
|
||||
if (commonAncestor !== input) {
|
||||
return {start: 0, end: 0};
|
||||
} else {
|
||||
return {start: range.startOffset, end: range.endOffset};
|
||||
}
|
||||
}
|
||||
|
||||
if (!document.selection) {
|
||||
// Mozilla, Safari, etc.
|
||||
return {start: input.selectionStart, end: input.selectionEnd};
|
||||
}
|
||||
|
||||
range = document.selection.createRange();
|
||||
if (range.parentElement() !== input) {
|
||||
// There can only be one selection per document in IE, so if the
|
||||
// containing element of the document's selection isn't our text field,
|
||||
// our text field must have no selection.
|
||||
return {start: 0, end: 0};
|
||||
}
|
||||
|
||||
var length = input.value.length;
|
||||
|
||||
if (input.nodeName === 'INPUT') {
|
||||
return {
|
||||
start: -range.moveStart('character', -length),
|
||||
end: -range.moveEnd('character', -length)
|
||||
};
|
||||
} else {
|
||||
var range2 = range.duplicate();
|
||||
range2.moveToElementText(input);
|
||||
range2.setEndPoint('StartToEnd', range);
|
||||
var end = length - range2.text.length;
|
||||
range2.setEndPoint('StartToStart', range);
|
||||
return {
|
||||
start: length - range2.text.length,
|
||||
end: end
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @setSelection: Sets the selection bounds of a textarea or input and focuses
|
||||
* the input.
|
||||
* -@input Set selection bounds of this input or textarea
|
||||
* -@rangeObj Object of same form that is returned from get*
|
||||
*/
|
||||
setSelection: function(input, rangeObj) {
|
||||
var range;
|
||||
var start = rangeObj.start;
|
||||
var end = rangeObj.end;
|
||||
if (typeof end === 'undefined') {
|
||||
end = start;
|
||||
}
|
||||
if (document.selection) {
|
||||
// IE is inconsistent about character offsets when it comes to carriage
|
||||
// returns, so we need to manually take them into account
|
||||
if (input.tagName === 'TEXTAREA') {
|
||||
var cr_before =
|
||||
(input.value.slice(0, start).match(/\r/g) || []).length;
|
||||
var cr_inside =
|
||||
(input.value.slice(start, end).match(/\r/g) || []).length;
|
||||
start -= cr_before;
|
||||
end -= cr_before + cr_inside;
|
||||
}
|
||||
range = input.createTextRange();
|
||||
range.collapse(true);
|
||||
range.moveStart('character', start);
|
||||
range.moveEnd('character', end - start);
|
||||
range.select();
|
||||
} else {
|
||||
if (input.contentEditable === 'true') {
|
||||
if (input.childNodes.length === 1) {
|
||||
range = document.createRange();
|
||||
range.setStart(input.childNodes[0], start);
|
||||
range.setEnd(input.childNodes[0], end);
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
} else {
|
||||
input.selectionStart = start;
|
||||
input.selectionEnd = Math.min(end, input.value.length);
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactInputSelection;
|
||||
@@ -0,0 +1,326 @@
|
||||
/**
|
||||
* 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 ReactInstanceHandles
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var getDOMNodeID = require("./getDOMNodeID");
|
||||
var invariant = require("./invariant");
|
||||
|
||||
var SEPARATOR = '.';
|
||||
var SEPARATOR_LENGTH = SEPARATOR.length;
|
||||
|
||||
/**
|
||||
* Maximum depth of traversals before we consider the possibility of a bad ID.
|
||||
*/
|
||||
var MAX_TREE_DEPTH = 100;
|
||||
|
||||
/**
|
||||
* Checks if a character in the supplied ID is a separator or the end.
|
||||
*
|
||||
* @param {string} id A React DOM ID.
|
||||
* @param {number} index Index of the character to check.
|
||||
* @return {boolean} True if the character is a separator or end of the ID.
|
||||
* @private
|
||||
*/
|
||||
function isMarker(id, index) {
|
||||
return id.charAt(index) === SEPARATOR || index === id.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the supplied string is a valid React DOM ID.
|
||||
*
|
||||
* @param {string} id A React DOM ID, maybe.
|
||||
* @return {boolean} True if the string is a valid React DOM ID.
|
||||
* @private
|
||||
*/
|
||||
function isValidID(id) {
|
||||
return id === '' || (
|
||||
id.charAt(0) === SEPARATOR && id.charAt(id.length - 1) !== SEPARATOR
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the supplied `node` is rendered by React.
|
||||
*
|
||||
* @param {DOMElement} node DOM Element to check.
|
||||
* @return {boolean} True if the DOM Element appears to be rendered by React.
|
||||
* @private
|
||||
*/
|
||||
function isRenderedByReact(node) {
|
||||
var id = getDOMNodeID(node);
|
||||
return id && id.charAt(0) === SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent ID of the supplied React DOM ID, `id`.
|
||||
*
|
||||
* @param {string} id ID of a component.
|
||||
* @return {string} ID of the parent, or an empty string.
|
||||
* @private
|
||||
*/
|
||||
function parentID(id) {
|
||||
return id ? id.substr(0, id.lastIndexOf(SEPARATOR)) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {?string} start ID at which to start traversal.
|
||||
* @param {?string} stop ID at which to end traversal.
|
||||
* @param {function} cb Callback to invoke each ID with.
|
||||
* @param {?boolean} skipFirst Whether or not to skip the first node.
|
||||
* @param {?boolean} skipLast Whether or not to skip the last node.
|
||||
* @private
|
||||
*/
|
||||
function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) {
|
||||
start = start || '';
|
||||
stop = stop || '';
|
||||
invariant(
|
||||
start !== stop,
|
||||
'traverseParentPath(...): Cannot traverse from and to the same ID, `%s`.',
|
||||
start
|
||||
);
|
||||
var ancestorID = ReactInstanceHandles.getFirstCommonAncestorID(start, stop);
|
||||
var traverseUp = ancestorID === stop;
|
||||
invariant(
|
||||
traverseUp || ancestorID === start,
|
||||
'traverseParentPath(%s, %s, ...): Cannot traverse from two IDs that do ' +
|
||||
'not have a parent path.',
|
||||
start,
|
||||
stop
|
||||
);
|
||||
// Traverse from `start` to `stop` one depth at a time.
|
||||
var depth = 0;
|
||||
var traverse = traverseUp ? parentID : ReactInstanceHandles.nextDescendantID;
|
||||
for (var id = start; /* until break */; id = traverse(id, stop)) {
|
||||
if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) {
|
||||
cb(id, traverseUp, arg);
|
||||
}
|
||||
if (id === stop) {
|
||||
// Only break //after// visiting `stop`.
|
||||
break;
|
||||
}
|
||||
invariant(
|
||||
depth++ < MAX_TREE_DEPTH,
|
||||
'traverseParentPath(%s, %s, ...): Detected an infinite loop while ' +
|
||||
'traversing the React DOM ID tree. This may be due to malformed IDs: %s',
|
||||
start, stop
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the IDs assigned to DOM representations of React components. This
|
||||
* uses a specific scheme in order to traverse the DOM efficiently (e.g. in
|
||||
* order to simulate events).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
var ReactInstanceHandles = {
|
||||
|
||||
separator: SEPARATOR,
|
||||
|
||||
/**
|
||||
* Traverses up the ancestors of the supplied node to find a node that is a
|
||||
* DOM representation of a React component.
|
||||
*
|
||||
* @param {DOMElement} node
|
||||
* @return {?DOMElement}
|
||||
* @internal
|
||||
*/
|
||||
getFirstReactDOM: function(node) {
|
||||
var current = node;
|
||||
while (current && current.parentNode !== current) {
|
||||
if (isRenderedByReact(current)) {
|
||||
return current;
|
||||
}
|
||||
current = current.parentNode;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds a node with the supplied `id` inside of the supplied `ancestorNode`.
|
||||
* Exploits the ID naming scheme to perform the search quickly.
|
||||
*
|
||||
* @param {DOMElement} ancestorNode Search from this root.
|
||||
* @pararm {string} id ID of the DOM representation of the component.
|
||||
* @return {?DOMElement} DOM element with the supplied `id`, if one exists.
|
||||
* @internal
|
||||
*/
|
||||
findComponentRoot: function(ancestorNode, id) {
|
||||
var child = ancestorNode.firstChild;
|
||||
while (child) {
|
||||
if (id === child.id) {
|
||||
return child;
|
||||
} else if (id.indexOf(child.id) === 0) {
|
||||
return ReactInstanceHandles.findComponentRoot(child, id);
|
||||
}
|
||||
child = child.nextSibling;
|
||||
}
|
||||
// Effectively: return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the nearest common ancestor ID of two IDs.
|
||||
*
|
||||
* Using this ID scheme, the nearest common ancestor ID is the longest common
|
||||
* prefix of the two IDs that immediately preceded a "marker" in both strings.
|
||||
*
|
||||
* @param {string} oneID
|
||||
* @param {string} twoID
|
||||
* @return {string} Nearest common ancestor ID, or the empty string if none.
|
||||
* @internal
|
||||
*/
|
||||
getFirstCommonAncestorID: function(oneID, twoID) {
|
||||
var minLength = Math.min(oneID.length, twoID.length);
|
||||
if (minLength === 0) {
|
||||
return '';
|
||||
}
|
||||
var lastCommonMarkerIndex = 0;
|
||||
// Use `<=` to traverse until the "EOL" of the shorter string.
|
||||
for (var i = 0; i <= minLength; i++) {
|
||||
if (isMarker(oneID, i) && isMarker(twoID, i)) {
|
||||
lastCommonMarkerIndex = i;
|
||||
} else if (oneID.charAt(i) !== twoID.charAt(i)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
var longestCommonID = oneID.substr(0, lastCommonMarkerIndex);
|
||||
invariant(
|
||||
isValidID(longestCommonID),
|
||||
'getFirstCommonAncestorID(%s, %s): Expected a valid React DOM ID: %s',
|
||||
oneID,
|
||||
twoID,
|
||||
longestCommonID
|
||||
);
|
||||
return longestCommonID;
|
||||
},
|
||||
/**
|
||||
* Creates a DOM ID to use when mounting React components.
|
||||
*
|
||||
* @param {number} mountPointCount The count of React renders so far.
|
||||
* @return {string} React root ID.
|
||||
* @internal
|
||||
*/
|
||||
getReactRootID: function(mountPointCount) {
|
||||
return '.reactRoot[' + mountPointCount + ']';
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the DOM ID of the React component that is the root of the tree that
|
||||
* contains the React component with the supplied DOM ID.
|
||||
*
|
||||
* @param {string} id DOM ID of a React component.
|
||||
* @return {string} DOM ID of the React component that is the root.
|
||||
* @internal
|
||||
*/
|
||||
getReactRootIDFromNodeID: function(id) {
|
||||
var regexResult = /\.reactRoot\[[^\]]+\]/.exec(id);
|
||||
return regexResult && regexResult[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
|
||||
* should would receive a `mouseEnter` or `mouseLeave` event.
|
||||
*
|
||||
* NOTE: Does not invoke the callback on the nearest common ancestor because
|
||||
* nothing "entered" or "left" that element.
|
||||
*
|
||||
* @param {string} leaveID ID being left.
|
||||
* @param {string} enterID ID being entered.
|
||||
* @param {function} cb Callback to invoke on each entered/left ID.
|
||||
* @param {*} upArg Argument to invoke the callback with on left IDs.
|
||||
* @param {*} downArg Argument to invoke the callback with on entered IDs.
|
||||
* @internal
|
||||
*/
|
||||
traverseEnterLeave: function(leaveID, enterID, cb, upArg, downArg) {
|
||||
var longestCommonID = ReactInstanceHandles.getFirstCommonAncestorID(
|
||||
leaveID,
|
||||
enterID
|
||||
);
|
||||
if (longestCommonID !== leaveID) {
|
||||
traverseParentPath(leaveID, longestCommonID, cb, upArg, false, true);
|
||||
}
|
||||
if (longestCommonID !== enterID) {
|
||||
traverseParentPath(longestCommonID, enterID, cb, downArg, true, false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
traverseTwoPhase: function(targetID, cb, arg) {
|
||||
if (targetID) {
|
||||
traverseParentPath('', targetID, cb, arg, true, false);
|
||||
traverseParentPath(targetID, '', cb, arg, false, true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the next DOM ID on the tree path from the supplied `ancestorID` to the
|
||||
* supplied `destinationID`. If they are equal, the ID is returned.
|
||||
*
|
||||
* @param {string} ancestorID ID of an ancestor node of `destinationID`.
|
||||
* @param {string} destinationID ID of the destination node.
|
||||
* @return {string} Next ID on the path from `ancestorID` to `destinationID`.
|
||||
* @internal
|
||||
*/
|
||||
nextDescendantID: function(ancestorID, destinationID) {
|
||||
invariant(
|
||||
isValidID(ancestorID) && isValidID(destinationID),
|
||||
'nextDescendantID(%s, %s): Received an invalid React DOM ID.',
|
||||
ancestorID,
|
||||
destinationID
|
||||
);
|
||||
var longestCommonID = ReactInstanceHandles.getFirstCommonAncestorID(
|
||||
ancestorID,
|
||||
destinationID
|
||||
);
|
||||
invariant(
|
||||
longestCommonID === ancestorID,
|
||||
'nextDescendantID(...): React has made an invalid assumption about the ' +
|
||||
'DOM hierarchy. Expected `%s` to be an ancestor of `%s`.',
|
||||
ancestorID,
|
||||
destinationID
|
||||
);
|
||||
if (ancestorID === destinationID) {
|
||||
return ancestorID;
|
||||
}
|
||||
// Skip over the ancestor and the immediate separator. Traverse until we hit
|
||||
// another separator or we reach the end of `destinationID`.
|
||||
var start = ancestorID.length + SEPARATOR_LENGTH;
|
||||
for (var i = start; i < destinationID.length; i++) {
|
||||
if (isMarker(destinationID, i)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return destinationID.substr(0, i);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactInstanceHandles;
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 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 ReactMount
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactEvent = require("./ReactEvent");
|
||||
var ReactInstanceHandles = require("./ReactInstanceHandles");
|
||||
var ReactEventTopLevelCallback = require("./ReactEventTopLevelCallback");
|
||||
|
||||
var $ = require("./$");
|
||||
|
||||
var globalMountPointCounter = 0;
|
||||
|
||||
/** Mapping from reactRoot DOM ID to React component instance. */
|
||||
var instanceByReactRootID = {};
|
||||
|
||||
/** Mapping from reactRoot DOM ID to `container` nodes. */
|
||||
var containersByReactRootID = {};
|
||||
|
||||
/**
|
||||
* @param {DOMElement} container DOM element that may contain a React component.
|
||||
* @return {?string} A "reactRoot" ID, if a React component is rendered.
|
||||
*/
|
||||
function getReactRootID(container) {
|
||||
return container.firstChild && container.firstChild.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounting is the process of initializing a React component by creatings its
|
||||
* representative DOM elements and inserting them into a supplied `container`.
|
||||
* Any prior content inside `container` is destroyed in the process.
|
||||
*
|
||||
* ReactMount.renderComponent(component, $('container'));
|
||||
*
|
||||
* <div id="container"> <-- Supplied `container`.
|
||||
* <div id=".reactRoot[3]"> <-- Rendered reactRoot of React component.
|
||||
* // ...
|
||||
* </div>
|
||||
* </div>
|
||||
*
|
||||
* Inside of `container`, the first element rendered is the "reactRoot".
|
||||
*/
|
||||
var ReactMount = {
|
||||
|
||||
/** Time spent generating markup. */
|
||||
totalInstantiationTime: 0,
|
||||
|
||||
/** Time spent inserting markup into the DOM. */
|
||||
totalInjectionTime: 0,
|
||||
|
||||
/** Whether support for touch events should be initialized. */
|
||||
useTouchEvents: false,
|
||||
|
||||
/**
|
||||
* This is a hook provided to support rendering React components while
|
||||
* ensuring that the apparent scroll position of its `container` does not
|
||||
* change.
|
||||
*
|
||||
* @param {DOMElement} container The `container` being rendered into.
|
||||
* @param {function} renderCallback This must be called once to do the render.
|
||||
*/
|
||||
scrollMonitor: function(container, renderCallback) {
|
||||
renderCallback();
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures tht 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 {object} TopLevelCallbackCreator
|
||||
* @private
|
||||
*/
|
||||
prepareTopLevelEvents: function(TopLevelCallbackCreator) {
|
||||
ReactEvent.ensureListening(
|
||||
ReactMount.useTouchEvents,
|
||||
TopLevelCallbackCreator
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders a React component into the DOM in the supplied `container`.
|
||||
*
|
||||
* If the React component was previously rendered into `container`, this will
|
||||
* perform an update on it and only mutate the DOM as necessary to reflect the
|
||||
* latest React component.
|
||||
*
|
||||
* @param {ReactComponent} nextComponent Component instance to render.
|
||||
* @param {DOMElement} container DOM element to render into.
|
||||
* @return {ReactComponent} Component instance rendered in `container`.
|
||||
*/
|
||||
renderComponent: function(nextComponent, container) {
|
||||
var prevComponent = instanceByReactRootID[getReactRootID(container)];
|
||||
if (prevComponent) {
|
||||
var nextProps = nextComponent.props;
|
||||
ReactMount.scrollMonitor(container, function() {
|
||||
prevComponent.replaceProps(nextProps);
|
||||
});
|
||||
return prevComponent;
|
||||
}
|
||||
|
||||
ReactMount.prepareTopLevelEvents(ReactEventTopLevelCallback);
|
||||
|
||||
var reactRootID = ReactMount.registerContainer(container);
|
||||
instanceByReactRootID[reactRootID] = nextComponent;
|
||||
nextComponent.mountComponentIntoNode(reactRootID, container);
|
||||
return nextComponent;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a function that accepts a `container` and renders the supplied
|
||||
* React component instance into it.
|
||||
*
|
||||
* var renderInto = ReactMount.createComponentRenderer(component);
|
||||
* // ...
|
||||
* var component = renderInto($('container'));
|
||||
*
|
||||
* @param {ReactComponent} component Component instance to render.
|
||||
* @return {function(DOMElement): ReactComponent}
|
||||
*/
|
||||
createComponentRenderer: function(component) {
|
||||
return function(container) {
|
||||
return ReactMount.renderComponent(component, container);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructs a component instance of `constructor` with `initialProps` and
|
||||
* renders it into the supplied `container`.
|
||||
*
|
||||
* @param {function} constructor React component constructor.
|
||||
* @param {?object} props Initial props of the component instance.
|
||||
* @param {DOMElement} container DOM element to render into.
|
||||
* @return {ReactComponent} Component instance rendered in `container`.
|
||||
*/
|
||||
constructAndRenderComponent: function(constructor, props, container) {
|
||||
return ReactMount.renderComponent(constructor(props), container);
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructs a component instance of `constructor` with `initialProps` and
|
||||
* renders it into a container node identified by supplied `id`.
|
||||
*
|
||||
* @param {function} componentConstructor React component constructor
|
||||
* @param {?object} props Initial props of the component instance.
|
||||
* @param {string} id ID of the DOM element to render into.
|
||||
* @return {ReactComponent} Component instance rendered in the container node.
|
||||
*/
|
||||
constructAndRenderComponentByID: function(constructor, props, id) {
|
||||
return ReactMount.constructAndRenderComponent(constructor, props, $(id));
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a container node into which React components will be rendered.
|
||||
* This also creates the "reatRoot" ID that will be assigned to the element
|
||||
* rendered within.
|
||||
*
|
||||
* @param {DOMElement} container DOM element to register as a container.
|
||||
* @return {string} The "reactRoot" ID of elements rendered within.
|
||||
*/
|
||||
registerContainer: function(container) {
|
||||
var reactRootID = getReactRootID(container);
|
||||
if (reactRootID) {
|
||||
// If one exists, make sure it is a valid "reactRoot" ID.
|
||||
reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID);
|
||||
}
|
||||
if (!reactRootID) {
|
||||
// No valid "reactRoot" ID found, create one.
|
||||
reactRootID = ReactInstanceHandles.getReactRootID(
|
||||
globalMountPointCounter++
|
||||
);
|
||||
}
|
||||
containersByReactRootID[reactRootID] = container;
|
||||
return reactRootID;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unmounts and destroys the React component rendered in the `container`.
|
||||
*
|
||||
* @param {DOMElement} container DOM element containing a React component.
|
||||
*/
|
||||
unmountAndReleaseReactRootNode: function(container) {
|
||||
var reactRootID = getReactRootID(container);
|
||||
var component = instanceByReactRootID[reactRootID];
|
||||
// TODO: Consider throwing if no `component` was found.
|
||||
component.unmountComponentFromNode(container);
|
||||
delete instanceByReactRootID[reactRootID];
|
||||
delete containersByReactRootID[reactRootID];
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds the container DOM element that contains React component to which the
|
||||
* supplied DOM `id` belongs.
|
||||
*
|
||||
* @param {string} id The ID of an element rendered by a React component.
|
||||
* @return {?DOMElement} DOM element that contains the `id`.
|
||||
*/
|
||||
findReactContainerForID: function(id) {
|
||||
var reatRootID = ReactInstanceHandles.getReactRootIDFromNodeID(id);
|
||||
// TODO: Consider throwing if `id` is not a valid React element ID.
|
||||
return containersByReactRootID[reatRootID];
|
||||
},
|
||||
|
||||
/**
|
||||
* Given the ID of a DOM node rendered by a React component, finds the root
|
||||
* DOM node of the React component.
|
||||
*
|
||||
* @param {string} id ID of a DOM node in the React component.
|
||||
* @return {?DOMElement} Root DOM node of the React component.
|
||||
*/
|
||||
findReactRenderedDOMNodeSlow: function(id) {
|
||||
var reactRoot = ReactMount.findReactContainerForID(id);
|
||||
return ReactInstanceHandles.findComponentRoot(reactRoot, id);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactMount;
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 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 ReactMultiChild
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactComponent = require("./ReactComponent");
|
||||
|
||||
/**
|
||||
* Given a `curChild` and `newChild`, determines if `curChild` should be managed
|
||||
* as it exists, as opposed to being destroyed and/or replaced.
|
||||
* @param {?ReactComponent} curChild
|
||||
* @param {?ReactComponent} newChild
|
||||
* @return {!boolean} Whether or not `curChild` should be updated with
|
||||
* `newChild`'s props
|
||||
*/
|
||||
function shouldManageExisting(curChild, newChild) {
|
||||
return curChild && newChild && curChild.constructor === newChild.constructor;
|
||||
}
|
||||
|
||||
/**
|
||||
* `ReactMultiChild` provides common functionality for components that have
|
||||
* multiple children. Standard `ReactCompositeComponent`s do not currently have
|
||||
* multiple children. `ReactNativeComponent`s do, however. Other specially
|
||||
* reconciled components will also have multiple children. Contains three
|
||||
* internally used properties that are used to keep track of state throughout
|
||||
* the `updateMultiChild` process.
|
||||
*
|
||||
* @class ReactMultiChild
|
||||
*/
|
||||
|
||||
/**
|
||||
* @lends `ReactMultiChildMixin`.
|
||||
*/
|
||||
var ReactMultiChildMixin = {
|
||||
|
||||
enqueueMarkupAt: function(markup, insertAt) {
|
||||
this.domOperations = this.domOperations || [];
|
||||
this.domOperations.push({insertMarkup: markup, finalIndex: insertAt});
|
||||
},
|
||||
|
||||
enqueueMove: function(originalIndex, finalIndex) {
|
||||
this.domOperations = this.domOperations || [];
|
||||
this.domOperations.push({moveFrom: originalIndex, finalIndex: finalIndex});
|
||||
},
|
||||
|
||||
enqueueUnmountChildByName: function(name, removeChild) {
|
||||
if (ReactComponent.isValidComponent(removeChild)) {
|
||||
this.domOperations = this.domOperations || [];
|
||||
this.domOperations.push({removeAt: removeChild._domIndex});
|
||||
removeChild.unmountComponent && removeChild.unmountComponent();
|
||||
delete this._renderedChildren[name];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Process any pending DOM operations that have been accumulated when updating
|
||||
* the UI. By default, we execute the injected `DOMIDOperations` module's
|
||||
* `manageChildrenByParentID` which does executes the DOM operations without
|
||||
* any animation. It can be used as a reference implementation for special
|
||||
* animation based implementations.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
processChildDOMOperationsQueue: function() {
|
||||
if (this.domOperations) {
|
||||
ReactComponent.DOMIDOperations
|
||||
.manageChildrenByParentID(this._rootNodeID, this.domOperations);
|
||||
this.domOperations = null;
|
||||
}
|
||||
},
|
||||
|
||||
unmountMultiChild: function() {
|
||||
var renderedChildren = this._renderedChildren;
|
||||
for (var name in renderedChildren) {
|
||||
if (renderedChildren.hasOwnProperty(name) && renderedChildren[name]) {
|
||||
var renderedChild = renderedChildren[name];
|
||||
renderedChild.unmountComponent && renderedChild.unmountComponent();
|
||||
}
|
||||
}
|
||||
this._renderedChildren = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates markup for a component that holds multiple children. #todo: Allow
|
||||
* all `ReactMultiChildMixin`s to support having arrays of children without a
|
||||
* container node. This current implementation may assume that children exist
|
||||
* at domIndices [0, parentNode.length].
|
||||
*
|
||||
* Has side effects of (likely) causing events to be registered. Also, every
|
||||
* component instance may only be rendered once.
|
||||
*
|
||||
* @param {?Object} children Flattened children object.
|
||||
* @return {!String} The rendered markup.
|
||||
*/
|
||||
mountMultiChild: function(children, transaction) {
|
||||
var accum = '';
|
||||
var index = 0;
|
||||
for (var name in children) {
|
||||
var child = children[name];
|
||||
if (children.hasOwnProperty(name) && child) {
|
||||
accum += child.mountComponent(
|
||||
this._rootNodeID + '.' + name,
|
||||
transaction
|
||||
);
|
||||
child._domIndex = index;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
this._renderedChildren = children; // children are in just the right form!
|
||||
this.domOperations = null;
|
||||
return accum;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reconciles new children with old children in three phases.
|
||||
*
|
||||
* - Adds new content while updating existing children that should remain.
|
||||
* - Remove children that are no longer present in the next children.
|
||||
* - As a very last step, moves existing dom structures around.
|
||||
* - (Comment 1) `curChildrenDOMIndex` is the largest index of the current
|
||||
* rendered children that appears in the next children and did not need to
|
||||
* be "moved".
|
||||
* - (Comment 2) This is the key insight. If any non-removed child's previous
|
||||
* index is less than `curChildrenDOMIndex` it must be moved.
|
||||
*
|
||||
* @param {?Object} children Flattened children object.
|
||||
*/
|
||||
updateMultiChild: function(nextChildren, transaction) {
|
||||
if (!nextChildren && !this._renderedChildren) {
|
||||
return;
|
||||
} else if (nextChildren && !this._renderedChildren) {
|
||||
this._renderedChildren = {}; // lazily allocate backing store with nothing
|
||||
} else if (!nextChildren && this._renderedChildren) {
|
||||
nextChildren = {};
|
||||
}
|
||||
var rootDomIdDot = this._rootNodeID + '.';
|
||||
var markupBuffer = null; // Accumulate adjacent new children markup.
|
||||
var numPendingInsert = 0; // How many root nodes are waiting in markupBuffer
|
||||
var loopDomIndex = 0; // Index of loop through new children.
|
||||
var curChildrenDOMIndex = 0; // See (Comment 1)
|
||||
for (var name in nextChildren) {
|
||||
if (!nextChildren.hasOwnProperty(name)) {continue;}
|
||||
var curChild = this._renderedChildren[name];
|
||||
var nextChild = nextChildren[name];
|
||||
if (shouldManageExisting(curChild, nextChild)) {
|
||||
if (markupBuffer) {
|
||||
this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
|
||||
markupBuffer = null;
|
||||
}
|
||||
numPendingInsert = 0;
|
||||
if (curChild._domIndex < curChildrenDOMIndex) { // (Comment 2)
|
||||
this.enqueueMove(curChild._domIndex, loopDomIndex);
|
||||
}
|
||||
curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex);
|
||||
!nextChild.props.isStatic &&
|
||||
curChild.receiveProps(nextChild.props, transaction);
|
||||
curChild._domIndex = loopDomIndex;
|
||||
} else {
|
||||
if (curChild) { // !shouldUpdate && curChild => delete
|
||||
this.enqueueUnmountChildByName(name, curChild);
|
||||
curChildrenDOMIndex =
|
||||
Math.max(curChild._domIndex, curChildrenDOMIndex);
|
||||
}
|
||||
if (nextChild) { // !shouldUpdate && nextChild => insert
|
||||
this._renderedChildren[name] = nextChild;
|
||||
var nextMarkup =
|
||||
nextChild.mountComponent(rootDomIdDot + name, transaction);
|
||||
markupBuffer = markupBuffer ? markupBuffer + nextMarkup : nextMarkup;
|
||||
numPendingInsert++;
|
||||
nextChild._domIndex = loopDomIndex;
|
||||
}
|
||||
}
|
||||
loopDomIndex = nextChild ? loopDomIndex + 1 : loopDomIndex;
|
||||
}
|
||||
if (markupBuffer) {
|
||||
this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
|
||||
}
|
||||
for (var childName in this._renderedChildren) { // from other direction
|
||||
if (!this._renderedChildren.hasOwnProperty(childName)) { continue; }
|
||||
var child = this._renderedChildren[childName];
|
||||
if (child && !nextChildren[childName]) {
|
||||
this.enqueueUnmountChildByName(childName, child);
|
||||
}
|
||||
}
|
||||
this.processChildDOMOperationsQueue();
|
||||
}
|
||||
};
|
||||
|
||||
var ReactMultiChild = {
|
||||
Mixin: ReactMultiChildMixin
|
||||
};
|
||||
|
||||
module.exports = ReactMultiChild;
|
||||
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* 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 ReactNativeComponent
|
||||
* @typechecks
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var CSSPropertyOperations = require("./CSSPropertyOperations");
|
||||
var DOMPropertyOperations = require("./DOMPropertyOperations");
|
||||
var ReactComponent = require("./ReactComponent");
|
||||
var ReactEvent = require("./ReactEvent");
|
||||
var ReactMultiChild = require("./ReactMultiChild");
|
||||
|
||||
var escapeTextForBrowser = require("./escapeTextForBrowser");
|
||||
var flattenChildren = require("./flattenChildren");
|
||||
var invariant = require("./invariant");
|
||||
var keyOf = require("./keyOf");
|
||||
var merge = require("./merge");
|
||||
var mixInto = require("./mixInto");
|
||||
|
||||
var putListener = ReactEvent.putListener;
|
||||
var registrationNames = ReactEvent.registrationNames;
|
||||
|
||||
// For quickly matching children type, to test if can be treated as content.
|
||||
var CONTENT_TYPES = {'string': true, 'number': true};
|
||||
|
||||
var CONTENT = keyOf({content: null});
|
||||
var DANGEROUSLY_SET_INNER_HTML = keyOf({dangerouslySetInnerHTML: null});
|
||||
var STYLE = keyOf({style: null});
|
||||
|
||||
/**
|
||||
* @param {?object} props
|
||||
*/
|
||||
function assertValidProps(props) {
|
||||
if (!props) {
|
||||
return;
|
||||
}
|
||||
// Note the use of `!=` which checks for null or undefined.
|
||||
var hasChildren = props.children != null ? 1 : 0;
|
||||
var hasContent = props.content != null ? 1 : 0;
|
||||
var hasInnerHTML = props.dangerouslySetInnerHTML != null ? 1 : 0;
|
||||
invariant(
|
||||
hasChildren + hasContent + hasInnerHTML <= 1,
|
||||
'Can only set one of `children`, `props.content`, or ' +
|
||||
'`props.dangerouslySetInnerHTML`.'
|
||||
);
|
||||
invariant(
|
||||
props.style == null || typeof props.style === 'object',
|
||||
'The `style` prop expects a mapping from style properties to values, ' +
|
||||
'not a string.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor ReactNativeComponent
|
||||
* @extends ReactComponent
|
||||
* @extends ReactMultiChild
|
||||
*/
|
||||
function ReactNativeComponent(tag, omitClose) {
|
||||
this._tagOpen = '<' + tag + ' ';
|
||||
this._tagClose = omitClose ? '' : '</' + tag + '>';
|
||||
this.tagName = tag.toUpperCase();
|
||||
}
|
||||
|
||||
ReactNativeComponent.Mixin = {
|
||||
|
||||
/**
|
||||
* Generates root tag markup then recurses. This method has side effects and
|
||||
* is not idempotent.
|
||||
*
|
||||
* @internal
|
||||
* @param {string} rootID The root DOM ID for this node.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @return {string} The computed markup.
|
||||
*/
|
||||
mountComponent: function(rootID, transaction) {
|
||||
ReactComponent.Mixin.mountComponent.call(this, rootID, transaction);
|
||||
assertValidProps(this.props);
|
||||
return (
|
||||
this._createOpenTagMarkup() +
|
||||
this._createContentMarkup(transaction) +
|
||||
this._tagClose
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates markup for the open tag and all attributes.
|
||||
*
|
||||
* This method has side effects because events get registered.
|
||||
*
|
||||
* Iterating over object properties is faster than iterating over arrays.
|
||||
* @see http://jsperf.com/obj-vs-arr-iteration
|
||||
*
|
||||
* @private
|
||||
* @return {string} Markup of opening tag.
|
||||
*/
|
||||
_createOpenTagMarkup: function() {
|
||||
var props = this.props;
|
||||
var ret = this._tagOpen;
|
||||
|
||||
for (var propKey in props) {
|
||||
if (!props.hasOwnProperty(propKey)) {
|
||||
continue;
|
||||
}
|
||||
var propValue = props[propKey];
|
||||
if (propValue == null) {
|
||||
continue;
|
||||
}
|
||||
if (registrationNames[propKey]) {
|
||||
putListener(this._rootNodeID, propKey, propValue);
|
||||
} else {
|
||||
if (propKey === STYLE) {
|
||||
if (propValue) {
|
||||
propValue = props.style = merge(props.style);
|
||||
}
|
||||
propValue = CSSPropertyOperations.createMarkupForStyles(propValue);
|
||||
}
|
||||
var markup =
|
||||
DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
|
||||
if (markup) {
|
||||
ret += ' ' + markup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret + ' id="' + this._rootNodeID + '">';
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates markup for the content between the tags.
|
||||
*
|
||||
* @private
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @return {string} Content markup.
|
||||
*/
|
||||
_createContentMarkup: function(transaction) {
|
||||
// Intentional use of != to avoid catching zero/false.
|
||||
var innerHTML = this.props.dangerouslySetInnerHTML;
|
||||
if (innerHTML != null) {
|
||||
if (innerHTML.__html != null) {
|
||||
return innerHTML.__html;
|
||||
}
|
||||
} else {
|
||||
var contentToUse = this.props.content != null ? this.props.content :
|
||||
CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
|
||||
var childrenToUse = contentToUse != null ? null : this.props.children;
|
||||
if (contentToUse != null) {
|
||||
return escapeTextForBrowser(contentToUse);
|
||||
} else if (childrenToUse != null) {
|
||||
return this.mountMultiChild(
|
||||
flattenChildren(childrenToUse),
|
||||
transaction
|
||||
);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Controls a native DOM component after it has already been allocated and
|
||||
* attached to the DOM. Reconciles the root DOM node, then recurses.
|
||||
*
|
||||
* @internal
|
||||
* @param {object} nextProps
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
*/
|
||||
receiveProps: function(nextProps, transaction) {
|
||||
invariant(
|
||||
this._rootNodeID,
|
||||
'Trying to control a native dom element without a backing id'
|
||||
);
|
||||
assertValidProps(nextProps);
|
||||
ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
|
||||
this._updateDOMProperties(nextProps);
|
||||
this._updateDOMChildren(nextProps, transaction);
|
||||
this.props = nextProps;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reconciles the properties by detecting differences in property values and
|
||||
* updating the DOM as necessary. This function is probably the single most
|
||||
* critical path for performance optimization.
|
||||
*
|
||||
* TODO: Benchmark whether checking for changed values in memory actually
|
||||
* improves performance (especially statically positioned elements).
|
||||
* TODO: Benchmark the effects of putting this at the top since 99% of props
|
||||
* do not change for a given reconciliation.
|
||||
* TODO: Benchmark areas that can be improved with caching.
|
||||
*
|
||||
* @private
|
||||
* @param {object} nextProps
|
||||
*/
|
||||
_updateDOMProperties: function(nextProps) {
|
||||
var lastProps = this.props;
|
||||
for (var propKey in nextProps) {
|
||||
var nextProp = nextProps[propKey];
|
||||
var lastProp = lastProps[propKey];
|
||||
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
|
||||
continue;
|
||||
}
|
||||
if (propKey === STYLE) {
|
||||
if (nextProp) {
|
||||
nextProp = nextProps.style = merge(nextProp);
|
||||
}
|
||||
var styleUpdates;
|
||||
for (var styleName in nextProp) {
|
||||
if (!nextProp.hasOwnProperty(styleName)) {
|
||||
continue;
|
||||
}
|
||||
if (!lastProp || lastProp[styleName] !== nextProp[styleName]) {
|
||||
if (!styleUpdates) {
|
||||
styleUpdates = {};
|
||||
}
|
||||
styleUpdates[styleName] = nextProp[styleName];
|
||||
}
|
||||
}
|
||||
if (styleUpdates) {
|
||||
ReactComponent.DOMIDOperations.updateStylesByID(
|
||||
this._rootNodeID,
|
||||
styleUpdates
|
||||
);
|
||||
}
|
||||
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
|
||||
var lastHtml = lastProp && lastProp.__html;
|
||||
var nextHtml = nextProp && nextProp.__html;
|
||||
if (lastHtml !== nextHtml) {
|
||||
ReactComponent.DOMIDOperations.updateInnerHTMLByID(
|
||||
this._rootNodeID,
|
||||
nextProp
|
||||
);
|
||||
}
|
||||
} else if (propKey === CONTENT) {
|
||||
ReactComponent.DOMIDOperations.updateTextContentByID(
|
||||
this._rootNodeID,
|
||||
'' + nextProp
|
||||
);
|
||||
} else if (registrationNames[propKey]) {
|
||||
putListener(this._rootNodeID, propKey, nextProp);
|
||||
} else {
|
||||
ReactComponent.DOMIDOperations.updatePropertyByID(
|
||||
this._rootNodeID,
|
||||
propKey,
|
||||
nextProp
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reconciles the children with the various properties that affect the
|
||||
* children content.
|
||||
*
|
||||
* @param {object} nextProps
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
*/
|
||||
_updateDOMChildren: function(nextProps, transaction) {
|
||||
var thisPropsContentType = typeof this.props.content;
|
||||
var thisPropsContentEmpty =
|
||||
this.props.content == null || thisPropsContentType === 'boolean';
|
||||
var nextPropsContentType = typeof nextProps.content;
|
||||
var nextPropsContentEmpty =
|
||||
nextProps.content == null || nextPropsContentType === 'boolean';
|
||||
|
||||
var lastUsedContent = !thisPropsContentEmpty ? this.props.content :
|
||||
CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
|
||||
|
||||
var contentToUse = !nextPropsContentEmpty ? nextProps.content :
|
||||
CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
|
||||
|
||||
// Note the use of `!=` which checks for null or undefined.
|
||||
|
||||
var lastUsedChildren =
|
||||
lastUsedContent != null ? null : this.props.children;
|
||||
var childrenToUse = contentToUse != null ? null : nextProps.children;
|
||||
|
||||
if (contentToUse != null) {
|
||||
var childrenRemoved = lastUsedChildren != null && childrenToUse == null;
|
||||
if (childrenRemoved) {
|
||||
this.updateMultiChild(null, transaction);
|
||||
}
|
||||
if (lastUsedContent !== contentToUse) {
|
||||
ReactComponent.DOMIDOperations.updateTextContentByID(
|
||||
this._rootNodeID,
|
||||
'' + contentToUse
|
||||
);
|
||||
}
|
||||
} else {
|
||||
var contentRemoved = lastUsedContent != null && contentToUse == null;
|
||||
if (contentRemoved) {
|
||||
ReactComponent.DOMIDOperations.updateTextContentByID(
|
||||
this._rootNodeID,
|
||||
''
|
||||
);
|
||||
}
|
||||
this.updateMultiChild(flattenChildren(nextProps.children), transaction);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroys all event registrations for this instance. Does not remove from
|
||||
* the DOM. That must be done by the parent.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
unmountComponent: function() {
|
||||
ReactComponent.Mixin.unmountComponent.call(this);
|
||||
this.unmountMultiChild();
|
||||
ReactEvent.deleteAllListeners(this._rootNodeID);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
mixInto(ReactNativeComponent, ReactComponent.Mixin);
|
||||
mixInto(ReactNativeComponent, ReactNativeComponent.Mixin);
|
||||
mixInto(ReactNativeComponent, ReactMultiChild.Mixin);
|
||||
|
||||
module.exports = ReactNativeComponent;
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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 ReactOnDOMReady
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var PooledClass = require("./PooledClass");
|
||||
|
||||
var mixInto = require("./mixInto");
|
||||
|
||||
/**
|
||||
* A specialized pseudo-event module to help keep track of components waiting to
|
||||
* be notified when their DOM representations are available for use.
|
||||
*
|
||||
* This implements `PooledClass`, so you should never need to instantiate this.
|
||||
* Instead, use `ReactOnDOMReady.getPooled()`.
|
||||
*
|
||||
* @param {?array<function>} initialCollection
|
||||
* @class ReactOnDOMReady
|
||||
* @implements PooledClass
|
||||
* @internal
|
||||
*/
|
||||
function ReactOnDOMReady(initialCollection) {
|
||||
this._queue = initialCollection || null;
|
||||
}
|
||||
|
||||
mixInto(ReactOnDOMReady, {
|
||||
|
||||
/**
|
||||
* Enqueues a callback to be invoked when `notifyAll` is invoked. This is used
|
||||
* to enqueue calls to `componentDidMount` and `componentDidUpdate`.
|
||||
*
|
||||
* @param {ReactComponent} component Component being rendered.
|
||||
* @param {function(DOMElement)} callback Invoked when `notifyAll` is invoked.
|
||||
* @internal
|
||||
*/
|
||||
enqueue: function(component, callback) {
|
||||
this._queue = this._queue || [];
|
||||
this._queue.push({component: component, callback: callback});
|
||||
},
|
||||
|
||||
/**
|
||||
* Invokes all enqueued callbacks and clears the queue. This is invoked after
|
||||
* the DOM representation of a component has been created or updated.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
notifyAll: function() {
|
||||
var queue = this._queue;
|
||||
if (queue) {
|
||||
this._queue = null;
|
||||
for (var i = 0, l = queue.length; i < l; i++) {
|
||||
var component = queue[i].component;
|
||||
var callback = queue[i].callback;
|
||||
callback.call(component, component.getDOMNode());
|
||||
}
|
||||
queue.length = 0;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets the internal queue.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
reset: function() {
|
||||
this._queue = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* `PooledClass` looks for this.
|
||||
*/
|
||||
destructor: function() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
PooledClass.addPoolingTo(ReactOnDOMReady);
|
||||
|
||||
module.exports = ReactOnDOMReady;
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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 ReactOwner
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var invariant = require("./invariant");
|
||||
|
||||
/**
|
||||
* ReactOwners are capable of storing references to owned components.
|
||||
*
|
||||
* All components are capable of //being// referenced by owner components, but
|
||||
* only ReactOwner components are capable of //referencing// owned components.
|
||||
* The named reference is known as a "ref".
|
||||
*
|
||||
* Refs are available when mounted and updated during reconciliation.
|
||||
*
|
||||
* var MyComponent = React.createClass({
|
||||
* render: function() {
|
||||
* return (
|
||||
* <div onClick={this.handleClick}>
|
||||
* <CustomComponent ref="custom" />
|
||||
* </div>
|
||||
* );
|
||||
* },
|
||||
* handleClick: React.autoBind(function() {
|
||||
* this.refs.custom.handleClick();
|
||||
* }),
|
||||
* componentDidMount: function() {
|
||||
* this.refs.custom.initialize();
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* Refs should rarely be used. When refs are used, they should only be done to
|
||||
* control data that is not handled by React's data flow.
|
||||
*
|
||||
* @class ReactOwner
|
||||
*/
|
||||
var ReactOwner = {
|
||||
|
||||
/**
|
||||
* @param {?object} object
|
||||
* @return {boolean} True if `object` is a valid owner.
|
||||
* @final
|
||||
*/
|
||||
isValidOwner: function(object) {
|
||||
return !!(
|
||||
object &&
|
||||
typeof object.attachRef === 'function' &&
|
||||
typeof object.detachRef === 'function'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a component by ref to an owner component.
|
||||
*
|
||||
* @param {ReactComponent} component Component to reference.
|
||||
* @param {string} ref Name by which to refer to the component.
|
||||
* @param {ReactOwner} owner Component on which to record the ref.
|
||||
* @final
|
||||
* @internal
|
||||
*/
|
||||
addComponentAsRefTo: function(component, ref, owner) {
|
||||
invariant(
|
||||
ReactOwner.isValidOwner(owner),
|
||||
'addComponentAsRefTo(...): Only a ReactOwner can have refs.'
|
||||
);
|
||||
owner.attachRef(ref, component);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a component by ref from an owner component.
|
||||
*
|
||||
* @param {ReactComponent} component Component to dereference.
|
||||
* @param {string} ref Name of the ref to remove.
|
||||
* @param {ReactOwner} owner Component on which the ref is recorded.
|
||||
* @final
|
||||
* @internal
|
||||
*/
|
||||
removeComponentAsRefFrom: function(component, ref, owner) {
|
||||
invariant(
|
||||
ReactOwner.isValidOwner(owner),
|
||||
'removeComponentAsRefFrom(...): Only a ReactOwner can have refs.'
|
||||
);
|
||||
// Check that `component` is still the current ref because we do not want to
|
||||
// detach the ref if another component stole it.
|
||||
if (owner.refs[ref] === component) {
|
||||
owner.detachRef(ref);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A ReactComponent must mix this in to have refs.
|
||||
*
|
||||
* @lends {ReactOwner.prototype}
|
||||
*/
|
||||
Mixin: {
|
||||
|
||||
/**
|
||||
* Lazily allocates the refs object and stores `component` as `ref`.
|
||||
*
|
||||
* @param {string} ref Reference name.
|
||||
* @param {component} component Component to store as `ref`.
|
||||
* @final
|
||||
* @private
|
||||
*/
|
||||
attachRef: function(ref, component) {
|
||||
invariant(
|
||||
component.isOwnedBy(this),
|
||||
'attachRef(%s, ...): Only a component\'s owner can store a ref to it.',
|
||||
ref
|
||||
);
|
||||
var refs = this.refs || (this.refs = {});
|
||||
refs[ref] = component;
|
||||
},
|
||||
|
||||
/**
|
||||
* Detaches a reference name.
|
||||
*
|
||||
* @param {string} ref Name to dereference.
|
||||
* @final
|
||||
* @private
|
||||
*/
|
||||
detachRef: function(ref) {
|
||||
delete this.refs[ref];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactOwner;
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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 ReactPropTransferer
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var emptyFunction = require("./emptyFunction");
|
||||
var joinClasses = require("./joinClasses");
|
||||
var merge = require("./merge");
|
||||
|
||||
/**
|
||||
* Creates a transfer strategy that will merge prop values using the supplied
|
||||
* `mergeStrategy`. If a prop was previously unset, this just sets it.
|
||||
*
|
||||
* @param {function} mergeStrategy
|
||||
* @return {function}
|
||||
*/
|
||||
function createTransferStrategy(mergeStrategy) {
|
||||
return function(props, key, value) {
|
||||
if (!props.hasOwnProperty(key)) {
|
||||
props[key] = value;
|
||||
} else {
|
||||
props[key] = mergeStrategy(props[key], value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer strategies dictate how props are transferred by `transferPropsTo`.
|
||||
*/
|
||||
var TransferStrategies = {
|
||||
/**
|
||||
* Never transfer the `ref` prop.
|
||||
*/
|
||||
ref: emptyFunction,
|
||||
/**
|
||||
* Transfer the `className` prop by merging them.
|
||||
*/
|
||||
className: createTransferStrategy(joinClasses),
|
||||
/**
|
||||
* Transfer the `style` prop (which is an object) by merging them.
|
||||
*/
|
||||
style: createTransferStrategy(merge)
|
||||
};
|
||||
|
||||
/**
|
||||
* ReactPropTransferer are capable of transferring props to another component
|
||||
* using a `transferPropsTo` method.
|
||||
*
|
||||
* @class ReactPropTransferer
|
||||
*/
|
||||
var ReactPropTransferer = {
|
||||
|
||||
TransferStrategies: TransferStrategies,
|
||||
|
||||
/**
|
||||
* @lends {ReactPropTransferer.prototype}
|
||||
*/
|
||||
Mixin: {
|
||||
|
||||
/**
|
||||
* Transfer props from this component to a target component.
|
||||
*
|
||||
* Props that do not have an explicit transfer strategy will be transferred
|
||||
* only if the target component does not already have the prop set.
|
||||
*
|
||||
* This is usually used to pass down props to a returned root component.
|
||||
*
|
||||
* @param {ReactComponent} component Component receiving the properties.
|
||||
* @return {ReactComponent} The supplied `component`.
|
||||
* @final
|
||||
* @protected
|
||||
*/
|
||||
transferPropsTo: function(component) {
|
||||
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;
|
||||
return component;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactPropTransferer;
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 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 ReactProps
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var createObjectFrom = require("./createObjectFrom");
|
||||
var invariant = require("./invariant");
|
||||
|
||||
/**
|
||||
* Collection of methods that allow declaration and validation of props that are
|
||||
* supplied to React components. Example usage:
|
||||
*
|
||||
* var Props = require('ReactProps');
|
||||
* var MyArticle = React.createClass({
|
||||
* props: {
|
||||
* // An optional string prop named "description".
|
||||
* description: Props.string,
|
||||
*
|
||||
* // A required enum prop named "category".
|
||||
* category: Props.oneOf(['News','Photos']).isRequired,
|
||||
*
|
||||
* // A prop named "dialog" that requires an instance of Dialog.
|
||||
* dialog: Props.instanceOf(Dialog).isRequired
|
||||
* },
|
||||
* render: function() { ... }
|
||||
* });
|
||||
*
|
||||
* A more formal specification of how these methods are used:
|
||||
*
|
||||
* type := array|bool|object|number|string|oneOf([...])|instanceOf(...)
|
||||
* decl := ReactProps.{type}(.isRequired)?
|
||||
*
|
||||
* Each and every declaration produces a function with the same signature. This
|
||||
* allows the creation of custom validation functions. For example:
|
||||
*
|
||||
* var Props = require('ReactProps');
|
||||
* var MyLink = React.createClass({
|
||||
* props: {
|
||||
* // An optional string or URI prop named "href".
|
||||
* href: function(props, propName, componentName) {
|
||||
* var propValue = props[propName];
|
||||
* invariant(
|
||||
* propValue == null ||
|
||||
* typeof propValue === string ||
|
||||
* propValue instanceof URI,
|
||||
* 'Invalid `%s` supplied to `%s`, expected string or URI.',
|
||||
* propName,
|
||||
* componentName
|
||||
* );
|
||||
* }
|
||||
* },
|
||||
* render: function() { ... }
|
||||
* });
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
var Props = {
|
||||
|
||||
array: createPrimitiveTypeChecker('array'),
|
||||
bool: createPrimitiveTypeChecker('boolean'),
|
||||
number: createPrimitiveTypeChecker('number'),
|
||||
object: createPrimitiveTypeChecker('object'),
|
||||
string: createPrimitiveTypeChecker('string'),
|
||||
|
||||
oneOf: createEnumTypeChecker,
|
||||
|
||||
instanceOf: createInstanceTypeChecker
|
||||
|
||||
};
|
||||
|
||||
var ANONYMOUS = '<<anonymous>>';
|
||||
|
||||
function createPrimitiveTypeChecker(expectedType) {
|
||||
function validatePrimitiveType(propValue, propName, componentName) {
|
||||
var propType = typeof propValue;
|
||||
if (propType === 'object' && Array.isArray(propValue)) {
|
||||
propType = 'array';
|
||||
}
|
||||
invariant(
|
||||
propType === expectedType,
|
||||
'Invalid prop `%s` of type `%s` supplied to `%s`, expected `%s`.',
|
||||
propName,
|
||||
propType,
|
||||
componentName,
|
||||
expectedType
|
||||
);
|
||||
}
|
||||
return createChainableTypeChecker(validatePrimitiveType);
|
||||
}
|
||||
|
||||
function createEnumTypeChecker(expectedValues) {
|
||||
var expectedEnum = createObjectFrom(expectedValues);
|
||||
function validateEnumType(propValue, propName, componentName) {
|
||||
invariant(
|
||||
expectedEnum[propValue],
|
||||
'Invalid prop `%s` supplied to `%s`, expected one of %s.',
|
||||
propName,
|
||||
componentName,
|
||||
JSON.stringify(Object.keys(expectedEnum))
|
||||
);
|
||||
}
|
||||
return createChainableTypeChecker(validateEnumType);
|
||||
}
|
||||
|
||||
function createInstanceTypeChecker(expectedClass) {
|
||||
function validateInstanceType(propValue, propName, componentName) {
|
||||
invariant(
|
||||
propValue instanceof expectedClass,
|
||||
'Invalid prop `%s` supplied to `%s`, expected instance of `%s`.',
|
||||
propName,
|
||||
componentName,
|
||||
expectedClass.name || ANONYMOUS
|
||||
);
|
||||
}
|
||||
return createChainableTypeChecker(validateInstanceType);
|
||||
}
|
||||
|
||||
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 {
|
||||
invariant(
|
||||
!isRequired,
|
||||
'Required prop `%s` was not specified in `%s`.',
|
||||
propName,
|
||||
componentName || ANONYMOUS
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!isRequired) {
|
||||
checkType.isRequired = createTypeChecker(true);
|
||||
}
|
||||
return checkType;
|
||||
}
|
||||
return createTypeChecker(false);
|
||||
}
|
||||
|
||||
module.exports = Props;
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 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 ReactReconcileTransaction
|
||||
* @typechecks
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ExecutionEnvironment = require("./ExecutionEnvironment");
|
||||
var PooledClass = require("./PooledClass");
|
||||
var ReactEvent = require("./ReactEvent");
|
||||
var ReactInputSelection = require("./ReactInputSelection");
|
||||
var ReactOnDOMReady = require("./ReactOnDOMReady");
|
||||
var Transaction = require("./Transaction");
|
||||
|
||||
var mixInto = require("./mixInto");
|
||||
|
||||
/**
|
||||
* Ensures that, when possible, the selection range (currently selected text
|
||||
* input) is not disturbed by performing the transaction.
|
||||
*/
|
||||
var SELECTION_RESTORATION = {
|
||||
/**
|
||||
* @return {Selection} Selection information.
|
||||
*/
|
||||
initialize: ReactInputSelection.getSelectionInformation,
|
||||
/**
|
||||
* @param {Selection} sel Selection information returned from `initialize`.
|
||||
*/
|
||||
close: ReactInputSelection.restoreSelection
|
||||
};
|
||||
|
||||
/**
|
||||
* Suppresses events (blur/focus) that could be inadvertently dispatched due to
|
||||
* high level DOM manipulations (like temporarily removing a text input from the
|
||||
* DOM).
|
||||
*/
|
||||
var EVENT_SUPPRESSION = {
|
||||
/**
|
||||
* @return {boolean} The enabled status of `ReactEvent` before the
|
||||
* reconciliation.
|
||||
*/
|
||||
initialize: function() {
|
||||
var currentlyEnabled = ReactEvent.isEnabled();
|
||||
ReactEvent.setEnabled(false);
|
||||
return currentlyEnabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {boolean} previouslyEnabled The enabled status of `ReactEvent`
|
||||
* before the reconciliation occured. `close` restores the previous value.
|
||||
*/
|
||||
close: function(previouslyEnabled) {
|
||||
ReactEvent.setEnabled(previouslyEnabled);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides a `ReactOnDOMReady` queue for collecting `onDOMReady` callbacks
|
||||
* during the performing of the transaction.
|
||||
*/
|
||||
var ON_DOM_READY_QUEUEING = {
|
||||
/**
|
||||
* Initializes the internal `onDOMReady` queue.
|
||||
*/
|
||||
initialize: function() {
|
||||
this.reactOnDOMReady.reset();
|
||||
},
|
||||
|
||||
/**
|
||||
* After DOM is flushed, invoke all registered `onDOMReady` callbacks.
|
||||
*/
|
||||
close: function() {
|
||||
this.reactOnDOMReady.notifyAll();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Executed within the scope of the `Transaction` instance. Consider these as
|
||||
* being member methods, but with an implied ordering while being isolated from
|
||||
* each other.
|
||||
*/
|
||||
var TRANSACTION_WRAPPERS = [
|
||||
SELECTION_RESTORATION,
|
||||
EVENT_SUPPRESSION,
|
||||
ON_DOM_READY_QUEUEING
|
||||
];
|
||||
|
||||
/**
|
||||
* Currently:
|
||||
* - The order that these are listed in the transaction is critical:
|
||||
* - Suppresses events.
|
||||
* - Restores selection range.
|
||||
*
|
||||
* Future:
|
||||
* - Restore document/overflow scroll positions that were unintentionally
|
||||
* modified via DOM insertions above the top viewport boundary.
|
||||
* - Implement/integrate with customized constraint based layout system and keep
|
||||
* track of which dimensions must be remeasured.
|
||||
*
|
||||
* @class ReactReconcileTransaction
|
||||
*/
|
||||
function ReactReconcileTransaction() {
|
||||
this.reinitializeTransaction();
|
||||
this.reactOnDOMReady = ReactOnDOMReady.getPooled(null);
|
||||
}
|
||||
|
||||
var Mixin = {
|
||||
/**
|
||||
* @see Transaction
|
||||
* @abstract
|
||||
* @final
|
||||
* @return {array<object>} List of operation wrap proceedures.
|
||||
* TODO: convert to array<TransactionWrapper>
|
||||
*/
|
||||
getTransactionWrappers: function() {
|
||||
if (ExecutionEnvironment.canUseDOM) {
|
||||
return TRANSACTION_WRAPPERS;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {object} The queue to collect `onDOMReady` callbacks with.
|
||||
* TODO: convert to ReactOnDOMReady
|
||||
*/
|
||||
getReactOnDOMReady: function() {
|
||||
return this.reactOnDOMReady;
|
||||
},
|
||||
|
||||
/**
|
||||
* `PooledClass` looks for this, and will invoke this before allowing this
|
||||
* instance to be resused.
|
||||
*/
|
||||
destructor: function() {
|
||||
ReactOnDOMReady.release(this.reactOnDOMReady);
|
||||
this.reactOnDOMReady = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
mixInto(ReactReconcileTransaction, Transaction.Mixin);
|
||||
mixInto(ReactReconcileTransaction, Mixin);
|
||||
|
||||
PooledClass.addPoolingTo(ReactReconcileTransaction);
|
||||
|
||||
module.exports = ReactReconcileTransaction;
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 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 ReactStateSetters
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactStateSetters = {
|
||||
/**
|
||||
* Returns a function that calls the provided function, and uses the result
|
||||
* of that to set the component's state.
|
||||
*
|
||||
* @param {ReactCompositeComponent} component
|
||||
* @param {function} funcReturningState Returned callback uses this to
|
||||
* determine how to update state.
|
||||
* @return {function} callback that when invoked uses funcReturningState to
|
||||
* determined the object literal to setState.
|
||||
*/
|
||||
createStateSetter: function(component, funcReturningState) {
|
||||
return function(a, b, c, d, e, f) {
|
||||
var partialState = funcReturningState.call(component, a, b, c, d, e, f);
|
||||
if (partialState) {
|
||||
component.setState(partialState);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a single-argument callback that can be used to update a single
|
||||
* key in the component's state.
|
||||
*
|
||||
* Note: this is memoized function, which makes it inexpensive to call.
|
||||
*
|
||||
* @param {ReactCompositeComponent} component
|
||||
* @param {string} key The key in the state that you should update.
|
||||
* @return {function} callback of 1 argument which calls setState() with
|
||||
* the provided keyName and callback argument.
|
||||
*/
|
||||
createStateKeySetter: function(component, key) {
|
||||
// Memoize the setters.
|
||||
var cache = component.__keySetters || (component.__keySetters = {});
|
||||
return cache[key] || (cache[key] = createStateKeySetter(component, key));
|
||||
}
|
||||
};
|
||||
|
||||
function createStateKeySetter(component, key) {
|
||||
// Partial state is allocated outside of the function closure so it can be
|
||||
// reused with every call, avoiding memory allocation when this function
|
||||
// is called.
|
||||
var partialState = {};
|
||||
return function stateKeySetter(value) {
|
||||
partialState[key] = value;
|
||||
component.setState(partialState);
|
||||
};
|
||||
}
|
||||
|
||||
ReactStateSetters.Mixin = {
|
||||
/**
|
||||
* Returns a function that calls the provided function, and uses the result
|
||||
* of that to set the component's state.
|
||||
*
|
||||
* For example, these statements are equivalent:
|
||||
*
|
||||
* this.setState({x: 1});
|
||||
* this.createStateSetter(function(xValue) {
|
||||
* return {x: xValue};
|
||||
* })(1);
|
||||
*
|
||||
* @param {function} funcReturningState Returned callback uses this to
|
||||
* determine how to update state.
|
||||
* @return {function} callback that when invoked uses funcReturningState to
|
||||
* determined the object literal to setState.
|
||||
*/
|
||||
createStateSetter: function(funcReturningState) {
|
||||
return ReactStateSetters.createStateSetter(this, funcReturningState);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a single-argument callback that can be used to update a single
|
||||
* key in the component's state.
|
||||
*
|
||||
* For example, these statements are equivalent:
|
||||
*
|
||||
* this.setState({x: 1});
|
||||
* this.createStateKeySetter('x')(1);
|
||||
*
|
||||
* Note: this is memoized function, which makes it inexpensive to call.
|
||||
*
|
||||
* @param {string} key The key in the state that you should update.
|
||||
* @return {function} callback of 1 argument which calls setState() with
|
||||
* the provided keyName and callback argument.
|
||||
*/
|
||||
createStateKeySetter: function(key) {
|
||||
return ReactStateSetters.createStateKeySetter(this, key);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ReactStateSetters;
|
||||
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* 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 ReactTestUtils
|
||||
*/
|
||||
|
||||
var EventConstants = require("./EventConstants");
|
||||
var React = require("./React");
|
||||
var ReactComponent = require("./ReactComponent");
|
||||
var ReactEvent = require("./ReactEvent");
|
||||
var ReactTextComponent = require("./ReactTextComponent");
|
||||
|
||||
var ge = require("./ge");
|
||||
var mergeInto = require("./mergeInto");
|
||||
|
||||
var topLevelTypes = EventConstants.topLevelTypes;
|
||||
|
||||
function Event(suffix) {}
|
||||
|
||||
/**
|
||||
* @class ReactTestUtils
|
||||
*/
|
||||
|
||||
/**
|
||||
* Todo: Support the entire DOM.scry query syntax. For now, these simple
|
||||
* utilities will suffice for testing purposes.
|
||||
* @lends ReactTestUtils
|
||||
*/
|
||||
var ReactTestUtils = {
|
||||
renderIntoDocument: function(instance) {
|
||||
var div = document.createElement('div');
|
||||
document.documentElement.appendChild(div);
|
||||
return React.renderComponent(instance, div);
|
||||
},
|
||||
|
||||
isDOMComponent: function(inst) {
|
||||
return !!(inst &&
|
||||
ReactComponent.isValidComponent(inst) &&
|
||||
!!inst.tagName);
|
||||
},
|
||||
|
||||
isCompositeComponent: function(inst) {
|
||||
return !!(
|
||||
inst &&
|
||||
ReactComponent.isValidComponent(inst) &&
|
||||
typeof inst.render === 'function' &&
|
||||
typeof inst.setState === 'function' &&
|
||||
typeof inst.updateComponent === 'function'
|
||||
);
|
||||
},
|
||||
|
||||
isCompositeComponentWithType: function(inst, type) {
|
||||
return !!(ReactTestUtils.isCompositeComponent(inst) &&
|
||||
(inst.constructor === type.componentConstructor ||
|
||||
inst.constructor === type));
|
||||
},
|
||||
|
||||
isTextComponent: function(inst) {
|
||||
return inst instanceof ReactTextComponent;
|
||||
},
|
||||
|
||||
findAllInRenderedTree: function(inst, test) {
|
||||
if (!inst) {
|
||||
return [];
|
||||
}
|
||||
var ret = test(inst) ? [inst] : [];
|
||||
if (ReactTestUtils.isDOMComponent(inst)) {
|
||||
var renderedChildren = inst._renderedChildren;
|
||||
var key;
|
||||
for (key in renderedChildren) {
|
||||
if (!renderedChildren.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
ret = ret.concat(
|
||||
ReactTestUtils.findAllInRenderedTree(renderedChildren[key], test)
|
||||
);
|
||||
}
|
||||
} else if (ReactTestUtils.isCompositeComponent(inst)) {
|
||||
ret = ret.concat(
|
||||
ReactTestUtils.findAllInRenderedTree(inst._renderedComponent, test)
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds all instance of components in the rendered tree that are DOM
|
||||
* components with the class name matching `className`.
|
||||
* @return an array of all the matches.
|
||||
*/
|
||||
scryRenderedDOMComponentsWithClass: function(root, className) {
|
||||
return ReactTestUtils.findAllInRenderedTree(root, function(inst) {
|
||||
var instClassName = inst.props.className;
|
||||
return ReactTestUtils.isDOMComponent(inst) && (
|
||||
instClassName &&
|
||||
(' ' + instClassName + ' ').indexOf(' ' + className + ' ') !== -1
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Like scryRenderedDOMComponentsWithClass but expects there to be one result,
|
||||
* and returns that one result, or throws exception if there is any other
|
||||
* number of matches besides one.
|
||||
* @return {!ReactNativeComponent} The one match.
|
||||
*/
|
||||
findRenderedDOMComponentWithClass: function(root, className) {
|
||||
var all =
|
||||
ReactTestUtils.scryRenderedDOMComponentsWithClass(root, className);
|
||||
if (all.length !== 1) {
|
||||
throw new Error('Did not find exactly one match for class:' + className);
|
||||
}
|
||||
return all[0];
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Finds all instance of components in the rendered tree that are DOM
|
||||
* components with the tag name matching `tagName`.
|
||||
* @return an array of all the matches.
|
||||
*/
|
||||
scryRenderedDOMComponentsWithTag: function(root, tagName) {
|
||||
return ReactTestUtils.findAllInRenderedTree(root, function(inst) {
|
||||
return ReactTestUtils.isDOMComponent(inst) &&
|
||||
inst.tagName === tagName.toUpperCase();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Like scryRenderedDOMComponentsWithTag but expects there to be one result,
|
||||
* and returns that one result, or throws exception if there is any other
|
||||
* number of matches besides one.
|
||||
* @return {!ReactNativeComponent} The one match.
|
||||
*/
|
||||
findRenderedDOMComponentWithTag: function(root, tagName) {
|
||||
var all = ReactTestUtils.scryRenderedDOMComponentsWithTag(root, tagName);
|
||||
if (all.length !== 1) {
|
||||
throw new Error('Did not find exactly one match for tag:' + tagName);
|
||||
}
|
||||
return all[0];
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Finds all instances of components with type equal to `componentType`.
|
||||
* @return an array of all the matches.
|
||||
*/
|
||||
scryRenderedComponentsWithType: function(root, componentType) {
|
||||
return ReactTestUtils.findAllInRenderedTree(root, function(inst) {
|
||||
return ReactTestUtils.isCompositeComponentWithType(inst, componentType);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Same as `scryRenderedComponentsWithType` but expects there to be one result
|
||||
* and returns that one result, or throws exception if there is any other
|
||||
* number of matches besides one.
|
||||
* @return {!ReactComponent} The one match.
|
||||
*/
|
||||
findRenderedComponentWithType: function(root, componentType) {
|
||||
var all = ReactTestUtils.scryRenderedComponentsWithType(
|
||||
root,
|
||||
componentType
|
||||
);
|
||||
if (all.length !== 1) {
|
||||
throw new Error(
|
||||
'Did not find exactly one match for componentType:' + componentType
|
||||
);
|
||||
}
|
||||
return all[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulates a top level event being dispatched from a raw event that occured
|
||||
* on and `Element` node.
|
||||
* @param topLevelType {Object} A type from `EventConstants.topLevelTypes`
|
||||
* @param {!Element} node The dom to simulate an event occurring on.
|
||||
* @param {?Event} fakeNativeEvent Fake native event to pass to ReactEvent.
|
||||
*/
|
||||
simulateEventOnNode: function(topLevelType, node, fakeNativeEvent) {
|
||||
var virtualHandler =
|
||||
ReactEvent.TopLevelCallbackCreator.createTopLevelCallback(topLevelType);
|
||||
fakeNativeEvent.target = node;
|
||||
virtualHandler(fakeNativeEvent);
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulates a top level event being dispatched from a raw event that occured
|
||||
* on the `ReactNativeComponent` `comp`.
|
||||
* @param topLevelType {Object} A type from `EventConstants.topLevelTypes`.
|
||||
* @param comp {!ReactNativeComponent}
|
||||
* @param {?Event} fakeNativeEvent Fake native event to pass to ReactEvent.
|
||||
*/
|
||||
simulateEventOnDOMComponent: function(topLevelType, comp, fakeNativeEvent) {
|
||||
var reactRootID = comp._rootNodeID || comp._rootDomId;
|
||||
if (!reactRootID) {
|
||||
throw new Error('Simulating event on non-rendered component');
|
||||
}
|
||||
var virtualHandler =
|
||||
ReactEvent.TopLevelCallbackCreator.createTopLevelCallback(topLevelType);
|
||||
var node = ge(reactRootID);
|
||||
fakeNativeEvent.target = node;
|
||||
/* jsdom is returning nodes without id's - fixing that issue here. */
|
||||
node.id = reactRootID;
|
||||
virtualHandler(fakeNativeEvent);
|
||||
},
|
||||
|
||||
nativeTouchData: function(x, y) {
|
||||
return {
|
||||
touches: [
|
||||
{pageX: x, pageY: y}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
Simulate: null // Will populate
|
||||
};
|
||||
|
||||
/**
|
||||
* Exports:
|
||||
*
|
||||
* - `ReactTestUtils.Simulate.click(Element/ReactNativeComponent)`
|
||||
* - `ReactTestUtils.Simulate.mouseMove(Element/ReactNativeComponent)`
|
||||
* - `ReactTestUtils.Simulate.mouseIn/ReactNativeComponent)`
|
||||
* - `ReactTestUtils.Simulate.mouseOut(Element/ReactNativeComponent)`
|
||||
* - ... (All keys from `EventConstants.topLevelTypes`)
|
||||
*
|
||||
* Note: Top level event types are a subset of the entire set of handler types
|
||||
* (which include a broader set of "synthetic" events). For example, onDragDone
|
||||
* is a synthetic event. You certainly may write test cases for these event
|
||||
* types, but it doesn't make sense to simulate them at this low of a level. In
|
||||
* this case, the way you test an `onDragDone` event is by simulating a series
|
||||
* of `mouseMove`/ `mouseDown`/`mouseUp` events - Then, a synthetic event of
|
||||
* type `onDragDone` will be constructed and dispached through your system
|
||||
* automatically.
|
||||
*/
|
||||
|
||||
function makeSimulator(eventType) {
|
||||
return function(domComponentOrNode, nativeEventData) {
|
||||
var fakeNativeEvent = new Event(eventType);
|
||||
mergeInto(fakeNativeEvent, nativeEventData);
|
||||
if (ReactTestUtils.isDOMComponent(domComponentOrNode)) {
|
||||
ReactTestUtils.simulateEventOnDOMComponent(
|
||||
eventType,
|
||||
domComponentOrNode,
|
||||
fakeNativeEvent
|
||||
);
|
||||
} else if (!!domComponentOrNode.tagName) {
|
||||
// Will allow on actual dom nodes.
|
||||
ReactTestUtils.simulateEventOnNode(
|
||||
eventType,
|
||||
domComponentOrNode,
|
||||
fakeNativeEvent
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ReactTestUtils.Simulate = {};
|
||||
var eventType;
|
||||
for (eventType in topLevelTypes) {
|
||||
// Event type is stored as 'topClick' - we transform that to 'click'
|
||||
var convenienceName = eventType.indexOf('top') === 0 ?
|
||||
eventType.charAt(3).toLowerCase() + eventType.substr(4) : eventType;
|
||||
/**
|
||||
* @param {!Element || ReactNativeComponent} domComponentOrNode
|
||||
* @param {?Event} nativeEventData Fake native event to pass to ReactEvent.
|
||||
*/
|
||||
ReactTestUtils.Simulate[convenienceName] = makeSimulator(eventType);
|
||||
}
|
||||
|
||||
module.exports = ReactTestUtils;
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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 ReactTextComponent
|
||||
* @typechecks
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactComponent = require("./ReactComponent");
|
||||
|
||||
var escapeTextForBrowser = require("./escapeTextForBrowser");
|
||||
var mixInto = require("./mixInto");
|
||||
|
||||
/**
|
||||
* Text nodes violate a couple assumptions that React makes about components:
|
||||
*
|
||||
* - When mounting text into the DOM, adjacent text nodes are merged.
|
||||
* - Text nodes cannot be assigned a React root ID.
|
||||
*
|
||||
* This component is used to wrap strings in elements so that they can undergo
|
||||
* the same reconciliation that is applied to elements.
|
||||
*
|
||||
* TODO: Investigate representing React components in the DOM with text nodes.
|
||||
*
|
||||
* @class ReactTextComponent
|
||||
* @extends ReactComponent
|
||||
* @internal
|
||||
*/
|
||||
var ReactTextComponent = function(initialText) {
|
||||
this.construct({text: initialText});
|
||||
};
|
||||
|
||||
mixInto(ReactTextComponent, ReactComponent.Mixin);
|
||||
mixInto(ReactTextComponent, {
|
||||
|
||||
/**
|
||||
* Creates the markup for this text node. This node is not intended to have
|
||||
* any features besides containing text content.
|
||||
*
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @return {string} Markup for this text node.
|
||||
* @internal
|
||||
*/
|
||||
mountComponent: function(rootID) {
|
||||
ReactComponent.Mixin.mountComponent.call(this, rootID);
|
||||
return (
|
||||
'<span id="' + rootID + '">' +
|
||||
escapeTextForBrowser(this.props.text) +
|
||||
'</span>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates this component by updating the text content.
|
||||
*
|
||||
* @param {object} nextProps Contains the next text content.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @internal
|
||||
*/
|
||||
receiveProps: function(nextProps, transaction) {
|
||||
if (nextProps.text !== this.props.text) {
|
||||
this.props.text = nextProps.text;
|
||||
ReactComponent.DOMIDOperations.updateTextContentByID(
|
||||
this._rootNodeID,
|
||||
nextProps.text
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.exports = ReactTextComponent;
|
||||
@@ -0,0 +1,312 @@
|
||||
/**
|
||||
* 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 ResponderEventPlugin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var AbstractEvent = require("./AbstractEvent");
|
||||
var EventConstants = require("./EventConstants");
|
||||
var EventPluginUtils = require("./EventPluginUtils");
|
||||
var EventPropagators = require("./EventPropagators");
|
||||
|
||||
var accumulate = require("./accumulate");
|
||||
var keyOf = require("./keyOf");
|
||||
|
||||
var isStartish = EventPluginUtils.isStartish;
|
||||
var isMoveish = EventPluginUtils.isMoveish;
|
||||
var isEndish = EventPluginUtils.isEndish;
|
||||
var executeDirectDispatch = EventPluginUtils.executeDirectDispatch;
|
||||
var hasDispatches = EventPluginUtils.hasDispatches;
|
||||
var executeDispatchesInOrderStopAtTrue =
|
||||
EventPluginUtils.executeDispatchesInOrderStopAtTrue;
|
||||
|
||||
/**
|
||||
* ID of element that should respond to touch/move types of interactions, as
|
||||
* indicated explicitly by relevant callbacks.
|
||||
*/
|
||||
var responderID = null;
|
||||
var isPressing = false;
|
||||
|
||||
var getResponderID = function() {
|
||||
return responderID;
|
||||
};
|
||||
|
||||
var abstractEventTypes = {
|
||||
/**
|
||||
* On a `touchStart`/`mouseDown`, is it desired that this element become the
|
||||
* responder?
|
||||
*/
|
||||
startShouldSetResponder: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onStartShouldSetResponder: null}),
|
||||
captured: keyOf({onStartShouldSetResponderCapture: null})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On a `scroll`, is it desired that this element become the responder? This
|
||||
* is usually not needed, but should be used to retroactively infer that a
|
||||
* `touchStart` had occured during momentum scroll. During a momentum scroll,
|
||||
* a touch start will be immediately followed by a scroll event if the view is
|
||||
* currently scrolling.
|
||||
*/
|
||||
scrollShouldSetResponder: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onScrollShouldSetResponder: null}),
|
||||
captured: keyOf({onScrollShouldSetResponderCapture: null})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On a `touchMove`/`mouseMove`, is it desired that this element become the
|
||||
* responder?
|
||||
*/
|
||||
moveShouldSetResponder: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onMoveShouldSetResponder: null}),
|
||||
captured: keyOf({onMoveShouldSetResponderCapture: null})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Direct responder events dispatched directly to responder. Do not bubble.
|
||||
*/
|
||||
responderMove: {registrationName: keyOf({onResponderMove: null})},
|
||||
responderRelease: {registrationName: keyOf({onResponderRelease: null})},
|
||||
responderTerminationRequest: {
|
||||
registrationName: keyOf({onResponderTerminationRequest: null})
|
||||
},
|
||||
responderGrant: {registrationName: keyOf({onResponderGrant: null})},
|
||||
responderReject: {registrationName: keyOf({onResponderReject: null})},
|
||||
responderTerminate: {registrationName: keyOf({onResponderTerminate: null})}
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs negotiation between any existing/current responder, checks to see if
|
||||
* any new entity is interested in becoming responder, performs that handshake
|
||||
* and returns any events that must be emitted to notify the relevant parties.
|
||||
*
|
||||
* A note about event ordering in the `EventPluginHub`.
|
||||
*
|
||||
* Suppose plugins are injected in the following order:
|
||||
*
|
||||
* `[R, S, C]`
|
||||
*
|
||||
* To help illustrate the example, assume `S` is `SimpleEventPlugin` (for
|
||||
* `onClick` etc) and `R` is `ResponderEventPlugin`.
|
||||
*
|
||||
* "Deferred-Dispatched Events":
|
||||
*
|
||||
* - The current event plugin system will traverse the list of injected plugins,
|
||||
* in order, and extract events by collecting the plugin's return value of
|
||||
* `extractAbstractEvents()`.
|
||||
* - These events that are returned from `extractAbstractEvents` are "deferred
|
||||
* dispatched events".
|
||||
* - When returned from `extractAbstractEvents`, deferred dispatched events
|
||||
* contain an "accumulation" of deferred dispatches.
|
||||
* -- These deferred dispatches are accumulated/collected before they are
|
||||
* returned, but processed at a later time by the `EventPluginHub` (hence the
|
||||
* name deferred).
|
||||
*
|
||||
* In the process of returning their deferred dispatched events, event plugins
|
||||
* themselves can dispatch events on-demand without returning them from
|
||||
* `extractAbstractEvents`. Plugins might want to do this, so that they can use
|
||||
* event dispatching as a tool that helps them decide which events should be
|
||||
* extracted in the first place.
|
||||
*
|
||||
* "On-Demand-Dispatched Events":
|
||||
*
|
||||
* - On-demand dispatched are not returned from `extractAbstractEvents`.
|
||||
* - On-demand dispatched events are dispatched during the process of returning
|
||||
* the deferred-dispatched events.
|
||||
* - They should not have side effects.
|
||||
* - They should be avoided, and/or eventually be replaced with another
|
||||
* abstraction that allows event plugins to perform multiple "rounds" of event
|
||||
* extraction.
|
||||
*
|
||||
* Therefore, the sequence of event dispatches becomes:
|
||||
*
|
||||
* - `R`s on-demand events (if any) (dispatched by `R` on-demand)
|
||||
* - `S`s on-demand events (if any) (dispatched by `S` on-demand)
|
||||
* - `C`s on-demand events (if any) (dispatched by `C` on-demand)
|
||||
* - `R`s extracted events (if any) (dispatched by `EventPluginHub`)
|
||||
* - `S`s extracted events (if any) (dispatched by `EventPluginHub`)
|
||||
* - `C`s extracted events (if any) (dispatched by `EventPluginHub`)
|
||||
*
|
||||
* In the case of `ResponderEventPlugin`: If the `startShouldSetResponder`
|
||||
* on-demand dispatch returns `true` (and some other details are satisfied) the
|
||||
* `onResponderGrant` deferred dispatched event is returned from
|
||||
* `extractAbstractEvents`. The sequence of dispatch executions in this case
|
||||
* will appear as follows:
|
||||
*
|
||||
* - `startShouldSetResponder` (`ResponderEventPlugin` dispatches on-demand)
|
||||
* - `touchStartCapture` (`EventPluginHub` dispatches as usual)
|
||||
* - `touchStart` (`EventPluginHub` dispatches as usual)
|
||||
* - `responderGrant/Reject` (`EventPluginHub` dispatches as usual)
|
||||
*
|
||||
* @returns {Accumulation<AbstractEvent>}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {TopLevelTypes} topLevelType Top level event type being examined.
|
||||
* @param {DOMEvent} nativeEvent Native DOM event.
|
||||
* @param {string} renderedTargetID ID of deepest React rendered element.
|
||||
*
|
||||
* @return {Accumulation<AbstractEvent>} Extracted events.
|
||||
*/
|
||||
var setResponderAndExtractTransfer =
|
||||
function(topLevelType, nativeEvent, renderedTargetID) {
|
||||
var type;
|
||||
var shouldSetEventType =
|
||||
isStartish(topLevelType) ? abstractEventTypes.startShouldSetResponder :
|
||||
isMoveish(topLevelType) ? abstractEventTypes.moveShouldSetResponder :
|
||||
abstractEventTypes.scrollShouldSetResponder;
|
||||
|
||||
var bubbleShouldSetFrom = responderID || renderedTargetID;
|
||||
var shouldSetEvent = AbstractEvent.getPooled(
|
||||
shouldSetEventType,
|
||||
bubbleShouldSetFrom,
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
AbstractEvent.normalizePointerData(nativeEvent)
|
||||
);
|
||||
EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent);
|
||||
var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
|
||||
AbstractEvent.release(shouldSetEvent);
|
||||
|
||||
if (!wantsResponderID || wantsResponderID === responderID) {
|
||||
return null;
|
||||
}
|
||||
var extracted;
|
||||
var grantEvent = AbstractEvent.getPooled(
|
||||
abstractEventTypes.responderGrant,
|
||||
wantsResponderID,
|
||||
topLevelType,
|
||||
nativeEvent
|
||||
);
|
||||
|
||||
EventPropagators.accumulateDirectDispatches(grantEvent);
|
||||
if (responderID) {
|
||||
type = abstractEventTypes.responderTerminationRequest;
|
||||
var terminationRequestEvent = AbstractEvent.getPooled(type, responderID);
|
||||
EventPropagators.accumulateDirectDispatches(terminationRequestEvent);
|
||||
var shouldSwitch = !hasDispatches(terminationRequestEvent) ||
|
||||
executeDirectDispatch(terminationRequestEvent);
|
||||
AbstractEvent.release(terminationRequestEvent);
|
||||
if (shouldSwitch) {
|
||||
var terminateType = abstractEventTypes.responderTerminate;
|
||||
var terminateEvent = AbstractEvent.getPooled(
|
||||
terminateType,
|
||||
responderID,
|
||||
topLevelType,
|
||||
nativeEvent
|
||||
);
|
||||
EventPropagators.accumulateDirectDispatches(terminateEvent);
|
||||
extracted = accumulate(extracted, [grantEvent, terminateEvent]);
|
||||
responderID = wantsResponderID;
|
||||
} else {
|
||||
var rejectEvent = AbstractEvent.getPooled(
|
||||
abstractEventTypes.responderReject,
|
||||
wantsResponderID,
|
||||
topLevelType,
|
||||
nativeEvent
|
||||
);
|
||||
EventPropagators.accumulateDirectDispatches(rejectEvent);
|
||||
extracted = accumulate(extracted, rejectEvent);
|
||||
}
|
||||
} else {
|
||||
extracted = accumulate(extracted, grantEvent);
|
||||
responderID = wantsResponderID;
|
||||
}
|
||||
return extracted;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A transfer is a negotiation between a currently set responder and the next
|
||||
* element to claim responder status. Any start event could trigger a transfer
|
||||
* of responderID. Any move event could trigger a transfer, so long as there is
|
||||
* currently a responder set (in other words as long as the user is pressing
|
||||
* down).
|
||||
*
|
||||
* @param {EventConstants.topLevelTypes} topLevelType
|
||||
* @return {boolean} Whether or not a transfer of responder could possibly
|
||||
* occur.
|
||||
*/
|
||||
function canTriggerTransfer(topLevelType) {
|
||||
return topLevelType === EventConstants.topLevelTypes.topScroll ||
|
||||
isStartish(topLevelType) ||
|
||||
(isPressing && isMoveish(topLevelType));
|
||||
}
|
||||
|
||||
var extractAbstractEvents =
|
||||
function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) {
|
||||
var extracted;
|
||||
// Must have missed an end event - reset the state here.
|
||||
if (responderID && isStartish(topLevelType)) {
|
||||
responderID = null;
|
||||
}
|
||||
if (isStartish(topLevelType)) {
|
||||
isPressing = true;
|
||||
} else if (isEndish(topLevelType)) {
|
||||
isPressing = false;
|
||||
}
|
||||
if (canTriggerTransfer(topLevelType)) {
|
||||
var transfer = setResponderAndExtractTransfer(
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
renderedTargetID,
|
||||
renderedTarget
|
||||
);
|
||||
if (transfer) {
|
||||
extracted = accumulate(extracted, transfer);
|
||||
}
|
||||
}
|
||||
// Now that we know the responder is set correctly, we can dispatch
|
||||
// responder type events (directly to the responder).
|
||||
var type = isMoveish(topLevelType) ? abstractEventTypes.responderMove :
|
||||
isEndish(topLevelType) ? abstractEventTypes.responderRelease :
|
||||
isStartish(topLevelType) ? abstractEventTypes.responderStart : null;
|
||||
if (type) {
|
||||
var data = AbstractEvent.normalizePointerData(nativeEvent);
|
||||
var gesture = AbstractEvent.getPooled(
|
||||
type,
|
||||
responderID,
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
data
|
||||
);
|
||||
EventPropagators.accumulateDirectDispatches(gesture);
|
||||
extracted = accumulate(extracted, gesture);
|
||||
}
|
||||
if (type === abstractEventTypes.responderRelease) {
|
||||
responderID = null;
|
||||
}
|
||||
return extracted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Event plugin for formalizing the negotiation between claiming locks on
|
||||
* receiving touches.
|
||||
*/
|
||||
var ResponderEventPlugin = {
|
||||
abstractEventTypes: abstractEventTypes,
|
||||
extractAbstractEvents: extractAbstractEvents,
|
||||
getResponderID: getResponderID
|
||||
};
|
||||
|
||||
module.exports = ResponderEventPlugin;
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 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 SimpleEventPlugin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var AbstractEvent = require("./AbstractEvent");
|
||||
var EventConstants = require("./EventConstants");
|
||||
var EventPropagators = require("./EventPropagators");
|
||||
|
||||
var keyOf = require("./keyOf");
|
||||
|
||||
var topLevelTypes = EventConstants.topLevelTypes;
|
||||
|
||||
var SimpleEventPlugin = {
|
||||
abstractEventTypes: {
|
||||
// Note: We do not allow listening to mouseOver events. Instead, use the
|
||||
// onMouseEnter/onMouseLeave created by `EnterLeaveEventPlugin`.
|
||||
mouseDown: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onMouseDown: true}),
|
||||
captured: keyOf({onMouseDownCapture: true})
|
||||
}
|
||||
},
|
||||
mouseUp: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onMouseUp: true}),
|
||||
captured: keyOf({onMouseUpCapture: true})
|
||||
}
|
||||
},
|
||||
mouseMove: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onMouseMove: true}),
|
||||
captured: keyOf({onMouseMoveCapture: true})
|
||||
}
|
||||
},
|
||||
doubleClick: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onDoubleClick: true}),
|
||||
captured: keyOf({onDoubleClickCapture: true})
|
||||
}
|
||||
},
|
||||
click: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onClick: true}),
|
||||
captured: keyOf({onClickCapture: true})
|
||||
}
|
||||
},
|
||||
mouseWheel: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onMouseWheel: true}),
|
||||
captured: keyOf({onMouseWheelCapture: true})
|
||||
}
|
||||
},
|
||||
touchStart: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onTouchStart: true}),
|
||||
captured: keyOf({onTouchStartCapture: true})
|
||||
}
|
||||
},
|
||||
touchEnd: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onTouchEnd: true}),
|
||||
captured: keyOf({onTouchEndCapture: true})
|
||||
}
|
||||
},
|
||||
touchCancel: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onTouchCancel: true}),
|
||||
captured: keyOf({onTouchCancelCapture: true})
|
||||
}
|
||||
},
|
||||
touchMove: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onTouchMove: true}),
|
||||
captured: keyOf({onTouchMoveCapture: true})
|
||||
}
|
||||
},
|
||||
keyUp: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onKeyUp: true}),
|
||||
captured: keyOf({onKeyUpCapture: true})
|
||||
}
|
||||
},
|
||||
keyPress: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onKeyPress: true}),
|
||||
captured: keyOf({onKeyPressCapture: true})
|
||||
}
|
||||
},
|
||||
keyDown: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onKeyDown: true}),
|
||||
captured: keyOf({onKeyDownCapture: true})
|
||||
}
|
||||
},
|
||||
focus: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onFocus: true}),
|
||||
captured: keyOf({onFocusCapture: true})
|
||||
}
|
||||
},
|
||||
blur: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onBlur: true}),
|
||||
captured: keyOf({onBlurCapture: true})
|
||||
}
|
||||
},
|
||||
scroll: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onScroll: true}),
|
||||
captured: keyOf({onScrollCapture: true})
|
||||
}
|
||||
},
|
||||
change: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onChange: true}),
|
||||
captured: keyOf({onChangeCapture: true})
|
||||
}
|
||||
},
|
||||
submit: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onSubmit: true}),
|
||||
captured: keyOf({onSubmitCapture: true})
|
||||
}
|
||||
},
|
||||
DOMCharacterDataModified: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onDOMCharacterDataModified: true}),
|
||||
captured: keyOf({onDOMCharacterDataModifiedCapture: true})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Same as the default implementation, except cancels the event when return
|
||||
* value is false.
|
||||
* @param {AbstractEvent} AbstractEvent to handle
|
||||
* @param {function} Application-level callback
|
||||
* @param {string} domID DOM id to pass to the callback.
|
||||
*/
|
||||
executeDispatch: function(abstractEvent, listener, domID) {
|
||||
var returnValue = listener(abstractEvent, domID);
|
||||
if (returnValue === false) {
|
||||
abstractEvent.stopPropagation();
|
||||
abstractEvent.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @see EventPluginHub.extractAbstractEvents
|
||||
*/
|
||||
extractAbstractEvents:
|
||||
function(topLevelType, nativeEvent, renderedTargetID, renderedTarget) {
|
||||
var data;
|
||||
var abstractEventType =
|
||||
SimpleEventPlugin.topLevelTypesToAbstract[topLevelType];
|
||||
if (!abstractEventType) {
|
||||
return null;
|
||||
}
|
||||
switch(topLevelType) {
|
||||
case topLevelTypes.topMouseWheel:
|
||||
data = AbstractEvent.normalizeMouseWheelData(nativeEvent);
|
||||
break;
|
||||
case topLevelTypes.topScroll:
|
||||
data = AbstractEvent.normalizeScrollDataFromTarget(renderedTarget);
|
||||
break;
|
||||
case topLevelTypes.topClick:
|
||||
case topLevelTypes.topDoubleClick:
|
||||
case topLevelTypes.topChange:
|
||||
case topLevelTypes.topDOMCharacterDataModified:
|
||||
case topLevelTypes.topMouseDown:
|
||||
case topLevelTypes.topMouseUp:
|
||||
case topLevelTypes.topMouseMove:
|
||||
case topLevelTypes.topTouchMove:
|
||||
case topLevelTypes.topTouchStart:
|
||||
case topLevelTypes.topTouchEnd:
|
||||
data = AbstractEvent.normalizePointerData(nativeEvent);
|
||||
break;
|
||||
default:
|
||||
data = null;
|
||||
}
|
||||
var abstractEvent = AbstractEvent.getPooled(
|
||||
abstractEventType,
|
||||
renderedTargetID,
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
data
|
||||
);
|
||||
EventPropagators.accumulateTwoPhaseDispatches(abstractEvent);
|
||||
return abstractEvent;
|
||||
}
|
||||
};
|
||||
|
||||
SimpleEventPlugin.topLevelTypesToAbstract = {
|
||||
topMouseDown: SimpleEventPlugin.abstractEventTypes.mouseDown,
|
||||
topMouseUp: SimpleEventPlugin.abstractEventTypes.mouseUp,
|
||||
topMouseMove: SimpleEventPlugin.abstractEventTypes.mouseMove,
|
||||
topClick: SimpleEventPlugin.abstractEventTypes.click,
|
||||
topDoubleClick: SimpleEventPlugin.abstractEventTypes.doubleClick,
|
||||
topMouseWheel: SimpleEventPlugin.abstractEventTypes.mouseWheel,
|
||||
topTouchStart: SimpleEventPlugin.abstractEventTypes.touchStart,
|
||||
topTouchEnd: SimpleEventPlugin.abstractEventTypes.touchEnd,
|
||||
topTouchMove: SimpleEventPlugin.abstractEventTypes.touchMove,
|
||||
topTouchCancel: SimpleEventPlugin.abstractEventTypes.touchCancel,
|
||||
topKeyUp: SimpleEventPlugin.abstractEventTypes.keyUp,
|
||||
topKeyPress: SimpleEventPlugin.abstractEventTypes.keyPress,
|
||||
topKeyDown: SimpleEventPlugin.abstractEventTypes.keyDown,
|
||||
topFocus: SimpleEventPlugin.abstractEventTypes.focus,
|
||||
topBlur: SimpleEventPlugin.abstractEventTypes.blur,
|
||||
topScroll: SimpleEventPlugin.abstractEventTypes.scroll,
|
||||
topChange: SimpleEventPlugin.abstractEventTypes.change,
|
||||
topSubmit: SimpleEventPlugin.abstractEventTypes.submit,
|
||||
topDOMCharacterDataModified:
|
||||
SimpleEventPlugin.abstractEventTypes.DOMCharacterDataModified
|
||||
};
|
||||
|
||||
module.exports = SimpleEventPlugin;
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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 TapEventPlugin
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var AbstractEvent = require("./AbstractEvent");
|
||||
var EventPluginUtils = require("./EventPluginUtils");
|
||||
var EventPropagators = require("./EventPropagators");
|
||||
|
||||
var keyOf = require("./keyOf");
|
||||
|
||||
var isStartish = EventPluginUtils.isStartish;
|
||||
var isEndish = EventPluginUtils.isEndish;
|
||||
var storePageCoordsIn = EventPluginUtils.storePageCoordsIn;
|
||||
var eventDistance = EventPluginUtils.eventDistance;
|
||||
|
||||
/**
|
||||
* The number of pixels that are tolerated in between a touchStart and
|
||||
* touchEnd in order to still be considered a 'tap' event.
|
||||
*/
|
||||
var tapMoveThreshold = 10;
|
||||
var startCoords = {x: null, y: null};
|
||||
|
||||
var abstractEventTypes = {
|
||||
touchTap: {
|
||||
phasedRegistrationNames: {
|
||||
bubbled: keyOf({onTouchTap: null}),
|
||||
captured: keyOf({onTouchTapCapture: null})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @see EventPluginHub.extractAbstractEvents
|
||||
*/
|
||||
var extractAbstractEvents = function(
|
||||
topLevelType,
|
||||
nativeEvent,
|
||||
renderedTargetID,
|
||||
renderedTarget) {
|
||||
|
||||
if (!isStartish(topLevelType) && !isEndish(topLevelType)) {
|
||||
return;
|
||||
}
|
||||
var abstractEvent;
|
||||
var dist = eventDistance(startCoords, nativeEvent);
|
||||
if (isEndish(topLevelType) && dist < tapMoveThreshold) {
|
||||
var type = abstractEventTypes.touchTap;
|
||||
var abstractTargetID = renderedTargetID;
|
||||
abstractEvent = AbstractEvent.getPooled(
|
||||
type,
|
||||
abstractTargetID,
|
||||
topLevelType,
|
||||
nativeEvent
|
||||
);
|
||||
}
|
||||
if (isStartish(topLevelType)) {
|
||||
storePageCoordsIn(startCoords, nativeEvent);
|
||||
} else if (isEndish(topLevelType)) {
|
||||
startCoords.x = 0;
|
||||
startCoords.y = 0;
|
||||
}
|
||||
EventPropagators.accumulateTwoPhaseDispatches(abstractEvent);
|
||||
return abstractEvent;
|
||||
};
|
||||
|
||||
var TapEventPlugin = {
|
||||
tapMoveThreshold: tapMoveThreshold,
|
||||
startCoords: startCoords,
|
||||
abstractEventTypes: abstractEventTypes,
|
||||
extractAbstractEvents: extractAbstractEvents
|
||||
};
|
||||
|
||||
module.exports = TapEventPlugin;
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 TouchEventUtils
|
||||
*/
|
||||
|
||||
var TouchEventUtils = {
|
||||
/**
|
||||
* Utility function for common case of extracting out the primary touch from a
|
||||
* touch event.
|
||||
* - `touchEnd` events usually do not have the `touches` property.
|
||||
* http://stackoverflow.com/questions/3666929/
|
||||
* mobile-sarai-touchend-event-not-firing-when-last-touch-is-removed
|
||||
*
|
||||
* @param {Event} nativeEvent Native event that may or may not be a touch.
|
||||
* @return {TouchesObject?} an object with pageX and pageY or null.
|
||||
*/
|
||||
extractSingleTouch: function(nativeEvent) {
|
||||
var touches = nativeEvent.touches;
|
||||
var changedTouches = nativeEvent.changedTouches;
|
||||
var hasTouches = touches && touches.length > 0;
|
||||
var hasChangedTouches = changedTouches && changedTouches.length > 0;
|
||||
|
||||
return !hasTouches && hasChangedTouches ? changedTouches[0] :
|
||||
hasTouches ? touches[0] :
|
||||
nativeEvent;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TouchEventUtils;
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* 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 Transaction
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var throwIf = require("./throwIf");
|
||||
|
||||
var DUAL_TRANSACTION = 'DUAL_TRANSACTION';
|
||||
var MISSING_TRANSACTION = 'MISSING_TRANSACTION';
|
||||
if (true) {
|
||||
DUAL_TRANSACTION =
|
||||
'Cannot initialize transaction when there is already an outstanding ' +
|
||||
'transaction. Common causes of this are trying to render a component ' +
|
||||
'when you are already rendering a component or attempting a state ' +
|
||||
'transition while in a render function. Another possibility is that ' +
|
||||
'you are rendering new content (or state transitioning) in a ' +
|
||||
'componentDidRender callback. If this is not the case, please report the ' +
|
||||
'issue immediately.';
|
||||
|
||||
MISSING_TRANSACTION =
|
||||
'Cannot close transaction when there is none open.';
|
||||
}
|
||||
|
||||
/**
|
||||
* `Transaction` creates a black box that is able to wrap any method such that
|
||||
* certain invariants are maintained before and after the method is invoked
|
||||
* (Even if an exception is thrown while invoking the wrapped method). Whoever
|
||||
* instantiates a transaction can provide enforcers of the invariants at
|
||||
* creation time. The `Transaction` class itself will supply one additional
|
||||
* 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
|
||||
* single instance of a `Transaction` for reuse multiple times, that potentially
|
||||
* is used to wrap several different methods. Wrappers are extremely simple -
|
||||
* they only require implementing two methods.
|
||||
*
|
||||
* <pre>
|
||||
* wrappers (injected at creation time)
|
||||
* + +
|
||||
* | |
|
||||
* +-----------------|--------|--------------+
|
||||
* | v | |
|
||||
* | +---------------+ | |
|
||||
* | +--| wrapper1 |---|----+ |
|
||||
* | | +---------------+ v | |
|
||||
* | | +-------------+ | |
|
||||
* | | +----| wrapper2 |--------+ |
|
||||
* | | | +-------------+ | | |
|
||||
* | | | | | |
|
||||
* | v v v v | wrapper
|
||||
* | +---+ +---+ +---------+ +---+ +---+ | invariants
|
||||
* perform(anyMethod) | | | | | | | | | | | | maintained
|
||||
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
|
||||
* | | | | | | | | | | | |
|
||||
* | | | | | | | | | | | |
|
||||
* | | | | | | | | | | | |
|
||||
* | +---+ +---+ +---------+ +---+ +---+ |
|
||||
* | initialize close |
|
||||
* +-----------------------------------------+
|
||||
* </pre>
|
||||
*
|
||||
* Bonus:
|
||||
* - Reports timing metrics by method name and wrapper index.
|
||||
*
|
||||
* Use cases:
|
||||
* - Preserving the input selection ranges before/after reconciliation.
|
||||
* Restoring selection even in the event of an unexpected error.
|
||||
* - Deactivating events while rearranging the DOM, preventing blurs/focuses,
|
||||
* while guaranteeing that afterwards, the event system is reactivated.
|
||||
* - Flushing a queue of collected DOM mutations to the main UI thread after a
|
||||
* reconciliation takes place in a worker thread.
|
||||
* - Invoking any collected `componentDidRender` callbacks after rendering new
|
||||
* content.
|
||||
* - (Future use case): Wrapping particular flushes of the `ReactWorker` queue
|
||||
* to preserve the `scrollTop` (an automatic scroll aware DOM).
|
||||
* - (Future use case): Layout calculations before and after DOM upates.
|
||||
*
|
||||
* Transactional plugin API:
|
||||
* - A module that has an `initialize` method that returns any precomputation.
|
||||
* - and a `close` method that accepts the precomputation. `close` is invoked
|
||||
* when the wrapped process is completed, or has failed.
|
||||
*
|
||||
* @param {Array<TransactionalWrapper>} transactionWrapper Wrapper modules
|
||||
* that implement `initialize` and `close`.
|
||||
* @return {Transaction} Single transaction for reuse in thread.
|
||||
*
|
||||
* @class Transaction
|
||||
*/
|
||||
var Mixin = {
|
||||
/**
|
||||
* Sets up this instance so that it is prepared for collecting metrics. Does
|
||||
* so such that this setup method may be used on an instance that is already
|
||||
* initialized, in a way that does not consume additional memory upon reuse.
|
||||
* That can be useful if you decide to make your subclass of this mixin a
|
||||
* "PooledClass".
|
||||
*/
|
||||
reinitializeTransaction: function() {
|
||||
this.transactionWrappers = this.getTransactionWrappers();
|
||||
if (!this.wrapperInitData) {
|
||||
this.wrapperInitData = [];
|
||||
} else {
|
||||
this.wrapperInitData.length = 0;
|
||||
}
|
||||
if (!this.timingMetrics) {
|
||||
this.timingMetrics = {};
|
||||
}
|
||||
this.timingMetrics.methodInvocationTime = 0;
|
||||
if (!this.timingMetrics.wrapperInitTimes) {
|
||||
this.timingMetrics.wrapperInitTimes = [];
|
||||
} else {
|
||||
this.timingMetrics.wrapperInitTimes.length = 0;
|
||||
}
|
||||
if (!this.timingMetrics.wrapperCloseTimes) {
|
||||
this.timingMetrics.wrapperCloseTimes = [];
|
||||
} else {
|
||||
this.timingMetrics.wrapperCloseTimes.length = 0;
|
||||
}
|
||||
this._isInTransaction = false;
|
||||
},
|
||||
|
||||
_isInTransaction: false,
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {Array<TransactionWrapper>} Array of transaction wrappers.
|
||||
*/
|
||||
getTransactionWrappers: null,
|
||||
|
||||
isInTransaction: function() {
|
||||
return !!this._isInTransaction;
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes the function within a safety window. Use this for the top level
|
||||
* methods that result in large amounts of computation/mutations that would
|
||||
* need to be safety checked.
|
||||
*
|
||||
* @param {function} method Member of scope to call.
|
||||
* @param {Object} scope Scope to invoke from.
|
||||
* @param {Object?=} args... Arguments to pass to the method (optional).
|
||||
* Helps prevent need to bind in many cases.
|
||||
* @returns Return value from `method`.
|
||||
*/
|
||||
perform: function(method, scope, a, b, c, d, e, f) {
|
||||
throwIf(this.isInTransaction(), DUAL_TRANSACTION);
|
||||
var memberStart = Date.now();
|
||||
var err = null;
|
||||
var ret;
|
||||
try {
|
||||
this.initializeAll();
|
||||
ret = method.call(scope, a, b, c, d, e, f);
|
||||
} catch (ie_requires_catch) {
|
||||
err = ie_requires_catch;
|
||||
} finally {
|
||||
var memberEnd = Date.now();
|
||||
this.methodInvocationTime += (memberEnd - memberStart);
|
||||
try {
|
||||
this.closeAll();
|
||||
} catch (closeAllErr) {
|
||||
err = err || closeAllErr;
|
||||
}
|
||||
}
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
initializeAll: function() {
|
||||
this._isInTransaction = true;
|
||||
var transactionWrappers = this.transactionWrappers;
|
||||
var wrapperInitTimes = this.timingMetrics.wrapperInitTimes;
|
||||
var err = null;
|
||||
for (var i = 0; i < transactionWrappers.length; i++) {
|
||||
var initStart = Date.now();
|
||||
var wrapper = transactionWrappers[i];
|
||||
try {
|
||||
this.wrapperInitData[i] =
|
||||
wrapper.initialize ? wrapper.initialize.call(this) : null;
|
||||
} catch (initErr) {
|
||||
err = err || initErr; // Remember the first error.
|
||||
this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
|
||||
} finally {
|
||||
var curInitTime = wrapperInitTimes[i];
|
||||
var initEnd = Date.now();
|
||||
wrapperInitTimes[i] = (curInitTime || 0) + (initEnd - initStart);
|
||||
}
|
||||
}
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Invokes each of `this.transactionWrappers.close[i]` functions, passing into
|
||||
* them the respective return values of `this.transactionWrappers.init[i]`
|
||||
* (`close`rs that correspond to initializers that failed will not be
|
||||
* invoked).
|
||||
*/
|
||||
closeAll: function() {
|
||||
throwIf(!this.isInTransaction(), MISSING_TRANSACTION);
|
||||
var transactionWrappers = this.transactionWrappers;
|
||||
var wrapperCloseTimes = this.timingMetrics.wrapperCloseTimes;
|
||||
var err = null;
|
||||
for (var i = 0; i < transactionWrappers.length; i++) {
|
||||
var wrapper = transactionWrappers[i];
|
||||
var closeStart = Date.now();
|
||||
var initData = this.wrapperInitData[i];
|
||||
try {
|
||||
if (initData !== Transaction.OBSERVED_ERROR) {
|
||||
wrapper.close && wrapper.close.call(this, initData);
|
||||
}
|
||||
} catch (closeErr) {
|
||||
err = err || closeErr; // Remember the first error.
|
||||
} finally {
|
||||
var closeEnd = Date.now();
|
||||
var curCloseTime = wrapperCloseTimes[i];
|
||||
wrapperCloseTimes[i] = (curCloseTime || 0) + (closeEnd - closeStart);
|
||||
}
|
||||
}
|
||||
this.wrapperInitData.length = 0;
|
||||
this._isInTransaction = false;
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var Transaction = {
|
||||
Mixin: Mixin,
|
||||
/**
|
||||
* Token to look for to determine if an error occured.
|
||||
*/
|
||||
OBSERVED_ERROR: {}
|
||||
|
||||
};
|
||||
|
||||
module.exports = Transaction;
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 accumulate
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var throwIf = require("./throwIf");
|
||||
|
||||
var INVALID_ARGS = 'INVALID_ACCUM_ARGS';
|
||||
|
||||
if (true) {
|
||||
INVALID_ARGS =
|
||||
'accumulate requires non empty (non-null, defined) next ' +
|
||||
'values. All arrays accumulated must not contain any empty items.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Accumulates items that must never be empty, into a result in a manner that
|
||||
* conserves memory - avoiding allocation of arrays until they are needed. The
|
||||
* accumulation may start and/or end up being a single element or an array
|
||||
* depending on the total count (if greater than one, an array is allocated).
|
||||
* Handles most common case first (starting with an empty current value and
|
||||
* acquiring one).
|
||||
* @returns {Accumulation} An accumulation which is either a single item or an
|
||||
* Array of items.
|
||||
*/
|
||||
function accumulate(cur, next) {
|
||||
var curValIsEmpty = cur == null; // Will test for emptiness (null/undef)
|
||||
var nextValIsEmpty = next === null;
|
||||
if (true) {
|
||||
throwIf(nextValIsEmpty, INVALID_ARGS);
|
||||
}
|
||||
if (nextValIsEmpty) {
|
||||
return cur;
|
||||
} else {
|
||||
if (curValIsEmpty) {
|
||||
return next;
|
||||
} else {
|
||||
// Both are not empty. Warning: Never call x.concat(y) when you are not
|
||||
// certain that x is an Array (x could be a string with concat method).
|
||||
var curIsArray = Array.isArray(cur);
|
||||
var nextIsArray = Array.isArray(next);
|
||||
if (curIsArray) {
|
||||
return cur.concat(next);
|
||||
} else {
|
||||
if (nextIsArray) {
|
||||
return [cur].concat(next);
|
||||
} else {
|
||||
return [cur, next];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = accumulate;
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 copyProperties
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copy properties from one or more objects (up to 5) into the first object.
|
||||
* This is a shallow copy. It mutates the first object and also returns it.
|
||||
*
|
||||
* NOTE: `arguments` has a very significant performance penalty, which is why
|
||||
* we don't support unlimited arguments.
|
||||
*/
|
||||
function copyProperties(obj, a, b, c, d, e, f) {
|
||||
obj = obj || {};
|
||||
|
||||
if (true) {
|
||||
if (f) {
|
||||
throw new Error('Too many arguments passed to copyProperties');
|
||||
}
|
||||
}
|
||||
|
||||
var args = [a, b, c, d, e];
|
||||
var ii = 0, v;
|
||||
while (args[ii]) {
|
||||
v = args[ii++];
|
||||
for (var k in v) {
|
||||
obj[k] = v[k];
|
||||
}
|
||||
|
||||
// IE ignores toString in object iteration.. See:
|
||||
// webreflection.blogspot.com/2007/07/quick-fix-internet-explorer-and.html
|
||||
if (v.hasOwnProperty && v.hasOwnProperty('toString') &&
|
||||
(typeof v.toString != 'undefined') && (obj.toString !== v.toString)) {
|
||||
obj.toString = v.toString;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
module.exports = copyProperties;
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("nonexistent module required: core/__tests__/ReactComponentFlattenChildren-test");
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// Requires
|
||||
var React;
|
||||
var ReactTestUtils;
|
||||
var reactComponentExpect;
|
||||
|
||||
// Test components
|
||||
var LowerLevelComposite;
|
||||
var MyCompositeComponent;
|
||||
|
||||
var expectSingleChildlessDiv;
|
||||
|
||||
/**
|
||||
* Integration test, testing the combination of JSX with our unit of
|
||||
* abstraction, `ReactCompositeComponent` does not ever add superfluous DOM
|
||||
* nodes.
|
||||
*/
|
||||
describe('ReactCompositeComponentDOMMinimalism', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
reactComponentExpect = require("../../reactComponentExpect");
|
||||
React = require("../../React");
|
||||
ReactTestUtils = require("../../ReactTestUtils");
|
||||
|
||||
LowerLevelComposite = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
React.DOM.div(null,
|
||||
this.props.children
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
MyCompositeComponent = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
LowerLevelComposite(null,
|
||||
this.props.children
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
expectSingleChildlessDiv = function(instance) {
|
||||
reactComponentExpect(instance)
|
||||
.expectRenderedChild()
|
||||
.toBeCompositeComponentWithType(LowerLevelComposite)
|
||||
.expectRenderedChild()
|
||||
.toBeDOMComponentWithTag('div')
|
||||
.toBeDOMComponentWithNoChildren();
|
||||
};
|
||||
});
|
||||
|
||||
it('should not render extra nodes for non-interpolated text', function() {
|
||||
var instance = (
|
||||
MyCompositeComponent(null,
|
||||
" A string child " )
|
||||
);
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
expectSingleChildlessDiv(instance);
|
||||
});
|
||||
|
||||
it('should not render extra nodes for non-interpolated text', function() {
|
||||
var instance = (
|
||||
MyCompositeComponent(null,
|
||||
'Interpolated String Child'
|
||||
)
|
||||
);
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
expectSingleChildlessDiv(instance);
|
||||
});
|
||||
|
||||
it('should not render extra nodes for non-interpolated text', function() {
|
||||
var instance = (
|
||||
MyCompositeComponent(null,
|
||||
React.DOM.ul(null,
|
||||
" This text causes no children in ul, just innerHTML " )
|
||||
)
|
||||
);
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
reactComponentExpect(instance)
|
||||
.expectRenderedChild()
|
||||
.toBeCompositeComponentWithType(LowerLevelComposite)
|
||||
.expectRenderedChild()
|
||||
.toBeDOMComponentWithTag('div')
|
||||
.toBeDOMComponentWithChildCount(1)
|
||||
.expectRenderedChildAt(0)
|
||||
.toBeDOMComponentWithTag('ul')
|
||||
.toBeDOMComponentWithNoChildren();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var mocks = require("../../mocks");
|
||||
|
||||
var React;
|
||||
var ReactTestUtils;
|
||||
var reactComponentExpect;
|
||||
|
||||
var TestComponent;
|
||||
|
||||
describe('ReactCompositeComponent-mixin', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
React = require("../../React");
|
||||
ReactTestUtils = require("../../ReactTestUtils");
|
||||
reactComponentExpect = require("../../reactComponentExpect");
|
||||
|
||||
var MixinA = {
|
||||
componentDidMount: function() {
|
||||
this.props.listener('MixinA didMount');
|
||||
}
|
||||
};
|
||||
|
||||
var MixinB = {
|
||||
mixins: [MixinA],
|
||||
componentDidMount: function() {
|
||||
this.props.listener('MixinB didMount');
|
||||
}
|
||||
};
|
||||
|
||||
var MixinC = {
|
||||
componentDidMount: function() {
|
||||
this.props.listener('MixinC didMount');
|
||||
}
|
||||
};
|
||||
|
||||
TestComponent = React.createClass({
|
||||
mixins: [MixinB, MixinC],
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.listener('Component didMount');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return React.DOM.div(null, null );
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should support chaining delegate functions', function() {
|
||||
var listener = mocks.getMockFunction();
|
||||
var instance = TestComponent( {listener:listener}, null );
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
|
||||
expect(listener.mock.calls).toEqual([
|
||||
['MixinA didMount'],
|
||||
['MixinB didMount'],
|
||||
['MixinC didMount'],
|
||||
['Component didMount']
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var mocks = require("../../mocks");
|
||||
|
||||
var React;
|
||||
var ReactTestUtils;
|
||||
var reactComponentExpect;
|
||||
|
||||
var TestComponent;
|
||||
|
||||
describe('ReactCompositeComponent-state', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
React = require("../../React");
|
||||
ReactTestUtils = require("../../ReactTestUtils");
|
||||
reactComponentExpect = require("../../reactComponentExpect");
|
||||
|
||||
TestComponent = React.createClass({
|
||||
peekAtState: function(from, state) {
|
||||
if (state) {
|
||||
this.props.stateListener(from, state && state.color);
|
||||
} else {
|
||||
this.props.stateListener(
|
||||
from,
|
||||
this.state && this.state.color,
|
||||
this._pendingState && this._pendingState.color
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
setFavoriteColor: function(nextColor) {
|
||||
this.setState({color: nextColor});
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
this.peekAtState('getInitialState');
|
||||
return {color: 'red'};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.peekAtState('render');
|
||||
return React.DOM.div(null, this.state.color);
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.peekAtState('componentWillMount-start');
|
||||
this.setState({color: 'sunrise'});
|
||||
this.peekAtState('componentWillMount-after-sunrise');
|
||||
this.setState({color: 'orange'});
|
||||
this.peekAtState('componentWillMount-end');
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.peekAtState('componentDidMount-start');
|
||||
this.setState({color: 'yellow'});
|
||||
this.peekAtState('componentDidMount-end');
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.peekAtState('componentWillReceiveProps-start');
|
||||
if (newProps.nextColor) {
|
||||
this.setState({color: newProps.nextColor});
|
||||
}
|
||||
this.peekAtState('componentWillReceiveProps-end');
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
this.peekAtState('shouldComponentUpdate-currentState');
|
||||
this.peekAtState('shouldComponentUpdate-nextState', nextState);
|
||||
return true;
|
||||
},
|
||||
|
||||
componentWillUpdate: function(nextProps, nextState) {
|
||||
this.peekAtState('componentWillUpdate-currentState');
|
||||
this.peekAtState('componentWillUpdate-nextState', nextState);
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps, prevState) {
|
||||
this.peekAtState('componentDidUpdate-currentState');
|
||||
this.peekAtState('componentDidUpdate-prevState', prevState);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.peekAtState('componentWillUnmount');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should support setting state', function() {
|
||||
var stateListener = mocks.getMockFunction();
|
||||
var instance = TestComponent( {stateListener:stateListener}, null );
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
instance.setProps({nextColor: 'green'});
|
||||
instance.setFavoriteColor('blue');
|
||||
instance.forceUpdate();
|
||||
instance.unmountComponent();
|
||||
|
||||
expect(stateListener.mock.calls).toEqual([
|
||||
// there is no state when getInitialState() is called
|
||||
[ 'getInitialState', null, null ],
|
||||
[ 'componentWillMount-start', 'red', null ],
|
||||
// setState() only enqueues a pending state.
|
||||
[ 'componentWillMount-after-sunrise', 'red', 'sunrise' ],
|
||||
[ 'componentWillMount-end', 'red', 'orange' ],
|
||||
// pending state has been applied
|
||||
[ 'render', 'orange', null ],
|
||||
[ 'componentDidMount-start', 'orange', null ],
|
||||
// componentDidMount() called setState({color:'yellow'}), currently this
|
||||
// occurs inline.
|
||||
// In a future where setState() is async, this test result will change.
|
||||
[ 'shouldComponentUpdate-currentState', 'orange', null ],
|
||||
[ 'shouldComponentUpdate-nextState', 'yellow' ],
|
||||
[ 'componentWillUpdate-currentState', 'orange', null ],
|
||||
[ 'componentWillUpdate-nextState', 'yellow' ],
|
||||
[ 'render', 'yellow', null ],
|
||||
[ 'componentDidUpdate-currentState', 'yellow', null ],
|
||||
[ 'componentDidUpdate-prevState', 'orange' ],
|
||||
// componentDidMount() finally closes.
|
||||
[ 'componentDidMount-end', 'yellow', null ],
|
||||
[ 'componentWillReceiveProps-start', 'yellow', null ],
|
||||
// setState({color:'green'}) only enqueues a pending state.
|
||||
[ 'componentWillReceiveProps-end', 'yellow', 'green' ],
|
||||
[ 'shouldComponentUpdate-currentState', 'yellow', null ],
|
||||
[ 'shouldComponentUpdate-nextState', 'green' ],
|
||||
[ 'componentWillUpdate-currentState', 'yellow', null ],
|
||||
[ 'componentWillUpdate-nextState', 'green' ],
|
||||
[ 'render', 'green', null ],
|
||||
[ 'componentDidUpdate-currentState', 'green', null ],
|
||||
[ 'componentDidUpdate-prevState', 'yellow' ],
|
||||
// setFavoriteColor('blue')
|
||||
[ 'shouldComponentUpdate-currentState', 'green', null ],
|
||||
[ 'shouldComponentUpdate-nextState', 'blue' ],
|
||||
[ 'componentWillUpdate-currentState', 'green', null ],
|
||||
[ 'componentWillUpdate-nextState', 'blue' ],
|
||||
[ 'render', 'blue', null ],
|
||||
[ 'componentDidUpdate-currentState', 'blue', null ],
|
||||
[ 'componentDidUpdate-prevState', 'green' ],
|
||||
// forceUpdate()
|
||||
[ 'componentWillUpdate-currentState', 'blue', null ],
|
||||
[ 'componentWillUpdate-nextState', 'blue' ],
|
||||
[ 'render', 'blue', null ],
|
||||
[ 'componentDidUpdate-currentState', 'blue', null ],
|
||||
[ 'componentDidUpdate-prevState', 'blue' ],
|
||||
// unmountComponent()
|
||||
// state is available within `componentWillUnmount()`
|
||||
[ 'componentWillUnmount', 'blue', null ]
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var React = require("../../React");
|
||||
var ReactDOM = require("../../ReactDOM");
|
||||
var ReactTestUtils = require("../../ReactTestUtils");
|
||||
var React = require("../../React");
|
||||
|
||||
describe('ref swapping', function() {
|
||||
// TODO: uncomment this test once we can run in phantom, which
|
||||
// supports real submit events.
|
||||
/*
|
||||
it('should bubble onSubmit', function() {
|
||||
var count = 0;
|
||||
var form;
|
||||
var Parent = React.createClass({
|
||||
handleSubmit: function() {
|
||||
count++;
|
||||
return false;
|
||||
},
|
||||
render: function() {
|
||||
return <Child />;
|
||||
}
|
||||
});
|
||||
var Child = React.createClass({
|
||||
render: function() {
|
||||
return <form><input type="submit" value="Submit" /></form>;
|
||||
},
|
||||
componentDidMount: function(node) {
|
||||
form = node;
|
||||
}
|
||||
});
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Parent />);
|
||||
form.submit();
|
||||
expect(count).toEqual(1);
|
||||
});
|
||||
*/
|
||||
|
||||
it("should allow children to be passed as an argument", function() {
|
||||
var argDiv = ReactTestUtils.renderIntoDocument(
|
||||
ReactDOM.div(null, 'child')
|
||||
);
|
||||
var argNode = document.getElementById(argDiv._rootNodeID);
|
||||
expect(argNode.innerHTML).toBe('child');
|
||||
});
|
||||
|
||||
it("should overwrite props.children with children argument", function() {
|
||||
var conflictDiv = ReactTestUtils.renderIntoDocument(
|
||||
ReactDOM.div({children: 'fakechild'}, 'child')
|
||||
);
|
||||
var conflictNode = document.getElementById(conflictDiv._rootNodeID);
|
||||
expect(conflictNode.innerHTML).toBe('child');
|
||||
});
|
||||
|
||||
/**
|
||||
* We need to make sure that updates occur to the actual node that's in the
|
||||
* DOM, instead of a stale cache.
|
||||
*/
|
||||
it("should purge the DOM cache when removing nodes", function() {
|
||||
var myDiv = ReactTestUtils.renderIntoDocument(
|
||||
React.DOM.div(null, {
|
||||
theDog: React.DOM.div( {className:"dog"}, null ),
|
||||
theBird: React.DOM.div( {className:"bird"}, null )
|
||||
})
|
||||
);
|
||||
myDiv.setProps({
|
||||
children: {
|
||||
theDog: React.DOM.div( {className:"dogbeforedelete"}, null ), // Warm the cache with theDog
|
||||
theBird: React.DOM.div( {className:"bird"}, null )
|
||||
}
|
||||
});
|
||||
myDiv.setProps({
|
||||
children: { // This better purge that cache
|
||||
theBird: React.DOM.div( {className:"bird"}, null )
|
||||
}
|
||||
});
|
||||
// Now, put the dog back.
|
||||
myDiv.setProps({
|
||||
children: {
|
||||
theDog: React.DOM.div( {className:"dog"}, null ), // This is a different node than before
|
||||
theBird: React.DOM.div( {className:"bird"}, null )
|
||||
}
|
||||
});
|
||||
myDiv.setProps({
|
||||
children: { // className changed to bigdog.
|
||||
theDog: React.DOM.div( {className:"bigdog"}, null ), // but will it use the proper element
|
||||
theBird: React.DOM.div( {className:"bird"}, null )
|
||||
}
|
||||
});
|
||||
var root = document.getElementById(myDiv._rootNodeID);
|
||||
var dog = root.childNodes[0];
|
||||
expect(dog.className).toBe('bigdog');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
/*jslint evil: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
describe('ReactDOMIDOperations', function() {
|
||||
var DOMPropertyOperations = require("../../DOMPropertyOperations");
|
||||
var ReactDOMIDOperations = require("../../ReactDOMIDOperations");
|
||||
var ReactDOMNodeCache = require("../../ReactDOMNodeCache");
|
||||
var keyOf = require("../../keyOf");
|
||||
|
||||
it('should disallow updating special properties', function() {
|
||||
spyOn(ReactDOMNodeCache, "getCachedNodeByID");
|
||||
spyOn(DOMPropertyOperations, "setValueForProperty");
|
||||
|
||||
expect(function() {
|
||||
ReactDOMIDOperations.updatePropertyByID(
|
||||
'testID',
|
||||
keyOf({content: null}),
|
||||
'testContent'
|
||||
);
|
||||
}).toThrow();
|
||||
|
||||
expect(
|
||||
ReactDOMNodeCache.getCachedNodeByID.argsForCall[0][0]
|
||||
).toBe('testID');
|
||||
|
||||
expect(
|
||||
DOMPropertyOperations.setValueForProperty.callCount
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('should update innerHTML and special-case whitespace', function() {
|
||||
var stubNode = document.createElement('div');
|
||||
spyOn(ReactDOMNodeCache, "getCachedNodeByID").andReturn(stubNode);
|
||||
|
||||
ReactDOMIDOperations.updateInnerHTMLByID(
|
||||
'testID',
|
||||
{__html: ' testContent'}
|
||||
);
|
||||
|
||||
expect(
|
||||
ReactDOMNodeCache.getCachedNodeByID.argsForCall[0][0]
|
||||
).toBe('testID');
|
||||
|
||||
expect(stubNode.innerHTML).toBe(' testContent');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
require("../../mock-modules")
|
||||
.dontMock('BrowserScroll')
|
||||
.dontMock('CallbackRegistry')
|
||||
.dontMock('EventPluginHub')
|
||||
.dontMock('ReactEvent')
|
||||
.dontMock('ReactInstanceHandles')
|
||||
.dontMock('EventPluginHub')
|
||||
.dontMock('TapEventPlugin')
|
||||
.dontMock('TouchEventUtils')
|
||||
.dontMock('keyOf');
|
||||
|
||||
|
||||
var keyOf = require("../../keyOf");
|
||||
var mocks = require("../../mocks");
|
||||
|
||||
var EventPluginHub;
|
||||
var ReactEvent;
|
||||
var ReactEventTopLevelCallback;
|
||||
var ReactTestUtils;
|
||||
var TapEventPlugin;
|
||||
|
||||
var tapMoveThreshold;
|
||||
var idCallOrder = [];
|
||||
var recordID = function(id) {
|
||||
idCallOrder.push(id);
|
||||
};
|
||||
var recordIDAndStopPropagation = function(id, event) {
|
||||
recordID(id);
|
||||
event.stopPropagation();
|
||||
};
|
||||
var recordIDAndReturnFalse = function(id, event) {
|
||||
recordID(id);
|
||||
return false;
|
||||
};
|
||||
var LISTENER = mocks.getMockFunction();
|
||||
var ON_CLICK_KEY = keyOf({onClick: null});
|
||||
var ON_TOUCH_TAP_KEY = keyOf({onTouchTap: null});
|
||||
|
||||
|
||||
/**
|
||||
* Since `ReactEvent` is fairly well separated from the DOM, we can test almost
|
||||
* all of `ReactEvent` without ever rendering anything in the DOM. As long as we
|
||||
* provide IDs that follow `React's` conventional id namespace hierarchy.
|
||||
* The only reason why we create these DOM nodes, is so that when we feed them
|
||||
* into `ReactEvent` (through `ReactTestUtils`), the event handlers may receive
|
||||
* a DOM node to inspect.
|
||||
*/
|
||||
var CHILD = document.createElement('div');
|
||||
var PARENT = document.createElement('div');
|
||||
var GRANDPARENT = document.createElement('div');
|
||||
CHILD.id = '.reactRoot.[0].[0].[0]';
|
||||
PARENT.id = '.reactRoot.[0].[0]';
|
||||
GRANDPARENT.id = '.reactRoot.[0]';
|
||||
|
||||
function registerSimpleTestHandler() {
|
||||
ReactEvent.putListener(CHILD.id, ON_CLICK_KEY, LISTENER);
|
||||
var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY);
|
||||
expect(listener).toEqual(LISTENER);
|
||||
return ReactEvent.getListener(CHILD.id, ON_CLICK_KEY);
|
||||
}
|
||||
|
||||
|
||||
describe('ReactEvent', function() {
|
||||
beforeEach(function() {
|
||||
require("../../mock-modules").dumpCache();
|
||||
EventPluginHub = require("../../EventPluginHub");
|
||||
TapEventPlugin = require("../../TapEventPlugin");
|
||||
ReactEvent = require("../../ReactEvent");
|
||||
ReactTestUtils = require("../../ReactTestUtils");
|
||||
ReactEventTopLevelCallback = require("../../ReactEventTopLevelCallback");
|
||||
idCallOrder = [];
|
||||
tapMoveThreshold = TapEventPlugin.tapMoveThreshold;
|
||||
ReactEvent.ensureListening(false, ReactEventTopLevelCallback);
|
||||
EventPluginHub.injection.injectEventPluginsByName({
|
||||
TapEventPlugin: TapEventPlugin
|
||||
});
|
||||
});
|
||||
|
||||
it('should store a listener correctly', function() {
|
||||
registerSimpleTestHandler();
|
||||
var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY);
|
||||
expect(listener).toBe(LISTENER);
|
||||
});
|
||||
|
||||
it('should retrieve a listener correctly', function() {
|
||||
registerSimpleTestHandler();
|
||||
var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY);
|
||||
expect(listener).toEqual(LISTENER);
|
||||
});
|
||||
|
||||
it('should clear all handlers when asked to', function() {
|
||||
registerSimpleTestHandler();
|
||||
ReactEvent.deleteAllListeners(CHILD.id);
|
||||
var listener = ReactEvent.getListener(CHILD.id, ON_CLICK_KEY);
|
||||
expect(listener).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should invoke a simple handler registered on a node', function() {
|
||||
registerSimpleTestHandler();
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(LISTENER.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should not invoke handlers when ReactEvent is disabled', function() {
|
||||
registerSimpleTestHandler();
|
||||
ReactEvent.setEnabled(false);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(LISTENER.mock.calls.length).toBe(0);
|
||||
ReactEvent.setEnabled(true);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(LISTENER.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should bubble simply', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
PARENT.id,
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, PARENT.id)
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
GRANDPARENT.id,
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, GRANDPARENT.id)
|
||||
);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(idCallOrder.length).toBe(3);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
expect(idCallOrder[1]).toBe(PARENT.id);
|
||||
expect(idCallOrder[2]).toBe(GRANDPARENT.id);
|
||||
});
|
||||
|
||||
it('should support stopPropagation()', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
PARENT.id,
|
||||
ON_CLICK_KEY,
|
||||
recordIDAndStopPropagation.bind(null, PARENT.id)
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
GRANDPARENT.id,
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, GRANDPARENT.id)
|
||||
);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(idCallOrder.length).toBe(2);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
expect(idCallOrder[1]).toBe(PARENT.id);
|
||||
});
|
||||
|
||||
it('should stop after first dispatch if stopPropagation', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ON_CLICK_KEY,
|
||||
recordIDAndStopPropagation.bind(null, CHILD.id)
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
PARENT.id,
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, PARENT.id)
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
GRANDPARENT.id,
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, GRANDPARENT.id)
|
||||
);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(idCallOrder.length).toBe(1);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
});
|
||||
|
||||
it('should stopPropagation if false is returned', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ON_CLICK_KEY,
|
||||
recordIDAndReturnFalse.bind(null, CHILD.id)
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
PARENT.id,
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, PARENT.id)
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
GRANDPARENT.id,
|
||||
ON_CLICK_KEY,
|
||||
recordID.bind(null, GRANDPARENT.id)
|
||||
);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(idCallOrder.length).toBe(1);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
});
|
||||
|
||||
/**
|
||||
* The entire event registration state of the world should be "locked-in" at
|
||||
* the time the event occurs. This is to resolve many edge cases that come
|
||||
* about from a listener on a lower-in-DOM node causing structural changes at
|
||||
* places higher in the DOM. If this lower-in-DOM node causes new content to
|
||||
* be rendered at a place higher-in-DOM, we need to be careful not to invoke
|
||||
* these new listeners.
|
||||
*/
|
||||
|
||||
it('should invoke handlers that were removed while bubbling', function() {
|
||||
var handleParentClick = mocks.getMockFunction();
|
||||
var handleChildClick = function(abstractEvent) {
|
||||
ReactEvent.deleteAllListeners(PARENT.id);
|
||||
};
|
||||
ReactEvent.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick);
|
||||
ReactEvent.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(handleParentClick.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should not invoke newly inserted handlers while bubbling', function() {
|
||||
var handleParentClick = mocks.getMockFunction();
|
||||
var handleChildClick = function(abstractEvent) {
|
||||
ReactEvent.putListener(PARENT.id, ON_CLICK_KEY, handleParentClick);
|
||||
};
|
||||
ReactEvent.putListener(CHILD.id, ON_CLICK_KEY, handleChildClick);
|
||||
ReactTestUtils.Simulate.click(CHILD);
|
||||
expect(handleParentClick.mock.calls.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should infer onTouchTap from a touchStart/End', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
);
|
||||
ReactTestUtils.Simulate.touchStart(
|
||||
CHILD,
|
||||
ReactTestUtils.nativeTouchData(0, 0)
|
||||
);
|
||||
ReactTestUtils.Simulate.touchEnd(
|
||||
CHILD,
|
||||
ReactTestUtils.nativeTouchData(0, 0)
|
||||
);
|
||||
expect(idCallOrder.length).toBe(1);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
});
|
||||
|
||||
it('should infer onTouchTap from when dragging below threshold', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
);
|
||||
ReactTestUtils.Simulate.touchStart(
|
||||
CHILD,
|
||||
ReactTestUtils.nativeTouchData(0, 0)
|
||||
);
|
||||
ReactTestUtils.Simulate.touchEnd(
|
||||
CHILD,
|
||||
ReactTestUtils.nativeTouchData(0, tapMoveThreshold - 1)
|
||||
);
|
||||
expect(idCallOrder.length).toBe(1);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
});
|
||||
|
||||
it('should not onTouchTap from when dragging beyond threshold', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
);
|
||||
ReactTestUtils.Simulate.touchStart(
|
||||
CHILD,
|
||||
ReactTestUtils.nativeTouchData(0, 0)
|
||||
);
|
||||
ReactTestUtils.Simulate.touchEnd(
|
||||
CHILD,
|
||||
ReactTestUtils.nativeTouchData(0, tapMoveThreshold + 1)
|
||||
);
|
||||
expect(idCallOrder.length).toBe(0);
|
||||
});
|
||||
|
||||
|
||||
it('should bubble onTouchTap', function() {
|
||||
ReactEvent.putListener(
|
||||
CHILD.id,
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, CHILD.id)
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
PARENT.id,
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, PARENT.id)
|
||||
);
|
||||
ReactEvent.putListener(
|
||||
GRANDPARENT.id,
|
||||
ON_TOUCH_TAP_KEY,
|
||||
recordID.bind(null, GRANDPARENT.id)
|
||||
);
|
||||
ReactTestUtils.Simulate.touchStart(
|
||||
CHILD,
|
||||
ReactTestUtils.nativeTouchData(0, 0)
|
||||
);
|
||||
ReactTestUtils.Simulate.touchEnd(
|
||||
CHILD,
|
||||
ReactTestUtils.nativeTouchData(0, 0)
|
||||
);
|
||||
expect(idCallOrder.length).toBe(3);
|
||||
expect(idCallOrder[0]).toBe(CHILD.id);
|
||||
expect(idCallOrder[1]).toBe(PARENT.id);
|
||||
expect(idCallOrder[2]).toBe(GRANDPARENT.id);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("nonexistent module required: core/__tests__/ReactIdentity-test");
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("nonexistent module required: core/__tests__/ReactMount-test");
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactMount = require("../../ReactMount");
|
||||
var React = require("../../React");
|
||||
var ReactTestUtils = require("../../ReactTestUtils");
|
||||
var ReactMount = require("../../ReactMount");
|
||||
|
||||
var reactComponentExpect = require("../../reactComponentExpect");
|
||||
|
||||
describe('ReactMount', function() {
|
||||
it("should destroy a react root upon request", function() {
|
||||
var mainContainerDiv = document.createElement('div');
|
||||
document.documentElement.appendChild(mainContainerDiv);
|
||||
|
||||
var instanceOne = (
|
||||
React.DOM.div( {className:"firstReactDiv"}, null
|
||||
)
|
||||
);
|
||||
var firstRootDiv = document.createElement('div');
|
||||
mainContainerDiv.appendChild(firstRootDiv);
|
||||
React.renderComponent(instanceOne, firstRootDiv);
|
||||
|
||||
var instanceTwo = (
|
||||
React.DOM.div( {className:"secondReactDiv"}, null
|
||||
)
|
||||
);
|
||||
var secondRootDiv = document.createElement('div');
|
||||
mainContainerDiv.appendChild(secondRootDiv);
|
||||
React.renderComponent(instanceTwo, secondRootDiv);
|
||||
|
||||
// Test that two react roots are rendered in isolation
|
||||
expect(firstRootDiv.getElementsByClassName('firstReactDiv').length)
|
||||
.toBe(1);
|
||||
expect(secondRootDiv.getElementsByClassName('secondReactDiv').length)
|
||||
.toBe(1);
|
||||
|
||||
// Test that after unmounting each, they are no longer in the document.
|
||||
React.unmountAndReleaseReactRootNode(firstRootDiv);
|
||||
expect(firstRootDiv.getElementsByClassName('firstReactDiv').length)
|
||||
.toBe(0);
|
||||
React.unmountAndReleaseReactRootNode(secondRootDiv);
|
||||
expect(secondRootDiv.getElementsByClassName('secondReactDiv').length)
|
||||
.toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
/*jslint evil: true */
|
||||
|
||||
"use strict";
|
||||
|
||||
describe('ReactNativeComponent', function() {
|
||||
|
||||
describe('updateDOM', function() {
|
||||
var React;
|
||||
var ReactTestUtils;
|
||||
var transaction;
|
||||
|
||||
beforeEach(function() {
|
||||
React = require("../../React");
|
||||
ReactTestUtils = require("../../ReactTestUtils");
|
||||
|
||||
var ReactReconcileTransaction = require("../../ReactReconcileTransaction");
|
||||
transaction = new ReactReconcileTransaction();
|
||||
});
|
||||
|
||||
it("should handle className", function() {
|
||||
var stub = ReactTestUtils.renderIntoDocument(React.DOM.div( {style:{}}, null ));
|
||||
|
||||
stub.receiveProps({ className: 'foo' }, transaction);
|
||||
expect(stub.getDOMNode().className).toEqual('foo');
|
||||
stub.receiveProps({ className: 'bar' }, transaction);
|
||||
expect(stub.getDOMNode().className).toEqual('bar');
|
||||
stub.receiveProps({ className: null }, transaction);
|
||||
expect(stub.getDOMNode().className).toEqual('');
|
||||
});
|
||||
|
||||
it("should gracefully handle various style value types", function() {
|
||||
var stub = ReactTestUtils.renderIntoDocument(React.DOM.div( {style:{}}, null ));
|
||||
var stubStyle = stub.getDOMNode().style;
|
||||
|
||||
// set initial style
|
||||
var setup = { display: 'block', left: '1', top: 2, fontFamily: 'Arial' };
|
||||
stub.receiveProps({ style: setup }, transaction);
|
||||
expect(stubStyle.display).toEqual('block');
|
||||
expect(stubStyle.left).toEqual('1px');
|
||||
expect(stubStyle.fontFamily).toEqual('Arial');
|
||||
|
||||
// reset the style to their default state
|
||||
var reset = { display: '', left: null, top: false, fontFamily: true };
|
||||
stub.receiveProps({ style: reset }, transaction);
|
||||
expect(stubStyle.display).toEqual('');
|
||||
expect(stubStyle.left).toEqual('');
|
||||
expect(stubStyle.top).toEqual('');
|
||||
expect(stubStyle.fontFamily).toEqual('');
|
||||
});
|
||||
|
||||
it("should update styles when mutating style object", function() {
|
||||
var styles = { display: 'none', fontFamily: 'Arial' };
|
||||
var stub = ReactTestUtils.renderIntoDocument(React.DOM.div( {style:styles}, null ));
|
||||
|
||||
var stubStyle = stub.getDOMNode().style;
|
||||
stubStyle.display = styles.display;
|
||||
stubStyle.fontFamily = styles.fontFamily;
|
||||
|
||||
styles.display = 'block';
|
||||
|
||||
stub.receiveProps({ style: styles }, transaction);
|
||||
expect(stubStyle.display).toEqual('block');
|
||||
expect(stubStyle.fontFamily).toEqual('Arial');
|
||||
|
||||
styles.fontFamily = 'Helvetica';
|
||||
|
||||
stub.receiveProps({ style: styles }, transaction);
|
||||
expect(stubStyle.display).toEqual('block');
|
||||
expect(stubStyle.fontFamily).toEqual('Helvetica');
|
||||
});
|
||||
|
||||
it("should update styles if initially null", function() {
|
||||
var styles = null;
|
||||
var stub = ReactTestUtils.renderIntoDocument(React.DOM.div( {style:styles}, null ));
|
||||
|
||||
var stubStyle = stub.getDOMNode().style;
|
||||
|
||||
styles = {display: 'block'};
|
||||
|
||||
stub.receiveProps({ style: styles }, transaction);
|
||||
expect(stubStyle.display).toEqual('block');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('createOpenTagMarkup', function() {
|
||||
var genMarkup;
|
||||
|
||||
function quoteRegexp(str) {
|
||||
return (str+'').replace(/([.?*+\^$\[\]\\(){}|-])/g, "\\$1");
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
require("../../mock-modules").dumpCache();
|
||||
|
||||
var mixInto = require("../../mixInto");
|
||||
var ReactNativeComponent = require("../../ReactNativeComponent");
|
||||
|
||||
var NodeStub = function(initialProps) {
|
||||
this.props = initialProps || {};
|
||||
this._rootNodeID = 'test';
|
||||
};
|
||||
mixInto(NodeStub, ReactNativeComponent.Mixin);
|
||||
|
||||
genMarkup = function(props) {
|
||||
return (new NodeStub(props))._createOpenTagMarkup();
|
||||
};
|
||||
|
||||
this.addMatchers({
|
||||
toHaveAttribute: function(attr, value) {
|
||||
var expected = '(?:^|\\s)' + attr + '=[\\\'"]';
|
||||
if (typeof value != 'undefined') {
|
||||
expected += quoteRegexp(value) + '[\\\'"]';
|
||||
}
|
||||
return this.actual.match(new RegExp(expected));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle className", function() {
|
||||
expect(genMarkup({ className: 'a' })).toHaveAttribute('class', 'a');
|
||||
expect(genMarkup({ className: 'a b' })).toHaveAttribute('class', 'a b');
|
||||
expect(genMarkup({ className: '' })).toHaveAttribute('class', '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createContentMarkup', function() {
|
||||
var genMarkup;
|
||||
|
||||
function quoteRegexp(str) {
|
||||
return (str+'').replace(/([.?*+\^$\[\]\\(){}|-])/g, "\\$1");
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
require("../../mock-modules").dumpCache();
|
||||
|
||||
var mixInto = require("../../mixInto");
|
||||
var ReactNativeComponent = require("../../ReactNativeComponent");
|
||||
var ReactReconcileTransaction = require("../../ReactReconcileTransaction");
|
||||
|
||||
var NodeStub = function(initialProps) {
|
||||
this.props = initialProps || {};
|
||||
this._rootNodeID = 'test';
|
||||
};
|
||||
mixInto(NodeStub, ReactNativeComponent.Mixin);
|
||||
|
||||
genMarkup = function(props) {
|
||||
var transaction = new ReactReconcileTransaction();
|
||||
return (new NodeStub(props))._createContentMarkup(transaction);
|
||||
};
|
||||
|
||||
this.addMatchers({
|
||||
toHaveInnerhtml: function(html) {
|
||||
var expected = '^' + quoteRegexp(html) + '$';
|
||||
return this.actual.match(new RegExp(expected));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle dangerouslySetInnerHTML", function() {
|
||||
var innerHTML = {__html: 'testContent'};
|
||||
expect(
|
||||
genMarkup({ dangerouslySetInnerHTML: innerHTML })
|
||||
).toHaveInnerhtml('testContent');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mountComponent', function() {
|
||||
var mountComponent;
|
||||
|
||||
beforeEach(function() {
|
||||
require("../../mock-modules").dumpCache();
|
||||
|
||||
var mixInto = require("../../mixInto");
|
||||
var ReactComponent = require("../../ReactComponent");
|
||||
var ReactNativeComponent = require("../../ReactNativeComponent");
|
||||
var ReactReconcileTransaction = require("../../ReactReconcileTransaction");
|
||||
|
||||
var NodeStub = function(initialProps) {
|
||||
ReactComponent.Mixin.construct.call(this, initialProps);
|
||||
};
|
||||
mixInto(NodeStub, ReactNativeComponent.Mixin);
|
||||
|
||||
mountComponent = function(props) {
|
||||
var transaction = new ReactReconcileTransaction();
|
||||
return (new NodeStub(props)).mountComponent('test', transaction);
|
||||
};
|
||||
});
|
||||
|
||||
it("should validate against multiple children props", function() {
|
||||
expect(function() {
|
||||
mountComponent({ content: '', children: '' });
|
||||
}).toThrow(
|
||||
'Invariant Violation: Can only set one of `children`, ' +
|
||||
'`props.content`, or `props.dangerouslySetInnerHTML`.'
|
||||
);
|
||||
|
||||
expect(function() {
|
||||
mountComponent({ content: '', dangerouslySetInnerHTML: '' });
|
||||
}).toThrow(
|
||||
'Invariant Violation: Can only set one of `children`, ' +
|
||||
'`props.content`, or `props.dangerouslySetInnerHTML`.'
|
||||
);
|
||||
|
||||
expect(function() {
|
||||
mountComponent({ children: '', dangerouslySetInnerHTML: '' });
|
||||
}).toThrow(
|
||||
'Invariant Violation: Can only set one of `children`, ' +
|
||||
'`props.content`, or `props.dangerouslySetInnerHTML`.'
|
||||
);
|
||||
});
|
||||
|
||||
it("should validate against invalid styles", function() {
|
||||
expect(function() {
|
||||
mountComponent({ style: 'display: none' });
|
||||
}).toThrow(
|
||||
'Invariant Violation: The `style` prop expects a mapping from style ' +
|
||||
'properties to values, not a string.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var Props = require("../../ReactProps");
|
||||
|
||||
function typeCheck(declaration, value) {
|
||||
var props = {};
|
||||
if (arguments.length > 1) {
|
||||
props.testProp = value;
|
||||
}
|
||||
return declaration.bind(this, props, 'testProp', 'testComponent');
|
||||
}
|
||||
|
||||
describe('Primitive Types', function() {
|
||||
beforeEach(function() {
|
||||
require("../../mock-modules").dumpCache();
|
||||
});
|
||||
|
||||
it("should throw for invalid strings", function() {
|
||||
expect(typeCheck(Props.string, [])).toThrow(
|
||||
'Invariant Violation: Invalid prop `testProp` of type `array` ' +
|
||||
'supplied to `testComponent`, expected `string`.'
|
||||
);
|
||||
expect(typeCheck(Props.string, false)).toThrow(
|
||||
'Invariant Violation: Invalid prop `testProp` of type `boolean` ' +
|
||||
'supplied to `testComponent`, expected `string`.'
|
||||
);
|
||||
expect(typeCheck(Props.string, 1)).toThrow(
|
||||
'Invariant Violation: Invalid prop `testProp` of type `number` ' +
|
||||
'supplied to `testComponent`, expected `string`.'
|
||||
);
|
||||
expect(typeCheck(Props.string, {})).toThrow(
|
||||
'Invariant Violation: Invalid prop `testProp` of type `object` ' +
|
||||
'supplied to `testComponent`, expected `string`.'
|
||||
);
|
||||
});
|
||||
|
||||
it("should not throw for valid values", function() {
|
||||
expect(typeCheck(Props.array, [])).not.toThrow();
|
||||
expect(typeCheck(Props.bool, false)).not.toThrow();
|
||||
expect(typeCheck(Props.number, 0)).not.toThrow();
|
||||
expect(typeCheck(Props.object, {})).not.toThrow();
|
||||
expect(typeCheck(Props.string, '')).not.toThrow();
|
||||
});
|
||||
|
||||
it("should be implicitly optional and not throw without values", function() {
|
||||
expect(typeCheck(Props.string, null)).not.toThrow();
|
||||
expect(typeCheck(Props.string, undefined)).not.toThrow();
|
||||
});
|
||||
|
||||
it("should throw for missing required values", function() {
|
||||
expect(typeCheck(Props.string.isRequired, null)).toThrow(
|
||||
'Invariant Violation: Required prop `testProp` was not specified in ' +
|
||||
'`testComponent`.'
|
||||
);
|
||||
expect(typeCheck(Props.string.isRequired, undefined)).toThrow(
|
||||
'Invariant Violation: Required prop `testProp` was not specified in ' +
|
||||
'`testComponent`.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enum Types', function() {
|
||||
beforeEach(function() {
|
||||
require("../../mock-modules").dumpCache();
|
||||
});
|
||||
|
||||
it("should throw for invalid strings", function() {
|
||||
expect(typeCheck(Props.oneOf(['red', 'blue']), true)).toThrow(
|
||||
'Invariant Violation: Invalid prop `testProp` supplied to ' +
|
||||
'`testComponent`, expected one of ["blue","red"].'
|
||||
);
|
||||
expect(typeCheck(Props.oneOf(['red', 'blue']), [])).toThrow(
|
||||
'Invariant Violation: Invalid prop `testProp` supplied to ' +
|
||||
'`testComponent`, expected one of ["blue","red"].'
|
||||
);
|
||||
expect(typeCheck(Props.oneOf(['red', 'blue']), '')).toThrow(
|
||||
'Invariant Violation: Invalid prop `testProp` supplied to ' +
|
||||
'`testComponent`, expected one of ["blue","red"].'
|
||||
);
|
||||
});
|
||||
|
||||
it("should not throw for valid values", function() {
|
||||
expect(typeCheck(Props.oneOf(['red', 'blue']), 'red')).not.toThrow();
|
||||
expect(typeCheck(Props.oneOf(['red', 'blue']), 'blue')).not.toThrow();
|
||||
});
|
||||
|
||||
it("should be implicitly optional and not throw without values", function() {
|
||||
expect(typeCheck(Props.oneOf(['red', 'blue']), null)).not.toThrow();
|
||||
expect(typeCheck(Props.oneOf(['red', 'blue']), undefined)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Instance Types', function() {
|
||||
beforeEach(function() {
|
||||
require("../../mock-modules").dumpCache();
|
||||
});
|
||||
|
||||
it("should throw for invalid instances", function() {
|
||||
function Person() {}
|
||||
|
||||
expect(typeCheck(Props.instanceOf(Person), false)).toThrow(
|
||||
'Invariant Violation: Invalid prop `testProp` supplied to ' +
|
||||
'`testComponent`, expected instance of `Person`.'
|
||||
);
|
||||
expect(typeCheck(Props.instanceOf(Person), {})).toThrow(
|
||||
'Invariant Violation: Invalid prop `testProp` supplied to ' +
|
||||
'`testComponent`, expected instance of `Person`.'
|
||||
);
|
||||
expect(typeCheck(Props.instanceOf(Person), '')).toThrow(
|
||||
'Invariant Violation: Invalid prop `testProp` supplied to ' +
|
||||
'`testComponent`, expected instance of `Person`.'
|
||||
);
|
||||
});
|
||||
|
||||
it("should not throw for valid values", function() {
|
||||
function Person() {}
|
||||
function Engineer() {}
|
||||
Engineer.prototype = new Person();
|
||||
|
||||
expect(typeCheck(Props.instanceOf(Person), new Person())).not.toThrow();
|
||||
expect(typeCheck(Props.instanceOf(Person), new Engineer())).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var React = require("../../React");
|
||||
var ReactStateSetters = require("../../ReactStateSetters");
|
||||
var ReactTestUtils = require("../../ReactTestUtils");
|
||||
|
||||
var TestComponent;
|
||||
var TestComponentWithMixin;
|
||||
|
||||
describe('ReactStateSetters', function() {
|
||||
beforeEach(function() {
|
||||
require("../../mock-modules").dumpCache();
|
||||
|
||||
TestComponent = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {foo: 'foo'};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return React.DOM.div(null, null );
|
||||
}
|
||||
});
|
||||
|
||||
TestComponentWithMixin = React.createClass({
|
||||
mixins: [ReactStateSetters.Mixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return {foo: 'foo'};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return React.DOM.div(null, null );
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('createStateSetter should update state', function() {
|
||||
var instance = TestComponent(null, null );
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
expect(instance.state).toEqual({foo: 'foo'});
|
||||
|
||||
var setter = ReactStateSetters.createStateSetter(
|
||||
instance,
|
||||
function(a, b, c) {
|
||||
return {
|
||||
foo: a + b + c,
|
||||
bar: a * b * c
|
||||
};
|
||||
}
|
||||
);
|
||||
expect(instance.state).toEqual({foo: 'foo'});
|
||||
|
||||
setter(1, 2, 3);
|
||||
expect(instance.state).toEqual({foo: 6, bar: 6});
|
||||
|
||||
setter(10, 11, 12);
|
||||
expect(instance.state).toEqual({foo: 33, bar: 1320});
|
||||
});
|
||||
|
||||
it('createStateKeySetter should update state', function() {
|
||||
var instance = TestComponent(null, null );
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
expect(instance.state).toEqual({foo: 'foo'});
|
||||
|
||||
var setter = ReactStateSetters.createStateKeySetter(instance, 'foo');
|
||||
|
||||
expect(instance.state).toEqual({foo: 'foo'});
|
||||
|
||||
setter('bar');
|
||||
expect(instance.state).toEqual({foo: 'bar'});
|
||||
|
||||
setter('baz');
|
||||
expect(instance.state).toEqual({foo: 'baz'});
|
||||
});
|
||||
|
||||
it('createStateKeySetter is memoized', function() {
|
||||
var instance = TestComponent(null, null );
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
expect(instance.state).toEqual({foo: 'foo'});
|
||||
|
||||
var foo1 = ReactStateSetters.createStateKeySetter(instance, 'foo');
|
||||
var bar1 = ReactStateSetters.createStateKeySetter(instance, 'bar');
|
||||
|
||||
var foo2 = ReactStateSetters.createStateKeySetter(instance, 'foo');
|
||||
var bar2 = ReactStateSetters.createStateKeySetter(instance, 'bar');
|
||||
|
||||
expect(foo2).toBe(foo1);
|
||||
expect(bar2).toBe(bar1);
|
||||
});
|
||||
|
||||
it('createStateSetter should update state from mixin', function() {
|
||||
var instance = TestComponentWithMixin(null, null );
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
expect(instance.state).toEqual({foo: 'foo'});
|
||||
|
||||
var setter = instance.createStateSetter(
|
||||
function(a, b, c) {
|
||||
return {
|
||||
foo: a + b + c,
|
||||
bar: a * b * c
|
||||
};
|
||||
}
|
||||
);
|
||||
expect(instance.state).toEqual({foo: 'foo'});
|
||||
|
||||
setter(1, 2, 3);
|
||||
expect(instance.state).toEqual({foo: 6, bar: 6});
|
||||
|
||||
setter(10, 11, 12);
|
||||
expect(instance.state).toEqual({foo: 33, bar: 1320});
|
||||
});
|
||||
|
||||
it('createStateKeySetter should update state with mixin', function() {
|
||||
var instance = TestComponentWithMixin(null, null );
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
expect(instance.state).toEqual({foo: 'foo'});
|
||||
|
||||
var setter = instance.createStateKeySetter('foo');
|
||||
|
||||
expect(instance.state).toEqual({foo: 'foo'});
|
||||
|
||||
setter('bar');
|
||||
expect(instance.state).toEqual({foo: 'bar'});
|
||||
|
||||
setter('baz');
|
||||
expect(instance.state).toEqual({foo: 'baz'});
|
||||
});
|
||||
|
||||
it('createStateKeySetter is memoized with mixin', function() {
|
||||
var instance = TestComponentWithMixin(null, null );
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
expect(instance.state).toEqual({foo: 'foo'});
|
||||
|
||||
var foo1 = instance.createStateKeySetter('foo');
|
||||
var bar1 = instance.createStateKeySetter('bar');
|
||||
|
||||
var foo2 = instance.createStateKeySetter('foo');
|
||||
var bar2 = instance.createStateKeySetter('bar');
|
||||
|
||||
expect(foo2).toBe(foo1);
|
||||
expect(bar2).toBe(bar1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 createObjectFrom
|
||||
*/
|
||||
|
||||
var hasArrayNature = require("./hasArrayNature");
|
||||
|
||||
/**
|
||||
* Construct an object from an array of keys
|
||||
* and optionally specified value or list of values.
|
||||
*
|
||||
* >>> createObjectFrom(['a','b','c']);
|
||||
* {a: true, b: true, c: true}
|
||||
*
|
||||
* >>> createObjectFrom(['a','b','c'], false);
|
||||
* {a: false, b: false, c: false}
|
||||
*
|
||||
* >>> createObjectFrom(['a','b','c'], 'monkey');
|
||||
* {c:'monkey', b:'monkey' c:'monkey'}
|
||||
*
|
||||
* >>> createObjectFrom(['a','b','c'], [1,2,3]);
|
||||
* {a: 1, b: 2, c: 3}
|
||||
*
|
||||
* >>> createObjectFrom(['women', 'men'], [true, false]);
|
||||
* {women: true, men: false}
|
||||
*
|
||||
* @param Array list of keys
|
||||
* @param mixed optional value or value array. defaults true.
|
||||
* @returns object
|
||||
*/
|
||||
function createObjectFrom(keys, values /* = true */) {
|
||||
if (true) {
|
||||
if (!hasArrayNature(keys)) {
|
||||
throw new TypeError('Must pass an array of keys.');
|
||||
}
|
||||
}
|
||||
|
||||
var object = {};
|
||||
var is_array = hasArrayNature(values);
|
||||
if (typeof values == 'undefined') {
|
||||
values = true;
|
||||
}
|
||||
|
||||
for (var ii = keys.length; ii--;) {
|
||||
object[keys[ii]] = is_array ? values[ii] : values;
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
module.exports = createObjectFrom;
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 cx
|
||||
*/
|
||||
|
||||
/**
|
||||
* This function is used to mark string literals representing CSS class names
|
||||
* so that they can be transformed statically. This allows for modularization
|
||||
* and minification of CSS class names.
|
||||
*
|
||||
* In static_upstream, this function is actually implemented, but it should
|
||||
* eventually be replaced with something more descriptive, and the transform
|
||||
* that is used in the main stack should be ported for use elsewhere.
|
||||
*
|
||||
* @param string|object className to modularize, or an object of key/values.
|
||||
* In the object case, the values are conditions that
|
||||
* determine if the className keys should be included.
|
||||
* @param [string ...] Variable list of classNames in the string case.
|
||||
* @return string Renderable space-separated CSS className.
|
||||
*/
|
||||
function cx(classNames) {
|
||||
if (typeof classNames == 'object') {
|
||||
return Object.keys(classNames).map(function(className) {
|
||||
return classNames[className] ? className : '';
|
||||
}).join(' ');
|
||||
} else {
|
||||
return Array.prototype.join.call(arguments, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = cx;
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 dangerousStyleValue
|
||||
* @typechecks
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var CSSProperty = require("./CSSProperty");
|
||||
|
||||
/**
|
||||
* Convert a value into the proper css writable value. The `styleName` name
|
||||
* name should be logical (no hyphens), as specified in `CSSProperty.isNumber`.
|
||||
*
|
||||
* @param {string} styleName CSS property name such as `topMargin`.
|
||||
* @param {*} value CSS property value such as `10px`.
|
||||
* @return {string} Normalized style value with dimensions applied.
|
||||
*/
|
||||
function dangerousStyleValue(styleName, value) {
|
||||
// Note that we've removed escapeTextForBrowser() calls here since the
|
||||
// whole string will be escaped when the attribute is injected into
|
||||
// the markup. If you provide unsafe user data here they can inject
|
||||
// arbitrary CSS which may be problematic (I couldn't repro this):
|
||||
// https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
|
||||
// http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
|
||||
// This is not an XSS hole but instead a potential CSS injection issue
|
||||
// which has lead to a greater discussion about how we're going to
|
||||
// trust URLs moving forward. See #2115901
|
||||
if (value === null || value === false || value === true || value === '') {
|
||||
return '';
|
||||
}
|
||||
if (isNaN(value)) {
|
||||
return !value ? '' : '' + value;
|
||||
}
|
||||
return CSSProperty.isNumber[styleName] ? '' + value : (value + 'px');
|
||||
}
|
||||
|
||||
module.exports = dangerousStyleValue;
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("nonexistent module required: dom/__tests__/CSSPropertyOperations-test");
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("nonexistent module required: dom/__tests__/DOMPropertyOperations-test");
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
describe('CSSPropertyOperations', function() {
|
||||
var CSSPropertyOperations;
|
||||
|
||||
beforeEach(function() {
|
||||
require("../../mock-modules").dumpCache();
|
||||
CSSPropertyOperations = require("../../CSSPropertyOperations");
|
||||
});
|
||||
|
||||
it('should create markup for simple styles', function() {
|
||||
expect(CSSPropertyOperations.createMarkupForStyles({
|
||||
backgroundColor: '#3b5998',
|
||||
display: 'none'
|
||||
})).toBe('background-color:#3b5998;display:none;');
|
||||
});
|
||||
|
||||
it('should ignore undefined styles', function() {
|
||||
expect(CSSPropertyOperations.createMarkupForStyles({
|
||||
backgroundColor: undefined,
|
||||
display: 'none'
|
||||
})).toBe('display:none;');
|
||||
});
|
||||
|
||||
it('should automatically append `px` to relevant styles', function() {
|
||||
expect(CSSPropertyOperations.createMarkupForStyles({
|
||||
margin: 16,
|
||||
opacity: 0.5,
|
||||
padding: '4px'
|
||||
})).toBe('margin:16px;opacity:0.5;padding:4px;');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
describe('DOMPropertyOperations', function() {
|
||||
var DOMPropertyOperations;
|
||||
|
||||
beforeEach(function() {
|
||||
require("../../mock-modules").dumpCache();
|
||||
DOMPropertyOperations = require("../../DOMPropertyOperations");
|
||||
});
|
||||
|
||||
describe('createMarkupForProperty', function() {
|
||||
|
||||
it('should create markup for simple properties', function() {
|
||||
expect(DOMPropertyOperations.createMarkupForProperty(
|
||||
'name',
|
||||
'simple'
|
||||
)).toBe('name="simple"');
|
||||
|
||||
expect(DOMPropertyOperations.createMarkupForProperty(
|
||||
'name',
|
||||
false
|
||||
)).toBe('name="false"');
|
||||
|
||||
expect(DOMPropertyOperations.createMarkupForProperty(
|
||||
'name',
|
||||
null
|
||||
)).toBe('');
|
||||
});
|
||||
|
||||
it('should create markup for boolean properties', function() {
|
||||
expect(DOMPropertyOperations.createMarkupForProperty(
|
||||
'checked',
|
||||
'simple'
|
||||
)).toBe('checked="simple"');
|
||||
|
||||
expect(DOMPropertyOperations.createMarkupForProperty(
|
||||
'checked',
|
||||
true
|
||||
)).toBe('checked="true"');
|
||||
|
||||
expect(DOMPropertyOperations.createMarkupForProperty(
|
||||
'checked',
|
||||
false
|
||||
)).toBe('');
|
||||
});
|
||||
|
||||
it('should create markup for custom attributes', function() {
|
||||
expect(DOMPropertyOperations.createMarkupForProperty(
|
||||
'aria-label',
|
||||
'simple'
|
||||
)).toBe('aria-label="simple"');
|
||||
|
||||
expect(DOMPropertyOperations.createMarkupForProperty(
|
||||
'aria-label',
|
||||
false
|
||||
)).toBe('aria-label="false"');
|
||||
|
||||
expect(DOMPropertyOperations.createMarkupForProperty(
|
||||
'aria-label',
|
||||
null
|
||||
)).toBe('');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('setValueForProperty', function() {
|
||||
var stubNode;
|
||||
|
||||
beforeEach(function() {
|
||||
stubNode = document.createElement('div');
|
||||
});
|
||||
|
||||
it('should set values as properties by default', function() {
|
||||
DOMPropertyOperations.setValueForProperty(stubNode, 'title', 'Tip!');
|
||||
expect(stubNode.title).toBe('Tip!');
|
||||
});
|
||||
|
||||
it('should set values as attributes if necessary', function() {
|
||||
DOMPropertyOperations.setValueForProperty(stubNode, 'ajaxify', '#');
|
||||
expect(stubNode.getAttribute('ajaxify')).toBe('#');
|
||||
expect(stubNode.ajaxify).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove for falsey boolean properties', function() {
|
||||
DOMPropertyOperations.setValueForProperty(
|
||||
stubNode,
|
||||
'allowFullScreen',
|
||||
false
|
||||
);
|
||||
expect(stubNode.hasAttribute('allowFullScreen')).toBe(false);
|
||||
});
|
||||
|
||||
it('should use mutation method where applicable', function() {
|
||||
DOMPropertyOperations.setValueForProperty(
|
||||
stubNode,
|
||||
'className',
|
||||
'selected'
|
||||
);
|
||||
expect(stubNode.className).toBe('selected');
|
||||
|
||||
DOMPropertyOperations.setValueForProperty(
|
||||
stubNode,
|
||||
'className',
|
||||
null
|
||||
);
|
||||
expect(stubNode.className).toBe('');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 emptyFunction
|
||||
*/
|
||||
|
||||
var copyProperties = require("./copyProperties");
|
||||
|
||||
function makeEmptyFunction(arg) {
|
||||
return function() {
|
||||
return arg;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This function accepts and discards inputs; it has no side effects. This is
|
||||
* primarily useful idiomatically for overridable function endpoints which
|
||||
* always need to be callable, since JS lacks a null-call idiom ala Cocoa.
|
||||
*/
|
||||
function emptyFunction() {}
|
||||
|
||||
copyProperties(emptyFunction, {
|
||||
thatReturns: makeEmptyFunction,
|
||||
thatReturnsFalse: makeEmptyFunction(false),
|
||||
thatReturnsTrue: makeEmptyFunction(true),
|
||||
thatReturnsNull: makeEmptyFunction(null),
|
||||
thatReturnsThis: function() { return this; },
|
||||
thatReturnsArgument: function(arg) { return arg; },
|
||||
mustImplement: function(module, property) {
|
||||
return function() {
|
||||
if (true) {
|
||||
throw new Error(module + '.' + property + ' must be implemented!');
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = emptyFunction;
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("nonexistent module required: environment/__tests__/ReactServerRendering-test");
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 erx
|
||||
* @typechecks
|
||||
* @nostacktrace
|
||||
*/
|
||||
|
||||
var ex = require("./ex");
|
||||
|
||||
/**
|
||||
* This function reverse transform done by `ex()`.
|
||||
*
|
||||
* This function only transforms plain text message back to error message with
|
||||
* arguments. If the error message is minified by the server, it has to be
|
||||
* looked up on the server.
|
||||
*
|
||||
* This function is designed to be idempotent, so erx(erx(whatever)) will be
|
||||
* the same as erx(whatever).
|
||||
*
|
||||
* @param {string|array<string>} transformedMessage
|
||||
*/
|
||||
|
||||
var erx = function(transformedMessage) {
|
||||
if (typeof transformedMessage !== 'string') {
|
||||
// possibly double `erx`ed and return original value
|
||||
return transformedMessage;
|
||||
}
|
||||
|
||||
var prefixLeft = transformedMessage.indexOf(ex._prefix);
|
||||
var suffixLeft = transformedMessage.lastIndexOf(ex._suffix);
|
||||
if (prefixLeft < 0 || suffixLeft < 0) {
|
||||
// plain error message without any transformation
|
||||
return [transformedMessage];
|
||||
}
|
||||
|
||||
var prefixRight = prefixLeft + ex._prefix.length;
|
||||
var suffixRight = suffixLeft + ex._suffix.length;
|
||||
if (prefixRight >= suffixLeft) {
|
||||
return ['erx slice failure: %s', transformedMessage];
|
||||
}
|
||||
|
||||
var leftSlice = transformedMessage.substring(0, prefixLeft);
|
||||
var rightSlice = transformedMessage.substring(suffixRight);
|
||||
transformedMessage = transformedMessage.substring(prefixRight, suffixLeft);
|
||||
|
||||
var messageWithParams;
|
||||
try {
|
||||
messageWithParams = JSON.parse(transformedMessage);
|
||||
messageWithParams[0] =
|
||||
leftSlice + messageWithParams[0] + rightSlice;
|
||||
} catch(err) {
|
||||
return ['erx parse failure: %s', transformedMessage];
|
||||
}
|
||||
|
||||
return messageWithParams;
|
||||
};
|
||||
|
||||
module.exports = erx;
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 escapeTextForBrowser
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var throwIf = require("./throwIf");
|
||||
|
||||
var ESCAPE_TYPE_ERR;
|
||||
|
||||
if (true) {
|
||||
ESCAPE_TYPE_ERR =
|
||||
'The React core has attempted to escape content that is of a ' +
|
||||
'mysterious type (object etc) Escaping only works on numbers and strings';
|
||||
}
|
||||
|
||||
var ESCAPE_LOOKUP = {
|
||||
"&": "&",
|
||||
">": ">",
|
||||
"<": "<",
|
||||
"\"": """,
|
||||
"'": "'",
|
||||
"/": "/"
|
||||
};
|
||||
|
||||
function escaper(match) {
|
||||
return ESCAPE_LOOKUP[match];
|
||||
}
|
||||
|
||||
var escapeTextForBrowser = function (text) {
|
||||
var type = typeof text;
|
||||
var invalid = type === 'object';
|
||||
if (true) {
|
||||
throwIf(invalid, ESCAPE_TYPE_ERR);
|
||||
}
|
||||
if (text === '' || invalid) {
|
||||
return '';
|
||||
} else {
|
||||
if (type === 'string') {
|
||||
return text.replace(/[&><"'\/]/g, escaper);
|
||||
} else {
|
||||
return (''+text).replace(/[&><"'\/]/g, escaper);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = escapeTextForBrowser;
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("nonexistent module required: event/__tests__/EventPluginRegistry-test");
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("nonexistent module required: event/synthetic/__tests__/SyntheticEvent-test");
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("nonexistent module required: event/synthetic/__tests__/SyntheticWheelEvent-test");
|
||||
@@ -0,0 +1,501 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var CallbackRegistry;
|
||||
var EventConstants;
|
||||
var EventPropagators;
|
||||
var ReactInstanceHandles;
|
||||
var ResponderEventPlugin;
|
||||
var AbstractEvent;
|
||||
|
||||
var GRANDPARENT_ID = '.reactRoot[0]';
|
||||
var PARENT_ID = '.reactRoot[0].0';
|
||||
var CHILD_ID = '.reactRoot[0].0.0';
|
||||
|
||||
var topLevelTypes;
|
||||
var responderAbstractEventTypes;
|
||||
var spies;
|
||||
|
||||
var DUMMY_NATIVE_EVENT = {};
|
||||
var DUMMY_RENDERED_TARGET = {};
|
||||
|
||||
var onStartShouldSetResponder = function(id, cb, capture) {
|
||||
var registrationNames = responderAbstractEventTypes
|
||||
.startShouldSetResponder
|
||||
.phasedRegistrationNames;
|
||||
CallbackRegistry.putListener(
|
||||
id,
|
||||
capture ? registrationNames.captured : registrationNames.bubbled,
|
||||
cb
|
||||
);
|
||||
};
|
||||
|
||||
var onScrollShouldSetResponder = function(id, cb, capture) {
|
||||
var registrationNames = responderAbstractEventTypes
|
||||
.scrollShouldSetResponder
|
||||
.phasedRegistrationNames;
|
||||
CallbackRegistry.putListener(
|
||||
id,
|
||||
capture ? registrationNames.captured : registrationNames.bubbled,
|
||||
cb
|
||||
);
|
||||
};
|
||||
|
||||
var onMoveShouldSetResponder = function(id, cb, capture) {
|
||||
var registrationNames = responderAbstractEventTypes
|
||||
.moveShouldSetResponder
|
||||
.phasedRegistrationNames;
|
||||
CallbackRegistry.putListener(
|
||||
id,
|
||||
capture ? registrationNames.captured : registrationNames.bubbled,
|
||||
cb
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
var onResponderGrant = function(id, cb) {
|
||||
CallbackRegistry.putListener(
|
||||
id,
|
||||
responderAbstractEventTypes.responderGrant.registrationName,
|
||||
cb
|
||||
);
|
||||
};
|
||||
|
||||
var extractForTouchStart = function(renderedTargetID) {
|
||||
return ResponderEventPlugin.extractAbstractEvents(
|
||||
topLevelTypes.topTouchStart,
|
||||
DUMMY_NATIVE_EVENT,
|
||||
renderedTargetID,
|
||||
DUMMY_RENDERED_TARGET
|
||||
);
|
||||
};
|
||||
|
||||
var extractForTouchMove = function(renderedTargetID) {
|
||||
return ResponderEventPlugin.extractAbstractEvents(
|
||||
topLevelTypes.topTouchMove,
|
||||
DUMMY_NATIVE_EVENT,
|
||||
renderedTargetID,
|
||||
DUMMY_RENDERED_TARGET
|
||||
);
|
||||
};
|
||||
|
||||
var extractForTouchEnd = function(renderedTargetID) {
|
||||
return ResponderEventPlugin.extractAbstractEvents(
|
||||
topLevelTypes.topTouchEnd,
|
||||
DUMMY_NATIVE_EVENT,
|
||||
renderedTargetID,
|
||||
DUMMY_RENDERED_TARGET
|
||||
);
|
||||
};
|
||||
|
||||
var extractForMouseDown = function(renderedTargetID) {
|
||||
return ResponderEventPlugin.extractAbstractEvents(
|
||||
topLevelTypes.topMouseDown,
|
||||
DUMMY_NATIVE_EVENT,
|
||||
renderedTargetID,
|
||||
DUMMY_RENDERED_TARGET
|
||||
);
|
||||
};
|
||||
|
||||
var extractForMouseMove = function(renderedTargetID) {
|
||||
return ResponderEventPlugin.extractAbstractEvents(
|
||||
topLevelTypes.topMouseMove,
|
||||
DUMMY_NATIVE_EVENT,
|
||||
renderedTargetID,
|
||||
DUMMY_RENDERED_TARGET
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
var extractForMouseUp = function(renderedTargetID) {
|
||||
return ResponderEventPlugin.extractAbstractEvents(
|
||||
topLevelTypes.topMouseUp,
|
||||
DUMMY_NATIVE_EVENT,
|
||||
renderedTargetID,
|
||||
DUMMY_RENDERED_TARGET
|
||||
);
|
||||
};
|
||||
|
||||
var extractForScroll = function(renderedTargetID) {
|
||||
return ResponderEventPlugin.extractAbstractEvents(
|
||||
topLevelTypes.topScroll,
|
||||
DUMMY_NATIVE_EVENT,
|
||||
renderedTargetID,
|
||||
DUMMY_RENDERED_TARGET
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
var onGrantChild;
|
||||
var onGrantParent;
|
||||
var onGrantGrandParent;
|
||||
|
||||
|
||||
var existsInExtraction = function(extracted, test) {
|
||||
if (Array.isArray(extracted)) {
|
||||
for (var i = 0; i < extracted.length; i++) {
|
||||
if (test(extracted[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (extracted) {
|
||||
return test(extracted);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper validators.
|
||||
*/
|
||||
function assertGrantEvent(id, extracted) {
|
||||
var test = function(abstractEvent) {
|
||||
return abstractEvent instanceof AbstractEvent &&
|
||||
abstractEvent.type === responderAbstractEventTypes.responderGrant &&
|
||||
abstractEvent.abstractTargetID === id;
|
||||
};
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(id);
|
||||
expect(existsInExtraction(extracted, test)).toBe(true);
|
||||
}
|
||||
|
||||
function assertResponderMoveEvent(id, extracted) {
|
||||
var test = function(abstractEvent) {
|
||||
return abstractEvent instanceof AbstractEvent &&
|
||||
abstractEvent.type === responderAbstractEventTypes.responderMove &&
|
||||
abstractEvent.abstractTargetID === id;
|
||||
};
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(id);
|
||||
expect(existsInExtraction(extracted, test)).toBe(true);
|
||||
}
|
||||
|
||||
function assertTerminateEvent(id, extracted) {
|
||||
var test = function(abstractEvent) {
|
||||
return abstractEvent instanceof AbstractEvent &&
|
||||
abstractEvent.type === responderAbstractEventTypes.responderTerminate &&
|
||||
abstractEvent.abstractTargetID === id;
|
||||
};
|
||||
expect(ResponderEventPlugin.getResponderID()).not.toBe(id);
|
||||
expect(existsInExtraction(extracted, test)).toBe(true);
|
||||
}
|
||||
|
||||
function assertRelease(id, extracted) {
|
||||
var test = function(abstractEvent) {
|
||||
return abstractEvent instanceof AbstractEvent &&
|
||||
abstractEvent.type === responderAbstractEventTypes.responderRelease &&
|
||||
abstractEvent.abstractTargetID === id;
|
||||
};
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(null);
|
||||
expect(existsInExtraction(extracted, test)).toBe(true);
|
||||
}
|
||||
|
||||
|
||||
function assertNothingExtracted(extracted) {
|
||||
expect(Array.isArray(extracted)).toBe(false); // No grant events.
|
||||
expect(Array.isArray(extracted)).toBeFalsy();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* - Test that returning false from `responderTerminationRequest` will never
|
||||
* cause the responder to be lost.
|
||||
* - Automate some of this testing by providing config data - generalize.
|
||||
*/
|
||||
|
||||
describe('ResponderEventPlugin', function() {
|
||||
beforeEach(function() {
|
||||
require("../../mock-modules").dumpCache();
|
||||
|
||||
AbstractEvent = require("../../AbstractEvent");
|
||||
CallbackRegistry = require("../../CallbackRegistry");
|
||||
EventConstants = require("../../EventConstants");
|
||||
EventPropagators = require("../../EventPropagators");
|
||||
ReactInstanceHandles = require("../../ReactInstanceHandles");
|
||||
ResponderEventPlugin = require("../../ResponderEventPlugin");
|
||||
EventPropagators.injection.injectInstanceHandle(ReactInstanceHandles);
|
||||
|
||||
// dumpCache, in open-source tests, only resets existing mocks. It does not
|
||||
// reset module-state though -- so we need to do this explicitly in the test
|
||||
// for now. Once that's no longer the case, we can delete this line.
|
||||
CallbackRegistry.__purge();
|
||||
|
||||
topLevelTypes = EventConstants.topLevelTypes;
|
||||
responderAbstractEventTypes = ResponderEventPlugin.abstractEventTypes;
|
||||
|
||||
spies = {
|
||||
onStartShouldSetResponderChild: function() {},
|
||||
onStartShouldSetResponderParent: function() {},
|
||||
onStartShouldSetResponderParentCapture: function() {},
|
||||
onStartShouldSetResponderGrandParent: function() {},
|
||||
onMoveShouldSetResponderParent: function() {},
|
||||
onScrollShouldSetResponderParent: function() {}
|
||||
};
|
||||
|
||||
onGrantChild = function() {};
|
||||
onGrantParent = function() {};
|
||||
onGrantGrandParent = function() {};
|
||||
});
|
||||
|
||||
it('should not auto-set responder on touch start', function() {
|
||||
// Notice we're not registering the startShould* handler.
|
||||
var extracted = extractForTouchStart(CHILD_ID);
|
||||
assertNothingExtracted(extracted);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(null);
|
||||
});
|
||||
|
||||
it('should not auto-set responder on mouse down', function() {
|
||||
// Notice we're not registering the startShould* handler.
|
||||
var extracted = extractForMouseDown(CHILD_ID);
|
||||
assertNothingExtracted(extracted);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(null);
|
||||
extractForMouseUp(CHILD_ID); // Let up!
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(null);
|
||||
|
||||
// Register `onMoveShould*` handler.
|
||||
spyOn(spies, 'onMoveShouldSetResponderParent').andReturn(true);
|
||||
onMoveShouldSetResponder(PARENT_ID, spies.onMoveShouldSetResponderParent);
|
||||
onResponderGrant(PARENT_ID, onGrantParent);
|
||||
// Move mouse while not pressing down
|
||||
extracted = extractForMouseMove(CHILD_ID);
|
||||
assertNothingExtracted(extracted);
|
||||
// Not going to call `onMoveShould`* if not touching.
|
||||
expect(spies.onMoveShouldSetResponderParent.callCount).toBe(0);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(null);
|
||||
|
||||
// Now try the move extraction again, this time while holding down, and not
|
||||
// letting up.
|
||||
extracted = extractForMouseDown(CHILD_ID);
|
||||
assertNothingExtracted(extracted);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(null);
|
||||
|
||||
// Now moving can set the responder, if pressing down, even if there is no
|
||||
// current responder.
|
||||
extracted = extractForMouseMove(CHILD_ID);
|
||||
expect(spies.onMoveShouldSetResponderParent.callCount).toBe(1);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
|
||||
assertGrantEvent(PARENT_ID, extracted);
|
||||
|
||||
extractForMouseUp(CHILD_ID);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(null);
|
||||
});
|
||||
|
||||
it('should not extract a grant/release event if double start', function() {
|
||||
// Return true - we should become the responder.
|
||||
var extracted;
|
||||
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
onResponderGrant(CHILD_ID, onGrantChild);
|
||||
|
||||
extracted = extractForTouchStart(CHILD_ID);
|
||||
assertGrantEvent(CHILD_ID, extracted);
|
||||
expect(spies.onStartShouldSetResponderChild.callCount).toBe(1);
|
||||
|
||||
// Now we do *not* clear out the touch via a simulated touch end. This mocks
|
||||
// out an environment that likely will never happen, but could in some odd
|
||||
// error state so it's nice to make sure we recover gracefully.
|
||||
// extractForTouchEnd(CHILD_ID); // Clear the responder
|
||||
extracted = extractForTouchStart(CHILD_ID);
|
||||
assertNothingExtracted();
|
||||
expect(spies.onStartShouldSetResponderChild.callCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should bubble/capture responder on start', function() {
|
||||
// Return true - we should become the responder.
|
||||
var extracted;
|
||||
spyOn(spies, 'onStartShouldSetResponderParent').andReturn(true);
|
||||
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
onStartShouldSetResponder(PARENT_ID, spies.onStartShouldSetResponderParent);
|
||||
onResponderGrant(CHILD_ID, onGrantChild);
|
||||
onResponderGrant(PARENT_ID, onGrantParent);
|
||||
|
||||
// Nothing extracted if no responder.
|
||||
extracted = extractForTouchMove(GRANDPARENT_ID);
|
||||
assertNothingExtracted(extracted);
|
||||
|
||||
extracted = extractForTouchStart(CHILD_ID);
|
||||
assertGrantEvent(CHILD_ID, extracted);
|
||||
expect(spies.onStartShouldSetResponderChild.callCount).toBe(1);
|
||||
expect(spies.onStartShouldSetResponderParent.callCount).toBe(0);
|
||||
|
||||
// Even if moving on the grandparent, the child will receive responder moves
|
||||
// (This is even true for mouse interactions - which we should absolutely
|
||||
// test)
|
||||
extracted = extractForTouchMove(GRANDPARENT_ID);
|
||||
assertResponderMoveEvent(CHILD_ID, extracted);
|
||||
extracted = extractForTouchMove(CHILD_ID); // Test move on child node too.
|
||||
assertResponderMoveEvent(CHILD_ID, extracted);
|
||||
|
||||
// Reset the responder - id passed here shouldn't matter:
|
||||
// TODO: Test varying the id here.
|
||||
extracted = extractForTouchEnd(GRANDPARENT_ID); // Clear the responder
|
||||
assertRelease(CHILD_ID, extracted);
|
||||
|
||||
// Now make sure the parent requests responder on capture.
|
||||
spyOn(spies, 'onStartShouldSetResponderParentCapture').andReturn(true);
|
||||
onStartShouldSetResponder(
|
||||
PARENT_ID,
|
||||
spies.onStartShouldSetResponderParent,
|
||||
true // Capture
|
||||
);
|
||||
onResponderGrant(PARENT_ID, onGrantGrandParent);
|
||||
extracted = extractForTouchStart(PARENT_ID);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(PARENT_ID);
|
||||
assertGrantEvent(PARENT_ID, extracted);
|
||||
// Now move on various nodes, ensuring that the responder move is emitted to
|
||||
// the parent node.
|
||||
extracted = extractForTouchMove(GRANDPARENT_ID);
|
||||
assertResponderMoveEvent(PARENT_ID, extracted);
|
||||
extracted = extractForTouchMove(CHILD_ID); // Test move on child node too.
|
||||
assertResponderMoveEvent(PARENT_ID, extracted);
|
||||
|
||||
// Reset the responder - id passed here shouldn't matter:
|
||||
// TODO: Test varying the id here.
|
||||
extracted = extractForTouchEnd(GRANDPARENT_ID); // Clear the responder
|
||||
assertRelease(PARENT_ID, extracted);
|
||||
|
||||
});
|
||||
|
||||
it('should invoke callback to ask if responder is desired', function() {
|
||||
// Return true - we should become the responder.
|
||||
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
|
||||
var extracted = extractForTouchStart(CHILD_ID);
|
||||
assertNothingExtracted(extracted);
|
||||
expect(spies.onStartShouldSetResponderChild.callCount).toBe(1);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(CHILD_ID);
|
||||
extractForTouchEnd(CHILD_ID); // Clear the responder
|
||||
|
||||
// Now try returning false - we should not become the responder.
|
||||
spies.onStartShouldSetResponderChild.andReturn(false);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
extracted = extractForTouchStart(CHILD_ID);
|
||||
assertNothingExtracted(extracted);
|
||||
expect(spies.onStartShouldSetResponderChild.callCount).toBe(2);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(null);
|
||||
extractForTouchEnd(CHILD_ID);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(null); // Still null
|
||||
|
||||
// Same thing as before but return true from "shouldSet".
|
||||
spies.onStartShouldSetResponderChild.andReturn(true);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
onResponderGrant(CHILD_ID, onGrantChild);
|
||||
extracted = extractForTouchStart(CHILD_ID);
|
||||
expect(spies.onStartShouldSetResponderChild.callCount).toBe(3);
|
||||
assertGrantEvent(CHILD_ID, extracted);
|
||||
extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
|
||||
assertRelease(CHILD_ID, extracted);
|
||||
});
|
||||
|
||||
it('should give up responder to parent on move iff allowed', function() {
|
||||
// Return true - we should become the responder.
|
||||
var extracted;
|
||||
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
|
||||
spyOn(spies, 'onMoveShouldSetResponderParent').andReturn(true);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
onMoveShouldSetResponder(PARENT_ID, spies.onMoveShouldSetResponderParent);
|
||||
onResponderGrant(CHILD_ID, onGrantChild);
|
||||
onResponderGrant(PARENT_ID, onGrantParent);
|
||||
|
||||
spies.onStartShouldSetResponderChild.andReturn(true);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
extracted = extractForTouchStart(CHILD_ID);
|
||||
expect(spies.onStartShouldSetResponderChild.callCount).toBe(1);
|
||||
expect(spies.onMoveShouldSetResponderParent.callCount).toBe(0); // none yet
|
||||
assertGrantEvent(CHILD_ID, extracted); // Child is the current responder
|
||||
|
||||
extracted = extractForTouchMove(CHILD_ID);
|
||||
expect(spies.onMoveShouldSetResponderParent.callCount).toBe(1);
|
||||
assertGrantEvent(PARENT_ID, extracted);
|
||||
assertTerminateEvent(CHILD_ID, extracted);
|
||||
|
||||
extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
|
||||
assertRelease(PARENT_ID, extracted);
|
||||
});
|
||||
|
||||
it('should responder move only on direct responder', function() {
|
||||
// Return true - we should become the responder.
|
||||
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
|
||||
var extracted = extractForTouchStart(CHILD_ID);
|
||||
assertNothingExtracted(extracted);
|
||||
expect(spies.onStartShouldSetResponderChild.callCount).toBe(1);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(CHILD_ID);
|
||||
extractForTouchEnd(CHILD_ID); // Clear the responder
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(null);
|
||||
|
||||
// Now try returning false - we should not become the responder.
|
||||
spies.onStartShouldSetResponderChild.andReturn(false);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
extracted = extractForTouchStart(CHILD_ID);
|
||||
assertNothingExtracted(extracted);
|
||||
expect(spies.onStartShouldSetResponderChild.callCount).toBe(2);
|
||||
expect(ResponderEventPlugin.getResponderID()).toBe(null);
|
||||
extractForTouchEnd(CHILD_ID); // Clear the responder
|
||||
|
||||
// Same thing as before but return true from "shouldSet".
|
||||
spies.onStartShouldSetResponderChild.andReturn(true);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
onResponderGrant(CHILD_ID, onGrantChild);
|
||||
extracted = extractForTouchStart(CHILD_ID);
|
||||
expect(spies.onStartShouldSetResponderChild.callCount).toBe(3);
|
||||
assertGrantEvent(CHILD_ID, extracted);
|
||||
extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
|
||||
assertRelease(CHILD_ID, extracted);
|
||||
});
|
||||
|
||||
it('should give up responder to parent on scroll iff allowed', function() {
|
||||
// Return true - we should become the responder.
|
||||
var extracted;
|
||||
spyOn(spies, 'onStartShouldSetResponderChild').andReturn(true);
|
||||
spyOn(spies, 'onMoveShouldSetResponderParent').andReturn(false);
|
||||
spyOn(spies, 'onScrollShouldSetResponderParent').andReturn(true);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
onMoveShouldSetResponder(PARENT_ID, spies.onMoveShouldSetResponderParent);
|
||||
onScrollShouldSetResponder(
|
||||
PARENT_ID,
|
||||
spies.onScrollShouldSetResponderParent
|
||||
);
|
||||
onResponderGrant(CHILD_ID, onGrantChild);
|
||||
onResponderGrant(PARENT_ID, onGrantParent);
|
||||
|
||||
spies.onStartShouldSetResponderChild.andReturn(true);
|
||||
onStartShouldSetResponder(CHILD_ID, spies.onStartShouldSetResponderChild);
|
||||
extracted = extractForTouchStart(CHILD_ID);
|
||||
expect(spies.onStartShouldSetResponderChild.callCount).toBe(1);
|
||||
expect(spies.onMoveShouldSetResponderParent.callCount).toBe(0); // none yet
|
||||
assertGrantEvent(CHILD_ID, extracted); // Child is the current responder
|
||||
|
||||
extracted = extractForTouchMove(CHILD_ID);
|
||||
expect(spies.onMoveShouldSetResponderParent.callCount).toBe(1);
|
||||
assertNothingExtracted(extracted);
|
||||
|
||||
extracted = extractForScroll(CHILD_ID); // Could have been parent here too.
|
||||
expect(spies.onScrollShouldSetResponderParent.callCount).toBe(1);
|
||||
assertGrantEvent(PARENT_ID, extracted);
|
||||
assertTerminateEvent(CHILD_ID, extracted);
|
||||
|
||||
extracted = extractForTouchEnd(CHILD_ID); // Clear the responder
|
||||
assertRelease(PARENT_ID, extracted);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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 ex
|
||||
* @typechecks
|
||||
* @nostacktrace
|
||||
*/
|
||||
|
||||
/**
|
||||
* This function transforms error message with arguments into plain text error
|
||||
* message, so that it can be passed to window.onerror without losing anything.
|
||||
* It can then be transformed back by `erx()` function.
|
||||
*
|
||||
* Usage:
|
||||
* throw new Error(ex('Error %s from %s', errorCode, userID));
|
||||
*
|
||||
* @param {string} errorMessage
|
||||
*/
|
||||
|
||||
var ex = function(errorMessage/*, arg1, arg2, ...*/) {
|
||||
var args = Array.prototype.slice.call(arguments).map(function(arg) {
|
||||
return String(arg);
|
||||
});
|
||||
var expectedLength = errorMessage.split('%s').length - 1;
|
||||
|
||||
if (expectedLength !== args.length - 1) {
|
||||
// something wrong with the formatting string
|
||||
return ex('ex args number mismatch: %s', JSON.stringify(args));
|
||||
}
|
||||
|
||||
return ex._prefix + JSON.stringify(args) + ex._suffix;
|
||||
};
|
||||
|
||||
ex._prefix = '<![EX[';
|
||||
ex._suffix = ']]>';
|
||||
|
||||
module.exports = ex;
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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 flattenChildren
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ReactTextComponent = require("./ReactTextComponent");
|
||||
|
||||
var escapeTextForBrowser = require("./escapeTextForBrowser");
|
||||
|
||||
var throwIf = require("./throwIf");
|
||||
|
||||
/**
|
||||
* @polyFill Array.isArray
|
||||
*/
|
||||
|
||||
|
||||
var INVALID_CHILD = 'INVALID_CHILD';
|
||||
if (true) {
|
||||
INVALID_CHILD =
|
||||
'You may not pass a child of that type to a React component. It ' +
|
||||
'is a common mistake to try to pass a standard browser DOM element ' +
|
||||
'as a child of a React component.';
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is only a single child, it still needs a name.
|
||||
*/
|
||||
var ONLY_CHILD_NAME = '0';
|
||||
|
||||
var flattenChildrenImpl = function(res, children, nameSoFar) {
|
||||
if (Array.isArray(children)) {
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
flattenChildrenImpl(res, children[i], nameSoFar + '[' + i + ']');
|
||||
}
|
||||
} else {
|
||||
var type = typeof children;
|
||||
var isOnlyChild = nameSoFar === '';
|
||||
var storageName = isOnlyChild ? ONLY_CHILD_NAME : nameSoFar;
|
||||
if (children === null || children === undefined || type === 'boolean') {
|
||||
res[storageName] = null;
|
||||
} else if (children.mountComponentIntoNode) {
|
||||
/* We found a component instance */
|
||||
res[storageName] = children;
|
||||
} else {
|
||||
if (type === 'object') {
|
||||
throwIf(children && children.nodeType === 1, INVALID_CHILD);
|
||||
for (var key in children) {
|
||||
if (children.hasOwnProperty(key)) {
|
||||
flattenChildrenImpl(
|
||||
res,
|
||||
children[key],
|
||||
nameSoFar + '{' + escapeTextForBrowser(key) + '}'
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (type === 'string') {
|
||||
res[storageName] = new ReactTextComponent(children);
|
||||
} else if (type === 'number') {
|
||||
res[storageName] = new ReactTextComponent('' + children);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Flattens children that are typically specified as `props.children`.
|
||||
* @return {!Object} flattened children keyed by name.
|
||||
*/
|
||||
function flattenChildren(children) {
|
||||
if (children === null || children === undefined) {
|
||||
return children;
|
||||
}
|
||||
var result = {};
|
||||
flattenChildrenImpl(result, children, '');
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = flattenChildren;
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 forEachAccumulated
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @param {array} an "accumulation" of items which is either an Array or
|
||||
* a single item. Useful when paired with the `accumulate` module. This is a
|
||||
* simple utility that allows us to reason about a collection of items, but
|
||||
* handling the case when there is exactly one item (and we do not need to
|
||||
* allocate an array).
|
||||
*/
|
||||
var forEachAccumulated = function(arr, cb, scope) {
|
||||
if (Array.isArray(arr)) {
|
||||
arr.forEach(cb, scope);
|
||||
} else if (arr) {
|
||||
cb.call(scope, arr);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = forEachAccumulated;
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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 ge
|
||||
*/
|
||||
|
||||
/**
|
||||
* Find a node by ID. Optionally search a sub-tree outside of the document
|
||||
*
|
||||
* Use ge if you're not sure whether or not the element exists. You can test
|
||||
* for existence yourself in your application code.
|
||||
*
|
||||
* If your application code depends on the existence of the element, use $
|
||||
* instead, which will throw in DEV if the element doesn't exist.
|
||||
*/
|
||||
function ge(arg, root, tag) {
|
||||
return typeof arg != 'string' ? arg :
|
||||
!root ? document.getElementById(arg) :
|
||||
_geFromSubtree(arg, root, tag);
|
||||
}
|
||||
|
||||
function _geFromSubtree(id, root, tag) {
|
||||
var elem, children, ii;
|
||||
|
||||
if (_getNodeID(root) == id) {
|
||||
return root;
|
||||
} else if (root.getElementsByTagName) {
|
||||
// All Elements implement this, which does an iterative DFS, which is
|
||||
// faster than recursion and doesn't run into stack depth issues.
|
||||
children = root.getElementsByTagName(tag || '*');
|
||||
for (ii = 0; ii < children.length; ii++) {
|
||||
if (_getNodeID(children[ii]) == id) {
|
||||
return children[ii];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// DocumentFragment does not implement getElementsByTagName, so
|
||||
// recurse over its children. Its children must be Elements, so
|
||||
// each child will use the getElementsByTagName case instead.
|
||||
children = root.childNodes;
|
||||
for (ii = 0; ii < children.length; ii++) {
|
||||
elem = _geFromSubtree(id, children[ii]);
|
||||
if (elem) {
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID value for a given node. This allows us to avoid issues
|
||||
* with forms that contain inputs with name="id".
|
||||
*
|
||||
* @return string (null if attribute not set)
|
||||
*/
|
||||
function _getNodeID(node) {
|
||||
// #document and #document-fragment do not have getAttributeNode.
|
||||
var id = node.getAttributeNode && node.getAttributeNode('id');
|
||||
return id ? id.value : null;
|
||||
}
|
||||
|
||||
module.exports = ge;
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 getDOMNodeID
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Accessing "id" or calling getAttribute('id') on a form element can return its
|
||||
* control whose name or ID is "id". However, not all DOM nodes support
|
||||
* `getAttributeNode` (document - which is not a form) so that is checked first.
|
||||
*
|
||||
* @param {Element} domNode DOM node element to return ID of.
|
||||
* @returns {string} The ID of `domNode`.
|
||||
*/
|
||||
function getDOMNodeID(domNode) {
|
||||
if (domNode.getAttributeNode) {
|
||||
var attributeNode = domNode.getAttributeNode('id');
|
||||
return attributeNode && attributeNode.value || '';
|
||||
} else {
|
||||
return domNode.id || '';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = getDOMNodeID;
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 getTextContentAccessor
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var ExecutionEnvironment = require("./ExecutionEnvironment");
|
||||
|
||||
var contentKey = null;
|
||||
|
||||
/**
|
||||
* Gets the key used to access text content on a DOM node.
|
||||
*
|
||||
* @return {?string} Key used to access text content.
|
||||
* @internal
|
||||
*/
|
||||
function getTextContentAccessor() {
|
||||
if (!contentKey && ExecutionEnvironment.canUseDOM) {
|
||||
contentKey = 'innerText' in document.createElement('div') ?
|
||||
'innerText' :
|
||||
'textContent';
|
||||
}
|
||||
return contentKey;
|
||||
}
|
||||
|
||||
module.exports = getTextContentAccessor;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user