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 { initializeSettingsState, settingsState } from './seqta/utils/listeners/SettingsState'
import { StorageChangeHandler } from './seqta/utils/listeners/StorageChanges' import { StorageChangeHandler } from './seqta/utils/listeners/StorageChanges'
import { AddBetterSEQTAElements } from './seqta/ui/AddBetterSEQTAElements' import { AddBetterSEQTAElements } from './seqta/ui/AddBetterSEQTAElements'
import { eventManager } from './seqta/utils/listeners/EventManager'
declare global { declare global {
interface Window { interface Window {
@@ -376,7 +377,7 @@ export function RemoveBackground() {
bk3[0].remove() bk3[0].remove()
} }
export function waitForElm(selector: string) { export async function waitForElm(selector: string) {
return new Promise((resolve) => { return new Promise((resolve) => {
const querySelector = () => document.querySelector(selector); const querySelector = () => document.querySelector(selector);
@@ -434,65 +435,47 @@ function removeThemeTagsFromNotices () {
} }
async function updateIframesWithDarkMode(): Promise<void> { async function updateIframesWithDarkMode(): Promise<void> {
// Load the CSS file to overwrite iFrame default CSS const cssLink = document.createElement('style');
const cssLink = document.createElement('style') cssLink.classList.add('iframecss');
cssLink.classList.add('iframecss') const cssContent = document.createTextNode(iframeCSS);
const cssContent = document.createTextNode(iframeCSS) cssLink.appendChild(cssContent);
cssLink.appendChild(cssContent)
const observer = new MutationObserver(async (mutationsList) => { eventManager.register('iframeAdded', {
for (const mutation of mutationsList) { elementType: 'iframe',
for (const node of mutation.addedNodes) { customCheck: (element: Element) => !element.classList.contains('iframecss'),
if (node.nodeName === 'IFRAME') { }, (element) => {
const iframe = node as HTMLIFrameElement const iframe = element as HTMLIFrameElement;
try { try {
applyDarkModeToIframe(iframe, cssLink, settingsState.DarkMode); applyDarkModeToIframe(iframe, cssLink, settingsState.DarkMode);
// check if it is a text editor frame if (element.classList.contains('cke_wysiwyg_frame')) {
if (node instanceof HTMLElement && node.classList.contains('cke_wysiwyg_frame')) { (async () => {
await delay(100) await delay(100);
// enable spellcheck iframe.contentDocument?.body.setAttribute('spellcheck', 'true');
iframe.contentDocument?.body.setAttribute('spellcheck', 'true') })();
} }
} catch (error) { } 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 { function applyDarkModeToIframe(iframe: HTMLIFrameElement, cssLink: HTMLStyleElement, DarkMode: boolean): void {
const iframeDocument = iframe.contentDocument const iframeDocument = iframe.contentDocument;
if (!iframeDocument) return if (!iframeDocument) return;
if (iframeDocument.readyState !== 'complete') { if (iframeDocument.readyState !== 'complete') {
iframe.onload = () => { iframe.onload = () => {
applyDarkModeToIframe(iframe, cssLink, DarkMode) applyDarkModeToIframe(iframe, cssLink, DarkMode);
} };
return 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')) { 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> { async function LoadPageElements(): Promise<void> {
await AddBetterSEQTAElements() await AddBetterSEQTAElements();
const sublink: string | undefined = window.location.href.split('/')[4] const sublink: string | undefined = window.location.href.split('/')[4];
const observer = new MutationObserver(function (mutations_list) { eventManager.register('messagesAdded', {
mutations_list.forEach(function (mutation) { elementType: 'div',
mutation.addedNodes.forEach(function (added_node) { className: 'messages',
const node = added_node as HTMLElement }, handleMessages);
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)
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( animate(
'[data-message]', '[data-message]',
{ opacity: [0, 1], y: [10, 0] }, { opacity: [0, 1], y: [10, 0] },
@@ -531,13 +574,13 @@ async function LoadPageElements(): Promise<void> {
duration: 0.5, duration: 0.5,
easing: [.22, .03, .26, 1] easing: [.22, .03, .26, 1]
} }
) );
}) }
} else if (node.classList.contains('notices')) {
CheckNoticeTextColour(added_node) async function handleDashboard(node: Element): Promise<void> {
} else if (node.classList.contains('dashboard')) { if (!(node instanceof HTMLElement)) return;
let ranOnce = false;
waitForElm('.dashlet').then(() => { await waitForElm('.dashlet');
animate( animate(
'.dashboard > *', '.dashboard > *',
{ opacity: [0, 1], y: [10, 0] }, { opacity: [0, 1], y: [10, 0] },
@@ -546,15 +589,13 @@ async function LoadPageElements(): Promise<void> {
duration: 0.5, duration: 0.5,
easing: [.22, .03, .26, 1] easing: [.22, .03, .26, 1]
} }
) );
if (ranOnce) return; }
ranOnce = true;
}) async function handleDocuments(node: Element): Promise<void> {
} else if (node.classList.contains('documents')) { if (!(node instanceof HTMLElement)) return;
let ranOnce = false;
waitForElm('.document').then(() => { await waitForElm('.document');
if (ranOnce) return;
ranOnce = true;
animate( animate(
'.documents tbody tr.document', '.documents tbody tr.document',
{ opacity: [0, 1], y: [10, 0] }, { opacity: [0, 1], y: [10, 0] },
@@ -563,13 +604,13 @@ async function LoadPageElements(): Promise<void> {
duration: 0.5, duration: 0.5,
easing: [.22, .03, .26, 1] easing: [.22, .03, .26, 1]
} }
) );
}) }
} else if (node.classList.contains('reports')) {
let ranOnce = false; async function handleReports(node: Element): Promise<void> {
waitForElm('.report').then(() => { if (!(node instanceof HTMLElement)) return;
if (ranOnce) return;
ranOnce = true; await waitForElm('.report');
animate( animate(
'.reports .item', '.reports .item',
{ opacity: [0, 1], y: [10, 0] }, { opacity: [0, 1], y: [10, 0] },
@@ -578,74 +619,24 @@ async function LoadPageElements(): Promise<void> {
duration: 0.5, duration: 0.5,
easing: [.22, .03, .26, 1] 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) { function CheckNoticeTextColour(notice: any) {
const observer = new MutationObserver(function (mutations_list) { eventManager.register('noticeAdded', {
mutations_list.forEach(function (mutation) { elementType: 'div',
mutation.addedNodes.forEach(function (added_node) { className: 'notice',
const node = added_node as HTMLElement parentElement: notice
if (node.classList.contains('notice')) { }, (node) => {
var hex = node.style.cssText.split(' ')[1] var hex = (node as HTMLElement).style.cssText.split(' ')[1];
if (hex) { if (hex) {
const hex1 = hex.slice(0,-1) const hex1 = hex.slice(0,-1);
var threshold = GetThresholdOfColor(hex1) var threshold = GetThresholdOfColor(hex1);
if (settingsState.DarkMode && threshold < 100) { 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() { export function tryLoad() {
@@ -665,11 +656,11 @@ export function tryLoad() {
if (!elm.innerText.includes('BetterSEQTA')) LoadPageElements() if (!elm.innerText.includes('BetterSEQTA')) LoadPageElements()
}) })
updateIframesWithDarkMode()
// Waits for page to call on load, run scripts // Waits for page to call on load, run scripts
document.addEventListener( document.addEventListener(
'load', 'load',
function () { function () {
updateIframesWithDarkMode()
removeThemeTagsFromNotices() removeThemeTagsFromNotices()
documentTextColor() 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();