fix today's lessons tweaking out (#420)

This commit is contained in:
Aden Lindsay
2026-04-07 09:14:08 +09:30
committed by GitHub
+50 -21
View File
@@ -3,6 +3,8 @@ interface ElementConfig {
action: (element: Element) => void; action: (element: Element) => void;
/** When true, element is not added to processedElements so the action runs every time (e.g. overwriting container content) */ /** When true, element is not added to processedElements so the action runs every time (e.g. overwriting container content) */
alwaysRun?: boolean; alwaysRun?: boolean;
/** When true, never add to processedElements so the action can run again after DOM resets (e.g. home day column) */
neverMarkProcessed?: boolean;
} }
interface ContentConfig { interface ContentConfig {
@@ -12,6 +14,12 @@ interface ContentConfig {
// Track processed elements to avoid re-randomizing // Track processed elements to avoid re-randomizing
const processedElements = new WeakSet<Element>(); const processedElements = new WeakSet<Element>();
/** Marks mock-generated `.day` rows so granular rules do not re-randomize them */
const MOCK_DAY_ATTR = "data-bsp-mock-day";
/** Skip MutationObserver-driven reprocessing while we inject the home mock (avoids feedback loops) */
let suppressMockMutations = false;
function debounce(func: Function, wait: number): Function { function debounce(func: Function, wait: number): Function {
let timeout: NodeJS.Timeout; let timeout: NodeJS.Timeout;
return function executedFunction(...args: any[]) { return function executedFunction(...args: any[]) {
@@ -44,19 +52,19 @@ function getRandomDate(): Date {
const contentConfig: ContentConfig = { const contentConfig: ContentConfig = {
lessonTitle: { lessonTitle: {
selector: ".day h2", selector: `.day:not([${MOCK_DAY_ATTR}]) h2`,
action: (element) => { action: (element) => {
element.textContent = getRandomElement(mockData.subjects); element.textContent = getRandomElement(mockData.subjects);
}, },
}, },
teacher: { teacher: {
selector: ".day h3:first-of-type", selector: `.day:not([${MOCK_DAY_ATTR}]) h3:first-of-type`,
action: (element) => { action: (element) => {
element.textContent = getRandomElement(mockData.teachers); element.textContent = getRandomElement(mockData.teachers);
}, },
}, },
classroom: { classroom: {
selector: ".day h3:last-of-type", selector: `.day:not([${MOCK_DAY_ATTR}]) h3:last-of-type`,
action: (element) => { action: (element) => {
element.textContent = getRandomElement(mockData.classrooms); element.textContent = getRandomElement(mockData.classrooms);
}, },
@@ -283,13 +291,28 @@ const contentConfig: ContentConfig = {
// Home page: replace entire day with mock schedule (care + 7 lessons 8:553:15) // Home page: replace entire day with mock schedule (care + 7 lessons 8:553:15)
homeDayContainer: { homeDayContainer: {
selector: "#day-container", selector: "#day-container",
alwaysRun: true, neverMarkProcessed: true,
action: (element) => { action: (element) => {
const container = element as HTMLElement; const container = element as HTMLElement;
if (!container.closest(".timetable-container")) return; // only on home if (!container.closest(".timetable-container")) return; // only on home
if (container.classList.contains("loading") || container.innerHTML.trim() === "") {
delete container.dataset.bspMockSchedule;
return;
}
if (
container.dataset.bspMockSchedule === "1" &&
container.querySelector(`[${MOCK_DAY_ATTR}]`)
) {
return;
}
suppressMockMutations = true;
const schedule = getMockDaySchedule(); const schedule = getMockDaySchedule();
container.innerHTML = schedule; container.innerHTML = schedule;
container.classList.remove("loading"); container.classList.remove("loading");
container.dataset.bspMockSchedule = "1";
requestAnimationFrame(() => {
suppressMockMutations = false;
});
}, },
}, },
}; };
@@ -665,7 +688,7 @@ function getMockDaySchedule(): string {
return blocks return blocks
.map( .map(
(b, i) => (b, i) =>
`<div class="day" style="--item-colour: ${colours[i % colours.length]};"> `<div class="day" ${MOCK_DAY_ATTR} style="--item-colour: ${colours[i % colours.length]};">
<h2>${b.title}</h2> <h2>${b.title}</h2>
<h3>${b.teacher}</h3> <h3>${b.teacher}</h3>
<h3>${b.room}</h3> <h3>${b.room}</h3>
@@ -758,12 +781,12 @@ const debouncedProcessElements = debounce(processNewElements, 1);
function processNewElements() { function processNewElements() {
Object.entries(contentConfig).forEach(([_, config]) => { Object.entries(contentConfig).forEach(([_, config]) => {
const { selector, action, alwaysRun } = config; const { selector, action, alwaysRun, neverMarkProcessed } = config;
const elements = document.querySelectorAll(selector); const elements = document.querySelectorAll(selector);
elements.forEach((element: Element) => { elements.forEach((element: Element) => {
if (alwaysRun || !processedElements.has(element)) { if (alwaysRun || neverMarkProcessed || !processedElements.has(element)) {
action(element); action(element);
if (!alwaysRun) { if (!alwaysRun && !neverMarkProcessed) {
processedElements.add(element); processedElements.add(element);
} }
} }
@@ -772,7 +795,6 @@ function processNewElements() {
} }
let observer: MutationObserver | null = null; let observer: MutationObserver | null = null;
let intervalId: NodeJS.Timeout | null = null;
export default function hideSensitiveContent() { export default function hideSensitiveContent() {
// Initial processing of existing elements // Initial processing of existing elements
@@ -781,6 +803,8 @@ export default function hideSensitiveContent() {
// Set up MutationObserver if not already created // Set up MutationObserver if not already created
if (!observer) { if (!observer) {
observer = new MutationObserver((mutations) => { observer = new MutationObserver((mutations) => {
if (suppressMockMutations) return;
let shouldProcess = false; let shouldProcess = false;
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
@@ -802,9 +826,25 @@ export default function hideSensitiveContent() {
}); });
} }
// Also trigger on large DOM replacements (like page navigation) // Large DOM replacements (e.g. page navigation). Skip only when #day-container gains many *mock* rows (our inject).
if (mutation.addedNodes.length > 5 || mutation.removedNodes.length > 5) { if (mutation.addedNodes.length > 5 || mutation.removedNodes.length > 5) {
const target = mutation.target as Element;
if (target.id === "day-container") {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as Element;
if (
el.classList?.contains("day") &&
!el.hasAttribute(MOCK_DAY_ATTR)
) {
shouldProcess = true; shouldProcess = true;
break;
}
}
}
} else {
shouldProcess = true;
}
} }
} }
@@ -833,13 +873,6 @@ export default function hideSensitiveContent() {
attributeFilter: ['class', 'id'] // Watch for class/id changes that might affect our selectors attributeFilter: ['class', 'id'] // Watch for class/id changes that might affect our selectors
}); });
} }
// Fallback: periodic check for new elements (especially useful for SPA navigation)
if (!intervalId) {
intervalId = setInterval(() => {
debouncedProcessElements();
}, 500); // Check every 500ms as a fallback
}
} }
// Function to stop observing (useful for cleanup) // Function to stop observing (useful for cleanup)
@@ -848,8 +881,4 @@ export function stopHidingSensitiveContent() {
observer.disconnect(); observer.disconnect();
observer = null; observer = null;
} }
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
} }