UNPKG

15.3 kBPlain TextView Raw
1import xs, {Stream, Subscription} from 'xstream';
2import {ScopeChecker} from './ScopeChecker';
3import {IsolateModule} from './IsolateModule';
4import {getSelectors, isEqualNamespace} from './utils';
5import {ElementFinder} from './ElementFinder';
6import {EventsFnOptions} from './DOMSource';
7import {Scope} from './isolate';
8import SymbolTree from './SymbolTree';
9import PriorityQueue from './PriorityQueue';
10import {
11 fromEvent,
12 preventDefaultConditional,
13 PreventDefaultOpt,
14} from './fromEvent';
15
16declare var requestIdleCallback: any;
17
18interface Destination {
19 useCapture: boolean;
20 bubbles: boolean;
21 passive: boolean;
22 scopeChecker: ScopeChecker;
23 subject: Stream<Event>;
24 preventDefault?: PreventDefaultOpt;
25}
26
27export interface CycleDOMEvent extends Event {
28 propagationHasBeenStopped: boolean;
29 ownerTarget: Element;
30}
31
32export const eventTypesThatDontBubble = [
33 `blur`,
34 `canplay`,
35 `canplaythrough`,
36 `durationchange`,
37 `emptied`,
38 `ended`,
39 `focus`,
40 `load`,
41 `loadeddata`,
42 `loadedmetadata`,
43 `mouseenter`,
44 `mouseleave`,
45 `pause`,
46 `play`,
47 `playing`,
48 `ratechange`,
49 `reset`,
50 `scroll`,
51 `seeked`,
52 `seeking`,
53 `stalled`,
54 `submit`,
55 `suspend`,
56 `timeupdate`,
57 `unload`,
58 `volumechange`,
59 `waiting`,
60];
61
62interface DOMListener {
63 sub: Subscription;
64 passive: boolean;
65}
66
67interface NonBubblingListener {
68 sub: Subscription | undefined;
69 destination: Destination;
70}
71
72type NonBubblingMeta = [Stream<Event>, string, ElementFinder, Destination]
73
74/**
75 * Manages "Event delegation", by connecting an origin with multiple
76 * destinations.
77 *
78 * Attaches a DOM event listener to the DOM element called the "origin",
79 * and delegates events to "destinations", which are subjects as outputs
80 * for the DOMSource. Simulates bubbling or capturing, with regards to
81 * isolation boundaries too.
82 */
83export class EventDelegator {
84 private virtualListeners = new SymbolTree<
85 Map<string, PriorityQueue<Destination>>,
86 Scope
87 >(x => x.scope);
88 private origin: Element | undefined;
89
90 private domListeners: Map<string, DOMListener>;
91 private nonBubblingListeners: Map<string, Map<Element, NonBubblingListener>>;
92 private domListenersToAdd: Map<string, boolean>;
93 private nonBubblingListenersToAdd = new Set<NonBubblingMeta>();
94
95 private virtualNonBubblingListener: Array<Destination> = [];
96
97 constructor(
98 private rootElement$: Stream<Element>,
99 public isolateModule: IsolateModule
100 ) {
101 this.isolateModule.setEventDelegator(this);
102 this.domListeners = new Map<string, DOMListener>();
103 this.domListenersToAdd = new Map<string, boolean>();
104 this.nonBubblingListeners = new Map<
105 string,
106 Map<Element, NonBubblingListener>
107 >();
108 rootElement$.addListener({
109 next: (el: Element) => {
110 if (this.origin !== el) {
111 this.origin = el;
112 this.resetEventListeners();
113 this.domListenersToAdd.forEach((passive, type) =>
114 this.setupDOMListener(type, passive)
115 );
116 this.domListenersToAdd.clear();
117 }
118
119 this.nonBubblingListenersToAdd.forEach(arr => {
120 this.setupNonBubblingListener(arr);
121 });
122 },
123 });
124 }
125
126 public addEventListener(
127 eventType: string,
128 namespace: Array<Scope>,
129 options: EventsFnOptions,
130 bubbles?: boolean
131 ): Stream<Event> {
132 const subject = xs.never();
133 let dest;
134
135 const scopeChecker = new ScopeChecker(namespace, this.isolateModule);
136
137 const shouldBubble =
138 bubbles === undefined
139 ? eventTypesThatDontBubble.indexOf(eventType) === -1
140 : bubbles;
141
142 if (shouldBubble) {
143 if (!this.domListeners.has(eventType)) {
144 this.setupDOMListener(eventType, !!options.passive);
145 }
146
147 dest = this.insertListener(subject, scopeChecker, eventType, options);
148 return subject;
149 } else {
150 const setArray: Array<NonBubblingMeta> = [];
151 this.nonBubblingListenersToAdd.forEach(v => setArray.push(v));
152 let found = undefined, index = 0;
153 const length = setArray.length;
154 const tester = (x: NonBubblingMeta) => {
155 const [_sub, et, ef, _] = x;
156 return eventType === et && isEqualNamespace(ef.namespace, namespace);
157 }
158
159 while (!found && index < length) {
160 const item = setArray[index]
161 found = tester(item) ? item : found;
162 index++;
163 }
164
165 let input: NonBubblingMeta = found as NonBubblingMeta;
166
167 let nonBubbleSubject: Stream<Event>;
168 if (!input) {
169 const finder = new ElementFinder(namespace, this.isolateModule);
170 dest = this.insertListener(subject, scopeChecker, eventType, options);
171 input = [subject, eventType, finder, dest];
172 nonBubbleSubject = subject;
173 this.nonBubblingListenersToAdd.add(input);
174 this.setupNonBubblingListener(input);
175 } else {
176 const [sub] = input;
177 nonBubbleSubject = sub;
178 }
179
180 const self = this;
181
182 let subscription: any = null;
183 return xs.create({
184 start: listener => {
185 subscription = nonBubbleSubject.subscribe(listener);
186 },
187 stop: () => {
188 const [_s, et, ef, _d] = input;
189 const elements = ef.call();
190
191 elements.forEach(function(element: any) {
192 const subs = element.subs;
193 if (subs && subs[et]) {
194 subs[et].unsubscribe();
195 delete subs[et];
196 }
197 });
198
199 self.nonBubblingListenersToAdd.delete(input as any);
200
201 subscription.unsubscribe();
202 }
203 });
204 }
205 }
206
207 public removeElement(element: Element, namespace?: Array<Scope>): void {
208 if (namespace !== undefined) {
209 this.virtualListeners.delete(namespace);
210 }
211 const toRemove: Array<[string, Element]> = [];
212 this.nonBubblingListeners.forEach((map, type) => {
213 if (map.has(element)) {
214 toRemove.push([type, element]);
215 const subs = (element as any).subs;
216 if (subs) {
217 Object.keys(subs).forEach((key: any) => {
218 subs[key].unsubscribe();
219 });
220 }
221 }
222 });
223 for (let i = 0; i < toRemove.length; i++) {
224 const map = this.nonBubblingListeners.get(toRemove[i][0]);
225 if (!map) {
226 continue;
227 }
228 map.delete(toRemove[i][1]);
229 if (map.size === 0) {
230 this.nonBubblingListeners.delete(toRemove[i][0]);
231 } else {
232 this.nonBubblingListeners.set(toRemove[i][0], map);
233 }
234 }
235 }
236
237 private insertListener(
238 subject: Stream<Event>,
239 scopeChecker: ScopeChecker,
240 eventType: string,
241 options: EventsFnOptions
242 ): Destination {
243 const relevantSets: Array<PriorityQueue<Destination>> = [];
244 const n = scopeChecker._namespace;
245 let max = n.length;
246
247 do {
248 relevantSets.push(this.getVirtualListeners(eventType, n, true, max));
249 max--;
250 } while (max >= 0 && n[max].type !== 'total');
251
252 const destination = {
253 ...options,
254 scopeChecker,
255 subject,
256 bubbles: !!options.bubbles,
257 useCapture: !!options.useCapture,
258 passive: !!options.passive,
259 };
260
261 for (let i = 0; i < relevantSets.length; i++) {
262 relevantSets[i].add(destination, n.length);
263 }
264
265 return destination;
266 }
267
268 /**
269 * Returns a set of all virtual listeners in the scope of the namespace
270 * Set `exact` to true to treat sibiling isolated scopes as total scopes
271 */
272 private getVirtualListeners(
273 eventType: string,
274 namespace: Array<Scope>,
275 exact = false,
276 max?: number
277 ): PriorityQueue<Destination> {
278 let _max = max !== undefined ? max : namespace.length;
279 if (!exact) {
280 for (let i = _max - 1; i >= 0; i--) {
281 if (namespace[i].type === 'total') {
282 _max = i + 1;
283 break;
284 }
285 _max = i;
286 }
287 }
288
289 const map = this.virtualListeners.getDefault(
290 namespace,
291 () => new Map<string, PriorityQueue<Destination>>(),
292 _max
293 );
294
295 if (!map.has(eventType)) {
296 map.set(eventType, new PriorityQueue<Destination>());
297 }
298 return map.get(eventType) as PriorityQueue<Destination>;
299 }
300
301 private setupDOMListener(eventType: string, passive: boolean): void {
302 if (this.origin) {
303 const sub = fromEvent(
304 this.origin,
305 eventType,
306 false,
307 false,
308 passive
309 ).subscribe({
310 next: (event: Event) => this.onEvent(eventType, event, passive),
311 error: () => {},
312 complete: () => {},
313 });
314 this.domListeners.set(eventType, {sub, passive});
315 } else {
316 this.domListenersToAdd.set(eventType, passive);
317 }
318 }
319
320 private setupNonBubblingListener(
321 input: NonBubblingMeta
322 ): void {
323 const [_, eventType, elementFinder, destination] = input;
324 if (!this.origin) {
325 return;
326 }
327
328 const elements = elementFinder.call();
329 if (elements.length) {
330 const self = this;
331 elements.forEach((element: Element) => {
332 const subs = (element as any).subs;
333 if (!subs || !subs[eventType]) {
334 const sub = fromEvent(
335 element,
336 eventType,
337 false,
338 false,
339 destination.passive
340 ).subscribe({
341 next: (ev: Event) =>
342 self.onEvent(eventType, ev, !!destination.passive, false),
343 error: () => {},
344 complete: () => {},
345 });
346 if (!self.nonBubblingListeners.has(eventType)) {
347 self.nonBubblingListeners.set(
348 eventType,
349 new Map<Element, NonBubblingListener>()
350 );
351 }
352 const map = self.nonBubblingListeners.get(eventType);
353 if (!map) {
354 return;
355 }
356 map.set(element, {sub, destination});
357
358 (element as any).subs = {
359 ...subs,
360 [eventType]: sub,
361 };
362 }
363 });
364 }
365 }
366
367 private resetEventListeners(): void {
368 const iter = this.domListeners.entries();
369 let curr = iter.next();
370 while (!curr.done) {
371 const [type, {sub, passive}] = curr.value;
372 sub.unsubscribe();
373 this.setupDOMListener(type, passive);
374 curr = iter.next();
375 }
376 }
377
378 private putNonBubblingListener(
379 eventType: string,
380 elm: Element,
381 useCapture: boolean,
382 passive: boolean
383 ): void {
384 const map = this.nonBubblingListeners.get(eventType);
385 if (!map) {
386 return;
387 }
388 const listener = map.get(elm);
389 if (
390 listener &&
391 listener.destination.passive === passive &&
392 listener.destination.useCapture === useCapture
393 ) {
394 this.virtualNonBubblingListener[0] = listener.destination;
395 }
396 }
397
398 private onEvent(
399 eventType: string,
400 event: Event,
401 passive: boolean,
402 bubbles = true
403 ): void {
404 const cycleEvent = this.patchEvent(event);
405 const rootElement = this.isolateModule.getRootElement(
406 event.target as Element
407 );
408
409 if (bubbles) {
410 const namespace = this.isolateModule.getNamespace(
411 event.target as Element
412 );
413 if (!namespace) {
414 return;
415 }
416 const listeners = this.getVirtualListeners(eventType, namespace);
417 this.bubble(
418 eventType,
419 event.target as Element,
420 rootElement,
421 cycleEvent,
422 listeners,
423 namespace,
424 namespace.length - 1,
425 true,
426 passive
427 );
428
429 this.bubble(
430 eventType,
431 event.target as Element,
432 rootElement,
433 cycleEvent,
434 listeners,
435 namespace,
436 namespace.length - 1,
437 false,
438 passive
439 );
440 } else {
441 this.putNonBubblingListener(
442 eventType,
443 event.target as Element,
444 true,
445 passive
446 );
447 this.doBubbleStep(
448 eventType,
449 event.target as Element,
450 rootElement,
451 cycleEvent,
452 this.virtualNonBubblingListener,
453 true,
454 passive
455 );
456
457 this.putNonBubblingListener(
458 eventType,
459 event.target as Element,
460 false,
461 passive
462 );
463 this.doBubbleStep(
464 eventType,
465 event.target as Element,
466 rootElement,
467 cycleEvent,
468 this.virtualNonBubblingListener,
469 false,
470 passive
471 );
472 event.stopPropagation(); //fix reset event (spec'ed as non-bubbling, but bubbles in reality
473 }
474 }
475
476 private bubble(
477 eventType: string,
478 elm: Element,
479 rootElement: Element | undefined,
480 event: CycleDOMEvent,
481 listeners: PriorityQueue<Destination>,
482 namespace: Array<Scope>,
483 index: number,
484 useCapture: boolean,
485 passive: boolean
486 ): void {
487 if (!useCapture && !event.propagationHasBeenStopped) {
488 this.doBubbleStep(
489 eventType,
490 elm,
491 rootElement,
492 event,
493 listeners,
494 useCapture,
495 passive
496 );
497 }
498
499 let newRoot: Element | undefined = rootElement;
500 let newIndex = index;
501 if (elm === rootElement) {
502 if (index >= 0 && namespace[index].type === 'sibling') {
503 newRoot = this.isolateModule.getElement(namespace, index);
504 newIndex--;
505 } else {
506 return;
507 }
508 }
509
510 if (elm.parentNode && newRoot) {
511 this.bubble(
512 eventType,
513 elm.parentNode as Element,
514 newRoot,
515 event,
516 listeners,
517 namespace,
518 newIndex,
519 useCapture,
520 passive
521 );
522 }
523
524 if (useCapture && !event.propagationHasBeenStopped) {
525 this.doBubbleStep(
526 eventType,
527 elm,
528 rootElement,
529 event,
530 listeners,
531 useCapture,
532 passive
533 );
534 }
535 }
536
537 private doBubbleStep(
538 eventType: string,
539 elm: Element,
540 rootElement: Element | undefined,
541 event: CycleDOMEvent,
542 listeners: PriorityQueue<Destination> | Array<Destination>,
543 useCapture: boolean,
544 passive: boolean
545 ): void {
546 if (!rootElement) {
547 return;
548 }
549 this.mutateEventCurrentTarget(event, elm);
550 listeners.forEach(dest => {
551 if (dest.passive === passive && dest.useCapture === useCapture) {
552 const sel = getSelectors(dest.scopeChecker.namespace);
553 if (
554 !event.propagationHasBeenStopped &&
555 dest.scopeChecker.isDirectlyInScope(elm) &&
556 ((sel !== '' && elm.matches(sel)) ||
557 (sel === '' && elm === rootElement))
558 ) {
559 preventDefaultConditional(
560 event,
561 dest.preventDefault as PreventDefaultOpt
562 );
563
564 dest.subject.shamefullySendNext(event);
565 }
566 }
567 });
568 }
569
570 private patchEvent(event: Event): CycleDOMEvent {
571 const pEvent = event as CycleDOMEvent;
572 pEvent.propagationHasBeenStopped = false;
573 const oldStopPropagation = pEvent.stopPropagation;
574 pEvent.stopPropagation = function stopPropagation() {
575 oldStopPropagation.call(this);
576 this.propagationHasBeenStopped = true;
577 };
578 return pEvent;
579 }
580
581 private mutateEventCurrentTarget(
582 event: CycleDOMEvent,
583 currentTargetElement: Element
584 ) {
585 try {
586 Object.defineProperty(event, `currentTarget`, {
587 value: currentTargetElement,
588 configurable: true,
589 });
590 } catch (err) {
591 console.log(`please use event.ownerTarget`);
592 }
593 event.ownerTarget = currentTargetElement;
594 }
595}