| import { innerFrom } from '../observable/innerFrom'; |
| import { Observable } from '../Observable'; |
| import { mergeMap } from '../operators/mergeMap'; |
| import { isArrayLike } from '../util/isArrayLike'; |
| import { isFunction } from '../util/isFunction'; |
| import { mapOneOrManyArgs } from '../util/mapOneOrManyArgs'; |
| |
| // These constants are used to create handler registry functions using array mapping below. |
| const nodeEventEmitterMethods = ['addListener', 'removeListener'] as const; |
| const eventTargetMethods = ['addEventListener', 'removeEventListener'] as const; |
| const jqueryMethods = ['on', 'off'] as const; |
| |
| export interface NodeStyleEventEmitter { |
| addListener(eventName: string | symbol, handler: NodeEventHandler): this; |
| removeListener(eventName: string | symbol, handler: NodeEventHandler): this; |
| } |
| |
| export type NodeEventHandler = (...args: any[]) => void; |
| |
| // For APIs that implement `addListener` and `removeListener` methods that may |
| // not use the same arguments or return EventEmitter values |
| // such as React Native |
| export interface NodeCompatibleEventEmitter { |
| addListener(eventName: string, handler: NodeEventHandler): void | {}; |
| removeListener(eventName: string, handler: NodeEventHandler): void | {}; |
| } |
| |
| // Use handler types like those in @types/jquery. See: |
| // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/847731ba1d7fa6db6b911c0e43aa0afe596e7723/types/jquery/misc.d.ts#L6395 |
| export interface JQueryStyleEventEmitter<TContext, T> { |
| on(eventName: string, handler: (this: TContext, t: T, ...args: any[]) => any): void; |
| off(eventName: string, handler: (this: TContext, t: T, ...args: any[]) => any): void; |
| } |
| |
| export interface EventListenerObject<E> { |
| handleEvent(evt: E): void; |
| } |
| |
| export interface HasEventTargetAddRemove<E> { |
| addEventListener( |
| type: string, |
| listener: ((evt: E) => void) | EventListenerObject<E> | null, |
| options?: boolean | AddEventListenerOptions |
| ): void; |
| removeEventListener( |
| type: string, |
| listener: ((evt: E) => void) | EventListenerObject<E> | null, |
| options?: EventListenerOptions | boolean |
| ): void; |
| } |
| |
| export interface EventListenerOptions { |
| capture?: boolean; |
| passive?: boolean; |
| once?: boolean; |
| } |
| |
| export interface AddEventListenerOptions extends EventListenerOptions { |
| once?: boolean; |
| passive?: boolean; |
| } |
| |
| export function fromEvent<T>(target: HasEventTargetAddRemove<T> | ArrayLike<HasEventTargetAddRemove<T>>, eventName: string): Observable<T>; |
| export function fromEvent<T, R>( |
| target: HasEventTargetAddRemove<T> | ArrayLike<HasEventTargetAddRemove<T>>, |
| eventName: string, |
| resultSelector: (event: T) => R |
| ): Observable<R>; |
| export function fromEvent<T>( |
| target: HasEventTargetAddRemove<T> | ArrayLike<HasEventTargetAddRemove<T>>, |
| eventName: string, |
| options: EventListenerOptions |
| ): Observable<T>; |
| export function fromEvent<T, R>( |
| target: HasEventTargetAddRemove<T> | ArrayLike<HasEventTargetAddRemove<T>>, |
| eventName: string, |
| options: EventListenerOptions, |
| resultSelector: (event: T) => R |
| ): Observable<R>; |
| |
| export function fromEvent(target: NodeStyleEventEmitter | ArrayLike<NodeStyleEventEmitter>, eventName: string): Observable<unknown>; |
| /** @deprecated Do not specify explicit type parameters. Signatures with type parameters that cannot be inferred will be removed in v8. */ |
| export function fromEvent<T>(target: NodeStyleEventEmitter | ArrayLike<NodeStyleEventEmitter>, eventName: string): Observable<T>; |
| export function fromEvent<R>( |
| target: NodeStyleEventEmitter | ArrayLike<NodeStyleEventEmitter>, |
| eventName: string, |
| resultSelector: (...args: any[]) => R |
| ): Observable<R>; |
| |
| export function fromEvent( |
| target: NodeCompatibleEventEmitter | ArrayLike<NodeCompatibleEventEmitter>, |
| eventName: string |
| ): Observable<unknown>; |
| /** @deprecated Do not specify explicit type parameters. Signatures with type parameters that cannot be inferred will be removed in v8. */ |
| export function fromEvent<T>(target: NodeCompatibleEventEmitter | ArrayLike<NodeCompatibleEventEmitter>, eventName: string): Observable<T>; |
| export function fromEvent<R>( |
| target: NodeCompatibleEventEmitter | ArrayLike<NodeCompatibleEventEmitter>, |
| eventName: string, |
| resultSelector: (...args: any[]) => R |
| ): Observable<R>; |
| |
| export function fromEvent<T>( |
| target: JQueryStyleEventEmitter<any, T> | ArrayLike<JQueryStyleEventEmitter<any, T>>, |
| eventName: string |
| ): Observable<T>; |
| export function fromEvent<T, R>( |
| target: JQueryStyleEventEmitter<any, T> | ArrayLike<JQueryStyleEventEmitter<any, T>>, |
| eventName: string, |
| resultSelector: (value: T, ...args: any[]) => R |
| ): Observable<R>; |
| |
| /** |
| * Creates an Observable that emits events of a specific type coming from the |
| * given event target. |
| * |
| * <span class="informal">Creates an Observable from DOM events, or Node.js |
| * EventEmitter events or others.</span> |
| * |
| *  |
| * |
| * `fromEvent` accepts as a first argument event target, which is an object with methods |
| * for registering event handler functions. As a second argument it takes string that indicates |
| * type of event we want to listen for. `fromEvent` supports selected types of event targets, |
| * which are described in detail below. If your event target does not match any of the ones listed, |
| * you should use {@link fromEventPattern}, which can be used on arbitrary APIs. |
| * When it comes to APIs supported by `fromEvent`, their methods for adding and removing event |
| * handler functions have different names, but they all accept a string describing event type |
| * and function itself, which will be called whenever said event happens. |
| * |
| * Every time resulting Observable is subscribed, event handler function will be registered |
| * to event target on given event type. When that event fires, value |
| * passed as a first argument to registered function will be emitted by output Observable. |
| * When Observable is unsubscribed, function will be unregistered from event target. |
| * |
| * Note that if event target calls registered function with more than one argument, second |
| * and following arguments will not appear in resulting stream. In order to get access to them, |
| * you can pass to `fromEvent` optional project function, which will be called with all arguments |
| * passed to event handler. Output Observable will then emit value returned by project function, |
| * instead of the usual value. |
| * |
| * Remember that event targets listed below are checked via duck typing. It means that |
| * no matter what kind of object you have and no matter what environment you work in, |
| * you can safely use `fromEvent` on that object if it exposes described methods (provided |
| * of course they behave as was described above). So for example if Node.js library exposes |
| * event target which has the same method names as DOM EventTarget, `fromEvent` is still |
| * a good choice. |
| * |
| * If the API you use is more callback then event handler oriented (subscribed |
| * callback function fires only once and thus there is no need to manually |
| * unregister it), you should use {@link bindCallback} or {@link bindNodeCallback} |
| * instead. |
| * |
| * `fromEvent` supports following types of event targets: |
| * |
| * **DOM EventTarget** |
| * |
| * This is an object with `addEventListener` and `removeEventListener` methods. |
| * |
| * In the browser, `addEventListener` accepts - apart from event type string and event |
| * handler function arguments - optional third parameter, which is either an object or boolean, |
| * both used for additional configuration how and when passed function will be called. When |
| * `fromEvent` is used with event target of that type, you can provide this values |
| * as third parameter as well. |
| * |
| * **Node.js EventEmitter** |
| * |
| * An object with `addListener` and `removeListener` methods. |
| * |
| * **JQuery-style event target** |
| * |
| * An object with `on` and `off` methods |
| * |
| * **DOM NodeList** |
| * |
| * List of DOM Nodes, returned for example by `document.querySelectorAll` or `Node.childNodes`. |
| * |
| * Although this collection is not event target in itself, `fromEvent` will iterate over all Nodes |
| * it contains and install event handler function in every of them. When returned Observable |
| * is unsubscribed, function will be removed from all Nodes. |
| * |
| * **DOM HtmlCollection** |
| * |
| * Just as in case of NodeList it is a collection of DOM nodes. Here as well event handler function is |
| * installed and removed in each of elements. |
| * |
| * |
| * ## Examples |
| * |
| * Emit clicks happening on the DOM document |
| * |
| * ```ts |
| * import { fromEvent } from 'rxjs'; |
| * |
| * const clicks = fromEvent(document, 'click'); |
| * clicks.subscribe(x => console.log(x)); |
| * |
| * // Results in: |
| * // MouseEvent object logged to console every time a click |
| * // occurs on the document. |
| * ``` |
| * |
| * Use `addEventListener` with capture option |
| * |
| * ```ts |
| * import { fromEvent } from 'rxjs'; |
| * |
| * const div = document.createElement('div'); |
| * div.style.cssText = 'width: 200px; height: 200px; background: #09c;'; |
| * document.body.appendChild(div); |
| * |
| * // note optional configuration parameter which will be passed to addEventListener |
| * const clicksInDocument = fromEvent(document, 'click', { capture: true }); |
| * const clicksInDiv = fromEvent(div, 'click'); |
| * |
| * clicksInDocument.subscribe(() => console.log('document')); |
| * clicksInDiv.subscribe(() => console.log('div')); |
| * |
| * // By default events bubble UP in DOM tree, so normally |
| * // when we would click on div in document |
| * // "div" would be logged first and then "document". |
| * // Since we specified optional `capture` option, document |
| * // will catch event when it goes DOWN DOM tree, so console |
| * // will log "document" and then "div". |
| * ``` |
| * |
| * @see {@link bindCallback} |
| * @see {@link bindNodeCallback} |
| * @see {@link fromEventPattern} |
| * |
| * @param {FromEventTarget<T>} target The DOM EventTarget, Node.js |
| * EventEmitter, JQuery-like event target, NodeList or HTMLCollection to attach the event handler to. |
| * @param {string} eventName The event name of interest, being emitted by the |
| * `target`. |
| * @param {EventListenerOptions} [options] Options to pass through to addEventListener |
| * @return {Observable<T>} |
| */ |
| export function fromEvent<T>( |
| target: any, |
| eventName: string, |
| options?: EventListenerOptions | ((...args: any[]) => T), |
| resultSelector?: (...args: any[]) => T |
| ): Observable<T> { |
| if (isFunction(options)) { |
| resultSelector = options; |
| options = undefined; |
| } |
| if (resultSelector) { |
| return fromEvent<T>(target, eventName, options as EventListenerOptions).pipe(mapOneOrManyArgs(resultSelector)); |
| } |
| |
| // Figure out our add and remove methods. In order to do this, |
| // we are going to analyze the target in a preferred order, if |
| // the target matches a given signature, we take the two "add" and "remove" |
| // method names and apply them to a map to create opposite versions of the |
| // same function. This is because they all operate in duplicate pairs, |
| // `addListener(name, handler)`, `removeListener(name, handler)`, for example. |
| // The call only differs by method name, as to whether or not you're adding or removing. |
| const [add, remove] = |
| // If it is an EventTarget, we need to use a slightly different method than the other two patterns. |
| isEventTarget(target) |
| ? eventTargetMethods.map((methodName) => (handler: any) => target[methodName](eventName, handler, options as EventListenerOptions)) |
| : // In all other cases, the call pattern is identical with the exception of the method names. |
| isNodeStyleEventEmitter(target) |
| ? nodeEventEmitterMethods.map(toCommonHandlerRegistry(target, eventName)) |
| : isJQueryStyleEventEmitter(target) |
| ? jqueryMethods.map(toCommonHandlerRegistry(target, eventName)) |
| : []; |
| |
| // If add is falsy, it's because we didn't match a pattern above. |
| // Check to see if it is an ArrayLike, because if it is, we want to |
| // try to apply fromEvent to all of it's items. We do this check last, |
| // because there are may be some types that are both ArrayLike *and* implement |
| // event registry points, and we'd rather delegate to that when possible. |
| if (!add) { |
| if (isArrayLike(target)) { |
| return mergeMap((subTarget: any) => fromEvent(subTarget, eventName, options as EventListenerOptions))( |
| innerFrom(target) |
| ) as Observable<T>; |
| } |
| } |
| |
| // If add is falsy and we made it here, it's because we didn't |
| // match any valid target objects above. |
| if (!add) { |
| throw new TypeError('Invalid event target'); |
| } |
| |
| return new Observable<T>((subscriber) => { |
| // The handler we are going to register. Forwards the event object, by itself, or |
| // an array of arguments to the event handler, if there is more than one argument, |
| // to the consumer. |
| const handler = (...args: any[]) => subscriber.next(1 < args.length ? args : args[0]); |
| // Do the work of adding the handler to the target. |
| add(handler); |
| // When we finalize, we want to remove the handler and free up memory. |
| return () => remove!(handler); |
| }); |
| } |
| |
| /** |
| * Used to create `add` and `remove` functions to register and unregister event handlers |
| * from a target in the most common handler pattern, where there are only two arguments. |
| * (e.g. `on(name, fn)`, `off(name, fn)`, `addListener(name, fn)`, or `removeListener(name, fn)`) |
| * @param target The target we're calling methods on |
| * @param eventName The event name for the event we're creating register or unregister functions for |
| */ |
| function toCommonHandlerRegistry(target: any, eventName: string) { |
| return (methodName: string) => (handler: any) => target[methodName](eventName, handler); |
| } |
| |
| /** |
| * Checks to see if the target implements the required node-style EventEmitter methods |
| * for adding and removing event handlers. |
| * @param target the object to check |
| */ |
| function isNodeStyleEventEmitter(target: any): target is NodeStyleEventEmitter { |
| return isFunction(target.addListener) && isFunction(target.removeListener); |
| } |
| |
| /** |
| * Checks to see if the target implements the required jQuery-style EventEmitter methods |
| * for adding and removing event handlers. |
| * @param target the object to check |
| */ |
| function isJQueryStyleEventEmitter(target: any): target is JQueryStyleEventEmitter<any, any> { |
| return isFunction(target.on) && isFunction(target.off); |
| } |
| |
| /** |
| * Checks to see if the target implements the required EventTarget methods |
| * for adding and removing event handlers. |
| * @param target the object to check |
| */ |
| function isEventTarget(target: any): target is HasEventTargetAddRemove<any> { |
| return isFunction(target.addEventListener) && isFunction(target.removeEventListener); |
| } |