`;
this.ecamDash.insertBefore(updateAvailableNotif, document.querySelector("#dash-header"));
setTimeout(() => {updateAvailableNotif.classList.add("on")}, 300);
updateAvailableNotif.querySelector(".dismiss-update-btn").onclick = () => {
updateAvailableNotif.classList.remove("on");
setTimeout(() => {updateAvailableNotif.remove()}, 300)
this.dateTimeOfLastUpdateCheck = this.today;
this.saveDateTimeOfLastUpdateCheck();
};
updateAvailableNotif.querySelector(".update-btn").onclick = () => {
this.dateTimeOfLastUpdateCheck = this.now();
this.saveDateTimeOfLastUpdateCheck();
this.appendFullScreenNotif();
};
}
//#endregion
//#region ____________ — Render — ______________
//#region -Main Dashboard generation
// MARK: createDashboard
createDashboard() {
this.ecamDash.className = "ecam-dash";
if (this.error) {
this.ecamDash.style.width = "94%";
this.ecamDash.style.margin = "40px 3% 30px";
}
const moyenneGenerale = this.moyennePonderee(this.grades);
const totalGrades = this.grades.length;
const moduleStats = this.getModuleStats();
document.querySelector(".site-breadcrumbs")?.remove();
document.querySelector(".portlet-topper")?.remove();
// Creating the content of the dashboard that doesn't vary along with the user's actions besides the language selection.
// Therefore, besides the text that doesn't vary with the language, the text isn't yet created,
// but will be in the generateContent() method later on, to regenerate the text in case the language is changed
this.ecamDash.innerHTML = `
${this.error ? `
`;
const notifContainer = document.createElement("div");
notifContainer.className = "selected-card-notif-container";
const intranetFold = document.querySelector(".intranet-fold");
if (intranetFold) {
intranetFold.parentNode.insertBefore(this.ecamDash, intranetFold);
}
else {
Object.values(document.body.children).forEach(child => {child.remove()});
document.body.appendChild(this.ecamDash);
}
this.ecamDash.insertBefore(notifContainer, this.ecamDash.querySelector("#dash-header"));
// Create the new grades notification and its associated new grades table if at least one new grade is detected
this.createNewGradesNotifDiv();
if (this.error) {
setTimeout(() => {
const offlineTitle = document.querySelector(".offline-mode-title");
const offlineSubTitle = document.querySelector(".offline-mode-subtitle");
offlineTitle.classList.add("show");
offlineSubTitle.classList.add("show");
}, 1)
}
this.generateContent({manageIndividualSubjectCardFolding: false, fadeIn:false});
}
//MARK: language Sensitive
async languageSensitiveContent(fadeIn=true) {
// Language Sensitive text in the Dashboard Header and Semester filter tab (which don't refresh on calling the generateContent() method)
if (this.error) {
const offlineTitle = document.querySelector(".offline-mode-title");
const offlineSubTitle = document.querySelector(".offline-mode-subtitle");
offlineSubTitle.innerHTML = this.lang == "fr"
? `Les serveurs de l'ECAM sont actuellement inaccessibles ! ${!this.firstLoad
? `Pour l'instant, tu ne peux pas voir si tu as des nouvelles notes... En attendant, voici le tableau de bord en mode offline, tu as donc accès aux notes que j'ai gentiment sauvegardées dans le cache la dernière fois ! De rien ! <3`
: "Pour l'instant, tu ne peux pas voir tes notes... Tu peux quand même commencer à configurer tes modules, et reviens quand les serveurs sont de nouveau opérationnels pour voir tes notes !"
}`
: `ECAM's servers are currently down! ${!this.firstLoad
? `For now, you can't see if you have new grades... While waiting for the servers to be back up, here are the grades I nicely saved in your cache last time, so you can still see them even with the server down! You're welcome! <3`
: "For now, you can't see your grades... You can still start by configuring your modules, and come back once the servers are up again to see your grades!"
}`
;
}
const dashTitle = document.querySelector(".dash-title-text");
const dashPatchNotesLink = document.querySelector(".patch-notes-link");
const dashSubtitle = document.querySelector(".dash-subtitle");
dashTitle.innerHTML = this.lang == "fr" ? 'Tableau de Bord des Notes ECAM' : "ECAM Grades Dashboard";
dashPatchNotesLink.title = this.lang == "fr" ? "Aller voir les notes de cette mise à jour" : "Go see this update's notes";
dashSubtitle.innerHTML = this.lang == "fr" ? 'Vue complète de vos résultats académiques !' : "Complete view of your academic results!";
const infoNotif = document.querySelector(".temp-notif");
if (infoNotif) {
infoNotif.classList.toggle("fr");
infoNotif.classList.toggle("en");
}
const frBtn = document.querySelectorAll("#fr-lang-btn");
const enBtn = document.querySelectorAll("#en-lang-btn");
frBtn.title = this.lang == "fr" ? "Maj+L" : "Shift+L";
enBtn.title = this.lang == "fr" ? "Maj+L" : "Shift+L";
const reportIssueBtn = document.querySelector(".issue.issue-btn");
const mailInfo = document.querySelector(".issue.mail-info");
const mailInfoText = document.querySelector(".over-header-btn-mail-info-text");
const mailInfoCopied = document.querySelector(".over-header-btn-copied-cue");
const shareConfig = document.querySelector(".issue.share-config");
const suggestIdea = document.querySelector(".issue.suggest-idea");
const reportIssue = document.querySelector(".issue.report-issue");
const helpMenu = document.querySelector(".over-header-btn.how-to-use-btn");
const docBtn = document.querySelector(".over-header-btn.doc-btn");
// const tutoBtn = document.querySelector(".over-header-btn.tuto-btn");
const keybindsBtn = document.querySelector(".over-header-btn.keybinds-btn");
const settingsBtn = document.querySelector(".over-header-btn.settings-btn");
const newUserNotif = document.querySelector(".new-user-notif");
const newUserNotifText = document.querySelector(".new-user-notif-text");
if (this.lang == "fr") {
reportIssueBtn .title = "Signaler...";
mailInfo .title = "Clique pour copier mon adresse email !";
shareConfig .title = "Partage une configuration sur mon GitHub";
suggestIdea .title = "Suggère une idée sur mon GitHub";
reportIssue .title = "Signale un problème sur mon GitHub";
helpMenu .title = "Comment s'en servir?";
docBtn .title = "Aller vers la documentation";
// tutoBtn .title = "Démarrer le tutoriel";
keybindsBtn .title = "Voir les raccourcis clavier";
settingsBtn .title = "Ouvrir les paramètres";
if (((newUserNotif?.style?.display || "none" == "none")?.toString() || "true") == "false") newUserNotif.title = "Clique pour fermer";
shareConfig .innerHTML = `Partager une config ${this.createExternalLinkSymbol({margin: [0,0,0,4]})}`;
suggestIdea .innerHTML = `Suggérer une idée ${this.createExternalLinkSymbol({margin: [0,0,0,4]})}`;
reportIssue .innerHTML = `Signaler un problème ${this.createExternalLinkSymbol({margin: [0,0,0,4]})}`;
mailInfoText .innerHTML = "Par mail: baptiste.jacquin@ecam.fr 📋";
mailInfoCopied .innerHTML = "Copié !";
if (((newUserNotif?.style?.display || "none" == "none")?.toString() || "true") == "false") newUserNotifText.innerHTML = "Bonjour! Nouveau ici? Clique ici pour apprendre à utiliser cette extension!";
mailInfo .classList.replace("en", "fr");
shareConfig.classList.replace("en", "fr");
suggestIdea.classList.replace("en", "fr");
reportIssue.classList.replace("en", "fr");
docBtn .classList.replace("en", "fr");
// tutoBtn .classList.replace("en", "fr");
keybindsBtn.classList.replace("en", "fr");
}
else {
reportIssueBtn .title = "Report...";
mailInfo .title = "Click to copy my email adress!";
shareConfig .title = "Share a configuration on my GitHub";
suggestIdea .title = "Suggest an idea on my GitHub";
reportIssue .title = "Report an issue on my GitHub";
helpMenu .title = "How to use?";
docBtn .title = "Go to the documentation";
// tutoBtn .title = "Start the tutorial";
keybindsBtn .title = "See the keyboard shortcuts";
settingsBtn .title = "Open the settings";
if (((newUserNotif?.style?.display || "none" == "none")?.toString() || "true") == "false") newUserNotif.title = "Click to dismiss";
shareConfig .innerHTML = `Share a config ${this.createExternalLinkSymbol({margin: [0,0,0,4]})}`;
suggestIdea .innerHTML = `Suggest an idea ${this.createExternalLinkSymbol({margin: [0,0,0,4]})}`;
reportIssue .innerHTML = `Report an issue ${this.createExternalLinkSymbol({margin: [0,0,0,4]})}`;
mailInfoText .innerHTML = "By mail: baptiste.jacquin@ecam.fr 📋";
mailInfoCopied .innerHTML = "Copied!";
if (((newUserNotif?.style?.display || "none" == "none")?.toString() || "true") == "false") newUserNotifText.innerHTML = "Hey! New here? Click here to find a tutorial on how to use this extension!";
mailInfo .classList.replace("fr", "en");
shareConfig.classList.replace("fr", "en");
suggestIdea.classList.replace("fr", "en");
reportIssue.classList.replace("fr", "en");
docBtn .classList.replace("fr", "en");
// tutoBtn .classList.replace("fr", "en");
keybindsBtn.classList.replace("fr", "en");
}
const keybindsTableModalBody = document.querySelector("#keyboardShortcutListModalBody");
if (keybindsTableModalBody) { this.appendKeyboardShortcutsList(keybindsTableModalBody); }
const settingsModal = document.querySelector("#settingsModal");
const settingsModalBody = document.querySelector("#settingsModalBody");
if (settingsModal) { if (settingsModalBody) {settingsModalBody.remove()} this.appendSettingsModalBody(); }
const importBtn = document.getElementById("importBtn");
const editModeBtn = document.getElementById("editModeBtn");
const exportBtn = document.getElementById("exportBtn");
importBtn .innerHTML = `${this.lang == "fr" ? "Importer Config": "Import Config"}⬇️`;
editModeBtn .innerHTML = `
`;
return html
}
// MARK: Create Subject Card
/**
* Call this method to create the outer HTML of all subject cards of a module.
*
* Detects automatically from the name of the moduleName and from `this.gradesDatas` (as a safe guard, also from `this.moduleConfig`) if the card is classified or unclassified,
* and detects automatically from this.compactSubjCardsId if the card is detailed or compact.
*
* @param {number | string} sem Number of the semester of the subject
* @param {string} moduleName Name of the subject's module
* @return {string} The outer HTML of all the subject cards of a module, in a single string
*/
createAllSubjCards(sem, moduleName, manageIndividualSubjectCardFolding=true) {
const moduleData = this.gradesDatas[sem][moduleName];
let html = this.editMode && moduleName != "__#unclassified#__" && this.moduleConfig[sem]?.[moduleName] != undefined
? this.createDropFieldInsertionField("subject", {sem, moduleName, index:0})
: ""
;
Object.values(moduleData.subjects).forEach((_value, _index) => {
html += this.createSubjCard(sem, moduleName, _value.subjName, _index, manageIndividualSubjectCardFolding);
html += this.editMode && moduleName != "__#unclassified#__" && this.moduleConfig[sem]?.[moduleName] != undefined
? this.createDropFieldInsertionField("subject", {sem, moduleName, index:_index+1})
: ""
;
})
return html;
}
/**
* Call this method to create the outer HTML of a subject card.
*
* Detects automatically from the name of the moduleName and from `this.gradesDatas` (to error-proof the moduleName, also from `this.moduleConfig`) if the card is classified or unclassified,
* and detects automatically from this.compactSubjCardsId if the card is detailed or compact.
*
* @param {number | string} sem Number of the semester of the subject
* @param {string} moduleName Name of the subject's module
* @param {string} subject Name of the subject
* @param {number} [index=-1] Default: -1 — Index of the subject in its module, necessary if the subject is classified, useless if the subject is unclassified
* @return {string} The outer HTML of the subject card
*/
createSubjCard(sem, moduleName, subject, index=-1, manageIndividualSubjectCardFolding=true) {
const moduleData = this.gradesDatas[sem][moduleName];
const subjectData = moduleData.subjects[subject];
const subjectGrades = subjectData.grades;
const moduleMoy = moduleData.average;
const subjAvg = subjectData?.average >= 0 ? subjectData.average : " - ";
const pct = subjectData.coef;
const isCustom = subjectData.isCustom;
const nbGrades = subjectGrades.length;
const includedGradesLength = nbGrades - subjectData.disabledRealGrades.length - subjectData.disabledSimGrades.length;
const nbSimGrades = subjectData.simGrades.length;
const nbRealGrades = nbGrades - nbSimGrades;
const classified = moduleName != "__#unclassified#__" && this.moduleConfig[sem]?.[moduleName] != undefined;
const subjectCardId = `subject-card-semester-${sem}-subject-${subject}`;
const detailed = (this.detailedSubjCardsId.includes(subjectCardId) && manageIndividualSubjectCardFolding) || (this.viewMode == "detailed" && !manageIndividualSubjectCardFolding);
const cardIsSelected = this.selectedSubjectCardsId.includes(`subject-card-semester-${sem}-subject-${subject}`);
const cardClientHeight = 166 + 40*nbGrades;
let html = `
${
this.lang == "fr"
? "Bonjour! Nouveau ici? Clique ici pour apprendre à utiliser cette extension!"
: "Hey! New here? Click here to find a tutorial on how to use this extension!"
}
`;
this.ecamDash.appendChild(keybindsMenu);
this.appendCloseModalIcon(document.querySelector("#keyboardShortcutListModal"))
this.appendKeyboardShortcutsList(keybindsMenu.querySelector("#keyboardShortcutListModalBody"));
setTimeout(() => {keybindsMenu.querySelector("#keyboardShortcutListModal").classList.add("show");}, 5);
keybindsMenu.onmousedown = (e) => {
if (e.target.closest(".modal-close-btn") || !e.target.closest("#keyboardShortcutListModal")) {
keybindsMenu.onmouseup = (e) => {
if (e.target.closest(".modal-close-btn") || !e.target.closest("#keyboardShortcutListModal")) {
this.closeKeybindsModal()
}
keybindsMenu.onmouseup = null;
}
}
}
}
closeKeybindsModal() {
const keybindsMenu = document.querySelector("#keyboardShortcutListContainer");
if (keybindsMenu) {
keybindsMenu.querySelector(".keyboard-shortcut-list-modal").classList.remove("show");
setTimeout(() => {keybindsMenu.remove()}, 300);
}
}
openSettingsModal(closeOtherModals=true) {
if (closeOtherModals) { this.closeEveryModal() }
const settingsModalContainer = document.createElement("div");
settingsModalContainer.className = "settings-modal-container";
settingsModalContainer.id = "settingsModalContainer";
settingsModalContainer.innerHTML = ``;
this.ecamDash.appendChild(settingsModalContainer);
const settingsModal = settingsModalContainer.querySelector("#settingsModal");
this.appendCloseModalIcon(settingsModal)
this.appendSettingsModalBody();
setTimeout(() => {settingsModal.classList.add("show")}, 5);
this.attachSettingsModalContainerListeners();
}
closeSettingsModal() {
const settingsModal = document.querySelector("#settingsModal");
if (settingsModal) {
settingsModal.classList.remove("show");
setTimeout(() => {settingsModal.parentElement.remove()}, 300);
}
}
//#endregion
//#region Semester
semesterHeaderMouseUpNoMoveAction() {
document.body.onmousemove = (e) => {
e.preventDefault();
document.body.onmouseup = null;
document.body.onmousemove = null;
}
document.body.onmouseup = (e) => {
const header = e.target.closest('.semester-header');
const sem = header.dataset.semester;
const content = document.getElementById(`sem-content-${sem}`);
const toggle = header.querySelector('.semester-toggle');
if (content.classList.contains('show')) {
content.classList.remove('show'); toggle.classList.remove('open'); content.style.display = 'none';
} else {
content.classList.add('show'); toggle.classList.add('open'); content.style.display = 'flex';
}
if (document.body.clientHeight > document.body.offsetHeight) {
// Semester has been collapsed, and now the page is tinier than the window, and i want to avoid the slider to offset the page. Only useful in Offline mode
this.ecamDash.style.paddingRight = "10px";
}
else {
this.ecamDash.style.paddingRight = "";
}
document.body.onmouseup = null;
document.body.onmousemove = null;
}
}
releaseUnclassifiedSection() {
const unclassifiedSection = document.querySelector(".unclassified-section");
unclassifiedSection.style.height = "";
}
//#endregion
//#region Module
// MARK: - module header mouse action
/** Method temporarily attaching an onmousemove and an onmouseup event listener to the document's body.
*
* Meant to be invoked when the mouse down event is triggered if the target is or is contained in a module header.
*
* In practice, when the onmousedown event of the document is triggered on a module card, call this method to:
* - attach an onmousemove event listener to the document's body that will clear the onmousemove and onmouseup events of the document's body in order to "cancel" the action (safe guard for when the edit mode is off and the user attempts to drag the module header, it will not do anything instead of triggering an onclick event)
* - attach an onmouseup event listener to the document's body that will make the action intended to happen when the user clicks on the module header (folding the module card) WITHOUT moving the mouse (so if it wasn't an attempt to drag the module header). Both the onmousemove and onmouseup event listeners of the document's body will then be cleared.
*/
moduleHeaderMouseUpNoMoveAction() {
document.body.onmousemove = (e) => {
e.preventDefault();
document.body.onmouseup = null;
document.body.onmousemove = null;
};
document.body.onmouseup = (e) => {
const moduleCard = e.target.closest('.module-card');
const moduleDetails = moduleCard.querySelector(".module-details");
// moduleDetails.querySelectorAll(".subject-card").forEach(subjCard => {
// if (this.selectedSubjectCardsId.includes(subjCard.id)) {
// this.changeDragIconToTickIcon(subjCard);
// }
// })
this.toggleFoldModuleCard(moduleCard);
this.attachDropFieldsEventListeners("insert", moduleDetails);
document.body.onmousemove = null;
document.body.onmouseup = null;
}
}
// MARK: -toggle module card folding
/** Call this method to switch all Module cards' state between folded and unfolded
*
* @param {HTMLElement | Event} trigger The trigger of the folding action. Can be a module card HTML Element or an event triggered by a module card
* @param {Boolean} [hideOtherSubjectInsertionFields = false] Default: false — Destined to control whether all the subject insertion fields of all the other modules are to be hidden (if true) or not (if false)
* @param {Boolean} [hideAdjacentModuleInsertionFields = false] Default: false — Destined to control whether the upper and lower module insertion fields are to be hidden (if true) or not (if false). Makes this method ONLY hide the said insertion fields if its value is "only"
* @param {Boolean} [bypassFoldedModuleCardsId = false] Default: false — Destined to control whether the folded module card ID's addition to/deletion from this.foldedModuleCardsId will be bypassed (if true) or not (if false)
*/
toggleFoldAllModuleCards(hideOtherSubjectInsertionFields=false, hideAdjacentModuleInsertionFields=false, bypassFoldedModuleCardsId=false) {
document.querySelectorAll(".module-card").forEach(moduleCard => {
this.toggleFoldModuleCard(moduleCard, hideOtherSubjectInsertionFields, hideAdjacentModuleInsertionFields, bypassFoldedModuleCardsId)
})
}
/** Call this method to switch a module card's state between folded and unfolded
*
* @param {HTMLElement | Event} trigger The trigger of the folding action. Can be a module card HTML Element or an event triggered by a module card
* @param {Boolean} [hideOtherSubjectInsertionFields = false] Default: false — Destined to control whether all the subject insertion fields of all the other modules are to be hidden (if true) or not (if false)
* @param {Boolean} [hideAdjacentModuleInsertionFields = false] Default: false — Destined to control whether the upper and lower module insertion fields are to be hidden (if true) or not (if false). Makes this method ONLY hide the said insertion fields if its value is "only"
* @param {Boolean} [bypassFoldedModuleCardsId = false] Default: false — Destined to control whether the folded module card ID's addition to/deletion from this.foldedModuleCardsId will be bypassed (if true) or not (if false)
*/
toggleFoldModuleCard(trigger, hideOtherSubjectInsertionFields=false, hideAdjacentModuleInsertionFields=false, bypassFoldedModuleCardsId=false) {
if (trigger?.classList?.contains("module-card") || (trigger?.target?.classList?.contains("module-card"))) {
const moduleCard = trigger?.target || trigger;
if (moduleCard.classList.contains("fold")) {
this.unfoldModuleCard(moduleCard, hideOtherSubjectInsertionFields, hideAdjacentModuleInsertionFields, bypassFoldedModuleCardsId)
}
else {
this.foldModuleCard(moduleCard, hideOtherSubjectInsertionFields, hideAdjacentModuleInsertionFields, bypassFoldedModuleCardsId)
}
}
}
// MARK: -fold module card
/** Call this method to fold all module cards
*
* @param {HTMLElement | Event} trigger The trigger of the folding action. Can be a module card HTML Element or an event triggered by a module card
* @param {Boolean} [hideOtherSubjectInsertionFields = false] Default: false — Destined to control whether all the subject insertion fields of all the other modules are to be hidden (if true) or not (if false)
* @param {Boolean} [hideAdjacentModuleInsertionFields = false] Default: false — Destined to control whether the upper and lower module insertion fields are to be hidden (if true) or not (if false). Makes this method ONLY hide the said insertion fields if its value is "only"
* @param {Boolean} [bypassFoldedModuleCardsId = false] Default: false — Destined to control whether the folded module card ID's addition to this.foldedModuleCardsId will be bypassed (if true) or not (if false)
*/
foldAllModuleCards(hideOtherSubjectInsertionFields=false, hideAdjacentModuleInsertionFields=false, bypassFoldedModuleCardsId=false) {
document.querySelectorAll(".module-card").forEach(moduleCard => {
this.foldModuleCard(moduleCard, hideOtherSubjectInsertionFields, hideAdjacentModuleInsertionFields, bypassFoldedModuleCardsId)
})
}
/** Call this method to fold a module card
*
* @param {HTMLElement | Event} trigger The trigger of the folding action. Can be a module card HTML Element or an event triggered by a module card
* @param {Boolean} [hideOtherSubjectInsertionFields = false] Default: false — Destined to control whether all the subject insertion fields of all the other modules are to be hidden (if true) or not (if false)
* @param {Boolean} [hideAdjacentModuleInsertionFields = false] Default: false — Destined to control whether the upper and lower module insertion fields are to be hidden (if true) or not (if false). Makes this method ONLY hide the said insertion fields if its value is "only"
* @param {Boolean} [bypassFoldedModuleCardsId = false] Default: false — Destined to control whether the folded module card ID's addition to this.foldedModuleCardsId will be bypassed (if true) or not (if false)
*/
async foldModuleCard(trigger, hideOtherSubjectInsertionFields=false, hideAdjacentModuleInsertionFields=false, bypassFoldedModuleCardsId=false) {
// testing if the trigger argument is an HTML of class module-card or an Event triggered by a module card or one of its elements
if (trigger?.classList?.contains("module-card") || (trigger?.target?.classList?.contains("module-card"))) {
// Identifying the moduleCard depending on whether the trigger argument is a module card or an event triggered by a module card
const moduleCard = trigger?.target || trigger;
const moduleHeader = moduleCard.querySelector(".module-header");
const toggle = moduleCard.querySelector('.module-toggle');
const sem = moduleCard.dataset.semester;
const module = moduleCard.dataset.module;
const index = moduleCard.dataset.index;
if (hideAdjacentModuleInsertionFields != "only") {
if (!bypassFoldedModuleCardsId) {
this.foldedModuleCardsId.push(moduleCard.id);
}
toggle.classList.remove("open");
const subjectInsertFields = document.querySelectorAll(`.drop-field.insert-field.subject[data-semester="${sem}"]${hideOtherSubjectInsertionFields ? "" : `[data-module="${module}"]`}`)
const subjectInsertFieldHitboxes = Object.values(subjectInsertFields).map(elem => {return elem.querySelector(".drop-subject-card-insert-hitbox")});
subjectInsertFieldHitboxes.forEach(subjInsFieldHitbox => {
this.detachInsertFieldHitboxEventListeners(subjInsFieldHitbox);
})
moduleCard.style.height = Array.from(moduleCard.children).reduce((total, child) => {return parseInt(total?.offsetHeight || total) + parseInt(child.offsetHeight)}) + "px";
moduleHeader.classList.add("fold");
moduleCard.classList.add("fold");
setTimeout(() => {
moduleCard.style.height = "77px";
}, 1)
}
let upperInsertField = "";
let lowerInsertField = "";
if (hideAdjacentModuleInsertionFields) {
upperInsertField = document.querySelector(`.drop-field.insert-field.module[data-semester="${sem}"][data-index="${parseInt(index)+0}"]`)
const upperInsertFieldHitbox = upperInsertField.querySelector(".drop-module-card-insert-hitbox");
lowerInsertField = document.querySelector(`.drop-field.insert-field.module[data-semester="${sem}"][data-index="${parseInt(index)+1}"]`)
const lowerInsertFieldHitbox = lowerInsertField.querySelector(".drop-module-card-insert-hitbox");
this.detachInsertFieldHitboxEventListeners(upperInsertFieldHitbox);
this.detachInsertFieldHitboxEventListeners(lowerInsertFieldHitbox);
upperInsertField.classList.remove("show");
lowerInsertField.classList.remove("show");
}
clearTimeout(this.timeouts.upperInsertFieldUnfoldTimeout);
clearTimeout(this.timeouts.lowerInsertFieldUnfoldTimeout);
clearTimeout(this.timeouts.subjectInsertFieldUnfoldTimeout);
clearTimeout(this.timeouts.subjectCardsUnfoldTimeout);
clearTimeout(this.timeouts.moduleCardElemsUnfoldTimeout);
clearTimeout(this.timeouts.moduleCardUnfoldTimeout);
}
}
// MARK: -unfold module card
/** Call this method to unfold all module cards
*
* @param {HTMLElement | Event} trigger The trigger of the folding action. Can be a module card HTML Element or an event triggered by a module card
* @param {Boolean} [hideOtherSubjectInsertionFields = false] Default: false — Destined to control whether all the subject insertion fields of all the other modules are to be shown (if true) or not (if false)
* @param {Boolean} [hideAdjacentModuleInsertionFields = false] Default: false — Destined to control whether the upper and lower module insertion fields are to be hidden (if true) or not (if false). Makes this method ONLY hide the said insertion fields if its value is "only"
* @param {Boolean} [bypassFoldedModuleCardsId = false] Default: false — Destined to control whether the unfolded module card ID's deletion from this.foldedModuleCardsId will be bypassed (if true) or not (if false)
*/
unfoldAllModuleCards(hideOtherSubjectInsertionFields=false, hideAdjacentModuleInsertionFields=false, bypassFoldedModuleCardsId=false) {
document.querySelectorAll(".module-card").forEach(moduleCard => {
this.unfoldModuleCard(moduleCard, hideOtherSubjectInsertionFields, hideAdjacentModuleInsertionFields, bypassFoldedModuleCardsId)
})
}
/** Call this method to unfold a module card
*
* @param {HTMLElement | Event} trigger The trigger of the folding action. Can be a module card HTML Element or an event triggered by a module card
* @param {Boolean} [hideOtherSubjectInsertionFields = false] Default: false — Destined to control whether all the subject insertion fields of all the other modules are to be shown (if true) or not (if false)
* @param {Boolean} [hideAdjacentModuleInsertionFields = false] Default: false — Destined to control whether the upper and lower module insertion fields are to be hidden (if true) or not (if false). Makes this method ONLY hide the said insertion fields if its value is "only"
* @param {Boolean} [bypassFoldedModuleCardsId = false] Default: false — Destined to control whether the unfolded module card ID's deletion from this.foldedModuleCardsId will be bypassed (if true) or not (if false)
*/
async unfoldModuleCard(trigger, hideOtherSubjectInsertionFields=false, hideAdjacentModuleInsertionFields=false, bypassFoldedModuleCardsId=false) {
// testing if the trigger argument is an HTML of class module-card or an Event triggered by a module card or one of its elements
if (trigger?.classList?.contains("module-card") || (trigger?.target?.classList?.contains("module-card"))) {
// Identifying the moduleCard depending on whether the trigger argument is a module card or an event triggered by a module card
const moduleCard = trigger?.target || trigger;
const moduleHeader = moduleCard.querySelector(".module-header");
const toggle = moduleCard.querySelector('.module-toggle');
const sem = moduleCard.dataset.semester;
const module = moduleCard.dataset.module;
const index = moduleCard.dataset.index;
clearTimeout(this.timeouts.foldModuleCardTimeout);
if (hideAdjacentModuleInsertionFields) {
const upperInsertField = document.querySelector( `.drop-field.insert-field.module[data-semester="${sem}"][data-index="${parseInt(index)+0}"]`);
const lowerInsertField = document.querySelector( `.drop-field.insert-field.module[data-semester="${sem}"][data-index="${parseInt(index)+1}"]`);
if (upperInsertField) {
upperInsertField.classList.add("show");
const upperInsertFieldHitbox = upperInsertField.querySelector(".drop-module-card-insert-hitbox");
this.attachInsertFieldHitboxEventListeners(upperInsertFieldHitbox)
}
if (lowerInsertField) {
lowerInsertField.classList.add("show");
const lowerInsertFieldHitbox = lowerInsertField.querySelector(".drop-module-card-insert-hitbox");
this.attachInsertFieldHitboxEventListeners(lowerInsertFieldHitbox)
}
}
if (hideAdjacentModuleInsertionFields != "only") {
toggle.classList.add("open");
const subjectInsertFields = document.querySelectorAll(`.drop-field.insert-field.subject[data-semester="${sem}"]${hideOtherSubjectInsertionFields ? `[data-module="${module}"]` : ""}`);
if (subjectInsertFields.length > 0) {
this.timeouts.subjectInsertFieldUnfoldTimeout = setTimeout(() => {
const subjectInsertFieldHitboxes = Object.values(subjectInsertFields).map(elem => {return elem.querySelector(".drop-subject-card-insert-hitbox")});
subjectInsertFieldHitboxes.forEach(subjInsFieldHitbox => {this.attachInsertFieldHitboxEventListeners(subjInsFieldHitbox)})
}, 1)
}
if (moduleHeader) {
setTimeout(() => {
moduleHeader.classList.remove("fold");
}, 1)
}
if (moduleCard) {
setTimeout(() => {
moduleCard.style.height = Array.from(moduleCard.children).reduce((total, child) => {return parseInt(total?.offsetHeight || total) + parseInt(child.offsetHeight)}) + "px";
moduleCard.classList.remove("fold");
this.timeouts.moduleCardUnfoldTimeout = setTimeout(() => {moduleCard.style.height = ""}, 300)
}, 1)
}
if (!bypassFoldedModuleCardsId) {
setTimeout(() => {
this.foldedModuleCardsId.splice(this.foldedModuleCardsId.indexOf(moduleCard.id), 1);
}, 2)
}
}
}
}
ensureAllModuleCardsFoldingState(container=document.body) {
if (container instanceof HTMLElement || container instanceof HTMLDocument) {
container.querySelectorAll(".module-card").forEach(moduleCard => {
this.ensureModuleCardFoldingState(moduleCard);
})
}
}
ensureModuleCardFoldingState(moduleCard) {
if (moduleCard?.classList?.contains(".module-card")) {
if (this.foldedModuleCardsId.contains(moduleCard)) {
this.foldModuleCard(moduleCard);
}
else {
this.unfoldModuleCard(moduleCard);
}
}
}
moduleTitleInputChangeAction(target) {
const sem = target.dataset.semester;
const newModuleName = target.value;
const oldModuleName = target.dataset.module;
const oldModuleIndex = this.moduleConfig[sem].__modules__.indexOf(oldModuleName);
let diffName = true;
if (newModuleName == "__#unclassified#__") {
alert(this.lang == "fr"
? "Ce nom n'est pas autorisé ! C'est le nom utilisé en interne pour les matières non-classifiées... Choisis-en un autre!"
: "This name isn't allowed! That's the name used internally for unclassified subjects... Choose another one!"
)
return;
}
else {
Object.keys(this.moduleConfig[sem]).forEach(_moduleName => {
if (_moduleName == newModuleName && _moduleName != oldModuleName) {
alert(this.lang == "fr"
? "Cette matière existe déjà! Choisis un autre nom, s'il te plait"
: "This subject already exists! Please choose a different name"
)
diffName = false;
this.scrollToClientHighestElem({id: target.id, smooth: true, block: "center"})
target.focus();
target.style.background = "#ff7979";
}
});
}
if (diffName) {
this.moduleConfig[sem][newModuleName] = this.moduleConfig[sem][oldModuleName];
delete this.moduleConfig[sem][oldModuleName];
this.moduleConfig[sem].__modules__[oldModuleIndex] = newModuleName;
this.saveConfig()
this.getGradesDatas();
this.generateContent({fadeIn: false});
this.foldedModuleCardsId.forEach(foldedModuleCardId => {
if (foldedModuleCardId == `module-card-${oldModuleName}-in-semester-${sem}`) {
const moduleCardToFold = document.getElementById(foldedModuleCardId);
if (!moduleCardToFold) {
const newModuleCardToFold = document.getElementById(`module-card-${newModuleName}-in-semester-${sem}`);
this.foldModuleCard(newModuleCardToFold.querySelector(`.module-header`));
}
}
})
this.attachOnDragEventListeners();
this.scrollToClientHighestElem({id: `module-card-${newModuleName}-in-semester-${sem}`, smooth: true})
}
}
moduleCardDeleteBtnAction(e) {
const moduleCard = e instanceof Event ? e.target.closest(".module-card") : (e instanceof HTMLElement ? e : undefined);
let sem = moduleCard.dataset.semester;
let moduleName = moduleCard.dataset.module;
if (this.selectedModuleCardsId.includes(moduleCard.id)) {
this.selectedModuleCardsId.forEach(selectedModuleCardId => {
const selectedModuleCard = document.getElementById(selectedModuleCardId);
sem = selectedModuleCard.dataset.semester;
moduleName = selectedModuleCard.dataset.module;
const moduleIndex = this.moduleConfig[sem].__modules__.indexOf(moduleName);
this.moduleConfig[sem].__modules__.splice(moduleIndex, 1);
delete this.moduleConfig[sem][moduleName];
if (this.moduleConfig[sem].__modules__.length == 0) {delete this.moduleConfig[sem]}
this.removeCardFromSelection()
})
}
else {
const moduleIndex = this.moduleConfig[sem].__modules__.indexOf(moduleName);
this.moduleConfig[sem].__modules__.splice(moduleIndex, 1);
delete this.moduleConfig[sem][moduleName];
if (this.moduleConfig[sem].__modules__.length == 0) {delete this.moduleConfig[sem]}
}
this.clearSimGrades(sem, moduleName);
this.saveConfig();
this.getGradesDatas();
this.generateContent({fadeIn: true});
}
//#endregion
//#region Subject
/** Method temporarily attaching an onmousemove and an onmouseup event listener to the document's body.
*
* Meant to be invoked when the mouse down event is triggered if the target is or is contained in a card header.
*
* In practice, when the onmousedown event of the document is triggered on a card header, call this method to:
* - attach an onmousemove event listener to the document's body that will clear the onmousemove and onmouseup events of the document's body in order to "cancel" the action (safe guard for when the edit mode is off and the user attempts to drag the card header, it will not do anything instead of triggering an onclick event)
* - attach an onmouseup event listener to the document's body that will make the action intended to happen when the user clicks on the card header (switching the card card between detailed and comapct view modes) WITHOUT moving the mouse (so if it wasn't an attempt to drag the card header). Both the onmousemove and onmouseup event listeners of the document's body will then be cleared.
*/
subjectHeaderMouseUpNoMoveAction() {
document.body.onmousemove = (e) => {
e.preventDefault();
document.body.onmouseup = null;
document.body.onmousemove = null;
};
document.body.onmouseup = (e) => {
const subjCard = e.target.closest('.subject-card');
const unclassifiedSection = document.querySelector(".unclassified-section");
if (subjCard) {
if (subjCard.classList.contains("unclassified")) {
this.releaseElementHeight(unclassifiedSection);
}
this.toggleFoldSubjCard(subjCard);
this.setGradesTableTotalCoef(subjCard);
this.attachAllSubjectCardRelatedEventListeners(subjCard);
if (subjCard.classList.contains("unclassified")) {
this.holdElementHeight(unclassifiedSection, 100, {offset: 4});
}
}
document.body.onmousemove = null;
document.body.onmouseup = null;
}
}
/** Toggle the folding of all the subject cards inside the given container
* @param {HTMLElement} [container=document.body] The HTML element containing the subject cards whose fold mode will be toggled
* @param {boolean} [smart=true] If true, takes into consideration the current view mode to know if the subject card should be folded of unfolded. If false, simply toggle the folding mode.
*/
async toggleFoldAllSubjCards(container=document.body, smart=true) {
if (container instanceof HTMLElement) {
if (smart) {
if (this.viewMode == "detailed") {
this.unfoldAllSubjCards(container);
}
else if (this.viewMode == "compact") {
this.foldAllSubjCards(container);
}
}
else {
container.querySelectorAll(".subject-card").forEach(subjCard => {
if (subjCard?.classList?.contains("detailed")) {
this.foldSubjCard(subjCard);
}
else if (subjCard?.classList?.contains("compact")) {
this.unfoldSubjCard(subjCard);
}
})
}
}
}
/** Toggle the folding of the given subject card
* @param {HTMLElement} subjCard The subject cards whose fold mode will be toggled
*/
async toggleFoldSubjCard(subjCard) {
if (subjCard?.classList?.contains("detailed")) {
this.foldSubjCard(subjCard);
}
else if (subjCard?.classList?.contains("compact")) {
this.unfoldSubjCard(subjCard);
}
}
// MARK: fold subject card
/** Fold all the subject cards inside the given container
* @param {HTMLElement} [container=document.body] The HTML element containing the subject cards to fold
*/
async foldAllSubjCards(container=document.body) {
if (container instanceof HTMLElement) {
container.querySelectorAll(".subject-card.detailed").forEach(detailedSubjCard => {
this.foldSubjCard(detailedSubjCard);
})
}
}
/** Fold the given subject card
* @param {HTMLElement} [subjCard] The subject card to fold
*/
async foldSubjCard(subjCard) {
if (subjCard?.classList?.contains("subject-card")) {
const subjCardHeader = subjCard.querySelector(".subject-card-header");
subjCardHeader.classList.add("fold");
subjCardHeader.classList.replace("detailed", "compact");
subjCard.style.height = "";
// prepare the aimed height below the higher instance height style
subjCard.classList.replace("detailed", "compact");
subjCard.querySelector(".subject-card-header-grades-details").classList.add("show");
this.compactSubjCardsId.push(subjCard.id);
this.detailedSubjCardsId.splice(this.detailedSubjCardsId.indexOf(subjCard.id), 1);
}
}
// MARK: unfold subject card
/** Unfold all the subject cards inside the given container
* @param {HTMLElement} [container=document.body] The HTML element containing the subject cards to unfold
*/
async unfoldAllSubjCards(container=document.body) {
if (container instanceof HTMLElement) {
container.querySelectorAll(".subject-card.compact").forEach(compactSubjCard => {
this.unfoldSubjCard(compactSubjCard);
})
}
}
/** Unfold the given subject card
* @param {HTMLElement} [subjCard] The subject card to unfold
*/
async unfoldSubjCard(subjCard) {
if (subjCard?.classList?.contains("subject-card")) {
const subjCardHeader = subjCard.querySelector(".subject-card-header");
subjCardHeader.classList.remove("fold");
subjCardHeader.classList.replace("compact", "detailed");
subjCard.style.height = subjCard.dataset.height+"px";
// makes the height go automatic -> correspond to the desired height
subjCard.classList.replace("compact", "detailed");
subjCard.querySelector(".subject-card-header-grades-details").classList.remove("show");
this.compactSubjCardsId.splice(this.compactSubjCardsId.indexOf(subjCard.id), 1);
this.detailedSubjCardsId.push(subjCard.id);
}
}
subjectCardDeleteBtnAction(target) {
const subjectCard = document.getElementById(target.dataset.targetid);
if (this.selectedSubjectCardsId.includes(subjectCard.id)) {
this.selectedSubjectCardsId.forEach(selectedSubjCardId => {
const selectedSubjectCard = document.getElementById(selectedSubjCardId);
const sem = selectedSubjectCard.dataset.semester;
const moduleName = selectedSubjectCard.dataset.module;
const subject = selectedSubjectCard.dataset.subject;
const semData = this.moduleConfig[sem];
const moduleData = semData[moduleName];
const moduleIndex = semData.__modules__.indexOf(moduleName);
const subjectIndex = moduleData.subjects.indexOf(subject);
moduleData.subjects.splice(subjectIndex, 1);
delete moduleData.coefficients[subject];
if (moduleData.subjects.length == 0) {semData.__modules__.splice(moduleIndex, 1); delete semData[moduleName];}
if (semData.__modules__.length == 0) {delete this.moduleConfig[sem]}
this.removeCardFromSelection()
this.clearSimGrades(sem, moduleName, subject);
})
}
else {
const sem = target.dataset.semester;
const moduleName = target.dataset.module;
const subject = target.dataset.subject;
const semData = this.moduleConfig[sem];
const moduleData = semData[moduleName];
const moduleIndex = semData.__modules__.indexOf(moduleName);
const subjectIndex = moduleData.subjects.indexOf(subject);
moduleData.subjects.splice(subjectIndex, 1);
delete moduleData.coefficients[subject];
if (moduleData.subjects.length == 0) {semData.__modules__.splice(moduleIndex, 1); delete semData[moduleName];}
if (semData.__modules__.length == 0) {delete this.moduleConfig[sem]}
this.clearSimGrades(sem, moduleName, subject);
}
this.saveConfig();
this.getGradesDatas();
this.generateContent({fadeIn: true});
}
subjectCardNameInputAction(target) {
const subjNewName = target.value;
const subjectCardId = target.id.replace(/\bsubject-name-input/, "subject-card");
const subjectCard = document.getElementById(subjectCardId);
const sem = subjectCard.dataset.semester;
const moduleName = subjectCard.dataset.module;
const subjOldName = subjectCard.dataset.subject;
const moduleDetails = subjectCard.parentElement;
const moduleCard = moduleDetails.parentElement;
let diffName = true;
if (subjNewName == "__#unclassified#__") {
alert(this.lang == "fr"
? "Ce nom n'est pas autorisé ! C'est le nom utilisé en interne pour les matières non-classifiées... Choisis-en un autre!"
: "This name isn't allowed! That's the name used internally for unclassified subjects... Choose another one!"
)
return;
}
else {
this.moduleConfig[sem][moduleName].subjects.forEach(_subj => {
if (_subj == subjNewName && _subj != subjOldName) {
alert(this.lang == "fr"
? "Cette matière existe déjà! Choisis un autre nom, s'il te plait"
: "This subject already exists! Please choose a different name"
)
diffName = false;
this.scrollToClientHighestElem({id: target.id, smooth: true, block: "center"})
target.focus();
target.style.background = "#ff7979";
return;
}
});
}
if (diffName) {
this.moduleConfig[sem].__modules__.forEach(moduleName => {
this.moduleConfig[sem][moduleName].subjects.forEach(_subj => {
if (_subj == subjNewName && _subj != subjOldName) {
alert(this.lang == "fr"
? "Cette matière existe déjà! Choisis un autre nom, s'il te plait"
: "This subject already exists! Please choose a different name"
)
diffName = false;
this.scrollToClientHighestElem({id: subjectCardId, smooth: true, block: "center"})
}
})
})
const oldSubjIndex = this.moduleConfig[sem][moduleName].subjects.indexOf(subjOldName);
const pct = Number(this.moduleConfig[sem][moduleName].coefficients [subjOldName]);
this.moduleConfig[sem][moduleName].subjects[oldSubjIndex]=subjNewName ; // Replace the subject's old name by the subject's new name
delete this.moduleConfig[sem][moduleName].coefficients [subjOldName];
this.moduleConfig[sem][moduleName].coefficients [subjNewName] = pct;
this.getGradesDatas();
moduleDetails.innerHTML = this.createSubjCard(sem, moduleName, subjNewName);
const unclassifiedSection = document.querySelector(".unclassified-section");
const unclassifiedContent = unclassifiedSection.querySelector(".unclassified-content");
unclassifiedSection.style.height = "";
unclassifiedContent.innerHTML = this.createAllSubjCards(sem, "__#unclassified#__");
this.resetFixedUnclassifiedSectionHeight();
this.attachAllSubjectCardRelatedEvenListenersForEverySubjectCard();
this.setGradesTableTotalCoef();
this.saveConfig()
this.getGradesDatas();
}
}
subjectCardSimAddBtnAction(target) {
const moduleName = target.dataset.module;
const semX = target.dataset.semester;
const subj = target.dataset.subj;
this.ensureSimPath(semX, moduleName, subj);
const typeInp = document.querySelector(`.simulated-grade-input.sim-inp-type[data-semester="${semX}"][data-subj="${subj}"]`);
const gradeInp = document.querySelector(`.simulated-grade-input.sim-inp-grade[data-semester="${semX}"][data-subj="${subj}"]`);
const coefInp = document.querySelector(`.simulated-grade-input.sim-inp-coef[data-semester="${semX}"][data-subj="${subj}"]`);
const dateInp = document.querySelector(`.simulated-grade-input.sim-inp-date[data-semester="${semX}"][data-subj="${subj}"]`);
const type = typeInp?.value||`${this.lang=="fr"? 'Simulé' : "Simulated"}`;
const grade = parseFloat(gradeInp?.value||'');
const coef = parseFloat(coefInp?.value||'');
const date = dateInp?.value||'';
if(isNaN(grade) || isNaN(coef)){ alert(this.lang == "fr" ? "Grade et coef requis" : "Grade and coef required"); return; }
this.ensureSimPath(semX, moduleName, subj);
// Making sure the automatically generated name (if the user didn't input any type name) isn't the same as one that already exists
// (incrementing an index every time it's the case and add it at the end of the new sim grade's name)
let newName = type, validNewName = newName != type, count = 2;
while (!validNewName && this.sim[semX][moduleName][subj].length > 0) {
validNewName = true;
this.sim[semX][moduleName][subj].forEach((_grade, _index) => {
if (_grade.type == newName && validNewName) {
validNewName = false;
newName = type + ` (${count})`;
count++;
}
})
}
this.sim[semX][moduleName][subj].push({
grade,
coef,
classAvg: '—',
type: newName,
date: '—',
prof: '—',
subject: subj,
semester: semX,
libelle: `[SIM] ${subj} - ${type}`,
__sim: true,
id: new Date().getYear() + "" + new Date().getMonth() + "" + new Date().getDay() + "" + new Date().getHours() + "" + new Date().getMinutes() + "" + new Date().getSeconds() + "" + new Date().getMilliseconds()
});
this.saveSim();
this.getGradesDatas();
this.generateContent({fadeIn: true});
}
subjectCardSimDelBtnAction(target) {
const semX = target.dataset.semester;
const moduleName = target.dataset.module;
const subj = target.dataset.subj;
const id = target.dataset.simid;
this.sim[semX][moduleName][subj].splice(id, 1);
this.deleteUnusedSimPath(false, semX, moduleName, subj);
this.saveSim();
this.getGradesDatas();
this.generateContent({fadeIn: false});
}
subjectCardSimInputEditAction(target) {
const moduleName = target.dataset.module;
const semX = target.dataset.semester;
const subj = target.dataset.subj;
const id = target.dataset.simid;
const gradeRow = target.parentElement.parentElement;
const gradeInp = gradeRow.querySelector(`.simulated-grade-input-edit.sim-inp-grade`);
const coefInp = gradeRow.querySelector(`.simulated-grade-input-edit.sim-inp-coef `);
const newGrade = parseFloat(gradeInp?.value||'');
const newCoef = parseFloat(coefInp?.value||'');
if(isNaN(newGrade) || isNaN(newCoef)){ alert(this.lang == "fr" ? "Grade et coef requis" : "Grade and coef required"); return; }
this.sim[semX][moduleName][subj][id][target.dataset.modiftype] = target.value;
this.saveSim();
this.getGradesDatas();
this.regenAveragesAndTotalCoefs(semX, moduleName, subj);
}
//#endregion
//#region Drag/Tick icon
changeDragIconToTickIcon(card) {
const dragIcon = card.querySelector(".drag-icon");
if (dragIcon) {
const type = dragIcon.classList.contains("subject") ? "subject" : "module";
dragIcon.outerHTML = `
✔
`;
this.attachDragOrTickIconListener(card);
}
}
changeTickIconToDragIcon(card) {
const tickIcon = card.querySelector(".tick-icon");
if (tickIcon) {
const type = tickIcon.classList.contains("subject") ? "subject" : "module";
tickIcon.outerHTML = this.createDraggableIcon(type, {targetId: card.id});
this.attachDragOrTickIconListener(card);
}
}
switchBetweenDragAndTickIcon(card) {
const dragIcon = card.querySelector(".drag-icon");
if (dragIcon) {
this.changeDragIconToTickIcon(card);
}
else {
this.changeTickIconToDragIcon(card);
}
}
// MARK: dragIconOnClickEvent
dragIconOnClickEvent(e, dontAddToSelection=false) {
const card = e?.target instanceof HTMLElement ? document.getElementById(e.target.dataset.targetid) : (e instanceof HTMLElement ? e : undefined);
const type = card.classList.contains("subject-card") ? "subject" : "module";
const dropFieldAdd = document.querySelector(".drop-field.create-module");
const dropFieldAddHitbox = document.querySelector(".drop-field-create-module-hitbox");
const dropFieldRemove = document.querySelector(".drop-field.remove-from-module");
const dropFieldRemoveHitbox = document.querySelector(".drop-field-remove-from-module-hitbox");
const sem = card.dataset.semester;
const moduleName = card.dataset.module;
const subject = card.dataset.subject;
card.draggable = true;
if (!dontAddToSelection) {
if ((type == "subject" && this.selectedModuleCardsId.length > 0) || (type == "module" && this.selectedSubjectCardsId.length > 0)) {
const oldInfoNotif = document.querySelector(".temp-notif");
if (oldInfoNotif) {
clearTimeout(this?.timeouts?.dragIconOnClickEvent?.hideInfoNotif);
clearTimeout(this?.timeouts?.dragIconOnClickEvent?.removeInfoNotif);
oldInfoNotif.remove();
}
const infoNotif = document.createElement("div");
infoNotif.className = `temp-notif ${this.lang}`;
this.ecamDash.appendChild(infoNotif);
setTimeout(() => {infoNotif.classList.add("show")}, 1);
if (!this?.timeouts?.dragIconOnClickEvent) {this.timeouts.dragIconOnClickEvent = {}}
this.timeouts.dragIconOnClickEvent.hideInfoNotif = setTimeout(() => {
infoNotif.classList.remove("show");
this.timeouts.dragIconOnClickEvent.removeInfoNotif = setTimeout(() => {oldInfoNotif.remove()}, 1000);
}, 4000);
card.classList.add("slight-horiz-shake");
card.onanimationend = (e) => {e.target.classList.remove("slight-horiz-shake");};
return
}
else if (type == "subject") {
this.selectedSubjectCardsId.push(card.id);
if (!this.selectedSubjectCardsSortedByModule[card.dataset.module]) { this.selectedSubjectCardsSortedByModule[card.dataset.module] = []; };
this.selectedSubjectCardsSortedByModule[card.dataset.module].push({cardId: card.id, selectionIndex: this.selectedSubjectCardsId.length-1});
}
else if (type == "module") {
this.selectedModuleCardsId.push(card.id);
}
const selectionNotifDiv = this.createSelectedCardNotifDiv(card);
document.querySelector(".selected-card-notif-container").appendChild(selectionNotifDiv);
this.attachNotifBtnsListener(selectionNotifDiv);
setTimeout(()=>{selectionNotifDiv.classList.add("on")}, 10)
// Ensure the subject insertion drop fields are showing the right text
document.querySelectorAll(".drop-field.insert-field").forEach(subjInsertField => {
if (this.selectedSubjectCardsId.length > 0 || this.selectedModuleCardsId.length > 0) {
subjInsertField.querySelector(".drop-module-card-insert-plus , .drop-subject-card-insert-plus ").classList.remove("show");
subjInsertField.querySelector(".drop-module-card-insert-arrow, .drop-subject-card-insert-arrow").classList.add("show");
subjInsertField.querySelector(".drop-module-card-insert-text, .drop-subject-card-insert-text").classList.replace("add", "insert");
subjInsertField.querySelector(".drop-module-card-insert-text, .drop-subject-card-insert-text").parentElement.classList.replace("add", "insert");
}
});
}
dropFieldAdd.classList.add("show");
dropFieldAddHitbox.classList.add("show");
dropFieldRemove.classList.add("show");
dropFieldRemoveHitbox.classList.add("show");
document.querySelector(".semester-content").classList.add("dragging");
this.changeDragIconToTickIcon(card);
}
// MARK: tickIconOnClickEvent
tickIconOnClickEvent(e, tick) {
e.preventDefault();
const targetId = tick.dataset.targetid;
const notifDiv = document.querySelector(`.selected-card-notif-div.on[data-targetid="${targetId}"]`);
this.removeCardFromSelection(notifDiv);
}
//#endregion
//#endregion
//#endregion
//#region __________ — Drag Events — __________
//#region -— Event listeners _____________________
// MARK: attach ondrag events
attachOnDragEventListeners(container=document.body, descendants=true) { // Add ONDRAG cards event
if (container instanceof HTMLElement || container == document) {
if (container?.classList?.contains("module-card-content") || container?.classList?.contains("module-details") || container?.classList?.contains("subject-card")) {
(container?.classList?.contains("subject-card") ? [container] : container.querySelectorAll(".subject-card") || []).forEach(subjectCard => {
this.attachSubjectCardOnDragEventListeners(subjectCard);
})
}
else if (container?.classList?.contains("semester-content") || container?.classList?.contains("semester-grid") || container?.classList?.contains("modules-section") || container?.classList?.contains("semester-section") || container?.classList?.contains("module-card") || container == document.body) {
(container?.classList?.contains("module-card") ? [container] : container.querySelectorAll(".module-card") || []).forEach(moduleCard => {
this.attachModuleCardOnDragEventListeners(moduleCard);
if (descendants) {
moduleCard.querySelectorAll(`.subject-card`).forEach(subjectCard => {
this.attachSubjectCardOnDragEventListeners(subjectCard);
})
container.querySelectorAll(`.subject-card.unclassified`).forEach(subjectCard => {
this.attachSubjectCardOnDragEventListeners(subjectCard);
})
}
})
}
}
this.attachNotifBtnsListener();
}
attachSubjectCardOnDragEventListeners(subjectCard) {
let draggableElement = subjectCard.querySelector(".subject-card-header");
draggableElement.draggable = true;
draggableElement.ondragstart = (e) => {this.draggedElementOnDragStartAction( e, subjectCard)};
draggableElement.ondragend = (e) => {this.draggedElementOnDragEndAction( e, subjectCard)};
}
attachModuleCardOnDragEventListeners(moduleCard) {
const moduleHeader = moduleCard.querySelector(".module-header");
moduleHeader.draggable = true;
moduleHeader.ondragstart = (e) => {this.draggedElementOnDragStartAction(e, moduleCard)}
moduleHeader.ondragend = (e) => {this.draggedElementOnDragEndAction( e, moduleCard)}
}
// MARK: detach ondrag events
detachOnDragEventListeners() { // Remove ONDRAG cards event
document.querySelectorAll(".subject-card").forEach(subjectCard => {
let draggableElement = "";
const isCompact = subjectCard.classList.contains("compact");
if (isCompact) {draggableElement = subjectCard;}
else {draggableElement = subjectCard.querySelector(".subject-card-header");}
draggableElement.draggable = false;
draggableElement.ondragstart = null;
draggableElement.ondragend = null;
})
document.querySelectorAll(".module-header").forEach(moduleHeader => {
moduleHeader.draggable = false;
moduleHeader.ondragstart = null;
moduleHeader.ondragend = null;
})
}
//#region insertion fields ________________________
insertFieldHitboxOnDragOverEvent(e) {
const type = e.target.dataset.type;
const insertField = e.target.closest(`.drop-field.insert-field.${type}`);
const insertFieldArrow = insertField.querySelector(`.drop-${type}-card-insert-arrow`);
const insertFieldPlus = insertField.querySelector(`.drop-${type}-card-insert-plus`);
const insertFieldText = insertField.querySelector(`.drop-${type}-card-insert-text`);
const insertFieldHitbox = insertField.querySelector(`.drop-${type}-card-insert-hitbox`);
e.preventDefault();
e.dataTransfer.dropEffect = "link";
insertField.classList.add("hover");
insertFieldArrow?.classList?.add("hover");
insertFieldPlus?.classList?.add("hover");
insertFieldText.classList.add("hover");
}
insertFieldHitboxOnDragLeaveEvent(e) {
const type = e.target.dataset.type;
const insertField = e.target.closest(`.drop-field.insert-field.${type}`);
const insertFieldArrow = insertField.querySelector(`.drop-${type}-card-insert-arrow`);
const insertFieldPlus = insertField.querySelector(`.drop-${type}-card-insert-plus`);
const insertFieldText = insertField.querySelector(`.drop-${type}-card-insert-text`);
const insertFieldHitbox = insertField.querySelector(`.drop-${type}-card-insert-hitbox`);
e.preventDefault();
insertField.classList.remove("hover");
insertFieldArrow?.classList?.remove("hover");
insertFieldPlus?.classList?.remove("hover");
insertFieldText?.classList?.remove("hover");
}
insertFieldHitboxOnDropEvent(e) {
const type = e.target.dataset.type;
const index = e.target.dataset.index;
const insertField = e.target.closest(`.drop-field.insert-field.${type}`);
const insertFieldHitbox = insertField.querySelector(`.drop-${type}-card-insert-hitbox`);
const dataTransfer = e.dataTransfer.getData("text");
const dataTransferMatch = dataTransfer.match(/module-card|subject-card/);
const sourceType = dataTransferMatch?.[0] ? dataTransferMatch[0] : "errr... something wrong, probably?";
e.preventDefault();
insertFieldHitbox.ondragover = (e) => {this.insertFieldHitboxOnDragOverEvent(e)};
switch (`${sourceType} dropped in a ${type} insertion field`) {
case "module-card dropped in a module insertion field":
this.dropFieldSubjectInsertAction(dataTransfer, insertField);
break;
case "module-card dropped in a subject insertion field":
this.dropFieldSubjectInsertAction(dataTransfer, insertField);
break;
case "subject-card dropped in a module insertion field":
this.dropFieldToNewModuleAction(dataTransfer, index);
break;
case "subject-card dropped in a subject insertion field":
this.dropFieldSubjectInsertAction(dataTransfer, insertField);
break;
}
}
insertFieldHitboxOnMouseEnterEvent(e) {
const type = e.target.dataset.type;
const insertField = e.target.closest( `.drop-field.insert-field.${type}`);
const insertFieldArrow = insertField.querySelector(`.drop-${type}-card-insert-arrow`);
const insertFieldPlus = insertField.querySelector(`.drop-${type}-card-insert-plus`);
const insertFieldText = insertField.querySelector(`.drop-${type}-card-insert-text`);
insertField.classList.add("hover");
insertFieldArrow?.classList?.add("hover");
insertFieldPlus?.classList?.add("hover");
insertFieldText?.classList?.add("hover");
}
insertFieldHitboxOnMouseLeaveEvent(e) {
const type = e.target.dataset.type;
const insertField = e.target.closest( `.drop-field.insert-field.${type}`);
const insertFieldArrow = insertField.querySelector(`.drop-${type}-card-insert-arrow`);
const insertFieldPlus = insertField.querySelector(`.drop-${type}-card-insert-plus`);
const insertFieldText = insertField.querySelector(`.drop-${type}-card-insert-text`);
insertField.classList.remove("hover");
insertFieldArrow?.classList?.remove("hover");
insertFieldPlus?.classList?.remove("hover");
insertFieldText?.classList?.remove("hover");
}
insertFieldHitboxOnClickEvent(e) {
const type = e.target.dataset.type;
const insertField = e.target.closest( `.drop-field.insert-field.${type}`);
const insertFieldArrow = insertField.querySelector(`.drop-${type}-card-insert-arrow`);
const insertFieldPlus = insertField.querySelector(`.drop-${type}-card-insert-plus`);
const insertFieldText = insertField.querySelector(`.drop-${type}-card-insert-text`);
e.preventDefault();
if (this.selectedSubjectCardsId.length == 0) {
this.dropFieldSubjectInsertAction(null, insertField)
}
else {
this.dropFieldSubjectInsertAction(this.selectedSubjectCardsId[0], insertField);
}
}
async attachInsertFieldHitboxEventListeners(insertFieldHitbox) {
insertFieldHitbox.ondragover = (e) => {this.insertFieldHitboxOnDragOverEvent(e)};
insertFieldHitbox.ondragleave = (e) => {this.insertFieldHitboxOnDragLeaveEvent(e)};
insertFieldHitbox.ondrop = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "link";
const data = e.target.dataset;
if (data.type.match(/subject|module/)) {
this.insertFieldHitboxOnDropEvent(e);
}
else {
this.dropFieldToNewModuleAction(e.dataTransfer.getData("text"), data.index);
}
};
insertFieldHitbox.onmouseenter = (e) => {this.insertFieldHitboxOnMouseEnterEvent(e)};
insertFieldHitbox.onmouseleave = (e) => {this.insertFieldHitboxOnMouseLeaveEvent(e)};
insertFieldHitbox.onclick = (e) => {
const data = e.target.dataset;
if (data.type == "subject") {
this.insertFieldHitboxOnClickEvent(e);
}
else if (data.type == "module") {
if (this.selectedSubjectCardsId.length > 0) {
this.dropFieldToNewModuleAction(this.selectedSubjectCardsId[0], data.index);
}
else {
this.dropFieldToNewModuleAction(null, data.index);
}
}
};
}
detachInsertFieldHitboxEventListeners(insertFieldHitbox) {
insertFieldHitbox.ondragover = (e) => {e.preventDefault(); e.dataTransfer.dropEffect = "none";};
insertFieldHitbox.ondragleave = (e) => {e.preventDefault()};
insertFieldHitbox.ondrop = (e) => {e.preventDefault(); e.dataTransfer.dropEffect = "none";};
insertFieldHitbox.onmouseenter = (e) => {e.preventDefault()};
insertFieldHitbox.onmouseleave = (e) => {e.preventDefault()};
insertFieldHitbox.onclick = (e) => {e.preventDefault()};
}
//#endregion
//#endregion
//#region -— Dragged element actions __________
// MARK: ON DRAG START
async draggedElementOnDragStartAction(e, card) {
if (e instanceof Event) {
if (e?.target?.classList?.contains("any-input")) {return}
e.dataTransfer.effectAllowed = "link";
e.dataTransfer.setDragImage(document.getElementById("emptyDivToRemoveTheDragImage"), 0, 0);
e.dataTransfer.setData("text", card.id);
}
const type = card.classList.contains("subject-card") ? "subject" : "module";
let selectionGoingOn = false, draggedCardIsSelected = false;
if (this.selectedModuleCardsId.length > 0 || this.selectedSubjectCardsId.length > 0) {
selectionGoingOn = true;
}
if (this.selectedSubjectCardsId.includes(card.id) || this.selectedModuleCardsId.includes(card.id)) {
draggedCardIsSelected = true;
}
if (type == "subject") {
(draggedCardIsSelected ? this.selectedSubjectCardsId : [card.id]).forEach(subjectCardId => {
const subjectCard = document.getElementById(subjectCardId);
const subjectTotalCoef = subjectCard.querySelector(".subject-total-coef-div");
subjectCard.style.width = "50%";
subjectTotalCoef.style.opacity = "0";
if (!this.compactSubjCardsId.includes(subjectCardId) || this.detailedSubjCardsId.includes(subjectCardId)) {
this.foldSubjCard(subjectCard);
}
})
clearTimeout(this?.timeouts?.documentOnDragEnd?.hideTeacherTable);
clearTimeout(this?.timeouts?.draggedElementOnDragEndAction?.showTeacherTable);
if (!this.timeouts?.draggedElementOnDragStartAction) {this.timeouts.draggedElementOnDragStartAction = {};}
this.timeouts.draggedElementOnDragStartAction.hideTeacherTable = setTimeout(() => {
document.querySelectorAll(".grades-table-header-teacher").forEach(teacher => {teacher.style.display = "none";})
}, 50);
if (!card.classList.contains("unclassified") && !draggedCardIsSelected) {
const sem = card.dataset.semester;
const moduleName = card.dataset.module;
const index = card.dataset.index;
const upperInsertField = document.querySelector(`.drop-field.insert-field.subject[data-semester="${sem}"][data-module="${moduleName}"][data-index="${parseInt(index)+0}"]`)
const upperInsertFieldHitbox = upperInsertField.querySelector(".drop-subject-card-insert-hitbox");
const lowerInsertField = document.querySelector(`.drop-field.insert-field.subject[data-semester="${sem}"][data-module="${moduleName}"][data-index="${parseInt(index)+1}"]`)
const lowerInsertFieldHitbox = lowerInsertField.querySelector(".drop-subject-card-insert-hitbox");
this.detachInsertFieldHitboxEventListeners(upperInsertFieldHitbox);
this.detachInsertFieldHitboxEventListeners(lowerInsertFieldHitbox);
upperInsertField.classList.remove("show");
lowerInsertField.classList.remove("show");
}
}
else if (type == "module") {
(draggedCardIsSelected ? this.selectedModuleCardsId : [card.id]).forEach(moduleCardId => {
const moduleCard = document.getElementById(moduleCardId);
const moduleTotalCoef = moduleCard.querySelector(".module-subject-total-coef-div");
const moduleHeaderLeftSize = moduleCard.querySelector(".module-header-left-side");
moduleCard.style.width = "50%";
moduleTotalCoef.style.display = "none";
if (!draggedCardIsSelected) {
// A non-selected module card has been dragged: if it was already folded, we only remove its adjacent module insertion fields, but fold it as well otherwise
this.foldModuleCard(moduleCard, false, this.foldedModuleCardsId.includes(moduleCardId) ? "only" : true, true);
}
else if (!this.foldedModuleCardsId.includes(moduleCardId)) {
// A selected unfolded module card has been dragged: we fold it while leaving its adjacent module insertion fields displayed
this.foldModuleCard(moduleCard, false, false, true);
}
// A selected folded module card doesn't need to do anything other then changing its width
})
}
if (!selectionGoingOn) {
document.querySelector(".semester-content") .classList.add("dragging");
document.querySelector(".drop-field.create-module") .classList.add("show");
document.querySelector(".drop-field.remove-from-module") .classList.add("show");
document.querySelector(".drop-field-create-module-hitbox") .classList.add("show");
document.querySelector(".drop-field-remove-from-module-hitbox") .classList.add("show");
// Making sure there's no remaining hover class
document.querySelector(".drop-field.create-module") .classList.remove("hover");
document.querySelector(".drop-field-create-module-text.top") .classList.remove("hover");
document.querySelector(".drop-field-create-module-text.bottom") .classList.remove("hover");
document.querySelector(".drop-field-create-module-plus") .classList.remove("hover");
document.querySelector(".drop-field.remove-from-module") .classList.remove("hover");
document.querySelector(".drop-field-remove-from-module-text.top") .classList.remove("hover");
document.querySelector(".drop-field-remove-from-module-text.bottom") .classList.remove("hover");
document.querySelector(".drop-field-remove-from-module-minus") .classList.remove("hover");
// Select all the shown drop fields.
// Since it occurs after folding the subject card is being dragged, it won't select the 2 subject insertion fields adjacent to this subject card.
document.querySelectorAll(".drop-field.insert-field.show").forEach(insertField => {
const type = insertField.classList.contains("subject") ? "subject" : "module";
insertField.querySelector(`.drop-${type}-card-insert-plus`) .classList.remove("show");
insertField.querySelector(`.drop-${type}-card-insert-arrow`).classList.add("show");
insertField.querySelector(`.drop-${type}-card-insert-text`) .classList.replace("add", "insert");
insertField.querySelector(`.drop-${type}-card-insert-text`) .parentElement.classList.replace("add", "insert");
})
}
}
// MARK: ON DRAG END
async draggedElementOnDragEndAction(e, card) {
if (e?.target?.classList?.contains("any-input")) {return}
let selectionGoingOn = false, draggedCardIsSelected = false;
if (this.selectedModuleCardsId.length > 0 || this.selectedSubjectCardsId.length > 0) {
selectionGoingOn = true;
}
if (this.selectedSubjectCardsId.includes(card.id) || this.selectedModuleCardsId.includes(card.id)) {
draggedCardIsSelected = true;
}
if (card.classList.contains("subject-card")) {
(draggedCardIsSelected ? this.selectedSubjectCardsId : [card.id]).forEach(subjectCardId => {
const subjectCard = document.getElementById(subjectCardId);
if (subjectCard) {
const subjectTotalCoef = subjectCard.querySelector(".subject-total-coef-div");
subjectCard.style.width = "";
subjectTotalCoef.style.opacity = "";
if (!this.compactSubjCardsId.includes(subjectCardId) || this.detailedSubjCardsId.includes(subjectCardId)) {
this.unfoldSubjCard(subjectCard);
}
}
})
clearTimeout(this?.timeouts?.documentOnDragEnd?.hideTeacherTable);
clearTimeout(this?.timeouts?.draggedElementOnDragStartAction?.hideTeacherTable);
if (!this?.timeouts?.draggedElementOnDragEndAction) {this.timeouts.draggedElementOnDragEndAction = {};}
this.timeouts.draggedElementOnDragEndAction.showTeacherTable = setTimeout(() => {
document.querySelectorAll(".grades-table-header-teacher").forEach(teacher => {teacher.style.display = "table-cell"})
}, 50);
if (!card.classList.contains("unclassified") && !draggedCardIsSelected) {
const sem = card.dataset.semester;
const moduleName = card.dataset.module;
const index = card.dataset.index;
const upperInsertField = document.querySelector(`.drop-field.insert-field.subject[data-semester="${sem}"][data-module="${moduleName}"][data-index="${parseInt(index)+0}"]`)
const upperInsertFieldHitbox = upperInsertField.querySelector(".drop-subject-card-insert-hitbox");
const lowerInsertField = document.querySelector(`.drop-field.insert-field.subject[data-semester="${sem}"][data-module="${moduleName}"][data-index="${parseInt(index)+1}"]`)
const lowerInsertFieldHitbox = lowerInsertField.querySelector(".drop-subject-card-insert-hitbox");
this.attachInsertFieldHitboxEventListeners(upperInsertFieldHitbox);
this.attachInsertFieldHitboxEventListeners(lowerInsertFieldHitbox);
upperInsertField.classList.add("show");
lowerInsertField.classList.add("show");
}
}
else if (card.classList.contains("module-card")) {
(draggedCardIsSelected ? this.selectedModuleCardsId : [card.id]).forEach(moduleCardId => {
const moduleCard = document.getElementById(moduleCardId);
if (moduleCard) {
const moduleTotalCoef = moduleCard.querySelector(".module-subject-total-coef-div");
const moduleHeaderLeftSize = moduleCard.querySelector(".module-header-left-side");
moduleCard.style.width = "";
moduleTotalCoef.style.display = "";
if (!draggedCardIsSelected) {
// A non-selected module card has been dropped: if it was already folded before being dragged, we only show its adjacent module insertion fields, but unfold it as well otherwise
this.unfoldModuleCard(moduleCard, false, this.foldedModuleCardsId.includes(moduleCardId) ? "only" : true, true);
}
else if (!this.foldedModuleCardsId.includes(moduleCardId)) {
// A selected unfolded module card has been dropped: we unfold it while leaving its adjacent module insertion fields displayed, since it wasn't folded before being dragged
this.unfoldModuleCard(moduleCard, false, false, true);
}
}
})
}
if (!selectionGoingOn) {
document.querySelector(".semester-content") .classList.remove("dragging");
document.querySelector(".drop-field.create-module") .classList.remove("show");
document.querySelector(".drop-field-create-module-hitbox") .classList.remove("show");
document.querySelector(".drop-field.remove-from-module") .classList.remove("show");
document.querySelector(".drop-field-remove-from-module-hitbox") .classList.remove("show");
// Select all the shown drop fields.
// Since it occurs after unfolding the subject card is being dragged, it WILL also select the 2 subject insertion fields adjacent to this subject card.
// Though in this case, nothing will change for the 2 adjacent subject insertion fields.
document.querySelectorAll(".drop-field.insert-field.show").forEach(insertField => {
const type = insertField.classList.contains("subject") ? "subject" : "module";
insertField.querySelector(`.drop-${type}-card-insert-plus`) .classList.add("show");
insertField.querySelector(`.drop-${type}-card-insert-arrow`).classList.remove("show");
insertField.querySelector(`.drop-${type}-card-insert-text`) .classList.replace("insert", "add");
insertField.querySelector(`.drop-${type}-card-insert-text`) .parentElement.classList.replace("insert", "add");
})
}
}
// #endregion
//#region -— Card selection ______________________
// MARK: createSelectedCardNotifDiv
createSelectedCardNotifDiv(card) {
const semester = card.dataset.semester;
const isSubject = card.classList.contains("subject-card");
const target = isSubject ? card.dataset.subject : card.dataset.module;
const targetId = card.id;
const selectionNotifDiv = document.createElement("div");
selectionNotifDiv.className = `selected-card-notif-div ${target}`;
selectionNotifDiv.id = `selected-card-notif-div-for-${target}-from-semester-${semester}`;
selectionNotifDiv.dataset.type = isSubject ? "subject" : "module";
selectionNotifDiv.dataset.target = target;
selectionNotifDiv.dataset.semester = semester;
selectionNotifDiv.dataset.targetid = targetId;
selectionNotifDiv.innerHTML = `
`;
return selectionNotifDiv;
}
// MARK: remove from subject selection
/**
* Manage all the actions involving the deletion of a card from the selection of cards
*
* @param {String} notifDiv the div of the notif linked to the selected subject card
*/
removeCardFromSelection(notifDiv="all") {
if (notifDiv=="all") { // clear all subject card selection as well as their respective notif
const selectedCardsId = this.selectedSubjectCardsId.length > 0 ? this.selectedSubjectCardsId : this.selectedModuleCardsId;
selectedCardsId.forEach(selectedCardId => {
const correspNotifDiv = document.querySelector(`.selected-card-notif-div[data-targetid="${selectedCardId}"]`);
correspNotifDiv.classList.remove("on");
setTimeout(() => {correspNotifDiv.remove();}, 300);
this.changeTickIconToDragIcon(document.getElementById(selectedCardId));
})
clearTimeout(this?.timeouts?.documentOnDragEnd?.hideTeacherTable);
if (!this?.timeouts?.removeCardFromSelection) { this.timeouts.removeCardFromSelection = {} }
this.timeouts.removeCardFromSelection.hideTeacherTable = setTimeout(() => {document.querySelectorAll(".grades-table-header-teacher").forEach(teacher => {teacher.style.display = "table-cell"})}, 100);
document.querySelector(".semester-content") .classList.remove("dragging");
document.querySelector(".drop-field.create-module") .classList.remove("show");
document.querySelector(".drop-field-create-module-hitbox") .classList.remove("show");
document.querySelector(".drop-field.remove-from-module") .classList.remove("show");
document.querySelector(".drop-field-remove-from-module-hitbox") .classList.remove("show");
this.selectedSubjectCardsId = [];
this.selectedModuleCardsId = [];
this.selectedSubjectCardsSortedByModule = {};
}
else if (notifDiv?.classList?.contains("selected-card-notif-div")) { // clear the specifically given notifDiv from the selection
const card = document.getElementById(notifDiv.dataset.targetid);
const type = notifDiv.dataset.type;
notifDiv.classList.remove("on");
setTimeout(()=>{notifDiv.remove()}, 300);
if (type == "subject") {
if (this.selectedSubjectCardsId.includes(card.id)) this.selectedSubjectCardsId.splice(this.selectedSubjectCardsId.indexOf(card.id), 1);
Object.keys(this.selectedSubjectCardsSortedByModule).forEach(moduleName => {
this.selectedSubjectCardsSortedByModule[moduleName].forEach((selectedSubjectCard, subjIndex) => {
this.selectedSubjectCardsSortedByModule[moduleName].splice(subjIndex, 1);
})
if (this.selectedSubjectCardsSortedByModule[moduleName].length == 0) {
delete this.selectedSubjectCardsSortedByModule[moduleName];
}
})
if (this.selectedSubjectCardsId.length == 0) this.draggedElementOnDragEndAction(null, card);
}
else if (type == "module") {
if (this.selectedModuleCardsId.includes(card.id)) this.selectedModuleCardsId.splice(this.selectedModuleCardsId.indexOf(card.id), 1);
if (this.selectedModuleCardsId.length == 0) this.draggedElementOnDragEndAction(null, card);
}
this.changeTickIconToDragIcon(card);
}
// Ensure the subject insertion drop fields are displaying the right text
document.querySelectorAll(".drop-field.insert-field").forEach(subjInsertField => {
if (this.selectedSubjectCardsId.length == 0 && this.selectedModuleCardsId.length == 0) {
subjInsertField.querySelector(".drop-module-card-insert-plus , .drop-subject-card-insert-plus ").classList.add("show");
subjInsertField.querySelector(".drop-module-card-insert-arrow, .drop-subject-card-insert-arrow").classList.remove("show");
subjInsertField.querySelector(".drop-module-card-insert-text, .drop-subject-card-insert-text" ).classList.replace("insert", "add");
subjInsertField.querySelector(".drop-module-card-insert-text, .drop-subject-card-insert-text" ).parentElement.classList.replace("insert", "add");
}
else {
subjInsertField.querySelector(".drop-module-card-insert-plus , .drop-subject-card-insert-plus ").classList.remove("show");
subjInsertField.querySelector(".drop-module-card-insert-arrow, .drop-subject-card-insert-arrow").classList.add("show");
subjInsertField.querySelector(".drop-module-card-insert-text, .drop-subject-card-insert-text" ).classList.replace("add", "insert");
subjInsertField.querySelector(".drop-module-card-insert-text, .drop-subject-card-insert-text" ).parentElement.classList.replace("add", "insert");
}
});
}
//#endregion
//#region -— Drop fields actions ________________
// MARK: attach dropFields listeners
attachDropFieldsEventListeners(target="all", insertFieldsContainer="") {
const dropFieldAdd = document.querySelector(".drop-field.create-module");
const dropFieldAddHitbox = document.querySelector(".drop-field-create-module-hitbox");
const dropFieldRemove = document.querySelector(".drop-field.remove-from-module");
const dropFieldRemoveHitbox = document.querySelector(".drop-field-remove-from-module-hitbox");
const insertFieldHitboxes = (insertFieldsContainer || document).querySelectorAll(".drop-subject-card-insert-hitbox, .drop-module-card-insert-hitbox");
if (target == "add" || target == "all") {
dropFieldAdd.style.background = "";
dropFieldAddHitbox.ondragover = (e) => {if (e.target.classList.contains("show")) {
e.preventDefault();
dropFieldAdd.classList.add("hover");
dropFieldAdd.querySelectorAll(".drop-field-create-module-text, .drop-field-create-module-plus").forEach(text => {text.classList.add("hover");})
}};
dropFieldAddHitbox.ondragleave = (e) => {if (e.target.classList.contains("show")) {
e.preventDefault();
dropFieldAdd.classList.remove("hover");
dropFieldAdd.querySelectorAll(".drop-field-create-module-text, .drop-field-create-module-plus").forEach(text => {text.classList.remove("hover");})
}};
dropFieldAddHitbox.ondrop = (e) => {if (e.target.classList.contains("show")) {
e.preventDefault();
e.dataTransfer.dropEffect = "link";
dropFieldAdd.classList.remove("hover");
dropFieldAdd.querySelectorAll(".drop-field-create-module-text, .drop-field-create-module-plus").forEach(text => {text.classList.remove("hover");})
this.dropFieldToNewModuleAction(e.dataTransfer.getData("text"));
}};
// Custom :hover event, cuz otherwise it would trigger when the fields are not shown
dropFieldAddHitbox.onmouseenter = (e) => {if (e.target.classList.contains("show")) {
e.preventDefault();
dropFieldAdd.classList.add("hover");
dropFieldAdd.querySelectorAll(".drop-field-create-module-text, .drop-field-create-module-plus").forEach(text => {text.classList.add("hover");})
}};
dropFieldAddHitbox.onmouseleave = (e) => {if (e.target.classList.contains("show")) {
e.preventDefault();
dropFieldAdd.classList.remove("hover");
dropFieldAdd.querySelectorAll(".drop-field-create-module-text, .drop-field-create-module-plus").forEach(text => {text.classList.remove("hover");})
}};
dropFieldAddHitbox.onclick = (e) => {if (e.target.classList.contains("show")) {
e.preventDefault();
e.target.classList.remove("hover");
e.target.querySelectorAll(".drop-field-create-module-text, .drop-field-create-module-plus").forEach(text => {text.classList.remove("hover");})
if (this.selectedSubjectCardsId.length > 0) {
this.dropFieldToNewModuleAction(this.selectedSubjectCardsId[0]);
}
}};
}
if (target == "remove" || target == "all") {
dropFieldRemove.style.background = "";
dropFieldRemoveHitbox.ondragover = (e) => {if (e.target.classList.contains("show")) {
e.preventDefault();
dropFieldRemove.classList.add("hover");
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-text").forEach(text => {text.classList.add("hover");})
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-minus").forEach(text => {text.classList.add("hover", "slight-horiz-shake"); text.onanimationend = () => {text.classList.remove("slight-horiz-shake"); text.onanimationend = null;}})
}};
dropFieldRemoveHitbox.ondragleave = (e) => {if (e.target.classList.contains("show")) {
e.preventDefault();
dropFieldRemove.classList.remove("hover");
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-text").forEach(text => {text.classList.remove("hover");})
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-minus").forEach(text => {text.classList.remove("hover", "slight-horiz-shake"); text.onanimationend = null;})
}};
dropFieldRemoveHitbox.ondrop = (e) => {if (e.target.classList.contains("show")){
e.preventDefault();
e.dataTransfer.dropEffect = "link";
dropFieldRemove.classList.remove("hover");
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-text").forEach(text => {text.classList.remove("hover");})
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-minus").forEach(text => {text.classList.remove("hover", "slight-horiz-shake"); text.onanimationend = null;})
this.dropFieldRemoveAction(e.dataTransfer.getData("text"));
}};
// Custom :hover event, cuz otherwise it would trigger when the fields are not shown
dropFieldRemoveHitbox.onmouseenter = (e) => {if (e.target.classList.contains("show")) {
e.preventDefault();
dropFieldRemove.classList.add("hover");
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-text").forEach(text => {text.classList.add("hover");})
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-minus").forEach(text => {text.classList.add("hover", "slight-horiz-shake"); text.onanimationend = () => {text.classList.remove("slight-horiz-shake"); text.onanimationend = null;}})
}};
dropFieldRemoveHitbox.onmouseleave = (e) => {if (e.target.classList.contains("show")) {
e.preventDefault();
dropFieldRemove.classList.remove("hover");
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-text").forEach(text => {text.classList.remove("hover");})
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-minus").forEach(text => {text.classList.remove("hover", "slight-horiz-shake"); text.onanimationend = null;})
}};
dropFieldRemoveHitbox.onclick = (e) => {if (e.target.classList.contains("show")) {
e.preventDefault();
dropFieldRemove.classList.remove("hover");
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-text").forEach(text => {text.classList.remove("hover");})
dropFieldRemove.querySelectorAll(".drop-field-remove-from-module-minus").forEach(text => {text.classList.remove("hover", "slight-horiz-shake"); text.onanimationend = null;})
if (this.selectedSubjectCardsId.length > 0) {
this.dropFieldRemoveAction(this.selectedSubjectCardsId[0]);
}
}};
}
if (target == "insert" || target == "all") {
insertFieldHitboxes.forEach(insertFieldHitbox => {
this.attachInsertFieldHitboxEventListeners(insertFieldHitbox)
})
}
}
// MARK: dropFieldToNewModuleAction
dropFieldToNewModuleAction(cardId, index=0) {
const sem = this.currentSemester;
let bypasseReplacement = false;
let newModuleConfig = {subjects: [], coefficients: {}};
let newModuleName = "Module 1"; let count = 1;
if (!this.moduleConfig[sem]) this.moduleConfig[sem] = {__modules__: []};
while (this.moduleConfig?.[sem]?.[newModuleName]) {count++; newModuleName = `Module ${count}`;}
if (cardId) {
const card = document.getElementById(cardId);
if (card.classList.contains('subject-card')) {
let cardIsSelected = false;
this.selectedSubjectCardsId.forEach(selectedSubjectCardId => {if (selectedSubjectCardId == card.id) cardIsSelected = true;});
let subject, oldModuleName, manageSim = true;
if (!this.sim[sem]) manageSim = false;
if (!cardIsSelected) { // 1 unselected subj card dropped in the drop field "add"
subject = card.dataset.subject;
oldModuleName = card.dataset.module;
const moduleIndex = this.moduleConfig[sem].__modules__.indexOf(oldModuleName);
if (!card.classList.contains("unclassified")) { // If the subj card doesn't come from the unclassified container:
// We get its index in its module configured in moduleConfig
const subjectIndex = this.moduleConfig[sem][oldModuleName].subjects.indexOf(subject);
if (this.moduleConfig[sem][oldModuleName].subjects.toSpliced(subjectIndex,1).length == 0 && oldModuleName.match(/Module (\d)/)) {
// If the action of removing the subject's name from the list of subject names of the module empties the list, then we don't delete anything at all:
// the subj card was the only subj card of its previous module card, therefore we don't need to create nor make a new one, we just set the subject's coef to 100%.
// This case is only to avoid taking a subj card from a module named "Module [X]", putting it in a new module named "Module [X+1]", deleting "Module [X]",
// and realizing that it was pointless lol
// newModuleName = oldModuleName;
// newModuleConfig.coefficients[subject] = 100;
bypasseReplacement = true;
}
else {
newModuleConfig = {subjects: [subject], coefficients: {[subject]: 100}};
this.moduleConfig[sem][oldModuleName].subjects.splice(subjectIndex,1);
delete this.moduleConfig[sem][oldModuleName].coefficients[subject];
if (manageSim) {if (!this.sim[sem][oldModuleName]) manageSim = false;}
if (manageSim) {
this.sim[sem] = {[newModuleName]: {}, ...this.sim[sem]}
this.sim[sem][newModuleName][subject] = [];
this.sim[sem][oldModuleName][subject].forEach((_, index) => {
this.sim[sem][newModuleName][subject].push(this.sim[sem][oldModuleName][subject][index].shift())
})
this.deleteUnusedSimPath(false, sem, oldModuleName, subject);
this.saveSim();
}
}
if (this.moduleConfig[sem][oldModuleName].subjects.length == 0) {
this.moduleConfig[sem].__modules__.splice(moduleIndex, 1);
delete this.moduleConfig[sem][oldModuleName];
}
}
else {
newModuleConfig = {subjects: [subject], coefficients: {[subject]: 100}};
}
} else { // multiple subj cards dropped through selection in the drop field "add"
let remainingCoef = 100;
// Scanning through all the modules of the selected matiere cards to get the name of the module of name "Module [x]", so that instead of creating a new Module,
// we replace the module with the lowest x that would have been deleted
let lowestModuleIndexNameToReplace = -1;
Object.keys(this.selectedSubjectCardsSortedByModule).forEach((_moduleName, _moduleIndex) => {
const _moduleSelection = this.selectedSubjectCardsSortedByModule[_moduleName];
const match = _moduleName.match(/Module (\d+)/);
// if the name matches "Module [x]" (1st condition)
// and if the selection of subj cards of same module that will be removed from their module matches the number of subj in the said module (cond 2):
// we save the number of the module
if (match && _moduleSelection.length == this.moduleConfig[sem][_moduleName].subjects.length) {
lowestModuleIndexNameToReplace = match[1];
}
})
if (lowestModuleIndexNameToReplace > -1) {
newModuleName = "Module "+lowestModuleIndexNameToReplace;
}
Object.keys(this.selectedSubjectCardsSortedByModule).forEach((_moduleName, _moduleIndex) => {
oldModuleName = _moduleName;
const _moduleSelection = this.selectedSubjectCardsSortedByModule[oldModuleName];
_moduleSelection.forEach((selectedSubjectCard, _subjIndex) => {
const subjectCard = document.getElementById(selectedSubjectCard.cardId);
const selectionIndex = selectedSubjectCard.selectionIndex;
subject = subjectCard.dataset.subject;
if (selectionIndex+1 == this.selectedSubjectCardsId.length) {
newModuleConfig.coefficients[subject] = remainingCoef;
} else {
const coef = Math.round(100/this.selectedSubjectCardsId.length);
newModuleConfig.coefficients[subject] = coef;
remainingCoef -= coef;
}
newModuleConfig.subjects[selectionIndex] = subject;
if (!subjectCard.classList.contains("unclassified")) {
// removing the subject card from its former module
const oldModuleIndex = this.moduleConfig[sem].__modules__.indexOf(oldModuleName); // get the old module's index in the modules ordered array of the semester
const subjectIndexInOldModule = this.moduleConfig[sem][oldModuleName].subjects.indexOf(subject); // get the subject's index in the subjects ordered array of the old module
delete this.moduleConfig[sem][oldModuleName].coefficients[subject]; // delete coefficient data
this.moduleConfig[sem][oldModuleName].subjects.splice(subjectIndexInOldModule,1); // remove the subject from the subjects ordered array of the old module
if (this.moduleConfig[sem][oldModuleName].subjects.length == 0) {
// If, after removing the subject card from its former module, the said module is empty, we remove it
delete this.moduleConfig[sem][oldModuleName];
this.moduleConfig[sem].__modules__.splice(oldModuleIndex, 1);
}
if (manageSim) {if (!this.sim[sem][oldModuleName][subject]) manageSim = false} // checking if the subject card had sim grades
if (manageSim) {
// if the subject card had sim grades, change their path in this.sim to match the module change
this.sim[sem][newModuleName][subject] = [];
this.sim[sem][oldModuleName][subject].forEach((_, index) => {
this.sim[sem][newModuleName][subject].push(this.sim[sem][oldModuleName][subject][index].shift())
})
this.deleteUnusedSimPath(false, sem, oldModuleName, subject);
this.saveSim();
}
}
})
})
// this the last step, so that if the new module has the same same as an old module that gets deleted (in order to replace it, "Module [x]" case), we don't remove the wrong one
this.moduleConfig[sem][newModuleName] = newModuleConfig;
this.moduleConfig[sem].__modules__.splice(index, 0, newModuleName);
}
}
else if (card.classList.contains('module-card')) {
let cardIsSelected = false;
this.selectedModuleCardsId.forEach(selectedModuleCardId => {if (selectedModuleCardId == card.id) cardIsSelected = true;});
let oldModuleName, manageSim = true;
if (!this.sim[sem]) manageSim = false;
if (!cardIsSelected) { // 1 unselected module card dropped in the "add" drop field
oldModuleName = card.dataset.module;
newModuleName = oldModuleName;
const moduleIndex = this.moduleConfig[sem].__modules__.indexOf(oldModuleName);
// reordering the dropped module card first in the list of modules
this.moduleConfig[sem].__modules__.splice(moduleIndex,1);
this.moduleConfig[sem].__modules__.splice(0,0,oldModuleName);
} else { // multiple module cards dropped through selection in the "add" drop field
let remainingCoef = 100;
// Scanning through all the selected module cards to get their name if they match "Module [x]", so that instead of creating a new module name,
// we replace the module with the lowest x that would have been deleted
let lowestModuleIndexNameToReplace = count;
Object.keys(this.selectedModuleCardsId).forEach(_moduleName => {
const match = _moduleName.match(/Module (\d+)/);
// if the name matches "Module [x]" (1st condition)
// and if the selection of subj cards of same module that will be removed from their module matches the number of subj in the said module (2nd condition):
// we save the number of the module
if (parseInt(match?.[1] || lowestModuleIndexNameToReplace) < lowestModuleIndexNameToReplace) {
lowestModuleIndexNameToReplace = parseInt(match[1]);
}
})
// correcting the the name of the new module in case it should inherit the name of one of the selected module card
// because the later had a lower index than the new module's name anticipated
if (lowestModuleIndexNameToReplace < count) {
newModuleName = "Module "+lowestModuleIndexNameToReplace;
}
// Initiating the new module's data
newModuleConfig = {subjects: [], coefficients: {}};
// scanning through all the selected modules
this.selectedModuleCardsId.forEach(_moduleCardId => {
const selectedModuleCard = document.getElementById(_moduleCardId);
const selectedModuleName = selectedModuleCard.dataset.module;
// scanning through all the currently scanned selected module's subjects
this.moduleConfig[sem][selectedModuleName].subjects.forEach(_subjectName => {
const coef = this.moduleConfig[sem][selectedModuleName].coefficients[_subjectName];
// adding the info of the subject in the new module
newModuleConfig.subjects.push(_subjectName);
newModuleConfig.coefficients[_subjectName] = coef;
})
// deleting the selected module once all its informations have been transfered to the new module
delete this.moduleConfig[sem][selectedModuleName]
})
// this the last step, so that if the new module has the same same as an old module that gets deleted (in order to replace it, "Module [x]" case), we don't remove the wrong one
this.moduleConfig[sem][newModuleName] = newModuleConfig;
this.moduleConfig[sem].__modules__.splice(index, 0, newModuleName);
}
}
}
else {
const newSubjName = this.lang == "fr" ? "Nouvelle matière" : "New subject";
newModuleConfig.subjects.push(newSubjName);
newModuleConfig.coefficients[newSubjName] = 100;
}
if (!document.getElementById(cardId)?.classList?.contains('module-card') && !bypasseReplacement) {
// Editing the module config
this.moduleConfig[sem][newModuleName] = newModuleConfig;
const newModuleIndexInSem = this.moduleConfig[sem].__modules__.indexOf(newModuleName);
if (newModuleIndexInSem > -1) {
this.moduleConfig[sem].__modules__.splice(newModuleIndexInSem, 1, newModuleName)
}
else {
this.moduleConfig[sem].__modules__.splice(index, 0, newModuleName)
}
}
this.removeCardFromSelection();
this.saveConfig();
this.getGradesDatas();
this.generateContent();
this.scrollToClientHighestElem("first/ignore-setting", {id: `module-card-${newModuleName}-in-semester-${sem}`, smooth: true})
}
// MARK: dropFieldRemoveAction
dropFieldRemoveAction(cardId) {
const card = document.getElementById(cardId);
let cardIsSelected = false;
this.selectedSubjectCardsId.forEach(selectedSubjectCardId => {if (selectedSubjectCardId == card.id) cardIsSelected = true;})
if (card?.classList?.contains("subject-card") && !card?.classList?.contains("unclassified")) {
const sem = card.dataset.semester;
const module = card.dataset.module;
const subj = card.dataset.subject;
if (!cardIsSelected) {
const moduleIndex = this.moduleConfig[sem].__modules__.indexOf(module);
const subjectIndex = this.moduleConfig[sem][module].subjects.indexOf(subj);
this.moduleConfig[sem][module].subjects.splice(subjectIndex,1);
delete this.moduleConfig[sem][module].coefficients[subj];
if (this.moduleConfig[sem][module].subjects.length == 0) {
this.moduleConfig[sem].__modules__.splice(moduleIndex, 1);
delete this.moduleConfig[sem][module];
}
}
else {
let subject = "";
this.selectedSubjectCardsId.forEach(selectedSubjectCardId => {
const selectedSubjectCard = document.getElementById(selectedSubjectCardId);
subject = selectedSubjectCard.dataset.subject;
const moduleIndex = this.moduleConfig[sem].__modules__.indexOf(module);
const subjectIndex = this.moduleConfig[sem][module].subjects.indexOf(subject);
this.moduleConfig[sem][module].subjects.splice(subjectIndex,1);
delete this.moduleConfig[sem][module].coefficients[subject];
if (this.moduleConfig[sem][module].subjects.length == 0) {
this.moduleConfig[sem].__modules__.splice(moduleIndex, 1);
delete this.moduleConfig[sem][module];
}
if (this.compactSubjCardsId.includes(selectedSubjectCard)) {
this.compactSubjCardsId.splice(this.compactSubjCardsId.indexOf(selectedSubjectCard), 1);
}
if (this.detailedSubjCardsId.includes(selectedSubjectCard)) {
this.detailedSubjCardsId.splice(this.detailedSubjCardsId.indexOf(selectedSubjectCard), 1);
}
})
}
if (this.moduleConfig[sem].__modules__.length == 0) {delete this.moduleConfig[sem]}
this.removeCardFromSelection();
this.saveConfig();
this.getGradesDatas();
this.generateContent();
}
else if (card?.classList?.contains("subject-card") && card?.classList?.contains("unclassified") && cardIsSelected) {
this.removeCardFromSelection();
}
else if (card?.classList?.contains("module-card")) {
if (cardIsSelected) {
this.selectedModuleCardsId.forEach(cardId => {
const moduleCard = document.getElementById(cardId);
if (moduleCard) {
this.moduleCardDeleteBtnAction(moduleCard);
}
this.removeCardFromSelection();
})
}
else {
this.moduleCardDeleteBtnAction(card);
}
}
}
// MARK: dropFieldSubjectInsertAction
dropFieldSubjectInsertAction(cardId=null, methodCaller=null) {
const sem = this.currentSemester;
if (cardId) { // When dropping a ".drop-field.insert-field.subject" class div
const card = document.getElementById(cardId);
if (card?.classList?.contains('subject-card')) {
let cardIsSelected = false;
this.selectedSubjectCardsId.forEach(selectedSubjectCardId => {if (selectedSubjectCardId == card.id) cardIsSelected = true;});
const targetModuleName = methodCaller.dataset.module;
const insertionIndex = methodCaller.dataset.index;
// The same thing happens whether this method is triggered from a selection or a single subject card, we just have to choose the right card id
(cardIsSelected ? this.selectedSubjectCardsId : [card.id]).forEach(subjectCardId => {
const subjectCard = document.getElementById(subjectCardId);
const subject = subjectCard.dataset.subject;
const oldModuleName = subjectCard.dataset.module;
const oldModuleIndex = this.moduleConfig[sem].__modules__.indexOf(oldModuleName);
const subjectOldIndex = this.moduleConfig?.[sem]?.[oldModuleName]?.subjects?.indexOf(subject);
// CASE 1: subject card comes from unclassified section to a module -> (default/easy case)
// CASE 2: subject card comes from a module to another module -> (moving case)
// CASE 3: subject card comes from a module to the same module at a different index -> (reordering case)
// CASE 4: subject card comes from a module to the same module at the same index -> (it's no-use, so nothing happens. It shouldn't be reached though, since the adjacent insertion fields disappear when dragging a card)
switch (`
subject card comes from ${oldModuleName
? `a module and is ${targetModuleName==oldModuleName
? `reorganized to ${subjectOldIndex == insertionIndex || subjectOldIndex+1 == insertionIndex
? "the same index"
: "a different index"}`
: "moved to a different module"}`
: "the unclassified section"}
`.trim()) {
case "subject card comes from the unclassified section":
// Just set the unclassified subject in the moduleConfig
this.moduleConfig[sem][targetModuleName].subjects.splice(insertionIndex, 0, subject);
this.moduleConfig[sem][targetModuleName].coefficients[subject] = this.gradesDatas[sem][targetModuleName].totalCoefSubjects <= 100 ? (100 - this.gradesDatas[sem][targetModuleName].totalCoefSubjects) : 0;
break;
case "subject card comes from a module and is moved to a different module":
// We move the datas from the old module to the new module
this.moduleConfig[sem][targetModuleName].subjects.splice(insertionIndex, 0, subject);
this.moduleConfig[sem][targetModuleName].coefficients[subject] = Number (this.moduleConfig[sem][oldModuleName].coefficients[subject]);
this.moduleConfig[sem][oldModuleName].subjects.splice(subjectOldIndex, 1);
delete this.moduleConfig[sem][oldModuleName].coefficients[subject];
break;
case "subject card comes from a module and is reorganized to a different index":
// We move the datas while paying attention to at which index was the original subject before moving it (in order to not mess up with the insertion index)
this.moduleConfig[sem][targetModuleName].subjects.splice(insertionIndex, 0, subject);
this.moduleConfig[sem][targetModuleName].coefficients[subject] = Number (this.moduleConfig[sem][oldModuleName].coefficients[subject]);
const subjectCorrectOldIndex = subjectOldIndex + (insertionIndex<=subjectOldIndex && this.moduleConfig[sem][targetModuleName].subjects.includes(subject) ? 1 : 0);
this.moduleConfig[sem][oldModuleName].subjects.splice(subjectCorrectOldIndex, 1);
break;
case "subject card comes from a module and is reorganized to the same index":
"Alas, nothing happens... This case is never reached!";
break;
}
if (this.moduleConfig[sem]?.[oldModuleIndex]?.subjects?.length == 0) {
this.moduleConfig[sem]?.__modules__?.splice(oldModuleIndex, 1);
delete this.moduleConfig[sem][oldModuleIndex];
if (this.moduleConfig?.[sem]?.__modules__?.length == 0) {
delete this.moduleConfig[sem]
}
}
})
this.removeCardFromSelection();
this.saveConfig();
this.getGradesDatas();
this.generateContent();
this.setGradesTableTotalCoef();
}
else if (card?.classList?.contains('module-card')) { // Inserting all selected module cards at the place of the insertion field, in order of selection
let cardIsSelected = false;
this.selectedModuleCardsId.forEach(selectedModuleCardId => {if (selectedModuleCardId == card.id) cardIsSelected = true;});
const targetModuleName = methodCaller.dataset.module;
const insertionIndex = parseInt(methodCaller.dataset.index);
if (targetModuleName) {
// reversing the list of selected module cards so that the insertion index can remain constant
(cardIsSelected ? this.selectedModuleCardsId.reverse() : [card.id]).forEach(moduleCardId => {
const moduleCard = document.getElementById(moduleCardId);
const oldModuleName = moduleCard.dataset.module;
const oldModuleIndex = this.moduleConfig[sem].__modules__.indexOf(oldModuleName);
const subjectCards = Array.from(moduleCard.querySelectorAll(".subject-card"));
// reversing the list of subject cards so that the insertion index can remain constant
subjectCards.reverse().forEach((subjectCard, _index) => {
const subject = subjectCard.dataset.subject;
this.moduleConfig[sem][targetModuleName].subjects.splice(insertionIndex, 0, subject);
this.moduleConfig[sem][targetModuleName].coefficients[subject] = Number(this.moduleConfig[sem][oldModuleName].coefficients[subject]);
this.moduleConfig[sem][oldModuleName].subjects.splice(this.moduleConfig[sem][oldModuleName].subjects.length-1,1);
delete this.moduleConfig[sem][oldModuleName].coefficients[subject];
})
this.moduleConfig[sem].__modules__.splice(oldModuleIndex, 1)
delete this.moduleConfig[sem][oldModuleName];
if (this.moduleConfig?.[sem]?.__modules__?.length == 0) {
delete this.moduleConfig[sem]
}
})
}
else {
(cardIsSelected ? this.selectedModuleCardsId : [card.id]).forEach((moduleCardId, _index) => {
const moduleCard = document.getElementById(moduleCardId);
const oldModuleName = moduleCard.dataset.module;
const oldModuleIndex = this.moduleConfig[sem].__modules__.indexOf(oldModuleName);
const correctedInsertionIndex = insertionIndex < oldModuleIndex ? insertionIndex + _index : insertionIndex;
const correctedOldModuleIndex = correctedInsertionIndex < oldModuleIndex ? oldModuleIndex + 1 : oldModuleIndex;
// Insert the module at the right place,
this.moduleConfig[sem].__modules__.splice(correctedInsertionIndex, 0, oldModuleName)
this.moduleConfig[sem].__modules__.splice(correctedOldModuleIndex, 1)
})
}
this.removeCardFromSelection();
this.saveConfig();
this.getGradesDatas();
this.generateContent();
this.setGradesTableTotalCoef();
}
}
else { // When clicking on a ".drop-field.insert-field.subject" class div
const addDivClicked = methodCaller;
const sem = addDivClicked.dataset.semester;
const module = addDivClicked.dataset.module;
const moduleCard = document.getElementById(`module-card-${module}-in-semester-${sem}`);
const moduleDetails = moduleCard.querySelector(".module-details");
let newSubjName = `${this.lang == "fr" ? "Nouvelle matière" : "New subject"} 1`; let count = 1;
while (this.gradesDatas[sem][module].subjects[newSubjName]) {
count++; newSubjName = `${this.lang == "fr" ? "Nouvelle matière" : "New subject"} ${count}`;
}
const insertionIndex = methodCaller ? methodCaller.dataset.index : this.moduleConfig[sem][module].subjects.length;
this.moduleConfig [sem][module].subjects.splice(insertionIndex, 0, newSubjName);
this.moduleConfig [sem][module].coefficients [newSubjName] = 0;
this.saveConfig();
this.getGradesDatas();
moduleDetails.innerHTML = this.createAllSubjCards(sem, module);
this.attachAllSubjectCardRelatedEvenListenersForEverySubjectCard();
this.setGradesTableTotalCoef();
}
}
// MARK: dropFieldModuleInsertAction
dropFieldModuleInsertAction(cardId=null, methodCaller=null) {
const sem = this.currentSemester;
debugger;
if (cardId) {
const card = document.getElementById(cardId);
if (card?.classList?.contains("module-card")) {
const oldModuleIndex = card.dataset.index;
const newModuleIndex = methodCaller?.dataset?.index || 0;
const compensatedNewModuleIndex = oldModuleIndex > newModuleIndex ? newModuleIndex : newModuleIndex - 1;
const moduleName = this.moduleConfig[sem].__modules__.splice(oldModuleIndex, 1)[0];
this.moduleConfig[sem].__modules__.splice(compensatedNewModuleIndex,0,moduleName);
this.saveConfig();
this.getGradesDatas();
this.generateContent();
this.setGradesTableTotalCoef();
this.scrollToClientHighestElem({id: card.id, smooth: true})
}
}
}
// MARK: createDropFieldInsertionField
createDropFieldInsertionField(type="subject", {sem=0, moduleName="", index=-1}={sem:0, moduleName:"", index:-1}) {
const thereIsSelection = this.selectedSubjectCardsId.length > 0;
return `
${this.lang == "fr"
? "Note: Choisir une configuration effacera les traces de configuration pré-existante de l'année correspondante, mais pas des autres années"
: "Tip: Choosing a configuration will erase all traces of pre-existing configuration of the corresponding year, but not of the other years"
}
`;
this.ecamDash.appendChild(pickerMenuContainer);
const pickerMenu = document.querySelector("#pickerMenu");
this.appendCloseModalIcon(pickerMenu)
clearTimeout(this.timeouts?.closePickerMenu)
setTimeout(() => {pickerMenu.classList.add("show");}, 10)
pickerMenuContainer.onmousedown = (e) => {
if (e.target.closest(".modal-close-btn") || !e.target.closest("#pickerMenu")) {
pickerMenuContainer.onmouseup = (e) => {
if (e.target.closest(".modal-close-btn") || !e.target.closest("#pickerMenu")) {
this.closeOnlineCfgPickerModal()
}
pickerMenuContainer.onmouseup = null;
}
}
}
pickerMenu.onclick = (e) => {
const dirCard = e.target.closest(".online-cfg-picker-menu-dir-card");
if (dirCard) {
const addOnToDirCard = dirCard.classList.contains("on");
const path = dirCard.dataset.path;
// Start by removing all "show" and "on" classes to all descendant dirTrees and descendant/sibling dirCards
if (dirCard.classList.contains("prom") || dirCard.classList.contains("year") || dirCard.classList.contains("section")) {
pickerMenu.querySelectorAll(`.online-cfg-picker-menu-dir-tree.config.show`).forEach(dirTree => { dirTree.classList.remove("show")})
pickerMenu.querySelectorAll(`.online-cfg-picker-menu-dir-card.prom.on`) .forEach(_dirCard => {_dirCard.classList.remove("on")})
}
if (dirCard.classList.contains("year") || dirCard.classList.contains("section")) {
pickerMenu.querySelectorAll(`.online-cfg-picker-menu-dir-tree.prom.show`) .forEach(dirTree => { dirTree.classList.remove("show")})
pickerMenu.querySelectorAll(`.online-cfg-picker-menu-dir-card.year.on`) .forEach(_dirCard => {_dirCard.classList.remove("on")})
}
if (dirCard.classList.contains("section")) {
pickerMenu.querySelectorAll(`.online-cfg-picker-menu-dir-tree.year.show`) .forEach(dirTree => { dirTree.classList.remove("show")})
pickerMenu.querySelectorAll(`.online-cfg-picker-menu-dir-card.section.on`) .forEach(_dirCard => {_dirCard.classList.remove("on")})
}
// Then, adding the "on" class to the clicked dirCard and the "show" class to the target dirTree associated to the dirCard clicked
if (!addOnToDirCard) {
dirCard.classList.add("on");
}
if (dirCard.classList.contains("on") && !dirCard.classList.contains("config")) {
pickerMenu.querySelector(`.online-cfg-picker-menu-dir-tree[data-path="${path}"]`).classList.add("show");
}
// Import that data of the url dataset of the dir card clicked if it's a config dir card
if (dirCard.classList.contains("config")) {
this.importData(dirCard.dataset.url);
}
}
}
}
closeOnlineCfgPickerModal() {
const pickerMenu = document.querySelector("#pickerMenu");
if (pickerMenu) {
pickerMenu.classList.remove("show");
this.timeouts.closePickerMenu = setTimeout(() => {pickerMenu.parentElement.remove(); this.attachDocumentMouseListeners()}, 300);
}
}
generateOnlineCfgPickerMenuDirTree(type="section") {
// Creating an array containing all the properties' value of this.onlineConfigs.Configs that are objects (so that have a descendance) with at least one property: they are the data of the section folders
const sectionsData = this.onlineConfigs.Configs;
const sectionsArray = Object.values(sectionsData).map(value => {if (value instanceof Object && Object.keys(value).length>0) {return value}}).filter(value => {return value});
let html = type == "section" ? `
` : "";
html += sectionsArray.map(sectionDirData => { // Dir: Section
// Creating an array containing all the properties' value of sectionDirData that are objects (so that have a descendance) with at least one property: they are the data of the year folders
const yearsArray = Object.values(sectionDirData).map(value => {if (value instanceof Object && Object.keys(value).length>0) {return value}}).filter(value => {return value});
const name = sectionDirData.path.split("/").at(-1);
let out = type == "year" ? `
`
: yearsArray.map(yearDirData => { // Dir: Year
// Creating an array containing all the properties' value of yearDirData that are objects (so that have a descendance) with at least one property: they are the data of the prom folders
const promsArray = Object.values(yearDirData).map(value => {if (value instanceof Object && Object.keys(value).length>0) {return value}}).filter(value => {return value});
const name = yearDirData.path.split("/").at(-1);
let out = type == "prom" ? `
`
: promsArray.map(promDirData => { // Dir: Prom
// Creating an array containing all the properties' value of promDirData that are objects (so that have a descendance) with at least one property: they are the data of the configs
const configsArray = Object.keys(promDirData).map(key => {if (key != "nbCfgs" && key!= "path") {return key}}).filter(value => {return value});
const name = promDirData.path.split("/").at(-1);
this.tempGitConfigParentDirData = promDirData;
let out = type == "config" ? `