classes/daily-log.js

import { Database } from './database.js';
import { IDConverter } from './IDConverter.js';

/**
 * This class contains functions to construct and edit the daily log custom HTML element.
 *
 * @classdesc
 * @example <caption>Daily Log class</caption>
 * // Example of a daily JSON object used to generate a daily-log element
 * const exampleDailyJSON = {
 *   trackers: [
 *     {
 *       type: 'slider',
 *       name: 'Mood',
 *       value: 3
 *     },
 *     {
 *       type: 'slider',
 *       name: 'Sleep Quality',
 *       value: 5
 *     },
 *     {
 *       type: 'numInput',
 *       name: 'Calorie Intake',
 *       value: 2500
 *     },
 *     {
 *       type: 'numInput',
 *       name: 'Money Spent',
 *       value: 25
 *     },
 *     {
 *       type: 'checkbox',
 *       name: 'Exercise',
 *       value: 0
 *     }
 *   ],
 *   sections: [
 *     {
 *       id: '00',
 *       name: 'Reminders',
 *       type: 'log',
 *       bulletIDs: [
 *         'B 210515 00 00',
 *         'B 210515 00 01'
 *       ],
 *       nextBulNum: 2
 *     },
 *     {
 *       id: '01',
 *       name: 'Daily Notes',
 *       type: 'log',
 *       bulletIDs: [
 *         'B 210515 01 00',
 *         'B 210515 01 01',
 *         'B 210515 01 02'
 *       ],
 *       nextBulNum: 3
 *     },
 *     {
 *       id: '02',
 *       name: 'Shopping List',
 *       type: 'log',
 *       bulletIDs: [
 *         'B 210515 02 00',
 *         'B 210515 02 01'
 *       ],
 *       nextBulNum: 2
 *     },
 *     {
 *       id: '03',
 *       name: 'Daily Goals',
 *       type: 'log',
 *       bulletIDs: [
 *         'B 210515 03 00',
 *         'B 210515 03 01'
 *       ],
 *       nextBulNum: 2
 *     }
 *   ]
 * }
 * // Create a new daily log HTML element using the object
 * let daily = document.createElement('daily-log');
 * daily.data = exampleDailyJSON;
 */

class DailyLog extends HTMLElement {
  // -------------------------------------- Start Constructor -------------------------------------

  /**
   * Constructs a blank daily log HTML element using the defined HTML template
   *
   * @constructor
   */
  constructor () {
    super();

    this.nextSectionNum = 0;

    const template = document.createElement('template');

    template.innerHTML = `
      <link rel="stylesheet" href="../style/style.css">
      <link rel="stylesheet" href="../assets/css/all.css">
      <div class="daily">
        <section class="header" id="daily-header" style="display: flex;">
          <h1></h1>
          <button class="main-buttons" id="related-sections-button"><i class="fas fa-plus icon-size"></i></button>
        </section>
      </div>
    `;

    this.attachShadow({ mode: 'open' });
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }

  // --------------------------------------- End Constructor --------------------------------------

  // ---------------------------------- Start Get/Set Functions -----------------------------------

  /**
   * This function returns the data stored in this daily element as a JSON object.
   *
   * @returns {Object} JSON representation of data used to generate this daily log element
   */
  get data () {
    return JSON.parse(this.getAttribute('data'));
  }

  /**
   * This function constructs the daily log HTML element using the given ID and daily log
   * object data. It starts by constructing and setting the header text for the element, then
   * goes through each attribute of the daily log object to construct the trackers and notes
   * sections of the element. Each notes section is constructed by fetching the data of
   * each bullet in the section from the database, and creating a custom bullet-entry HTML
   * element that is appended to the section. Finally, the setAttribute function is called
   * on this element to set the 'data' attribute of the element to be the given JSON data,
   * so that the data can be retrieved from the element later if needed.
   *
   * @param {Array.<{id: string, jsonData: Object, callback: function}>} data - Array of three
   * elements (first element is the string ID of the object, second element is the JSON object
   * data, third element is a callback to update the dayID[] "entries" for jumping between
   * entries) that is used to construct and set the data in this HTML element
   */
  set data ([id, jsonData, callback]) {
    // store this object in a variable so it can be passed to handlers later
    const dailyLog = this;

    // set the id of the custon element to the given id
    this.id = id;

    // if the jsonData is an empty object, then we should create an empty daily element
    if (Object.entries(jsonData).length === 0) {
      jsonData = {
        trackers: [
          {
            type: 'slider',
            name: 'Mood',
            value: 1
          },
          {
            type: 'slider',
            name: 'Sleep Quality',
            value: 1
          },
          {
            type: 'numInput',
            name: 'Calorie Intake',
            value: 0
          },
          {
            type: 'numInput',
            name: 'Money Spent',
            value: 0
          },
          {
            type: 'checkbox',
            name: 'Exercise',
            value: 0
          }
        ],
        sections: [
          {
            id: '00',
            name: 'Reminders',
            type: 'log',
            bulletIDs: [],
            nextBulNum: 0
          },
          {
            id: '01',
            name: 'Daily Notes',
            type: 'log',
            bulletIDs: [],
            nextBulNum: 0
          }
        ]
      };
    }

    // get the shadow root of this custom HTML element and set its ID to the given ID
    const root = this.shadowRoot.querySelector('div.daily');
    root.id = id;

    // add event listener for the add section button in the header
    const newSectionButton = root.querySelector('#related-sections-button');
    newSectionButton.addEventListener('click', function (event) {
      dailyLog.newSectionHandler(event.target.closest('div.daily'));
      callback();
    });

    // get all information about the date that is needed for the header display
    const dateObj = IDConverter.getDateFromID(id, 'day');
    const day = IDConverter.getDayFromDate(dateObj);
    const month = IDConverter.getMonthFromDate(dateObj);
    const date = dateObj.getDate();
    const suffix = IDConverter.getSuffixOfDate(dateObj);
    const dateString = `${day}, ${month} ${date}${suffix}`;

    // get the header text of this custom HTML element and set its contents to the constructed date string
    const headerText = root.querySelector('#daily-header > h1');
    headerText.innerText = dateString;

    // creates all trackers in one section
    if (jsonData.trackers && jsonData.trackers.length > 0) {
      // construct tracker section element
      const trackerSection = document.createElement('section');
      trackerSection.className = 'trackers';

      // optional tracker elements if we want to implement
      // <button class="add" id="add-tracker">Add Tracker</button>
      // <button class="minimize" id="minimize-trackers">Minimize Trackers</button>
      trackerSection.innerHTML = `
        <h2>Trackers</h2>
      `;

      for (const tracker of jsonData.trackers) {
        // create a div for the tracker
        const trackerDiv = document.createElement('div');
        trackerDiv.className = 'tracker';

        // construct the title of the tracker
        const trackerTitle = document.createElement('h3');
        trackerTitle.innerText = tracker.name;
        trackerDiv.appendChild(trackerTitle);

        // get the appropriate tracker element based on what type of tracker this is
        let trackerEl;
        switch (tracker.type) {
          case 'slider':
            trackerEl = this.createSlider(tracker);
            break;
          case 'checkbox':
            trackerEl = this.createCheckbox(tracker);
            break;
          case 'numInput':
            trackerEl = this.createNumInput(tracker);
            break;
          default:
            console.log('No tracker implementation found: ' + tracker.type);
            break;
        }
        trackerDiv.appendChild(trackerEl);

        // add the tracker to the tracker section
        trackerSection.appendChild(trackerDiv);
      }

      root.appendChild(trackerSection);
    }

    // loop through all sections in JSON data and construct and populate them
    if (jsonData.sections) {
      for (const section of jsonData.sections) {
        // get the second section of the daily log object (the first section is always the header and should always be on the top)
        // this element will be used to insert the daily notes at the top of the log object
        let refElement = root.children[1];

        const sectionID = section.id;

        // update next section number
        if (Number(section.id) >= this.nextSectionNum) {
          this.nextSectionNum = Number(section.id) + 1;
        }

        // construct section element
        const sectionElement = document.createElement('section');
        sectionElement.id = section.id;
        sectionElement.className = section.type;

        // added a margin to the bottom of each section
        sectionElement.style = 'margin-bottom: 1vw';

        // construct section header element
        const sectionHeader = document.createElement('div');
        const sectionTitle = document.createElement('h2');
        sectionTitle.innerText = section.name;
        sectionTitle.style.paddingLeft = '2vw';
        sectionHeader.appendChild(sectionTitle);
        sectionElement.appendChild(sectionHeader);

        // check if we are creating a custom section
        // (reminders section always has default section ID of '00')
        // (daily notes section always has default section ID of '01')
        if (sectionID !== '00' && sectionID !== '01') {
          // other sections should be added at the bottom of the daily log
          refElement = null;

          // allow the user to change the section title
          sectionTitle.contentEditable = 'true';

          // add event listener to the header to update the daily log element when the header text is updated
          sectionTitle.addEventListener('blur', (event) => {
            const sectionName = dailyLog.data.sections.filter((section) => section.id === sectionID)[0].name;
            if (sectionTitle.innerText !== sectionName) {
              const dailyData = dailyLog.data;
              for (const sec of dailyData.sections) {
                if (sec.id === sectionID) {
                  sec.name = sectionTitle.innerText;
                }
              }
              this.setAttribute('data', JSON.stringify(dailyData));
              Database.store(id, dailyData);
            }
          });

          // add event listener to the header to prevent newlines in headers
          sectionTitle.addEventListener('keypress', (event) => {
            if (event.key === 'Enter') {
              sectionTitle.blur();
            }
          });

          // create a button to delete the section and add the delete section event listener to it
          const deleteSectionButton = document.createElement('button');
          deleteSectionButton.className = 'delete-section';
          deleteSectionButton.innerHTML = `
            <i class="fas fa-times"></i>
          `;
          deleteSectionButton.style.float = 'right';
          deleteSectionButton.style.marginRight = '2vw';
          deleteSectionButton.addEventListener('click', function (event) {
            dailyLog.deleteSectionHandler(sectionElement);
          });
          sectionHeader.insertBefore(deleteSectionButton, sectionTitle);
        }

        // construct bullet elements
        for (const bulletID of section.bulletIDs) {
          // create bullet element and add the deletion event listeners to it
          const bulletElement = document.createElement('bullet-entry');
          bulletElement.shadowRoot.querySelector('.bullet-text').addEventListener('keydown', function (event) {
            dailyLog.keyCodeListener(event, bulletElement, bulletID, sectionElement, sectionID);
          });

          bulletElement.shadowRoot.querySelector('.bullet-remove').addEventListener('click', function (event) {
            dailyLog.deleteNoteHandler(bulletElement);
          });

          sectionElement.appendChild(bulletElement);

          // fetch the bullet data and set the bullet element's data in the callback
          Database.fetch(bulletID, function (bulletData, bulletID, bulletElement, sectionID) {
            dailyLog.setBulletData(bulletData, bulletID, bulletElement, sectionID);
          }, bulletID, bulletElement, sectionID);
        }

        // create a button to add new notes to the section and add the add new bullet event listener to it
        const newNoteButton = document.createElement('button');
        newNoteButton.className = 'new-bullet';
        newNoteButton.innerHTML = `
          <i class="fas fa-plus"></i>
        `;
        newNoteButton.addEventListener('click', function (event) {
          dailyLog.newNoteHandler(event.target.closest('section'));
          callback();
        });
        sectionElement.appendChild(newNoteButton);

        // if we have a reference element, insert the new section before it, otherwise just add the new section to the bottom
        if (refElement) {
          root.insertBefore(sectionElement, refElement);
        } else {
          root.appendChild(sectionElement);
        }
      }
    }

    // set the data attribute of this element to the given JSON data so it can be retrieved later
    this.setAttribute('data', JSON.stringify(jsonData));
  }

  // ----------------------------------- End Get/Set Functions ------------------------------------

  // ----------------------------------- Start Helper Functions -----------------------------------

  /**
   * This function is a helper function that is used as the callback for when we fetch bullet
   * data from the database or when we are creating a new bullet. The function first creates a
   * function that will be used by the created bullet object to update the bullet count in
   * the appropriate section of the daily log and generate a bullet ID for sub-bullets (nested
   * bullets that are children of this created bullet). The function then checks if the data
   * is not null or undefined. If the data isn't null/undefined, the bullet element's data is
   * set to the given data. If the data is null/undefined, the bullet element's data is set to
   * an empty JSON object, which creates a blank bullet.
   *
   * @param {Object} bulletData - The JSON object data that will be stored in the bullet
   * @param {string} bulletID - The string ID of the bullet object
   * @param {HTMLElement} bulletElement - The bullet-entry element whose data will be set
   * @param {string} sectionID - The string ID of the section in which the bullet is being created
   */
  setBulletData (bulletData, bulletID, bulletElement, sectionID) {
    const dailyLog = this;
    const newBulletID = function () {
      const newID = dailyLog.generateBulletID(sectionID);
      return newID;
    };

    if (bulletData) {
      bulletElement.data = [bulletID, bulletData, newBulletID];
    } else {
      bulletElement.data = [bulletID, {}, newBulletID];
    }
  }

  /**
   * This function is a helper function that is used to create a new bullet ID. The given section
   * ID determines which section the bullet is being created in. The function then combines the
   * bullet count for that section, the section ID, and the date of the daily object in which
   * the bullet is being added in order to create a new bullet ID, which is then returned.
   *
   * @param {string} sectionID - The string ID of the section in which the bullet is being created
   * @returns {string} The string ID to be used for the new bullet
   */
  generateBulletID (sectionID) {
    // get the data and find the section in which the bullet is being added to determine the new bullet number
    const data = this.data;
    let newBulNum;
    for (const section of data.sections) {
      if (section.id === sectionID) {
        newBulNum = section.nextBulNum;
        section.nextBulNum++;
      }
    }

    // store the udpated data
    this.setAttribute('data', JSON.stringify(data));
    Database.store(this.id, data);

    // generate the new bullet ID
    const bulletCount = IDConverter.stringifyNum(newBulNum);
    const dailyID = this.shadowRoot.querySelector('div.daily').id;

    return `B ${dailyID.substring(2)} ${sectionID} ${bulletCount}`;
  }

  /**
   * This function is a helper function to create a slider for trackers that are to be presented
   * in the slider format. The function first creates a div element to hold the 3 pieces of the
   * slider element. The first two elements in the slider div are the frown and smile icons that
   * are there to help the user see what each extreme of the slider means. The function then
   * creates the html input element with the type 'range', and sets its value based on the value
   * in the tracker object. Then, the function adds an event listener to the slider so that
   * the JSON object data gets updated for the daily log object and in the database when the slider
   * value gets changed. Finally, the slider div is returned so that it can be appended to the
   * trackers section.
   *
   * @param {Object} tracker - The JSON object of the tracker being created
   * @returns {HTMLElement} A div element containing the slider and the icons to show what each
   * extreme of the slider signifies
   */
  createSlider (tracker) {
    // slider overlay div creation
    const sliderDiv = document.createElement('div');
    sliderDiv.className = 'slider-div';

    // frowny face and smiley face for the ends of the slider
    sliderDiv.innerHTML = `
      <i class="far fa-frown"></i>
      <i class="far fa-smile"></i>
    `;

    // actual slider creation
    const slider = document.createElement('input');
    slider.id = tracker.name;
    slider.className = 'slider';
    slider.type = 'range';
    slider.min = '1';
    slider.max = '10';
    slider.step = '1';
    // set value of slider based on data
    slider.value = `${tracker.value}`;

    // add change listener for updates
    const dailyLog = this;
    slider.addEventListener('change', (event) => {
      const data = dailyLog.data;
      // finds the tracker associated with this slider
      // then changes it based on the new value
      for (const trackerObj of data.trackers) {
        if (trackerObj.name === tracker.name) {
          trackerObj.value = Number(String(event.target.value));
        }
      }
      dailyLog.setAttribute('data', JSON.stringify(data));
      Database.store(dailyLog.id, data);
    });

    // get the icon that should be on the right of the slider, and insert the slider before it
    const rightIcon = sliderDiv.querySelector('i:nth-child(2)');
    sliderDiv.insertBefore(slider, rightIcon);

    return sliderDiv;
  }

  /**
   * This function is a helper function to create a checkbox for trackers that are to be presented
   * in the checkbox format. The function first creates the html input element with the type 'checkbox',
   * and sets its checked attribute based on the value in the tracker object. Then, the function adds
   * an event listener to the checkbox so that the JSON object data gets updated for the daily log
   * object and in the database when the checkbox gets checked or unchecked. Finally, the checkbox
   * element is returned so that it can be appended to the trackers section.
   *
   * @param {Object} tracker - The JSON object of the tracker being created
   * @returns {HTMLElement} The checkbox input element to be displayed as the tracker
   */
  createCheckbox (tracker) {
    // checkbox element creation
    const checkboxEl = document.createElement('input');
    checkboxEl.id = tracker.name;
    checkboxEl.className = 'checkbox';
    checkboxEl.type = 'checkbox';
    checkboxEl.value = tracker.name;
    // set checked value based on data
    checkboxEl.checked = tracker.value === 1;

    // add change listener for updates
    const dailyLog = this;
    checkboxEl.addEventListener('change', (event) => {
      const data = dailyLog.data;
      for (const trackerObj of data.trackers) {
        // check if this is the right tracker object
        if (trackerObj.name === tracker.name) {
          // if the tracker value is currently 1 (means it is checked), we want to uncheck it
          // if the tracker object is 0 (means it is unchecked), we want to check it
          if (trackerObj.value === 1) {
            trackerObj.value = 0;
          } else {
            trackerObj.value = 1;
          }
        }
      }
      dailyLog.setAttribute('data', JSON.stringify(data));
      Database.store(dailyLog.id, data);
    });

    return checkboxEl;
  }

  /**
   * This function is a helper function to create a number input for trackers that are to be presented
   * in the number input format. The function first creates the html input element with the type 'number',
   * and sets its value based on the value in the tracker object. Then, the function adds an event listener
   * to the input element so that the JSON object data gets updated for the daily log object and in the
   * database when the number input gets changed (note that if the inputted number is negative, the data
   * is not updated because a negative input does not make sense in the context of these trackers). Finally,
   * the number input element is returned so that it can be appended to the trackers section.
   *
   * @param {Object} tracker - The JSON object of the tracker being created
   * @returns {HTMLElement} The number input element to be displayed as the tracker
   */
  createNumInput (tracker) {
    // number input element creation
    const numInputEl = document.createElement('input');
    numInputEl.id = tracker.name;
    numInputEl.className = 'numInput';
    numInputEl.type = 'number';
    numInputEl.min = 0;
    // set num input based on data
    numInputEl.value = `${tracker.value}`;

    // add change listener for updates
    const dailyLog = this;
    numInputEl.addEventListener('change', (event) => {
      const inputVal = Number(String(event.target.value));
      // only record if it is non-negative
      if (inputVal >= 0) {
        const data = dailyLog.data;
        for (const trackerObj of data.trackers) {
          // check that this is the right tracker object
          if (trackerObj.name === tracker.name) {
            trackerObj.value = inputVal;
          }
        }
        dailyLog.setAttribute('data', JSON.stringify(data));
        Database.store(dailyLog.id, data);
      }
    });

    return numInputEl;
  }

  // ------------------------------------ End Helper Functions ------------------------------------

  // ------------------------------------ Start Event Handlers ------------------------------------

  /**
   * This function handles the keyboard input and looks for special keycodes: <p>
   * Enter -  creates a new sibling bullet below current bullet <p>
   * Up -  moves the focus up the onscreen bullets <p>
   * Down -  moves the focus down the onscreen bullets <p>
   * Tab -  indents the bullet, essentially making the current bullet a child of it's previuos sibling <p>
   * Backspace -  deletes the bullet and moves the focus up on teh onscreen bullets
   *
   * @param {keydownEvent} event - provides access to the keyCode input
   * @param {HTMLElement} bulletElement - current bullet element, mainly used for display related logic
   * @param {String} bulletID - current bullet's ID, mainly used for data related logic
   * @param {HTMLElement} sectionElement - current section element, mainly used for display related logic
   * @param {String} sectionID - current section ID, mainly used for data related logic
   */
  keyCodeListener (event, bulletElement, bulletID, sectionElement, sectionID) {
    const dailyLog = this;

    /**
     * Workaround helper function to set cursor to end of line <p>
     *
     * Select's everything in the currently focused input, then collapses that selection to the end
     */
    function gotoEOL () {
      document.execCommand('selectAll', false, null);
      document.getSelection().collapseToEnd();
    }

    // condition check to determine if the listener was triggered when backspace was pressed on an empty note
    if (event.keyCode === 8 && event.target.innerText.length === 0) {
      const prevSibling = bulletElement.previousSibling;
      // Check if there is some bullet to fall back to
      if (prevSibling && prevSibling.nodeName === 'BULLET-ENTRY') {
        // Blur current, then focus fallback bullet (given by a helper), use a workaround to move cursor to end of line
        event.target.blur();
        dailyLog.getLastChild(prevSibling).focus();
        gotoEOL();
        // prevent default to avoid deleting the first character of fallback bullet
        event.preventDefault();
      }

      dailyLog.deleteNoteHandler(bulletElement);
    }

    /** Enter button will "create a new bullet below"
     * will replace default action
     * iterate to find index
     * insertion will occur at index + 1 (data)
     * insertion will occur after current bullet element (display)
    */
    if (event.keyCode === 13) {
      event.preventDefault();

      let index = -1;
      sectionElement.querySelectorAll('bullet-entry').forEach((elem, ind) => {
        if (elem.id === bulletID) {
          index = ind;
        }
      });
      dailyLog.newNoteHandler(sectionElement, index + 1, bulletElement);
    }

    /** Tab button will "Turn current bullet into last child of previous sibling"
     * - replace default action
     * - use elements to modify data + display
     */
    if (event.keyCode === 9) {
      event.preventDefault();
      const prevSibling = bulletElement.previousSibling;
      if (prevSibling && prevSibling.nodeName === 'BULLET-ENTRY') {
        // Call on deleteNoteHandler to remove bullet from parent's childIDs (will also remove from DB)
        dailyLog.deleteNoteHandler(bulletElement);

        // Add bullet back into database
        bulletElement.storeToDatabase(bulletElement.id, bulletElement.data, false);

        // Update previous siblings data to include this bullet becoming a new child (local and database)
        const prevData = prevSibling.data;
        prevData.childrenIDs.push(bulletElement.id);
        prevSibling.setAttribute('data', JSON.stringify(prevData));
        prevSibling.storeToDatabase(prevSibling.id, prevData, false);

        // Generate the callback to be used by future children
        const newBulletID = function () {
          const newID = dailyLog.generateBulletID(sectionID);
          return newID;
        };

        // Call on createChild to handle display
        prevSibling.createChild(bulletElement.id, bulletElement.data, newBulletID);
      }
    }

    /** Up/Down button will blur current and focus target sibling
     * Checks for prev/next sibling that are bullets
     * - blurs current and focuses on sibling
     * - selects all and collapses selection to end (workarount to get cursor to end of input)
     */

    if (event.keyCode === 38) { // UP
      const prevSibling = bulletElement.previousSibling;
      if (prevSibling && prevSibling.nodeName === 'BULLET-ENTRY') {
        // Prevent default of moving back to beginning of line
        event.preventDefault();

        // Blur current, then focus fallback bullet (given by a helper), use a workaround to move cursor to end of line
        event.target.blur();
        dailyLog.getLastChild(prevSibling).focus();
        gotoEOL();
      }
    }

    if (event.keyCode === 40) { // DOWN
      const children = bulletElement.shadowRoot.querySelectorAll('bullet-entry');
      const nextSibling = bulletElement.nextSibling;

      // Go to first child if bullet has children
      if (children.length) {
        event.target.blur();
        children[0].shadowRoot.querySelector('.bullet-text').focus();
        gotoEOL();
      } else if (nextSibling && nextSibling.nodeName === 'BULLET-ENTRY') {
        // Otherwise continue with siblings
        event.target.blur();
        nextSibling.shadowRoot.querySelector('.bullet-text').focus();
        gotoEOL();
      }
    }
  }

  /**
   * Recursive function to determine the previous bullet to focus on when moving up (and also after deletion) <p>
   *
   * If previous sibling has children, will recursively call function on the last child
   * of previous sibling's children <p>
   *
   * This continues until previous sibling doesn't have children, then returning the currentElement <p>
   *
   * Base case of previous sibling never having children will return the element pased in
   *
   * @param {HTMLElement} currElem - current element in the recursion, will check to make sure prevChildren don't exist before turning self in
  */
  getLastChild (currElem) {
    const prevChildren = currElem.shadowRoot.querySelectorAll('bullet-entry');
    if (prevChildren.length) {
      return this.getLastChild(prevChildren[prevChildren.length - 1]);
    } else {
      return currElem.shadowRoot.querySelector('.bullet-text');
    }
  }

  /**
   * This function creates a new bullet. It first generates a bullet ID by combining the date of
   * the daily log to which the bullet will belong, the ID of the section to which the bullet
   * is being added, and the ID of the new bullet, which is determined based on the number of
   * bullets currently in the section. It then adds the bullet ID to the daily JSON object in
   * the appropriate section, and stores the updated daily JSON object in the database. Lastly,
   * it creates a new bullet-entry HTML element, and adds the appropriate event listener to it
   * to allow for future deletion of the bullet element.
   *
   * @param {HTMLElement} sectionElement - The section element in which the new note button
   * @param {Number} index - optional (on Enter) target index in bulletIDs to place the generated newNote (data)
   * @param {HTMLElement} siblingElem - optional (on Enter) sibling bullet to place the generated newNote after (display)
   * was clicked to trigger the listener
   */
  newNoteHandler (sectionElement, index, siblingElem) {
    // generate a bullet ID
    const sectionID = sectionElement.id;
    const dailyID = this.shadowRoot.querySelector('div.daily').id;
    const bulletID = this.generateBulletID(sectionID);

    // add bullet ID to the daily JSON object bulletIDs in the right section
    const data = this.data;
    for (const sec of data.sections) {
      if (sec.id === sectionID) {
        // Figure out where to place the new bullet in data (using passed in index if triggered by pressing Enter)
        if (!index) {
          sec.bulletIDs.push(bulletID);
        } else {
          sec.bulletIDs.splice(index, 0, bulletID);
        }
      }
    }
    this.setAttribute('data', JSON.stringify(data));

    // store the updated daily JSON object in the database
    Database.store(dailyID, data);

    // create a blank bullet element with the generated ID
    const bullet = document.createElement('bullet-entry');
    this.setBulletData({}, bulletID, bullet, sectionID);

    // add event listeners to the bullet element to handle deletion
    const dailyLog = this;
    bullet.shadowRoot.querySelector('.bullet-text').addEventListener('keydown', function (event) {
      dailyLog.keyCodeListener(event, bullet, bulletID, sectionElement, sectionID);
    });

    bullet.shadowRoot.querySelector('.bullet-remove').addEventListener('click', function (event) {
      dailyLog.deleteNoteHandler(bullet);
    });

    if (!siblingElem) {
      // insert the new bullet element child before the new note button
      const newNote = sectionElement.querySelector('button.new-bullet');
      sectionElement.insertBefore(bullet, newNote);
    } else {
      // insert after passed in bulletElement (using passed in siblingElem if triggered by pressing Enter)
      siblingElem.insertAdjacentElement('afterend', bullet);
    }

    // prompt user to start typing note
    bullet.shadowRoot.querySelector('.bullet-text').focus();
  }

  /**
   * This function handles the deletion of a bullet. The function looks through the daily JSON
   * object to remove the ID of the bullet being deleted from the appropriate section, and then
   * stores the updated daily JSON object in the database and removes the bullet-entry HTML
   * element from the section.
   *
   * @param {HTMLElement} bulletElement - The bullet-entry element that is being deleted
   */
  deleteNoteHandler (bulletElement) {
    // get the section element that the bullet is a child of
    const section = bulletElement.closest('section');

    // loop through the daily JSON object to find the bullet ID to be deleted
    const sectionID = section.id;
    const data = this.data;
    for (const sec of data.sections) {
      // condition check to determine if this is the right section in the daily JSON object
      if (sec.id === sectionID) {
        sec.bulletIDs = sec.bulletIDs.filter((bulletID) => bulletID !== bulletElement.id);
      }
    }
    this.setAttribute('data', JSON.stringify(data));

    // store the updated daily JSON object in the database
    const dailyID = this.shadowRoot.querySelector('div.daily').id;
    Database.store(dailyID, data);

    // delete the bullet from the database
    Database.delete(bulletElement.id);

    // remove the bullet-entry HTML element from the section
    section.removeChild(bulletElement);
  }

  /**
   * This function creates a new section. It first creates a section ID by looking at how many
   * sections are currently in the daily log and adding 1. It then creates the new section
   * object for storage and adds it to the daily JSON object, then updates the daily JSON object
   * in the database. Lastly, it creates a new section HTML element, and adds the appropriate
   * buttons for note addition and section deletion, adds event listeners to those buttons, and
   * appends the section to the daily-log element.
   *
   * @param {HTMLElement} divElement - The daily-log div element in which the new section is
   * being created was clicked to trigger the listener
   */
  newSectionHandler (divElement) {
    const dailyLog = this;
    const dailyID = divElement.id;
    const sectionID = IDConverter.stringifyNum(this.nextSectionNum);
    this.nextSectionNum++;

    // create new section object
    const sectionObj = {
      id: sectionID,
      name: '',
      type: 'log',
      bulletIDs: [],
      nextBulNum: 0
    };

    // add section to the daily JSON object
    const data = this.data;
    data.sections.push(sectionObj);
    this.setAttribute('data', JSON.stringify(data));

    // store the updated daily JSON object in the database
    Database.store(dailyID, data);

    // create a blank section element with the section ID
    const section = document.createElement('section');
    section.id = sectionObj.id;
    section.className = sectionObj.type;

    // added a margin to the bottom of each section
    section.style = 'margin-bottom: 1vw';

    // create the editable section title element
    const sectionHead = document.createElement('div');
    const sectionName = document.createElement('h2');
    sectionName.contentEditable = 'true';
    sectionName.style.paddingLeft = '2vw';
    sectionHead.appendChild(sectionName);
    section.appendChild(sectionHead);

    // add event listener to the title to update the daily log element when the title text is updated
    sectionName.addEventListener('blur', (event) => {
      const secName = dailyLog.data.sections.filter((section) => section.id === sectionID)[0].name;
      if (sectionName.innerText !== secName) {
        const dailyData = dailyLog.data;
        for (const sec of dailyData.sections) {
          if (sec.id === sectionID) {
            sec.name = sectionName.innerText;
          }
        }
        this.setAttribute('data', JSON.stringify(dailyData));
        Database.store(dailyID, dailyData);
      }
    });

    // add event listener to the title to prevent newlines
    sectionName.addEventListener('keypress', (event) => {
      if (event.key === 'Enter') {
        sectionName.blur();
      }
    });

    // create a button to delete the section and add the delete section event listener to it
    const deleteSection = document.createElement('button');
    deleteSection.className = 'delete-section';
    deleteSection.innerHTML = `
      <i class="fas fa-times"></i>
    `;
    deleteSection.style.float = 'right';
    deleteSection.style.marginRight = '2vw';
    deleteSection.addEventListener('click', function (event) {
      dailyLog.deleteSectionHandler(event.target.closest('section'));
    });
    sectionHead.insertBefore(deleteSection, sectionName);

    // create a button to add new notes to the section and add the add new bullet event listener to it
    const newNote = document.createElement('button');
    newNote.className = 'new-bullet';
    newNote.innerHTML = `
      <i class="fas fa-plus"></i>
    `;
    newNote.addEventListener('click', function (event) {
      dailyLog.newNoteHandler(event.target.closest('section'));
    });
    section.appendChild(newNote);

    divElement.appendChild(section);

    // prompt user to start typing section name
    sectionName.focus();
  }

  /**
   * This function handles the deletion of a section. The function looks through the daily JSON
   * object to remove the section object that is being deleted, and then stores the updated daily
   * JSON object in the database and removes the section HTML element from the daily log div.
   *
   * @param {HTMLElement} sectionElement - The section element that is being deleted
   */
  deleteSectionHandler (sectionElement) {
    // get the div element that the section is a child of
    const dailyDiv = this.shadowRoot.querySelector('div.daily');

    // delete the section object from the daily JSON object
    const sectionID = sectionElement.id;
    const data = this.data;
    data.sections = data.sections.filter((sec) => sec.id !== sectionID);
    this.setAttribute('data', JSON.stringify(data));

    // store the updated daily JSON object in the database
    const dailyID = dailyDiv.id;
    Database.store(dailyID, data);

    // remove the section HTML element from the daily div
    dailyDiv.removeChild(sectionElement);
  }

  // ------------------------------------- End Event Handlers -------------------------------------
} // end class DailyLog

// define a custom element for the DailyLog web component
customElements.define('daily-log', DailyLog);