diff --git a/src/css/injected.scss b/src/css/injected.scss index dea7df9a..358da60e 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -2794,39 +2794,9 @@ body { .defaultWelcomeWrapper { background: unset !important; } -.clr-swatches button::after, -.clr-dark .clr-preview::after, -.clr-field button::after { - opacity: unset; - padding-top: unset; - -webkit-transform: unset; - transform: unset; - -webkit-transform-origin: unset; - transform-origin: unset; - visibility: unset; - -webkit-animation-name: unset !important; - animation-name: unset !important; - background-color: currentColor !important; -} -.clr-swatches button { - align-items: unset; - display: block; - padding: unset; - transition: none; -} -.clr-clear { - display: none !important; -} -.clr-preview::before, -.clr-preview::after { - visibility: unset; - -webkit-transform-origin: unset; - transform-origin: unset; - -webkit-transform: unset; - transform: unset; - padding-top: unset; - opacity: unset; -} + +/* Coloris (timetable subject colours): cosmetic only — do not unset + transforms/animations on ::after (breaks picker reopen). */ #clr-color-preview { margin: 15px 0 20px 20px; border: 0; @@ -2835,6 +2805,10 @@ body { cursor: pointer; } +.clr-swatches button { + border-radius: 4px; +} + .dark [class*="MessageList__MessageList___"] > ol diff --git a/src/plugins/built-in/gradeAnalytics/core/index.ts b/src/plugins/built-in/gradeAnalytics/core/index.ts index 43313b88..e32da32f 100644 --- a/src/plugins/built-in/gradeAnalytics/core/index.ts +++ b/src/plugins/built-in/gradeAnalytics/core/index.ts @@ -2,8 +2,14 @@ import type { Plugin } from "@/plugins/core/types"; import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json"; import { waitForElm } from "@/seqta/utils/waitForElm"; import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"; -import { processMenuItemNode } from "@/seqta/utils/sidebarMenuIcons"; +import { + ChangeMenuItemPositions, + ensureAnalyticsMenuOrder, + insertMenuItemAfterKey, + processMenuItemNode, +} from "@/seqta/utils/sidebarMenuIcons"; import { MenuOptionsOpen } from "@/seqta/utils/Openers/OpenMenuOptions"; +import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { applyMenuItemVisibility, isMenuItemHidden, @@ -45,11 +51,14 @@ const gradeAnalyticsPlugin: Plugin<{}> = { analyticsItem.dataset.betterseqta = "true"; analyticsItem.innerHTML = ``; - const homeButton = document.getElementById("homebutton"); - if (homeButton?.parentElement === menuList) { - homeButton.insertAdjacentElement("afterend", analyticsItem); - } else { - menuList.insertBefore(analyticsItem, menuList.firstChild); + const placeAnalyticsItem = () => { + insertMenuItemAfterKey(menuList, analyticsItem, "courses"); + }; + + placeAnalyticsItem(); + ensureAnalyticsMenuOrder(); + if (settingsState.menuorder.length > 0) { + ChangeMenuItemPositions(settingsState.menuorder); } processMenuItemNode(analyticsItem); @@ -59,10 +68,10 @@ const gradeAnalyticsPlugin: Plugin<{}> = { if (MenuOptionsOpen) return; if (isMenuItemHidden("analytics")) return; if (!menuList.contains(analyticsItem)) { - if (homeButton?.parentElement === menuList) { - homeButton.insertAdjacentElement("afterend", analyticsItem); - } else { - menuList.insertBefore(analyticsItem, menuList.firstChild); + placeAnalyticsItem(); + ensureAnalyticsMenuOrder(); + if (settingsState.menuorder.length > 0) { + ChangeMenuItemPositions(settingsState.menuorder); } processMenuItemNode(analyticsItem); applyMenuItemVisibility(); diff --git a/src/plugins/built-in/timetable/index.ts b/src/plugins/built-in/timetable/index.ts index 99c9bd0a..950e06bc 100644 --- a/src/plugins/built-in/timetable/index.ts +++ b/src/plugins/built-in/timetable/index.ts @@ -1,6 +1,9 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import type { Plugin } from "../../core/types"; -import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat"; +import { + attachTimetableColorisRecovery, + resetStuckColorisPicker, +} from "@/seqta/utils/timetableColoris"; import { waitForElm } from "@/seqta/utils/waitForElm"; const timetablePlugin: Plugin<{}, {}> = { @@ -63,6 +66,9 @@ function resetTimetableStyles(): void { } async function handleTimetable(): Promise { + attachTimetableColorisRecovery(); + resetStuckColorisPicker(); + // SEQTA uses `.times` blocks on entries, not necessarily `.time`; avoid infinite polling on a missing selector. try { await waitForElm(".timetablepage .times, .timetablepage .entry.class", true, 50, 200); diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index da224f48..afb53e55 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -30,6 +30,7 @@ import { loadAnalyticsPage } from "@/plugins/built-in/gradeAnalytics/loadAnalyti import { runStartupPopupQueue } from "@/seqta/utils/Openers/StartupPopupQueue"; import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes"; +import { attachTimetableColorisRecovery } from "@/seqta/utils/timetableColoris"; // JSON content import { observeMenuItemPosition } from "@/seqta/utils/sidebarMenuIcons"; @@ -247,6 +248,7 @@ async function LoadPageElements(): Promise { className: "timetablepage", }, async () => { + attachTimetableColorisRecovery(); await updateTimetableTimes(); }, ); diff --git a/src/seqta/utils/Openers/OpenMenuOptions.ts b/src/seqta/utils/Openers/OpenMenuOptions.ts index 157efbe5..ac774238 100644 --- a/src/seqta/utils/Openers/OpenMenuOptions.ts +++ b/src/seqta/utils/Openers/OpenMenuOptions.ts @@ -1,6 +1,7 @@ import type { SettingsState } from "@/types/storage"; import { settingsState } from "../listeners/SettingsState"; import { applyMenuItemVisibility } from "../menuItemVisibility"; +import { insertKeyAfterInOrder } from "@/seqta/utils/sidebarMenuIcons"; import stringToHTML from "../stringToHTML"; import Sortable from "sortablejs"; @@ -57,9 +58,18 @@ export function OpenMenuOptions() { key && settingsState.defaultmenuorder.indexOf(key) === -1 ) { - let newdefaultmenuorder = settingsState.defaultmenuorder; - newdefaultmenuorder.push(key); - settingsState.defaultmenuorder = newdefaultmenuorder; + if (key === "analytics") { + settingsState.defaultmenuorder = insertKeyAfterInOrder( + settingsState.defaultmenuorder, + key, + "courses", + ); + } else { + settingsState.defaultmenuorder = [ + ...settingsState.defaultmenuorder, + key, + ]; + } } } } diff --git a/src/seqta/utils/sidebarMenuIcons.ts b/src/seqta/utils/sidebarMenuIcons.ts index 48b49875..b275174d 100644 --- a/src/seqta/utils/sidebarMenuIcons.ts +++ b/src/seqta/utils/sidebarMenuIcons.ts @@ -12,6 +12,58 @@ import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"; const BETTERSEQTA_ICON_ATTR = "data-betterseqta-icon"; +export function insertMenuItemAfterKey( + menuList: HTMLElement, + item: HTMLElement, + afterKey: string, +): void { + const after = menuList.querySelector( + `:scope > li[data-key="${afterKey}"], :scope > section[data-key="${afterKey}"]`, + ); + if (after instanceof HTMLElement) { + after.insertAdjacentElement("afterend", item); + } else { + menuList.appendChild(item); + } +} + +export function insertKeyAfterInOrder( + order: string[], + key: string, + afterKey: string, +): string[] { + if (order.includes(key)) return order; + const copy = [...order]; + const afterIdx = copy.indexOf(afterKey); + if (afterIdx >= 0) { + copy.splice(afterIdx + 1, 0, key); + } else { + copy.push(key); + } + return copy; +} + +/** Default Analytics immediately below Courses in saved menu order. */ +export function ensureAnalyticsMenuOrder(): void { + if (!settingsState.defaultmenuorder.includes("analytics")) { + settingsState.defaultmenuorder = insertKeyAfterInOrder( + settingsState.defaultmenuorder, + "analytics", + "courses", + ); + } + if ( + settingsState.menuorder.length > 0 && + !settingsState.menuorder.includes("analytics") + ) { + settingsState.menuorder = insertKeyAfterInOrder( + settingsState.menuorder, + "analytics", + "courses", + ); + } +} + function getMenuLabel(element: HTMLElement): HTMLElement | null { const label = element.querySelector(":scope > label"); return label instanceof HTMLElement ? label : null; diff --git a/src/seqta/utils/timetableColoris.ts b/src/seqta/utils/timetableColoris.ts new file mode 100644 index 00000000..f9adcd7e --- /dev/null +++ b/src/seqta/utils/timetableColoris.ts @@ -0,0 +1,47 @@ +/** + * SEQTA timetable uses Coloris for subject colours. Extension CSS previously + * unset Coloris ::after animations, which left the picker unable to reopen. + * This module clears stuck `clr-open` / hidden picker state after each use. + */ + +let attached = false; + +export function resetStuckColorisPicker(): void { + document.body.classList.remove("clr-open"); + document.documentElement.classList.remove("clr-open"); + + for (const picker of document.querySelectorAll(".clr-picker")) { + picker.classList.remove("clr-open"); + if (picker instanceof HTMLElement) { + picker.style.removeProperty("display"); + picker.style.removeProperty("pointer-events"); + picker.style.removeProperty("visibility"); + } + } + + for (const field of document.querySelectorAll(".clr-field")) { + field.classList.remove("clr-open"); + } +} + +export function attachTimetableColorisRecovery(): void { + if (attached) return; + attached = true; + + const afterColorisEvent = () => { + requestAnimationFrame(() => resetStuckColorisPicker()); + }; + + document.addEventListener("coloris:pick", afterColorisEvent); + document.addEventListener("coloris:close", afterColorisEvent); + + document.addEventListener( + "click", + (event) => { + const target = event.target as HTMLElement; + if (!target.closest(".timetablepage [title='Choose a colour']")) return; + resetStuckColorisPicker(); + }, + true, + ); +}