mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
fix(timetable): reopen colour picker
+ some quick fixes to dropdowns again
This commit is contained in:
@@ -3,12 +3,14 @@ import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||
import {
|
||||
ChangeMenuItemPositions,
|
||||
ensureAnalyticsMenuOrder,
|
||||
insertMenuItemAfterKey,
|
||||
processMenuItemNode,
|
||||
} 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 {
|
||||
applyMenuItemVisibility,
|
||||
|
||||
@@ -885,7 +885,7 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bsplus-analytics-chart-cell > :global(.bsplus-analytics-card) {
|
||||
.bsplus-analytics-chart-cell > .bsplus-analytics-card {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import type { Plugin } from "../../core/types";
|
||||
import {
|
||||
attachTimetableColorisRecovery,
|
||||
resetStuckColorisPicker,
|
||||
} from "@/seqta/utils/timetableColoris";
|
||||
import { attachTimetableColorisRecovery } from "@/seqta/utils/timetableColoris";
|
||||
import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
|
||||
const timetablePlugin: Plugin<{}, {}> = {
|
||||
@@ -67,7 +65,6 @@ function resetTimetableStyles(): void {
|
||||
|
||||
async function handleTimetable(): Promise<void> {
|
||||
attachTimetableColorisRecovery();
|
||||
resetStuckColorisPicker();
|
||||
|
||||
// SEQTA uses `.times` blocks on entries, not necessarily `.time`; avoid infinite polling on a missing selector.
|
||||
try {
|
||||
@@ -97,11 +94,13 @@ function handleTimetableZoom(): void {
|
||||
zoomControls.className = "timetable-zoom-controls";
|
||||
|
||||
const zoomIn = document.createElement("button");
|
||||
zoomIn.className = "uiButton timetable-zoom iconFamily";
|
||||
zoomIn.type = "button";
|
||||
zoomIn.className = "timetable-zoom iconFamily bsplus-timetable-control";
|
||||
zoomIn.innerHTML = ""; // Unicode for zoom in icon (custom iconfamily)
|
||||
|
||||
const zoomOut = document.createElement("button");
|
||||
zoomOut.className = "uiButton timetable-zoom iconFamily";
|
||||
zoomOut.type = "button";
|
||||
zoomOut.className = "timetable-zoom iconFamily bsplus-timetable-control";
|
||||
zoomOut.innerHTML = ""; // Unicode for zoom out icon (custom iconfamily)
|
||||
|
||||
zoomControls.appendChild(zoomOut);
|
||||
@@ -140,7 +139,8 @@ function handleTimetableAssessmentHide(): void {
|
||||
hideControls.className = "timetable-hide-controls";
|
||||
|
||||
const hideOn = document.createElement("button");
|
||||
hideOn.className = "uiButton timetable-hide iconFamily";
|
||||
hideOn.type = "button";
|
||||
hideOn.className = "timetable-hide iconFamily bsplus-timetable-control";
|
||||
hideOn.innerHTML = "";
|
||||
|
||||
hideControls.appendChild(hideOn);
|
||||
|
||||
@@ -33,7 +33,7 @@ function getRoomAndTeacherElements(entry: HTMLElement): {
|
||||
}
|
||||
|
||||
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(
|
||||
item: TimetableEntryData,
|
||||
@@ -147,7 +147,11 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
let quickbarObserver: MutationObserver | null = null;
|
||||
let quickbarSyncTimer: ReturnType<typeof setTimeout> | 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;
|
||||
|
||||
const getOverrides = (): TimetableOverrides =>
|
||||
@@ -161,6 +165,64 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
): { room?: string; staff?: string } | undefined =>
|
||||
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 => {
|
||||
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 => {
|
||||
if (lastClickedCi === null) return;
|
||||
if (lastSyncedQuickbarCi === lastClickedCi) return;
|
||||
resolveContextFromQuickbar(quickbar);
|
||||
|
||||
const description =
|
||||
quickbar.querySelector(".title")?.textContent?.trim() ??
|
||||
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) {
|
||||
lastSyncedQuickbarCi = lastClickedCi;
|
||||
lastSyncedQuickbarCi = ci;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -237,7 +303,7 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
teacherEl.textContent = override.staff;
|
||||
}
|
||||
|
||||
lastSyncedQuickbarCi = lastClickedCi;
|
||||
lastSyncedQuickbarCi = ci;
|
||||
};
|
||||
|
||||
const updateVisibleQuickbar = (room: string, staff: string): void => {
|
||||
@@ -259,13 +325,21 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer);
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 15;
|
||||
const trySync = (): void => {
|
||||
const quickbar = getVisibleClassQuickbar();
|
||||
if (quickbar && lastClickedCi !== null) {
|
||||
syncClassQuickbar(quickbar);
|
||||
if (!quickbar) {
|
||||
if (++attempts < maxAttempts) {
|
||||
quickbarSyncTimer = setTimeout(trySync, 50);
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -279,24 +353,33 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
const actions = quickbar.querySelector(".actions");
|
||||
if (!actions) return;
|
||||
|
||||
const colourBtn = actions.querySelector(
|
||||
"[title='Choose a colour'], button.uiButton",
|
||||
);
|
||||
const btn = document.createElement("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.innerHTML = EDIT_ICON_SVG;
|
||||
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const ci = lastClickedCi;
|
||||
const entryData = lastClickedEntry;
|
||||
if (!ci || !entryData) return;
|
||||
|
||||
const qb = (e.currentTarget as HTMLElement).closest(".quickbar");
|
||||
if (!qb) return;
|
||||
|
||||
resolveContextFromQuickbar(qb as HTMLElement);
|
||||
const entryData = lastClickedEntry;
|
||||
if (!entryData) return;
|
||||
|
||||
const quickbarRoom = qb.querySelector(".meta .room")?.textContent?.trim() ?? "";
|
||||
const quickbarTeacher = qb.querySelector(".meta .teacher")?.textContent?.trim() ?? "";
|
||||
const quickbarTitle = qb.querySelector(".title")?.textContent?.trim() ?? "";
|
||||
const ci = lastClickedCi ?? entryData.item.ci;
|
||||
const item: TimetableEntryData = {
|
||||
ci,
|
||||
description: quickbarTitle || entryData.item.description,
|
||||
@@ -308,8 +391,8 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
item,
|
||||
getOverrides(),
|
||||
getOverridesBySubject(),
|
||||
(ci, room, staff, applyToFuture) => {
|
||||
if (applyToFuture) {
|
||||
(saveCi, room, staff, applyToFuture) => {
|
||||
if (applyToFuture || !saveCi) {
|
||||
const bySubject = { ...getOverridesBySubject() };
|
||||
bySubject[item.description] = {
|
||||
room: room || undefined,
|
||||
@@ -320,7 +403,7 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
const current = getOverrides();
|
||||
api.storage.timetableOverrides = {
|
||||
...current,
|
||||
[String(ci)]: { room: room || undefined, staff: staff || undefined },
|
||||
[String(saveCi)]: { room: room || undefined, staff: staff || undefined },
|
||||
};
|
||||
}
|
||||
if (entryData.roomEl) entryData.roomEl.textContent = room;
|
||||
@@ -328,10 +411,12 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
updateVisibleQuickbar(room, staff);
|
||||
processAllEntries();
|
||||
},
|
||||
(ci) => {
|
||||
const current = getOverrides();
|
||||
delete current[String(ci)];
|
||||
api.storage.timetableOverrides = current;
|
||||
(clearCi) => {
|
||||
if (clearCi) {
|
||||
const current = getOverrides();
|
||||
delete current[String(clearCi)];
|
||||
api.storage.timetableOverrides = current;
|
||||
}
|
||||
const bySubject = getOverridesBySubject();
|
||||
delete bySubject[item.description];
|
||||
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 quickbar = getVisibleClassQuickbar();
|
||||
if (!quickbar || lastClickedCi === null || !lastClickedEntry) return;
|
||||
if (!quickbar) return;
|
||||
syncClassQuickbar(quickbar);
|
||||
};
|
||||
|
||||
@@ -357,26 +445,41 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
if (!timetablePage || quickbarObserver) return;
|
||||
|
||||
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();
|
||||
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, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributes: true,
|
||||
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 () => {
|
||||
// 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.
|
||||
@@ -406,6 +509,7 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
|
||||
return () => {
|
||||
unregister();
|
||||
document.removeEventListener("click", onTimetableEntryClick, true);
|
||||
observer?.disconnect();
|
||||
quickbarObserver?.disconnect();
|
||||
if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer);
|
||||
|
||||
@@ -1,30 +1,8 @@
|
||||
/* Timetable Edit Plugin - BetterSEQTA Plus style */
|
||||
|
||||
/* Edit button in quickbar */
|
||||
.timetable-edit-quickbar-btn {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
/* SEQTA sizes quickbar actions at 32×32 — layout comes from SEQTA + injected.scss */
|
||||
.timetablepage .quickbar .actions .timetable-edit-quickbar-btn {
|
||||
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 */
|
||||
|
||||
Reference in New Issue
Block a user