-
Notifications
You must be signed in to change notification settings - Fork 8
/
mutation-observer.ts
113 lines (90 loc) · 3.97 KB
/
mutation-observer.ts
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
/*
* The mutation-observer is a global instance of browsers MutationObserver
* which tracks when nodes are added or removed.
*
* When nodes are added we process their effects. When nodes are removed we process any cleanup,
* and stop observers that would trigger for that node.
*/
const { observe, unobserve } = require('@nx-js/observer-util');
import { TRAM_TAG, TRAM_TAG_REACTION, TRAM_TAG_NEW_EFFECTS, TRAM_TAG_CLEANUP_EFFECTS } from './node-names';
import { buildNamespace } from './namespace';
import { TramOneElement } from './types';
// process new effects for new nodes
const processEffects = (node: Node | TramOneElement) => {
// if this element doesn't have new effects, it is not be a Tram-One Element
if (!(TRAM_TAG_NEW_EFFECTS in node)) {
return;
}
const hasEffects = node[TRAM_TAG_NEW_EFFECTS];
if (hasEffects) {
// create an array for the cleanup effects
node[TRAM_TAG_CLEANUP_EFFECTS] = [];
// run all the effects, saving any cleanup functions to the node
node[TRAM_TAG_NEW_EFFECTS].forEach((effect) => {
let cleanup: unknown;
// this is called when an effect is re-triggered
const effectReaction = observe(() => {
// verify that cleanup is a function before calling it (in case it was a promise)
if (typeof cleanup === 'function') cleanup();
cleanup = effect();
});
// this is called when a component with an effect is removed
const totalCleanup = () => {
// verify that cleanup is a function before calling it (in case it was a promise)
if (typeof cleanup === 'function') cleanup();
unobserve(effectReaction);
};
node[TRAM_TAG_CLEANUP_EFFECTS].push(totalCleanup);
});
// set new tag effects to an empty array
node[TRAM_TAG_NEW_EFFECTS] = [];
}
};
// call all cleanup effects on the node
const cleanupEffects = (cleanupEffects: (() => void)[]) => {
cleanupEffects.forEach((cleanup) => cleanup());
};
// unobserve the reaction tied to the node, and run all cleanup effects for the node
const clearNode = (node: Node | TramOneElement) => {
// if this element doesn't have a Reaction, it is not be a Tram-One Element
if (!(TRAM_TAG in node)) {
return;
}
unobserve(node[TRAM_TAG_REACTION]);
cleanupEffects(node[TRAM_TAG_CLEANUP_EFFECTS]);
};
const isTramOneComponent = (node: Node | TramOneElement) => {
// a node is a component if it has `TRAM_TAG` key on it
const nodeIsATramOneComponent = TRAM_TAG in node;
// if it is a tram-one component, we want to process it, otherwise skip it
return nodeIsATramOneComponent ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
};
// function to get the children (as a list) of the node passed in
// this only needs to query tram-one components, so we can use the attribute `tram`
const childrenComponents = (node: Node | TramOneElement) => {
const componentWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT, isTramOneComponent);
const children = [];
while (componentWalker.nextNode()) {
children.push(componentWalker.currentNode);
}
return children;
};
const mutationObserverNamespaceConstructor = () =>
new MutationObserver((mutationList) => {
// cleanup orphaned nodes that are no longer on the DOM
const removedNodesInMutation = (mutation: MutationRecord) => [...mutation.removedNodes];
const removedNodes = mutationList.flatMap(removedNodesInMutation).flatMap(childrenComponents);
removedNodes.forEach(clearNode);
// call new effects on any new nodes
const addedNodesInMutation = (mutation: MutationRecord) => [...mutation.addedNodes];
const newNodes = mutationList.flatMap(addedNodesInMutation).flatMap(childrenComponents);
newNodes.forEach(processEffects);
});
export const { setup: setupMutationObserver, get: getMutationObserver } = buildNamespace(
mutationObserverNamespaceConstructor
);
// tell the mutation observer to watch the given node for changes
export const startWatcher = (observerName: string, node: Element) => {
const observerStore = getMutationObserver(observerName);
observerStore.observe(node, { childList: true, subtree: true });
};