Source: page_handler.js

/** 
 * This file handles all logic that involves the UI.
 * 
 * @module scripts/page_handler
 */

/**
 * Class for handling all UI modifications and updates.
 * 
 * @class PageHandler
 */
class PageHandler {

    ERROR_TIMEOUT_DURATION = 3000;
    ERROR_ANIMATION_DURATION = 500;

    ERROR_TIMEOUT_NAME = "error_timeout";
    ERROR_ANIMATION_NAME = "error_animation";

    BOOLEAN_TRUE_NAME = "true";
    BOOLEAN_FALSE_NAME = "false";

    DATA_TAG_QUERY_FORMAT = (name) => `[data-${name}]`;
    DATA_TAG_NAME = "data-";
    DATA_TAG_ARGS_BLACKLIST = [ "event" ];

    DATA_EVENT_TAG_NAME = "event";
    DATA_TOGGLED_TAG_NAME = "toggled";

    DATA_SETTINGS_TAG_NAME = "settings";
    DATA_RUN_TAG_NAME = "run";
    DATA_CHANGE_TAG_NAME = "change";
    DATA_FRAME_TAG_NAME = "frame";
    DATA_SETTINGS_FRAME_NAME = "settings-frame";
    DATA_VERSIONS_FRAME_NAME = "versions-frame";
    DATA_PROJECTS_FRAME_NAME = "projects-frame";

    DATA_LOADED_TAG_NAME = "loaded";

    DATA_PROJECT_NAME_TAG_NAME = "project-name";
    DATA_VERSION_ID_TAG_NAME = "version-id";

    CLASS_NAME_QUERY_FORMAT = (name) => `.${name}`;
    ERRORS_CLASS_NAME = "errors";
    ERRORS_INFO_CLASS_NAME = "info";
    ERRORS_OUT_CLASS_NAME = "out";
    DEBUG_TOGGLE_CLASS_NAME = "on";
    FRAME_OPEN_CLASS_NAME = "frame-open";

    GLYPHICON_CLASS_NAME = "glyphicon";

    VERSION_FRAME_TITLE_FORMAT = (name) => `Versions (${name})`;
    FRAME_TOP_BAR_CLASS_NAME = "top-bar";

    CONSOLE_CONTAINER_CLASS_NAME = "console-wrapper";

    SETTINGS_FRAME_CONTENT_CLASS_NAME = "settings-container";
    SETTINGS_ELEMENT_MAIN_CLASS_NAME = "settings-main";
    CURRENT_PROJECT_CONTAINER_CLASS_NAME = "current-name";
    PROJECTS_FRAME_CONTENT_CLASS_NAME = "frame-content-wrapper";
    PROJECTS_ELEMENT_MAIN_CLASS_NAME = "project-container";
    VERSIONS_CONTAINER_CONTENT_CLASS_NAME = "version-container";
    VERSIONS_ELEMENT_MAIN_CLASS_NAME = "version-main";
    VERSIONS_ELEMENT_FAVORITED_CLASS_NAME = "glyphicon-star";
    VERSIONS_ELEMENT_UNFAVORITED_CLASS_NAME = "glyphicon-star-empty";

    CONSOLE_MESSAGE_MAIN_CLASS_NAME = "message";

    PROJECT_TOGGLE_CLASS_NAME = "opened";

    ANIM_LENGTH_CSS_VARIABLE = "--animLength";
    NUM_VERSIONS_CSS_VARIABLE = "--numVersions";

    VERSION_LIMIT_NAME = "Set max. number of versions per project: ";
    NUMBER_TYPE_INPUT = "number";
    VERSION_LIMIT_VALNAME = "version_max";
    VERSION_LIMIT_TITLE = "Set Version Limit";
    VERSION_LIMIT_INPUT_MIN = 1;
    VERSION_LIMIT_INPUT_MAX = 1000;

    VERSION_FAVORITE_TITLE = "Favorite Version";
    VERSION_UNFAVORITE_TITLE = "Unfavorite Version";

    /**
     * Constructs a new Page Handler.
     * 
     * @constructor
     */
    constructor() {

        this.errorQueue = [];

        this.timers = {};

        this.DATA_FRAME_EVENT_MAP = {};
        this.DATA_FRAME_EVENT_MAP[this.DATA_SETTINGS_TAG_NAME] = this.DATA_SETTINGS_FRAME_NAME;
        this.DATA_FRAME_EVENT_MAP[this.DATA_RUN_TAG_NAME] = this.DATA_VERSIONS_FRAME_NAME;
        this.DATA_FRAME_EVENT_MAP[this.DATA_CHANGE_TAG_NAME] = this.DATA_PROJECTS_FRAME_NAME;

        this.settings = [];
        this.settings.push(new Setting(this.VERSION_LIMIT_NAME, this.NUMBER_TYPE_INPUT, this.VERSION_LIMIT_VALNAME, 
            this.VERSION_LIMIT_TITLE, this.VERSION_LIMIT_INPUT_MIN, this.VERSION_LIMIT_INPUT_MAX
        ));

        /**
         * Opens the settings frame and fills with fetched data if needed.
         * 
         * @function openSettingsFrame
         * 
         * @param {object} data - Either null or holds the settings data to be rendered.
         */
        this.openSettingsFrame = (data) => {

            let frameData = {};
            frameData[this.DATA_EVENT_TAG_NAME] = this.DATA_SETTINGS_TAG_NAME;

            const frame = this.getFrame(frameData);

            if (data != null) {

                let frameContentContainer = frame.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.SETTINGS_FRAME_CONTENT_CLASS_NAME));
                this.clearChildren(frameContentContainer);

                for (let i = 0; i < this.settings.length; i ++) {

                    this.settings[i].val = data[this.settings[i].valName];
                    let newSettingElement = this.buildSettingsElement(this.settings[i]);
                    frameContentContainer.appendChild(newSettingElement);

                }

            }

            this.openFrame(frame);
            this.setFrameLoaded(frame);

        }

        /**
         * Constructs a new settings HTML element.
         * 
         * @function buildSettingsElement
         * 
         * @param {object} data - Holds the settings name, type, and so on for creating a settings element.
         * 
         * @returns {HTMLElement} The newly created settings element.
         */
        this.buildSettingsElement = (data) => {

            const mainElement = document.createElement('div');
            mainElement.classList.add(this.SETTINGS_ELEMENT_MAIN_CLASS_NAME);

            let internalElements = 
            `<div class="settings-name" title="${data.name}">
                <div>${data.name}</div>
            </div>
            <input class="settings-input" type="${data.type}" name="${data.valName}" title="${data.title}" min="${data.min}" max="${data.max}" value="${data.val}">`;

            mainElement.innerHTML = internalElements;

            return mainElement;

        }

        /**
         * Opens the projects frame and fills with fetched data if needed.
         * 
         * @function openProjectsFrame
         * 
         * @param {object} data - Either null or holds the project data to be rendered.
         */
        this.openProjectsFrame = (data) => {

            let frameData = {};
            frameData[this.DATA_EVENT_TAG_NAME] = this.DATA_CHANGE_TAG_NAME;

            const frame = this.getFrame(frameData);

            if (data != null) {

                let frameContentContainer = frame.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.PROJECTS_FRAME_CONTENT_CLASS_NAME));
                this.clearChildren(frameContentContainer);

                for (let proj of data) {

                    this.addProjectElement(proj, frame);

                }

            }

            this.openFrame(frame);
            this.setFrameLoaded(frame);

        }

        /**
         * Constructs a new project HTML element.
         * 
         * @function buildProjectElement
         * 
         * @param {object} data - Holds the project name and number of versions for creating a project element.
         * 
         * @returns {HTMLElement} The newly created project element.
         */
        this.buildProjectElement = (data) => {

            const mainElement = document.createElement('div');
            mainElement.classList.add(this.PROJECTS_ELEMENT_MAIN_CLASS_NAME);

            mainElement.setAttribute(this.formatDataTagName(this.DATA_PROJECT_NAME_TAG_NAME), data.name);

            let numVersions = 0;

            if (data.versions != null) {
                numVersions = data.versions.length;
            }

            let internalElements = 
            `<div class="project-main">

                <div class="project-data">
                    <div class="data-name" title="${data.name}" data-event="select_project">${data.name}</div>
                    <div class="icon-container"><div class="glyphicon glyphicon-menu-right" title="Toggle Project" data-event="toggle"></div></div>
                </div>

                <div class="icon-container"><div class="glyphicon glyphicon-trash" title="Delete Project" data-event="delete_project"></div></div>

            </div>

            <div style="--numVersions: ${numVersions};" class="version-container"></div>`;

            mainElement.innerHTML = internalElements;

            return mainElement;

        }

        /**
         * Constructs a new version HTML element for the projects frame.
         * 
         * @function buildProjectVersionElement
         * 
         * @param {object} data - Holds the version id, name, date and so on for creating a version element for the projects frame.
         * 
         * @returns {HTMLElement} The newly created version element.
         */
        this.buildProjectVersionElement = (data) => {

            const mainElement = document.createElement('div');
            mainElement.classList.add(this.VERSIONS_ELEMENT_MAIN_CLASS_NAME);

            mainElement.setAttribute(this.formatDataTagName(this.DATA_VERSION_ID_TAG_NAME), data.v_id);

            let favoriteClassName = (data.favorite) ? this.VERSIONS_ELEMENT_FAVORITED_CLASS_NAME : this.VERSIONS_ELEMENT_UNFAVORITED_CLASS_NAME;
            let favoriteTitle = (!data.favorite) ? this.VERSION_FAVORITE_TITLE : this.VERSION_UNFAVORITE_TITLE;

            let internalElements = 
            `<div class="version-data nobord-t">
                <div class="data-name" title="${data.name}">${data.name}</div>
                <div class="version-data-right">
                    <div class="data-date" title="${data.date}">${data.date}</div>
                    <div class="icon-container"><div class="glyphicon ${favoriteClassName}" title="${favoriteTitle}" data-event="favorite"></div></div>
                </div>
            </div>

            <div class="icon-container"><div class="glyphicon glyphicon-trash" title="Delete Version" data-event="delete_version"></div></div>`;

            mainElement.innerHTML = internalElements;

            return mainElement;

        }

        /**
         * Opens the versions frame and fills with fetched data if needed.
         * 
         * @function openVersionsFrame
         * 
         * @param {object} data - Either null or holds the version data to be rendered.
         */
        this.openVersionsFrame = (versionData, currProjData) => {

            let frameData = {};
            frameData[this.DATA_EVENT_TAG_NAME] = this.DATA_RUN_TAG_NAME;

            const frame = this.getFrame(frameData);

            if (versionData != null) {

                let frameContentContainer = frame.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.VERSIONS_CONTAINER_CONTENT_CLASS_NAME));
                this.clearChildren(frameContentContainer);
                
                for (let ver of versionData) {

                    let versionElement = this.buildVersionElement(ver);
                    frameContentContainer.appendChild(versionElement);

                }

            }

            if (currProjData != null) {

                let topBarTitle = frame.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.FRAME_TOP_BAR_CLASS_NAME)).querySelector('div');
                topBarTitle.innerText = this.VERSION_FRAME_TITLE_FORMAT(currProjData.name);

            }

            this.openFrame(frame);
            this.setFrameLoaded(frame);
                
        }

        /**
         * Constructs a new version HTML element for the versions frame.
         * 
         * @function buildVersionElement
         * 
         * @param {object} data - Holds the version id, name, date for creating a version element for the versions frame.
         * 
         * @returns {HTMLElement} The newly created version element.
         */
        this.buildVersionElement = (data) => {

            const mainElement = document.createElement('div');
            mainElement.classList.add(this.VERSIONS_ELEMENT_MAIN_CLASS_NAME);

            mainElement.setAttribute(this.formatDataTagName(this.DATA_VERSION_ID_TAG_NAME), data.v_id);

            let internalElements = 
            `<div class="version-data nobord-t">
                <div class="data-name" title="${data.name}" data-event="select_version">${data.name}</div>
                <div class="version-data-right">
                    <div class="data-date" title="${data.date}">${data.date}</div>
                </div>
            </div>`;

            mainElement.innerHTML = internalElements;

            return mainElement;

        }


        /**
         * Closes the settings frame.
         * 
         * @function closeSettingsFrame
         */
        this.closeSettingsFrame = () => {

            let frameData = {};
            frameData[this.DATA_EVENT_TAG_NAME] = this.DATA_SETTINGS_TAG_NAME;

            const frame = this.getFrame(frameData);

            frame.classList.remove(this.FRAME_OPEN_CLASS_NAME);

        }

        /**
         * Closes the projects frame.
         * 
         * @function closeProjectsFrame
         */
        this.closeProjectsFrame = () => {

            let frameData = {};
            frameData[this.DATA_EVENT_TAG_NAME] = this.DATA_CHANGE_TAG_NAME;

            const frame = this.getFrame(frameData);

            frame.classList.remove(this.FRAME_OPEN_CLASS_NAME);

        }

        /**
         * Closes the versions frame.
         * 
         * @function closeVersionsFrame
         */
        this.closeVersionsFrame = () => {

            let frameData = {};
            frameData[this.DATA_EVENT_TAG_NAME] = this.DATA_RUN_TAG_NAME;

            const frame = this.getFrame(frameData);

            frame.classList.remove(this.FRAME_OPEN_CLASS_NAME);

        }


        /**
         * Deletes a certain project from the projects frame.
         * 
         * @function deleteProject
         * 
         * @param {object} data        - Holds the data of the project to be deleted, so the name.
         * @param {boolean} wasCurrent - Says whether the deleted project was the current project.
         */
        this.deleteProject = (data, wasCurrent) => {

            let projectContainer = this.queryElementsWithDataAttribute(this.DATA_PROJECT_NAME_TAG_NAME, data.name)[0];

            projectContainer.remove();

            if (wasCurrent) {

                this.selectProject(null);

            }

        }

        /**
         * Adds a new version to the projects and versions frames.
         * 
         * @function addVersion
         * 
         * @param {object} versionData  - Holds the data of the version to be added, so the id, name, date and so on.
         * @param {object} currProjData - Holds the data of the current project, so the name.
         * 
         * @returns {HTMLElement} The newly created version element from the projects frame.
         */
        this.addVersion = (versionData, currProjData) => {

            let addedVersion = versionData[0];
            let removedVersion = versionData[1];

            let projectElement = this.queryElementsWithDataAttribute(this.DATA_PROJECT_NAME_TAG_NAME, currProjData.name)[0];
            let projectVersionContentContainer = projectElement.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.VERSIONS_CONTAINER_CONTENT_CLASS_NAME));

            let frameData = {};
            frameData[this.DATA_EVENT_TAG_NAME] = this.DATA_RUN_TAG_NAME;

            const frame = this.getFrame(frameData);

            let versionContentContainer = frame.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.VERSIONS_CONTAINER_CONTENT_CLASS_NAME));

            let newProjectVersionElement = this.buildProjectVersionElement(addedVersion);
            let newVersionElement = this.buildVersionElement(addedVersion);

            projectVersionContentContainer.prepend(newProjectVersionElement);
            versionContentContainer.prepend(newVersionElement);

            this.increaseNumVersions(newProjectVersionElement, 1);

            if (removedVersion != null) {

                this.deleteVersionElements(removedVersion);
                this.increaseNumVersions(newProjectVersionElement, -1);

            }

            return newProjectVersionElement;

        }

        /**
         * Decreases the ammount of versions of a project and deletes the version.
         * 
         * @function deleteVersion
         * 
         * @param {object} data        - Holds the data of the version to be deleted, id, name, date and so on.
         * @param {HTMLElement} target - Is the targeted UI element that was clicked.
         */
        this.deleteVersion = (data, target) => {

            this.increaseNumVersions(target, -1);
            this.deleteVersionElements(data);

        }

        /**
         * Increases or decreases the number of versions stored in a project element on the page.
         * 
         * @function increaseNumVersions
         * 
         * @param {HTMLElement} target - Is the targeted UI element that was clicked.
         * @param {number} num         - Is the ammount, the number of versions should be increased or decreased to.
         */
        this.increaseNumVersions = (target, num) => {

            let versionContainer = this.getParentWithClassName(this.VERSIONS_CONTAINER_CONTENT_CLASS_NAME, target);
            let currentNumVersions = getComputedStyle(versionContainer).getPropertyValue(this.NUM_VERSIONS_CSS_VARIABLE);

            versionContainer.style.setProperty(this.NUM_VERSIONS_CSS_VARIABLE, parseInt(currentNumVersions) + num);

        }

        /**
         * Deletes a certain version from the projects and versions frames.
         * 
         * @function deleteVersionElements
         * 
         * @param {object} data - Holds the version id, name, date for removing the version element.
         */
        this.deleteVersionElements = (data) => {

            let versionElements = this.queryElementsWithDataAttribute(this.DATA_VERSION_ID_TAG_NAME, data.v_id);

            for (const versionElem of versionElements) {
                versionElem.remove();
            }

        }

        /**
         * Favorites a certain version on the projects frame.
         * 
         * @function favoriteVersion
         * 
         * @param {object} data - Holds the version id, name, date of the version that is to be favorited.
         */
        this.favoriteVersion = (data) => {

            let versionElement = this.queryElementsWithDataAttribute(this.DATA_VERSION_ID_TAG_NAME, data.v_id)[0];
            let favoriteElement = versionElement.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.GLYPHICON_CLASS_NAME));

            if (this.getIsFavorited(favoriteElement)) {

                favoriteElement.classList.remove(this.VERSIONS_ELEMENT_FAVORITED_CLASS_NAME);
                favoriteElement.classList.add(this.VERSIONS_ELEMENT_UNFAVORITED_CLASS_NAME);

                favoriteElement.title = this.VERSION_FAVORITE_TITLE;

            } else {

                favoriteElement.classList.remove(this.VERSIONS_ELEMENT_UNFAVORITED_CLASS_NAME);
                favoriteElement.classList.add(this.VERSIONS_ELEMENT_FAVORITED_CLASS_NAME);

                favoriteElement.title = this.VERSION_UNFAVORITE_TITLE;

            }

        }

        /**
         * Checks whether a given version is favorited or not.
         * 
         * @function getIsFavorited
         * 
         * @param {HTMLElement} data - Is the version element itself or one of its children.
         * 
         * @returns {boolean} Whether the version element is favorited or not.
         */
        this.getIsFavorited = (data) => {

            let versionMain = this.getParentWithDataAttribute(this.DATA_VERSION_ID_TAG_NAME, data);
            let favoriteElement = versionMain.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.GLYPHICON_CLASS_NAME));

            return favoriteElement.classList.contains(this.VERSIONS_ELEMENT_FAVORITED_CLASS_NAME);

        }

        /**
         * Retrieves the id of a version stored in the version element.
         * 
         * @function getVersionId
         * 
         * @param {HTMLElement} target - Is the targeted UI element that was clicked.
         * 
         * @returns {string} The version id stored in the targeted element.
         */
        this.getVersionId = (target) => {

            let nameTagName = this.DATA_VERSION_ID_TAG_NAME;
            let versionMain = this.getParentWithDataAttribute(nameTagName, target);

            return this.getDataAttributes(versionMain)[nameTagName];

        }

        /**
         * Retrieves the name of a project stored in the project element.
         * 
         * @function getProjectName
         * 
         * @param {HTMLElement} target - Is the targeted UI element that was clicked.
         * 
         * @returns {object} The data attributes stored in the target element with their names.
         */
        this.getProjectName = (target) => {

            let nameTagName = this.DATA_PROJECT_NAME_TAG_NAME;
            let projectContainer = this.getParentWithDataAttribute(nameTagName, target);

            return this.getDataAttributes(projectContainer)[nameTagName];

        }

        /**
         * Selects and adds a new project.
         * 
         * @function addNewProject
         * 
         * @param {object} data - Holds the data of the project to be added, so the name, date and its versions.
         */
        this.addNewProject = (data) => {

            this.selectProject(data);

            let frameData = {};
            frameData[this.DATA_EVENT_TAG_NAME] = this.DATA_CHANGE_TAG_NAME;

            const frame = this.getFrame(frameData);

            this.addProjectElement(data, frame, true);

        }

        /**
         * Adds a new project element to the projects frame.
         * 
         * @function addProjectElement
         * 
         * @param {object} data       - Holds the data of the project to be added, so the name, date and its versions.
         * @param {HTMLElement} frame - Is the frame element the project element should be added to.
         * @param {boolean} addFront  - Says whether to add the project to the front or back of the projects.
         */
        this.addProjectElement = (data, frame, addFront=false) => {

            let frameContentContainer = frame.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.PROJECTS_FRAME_CONTENT_CLASS_NAME));
            let newProjectElement = this.buildProjectElement(data);

            if (data.versions != null) {

                let versionContentContainer = newProjectElement.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.VERSIONS_CONTAINER_CONTENT_CLASS_NAME));

                for (let ver of data.versions) {

                    let newVersionElement = this.buildProjectVersionElement(ver);
                    versionContentContainer.appendChild(newVersionElement);

                }

            }

            if (addFront) {

                frameContentContainer.prepend(newProjectElement);

            } else {

                frameContentContainer.appendChild(newProjectElement);

            }

        }

        /**
         * Selects a certain project to be the new current project.
         * 
         * @function selectProject
         * 
         * @param {object} data - Holds the data of the project to be selected, so the name, date and its versions.
         */
        this.selectProject = (data) => {

            let frameData = {};
            frameData[this.DATA_EVENT_TAG_NAME] = this.DATA_RUN_TAG_NAME;

            const frame = this.getFrame(frameData);

            this.setFrameUnloaded(frame);
            this.updateCurrentProject(data);

        }

        /**
         * Updates the current project.
         * 
         * @function updateCurrentProject
         * 
         * @param {object} data - Holds the data of the project to be set as the new current project, so the name, date and its versions.
         */
        this.updateCurrentProject = (data) => {

            let currentNameContainer = document.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.CURRENT_PROJECT_CONTAINER_CLASS_NAME));

            let newName = "";
            let newTitle = '';

            if (data != null && data.name != null && data.name.length > 0) {

                newName = data.name;
                newTitle = data.name;

            }

            currentNameContainer.innerHTML = `<div>${newName}</div>`;
            currentNameContainer.title = newTitle;

        }

        /**
         * Collapses or opens a certain project element to hide or reveal its versions.
         * 
         * @function toggleProject
         * 
         * @param {HTMLElement} data - Is the project element itself or one of its children.
         */
        this.toggleProject = (data) => {

            let projectContainer = this.getParentWithClassName(this.PROJECTS_ELEMENT_MAIN_CLASS_NAME, data);

            if (projectContainer.classList.contains(this.PROJECT_TOGGLE_CLASS_NAME)) {

                projectContainer.classList.remove(this.PROJECT_TOGGLE_CLASS_NAME);

            } else {

                projectContainer.classList.add(this.PROJECT_TOGGLE_CLASS_NAME);

            }

        }

        /**
         * Turns the debug mode on or off and updates the debug slider.
         * 
         * @function toggleDebug
         * 
         * @param {object} data - Holds the name of the event that was called and whether the debug was toggled or not.
         */
        this.toggleDebug = (data) => {

            const debugSlider = this.queryElementsWithDataAttribute(this.DATA_EVENT_TAG_NAME, data[this.DATA_EVENT_TAG_NAME])[0];

            let isToggled = this.parseStringToBool(data[this.DATA_TOGGLED_TAG_NAME]);

            if (isToggled) {

                debugSlider.setAttribute(this.formatDataTagName(this.DATA_TOGGLED_TAG_NAME), this.BOOLEAN_FALSE_NAME);
                debugSlider.classList.remove(this.DEBUG_TOGGLE_CLASS_NAME);

            } else {

                debugSlider.setAttribute(this.formatDataTagName(this.DATA_TOGGLED_TAG_NAME), this.BOOLEAN_TRUE_NAME);
                debugSlider.classList.add(this.DEBUG_TOGGLE_CLASS_NAME);

            }

        }

        /**
         * Opens a certain frame.
         * 
         * @function openFrame
         * 
         * @param {HTMLElement} data - Is the frame element to open.
         */
        this.openFrame = (data) => {

            data.classList.add(this.FRAME_OPEN_CLASS_NAME);

        }

        /**
         * Checks whether a certain frame has been loaded yet or needs to be fetched.
         * 
         * @function getIsFrameLoaded
         * 
         * @param {object} data - Is the data of the frame, so its name.
         * 
         * @returns {boolean} Whether the frame is loaded or not.
         */
        this.getIsFrameLoaded = (data) => {

            const frame = this.getFrame(data);

            let argsFrame = this.getDataAttributes(frame);

            return this.parseStringToBool(argsFrame[this.DATA_LOADED_TAG_NAME]);

        }

        /**
         * Retrieves a certain frame based on a given name.
         * 
         * @function getFrame
         * 
         * @param {object} data - Is the data of the frame, so its name.
         * 
         * @returns {HTMLElement} The targeted frame element.
         */
        this.getFrame = (data) => {

            let frameName = this.DATA_FRAME_EVENT_MAP[data[this.DATA_EVENT_TAG_NAME]];

            return this.queryElementsWithDataAttribute(this.DATA_FRAME_TAG_NAME, frameName)[0];

        }

        /**
         * Sets a frame as loaded, so it doesnt need to be fetched again.
         * 
         * @function setFrameLoaded
         * 
         * @param {HTMLElement} data - Is the frame element to be set as loaded.
         */
        this.setFrameLoaded = (data) => {

            data.setAttribute(this.formatDataTagName(this.DATA_LOADED_TAG_NAME), this.BOOLEAN_TRUE_NAME);

        }

        /**
         * Sets a frame as unloaded, so it can to be fetched again.
         * 
         * @function setFrameUnloaded
         * 
         * @param {HTMLElement} data - Is the frame element to be set as unloaded.
         */
        this.setFrameUnloaded = (data) => {

            data.setAttribute(this.formatDataTagName(this.DATA_LOADED_TAG_NAME), this.BOOLEAN_FALSE_NAME);

        }


        /**
         * Retrieves the settings currently displayed in the settings frame.
         * 
         * @function setFrameLoaded
         * 
         * @returns {object} The settings values with their names.
         */
        this.getSettings = () => {
            
            let frameData = {};
            frameData[this.DATA_EVENT_TAG_NAME] = this.DATA_SETTINGS_TAG_NAME;

            const frame = this.getFrame(frameData);
            const inputs = frame.querySelectorAll('input');

            let settingsData = {};

            for (const input of inputs) {

                settingsData[input.name] = input.value;

            }

            return settingsData;

        }

        /**
         * Updates the main console with a message.
         * 
         * @function updateConsole
         * 
         * @param {string} data - Is the message to be displayed on the console.
         */
        this.updateConsole = (data) => {

            let newConsoleMessage = this.buildConsoleMessage(data);
            let consoleContainer = this.getConsoleContainer();

            consoleContainer.appendChild(newConsoleMessage);
            newConsoleMessage.scrollIntoView();

        }

        /**
         * Constructs a new message element for the console.
         * 
         * @function buildConsoleMessage
         * 
         * @param {string} data - Is the message to be displayed on the console.
         * 
         * @returns {HTMLElement} The newly created console message element.
         */
        this.buildConsoleMessage = (data) => {

            const mainElement = document.createElement('div');
            mainElement.classList.add(this.CONSOLE_MESSAGE_MAIN_CLASS_NAME);

            let currentTime = new Date();

            let year = this.formatTimeString(currentTime.getFullYear());
            let month = this.formatTimeString(currentTime.getMonth() + 1);
            let day = this.formatTimeString(currentTime.getDate());
            let hour = this.formatTimeString(currentTime.getHours());
            let minute = this.formatTimeString(currentTime.getMinutes());
            let second = this.formatTimeString(currentTime.getSeconds());

            let internalElements =
            `<div class="time">[${year}-${month}-${day}-${hour}:${minute}:${second}]: </div>
            <div class="content">${data}</div>`;

            mainElement.innerHTML = internalElements;

            return mainElement;

        }

        /**
         * Clears all messages in the console.
         * 
         * @function clearConsole
         */
        this.clearConsole = () => {

            this.clearChildren(this.getConsoleContainer());

        }

        /**
         * Format a certain time number.
         * 
         * @function formatTimeString
         * 
         * @param {number} num - Is the time number to format.
         * 
         * @returns {string} The formatted time number as a string.
         */
        this.formatTimeString = (num) => {

            let strNum = num.toString();

            if (strNum.length < 2) {

                strNum = "0" + strNum;

            }

            return strNum;

        }

        /**
         * Retrieves the container that holds all console messages.
         * 
         * @function getConsoleContainer
         * 
         * @returns {HTMLElement} The container that holds the console messages.
         */
        this.getConsoleContainer = () => {

            return document.querySelector(this.CLASS_NAME_QUERY_FORMAT(this.CONSOLE_CONTAINER_CLASS_NAME));

        }



        /**
         * Adds an error or message to the queue and displays it if no error is currently being displayed.
         * 
         * @function addError
         * 
         * @param {string} msg - The message to display.
         * @param {boolean} isError - Whether the message is an error or just information.
         */
        this.addError = (msg, isError=true) => {

            this.errorQueue.push({ "message": msg, "isError": isError });

            if (this.timers[this.ERROR_TIMEOUT_NAME] == null 
                && this.timers[this.ERROR_ANIMATION_NAME] == null) this.showError();

        }

        /**
         * Displays the next error or message in the queue on the page.
         * 
         * @function showError
         */
        this.showError = () => {

            if (this.errorQueue.length == 0) return; 

            let error = this.errorQueue.shift();
            let errorMessage = error.message;
            
            const errorElement = document.createElement('div');
            errorElement.classList.add(this.ERRORS_CLASS_NAME);
            errorElement.innerText = errorMessage;
            errorElement.style.setProperty(this.ANIM_LENGTH_CSS_VARIABLE, `${this.ERROR_ANIMATION_DURATION}ms`);

            if (!error.isError) {

                errorElement.classList.add(this.ERRORS_INFO_CLASS_NAME);

            }

            document.body.appendChild(errorElement);

            this.timers[this.ERROR_TIMEOUT_NAME] = setTimeout(() => {

                errorElement.classList.add(this.ERRORS_OUT_CLASS_NAME);

                this.timers[this.ERROR_ANIMATION_NAME] = setTimeout(() => {

                    errorElement.remove();

                    this.timers[this.ERROR_ANIMATION_NAME] = null;

                    if (this.errorQueue.length != 0) this.showError();

                }, this.ERROR_ANIMATION_DURATION);

                this.timers[this.ERROR_TIMEOUT_NAME] = null;

            }, this.ERROR_TIMEOUT_DURATION);

        }

        /**
         * Formats a data tag name to match format "data-" + name
         * 
         * @function formatDataTagName
         * 
         * @param {string} name - The data tag name.
         * 
         * @returns {string} The formatted data tag name.
         */
        this.formatDataTagName = (name) => {

            return this.DATA_TAG_NAME + name;

        }

        /**
         * Parses a string to a boolean.
         * 
         * @function parseStringToBool
         * 
         * @param {string} string - The string to parse.
         * 
         * @returns {boolean} The parsed string as a boolean.
         */
        this.parseStringToBool = (string) => {

            return (string == this.BOOLEAN_TRUE_NAME);

        }

        /**
         * Retrieves all data attributes of an HTML element.
         * 
         * @function getDataAttributes
         * 
         * @param {HTMLElement} element - The element to retrieve the attributes of.
         * 
         * @returns {object} The data attributes with their names.
         */
        this.getDataAttributes = (element) => {

            let attributes = element.attributes;
            let dataAttributes = {};

            for (let attribute of attributes) {

                if (attribute.name.startsWith(this.DATA_TAG_NAME)) {

                    let dataName = attribute.name.substring(this.DATA_TAG_NAME.length);
                    dataAttributes[dataName] = attribute.value;

                }

            }

            return dataAttributes;

        }

        /**
         * Retrieves all HTML elements with a certain data tag with a certain value.
         * 
         * @function queryElementsWithDataAttribute
         * 
         * @param {string} attrName  - The data attribute to query for.
         * @param {string} attrValue - The value the attribute.
         * 
         * @returns {Array<HTMLElement>} The found HTML elements.
         */
        this.queryElementsWithDataAttribute = (attrName, attrValue=null) => {

            const elements = document.querySelectorAll(this.DATA_TAG_QUERY_FORMAT(attrName));
            
            if (attrValue == null) return elements;

            let targetElements = [];

            for (const element of elements) {

                if (attrValue == null && element.hasAttribute(this.formatDataTagName(attrName))) {

                    targetElements.push(element);

                } else if (element.getAttribute(this.formatDataTagName(attrName)) == attrValue) {

                    targetElements.push(element);

                }

            }

            return targetElements;

        }

        /**
         * Retrieves a parent HTML element which has a certain data attribute.
         * 
         * @function getParentWithDataAttribute
         * 
         * @param {string} attrName     - The data attribute to query for.
         * @param {HTMLElement} element - The HTML element to start at.
         * 
         * @returns {HTMLElement} The found HTML parent.
         */
        this.getParentWithDataAttribute = (attrName, element) => {

            let parent = element.parentElement;

            while (parent != null) {

                if (parent.getAttribute(this.formatDataTagName(attrName)) != null) return parent;

                parent = parent.parentElement;

            }

            return null;

        }

        /**
         * Retrieves a parent HTML element which has a certain class.
         * 
         * @function getParentWithClassName
         * 
         * @param {string} name         - The class name attribute to query for.
         * @param {HTMLElement} element - The HTML element to start at.
         * 
         * @returns {HTMLElement} The found HTML parent.
         */
        this.getParentWithClassName = (name, element) => {

            let parent = element.parentElement;

            while (parent != null) {

                if (parent.classList.contains(name)) return parent;

                parent = parent.parentElement;

            }

            return null;

        }

        /**
         * Clears all children of a certain HTML element.
         * 
         * @function clearChildren
         * 
         * @param {HTMLElement} element - The element to clear.
         */
        this.clearChildren = (element) => {

            while (element.firstChild) {

                element.removeChild(element.lastChild);

            }

        }

        /**
         * Requests a file from the user.
         * 
         * @function requestFile
         * 
         * @param {Function} callback - The callback to call after execution.
         */
        this.requestFile = (callback) => {

            const input = document.createElement('input');
            input.type = 'file';
            input.name = 'file';
            input.accept = '.xml';

            input.onchange = () => {

                callback(input);

            };

            input.oncancel = () => {

                callback(null);

            }

            input.click();

        }

        /**
         * Requests input from the user based off a statement.
         * 
         * @function requestInput
         * 
         * @param {Function} callback - The callback to call after execution.
         * @param {string} request    - The message to convey.
         */
        this.requestInput = (callback, request) => {

            let input = prompt(request);
            callback(input);

        }

        /**
         * Requests the user to confirm a statement.
         * 
         * @function requestConfirm
         * 
         * @param {Function} callback - The callback to call after execution.
         * @param {string} request    - The message to convey.
         */
        this.requestConfirm = (callback, request) => {

            let confirmed = confirm(request);
            callback(confirmed);

        }

    }

}

/**
 * Class for holding the attributes of a certain setting.
 * 
 * @class Setting
 */
class Setting {

    /**
     * Constructs a new Setting.
     * 
     * @constructor
     */
    constructor(name, type, valName, title, min, max, val=null) {

        this.name = name;
        this.type = type;
        this.valName = valName;
        this.title = title;
        this.min = min;
        this.max = max;
        this.val = val;

    }

}

export default PageHandler;