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 clicksInDocument = fromEvent(document, 'click', true); // note optional configuration parameter
|
* // which will be passed to addEventListener
|
* const clicksInDiv = fromEvent(someDivInDocument, '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);
|
}
|