perf: create eventManager with more efficient mutation observation

This commit is contained in:
sethburkart123
2024-06-18 12:32:43 +10:00
parent b5c8daafb9
commit 81a72061ca
2 changed files with 302 additions and 169 deletions
+130 -139
View File
@@ -26,6 +26,7 @@ import { injectYouTubeVideo } from './seqta/ui/VideoLoader'
import { initializeSettingsState, settingsState } from './seqta/utils/listeners/SettingsState'
import { StorageChangeHandler } from './seqta/utils/listeners/StorageChanges'
import { AddBetterSEQTAElements } from './seqta/ui/AddBetterSEQTAElements'
import { eventManager } from './seqta/utils/listeners/EventManager'
declare global {
interface Window {
@@ -376,7 +377,7 @@ export function RemoveBackground() {
bk3[0].remove()
}
export function waitForElm(selector: string) {
export async function waitForElm(selector: string) {
return new Promise((resolve) => {
const querySelector = () => document.querySelector(selector);
@@ -434,65 +435,47 @@ function removeThemeTagsFromNotices () {
}
async function updateIframesWithDarkMode(): Promise<void> {
// Load the CSS file to overwrite iFrame default CSS
const cssLink = document.createElement('style')
cssLink.classList.add('iframecss')
const cssContent = document.createTextNode(iframeCSS)
cssLink.appendChild(cssContent)
const cssLink = document.createElement('style');
cssLink.classList.add('iframecss');
const cssContent = document.createTextNode(iframeCSS);
cssLink.appendChild(cssContent);
const observer = new MutationObserver(async (mutationsList) => {
for (const mutation of mutationsList) {
for (const node of mutation.addedNodes) {
if (node.nodeName === 'IFRAME') {
const iframe = node as HTMLIFrameElement
eventManager.register('iframeAdded', {
elementType: 'iframe',
customCheck: (element: Element) => !element.classList.contains('iframecss'),
}, (element) => {
const iframe = element as HTMLIFrameElement;
try {
applyDarkModeToIframe(iframe, cssLink, settingsState.DarkMode);
// check if it is a text editor frame
if (node instanceof HTMLElement && node.classList.contains('cke_wysiwyg_frame')) {
await delay(100)
// enable spellcheck
iframe.contentDocument?.body.setAttribute('spellcheck', 'true')
if (element.classList.contains('cke_wysiwyg_frame')) {
(async () => {
await delay(100);
iframe.contentDocument?.body.setAttribute('spellcheck', 'true');
})();
}
} catch (error) {
console.error('Error applying dark mode:', error)
}
}
}
console.error('Error applying dark mode:', error);
}
});
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true,
});
} else {
document.addEventListener('DOMContentLoaded', () => {
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
}
}
function applyDarkModeToIframe(iframe: HTMLIFrameElement, cssLink: HTMLStyleElement, DarkMode: boolean): void {
const iframeDocument = iframe.contentDocument
if (!iframeDocument) return
const iframeDocument = iframe.contentDocument;
if (!iframeDocument) return;
if (iframeDocument.readyState !== 'complete') {
iframe.onload = () => {
applyDarkModeToIframe(iframe, cssLink, DarkMode)
}
return
applyDarkModeToIframe(iframe, cssLink, DarkMode);
};
return;
}
if (DarkMode) iframeDocument.documentElement.classList.add('dark')
if (DarkMode) iframeDocument.documentElement.classList.add('dark');
const head = iframeDocument.head
const head = iframeDocument.head;
if (head && !head.innerHTML.includes('iframecss')) {
head.innerHTML += cssLink.outerHTML
head.innerHTML += cssLink.outerHTML;
}
}
@@ -509,20 +492,80 @@ function SortMessagePageItems(messagesParentElement: any) {
}
async function LoadPageElements(): Promise<void> {
await AddBetterSEQTAElements()
const sublink: string | undefined = window.location.href.split('/')[4]
await AddBetterSEQTAElements();
const sublink: string | undefined = window.location.href.split('/')[4];
const observer = new MutationObserver(function (mutations_list) {
mutations_list.forEach(function (mutation) {
mutation.addedNodes.forEach(function (added_node) {
const node = added_node as HTMLElement
if (node.classList.contains('messages')) {
let element = document.getElementById('title')!.firstChild as HTMLElement
element.innerText = 'Direct Messages'
document.title = 'Direct Messages ― SEQTA Learn'
SortMessagePageItems(added_node)
eventManager.register('messagesAdded', {
elementType: 'div',
className: 'messages',
}, handleMessages);
waitForElm('[data-message]').then(() => {
eventManager.register('noticesAdded', {
elementType: 'div',
className: 'notices',
}, CheckNoticeTextColour);
eventManager.register('dashboardAdded', {
elementType: 'div',
className: 'dashboard',
}, handleDashboard);
eventManager.register('documentsAdded', {
elementType: 'div',
className: 'documents',
}, handleDocuments);
eventManager.register('reportsAdded', {
elementType: 'div',
className: 'reports',
}, handleReports);
await handleSublink(sublink);
}
async function handleSublink(sublink: string | undefined): Promise<void> {
switch (sublink) {
case 'news':
await handleNewsPage();
break;
case 'home':
case undefined:
window.location.replace(`${location.origin}/#?page=/home`);
LoadInit();
break;
default:
await handleDefault();
break;
}
}
async function handleNewsPage(): Promise<void> {
console.log('[BetterSEQTA+] Started Init');
if (settingsState.onoff) {
SendNewsPage();
if (settingsState.notificationcollector) {
enableNotificationCollector();
}
finishLoad();
}
}
async function handleDefault(): Promise<void> {
finishLoad();
if (settingsState.notificationcollector) {
enableNotificationCollector();
}
}
async function handleMessages(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return;
const element = document.getElementById('title')!.firstChild as HTMLElement;
element.innerText = 'Direct Messages';
document.title = 'Direct Messages ― SEQTA Learn';
SortMessagePageItems(node);
await waitForElm('[data-message]');
animate(
'[data-message]',
{ opacity: [0, 1], y: [10, 0] },
@@ -531,13 +574,13 @@ async function LoadPageElements(): Promise<void> {
duration: 0.5,
easing: [.22, .03, .26, 1]
}
)
})
} else if (node.classList.contains('notices')) {
CheckNoticeTextColour(added_node)
} else if (node.classList.contains('dashboard')) {
let ranOnce = false;
waitForElm('.dashlet').then(() => {
);
}
async function handleDashboard(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return;
await waitForElm('.dashlet');
animate(
'.dashboard > *',
{ opacity: [0, 1], y: [10, 0] },
@@ -546,15 +589,13 @@ async function LoadPageElements(): Promise<void> {
duration: 0.5,
easing: [.22, .03, .26, 1]
}
)
if (ranOnce) return;
ranOnce = true;
})
} else if (node.classList.contains('documents')) {
let ranOnce = false;
waitForElm('.document').then(() => {
if (ranOnce) return;
ranOnce = true;
);
}
async function handleDocuments(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return;
await waitForElm('.document');
animate(
'.documents tbody tr.document',
{ opacity: [0, 1], y: [10, 0] },
@@ -563,13 +604,13 @@ async function LoadPageElements(): Promise<void> {
duration: 0.5,
easing: [.22, .03, .26, 1]
}
)
})
} else if (node.classList.contains('reports')) {
let ranOnce = false;
waitForElm('.report').then(() => {
if (ranOnce) return;
ranOnce = true;
);
}
async function handleReports(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return;
await waitForElm('.report');
animate(
'.reports .item',
{ opacity: [0, 1], y: [10, 0] },
@@ -578,74 +619,24 @@ async function LoadPageElements(): Promise<void> {
duration: 0.5,
easing: [.22, .03, .26, 1]
}
)
})
}
})
})
})
observer.observe(document.querySelector('#main') as HTMLElement, {
subtree: false,
childList: true,
})
async function handleNewsPage(): Promise<void> {
console.log('[BetterSEQTA+] Started Init')
if (settingsState.onoff) {
SendNewsPage()
if (settingsState.notificationcollector) {
enableNotificationCollector()
}
finishLoad()
}
}
async function handleDefault(): Promise<void> {
finishLoad()
if (settingsState.notificationcollector) {
enableNotificationCollector()
}
}
switch (sublink) {
case 'news':
await handleNewsPage()
break
case 'home':
case undefined:
window.location.replace(`${location.origin}/#?page=/home`)
LoadInit()
break
default:
await handleDefault()
break
}
);
}
function CheckNoticeTextColour(notice: any) {
const observer = new MutationObserver(function (mutations_list) {
mutations_list.forEach(function (mutation) {
mutation.addedNodes.forEach(function (added_node) {
const node = added_node as HTMLElement
if (node.classList.contains('notice')) {
var hex = node.style.cssText.split(' ')[1]
eventManager.register('noticeAdded', {
elementType: 'div',
className: 'notice',
parentElement: notice
}, (node) => {
var hex = (node as HTMLElement).style.cssText.split(' ')[1];
if (hex) {
const hex1 = hex.slice(0,-1)
var threshold = GetThresholdOfColor(hex1)
const hex1 = hex.slice(0,-1);
var threshold = GetThresholdOfColor(hex1);
if (settingsState.DarkMode && threshold < 100) {
node.style.cssText = '--color: undefined;'
(node as HTMLElement).style.cssText = '--color: undefined;';
}
}
}
})
})
})
observer.observe(notice, {
subtree: true,
childList: true,
})
});
}
export function tryLoad() {
@@ -665,11 +656,11 @@ export function tryLoad() {
if (!elm.innerText.includes('BetterSEQTA')) LoadPageElements()
})
updateIframesWithDarkMode()
// Waits for page to call on load, run scripts
document.addEventListener(
'load',
function () {
updateIframesWithDarkMode()
removeThemeTagsFromNotices()
documentTextColor()
},
+142
View File
@@ -0,0 +1,142 @@
interface EventListenerOptions {
elementType?: string;
textContent?: string;
className?: string;
id?: string;
customCheck?: (element: Element) => boolean;
once?: boolean;
parentElement?: Element;
}
interface EventListener {
id: string;
options: EventListenerOptions;
callback: (element: Element) => void;
unregister: () => void;
}
class EventManager {
private static instance: EventManager;
private listeners: Map<string, EventListener[]> = new Map();
private mutationObservers: Map<Element, MutationObserver> = new Map();
private pendingElements: Set<Element> = new Set();
private throttleTimeout: number = 5; // 5ms throttle
private throttleTimer: number | undefined;
private chunkSize: number = 50; // Process 50 elements per chunk
private constructor() {}
public static getInstance(): EventManager {
if (!EventManager.instance) {
EventManager.instance = new EventManager();
}
return EventManager.instance;
}
public register(event: string, options: EventListenerOptions, callback: (element: Element) => void): { unregister: () => void } {
const id = this.generateUniqueId();
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
const unregister = () => this.unregisterById(event, id);
this.listeners.get(event)!.push({ id, options, callback, unregister });
this.startObserving(options.parentElement);
return { unregister };
}
public unregister(event: string): void {
if (this.listeners.has(event)) {
this.listeners.delete(event);
}
}
private unregisterById(event: string, id: string): void {
if (this.listeners.has(event)) {
const listeners = this.listeners.get(event)!;
this.listeners.set(event, listeners.filter(listener => listener.id !== id));
}
}
private startObserving(parentElement?: Element): void {
const elementToObserve = parentElement || document.documentElement;
if (!this.mutationObservers.has(elementToObserve)) {
const observer = new MutationObserver(this.handleMutations.bind(this));
observer.observe(elementToObserve, {
childList: true,
subtree: true,
});
this.mutationObservers.set(elementToObserve, observer);
}
}
private handleMutations(mutations: MutationRecord[]): void {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
this.pendingElements.add(node as Element);
}
});
}
});
this.throttleCheckElements();
}
private throttleCheckElements(): void {
if (this.throttleTimer) return;
this.throttleTimer = window.setTimeout(() => {
this.processPendingElements();
this.throttleTimer = undefined;
}, this.throttleTimeout);
}
private async processPendingElements(): Promise<void> {
const elements = Array.from(this.pendingElements);
this.pendingElements.clear();
for (let i = 0; i < elements.length; i += this.chunkSize) {
const chunk = elements.slice(i, i + this.chunkSize);
await this.processChunk(chunk);
}
}
private async processChunk(chunk: Element[]): Promise<void> {
return new Promise((resolve) => {
requestAnimationFrame(async () => {
for (const element of chunk) {
await this.checkElement(element);
}
resolve();
});
});
}
private async checkElement(element: Element): Promise<void> {
for (const [event, listeners] of this.listeners.entries()) {
for (const { id, options, callback } of listeners) {
if (this.matchesOptions(element, options)) {
await callback(element);
if (options.once) {
this.unregisterById(event, id);
}
}
}
}
}
private matchesOptions(element: Element, options: EventListenerOptions): boolean {
if (options.elementType && element.tagName.toLowerCase() !== options.elementType.toLowerCase()) return false;
if (options.textContent && element.textContent !== options.textContent) return false;
if (options.className && !element.classList.contains(options.className)) return false;
if (options.id && element.id !== options.id) return false;
if (options.customCheck && !options.customCheck(element)) return false;
return true;
}
private generateUniqueId(): string {
return '_' + Math.random().toString(36).substr(2, 9);
}
}
export const eventManager = EventManager.getInstance();