File size: 14,537 Bytes
bc20498 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
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);
}
|