/** * @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': //