/** * DataFormsJS Framework Application Object * * This script creates a global [DataFormJS] object which can also be * referenced as [app]. One or more optional template rending engines are the * only dependencies. In most apps the core [jsonData] page object from file * [pages/jsonData.js] will also be included. DataFormsJS Framework uses * ES5 syntax so that it can work with all browsers including older * Mobile Devices and supported versions of IE. For older browsers and * devices functions [fetch, Promise, etc] will be loaded as polyfill * functions when the page is loaded. While the Framework is developed * using ES5 custom code for apps that use it can use modern JS classes. * * View Engine Options: * https://handlebarsjs.com * https://vuejs.org * # Both Vue 2 and Vue 3 are supported * https://mozilla.github.io/nunjucks/ * https://underscorejs.org * * Common API Functions for creating Single Page Apps (SPA): * # Pages `app.addPage()` and Plugins `app.addPlugin()` are the recommended * # method for creating most custom app logic. JavaScript Controls * # `app.addControl()` are similar in concept to Web Components and * # recommended for complex page component/control logic that needs to render * # custom HTML and call plugins or use other features from within the control. * app.addPage() * app.addPlugin() * app.addControl() * app.refreshAllHtmlControls(callback, model) * app.refreshHtmlControl(element, callback, model) * app.escapeHtml(value) * app.showErrorAlert('message') * app.showError(document.querySelector('h1'), 'Error Text') * app.isUsingVue() * * Common API Properties for working with app data and for debugging with * Browser DevTools: * app.activeController * app.activeModel * app.activeJsControls * app.activeVueModel * app.activeParameterList * app.controllers * app.models * * Many more API functions exist and the demos provide many examples of how * apps can be built with DataFormsJS. DataFormsJS is built to run directly * in a browser without a build process; this allows for fast prototyping, * development, and a clearly defined structure for apps built with DataFormsJS. * * In addition to the standard DataFormsJS Framework standalone classes for * React and Web Components are available which provide similar functionality. * * Copyright Conrad Sollitt and Authors. For full details of copyright * and license, view the LICENSE file that is distributed with DataFormsJS. * * @link https://www.dataformsjs.com * @author Conrad Sollitt (https://conradsollitt.com) * @license MIT */ /* Validates with both [jshint] and [eslint] */ /* global Handlebars, nunjucks, _, Vue, Promise */ /* jshint strict: true */ /* eslint-env browser */ /* eslint quotes: ["error", "single", { "avoidEscape": true }] */ /* eslint strict: ["error", "function"] */ /* eslint spaced-comment: ["error", "always"] */ /* eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */ /* eslint no-prototype-builtins: "off" */ (function () { 'use strict'; // Invoke strict mode // Object for Enum var ViewEngines = { NotSet: 'Not Set', Unknown: 'Unknown', Mixed: 'Mixed', Handlebars: 'Handlebars', Vue: 'Vue', Nunjucks: 'Nunjucks', Underscore: 'Underscore', Text: 'Text', }; // Private variables available to this function scope only var viewEngine = ViewEngines.NotSet; var validViewEngines = [ ViewEngines.Handlebars, ViewEngines.Vue, ViewEngines.Nunjucks, ViewEngines.Underscore, ViewEngines.Text, ]; var isUpdatingView = false; var isUpdatingAllControls = false; var isLoadingRoute = false; var previousUrl = null; var routeLoadingCount = 0; var renderInterval = null; var vueUpdateView = false; var vueWatcherDepPrevLen = 0; var isIE = (navigator.userAgent.indexOf('Trident/') !== -1); var routingMode = null; var checkedForCssVarPolyfill = false; function validateTypeOf(value, typeName, propName, callingFunction) { if (typeof value !== typeName) { console.log(value); throw new TypeError('[' + propName + '] was not defined as a ' + typeName + ' when the function ' + callingFunction + ' was called'); } } function validateObjectExists(value, propName, callingFunction) { if (value === undefined) { throw new TypeError('[' + propName + '] must first be defined before the function ' + callingFunction + ' is called'); } else if (typeof value !== 'object') { console.log(value); throw new TypeError('[' + propName + '] was not defined as an object when the function ' + callingFunction + ' was called'); } } function validateStringWithValue(value, propName, callingFunction) { if (value === '') { throw new TypeError('[' + propName + '] must have a value when defined when the function ' + callingFunction + ' is called'); } } function requireUndefinedProperty(object, objName, property) { if (object[property] !== undefined) { console.log(object); throw new TypeError('[' + objName + '.' + property + '] is already defined'); } } function validateOptionalFunctions(obj, objName, objType, func) { for (var n = 0, m = func.length; n < m; n++) { if (obj[func[n]] !== undefined) { if (typeof obj[func[n]] !== 'function') { console.log(obj); throw new TypeError(objType + '[' + objName + '].' + func[n] + ' is not defined as a function.'); } } } } function requireOneNamedProperty(obj, objName, objType, props) { for (var n = 0, m = props.length; n < m; n++) { if (obj[props[n]] !== undefined && obj[props[n]] !== null) { return; } } // If code execution makes it here the object is not valid console.log(obj); throw new TypeError(objType + '[' + objName + '] must have one of the following properties defined: ' + props.join(', ')); } function validateElementExists(id, propName, callingFunction) { if (document.getElementById(id) === null) { throw new TypeError('An element was not found on the page with [' + propName + '][id=' + id + '] when the function ' + callingFunction + ' was called'); } } /** * Private function used when rendering template errors. It creates a prefix * for the error message that is relevant to the control had the error. * * @param {HTMLElement|null} element If the source of the error was from an HTML element * @param {object|null} controller If the source of the error was from a controller */ function getTemplateErrorPrefix(element, controller) { var errorMessage = ''; try { // Where was the error from? A Control or an HTML Element? if (controller !== null && controller.path) { errorMessage = 'Error with [Controller.path = "' + controller.path + '"] - '; } else if (element !== null && element instanceof HTMLElement) { // If a HTML Element then get the attributes [data-template-id] // and [data-template-url] var attrId = element.getAttribute('data-template-id'); var attrUrl = element.getAttribute('data-template-url'); attrId = (attrId === null ? '' : ' data-template-id="' + attrId + '"'); attrUrl = (attrUrl === null ? '' : ' data-template-url="' + attrUrl + '"'); // Include [id] and [class] attributes along with the relevant data attributes. errorMessage = 'Error with Element <' + element.tagName.toLowerCase(); errorMessage += ' id="' + element.id + '"'; errorMessage += ' class="' + element.className + '"'; errorMessage += attrId + attrUrl + '> - '; } return errorMessage; } catch (e) { console.error(e); return errorMessage; } } // Used by [app.showError] and [app.showErrorAlert] function createElement(type, textContent, className) { var el = document.createElement(type); el.textContent = textContent; if (className) { el.className = className; } return el; } // Use to handle Vue Errors and Warnings. If a rendering error // occurs the main View Element can end up being removed from the // screen so this function handles it. function showVueError(err, viewEl) { // Wait half a second, this is not ideal but documented Vue functions // do not appear to provide a callback to handle this. console.error(err); window.setTimeout(function () { // Attach view element back to DOM if if was removed. if (viewEl) { if (viewEl.parentNode === null) { var docEl = document.querySelector('body'); if (!docEl) { docEl = document.documentElement; } docEl.appendChild(viewEl); // Update active template so error shows again otherwise the // view will disappear if a rendering error is view again for // the same controller. if (app.activeTemplate) { app.activeTemplate.error = true; app.activeTemplate.errorMessage = err.toString(); } } } // Show Error app.showError(viewEl, err); }, 500); } /** * Read or download and then compiles a template. This function is optimized so * if an existing template is already compiled then it will not be compiled twice. * * @param {HTMLElement|null} element Element where the template was called from * @param {object|null} controller Controller object * @param {function} callback Callback function(template) that gets called once the template is compiled */ function compileTemplate(element, controller, callback) { var script = null, scriptUrl = null, n, m, errorMessage = null, templateId = null, templateUrl = null, templateEngine = null; // Use Template ID if loading template embedded on the page or Template URL to download a new template if (controller === null) { templateId = element.getAttribute('data-template-id'); templateUrl = element.getAttribute('data-template-url'); if (app.activeController !== null) { templateEngine = app.activeController.viewEngine; } } else { // Get Controller Settings templateId = controller.viewId; templateUrl = controller.viewUrl; if (controller.viewEngine !== undefined) { templateEngine = controller.viewEngine; } // Make sure they are null. This would only happen if calling code // redefines controller properties after they have been added. if (templateId === undefined) { templateId = null; } if (templateUrl === undefined) { templateUrl = null; } // Validate Properties. The only way that these error can happen is if the controller is // modified manually after calling addController() so these error would not be common. if (templateId !== null && templateUrl !== null) { errorMessage = 'A controller must have either [viewId] or [viewUrl] defined but not both properties. This error is not possible when calling the [addController()] so one or more of the properties were modified by JavaScript code after the controller was already added.'; callback(addTemplate(null, templateId, templateUrl, templateEngine, true, errorMessage)); return; } else if (templateId === null && templateUrl === null) { // Check if the controller being rendered only uses functions // and not a template engine. If so then pass null to the callback. if (typeof controller.onRouteLoad === 'function' || typeof controller.onBeforeRender === 'function' || typeof controller.onRendered === 'function') { callback(null); return; } // Missing required properties or functions errorMessage = 'A controller must have either [viewId] or [viewUrl] defined but neither property is defined. This error is not possible when calling the [addController()] so one or more of the properties were modified by JavaScript code after the controller was already added.'; callback(addTemplate(null, templateId, templateUrl, templateEngine, true, errorMessage)); return; } } // First check the template cache in case it has already been compiled for (n = 0, m = app.compiledTemplates.length; n < m; n++) { if (templateId !== null && app.compiledTemplates[n].id === templateId) { callback(app.compiledTemplates[n]); return; } else if (templateUrl !== null && app.compiledTemplates[n].url === templateUrl) { callback(app.compiledTemplates[n]); return; } } // If the template was not found from cached array, compile it and add it to the compiledTemplates Array. // Determine download settings or read from