Format Messages with Dates

by MegaScience

HTML

<form id="messageFormatter" name="messageFormatter" method="post" onsubmit="return submitPost()">
    <table>
        <tr>
            <th colspan="2">Format Messages with Dates</th>
        </tr>
        <tr>
            <td colspan="2">Use this to format messages. Format is in the form <code>%L#</code>, where <code>%</code> identifies the replacement, <code>L</code> is any letter for the format type, and <code>#</code> is any number between <code>1</code> and <code>100</code> matching the number adjacent to a date in the below list. For example, typing <code>Hello %F1!</code> with the date <code>July 1, 2021 at 6:00 AM PDT</code> in slot 1 will result in the message <code>Hello &lt;t:1625144400:F&gt;!</code>, which displays in English language in EDT as <code>Hello Thursday, July 1, 2021 9:00 AM!</code> For more information, <a href="https://c.r74n.com/discord/formatting" target="_blank">this website shows the formatting</a>. Note that the letters are case sensitive.</td>
        </tr>
        <tr>
            <th>Message Format:</th>
            <td>
                <textarea type="text" id="messageFormat" name="messageFormat" oninput="expandTextarea(this)" onblur="storeData()" required></textarea>
            </td>
        </tr>
        <tr>
            <th>Dates/Times:</th>
            <td>
                <ol id="datesTimes">
                    <li><button type="button" tabindex="-1" onclick="addListItem()">+</button><span><input type="text" name="date" placeholder="Example: July 11, 2021, 12:00 PM PDT" onblur="processDate(this) && storeData()"></span></li>
                </ol>
            </td>
        </tr>
        <tr>
            <td class="controls" colspan="2">
                <input type="submit" name="submit" value="Submit" />
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <textarea type="text" id="messageOutput" name="messageOutput" placeholder="Formatted message appears...

CSS

html, body {
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #ccccc4;
  min-height: 100%;
  margin: 0;
  font-family: arial, sans-serif;
  font-size: 14px;
}

form {
  /*max-width: 960px;*/
  max-width: 880px;
  margin: 5px auto;
  min-height: calc(100% - 35px);
  background-color: #FFF;
  border: 1px solid black;
  border-radius: 25px;
  padding-bottom: 25px;
  /*max-height: 100vh;*/
  max-height: 50%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th[colspan="2"] {
  padding: 5px 0;
  font-size: 24px;
}

th:not([colspan="2"]) {
  text-align: left;
  vertical-align: top;
  width: 14em;
}

td, ol#datesTimes {
  overflow: visible;
}

tr :is(th, td) {
  --tr-padding: 10px;
  &:first-child {
    padding-left: var(--tr-padding);
  }
  &:last-child {
    padding-right: var(--tr-padding);
  }
}

textarea {
  resize: vertical;
  overflow-y: auto;
}

input, textarea {
  width: 100%;
}

input, button, textarea {
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
}

input[name="date"][data-epoch="NaN"i] {
  background-color: rgba(255, 170, 170, 100);
}

ol {
  margin: 0;
  padding-left: 0;
  list-style-position: outside;
  overflow-y: auto;
}

li span {
  display: block;
  overflow: hidden;
}

li button {
  float: right;
  padding: 0;
  width: 1.65em;
  height: 1.65em;
}

.controls input {
  display: inline-block;
  width: 100%;
}

JavaScript

class dataObject {
  constructor({ messageFormat = '', epoches = [], messageOutput = '' } = { messageFormat: '', epoches: [], messageOutput: '' }) {
    this.messageFormat = isString(messageFormat) ? messageFormat : ''
    //this.epoches = Array.isArray(epoches) && epoches.every(v => Array.isArray(v) && v.length == 2 && v.every(isString) && regex.numberOrNaNOnly.test(v[1])) ? epoches : []
    this.epoches = Array.isArray(epoches) ? epoches.map(v => Array.isArray(v) && v.length == 2 && v.every(isString) && regex.numberOrNaNOnly.test(v[1]) ? v : ['', 'NaN']) : []
    this.messageOutput = isString(messageOutput) ? messageOutput : ''
  }
}

const lsTag = 'MessageFormatter'
//const regex = /\b%(\w)([0-9]{1,2})\b/g
//const regex = /(^|\s)%([a-zA-Z])([0-9]{1,2})\b/g
const regex = {
  tags: /(^|\s)%([a-zA-Z])([0-9]{1,2})\b/g,
  ordinalIndicator: /\b([0-9]{1,2})(?:st|nd|rd|th)\b/i,
  leadingDay: /^(?:Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday), /i,
  hourOnly: /(^|\s)([0-9]{1,2}) ?(AM|PM)\b/i,
  UTF8: /%([0-9A-F]{2})/g,
  numberOrNaNOnly: /^(-?\d+|NaN)$/i,
	abbreviations: /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|Mon|Tue|Wed|Thu|Fri|Sat|Sun)\./i,
	ampm: /\b(A|P)\.?(M)(?:\.|\b)?/i
}
const dates = document.getElementsByName('date')
const dateTimesE = document.getElementById('datesTimes')
const dateRowTemplate = document.getElementById('dateRow')
const messageFormatE = document.getElementById('messageFormat')
const messageOutputE = document.getElementById('messageOutput')
let saveInterval

const isString = s => typeof s === 'string' || s instanceof String
// https://stackoverflow.com/a/1353711
//const isValidDate = d => d instanceof Date && !isNaN(d)
//const isInvalidDate = d => !(d instanceof Date && !isNaN(d))
const addListItem = () => {
  const n = dateRowTemplate.content.cloneNode(true)
  dateTimesE.appendChild(n)
  return n
}
const removeListItem = e => e.parentNode.remove()
const processDate = e =>...