From fcd95f68238aa58b027b046559f390e2893fd69e Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Sat, 30 Nov 2024 21:55:10 +1100 Subject: [PATCH] perf(homePage): add fragmentation and refactor code --- src/SEQTA.ts | 529 ++++++++++++++++++++++++++------------------------- 1 file changed, 273 insertions(+), 256 deletions(-) diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 60592ffa..c6699e64 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -1763,8 +1763,8 @@ function createAssessmentDateDiv(date: string, value: any, datecase?: any) { let titlesvg = stringToHTML(` - - `).firstChild + + `).firstChild titlediv.append(titlesvg!) let detailsdiv = document.createElement('div') @@ -2143,174 +2143,159 @@ async function AddCustomShortcutsToPage() { } export async function loadHomePage() { - // Sends the html data for the home page console.info('[BetterSEQTA+] Started Loading Home Page') document.title = 'Home ― SEQTA Learn' const element = document.querySelector('[data-key=home]') + element?.classList.add('active') - await delay(8) - - // Apply the active class to indicate clicked on home button - element!.classList.add('active') - - // Remove all current elements in the main div to add new elements + // Cache DOM queries const main = document.getElementById('main') - if (!main) { console.error('Main element not found.') return - } else { - main!.innerHTML = '' } - currentSelectedDate = new Date() - - // Creates the root of the home page added to the main div - let homeContainer = stringToHTML(/* html */` + // Create root container first + const homeRoot = stringToHTML(`
`) - // Appends the html file to main div - // Note: firstChild of html is done due to needing to grab the body from the stringToHTML function - main.append(homeContainer?.firstChild!) + // Clear main and add home root + main.innerHTML = '' + main.appendChild(homeRoot?.firstChild!) - // Gets the current date + // Get reference to home container for all subsequent additions + const homeContainer = document.getElementById('home-container') + if (!homeContainer) return + + // Add initial style to prevent flash + if (settingsState.animations) { + const style = document.createElement('style') + style.textContent = ` + .home-container > div { + opacity: 0; + transform: translateY(10px) scale(0.99); + } + ` + document.head.appendChild(style) + } + + // Use DocumentFragment for batch DOM updates inside home-container + const fragment = document.createDocumentFragment() + + // Batch state updates const date = new Date() + currentSelectedDate = new Date() + const TodayFormatted = formatDate(date) - // Creates the shortcut container into the home container - const Shortcut = stringToHTML(/* html */` + // Create shortcuts section + const shortcutContainer = stringToHTML(`
`) + fragment.appendChild(shortcutContainer?.firstChild!) - // Appends the shortcut container into the home container - document.getElementById('home-container')?.append(Shortcut?.firstChild!) - - // Creates the container div for the timetable portion of the home page - const Timetable = stringToHTML(/* html */` -
+ // Create timetable section with optimized structure + const timetable = stringToHTML(` +
-

Today\'s Lessons

+

Today's Lessons

- - + + + + + +
`) + fragment.appendChild(timetable?.firstChild!) + + // Create upcoming assessments section + const upcomingContainer = document.createElement('div') + upcomingContainer.classList.add('upcoming-container', 'border') - // Appends the timetable container into the home container - document.getElementById('home-container')?.append(Timetable?.firstChild!) + const upcomingTitleDiv = CreateElement('div', 'upcoming-title') + const upcomingTitle = document.createElement('h2') + upcomingTitle.classList.add('home-subtitle') + upcomingTitle.innerText = 'Upcoming Assessments' + upcomingTitleDiv.append(upcomingTitle) - // Formats the current date used send a request for timetable and notices later - const TodayFormatted = - date.getFullYear() + '-' + ((date.getMonth() + 1) < 10 ? '0' : '') + (date.getMonth() + 1) + '-' + (date.getDate() < 10 ? '0' : '') + date.getDate() + const upcomingFilterDiv = CreateElement('div', 'upcoming-filters', 'upcoming-filters') + upcomingTitleDiv.append(upcomingFilterDiv) + upcomingContainer.append(upcomingTitleDiv) - callHomeTimetable(TodayFormatted, true) + const upcomingItems = document.createElement('div') + upcomingItems.id = 'upcoming-items' + upcomingItems.classList.add('upcoming-items') + upcomingContainer.append(upcomingItems) + fragment.appendChild(upcomingContainer) - - const timetablearrowback = document.getElementById('home-timetable-back') - const timetablearrowforward = document.getElementById('home-timetable-forward') - - function SetTimetableSubtitle() { - var homelessonsubtitle = document.getElementById('home-lesson-subtitle') - const date = new Date() - if ( - date.getFullYear() == currentSelectedDate.getFullYear() && - date.getMonth() == currentSelectedDate.getMonth() - ) { - if (date.getDate() == currentSelectedDate.getDate()) { - // Change text to Today's Lessons - homelessonsubtitle!.innerText = 'Today\'s Lessons' - } else if (date.getDate() - 1 == currentSelectedDate.getDate()) { - // Change text to Yesterday's Lessons - homelessonsubtitle!.innerText = 'Yesterday\'s Lessons' - } else if (date.getDate() + 1 == currentSelectedDate.getDate()) { - // Change text to Tomorrow's Lessons - homelessonsubtitle!.innerText = 'Tomorrow\'s Lessons' - } else { - // Change text to date of the day - homelessonsubtitle!.innerText = `${currentSelectedDate.toLocaleString( - 'en-us', - { weekday: 'short' }, - )} ${currentSelectedDate.toLocaleDateString('en-au')}` - } - } else { - // Change text to date of the day - homelessonsubtitle!.innerText = `${currentSelectedDate.toLocaleString( - 'en-us', - { weekday: 'short' }, - )} ${currentSelectedDate.toLocaleDateString('en-au')}` - } - } - - function changeTimetable(value: any) { - currentSelectedDate.setDate(currentSelectedDate.getDate() + value) - let FormattedDate = - currentSelectedDate.getFullYear() + - '-' + - (currentSelectedDate.getMonth() + 1) + - '-' + - currentSelectedDate.getDate() - callHomeTimetable(FormattedDate, true) - SetTimetableSubtitle() - } - - timetablearrowback!.addEventListener('click', function () { - changeTimetable(-1) - }) - timetablearrowforward!.addEventListener('click', function () { - changeTimetable(1) - }) - - addShortcuts(settingsState.shortcuts) - AddCustomShortcutsToPage() - - // Creates the upcoming container and appends to the home container - const upcomingcontainer = document.createElement('div') - upcomingcontainer.classList.add('upcoming-container') - upcomingcontainer.classList.add('border') - - const upcomingtitlediv = CreateElement('div', 'upcoming-title') - const upcomingtitle = document.createElement('h2') - upcomingtitle.classList.add('home-subtitle') - upcomingtitle.innerText = 'Upcoming Assessments' - upcomingtitlediv.append(upcomingtitle) - - let upcomingfilterdiv = CreateElement( - 'div', - 'upcoming-filters', - 'upcoming-filters', - ) - upcomingtitlediv.append(upcomingfilterdiv) - - upcomingcontainer.append(upcomingtitlediv) - - const upcomingitems = document.createElement('div') - upcomingitems.id = 'upcoming-items' - upcomingitems.classList.add('upcoming-items') - - upcomingcontainer.append(upcomingitems) - - document.getElementById('home-container')!.append(upcomingcontainer) - - // Creates the notices container into the home container - const NoticesStr = /* html */ ` -
+ // Create notices section + const notices = stringToHTML(` +

Notices

- +
-
` - - var Notices = stringToHTML(NoticesStr) - // Appends the shortcut container into the home container - document.getElementById('home-container')!.append(Notices.firstChild!) // HERE!!! +
`) + fragment.appendChild(notices?.firstChild!) + // Single DOM update to home-container + homeContainer.appendChild(fragment) + + // Setup event listeners with cleanup + const cleanup = setupTimetableListeners() + + // Parallel data fetching + const [assessments, classes, prefs] = await Promise.all([ + GetUpcomingAssessments(), + GetActiveClasses(), + fetch(`${location.origin}/seqta/student/load/prefs?`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ asArray: true, request: 'userPrefs' }) + }).then(res => res.json()) + ]) + + // Process data + const activeClass = classes.find((c: any) => c.hasOwnProperty("active")) + const activeSubjects = activeClass?.subjects || [] + const activeSubjectCodes = activeSubjects.map((s: any) => s.code) + const currentAssessments = assessments + .filter((a: any) => activeSubjectCodes.includes(a.code)) + .sort(comparedate) + + // Initialize components + addShortcuts(settingsState.shortcuts) + AddCustomShortcutsToPage() + await callHomeTimetable(TodayFormatted, true) + await CreateUpcomingSection(currentAssessments, activeSubjects) + + // Setup notices + const labelArray = prefs.payload + .filter((item: any) => item.name === 'notices.filters') + .map((item: any) => item.value) + + if (labelArray.length > 0) { + setupNotices(labelArray[0].split(' '), TodayFormatted) + } + + if (settingsState.notificationcollector) { + enableNotificationCollector() + } + + // Setup animations if (settingsState.animations) { + // Remove the initial style + document.head.querySelector('style:last-child')?.remove() + + // Animate with motion animate( '.home-container > div', { opacity: [0, 1], y: [10, 0], scale: [0.99, 1] }, @@ -2324,135 +2309,76 @@ export async function loadHomePage() { ) } - callHomeTimetable(TodayFormatted) - - const GetPrefs = await fetch(`${location.origin}/seqta/student/load/prefs?`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ asArray: true, request: 'userPrefs' }) - }) - - const response = await GetPrefs.json() - - const labelArray = response.payload.filter((item: any) => item.name === 'notices.filters').map((item: any) => item.value) - - if (labelArray.length !== 0) { - const labelArray = response.payload.filter((item: any) => item.name === 'notices.filters').map((item: any) => item.value)[0].split(' ') - const xhr2 = new XMLHttpRequest() - xhr2.open( - 'POST', - `${location.origin}/seqta/student/load/notices?`, - true - ) - xhr2.setRequestHeader('Content-Type', 'application/json; charset=utf-8') - - xhr2.onreadystatechange = function () { - if (xhr2.readyState === 4) { - processNotices(xhr2.response, labelArray); - } - }; - - const dateControl = document.querySelector('input[type="date"]') as HTMLInputElement; - xhr2.send(JSON.stringify({ date: dateControl.value })); - - function onInputChange(e: any) { - xhr2.open('POST', `${location.origin}/seqta/student/load/notices?`, true); - xhr2.setRequestHeader('Content-Type', 'application/json; charset=utf-8'); - xhr2.send(JSON.stringify({ date: e.target.value })); - - xhr2.onreadystatechange = function () { - if (xhr2.readyState === 4) { - processNotices(xhr2.response, labelArray); - } - }; - } - - dateControl.addEventListener('input', onInputChange); - } - - if (settingsState.notificationcollector) { - enableNotificationCollector() - } - - const assessments = await GetUpcomingAssessments() - const classes = await GetActiveClasses() - - let activeClass; - - // Gets all subjects for the student - for (let i = 0; i < classes.length; i++) { - const element = classes[i]; - - if (element.hasOwnProperty("active")) { - // Finds the active class list with the current subjects - activeClass = classes[i] - } - } - - let activeSubjects = [] - if (activeClass?.subjects) { - activeSubjects = activeClass.subjects - } - - let activeSubjectCodes = [] - - // Gets the code for each of the subjects and puts them in an array - for (let i = 0; i < activeSubjects.length; i++) { - activeSubjectCodes.push(activeSubjects[i].code) - } - - let CurrentAssessments = [] - for (let i = 0; i < assessments.length; i++) { - if (activeSubjectCodes.includes(assessments[i].code)) { - CurrentAssessments.push(assessments[i]) - } - } - - CurrentAssessments.sort(comparedate) - - await CreateUpcomingSection(CurrentAssessments, activeSubjects) + return cleanup } -function processNotices(responseText: any, labelArray: any) { - const NoticesPayload = JSON.parse(responseText); - const NoticeContainer = document.getElementById('notice-container'); - if (NoticesPayload.payload.length === 0) { - if (!NoticeContainer?.innerText) { - const dummyNotice = document.createElement('div'); - dummyNotice.textContent = 'No notices for today.'; - dummyNotice.classList.add('dummynotice'); - NoticeContainer?.append(dummyNotice); - } - } else { - if (!NoticeContainer?.innerText) { - document.querySelectorAll('.notice').forEach(e => e.remove()); +// Helper functions +function formatDate(date: Date): string { + const year = date.getFullYear() + const month = (date.getMonth() + 1).toString().padStart(2, '0') + const day = date.getDate().toString().padStart(2, '0') + return `${year}-${month}-${day}` +} - NoticesPayload.payload.forEach((notice: any) => { - if (labelArray.includes(JSON.stringify(notice.label))) { - let htmlContent = ` -
-

${notice.title}

- ${notice.label_title !== undefined ? `
${notice.label_title}
` : ''} -
${notice.staff}
- ${notice.contents.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, '').replace(/ +/, ' ')} -
-
- `; - const NewNotice = stringToHTML(htmlContent).firstChild; +function setupTimetableListeners() { + const listeners: Array<() => void> = [] + const timetableBack = document.getElementById('home-timetable-back') + const timetableForward = document.getElementById('home-timetable-forward') - let colour = notice.colour; - if (typeof colour === 'string') { - const rgb = GetThresholdOfColor(colour); - if (rgb < 100 && settingsState.DarkMode) { - colour = undefined; - } - } - (NewNotice as HTMLElement).style.cssText = `--colour: ${colour}`; + function changeTimetable(value: number) { + currentSelectedDate.setDate(currentSelectedDate.getDate() + value) + const formattedDate = formatDate(currentSelectedDate) + callHomeTimetable(formattedDate, true) + SetTimetableSubtitle() + } - NoticeContainer!.append(NewNotice!); - } - }); - } + const backHandler = () => changeTimetable(-1) + const forwardHandler = () => changeTimetable(1) + + timetableBack?.addEventListener('click', backHandler) + timetableForward?.addEventListener('click', forwardHandler) + + listeners.push( + () => timetableBack?.removeEventListener('click', backHandler), + () => timetableForward?.removeEventListener('click', forwardHandler) + ) + + return () => listeners.forEach(cleanup => cleanup()) +} + +function setupNotices(labelArray: string[], date: string) { + const dateControl = document.querySelector('input[type="date"]') as HTMLInputElement + + const fetchNotices = async (date: string) => { + const response = await fetch(`${location.origin}/seqta/student/load/notices?`, { + method: 'POST', + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + body: JSON.stringify({ date }) + }) + const data = await response.json() + processNotices(data, labelArray) + } + + // Debounce the input handler + const debouncedInputChange = debounce((e: Event) => { + const target = e.target as HTMLInputElement + fetchNotices(target.value) + }, 250) + + dateControl?.addEventListener('input', debouncedInputChange) + fetchNotices(date) + + return () => dateControl?.removeEventListener('input', debouncedInputChange) +} + +function debounce any>( + func: T, + wait: number +): (...args: Parameters) => void { + let timeout: NodeJS.Timeout + return (...args: Parameters) => { + clearTimeout(timeout) + timeout = setTimeout(() => func(...args), wait) } } @@ -2630,3 +2556,94 @@ async function CheckForMenuList() { return; } } + +function SetTimetableSubtitle() { + const homelessonsubtitle = document.getElementById('home-lesson-subtitle') + if (!homelessonsubtitle) return + + const date = new Date() + const isSameMonth = date.getFullYear() === currentSelectedDate.getFullYear() && + date.getMonth() === currentSelectedDate.getMonth() + + if (isSameMonth) { + const dayDiff = date.getDate() - currentSelectedDate.getDate() + switch(dayDiff) { + case 0: + homelessonsubtitle.innerText = 'Today\'s Lessons' + break + case 1: + homelessonsubtitle.innerText = 'Yesterday\'s Lessons' + break + case -1: + homelessonsubtitle.innerText = 'Tomorrow\'s Lessons' + break + default: + homelessonsubtitle.innerText = formatDateString(currentSelectedDate) + } + } else { + homelessonsubtitle.innerText = formatDateString(currentSelectedDate) + } +} + +function formatDateString(date: Date): string { + return `${date.toLocaleString('en-us', { weekday: 'short' })} ${date.toLocaleDateString('en-au')}` +} + +function processNotices(response: any, labelArray: string[]) { + const NoticeContainer = document.getElementById('notice-container') + if (!NoticeContainer) return + + // Clear existing notices + NoticeContainer.innerHTML = '' + + const notices = response.payload + if (!notices.length) { + const dummyNotice = document.createElement('div') + dummyNotice.textContent = 'No notices for today.' + dummyNotice.classList.add('dummynotice') + NoticeContainer.append(dummyNotice) + return + } + + // Create document fragment for batch DOM updates + const fragment = document.createDocumentFragment() + + // Process notices in batch + notices.forEach((notice: any) => { + if (labelArray.includes(JSON.stringify(notice.label))) { + const colour = processNoticeColor(notice.colour) + const noticeElement = createNoticeElement(notice, colour) + fragment.appendChild(noticeElement) + } + }) + + // Single DOM update + NoticeContainer.appendChild(fragment) +} + +function processNoticeColor(colour: string): string | undefined { + if (typeof colour === 'string') { + const rgb = GetThresholdOfColor(colour) + if (rgb < 100 && settingsState.DarkMode) { + return undefined + } + } + return colour +} + +function createNoticeElement(notice: any, colour: string | undefined): Node { + const htmlContent = ` +
+

${notice.title}

+ ${notice.label_title !== undefined ? `
${notice.label_title}
` : ''} +
${notice.staff}
+ ${notice.contents.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, '').replace(/ +/, ' ')} +
+
` + + const element = stringToHTML(htmlContent).firstChild + if (element instanceof HTMLElement) { + element.style.setProperty('--colour', colour ?? '') + } + return element! +}