/** @license React v16.2.0 * react-dom-unstable-native-dependencies.development.js * * Copyright (c) 2013-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; if (process.env.NODE_ENV !== "production") { (function() { 'use strict'; var ReactDOM = require('react-dom'); var invariant = require('fbjs/lib/invariant'); var warning = require('fbjs/lib/warning'); var _assign = require('object-assign'); var emptyFunction = require('fbjs/lib/emptyFunction'); /** * WARNING: DO NOT manually require this module. * This is a replacement for `invariant(...)` used by the error code system * and will _only_ be required by the corresponding babel pass. * It always throws. */ { // In DEV mode, we swap out invokeGuardedCallback for a special version // that plays more nicely with the browser's DevTools. The idea is to preserve // "Pause on exceptions" behavior. Because React wraps all user-provided // functions in invokeGuardedCallback, and the production version of // invokeGuardedCallback uses a try-catch, all user exceptions are treated // like caught exceptions, and the DevTools won't pause unless the developer // takes the extra step of enabling pause on caught exceptions. This is // untintuitive, though, because even though React has caught the error, from // the developer's perspective, the error is uncaught. // // To preserve the expected "Pause on exceptions" behavior, we don't use a // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake // DOM node, and call the user-provided callback from inside an event handler // for that fake event. If the callback throws, the error is "captured" using // a global event handler. But because the error happens in a different // event loop context, it does not interrupt the normal program flow. // Effectively, this gives us try-catch behavior without actually using // try-catch. Neat! // Check that the browser supports the APIs we need to implement our special // DEV version of invokeGuardedCallback if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') { var fakeNode = document.createElement('react'); } } var getFiberCurrentPropsFromNode = null; var getInstanceFromNode = null; var getNodeFromInstance = null; var injection = { injectComponentTree: function (Injected) { getFiberCurrentPropsFromNode = Injected.getFiberCurrentPropsFromNode; getInstanceFromNode = Injected.getInstanceFromNode; getNodeFromInstance = Injected.getNodeFromInstance; { warning(getNodeFromInstance && getInstanceFromNode, 'EventPluginUtils.injection.injectComponentTree(...): Injected ' + 'module is missing getNodeFromInstance or getInstanceFromNode.'); } } }; function isEndish(topLevelType) { return topLevelType === 'topMouseUp' || topLevelType === 'topTouchEnd' || topLevelType === 'topTouchCancel'; } function isMoveish(topLevelType) { return topLevelType === 'topMouseMove' || topLevelType === 'topTouchMove'; } function isStartish(topLevelType) { return topLevelType === 'topMouseDown' || topLevelType === 'topTouchStart'; } var validateEventDispatches; { validateEventDispatches = function (event) { var dispatchListeners = event._dispatchListeners; var dispatchInstances = event._dispatchInstances; var listenersIsArr = Array.isArray(dispatchListeners); var listenersLen = listenersIsArr ? dispatchListeners.length : dispatchListeners ? 1 : 0; var instancesIsArr = Array.isArray(dispatchInstances); var instancesLen = instancesIsArr ? dispatchInstances.length : dispatchInstances ? 1 : 0; warning(instancesIsArr === listenersIsArr && instancesLen === listenersLen, 'EventPluginUtils: Invalid `event`.'); }; } /** * Standard/simple iteration through an event's collected dispatches. */ /** * Standard/simple iteration through an event's collected dispatches, but stops * at the first dispatch execution returning true, and returns that id. * * @return {?string} id of the first dispatch execution who's listener returns * true, or null if no listener returned true. */ function executeDispatchesInOrderStopAtTrueImpl(event) { var dispatchListeners = event._dispatchListeners; var dispatchInstances = event._dispatchInstances; { validateEventDispatches(event); } if (Array.isArray(dispatchListeners)) { for (var i = 0; i < dispatchListeners.length; i++) { if (event.isPropagationStopped()) { break; } // Listeners and Instances are two parallel arrays that are always in sync. if (dispatchListeners[i](event, dispatchInstances[i])) { return dispatchInstances[i]; } } } else if (dispatchListeners) { if (dispatchListeners(event, dispatchInstances)) { return dispatchInstances; } } return null; } /** * @see executeDispatchesInOrderStopAtTrueImpl */ function executeDispatchesInOrderStopAtTrue(event) { var ret = executeDispatchesInOrderStopAtTrueImpl(event); event._dispatchInstances = null; event._dispatchListeners = null; return ret; } /** * 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. * * @return {*} The return value of executing the single dispatch. */ function executeDirectDispatch(event) { { validateEventDispatches(event); } var dispatchListener = event._dispatchListeners; var dispatchInstance = event._dispatchInstances; !!Array.isArray(dispatchListener) ? invariant(false, 'executeDirectDispatch(...): Invalid `event`.') : void 0; event.currentTarget = dispatchListener ? getNodeFromInstance(dispatchInstance) : null; var res = dispatchListener ? dispatchListener(event) : null; event.currentTarget = null; event._dispatchListeners = null; event._dispatchInstances = null; return res; } /** * @param {SyntheticEvent} event * @return {boolean} True iff number of dispatches accumulated is greater than 0. */ function hasDispatches(event) { return !!event._dispatchListeners; } // Before we know whether it is functional or class // Root of a host tree. Could be nested inside another node. // A subtree. Could be an entry point to a different renderer. var HostComponent = 5; function getParent(inst) { do { inst = inst['return']; // TODO: If this is a HostRoot we might want to bail out. // That is depending on if we want nested subtrees (layers) to bubble // events to their parent. We could also go through parentNode on the // host node but that wouldn't work for React Native and doesn't let us // do the portal feature. } while (inst && inst.tag !== HostComponent); if (inst) { return inst; } return null; } /** * Return the lowest common ancestor of A and B, or null if they are in * different trees. */ function getLowestCommonAncestor(instA, instB) { var depthA = 0; for (var tempA = instA; tempA; tempA = getParent(tempA)) { depthA++; } var depthB = 0; for (var tempB = instB; tempB; tempB = getParent(tempB)) { depthB++; } // If A is deeper, crawl up. while (depthA - depthB > 0) { instA = getParent(instA); depthA--; } // If B is deeper, crawl up. while (depthB - depthA > 0) { instB = getParent(instB); depthB--; } // Walk in lockstep until we find a match. var depth = depthA; while (depth--) { if (instA === instB || instA === instB.alternate) { return instA; } instA = getParent(instA); instB = getParent(instB); } return null; } /** * Return if A is an ancestor of B. */ function isAncestor(instA, instB) { while (instB) { if (instA === instB || instA === instB.alternate) { return true; } instB = getParent(instB); } return false; } /** * Return the parent instance of the passed-in instance. */ function getParentInstance(inst) { return getParent(inst); } /** * Simulates the traversal of a two-phase, capture/bubble event dispatch. */ function traverseTwoPhase(inst, fn, arg) { var path = []; while (inst) { path.push(inst); inst = getParent(inst); } var i; for (i = path.length; i-- > 0;) { fn(path[i], 'captured', arg); } for (i = 0; i < path.length; i++) { fn(path[i], 'bubbled', arg); } } /** * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that * should would receive a `mouseEnter` or `mouseLeave` event. * * Does not invoke the callback on the nearest common ancestor because nothing * "entered" or "left" that element. */ /** * Registers plugins so that they can extract and dispatch events. * * @see {EventPluginHub} */ /** * Ordered list of injected plugins. */ /** * Mapping from event name to dispatch config */ /** * Mapping from registration name to plugin module */ /** * Mapping from registration name to event name */ /** * Mapping from lowercase registration names to the properly cased version, * used to warn in the case of missing event handlers. Available * only in true. * @type {Object} */ // Trust the developer to only use possibleRegistrationNames in true /** * Injects an ordering of plugins (by plugin name). This allows the ordering * to be decoupled from injection of the actual plugins so that ordering is * always deterministic regardless of packaging, on-the-fly injection, etc. * * @param {array} InjectedEventPluginOrder * @internal * @see {EventPluginHub.injection.injectEventPluginOrder} */ /** * Injects plugins to be used by `EventPluginHub`. The plugin names must be * in the ordering injected by `injectEventPluginOrder`. * * Plugins can be injected as part of page initialization or on-the-fly. * * @param {object} injectedNamesToPlugins Map from names to plugin modules. * @internal * @see {EventPluginHub.injection.injectEventPluginsByName} */ /** * Accumulates items that must not be null or undefined into the first one. This * is used to conserve memory by avoiding array allocations, and thus sacrifices * API cleanness. Since `current` can be null before being passed in and not * null after this function, make sure to assign it back to `current`: * * `a = accumulateInto(a, b);` * * This API should be sparingly used. Try `accumulate` for something cleaner. * * @return {*|array<*>} An accumulation of items. */ function accumulateInto(current, next) { !(next != null) ? invariant(false, 'accumulateInto(...): Accumulated items must not be null or undefined.') : void 0; if (current == null) { return next; } // 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). if (Array.isArray(current)) { if (Array.isArray(next)) { current.push.apply(current, next); return current; } current.push(next); return current; } if (Array.isArray(next)) { // A bit too dangerous to mutate `next`. return [current].concat(next); } return [current, next]; } /** * @param {array} arr 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). * @param {function} cb Callback invoked with each element or a collection. * @param {?} [scope] Scope used as `this` in a callback. */ function forEachAccumulated(arr, cb, scope) { if (Array.isArray(arr)) { arr.forEach(cb, scope); } else if (arr) { cb.call(scope, arr); } } function isInteractive(tag) { return tag === 'button' || tag === 'input' || tag === 'select' || tag === 'textarea'; } function shouldPreventMouseEvent(name, type, props) { switch (name) { case 'onClick': case 'onClickCapture': case 'onDoubleClick': case 'onDoubleClickCapture': case 'onMouseDown': case 'onMouseDownCapture': case 'onMouseMove': case 'onMouseMoveCapture': case 'onMouseUp': case 'onMouseUpCapture': return !!(props.disabled && isInteractive(type)); default: return false; } } /** * This is a unified interface for event plugins to be installed and configured. * * Event plugins can implement the following properties: * * `extractEvents` {function(string, DOMEventTarget, string, object): *} * Required. When a top-level event is fired, this method is expected to * extract synthetic events that will in turn be queued and dispatched. * * `eventTypes` {object} * Optional, plugins that fire events must publish a mapping of registration * names that are used to register listeners. Values of this mapping must * be objects that contain `registrationName` or `phasedRegistrationNames`. * * `executeDispatch` {function(object, function, string)} * Optional, allows plugins to override how an event gets dispatched. By * default, the listener is simply invoked. * * Each plugin that is injected into `EventsPluginHub` is immediately operable. * * @public */ /** * Methods for injecting dependencies. */ /** * @param {object} inst The instance, which is the source of events. * @param {string} registrationName Name of listener (e.g. `onClick`). * @return {?function} The stored callback. */ function getListener(inst, registrationName) { var listener; // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not // live here; needs to be moved to a better place soon var stateNode = inst.stateNode; if (!stateNode) { // Work in progress (ex: onload events in incremental mode). return null; } var props = getFiberCurrentPropsFromNode(stateNode); if (!props) { // Work in progress. return null; } listener = props[registrationName]; if (shouldPreventMouseEvent(registrationName, inst.type, props)) { return null; } !(!listener || typeof listener === 'function') ? invariant(false, 'Expected `%s` listener to be a function, instead got a value of `%s` type.', registrationName, typeof listener) : void 0; return listener; } /** * Allows registered plugins an opportunity to extract events from top-level * native browser events. * * @return {*} An accumulation of synthetic events. * @internal */ /** * Enqueues a synthetic event that should be dispatched when * `processEventQueue` is invoked. * * @param {*} events An accumulation of synthetic events. * @internal */ /** * Dispatches all synthetic events on the event queue. * * @internal */ /** * Some event types have a notion of different registration names for different * "phases" of propagation. This finds listeners by a given phase. */ function listenerAtPhase(inst, event, propagationPhase) { var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; return getListener(inst, registrationName); } /** * 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 even a * single one. */ /** * Tags a `SyntheticEvent` 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(inst, phase, event) { { warning(inst, 'Dispatching inst must not be null'); } var listener = listenerAtPhase(inst, event, phase); if (listener) { event._dispatchListeners = accumulateInto(event._dispatchListeners, listener); event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } } /** * 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 cannot perform a * single traversal for the entire collection of events because each event may * have a different target. */ function accumulateTwoPhaseDispatchesSingle(event) { if (event && event.dispatchConfig.phasedRegistrationNames) { traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event); } } /** * Same as `accumulateTwoPhaseDispatchesSingle`, but skips over the targetID. */ function accumulateTwoPhaseDispatchesSingleSkipTarget(event) { if (event && event.dispatchConfig.phasedRegistrationNames) { var targetInst = event._targetInst; var parentInst = targetInst ? getParentInstance(targetInst) : null; traverseTwoPhase(parentInst, accumulateDirectionalDispatches, event); } } /** * Accumulates without regard to direction, does not look for phased * registration names. Same as `accumulateDirectDispatchesSingle` but without * requiring that the `dispatchMarker` be the same as the dispatched ID. */ function accumulateDispatches(inst, ignoredDirection, event) { if (inst && event && event.dispatchConfig.registrationName) { var registrationName = event.dispatchConfig.registrationName; var listener = getListener(inst, registrationName); if (listener) { event._dispatchListeners = accumulateInto(event._dispatchListeners, listener); event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } } } /** * Accumulates dispatches on an `SyntheticEvent`, but only for the * `dispatchMarker`. * @param {SyntheticEvent} event */ function accumulateDirectDispatchesSingle(event) { if (event && event.dispatchConfig.registrationName) { accumulateDispatches(event._targetInst, null, event); } } function accumulateTwoPhaseDispatches(events) { forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle); } function accumulateTwoPhaseDispatchesSkipTarget(events) { forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget); } function accumulateDirectDispatches(events) { forEachAccumulated(events, accumulateDirectDispatchesSingle); } /* eslint valid-typeof: 0 */ var didWarnForAddedNewProperty = false; var isProxySupported = typeof Proxy === 'function'; var EVENT_POOL_SIZE = 10; var shouldBeReleasedProperties = ['dispatchConfig', '_targetInst', 'nativeEvent', 'isDefaultPrevented', 'isPropagationStopped', '_dispatchListeners', '_dispatchInstances']; /** * @interface Event * @see http://www.w3.org/TR/DOM-Level-3-Events/ */ var EventInterface = { type: null, target: null, // currentTarget is set when dispatching; no use in copying it here currentTarget: emptyFunction.thatReturnsNull, eventPhase: null, bubbles: null, cancelable: null, timeStamp: function (event) { return event.timeStamp || Date.now(); }, defaultPrevented: null, isTrusted: null }; /** * Synthetic events are dispatched by event plugins, typically in response to a * top-level event delegation handler. * * These systems should generally use pooling to reduce the frequency of garbage * collection. The system should check `isPersistent` to determine whether the * event should be released into the pool after being dispatched. Users that * need a persisted event should invoke `persist`. * * Synthetic events (and subclasses) implement the DOM Level 3 Events API by * normalizing browser quirks. Subclasses do not necessarily have to implement a * DOM interface; custom application-specific events can also subclass this. * * @param {object} dispatchConfig Configuration used to dispatch this event. * @param {*} targetInst Marker identifying the event target. * @param {object} nativeEvent Native browser event. * @param {DOMEventTarget} nativeEventTarget Target node. */ function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) { { // these have a getter/setter for warnings delete this.nativeEvent; delete this.preventDefault; delete this.stopPropagation; } this.dispatchConfig = dispatchConfig; this._targetInst = targetInst; this.nativeEvent = nativeEvent; var Interface = this.constructor.Interface; for (var propName in Interface) { if (!Interface.hasOwnProperty(propName)) { continue; } { delete this[propName]; // this has a getter/setter for warnings } var normalize = Interface[propName]; if (normalize) { this[propName] = normalize(nativeEvent); } else { if (propName === 'target') { this.target = nativeEventTarget; } else { this[propName] = nativeEvent[propName]; } } } var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false; if (defaultPrevented) { this.isDefaultPrevented = emptyFunction.thatReturnsTrue; } else { this.isDefaultPrevented = emptyFunction.thatReturnsFalse; } this.isPropagationStopped = emptyFunction.thatReturnsFalse; return this; } _assign(SyntheticEvent.prototype, { preventDefault: function () { this.defaultPrevented = true; var event = this.nativeEvent; if (!event) { return; } if (event.preventDefault) { event.preventDefault(); } else if (typeof event.returnValue !== 'unknown') { event.returnValue = false; } this.isDefaultPrevented = emptyFunction.thatReturnsTrue; }, stopPropagation: function () { var event = this.nativeEvent; if (!event) { return; } if (event.stopPropagation) { event.stopPropagation(); } else if (typeof event.cancelBubble !== 'unknown') { // The ChangeEventPlugin registers a "propertychange" event for // IE. This event does not support bubbling or cancelling, and // any references to cancelBubble throw "Member not found". A // typeof check of "unknown" circumvents this issue (and is also // IE specific). event.cancelBubble = true; } this.isPropagationStopped = emptyFunction.thatReturnsTrue; }, /** * We release all dispatched `SyntheticEvent`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. */ persist: function () { this.isPersistent = emptyFunction.thatReturnsTrue; }, /** * Checks if this event should be released back into the pool. * * @return {boolean} True if this should not be released, false otherwise. */ isPersistent: emptyFunction.thatReturnsFalse, /** * `PooledClass` looks for `destructor` on each instance it releases. */ destructor: function () { var Interface = this.constructor.Interface; for (var propName in Interface) { { Object.defineProperty(this, propName, getPooledWarningPropertyDefinition(propName, Interface[propName])); } } for (var i = 0; i < shouldBeReleasedProperties.length; i++) { this[shouldBeReleasedProperties[i]] = null; } { Object.defineProperty(this, 'nativeEvent', getPooledWarningPropertyDefinition('nativeEvent', null)); Object.defineProperty(this, 'preventDefault', getPooledWarningPropertyDefinition('preventDefault', emptyFunction)); Object.defineProperty(this, 'stopPropagation', getPooledWarningPropertyDefinition('stopPropagation', emptyFunction)); } } }); SyntheticEvent.Interface = EventInterface; /** * Helper to reduce boilerplate when creating subclasses. * * @param {function} Class * @param {?object} Interface */ SyntheticEvent.augmentClass = function (Class, Interface) { var Super = this; var E = function () {}; E.prototype = Super.prototype; var prototype = new E(); _assign(prototype, Class.prototype); Class.prototype = prototype; Class.prototype.constructor = Class; Class.Interface = _assign({}, Super.Interface, Interface); Class.augmentClass = Super.augmentClass; addEventPoolingTo(Class); }; /** Proxying after everything set on SyntheticEvent * to resolve Proxy issue on some WebKit browsers * in which some Event properties are set to undefined (GH#10010) */ { if (isProxySupported) { /*eslint-disable no-func-assign */ SyntheticEvent = new Proxy(SyntheticEvent, { construct: function (target, args) { return this.apply(target, Object.create(target.prototype), args); }, apply: function (constructor, that, args) { return new Proxy(constructor.apply(that, args), { set: function (target, prop, value) { if (prop !== 'isPersistent' && !target.constructor.Interface.hasOwnProperty(prop) && shouldBeReleasedProperties.indexOf(prop) === -1) { warning(didWarnForAddedNewProperty || target.isPersistent(), "This synthetic event is reused for performance reasons. If you're " + "seeing this, you're adding a new property in the synthetic event object. " + 'The property is never released. See ' + 'https://fb.me/react-event-pooling for more information.'); didWarnForAddedNewProperty = true; } target[prop] = value; return true; } }); } }); /*eslint-enable no-func-assign */ } } addEventPoolingTo(SyntheticEvent); /** * Helper to nullify syntheticEvent instance properties when destructing * * @param {String} propName * @param {?object} getVal * @return {object} defineProperty object */ function getPooledWarningPropertyDefinition(propName, getVal) { var isFunction = typeof getVal === 'function'; return { configurable: true, set: set, get: get }; function set(val) { var action = isFunction ? 'setting the method' : 'setting the property'; warn(action, 'This is effectively a no-op'); return val; } function get() { var action = isFunction ? 'accessing the method' : 'accessing the property'; var result = isFunction ? 'This is a no-op function' : 'This is set to null'; warn(action, result); return getVal; } function warn(action, result) { var warningCondition = false; warning(warningCondition, "This synthetic event is reused for performance reasons. If you're seeing this, " + "you're %s `%s` on a released/nullified synthetic event. %s. " + 'If you must keep the original synthetic event around, use event.persist(). ' + 'See https://fb.me/react-event-pooling for more information.', action, propName, result); } } function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) { var EventConstructor = this; if (EventConstructor.eventPool.length) { var instance = EventConstructor.eventPool.pop(); EventConstructor.call(instance, dispatchConfig, targetInst, nativeEvent, nativeInst); return instance; } return new EventConstructor(dispatchConfig, targetInst, nativeEvent, nativeInst); } function releasePooledEvent(event) { var EventConstructor = this; !(event instanceof EventConstructor) ? invariant(false, 'Trying to release an event instance into a pool of a different type.') : void 0; event.destructor(); if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) { EventConstructor.eventPool.push(event); } } function addEventPoolingTo(EventConstructor) { EventConstructor.eventPool = []; EventConstructor.getPooled = getPooledEvent; EventConstructor.release = releasePooledEvent; } var SyntheticEvent$1 = SyntheticEvent; /** * `touchHistory` isn't actually on the native event, but putting it in the * interface will ensure that it is cleaned up when pooled/destroyed. The * `ResponderEventPlugin` will populate it appropriately. */ var ResponderEventInterface = { touchHistory: function (nativeEvent) { return null; // Actually doesn't even look at the native event. } }; /** * @param {object} dispatchConfig Configuration used to dispatch this event. * @param {string} dispatchMarker Marker identifying the event target. * @param {object} nativeEvent Native event. * @extends {SyntheticEvent} */ function ResponderSyntheticEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) { return SyntheticEvent$1.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget); } SyntheticEvent$1.augmentClass(ResponderSyntheticEvent, ResponderEventInterface); /** * Tracks the position and time of each active touch by `touch.identifier`. We * should typically only see IDs in the range of 1-20 because IDs get recycled * when touches end and start again. */ var MAX_TOUCH_BANK = 20; var touchBank = []; var touchHistory = { touchBank: touchBank, numberActiveTouches: 0, // If there is only one active touch, we remember its location. This prevents // us having to loop through all of the touches all the time in the most // common case. indexOfSingleActiveTouch: -1, mostRecentTimeStamp: 0 }; function timestampForTouch(touch) { // The legacy internal implementation provides "timeStamp", which has been // renamed to "timestamp". Let both work for now while we iron it out // TODO (evv): rename timeStamp to timestamp in internal code return touch.timeStamp || touch.timestamp; } /** * TODO: Instead of making gestures recompute filtered velocity, we could * include a built in velocity computation that can be reused globally. */ function createTouchRecord(touch) { return { touchActive: true, startPageX: touch.pageX, startPageY: touch.pageY, startTimeStamp: timestampForTouch(touch), currentPageX: touch.pageX, currentPageY: touch.pageY, currentTimeStamp: timestampForTouch(touch), previousPageX: touch.pageX, previousPageY: touch.pageY, previousTimeStamp: timestampForTouch(touch) }; } function resetTouchRecord(touchRecord, touch) { touchRecord.touchActive = true; touchRecord.startPageX = touch.pageX; touchRecord.startPageY = touch.pageY; touchRecord.startTimeStamp = timestampForTouch(touch); touchRecord.currentPageX = touch.pageX; touchRecord.currentPageY = touch.pageY; touchRecord.currentTimeStamp = timestampForTouch(touch); touchRecord.previousPageX = touch.pageX; touchRecord.previousPageY = touch.pageY; touchRecord.previousTimeStamp = timestampForTouch(touch); } function getTouchIdentifier(_ref) { var identifier = _ref.identifier; !(identifier != null) ? invariant(false, 'Touch object is missing identifier.') : void 0; { warning(identifier <= MAX_TOUCH_BANK, 'Touch identifier %s is greater than maximum supported %s which causes ' + 'performance issues backfilling array locations for all of the indices.', identifier, MAX_TOUCH_BANK); } return identifier; } function recordTouchStart(touch) { var identifier = getTouchIdentifier(touch); var touchRecord = touchBank[identifier]; if (touchRecord) { resetTouchRecord(touchRecord, touch); } else { touchBank[identifier] = createTouchRecord(touch); } touchHistory.mostRecentTimeStamp = timestampForTouch(touch); } function recordTouchMove(touch) { var touchRecord = touchBank[getTouchIdentifier(touch)]; if (touchRecord) { touchRecord.touchActive = true; touchRecord.previousPageX = touchRecord.currentPageX; touchRecord.previousPageY = touchRecord.currentPageY; touchRecord.previousTimeStamp = touchRecord.currentTimeStamp; touchRecord.currentPageX = touch.pageX; touchRecord.currentPageY = touch.pageY; touchRecord.currentTimeStamp = timestampForTouch(touch); touchHistory.mostRecentTimeStamp = timestampForTouch(touch); } else { console.error('Cannot record touch move without a touch start.\n' + 'Touch Move: %s\n', 'Touch Bank: %s', printTouch(touch), printTouchBank()); } } function recordTouchEnd(touch) { var touchRecord = touchBank[getTouchIdentifier(touch)]; if (touchRecord) { touchRecord.touchActive = false; touchRecord.previousPageX = touchRecord.currentPageX; touchRecord.previousPageY = touchRecord.currentPageY; touchRecord.previousTimeStamp = touchRecord.currentTimeStamp; touchRecord.currentPageX = touch.pageX; touchRecord.currentPageY = touch.pageY; touchRecord.currentTimeStamp = timestampForTouch(touch); touchHistory.mostRecentTimeStamp = timestampForTouch(touch); } else { console.error('Cannot record touch end without a touch start.\n' + 'Touch End: %s\n', 'Touch Bank: %s', printTouch(touch), printTouchBank()); } } function printTouch(touch) { return JSON.stringify({ identifier: touch.identifier, pageX: touch.pageX, pageY: touch.pageY, timestamp: timestampForTouch(touch) }); } function printTouchBank() { var printed = JSON.stringify(touchBank.slice(0, MAX_TOUCH_BANK)); if (touchBank.length > MAX_TOUCH_BANK) { printed += ' (original size: ' + touchBank.length + ')'; } return printed; } var ResponderTouchHistoryStore = { recordTouchTrack: function (topLevelType, nativeEvent) { if (isMoveish(topLevelType)) { nativeEvent.changedTouches.forEach(recordTouchMove); } else if (isStartish(topLevelType)) { nativeEvent.changedTouches.forEach(recordTouchStart); touchHistory.numberActiveTouches = nativeEvent.touches.length; if (touchHistory.numberActiveTouches === 1) { touchHistory.indexOfSingleActiveTouch = nativeEvent.touches[0].identifier; } } else if (isEndish(topLevelType)) { nativeEvent.changedTouches.forEach(recordTouchEnd); touchHistory.numberActiveTouches = nativeEvent.touches.length; if (touchHistory.numberActiveTouches === 1) { for (var i = 0; i < touchBank.length; i++) { var touchTrackToCheck = touchBank[i]; if (touchTrackToCheck != null && touchTrackToCheck.touchActive) { touchHistory.indexOfSingleActiveTouch = i; break; } } { var activeRecord = touchBank[touchHistory.indexOfSingleActiveTouch]; warning(activeRecord != null && activeRecord.touchActive, 'Cannot find single active touch.'); } } } }, touchHistory: touchHistory }; /** * Accumulates items that must not be null or undefined. * * This is used to conserve memory by avoiding array allocations. * * @return {*|array<*>} An accumulation of items. */ function accumulate(current, next) { !(next != null) ? invariant(false, 'accumulate(...): Accumulated items must be not be null or undefined.') : void 0; if (current == null) { return next; } // 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). if (Array.isArray(current)) { return current.concat(next); } if (Array.isArray(next)) { return [current].concat(next); } return [current, next]; } /** * Instance of element that should respond to touch/move types of interactions, * as indicated explicitly by relevant callbacks. */ var responderInst = null; /** * Count of current touches. A textInput should become responder iff the * selection changes while there is a touch on the screen. */ var trackedTouchCount = 0; /** * Last reported number of active touches. */ var previousActiveTouches = 0; var changeResponder = function (nextResponderInst, blockHostResponder) { var oldResponderInst = responderInst; responderInst = nextResponderInst; if (ResponderEventPlugin.GlobalResponderHandler !== null) { ResponderEventPlugin.GlobalResponderHandler.onChange(oldResponderInst, nextResponderInst, blockHostResponder); } }; var eventTypes = { /** * On a `touchStart`/`mouseDown`, is it desired that this element become the * responder? */ startShouldSetResponder: { phasedRegistrationNames: { bubbled: 'onStartShouldSetResponder', captured: 'onStartShouldSetResponderCapture' } }, /** * 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 occurred during momentum scroll. During a momentum scroll, * a touch start will be immediately followed by a scroll event if the view is * currently scrolling. * * TODO: This shouldn't bubble. */ scrollShouldSetResponder: { phasedRegistrationNames: { bubbled: 'onScrollShouldSetResponder', captured: 'onScrollShouldSetResponderCapture' } }, /** * On text selection change, should this element become the responder? This * is needed for text inputs or other views with native selection, so the * JS view can claim the responder. * * TODO: This shouldn't bubble. */ selectionChangeShouldSetResponder: { phasedRegistrationNames: { bubbled: 'onSelectionChangeShouldSetResponder', captured: 'onSelectionChangeShouldSetResponderCapture' } }, /** * On a `touchMove`/`mouseMove`, is it desired that this element become the * responder? */ moveShouldSetResponder: { phasedRegistrationNames: { bubbled: 'onMoveShouldSetResponder', captured: 'onMoveShouldSetResponderCapture' } }, /** * Direct responder events dispatched directly to responder. Do not bubble. */ responderStart: { registrationName: 'onResponderStart' }, responderMove: { registrationName: 'onResponderMove' }, responderEnd: { registrationName: 'onResponderEnd' }, responderRelease: { registrationName: 'onResponderRelease' }, responderTerminationRequest: { registrationName: 'onResponderTerminationRequest' }, responderGrant: { registrationName: 'onResponderGrant' }, responderReject: { registrationName: 'onResponderReject' }, responderTerminate: { registrationName: 'onResponderTerminate' } }; /** * * Responder System: * ---------------- * * - A global, solitary "interaction lock" on a view. * - If a node becomes the responder, it should convey visual feedback * immediately to indicate so, either by highlighting or moving accordingly. * - To be the responder means, that touches are exclusively important to that * responder view, and no other view. * - While touches are still occurring, the responder lock can be transferred to * a new view, but only to increasingly "higher" views (meaning ancestors of * the current responder). * * Responder being granted: * ------------------------ * * - Touch starts, moves, and scrolls can cause an ID to become the responder. * - We capture/bubble `startShouldSetResponder`/`moveShouldSetResponder` to * the "appropriate place". * - If nothing is currently the responder, the "appropriate place" is the * initiating event's `targetID`. * - If something *is* already the responder, the "appropriate place" is the * first common ancestor of the event target and the current `responderInst`. * - Some negotiation happens: See the timing diagram below. * - Scrolled views automatically become responder. The reasoning is that a * platform scroll view that isn't built on top of the responder system has * began scrolling, and the active responder must now be notified that the * interaction is no longer locked to it - the system has taken over. * * - Responder being released: * As soon as no more touches that *started* inside of descendants of the * *current* responderInst, an `onResponderRelease` event is dispatched to the * current responder, and the responder lock is released. * * TODO: * - on "end", a callback hook for `onResponderEndShouldRemainResponder` that * determines if the responder lock should remain. * - If a view shouldn't "remain" the responder, any active touches should by * default be considered "dead" and do not influence future negotiations or * bubble paths. It should be as if those touches do not exist. * -- For multitouch: Usually a translate-z will choose to "remain" responder * after one out of many touches ended. For translate-y, usually the view * doesn't wish to "remain" responder after one of many touches end. * - Consider building this on top of a `stopPropagation` model similar to * `W3C` events. * - Ensure that `onResponderTerminate` is called on touch cancels, whether or * not `onResponderTerminationRequest` returns `true` or `false`. * */ /* Negotiation Performed +-----------------------+ / \ Process low level events to + Current Responder + wantsResponderID determine who to perform negot-| (if any exists at all) | iation/transition | Otherwise just pass through| -------------------------------+----------------------------+------------------+ Bubble to find first ID | | to return true:wantsResponderID| | | | +-------------+ | | | onTouchStart| | | +------+------+ none | | | return| | +-----------v-------------+true| +------------------------+ | |onStartShouldSetResponder|----->|onResponderStart (cur) |<-----------+ +-----------+-------------+ | +------------------------+ | | | | | +--------+-------+ | returned true for| false:REJECT +-------->|onResponderReject | wantsResponderID | | | +----------------+ | (now attempt | +------------------+-----+ | | handoff) | | onResponder | | +------------------->| TerminationRequest| | | +------------------+-----+ | | | | +----------------+ | true:GRANT +-------->|onResponderGrant| | | +--------+-------+ | +------------------------+ | | | | onResponderTerminate |<-----------+ | +------------------+-----+ | | | | +----------------+ | +-------->|onResponderStart| | | +----------------+ Bubble to find first ID | | to return true:wantsResponderID| | | | +-------------+ | | | onTouchMove | | | +------+------+ none | | | return| | +-----------v-------------+true| +------------------------+ | |onMoveShouldSetResponder |----->|onResponderMove (cur) |<-----------+ +-----------+-------------+ | +------------------------+ | | | | | +--------+-------+ | returned true for| false:REJECT +-------->|onResponderRejec| | wantsResponderID | | | +----------------+ | (now attempt | +------------------+-----+ | | handoff) | | onResponder | | +------------------->| TerminationRequest| | | +------------------+-----+ | | | | +----------------+ | true:GRANT +-------->|onResponderGrant| | | +--------+-------+ | +------------------------+ | | | | onResponderTerminate |<-----------+ | +------------------+-----+ | | | | +----------------+ | +-------->|onResponderMove | | | +----------------+ | | | | Some active touch started| | inside current responder | +------------------------+ | +------------------------->| onResponderEnd | | | | +------------------------+ | +---+---------+ | | | onTouchEnd | | | +---+---------+ | | | | +------------------------+ | +------------------------->| onResponderEnd | | No active touches started| +-----------+------------+ | inside current responder | | | | v | | +------------------------+ | | | onResponderRelease | | | +------------------------+ | | | + + */ /** * 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 * `extractEvents()`. * - These events that are returned from `extractEvents` are "deferred * dispatched events". * - When returned from `extractEvents`, 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 * `extractEvents`. 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 events are not returned from `extractEvents`. * - 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 * `extractEvents`. 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) */ function setResponderAndExtractTransfer(topLevelType, targetInst, nativeEvent, nativeEventTarget) { var shouldSetEventType = isStartish(topLevelType) ? eventTypes.startShouldSetResponder : isMoveish(topLevelType) ? eventTypes.moveShouldSetResponder : topLevelType === 'topSelectionChange' ? eventTypes.selectionChangeShouldSetResponder : eventTypes.scrollShouldSetResponder; // TODO: stop one short of the current responder. var bubbleShouldSetFrom = !responderInst ? targetInst : getLowestCommonAncestor(responderInst, targetInst); // When capturing/bubbling the "shouldSet" event, we want to skip the target // (deepest ID) if it happens to be the current responder. The reasoning: // It's strange to get an `onMoveShouldSetResponder` when you're *already* // the responder. var skipOverBubbleShouldSetFrom = bubbleShouldSetFrom === responderInst; var shouldSetEvent = ResponderSyntheticEvent.getPooled(shouldSetEventType, bubbleShouldSetFrom, nativeEvent, nativeEventTarget); shouldSetEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; if (skipOverBubbleShouldSetFrom) { accumulateTwoPhaseDispatchesSkipTarget(shouldSetEvent); } else { accumulateTwoPhaseDispatches(shouldSetEvent); } var wantsResponderInst = executeDispatchesInOrderStopAtTrue(shouldSetEvent); if (!shouldSetEvent.isPersistent()) { shouldSetEvent.constructor.release(shouldSetEvent); } if (!wantsResponderInst || wantsResponderInst === responderInst) { return null; } var extracted; var grantEvent = ResponderSyntheticEvent.getPooled(eventTypes.responderGrant, wantsResponderInst, nativeEvent, nativeEventTarget); grantEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; accumulateDirectDispatches(grantEvent); var blockHostResponder = executeDirectDispatch(grantEvent) === true; if (responderInst) { var terminationRequestEvent = ResponderSyntheticEvent.getPooled(eventTypes.responderTerminationRequest, responderInst, nativeEvent, nativeEventTarget); terminationRequestEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; accumulateDirectDispatches(terminationRequestEvent); var shouldSwitch = !hasDispatches(terminationRequestEvent) || executeDirectDispatch(terminationRequestEvent); if (!terminationRequestEvent.isPersistent()) { terminationRequestEvent.constructor.release(terminationRequestEvent); } if (shouldSwitch) { var terminateEvent = ResponderSyntheticEvent.getPooled(eventTypes.responderTerminate, responderInst, nativeEvent, nativeEventTarget); terminateEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; accumulateDirectDispatches(terminateEvent); extracted = accumulate(extracted, [grantEvent, terminateEvent]); changeResponder(wantsResponderInst, blockHostResponder); } else { var rejectEvent = ResponderSyntheticEvent.getPooled(eventTypes.responderReject, wantsResponderInst, nativeEvent, nativeEventTarget); rejectEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; accumulateDirectDispatches(rejectEvent); extracted = accumulate(extracted, rejectEvent); } } else { extracted = accumulate(extracted, grantEvent); changeResponder(wantsResponderInst, blockHostResponder); } 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 responderInst. Any move event could trigger a transfer. * * @param {string} topLevelType Record from `BrowserEventConstants`. * @return {boolean} True if a transfer of responder could possibly occur. */ function canTriggerTransfer(topLevelType, topLevelInst, nativeEvent) { return topLevelInst && ( // responderIgnoreScroll: We are trying to migrate away from specifically // tracking native scroll events here and responderIgnoreScroll indicates we // will send topTouchCancel to handle canceling touch events instead topLevelType === 'topScroll' && !nativeEvent.responderIgnoreScroll || trackedTouchCount > 0 && topLevelType === 'topSelectionChange' || isStartish(topLevelType) || isMoveish(topLevelType)); } /** * Returns whether or not this touch end event makes it such that there are no * longer any touches that started inside of the current `responderInst`. * * @param {NativeEvent} nativeEvent Native touch end event. * @return {boolean} Whether or not this touch end event ends the responder. */ function noResponderTouches(nativeEvent) { var touches = nativeEvent.touches; if (!touches || touches.length === 0) { return true; } for (var i = 0; i < touches.length; i++) { var activeTouch = touches[i]; var target = activeTouch.target; if (target !== null && target !== undefined && target !== 0) { // Is the original touch location inside of the current responder? var targetInst = getInstanceFromNode(target); if (isAncestor(responderInst, targetInst)) { return false; } } } return true; } var ResponderEventPlugin = { /* For unit testing only */ _getResponder: function () { return responderInst; }, eventTypes: eventTypes, /** * We must be resilient to `targetInst` being `null` on `touchMove` or * `touchEnd`. On certain platforms, this means that a native scroll has * assumed control and the original touch targets are destroyed. */ extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { if (isStartish(topLevelType)) { trackedTouchCount += 1; } else if (isEndish(topLevelType)) { if (trackedTouchCount >= 0) { trackedTouchCount -= 1; } else { console.error('Ended a touch event which was not counted in `trackedTouchCount`.'); return null; } } ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent); var extracted = canTriggerTransfer(topLevelType, targetInst, nativeEvent) ? setResponderAndExtractTransfer(topLevelType, targetInst, nativeEvent, nativeEventTarget) : null; // Responder may or may not have transferred on a new touch start/move. // Regardless, whoever is the responder after any potential transfer, we // direct all touch start/move/ends to them in the form of // `onResponderMove/Start/End`. These will be called for *every* additional // finger that move/start/end, dispatched directly to whoever is the // current responder at that moment, until the responder is "released". // // These multiple individual change touch events are are always bookended // by `onResponderGrant`, and one of // (`onResponderRelease/onResponderTerminate`). var isResponderTouchStart = responderInst && isStartish(topLevelType); var isResponderTouchMove = responderInst && isMoveish(topLevelType); var isResponderTouchEnd = responderInst && isEndish(topLevelType); var incrementalTouch = isResponderTouchStart ? eventTypes.responderStart : isResponderTouchMove ? eventTypes.responderMove : isResponderTouchEnd ? eventTypes.responderEnd : null; if (incrementalTouch) { var gesture = ResponderSyntheticEvent.getPooled(incrementalTouch, responderInst, nativeEvent, nativeEventTarget); gesture.touchHistory = ResponderTouchHistoryStore.touchHistory; accumulateDirectDispatches(gesture); extracted = accumulate(extracted, gesture); } var isResponderTerminate = responderInst && topLevelType === 'topTouchCancel'; var isResponderRelease = responderInst && !isResponderTerminate && isEndish(topLevelType) && noResponderTouches(nativeEvent); var finalTouch = isResponderTerminate ? eventTypes.responderTerminate : isResponderRelease ? eventTypes.responderRelease : null; if (finalTouch) { var finalEvent = ResponderSyntheticEvent.getPooled(finalTouch, responderInst, nativeEvent, nativeEventTarget); finalEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; accumulateDirectDispatches(finalEvent); extracted = accumulate(extracted, finalEvent); changeResponder(null); } var numberActiveTouches = ResponderTouchHistoryStore.touchHistory.numberActiveTouches; if (ResponderEventPlugin.GlobalInteractionHandler && numberActiveTouches !== previousActiveTouches) { ResponderEventPlugin.GlobalInteractionHandler.onChange(numberActiveTouches); } previousActiveTouches = numberActiveTouches; return extracted; }, GlobalResponderHandler: null, GlobalInteractionHandler: null, injection: { /** * @param {{onChange: (ReactID, ReactID) => void} GlobalResponderHandler * Object that handles any change in responder. Use this to inject * integration with an existing touch handling system etc. */ injectGlobalResponderHandler: function (GlobalResponderHandler) { ResponderEventPlugin.GlobalResponderHandler = GlobalResponderHandler; }, /** * @param {{onChange: (numberActiveTouches) => void} GlobalInteractionHandler * Object that handles any change in the number of active touches. */ injectGlobalInteractionHandler: function (GlobalInteractionHandler) { ResponderEventPlugin.GlobalInteractionHandler = GlobalInteractionHandler; } } }; // This is used by react-native-web. var injectComponentTree = injection.injectComponentTree; // Inject react-dom's ComponentTree into this module. var ReactDOMComponentTree = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactDOMComponentTree; injectComponentTree(ReactDOMComponentTree); var ReactDOMUnstableNativeDependencies = Object.freeze({ injectComponentTree: injectComponentTree, ResponderEventPlugin: ResponderEventPlugin, ResponderTouchHistoryStore: ResponderTouchHistoryStore }); var unstableNativeDependencies = ReactDOMUnstableNativeDependencies; module.exports = unstableNativeDependencies; })(); }