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
+160 -169
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) {
console.error('Error applying dark mode:', error)
}
}
} }
} catch (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')
const head = iframeDocument.head if (DarkMode) iframeDocument.documentElement.classList.add('dark');
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,143 +492,151 @@ 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', {
animate( elementType: 'div',
'[data-message]', className: 'notices',
{ opacity: [0, 1], y: [10, 0] }, }, CheckNoticeTextColour);
{
delay: stagger(0.05),
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(() => {
animate(
'.dashboard > *',
{ opacity: [0, 1], y: [10, 0] },
{
delay: stagger(0.1),
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;
animate(
'.documents tbody tr.document',
{ opacity: [0, 1], y: [10, 0] },
{
delay: stagger(0.05),
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;
animate(
'.reports .item',
{ opacity: [0, 1], y: [10, 0] },
{
delay: stagger(0.05, { start: 0.2 }),
duration: 0.5,
easing: [.22, .03, .26, 1]
}
)
})
}
})
})
})
observer.observe(document.querySelector('#main') as HTMLElement, { eventManager.register('dashboardAdded', {
subtree: false, elementType: 'div',
childList: true, className: 'dashboard',
}) }, handleDashboard);
async function handleNewsPage(): Promise<void> { eventManager.register('documentsAdded', {
console.log('[BetterSEQTA+] Started Init') elementType: 'div',
if (settingsState.onoff) { className: 'documents',
SendNewsPage() }, handleDocuments);
if (settingsState.notificationcollector) {
enableNotificationCollector()
}
finishLoad()
}
}
async function handleDefault(): Promise<void> { eventManager.register('reportsAdded', {
finishLoad() elementType: 'div',
if (settingsState.notificationcollector) { className: 'reports',
enableNotificationCollector() }, handleReports);
}
}
await handleSublink(sublink);
}
async function handleSublink(sublink: string | undefined): Promise<void> {
switch (sublink) { switch (sublink) {
case 'news': case 'news':
await handleNewsPage() await handleNewsPage();
break break;
case 'home': case 'home':
case undefined: case undefined:
window.location.replace(`${location.origin}/#?page=/home`) window.location.replace(`${location.origin}/#?page=/home`);
LoadInit() LoadInit();
break break;
default: default:
await handleDefault() await handleDefault();
break break;
} }
} }
function CheckNoticeTextColour(notice: any) { async function handleNewsPage(): Promise<void> {
const observer = new MutationObserver(function (mutations_list) { console.log('[BetterSEQTA+] Started Init');
mutations_list.forEach(function (mutation) { if (settingsState.onoff) {
mutation.addedNodes.forEach(function (added_node) { SendNewsPage();
const node = added_node as HTMLElement if (settingsState.notificationcollector) {
if (node.classList.contains('notice')) { enableNotificationCollector();
var hex = node.style.cssText.split(' ')[1] }
if (hex) { finishLoad();
const hex1 = hex.slice(0,-1) }
var threshold = GetThresholdOfColor(hex1) }
if (settingsState.DarkMode && threshold < 100) {
node.style.cssText = '--color: undefined;'
}
}
}
})
})
})
observer.observe(notice, { async function handleDefault(): Promise<void> {
subtree: true, finishLoad();
childList: true, 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] },
{
delay: stagger(0.05),
duration: 0.5,
easing: [.22, .03, .26, 1]
}
);
}
async function handleDashboard(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return;
await waitForElm('.dashlet');
animate(
'.dashboard > *',
{ opacity: [0, 1], y: [10, 0] },
{
delay: stagger(0.1),
duration: 0.5,
easing: [.22, .03, .26, 1]
}
);
}
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] },
{
delay: stagger(0.05),
duration: 0.5,
easing: [.22, .03, .26, 1]
}
);
}
async function handleReports(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return;
await waitForElm('.report');
animate(
'.reports .item',
{ opacity: [0, 1], y: [10, 0] },
{
delay: stagger(0.05, { start: 0.2 }),
duration: 0.5,
easing: [.22, .03, .26, 1]
}
);
}
function CheckNoticeTextColour(notice: any) {
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);
if (settingsState.DarkMode && threshold < 100) {
(node as HTMLElement).style.cssText = '--color: undefined;';
}
}
});
} }
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();