1 | import xs, {Stream, Subscription} from 'xstream';
|
2 | import {ScopeChecker} from './ScopeChecker';
|
3 | import {IsolateModule} from './IsolateModule';
|
4 | import {getSelectors, isEqualNamespace} from './utils';
|
5 | import {ElementFinder} from './ElementFinder';
|
6 | import {EventsFnOptions} from './DOMSource';
|
7 | import {Scope} from './isolate';
|
8 | import SymbolTree from './SymbolTree';
|
9 | import PriorityQueue from './PriorityQueue';
|
10 | import {
|
11 | fromEvent,
|
12 | preventDefaultConditional,
|
13 | PreventDefaultOpt,
|
14 | } from './fromEvent';
|
15 |
|
16 | declare var requestIdleCallback: any;
|
17 |
|
18 | interface Destination {
|
19 | useCapture: boolean;
|
20 | bubbles: boolean;
|
21 | passive: boolean;
|
22 | scopeChecker: ScopeChecker;
|
23 | subject: Stream<Event>;
|
24 | preventDefault?: PreventDefaultOpt;
|
25 | }
|
26 |
|
27 | export interface CycleDOMEvent extends Event {
|
28 | propagationHasBeenStopped: boolean;
|
29 | ownerTarget: Element;
|
30 | }
|
31 |
|
32 | export 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 |
|
62 | interface DOMListener {
|
63 | sub: Subscription;
|
64 | passive: boolean;
|
65 | }
|
66 |
|
67 | interface NonBubblingListener {
|
68 | sub: Subscription | undefined;
|
69 | destination: Destination;
|
70 | }
|
71 |
|
72 | type NonBubblingMeta = [Stream<Event>, string, ElementFinder, Destination]
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 | export 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 |
|
270 |
|
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();
|
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 | }
|