/**
* Destructures the columns from the DOM elements.
*
* @type {Object}
* @property {HTMLElement} todoColumn - The todo column element.
* @property {HTMLElement} progressColumn - The progress column element.
* @property {HTMLElement} feedbackColumn - The feedback column element.
* @property {HTMLElement} doneColumn - The done column element.
*/
let { todoColumn, progressColumn, feedbackColumn, doneColumn } = {};
/**
* An object representing the different states a task can be in.
*
* @type {Object}
* @property {string} todo - Represents tasks that are to be done.
* @property {string} progress - Represents tasks that are in progress.
* @property {string} feedback - Represents tasks awaiting feedback.
* @property {string} done - Represents tasks that are completed.
*/
const todoStates = {
todo: "Todo",
progress: "In Progress",
feedback: "Awaiting Feedback",
done: "Done",
};
/**
* Reference to the search task input element.
*
* @type {HTMLInputElement}
*/
let searchTodoRef;
/**
* Reference to the currently dragged element.
*
* @type {number}
*/
let currentlyDraggedElement;
/**
* The current target element being interacted with, used to determine
* whether to highlight a drag area or not.
*
* @type {HTMLElement|null}
*/
let currentTarget = null;
/**
* Generates a string that describes the number of done subtasks out of the total number of subtasks.
* @param {Object<string, import('./types').SubTask>} subTasks - Object of subtasks with their IDs as keys.
* @returns {string} The string that describes the number of done subtasks out of the total number of subtasks.
*/
function getSubtasksText(subTasks) {
const subTasksArray = objectToArray(subTasks);
const doneSubtasks = getDoneAmount(subTasks);
return `${doneSubtasks}/${subTasksArray.length} Subtasks`;
}
/**
* Calculates the progress value in percent from the given subTasks.
* The progress value is determined by the number of done subtasks divided by the total number of subtasks.
* @param {Object<string, import('./types').SubTask>} subTasks - Object of subtasks with their IDs as keys.
* @returns {number} The progress value in percent.
*/
function getProgressValueFromSubTasks(subTasks) {
const subTasksArray = objectToArray(subTasks);
const subTasksDone = getDoneAmount(subTasks);
const subTasksTotal = subTasksArray.length;
const progressValue = (subTasksDone / subTasksTotal) * 100;
return progressValue;
}
/**
* Counts the number of tasks that are marked as done in the given subTasks array.
*
* @param {Array<Object>} subTasks - The array of subTasks to count done tasks from.
* @returns {number} The number of tasks that are marked as done.
*/
function getDoneAmount(subTasks) {
const taskArray = objectToArray(subTasks);
let tasksDone = 0;
taskArray.forEach((task) => {
if (task.state) {
tasksDone++;
}
});
return tasksDone;
}
/**
* Sets the title and onclick attribute on the progress bar for each task so that it displays a tooltip with the text
* returned by getSubtasksText when clicked.
* @param {number} taskIndex - The index of the current task.
* @param {Object<string, import('./types').SubTask>} taskSubTasks - Object of subtasks with their IDs as keys.
*/
function setProgressBarTooltip(taskIndex, taskSubTasks) {
const progressBar = document.getElementById(`progress-bar-${taskIndex}`);
if (!progressBar) return;
let isTooltipVisible = false;
const subtasksText = getSubtasksText(taskSubTasks);
progressBar.title = `${subtasksText} done`;
progressBar.addEventListener("click", (event) => {
event.stopPropagation();
if (isTooltipVisible) return;
const tooltip = document.createElement("div");
tooltip.classList.add("tooltip");
tooltip.textContent = `${subtasksText} done`;
tooltip.style.top = `${event.offsetY + 15}px`;
tooltip.style.left = `${event.offsetX + 10}px`;
progressBar.appendChild(tooltip);
isTooltipVisible = true;
setTimeout(() => {
tooltip.remove();
isTooltipVisible = false;
}, 1500);
});
}
/**
* Converts a given date string to a human-readable string in the default
* locale format.
*
* @param {string} dueDate - The date string to be formatted.
* @returns {string} The formatted date string.
*/
function formatDueDate(dueDate) {
return new Date(dueDate).toLocaleDateString();
}
/**
* Returns a placeholder text for a given column element.
*
* @param {Object} columnElement - A column element from the board.
* @param {string} columnElement.id - The id of the column element.
*
* @returns {string} The placeholder text for the column.
*/
function getPlaceholderText({ id }) {
return `No tasks in ${id.split("-")[1].charAt(0).toUpperCase() + id.split("-")[1].slice(1)}`;
}
/**
* Truncates a given text string to a maximum length of 45 characters.
* If the given text is longer than 45 characters, it will be truncated
* and an ellipsis will be appended to the end to indicate that the
* text has been shortened.
*
* @param {string} text - The text to be truncated.
* @returns {string} The truncated text.
*/
function truncateText(text) {
if (typeof text !== "string" || text.length === 0) return text;
const maxLength = 45;
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
}
/**
* Sets the minimum date for the due date input to the current date, to
* prevent the user from selecting a date in the past.
*/
function restrictPastDatePick() {
const dueDateElement = document.getElementById("due-date");
if (!dueDateElement) return;
const currentDate = new Date().toISOString().split("T")[0];
dueDateElement.min = currentDate;
}
/**
* Loads an array of scripts into the document, and executes a callback
* when all have finished loading.
*
* @param {string[]} scripts - An array of script URLs to load.
* @param {Function} callback - A function to execute when all scripts have finished loading.
*/
function loadScripts(scripts, callback) {
let scriptsLoaded = 0;
scripts.forEach((src) => {
if (!document.querySelector(`script[src="${src}"]`)) {
const scriptElement = document.createElement("script");
scriptElement.src = src;
/**
* Increments the count of loaded scripts and checks if all scripts
* have been loaded. If so, executes the provided callback function.
*/
scriptElement.onload = () => {
scriptsLoaded++;
if (scriptsLoaded === scripts.length) callback();
};
document.head.appendChild(scriptElement);
} else scriptsLoaded++;
});
}
/**
* Collects the values of the various form elements and returns an object containing
* the task title, description, due date, priority and assigned members.
*
* @param {string} urgentElementId - The ID of the element representing the urgent priority.
* @param {string} mediumElementId - The ID of the element representing the medium priority.
* @param {string} lowElementId - The ID of the element representing the low priority.
* @param {string} titleId - The ID of the title input element.
* @param {string} descriptionId - The ID of the description textarea element.
* @param {string} dueDateId - The ID of the due date input element.
* @returns {Object} An object containing the task title, description, due date, priority and assigned members.
*/
function getFormData(urgentElementId, mediumElementId, lowElementId, titleId, descriptionId, dueDateId) {
const selectedPriority = getSelectedPriorityValue(urgentElementId, mediumElementId, lowElementId);
const title = getNewTitleValue(titleId);
const description = getNewDescriptionValue(descriptionId);
const dueDate = getNewDueDateValue(dueDateId);
const assignedMembers = getAssignedMembersNames();
return { title, description, dueDate, selectedPriority, assignedMembers };
}
/**
* Retrieves the names of the assigned members based on the selected options.
*
* @returns {string[]} An array of names of the assigned members.
*/
function getAssignedMembersNames() {
return selectedOptions.reduce((acc, id) => {
acc[globalContacts[id].name] = globalContacts[id];
return acc;
}, {});
}
/**
* Given the element IDs of the urgent, medium and low priority elements, returns
* the string value of the currently selected priority.
*
* @param {string} urgentElementID - The ID of the element representing the urgent priority.
* @param {string} mediumElementID - The ID of the element representing the medium priority.
* @param {string} lowElementID - The ID of the element representing the low priority.
* @returns {string|null} The string value of the currently selected priority, or null if none is selected.
*/
function getSelectedPriorityValue(urgentElementID, mediumElementID, lowElementID) {
const isUrgentActive = document.getElementById(urgentElementID).classList.contains("active");
const isMediumActive = document.getElementById(mediumElementID).classList.contains("active");
const isLowActive = document.getElementById(lowElementID).classList.contains("active");
if (isUrgentActive) return "high";
if (isMediumActive) return "medium";
if (isLowActive) return "low";
return null;
}
/**
* Retrieves the new title value from the title input field.
*
* @param {string} todoTitelID - The ID of the input field containing the new title.
* @returns {string} The value of the input field with the given ID, representing the new title of the todo.
*/
function getNewTitleValue(todoTitelID) {
return document.getElementById(todoTitelID).value;
}
/**
* Retrieves the new description value from the description textarea field.
*
* @param {string} todoDescriptionElementID - The ID of the textarea field containing the new description.
* @returns {string} The value of the textarea field with the given ID, representing the new description of the todo.
*/
function getNewDescriptionValue(todoDescriptionElementID) {
return document.getElementById(todoDescriptionElementID).value;
}
/**
* Retrieves the new due date value from the due date input field.
*
* @param {string} duoDateElementID - The ID of the input field containing the new due date.
* @returns {string} The value of the input field with the given ID, representing the new due date of the todo.
*/
function getNewDueDateValue(duoDateElementID) {
return document.getElementById(duoDateElementID).value;
}
/**
* Determines if the due date of a todo item is today or earlier, excluding completed tasks.
*
* @param {number} index - The index of the todo item in the globalTodos array.
* @returns {boolean|undefined} Returns true if the due date is today or earlier, false otherwise.
* Returns undefined if the task is completed.
*/
function isDueOrOverdue(index) {
const { date: dueDateString, state } = globalTodos[index];
if (state === "done") return;
const today = new Date();
const dueDate = new Date(dueDateString);
return (
dueDate.getFullYear() === today.getFullYear() &&
dueDate.getMonth() === today.getMonth() &&
dueDate.getDate() < today.getDate()
);
}
/**
* Clears all task cards from all columns on the board.
*
* This function is called whenever the user searches for a task, in order to clear the
* board and then render only the found tasks.
*
* @returns {void}
*/
function clearBoardColumns() {
todoColumn.innerHTML = "";
progressColumn.innerHTML = "";
feedbackColumn.innerHTML = "";
doneColumn.innerHTML = "";
}
/**
* Removes the "drag-area" class from all elements, effectively removing
* the highlighting from all drag areas on the board.
*
* @returns {void}
*/
function removeAllHighlights() {
const dragAreas = document.querySelectorAll(".drag-area");
if (!dragAreas) return;
dragAreas.forEach((dragArea) => dragArea.classList.remove("drag-area"));
}
/**
* Triggers the rendering of the entire board by clearing the columns, rendering all
* todo items and rendering all placeholder elements.
*
* @returns {void}
*/
function triggerRender() {
clearBoardColumns();
renderTodos(globalTodos);
renderAllPlaceholder();
}
/**
* Renders a hollow drag area placeholder in each column, which is used to
* indicate a place where a task card can be dragged to.
*
* @returns {void}
*/
function renderHollowPlaceholder() {
if (!todoColumn || !progressColumn || !feedbackColumn || !doneColumn) return;
const columns = [todoColumn, progressColumn, feedbackColumn, doneColumn];
columns.forEach((column) => column.insertAdjacentHTML("beforeend", getDragAreaHollowPlaceholder()));
}
/**
* Renders all placeholder elements on the board, including the regular placeholder and the hollow drag area placeholder.
*/
function renderAllPlaceholder() {
renderPlaceholder();
renderHollowPlaceholder();
}
/**
* Retrieves the column elements from the DOM and assigns them to the corresponding variables.
* The todo, progress, feedback, and done column elements are obtained using their respective IDs.
*
* @returns {void}
*/
function getBoardColumnsFromDOM() {
searchTodoRef = document.getElementById("search-task");
({ todoColumn, progressColumn, feedbackColumn, doneColumn } = getBoardColumns());
}
/**
* Returns the color of the assigned member specified by the given name.
*
* @param {string} assignedMemberName - The name of the assigned member.
* @returns {string|undefined} The color of the assigned member or undefined if no contact with the given name is found.
*/
function getAssignedMemberColor(assignedMemberName) {
const contact = globalContacts.find((contact) => contact.name === assignedMemberName);
return contact ? contact.color : undefined;
}
/**
* Clears the board columns and renders only the specified todos. Additionally, renders
* the regular placeholder and hollow drag area placeholder.
*
* @param {Todo[]} todos The list of todos to render.
* @returns {void}
*/
function renderSpecificTodos(todos) {
clearBoardColumns();
renderTodos(todos);
renderPlaceholder();
renderHollowPlaceholder();
}