mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
perf: create eventManager with more efficient mutation observation
This commit is contained in:
+160
-169
@@ -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
|
||||
try {
|
||||
applyDarkModeToIframe(iframe, cssLink, settingsState.DarkMode);
|
||||
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')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error applying dark mode:', error)
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
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')
|
||||
|
||||
const head = iframeDocument.head
|
||||
if (DarkMode) iframeDocument.documentElement.classList.add('dark');
|
||||
|
||||
const head = iframeDocument.head;
|
||||
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> {
|
||||
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(() => {
|
||||
animate(
|
||||
'[data-message]',
|
||||
{ opacity: [0, 1], y: [10, 0] },
|
||||
{
|
||||
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]
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
eventManager.register('noticesAdded', {
|
||||
elementType: 'div',
|
||||
className: 'notices',
|
||||
}, CheckNoticeTextColour);
|
||||
|
||||
observer.observe(document.querySelector('#main') as HTMLElement, {
|
||||
subtree: false,
|
||||
childList: true,
|
||||
})
|
||||
eventManager.register('dashboardAdded', {
|
||||
elementType: 'div',
|
||||
className: 'dashboard',
|
||||
}, handleDashboard);
|
||||
|
||||
async function handleNewsPage(): Promise<void> {
|
||||
console.log('[BetterSEQTA+] Started Init')
|
||||
if (settingsState.onoff) {
|
||||
SendNewsPage()
|
||||
if (settingsState.notificationcollector) {
|
||||
enableNotificationCollector()
|
||||
}
|
||||
finishLoad()
|
||||
}
|
||||
}
|
||||
eventManager.register('documentsAdded', {
|
||||
elementType: 'div',
|
||||
className: 'documents',
|
||||
}, handleDocuments);
|
||||
|
||||
async function handleDefault(): Promise<void> {
|
||||
finishLoad()
|
||||
if (settingsState.notificationcollector) {
|
||||
enableNotificationCollector()
|
||||
}
|
||||
}
|
||||
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
|
||||
await handleNewsPage();
|
||||
break;
|
||||
case 'home':
|
||||
case undefined:
|
||||
window.location.replace(`${location.origin}/#?page=/home`)
|
||||
LoadInit()
|
||||
break
|
||||
window.location.replace(`${location.origin}/#?page=/home`);
|
||||
LoadInit();
|
||||
break;
|
||||
default:
|
||||
await handleDefault()
|
||||
break
|
||||
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]
|
||||
if (hex) {
|
||||
const hex1 = hex.slice(0,-1)
|
||||
var threshold = GetThresholdOfColor(hex1)
|
||||
if (settingsState.DarkMode && threshold < 100) {
|
||||
node.style.cssText = '--color: undefined;'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
async function handleNewsPage(): Promise<void> {
|
||||
console.log('[BetterSEQTA+] Started Init');
|
||||
if (settingsState.onoff) {
|
||||
SendNewsPage();
|
||||
if (settingsState.notificationcollector) {
|
||||
enableNotificationCollector();
|
||||
}
|
||||
finishLoad();
|
||||
}
|
||||
}
|
||||
|
||||
observer.observe(notice, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
})
|
||||
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] },
|
||||
{
|
||||
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() {
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user