diff --git a/frontend/src/Popover.jsx b/frontend/src/Popover.jsx index 6e4b8513..25417828 100644 --- a/frontend/src/Popover.jsx +++ b/frontend/src/Popover.jsx @@ -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'; @@ -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) { @@ -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; @@ -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; } diff --git a/frontend/src/components/Requirements.jsx b/frontend/src/components/Requirements.jsx index b3bdd6a9..1e2e4048 100644 --- a/frontend/src/components/Requirements.jsx +++ b/frontend/src/components/Requirements.jsx @@ -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) { @@ -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) => { @@ -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); @@ -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 @@ -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); @@ -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; }); }, []); diff --git a/frontend/src/components/Schedule.jsx b/frontend/src/components/Schedule.jsx index 1becc8a8..27e048d1 100644 --- a/frontend/src/components/Schedule.jsx +++ b/frontend/src/components/Schedule.jsx @@ -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 = [ @@ -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); } } diff --git a/frontend/src/components/Semester.jsx b/frontend/src/components/Semester.jsx index d1d4dad6..63ffa535 100644 --- a/frontend/src/components/Semester.jsx +++ b/frontend/src/components/Semester.jsx @@ -6,6 +6,7 @@ import { SEMESTER_TYPE, EXTERNAL_CREDITS_SEMESTER_INDEX, getSemesterType, + getScheduledCourseKey, isFallSemester, isSpringSemester, } from 'utils/SemesterUtils'; @@ -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 (