fix(timetable): reopen colour picker

+ some quick fixes to dropdowns again
This commit is contained in:
2026-06-22 20:19:17 +09:30
parent fadbca5969
commit 2be27299a5
18 changed files with 922 additions and 140 deletions
+2 -1
View File
@@ -52,7 +52,8 @@ export default function ClosePlugin(): Plugin {
*/ */
closeBundle() { closeBundle() {
console.log("Bundle closed"); // Log successful closure of the bundle console.log("Bundle closed"); // Log successful closure of the bundle
process.exit(0); // Exit with status 0 indicating a successful build // Do not process.exit here — it can mask Vite render errors and break
// multi-target builds (`npm run build` runs chrome then firefox).
}, },
}; };
} }
+4
View File
@@ -10,6 +10,7 @@ import * as plugins from "@/plugins";
import { main } from "@/seqta/main"; import { main } from "@/seqta/main";
import { delay } from "./seqta/utils/delay"; import { delay } from "./seqta/utils/delay";
import { initializeHideSensitiveToggle } from "@/seqta/utils/hideSensitiveToggle"; import { initializeHideSensitiveToggle } from "@/seqta/utils/hideSensitiveToggle";
import { installSeqtaMenuColourPatch } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
function registerFetchSeqtaAppLinkListener() { function registerFetchSeqtaAppLinkListener() {
browser.runtime.onMessage.addListener((request, _sender, sendResponse) => { browser.runtime.onMessage.addListener((request, _sender, sendResponse) => {
@@ -46,6 +47,9 @@ if (document.childNodes[1]) {
document.childNodes[1].textContent?.includes( document.childNodes[1].textContent?.includes(
"Copyright (c) SEQTA Software", "Copyright (c) SEQTA Software",
) ?? false; ) ?? false;
if (hasSEQTAText) {
installSeqtaMenuColourPatch();
}
init(); init();
} }
+50 -4
View File
@@ -34,7 +34,8 @@
display: none; display: none;
} }
button.uiButton.timetable-zoom.iconFamily, button.timetable-zoom.iconFamily,
button.bsplus-timetable-control.iconFamily,
.iconFamily { .iconFamily {
font-family: "IconFamily" !important; font-family: "IconFamily" !important;
} }
@@ -219,6 +220,13 @@ select option {
pointer-events: none !important; pointer-events: none !important;
} }
/* Colour picker dialog teardown can leave an empty shell that blocks clicks */
.modaliser-container:not(:has(.modaliser > *)) {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
}
.connectedNotificationsWrapper > div > button > svg > g { .connectedNotificationsWrapper > div > button > svg > g {
fill: var(--theme-primary) !important; fill: var(--theme-primary) !important;
} }
@@ -335,12 +343,18 @@ select option {
} }
.timetable-zoom, .timetable-zoom,
.timetable-hide { .timetable-hide,
.bsplus-timetable-control {
font-size: 14px !important; font-size: 14px !important;
line-height: 1 !important; line-height: 1 !important;
display: inline-flex !important; display: inline-flex !important;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: transparent;
border: none;
color: var(--text-primary);
cursor: pointer;
padding: 4px 8px;
} }
#main > .dashboard { #main > .dashboard {
@@ -848,6 +862,11 @@ ol:has([class*="MessageList__avatar___"] svg) {
.quickbar .actions [title="Choose a colour"] > svg { .quickbar .actions [title="Choose a colour"] > svg {
scale: 0.9; scale: 0.9;
} }
.quickbar .actions .timetable-edit-quickbar-btn > svg {
scale: 0.9;
padding-top: 1px;
}
.quickbar[data-yiq="light"] .actions { .quickbar[data-yiq="light"] .actions {
color: white !important; color: white !important;
} }
@@ -1078,7 +1097,13 @@ div > ol:has(.uiFileHandlerWrapper) {
min-height: 128px !important; min-height: 128px !important;
} }
body.student #menu > ul::before { body.student #menu > ul::before {
content: "";
display: block;
width: 100%;
background-image: var(--betterseqta-logo) !important; background-image: var(--betterseqta-logo) !important;
background-position: center;
background-repeat: no-repeat;
background-size: auto 48px;
position: -webkit-sticky; position: -webkit-sticky;
position: sticky; position: sticky;
top: 0; top: 0;
@@ -2712,11 +2737,24 @@ body {
.days { .days {
width: 100%; width: 100%;
} }
.modaliser { /* Do not hide .modaliser globally — SEQTA Modaliser relies on transitionend to
display: none; dispose; display:none prevents that and leaves empty shells that block clicks. */
.modaliser-container:not(.visible) {
display: none !important;
pointer-events: none !important;
}
.modaliser-container.visible .modaliser {
background: var(--better-main); background: var(--better-main);
} }
/* ColourChooser teardown can leave a full-screen uiSlidePane that blocks entry clicks */
.uiSlidePane:not(.shown):has(.pane.colourChooser) {
display: none !important;
pointer-events: none !important;
visibility: hidden !important;
}
[class*="MessageList__unread___"] { [class*="MessageList__unread___"] {
position: relative; position: relative;
background: var(--background-secondary, rgb(228 225 225)); background: var(--background-secondary, rgb(228 225 225));
@@ -2809,6 +2847,14 @@ body {
border-radius: 4px; border-radius: 4px;
} }
/* Never let a closed Coloris picker intercept timetable clicks */
body:not(.clr-open) .clr-picker,
.clr-picker:not(.clr-open) {
display: none !important;
pointer-events: none !important;
visibility: hidden !important;
}
.dark .dark
[class*="MessageList__MessageList___"] [class*="MessageList__MessageList___"]
> ol > ol
+127 -40
View File
@@ -1,83 +1,170 @@
<script lang="ts"> <script lang="ts">
let { state, onChange, options } = $props<{ let { value, onChange, options } = $props<{
state: string, value: string,
onChange: (newState: string) => void, onChange: (newValue: string) => void,
options: Array<{ value: string, label: string }> options: Array<{ value: string, label: string }>
}>(); }>();
let select: HTMLSelectElement; let isOpen = $state(false);
let root: HTMLDivElement | undefined = $state();
const selectedLabel = $derived(
options.find((option) => option.value === value)?.label ?? value,
);
function toggleOpen() {
isOpen = !isOpen;
}
function selectValue(nextValue: string) {
onChange(nextValue);
isOpen = false;
}
$effect(() => {
if (!isOpen) return;
const onPointerDown = (event: PointerEvent) => {
const path = event.composedPath();
if (root && path.includes(root)) return;
isOpen = false;
};
document.addEventListener("pointerdown", onPointerDown, true);
return () => document.removeEventListener("pointerdown", onPointerDown, true);
});
</script> </script>
<div class="select-wrapper relative w-full overflow-hidden rounded-2xl border shadow-2xl"> <div class="select-wrapper" bind:this={root}>
<select <button
bind:this={select} type="button"
value={state} class="select-trigger"
onchange={() => onChange(select.value)} aria-haspopup="listbox"
class="select-input w-full appearance-none border-none bg-transparent px-4 py-2.5 pr-10 text-[0.875rem] font-medium transition-colors" aria-expanded={isOpen}
onclick={toggleOpen}
> >
{#each options as option} <span class="select-label">{selectedLabel}</span>
<option value={option.value}> <span class="select-icon" aria-hidden="true">
{option.label}
</option>
{/each}
</select>
<span class="select-icon pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3" aria-hidden="true">
<svg viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4"> <svg viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z" clip-rule="evenodd"></path> <path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z"
clip-rule="evenodd"
></path>
</svg> </svg>
</span> </span>
</button>
{#if isOpen}
<ul class="select-menu" role="listbox">
{#each options as option (option.value)}
<li role="option" aria-selected={option.value === value}>
<button
type="button"
class="select-option"
class:is-selected={option.value === value}
onclick={() => selectValue(option.value)}
>
{option.label}
</button>
</li>
{/each}
</ul>
{/if}
</div> </div>
<style> <style>
.select-wrapper { .select-wrapper {
background: color-mix(in srgb, var(--background-primary) 88%, transparent); position: relative;
border-color: color-mix(in srgb, var(--theme-offset-bg, var(--background-secondary)) 72%, transparent); width: 100%;
}
.select-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
width: 100%;
border: 1px solid color-mix(in srgb, var(--theme-offset-bg, var(--background-secondary)) 72%, transparent);
border-radius: 18px; border-radius: 18px;
background: color-mix(in srgb, var(--background-primary) 88%, transparent);
color: var(--text-primary); color: var(--text-primary);
padding: 0.625rem 1rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25;
cursor: pointer;
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
transition: transition:
background-color 180ms ease, background-color 180ms ease,
border-color 180ms ease, border-color 180ms ease,
box-shadow 180ms ease, box-shadow 180ms ease;
transform 180ms ease;
} }
.select-wrapper:hover { .select-trigger:hover {
background: color-mix(in srgb, var(--background-primary) 94%, var(--background-secondary) 6%); background: color-mix(in srgb, var(--background-primary) 94%, var(--background-secondary) 6%);
border-color: color-mix(in srgb, var(--theme-offset-bg, var(--background-secondary)) 88%, transparent); border-color: color-mix(in srgb, var(--theme-offset-bg, var(--background-secondary)) 88%, transparent);
} }
.select-wrapper:focus-within { .select-trigger:focus-visible {
outline: none;
background: color-mix(in srgb, var(--background-primary) 96%, var(--background-secondary) 4%); background: color-mix(in srgb, var(--background-primary) 96%, var(--background-secondary) 4%);
border-color: color-mix(in srgb, var(--text-primary) 22%, var(--theme-offset-bg, var(--background-secondary)) 78%); border-color: color-mix(in srgb, var(--text-primary) 22%, var(--theme-offset-bg, var(--background-secondary)) 78%);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-primary) 12%, transparent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-primary) 12%, transparent);
} }
.select-input { .select-label {
color: var(--text-primary); overflow: hidden;
outline: none;
text-overflow: ellipsis; text-overflow: ellipsis;
} white-space: nowrap;
.select-input:hover,
.select-input:focus {
background: transparent;
} }
.select-icon { .select-icon {
flex-shrink: 0;
color: color-mix(in srgb, var(--text-primary) 60%, transparent); color: color-mix(in srgb, var(--text-primary) 60%, transparent);
} }
.select-input { .select-menu {
color-scheme: light; position: absolute;
top: calc(100% + 0.35rem);
left: 0;
right: 0;
z-index: 50;
margin: 0;
padding: 0.35rem;
list-style: none;
border: 1px solid color-mix(in srgb, var(--theme-offset-bg, var(--background-secondary)) 72%, transparent);
border-radius: 14px;
background: var(--background-primary);
box-shadow:
0 10px 25px -5px rgb(0 0 0 / 0.25),
0 8px 10px -6px rgb(0 0 0 / 0.2);
max-height: 16rem;
overflow-y: auto;
} }
:global(.dark) .select-input { .select-option {
color-scheme: dark; display: block;
width: 100%;
border: none;
border-radius: 10px;
background: transparent;
color: var(--text-primary);
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
font-weight: 500;
text-align: left;
cursor: pointer;
transition: background-color 150ms ease;
} }
/* Native option lists on Windows/Edge often stay light regardless of color-scheme */ .select-option:hover,
.select-input option { .select-option:focus-visible {
background-color: #ffffff; outline: none;
color: #18181b; background: color-mix(in srgb, var(--background-secondary) 55%, transparent);
}
.select-option.is-selected {
background: color-mix(in srgb, var(--background-secondary) 70%, transparent);
} }
</style> </style>
+3 -3
View File
@@ -249,7 +249,7 @@
id: 10, id: 10,
Component: Select, Component: Select,
props: { props: {
state: $settingsState.defaultPage ?? "home", value: $settingsState.defaultPage ?? "home",
onChange: (value: string) => (settingsState.defaultPage = value), onChange: (value: string) => (settingsState.defaultPage = value),
options: [ options: [
{ value: "home", label: "Home" }, { value: "home", label: "Home" },
@@ -268,7 +268,7 @@
id: 11, id: 11,
Component: Select, Component: Select,
props: { props: {
state: $settingsState.newsSource, value: $settingsState.newsSource,
onChange: (value: string) => settingsState.newsSource = value, onChange: (value: string) => settingsState.newsSource = value,
options: [ options: [
{ value: "australia", label: "Australia" }, { value: "australia", label: "Australia" },
@@ -405,7 +405,7 @@
/> />
{:else if setting.type === 'select'} {:else if setting.type === 'select'}
<Select <Select
state={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default} value={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)} onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
options={(setting.options as string[]).map(opt => ({ options={(setting.options as string[]).map(opt => ({
value: opt, value: opt,
+9
View File
@@ -0,0 +1,9 @@
import browser from "webextension-polyfill";
/** Vite `?url` imports are already absolute extension URLs in production bundles. */
export function resolveExtensionAssetUrl(importedUrl: string): string {
if (/^(chrome-extension|moz-extension|https?):/.test(importedUrl)) {
return importedUrl;
}
return browser.runtime.getURL(importedUrl.replace(/^\/+/, ""));
}
@@ -3,12 +3,14 @@ import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
import { waitForElm } from "@/seqta/utils/waitForElm"; import { waitForElm } from "@/seqta/utils/waitForElm";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"; import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
import { import {
ChangeMenuItemPositions,
ensureAnalyticsMenuOrder, ensureAnalyticsMenuOrder,
insertMenuItemAfterKey, insertMenuItemAfterKey,
processMenuItemNode, processMenuItemNode,
} from "@/seqta/utils/sidebarMenuIcons"; } from "@/seqta/utils/sidebarMenuIcons";
import { MenuOptionsOpen } from "@/seqta/utils/Openers/OpenMenuOptions"; import {
ChangeMenuItemPositions,
MenuOptionsOpen,
} from "@/seqta/utils/Openers/OpenMenuOptions";
import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { import {
applyMenuItemVisibility, applyMenuItemVisibility,
@@ -885,7 +885,7 @@
min-width: 0; min-width: 0;
} }
.bsplus-analytics-chart-cell > :global(.bsplus-analytics-card) { .bsplus-analytics-chart-cell > .bsplus-analytics-card {
flex: 1; flex: 1;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
+8 -8
View File
@@ -1,9 +1,7 @@
import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import type { Plugin } from "../../core/types"; import type { Plugin } from "../../core/types";
import { import { attachTimetableColorisRecovery } from "@/seqta/utils/timetableColoris";
attachTimetableColorisRecovery, import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
resetStuckColorisPicker,
} from "@/seqta/utils/timetableColoris";
import { waitForElm } from "@/seqta/utils/waitForElm"; import { waitForElm } from "@/seqta/utils/waitForElm";
const timetablePlugin: Plugin<{}, {}> = { const timetablePlugin: Plugin<{}, {}> = {
@@ -67,7 +65,6 @@ function resetTimetableStyles(): void {
async function handleTimetable(): Promise<void> { async function handleTimetable(): Promise<void> {
attachTimetableColorisRecovery(); attachTimetableColorisRecovery();
resetStuckColorisPicker();
// SEQTA uses `.times` blocks on entries, not necessarily `.time`; avoid infinite polling on a missing selector. // SEQTA uses `.times` blocks on entries, not necessarily `.time`; avoid infinite polling on a missing selector.
try { try {
@@ -97,11 +94,13 @@ function handleTimetableZoom(): void {
zoomControls.className = "timetable-zoom-controls"; zoomControls.className = "timetable-zoom-controls";
const zoomIn = document.createElement("button"); const zoomIn = document.createElement("button");
zoomIn.className = "uiButton timetable-zoom iconFamily"; zoomIn.type = "button";
zoomIn.className = "timetable-zoom iconFamily bsplus-timetable-control";
zoomIn.innerHTML = "&#xed93;"; // Unicode for zoom in icon (custom iconfamily) zoomIn.innerHTML = "&#xed93;"; // Unicode for zoom in icon (custom iconfamily)
const zoomOut = document.createElement("button"); const zoomOut = document.createElement("button");
zoomOut.className = "uiButton timetable-zoom iconFamily"; zoomOut.type = "button";
zoomOut.className = "timetable-zoom iconFamily bsplus-timetable-control";
zoomOut.innerHTML = "&#xed94;"; // Unicode for zoom out icon (custom iconfamily) zoomOut.innerHTML = "&#xed94;"; // Unicode for zoom out icon (custom iconfamily)
zoomControls.appendChild(zoomOut); zoomControls.appendChild(zoomOut);
@@ -140,7 +139,8 @@ function handleTimetableAssessmentHide(): void {
hideControls.className = "timetable-hide-controls"; hideControls.className = "timetable-hide-controls";
const hideOn = document.createElement("button"); const hideOn = document.createElement("button");
hideOn.className = "uiButton timetable-hide iconFamily"; hideOn.type = "button";
hideOn.className = "timetable-hide iconFamily bsplus-timetable-control";
hideOn.innerHTML = "&#xeab3;"; hideOn.innerHTML = "&#xeab3;";
hideControls.appendChild(hideOn); hideControls.appendChild(hideOn);
+135 -31
View File
@@ -33,7 +33,7 @@ function getRoomAndTeacherElements(entry: HTMLElement): {
} }
const EDIT_ICON_SVG = const EDIT_ICON_SVG =
'<svg width="24" height="24" viewBox="0 0 24 24"><g style="fill: currentcolor;"><path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z"/></g></svg>'; '<svg width="20" height="20" viewBox="0 0 24 24"><g style="fill: currentcolor;"><path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z"/></g></svg>';
function showEditModal( function showEditModal(
item: TimetableEntryData, item: TimetableEntryData,
@@ -147,7 +147,11 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
let quickbarObserver: MutationObserver | null = null; let quickbarObserver: MutationObserver | null = null;
let quickbarSyncTimer: ReturnType<typeof setTimeout> | null = null; let quickbarSyncTimer: ReturnType<typeof setTimeout> | null = null;
let lastClickedCi: number | null = null; let lastClickedCi: number | null = null;
let lastClickedEntry: { roomEl: HTMLElement; teacherEl: HTMLElement; item: TimetableEntryData } | null = null; let lastClickedEntry: {
roomEl: HTMLElement | null;
teacherEl: HTMLElement | null;
item: TimetableEntryData;
} | null = null;
let lastSyncedQuickbarCi: number | null = null; let lastSyncedQuickbarCi: number | null = null;
const getOverrides = (): TimetableOverrides => const getOverrides = (): TimetableOverrides =>
@@ -161,6 +165,64 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
): { room?: string; staff?: string } | undefined => ): { room?: string; staff?: string } | undefined =>
getOverrides()[String(ci)] ?? getOverridesBySubject()[description]; getOverrides()[String(ci)] ?? getOverridesBySubject()[description];
const findClassEntry = (
title: string,
calendarId?: string | null,
): HTMLElement | null => {
if (calendarId) {
const byCalendar = document.querySelector(
`.timetablepage .entry.class[data-calendarid="${calendarId}"]`,
);
if (byCalendar) return byCalendar as HTMLElement;
}
for (const entry of document.querySelectorAll(".timetablepage .entry.class")) {
const entryTitle = entry.querySelector(".title")?.textContent?.trim();
if (entryTitle === title) return entry as HTMLElement;
}
return null;
};
const resolveContextFromQuickbar = (quickbar: HTMLElement): void => {
const title = quickbar.querySelector(".title")?.textContent?.trim() ?? "";
if (!title) return;
const quickbarRoom = quickbar.querySelector(".meta .room")?.textContent?.trim() ?? "";
const quickbarStaff =
quickbar.querySelector(".meta .teacher")?.textContent?.trim() ?? "";
const entry = findClassEntry(title);
if (entry) {
const ciStr = entry.getAttribute("data-instance");
const ci = ciStr ? parseInt(ciStr, 10) : NaN;
const { roomEl, teacherEl } = getRoomAndTeacherElements(entry);
const description = title;
const room = roomEl?.textContent?.trim() ?? quickbarRoom;
const staff = teacherEl?.textContent?.trim() ?? quickbarStaff;
lastClickedCi = isNaN(ci) ? null : ci;
lastClickedEntry = {
roomEl,
teacherEl,
item: { ci: isNaN(ci) ? 0 : ci, description, room, staff },
};
lastSyncedQuickbarCi = null;
return;
}
lastClickedCi = null;
lastClickedEntry = {
roomEl: null,
teacherEl: null,
item: {
ci: 0,
description: title,
room: quickbarRoom,
staff: quickbarStaff,
},
};
lastSyncedQuickbarCi = null;
};
const processEntry = (entry: HTMLElement): void => { const processEntry = (entry: HTMLElement): void => {
if (entry.classList.contains("assessment") || entry.hasAttribute("data-timetable-edit-processed")) return; if (entry.classList.contains("assessment") || entry.hasAttribute("data-timetable-edit-processed")) return;
@@ -212,16 +274,20 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
}; };
const applyOverridesToQuickbar = (quickbar: HTMLElement): void => { const applyOverridesToQuickbar = (quickbar: HTMLElement): void => {
if (lastClickedCi === null) return; resolveContextFromQuickbar(quickbar);
if (lastSyncedQuickbarCi === lastClickedCi) return;
const description = const description =
quickbar.querySelector(".title")?.textContent?.trim() ?? quickbar.querySelector(".title")?.textContent?.trim() ??
lastClickedEntry?.item.description ?? lastClickedEntry?.item.description ??
""; "";
const override = getEffectiveOverride(lastClickedCi, description); if (!description) return;
const ci = lastClickedCi ?? lastClickedEntry?.item.ci ?? 0;
if (lastSyncedQuickbarCi === ci && lastClickedCi !== null) return;
const override = getEffectiveOverride(ci, description);
if (!override) { if (!override) {
lastSyncedQuickbarCi = lastClickedCi; lastSyncedQuickbarCi = ci;
return; return;
} }
@@ -237,7 +303,7 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
teacherEl.textContent = override.staff; teacherEl.textContent = override.staff;
} }
lastSyncedQuickbarCi = lastClickedCi; lastSyncedQuickbarCi = ci;
}; };
const updateVisibleQuickbar = (room: string, staff: string): void => { const updateVisibleQuickbar = (room: string, staff: string): void => {
@@ -259,13 +325,21 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer); if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer);
let attempts = 0; let attempts = 0;
const maxAttempts = 15;
const trySync = (): void => { const trySync = (): void => {
const quickbar = getVisibleClassQuickbar(); const quickbar = getVisibleClassQuickbar();
if (quickbar && lastClickedCi !== null) { if (!quickbar) {
syncClassQuickbar(quickbar); if (++attempts < maxAttempts) {
quickbarSyncTimer = setTimeout(trySync, 50);
}
return; return;
} }
if (++attempts < 6) {
syncClassQuickbar(quickbar);
const hasButton = quickbar.querySelector(".timetable-edit-quickbar-btn");
const hasActions = quickbar.querySelector(".actions");
if ((!hasButton || !hasActions) && ++attempts < maxAttempts) {
quickbarSyncTimer = setTimeout(trySync, 50); quickbarSyncTimer = setTimeout(trySync, 50);
} }
}; };
@@ -279,24 +353,33 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
const actions = quickbar.querySelector(".actions"); const actions = quickbar.querySelector(".actions");
if (!actions) return; if (!actions) return;
const colourBtn = actions.querySelector(
"[title='Choose a colour'], button.uiButton",
);
const btn = document.createElement("button"); const btn = document.createElement("button");
btn.type = "button"; btn.type = "button";
btn.className = "uiButton timetable-edit-quickbar-btn"; btn.className =
colourBtn instanceof HTMLElement
? `${colourBtn.className} timetable-edit-quickbar-btn`
: "timetable-edit-quickbar-btn";
btn.title = "Edit room and teacher"; btn.title = "Edit room and teacher";
btn.innerHTML = EDIT_ICON_SVG; btn.innerHTML = EDIT_ICON_SVG;
btn.addEventListener("click", (e) => { btn.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const ci = lastClickedCi;
const entryData = lastClickedEntry;
if (!ci || !entryData) return;
const qb = (e.currentTarget as HTMLElement).closest(".quickbar"); const qb = (e.currentTarget as HTMLElement).closest(".quickbar");
if (!qb) return; if (!qb) return;
resolveContextFromQuickbar(qb as HTMLElement);
const entryData = lastClickedEntry;
if (!entryData) return;
const quickbarRoom = qb.querySelector(".meta .room")?.textContent?.trim() ?? ""; const quickbarRoom = qb.querySelector(".meta .room")?.textContent?.trim() ?? "";
const quickbarTeacher = qb.querySelector(".meta .teacher")?.textContent?.trim() ?? ""; const quickbarTeacher = qb.querySelector(".meta .teacher")?.textContent?.trim() ?? "";
const quickbarTitle = qb.querySelector(".title")?.textContent?.trim() ?? ""; const quickbarTitle = qb.querySelector(".title")?.textContent?.trim() ?? "";
const ci = lastClickedCi ?? entryData.item.ci;
const item: TimetableEntryData = { const item: TimetableEntryData = {
ci, ci,
description: quickbarTitle || entryData.item.description, description: quickbarTitle || entryData.item.description,
@@ -308,8 +391,8 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
item, item,
getOverrides(), getOverrides(),
getOverridesBySubject(), getOverridesBySubject(),
(ci, room, staff, applyToFuture) => { (saveCi, room, staff, applyToFuture) => {
if (applyToFuture) { if (applyToFuture || !saveCi) {
const bySubject = { ...getOverridesBySubject() }; const bySubject = { ...getOverridesBySubject() };
bySubject[item.description] = { bySubject[item.description] = {
room: room || undefined, room: room || undefined,
@@ -320,7 +403,7 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
const current = getOverrides(); const current = getOverrides();
api.storage.timetableOverrides = { api.storage.timetableOverrides = {
...current, ...current,
[String(ci)]: { room: room || undefined, staff: staff || undefined }, [String(saveCi)]: { room: room || undefined, staff: staff || undefined },
}; };
} }
if (entryData.roomEl) entryData.roomEl.textContent = room; if (entryData.roomEl) entryData.roomEl.textContent = room;
@@ -328,10 +411,12 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
updateVisibleQuickbar(room, staff); updateVisibleQuickbar(room, staff);
processAllEntries(); processAllEntries();
}, },
(ci) => { (clearCi) => {
if (clearCi) {
const current = getOverrides(); const current = getOverrides();
delete current[String(ci)]; delete current[String(clearCi)];
api.storage.timetableOverrides = current; api.storage.timetableOverrides = current;
}
const bySubject = getOverridesBySubject(); const bySubject = getOverridesBySubject();
delete bySubject[item.description]; delete bySubject[item.description];
api.storage.timetableOverridesBySubject = bySubject; api.storage.timetableOverridesBySubject = bySubject;
@@ -343,12 +428,15 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
); );
}); });
actions.insertBefore(btn, actions.firstChild); actions.insertBefore(
btn,
colourBtn ?? actions.firstChild,
);
}; };
const syncQuickbarFromDOM = () => { const syncQuickbarFromDOM = () => {
const quickbar = getVisibleClassQuickbar(); const quickbar = getVisibleClassQuickbar();
if (!quickbar || lastClickedCi === null || !lastClickedEntry) return; if (!quickbar) return;
syncClassQuickbar(quickbar); syncClassQuickbar(quickbar);
}; };
@@ -357,26 +445,41 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
if (!timetablePage || quickbarObserver) return; if (!timetablePage || quickbarObserver) return;
quickbarObserver = new MutationObserver((mutations) => { quickbarObserver = new MutationObserver((mutations) => {
const quickbarBecameVisible = mutations.some(
(mutation) =>
mutation.type === "attributes" &&
mutation.attributeName === "class" &&
(mutation.target as HTMLElement).classList.contains("quickbar") &&
(mutation.target as HTMLElement).classList.contains("visible"),
);
if (!quickbarBecameVisible || lastClickedCi === null) return;
const quickbar = getVisibleClassQuickbar(); const quickbar = getVisibleClassQuickbar();
if (quickbar) syncClassQuickbar(quickbar); if (!quickbar) return;
const shouldSync = mutations.some((mutation) => {
if (mutation.type === "attributes" && mutation.attributeName === "class") {
const target = mutation.target as HTMLElement;
return target.classList.contains("quickbar");
}
if (mutation.type === "childList") {
const target = mutation.target as HTMLElement;
return target.classList?.contains("quickbar") || target.closest?.(".quickbar");
}
return false;
});
if (shouldSync) scheduleQuickbarSync();
}); });
quickbarObserver.observe(timetablePage, { quickbarObserver.observe(timetablePage, {
subtree: true, subtree: true,
childList: true,
attributes: true, attributes: true,
attributeFilter: ["class"], attributeFilter: ["class"],
}); });
}; };
const onTimetableEntryClick = (event: Event): void => {
const target = event.target as HTMLElement;
if (!target.closest?.(".timetablepage .entry.class")) return;
lastSyncedQuickbarCi = null;
scheduleQuickbarSync();
};
document.addEventListener("click", onTimetableEntryClick, true);
const handleTimetable = async () => { const handleTimetable = async () => {
// Class entries (`div.entry.class`) load after the page shell; don't fail the whole // Class entries (`div.entry.class`) load after the page shell; don't fail the whole
// setup if they are slow or briefly absent (e.g. navigation). Observers still catch them. // setup if they are slow or briefly absent (e.g. navigation). Observers still catch them.
@@ -406,6 +509,7 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
return () => { return () => {
unregister(); unregister();
document.removeEventListener("click", onTimetableEntryClick, true);
observer?.disconnect(); observer?.disconnect();
quickbarObserver?.disconnect(); quickbarObserver?.disconnect();
if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer); if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer);
+2 -24
View File
@@ -1,30 +1,8 @@
/* Timetable Edit Plugin - BetterSEQTA Plus style */ /* Timetable Edit Plugin - BetterSEQTA Plus style */
/* Edit button in quickbar */ /* SEQTA sizes quickbar actions at 32×32 — layout comes from SEQTA + injected.scss */
.timetable-edit-quickbar-btn { .timetablepage .quickbar .actions .timetable-edit-quickbar-btn {
padding: 0;
margin: 0;
background: transparent !important;
border: none !important;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
}
.timetable-edit-quickbar-btn:hover {
transform: scale(1.05);
}
.timetable-edit-quickbar-btn:active {
transform: scale(0.95);
}
.timetable-edit-quickbar-btn svg {
fill: currentColor;
width: 24px;
height: 24px;
} }
/* Edit modal animations */ /* Edit modal animations */
+4 -1
View File
@@ -6,6 +6,8 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
// UI and theme management // UI and theme management
import pageState from "@/pageState.js?url"; import pageState from "@/pageState.js?url";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
import { installSeqtaMenuColourPatch } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
// Stylesheets // Stylesheets
import injectedCSS from "@/css/injected.scss?inline"; import injectedCSS from "@/css/injected.scss?inline";
@@ -15,6 +17,7 @@ export async function main() {
try { try {
if (settingsState.onoff) { if (settingsState.onoff) {
injectPageState(); injectPageState();
installSeqtaMenuColourPatch();
// Rather permanent FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs // Rather permanent FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
if (import.meta.env.MODE === "development") { if (import.meta.env.MODE === "development") {
@@ -35,6 +38,6 @@ export async function main() {
function injectPageState() { function injectPageState() {
const mainScript = document.createElement("script"); const mainScript = document.createElement("script");
mainScript.src = browser.runtime.getURL(pageState); mainScript.src = resolveExtensionAssetUrl(pageState);
document.head.appendChild(mainScript); document.head.appendChild(mainScript);
} }
+23 -2
View File
@@ -7,10 +7,12 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour"; import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour";
import { getCustomThemeAdaptiveCssVariableBindings } from "@/seqta/ui/colors/customThemeAdaptiveBindings"; import { getCustomThemeAdaptiveCssVariableBindings } from "@/seqta/ui/colors/customThemeAdaptiveBindings";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
import darkLogo from "@/resources/icons/betterseqta-light-full.png"; import darkLogo from "@/resources/icons/betterseqta-light-full.png";
import lightLogo from "@/resources/icons/betterseqta-dark-full.png"; import lightLogo from "@/resources/icons/betterseqta-dark-full.png";
const ADAPTIVE_THEME_TRANSITION_MS = 400; const ADAPTIVE_THEME_TRANSITION_MS = 400;
const LOGO_STYLE_ID = "bsplus-logo-style";
let colorTransitionRafId: number | null = null; let colorTransitionRafId: number | null = null;
let lastInterpolatedHex: string | null = null; let lastInterpolatedHex: string | null = null;
@@ -84,6 +86,23 @@ function cancelColorTransition() {
} }
} }
/** Chromium does not always resolve extension URLs inside CSS variables on ::before. */
function applyBetterseqtaLogoBackground(isDark: boolean) {
const url = resolveExtensionAssetUrl(isDark ? darkLogo : lightLogo);
let styleEl = document.getElementById(LOGO_STYLE_ID);
if (!styleEl) {
styleEl = document.createElement("style");
styleEl.id = LOGO_STYLE_ID;
document.head.appendChild(styleEl);
}
styleEl.textContent = `
body.student #menu > ul::before,
#title::before {
background-image: url("${url}") !important;
}
`;
}
function getRepresentativeRgbChannels(s: string): { r: number; g: number; b: number } | null { function getRepresentativeRgbChannels(s: string): { r: number; g: number; b: number } | null {
const parsedHex = parseRepresentativeHex(s); const parsedHex = parseRepresentativeHex(s);
if (!parsedHex) return null; if (!parsedHex) return null;
@@ -115,11 +134,11 @@ function applyColorsWith(selectedColor: string) {
let modeProps = {}; let modeProps = {};
modeProps = settingsState.DarkMode modeProps = settingsState.DarkMode
? { ? {
"--betterseqta-logo": `url(${browser.runtime.getURL(darkLogo)})`, "--betterseqta-logo": `url(${resolveExtensionAssetUrl(darkLogo)})`,
} }
: { : {
"--better-pale": lightenAndPaleColor(selectedColor), "--better-pale": lightenAndPaleColor(selectedColor),
"--betterseqta-logo": `url(${browser.runtime.getURL(lightLogo)})`, "--betterseqta-logo": `url(${resolveExtensionAssetUrl(lightLogo)})`,
}; };
if (settingsState.DarkMode) { if (settingsState.DarkMode) {
@@ -129,6 +148,8 @@ function applyColorsWith(selectedColor: string) {
document.documentElement.classList.remove("dark"); document.documentElement.classList.remove("dark");
} }
applyBetterseqtaLogoBackground(settingsState.DarkMode);
// Dynamic properties, always applied // Dynamic properties, always applied
const rgbThreshold = GetThresholdOfColor(selectedColor); const rgbThreshold = GetThresholdOfColor(selectedColor);
const isBright = rgbThreshold > 210; const isBright = rgbThreshold > 210;
+2 -1
View File
@@ -106,7 +106,8 @@ function buildFontOverrideCss(family: string): string {
.iconFamily, .iconFamily,
.iconFamily *, .iconFamily *,
button.uiButton.timetable-zoom.iconFamily, button.timetable-zoom.iconFamily,
button.bsplus-timetable-control.iconFamily,
[class~="iconFamily"], [class~="iconFamily"],
[class~="iconFamily"] * { [class~="iconFamily"] * {
font-family: "IconFamily" !important; font-family: "IconFamily" !important;
@@ -0,0 +1,74 @@
/**
* SEQTA Learn bug (vanilla too): MainMenu.updateColours uses
* `.each(function (item) { this.options... }).bind(this)` the bind is on
* `.each()`'s return value, not the callback. Saving a timetable subject colour
* sends `menu.update.colours` and throws.
*
* Also: ColourChooser (SlidePane + Modaliser) can leave a full-screen
* uiSlidePane / empty modaliser-container that blocks timetable clicks.
*
* Must run in the PAGE JavaScript context inject via web_accessible script URL.
*/
import browser from "webextension-polyfill";
import patchScript from "@/seqta/utils/seqtaMenuColourPatch.js?url";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
const PAGE_PATCH_LOADER_ID = "bsplus-seqta-menu-colour-patch-loader";
/** Remove empty or hidden modaliser shells left after colour dialog teardown. */
export function dismissStaleModaliserContainers(): number {
let removed = 0;
for (const container of document.querySelectorAll(".modaliser-container")) {
const modal = container.querySelector(".modaliser");
const empty = !modal || modal.childElementCount === 0;
const hidden = !container.classList.contains("visible");
if (empty || hidden) {
container.remove();
removed++;
}
}
return removed;
}
/** Remove stuck SEQTA colour chooser slide panes that intercept timetable clicks. */
export function dismissStaleColourSlidePanes(
forceColourChooser = false,
): number {
let removed = 0;
for (const pane of document.querySelectorAll(".uiSlidePane")) {
const isColourChooser = pane.querySelector(".pane.colourChooser");
if (isColourChooser) {
pane.remove();
removed++;
continue;
}
if (!forceColourChooser && pane.classList.contains("shown")) continue;
if (!pane.classList.contains("shown")) {
pane.remove();
removed++;
}
}
return removed;
}
export function dismissStaleColourDialogs(forceColourChooser = false): {
slideRemoved: number;
modalRemoved: number;
} {
const slideRemoved = dismissStaleColourSlidePanes(forceColourChooser);
const modalRemoved = dismissStaleModaliserContainers();
document.body.classList.remove("clr-open");
document.documentElement.classList.remove("clr-open");
return { slideRemoved, modalRemoved };
}
export function installSeqtaMenuColourPatch(): void {
if (document.getElementById(PAGE_PATCH_LOADER_ID)) return;
const script = document.createElement("script");
script.id = PAGE_PATCH_LOADER_ID;
script.src = resolveExtensionAssetUrl(patchScript);
script.addEventListener("load", () => script.remove());
(document.documentElement || document.head).appendChild(script);
}
+397
View File
@@ -0,0 +1,397 @@
/**
* PAGE context only patches SEQTA menu.update.colours and removes stuck colour-dialog
* layers (uiSlidePane + modaliser) that block timetable entry clicks after a colour save.
*/
(function () {
if (window.__bsplusMenuColoursPatched) return;
var LOG = "[BetterSEQTA+] timetable colour:";
var MENU_UPDATE_COLOURS = "menu.update.colours";
var SUBJECT_COLOUR_PREF_PREFIX = "timetable.subject.colour.";
var TUTOR_COLOUR_PREF_PREFIX = "timetable.tutor.";
function log(event, detail) {
if (detail !== undefined) {
console.info(LOG, event, detail);
} else {
console.info(LOG, event);
}
}
function isTesStylingEnabled() {
var logoStyle = document.getElementById("logo-style");
return logoStyle && logoStyle.textContent.indexOf("tesSeqta") !== -1;
}
function countOverlayState() {
return {
slidePanes: document.querySelectorAll(".uiSlidePane").length,
slidePanesShown: document.querySelectorAll(".uiSlidePane.shown").length,
colourChoosers: document.querySelectorAll(
".uiSlidePane .pane.colourChooser",
).length,
modalisers: document.querySelectorAll(".modaliser-container").length,
modalisersVisible: document.querySelectorAll(
".modaliser-container.visible",
).length,
quickbarsVisible: document.querySelectorAll(
".timetablepage .quickbar.visible",
).length,
};
}
function applyMenuSubjectColours() {
if (!window.user) return;
var defaultColour = isTesStylingEnabled() ? "#2b3547" : "#dddddd";
var items = document.querySelectorAll("#menu li[data-colour]");
for (var i = 0; i < items.length; i++) {
var item = items[i];
var prefName = item.getAttribute("data-colour");
if (!prefName) continue;
var pref = window.user.getPreference(prefName);
var colour = (pref && pref.value) || defaultColour;
item.style.setProperty("--item-colour", colour);
}
}
function dismissStaleModaliserContainers() {
var removed = 0;
var containers = document.querySelectorAll(".modaliser-container");
for (var i = 0; i < containers.length; i++) {
var container = containers[i];
var modal = container.querySelector(".modaliser");
var empty = !modal || modal.childElementCount === 0;
var hidden = !container.classList.contains("visible");
if (empty || hidden) {
container.remove();
removed++;
}
}
return removed;
}
function dismissStaleColourSlidePanes(forceColourChooser) {
var removed = 0;
var panes = document.querySelectorAll(".uiSlidePane");
for (var i = 0; i < panes.length; i++) {
var pane = panes[i];
var isColourChooser = pane.querySelector(".pane.colourChooser");
if (isColourChooser) {
pane.remove();
removed++;
continue;
}
if (!forceColourChooser && pane.classList.contains("shown")) continue;
if (!pane.classList.contains("shown")) {
pane.remove();
removed++;
}
}
return removed;
}
function dismissStaleColourDialogs(forceColourChooser) {
var slideRemoved = dismissStaleColourSlidePanes(forceColourChooser);
var modalRemoved = dismissStaleModaliserContainers();
document.body.classList.remove("clr-open");
document.documentElement.classList.remove("clr-open");
return {
slideRemoved: slideRemoved,
modalRemoved: modalRemoved,
overlays: countOverlayState(),
};
}
function reconcileStuckQuickbars(reason) {
var fixed = 0;
var quickbars = document.querySelectorAll(".timetablepage .quickbar.visible");
for (var i = 0; i < quickbars.length; i++) {
var qb = quickbars[i];
var wrapper = qb.querySelector(".wrapper");
if (!wrapper || !wrapper.childElementCount) {
qb.classList.remove("visible");
fixed++;
}
}
if (fixed > 0) {
log("cleared stuck quickbar shell (" + reason + ")", { fixed: fixed });
try {
window.msg.send("calendar.quickbar.hide");
} catch (err) {
/* ignore */
}
}
return fixed;
}
function findEntryElement(calendarId, code) {
if (calendarId) {
var byCalendar = document.querySelector(
".timetablepage .entry[data-calendarid=\"" + calendarId + "\"]",
);
if (byCalendar) return byCalendar;
}
if (code) {
var entries = document.querySelectorAll(".timetablepage .entry.class");
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var titleEl = entry.querySelector(".title");
var title =
titleEl && titleEl.textContent ? titleEl.textContent.trim() : "";
if (title && title.indexOf(code) !== -1) return entry;
}
}
return null;
}
function normalizeQuickbarOpenContext(contents) {
if (!contents) return contents;
reconcileStuckQuickbars("before-open");
var element = contents.element;
var calendarId =
(element && element.getAttribute && element.getAttribute("data-calendarid")) ||
(contents.data && contents.data.calendarid);
var code = contents.data && contents.data.code;
var connected =
element &&
element.isConnected &&
typeof document.contains === "function" &&
document.contains(element);
if (!connected) {
var replacement = findEntryElement(calendarId, code);
if (replacement) {
contents.element = replacement;
log("replaced detached quickbar entry element", {
calendarId: calendarId,
code: code,
});
} else {
log("quickbar entry element detached, no replacement found", {
calendarId: calendarId,
code: code,
});
}
}
return contents;
}
function logQuickbarOpenResult(phase) {
var qb = document.querySelector(".timetablepage .quickbar.visible");
var wrapper = qb && qb.querySelector(".wrapper");
log("quickbar open result (" + phase + ")", {
visible: !!qb,
hasWrapper: !!wrapper,
wrapperChildren: wrapper ? wrapper.childElementCount : 0,
overlays: countOverlayState(),
});
}
function scheduleColourDialogCleanup(reason) {
var delays = [0, 100, 300, 600];
for (var i = 0; i < delays.length; i++) {
(function (delay) {
setTimeout(function () {
var result = dismissStaleColourDialogs(true);
if (
result.slideRemoved > 0 ||
result.modalRemoved > 0 ||
delay === 0
) {
log("cleanup (" + reason + ", +" + delay + "ms)", result);
}
}, delay);
})(delays[i]);
}
}
function isSubjectOrTutorColourPref(handle) {
return (
typeof handle === "string" &&
(handle.indexOf(SUBJECT_COLOUR_PREF_PREFIX) === 0 ||
handle.indexOf(TUTOR_COLOUR_PREF_PREFIX) === 0)
);
}
function runMenuColourUpdate() {
log("menu.update.colours intercepted", countOverlayState());
try {
applyMenuSubjectColours();
} catch (err) {
console.error("[BetterSEQTA+] menu.update.colours failed:", err);
}
scheduleColourDialogCleanup("menu.update.colours");
reconcileStuckQuickbars("after-colour-save");
}
function fixedMenuColourHandler() {
runMenuColourUpdate();
}
function neutralizeBrokenMenuColourListeners(msg) {
var listeners = msg.listeners && msg.listeners[MENU_UPDATE_COLOURS];
if (!listeners) return;
for (var i = 0; i < listeners.length; i++) {
if (listeners[i]) {
listeners[i].fn = fixedMenuColourHandler;
}
}
}
function patchMsg(msg) {
if (!msg || msg.__bsplusPatched) return;
var originalSend = msg.send.bind(msg);
msg.send = function (handle, contents, suppressLogs, noRecord) {
if (handle === MENU_UPDATE_COLOURS) {
runMenuColourUpdate();
return;
}
if (isSubjectOrTutorColourPref(handle)) {
log("colour pref save detected", {
pref: handle,
colour: contents,
overlays: countOverlayState(),
});
var prefResult = originalSend(
handle,
contents,
suppressLogs,
noRecord,
);
scheduleColourDialogCleanup("pref:" + handle);
reconcileStuckQuickbars("after-colour-save");
return prefResult;
}
if (handle === "calendar.quickbar.class") {
var openContext = normalizeQuickbarOpenContext(contents);
var label =
openContext &&
openContext.data &&
(openContext.data.description || openContext.data.code);
log("quickbar open msg.send", {
subject: label,
elementConnected:
openContext &&
openContext.element &&
openContext.element.isConnected,
overlays: countOverlayState(),
});
var openResult;
try {
openResult = originalSend(
handle,
openContext,
suppressLogs,
noRecord,
);
} catch (err) {
console.error("[BetterSEQTA+] quickbar open failed:", err);
throw err;
}
setTimeout(function () {
logQuickbarOpenResult("+50ms");
}, 50);
setTimeout(function () {
logQuickbarOpenResult("+200ms");
}, 200);
return openResult;
}
if (handle === "calendar.quickbar.hide") {
log("quickbar hide msg.send", countOverlayState());
return originalSend(handle, contents, suppressLogs, noRecord);
}
return originalSend(handle, contents, suppressLogs, noRecord);
};
var originalRegister = msg.register.bind(msg);
msg.register = function (handle, callback, clear, ignoreHistory) {
if (handle === MENU_UPDATE_COLOURS) {
return originalRegister(
handle,
fixedMenuColourHandler,
clear,
ignoreHistory,
);
}
if (handle === "calendar.quickbar.class") {
return originalRegister(
handle,
function (context) {
log("quickbar class handler invoked", {
elementConnected:
context &&
context.element &&
context.element.isConnected,
subject:
context &&
context.data &&
(context.data.description || context.data.code),
});
return callback(context);
},
clear,
ignoreHistory,
);
}
return originalRegister(handle, callback, clear, ignoreHistory);
};
neutralizeBrokenMenuColourListeners(msg);
msg.__bsplusPatched = true;
}
function tryPatch() {
if (window.__bsplusMenuColoursPatched) return true;
if (!window.msg || !window.msg.send) return false;
patchMsg(window.msg);
window.__bsplusMenuColoursPatched = true;
log("patch active");
return true;
}
document.addEventListener(
"click",
function (event) {
var target = event.target;
if (!target || !target.closest) return;
var entry = target.closest(".timetablepage .entry");
if (!entry) return;
var before = countOverlayState();
var cleanup = dismissStaleColourDialogs(false);
var calendarId = entry.getAttribute("data-calendarid");
var instance = entry.getAttribute("data-instance");
var titleEl = entry.querySelector(".title");
var title = titleEl && titleEl.textContent ? titleEl.textContent.trim() : "";
log("entry click (capture)", {
calendarId: calendarId,
instance: instance,
title: title,
before: before,
cleanup: cleanup,
});
},
true,
);
if (!tryPatch()) {
var interval = setInterval(function () {
if (tryPatch()) {
clearInterval(interval);
} else if (window.msg) {
neutralizeBrokenMenuColourListeners(window.msg);
}
}, 25);
setTimeout(function () {
clearInterval(interval);
}, 120000);
}
})();
+65 -17
View File
@@ -1,12 +1,45 @@
/** /**
* SEQTA timetable uses Coloris for subject colours. Extension CSS previously * SEQTA timetable colour picker (Coloris) recovery and click-blocker cleanup.
* unset Coloris ::after animations, which left the picker unable to reopen. * Subject colour saves trigger SEQTA's broken menu.update.colours handler see
* This module clears stuck `clr-open` / hidden picker state after each use. * patchSeqtaMenuUpdateColours.ts.
*/ */
let attached = false; import { dismissStaleColourDialogs } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
export function resetStuckColorisPicker(): void { let attached = false;
let dismissTimer: ReturnType<typeof setTimeout> | null = null;
const DISMISS_DELAY_MS = 100;
function scheduleDismiss(): void {
if (dismissTimer !== null) clearTimeout(dismissTimer);
dismissTimer = setTimeout(() => {
dismissTimer = null;
dismissTimetableUiBlockers();
}, DISMISS_DELAY_MS);
}
/** Hide colour-picker / modal layers that intercept clicks after a colour save. */
export function dismissTimetableUiBlockers(): {
slideRemoved: number;
modalRemoved: number;
} {
document.body.style.removeProperty("overflow");
for (const picker of document.querySelectorAll(".clr-picker")) {
picker.classList.remove("clr-open");
if (picker instanceof HTMLElement) {
picker.style.display = "none";
picker.style.pointerEvents = "none";
picker.style.visibility = "hidden";
}
}
return dismissStaleColourDialogs();
}
/** Clear inline styles that can prevent Coloris from reopening. */
export function prepareColorisPickerOpen(): void {
document.body.classList.remove("clr-open"); document.body.classList.remove("clr-open");
document.documentElement.classList.remove("clr-open"); document.documentElement.classList.remove("clr-open");
@@ -18,29 +51,44 @@ export function resetStuckColorisPicker(): void {
picker.style.removeProperty("visibility"); picker.style.removeProperty("visibility");
} }
} }
for (const field of document.querySelectorAll(".clr-field")) {
field.classList.remove("clr-open");
}
} }
export function attachTimetableColorisRecovery(): void { export function attachTimetableColorisRecovery(): void {
if (attached) return; if (attached) return;
attached = true; attached = true;
const afterColorisEvent = () => { document.addEventListener("coloris:close", scheduleDismiss);
requestAnimationFrame(() => resetStuckColorisPicker()); document.addEventListener("coloris:pick", scheduleDismiss);
};
document.addEventListener("coloris:pick", afterColorisEvent);
document.addEventListener("coloris:close", afterColorisEvent);
document.addEventListener( document.addEventListener(
"click", "click",
(event) => { (event) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (!target.closest(".timetablepage [title='Choose a colour']")) return; if (!target.closest(".timetablepage")) return;
resetStuckColorisPicker();
if (target.closest("[title='Choose a colour']")) {
if (dismissTimer !== null) {
clearTimeout(dismissTimer);
dismissTimer = null;
}
prepareColorisPickerOpen();
return;
}
if (!target.closest(".entry")) return;
const pickerOpen =
document.body.classList.contains("clr-open") &&
document.querySelector(".clr-picker.clr-open");
if (!pickerOpen) {
const result = dismissTimetableUiBlockers();
if (result.slideRemoved > 0 || result.modalRemoved > 0) {
console.info(
"[BetterSEQTA+] timetable colour: content-script cleanup",
result,
);
}
}
}, },
true, true,
); );
+7
View File
@@ -133,6 +133,13 @@ export default defineConfig(({ command }) => ({
input: { input: {
settings: join(__dirname, "src", "interface", "index.html"), settings: join(__dirname, "src", "interface", "index.html"),
pageState: join(__dirname, "src", "pageState.js"), pageState: join(__dirname, "src", "pageState.js"),
seqtaMenuColourPatch: join(
__dirname,
"src",
"seqta",
"utils",
"seqtaMenuColourPatch.js",
),
}, },
output: { output: {
assetFileNames: "assets/[name]-[hash][extname]", assetFileNames: "assets/[name]-[hash][extname]",