Raw Source
deathhackz / PupilPath Plus

// ==UserScript==
// @name          PupilPath Plus
// @namespace     https://github.com/DeathHackz/PupilPathPlus
// @version       4.0.2
// @description   Calculate Your PupilPath Cumulative Average & More
// @match         https://*.pupilpath.skedula.com/*
// @author        DeathHackz
// @copyright     2019 DeathHackz
// @license       MIT
// @homepageURL   https://deathhackz.github.io/PupilPathPlus
// @supportURL    https://github.com/DeathHackz/PupilPathPlus/issues
// @icon          https://github.com/DeathHackz/PupilPathPlus/raw/master/icon.png
// @updateURL     https://raw.githubusercontent.com/DeathHackz/PupilPathPlus/master/src/PupilPathPlus.meta.js
// @downloadURL   https://raw.githubusercontent.com/DeathHackz/PupilPathPlus/master/src/PupilPathPlus.user.js
// @run-at        document-body
// @grant         GM_registerMenuCommand
/* global jQuery */
// ==/UserScript==

/*
=================================================================================================================================
PupilPath, and all related icons/images/assets, and names belong to IO Education
I am NOT affiliated with PupilPath or IO Education by no means, neither is this script
=================================================================================================================================
MIT License

Copyright (c) 2019 DeathHackz

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=================================================================================================================================
Supported Userscript Extensions:
* Tampermonkey (recommended)
* Violentmonkey
=================================================================================================================================
PupilPath Default Libraries
* jQuery v1.5.1
* jQuery-UI v1.8.11 CUSTOM
* jQuery FaceBox v1.2
* jQuery Timepicker Addon v0.9.3
=================================================================================================================================
*/

// Make Entire Script Run In Strict Mode
"use strict";

// Script Version
const scriptVersion = "4.0.2";

// Add Menu Items To UserScript Manager
GM_registerMenuCommand("Visit Homepage", () => {
  window.open("https://deathhackz.github.io/PupilPathPlus");
});
GM_registerMenuCommand("Check For Updates", () => {
  window.open("https://raw.githubusercontent.com/DeathHackz/PupilPathPlus/master/src/PupilPathPlus.user.js");
});

// Console Warning Message
// SetTimeout Is Used To Remove The Source Link
setTimeout(
  console.warn.bind(
    // Styling Is Used Within The Console Message
    console,
    `%c \n %c You Are Using PupilPath Plus%cv${scriptVersion} %c \n %c    Follow %c@DeathHackz%con GitHub    `,
    "",
    "background: #383b3e; color: #bada55; padding: 4px; padding-left: 0px; border-top-left-radius: 2px;",
    "background: #383b3e; color: #C8FF19; padding: 4px; padding-right: 0px; border-top-right-radius: 2px;",
    "",
    "background: #383b3e; color: #bada55; padding: 4px; padding-left: 2.8px; border-bottom-left-radius: 2px;",
    "background: #383b3e; color: #C8FF19; padding-top: 4px; padding-bottom: 4px;",
    "background: #383b3e; color: #bada55; padding: 4px; padding-right: 0px; padding-left: 8px; border-bottom-right-radius: 2px;",
    "\nDeathHackz: https://github.com/DeathHackz",
    "\nHomepage:   https://deathhackz.github.io/PupilPathPlus",
    "\nGitHub:     https://github.com/DeathHackz/PupilPathPlus",
    "\nSupport:    https://github.com/DeathHackz/PupilPathPlus/issues\n ",
  )
);

// Define All Functions Before Conditional Statement

// Function Accepts An Array And Returns The Average
function calculateAverage(grades) {
  // Adds Up All The Values From The Array
  const gradesTotal = grades.reduce((a, b) => a + b);
  const totalGrades = grades.length;
  const average = gradesTotal / totalGrades;
  return average;
}

// Function To See If Two Array's Are Identical (Have The Same Values)
function compareArrays(a, b) {
  // Check If Array's Have The Same Length
  if (a.length !== b.length) {
    return false;
  }
  // Check To See If All The Values Are The Same
  for (let i = a.length; i--;) {
    if (a[i] !== b[i]) {
      return false;
    }
  }
  return true;
}

// Function To See If All Values In An Array Are Between 0 - 110
function isValidGrades(array) {
  if (array >= 0 && array <= 110) {
    return true;
  } else {
    return false;
  }
}

// Function To Change Grade View
function setGradeView(view) {
  // Define All Needed DOM Elements
  const numberGrades = document.querySelectorAll("span.numberGrade");
  const letterGrades = document.querySelectorAll("span.letterGrade");
  const gpaGrades = document.querySelectorAll("span.gpaGrade");
  const numAverage = document.querySelector("span#totalAverage.numberGrade");
  const letterAverage = document.querySelector("span#totalAverage.letterGrade");
  const gpaAverage = document.querySelector("span#totalAverage.gpaGrade");
  // Use Conditionals To Show Selected Grade Type & Hide The Others
  if (view === "number") {
    localStorage.setItem("DefaultGradeView", "number");
    numAverage.style.display = "initial";
    letterAverage.style.display = "none";
    gpaAverage.style.display = "none";
    numberGrades.forEach(e => {
      e.style.display = "initial";
    });
    letterGrades.forEach(e => {
      e.style.display = "none";
    });
    gpaGrades.forEach(e => {
      e.style.display = "none";
    });
  }
  if (view === "letter") {
    localStorage.setItem("DefaultGradeView", "letter");
    numAverage.style.display = "none";
    letterAverage.style.display = "initial";
    gpaAverage.style.display = "none";
    numberGrades.forEach(e => {
      e.style.display = "none";
    });
    letterGrades.forEach(e => {
      e.style.display = "initial";
    });
    gpaGrades.forEach(e => {
      e.style.display = "none";
    });
  }
  if (view === "gpa") {
    localStorage.setItem("DefaultGradeView", "gpa");
    numAverage.style.display = "none";
    letterAverage.style.display = "none";
    gpaAverage.style.display = "initial";
    numberGrades.forEach(e => {
      e.style.display = "none";
    });
    letterGrades.forEach(e => {
      e.style.display = "none";
    });
    gpaGrades.forEach(e => {
      e.style.display = "initial";
    });
  }
}

// Puts The Inputted Grades Array Through Series Of Conditionals And Returns Appropriate Data For Each Grade
function convertGrades(grade) {
  // All The Data For Each Grade Is Pushed Into This Array
  const finalData = [];
  // Loops Through Entire Inputted Array
  grade.forEach(a => {
    // This Must Be A Whole Number Or An Error May Occur If The Decimal Is In Between The Ranges Defined Below
    const e = Math.floor(a);
    // Defining All Necessary Variables
    let letterGrade;
    let gpaGrade;
    let gradeIcon;
    let gradeColor;
    let gradeType;
    // Conditionals To Find Appropriate Values Based On Grade
    if (e > 100) {
      letterGrade = "A++";
      gpaGrade = 4.0;
      gradeIcon = "/img/ico/star.png";
      gradeColor = "#0087FF";
      gradeType = "Honors";
    }
    if (e >= 97 && e <= 100) {
      letterGrade = "A+";
      gpaGrade = 4.0;
      gradeIcon = "/img/ico/star.png";
      gradeColor = "#0087FF";
      gradeType = "Honors";
    }
    if (e >= 93 && e <= 96) {
      letterGrade = "A";
      gpaGrade = 4.0;
      gradeIcon = "/img/ico/star.png";
      gradeColor = "#0087FF";
      gradeType = "Honors";
    }
    if (e >= 90 && e <= 92) {
      letterGrade = "A-";
      gpaGrade = 3.67;
      gradeIcon = "/img/ico/star.png";
      gradeColor = "#0087FF";
      gradeType = "Honors";
    }
    if (e >= 87 && e <= 89) {
      letterGrade = "B+";
      gpaGrade = 3.33;
      gradeIcon = "/img/ico/tick.png";
      gradeColor = "#1FBA24";
      gradeType = "Passing";
    }
    if (e >= 83 && e <= 86) {
      letterGrade = "B";
      gpaGrade = 3.00;
      gradeIcon = "/img/ico/tick.png";
      gradeColor = "#1FBA24";
      gradeType = "Passing";
    }
    if (e >= 80 && e <= 82) {
      letterGrade = "B-";
      gpaGrade = 2.67;
      gradeIcon = "/img/ico/tick.png";
      gradeColor = "#1FBA24";
      gradeType = "Passing";
    }
    if (e >= 77 && e <= 79) {
      letterGrade = "C+";
      gpaGrade = 2.33;
      gradeIcon = "/img/ico/error.png";
      gradeColor = "#AA9901";
      gradeType = "Borderline";
    }
    if (e >= 73 && e <= 76) {
      letterGrade = "C";
      gpaGrade = 2.00;
      gradeIcon = "/img/ico/error.png";
      gradeColor = "#AA9901";
      gradeType = "Borderline";
    }
    if (e >= 70 && e <= 72) {
      letterGrade = "C-";
      gpaGrade = 1.67;
      gradeIcon = "/img/ico/error.png";
      gradeColor = "#AA9901";
      gradeType = "Borderline";
    }
    if (e >= 67 && e <= 69) {
      letterGrade = "D+";
      gpaGrade = 1.33;
      gradeIcon = "/img/ico/error.png";
      gradeColor = "#AA9901";
      gradeType = "Borderline";
    }
    if (e >= 63 && e <= 66) {
      letterGrade = "D";
      gpaGrade = 1.00;
      gradeIcon = "/img/ico/exclamation.png";
      gradeColor = "#CF1920";
      gradeType = "Failing";
    }
    if (e >= 60 && e <= 62) {
      letterGrade = "D-";
      gpaGrade = 0.67;
      gradeIcon = "/img/ico/exclamation.png";
      gradeColor = "#CF1920";
      gradeType = "Failing";
    }
    if (e < 60) {
      letterGrade = "F";
      gpaGrade = 0.00;
      gradeIcon = "/img/ico/exclamation.png";
      gradeColor = "#CF1920";
      gradeType = "Failing";
    }
    // Combines All Data Per Grade Into An Object
    const gradeData = {
      "Number_Grade": a,
      "Letter_Grade": letterGrade,
      "GPA_Grade": gpaGrade,
      "Grade_Icon": gradeIcon,
      "Grade_Color": gradeColor,
      "Grade_Type": gradeType
    };
    // Pushes Object Into FinalData Array
    finalData.push(gradeData);
  });
  return finalData;
}

// Sets The Value For The Average Element
// Icon, Color, & Type Can Be Automatically Set
function setAverage(average, custom) {
  // Get Average Data By Passing Average As An Array Through ConvertGrades Function
  const gradeData = convertGrades([average]);
  // Define All Necessary Items From GradeData
  const gradeColor = gradeData[0].Grade_Color;
  const gradeType = gradeData[0].Grade_Type;
  const gradeIcon = gradeData[0].Grade_Icon;
  const letterGrade = gradeData[0].Letter_Grade;
  const gpaGrade = gradeData[0].GPA_Grade;
  // Get Default Grade View
  const gradeView = localStorage.getItem("DefaultGradeView");
  // Only Show Grade View Which Is Default
  let numView = "none";
  let letterView = "none";
  let gpaView = "none";
  if (gradeView === "number") {
    numView = "initial";
  }
  if (gradeView === "letter") {
    letterView = "initial";
  }
  if (gradeView === "gpa") {
    gpaView = "initial";
  }
  // If Custom Is True Create New Element For Custom Average
  if (custom === true) {
    // Remove Custom Average HTML If It Exists To Replace With New HTML
    if (document.getElementById("customAverageParent") !== null) {
      document.getElementById("customAverageParent").remove();
    }
    // Select Average Container
    const averageContainer = document.querySelector("span#averageContainer");
    // Hide Original Average Element
    document.getElementById("averageParent").style.display = "none";
    // Add In Custom Average HTML
    averageContainer.insertAdjacentHTML("beforeend", `
    <a id="customAverageParent" style="display: initial; color: #585b66; position: static; float: right; padding-top: 10px; padding-right: 10px; cursor: pointer">
      Total Average:
      <img id="customAverageIcon" src="${gradeIcon}" />
      <span style="display: ${numView}; color: ${gradeColor};" data-gtype="${gradeType}" class="numberGrade" id="customTotalAverage">${average}%</span>
      <span style="display: ${letterView}; color: ${gradeColor};" data-gtype="${gradeType}" class="letterGrade" id="customTotalAverage">${letterGrade}</span>
      <span style="display: ${gpaView}; color: ${gradeColor};" data-gtype="${gradeType}" class="gpaGrade" id="customTotalAverage">${gpaGrade}</span>
    </a>
    `);
  } else {
    // Set All New Data
    document.querySelectorAll("span#totalAverage").forEach(e => {
        e.style.color = gradeColor;
        e.dataset.gtype = gradeType;
      });
    document.getElementById("averageIcon").src = gradeIcon;
    document.querySelector("span#totalAverage.numberGrade").innerText = `${average}%`;
    document.querySelector("span#totalAverage.letterGrade").innerText = letterGrade;
    document.querySelector("span#totalAverage.gpaGrade").innerText = gpaGrade;
  }
}

// Function To Load Saved Grades
function loadSavedGrades() {
  // Parse The Saved Grades So It Can Be Read As An Array
  const savedGrades = JSON.parse(localStorage.getItem("CustomGrades"));
  // Selects All The Grade Container Elements
  const gradeSpan = document.querySelectorAll("table#progress-card > tbody > tr > td > span");
  // All Grades Custom Included
  let allGrades = [];
  for (let i = 0; i < savedGrades.length; i++) {
    if (savedGrades[i] !== null) {
      // Hide Original Grade
      gradeSpan[i].style.display = "none";
      // Get Data On Custom Grade
      const gradeData = convertGrades([savedGrades[i]]);
      // Define All Necessary Items From GradeData
      const customNumberGrade = gradeData[0].Number_Grade;
      const customLetterGrade = gradeData[0].Letter_Grade;
      const customGpaGrade = gradeData[0].GPA_Grade;
      const customGradeType = gradeData[0].Grade_Type;
      const customGradeIcon = gradeData[0].Grade_Icon;
      const customGradeColor = gradeData[0].Grade_Color;
      // Get Default Grade View
      const gradeView = localStorage.getItem("DefaultGradeView");
      // Only Show Grade View Which Is Default
      let numView = "none";
      let letterView = "none";
      let gpaView = "none";
      if (gradeView === "number") {
        numView = "initial";
      }
      if (gradeView === "letter") {
        letterView = "initial";
      }
      if (gradeView === "gpa") {
        gpaView = "initial";
      }
      // Add Custom Grade Span Below Original Grade Span
      // Set All Custom Grades HTML
      gradeSpan[i].insertAdjacentHTML("beforebegin", `
      <span class="customGrade" style="display: initial">
        <span class="numberGrade" style="display: ${numView}; color: ${customGradeColor};" data-numbergrade="${customNumberGrade}" data-lettergrade="${customLetterGrade}" data-gpagrade="${customGpaGrade}" data-gtype="${customGradeType}">
          <img src="${customGradeIcon}"> ${customNumberGrade}
        </span>
        <span class="letterGrade" style="display: ${letterView}; color: ${customGradeColor}" data-numbergrade="${customNumberGrade}" data-lettergrade="${customLetterGrade}" data-gpagrade="${customGpaGrade}" data-gtype="${customGradeType}">
          <img src="${customGradeIcon}"> ${customLetterGrade}
        </span>
        <span class="gpaGrade" style="display: ${gpaView}; color: ${customGradeColor}" data-numbergrade="${customNumberGrade}" data-lettergrade="${customLetterGrade}" data-gpagrade="${customGpaGrade}" data-gtype="${customGradeType}">
          <img src="${customGradeIcon}"> ${customGpaGrade}
        </span>
      </span>
      `);
    }
  }
  // Selects All Visible Grade Spans
  const visibleGradeSpans = document.querySelectorAll("table#progress-card > tbody > tr > td > span[style='display: initial'] > span.numberGrade");
  visibleGradeSpans.forEach(e => {
    const grade = parseFloat(e.dataset.numbergrade);
    allGrades.push(grade);
  });
  // Calculate Custom Average
  let customGradeAverage = calculateAverage(allGrades);
  // Round Custom Average
  customGradeAverage = Math.round(customGradeAverage * 100) / 100;
  // Set Custom Average
  setAverage(customGradeAverage, true);
}

// Add Menu Item To UserScript Manager
// Grade Changer Function
GM_registerMenuCommand("Change Grades", () => {
  // Only Run If Progress Card Exists
  if (document.getElementById("progress-card") !== null) {
    // NOTE FACEBOX IS A DEFAULT LIBRARY PUPILPATH USES
    // Setting Up Grade Changer Modal HTML
    jQuery.facebox(`
    <div id="gradeChanger">
      <h2 style="text-align: center;">Grade Changer</h2>
      <b>
        <h5 id="gradesNotChanged" class="customError" style="display: none; text-align: center; padding: 5px; background-color: #f08080; border-radius: 5px;">Error: You Have Not Made Any Changes!</h5>
        <h5 id="gradesTooHigh" class="customError" style="display: none; text-align: center; padding: 5px; background-color: #f08080; border-radius: 5px;">Error: Grades Must Be Between 0 - 110!</h5>
        <h5 id="changedGrades" class="customWarning" style="display: none; text-align: center; padding: 5px; background-color: #f0eb80; border-radius: 5px;">Warning: Grades Have Been Changed!</h5>
      </b>
      <form>
        <a class="btn btn-danger" id="resetButton" style="margin-right: 5px; margin-top: 5px;" title="Reset all grades back to original grades">Reset</a>
        <a class="btn btn-warning" id="clearButton" style="margin-right: 5px; margin-top: 5px;" title="Clear all grades from inputs">Clear</a>
        <a class="btn btn-success" id="setButton" style="margin-right: 50px; margin-top: 5px;" title="Set all custom grades">Set</a>
        <a class="btn btn-danger" id="saveButton" style="float: right; margin-top: 5px;" title="Save/Delete all custom grades (survives reload)">Save</a>
      </form>
    </div>
    `);
    // Set Save Button Name To Delete If Custom Grades Exist
    if (localStorage.getItem("CustomGrades") !== null) {
      document.getElementById("saveButton").innerText = "Delete";
    }
    // Gets All Class Name's And Grades Then Adds Them Into The Grade Changer Modal
    const gradeSpan = document.querySelectorAll("table#progress-card > tbody > tr > td > span.originalGrade > span.numberGrade");
    // Convert NodeList To Array To Reverse Order Of Elements So They Will Be In Correct Order When The HTML Is Inserted Below
    const reversedGradeSpan = Array.from(gradeSpan).reverse();
    // Adding The Class Name's And Grade Input HTML To The Grade Changer Modal
    reversedGradeSpan.forEach(e => {
      const origGrade = parseFloat(e.innerText);
      const className = e.parentElement.parentElement.parentElement.childNodes[1].innerText.toUpperCase();
      document.querySelector("div#gradeChanger > form").insertAdjacentHTML("afterbegin", `
      <span>
        <b>${className}</b>
        <input style="margin: 5px;" class="classGrades" type="number" step="0.01" data-originalgrade="${origGrade}" value="" min="0" max="110">
        <img class="isError" style="display: none;" src="/img/ico/exclamation.png" title="Grade Must Be Between 0-110"/>
        <img class="isEdit" style="display: none;" src="/img/ico/pencil.png" title="This Grade Has Been Changed"/>
      </span>
      <br />
      `);
    });
    // Selects All Inputs
    const inputs = document.querySelectorAll("div#gradeChanger > form > span > input");
    // Select All Visible Grades
    const visibleGrades = document.querySelectorAll("table#progress-card > tbody > tr > td > span[style='display: initial'] > span.numberGrade");
    for (let i = 0; i < inputs.length; i++) {
      // Visible Grade
      const grade = visibleGrades[i].dataset.numbergrade;
      // Changed Grade Warning
      const changedGrades = document.getElementById("changedGrades");
      // If Grade Was Changed Do This
      if (grade != inputs[i].dataset.originalgrade) {
        // Show Edited Icon If Grade Was Changed
        inputs[i].parentElement.lastElementChild.style.display = "initial";
        // Show Grade Changed Warning
        changedGrades.style.display = "block";
      }
      // Sets The Current Visible Grade To Fill Input
      inputs[i].value = grade;
    }
    // Add CSS To Document HEAD To Remove Arrows From Number Input
    document.getElementsByTagName("head")[0].insertAdjacentHTML("beforeend", `
    <style>
      input[type='number'] {
        -moz-appearance:textfield;
      }
      input::-webkit-outer-spin-button,
      input::-webkit-inner-spin-button {
        -webkit-appearance: none;
      }
    </style>
    `);
    // This Is To Remove The Default 600px Width On FaceBox Element
    document.getElementById("gradeChanger").parentElement.parentElement.style = "width: auto";

    // Button Functions For Grade Changer

    // Reset Button
    document.getElementById("resetButton").addEventListener("click", () => {
        // Define Necessary Variables
        const inputs = document.querySelectorAll("div#gradeChanger > form > span > input");
        const customGradeSpan = document.querySelectorAll("span.customGrade");
        const originalGradeSpan = document.querySelectorAll("span.originalGrade");
        const changedGrades = document.getElementById("changedGrades");
        const customAverage = document.getElementById("customAverageParent");
        const originalAverage = document.getElementById("averageParent");
        // Remove Custom Average Element If It Exists
        if (customAverage !== null) {
          customAverage.remove();
        }
        // Show Original Average Element
        originalAverage.style.display = "initial";
        // Hide Grade Changed Warning
        changedGrades.style.display = "none";
        // Remove All Changed Grades Spans
        customGradeSpan.forEach(e => {
          e.remove();
        });
        // Show All Original Grades
        originalGradeSpan.forEach(e => {
          e.style.display = "initial";
        });
        // Set Each Input To Its Original Value
        inputs.forEach(e => {
          const origGrade = e.dataset.originalgrade;
          e.value = origGrade;
          // Hide Edited Icon
          e.parentElement.lastElementChild.style.display = "none";
          // Hide Error Icon
          e.parentElement.children[2].style.display = "none";
        });
    });

    // Clear Button
    document.getElementById("clearButton").addEventListener("click", () => {
        const inputs = document.querySelectorAll("div#gradeChanger > form > span> input");
        inputs.forEach(e => {
          // Set Each Input To Nothing
          e.value = "";
          // Hide Edited Icon
          e.parentElement.lastElementChild.style.display = "none";
          // Hide Error Icon
          e.parentElement.children[2].style.display = "none";
        });
    });

    // Set Button
    document.getElementById("setButton").addEventListener("click", () => {
      // Selects All The Grade Container Elements
      const gradeSpan = document.querySelectorAll("table#progress-card > tbody > tr > td > span.originalGrade > span.numberGrade");
      // All The Original Grades Are Pushed Into This Array
      const allOriginalGrades = [];
      // All Grade Changer Inputs
      const inputs = document.querySelectorAll("div#gradeChanger > form > span> input");
      // All Custom Grades Are Pushed Into This Array
      const customGrades = [];
      // Push All Original Grades Into Above Array
      gradeSpan.forEach(e => {
        // Make Sure To Use ParseFloat NOT ParseInt B/C ParseFloat Leaves Decimals Intact
        allOriginalGrades.push(parseFloat(e.innerText));
      });
      // Get All Custom Grades From Inputs And Push To Array
      inputs.forEach(e => {
        const grade = parseFloat(e.value);
        // Only Push Numbers To Array
        if (isNaN(grade) === false) {
          customGrades.push(grade);
        } else {
          customGrades.push(null);
        }
      });
      // Only Continue If The Custom Grades Are Different Than The Original Grades
      if (compareArrays(allOriginalGrades, customGrades) === false) {
        // Check If Every Value Of CustomGrades Is Between 0 - 110 Using isValidGrades Function
        if (customGrades.every(isValidGrades) === true) {
          // All Of The Final Custom Grades Will Be Pushed Into This
          const finalCustomGrades = [];
          // Loop Through The Original & Custom Grades
          for (let i = 0; i < allOriginalGrades.length; i++) {
            let grade;
            // If The Grades Are The Same Or Null Push Null
            if (allOriginalGrades[i] === customGrades[i] || customGrades[i] === null) {
              grade = null;
            }
            // If Grades Are Different Push The Custom Grade
            if (allOriginalGrades[i] !== customGrades[i]) {
              grade = customGrades[i];
            }
            // Push Grades To Array
            finalCustomGrades.push(grade);
          }
          // Loop Through The Final Custom Grades
          for (let i = 0; i < finalCustomGrades.length; i++) {
            // If The Value Is Null Hide Edited Icon
            // Else Show The Edited Icon
            if (finalCustomGrades[i] !== null) {
              inputs[i].parentElement.lastElementChild.style.display = "initial";
            } else {
              inputs[i].parentElement.lastElementChild.style.display = "none";
            }
            // Hide Error Icon
            inputs[i].parentElement.children[2].style.display = "none";
          }
          // Selects All Original Grade Container Spans
          const originalGradeSpan = document.querySelectorAll("span.originalGrade");
          // Loop Through All Custom Grade Spans
          for (let i = 0; i < finalCustomGrades.length; i++) {
            const firstGradeChild = originalGradeSpan[i].parentElement.firstElementChild;
            // Only Run On Classes With A Custom Grade
            if (finalCustomGrades[i] !== null) {
              const changedGrades = document.getElementById("changedGrades");
              // Show Grade Changed Warning
              changedGrades.style.display = "block";
              // Remove Custom Grade Span If It Exists
              if (firstGradeChild.className === "customGrade") {
                firstGradeChild.remove();
              }
              // Hide Original Grade
              firstGradeChild.style.display = "none";
              // Get Data On Custom Grade
              const gradeData = convertGrades([finalCustomGrades[i]]);
              // Define All Necessary Items From GradeData
              const customNumberGrade = gradeData[0].Number_Grade;
              const customLetterGrade = gradeData[0].Letter_Grade;
              const customGpaGrade = gradeData[0].GPA_Grade;
              const customGradeType = gradeData[0].Grade_Type;
              const customGradeIcon = gradeData[0].Grade_Icon;
              const customGradeColor = gradeData[0].Grade_Color;
              // Get Default Grade View
              const gradeView = localStorage.getItem("DefaultGradeView");
              // Only Show Grade View Which Is Default
              let numView = "none";
              let letterView = "none";
              let gpaView = "none";
              if (gradeView === "number") {
                numView = "initial";
              }
              if (gradeView === "letter") {
                letterView = "initial";
              }
              if (gradeView === "gpa") {
                gpaView = "initial";
              }
              // Add Custom Grade Span Below Original Grade Span
              // Set All Custom Grades HTML
              originalGradeSpan[i].insertAdjacentHTML("beforebegin", `
                <span class="customGrade" style="display: initial">
                  <span class="numberGrade" style="display: ${numView}; color: ${customGradeColor};" data-numbergrade="${customNumberGrade}" data-lettergrade="${customLetterGrade}" data-gpagrade="${customGpaGrade}" data-gtype="${customGradeType}">
                    <img src="${customGradeIcon}"> ${customNumberGrade}
                  </span>
                  <span class="letterGrade" style="display: ${letterView}; color: ${customGradeColor}" data-numbergrade="${customNumberGrade}" data-lettergrade="${customLetterGrade}" data-gpagrade="${customGpaGrade}" data-gtype="${customGradeType}">
                    <img src="${customGradeIcon}"> ${customLetterGrade}
                  </span>
                  <span class="gpaGrade" style="display: ${gpaView}; color: ${customGradeColor}" data-numbergrade="${customNumberGrade}" data-lettergrade="${customLetterGrade}" data-gpagrade="${customGpaGrade}" data-gtype="${customGradeType}">
                    <img src="${customGradeIcon}"> ${customGpaGrade}
                  </span>
                </span>
                `);
              // Select All Visible Grades
              const allVisibleGradesSpans = document.querySelectorAll("span[style='display: initial'] > span.numberGrade");
              // Array To Hold All Visible Grades
              const allVisibleGrades = [];
              allVisibleGradesSpans.forEach(e => {
                const grade = parseFloat(e.innerText);
                // Push All Visible Grades To Array
                allVisibleGrades.push(grade);
              });
              let customGradeAverage = calculateAverage(allVisibleGrades);
              customGradeAverage = Math.round(customGradeAverage * 100) / 100;
              setAverage(customGradeAverage, true);
            }
          }
        } else {
          inputs.forEach(e => {
            if (e.value > 110 || e.value < 0) {
              e.parentElement.children[2].style.display = "initial";
            }
          });
          // Select Grades Too High Error
          const gradesTooHigh = document.getElementById("gradesTooHigh");
          // Show Error
          gradesTooHigh.style.display = "block";
          // Hide Error After 5 Seconds
          setTimeout(() => {
            gradesTooHigh.style.display = "none";
          }, 5000);
        }
      } else {
        // Selects Changed Grades Warning
        const changedGrades = document.getElementById("changedGrades");
        // Hide Grade Changed Warning
        changedGrades.style.display = "none";
        // Selects All Grade Edited Icons
        const editedGradeIcon = document.querySelectorAll("img.isEdit");
        // Hides All Grade Edited Icons
        editedGradeIcon.forEach(e => {
          e.style.display = "none";
        });
        // Selects Grades Not Changed Error
        const gradesNotChanged = document.getElementById("gradesNotChanged");
        // Show Error
        gradesNotChanged.style.display = "block";
        // Hide Error After 5 Seconds
        setTimeout(() => {
          gradesNotChanged.style.display = "none";
        }, 5000);
      }
    });

    // Save Button
    document.getElementById("saveButton").addEventListener("click", () => {
      // Selects All The Grade Container Elements
      const gradeSpan = document.querySelectorAll("table#progress-card > tbody > tr > td > span.originalGrade > span.numberGrade");
      // All The Original Grades Are Pushed Into This Array
      const allOriginalGrades = [];
      // Push All Original Grades Into Above Array
      gradeSpan.forEach(e => {
        // Make Sure To Use ParseFloat NOT ParseInt B/C ParseFloat Leaves Decimals Intact
        allOriginalGrades.push(parseFloat(e.innerText));
      });
      // Do Correct Action Based On Button Name
      if (document.getElementById("saveButton").innerText === "Save") {
        // Change Button Name After Click
        document.getElementById("saveButton").innerText = "Delete";
        // Selects All Grade Changer Inputs
        const inputs = document.querySelectorAll("div#gradeChanger > form > span> input");
        // All Custom Grades Will Be Pushed Into This
        const customGrades = [];
        // Put All Custom Grades In An Array
        inputs.forEach(e => {
          customGrades.push(parseFloat(e.value));
        });
        // Do This If Grades Have Changed
        if (compareArrays(allOriginalGrades, customGrades) === false) {
          // All Final Custom Grades Will Be Pushed Into This
          const finalCustomGrades = [];
          // Loop Over All Grades
          for (let i = 0; i < allOriginalGrades.length; i++) {
            let grade;
            // If Grade Is Not Changed Push Null
            if (allOriginalGrades[i] === customGrades[i]) {
              grade = null;
            }
            // If Grade Is Different Push Custom Grade
            if (allOriginalGrades[i] !== customGrades[i]) {
              grade = customGrades[i];
            }
            // Push Grades To Array
            finalCustomGrades.push(grade);
          }
          // NOTE THE ARRAY MUST BE A STRING TO SAVE TO LOCALSTORAGE THUS THE ARRAY IS CONVERTED TO A STRING WITH JSON.STRINGIFY
          localStorage.setItem("CustomGrades", JSON.stringify(finalCustomGrades));
        } else {
          // Select Grades Not Changed Error
          const gradesNotChanged = document.getElementById("gradesNotChanged");
          // Show Error
          gradesNotChanged.style.display = "block";
          // Hide Error After 5 Seconds
          setTimeout(() => {
            gradesNotChanged.style.display = "none";
          }, 5000);
          // Change Button Name Back To "Save" B/C No Grades Have Changed
          document.getElementById("saveButton").innerText = "Save";
        }
      } else if (document.getElementById("saveButton").innerText === "Delete") {
        // Remove Custom Grades
        localStorage.removeItem("CustomGrades");
        // Change Button Name To "Save" After Click
        document.getElementById("saveButton").innerText = "Save";
      }
    });
  }
});

// Only Run If The Progress Card Exists
if (document.getElementById("progress-card") !== null) {
  // Creating The Average Container HTML
  document.getElementsByClassName("notification information")[0].insertAdjacentHTML("afterbegin", `
  <span id="averageContainer" title="Click For More Info">
    <a id="averageParent" style="display: initial; color: #585b66; position: static; float: right; padding-top: 10px; padding-right: 10px; cursor: pointer">
      Total Average:
      <img id="averageIcon" />
      <span style="display: initial;" class="numberGrade" id="totalAverage"></span>
      <span style="display: none;" class="letterGrade" id="totalAverage"></span>
      <span style="display: none;" class="gpaGrade" id="totalAverage"></span>
    </a>
  </span>
  `);
  // Selects All The Grade Container Elements
  const gradeSpan = document.querySelectorAll("table#progress-card > tbody > tr > td > span");
  // All The Original Grades Are Pushed Into This Array
  const allOriginalGrades = [];
  // Push All Original Grades Into Above Array
  gradeSpan.forEach(e => {
    // Make Sure To Use ParseFloat NOT ParseInt B/C ParseFloat Leaves Decimals Intact
    allOriginalGrades.push(parseFloat(e.innerText));
  });
  // Returns The Average Of All The Original Grades, In Some Cases As A Float
  const rawAverage = calculateAverage(allOriginalGrades);
  // Rounds The RawAverage In Case It Is A Float To The Hundredths Place (2 After Decimal)
  // NOTE DO NOT USE ParseFloat WITH ToFixed AS IT WILL LEAVE TRAILING ZEROS IF A WHOLE NUMBER IS PASSED
  const average = Math.round(rawAverage * 100) / 100;
  // Call SetAverage Function To Set The Average, With Automatic Data (Colors, Icons, etc..)
  setAverage(average);
  // Create Alternate Elements For Each Class Grade With Different Grading Standards
  gradeSpan.forEach(e => {
    // Original Grade
    const grade = parseFloat(e.innerText);
    // Get Data On Original Grade
    const gradeData = convertGrades([grade]);
    // Define All Necessary Items From GradeData
    const gradeColor = gradeData[0].Grade_Color;
    const numGrade = gradeData[0].Number_Grade;
    const letterGrade = gradeData[0].Letter_Grade;
    const gpaGrade = gradeData[0].GPA_Grade;
    const gradeType = gradeData[0].Grade_Type;
    const gradeIcon = gradeData[0].Grade_Icon;
    // Create The HTML For The Alternate Elements With Correct Data
    const numberGradeHTML = `
    <span class="numberGrade" style="color: ${gradeColor}; display: initial;" data-numbergrade="${numGrade}" data-lettergrade="${letterGrade}" data-gpagrade="${gpaGrade}" data-gtype="${gradeType}">
    <img src="${gradeIcon}"> ${numGrade}</span>
    `;
    const letterGradeHTML = `
    <span class="letterGrade" style="display: none; color: ${gradeColor}" data-numbergrade="${numGrade}" data-lettergrade="${letterGrade}" data-gpagrade="${gpaGrade}" data-gtype="${gradeType}">
    <img src="${gradeIcon}"> ${letterGrade}</span>
    `;
    const gpaGradeHTML = `
    <span class="gpaGrade" style="display: none; color: ${gradeColor}" data-numbergrade="${numGrade}" data-lettergrade="${letterGrade}" data-gpagrade="${gpaGrade}" data-gtype="${gradeType}">
    <img src="${gradeIcon}"> ${gpaGrade}</span>
    `;
    // Append The HTML Before The Original Grade Element
    e.insertAdjacentHTML("beforebegin", `<span style="display: initial" class="originalGrade">${numberGradeHTML}${letterGradeHTML}${gpaGradeHTML}</span>`);
    // Set The Original Grade HTML To Nothing To Remove It
    e.outerHTML = ``;
  });
  // If Custom Grades Are Avaliable Run LoadSavedGrades Function
  if (localStorage.getItem("CustomGrades") !== null) {
    loadSavedGrades();
  }
  // Set Default Grade View If It Does Not Exist
  if (localStorage.getItem("DefaultGradeView") === null) {
    localStorage.setItem("DefaultGradeView", "number");
  }
  // Default Grade View
  const gradeView = localStorage.getItem("DefaultGradeView");
  // Change Current View To Default Grade View
  setGradeView(gradeView);

  // Detailed Average Info On Average Container Click
  document.getElementById("averageContainer").addEventListener("click", () => {
    // Select The Average Anchor Element
    const averageAnchor = document.querySelectorAll("span#averageContainer > a");
    // The Current Average
    let visibleAverage = [];
    averageAnchor.forEach(e => {
      // If Element Is Visible Get Average Value
      if (e.style.display == "initial") {
        const average = parseFloat(e.childNodes[3].innerText.replace(/[^0-9.]/g, ""));
        // Push Average To Array
        visibleAverage.push(average);
      }
    });
    // Get Data On Visible Average
    const gradeData = convertGrades(visibleAverage);
    // Define All Necessary Items From GradeData
    const numGrade = gradeData[0].Number_Grade;
    const letterGrade = gradeData[0].Letter_Grade;
    const gpaGrade = gradeData[0].GPA_Grade;
    // NOTE FACEBOX IS A DEFAULT LIBRARY PUPILPATH USES
    // Setting Up Average Detail's Modal
    jQuery.facebox(`
      <div id="averageInfo">
        <h2>Average Conversion</h2>
        <h4>Percent Grade: ${numGrade}%</h4>
        <h4>Letter Grade: ${letterGrade}</h4>
        <h4>GPA: ${gpaGrade}</h4>
        <form>
          <label for="numberView" style="display: initial;" title="Show all grades as numbers">
            <input id="numberView" type="radio" name="gradeType" value="number">
            Number Grades
          </label>
          <label for="letterView" style="display: initial;" title="Show all grades as letters">
            <input id="letterView" type="radio" name="gradeType" value="letter">
            Letter Grades
          </label>
          <label for="gpaView" style="display: initial;"  title="Show all grades as gpa's">
            <input id="gpaView" type="radio" name="gradeType" value="gpa">
            GPA Grades
          </label>
        </form>
      </div>
      `);
    // Selects Default Grade View LocalStorage Item
    const gradeView = localStorage.getItem("DefaultGradeView");
    // Select All Necessary Elements Through Variables
    const numberViewRadio = document.getElementById("numberView");
    const letterViewRadio = document.getElementById("letterView");
    const gpaViewRadio = document.getElementById("gpaView");
    // Enable The Radio Button With The Currently Active Grade View
    if (gradeView === "number") {
      numberViewRadio.checked = true;
    }
    if (gradeView === "letter") {
      letterViewRadio.checked = true;
    }
    if (gradeView === "gpa") {
      gpaViewRadio.checked = true;
    }
    // This Is To Remove The Default 600px Width On FaceBox Element
    document.getElementById("averageInfo").parentElement.parentElement.style = "width: auto";

    // Change Grade View Type On Click
    document.querySelector("div#averageInfo > form").addEventListener("click", () => {
      // Select Form Element
      const form = document.querySelector("div#averageInfo > form");
      // Get User Selection From Form
      const data = new FormData(form);
      let choice;
      // Loop Through Form Data To Find User Choice
      for (const entry of data) {
        choice = entry[1];
      }
      setGradeView(choice);
    });
  });
}