56daffca587e6b58a073631683e405bb39d91fb101d24dd39594d7f387f990b645f79f3ade86077dbea0c61157b6ca599f70ad004ff596b6ff17c5dbd18e17 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import { innerFrom } from '../observable/innerFrom';
  2. import { Observable } from '../Observable';
  3. import { mergeMap } from '../operators/mergeMap';
  4. import { isArrayLike } from '../util/isArrayLike';
  5. import { isFunction } from '../util/isFunction';
  6. import { mapOneOrManyArgs } from '../util/mapOneOrManyArgs';
  7. // These constants are used to create handler registry functions using array mapping below.
  8. const nodeEventEmitterMethods = ['addListener', 'removeListener'] as const;
  9. const eventTargetMethods = ['addEventListener', 'removeEventListener'] as const;
  10. const jqueryMethods = ['on', 'off'] as const;
  11. export interface NodeStyleEventEmitter {
  12. addListener(eventName: string | symbol, handler: NodeEventHandler): this;
  13. removeListener(eventName: string | symbol, handler: NodeEventHandler): this;
  14. }
  15. export type NodeEventHandler = (...args: any[]) => void;
  16. // For APIs that implement `addListener` and `removeListener` methods that may
  17. // not use the same arguments or return EventEmitter values
  18. // such as React Native
  19. export interface NodeCompatibleEventEmitter {
  20. addListener(eventName: string, handler: NodeEventHandler): void | {};
  21. removeListener(eventName: string, handler: NodeEventHandler): void | {};
  22. }
  23. // Use handler types like those in @types/jquery. See:
  24. // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/847731ba1d7fa6db6b911c0e43aa0afe596e7723/types/jquery/misc.d.ts#L6395
  25. export interface JQueryStyleEventEmitter<TContext, T> {
  26. on(eventName: string, handler: (this: TContext, t: T, ...args: any[]) => any): void;
  27. off(eventName: string, handler: (this: TContext, t: T, ...args: any[]) => any): void;
  28. }
  29. export interface EventListenerObject<E> {
  30. handleEvent(evt: E): void;
  31. }
  32. export interface HasEventTargetAddRemove<E> {
  33. addEventListener(
  34. type: string,
  35. listener: ((evt: E) => void) | EventListenerObject<E> | null,
  36. options?: boolean | AddEventListenerOptions
  37. ): void;
  38. removeEventListener(
  39. type: string,
  40. listener: ((evt: E) => void) | EventListenerObject<E> | null,
  41. options?: EventListenerOptions | boolean
  42. ): void;
  43. }
  44. export interface EventListenerOptions {
  45. capture?: boolean;
  46. passive?: boolean;
  47. once?: boolean;
  48. }
  49. export interface AddEventListenerOptions extends EventListenerOptions {
  50. once?: boolean;
  51. passive?: boolean;
  52. }
  53. export function fromEvent<T>(target: HasEventTargetAddRemove<T> | ArrayLike<HasEventTargetAddRemove<T>>, eventName: string): Observable<T>;
  54. export function fromEvent<T, R>(
  55. target: HasEventTargetAddRemove<T> | ArrayLike<HasEventTargetAddRemove<T>>,
  56. eventName: string,
  57. resultSelector: (event: T) => R
  58. ): Observable<R>;
  59. export function fromEvent<T>(
  60. target: HasEventTargetAddRemove<T> | ArrayLike<HasEventTargetAddRemove<T>>,
  61. eventName: string,
  62. options: EventListenerOptions
  63. ): Observable<T>;
  64. export function fromEvent<T, R>(
  65. target: HasEventTargetAddRemove<T> | ArrayLike<HasEventTargetAddRemove<T>>,
  66. eventName: string,
  67. options: EventListenerOptions,
  68. resultSelector: (event: T) => R
  69. ): Observable<R>;
  70. export function fromEvent(target: NodeStyleEventEmitter | ArrayLike<NodeStyleEventEmitter>, eventName: string): Observable<unknown>;
  71. /** @deprecated Do not specify explicit type parameters. Signatures with type parameters that cannot be inferred will be removed in v8. */
  72. export function fromEvent<T>(target: NodeStyleEventEmitter | ArrayLike<NodeStyleEventEmitter>, eventName: string): Observable<T>;
  73. export function fromEvent<R>(
  74. target: NodeStyleEventEmitter | ArrayLike<NodeStyleEventEmitter>,
  75. eventName: string,
  76. resultSelector: (...args: any[]) => R
  77. ): Observable<R>;
  78. export function fromEvent(
  79. target: NodeCompatibleEventEmitter | ArrayLike<NodeCompatibleEventEmitter>,
  80. eventName: string
  81. ): Observable<unknown>;
  82. /** @deprecated Do not specify explicit type parameters. Signatures with type parameters that cannot be inferred will be removed in v8. */
  83. export function fromEvent<T>(target: NodeCompatibleEventEmitter | ArrayLike<NodeCompatibleEventEmitter>, eventName: string): Observable<T>;
  84. export function fromEvent<R>(
  85. target: NodeCompatibleEventEmitter | ArrayLike<NodeCompatibleEventEmitter>,
  86. eventName: string,
  87. resultSelector: (...args: any[]) => R
  88. ): Observable<R>;
  89. export function fromEvent<T>(
  90. target: JQueryStyleEventEmitter<any, T> | ArrayLike<JQueryStyleEventEmitter<any, T>>,
  91. eventName: string
  92. ): Observable<T>;
  93. export function fromEvent<T, R>(
  94. target: JQueryStyleEventEmitter<any, T> | ArrayLike<JQueryStyleEventEmitter<any, T>>,
  95. eventName: string,
  96. resultSelector: (value: T, ...args: any[]) => R
  97. ): Observable<R>;
  98. /**
  99. * Creates an Observable that emits events of a specific type coming from the
  100. * given event target.
  101. *
  102. * <span class="informal">Creates an Observable from DOM events, or Node.js
  103. * EventEmitter events or others.</span>
  104. *
  105. * ![](fromEvent.png)
  106. *
  107. * `fromEvent` accepts as a first argument event target, which is an object with methods
  108. * for registering event handler functions. As a second argument it takes string that indicates
  109. * type of event we want to listen for. `fromEvent` supports selected types of event targets,
  110. * which are described in detail below. If your event target does not match any of the ones listed,
  111. * you should use {@link fromEventPattern}, which can be used on arbitrary APIs.
  112. * When it comes to APIs supported by `fromEvent`, their methods for adding and removing event
  113. * handler functions have different names, but they all accept a string describing event type
  114. * and function itself, which will be called whenever said event happens.
  115. *
  116. * Every time resulting Observable is subscribed, event handler function will be registered
  117. * to event target on given event type. When that event fires, value
  118. * passed as a first argument to registered function will be emitted by output Observable.
  119. * When Observable is unsubscribed, function will be unregistered from event target.
  120. *
  121. * Note that if event target calls registered function with more than one argument, second
  122. * and following arguments will not appear in resulting stream. In order to get access to them,
  123. * you can pass to `fromEvent` optional project function, which will be called with all arguments
  124. * passed to event handler. Output Observable will then emit value returned by project function,
  125. * instead of the usual value.
  126. *
  127. * Remember that event targets listed below are checked via duck typing. It means that
  128. * no matter what kind of object you have and no matter what environment you work in,
  129. * you can safely use `fromEvent` on that object if it exposes described methods (provided
  130. * of course they behave as was described above). So for example if Node.js library exposes
  131. * event target which has the same method names as DOM EventTarget, `fromEvent` is still
  132. * a good choice.
  133. *
  134. * If the API you use is more callback then event handler oriented (subscribed
  135. * callback function fires only once and thus there is no need to manually
  136. * unregister it), you should use {@link bindCallback} or {@link bindNodeCallback}
  137. * instead.
  138. *
  139. * `fromEvent` supports following types of event targets:
  140. *
  141. * **DOM EventTarget**
  142. *
  143. * This is an object with `addEventListener` and `removeEventListener` methods.
  144. *
  145. * In the browser, `addEventListener` accepts - apart from event type string and event
  146. * handler function arguments - optional third parameter, which is either an object or boolean,
  147. * both used for additional configuration how and when passed function will be called. When
  148. * `fromEvent` is used with event target of that type, you can provide this values
  149. * as third parameter as well.
  150. *
  151. * **Node.js EventEmitter**
  152. *
  153. * An object with `addListener` and `removeListener` methods.
  154. *
  155. * **JQuery-style event target**
  156. *
  157. * An object with `on` and `off` methods
  158. *
  159. * **DOM NodeList**
  160. *
  161. * List of DOM Nodes, returned for example by `document.querySelectorAll` or `Node.childNodes`.
  162. *
  163. * Although this collection is not event target in itself, `fromEvent` will iterate over all Nodes
  164. * it contains and install event handler function in every of them. When returned Observable
  165. * is unsubscribed, function will be removed from all Nodes.
  166. *
  167. * **DOM HtmlCollection**
  168. *
  169. * Just as in case of NodeList it is a collection of DOM nodes. Here as well event handler function is
  170. * installed and removed in each of elements.
  171. *
  172. *
  173. * ## Examples
  174. *
  175. * Emit clicks happening on the DOM document
  176. *
  177. * ```ts
  178. * import { fromEvent } from 'rxjs';
  179. *
  180. * const clicks = fromEvent(document, 'click');
  181. * clicks.subscribe(x => console.log(x));
  182. *
  183. * // Results in:
  184. * // MouseEvent object logged to console every time a click
  185. * // occurs on the document.
  186. * ```
  187. *
  188. * Use `addEventListener` with capture option
  189. *
  190. * ```ts
  191. * import { fromEvent } from 'rxjs';
  192. *
  193. * const div = document.createElement('div');
  194. * div.style.cssText = 'width: 200px; height: 200px; background: #09c;';
  195. * document.body.appendChild(div);
  196. *
  197. * // note optional configuration parameter which will be passed to addEventListener
  198. * const clicksInDocument = fromEvent(document, 'click', { capture: true });
  199. * const clicksInDiv = fromEvent(div, 'click');
  200. *
  201. * clicksInDocument.subscribe(() => console.log('document'));
  202. * clicksInDiv.subscribe(() => console.log('div'));
  203. *
  204. * // By default events bubble UP in DOM tree, so normally
  205. * // when we would click on div in document
  206. * // "div" would be logged first and then "document".
  207. * // Since we specified optional `capture` option, document
  208. * // will catch event when it goes DOWN DOM tree, so console
  209. * // will log "document" and then "div".
  210. * ```
  211. *
  212. * @see {@link bindCallback}
  213. * @see {@link bindNodeCallback}
  214. * @see {@link fromEventPattern}
  215. *
  216. * @param target The DOM EventTarget, Node.js EventEmitter, JQuery-like event target,
  217. * NodeList or HTMLCollection to attach the event handler to.
  218. * @param eventName The event name of interest, being emitted by the `target`.
  219. * @param options Options to pass through to the underlying `addListener`,
  220. * `addEventListener` or `on` functions.
  221. * @param resultSelector A mapping function used to transform events. It takes the
  222. * arguments from the event handler and should return a single value.
  223. * @return An Observable emitting events registered through `target`'s
  224. * listener handlers.
  225. */
  226. export function fromEvent<T>(
  227. target: any,
  228. eventName: string,
  229. options?: EventListenerOptions | ((...args: any[]) => T),
  230. resultSelector?: (...args: any[]) => T
  231. ): Observable<T> {
  232. if (isFunction(options)) {
  233. resultSelector = options;
  234. options = undefined;
  235. }
  236. if (resultSelector) {
  237. return fromEvent<T>(target, eventName, options as EventListenerOptions).pipe(mapOneOrManyArgs(resultSelector));
  238. }
  239. // Figure out our add and remove methods. In order to do this,
  240. // we are going to analyze the target in a preferred order, if
  241. // the target matches a given signature, we take the two "add" and "remove"
  242. // method names and apply them to a map to create opposite versions of the
  243. // same function. This is because they all operate in duplicate pairs,
  244. // `addListener(name, handler)`, `removeListener(name, handler)`, for example.
  245. // The call only differs by method name, as to whether or not you're adding or removing.
  246. const [add, remove] =
  247. // If it is an EventTarget, we need to use a slightly different method than the other two patterns.
  248. isEventTarget(target)
  249. ? eventTargetMethods.map((methodName) => (handler: any) => target[methodName](eventName, handler, options as EventListenerOptions))
  250. : // In all other cases, the call pattern is identical with the exception of the method names.
  251. isNodeStyleEventEmitter(target)
  252. ? nodeEventEmitterMethods.map(toCommonHandlerRegistry(target, eventName))
  253. : isJQueryStyleEventEmitter(target)
  254. ? jqueryMethods.map(toCommonHandlerRegistry(target, eventName))
  255. : [];
  256. // If add is falsy, it's because we didn't match a pattern above.
  257. // Check to see if it is an ArrayLike, because if it is, we want to
  258. // try to apply fromEvent to all of it's items. We do this check last,
  259. // because there are may be some types that are both ArrayLike *and* implement
  260. // event registry points, and we'd rather delegate to that when possible.
  261. if (!add) {
  262. if (isArrayLike(target)) {
  263. return mergeMap((subTarget: any) => fromEvent(subTarget, eventName, options as EventListenerOptions))(
  264. innerFrom(target)
  265. ) as Observable<T>;
  266. }
  267. }
  268. // If add is falsy and we made it here, it's because we didn't
  269. // match any valid target objects above.
  270. if (!add) {
  271. throw new TypeError('Invalid event target');
  272. }
  273. return new Observable<T>((subscriber) => {
  274. // The handler we are going to register. Forwards the event object, by itself, or
  275. // an array of arguments to the event handler, if there is more than one argument,
  276. // to the consumer.
  277. const handler = (...args: any[]) => subscriber.next(1 < args.length ? args : args[0]);
  278. // Do the work of adding the handler to the target.
  279. add(handler);
  280. // When we finalize, we want to remove the handler and free up memory.
  281. return () => remove!(handler);
  282. });
  283. }
  284. /**
  285. * Used to create `add` and `remove` functions to register and unregister event handlers
  286. * from a target in the most common handler pattern, where there are only two arguments.
  287. * (e.g. `on(name, fn)`, `off(name, fn)`, `addListener(name, fn)`, or `removeListener(name, fn)`)
  288. * @param target The target we're calling methods on
  289. * @param eventName The event name for the event we're creating register or unregister functions for
  290. */
  291. function toCommonHandlerRegistry(target: any, eventName: string) {
  292. return (methodName: string) => (handler: any) => target[methodName](eventName, handler);
  293. }
  294. /**
  295. * Checks to see if the target implements the required node-style EventEmitter methods
  296. * for adding and removing event handlers.
  297. * @param target the object to check
  298. */
  299. function isNodeStyleEventEmitter(target: any): target is NodeStyleEventEmitter {
  300. return isFunction(target.addListener) && isFunction(target.removeListener);
  301. }
  302. /**
  303. * Checks to see if the target implements the required jQuery-style EventEmitter methods
  304. * for adding and removing event handlers.
  305. * @param target the object to check
  306. */
  307. function isJQueryStyleEventEmitter(target: any): target is JQueryStyleEventEmitter<any, any> {
  308. return isFunction(target.on) && isFunction(target.off);
  309. }
  310. /**
  311. * Checks to see if the target implements the required EventTarget methods
  312. * for adding and removing event handlers.
  313. * @param target the object to check
  314. */
  315. function isEventTarget(target: any): target is HasEventTargetAddRemove<any> {
  316. return isFunction(target.addEventListener) && isFunction(target.removeEventListener);
  317. }