-
-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathmutations.js
More file actions
123 lines (101 loc) · 3.86 KB
/
mutations.js
File metadata and controls
123 lines (101 loc) · 3.86 KB
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
import { keyToCss } from './css_map.js';
import { notificationSelector, postSelector } from './interface.js';
const rootNode = document.getElementById('root');
const headNode = document.querySelector('head');
const addedNodesPool = [];
let repaintQueued = false;
let timerId;
const isolateErrors = callback => {
try {
callback();
} catch (exception) {
console.error(exception);
}
};
export const pageModifications = Object.freeze({
/** @type {Map<(elements: Element[]) => void, string>} */ listeners: new Map(),
/**
* Register a page modification
* @param {string} selector CSS selector for elements to target
* @param {(elements: Element[]) => void} modifierFunction Function to handle matching elements
*/
register (selector, modifierFunction) {
if (this.listeners.has(modifierFunction) === false) {
this.listeners.set(modifierFunction, selector);
this.trigger(modifierFunction);
}
},
/**
* Unregister a page modification
* @param {(elements: Element[]) => void} modifierFunction Previously-registered function to remove
*/
unregister (modifierFunction) {
this.listeners.delete(modifierFunction);
},
/**
* Run a page modification on all existing matching elements
* @param {(elements: Element[]) => void} modifierFunction Previously-registered function to run
*/
trigger (modifierFunction) {
const selector = this.listeners.get(modifierFunction);
if (!selector) return;
if (modifierFunction.length === 0) {
const shouldRun =
rootNode.querySelector(selector) !== null || headNode.querySelector(selector) !== null;
if (shouldRun) modifierFunction();
return;
}
const matchingElements = [
...rootNode.querySelectorAll(selector),
...headNode.querySelectorAll(selector),
];
if (matchingElements.length !== 0) {
modifierFunction(matchingElements);
}
},
});
export const onNewPosts = Object.freeze({
addListener: callback => pageModifications.register(`${postSelector}:not(.sortable-fallback) article`, callback),
removeListener: callback => pageModifications.unregister(callback),
});
export const onNewNotifications = Object.freeze({
addListener: callback => pageModifications.register(notificationSelector, callback),
removeListener: callback => pageModifications.unregister(callback),
});
const onBeforeRepaint = () => {
repaintQueued = false;
const addedNodes = addedNodesPool
.splice(0)
.filter(addedNode => addedNode.isConnected);
if (addedNodes.length === 0) return;
for (const [modifierFunction, selector] of pageModifications.listeners) {
if (modifierFunction.length === 0) {
const shouldRun = addedNodes.some(addedNode => addedNode.matches(selector) || addedNode.querySelector(selector) !== null);
if (shouldRun) isolateErrors(() => modifierFunction());
continue;
}
const matchingElements = [
...addedNodes.filter(addedNode => addedNode.matches(selector)),
...addedNodes.flatMap(addedNode => [...addedNode.querySelectorAll(selector)]),
].filter((value, index, array) => index === array.indexOf(value));
if (matchingElements.length !== 0) {
isolateErrors(() => modifierFunction(matchingElements));
}
}
};
const cellSelector = keyToCss('cell');
const observer = new MutationObserver(mutations => {
const addedNodes = mutations
.flatMap(({ addedNodes }) => [...addedNodes])
.filter(addedNode => addedNode instanceof Element);
addedNodesPool.push(...addedNodes);
if (addedNodes.some(addedNode => addedNode.parentElement?.matches(cellSelector))) {
cancelAnimationFrame(timerId);
onBeforeRepaint();
} else if (repaintQueued === false) {
timerId = requestAnimationFrame(onBeforeRepaint);
repaintQueued = true;
}
});
observer.observe(rootNode, { childList: true, subtree: true });
observer.observe(headNode, { childList: true, subtree: true });