/** * @fileoverview The Component class used by C1Pjs and PCjs. * @author Jeff Parsons * @version 1.0 * Created 2012-May-14 * * Copyright © 2012-2015 Jeff Parsons * * This file is part of the JavaScript Machines Project (aka JSMachines) at * and . * * JSMachines is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * JSMachines is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with JSMachines. * If not, see . * * You are required to include the above copyright notice in every source code file of every * copy or modified version of this work, and to display that copyright notice on every screen * that loads or runs any version of this software (see Computer.sCopyright). * * Some JSMachines files also attempt to load external resource files, such as character-image files, * ROM files, and disk image files. Those external resource files are not considered part of the * JSMachines Project for purposes of the GNU General Public License, and the author does not claim * any copyright as to their contents. */ /* * All the C1Pjs and PCjs components now use JSDoc types, primarily so that Google's Closure Compiler * will compile everything with ZERO warnings. For more information about the JSDoc types supported by * the Closure Compiler: * * https://developers.google.com/closure/compiler/docs/js-for-compiler#types * * I also attempted to use JSLint, but it's excessively strict for my taste, so this is the only file * I tried massaging for JSLint's sake. I gave up when it complained about my use of "while (true)"; * replacing "true" with an assignment expression didn't make it any happier. * * I wasn't thrilled about replacing all "++" and "--" operators with "+= 1" and "-= 1", nor about using * "(s || '')" instead of "(s? s : '')", because while the former may seem simpler, it is NOT more portable. * It's not that I'm trying to write "portable JavaScript", but some of this code was ported from C code I'd * written about 14 years earlier, and portability is good, so I'm not going to rewrite if there's no need. * * UPDATE: I've since switched to JSHint, which seems to have more reasonable defaults. */ "use strict"; /* global window: true, DEBUG: true */ if (typeof module !== 'undefined') { require("./defines"); var usr = require("./usrlib"); var web = require("./weblib"); } /** * Component(type, parms, constructor, bitsMessage) * * A Component object requires: * * type: a user-defined type name (eg, "CPU") * * and accepts any or all of the following (parms) properties: * * id: component ID (default is "") * name: component name (default is ""; if blank, toString() will use the type name only) * comment: component comment string (default is undefined) * * Subclasses that use Component.subclass() to extend Component will likely have additional (parms) properties. * * @constructor * @param {string} type * @param {Object} [parms] * @param {Object} [constructor] * @param {number} [bitsMessage] */ function Component(type, parms, constructor, bitsMessage) { this.type = type; if (!parms) { parms = {'id': "", 'name': ""}; } this.id = parms['id']; this.name = parms['name']; this.comment = parms['comment']; this.parms = parms; if (this.id === undefined) this.id = ""; var i = this.id.indexOf('.'); if (i > 0) { this.idMachine = this.id.substr(0, i); this.idComponent = this.id.substr(i + 1); } else { this.idComponent = this.id; } /* * Recording the constructor is really just a debugging aid, because many of our constructors * have class constants, but they're hard to find when the constructors are buried among all the * other globals. */ this[type] = constructor; /* * Gather all the various component flags (booleans) into a single "flags" object, and encourage * subclasses to do the same, to reduce the property clutter we have to wade through while debugging. */ this.aFlags = { fReady: false, fBusy: false, fBusyCancel: false, fPowered: false, fError: false }; this.fnReady = null; this.clearError(); this.bindings = {}; this.dbg = null; // by default, no connection to a Debugger this.bitsMessage = bitsMessage || -1; Component.add(this); } /** * Component.parmsURL * * Initialized to the set of URL parameters, if any, for the current web page. * * @type {Object|null} */ Component.parmsURL = web.getURLParameters(); /** * Component.inherit(p) * * Returns a newly created object that inherits properties from the prototype object p. * It uses the ECMAScript 5 function Object.create() if it is defined, and otherwise falls back to an older technique. * * See: Flanagan, David (2011-04-18). JavaScript: The Definitive Guide: The Definitive Guide (Kindle Locations 9854-9903). OReilly Media - A. Kindle Edition (Example 6-1) * * @param {Object} p */ Component.inherit = function(p) { if (window) { if (!p) throw new TypeError(); if (Object.create) { return Object.create(p); } var t = typeof p; if (t !== "object" && t !== "function") throw new TypeError(); } /** * @constructor */ function F() {} F.prototype = p; return new F(); }; /** * Component.extend(o, p) * * Copies the enumerable properties of p to o and returns o. * If o and p have a property by the same name, o's property is overwritten. * * See: Flanagan, David (2011-04-18). JavaScript: The Definitive Guide: The Definitive Guide (Kindle Locations 9854-9903). OReilly Media - A. Kindle Edition (Example 6-2) * * @param {Object} o * @param {Object} p */ Component.extend = function(o, p) { for (var prop in p) { o[prop] = p[prop]; } return o; }; /** * Component.subclass(superclass, subclass, methods, statics) * * See: Flanagan, David (2011-04-18). JavaScript: The Definitive Guide: The Definitive Guide (Kindle Locations 9854-9903). OReilly Media - A. Kindle Edition (Example 9-11) * * @param {Object} superclass is the constructor of the superclass * @param {Object} subclass is the constructor for the new subclass * @param {Object} [methods] contains all instance methods * @param {Object} [statics] contains all class properties and methods */ Component.subclass = function(superclass, subclass, methods, statics) { subclass.prototype = Component.inherit(superclass.prototype); subclass.prototype.constructor = subclass; if (methods) { Component.extend(subclass.prototype, methods); } if (statics) { Component.extend(subclass, statics); } return subclass; }; /* * Every component created on the current page is recorded in this array (see Component.add()). * * This enables any component to locate another component by ID (see Component.getComponentByID()) * or by type (see Component.getComponentByType()). */ Component.all = []; /** * Component.add(component) * * @param {Component} component */ Component.add = function(component) { /* * This just generates a lot of useless noise, handy in the early days, not so much these days... * * if (DEBUG) Component.log("Component.add(" + component.type + "," + component.id + ")"); */ Component.all[Component.all.length] = component; }; /** * Component.log(s, type) * * For diagnostic output only. * * @param {string} [s] is the message text * @param {string} [type] is the message type */ Component.log = function(s, type) { if (DEBUG) { if (s) { var msElapsed, sMsg = (type? (type + ": ") : "") + s; if (Component.msStart === undefined) { Component.msStart = usr.getTime(); } msElapsed = usr.getTime() - Component.msStart; console.log(msElapsed + "ms: " + sMsg.replace(/\n/g, " ")); } } }; /** * Component.assert(f, s) * * Verifies conditions that must be true (for DEBUG builds only). * * The Closure Compiler should automatically remove all references to Component.assert() in non-DEBUG builds. * * TODO: Add a task to the build process that "asserts" there are no occurrences of "assertion failure" in the final code. * * @param {boolean} f is the expression we are asserting to be true * @param {string} [s] is description of the assertion on failure */ Component.assert = function(f, s) { if (DEBUG) { if (!f) { /* * TODO: An accompanying source file/line number/function call (eg, stack trace) would be nice. */ if (!s) s = "assertion failure"; Component.log(s); throw new Error(s); } } }; /** * Component.println(s, type, id) * * For non-diagnostic messages, which components may override to control the destination/appearance of their output. * * Components that inherit from this class should use the instance method, this.println(), rather than Component.println(), * because if a Control Panel is loaded, it will override only the instance method, not the class method (overriding the class * method would improperly affect any other machines loaded on the same page). * * @param {string} [s] is the message text * @param {string} [type] is the message type * @param {string} [id] is the caller's ID, if any */ Component.println = function(s, type, id) { if (DEBUG) { Component.log((id? (id + ": ") : "") + (s? ("\"" + s + "\"") : ""), type); } }; /** * Component.notice(s, fPrintOnly, id) * * notice() is like println() but implies a need for user notification, so we alert() as well. * * @param {string} s is the message text * @param {boolean} [fPrintOnly] * @param {string} [id] is the caller's ID, if any */ Component.notice = function(s, fPrintOnly, id) { if (DEBUG) { Component.println(s, "notice", id); } if (!fPrintOnly) web.alertUser(s); }; /** * Component.warning(s) * * @param {string} s describes the warning */ Component.warning = function(s) { if (DEBUG) { Component.println(s, "warning"); } web.alertUser(s); }; /** * Component.error(s) * * @param {string} s describes the error; an alert() is displayed as well */ Component.error = function(s) { if (DEBUG) { Component.println(s, "error"); } web.alertUser(s); }; /** * Component.getComponents(idRelated) * * We could store components as properties of an 'all' object, using the component's ID, * and change this linear lookup into a property lookup, but some components may have no ID. * * @param {string} [idRelated] of related component * @return {Array} of components */ Component.getComponents = function(idRelated) { var i; var aComponents = []; /* * getComponentByID(id, idRelated) * * If idRelated is provided, we check it for a machine prefix, and use any * existing prefix to constrain matches to IDs with the same prefix, in order to * avoid matching components belonging to other machines. */ if (idRelated) { if ((i = idRelated.indexOf('.')) > 0) idRelated = idRelated.substr(0, i + 1); else idRelated = ""; } for (i = 0; i < Component.all.length; i++) { var component = Component.all[i]; if (!idRelated || !component.id.indexOf(idRelated)) { aComponents.push(component); } } return aComponents; }; /** * Component.getComponentByID(id, idRelated) * * We could store components as properties of an 'all' object, using the component's ID, * and change this linear lookup into a property lookup, but some components may have no ID. * * @param {string} id of the desired component * @param {string} [idRelated] of related component * @return {Component|null} */ Component.getComponentByID = function(id, idRelated) { if (id !== undefined) { var i; /* * If idRelated is provided, we check it for a machine prefix, and use any * existing prefix to constrain matches to IDs with the same prefix, in order to * avoid matching components belonging to other machines. */ if (idRelated && (i = idRelated.indexOf('.')) > 0) { id = idRelated.substr(0, i + 1) + id; } for (i = 0; i < Component.all.length; i++) { if (Component.all[i].id === id) { return Component.all[i]; } } Component.log("Component ID '" + id + "' not found", "warning"); } return null; }; /** * Component.getComponentByType(sType, idRelated, componentPrev) * * @param {string} sType of the desired component * @param {string} [idRelated] of related component * @param {Component} [componentPrev] of previously returned component, if any * @return {Component|null} */ Component.getComponentByType = function(sType, idRelated, componentPrev) { if (sType !== undefined) { var i; /* * If idRelated is provided, we check it for a machine prefix, and use any * existing prefix to constrain matches to IDs with the same prefix, in order to * avoid matching components belonging to other machines. */ if (idRelated) { if ((i = idRelated.indexOf('.')) > 0) { idRelated = idRelated.substr(0, i + 1); } else { idRelated = ""; } } for (i = 0; i < Component.all.length; i++) { if (componentPrev) { if (componentPrev == Component.all[i]) componentPrev = null; continue; } if (sType == Component.all[i].type && (!idRelated || !Component.all[i].id.indexOf(idRelated))) { return Component.all[i]; } } Component.log("Component type '" + sType + "' not found", "warning"); } return null; }; /** * Component.getComponentParms(element) * * @param {Object} element from the DOM */ Component.getComponentParms = function(element) { var parms = null, sParms = element.getAttribute("data-value"); if (sParms) { try { parms = eval("({" + sParms + "})"); // jshint ignore:line /* * We can no longer invoke removeAttribute() because some components (eg, Panel) need * to run their initXXX() code more than once, to avoid initialization-order dependencies. * * if (!DEBUG) { * element.removeAttribute("data-value"); * } */ } catch(e) { Component.error(e.message + " (" + sParms + ")"); } } return parms; }; /** * Component.bindExternalControl(component, sControl, sBinding, sType) * * @param {Component} component * @param {string} sControl * @param {string} sBinding * @param {string} [sType] is the external component type */ Component.bindExternalControl = function(component, sControl, sBinding, sType) { if (sControl) { if (sType === undefined) sType = "Panel"; var target = Component.getComponentByType(sType, component.id); if (target) { var eBinding = target.bindings[sControl]; if (eBinding) { component.setBinding(null, sBinding, eBinding); } } } }; if (window && !window.document.ELEMENT_NODE) window.document.ELEMENT_NODE = 1; /** * Component.bindComponentControls(component, element, sAppClass) * * @param {Component} component * @param {Object} element from the DOM * @param {string} sAppClass */ Component.bindComponentControls = function(component, element, sAppClass) { var aeControls = Component.getElementsByClass(element.parentNode, sAppClass + "-control"); for (var iControl = 0; iControl < aeControls.length; iControl++) { var aeChildNodes = aeControls[iControl].childNodes; for (var iNode = 0; iNode < aeChildNodes.length; iNode++) { var control = aeChildNodes[iNode]; if (control.nodeType !== window.document.ELEMENT_NODE) { continue; } var sClass = control.getAttribute("class"); if (!sClass) continue; var aClasses = sClass.split(" "); for (var iClass = 0; iClass < aClasses.length; iClass++) { var parms; sClass = aClasses[iClass]; switch (sClass) { case sAppClass + "-binding": parms = Component.getComponentParms(control); if (parms && parms['binding']) { component.setBinding(parms['type'], parms['binding'], control); } else { Component.log("Component '" + component.toString() + "' missing binding" + (parms? " for " + parms['type'] : ""), "warning"); } iClass = aClasses.length; break; default: // if (DEBUG) Component.log("Component.bindComponentControls(" + component.toString() + "): unrecognized control class \"" + sClass + "\"", "warning"); break; } } } } }; /** * Component.getElementsByClass(element, sClass, sObjClass) * * This is a cross-browser helper function, since not all browser's support getElementsByClassName() * * TODO: This should probably be moved into weblib.js at some point, along with the control binding functions above, * to keep all the browser-related code together. * * @param {Object} element from the DOM * @param {string} sClass * @param {string} [sObjClass] * @return {Array|NodeList} */ Component.getElementsByClass = function(element, sClass, sObjClass) { if (sObjClass) sClass += '-' + sObjClass + "-object"; /* * Use the browser's built-in getElementsByClassName() if it appears to be available * (for example, it's not available in IE8, but it should be available in IE9 and up) */ if (element.getElementsByClassName) { return element.getElementsByClassName(sClass); } var i, j, ae = []; var aeAll = element.getElementsByTagName("*"); var re = new RegExp('(^| )' + sClass + '( |$)'); for (i = 0, j = aeAll.length; i < j; i++) { if (re.test(aeAll[i].className)) { ae.push(aeAll[i]); } } if (!ae.length) { Component.log('No elements of class "' + sClass + '" found'); } return ae; }; Component.prototype = { constructor: Component, /** * toString() * * @this {Component} * @return {string} */ toString: function() { return (this.name? this.name : (this.id || this.type)); }, /** * getMachineNum() * * @this {Component} * @return {number} unique machine number */ getMachineNum: function() { var nMachine = 1; if (this.idMachine) { var aDigits = this.idMachine.match(/\d+/); if (aDigits !== null) nMachine = parseInt(aDigits[0], 10); } return nMachine; }, /** * setBinding(sHTMLType, sBinding, control) * * Component's setBinding() method is intended to be overridden by subclasses. * * @this {Component} * @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas") * @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset") * @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement) * @return {boolean} true if binding was successful, false if unrecognized binding request */ setBinding: function(sHTMLType, sBinding, control) { switch (sBinding) { case "clear": if (!this.bindings[sBinding]) { this.bindings[sBinding] = control; control.onclick = (function(component) { return function clearPanel() { if (component.bindings['print']) { component.bindings['print'].value = ""; } }; }(this)); } return true; case "print": if (!this.bindings[sBinding]) { this.bindings[sBinding] = control; /* * HACK: Save this particular HTML element so that the Debugger can access it, too */ this.controlPrint = control; /* * This was added for Firefox (Safari automatically clears the