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 | }
|
---|