"use strict"; class SidemenuRenderCache { constructor ({$lastStageSaved, $lastWrpBtnLoadExisting}) { this.$lastStageSaved = $lastStageSaved; this.$lastWrpBtnLoadExisting = $lastWrpBtnLoadExisting; } } class PageUi { constructor () { this._builders = {}; this._$menuInner = null; this._$selBuilderMode = null; this._$wrpSource = null; this._$wrpMain = null; this._$wrpInput = null; this._$wrpInputControls = null; this._$wrpOutput = null; this._allSources = []; this._$selSource = null; this._isInitialLoad = true; this.doSaveDebounced = MiscUtil.debounce(() => this._doSave(), 50); this._settings = {}; this._saveSettingsDebounced = MiscUtil.debounce(() => this._doSaveSettings(), 50); this._isLastRenderInputFail = false; this._sidemenuRenderCache = null; this._sidemenuListRenderCache = null; } set creatureBuilder (creatureBuilder) { this._builders.creatureBuilder = creatureBuilder; } set legendaryGroupBuilder (legendaryGroupBuilder) { this._builders.legendaryGroupBuilder = legendaryGroupBuilder; } set spellBuilder (spellBuilder) { this._builders.spellBuilder = spellBuilder; } get creatureBuilder () { return this._builders.creatureBuilder; } get builders () { return this._builders; } get activeBuilder () { return this._settings.activeBuilder || PageUi._DEFAULT_ACTIVE_BUILDER; } get $wrpInput () { return this._$wrpInput; } get $wrpInputControls () { return this._$wrpInputControls; } get $wrpOutput () { return this._$wrpOutput; } get $wrpSideMenu () { return this._$menuInner; } get source () { return this._settings.activeSource || ""; } get allSources () { return this._allSources; } get sidemenuRenderCache () { return this._sidemenuRenderCache; } set sidemenuRenderCache (val) { this._sidemenuRenderCache = val; } _doSave () { if (this._isInitialLoad) return; return StorageUtil.pSetForPage( PageUi._STORAGE_STATE, { builders: Object.entries(this._builders).mergeMap(([name, builder]) => ({[name]: builder.getSaveableState()})), }, ); } _doSaveSettings () { return StorageUtil.pSetForPage(PageUi._STORAGE_SETTINGS, this._settings); } async init () { this._settings = await StorageUtil.pGetForPage(PageUi._STORAGE_SETTINGS) || {}; this._$wrpLoad = $(`#page_loading`); this._$wrpSource = $(`#page_source`); this._$wrpMain = $(`#page_main`); this._settings.activeBuilder = this._settings.activeBuilder || PageUi._DEFAULT_ACTIVE_BUILDER; this._initLhs(); this._initRhs(); await this._pInitSideMenu(); const storedState = await StorageUtil.pGetForPage(PageUi._STORAGE_STATE) || {}; if (storedState.builders) { Object.entries(storedState.builders).forEach(([name, state]) => { if (this._builders[name]) this._builders[name].setStateFromLoaded(state); }); } this._doRenderActiveBuilder(); this._doInitNavHandler(); const brewSources = BrewUtil2.getSources(); if (this._settings.activeSource && brewSources.some(it => it.json === this._settings.activeSource)) { this.__setStageMain(); this._sideMenuEnabled = true; } else if (brewSources.length) { this._doRebuildStageSource({mode: "select", isRequired: true}); this.__setStageSource(); } else { this._doRebuildStageSource({mode: "add", isRequired: true}); this.__setStageSource(); } this._isInitialLoad = false; } __setStageSource () { this._$wrpLoad.hide(); this._$wrpSource.show(); this._$wrpMain.hide(); } __setStageMain () { this._$wrpLoad.hide(); this._$wrpSource.hide(); this._$wrpMain.show(); } _doRebuildStageSource (options) { SourceUiUtil.render({ ...options, $parent: this._$wrpSource, cbConfirm: async (source, isNewSource) => { if (isNewSource) await BrewUtil2.pAddSource(source); else await BrewUtil2.pEditSource(source); this._settings.activeSource = source.json; if (isNewSource) this._doAddSourceOption(source); await this._pDoHandleUpdateSource(); this._sideMenuEnabled = true; this.__setStageMain(); }, cbConfirmExisting: async (source) => { this._settings.activeSource = source.json; await this._pDoHandleUpdateSource(); this._sideMenuEnabled = true; this.__setStageMain(); }, cbCancel: () => { this._sideMenuEnabled = true; this.__setStageMain(); }, }); } _initLhs () { this._$wrpInput = $(`#content_input`); this._$wrpInputControls = $(`#content_input_controls`); } _initRhs () { this._$wrpOutput = $(`#content_output`); } getBuilderById (id) { id = id.toLowerCase().trim(); const key = Object.keys(this._builders).find(k => k.toLowerCase().trim() === id); if (key) return this._builders[key]; } async pSetActiveBuilderById (id) { id = id.toLowerCase().trim(); const key = Object.keys(this._builders).find(k => k.toLowerCase().trim() === id); await this._pSetActiveBuilder(key); } async _pSetActiveBuilder (nxtActiveBuilder) { if (!this._builders[nxtActiveBuilder]) throw new Error(`Builder "${nxtActiveBuilder}" does not exist!`); this._$selBuilderMode.val(nxtActiveBuilder); this._settings.activeBuilder = nxtActiveBuilder; if (!Hist.initialLoad) Hist.replaceHistoryHash(UrlUtil.encodeForHash(this._settings.activeBuilder)); const builder = this._builders[this._settings.activeBuilder]; builder.renderInput(); builder.renderOutput(); await builder.pRenderSideMenu(); this._saveSettingsDebounced(); } async _pInitSideMenu () { const $mnu = $(`.sidemenu`); const prevMode = this._settings.activeBuilder; const $wrpMode = $(`
Mode
`).appendTo($mnu); this._$selBuilderMode = $(` `) .appendTo($wrpMode) .change(async () => { const val = this._$selBuilderMode.val(); if (val === "none") { InputUiUtil.pGetUserBoolean({ title: "Homebrew Builder Support", htmlDescription: `

The Homebrew Builder only supports a limited set of entity types. For everything else, you will need to manually create or convert content.

`, isAlert: true, }).then(null); this._$selBuilderMode.val(this._settings.activeBuilder); return; } await this._pSetActiveBuilder(val); }); $mnu.append(PageUi.__$getSideMenuDivider(true)); const $wrpSource = $(`
Source
`).appendTo($mnu); this._allSources = BrewUtil2.getSources().sort((a, b) => SortUtil.ascSortLower(a.full, b.full)) .map(it => it.json); this._$selSource = $$` ` .appendTo($wrpSource) .change(async () => { this._settings.activeSource = this._$selSource.val(); await this._pDoHandleUpdateSource(); }); if (this._settings.activeSource) this._$selSource.val(this._settings.activeSource); else this._$selSource[0].selectedIndex = 0; const $btnSourceEdit = $(``) .click(() => { const curSourceJson = this._settings.activeSource; const curSource = BrewUtil2.sourceJsonToSource(curSourceJson); if (!curSource) return; this._doRebuildStageSource({mode: "edit", source: MiscUtil.copy(curSource)}); this.__setStageSource(); }); $$`
${$btnSourceEdit}
`.appendTo($mnu); const $btnSourceAdd = $(``).click(() => { this._doRebuildStageSource({mode: "add"}); this.__setStageSource(); }); $$`
${$btnSourceAdd}
`.appendTo($mnu); $mnu.append(PageUi.__$getSideMenuDivider(true)); this._$menuInner = $(`
`).appendTo($mnu); if (prevMode) await this._pSetActiveBuilder(prevMode); } set _sideMenuEnabled (val) { $(`.sidemenu__toggle`).toggle(!!val); } static __$getSideMenuDivider (heavy) { return $(`
`); } _doRenderActiveBuilder () { const activeBuilder = this._builders[this._settings.activeBuilder]; activeBuilder.renderInput(); activeBuilder.renderOutput(); } _doInitNavHandler () { // More obnoxious than useful (the form is auto-saved automatically); disabled until further notice /* $(window).on("beforeunload", evt => { const message = this._builders[this._settings.activeBuilder].getOnNavMessage(); if (message) { (evt || window.event).message = message; return message; } }); */ } _doAddSourceOption (source) { this._allSources.push(source.json); // TODO this should detach + re-order. Ensure correct is re-selected; ensure disabled option is first this._$selSource.append(``); this._builders[this._settings.activeBuilder].doHandleSourcesAdd(); } async _pDoHandleUpdateSource () { if (this._$selSource) this._$selSource.val(this._settings.activeSource); this._saveSettingsDebounced(); await this._builders[this._settings.activeBuilder].pDoHandleSourceUpdate(); } _getJsonOutputTemplate () { const timestamp = Math.round(Date.now() / 1000); return { _meta: { sources: [MiscUtil.copy(BrewUtil2.sourceJsonToSource(this._settings.activeSource))], dateAdded: timestamp, dateLastModified: timestamp, }, }; } } PageUi._STORAGE_STATE = "brewbuilderState"; PageUi._STORAGE_SETTINGS = "brewbuilderSettings"; PageUi._DEFAULT_ACTIVE_BUILDER = "creatureBuilder"; class Builder extends ProxyBase { static async pInitAll () { return Promise.all(Builder._BUILDERS.map(b => b.pInit())); } /** * @param opts Options object. * @param opts.titleSidebarLoadExisting Text for "Load Existing" sidebar button. * @param opts.titleSidebarDownloadJson Text for "Download JSON" sidebar button. * @param opts.metaSidebarDownloadMarkdown Meta for a "Download Markdown" sidebar button. * @param opts.prop Homebrew prop. */ constructor (opts) { super(); opts = opts || {}; this._titleSidebarLoadExisting = opts.titleSidebarLoadExisting; this._titleSidebarDownloadJson = opts.titleSidebarDownloadJson; this._metaSidebarDownloadMarkdown = opts.metaSidebarDownloadMarkdown; this._prop = opts.prop; Builder._BUILDERS.push(this); TabUiUtil.decorate(this); this._ui = null; this._isInitialLoad = true; this._sourcesCache = []; // the JSON sources from the main UI this._$selSource = null; this._cbCache = null; this.__state = this._getInitialState(); this._state = null; // proxy used to access state this.__meta = this._getInitialMetaState(); // meta state this._meta = null; // proxy used to access meta state this._$wrpBtnLoadExisting = null; this._$sideMenuStageSaved = null; this._$sideMenuWrpList = null; this._$eles = {}; // Generic internal element storage } _doResetProxies () { this._resetHooks("state"); this._resetHooks("meta"); } doCreateProxies () { this._doResetProxies(); this._state = this._getProxy("state", this.__state); this._meta = this._getProxy("meta", this.__meta); } set ui (ui) { this._ui = ui; } get prop () { return this._prop; } prepareExistingEditableBrew ({brew}) { let isAnyMod = false; if (!brew.body[this.prop]?.length) return; brew.body[this.prop].forEach(ent => { if (ent.uniqueId) return; ent.uniqueId = CryptUtil.uid(); isAnyMod = true; }); return isAnyMod; } getSaveableState () { return { s: this.__state, m: this.__meta, }; } setStateFromLoaded () { throw new TypeError(`Unimplemented method!`); } async pDoHandleSourceUpdate () { const nuSource = this._ui.source; // if the source we were using is gone, update if (!this._sourcesCache.includes(nuSource)) { this._state.source = nuSource; this._sourcesCache = MiscUtil.copy(this._ui.allSources); const $cache = this._$selSource; this._$selSource = this.$getSourceInput(this._cbCache); $cache.replaceWith(this._$selSource); } this.renderInput(); this.renderOutput(); await this.pRenderSideMenu(); this.doUiSave(); } async _pHashChange_pHandleSubHashes (sub, toLoad) { return { isAllowEditExisting: true, toLoad, }; } $getSourceInput (cb) { return BuilderUi.$getStateIptEnum( "Source", cb, this._state, { vals: this._sourcesCache, fnDisplay: Parser.sourceJsonToFull, type: "string", nullable: false, }, "source", ); } doUiSave () { // Trigger a save at a higher level this._ui.doSaveDebounced(); } async pRenderSideMenu () { // region Detach any sidemenu renders from other builders if (this._ui.sidemenuRenderCache) { if (this._ui.sidemenuRenderCache.$lastStageSaved !== this._$sideMenuStageSaved) this._ui.sidemenuRenderCache.$lastStageSaved.detach(); if (this._ui.sidemenuRenderCache.$lastWrpBtnLoadExisting !== this._$wrpBtnLoadExisting) this._ui.sidemenuRenderCache.$lastWrpBtnLoadExisting.detach(); } // endregion // region If this is our first sidemenu render, create elements if (!this._$sideMenuStageSaved) { const $btnLoadExisting = $(``) .click(() => this.pHandleSidebarLoadExistingClick()); this._$wrpBtnLoadExisting = $$`
${$btnLoadExisting}
`; const $btnDownloadJson = $(``) .click(() => this.pHandleSidebarDownloadJsonClick()); const $wrpDownloadMarkdown = (() => { if (!this._metaSidebarDownloadMarkdown) return null; const $btnDownload = $(``) .click(async () => { const entities = await this._pGetSideMenuBrewEntities(); const mdOut = await this._metaSidebarDownloadMarkdown.pFnGetText(entities); DataUtil.userDownloadText(`${DataUtil.getCleanFilename(BrewUtil2.sourceJsonToFull(this._ui.source))}.md`, mdOut); }); const $btnSettings = $(``) .click(() => RendererMarkdown.pShowSettingsModal()); return $$`
${$btnDownload}${$btnSettings}
`; })(); this._$sideMenuWrpList = this._$sideMenuWrpList || $(`
`); this._$sideMenuStageSaved = $$`
${PageUi.__$getSideMenuDivider().hide()}
${$btnDownloadJson}
${$wrpDownloadMarkdown} ${this._$sideMenuWrpList}
`; } // endregion // Make our sidemenu internal wrapper visible this._$wrpBtnLoadExisting.appendTo(this._ui.$wrpSideMenu); this._$sideMenuStageSaved.appendTo(this._ui.$wrpSideMenu); this._ui.sidemenuRenderCache = new SidemenuRenderCache({ $lastWrpBtnLoadExisting: this._$wrpBtnLoadExisting, $lastStageSaved: this._$sideMenuStageSaved, }); await this._pDoUpdateSidemenu(); } getOnNavMessage () { if (this._meta.isModified) return "You have unsaved changes! Are you sure you want to leave?"; else return null; } async _pGetSideMenuBrewEntities () { const brew = await BrewUtil2.pGetOrCreateEditableBrewDoc(); return MiscUtil.copy((brew.body[this._prop] || []).filter(entry => entry.source === this._ui.source)) .sort((a, b) => SortUtil.ascSort(a.name, b.name)); } async _pDoUpdateSidemenu () { this._sidemenuListRenderCache = this._sidemenuListRenderCache || {}; const toList = await this._pGetSideMenuBrewEntities(); this._$sideMenuStageSaved.toggleVe(!!toList.length); const metasVisible = new Set(); toList.forEach((ent, ix) => { metasVisible.add(ent.uniqueId); if (this._sidemenuListRenderCache[ent.uniqueId]) { const meta = this._sidemenuListRenderCache[ent.uniqueId]; meta.$row.showVe(); if (meta.name !== ent.name) { meta.$dispName.text(ent.name); meta.name = ent.name; } if (meta.position !== ix) { meta.$row.css("order", ix); meta.position = ix; } return; } const $btnEdit = $(``) .click(async () => { if ( this.getOnNavMessage() && !await InputUiUtil.pGetUserBoolean({title: "Discard Unsaved Changes", htmlDescription: "You have unsaved changes. Are you sure?", textYes: "Yes", textNo: "Cancel"}) ) return; await this.pHandleSidebarEditUniqueId(ent.uniqueId); }); const menu = ContextUtil.getMenu([ new ContextUtil.Action( "Duplicate", async () => { const copy = MiscUtil.copy(await BrewUtil2.pGetEditableBrewEntity(this._prop, ent.uniqueId, {isDuplicate: true})); // Get the root name without trailing numbers, e.g. "Goblin (2)" -> "Goblin" const m = /^(.*?) \((\d+)\)$/.exec(copy.name.trim()); if (m) copy.name = `${m[1]} (${Number(m[2]) + 1})`; else copy.name = `${copy.name} (1)`; await BrewUtil2.pPersistEditableBrewEntity(this._prop, copy); await this._pDoUpdateSidemenu(); }, ), new ContextUtil.Action( "View JSON", async (evt) => { const out = this._ui._getJsonOutputTemplate(); out[this._prop] = [ PropOrder.getOrdered( DataUtil.cleanJson(MiscUtil.copy(await BrewUtil2.pGetEditableBrewEntity(this._prop, ent.uniqueId))), this._prop, ), ]; const $content = Renderer.hover.$getHoverContent_statsCode(this._state); Renderer.hover.getShowWindow( $content, Renderer.hover.getWindowPositionFromEvent(evt), { title: `${this._state.name} \u2014 Source Data`, isPermanent: true, isBookContent: true, }, ); }, ), new ContextUtil.Action( "Download JSON", async () => { const out = this._ui._getJsonOutputTemplate(); const cpy = MiscUtil.copy(await BrewUtil2.pGetEditableBrewEntity(this._prop, ent.uniqueId)); out[this._prop] = [DataUtil.cleanJson(cpy)]; DataUtil.userDownload(DataUtil.getCleanFilename(cpy.name), out); }, ), new ContextUtil.Action( "View Markdown", async (evt) => { const entry = MiscUtil.copy(await BrewUtil2.pGetEditableBrewEntity(this._prop, ent.uniqueId)); const name = `${entry._displayName || entry.name} \u2014 Markdown`; const mdText = RendererMarkdown.get().render({ entries: [ { type: "statblockInline", dataType: this._prop, data: entry, }, ], }); const $content = Renderer.hover.$getHoverContent_miscCode(name, mdText); Renderer.hover.getShowWindow( $content, Renderer.hover.getWindowPositionFromEvent(evt), { title: name, isPermanent: true, isBookContent: true, }, ); }, ), new ContextUtil.Action( "Download Markdown", async () => { const entry = MiscUtil.copy(await BrewUtil2.pGetEditableBrewEntity(this._prop, ent.uniqueId)); const mdText = CreatureBuilder._getAsMarkdown(entry).trim(); DataUtil.userDownloadText(`${DataUtil.getCleanFilename(entry.name)}.md`, mdText); }, ), ]); const $btnBurger = $(``) .click(evt => ContextUtil.pOpenMenu(evt, menu)); const $btnDelete = $(``) .click(async () => { if (!await InputUiUtil.pGetUserBoolean({title: "Delete Entity", htmlDescription: "Are you sure?", textYes: "Yes", textNo: "Cancel"})) return; if (this._state.uniqueId === ent.uniqueId) this.reset(); await BrewUtil2.pRemoveEditableBrewEntity(this._prop, ent.uniqueId); await this._pDoUpdateSidemenu(); await this.pDoPostDelete(); }); const $dispName = $$`${ent.name}`; const $row = $$`
${$dispName}
${$btnEdit}${$btnBurger}${$btnDelete}
`.appendTo(this._$sideMenuWrpList); this._sidemenuListRenderCache[ent.uniqueId] = { $dispName, $row, name: ent.name, ix, }; }); Object.entries(this._sidemenuListRenderCache) .filter(([uniqueId]) => !metasVisible.has(uniqueId)) .forEach(([, meta]) => meta.$row.hideVe()); } async pHandleSidebarEditUniqueId (uniqueId) { const entEditable = await BrewUtil2.pGetEditableBrewEntity(this._prop, uniqueId); this.setStateFromLoaded({ s: MiscUtil.copy(entEditable), m: this._getInitialMetaState({ isModified: false, isPersisted: false, }), }); this.renderInput(); this.renderOutput(); this.doUiSave(); } async pHandleSidebarDownloadJsonClick () { const out = this._ui._getJsonOutputTemplate(); out[this._prop] = (await this._pGetSideMenuBrewEntities()).map(entry => PropOrder.getOrdered(DataUtil.cleanJson(MiscUtil.copy(entry)), this._prop)); DataUtil.userDownload(DataUtil.getCleanFilename(BrewUtil2.sourceJsonToFull(this._ui.source)), out); } renderInputControls () { const $wrpControls = this._ui.$wrpInputControls.empty(); const $btnSave = $(``) .click(() => this._pHandleClick_pSaveBrew()) .appendTo($wrpControls); const hkBtnSaveText = () => $btnSave.text(this._meta.isModified ? "Save *" : "Saved"); this._addHook("meta", "isModified", hkBtnSaveText); hkBtnSaveText(); $(``) .click(async (evt) => { if (!await InputUiUtil.pGetUserBoolean({title: "Reset Builder", htmlDescription: "Are you sure?", textYes: "Yes", textNo: "Cancel"})) return; this.reset({isResetAllMeta: !!evt.shiftKey}); }) .appendTo($wrpControls); } reset ({isResetAllMeta = false} = {}) { const metaNext = this._getInitialMetaState(); if (!isResetAllMeta) this._reset_mutNextMetaState({metaNext}); this.setStateFromLoaded({ s: this._getInitialState(), m: metaNext, }); this.renderInput(); this.renderOutput(); this.doUiSave(); } _reset_mutNextMetaState ({metaNext}) { /* Implement as required */ } async _pHandleClick_pSaveBrew () { const source = this._state.source; if (!source) throw new Error(`Current state has no "source"!`); const clean = DataUtil.cleanJson(MiscUtil.copy(this.__state), {isDeleteUniqueId: false}); if (this._meta.isPersisted) { await BrewUtil2.pPersistEditableBrewEntity(this._prop, clean); await this.pRenderSideMenu(); } else { // If we are e.g. editing a copy of a non-editable brew's entity, we need to first convert the parent brew // to "editable." if ( BrewUtil2.sourceJsonToSource(source) && !await BrewUtil2.pIsEditableSourceJson(source) ) { const isMove = await InputUiUtil.pGetUserBoolean({ title: "Move to Editable Homebrew Document", htmlDescription: `
Saving "${this._state.name}" with source "${this._state.source}" will move all homebrew from that source to the editable homebrew document.
Moving homebrew to the editable document will prevent it from being automatically updated in future.
Do you wish to proceed?
Giving "${this._state.name}" an editable source will avoid this issue.
`, textYes: "Yes", textNo: "Cancel", }); if (!isMove) return; const brew = await BrewUtil2.pMoveOrCopyToEditableBySourceJson(source); if (!brew) throw new Error(`Failed to make brew for source "${source}" editable!`); const nxtBrew = MiscUtil.copy(brew); // Ensure everything has a `uniqueId` let isAnyMod = this.prepareExistingEditableBrew({brew: nxtBrew}); // We then need to attempt a find-replace on the hash of our current entity, as we may be trying to update // one exact entity. This is not needed if e.g. a renamed copy of an existing entity is being made. const hash = UrlUtil.URL_TO_HASH_BUILDER[this._prop](clean); const ixExisting = (brew.body[this._prop] || []).findIndex(it => UrlUtil.URL_TO_HASH_BUILDER[this._prop](it) === hash); if (~ixExisting) { clean.uniqueId = clean.uniqueId || nxtBrew.body[this._prop][ixExisting].uniqueId; nxtBrew.body[this._prop][ixExisting] = clean; isAnyMod = true; } if (isAnyMod) await BrewUtil2.pSetEditableBrewDoc(nxtBrew); } await BrewUtil2.pPersistEditableBrewEntity(this._prop, clean); this._meta.isPersisted = true; this._meta.isModified = false; await SearchWidget.P_LOADING_CONTENT; await SearchWidget.pAddToIndexes(this._prop, clean); } this._meta.isModified = false; this.doUiSave(); await this.pDoPostSave(); await this._pDoUpdateSidemenu(); } // TODO use this in creature builder /** * @param doUpdateState * @param rowArr * @param row * @param $wrpRow * @param title * @param [opts] Options object. * @param [opts.isProtectLast] * @param [opts.isExtraSmall] * @return {jQuery} */ static $getBtnRemoveRow (doUpdateState, rowArr, row, $wrpRow, title, opts) { opts = opts || {}; return $(``) .click(() => { rowArr.splice(rowArr.indexOf(row), 1); $wrpRow.empty().remove(); doUpdateState(); }); } $getFluffInput (cb) { const [$row, $rowInner] = BuilderUi.getLabelledRowTuple("Flavor Info"); const imageRows = []; const doUpdateState = () => { const out = {}; const entries = UiUtil.getTextAsEntries($iptEntries.val()); if (entries && entries.length) out.entries = entries; const images = imageRows.map(it => it.getState()).filter(Boolean); if (images.length) out.images = images; if (out.entries || out.images) this._state.fluff = out; else delete this._state.fluff; cb(); }; const doUpdateOrder = () => { imageRows.forEach(it => it.$ele.detach().appendTo($wrpRows)); doUpdateState(); }; const $wrpRowsOuter = $(`
`); const $wrpRows = $(`
`).appendTo($wrpRowsOuter); const rowOptions = {$wrpRowsOuter}; const $iptEntries = $(`