Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions frontend/src/Popover.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { bindManualHoverPopover } from 'utils/manualHoverPopover';

const BASE_COURSE_OFFERINGS_URL = 'https://www.princetoncourses.com/course/';
const COURSE_POPOVER_CLEANUP_KEY = '__tigerpathCoursePopoverCleanup';
const COURSE_POPOVER_CONTENT_KEY = '__tigerpathCoursePopoverContent';

function getRatingColor(rating) {
if (rating == null) return '#e0e0e0';
Expand Down Expand Up @@ -40,11 +41,6 @@ export function addPopover(course, courseKey, semIndex, duplicateCourseCounts =
const courseElement = document.getElementById(courseKey);
if (!courseElement) return;

const existingCleanup = courseElement[COURSE_POPOVER_CLEANUP_KEY];
if (typeof existingCleanup === 'function') {
existingCleanup();
}

let courseId = course['id'];
let courseSemList = course['semester_list'];
if (courseSemList && courseSemList.length > 0) {
Expand Down Expand Up @@ -97,6 +93,17 @@ export function addPopover(course, courseKey, semIndex, duplicateCourseCounts =
}

courseElement.setAttribute('data-bs-content', content);
const contentSignature = `${titleHtml}|${content}`;
const existingCleanup = courseElement[COURSE_POPOVER_CLEANUP_KEY];
if (
typeof existingCleanup === 'function' &&
courseElement[COURSE_POPOVER_CONTENT_KEY] === contentSignature
) {
return;
}
if (typeof existingCleanup === 'function') {
existingCleanup();
}

// Use Bootstrap 5 Popover API
const Popover = window.bootstrap?.Popover;
Expand All @@ -123,5 +130,7 @@ export function addPopover(course, courseKey, semIndex, duplicateCourseCounts =
cleanupHoverBehavior();
popoverInstance.dispose();
delete courseElement[COURSE_POPOVER_CLEANUP_KEY];
delete courseElement[COURSE_POPOVER_CONTENT_KEY];
};
courseElement[COURSE_POPOVER_CONTENT_KEY] = contentSignature;
}
31 changes: 30 additions & 1 deletion frontend/src/components/Requirements.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import TreeView from 'react-treeview/lib/react-treeview.js';
import { v1 as uuidv1 } from 'uuid';

const REQUIREMENTS_POPOVER_CLEANUP_KEY = '__tigerpathReqPopoverCleanup';
const REQUIREMENTS_POPOVER_CONTENT_KEY = '__tigerpathReqPopoverContent';
const HEADER_POPOVER_CLEANUP_KEY = '__tigerpathHeaderPopoverCleanup';
const HEADER_POPOVER_CONTENT_KEY = '__tigerpathHeaderPopoverContent';
const TREE_ITEM_CLICK_HANDLER_KEY = '__tigerpathTreeItemClickHandler';

function escapeHref(url) {
Expand Down Expand Up @@ -502,7 +504,7 @@ export default function Requirements({ onChange, requirements, schedule, activeP
items.forEach((item) => {
const existingHandler = item[TREE_ITEM_CLICK_HANDLER_KEY];
if (typeof existingHandler === 'function') {
item.removeEventListener('click', existingHandler);
return;
}

const clickHandler = (event) => {
Expand Down Expand Up @@ -541,7 +543,18 @@ export default function Requirements({ onChange, requirements, schedule, activeP
'.reqLabel:not(.reqLabel-main)'
);
reqLabels.forEach((reqLabel) => {
const contentSignature = [
reqLabel.getAttribute('reqpath') || '',
reqLabel.getAttribute('title') || '',
reqLabel.getAttribute('data-bs-content') || '',
].join('|');
const existingCleanup = reqLabel[REQUIREMENTS_POPOVER_CLEANUP_KEY];
if (
typeof existingCleanup === 'function' &&
reqLabel[REQUIREMENTS_POPOVER_CONTENT_KEY] === contentSignature
) {
return;
}
if (typeof existingCleanup === 'function') existingCleanup();

const existing = Popover.getInstance(reqLabel);
Expand Down Expand Up @@ -583,7 +596,9 @@ export default function Requirements({ onChange, requirements, schedule, activeP
cleanupHoverBehavior();
popoverInstance.dispose();
delete reqLabel[REQUIREMENTS_POPOVER_CLEANUP_KEY];
delete reqLabel[REQUIREMENTS_POPOVER_CONTENT_KEY];
};
reqLabel[REQUIREMENTS_POPOVER_CONTENT_KEY] = contentSignature;
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps

Expand All @@ -594,7 +609,19 @@ export default function Requirements({ onChange, requirements, schedule, activeP

const icons = containerRef.current.querySelectorAll('.info-icon');
icons.forEach((icon) => {
const contentSignature = [
icon.getAttribute('data-tp-info') || '',
icon.getAttribute('data-tp-ref0') || '',
icon.getAttribute('data-tp-ref1') || '',
icon.getAttribute('data-bs-content') || '',
].join('|');
const existingCleanup = icon[HEADER_POPOVER_CLEANUP_KEY];
if (
typeof existingCleanup === 'function' &&
icon[HEADER_POPOVER_CONTENT_KEY] === contentSignature
) {
return;
}
if (typeof existingCleanup === 'function') existingCleanup();

const existing = Popover.getInstance(icon);
Expand Down Expand Up @@ -640,7 +667,9 @@ export default function Requirements({ onChange, requirements, schedule, activeP
cleanupHover();
popoverInstance.dispose();
delete icon[HEADER_POPOVER_CLEANUP_KEY];
delete icon[HEADER_POPOVER_CONTENT_KEY];
};
icon[HEADER_POPOVER_CONTENT_KEY] = contentSignature;
});
}, []);

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Schedule.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { apiFetch } from 'utils/api';
import Semester from 'components/Semester';
import styled from 'styled-components';
import { DEFAULT_SCHEDULE } from 'utils/SemesterUtils';
import { DEFAULT_SCHEDULE, getScheduledCourseKey } from 'utils/SemesterUtils';
import { addPopover } from 'Popover';

const YEARS = [
Expand Down Expand Up @@ -112,7 +112,7 @@ export default function Schedule({ onChange, profile, schedule }) {
for (let semIndex = 0; semIndex < sched.length; semIndex++) {
for (let courseIndex = 0; courseIndex < sched[semIndex].length; courseIndex++) {
let course = sched[semIndex][courseIndex];
let courseKey = `course-card-${course['semester']}-${semIndex}-${courseIndex}`;
let courseKey = getScheduledCourseKey(course, semIndex, courseIndex);
addPopover(course, courseKey, semIndex, courseNameCounts);
}
}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Semester.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SEMESTER_TYPE,
EXTERNAL_CREDITS_SEMESTER_INDEX,
getSemesterType,
getScheduledCourseKey,
isFallSemester,
isSpringSemester,
} from 'utils/SemesterUtils';
Expand Down Expand Up @@ -111,7 +112,7 @@ export default function Semester({ onChange, schedule, semesterIndex, className,
return (
<>
{semester[semIndex].map((course, courseIndex) => {
let courseKey = `course-card-${course['semester']}-${semIndex}-${courseIndex}`;
let courseKey = getScheduledCourseKey(course, semIndex, courseIndex);
return (
<CourseCard
key={courseKey}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/utils/SemesterUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export function getSemesterNames(classYear) {
return semesterNames;
}

export function getScheduledCourseKey(course, semIndex, courseIndex) {
const stableId = course?.id || `${semIndex}-${courseIndex}`;
return `course-card-${course?.semester || 'both'}-${stableId}-${semIndex}-${courseIndex}`;
}

/* Converts semester to term code */
export function convertSemToTermCode(sem) {
let code = '1';
Expand Down
Loading