mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-17 17:07:07 +00:00
perf: reduce startup work and fix grade analytics bar chart animation
Batch settings storage writes, tier plugin startup, lazy-load heavy UI chunks, and optimize global search indexing. Stop tweening bar height in grade analytics to prevent invalid negative SVG rect values. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,11 +3,10 @@ import {
|
||||
closeExtensionPopup,
|
||||
SettingsClicked,
|
||||
} from "../Closers/closeExtensionPopup";
|
||||
import renderSvelte from "@/interface/main";
|
||||
import { SettingsResizer } from "@/seqta/ui/SettingsResizer";
|
||||
import Settings from "@/interface/pages/settings.svelte";
|
||||
|
||||
let isSettingsRendered = false;
|
||||
let settingsLoadPromise: Promise<void> | null = null;
|
||||
|
||||
function extensionOutsideClickHandler(extensionPopup: HTMLElement) {
|
||||
return (event: MouseEvent) => {
|
||||
@@ -38,21 +37,39 @@ export function addExtensionSettings() {
|
||||
(extensionContainer ?? document.body).addEventListener("click", handler, false);
|
||||
}
|
||||
|
||||
export function renderSettingsIfNeeded() {
|
||||
async function loadSettingsUi(extensionPopup: HTMLElement): Promise<void> {
|
||||
if (isSettingsRendered) return;
|
||||
|
||||
|
||||
const [{ default: renderSvelte }, { default: Settings }] = await Promise.all([
|
||||
import("@/interface/main"),
|
||||
import("@/interface/pages/settings.svelte"),
|
||||
]);
|
||||
|
||||
const shadow = extensionPopup.attachShadow({ mode: "open" });
|
||||
const mount = () => renderSvelte(Settings, shadow);
|
||||
|
||||
if ("requestIdleCallback" in window) {
|
||||
requestIdleCallback(mount);
|
||||
} else {
|
||||
mount();
|
||||
}
|
||||
|
||||
isSettingsRendered = true;
|
||||
}
|
||||
|
||||
export async function renderSettingsIfNeeded(): Promise<void> {
|
||||
if (isSettingsRendered) return;
|
||||
|
||||
const extensionPopup = document.getElementById("ExtensionPopup");
|
||||
if (!extensionPopup) return;
|
||||
|
||||
try {
|
||||
const shadow = extensionPopup.attachShadow({ mode: "open" });
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => renderSvelte(Settings, shadow));
|
||||
} else {
|
||||
renderSvelte(Settings, shadow);
|
||||
}
|
||||
isSettingsRendered = true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (!settingsLoadPromise) {
|
||||
settingsLoadPromise = loadSettingsUi(extensionPopup).catch((err) => {
|
||||
settingsLoadPromise = null;
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
await settingsLoadPromise;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { animate } from "motion";
|
||||
|
||||
import { settingsPopup } from "@/interface/hooks/SettingsPopup";
|
||||
import { settingsPopup } from "@/seqta/utils/settingsPopup";
|
||||
|
||||
export let SettingsClicked = false;
|
||||
|
||||
|
||||
@@ -785,7 +785,7 @@ export async function OpenThemeOfTheMonthPopup(
|
||||
card.querySelector(".themeOfTheMonthCardPrimary")?.addEventListener("click", () => {
|
||||
settingsState.themeOfTheMonthDismissedMonth = entry.month;
|
||||
dismissWithCleanup();
|
||||
openThemeStoreWithHighlight(linkedThemeId!);
|
||||
void openThemeStoreWithHighlight(linkedThemeId!);
|
||||
});
|
||||
|
||||
const openDontShowConfirm = () => {
|
||||
|
||||
@@ -37,6 +37,8 @@ const OPTIONAL_UNSET_MEANS_DEFAULT_KEYS = [
|
||||
"profile_picture_revision",
|
||||
] as const;
|
||||
|
||||
let defaultsEnsured = false;
|
||||
|
||||
/**
|
||||
* Flat default map in upload shape (plugin-format only; no legacy keys).
|
||||
*/
|
||||
@@ -76,6 +78,8 @@ function mergePluginSettingsDefaults(
|
||||
* Never overwrites existing values. Missing plugin settings respect legacy keys.
|
||||
*/
|
||||
export async function ensureSyncableStorageDefaults(): Promise<void> {
|
||||
if (defaultsEnsured) return;
|
||||
|
||||
const existing = await browser.storage.local.get();
|
||||
const migratedFromExisting = migrateLegacyToPluginSettings({
|
||||
...existing,
|
||||
@@ -101,4 +105,6 @@ export async function ensureSyncableStorageDefaults(): Promise<void> {
|
||||
if (Object.keys(patch).length > 0) {
|
||||
await browser.storage.local.set(patch);
|
||||
}
|
||||
|
||||
defaultsEnsured = true;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ interface EventListenerOptions {
|
||||
textContent?: string;
|
||||
className?: string;
|
||||
id?: string;
|
||||
selector?: string;
|
||||
customCheck?: (element: Element) => boolean;
|
||||
once?: boolean;
|
||||
parentElement?: Element;
|
||||
@@ -20,6 +21,7 @@ class EventManager {
|
||||
private listeners: Map<string, EventListener[]> = new Map();
|
||||
private mutationObservers: Map<Element, MutationObserver> = new Map();
|
||||
private pendingElements: Set<Element> = new Set();
|
||||
private firedOnceIds: Set<string> = new Set();
|
||||
private throttleTimeout: number = 5; // 5ms throttle
|
||||
private throttleTimer: number | undefined;
|
||||
private chunkSize: number = 50; // Process 50 elements per chunk
|
||||
@@ -58,6 +60,7 @@ class EventManager {
|
||||
}
|
||||
|
||||
private buildSelector(options: EventListenerOptions): string | null {
|
||||
if (options.selector) return options.selector;
|
||||
if (options.textContent || options.customCheck) return null;
|
||||
|
||||
let selector = options.elementType || "";
|
||||
@@ -71,6 +74,23 @@ class EventManager {
|
||||
return selector.trim() || null;
|
||||
}
|
||||
|
||||
private getElementsToCheck(
|
||||
element: Element,
|
||||
options: EventListenerOptions,
|
||||
): Element[] {
|
||||
const selector = this.buildSelector(options);
|
||||
if (!selector) return [element];
|
||||
|
||||
const targets = new Set<Element>();
|
||||
if (element.matches(selector)) {
|
||||
targets.add(element);
|
||||
}
|
||||
for (const match of element.querySelectorAll(selector)) {
|
||||
targets.add(match);
|
||||
}
|
||||
return Array.from(targets);
|
||||
}
|
||||
|
||||
private async scanExistingElements(
|
||||
options: EventListenerOptions,
|
||||
callback: (element: Element) => void,
|
||||
@@ -174,10 +194,17 @@ class EventManager {
|
||||
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)) {
|
||||
callback(element);
|
||||
if (options.once && this.firedOnceIds.has(id)) continue;
|
||||
|
||||
const targets = this.getElementsToCheck(element, options);
|
||||
for (const target of targets) {
|
||||
if (!this.matchesOptions(target, options)) continue;
|
||||
|
||||
callback(target);
|
||||
if (options.once) {
|
||||
this.firedOnceIds.add(id);
|
||||
this.unregisterById(event, id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,20 @@ import {
|
||||
OpenMenuOptions,
|
||||
} from "@/seqta/utils/Openers/OpenMenuOptions";
|
||||
|
||||
import { ThemeManager } from "@/plugins/built-in/themes/theme-manager";
|
||||
import {
|
||||
CloseThemeCreator,
|
||||
OpenThemeCreator,
|
||||
} from "@/plugins/built-in/themes/ThemeCreator";
|
||||
import sendThemeUpdate from "@/seqta/utils/sendThemeUpdate";
|
||||
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent";
|
||||
import type { ThemeManager } from "@/plugins/built-in/themes/theme-manager";
|
||||
|
||||
const themeManager = ThemeManager.getInstance();
|
||||
let themeManagerPromise: Promise<ThemeManager> | null = null;
|
||||
|
||||
function getThemeManager(): Promise<ThemeManager> {
|
||||
if (!themeManagerPromise) {
|
||||
themeManagerPromise = import("@/plugins/built-in/themes/theme-manager").then(
|
||||
({ ThemeManager }) => ThemeManager.getInstance(),
|
||||
);
|
||||
}
|
||||
return themeManagerPromise;
|
||||
}
|
||||
|
||||
export class MessageHandler {
|
||||
constructor() {
|
||||
@@ -34,6 +39,7 @@ export class MessageHandler {
|
||||
case "UpdateThemePreview":
|
||||
if (request?.save == true) {
|
||||
const save = async () => {
|
||||
const themeManager = await getThemeManager();
|
||||
await themeManager.saveTheme({
|
||||
...request.body,
|
||||
userEdited: true,
|
||||
@@ -44,65 +50,88 @@ export class MessageHandler {
|
||||
sendResponse({ status: "success" });
|
||||
sendThemeUpdate();
|
||||
};
|
||||
save();
|
||||
void save();
|
||||
} else {
|
||||
themeManager.updatePreview(request.body);
|
||||
sendResponse({ status: "success" });
|
||||
void getThemeManager().then((themeManager) => {
|
||||
themeManager.updatePreview(request.body);
|
||||
sendResponse({ status: "success" });
|
||||
});
|
||||
}
|
||||
return true;
|
||||
|
||||
case "GetTheme":
|
||||
themeManager.getTheme(request.body.themeID).then((theme) => {
|
||||
sendResponse(theme);
|
||||
void getThemeManager().then((themeManager) => {
|
||||
themeManager.getTheme(request.body.themeID).then((theme) => {
|
||||
sendResponse(theme);
|
||||
});
|
||||
});
|
||||
return true;
|
||||
|
||||
case "SetTheme":
|
||||
themeManager.setTheme(request.body.themeID).then(() => {
|
||||
sendResponse({ status: "success" });
|
||||
void getThemeManager().then((themeManager) => {
|
||||
themeManager.setTheme(request.body.themeID).then(() => {
|
||||
sendResponse({ status: "success" });
|
||||
});
|
||||
});
|
||||
return true;
|
||||
|
||||
case "DisableTheme":
|
||||
themeManager.disableTheme().then(() => {
|
||||
sendResponse({ status: "success" });
|
||||
void getThemeManager().then((themeManager) => {
|
||||
themeManager.disableTheme().then(() => {
|
||||
sendResponse({ status: "success" });
|
||||
});
|
||||
});
|
||||
return true;
|
||||
|
||||
case "DeleteTheme":
|
||||
themeManager.deleteTheme(request.body.themeID).then(() => {
|
||||
sendResponse({ status: "success" });
|
||||
void getThemeManager().then((themeManager) => {
|
||||
themeManager.deleteTheme(request.body.themeID).then(() => {
|
||||
sendResponse({ status: "success" });
|
||||
});
|
||||
});
|
||||
return true;
|
||||
|
||||
case "ListThemes":
|
||||
themeManager.getAvailableThemes().then((themes) => {
|
||||
sendResponse(themes);
|
||||
void getThemeManager().then((themeManager) => {
|
||||
themeManager.getAvailableThemes().then((themes) => {
|
||||
sendResponse(themes);
|
||||
});
|
||||
});
|
||||
return true;
|
||||
|
||||
case "OpenThemeCreator":
|
||||
case "OpenThemeCreator": {
|
||||
const themeID = request?.body?.themeID;
|
||||
OpenThemeCreator(themeID ? themeID : "");
|
||||
void import("@/plugins/built-in/themes/ThemeCreator").then(
|
||||
({ OpenThemeCreator }) => {
|
||||
void OpenThemeCreator(themeID ? themeID : "");
|
||||
},
|
||||
);
|
||||
closeExtensionPopup();
|
||||
sendResponse({ status: "success" });
|
||||
break;
|
||||
}
|
||||
|
||||
case "ShareTheme":
|
||||
themeManager.shareTheme(request.body.themeID).then((id) => {
|
||||
sendResponse({ status: "success", id });
|
||||
void getThemeManager().then((themeManager) => {
|
||||
themeManager.shareTheme(request.body.themeID).then((id) => {
|
||||
sendResponse({ status: "success", id });
|
||||
});
|
||||
});
|
||||
return true;
|
||||
|
||||
case "CloseThemeCreator":
|
||||
try {
|
||||
CloseThemeCreator();
|
||||
sendResponse({ status: "success" });
|
||||
} catch (error) {
|
||||
console.error("Error closing theme creator:", error);
|
||||
sendResponse({ status: "error" });
|
||||
}
|
||||
break;
|
||||
void import("@/plugins/built-in/themes/ThemeCreator").then(
|
||||
({ CloseThemeCreator }) => {
|
||||
try {
|
||||
CloseThemeCreator();
|
||||
sendResponse({ status: "success" });
|
||||
} catch (error) {
|
||||
console.error("Error closing theme creator:", error);
|
||||
sendResponse({ status: "error" });
|
||||
}
|
||||
},
|
||||
);
|
||||
return true;
|
||||
|
||||
case "HideSensitive":
|
||||
hideSensitiveContent();
|
||||
|
||||
@@ -16,6 +16,23 @@ function isExcludedSettingsKey(key: string): boolean {
|
||||
return EXCLUDED_FROM_SETTINGS_SURFACE.has(key);
|
||||
}
|
||||
|
||||
const SAVE_DEBOUNCE_MS = 200;
|
||||
|
||||
function storageChangeIsNoop(oldValue: unknown, newValue: unknown): boolean {
|
||||
if (oldValue === newValue) return true;
|
||||
if (
|
||||
oldValue === undefined ||
|
||||
newValue === undefined ||
|
||||
typeof oldValue !== "object" ||
|
||||
typeof newValue !== "object" ||
|
||||
oldValue === null ||
|
||||
newValue === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return JSON.stringify(oldValue) === JSON.stringify(newValue);
|
||||
}
|
||||
|
||||
type ChangeListener = (newValue: any, oldValue: any) => void;
|
||||
type GlobalChangeListener = (newValue: any, oldValue: any, key: string) => void;
|
||||
|
||||
@@ -25,9 +42,11 @@ class StorageManager {
|
||||
private listeners: Map<string, Set<ChangeListener>>;
|
||||
private globalListeners: Set<GlobalChangeListener>;
|
||||
private subscribers: Set<Subscriber<SettingsState>> = new Set();
|
||||
private saveTimeout: NodeJS.Timeout | null = null;
|
||||
private saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private pendingPatch: Record<string, unknown> = {};
|
||||
private initialized = false;
|
||||
private bootstrapping = false;
|
||||
private suppressWrites = false;
|
||||
|
||||
private constructor() {
|
||||
this.data = {} as SettingsState;
|
||||
@@ -151,12 +170,14 @@ class StorageManager {
|
||||
});
|
||||
}
|
||||
|
||||
public async saveToStorage(changedKeys?: string[]): Promise<void> {
|
||||
if (this.saveTimeout) {
|
||||
clearTimeout(this.saveTimeout);
|
||||
this.saveTimeout = null;
|
||||
public setSuppressWrites(suppress: boolean): void {
|
||||
this.suppressWrites = suppress;
|
||||
if (!suppress) {
|
||||
this.scheduleDebouncedSave();
|
||||
}
|
||||
const payload: Record<string, unknown> = {};
|
||||
}
|
||||
|
||||
private queueStoragePatch(changedKeys?: string[]): void {
|
||||
const keys =
|
||||
changedKeys && changedKeys.length > 0
|
||||
? changedKeys
|
||||
@@ -166,18 +187,42 @@ class StorageManager {
|
||||
if (isExcludedSettingsKey(key)) continue;
|
||||
const value = (this.data as Record<string, unknown>)[key];
|
||||
if (value !== undefined) {
|
||||
payload[key] = value;
|
||||
this.pendingPatch[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(payload).length === 0) return;
|
||||
private scheduleDebouncedSave(): void {
|
||||
if (this.bootstrapping || this.suppressWrites) return;
|
||||
if (Object.keys(this.pendingPatch).length === 0) return;
|
||||
|
||||
await browser.storage.local.set(payload);
|
||||
if (this.saveTimeout) {
|
||||
clearTimeout(this.saveTimeout);
|
||||
}
|
||||
this.saveTimeout = setTimeout(() => {
|
||||
void this.flushPendingPatch();
|
||||
}, SAVE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
private async flushPendingPatch(): Promise<void> {
|
||||
this.saveTimeout = null;
|
||||
if (this.bootstrapping || this.suppressWrites) return;
|
||||
|
||||
const patch = { ...this.pendingPatch };
|
||||
this.pendingPatch = {};
|
||||
if (Object.keys(patch).length === 0) return;
|
||||
|
||||
await browser.storage.local.set(patch);
|
||||
if (!this.bootstrapping) {
|
||||
this.notifySubscribers();
|
||||
}
|
||||
}
|
||||
|
||||
public saveToStorage(changedKeys?: string[]): void {
|
||||
this.queueStoragePatch(changedKeys);
|
||||
this.scheduleDebouncedSave();
|
||||
}
|
||||
|
||||
private async removeFromStorage(key: string): Promise<void> {
|
||||
await browser.storage.local.remove(key);
|
||||
}
|
||||
@@ -189,7 +234,7 @@ class StorageManager {
|
||||
const actualChanges: string[] = [];
|
||||
|
||||
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
|
||||
if (JSON.stringify(oldValue) === JSON.stringify(newValue)) continue;
|
||||
if (storageChangeIsNoop(oldValue, newValue)) continue;
|
||||
if (isExcludedSettingsKey(key)) continue;
|
||||
|
||||
if (newValue !== undefined) {
|
||||
@@ -292,3 +337,7 @@ class StorageManager {
|
||||
export const settingsState = StorageManager.getInstance();
|
||||
export const initializeSettingsState = async () =>
|
||||
await StorageManager.initialize();
|
||||
|
||||
export function setSettingsStateSuppressWrites(suppress: boolean): void {
|
||||
settingsState.setSuppressWrites(suppress);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,15 @@
|
||||
import { OpenStorePage } from "@/seqta/ui/renderStore";
|
||||
|
||||
/**
|
||||
* Module-level handoff for "open the theme store and highlight this theme".
|
||||
*
|
||||
* The store page is mounted lazily inside a Shadow DOM the first time it
|
||||
* opens, so a `CustomEvent` listener would have to be wired up before mount
|
||||
* (causing a race). Using a shared cell keeps the producer (popup button) and
|
||||
* consumer (store `onMount`) decoupled without that timing constraint.
|
||||
*
|
||||
* The store reads & clears this on mount via {@link consumePendingHighlightThemeId}.
|
||||
*/
|
||||
let pendingHighlightThemeId: string | null = null;
|
||||
|
||||
/** Read and clear the pending theme id (called by the store on mount). */
|
||||
export function consumePendingHighlightThemeId(): string | null {
|
||||
const id = pendingHighlightThemeId;
|
||||
pendingHighlightThemeId = null;
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the theme store and asks it to focus / highlight the given theme.
|
||||
* If the store is already mounted we dispatch a DOM event so it can react
|
||||
* without remounting; otherwise the store consumes the pending id on mount.
|
||||
*/
|
||||
export function openThemeStoreWithHighlight(themeId: string): void {
|
||||
export async function openThemeStoreWithHighlight(themeId: string): Promise<void> {
|
||||
pendingHighlightThemeId = themeId;
|
||||
|
||||
const existing = document.getElementById("store");
|
||||
@@ -35,5 +20,6 @@ export function openThemeStoreWithHighlight(themeId: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
OpenStorePage();
|
||||
const { OpenStorePage } = await import("@/seqta/ui/renderStore");
|
||||
await OpenStorePage();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
type SettingsPopupCallback = () => void;
|
||||
|
||||
/**
|
||||
* Singleton that notifies listeners when the in-page settings popup closes.
|
||||
* Used by the colour picker and other overlays tied to ExtensionPopup.
|
||||
*/
|
||||
class SettingsPopup {
|
||||
private static instance: SettingsPopup;
|
||||
private listeners: Set<SettingsPopupCallback> = new Set();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): SettingsPopup {
|
||||
if (!SettingsPopup.instance) {
|
||||
SettingsPopup.instance = new SettingsPopup();
|
||||
}
|
||||
return SettingsPopup.instance;
|
||||
}
|
||||
|
||||
public addListener(callback: SettingsPopupCallback): void {
|
||||
this.listeners.add(callback);
|
||||
}
|
||||
|
||||
public removeListener(callback: SettingsPopupCallback): void {
|
||||
this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
public triggerClose(): void {
|
||||
this.listeners.forEach((callback) => callback());
|
||||
}
|
||||
}
|
||||
|
||||
export const settingsPopup = SettingsPopup.getInstance();
|
||||
@@ -17,7 +17,7 @@ export function setupSettingsButton() {
|
||||
if (SettingsClicked) {
|
||||
closeExtensionPopup(extensionPopup as HTMLElement);
|
||||
} else {
|
||||
renderSettingsIfNeeded();
|
||||
await renderSettingsIfNeeded();
|
||||
|
||||
await delay(30);
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { eventManager } from "@/seqta/utils/listeners/EventManager";
|
||||
import { delay } from "@/seqta/utils/delay";
|
||||
|
||||
/**
|
||||
* Asynchronously waits for an element to be present in the DOM.
|
||||
*
|
||||
* This function can use either a polling mechanism (via `setTimeout`) or
|
||||
* a `MutationObserver` (via `eventManager.register`) to detect the element.
|
||||
* By default, it uses the `eventManager` which is more efficient.
|
||||
* By default uses direct `querySelector` plus a targeted `MutationObserver`
|
||||
* on `document.documentElement`. Polling via `setTimeout` is available as a
|
||||
* fallback when `usePolling` is true.
|
||||
*
|
||||
* @param {string} selector The CSS selector for the target element.
|
||||
* @param {boolean} [usePolling=false] If true, forces the use of `setTimeout` for polling.
|
||||
@@ -24,9 +21,6 @@ export async function waitForElm(
|
||||
if (usePolling) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let iterations = 0;
|
||||
if (maxIterations) {
|
||||
iterations = 0;
|
||||
}
|
||||
const checkForElement = () => {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
@@ -36,6 +30,7 @@ export async function waitForElm(
|
||||
iterations++;
|
||||
if (iterations >= maxIterations) {
|
||||
reject(new Error("Element not found"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
setTimeout(checkForElement, interval);
|
||||
@@ -43,47 +38,46 @@ export async function waitForElm(
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", checkForElement);
|
||||
document.addEventListener("DOMContentLoaded", checkForElement, {
|
||||
once: true,
|
||||
});
|
||||
} else {
|
||||
checkForElement();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
const registerObserver = () => {
|
||||
const { unregister } = eventManager.register(
|
||||
`${selector}`,
|
||||
{
|
||||
customCheck: (element) => element.matches(selector),
|
||||
},
|
||||
async (element) => {
|
||||
resolve(element);
|
||||
await delay(1);
|
||||
unregister(); // Remove the listener once the element is found
|
||||
},
|
||||
);
|
||||
return unregister;
|
||||
};
|
||||
|
||||
let unregister = null;
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
// DOM is still loading, wait for it to be ready
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
unregister = registerObserver();
|
||||
});
|
||||
} else {
|
||||
unregister = registerObserver();
|
||||
}
|
||||
|
||||
const querySelector = () => document.querySelector(selector);
|
||||
const element = querySelector();
|
||||
|
||||
if (element) {
|
||||
if (unregister) unregister();
|
||||
resolve(element);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const tryResolve = (): boolean => {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
resolve(element);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const startObserver = () => {
|
||||
if (tryResolve()) return;
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (tryResolve()) {
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", startObserver, {
|
||||
once: true,
|
||||
});
|
||||
} else {
|
||||
startObserver();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user