From ea744fe7809b82662e870da332b9599732b6672a Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Tue, 22 May 2018 13:23:34 -0700 Subject: [PATCH] [add] NativeEventEmitter export Export NativeEventEmitter and provide React Native's implementation. --- package.json | 4 +- .../src/moduleMap.js | 1 + .../src/exports/NativeEventEmitter/index.js | 11 + packages/react-native-web/src/index.js | 3 + .../src/modules/NativeEventEmitter/index.js | 22 -- .../Animated/NativeAnimatedHelper.js | 2 +- .../react-native/NativeEventEmitter/index.js | 67 ++++++ .../emitter/EmitterSubscription.js | 59 +++++ .../react-native/emitter/EventEmitter.js | 218 ++++++++++++++++++ .../emitter/EventEmitterWithHolding.js | 163 +++++++++++++ .../react-native/emitter/EventHolder.js | 119 ++++++++++ .../react-native/emitter/EventSubscription.js | 40 ++++ .../emitter/EventSubscriptionVendor.js | 98 ++++++++ .../react-native/emitter/EventValidator.js | 137 +++++++++++ .../react-native/emitter/mixInEventEmitter.js | 137 +++++++++++ 15 files changed, 1057 insertions(+), 24 deletions(-) create mode 100644 packages/react-native-web/src/exports/NativeEventEmitter/index.js delete mode 100644 packages/react-native-web/src/modules/NativeEventEmitter/index.js create mode 100644 packages/react-native-web/src/vendor/react-native/NativeEventEmitter/index.js create mode 100644 packages/react-native-web/src/vendor/react-native/emitter/EmitterSubscription.js create mode 100644 packages/react-native-web/src/vendor/react-native/emitter/EventEmitter.js create mode 100644 packages/react-native-web/src/vendor/react-native/emitter/EventEmitterWithHolding.js create mode 100644 packages/react-native-web/src/vendor/react-native/emitter/EventHolder.js create mode 100644 packages/react-native-web/src/vendor/react-native/emitter/EventSubscription.js create mode 100644 packages/react-native-web/src/vendor/react-native/emitter/EventSubscriptionVendor.js create mode 100644 packages/react-native-web/src/vendor/react-native/emitter/EventValidator.js create mode 100644 packages/react-native-web/src/vendor/react-native/emitter/mixInEventEmitter.js diff --git a/package.json b/package.json index 9c544060..4a140066 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,9 @@ ], "lint-staged": { "packages/react-native-web/src/index.js": [ - "node ./scripts/babel/createModuleMap.js" + "node ./scripts/babel/createModuleMap.js", + "prettier --write ./packages/babel-plugin-react-native-web/src/moduleMap.js", + "git add ./packages/babel-plugin-react-native-web/src/moduleMap.js" ], "**/*.js": [ "prettier --write", diff --git a/packages/babel-plugin-react-native-web/src/moduleMap.js b/packages/babel-plugin-react-native-web/src/moduleMap.js index fb9b6188..3bd6bf92 100644 --- a/packages/babel-plugin-react-native-web/src/moduleMap.js +++ b/packages/babel-plugin-react-native-web/src/moduleMap.js @@ -27,6 +27,7 @@ module.exports = { Linking: true, ListView: true, Modal: true, + NativeEventEmitter: true, NativeModules: true, NetInfo: true, PanResponder: true, diff --git a/packages/react-native-web/src/exports/NativeEventEmitter/index.js b/packages/react-native-web/src/exports/NativeEventEmitter/index.js new file mode 100644 index 00000000..c8d1caaa --- /dev/null +++ b/packages/react-native-web/src/exports/NativeEventEmitter/index.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2016-present, Nicolas Gallagher. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import NativeEventEmitter from '../../vendor/react-native/NativeEventEmitter'; +export default NativeEventEmitter; diff --git a/packages/react-native-web/src/index.js b/packages/react-native-web/src/index.js index 6167e586..8f44ddb7 100644 --- a/packages/react-native-web/src/index.js +++ b/packages/react-native-web/src/index.js @@ -23,6 +23,7 @@ import Keyboard from './exports/Keyboard'; import InteractionManager from './exports/InteractionManager'; import LayoutAnimation from './exports/LayoutAnimation'; import Linking from './exports/Linking'; +import NativeEventEmitter from './exports/NativeEventEmitter'; import NetInfo from './exports/NetInfo'; import PanResponder from './exports/PanResponder'; import PixelRatio from './exports/PixelRatio'; @@ -93,6 +94,7 @@ export { Keyboard, LayoutAnimation, Linking, + NativeEventEmitter, NetInfo, PanResponder, PixelRatio, @@ -162,6 +164,7 @@ const ReactNative = { Keyboard, LayoutAnimation, Linking, + NativeEventEmitter, NetInfo, PanResponder, PixelRatio, diff --git a/packages/react-native-web/src/modules/NativeEventEmitter/index.js b/packages/react-native-web/src/modules/NativeEventEmitter/index.js deleted file mode 100644 index bae9010a..00000000 --- a/packages/react-native-web/src/modules/NativeEventEmitter/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2015-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. - * - * @noflow - */ -'use strict'; - -class NativeEventEmitter { - addListener() {} - emit() {} - listeners() {} - once() {} - removeAllListeners() {} - removeCurrentListener() {} - removeListener() {} - removeSubscription() {} -} - -export default NativeEventEmitter; diff --git a/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedHelper.js b/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedHelper.js index 51a6f1be..8598714d 100644 --- a/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedHelper.js +++ b/packages/react-native-web/src/vendor/react-native/Animated/NativeAnimatedHelper.js @@ -11,7 +11,7 @@ import invariant from 'fbjs/lib/invariant'; import NativeModules from '../../../exports/NativeModules'; -import NativeEventEmitter from '../../../modules/NativeEventEmitter'; +import NativeEventEmitter from '../NativeEventEmitter'; import type {AnimationConfig} from './animations/Animation'; import type {EventConfig} from './AnimatedEvent'; diff --git a/packages/react-native-web/src/vendor/react-native/NativeEventEmitter/index.js b/packages/react-native-web/src/vendor/react-native/NativeEventEmitter/index.js new file mode 100644 index 00000000..1dbbdd43 --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/NativeEventEmitter/index.js @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule NativeEventEmitter + * @flow + */ +'use strict'; + +import EventEmitter from '../emitter/EventEmitter'; +import Platform from '../../../exports/Platform'; + +import invariant from 'fbjs/lib/invariant'; + +import type EmitterSubscription from '../emitter/EmitterSubscription'; + +type NativeModule = { + +addListener: (eventType: string) => void, + +removeListeners: (count: number) => void, +}; + +/** + * Abstract base class for implementing event-emitting modules. This implements + * a subset of the standard EventEmitter node module API. + */ +class NativeEventEmitter extends EventEmitter { + _nativeModule: ?NativeModule; + + constructor(nativeModule: ?NativeModule) { + super(); + if (Platform.OS === 'ios') { + invariant(nativeModule, 'Native module cannot be null.'); + this._nativeModule = nativeModule; + } + } + + addListener( + eventType: string, + listener: Function, + context: ?Object, + ): EmitterSubscription { + if (this._nativeModule != null) { + this._nativeModule.addListener(eventType); + } + return super.addListener(eventType, listener, context); + } + + removeAllListeners(eventType: string) { + invariant(eventType, 'eventType argument is required.'); + const count = this.listeners(eventType).length; + if (this._nativeModule != null) { + this._nativeModule.removeListeners(count); + } + super.removeAllListeners(eventType); + } + + removeSubscription(subscription: EmitterSubscription) { + if (this._nativeModule != null) { + this._nativeModule.removeListeners(1); + } + super.removeSubscription(subscription); + } +} + +export default NativeEventEmitter; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EmitterSubscription.js b/packages/react-native-web/src/vendor/react-native/emitter/EmitterSubscription.js new file mode 100644 index 00000000..63b1aa34 --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/emitter/EmitterSubscription.js @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule EmitterSubscription + * @flow + */ +'use strict'; + +import EventSubscription from './EventSubscription'; + +import type EventEmitter from './EventEmitter'; +import type EventSubscriptionVendor from './EventSubscriptionVendor'; + +/** + * EmitterSubscription represents a subscription with listener and context data. + */ +class EmitterSubscription extends EventSubscription { + + emitter: EventEmitter; + listener: Function; + context: ?Object; + + /** + * @param {EventEmitter} emitter - The event emitter that registered this + * subscription + * @param {EventSubscriptionVendor} subscriber - The subscriber that controls + * this subscription + * @param {function} listener - Function to invoke when the specified event is + * emitted + * @param {*} context - Optional context object to use when invoking the + * listener + */ + constructor( + emitter: EventEmitter, + subscriber: EventSubscriptionVendor, + listener: Function, + context: ?Object + ) { + super(subscriber); + this.emitter = emitter; + this.listener = listener; + this.context = context; + } + + /** + * Removes this subscription from the emitter that registered it. + * Note: we're overriding the `remove()` method of EventSubscription here + * but deliberately not calling `super.remove()` as the responsibility + * for removing the subscription lies with the EventEmitter. + */ + remove() { + this.emitter.removeSubscription(this); + } +} + +export default EmitterSubscription; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventEmitter.js b/packages/react-native-web/src/vendor/react-native/emitter/EventEmitter.js new file mode 100644 index 00000000..87b3782a --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/emitter/EventEmitter.js @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule EventEmitter + * @noflow + * @typecheck + */ +'use strict'; + +import EmitterSubscription from './EmitterSubscription'; +import EventSubscriptionVendor from './EventSubscriptionVendor'; + +import emptyFunction from 'fbjs/lib/emptyFunction'; +import invariant from 'fbjs/lib/invariant'; + +/** + * @class EventEmitter + * @description + * An EventEmitter is responsible for managing a set of listeners and publishing + * events to them when it is told that such events happened. In addition to the + * data for the given event it also sends a event control object which allows + * the listeners/handlers to prevent the default behavior of the given event. + * + * The emitter is designed to be generic enough to support all the different + * contexts in which one might want to emit events. It is a simple multicast + * mechanism on top of which extra functionality can be composed. For example, a + * more advanced emitter may use an EventHolder and EventFactory. + */ +class EventEmitter { + + _subscriber: EventSubscriptionVendor; + _currentSubscription: ?EmitterSubscription; + + /** + * @constructor + * + * @param {EventSubscriptionVendor} subscriber - Optional subscriber instance + * to use. If omitted, a new subscriber will be created for the emitter. + */ + constructor(subscriber: ?EventSubscriptionVendor) { + this._subscriber = subscriber || new EventSubscriptionVendor(); + } + + /** + * Adds a listener to be invoked when events of the specified type are + * emitted. An optional calling context may be provided. The data arguments + * emitted will be passed to the listener function. + * + * TODO: Annotate the listener arg's type. This is tricky because listeners + * can be invoked with varargs. + * + * @param {string} eventType - Name of the event to listen to + * @param {function} listener - Function to invoke when the specified event is + * emitted + * @param {*} context - Optional context object to use when invoking the + * listener + */ + addListener( + eventType: string, listener: Function, context: ?Object): EmitterSubscription { + + return (this._subscriber.addSubscription( + eventType, + new EmitterSubscription(this, this._subscriber, listener, context) + ) : any); + } + + /** + * Similar to addListener, except that the listener is removed after it is + * invoked once. + * + * @param {string} eventType - Name of the event to listen to + * @param {function} listener - Function to invoke only once when the + * specified event is emitted + * @param {*} context - Optional context object to use when invoking the + * listener + */ + once(eventType: string, listener: Function, context: ?Object): EmitterSubscription { + return this.addListener(eventType, (...args) => { + this.removeCurrentListener(); + listener.apply(context, args); + }); + } + + /** + * Removes all of the registered listeners, including those registered as + * listener maps. + * + * @param {?string} eventType - Optional name of the event whose registered + * listeners to remove + */ + removeAllListeners(eventType: ?string) { + this._subscriber.removeAllSubscriptions(eventType); + } + + /** + * Provides an API that can be called during an eventing cycle to remove the + * last listener that was invoked. This allows a developer to provide an event + * object that can remove the listener (or listener map) during the + * invocation. + * + * If it is called when not inside of an emitting cycle it will throw. + * + * @throws {Error} When called not during an eventing cycle + * + * @example + * var subscription = emitter.addListenerMap({ + * someEvent: function(data, event) { + * console.log(data); + * emitter.removeCurrentListener(); + * } + * }); + * + * emitter.emit('someEvent', 'abc'); // logs 'abc' + * emitter.emit('someEvent', 'def'); // does not log anything + */ + removeCurrentListener() { + invariant( + !!this._currentSubscription, + 'Not in an emitting cycle; there is no current subscription' + ); + this.removeSubscription(this._currentSubscription); + } + + /** + * Removes a specific subscription. Called by the `remove()` method of the + * subscription itself to ensure any necessary cleanup is performed. + */ + removeSubscription(subscription: EmitterSubscription) { + invariant( + subscription.emitter === this, + 'Subscription does not belong to this emitter.' + ); + this._subscriber.removeSubscription(subscription); + } + + /** + * Returns an array of listeners that are currently registered for the given + * event. + * + * @param {string} eventType - Name of the event to query + * @returns {array} + */ + listeners(eventType: string): [EmitterSubscription] { + const subscriptions: ?[EmitterSubscription] = (this._subscriber.getSubscriptionsForType(eventType): any); + return subscriptions + ? subscriptions.filter(emptyFunction.thatReturnsTrue).map( + function(subscription) { + return subscription.listener; + }) + : []; + } + + /** + * Emits an event of the given type with the given data. All handlers of that + * particular type will be notified. + * + * @param {string} eventType - Name of the event to emit + * @param {...*} Arbitrary arguments to be passed to each registered listener + * + * @example + * emitter.addListener('someEvent', function(message) { + * console.log(message); + * }); + * + * emitter.emit('someEvent', 'abc'); // logs 'abc' + */ + emit(eventType: string) { + const subscriptions: ?[EmitterSubscription] = (this._subscriber.getSubscriptionsForType(eventType): any); + if (subscriptions) { + for (let i = 0, l = subscriptions.length; i < l; i++) { + const subscription = subscriptions[i]; + + // The subscription may have been removed during this event loop. + if (subscription) { + this._currentSubscription = subscription; + subscription.listener.apply( + subscription.context, + Array.prototype.slice.call(arguments, 1) + ); + } + } + this._currentSubscription = null; + } + } + + /** + * Removes the given listener for event of specific type. + * + * @param {string} eventType - Name of the event to emit + * @param {function} listener - Function to invoke when the specified event is + * emitted + * + * @example + * emitter.removeListener('someEvent', function(message) { + * console.log(message); + * }); // removes the listener if already registered + * + */ + removeListener(eventType: String, listener) { + const subscriptions: ?[EmitterSubscription] = (this._subscriber.getSubscriptionsForType(eventType): any); + if (subscriptions) { + for (let i = 0, l = subscriptions.length; i < l; i++) { + const subscription = subscriptions[i]; + + // The subscription may have been removed during this event loop. + // its listener matches the listener in method parameters + if (subscription && subscription.listener === listener) { + subscription.remove(); + } + } + } + } +} + +export default EventEmitter; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventEmitterWithHolding.js b/packages/react-native-web/src/vendor/react-native/emitter/EventEmitterWithHolding.js new file mode 100644 index 00000000..45ba0df9 --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/emitter/EventEmitterWithHolding.js @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule EventEmitterWithHolding + * @flow + */ +'use strict'; + +import type EmitterSubscription from './EmitterSubscription'; +import type EventEmitter from './EventEmitter'; +import type EventHolder from './EventHolder'; + +/** + * @class EventEmitterWithHolding + * @description + * An EventEmitterWithHolding decorates an event emitter and enables one to + * "hold" or cache events and then have a handler register later to actually + * handle them. + * + * This is separated into its own decorator so that only those who want to use + * the holding functionality have to and others can just use an emitter. Since + * it implements the emitter interface it can also be combined with anything + * that uses an emitter. + */ +class EventEmitterWithHolding { + + _emitter: EventEmitter; + _eventHolder: EventHolder; + _currentEventToken: ?Object; + _emittingHeldEvents: boolean; + + /** + * @constructor + * @param {object} emitter - The object responsible for emitting the actual + * events. + * @param {object} holder - The event holder that is responsible for holding + * and then emitting held events. + */ + constructor(emitter: EventEmitter, holder: EventHolder) { + this._emitter = emitter; + this._eventHolder = holder; + this._currentEventToken = null; + this._emittingHeldEvents = false; + } + + /** + * @see EventEmitter#addListener + */ + addListener(eventType: string, listener: Function, context: ?Object) { + return this._emitter.addListener(eventType, listener, context); + } + + /** + * @see EventEmitter#once + */ + once(eventType: string, listener: Function, context: ?Object) { + return this._emitter.once(eventType, listener, context); + } + + /** + * Adds a listener to be invoked when events of the specified type are + * emitted. An optional calling context may be provided. The data arguments + * emitted will be passed to the listener function. In addition to subscribing + * to all subsequent events, this method will also handle any events that have + * already been emitted, held, and not released. + * + * @param {string} eventType - Name of the event to listen to + * @param {function} listener - Function to invoke when the specified event is + * emitted + * @param {*} context - Optional context object to use when invoking the + * listener + * + * @example + * emitter.emitAndHold('someEvent', 'abc'); + * + * emitter.addRetroactiveListener('someEvent', function(message) { + * console.log(message); + * }); // logs 'abc' + */ + addRetroactiveListener( + eventType: string, listener: Function, context: ?Object): EmitterSubscription { + const subscription = this._emitter.addListener(eventType, listener, context); + + this._emittingHeldEvents = true; + this._eventHolder.emitToListener(eventType, listener, context); + this._emittingHeldEvents = false; + + return subscription; + } + + /** + * @see EventEmitter#removeAllListeners + */ + removeAllListeners(eventType: string) { + this._emitter.removeAllListeners(eventType); + } + + /** + * @see EventEmitter#removeCurrentListener + */ + removeCurrentListener() { + this._emitter.removeCurrentListener(); + } + + /** + * @see EventEmitter#listeners + */ + listeners(eventType: string) /* TODO: Annotate return type here */ { + return this._emitter.listeners(eventType); + } + + /** + * @see EventEmitter#emit + */ + emit(eventType: string, ...args: any) { + this._emitter.emit(eventType, ...args); + } + + /** + * Emits an event of the given type with the given data, and holds that event + * in order to be able to dispatch it to a later subscriber when they say they + * want to handle held events. + * + * @param {string} eventType - Name of the event to emit + * @param {...*} Arbitrary arguments to be passed to each registered listener + * + * @example + * emitter.emitAndHold('someEvent', 'abc'); + * + * emitter.addRetroactiveListener('someEvent', function(message) { + * console.log(message); + * }); // logs 'abc' + */ + emitAndHold(eventType: string, ...args: any) { + this._currentEventToken = this._eventHolder.holdEvent(eventType, ...args); + this._emitter.emit(eventType, ...args); + this._currentEventToken = null; + } + + /** + * @see EventHolder#releaseCurrentEvent + */ + releaseCurrentEvent() { + if (this._currentEventToken) { + this._eventHolder.releaseEvent(this._currentEventToken); + } else if (this._emittingHeldEvents) { + this._eventHolder.releaseCurrentEvent(); + } + } + + /** + * @see EventHolder#releaseEventType + * @param {string} eventType + */ + releaseHeldEventType(eventType: string) { + this._eventHolder.releaseEventType(eventType); + } +} + +export default EventEmitterWithHolding; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventHolder.js b/packages/react-native-web/src/vendor/react-native/emitter/EventHolder.js new file mode 100644 index 00000000..4c4c1d8d --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/emitter/EventHolder.js @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule EventHolder + * @flow + */ +'use strict'; + +import invariant from 'fbjs/lib/invariant'; + +class EventHolder { + + _heldEvents: Object; + _currentEventKey: ?Object; + + constructor() { + this._heldEvents = {}; + this._currentEventKey = null; + } + + /** + * Holds a given event for processing later. + * + * TODO: Annotate return type better. The structural type of the return here + * is pretty obvious. + * + * @param {string} eventType - Name of the event to hold and later emit + * @param {...*} Arbitrary arguments to be passed to each registered listener + * @return {object} Token that can be used to release the held event + * + * @example + * + * holder.holdEvent({someEvent: 'abc'}); + * + * holder.emitToHandler({ + * someEvent: function(data, event) { + * console.log(data); + * } + * }); //logs 'abc' + * + */ + holdEvent(eventType: string, ...args: any) { + this._heldEvents[eventType] = this._heldEvents[eventType] || []; + const eventsOfType = this._heldEvents[eventType]; + const key = { + eventType: eventType, + index: eventsOfType.length + }; + eventsOfType.push(args); + return key; + } + + /** + * Emits the held events of the specified type to the given listener. + * + * @param {?string} eventType - Optional name of the events to replay + * @param {function} listener - The listener to which to dispatch the event + * @param {?object} context - Optional context object to use when invoking + * the listener + */ + emitToListener(eventType: ?string , listener: Function, context: ?Object) { + const eventsOfType = this._heldEvents[eventType]; + if (!eventsOfType) { + return; + } + const origEventKey = this._currentEventKey; + eventsOfType.forEach((/*?array*/ eventHeld, /*number*/ index) => { + if (!eventHeld) { + return; + } + this._currentEventKey = { + eventType: eventType, + index: index + }; + listener.apply(context, eventHeld); + }); + this._currentEventKey = origEventKey; + } + + /** + * Provides an API that can be called during an eventing cycle to release + * the last event that was invoked, so that it is no longer "held". + * + * If it is called when not inside of an emitting cycle it will throw. + * + * @throws {Error} When called not during an eventing cycle + */ + releaseCurrentEvent() { + invariant( + this._currentEventKey !== null, + 'Not in an emitting cycle; there is no current event' + ); + this._currentEventKey && this.releaseEvent(this._currentEventKey); + } + + /** + * Releases the event corresponding to the handle that was returned when the + * event was first held. + * + * @param {object} token - The token returned from holdEvent + */ + releaseEvent(token: Object) { + delete this._heldEvents[token.eventType][token.index]; + } + + /** + * Releases all events of a certain type. + * + * @param {string} type + */ + releaseEventType(type: string) { + this._heldEvents[type] = []; + } +} + +export default EventHolder; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventSubscription.js b/packages/react-native-web/src/vendor/react-native/emitter/EventSubscription.js new file mode 100644 index 00000000..fcb6896b --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/emitter/EventSubscription.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule EventSubscription + * @flow + */ +'use strict'; + +import type EventSubscriptionVendor from './EventSubscriptionVendor'; + +/** + * EventSubscription represents a subscription to a particular event. It can + * remove its own subscription. + */ +class EventSubscription { + + eventType: string; + key: number; + subscriber: EventSubscriptionVendor; + + /** + * @param {EventSubscriptionVendor} subscriber the subscriber that controls + * this subscription. + */ + constructor(subscriber: EventSubscriptionVendor) { + this.subscriber = subscriber; + } + + /** + * Removes this subscription from the subscriber that controls it. + */ + remove() { + this.subscriber.removeSubscription(this); + } +} + +export default EventSubscription; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventSubscriptionVendor.js b/packages/react-native-web/src/vendor/react-native/emitter/EventSubscriptionVendor.js new file mode 100644 index 00000000..b01bd150 --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/emitter/EventSubscriptionVendor.js @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule EventSubscriptionVendor + * @flow + */ +'use strict'; + +import invariant from 'fbjs/lib/invariant'; + +import type EventSubscription from './EventSubscription'; + +/** + * EventSubscriptionVendor stores a set of EventSubscriptions that are + * subscribed to a particular event type. + */ +class EventSubscriptionVendor { + + _subscriptionsForType: Object; + _currentSubscription: ?EventSubscription; + + constructor() { + this._subscriptionsForType = {}; + this._currentSubscription = null; + } + + /** + * Adds a subscription keyed by an event type. + * + * @param {string} eventType + * @param {EventSubscription} subscription + */ + addSubscription( + eventType: string, subscription: EventSubscription): EventSubscription { + invariant( + subscription.subscriber === this, + 'The subscriber of the subscription is incorrectly set.'); + if (!this._subscriptionsForType[eventType]) { + this._subscriptionsForType[eventType] = []; + } + const key = this._subscriptionsForType[eventType].length; + this._subscriptionsForType[eventType].push(subscription); + subscription.eventType = eventType; + subscription.key = key; + return subscription; + } + + /** + * Removes a bulk set of the subscriptions. + * + * @param {?string} eventType - Optional name of the event type whose + * registered supscriptions to remove, if null remove all subscriptions. + */ + removeAllSubscriptions(eventType: ?string) { + if (eventType === undefined) { + this._subscriptionsForType = {}; + } else { + delete this._subscriptionsForType[eventType]; + } + } + + /** + * Removes a specific subscription. Instead of calling this function, call + * `subscription.remove()` directly. + * + * @param {object} subscription + */ + removeSubscription(subscription: Object) { + const eventType = subscription.eventType; + const key = subscription.key; + + const subscriptionsForType = this._subscriptionsForType[eventType]; + if (subscriptionsForType) { + delete subscriptionsForType[key]; + } + } + + /** + * Returns the array of subscriptions that are currently registered for the + * given event type. + * + * Note: This array can be potentially sparse as subscriptions are deleted + * from it when they are removed. + * + * TODO: This returns a nullable array. wat? + * + * @param {string} eventType + * @returns {?array} + */ + getSubscriptionsForType(eventType: string): ?[EventSubscription] { + return this._subscriptionsForType[eventType]; + } +} + +export default EventSubscriptionVendor; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/EventValidator.js b/packages/react-native-web/src/vendor/react-native/emitter/EventValidator.js new file mode 100644 index 00000000..485bfb61 --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/emitter/EventValidator.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule EventValidator + * @flow + */ +'use strict'; + +const __DEV__ = process.env.NODE_ENV !== 'production'; + +/** + * EventValidator is designed to validate event types to make it easier to catch + * common mistakes. It accepts a map of all of the different types of events + * that the emitter can emit. Then, if a user attempts to emit an event that is + * not one of those specified types the emitter will throw an error. Also, it + * provides a relatively simple matcher so that if it thinks that you likely + * mistyped the event name it will suggest what you might have meant to type in + * the error message. + */ +const EventValidator = { + /** + * @param {Object} emitter - The object responsible for emitting the actual + * events + * @param {Object} types - The collection of valid types that will be used to + * check for errors + * @return {Object} A new emitter with event type validation + * @example + * const types = {someEvent: true, anotherEvent: true}; + * const emitter = EventValidator.addValidation(emitter, types); + */ + addValidation: function(emitter: Object, types: Object) { + const eventTypes = Object.keys(types); + const emitterWithValidation = Object.create(emitter); + + Object.assign(emitterWithValidation, { + emit: function emit(type, a, b, c, d, e, _) { + assertAllowsEventType(type, eventTypes); + return emitter.emit.call(this, type, a, b, c, d, e, _); + } + }); + + return emitterWithValidation; + } +}; + +function assertAllowsEventType(type, allowedTypes) { + if (allowedTypes.indexOf(type) === -1) { + throw new TypeError(errorMessageFor(type, allowedTypes)); + } +} + +function errorMessageFor(type, allowedTypes) { + let message = 'Unknown event type "' + type + '". '; + if (__DEV__) { + message += recommendationFor(type, allowedTypes); + } + message += 'Known event types: ' + allowedTypes.join(', ') + '.'; + return message; +} + +// Allow for good error messages +if (__DEV__) { + var recommendationFor = function (type, allowedTypes) { + const closestTypeRecommendation = closestTypeFor(type, allowedTypes); + if (isCloseEnough(closestTypeRecommendation, type)) { + return 'Did you mean "' + closestTypeRecommendation.type + '"? '; + } else { + return ''; + } + }; + + var closestTypeFor = function (type, allowedTypes) { + const typeRecommendations = allowedTypes.map( + typeRecommendationFor.bind(this, type) + ); + return typeRecommendations.sort(recommendationSort)[0]; + }; + + var typeRecommendationFor = function (type, recommendedType) { + return { + type: recommendedType, + distance: damerauLevenshteinDistance(type, recommendedType) + }; + }; + + var recommendationSort = function (recommendationA, recommendationB) { + if (recommendationA.distance < recommendationB.distance) { + return -1; + } else if (recommendationA.distance > recommendationB.distance) { + return 1; + } else { + return 0; + } + }; + + var isCloseEnough = function (closestType, actualType) { + return (closestType.distance / actualType.length) < 0.334; + }; + + var damerauLevenshteinDistance = function (a, b) { + let i, j; + const d = []; + + for (i = 0; i <= a.length; i++) { + d[i] = [i]; + } + + for (j = 1; j <= b.length; j++) { + d[0][j] = j; + } + + for (i = 1; i <= a.length; i++) { + for (j = 1; j <= b.length; j++) { + const cost = a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1; + + d[i][j] = Math.min( + d[i - 1][j] + 1, + d[i][j - 1] + 1, + d[i - 1][j - 1] + cost + ); + + if (i > 1 && j > 1 && + a.charAt(i - 1) === b.charAt(j - 2) && + a.charAt(i - 2) === b.charAt(j - 1)) { + d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost); + } + } + } + + return d[a.length][b.length]; + }; +} + +export default EventValidator; diff --git a/packages/react-native-web/src/vendor/react-native/emitter/mixInEventEmitter.js b/packages/react-native-web/src/vendor/react-native/emitter/mixInEventEmitter.js new file mode 100644 index 00000000..c869880f --- /dev/null +++ b/packages/react-native-web/src/vendor/react-native/emitter/mixInEventEmitter.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2015-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. + * + * @providesModule mixInEventEmitter + * @flow + */ +'use strict'; + +import EventEmitter from './EventEmitter'; +import EventEmitterWithHolding from './EventEmitterWithHolding'; +import EventHolder from './EventHolder'; +import EventValidator from './EventValidator'; + +import invariant from 'fbjs/lib/invariant'; +/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error + * found when Flow v0.54 was deployed. To see the error delete this comment and + * run Flow. */ +import keyOf from 'fbjs/lib/keyOf'; + +import type EmitterSubscription from './EmitterSubscription'; + +const __DEV__ = process.env.NODE_ENV !== 'production'; +const TYPES_KEY = keyOf({__types: true}); + +/** + * API to setup an object or constructor to be able to emit data events. + * + * @example + * function Dog() { ...dog stuff... } + * mixInEventEmitter(Dog, {bark: true}); + * + * var puppy = new Dog(); + * puppy.addListener('bark', function (volume) { + * console.log('Puppy', this, 'barked at volume:', volume); + * }); + * puppy.emit('bark', 'quiet'); + * // Puppy barked at volume: quiet + * + * + * // A "singleton" object may also be commissioned: + * + * var Singleton = {}; + * mixInEventEmitter(Singleton, {lonely: true}); + * Singleton.emit('lonely', true); + */ +function mixInEventEmitter(cls: Function | Object, types: Object) { + invariant(types, 'Must supply set of valid event types'); + + // If this is a constructor, write to the prototype, otherwise write to the + // singleton object. + const target = cls.prototype || cls; + + invariant(!target.__eventEmitter, 'An active emitter is already mixed in'); + + const ctor = cls.constructor; + if (ctor) { + invariant( + ctor === Object || ctor === Function, + 'Mix EventEmitter into a class, not an instance' + ); + } + + // Keep track of the provided types, union the types if they already exist, + // which allows for prototype subclasses to provide more types. + if (target.hasOwnProperty(TYPES_KEY)) { + Object.assign(target.__types, types); + } else if (target.__types) { + target.__types = Object.assign({}, target.__types, types); + } else { + target.__types = types; + } + Object.assign(target, EventEmitterMixin); +} + +const EventEmitterMixin = { + emit: function(eventType, a, b, c, d, e, _) { + return this.__getEventEmitter().emit(eventType, a, b, c, d, e, _); + }, + + emitAndHold: function(eventType, a, b, c, d, e, _) { + return this.__getEventEmitter().emitAndHold(eventType, a, b, c, d, e, _); + }, + + addListener: function(eventType, listener, context): EmitterSubscription { + return this.__getEventEmitter().addListener(eventType, listener, context); + }, + + once: function(eventType, listener, context) { + return this.__getEventEmitter().once(eventType, listener, context); + }, + + addRetroactiveListener: function(eventType, listener, context) { + return this.__getEventEmitter().addRetroactiveListener( + eventType, + listener, + context + ); + }, + + addListenerMap: function(listenerMap, context) { + return this.__getEventEmitter().addListenerMap(listenerMap, context); + }, + + addRetroactiveListenerMap: function(listenerMap, context) { + return this.__getEventEmitter().addListenerMap(listenerMap, context); + }, + + removeAllListeners: function() { + this.__getEventEmitter().removeAllListeners(); + }, + + removeCurrentListener: function() { + this.__getEventEmitter().removeCurrentListener(); + }, + + releaseHeldEventType: function(eventType) { + this.__getEventEmitter().releaseHeldEventType(eventType); + }, + + __getEventEmitter: function() { + if (!this.__eventEmitter) { + let emitter = new EventEmitter(); + if (__DEV__) { + emitter = EventValidator.addValidation(emitter, this.__types); + } + + const holder = new EventHolder(); + this.__eventEmitter = new EventEmitterWithHolding(emitter, holder); + } + return this.__eventEmitter; + } +}; + +export default mixInEventEmitter;