notes_notes.js

import { Record } from "../backend-storage/record-class.js";
import RecordsStorage from "../backend-storage/records-api.js";
import { initMDE } from "./simpleMDE-notes.js";

// ID of the most recently accessed (clicked, edited, etc.) note element
let CURRENT_NOTE_ID = null;
let CONTENT_MDE;
// IDs of JS created elements in the note editor form
const EDITOR_FORM_ID = "note-editor";
const EDITOR_TITLE_ID = "note-editor-title";
const EDITOR_CONTENT_ID = "note-editor-content";
const EDITOR_SAVE_ID = "editor-save-btn";
const EDITOR_CANCEL_ID = "editor-cancel-btn";

/**
 * Loads note records from storage and creates note-elements to display them in notes.html
 */
function loadAllNotesFromStorage() {
    // Get container where notes are displayed
    const notesDisplay = document.querySelector(".notes-display");
    // Retrieve notes from local storage
    const notesList = RecordsStorage.getAllRecords("note");
    // Loop through retrieved notes, creating a note element for each one
    for (const noteRecord of notesList) {
        // Create and initialize new note element
        let noteElem = document.createElement("note-element");
        noteElem.id = noteRecord.id;
        noteElem.title = noteRecord.title;
        noteElem.date = noteRecord.created;
        noteElem.content = noteRecord.field1;
        // Add note element to html page
        _addFadeIn(noteElem, notesDisplay);
        // Add listeners to note element that check if it is clicked to update it
        _addListeners(noteElem);
    }
    // Don't have delete note button if there are no notes to delete.
    const deleteNoteBtn = document.getElementById("delete-note-btn");
    if (notesList.length === 0) {
        deleteNoteBtn.classList.add("hidden");
    } else {
        deleteNoteBtn.classList.remove("hidden");
    }
}

/**
 * Checks if RecordsStorage has the ID, if it does, update the RecordStorage
 * Else create a new Record and store that into RecordStorage
 */
function submitToStorage() {
    // The title of the note
    const noteTitle = document.getElementById(EDITOR_TITLE_ID);
    const noteTitleVal = noteTitle.value;
    // The textbox to enter notes in
    const noteContent = document.getElementById(EDITOR_CONTENT_ID);
    const noteContentVal = noteContent.value;
    // In local storage, update note record if it exists, otherwise create a new one
    let noteRecord;
    if (
        CURRENT_NOTE_ID != null &&
        RecordsStorage.hasRecordById(CURRENT_NOTE_ID)
    ) {
        // Retrieve the record from storage
        noteRecord = RecordsStorage.getRecordById(CURRENT_NOTE_ID);
        // Update the record's information, then update it in storage.
        noteRecord.field1 = noteContentVal;
        noteRecord.title = noteTitleVal;
        RecordsStorage.updateRecord(noteRecord);
    } else {
        // Create a new record to store the note
        noteRecord = new Record("note", {
            field1: noteContentVal,
            title: noteTitleVal,
        });
        // Create the new record in storage
        RecordsStorage.createRecord(noteRecord);
    }
}

/**
 * Given a note id, delete the note from local storage and remove it from the html page
 * @param {String | Int} noteId
 */
function deleteFromStorage(noteId) {
    // Comes in as string, so we convert to a Number
    RecordsStorage.deleteRecord(parseInt(noteId));
    const notesDisplay = document.querySelector(".notes-display");
    const noteElem = notesDisplay.querySelector(`note-element[id="${noteId}"]`);
    _removeFadeOut(noteElem);
}

/**
 * Given a note element, remove it from the page while fading it out
 * @param {Note} noteElem: custom note element - reference to the note that is being deleted
 */
function _removeFadeOut(noteElem) {
    // Fade out for 300 ms
    const milliseconds = 300;
    noteElem.style.transition = "opacity " + milliseconds + "ms ease";
    // After the note had faded out, remove it from the page
    noteElem.style.opacity = 0;
    setTimeout(function () {
        noteElem.remove();
    }, milliseconds);
}

/*
Parameters:
    - note (optional): custom note element - reference to the note that is being edited
Returns: None
*/
/**
 * Given a note element, add it to the page while fading it in
 * @param {Note} noteElem: custom note element - reference to the note that is being added
 * @param {} parent: container that the note will be added to
 */
function _addFadeIn(noteElem, parent) {
    //Add the note onto the page at opacity 0
    parent.prepend(noteElem);
    noteElem.style.opacity = 0;
    //Fade in the note over 300 ms
    const milliseconds = 300;
    noteElem.style.transition = "opacity " + milliseconds + "ms ease-in";
    //Display the note at full opacity after 300 ms
    setTimeout(function () {
        noteElem.style.opacity = 1;
    }, milliseconds);
}

/**Given a note element, display the note editor and populate it with values of a note element if one is given
 * @param {Note} noteElem custom element Note, aka "note-elements" in HTML, reference to note that is being edited
 */
function _addListeners(noteElem) {
    // When this note is clicked, update the global CURRENT_NOTE_ID variable to be this note's id
    noteElem.addEventListener("click", () => {
        CURRENT_NOTE_ID = noteElem.id;
    });
    // When note is selected with Enter, update global CURRENT_NOTE_ID variable to be note's id
    noteElem.addEventListener("keypress", (event) => {
        // Only add listener if the enter key was pressed, not some random key
        if (event.key != "Enter") {
            return;
        }
        CURRENT_NOTE_ID = noteElem.id;
    });
    // When a note is clicked, open the editor for it
    noteElem.addEventListener("click", _editCurrentNote);
    noteElem.addEventListener("keypress", _editCurrentNote);
}

// Delete the current note - to be used with add/remove event listeners
function _deleteCurrentNote(event) {
    // Only add listeners if the event was a click or Enter. If it was a random keypress, return.
    const listenerConditions =
        event.type === "click" ||
        (event.type === "keypress" && event.key === "Enter");
    if (!listenerConditions) {
        return;
    }
    // HACK (ish) - seems to be the cleanest way to do this in vanilla JS
    // Wait 0.01s for the CURRENT_NOTE_ID to be updated by the noteElem event listener that updates CURRENT_NOTE_ID
    // This allows us to have _deleteCurrentNote be a function that doesn't take in any parameters
    // so that we can use it with addEventListener and deleteEventListener to add/remove it.
    setTimeout(() => {
        deleteFromStorage(CURRENT_NOTE_ID);
    }, 10);
}

/**
 * Edit the current note (displays note editor popup)
 * To be used with add/remove event listeners
 */
function _editCurrentNote(event) {
    // Only add listeners if the event was a click or Enter. If it was a random keypress, return.
    const listenerConditions =
        event.type === "click" ||
        (event.type === "keypress" && event.key === "Enter");
    if (!listenerConditions) {
        return;
    }
    // HACK (ish) - seems to be the cleanest way to do this in vanilla JS
    // Wait 0.1s for the CURRENT_NOTE_ID to be updated by the noteElem event listener that updates CURRENT_NOTE_ID
    // This allows us to have _editCurrentNote be a function that doesn't take in any parameters
    // so that we can use it with addEventListener and deleteEventListener to add/remove it.
    setTimeout(() => {
        // Get the note element that was clicked on
        const notesDisplay = document.querySelector(".notes-display");
        const noteElem = notesDisplay.querySelector(
            `note-element[id="${CURRENT_NOTE_ID}"]`
        );
        // Display the note editor of the newly retrieved current note
        _displayNoteEditor(noteElem);
    }, 100);
}

/**
 * Given the id of a note, load it from storage. If no id is given, it will load the most recently added note
 * @param {String} id id of note, initialized as null (optional)
 */
function _loadNotefromStorage(id = null) {
    const notesDisplay = document.querySelector(".notes-display");
    let noteRecord;
    // Get the note record from storage
    if (id !== null) {
        // If an id is passed in, get the note record by id
        noteRecord = RecordsStorage.getRecordById(parseInt(id));
    } else {
        // If id is null, get the most recently added note record
        const notesList = RecordsStorage.getAllRecords("note");
        noteRecord = notesList[notesList.length - 1];
    }
    // Create a note element to display the noteRecord's info
    let noteElem = document.createElement("note-element");
    noteElem.id = noteRecord.id;
    noteElem.title = noteRecord.title;
    noteElem.date = noteRecord.created;
    noteElem.content = noteRecord.field1;
    // Add note element to page and add its event listeners
    notesDisplay.prepend(noteElem);
    _addListeners(noteElem);
    // Make sure delete note button is visible
    const deleteNoteBtn = document.getElementById("delete-note-btn");
    deleteNoteBtn.classList.remove("hidden");
}

/**
 * Display the note editor and populate it with values of a note element if one is given
 * @param {Note} noteElem custom element Note, aka "note-elements" in HTML, reference to note that is being edited
 * initialized as null (optional)
 */
function _displayNoteEditor(noteElem = null) {
    // Scroll to top of page to see the the editor once it is created
    window.scrollTo(0, 0);
    // Set editorCreated as a boolean of if there is a noteEditor created yet
    // If the note editor exists, it won't be null
    const editorCreated = document.getElementById(EDITOR_FORM_ID) != null;
    // If the note Editor has already been created, update it, otherwise, create it
    if (editorCreated) {
        _updateNoteEditor(noteElem);
    } else {
        _createNoteEditor(noteElem);
        _addNoteEditorListeners();
    }
    // Make sure you can't delete notes while editor is open.
    const deleteNoteBtn = document.getElementById("delete-note-btn");
    deleteNoteBtn.classList.add("hidden");
}

/**
 * Create the note editor and populate it with values of a note element if one is given
 * @param {Note} noteElem custom element Note, aka "note-elements" in HTML, reference to note that is being edited
 * initialized as null (optional)
 */
function _createNoteEditor(noteElem = null) {
    // Elements for the note editor
    const noteEditor = document.createElement("form");
    const noteTitle = document.createElement("input");
    const noteContent = document.createElement("textarea");
    const saveBtn = document.createElement("button");
    const cancelBtn = document.createElement("button");
    // Add selectors to elements
    noteEditor.id = EDITOR_FORM_ID;
    noteTitle.id = EDITOR_TITLE_ID;
    noteContent.id = EDITOR_CONTENT_ID;
    saveBtn.id = EDITOR_SAVE_ID;
    cancelBtn.id = EDITOR_CANCEL_ID;
    // Add formats/attributes to elements
    noteTitle.type = "text";
    noteTitle.maxLength = "50"; // define a max amount of characters users can input
    noteTitle.placeholder = "Title"; // make it a text input
    noteContent.placeholder = "Notes";
    // Update button texts
    saveBtn.innerText = "Save";
    cancelBtn.innerText = "Cancel";
    // If there was a note passed in, populate values
    _initNoteEditorValues(noteElem, noteTitle, noteContent);
    // Populate note editor
    noteEditor.appendChild(noteTitle);
    noteEditor.appendChild(noteContent);
    noteEditor.appendChild(saveBtn);
    noteEditor.appendChild(cancelBtn);
    // Make the note editor's content be a markdown editor
    CONTENT_MDE = initMDE(noteContent);
    // Don't reload the page when the form is submitted - minimize unnecessary loads from storage
    noteEditor.addEventListener("submit", (event) => {
        // Prevent form from refreshing page upon submit
        event.preventDefault();
    });
    // Put note editor in the main notes container to be displayed
    const notesContainer = document.querySelector(".js-notes-container");
    notesContainer.prepend(noteEditor);
}

/**
 * Add event listeners for the note editor display's save and cancel buttons
 */
function _addNoteEditorListeners() {
    const noteEditor = document.getElementById(EDITOR_FORM_ID);
    const saveBtn = document.getElementById(EDITOR_SAVE_ID);
    const cancelBtn = document.getElementById(EDITOR_CANCEL_ID);
    // When the save is clicked, save to storage, update display
    saveBtn.addEventListener("click", () => {
        // Make sure you can delete notes now that editor is closed.
        const deleteNoteBtn = document.getElementById("delete-note-btn");
        deleteNoteBtn.classList.remove("hidden");
        // Scroll to top of page
        window.scrollTo(0, 0);
        // Put note in storage and hide the form
        submitToStorage();
        noteEditor.classList.add("hidden");
        // If we are editing a note
        if (CURRENT_NOTE_ID != null) {
            const noteElem = document.querySelector(
                `note-element[id="${CURRENT_NOTE_ID}"]`
            );
            // Display the updated note and remove the old note from view
            _loadNotefromStorage(CURRENT_NOTE_ID);
            noteElem.remove();
        } else {
            // Otherwise, show new note by loading in the most recently created note from storage
            _loadNotefromStorage();
        }
    });
    // When the cancel button is clicked, clear the form and hide it.
    cancelBtn.addEventListener("click", () => {
        // Make sure you can delete notes now that editor is closed.
        const deleteNoteBtn = document.getElementById("delete-note-btn");
        deleteNoteBtn.classList.remove("hidden");
        // Clear note editor form values
        _initNoteEditorValues(null);
        noteEditor.classList.add("hidden");
        // Scroll to top of page
        window.scrollTo(0, 0);
    });
}

/**
 * Given a note element, note title input (optional) and note content textarea (optional)
 * initialize the title and content displays of the poopup note editor to be the title
 * and content of the noteELem or empty if the noteElem is null
 * @param {Note} noteElem custom element Note, aka "note-elements" in HTML, reference to note that is being edited
 *                        initialized as null (optional)
 * @param {HTMLInputElement} editorTitle input element for the editor's title section (optional)
 * @param {HTMLTextAreaElement} editorContent textarea element for editor's content section(optional)
 */
function _initNoteEditorValues(
    noteElem,
    editorTitle = null,
    editorContent = null
) {
    // If either of note title or note content aren't given, define them
    if (!editorTitle || !editorContent) {
        editorTitle = document.getElementById(EDITOR_TITLE_ID);
        editorContent = document.getElementById(EDITOR_CONTENT_ID);
    }
    // If noteElem isn't null update the title and content of the note editor popup
    if (noteElem !== null) {
        editorTitle.value = noteElem.title;
        editorContent.value = noteElem.content;
        // If editor content is using markdown editing, set editor's value how simpleMDE requires
        if (CONTENT_MDE) {
            CONTENT_MDE.value(noteElem.content);
        }
    } else {
        // If noteELem is null, clear the title and content of the note editor popup
        editorTitle.value = "";
        editorContent.value = "";
        // If editor content is using markdown editing, set editor's value how simpleMDE requires
        if (CONTENT_MDE) {
            CONTENT_MDE.value("");
        }
    }
}

/**
 * Update the note editor with values of a note element if one is given, otherwise clear the values
 * @param {Note} noteElem custom element Note, aka "note-elements" in HTML, reference to note that is being edited
 * noteElem initialized as null (optional) - clears editor values if it is null
 */
function _updateNoteEditor(noteElem = null) {
    // Get note editor
    const noteEditor = document.getElementById(EDITOR_FORM_ID);
    // If it's hidden, show it
    noteEditor.classList.remove("hidden");

    // If there was a note passed in, populate values
    _initNoteEditorValues(noteElem);
}

/**
 * This function is called when the window is loaded.
 * @function
 * @name window.onload
 */
window.onload = function () {
    // Make sure there aren't any faulty records in storage, then load records in as note elements
    RecordsStorage.cleanse_records();
    loadAllNotesFromStorage();
    // Display note editor when "Add Note" button clicked
    const addNoteBtn = document.getElementById("add-note-btn");
    addNoteBtn.addEventListener("click", () => {
        CURRENT_NOTE_ID = null;
        _displayNoteEditor();
    });
    // Get delete note and done delete note buttons
    const deleteNoteBtn = document.getElementById("delete-note-btn");
    const doneDelNoteBtn = document.getElementById("done-deleting-note-btn");
    // Make sure delete button is displayed
    deleteNoteBtn.classList.remove("hidden");
    // Allow for deleting when delete button is clicked - display trash icons, edit event listeners
    deleteNoteBtn.addEventListener("click", () => {
        // Hide 'delete note', 'add note' buttons and show 'done deleting' button
        deleteNoteBtn.classList.add("hidden");
        addNoteBtn.classList.add("hidden");
        doneDelNoteBtn.classList.remove("hidden");
        // Loop through note elements, adding trash listeners and removing note edit listeners
        const noteElems = document.getElementsByClassName("note");
        for (const noteElem of noteElems) {
            // Get note's trash button (icon)
            const trashBtn = noteElem.shadowRoot.getElementById("js-trash");
            // Display trash button and add event listener that deletes its note when clicked
            trashBtn.classList.remove("hidden");
            trashBtn.addEventListener("click", _deleteCurrentNote);
            trashBtn.addEventListener("keypress", _deleteCurrentNote);
            // Remove edit popup listener while in delete mode
            // - makes sure edit popup doesn't display when trash icons are clicked
            noteElem.removeEventListener("click", _editCurrentNote);
            noteElem.removeEventListener("keypress", _editCurrentNote);
        }
    });
    // Block deleting when 'done deleting' button clicked - hide trash icons, edit event listeners
    doneDelNoteBtn.addEventListener("click", () => {
        // Hide 'done deleting' button, show 'delete notes' 'add note' buttons
        deleteNoteBtn.classList.remove("hidden");
        addNoteBtn.classList.remove("hidden");
        doneDelNoteBtn.classList.add("hidden");
        // Loop through note elements, removing trash listeners and adding note edit listeners
        const noteElems = document.getElementsByClassName("note");
        for (const noteElem of noteElems) {
            // Get note's trash button (icon)
            const trashBtn = noteElem.shadowRoot.getElementById("js-trash");
            // Hide trash button and remove event listener that deletes its note when clicked
            trashBtn.classList.add("hidden");
            trashBtn.removeEventListener("click", _deleteCurrentNote);
            trashBtn.removeEventListener("keypress", _deleteCurrentNote);
            // Add edit popup listener back - assures editor popup can display when note is clicked
            noteElem.addEventListener("click", _editCurrentNote);
            noteElem.addEventListener("keypress", _editCurrentNote);
        }
        // If there are no notes left, don't show the delete note button
        if (noteElems.length === 0) {
            deleteNoteBtn.classList.add("hidden");
        }
    });
};