/** * @author: Donnal Walter * DateTime: 2021-04-21 08:30 * Description: * TinyJS is a micro-framework for building single page applications in HTML, CSS, and JavaScript. * - The tinyJS library is added to the element of an HTML page along with other JavaScript * files needed for the application. * * - The app is initialized by calling tinyJS.init(). */ var tiny = tiny || { }; /** * tinyJS is a JavaScript module with immediate invocation upon definition. * The only required public method is tinyJS.init(). * Other public methods are defined for embedding in demonstration applications. */ var tinyJS = (function () { var app, // application object domContainer, // set to document.body AFTER page is loaded account, // the account id for this set of episodes dbase, // database instance for this account episodes, // an array of episodes (data objects) current, // index into episodes for current episode cursor, // index for scrolling back through episodes tinyDB, // internal module for local storage time, // internal module for dates display, // internal module for display refresh dataFlag, // internal module for data-flag attribute dataSingular, // internal module for data-singular attribute dataEpisode, // internal module for data-episode attribute dataModel, // internal module for data-model attribute tabButton, // internal module for tab buttons optionButton, // internal module for option buttons timeline, // internal module for timeline component dataList, // internal module for data-list attribute dataHistory, // internal module for dataHistory class (table) dataDisplay, dataHide, dataCPOE; function put(d, id, value) { id = id.split('.'); while (id.length > 1) {d = d[id.shift()]; } d[id] = value; } function get(d, id) { id = id.split('.'); while (id.length > 1) {d = d[id.shift()]; } return d[id]; } // wrapper function for "put" function enter(d, id, value, key) { if (tiny.logic.enter) { tiny.logic.enter(d, id, value, key); } else {put(d, id, value); } } // wrapper function for "get" function acquire(d, id) { try { var val = get(d, id); if (val === undefined) {throw 'error'; } return val; } catch(e) {console.log(id + " undefined"); } } // parse the id string, find the variable in dataArray, and // return the value of that variable function getDataValue(id) { var d; id = id.split('|'); if (id[0] === 'prev') { if (cursor > -1) { d = episodes[cursor]; // this is a "previous" data object id = id[1]; } // this is the variable name else return ''; } else { d = episodes[current]; // this is the current data object id = id[0]; } // this is the variable name return acquire(d, id); } function getHistoryValue(id, index) { var d = episodes[index]; // this is a "previous" data object return acquire(d, id); } function getFieldInfo(element) { var step = element.dataset.step || 1, places = 0; if (step < 1) { // places = Math.log10(step); // Math.log10 does not work in IE11 places = Math.log(step)/Math.log(10); // log10(X) = ln(X)/ln(10) places = Math.abs(places); places = Math.ceil(places); } return { step: step, places: places }; } // return as an object /** * An internal sub-module to provide local data storage for testing and * demonstration purposes. */ tinyDB = (function () { var id; return { init: function (account) { id = account; }, save: function (data) { var string = JSON.stringify(data); window.localStorage.setItem(id, string); }, retrieve: function () { var string = window.localStorage.getItem(id); return JSON.parse(string); }, clear: function (data) { window.localStorage.setItem(id, null); } }; }()); // immediate invocation /** * An internal sub-module to handle dates, abridged from the TINY application, * which may be re-expanded as needed. */ time = (function () { var one_hour = 1000 * 60 * 60, // milliseconds in an hour one_day = one_hour * 24; // milliseconds in a day return { // create a date string from a JS date object stringify: function (d) { function pad(n) {return n < 10 ? '0' + n : n; } return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }, // create a JS date object from a date string parse: function (string) { var dd = string.split('-'); return new Date(dd[0], dd[1] - 1, dd[2]); }, // create a time stamp for current time (date string only) stamp: function () { return this.stringify(new Date()); } }; }()); // immediate invocation /** * An internal sub-module to support the data-singular attribute, * which allows singular and plural versions of text */ dataSingular = (function () { function refresh() { var elements, element, i, id, attr, value; elements = domContainer.querySelectorAll("[data-singular]"); for (i = 0; i < elements.length; i = i + 1) { element = elements[i]; attr = element.dataset.singular.split(" "); id = attr[0]; value = getDataValue(id); if (value === 1) { element.innerText = attr[1]; } else {element.innerText = attr[2]; } } } return { refresh: refresh // expose this private function }; }()); // immediate invocation /** * An internal sub-module to support the data-display attribute, * which allows blocks to be displayed or removed */ dataDisplay = (function () { function refresh() { var elements, element, i, id, value; elements = domContainer.querySelectorAll("[data-display]"); for (i = 0; i < elements.length; i = i + 1) { element = elements[i]; id = element.dataset.display; value = getDataValue(id); switch (element.tagName) { case 'DIV': element.style.display = value ? "block" : "none"; break; case 'SPAN': element.style.display = value ? "inline" : "none"; break; default: // all other elements for now element.style.display = value ? "inline" : "none"; break; } } } return { refresh: refresh // expose this private function }; }()); // immediate invocation /** * An internal sub-module to support the data-hide attribute, * which allows element to be hidden (takes up space) or visible */ dataHide = (function () { function refresh() { var elements, element, i, id, value; elements = domContainer.querySelectorAll("[data-hide]"); for (i = 0; i < elements.length; i = i + 1) { element = elements[i]; id = element.dataset.hide; if (getDataValue(id)) { element.style.visibility = "hidden"; element.classList.add("hidden"); } else { element.style.visibility = "visible"; element.classList.remove("hidden"); } } } return { refresh: refresh // expose this private function }; }()); // immediate invocation /** * An internal sub-module to generate the Timeline component, * which provides a graphic visualization of feeding schedules */ timeline = (function () { function formatTable(table, name) { table.className = name; return table; } function buildSchedule(d) { var table = document.createElement("table"), row = document.createElement("tr"), on = d.on, off, cell, i; if (on < 0.25) {on = 0.33; } if (on > d.interval) {on = d.interval; } on = on / d.interval; off = 1 - on; on = 100 * on / d.number; off = 100 * off / d.number; for (i = 0; i < d.number; i++) { cell = document.createElement("td"); cell.className = "timeline--on"; cell.style.width = on + "%"; cell.style.height = "0.8rem"; row.appendChild(cell); if (off > 0) { cell = document.createElement("td"); cell.className = "timeline--off"; cell.style.width = off + "%"; row.appendChild(cell); } } table.appendChild(row); table.className = "timeline--schedule"; return table; } function buildScheduleContext(t0, frame, d) { var table = document.createElement("table"), row = document.createElement("tr"), cell = document.createElement("td"), pre = d.startHour - t0, post; if (pre < 0) {pre = pre + 24; } post = frame - pre - d.number * d.interval; pre = 100 * pre / frame; post = 100 * post / frame; cell.style.width = pre + "%"; row.appendChild(cell); cell = document.createElement("td"); cell.appendChild(buildSchedule(d)); row.appendChild(cell); cell = document.createElement("td"); cell.style.width = post + "%"; row.appendChild(cell); table.appendChild(row); return table; } function buildScheduleRow(name, start, frame, d) { var table = document.createElement("table"), row = document.createElement("tr"), cell = document.createElement("th"), text = document.createTextNode(name); cell.appendChild(text); cell.style.width = "6.25rem"; row.appendChild(cell); cell = document.createElement("td"); cell.appendChild(buildScheduleContext(start, frame, d)); row.appendChild(cell); table.appendChild(row); return table; } function buildHeader(start, frame, tick) { var table = document.createElement("table"), row = document.createElement("tr"), n = frame / tick, cell, text, i, t; for (i = 0; i < n; i++) { t = (start + tick * i) % 24; if (t < 10) {t = "0" + t; } cell = document.createElement("td"); // cell.className = "timeline--header"; text = document.createTextNode(t + "00 "); cell.appendChild(text); row.appendChild(cell); } table.appendChild(row); table.className = "timeline--header"; return table; } function buildHeaderRow(start, frame, tick) { var table = document.createElement("table"), row = document.createElement("tr"), cell = document.createElement("th"); cell.appendChild(document.createTextNode("Timeline")); cell.style.width = "6.25rem"; row.appendChild(cell); cell = document.createElement("td"); cell.appendChild(buildHeader(start, frame, tick)); row.appendChild(cell); table.appendChild(row); table.className = "timeline--headerRow"; return table; } function refresh() { var elements = document.querySelectorAll("[data-timeline]"), element, setup, start, tick, frame, schedules, id, parms, i, j, name; for (i = 0; i < elements.length; i = i + 1) { element = elements[i]; setup = "[" + element.dataset.timeline + "]"; setup = JSON.parse(setup); start = setup[0] || 16; frame = setup[1] || 36; tick = setup[2] || 4; while (element.firstChild) {element.removeChild(element.firstChild); } element.appendChild(buildHeaderRow(start, frame, tick)); schedules = element.dataset.schedules.replace(/ /g, ""); schedules = schedules.split(","); for (j = 0; j < schedules.length; j = j + 1) { id = schedules[j]; parms = getDataValue(id); if (parms && parms.number > 0 || j == 0) { name = "Schedule " + (j + 1); element.appendChild(buildScheduleRow(name, start, frame, parms)); } } } } return { refresh: refresh // expose this function }; }()); // immediate invocation /** * An internal sub-module to refresh the UI display. */ display = (function () { // remove trailing zeros from numeric string function unTrailZero(value) { while (value.slice(-1) === '0') { value = value.slice(0, -1); } if (value.slice(-1) === '.') { value = value.slice(0, -1); } return value; } // reformat numeric value for a given element function reNumber(element, value) { var field = getFieldInfo(element), digits = field.places; // console.log(value); value = value.toFixed(digits); if (digits > 0) { value = unTrailZero(value); } // no trailing zeros if (Math.abs(value) < 0.001 || !isFinite(value) || isNaN(value)) { value = ''; } // empty string return value; } // this is the work horse of TinyJS function refresh() { var elements, element, i, j, k, id, value, opts, btns, btn, group, items, item; elements = domContainer.querySelectorAll("[data-model]"); for (i = 0; i < elements.length-1; i = i + 1) { ////// I don't understand why it needs to be length-1 element = elements[i]; id = element.dataset.model; value = getDataValue(id); if (typeof value === 'number') { value = reNumber(element, value); } // reformat numeric value switch (element.tagName) { case 'SELECT': // element switch (element.getAttribute('type')) { case 'text': if (element.hasAttribute("data-list")) { opts = tiny.facts[element.dataset.list]; for (j = 0; j < opts.length; j++) { group = opts[j]; items = group.items; for (k = 0; k < items.length; k++) { item = items[k]; if (item.id === value) { value = item.label; j = opts.length; // break outer loop break; } } } } // break inner loop element.value = value; break; case 'checkbox': element.checked = value; // value never of type 'string' break; case 'radio': element.checked = (value === element.value); break; case 'button': // buttons not refreshed, write only default: // no other input types refreshed either break; } break; case 'DIV': switch (element.className) { case 'bar': // this
is an options toolbar btns = element.querySelectorAll("button.option"); for (j = 0; j < btns.length; j = j + 1) { btn = btns[j]; if (btn.value === value) { btn.classList.add("option--active"); } else { btn.classList.remove("option--active"); } } break; default: // all other
s use innerText element.innerText = value; break; } break; case 'SPAN': element.innerText = value; break; case 'TEXTAREA': element.value = value; break; case 'TD': element.innerText = value; break; case 'BUTTON': // buttons not refreshed, write only default: // no other elements refreshed break; } } } return { refresh: function () { dataDisplay.refresh(); dataHide.refresh(); dataSingular.refresh(); timeline.refresh(); refresh(); }, reNumber: reNumber }; }()); // immediate invocation /** * An internal sub-module to support the data-model attribute, * which is the bidirectional data connection model<==>view. */ dataModel = (function () { // Translate event.key for IE11 function getKey(e) { var key = e.key; if (key === 'Up' || key === 'Down' || key === 'Left' || key === 'Right') { key = 'Arrow' + key; } return key; } // Handle the keydown events, enforce numbers, arrows function keyDown(e) { var element = e.target, key = getKey(e), val = element.value, field = getFieldInfo(element), digits = field.places, step = field.step; if ((key >= 0 && key <= 9) || key === 'Backspace' || key === 'Tab' || key === 'ArrowLeft' || key === 'ArrowRight' || key === 'Delete' || key === 'Del') { return; } if (key === '.') { if (!(/\./.test(val))) { return; } } if (key === 'ArrowUp') { val = val - (-step); element.value = val.toFixed(digits); return; } if (key === 'ArrowDown') { val = val - step; if (val < 0) { val = 0; } element.value = val.toFixed(digits); return; } if (e.preventDefault) { e.preventDefault(); } // W3C else { e.returnValue = false; } } // IE // The final common pathway for all change events function change(e) { var d = episodes[current], key = getKey(e), element = e.target, // W3C value = element.value, id = element.dataset.model; if (element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox') { value = element.checked; } // make Boolean if (element.dataset.step) { value = +value; } // make number enter(d, id, value, key); tiny.logic.recalculate(d); // console.log(JSON.stringify(d)) display.refresh(); } // Keyup event looks for arrows or function keyUp(e) { var key = getKey(e); if (key === 'ArrowUp' || key === 'ArrowDown' || key === 'Enter') { change(e); } } return { // Bind all data-model fields to same change event handler, // which parses data-model attribute bind: function () { var mytype, element, i, elements = domContainer.querySelectorAll("input[data-model]"); for (i = 0; i < elements.length; i = i + 1) { element = elements[i]; mytype = element.getAttribute('type'); // parse data-model attribute switch (mytype) { case 'button': case 'checkbox': case 'radio': element.onclick = change; // readonly is meaningless break; case 'text': if (!element.readOnly) { if (element.dataset.step) { element.onkeydown = keyDown; } element.onkeyup = keyUp; element.onblur = change; } else {element.disabled = true; } break; } } elements = domContainer.querySelectorAll("select[data-model]"); for (i = 0; i < elements.length; i = i + 1) { element = elements[i]; element.onchange = change; } // should never be readonly elements = domContainer.querySelectorAll("textarea[data-model]"); for (i = 0; i < elements.length; i = i + 1) { element = elements[i]; if (!element.readonly) { element.onblur = change; } } elements = domContainer.querySelectorAll("button[data-model]"); for (i = 0; i < elements.length; i = i + 1) { element = elements[i]; element.onclick = change; } } }; }()); // immediate invocation /** ******************************************************************************************* * An internal sub-module to support tab buttons (and tabs toolbar) */ tabButton = (function () { // The onclick event handler for tab buttons in a tabs toolbar. function select(e) { var id, sheet, hidden, old, btn = e.target, bar = btn.parentElement; while (!bar.classList.contains("bar")) { bar = bar.parentElement; } old = bar.querySelector("button.tab--active"); if (old) { old.classList.remove("tab--active"); id = ".sheet--" + old.value; sheet = domContainer.querySelector(id); if(sheet) {sheet.style.display = "none"; } } if (btn.value === "add") { hidden = bar.querySelectorAll("button.tab--hide"); if (hidden) { if (hidden.length === 1) { btn.classList.add("tab--hide"); } btn = hidden[0]; btn.classList.remove("tab--hide"); } } btn.classList.add("tab--active"); id = ".sheet--" + btn.value; sheet = domContainer.querySelector(id); if(sheet) {sheet.style.display = "block"; } } return { // Bind the tab button onclick event to the "select Tab" handler. bind: function () { var tab, i, id, sheet, tabs = domContainer.querySelectorAll("button.tab"); for (i = 0; i < tabs.length; i = i + 1) { tab = tabs[i]; tab.onclick = select; id = ".sheet--" + tab.value; sheet = domContainer.querySelector(id); if (sheet) { if (tab.classList.contains("tab--active")) { sheet.style.display = "block"; } else { sheet.style.display = "none"; } } } } }; }()); // immediate invocation of tab-button sub-module /** ******************************************************************************************* * An internal sub-module to support option buttons (and options toolbar) */ optionButton = (function () { // The onclick event handler for option buttons in an options toolbar. function select(e) { var id, bar, old, d = episodes[current], btn = e.target, value = btn.value; bar = btn.parentElement; while (!bar.classList.contains("bar")) { bar = bar.parentElement; } old = bar.querySelector("button.option--active"); if (old) {old.classList.remove("option--active"); } btn.classList.add("option--active"); id = bar.dataset.model; enter(d, id, value); // will never have event.key tiny.logic.recalculate(d); display.refresh(); } return { // Bind the option button onclick event to the "select Option" handler. bind: function () { var btn, i, list = domContainer.querySelectorAll("button.option"); for (i = 0; i < list.length; i = i + 1) { btn = list[i]; btn.onclick = select; } } }; }()); // immediate invocation of option-button sub-module /** ******************************************************************************************* * An internal sub-module to support the data-list attribute * which turns in input element into a selection control */ dataList = (function () { var dropdown, // all data-lists use the same dropdown div focusDropDown, // onclick handler for nabla selectGroup, // onclick handler for group span selectItem, // onclick handler for item span openDropDown, // onfocus handler for input element searchDataList; // onkeyup handler for input element // generate the DOM for one group label function genGroupLabel(group) { var span, text, attr; span = document.createElement("span"); text = document.createTextNode(group.label); attr = document.createAttribute("data-param"); span.appendChild(text); span.setAttributeNode(attr); span.setAttribute("data-param", group.label); span.className = "list-group"; span.onclick = selectGroup; return span; } // generate the DOM for one item label function genItemLabel(item) { var span = document.createElement("span"), text = document.createTextNode("\u00a0 \u00a0" + item.label), attr = document.createAttribute("data-param"); span.appendChild(text); span.setAttributeNode(attr); span.setAttribute("data-param", item.id); span.className = "list-item"; span.onclick = selectItem; return span; } // generate the DOM for the dropdown list function genListDisplay(data) { var i, j, group, items, item, div = document.createElement("div"); for (i = 0; i < data.length; i = i + 1) { group = data[i]; div.appendChild(genGroupLabel(group, i)); div.appendChild(document.createElement("br")); items = group.items; for (j = 0; j < items.length; j = j + 1) { item = items[j]; div.appendChild(genItemLabel(item)); div.appendChild(document.createElement("br")); } } return div; } // seek only group and item labels matching one key from the input value function narrowByKey(list, key) { var i, j, group, group2, items, item, less = []; for (i = 0; i < list.length; i++) { group = list[i]; if (group.label.toLowerCase().indexOf(key) > -1) { less.push(group); } else { items = group.items; group2 = null; for (j = 0; j < items.length; j++) { item = items[j]; if (item.label.toLowerCase().indexOf(key) > -1) { group2 = group2 || {label: group.label, items: [] }; group2.items.push(item); } else if (item.aka.toLowerCase().indexOf(key) > -1) { group2 = group2 || {label: group.label, items: [] }; group2.items.push(item); } } if (group2) {less.push(group2); } } } return less; } // seek groups and items matching all keys in the input value function narrow(list, seek) { var k, keys = seek.toLowerCase().split(' '); for (k = 0; k < keys.length; k++) { list = narrowByKey(list, keys[k]); } return list; } // narrow the data-list, generate the display, replace dropdown
content function search(list, value) { while (dropdown.firstChild) {dropdown.removeChild(dropdown.firstChild); } dropdown.appendChild(genListDisplay(narrow(list, value))); } // onkeyup e handler for the element searchDataList = function (e) { var element = e.target, list = tiny.facts[element.dataset.list]; search(list, element.value); }; // onfocus e handler for the element openDropDown = function (e) { var element = e.target, rect = element.getBoundingClientRect(), left = parseInt(rect.left), bottom = parseInt(rect.bottom), width = parseInt(rect.width); bottom = bottom + window.pageYOffset; dropdown.style.display = "block"; dropdown.style.left = left + "px"; dropdown.style.top = bottom + "px"; dropdown.style.width = width + "px"; dropdown.style.fontSize = "0.8em"; dropdown.id = "dd" + element.id; dropdown.setAttribute("data-model", element.dataset.model); searchDataList(e); }; // onclick e handler on nabla to change focus to input element focusDropDown = function (e) { var element = e.target; element.previousElementSibling.focus(); }; // onclick e handler for group in dropdown selectGroup = function (e) { var item = e.target, dd = item.parentElement.parentElement, id = dd.id.slice(2), elem = document.getElementById(id), list = tiny.facts[elem.dataset.list]; elem.value = item.dataset.param; search(list, elem.value); }; // onclick e handler for item in dropdown selectItem = function (e) { var d = episodes[current], element = e.target, value = element.dataset.param, dd = element.parentElement.parentElement, id = dd.dataset.model; enter(d, id, value); // event.key unnecessary tiny.logic.recalculate(d); display.refresh(); dd.style.display = "none"; }; return { // create the dropdown div for the data-list init: function () { dropdown = document.createElement("div"); dropdown.style.border = "solid black thin"; dropdown.style.background = "#ffffff"; dropdown.style.maxHeight = "100px"; dropdown.style.overflowY = "scroll"; dropdown.style.position = "absolute"; dropdown.style.display = "none"; dropdown.setAttributeNode(document.createAttribute("data-model")); document.body.appendChild(dropdown); }, // bind the event handlers to input elements with data-list attributes bind: function () { var element, i, next, text='', list = domContainer.querySelectorAll("input[data-list]"); for (i = 0; i < list.length; i = i + 1) { element = list[i]; element.id = element.id || "dl-" + i; element.onfocus = openDropDown; element.onkeyup = searchDataList; element.onblur = null; // prevent data-model change event next = element.nextElementSibling; // look for nabla if (next) {text = next.textContent; } if (text.indexOf('\u25BC') > -1) { // U+9660 is hex 25BC (nabla) next.style.cursor = "pointer"; next.onclick = focusDropDown; } } } }; // end return }()); // immediate invocation of dataList sub-module /** ******************************************************************************************* * An internal sub-module to support the data-episode attribute * which turns button into episode cursor control */ dataEpisode = (function () { function select(e) { var action, L = episodes.length, btn = e.target; while (btn.tagName !== "BUTTON") {btn = btn.parentElement; } action = btn.dataset.episode; switch (action) { case 'reset': episodes = [ tiny.logic.make() ]; tiny.logic.recalculate(episodes[0]); episodes.push(clone(episodes[0])); current = 1; cursor = 0; console.log(episodes.length); console.log(current + " " + cursor); // episodes[current] = tiny.logic.make(); break; case 'prev': if (cursor > 0) { cursor += -1; } break; case 'next': if (cursor < L-2) { cursor += 1; } break; case 'last': cursor = L - 2; break; case 'save': episodes.push(clone(episodes[current])); current = current + 1; console.log(episodes.length); console.log(current + " " + cursor); // dbase.save(episodes); break; case 'clear': dbase.clear(); break; default: console.log("none of the above"); break; } display.refresh(); } return { init: function () { }, bind: function () { var element, i, elements = domContainer.querySelectorAll("button[data-episode]"); for (i = 0; i < elements.length; i = i + 1) { element = elements[i]; element.onclick = select; } } }; // end return }()); // immediate invocation of dataList sub-module /** ******************************************************************************************* * An internal sub-module to support the "data history" class * which generates an HTML grid from the episodes array. */ dataHistory = (function () { var i, j, id, item, list, value, cell, row, table; // cannot be assigned at initial invocation function generateHeader() { table = document.querySelector("table.history"); if (!table) {return; } row = table.rows[0]; for (j = episodes.length-2; j > -1; j--) { cell = document.createElement("td"); cell.className = "gridHeader"; value = getHistoryValue("date", j); value = value.slice(5,10); cell.innerText = value; row.appendChild(cell); } } function generate() { generateHeader(); if (!table) {return; } list = table.querySelectorAll("[data-model]"); for (i = 1; i < list.length; i++) { item = list[i]; id = item.dataset.model; row = item.parentElement.parentElement; for (j = episodes.length-2; j > -1; j--) { cell = document.createElement("td"); cell.className = "grid"; value = getHistoryValue(id, j); value = display.reNumber(item, value); cell.innerText = value; row.appendChild(cell); } } } return { init: generate }; // end return }()); // immediate invocation of dataHistory sub-module /** ******************************************************************************************* * An internal sub-module to support posting to the CPOE system */ dataCPOE = (function () { function place(e) { var opt = e.target.dataset.cpoe; tiny.cpoe.placeOrders(opt, episodes[current], episodes[current-1]); } return { bind: function () { var element = domContainer.querySelector("button[data-cpoe]"); if (element) {element.onclick = place; } } }; // end return }()); // immediate invocation of dataCPOE sub-module /** * Utility function used by an enclosing application to inject a partial data object * into an existing data object */ function copyInto(d, s) {var i; for (i in d) { if (s[i] instanceof Object) { // a branch copyInto(d[i], s[i]); } // recursion else if (s[i]) { // a leaf node d[i] = s[i]; } } } // copy value function clone(obj) { return JSON.parse(JSON.stringify(obj)); } return { init: function() { var theday, today = time.stamp(); // urlParams = new URLSearchParams(window.location.search); // account = urlParams.get('id') || "TinyJSDefault"; account = "TinyJSDefault"; console.log(account); domContainer = document.body; // console.log(JSON.stringify(tiny.facts)) tiny.logic.init(tiny.facts); dbase = tiny.dbase || tinyDB; dbase.init(account); episodes = dbase.retrieve(); // episodes = null; if (episodes === null) { episodes = [tiny.logic.make(), tiny.logic.make()]; // episodes = [tiny.logic.make()]; episodes[1].date = today; } current = episodes.length - 1; theday = episodes[current].date; if (theday.slice(8,10) !== today.slice(8,10)) { episodes.push(clone(episodes[current])); current += 1; episodes[current].date = today; } cursor = current - 1; tabButton.bind(); optionButton.bind(); dataModel.bind(); dataList.init(); dataList.bind(); dataEpisode.bind(); tiny.logic.recalculate(episodes[current]); tiny.logic.recalculate(episodes[cursor]); display.refresh(); dataHistory.init(); dataCPOE.bind(); }, reset: function(d) { episodes[0] = d; }, inject: function(s, t) { copyInto(episodes[current - t], s); tiny.logic.recalculate(episodes[current - t]); // console.log(JSON.stringify(episodes[current - t])); display.refresh(); }, change: function(id, val, key) { tiny.logic.enter(episodes[current], id, val, key); dataModel.refresh(); }, recalculate: function () { tiny.logic.recalculate(episodes[current]); }, getEpisodes: function() { return episodes; } }; }()); // immediate invocation of function as a module //window.onload = tinyJS.init; // the DOM must be fully loaded