// Browser host implementation of host-specific functions. // // Copyright (c) 2015-2017 The Regents of the University of California. // All rights reserved. // // Permission is hereby granted, without written agreement and without // license or royalty fees, to use, copy, modify, and distribute this // software and its documentation for any purpose, provided that the above // copyright notice and the following two paragraphs appear in all copies // of this software. // // IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY // FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES // ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF // THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. // // THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE // PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF // CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, // ENHANCEMENTS, OR MODIFICATIONS. // /** Browser host implementation of host-specific functions. * This module provides functions to generate web content for accessors. * It relies on the /common/commonHost.js module, which it loads when needed. * * To generate an interactive instance of an accessor, where you can provide * input and parameter values into entry boxes and cause the accessor to react, * Include in the head section of your web page the following: * * <script src="/accessors/hosts/browser/browser.js"></script> * * In the body of a web page, you can instantiate an accessor by creating an * HTML element with class "accessor" and specifying a source, which is the fully * qualified accessor name. For example: * * <div class="accessor" src="net/REST" id="REST"></div> * * The id attribute can have whatever value you like, but it must be unique on * the web page. * * You can also create a directory of accessors by including in your document * an element with class "accessorDirectory". For example: * * <div class="accessorDirectory"></div> * * This will provide a hierarchical expandable list of accessors that this host * can instantiate. In addition, if your document has an element with id equal to * "accessorDirectoryTarget", then clicking on an accessor in the directory will * cause that target to be filled with an instance of the accessor, similar to the * one above with class "accessor". For example, * * <div class="accessorDirectoryTarget"></div> * * The style of the generated web pages can be customized using CSS. A default * style is achieved by including in the head section of your document the following: * * <link rel="stylesheet" type="text/css" href="/accessors/hosts/browser/accessorStyle.css"> * * The main entry point to this module is the generate() function, which is * invoked when the web page DOM content has been loaded. * * @module @accessor-hosts/browser * @author Edward A. Lee, Elizabeth Osyk * @version $$Id$$ */ // Stop extra messages from jslint and jshint. Note that there should // be no space between the / and the * and global. See // https://chess.eecs.berkeley.edu/ptexternal/wiki/Main/JSHint */ // initialValues is provided by HTML pages that desire initial form field values // that are different from a particular accessor's default values /*globals alert, clearTimeout, console, document, Event, initialValues, Promise, setTimeout, window, XMLHttpRequest */ /*jshint globalstrict: true, multistr: true */ 'use strict'; /////////////////////////////////////////////////////////////////// //// Web page setup // Only add event listeners the first time the library is loaded. // (Otherwise, duplicate event listeners will generate duplicate accessor HTML). if (!window.hasOwnProperty('browserJSLoaded')) { // Note that the following will not work in IE 8 or older. window.addEventListener('DOMContentLoaded', function () { window.generate(); }); window.addEventListener('unload', function (event) { if (window.accessors) { for (var accessor in window.accessors) { if (accessor.initialized) { accessor.wrapup(); } } } }); window.browserJSLoaded = true; } // Check the URL for a querystring specifying an accessor to load (optional). // This code assumes that "accessor" is the only querystring parameter passed. // Multiple parameters are not supported. // The querystring uses . instead of / since / is a special character in URLs. --> window.onload = function () { var url = window.location.href; var index = url.lastIndexOf('?'); var querystring = "", accessor = ""; var slashIndex = -1; if (index >= 0) { querystring = url.substring(index + 1, url.length); // This code assumes that "accessor" is the only querystring parameter passed. if (querystring.startsWith("accessor=")) { querystring = querystring.substring(9, querystring.length); // Querystring uses . instead of / which is a special character. // Replace . with / querystring = querystring.replace('.', '/'); // Add trailing .js if not present if (querystring.indexOf('.js') < 0) { querystring = querystring + '.js'; } generateAccessorHTML(querystring, 'accessorDirectoryTarget'); // Call toggleVisbility() to expand the directory that this // this accessor is located in. For example, expand "net" // for "net/REST". slashIndex = querystring.indexOf("/"); if (slashIndex > 0) { toggleVisibility("/accessors/" + querystring.substring(0, slashIndex), 0, getIndex); } } } }; /////////////////////////////////////////////////////////////////// //// Functions // Export commonHost after it is loaded. Used by Test accessor. var commonHost; // These will be defined when commonHost.js is loaded. Used by Test accessor // and mocha test cases. var Accessor, instantiate; var isReifiableBy; // Needed by computervision module. The computer vision code did not originally // use strict mode. Declare variables here to avoid 'undeclared variable' error. var Module = {}; var i = 0; var h, w, x, y; /** Local function controlling how standard elements are rendered in the * document with an optional label. * @param target The target document element id. * @param label The label, or null to not have a label. * @param content The content. */ function appendDoc(target, label, content) { var pp = document.createElement('p'); var title = ''; if (label) { title = '' + label + ': '; } pp.innerHTML = title + content; target.appendChild(pp); } /** Local function to add a placeholder to later populate. * @param target The target document element id. * @param id The id for the placeholder. * @param element The element type (e.g. 'div', 'pp', or 'span'). * @return The placeholder element. */ function appendPlaceholder(target, id, element) { var pp = document.createElement(element); pp.setAttribute('id', id); target.appendChild(pp); return pp; } /** Populate the current page by searching for elements with class 'accessor' * and attribute 'src', generating HTML for the specified accessor, and * inserting that HTML content into the element. Also search for elements * with class 'accessorDirectory' and insert an accessory directory. */ function generate() { var i, element; var accessorCount = 0; var acessorElements = document.getElementsByClassName('accessor'); if (acessorElements && acessorElements.length > 0) { for (i = 0; i < acessorElements.length; i++) { element = acessorElements[i]; var src = element.getAttribute('src'); if (src) { accessorCount++; var id = element.getAttribute('id'); if (!id) { // No id. Assign one. id = 'accessor' + accessorCount; element.setAttribute('id', id); } generateAccessorHTML(src, id); } } } var elements = document.getElementsByClassName('accessorDirectory'); if (elements && elements.length > 0) { for (i = 0; i < elements.length; i++) { element = elements[i]; generateAccessorDirectory(element); } } } /** Generate HTML for an accessor defined at the specified path. * If the path is relative (does not begin with '/' or './'), then the accessor * specification will be loaded from the accessor library stored on the host. * If the path is absolute (beginning with '/'), then the accessor specification * will be loaded from the web server providing this swarmlet host at that path. * If the accessor has no inputs, then it will be initialized and fired. * Otherwise, a 'react to inputs' button will appear that will initialize and * fire the actor on command. * * As a side effect of invoking this, the window object for the web page * acquires a field ```accessors``` with a property whose name equals the * id argument whose value is the provided accessor * instance with some additional utilities to support the web page. * * If there was a previously generated accessor with this same id, and it has * has been initialized, then this * function will invoke its wrapup() function, if it defines one, before * generating the HTML. It will also clear the target element (which has * the same id as the accessor). * * Optionally, this method can accept the full text of an accessor via the * text parameter. This is used to create accessors on-the-fly. The tutorial * instantiates accessors this way. * * @path The path to the accessor. * @param id The id of the accessor, which is also the id of the target element * on the web page into which to insert the generated HTML. * @param text (Optional) The text of the accessor, to be used instead of * retrieving an accessor from the filesystem. */ function generateAccessorHTML(path, id, text) { // Unless an error occurs or required modules are missing, // assume the accessor is executable. var executable = true; // Cache modules loaded by require(). var loadedModules = {}; // Need to ensure the wrapup method of any // previous accessor at this target is invoked. if (window.accessors) { var accessor = window.accessors[id]; if (accessor) { if (accessor.initialized) { accessor.wrapup(); } } } // Clear any previous contents in the target element. var target = document.getElementById(id); target.innerHTML = ''; var code; if (text !== null && typeof text !== 'undefined') { code = text; } else { code = getAccessorCode(path); } // Create a header. target = document.getElementById(id); var h1 = document.createElement('h1'); h1.setAttribute('id', 'accessorTitle'); // Extract the class name from the path. var className = path; if (className.indexOf('/') === 0) { className = className.substring(1); } if (className.indexOf('accessors/') === 0) { className = className.substring(10); } h1.innerHTML = 'Accessor class: ' + className; target.appendChild(h1); // Create placeholders for the content. appendPlaceholder(target, id + 'RevealCode', 'span'); appendPlaceholder(target, id + 'Error', 'div'); appendPlaceholder(target, id + 'Base', 'div'); appendPlaceholder(target, id + 'Modules', 'div'); var docElement = appendPlaceholder(target, id + 'Documentation', 'p'); appendPlaceholder(target, id + 'Tables', 'p'); /////////////////////////////////////////////////////////////////// //// Define top-level functions that the accessor might invoke. // NOTE: alert(), clearInterval(), clearTimeout(), setInterval(), and // setTimeout() are all provided by the browser. // FIXME: Reimplement setInterval() and setTimeout() to make them // precise for composite accessors. // Report an error on the console and on the web page. // @param err The error. // @param detail Optional context information for the error function error(err, detail) { console.error(err); var pp = document.createElement('p'); pp.setAttribute('class', 'accessorError'); if (!detail) { detail = 'from accessor'; } pp.innerHTML = 'Error ' + detail + ' at ' + path + ': ' + err; var target = document.getElementById(id + 'Error'); target.appendChild(pp); executable = false; } // Get data from an input. This implementation assumes that the document // has an element with attribute 'id' equal to ```id.name```, // where id is the id of this accessor. // Such an attribute is created by the generate() function. // This implementation also assumes that the window object has a field // ```accessors``` with a property whose name equals the id of this accessor // whose value is an instance of the Accessor class of the common/commonHost.js // module. function get(name) { return getInputOrParameter(name, 'input', id); } // Return a resource, which in this implementation just attempts to read the // resource using HTTP. // @param uri The uri to be read. // @param timeout The time to wait before giving up. This defaults to 5000, // 5 seconds, if not provided. // @return The responseText from the request. function getResource(uri, timeout) { if (!timeout && timeout !== 0) { timeout = 5000; } // Check for a filename-only URL. E.g., GDPAuthorization.txt // In this case, grab the current window URL and check for the file in // the same directory. // Added to support similar behavior to CapeCode, so we can use the same // code-generated accessors in CapeCode and browser. if (uri.indexOf('/') === -1) { var path = window.location.href; var index = path.lastIndexOf('/'); if (index > 0) { path = window.location.href.substring(0, index); } uri = path + "/" + uri; } var request = new XMLHttpRequest(); // The third argument specifies a synchronous read. request.open("GET", uri, false); var timeoutHandler = setTimeout(handleTimeout, timeout); function handleTimeout() { request.abort(); error("getResource timed out at URI: " + uri); } // Null argument says there is no body. request.send(null); clearTimeout(timeoutHandler); // readyState === 4 is the same as readyState === request.DONE. if (request.readyState === request.DONE) { if (request.status <= 400) { return request.responseText; } throw "getResource failed with code " + request.status + " at URL: " + uri; } throw "getResource did not complete: " + uri; } // Perform a synchronous HTTP request. // @deprecated Use the httpClient module instead. // @param url The url. // @param method The method to be passed to the XMLHttpRequest.open() call. // @param properties Ignored in this implementation // @param body The body that is to be sent. If this argument // is null, then no body is sent. // @param timeout Ignored in this implementation. function httpRequest(url, method, properties, body, timeout) { var request = new XMLHttpRequest(); // The third argument specifies a synchronous read. request.open(method, url, false); // Null argument says there is no body. request.send(body); // readyState === 4 is the same as readyState === request.DONE. if (request.readyState === request.DONE) { if (request.status <= 400) { return request.responseText; } throw "httpRequest failed with code " + request.status + " at URL: " + url; } throw "httpRequest did not complete: " + url; } /** Fetch and execute a module or accessor whose functionality is given in JavaScript at * the specified path on the server. The path will be requested from the same server * that served the page executing this script. If no callback function is given, * then a synchronous (blocking) request will be made (best to avoid this in a web page). * If a callback function is given, then after receiving and evaluating the * JavaScript code, the callback function will be invoked. * * If the path begins with a '/' or './', then it will be interpreted as the path * to a resource provided by the web server serving this swarmlet host. * Otherwise, it will be interpreted as the name of a module provided by this * swarmlet host. * * The returned object includes any properties * that have been added to the 'exports' property in the specified code. * For example, if the module is to export a function, the code * could define the function as follows:
* * ```javascript * exports.myFunction = function () {...}; * ``` * * Alternatively, the code can explicitly define * the exports object as follows: * * ```javascript * var myFunction = function () {...}; * module.exports = { * myFunction : myFunction * }; * ``` * * The module can be an accessor. If the module or accessor * fails to load and no callback is given, then an exception will be thrown. * The caller should catch this exception and generate appropriate HTML content. * * This implementation is inspired by the requires() function implemented * by Walter Higgins, found here: * * https://github.com/walterhiggins/commonjs-modules-javax-script * * @param path The code to fetch (a JavaScript file or module name). * @param id The id on the page for which the module is needed. * @param callback The callback function, which gets two arguments: an error * message (or null if the request succeeded) and the response JavaScript text. * If this argument is omitted or null, then the path is retrieved synchronously * and either the JavaScript text will be returned or an exception will be thrown. * @see http://nodejs.org/api/modules.html#modules_the_module_object * @see also: http://wiki.commonjs.org/wiki/Modules */ function loadFromServer(path, id, callback) { var evaluate = function (code) { // Create the exports object to be populated. // Some libraries overwrite module.exports instead of adding to exports. var module = {}; module.exports = {}; var exports = module.exports; // In strict mode, eval() cannot modify the scope of this function. // Hence, we wrap the code in the function, and will pass in the // exports object that we want the code to modify. var wrapper = eval('(function (exports) {' + code + '})'); // Populate the exports field. wrapper(module.exports); return module.exports; }; if (callback) { // The third argument states that unless the path starts with '/' // or './', then the path should be searched for in the modules directory. getJavaScript(path, function (err, code) { if (err) { callback(err, code); } else { try { callback(null, evaluate(code)); } catch (err2) { callback(err2, null); } } }, true); } else { // Synchronous execution. // This could throw an exception. // The third argument states that unless the path starts with '/' // or './', then the path should be searched for in the modules directory. var code = getJavaScript(path, null, true); return evaluate(code); } } // Print a message to the console. // @param message The message that is passed // to console.log(). function print(message) { console.log(message); } // Synchronously read a URL. // @deprecated Use the httpClient module instead. // @param url The url to be read // @return The responseText from the request. function readURL(url) { return getResource(url); } // Load the specified module. function require(path) { // // FIXME: This is needed so that we can avoid platform // // dependent code in commonHost.js, but it is so ugly. -cxh // FIXME: Beth: This was commented out but I reinstated it as // the speech recognition demo was crashing. if (path === './modules/deterministicTemporalSemantics') { return require('/accessors/hosts/common/modules/deterministicTemporalSemantics'); } // If the commonHost is required, return it. if (path.indexOf("commonHost") >= 0) { return commonHost; } var sawAccessorsModules = false; //Delete @accessors-modules/ from path start; it's already accounted for. if (path.indexOf("@accessors-modules/") === 0) { // require('@accessors-modules/text-display') needs this because // text-display is in common/modules. sawAccessorsModules = true; path = path.substring(19); } // If module already loaded, return the cached copy. if (loadedModules.hasOwnProperty(path)) { return (loadedModules[path]); } else { // Otherwise, load the module. // Indicate required modules in the docs. var modules = document.getElementById(id + 'Modules'); var text = modules.innerHTML; // No need to mention deterministicTemporalSemantics in the list of modules. if (path.indexOf('deterministicTemporalSemantics') !== -1) { if (text) { // Remove the trailing '' text = text.replace('', ''); } } else { if (!text) { text = 'Modules required: ' + path; } else { // Remove the trailing '
' text = text.replace('', ''); text += ', ' + path; } } // Default return value. var result = 'Module failed to load'; // Load the module synchronously because the calling function needs the returned // value. If a module fails to load, however, we will still want to display a web // page. It's just that execution will fail. try { // The third argument (null) indicates synchronous load. result = loadFromServer(path, id, null); loadedModules[path] = result; // If successful, add the module name to the text of the modules list. } catch (err) { text += ' (Not supported by this host)'; // If the path includes modules, try common/modules (once). var newPath; if (path.indexOf('common/modules') < 0) { try { // @accessors-modules/text-display.js needs this. if (sawAccessorsModules) { newPath = '/accessors/hosts/common/modules/' + path; return require(newPath); } var index = path.indexOf('modules'); if (index !== -1) { newPath = '/accessors/hosts/common/' + path.substr(index); return require(newPath); } } catch (err2) { // Ignore and report original error. text += '(Also tried ' + newPath + ')'; } } text += ''; executable = false; } modules.innerHTML = text + ''; return result; } } // Send an output or to an input. This implementation assumes that the // document has an element with attribute 'id' equal to ```id.name```, where // id is the id of the accessor and name is the name of the output. // Such an attribute is created by the generate() function. // This implementation also assumes that the window object has a field // ```accessors``` that contains a property with name equal to the // whose value is an instance of the Accessor class of the // common/commonHost.js module. function send(name, value) { var isInput = false; var element = document.getElementById(id + '.' + name); if (!element) { alert('No output named ' + name + ' for accessor with id ' + id); return; } // Handle data types. Check output ports. var options = window.accessors[id].outputs[name]; if (!options) { // Check input ports options = window.accessors[id].inputs[name]; isInput = true; if (!options) { // This could only occur is somehow the document has an element // with the right name, but there is no such input. alert('No record of output or input named ' + name + ' for accessor with id ' + id); return null; } } options.latestValue = value; // Set value for HTML element. Call provideInput() on inputs. if (isInput) { if (options.type === 'string') { element.setAttribute("value", value); } else { element.setAttribute("value", JSON.stringify(value)); } provideInput(id, name, value); } else { if (options.type === 'string') { element.textContent = value; } else { element.textContent = JSON.stringify(value); } } } var util = require('util'); /////////////////////////////////////////////////////////////////// //// Instantiate the accessor and generate page contents. // Next, load and evaluate the accessor code to invoke the setup() function, which // will determine what the parameters, inputs, and outputs are, and will set // up the accessor to be executed. // Load common/commonHost.js code asynchronously. loadFromServer('/accessors/hosts/common/commonHost.js', id, function (err, theCommonHost) { var instance; if (err) { error(err, 'loading commonHost.js'); return; } else { // Function bindings for the accessor: // We will bind getParameter() later. // The browser's getParameter() retrieves values from the HTML page. // However, an accessor might call getParameter() in setup() // before the page has been created. In this case, we want to // get whatever value the accessor has provided in setup(). var bindings = { 'error': error, 'get': get, 'getResource': getResource, 'httpRequest': httpRequest, 'readURL': readURL, 'require': require, 'send': send, 'util': util }; try { // Make the commonHost globally visible. Used by Test accessor. commonHost = theCommonHost; Accessor = commonHost.Accessor; // Make isReifiableBy globally visible isReifiableBy = commonHost.isReifiableBy; // Override commonHost's require with browser's require. // 'Accessor' constructor is used in Mocha tests. commonHost.Accessor.prototype.require = require; // Needed for trusted accessors to call getTopLevelAccessors(). commonHost.allowTrustedAccessors(true); instance = new commonHost.Accessor( className, code, getAccessorCode, bindings); // Mocha tests use instantiate. Define it. // code = getAccessorCode('net/REST'); // instance = new commonHost.Accessor('REST', code); instantiate = function (className, path) { code = getAccessorCode(path); return new commonHost.Accessor(className, code); }; } catch (err2) { error(err2, 'instantiating accessor'); executable = false; // Failed to instantiate the accessor. Can't generate very much. // Generate docs and button to view the accessor code. generateAccessorDocumentation(path, id); generateAccessorCodeElement(code, id); return; } // If an error occurred or a module was found missing during instantiation, // then executable will be false. instance.executable = executable; } // Record the accessor instance. // The following will define a global variable 'accessors' // if it is not already defined. if (!window.accessors) { window.accessors = {}; } window.accessors[id] = instance; // Create documentation for the accessor. generateAccessorDocumentation(path, id); // Create a button to view the accessor code. generateAccessorCodeElement(code, id); // Generate tables for the accessor. // getParameter() is overriden here. generateTables(instance, id); // If the accessor has no inputs, then there will be no // 'react to inputs' button. In this case, attempt to initialize // and fire accessor. if (instance && (!instance.inputList || instance.inputList.length === 0)) { reactIfExecutable(id, true); } }); } /** Generate a button that will optionally reveal the accessor source code. * @param code The code. * @param id The ID of the accessor. */ function generateAccessorCodeElement(code, id) { var target = document.getElementById(id + 'RevealCode'); var button = document.createElement('button'); button.setAttribute('class', 'accessorButton ui-btn ui-corner-all'); button.innerHTML = 'reveal code'; button.id = 'revealCode'; button.onclick = function () { if (button.innerHTML === 'hide code') { pre.style.display = 'none'; button.innerHTML = 'reveal code'; } else { pre.style.display = 'block'; button.innerHTML = 'hide code'; } }; target.appendChild(button); // Include Google's pretty printer, if possible. var script = document.createElement('script'); script.src = "https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"; target.appendChild(script); var pre = document.createElement('pre'); // Class prettyprint invokes Google's pretty printer, if available. pre.setAttribute('class', 'prettyprint'); // Start out hidden. pre.style.display = 'none'; target.appendChild(pre); var codeElement = document.createElement('code'); pre.appendChild(codeElement); // Need to escape HTML markup. It is sufficient to just escape < codeElement.innerHTML = code.replace(/No documentation found for \ the accessor (tried ' + path + ').'; } else { // Request was successful. Parsed DOM is in responseXML. var properties = request.responseXML.getElementsByTagName('property'); for (i = 0; i < properties.length; i++) { var property = properties[i]; var name = property.getAttribute('name'); docs[name] = property.getAttribute('value'); } } // Now write contents to the web page. if (docs.description) { appendDoc(target, null, docs.description); } if (docs.author) { appendDoc(target, 'Author', docs.author); } if (docs.version) { appendDoc(target, 'Version', docs.version); } // Record the docs for future use in generating input/output/parameter // documentation. if (!window.accessorDocs) { window.accessorDocs = {}; } window.accessorDocs[id] = docs; } /** Generate parameter, input, and output tables for * the specified accessor instance and insert it * into the element on the page with the specified id. * Also generate a list of contained accessors, if there are any. * @param accessor An accessor instance created by common/commonHost.js. * @param id The id of the accessor. */ function generateTables(instance, id) { // Declare getParameter() here so we can override accessor's getParameter() // just after HTML page has been created. // Get data from a parameter. This implementation assumes that the document // has an element with attribute 'id' equal to ```id.name```. // Such an attribute is created by the generate() function. // This implementation also assumes that the window object has a field // ```accessors``` with a property whose name matches id // whose value is an instance of the Accessor class of the common/commonHost.js // module. function getParameter(name) { return getInputOrParameter(name, 'parameter', id); } var promises = []; // Generate a table for parameters. if (instance.parameterList && instance.parameterList.length > 0) { promises.push(generateTable("Parameters", instance.parameterList, instance.parameters, "parameter", id)); } // Generate a table for inputs. if (instance.inputList && instance.inputList.length > 0) { promises.push(generateTable("Inputs", instance.inputList, instance.inputs, "input", id)); } // Generate a table for outputs. if (instance.outputList && instance.outputList.length > 0) { promises.push(generateTable("Outputs", instance.outputList, instance.outputs, "output", id)); } // Generate an event when the table is done. // TODO: It would be even better to generate an event when all content // is done. This would probably require Promises everywhere... Promise.all(promises).catch(function(err) { console.log('Error generating accessor tables: ' + err); }) .then(function() { window.dispatchEvent(new Event('accessorTableDone')); // Override getParameter(). if (window.accessors[id] !== null && typeof window.accessors[id] !== 'undefined') { window.accessors[id].getParameter = getParameter; } }); // Generate a list of contained accessors, if any. generateListOfContainedAccessors(instance, id); } /** Generate a list of accessors contained by the specified accessor instance. * @param instance An accessor instance created by common/commonHost.js. * @param id The id of the accessor. */ function generateListOfContainedAccessors(instance, id) { var target = document.getElementById(id + 'Tables'); if (instance.containedAccessors && instance.containedAccessors.length > 0) { var header = document.createElement('h2'); header.innerHTML = 'Contained Accessors'; target.appendChild(header); var list = document.createElement('ol'); target.appendChild(list); for (var i = 0; i < instance.containedAccessors.length; i++) { var containedInstance = instance.containedAccessors[i]; var accessorClass = containedInstance.accessorClass; var listElement = document.createElement('li'); list.appendChild(listElement); listElement.innerHTML = 'Instance of: ' + accessorClass; } } } /** Generate a react button. * @param id The id of the accessor. */ function generateReactButton(id) { var target = document.getElementById(id + 'Tables'); var targetClass = target.getAttribute('class'); if (targetClass && (targetClass === 'containedAccessor')) { // A contained accessor cannot be asked to react independently. return; } var pp = document.createElement('span'); var button = document.createElement('button'); pp.appendChild(button); button.innerHTML = 'react to inputs'; button.setAttribute('class', 'accessorButton ui-btn ui-corner-all'); button.setAttribute('name', 'react'); button.setAttribute('type', 'button'); button.setAttribute('autofocus', 'true'); button.setAttribute('onclick', 'reactIfExecutable("' + id + '")'); button.setAttribute('id', 'reactToInputs'); target.appendChild(pp); } /** Generate a table with the specified title and contents and * append it to the element on the page with the specified id. * * @param title The title for the table. * @param names A list of field names in the contents object to include, in order. * @param contents An object containing one field for each object to include. * @param role One of 'input', 'output', or 'parameter'. * @param id The id of the accessor. */ function generateTable(title, names, contents, role, id) { return new Promise(function (resolve, reject) { var promises = []; var target = document.getElementById(id + 'Tables'); // Create header line. var header = document.createElement('h2'); header.innerHTML = title; header.setAttribute('class', 'accessorTableTitle'); target.appendChild(header); if (role === 'input') { // Generate a react button. generateReactButton(id); } var table = document.createElement('table'); table.setAttribute('class', 'accessorTable ui-responsive table-stroke'); table.setAttribute('width', '100%'); table.setAttribute('data-role', 'table'); var head = document.createElement('thead'); table.appendChild(head); var titleRow = document.createElement('tr'); titleRow.setAttribute('class', 'accessorTableRow'); head.appendChild(titleRow); var column = document.createElement('th'); column.setAttribute('class', 'accessorTableHeader'); // To not expand, use 1%. column.setAttribute('width', '1%'); column.innerHTML = 'Name'; titleRow.appendChild(column); column = document.createElement('th'); column.setAttribute('class', 'accessorTableHeader'); column.setAttribute('width', '1%'); column.innerHTML = 'Type'; titleRow.appendChild(column); column = document.createElement('th'); column.setAttribute('class', 'accessorTableHeader'); column.innerHTML = 'Value'; titleRow.appendChild(column); column = document.createElement('th'); column.setAttribute('class', 'accessorTableHeader'); column.innerHTML = 'Documentation'; titleRow.appendChild(column); var tbody = document.createElement('tbody'); table.appendChild(tbody); target.appendChild(table); var editable = true; if (role === 'output') { editable = false; } if (target.getAttribute('class') === 'containedAccessor') { editable = false; } for (var i = 0; i < names.length; i++) { var visible = true; var item = contents[names[i]]; if (item) { if (item.visibility) { var visibility = item.visibility; if (visibility == 'notEditable') { editable = false; } if (visibility == 'expert') { visible = false; } } promises.push(generateTableRow( tbody, names[i], id, item, editable, visible, role)); } } // Resolve promise once all rows are created. Promise.all(promises).then(function () { return resolve(true); }, function (error) { return reject(error); }); }); } /** Generate a table row for an input, parameter, or output. * Table rows are still created for invisible items so that the content is * available when the accessor is fired. Invisible rows are tagged with the * class "invisible" to instruct the CSS formatter not to show the row. * @param table The element into which to append the row. * @param name The text to put in the name column. * @param id The id of the accessor. * @param options The options. * @param editable True to make the value an editable input element. * @param visible True to make the table row visible. * @param role Can be parameter, input or output. */ function generateTableRow(table, name, id, options, editable, visible, role) { return new Promise(function (resolve, reject) { var row = document.createElement("tr"); var classTag; if (visible) { classTag = "accessorTableRow"; } else { classTag = "accessorTableRow invisible"; } row.setAttribute('class', classTag); // Insert the name. var nameCell = document.createElement("td"); nameCell.setAttribute('class', 'accessorTableData'); nameCell.innerHTML = name; row.appendChild(nameCell); // Insert the type. var typeCell = document.createElement("td"); typeCell.setAttribute('class', 'accessorTableData'); var type = options.type; if (!type) { type = ''; } typeCell.innerHTML = type; row.appendChild(typeCell); // Insert the value. // Initial values are optional. There are two ways to specify initial values. // To specify an initial value for all instances of an accessor, define a // value in setup(). Please see // /net/REST.js for example. // To specify an initial value for a web page, add a script element to the // page prior to browser.js defining an initialValues object. Please see // /web/hosts/browser/modules/test/httpClient/testREST.html for example. var valueCell = document.createElement("td"); valueCell.setAttribute('class', 'accessorTableData'); if ((typeof initialValues != "undefined") && (initialValues.hasOwnProperty(id + "." + name))) { options.initialValue = initialValues[id + "." + name]; } var value = options.currentValue || options.initialValue || // Page-specific initial value takes precedence // over accessor default value (options.value) options.value || options.latestOutput || ''; if (typeof value === 'object') { value = JSON.stringify(value); } if (!editable) { valueCell.innerHTML = value; // Set a unique ID so that this input can be retrieved by the get() // or set by the send() function defined in local.js. valueCell.setAttribute('id', id + '.' + name); } else { // Either a parameter or input. Outputs are not editable. var valueInput; if (options.options !== null && typeof options.options !== 'undefined') { valueInput = document.createElement('select'); var selectMe, optionElement; // Sometimes there is only one option. if (typeof options.options === 'string') { var optionsArray = []; optionsArray.push(options.options); options.options = optionsArray; } if (value !== null && typeof value !== 'undefined' && options.options.includes(value)) { selectMe = value; } else { selectMe = options.options[0]; } options.options.forEach(function (option) { optionElement = document.createElement('option'); optionElement.text = option; optionElement.value = option; if (option === selectMe) { optionElement.selected = true; optionElement.defaultSelected = true; } else { optionElement.selected = false; optionElement.defaultSelected = false; } valueInput.add(optionElement); }); } else { valueInput = document.createElement("input"); valueInput.setAttribute('type', 'text'); valueInput.setAttribute('value', value); } if (role === 'input') { // Do not invoke any handlers on input change. The user must // initiate invoctaion with the "react to inputs" button. valueInput.setAttribute('class', 'valueInputBox inputRole'); } else { // Invoke setParameter() on change. Note onchange() also fires when // the user deletes a form field value. valueInput.setAttribute('onchange', 'setParameter("' + id + '", name, value)'); valueInput.setAttribute('class', 'valueInputBox parameterRole'); } // Set a unique ID so that this input can be retrieved by the get() // function defined in local.js. valueInput.setAttribute('id', id + '.' + name); valueInput.setAttribute('name', name); valueCell.appendChild(valueInput); } row.appendChild(valueCell); // Insert the documentation, if any is found. var success = false; var docCell; if (window.accessorDocs) { var docs = window.accessorDocs[id]; if (docs) { // Try with various suffixes. var doc = docs[name]; if (!doc) { doc = docs[name + ' (parameter)']; } if (!doc) { doc = docs[name + ' (port)']; } if (!doc) { doc = docs[name + ' (port-parameter)']; } if (doc) { success = true; docCell = document.createElement("td"); docCell.className = 'accessorDocumentation accessorTableData'; docCell.innerHTML = doc; row.appendChild(docCell); } } } if (!success) { docCell = document.createElement("td"); docCell.setAttribute('class', 'accessorDocumentation accessorWarning'); docCell.innerHTML = 'No description found'; row.appendChild(docCell); } table.appendChild(row); return resolve(true); }); } /** Get default documentation from a base accessor or implemented interface. * This ignores description, author, and version fields of the base documentation. * @param docs The data structure to populate with documentation. * @param path The path of the base accessor or interface. */ function getBaseDocumentation(docs, path) { // Attempt to read the PtDoc file. path = normalizePath(path); path = path + 'PtDoc.xml'; var request = new XMLHttpRequest(); request.overrideMimeType("application/xml"); // FIXME: Have to do this as a synchronous request with this design // because we are populating the data structure that will be used upon // returning. request.open('GET', path, false); // Pass false for synchronous request.send(); // FIXME: Need to instantiate the base in order to know whether // it, in turn, extends or implements anything, and follow that here. // Need the id of that here: /* var implemented = window.accessors[id].implementedInterfaces; for (var i = 0; i < implemented.length; i++) { getBaseDocumentation(docs, implemented[i]); } */ // If the request was successful. if (request.status === 200) { var properties = request.responseXML.getElementsByTagName('property'); for (var i = 0; i < properties.length; i++) { var property = properties[i]; var name = property.getAttribute('name'); if (name !== 'description' && name !== 'author' && name !== 'version') { docs[name] = property.getAttribute('value'); } } } } /** Fetch the top-level index.json file and puts its contents in the specified * docElement. This function will be invoked recursively to populate * subdirectories. * @param baseDirectory The directory to fetch; for example, net for the * net/REST accessor. * @param docElement The HTML document element to add content to. * @param indent The amount of left indentation, in pixels. */ function getIndex(baseDirectory, docElement, indent) { var request = new XMLHttpRequest(); request.overrideMimeType("application/json"); var path = baseDirectory + 'index.json'; request.open('GET', path, true); // Pass true for asynchronous request.onreadystatechange = function () { // If the request is complete (state is 4) if (request.readyState === 4) { // If the request was successful. if (request.status === 200) { var response = JSON.parse(request.responseText); // Expected response is a list of directories and/or .js files. for (var i = 0; i < response.length; i++) { var item = response[i]; var content; if (item.indexOf('.js') === item.length - 3) { // Accessor reference. // Strip off the .js content = document.createElement('a'); // Remove .accessors/ from baseDirectory. var querystring = "index.html?accessor=" + baseDirectory.substring(11, baseDirectory.length) + item.substring(0, item.length - 3); querystring = querystring.replace('/', '.'); content.href = querystring; content.setAttribute('class', 'accessorDirectoryItem'); docElement.appendChild(content); content.innerHTML = item.substring(0, item.length - 3); } else if (item.indexOf('.xml') !== -1) { // Obsolete accessor reference. continue; } else { // Directory reference. // FIXME: + and - for expanded and not. content = document.createElement('div'); content.setAttribute('class', 'accessorDirectoryItem'); content.style.marginLeft = indent + 'px'; docElement.appendChild(content); content.innerHTML = item; var id = (baseDirectory + item); // Create an element for the subdirectory. var subElement = document.createElement('div'); // Start it hidden. subElement.style.display = 'none'; subElement.id = id; docElement.appendChild(subElement); // If 'image' is expanded, start loading cv.js so it // will be available earlier. It's large. // TODO: Put in onclick()≥ content.onclick = (function (id, indent, getIndex) { return function () { if (id.indexOf('image') > 0) { var request2 = new XMLHttpRequest(); request2.open('GET', '/accessors/hosts/browser/modules/cv.js', true); // Pass true for asynchronous request2.send(); } toggleVisibility(id, indent, getIndex); }; })(id, indent + 10, getIndex); } } } else { var pp = document.createElement('p'); pp.setAttribute('class', 'accessorError'); pp.innerHTML = 'No index.json file'; docElement.appendChild(pp); } } }; request.send(); } /** Get data from an input or parameter. This is used by get() and getParameter(). * @param name The name of the input (a string). * @param role One of 'input' or 'parameter'. * @param id The id of the accessor. * @return The value received on the input, or null if no value is received. */ function getInputOrParameter(name, role, id) { var element = document.getElementById(id + '.' + name); if (!element) { alert('No ' + role + ' named ' + name + ' for accessor with id ' + id); return null; } // Depending on the type, we should parse the input or parameter. if (!window.accessors || !window.accessors[id]) { // The accessors field of the window object has not been set, so // just interpret the value as a string. return element.value; } else { var options = window.accessors[id][role + 's'][name]; if (!options) { // This could only occur is somehow the document has an element // with the right name, but there is no such input or parameter. alert('No record of ' + role + ' named ' + name + ' for accessor with id ' + id); return null; } if (!options.type) { // The type is unspecified. // In this host, we attempt to parse it as JSON, and failing that // return a string. Note that we do not want to use eval(), as that // could create security risks. try { return JSON.parse(element.value); } catch (err) { return element.value; } } else if (options.type === 'string') { return element.value; } else { // Types JSON, boolean, int, and number should all be parsable // as JSON, so we proceed with parsing. This will throw an exception // if invalid JSON. Since null is invalid JSON, treat that specially. if (!element.value) { return null; } try { return JSON.parse(element.value); } catch (err) { alert('Invalid JSON on ' + role + ' named ' + name + ': ' + element.value + ' for accessor with id ' + id); return null; } } } } /** Return the text of an accessor definition from the accessor library on the host. * This implementation appends the string '.js' to the specified path * (if it is not already there) and retrieves from the server's accessor * repository the text of the accessor specification. * This is a blocking call. * * @param path The path on the server for the JavaScript code, e.g. 'net/REST'. */ function getAccessorCode(path) { // Strip off a leading '/' if provided. while (path.indexOf('/') === 0) { path = path.substring(1); } // The second argument indicates a blocking call, and the third indicates // to look in the accessor directory, not in the modules directory. return getJavaScript(path, null, false); } /** Return the source code of an accessor or module definition. * This implementation appends the string '.js' to the specified path * (if it is not already there) and issues an HTTP GET with the specified path. * If the path begins with '/' or './', then it is used as is. * Otherwise, depending on the third argument, it is prepended with the * location of the directory in which accessors are stored ('/accessors' on this host) * or the directory in which modules are stored ('/accessors/hosts/browser/modules' * on this host). * * If no callback function is given, then this is a blocking request. * It will not return until it has the text, and then will return that text. * If a callback is given, then this will issue the HTTP get and return, and * then later invoke the callback when the response has been completely received. * The callback function will be passed two argument: an error string (or null if * no error occurred) and the text of the response (or null if an error occurred). * @param path The path on the server for the JavaScript code. * @param callback The callback function. * @param module True to look in the modules directory for paths that do not * begin with '/' or './'. False (or omitted) to look in '/accessors'. */ function getJavaScript(path, callback, module) { var index = path.lastIndexOf('.js'); if (index != path.length - 3) { path = path + '.js'; } if (path.indexOf('/') !== 0 && path.indexOf('./') !== 0) { // A relative path is provided. // Convert this to an absolute path for either a module or an accessor. if (module) { path = '/accessors/hosts/browser/modules/' + path; } else if (path.indexOf('accessors/') !== 0) { path = '/accessors/' + path; } else { path = '/' + path; } } var request = new XMLHttpRequest(); request.overrideMimeType("application/javascript"); if (!callback) { // Synchronous version. request.open('GET', path, false); // Pass false for synchronous request.send(); // Send the request now // Throw an error if the request was not 200 OK if (request.status !== 200) { throw 'Failed to get ' + path + ': ' + request.statusText; } return request.responseText; } else { // Asynchronous version. request.open('GET', path, true); // Pass true for asynchronous request.onreadystatechange = function () { // If the request is complete (state is 4) if (request.readyState === 4) { // If the request was successful. if (request.status === 200) { callback(null, request.responseText); } else { callback('Failed to get ' + path + ': ' + request.statusText, null); } } }; request.send(); } } /** Return the name of this host. * * Return the string "Browser". * * @return In browser.js, return "Browser". */ function getHostName() { return "Browser"; }; /** Initialize the specified accessor instance if it has not been initialized and its * exports object has an initialize function. * @param instance The instance. */ function initializeIfNecessary(instance) { if (!instance.initialized) { try { instance.initialize(); } catch (err) { alert('Error initializing accessor: ' + err); return; } } } /** Normalize the specified accessor path by removing any trailing '.js' and * prepending, if necessary, with '/accessors/'. * @param path The path to normalize. */ function normalizePath(path) { // Remove any trailing '.js'. if (path.indexOf('.js') === path.length - 3) { path = path.substring(0, path.length - 3); } // Make sure the path starts with /accessors so that it will work // with the accessor host. if (path.indexOf('/') === 0) { path = path.substring(1); } if (path.indexOf('accessors/') !== 0) { path = '/accessors/' + path; } else { path = '/' + path; } return path; } /** Provide an input to the accessor with the specified id. * @param id The id of the accessor. * @param name The name of the input. * @param value The value to provide. */ function provideInput(id, name, value) { var instance = window.accessors[id]; initializeIfNecessary(instance); instance.provideInput(name, value); } /** If the accessor is marked executable, then invoke its react() function. * If it has not been previously initialized, then initialize it first. * Otherwise, provide a message that the accessor is not executable, unless * that message is suppressed by the second argument. * @param id The accessor ID. * @param suppress True to suppress the 'not executable' message. */ function reactIfExecutable(id, suppress) { if (window.accessors) { var instance = window.accessors[id]; if (instance && instance.executable) { initializeIfNecessary(instance); try { // Call provideInput() on all visible inputs for this accessor. // This enables inputHandlers for all inputs even if an input's // value has not changed since last execution. // Non-visible inputs are not triggered from the UI, but an // accessor might send to a non-visible input var period; var inputs = document.getElementsByClassName('inputRole'); var element; var found, visible; for (var i = 0; i < inputs.length; i++) { // Element at 6 parents up has accessor name. // (No ancestor function in plain Javascript.) // Check that this input belongs to the accessor that the // "react to inputs" button was clicked for. I.e., the // element should have an ancestor with the accessor id. // Also, check if this input is visible. I.e., does not // have an ancestor with class "invisible". element = inputs[i]; found = false; visible = true; while (element.parentNode !== null) { if (element.classList.contains("invisible")) { visible = false; } if (element.parentNode.id === id) { found = true; break; } element = element.parentNode; } if (found && visible) { if (inputs[i].value !== null && inputs[i].value !== "") { // Do not call provideInput for blank fields. // Use "" in a form field to send an empty string as input. provideInput(id, inputs[i].getAttribute('name'), inputs[i].value); } } } window.accessors[id].react(); } catch (err) { alert('Error executing accessor: ' + err); } return; } } if (!suppress) { alert('Accessor is not executable.'); } } /** Set a parameter of the accessor with the specified id. * @param id The id of the accessor. * @param name The name of the input. * @param value The value to provide. */ function setParameter(id, name, value) { var instance = window.accessors[id]; instance.setParameter(name, value); } /** Toggle the visibility of an element on the web page. * If the element is empty, then populate it from the directory. * @param id The id of the element. * @param indent The amount by which to indent the contents. * @param getIndex A function that will populate a specified element with an * index of accessors. */ function toggleVisibility(id, indent, getIndex) { var element = document.getElementById(id); if (element) { if (element.style.display == 'block') { element.style.display = 'none'; } else { if (element.innerHTML === '') { // Element is empty. Populate it. getIndex(id + '/', element, indent); } element.style.display = 'block'; } } }