feat: apply our exisitng icons to engage sidebar

This commit is contained in:
2026-05-24 17:11:47 +09:30
parent 304ce2e128
commit 475b865000
4 changed files with 195 additions and 113 deletions
+1 -38
View File
@@ -1,3 +1,4 @@
import { waitForEngageMenuList } from "@/seqta/utils/waitForEngageMenuList";
import { addExtensionSettings } from "@/seqta/utils/Adders/AddExtensionSettings";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
import { loadEngageHomePage } from "@/seqta/utils/Loaders/LoadEngageHomePage";
@@ -287,44 +288,6 @@ async function createSettingsButton(parent?: Element) {
);
}
/** Engage mounts the sidebar inside batched React trees; EventManager-based waitForElm can miss `#menu`. Polling `waitForElm` matches the real DOM reliably. */
async function waitForEngageMenuList(): Promise<HTMLElement | null> {
const poll = true as const;
const interval = 100;
const trySelectors: { selector: string; maxIterations: number }[] = [
{ selector: "#menu > ul > li", maxIterations: 500 },
{ selector: "#menu ul", maxIterations: 350 },
{ selector: "#menu", maxIterations: 350 },
];
for (const { selector, maxIterations } of trySelectors) {
try {
await waitForElm(selector, poll, interval, maxIterations);
} catch {
continue;
}
if (selector === "#menu > ul > li") {
const ul = document.querySelector("#menu > ul") as HTMLElement | null;
if (ul) return ul;
} else if (selector === "#menu ul") {
const ul = document.querySelector("#menu ul") as HTMLElement | null;
if (ul) return ul;
} else {
const menu = document.getElementById("menu");
const ul =
(menu?.querySelector("ul") as HTMLElement | null) ??
(menu?.firstElementChild as HTMLElement | null);
if (ul) return ul;
}
}
console.warn(
"[BetterSEQTA+] Engage: could not find a menu list to inject the home button",
);
return null;
}
async function injectEngageHomeButton() {
if (document.getElementById("homebutton")) return;
+153
View File
@@ -0,0 +1,153 @@
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
import {
ChangeMenuItemPositions,
MenuOptionsOpen,
} from "@/seqta/utils/Openers/OpenMenuOptions";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import stringToHTML from "@/seqta/utils/stringToHTML";
import { waitForEngageMenuList } from "@/seqta/utils/waitForEngageMenuList";
import { waitForElm } from "@/seqta/utils/waitForElm";
import { eventManager } from "@/seqta/utils/listeners/EventManager";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
const BETTERSEQTA_ICON_ATTR = "data-betterseqta-icon";
function getMenuLabel(element: HTMLElement): HTMLElement | null {
const label = element.querySelector(":scope > label");
return label instanceof HTMLElement ? label : null;
}
function getTopLevelMenuList(menu = document.getElementById("menu")): HTMLElement | null {
if (!menu) return null;
return (
(menu.querySelector(":scope > ul") as HTMLElement | null) ??
(menu.querySelector("ul") as HTMLElement | null)
);
}
export function isTopLevelSidebarItem(node: HTMLElement): boolean {
if (!node.classList.contains("item")) return false;
if (node.nodeName !== "LI" && node.nodeName !== "SECTION") return false;
const topList = getTopLevelMenuList();
return !!topList && node.parentElement === topList;
}
function wrapMenuLabelText(label: HTMLElement) {
const textNode = label.lastChild;
if (
textNode?.nodeType === 3 &&
textNode.parentNode &&
textNode.parentNode.nodeName !== "SPAN"
) {
const span = document.createElement("span");
span.textContent = textNode.nodeValue;
label.replaceChild(span, textNode);
}
}
export function replaceMenuSVG(element: HTMLElement, svg: string) {
const label = getMenuLabel(element);
if (!label?.firstChild) return;
if (label.firstElementChild?.getAttribute(BETTERSEQTA_ICON_ATTR) === "true") {
return;
}
label.firstChild.remove();
label.innerHTML = `<span>${label.innerHTML}</span>`;
const newSvg = stringToHTML(svg).firstChild;
if (!(newSvg instanceof Element)) return;
newSvg.setAttribute(BETTERSEQTA_ICON_ATTR, "true");
label.insertBefore(newSvg, label.firstChild);
}
export function processMenuItemNode(node: HTMLElement) {
if (!isTopLevelSidebarItem(node) || MenuOptionsOpen) return;
const key = node.dataset.key as keyof typeof MenuitemSVGKey | undefined;
if (key && MenuitemSVGKey[key]) {
replaceMenuSVG(node, MenuitemSVGKey[key]);
} else {
const label = getMenuLabel(node);
if (label) wrapMenuLabelText(label);
}
}
function processTopLevelMenuItems(reorder = !isSeqtaEngageExperience()) {
if (MenuOptionsOpen) return;
const topList = getTopLevelMenuList();
if (!topList) return;
for (const child of topList.children) {
if (child instanceof HTMLElement) {
processMenuItemNode(child);
}
}
if (reorder) {
ChangeMenuItemPositions(settingsState.menuorder);
}
}
let engageMenuIconObserver: MutationObserver | null = null;
let engageMenuIconFrame: number | null = null;
function scheduleEngageMenuIconPass() {
if (engageMenuIconFrame !== null) return;
engageMenuIconFrame = window.requestAnimationFrame(() => {
engageMenuIconFrame = null;
processTopLevelMenuItems(false);
});
}
async function observeEngageMenuIcons() {
const menuList = await waitForEngageMenuList();
const menu = document.getElementById("menu");
if (!menu || !menuList) return;
processTopLevelMenuItems(false);
engageMenuIconObserver?.disconnect();
engageMenuIconObserver = new MutationObserver(() => {
scheduleEngageMenuIconPass();
});
engageMenuIconObserver.observe(menu, {
childList: true,
subtree: true,
});
}
const processedSymbol = Symbol("processed");
export async function observeMenuItemPosition() {
if (isSeqtaEngageExperience()) {
await observeEngageMenuIcons();
return;
}
await waitForElm("#menu > ul > li");
eventManager.register(
"menuList",
{
parentElement: document.querySelector("#menu")!.firstChild as Element,
},
(element: Element) => {
const node = element as HTMLElement;
if (!isTopLevelSidebarItem(node)) return;
if ((element as any)[processedSymbol]) return;
if (!MenuOptionsOpen) {
processMenuItemNode(node);
ChangeMenuItemPositions(settingsState.menuorder);
(element as any)[processedSymbol] = true;
}
},
);
}
+39
View File
@@ -0,0 +1,39 @@
import { waitForElm } from "@/seqta/utils/waitForElm";
/** Engage mounts the sidebar inside batched React trees; polling `waitForElm` matches the real DOM reliably. */
export async function waitForEngageMenuList(): Promise<HTMLElement | null> {
const poll = true as const;
const interval = 100;
const trySelectors: { selector: string; maxIterations: number }[] = [
{ selector: "#menu > ul > li", maxIterations: 500 },
{ selector: "#menu ul", maxIterations: 350 },
{ selector: "#menu", maxIterations: 350 },
];
for (const { selector, maxIterations } of trySelectors) {
try {
await waitForElm(selector, poll, interval, maxIterations);
} catch {
continue;
}
if (selector === "#menu > ul > li") {
const ul = document.querySelector("#menu > ul") as HTMLElement | null;
if (ul) return ul;
} else if (selector === "#menu ul") {
const ul = document.querySelector("#menu ul") as HTMLElement | null;
if (ul) return ul;
} else {
const menu = document.getElementById("menu");
const ul =
(menu?.querySelector("ul") as HTMLElement | null) ??
(menu?.firstElementChild as HTMLElement | null);
if (ul) return ul;
}
}
console.warn(
"[BetterSEQTA+] Engage: could not find a menu list to inject the home button",
);
return null;
}