[6a3a178] | 1 | import { Observable } from '../Observable';
|
---|
| 2 | import { isArray } from '../util/isArray';
|
---|
| 3 | import { isFunction } from '../util/isFunction';
|
---|
| 4 | import { Subscriber } from '../Subscriber';
|
---|
| 5 | import { map } from '../operators/map';
|
---|
| 6 |
|
---|
| 7 | const toString: Function = (() => Object.prototype.toString)();
|
---|
| 8 |
|
---|
| 9 | export interface NodeStyleEventEmitter {
|
---|
| 10 | addListener: (eventName: string | symbol, handler: NodeEventHandler) => this;
|
---|
| 11 | removeListener: (eventName: string | symbol, handler: NodeEventHandler) => this;
|
---|
| 12 | }
|
---|
| 13 |
|
---|
| 14 | export type NodeEventHandler = (...args: any[]) => void;
|
---|
| 15 |
|
---|
| 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 |
|
---|
| 24 | export interface JQueryStyleEventEmitter {
|
---|
| 25 | on: (eventName: string, handler: Function) => void;
|
---|
| 26 | off: (eventName: string, handler: Function) => void;
|
---|
| 27 | }
|
---|
| 28 |
|
---|
| 29 | export interface HasEventTargetAddRemove<E> {
|
---|
| 30 | addEventListener(type: string, listener: ((evt: E) => void) | null, options?: boolean | AddEventListenerOptions): void;
|
---|
| 31 | removeEventListener(type: string, listener?: ((evt: E) => void) | null, options?: EventListenerOptions | boolean): void;
|
---|
| 32 | }
|
---|
| 33 |
|
---|
| 34 | export type EventTargetLike<T> = HasEventTargetAddRemove<T> | NodeStyleEventEmitter | NodeCompatibleEventEmitter | JQueryStyleEventEmitter;
|
---|
| 35 |
|
---|
| 36 | export type FromEventTarget<T> = EventTargetLike<T> | ArrayLike<EventTargetLike<T>>;
|
---|
| 37 |
|
---|
| 38 | export interface EventListenerOptions {
|
---|
| 39 | capture?: boolean;
|
---|
| 40 | passive?: boolean;
|
---|
| 41 | once?: boolean;
|
---|
| 42 | }
|
---|
| 43 |
|
---|
| 44 | export interface AddEventListenerOptions extends EventListenerOptions {
|
---|
| 45 | once?: boolean;
|
---|
| 46 | passive?: boolean;
|
---|
| 47 | }
|
---|
| 48 |
|
---|
| 49 | /* tslint:disable:max-line-length */
|
---|
| 50 | export function fromEvent<T>(target: FromEventTarget<T>, eventName: string): Observable<T>;
|
---|
| 51 | /** @deprecated resultSelector no longer supported, pipe to map instead */
|
---|
| 52 | export function fromEvent<T>(target: FromEventTarget<T>, eventName: string, resultSelector: (...args: any[]) => T): Observable<T>;
|
---|
| 53 | export function fromEvent<T>(target: FromEventTarget<T>, eventName: string, options: EventListenerOptions): Observable<T>;
|
---|
| 54 | /** @deprecated resultSelector no longer supported, pipe to map instead */
|
---|
| 55 | export function fromEvent<T>(target: FromEventTarget<T>, eventName: string, options: EventListenerOptions, resultSelector: (...args: any[]) => T): Observable<T>;
|
---|
| 56 | /* tslint:enable:max-line-length */
|
---|
| 57 |
|
---|
| 58 | /**
|
---|
| 59 | * Creates an Observable that emits events of a specific type coming from the
|
---|
| 60 | * given event target.
|
---|
| 61 | *
|
---|
| 62 | * <span class="informal">Creates an Observable from DOM events, or Node.js
|
---|
| 63 | * EventEmitter events or others.</span>
|
---|
| 64 | *
|
---|
| 65 | * ![](fromEvent.png)
|
---|
| 66 | *
|
---|
| 67 | * `fromEvent` accepts as a first argument event target, which is an object with methods
|
---|
| 68 | * for registering event handler functions. As a second argument it takes string that indicates
|
---|
| 69 | * type of event we want to listen for. `fromEvent` supports selected types of event targets,
|
---|
| 70 | * which are described in detail below. If your event target does not match any of the ones listed,
|
---|
| 71 | * you should use {@link fromEventPattern}, which can be used on arbitrary APIs.
|
---|
| 72 | * When it comes to APIs supported by `fromEvent`, their methods for adding and removing event
|
---|
| 73 | * handler functions have different names, but they all accept a string describing event type
|
---|
| 74 | * and function itself, which will be called whenever said event happens.
|
---|
| 75 | *
|
---|
| 76 | * Every time resulting Observable is subscribed, event handler function will be registered
|
---|
| 77 | * to event target on given event type. When that event fires, value
|
---|
| 78 | * passed as a first argument to registered function will be emitted by output Observable.
|
---|
| 79 | * When Observable is unsubscribed, function will be unregistered from event target.
|
---|
| 80 | *
|
---|
| 81 | * Note that if event target calls registered function with more than one argument, second
|
---|
| 82 | * and following arguments will not appear in resulting stream. In order to get access to them,
|
---|
| 83 | * you can pass to `fromEvent` optional project function, which will be called with all arguments
|
---|
| 84 | * passed to event handler. Output Observable will then emit value returned by project function,
|
---|
| 85 | * instead of the usual value.
|
---|
| 86 | *
|
---|
| 87 | * Remember that event targets listed below are checked via duck typing. It means that
|
---|
| 88 | * no matter what kind of object you have and no matter what environment you work in,
|
---|
| 89 | * you can safely use `fromEvent` on that object if it exposes described methods (provided
|
---|
| 90 | * of course they behave as was described above). So for example if Node.js library exposes
|
---|
| 91 | * event target which has the same method names as DOM EventTarget, `fromEvent` is still
|
---|
| 92 | * a good choice.
|
---|
| 93 | *
|
---|
| 94 | * If the API you use is more callback then event handler oriented (subscribed
|
---|
| 95 | * callback function fires only once and thus there is no need to manually
|
---|
| 96 | * unregister it), you should use {@link bindCallback} or {@link bindNodeCallback}
|
---|
| 97 | * instead.
|
---|
| 98 | *
|
---|
| 99 | * `fromEvent` supports following types of event targets:
|
---|
| 100 | *
|
---|
| 101 | * **DOM EventTarget**
|
---|
| 102 | *
|
---|
| 103 | * This is an object with `addEventListener` and `removeEventListener` methods.
|
---|
| 104 | *
|
---|
| 105 | * In the browser, `addEventListener` accepts - apart from event type string and event
|
---|
| 106 | * handler function arguments - optional third parameter, which is either an object or boolean,
|
---|
| 107 | * both used for additional configuration how and when passed function will be called. When
|
---|
| 108 | * `fromEvent` is used with event target of that type, you can provide this values
|
---|
| 109 | * as third parameter as well.
|
---|
| 110 | *
|
---|
| 111 | * **Node.js EventEmitter**
|
---|
| 112 | *
|
---|
| 113 | * An object with `addListener` and `removeListener` methods.
|
---|
| 114 | *
|
---|
| 115 | * **JQuery-style event target**
|
---|
| 116 | *
|
---|
| 117 | * An object with `on` and `off` methods
|
---|
| 118 | *
|
---|
| 119 | * **DOM NodeList**
|
---|
| 120 | *
|
---|
| 121 | * List of DOM Nodes, returned for example by `document.querySelectorAll` or `Node.childNodes`.
|
---|
| 122 | *
|
---|
| 123 | * Although this collection is not event target in itself, `fromEvent` will iterate over all Nodes
|
---|
| 124 | * it contains and install event handler function in every of them. When returned Observable
|
---|
| 125 | * is unsubscribed, function will be removed from all Nodes.
|
---|
| 126 | *
|
---|
| 127 | * **DOM HtmlCollection**
|
---|
| 128 | *
|
---|
| 129 | * Just as in case of NodeList it is a collection of DOM nodes. Here as well event handler function is
|
---|
| 130 | * installed and removed in each of elements.
|
---|
| 131 | *
|
---|
| 132 | *
|
---|
| 133 | * ## Examples
|
---|
| 134 | * ### Emits clicks happening on the DOM document
|
---|
| 135 | * ```ts
|
---|
| 136 | * import { fromEvent } from 'rxjs';
|
---|
| 137 | *
|
---|
| 138 | * const clicks = fromEvent(document, 'click');
|
---|
| 139 | * clicks.subscribe(x => console.log(x));
|
---|
| 140 | *
|
---|
| 141 | * // Results in:
|
---|
| 142 | * // MouseEvent object logged to console every time a click
|
---|
| 143 | * // occurs on the document.
|
---|
| 144 | * ```
|
---|
| 145 | *
|
---|
| 146 | * ### Use addEventListener with capture option
|
---|
| 147 | * ```ts
|
---|
| 148 | * import { fromEvent } from 'rxjs';
|
---|
| 149 | *
|
---|
| 150 | * const clicksInDocument = fromEvent(document, 'click', true); // note optional configuration parameter
|
---|
| 151 | * // which will be passed to addEventListener
|
---|
| 152 | * const clicksInDiv = fromEvent(someDivInDocument, 'click');
|
---|
| 153 | *
|
---|
| 154 | * clicksInDocument.subscribe(() => console.log('document'));
|
---|
| 155 | * clicksInDiv.subscribe(() => console.log('div'));
|
---|
| 156 | *
|
---|
| 157 | * // By default events bubble UP in DOM tree, so normally
|
---|
| 158 | * // when we would click on div in document
|
---|
| 159 | * // "div" would be logged first and then "document".
|
---|
| 160 | * // Since we specified optional `capture` option, document
|
---|
| 161 | * // will catch event when it goes DOWN DOM tree, so console
|
---|
| 162 | * // will log "document" and then "div".
|
---|
| 163 | * ```
|
---|
| 164 | *
|
---|
| 165 | * @see {@link bindCallback}
|
---|
| 166 | * @see {@link bindNodeCallback}
|
---|
| 167 | * @see {@link fromEventPattern}
|
---|
| 168 | *
|
---|
| 169 | * @param {FromEventTarget<T>} target The DOM EventTarget, Node.js
|
---|
| 170 | * EventEmitter, JQuery-like event target, NodeList or HTMLCollection to attach the event handler to.
|
---|
| 171 | * @param {string} eventName The event name of interest, being emitted by the
|
---|
| 172 | * `target`.
|
---|
| 173 | * @param {EventListenerOptions} [options] Options to pass through to addEventListener
|
---|
| 174 | * @return {Observable<T>}
|
---|
| 175 | * @name fromEvent
|
---|
| 176 | */
|
---|
| 177 | export function fromEvent<T>(
|
---|
| 178 | target: FromEventTarget<T>,
|
---|
| 179 | eventName: string,
|
---|
| 180 | options?: EventListenerOptions | ((...args: any[]) => T),
|
---|
| 181 | resultSelector?: ((...args: any[]) => T)
|
---|
| 182 | ): Observable<T> {
|
---|
| 183 |
|
---|
| 184 | if (isFunction(options)) {
|
---|
| 185 | // DEPRECATED PATH
|
---|
| 186 | resultSelector = options;
|
---|
| 187 | options = undefined;
|
---|
| 188 | }
|
---|
| 189 | if (resultSelector) {
|
---|
| 190 | // DEPRECATED PATH
|
---|
| 191 | return fromEvent<T>(target, eventName, <EventListenerOptions | undefined>options).pipe(
|
---|
| 192 | map(args => isArray(args) ? resultSelector(...args) : resultSelector(args))
|
---|
| 193 | );
|
---|
| 194 | }
|
---|
| 195 |
|
---|
| 196 | return new Observable<T>(subscriber => {
|
---|
| 197 | function handler(e: T) {
|
---|
| 198 | if (arguments.length > 1) {
|
---|
| 199 | subscriber.next(Array.prototype.slice.call(arguments));
|
---|
| 200 | } else {
|
---|
| 201 | subscriber.next(e);
|
---|
| 202 | }
|
---|
| 203 | }
|
---|
| 204 | setupSubscription(target, eventName, handler, subscriber, options as EventListenerOptions);
|
---|
| 205 | });
|
---|
| 206 | }
|
---|
| 207 |
|
---|
| 208 | function setupSubscription<T>(sourceObj: FromEventTarget<T>, eventName: string,
|
---|
| 209 | handler: (...args: any[]) => void, subscriber: Subscriber<T>,
|
---|
| 210 | options?: EventListenerOptions) {
|
---|
| 211 | let unsubscribe: () => void;
|
---|
| 212 | if (isEventTarget(sourceObj)) {
|
---|
| 213 | const source = sourceObj;
|
---|
| 214 | sourceObj.addEventListener(eventName, handler, options);
|
---|
| 215 | unsubscribe = () => source.removeEventListener(eventName, handler, options);
|
---|
| 216 | } else if (isJQueryStyleEventEmitter(sourceObj)) {
|
---|
| 217 | const source = sourceObj;
|
---|
| 218 | sourceObj.on(eventName, handler);
|
---|
| 219 | unsubscribe = () => source.off(eventName, handler);
|
---|
| 220 | } else if (isNodeStyleEventEmitter(sourceObj)) {
|
---|
| 221 | const source = sourceObj;
|
---|
| 222 | sourceObj.addListener(eventName, handler as NodeEventHandler);
|
---|
| 223 | unsubscribe = () => source.removeListener(eventName, handler as NodeEventHandler);
|
---|
| 224 | } else if (sourceObj && (sourceObj as any).length) {
|
---|
| 225 | for (let i = 0, len = (sourceObj as any).length; i < len; i++) {
|
---|
| 226 | setupSubscription(sourceObj[i], eventName, handler, subscriber, options);
|
---|
| 227 | }
|
---|
| 228 | } else {
|
---|
| 229 | throw new TypeError('Invalid event target');
|
---|
| 230 | }
|
---|
| 231 |
|
---|
| 232 | subscriber.add(unsubscribe);
|
---|
| 233 | }
|
---|
| 234 |
|
---|
| 235 | function isNodeStyleEventEmitter(sourceObj: any): sourceObj is NodeStyleEventEmitter {
|
---|
| 236 | return sourceObj && typeof sourceObj.addListener === 'function' && typeof sourceObj.removeListener === 'function';
|
---|
| 237 | }
|
---|
| 238 |
|
---|
| 239 | function isJQueryStyleEventEmitter(sourceObj: any): sourceObj is JQueryStyleEventEmitter {
|
---|
| 240 | return sourceObj && typeof sourceObj.on === 'function' && typeof sourceObj.off === 'function';
|
---|
| 241 | }
|
---|
| 242 |
|
---|
| 243 | function isEventTarget(sourceObj: any): sourceObj is HasEventTargetAddRemove<any> {
|
---|
| 244 | return sourceObj && typeof sourceObj.addEventListener === 'function' && typeof sourceObj.removeEventListener === 'function';
|
---|
| 245 | }
|
---|