blob: 73b9560aec0ee61ae202e5af703f3488d1545b6f [file] [log] [blame]
Adrià Vilanova Martínez18d03c42024-04-21 16:43:01 +02001import {
2 NodeWatcherHandler,
3 NodeMutation,
4 NodeMutationType,
5} from './NodeWatcherHandler';
6
7class NodeWatcher {
8 private handlers: Map<string, NodeWatcherHandler> = new Map();
9 private mutationObserver: MutationObserver;
10
11 constructor() {
12 this.mutationObserver = new MutationObserver(
13 this.mutationCallback.bind(this),
14 );
15 this.start();
16 }
17
18 start(): void {
19 this.mutationObserver.observe(document.body, {
20 childList: true,
21 subtree: true,
22 });
23 }
24
25 pause(): void {
26 this.mutationObserver.disconnect();
27 }
28
29 setHandler(key: string, handler: NodeWatcherHandler): void {
30 this.handlers.set(key, handler);
31 this.performInitialDiscovery(handler);
32 }
33
34 removeHandler(key: string): boolean {
35 return this.handlers.delete(key);
36 }
37
38 private mutationCallback(mutationRecords: MutationRecord[]): void {
39 for (const mutationRecord of mutationRecords) {
40 if (mutationRecord.type !== 'childList') continue;
41 this.handleAddedNodes(mutationRecord);
42 this.handleRemovedNodes(mutationRecord);
43 }
44 }
45
46 private handleAddedNodes(mutationRecord: MutationRecord): void {
47 for (const node of mutationRecord.addedNodes) {
48 this.handleMutatedNode({
49 node,
50 mutationRecord,
51 type: NodeMutationType.NewNode,
52 });
53 }
54 }
55
56 private handleRemovedNodes(mutationRecord: MutationRecord): void {
57 for (const node of mutationRecord.removedNodes) {
58 this.handleMutatedNode({
59 node,
60 mutationRecord,
61 type: NodeMutationType.RemovedNode,
62 });
63 }
64 }
65
66 private performInitialDiscovery(handler: NodeWatcherHandler): void {
67 if (handler.initialDiscoverySelector === undefined) return;
68 const candidateNodes = document.querySelectorAll(
69 handler.initialDiscoverySelector,
70 );
71 for (const candidateNode of candidateNodes) {
72 this.handleMutatedNodeWithHandler(
73 {
74 node: candidateNode,
75 type: NodeMutationType.InitialDiscovery,
76 mutationRecord: null,
77 },
78 handler,
79 );
80 }
81 }
82
83 private handleMutatedNode(nodeMutation: NodeMutation): void {
84 for (const [, handler] of this.handlers) {
85 this.handleMutatedNodeWithHandler(nodeMutation, handler);
86 }
87 }
88
89 private handleMutatedNodeWithHandler(
90 nodeMutation: NodeMutation,
91 handler: NodeWatcherHandler,
92 ): void {
93 if (handler.nodeFilter(nodeMutation)) {
94 handler.onMutatedNode(nodeMutation);
95 }
96 }
97}
98
99export default class NodeWatcherSingleton {
100 private static instance: NodeWatcher;
101
102 /**
103 * @see {@link NodeWatcherSingleton.getInstance}
104 */
105 private constructor() {}
106
107 public static getInstance(): NodeWatcher {
108 if (!NodeWatcherSingleton.instance) {
109 NodeWatcherSingleton.instance = new NodeWatcher();
110 }
111 return NodeWatcherSingleton.instance;
112 }
113}