/**
* @file Initalizes the calendar
*
* @author Ali Izoyev
* @version 1.0.0
* @module calendar.js
*/
let calendar;
document.addEventListener("DOMContentLoaded", function() {
calendar = new ScheduleCalendar();
})
class ScheduleCalendar
{
// VARIABLES
#calendar = null;
// Data structures (Map and Set)
#dateRangeSelected = null;
#daysInSelectedWeek = null;
// Boolean
#isCreatingSchedule = false;
// Others
#rightMouse = null;
#cancelButton = null;
#confirmButton = null;
#tableInfoDiv = null;
/**
* Creates a new P48Schedule instance and initializes calendar state,
* event handlers and UI elements.
*/
constructor()
{
this.#dateRangeSelected = new Map();
this.#daysInSelectedWeek = new Set();
this.#tableInfoDiv = document.querySelector("#table-info");
this.#initializeCalendar();
this.#setupRightMouseHandler();
this.#setupConfirmButton();
this.#setupCancelButton();
}
// Method for initializing the calendar
#initializeCalendar()
{
let calendarEle = document.querySelector("#calendar");
this.#calendar = new FullCalendar.Calendar(calendarEle, {
// Some of these are easy to explain
initialView: "dayGridMonth",
editable: true,
droppable: true,
selectable: true,
firstDay: 1,
eventOrder: "-type",
headerToolbar: this.#initializeToolBar(),
buttonText: this.#changeViewModeText(),
dateClick: (info) => this.#initializeDateClick(info),
dayCellClassNames: (info) => this.#initializeDayCellClassNames(info),
eventClick: (info) => this.#initializeEventClick(info)
})
this.#calendar.render();
}
// PRIVATE METHODS
// Method for event clicking
#initializeEventClick(eventClickInfo)
{
//activeEventsMap.get(eventClickInfo.event.id).tableElement.parentElement.parentElement.scrollIntoView();
const checkSectionDiv = document.querySelector(".check-section");
const overlay = document.querySelector(".overlay");
const checkSectionText = document.querySelector(".check-section").querySelector("p");
checkSectionDiv.style.display = "block";
overlay.style.display = "block";
checkSectionText.innerHTML = eventClickInfo.event.extendedProps.description;
//return this.#tableInfoDiv.innerHTML = eventClickInfo.event.extendedProps.description;
}
// Method for tool bar
#initializeToolBar()
{
// For more option, look at the docs
return {
start: "prev next today",
center: "title",
end: "timeGridDay dayGridWeek dayGridMonth dayGridYear multiMonthYear"
}
}
// Method for view modes
#changeViewModeText()
{
// Change the button text of the view modes
return {
timeGridDay: "Day View",
dayGridWeek: "Week View",
dayGridMonth: "Month View",
dayGridYear: "Year View",
multiMonthYear: "Overview"
}
}
// Left mouse clicking method
#initializeDateClick(info)
{
// If we are not creating a schedule, use the mouse short cuts
if (!this.#isCreatingSchedule)
{
if (this.#calendar.view.type === "multiMonthYear")
{
this.#calendar.changeView("dayGridYear", info.dateStr);
}
else if (this.#calendar.view.type === "dayGridYear")
{
this.#calendar.changeView("dayGridMonth", info.dateStr)
}
else if (this.#calendar.view.type === "dayGridMonth")
{
this.#calendar.changeView("dayGridWeek", info.dateStr);
}
else if (this.#calendar.view.type === "dayGridWeek")
{
this.#calendar.changeView("timeGridDay", info.dateStr);
}
}
else // Else, we are and make a start and end Date object
{
// Get the start date and end date
let start = new Date(info.date);
let end = new Date(info.date);
// Set the end date to last day of the week
end.setDate(end.getDate() + 6)
// Make a current date to get all dates between start and end
const current = new Date(start);
// Get all dates between the start and end dates and put them into the set
while (current <= end)
{
let dateToAdd = new Date(current).toDateString();
if (this.#daysInSelectedWeek.has(dateToAdd)) //Prevent duplicate events by ending date selection here
{
current.setDate(current.getDate() - 1);
end = current;
break;
}
this.#daysInSelectedWeek.add(dateToAdd);
current.setDate(current.getDate() + 1);
}
// Add both to map and render the calendar
this.#dateRangeSelected.set(start, end)
this.#calendar.render();
}
}
// Method for if there is the selected day in the set
#initializeDayCellClassNames(info)
{
return this.#daysInSelectedWeek.has(info.date.toDateString()) ? "selected-week" : ""
}
// // Using right mouse for calendar view modes (short cut)
#setupRightMouseHandler()
{
this.#rightMouse = document.querySelector("#calendar > :nth-child(2)")
this.#rightMouse.addEventListener("contextmenu", (event) =>
{
if (!this.#isCreatingSchedule)
{
if (event.button === 2)
{
event.preventDefault();
if (this.#calendar.view.type === "timeGridDay")
{
this.changeViewMode("dayGridWeek");
}
else if (this.#calendar.view.type === "dayGridWeek")
{
this.changeViewMode("dayGridMonth");
}
else if (this.#calendar.view.type === "dayGridMonth")
{
this.changeViewMode("dayGridYear");
}
else if (this.#calendar.view.type === "dayGridYear")
{
this.changeViewMode("multiMonthYear");
}
}
}
})
}
// To set all variables and set the mode to not creating events
#resetCalendar()
{
this.setIsCreatingSchedule(false);
this.#dateRangeSelected.clear();
this.#daysInSelectedWeek.clear();
this.setOptionForCalendar("weekNumbers", false);
closestTable = null;
closestStationName = null;
}
// When user clicks confirm button
#setupConfirmButton()
{
this.#confirmButton = document.querySelector("#confirm");
this.#confirmButton.addEventListener("click", () =>
{
// If we are creating the schedule
if (this.#isCreatingSchedule)
{
// Get all rows of table and make array
const rows = closestTable.querySelectorAll("tr > td:first-child");
const textareas = []
// If the rows contain a textarea, add it to array
for (let i = 0; i < rows.length - 2; i++)
{
if (rows[i].querySelector("textarea"))
{
textareas.push(rows[i].querySelector("textarea"));
}
}
// Make index variable
let index = 0;
// Make variable told hold each row text
let rowsText = "";
// For each textarea
for (let i = 0; i < textareas.length; i++)
{
rowsText += textareas[i].value + "<br>";
}
// Go through the map
for (const [start, end] of this.#dateRangeSelected)
{
// Get the text area row from the array and add it to the calendar
// const textarea = textareas[i];
// Make a temp date that actually includes the last date
const actualEnd = new Date(end);
actualEnd.setDate(actualEnd.getDate() + 1)
let event = this.createEventBlock(
closestStationName.value + ": " + closestTableName.value,
start,
actualEnd,
closestColorPicker.value,
true,
rowsText
)
let tableInstance = activeScheduleTables.get(closestTable.parentElement);
activeEventsMap.set(event.id, tableInstance);
tableInstance.events.add(event);
index++;
}
this.#resetCalendar()
}
})
}
// When user clicks cancel
#setupCancelButton()
{
this.#cancelButton = document.querySelector("#cancel")
this.#cancelButton.addEventListener("click", () => this.#resetCalendar())
}
// Code is commented for now
#changeCalendarDateRange()
{
// let startDate = document.querySelector("#campaign-start");
// let endDate = document.querySelector("#campaign-end");
// function updateDateRange()
// {
// calendar.setOption("validRange", {
// start: startDate.value || undefined,
// end: endDate.value || undefined
// })
// }
// startDate.addEventListener("change", updateDateRange);
// endDate.addEventListener("change", updateDateRange)
}
// UUID generation magic that doesn't require a secure context
#generateEventUUID()
{
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ window.crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
// PUBLIC METHODS
/**
* Changes an option of the calendar after initialization.
*
* This method takes in two parameters. The first parameter is the name of
* the calendar option that you want to change. The second option is to
* what you want to change it to.
*
* @param {string} optionName The calendar option you want to change
* @param {*} value The value you want to assign the first parameter
*/
setOptionForCalendar(optionName, value)
{
this.#calendar.setOption(optionName, value)
}
/**
* Changes the view mode of the calendar after initialization.
*
* This method takes in one parameter. That parameter is the view mode you want
* the calendar to show.
*
* There is a list of view modes that work for this calendar.
* Some of those include timeGridDay dayGridWeek dayGridMonth dayGridYear multiMonthYear
*
* @param {string} mode The view mode the calendar switches to
* @throws {Error} Throws if the value passed is not a string
*/
changeViewMode(mode)
{
if (typeof mode !== "string") {throw new Error("Value passed is not a string")}
this.#calendar.changeView(mode);
}
/**
* Sets the isCreatingSchedule boolean to the value given.
*
* Only works with boolean, nothing else.
*
* @param {boolean} bool The value the isCreatingSchedule will be set to.
* @throws {Error} Throws if the value passed is not a boolean
*/
setIsCreatingSchedule(bool)
{
if (typeof bool !== "boolean") {throw new Error("Value given is not boolean.")}
if (bool)
{
this.#confirmButton.style.display = "block";
this.#cancelButton.style.display = "block";
} else {
this.#confirmButton.style.display = "none";
this.#cancelButton.style.display = "none";
}
this.#isCreatingSchedule = bool;
}
/**
* Returns whether the client is making a schedule
*
* @returns {boolean} True if the client is making a schedule. Otherwise false.
*/
getIsCreatingSchedule()
{
return this.#isCreatingSchedule;
}
/**
* Creates an event block and places it on the calendar.
*
* There are a lot of parameters that can be passed through.
*
* If a specific parameter is not wanted, simply pass in undefined to give a default
* value for that parameter. Default values are explained in each parameter description
*
* This method requires at least a two Date objects for the event
* to be placed in the calendar. If either or both is not passed, It will throw an error
*
* @param {string} titleEvent The title of the event. If none is given, the title will be "Event"
* @param {Date} startDate A Date object that represents the start of the event.
* @param {Date} endDate A Date object that represents the end of the event
* @param {string} color A color for the event as a string. An input type color element with .value property will also work. if none is given, it will default to a blue color (#3788d8)
* @param {boolean} allDay A boolean if the event will be all day or not, if the boolean is not true, it will default to false and the time will be 1 hour
* @param {string} descriptionOfEvent A description of the event. If none is given, it will default to a empty string
* @returns A FullCalendar Event object representing the event that was added
* @throws {Error} If the start and endDate variables are null or if they are not an instance of Date
*/
createEventBlock(titleEvent, startDate, endDate, color, allDay, descriptionOfEvent)
{
if (startDate == null || !(startDate instanceof Date) ) {throw new Error("The start date is not a Date object or is null")}
if (endDate == null || !(endDate instanceof Date)) {throw new Error("The end date is not a Date object or is null")}
return this.#calendar.addEvent({
title: titleEvent ?? "Event",
start: startDate.toISOString(),
end: endDate.toISOString(),
color: color ?? "#3788d8",
allDay: allDay ?? false,
id: this.#generateEventUUID(),
extendedProps:
{
description: descriptionOfEvent ?? ""
}
})
}
}