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:
+130
-139
@@ -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()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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