"use strict"; // ./modules/shared/lib/defines.js /** * @fileoverview Compile-time definitions used by C1Pjs and PCjs. * @author Jeff Parsons * @version 1.0 * Created 2014-May-08 * * Copyright © 2012-2016 Jeff Parsons * * This file is part of PCjs, a computer emulation software project at . * * PCjs 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. * * PCjs 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 PCjs. 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 COPYRIGHT in /modules/shared/lib/defines.js). * * Some PCjs 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 PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ /** * @define {string} */ var APPVERSION = "1.x.x"; // this @define is overridden by the Closure Compiler with the version in package.json var XMLVERSION = null; // this is set in non-COMPILED builds by embedMachine() if a version number was found in the machine XML var COPYRIGHT = "Copyright © 2012-2016 Jeff Parsons "; var LICENSE = "License: GPL version 3 or later "; var CSSCLASS = "pcjs"; /** * @define {string} */ var SITEHOST = "localhost:8088";// this @define is overridden by the Closure Compiler with "www.pcjs.org" /** * @define {boolean} */ var COMPILED = false; // this @define is overridden by the Closure Compiler (to true) /** * @define {boolean} */ var DEBUG = true; // this @define is overridden by the Closure Compiler (to false) to remove DEBUG-only code /** * @define {boolean} */ var MAXDEBUG = false; // this @define is overridden by the Closure Compiler (to false) to remove MAXDEBUG-only code /** * @define {boolean} */ var PRIVATE = false; // this @define is overridden by the Closure Compiler (to false) to enable PRIVATE code /* * NODE should be true if we're running under NodeJS (eg, command-line), false if not (eg, web browser) */ var NODE = false; // ./modules/shared/lib/dumpapi.js /** * @fileoverview Disk APIs, as defined by diskdump.js and consumed by disk.js * @author Jeff Parsons * @version 1.0 * Created 2014-May-08 * * Copyright © 2012-2016 Jeff Parsons * * This file is part of PCjs, a computer emulation software project at . * * PCjs 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. * * PCjs 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 PCjs. 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 COPYRIGHT in /modules/shared/lib/defines.js). * * Some PCjs 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 PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ /* * Our "DiskDump API", such as it was, used to look like: * * http://jsmachines.net/bin/convdisk.php?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img * * To make it (a bit) more "REST-like", the above request now looks like: * * http://www.pcjs.org/api/v1/dump?disk=/disks/pc/dos/ibm/2.00/PCDOS200-DISK1.json&format=img * * Similarly, our "FileDump API" used to look like: * * http://jsmachines.net/bin/convrom.php?rom=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json * * and that request now looks like: * * http://www.pcjs.org/api/v1/dump?file=/devices/pc/rom/5150/1981-04-24/PCBIOS-REV1.rom&format=json * * I don't think it makes sense to avoid "query" parameters, because blending the path of a disk image with the * the rest of the URL would be (a) confusing, and (b) more work to parse. */ var DumpAPI = { ENDPOINT: "/api/v1/dump", QUERY: { DIR: "dir", // value is path of a directory (DiskDump only) DISK: "disk", // value is path of a disk image (DiskDump only) FILE: "file", // value is path of a ROM image file (FileDump only) IMG: "img", // alias for DISK PATH: "path", // value is path of a one or more files (DiskDump only) FORMAT: "format", // value is one of FORMAT values below COMMENTS: "comments", // value is either "true" or "false" DECIMAL: "decimal", // value is either "true" to force all numbers to decimal, "false" or undefined otherwise MBHD: "mbhd", // value is hard drive size in Mb (formerly "mbsize") (DiskDump only) (DEPRECATED) SIZE: "size" // value is target disk size in Kb (supersedes "mbhd") (DiskDump only) }, FORMAT: { JSON: "json", // default JSON_GZ: "gz", // gzip is currently used ONLY for compressed JSON DATA: "data", // same as "json", but built without JSON.stringify() (DiskDump only) HEX: "hex", // deprecated BYTES: "bytes", // displays data as hex bytes; normally used only when comments are enabled IMG: "img", // returns the raw disk data (ie, using a Buffer object) (DiskDump only) ROM: "rom" // returns the raw file data (ie, using a Buffer object) (FileDump only) } }; /* * Because we use an overloaded API endpoint (ie, one that's shared with the FileDump module), we must * also provide a list of commands which, when combined with the endpoint, define a unique request. */ DumpAPI.asDiskCommands = [DumpAPI.QUERY.DIR, DumpAPI.QUERY.DISK, DumpAPI.QUERY.PATH]; DumpAPI.asFileCommands = [DumpAPI.QUERY.FILE]; // ./modules/shared/lib/reportapi.js /** * @fileoverview Report API, as defined by httpapi.js * @author Jeff Parsons * @version 1.0 * Created 2014-May-13 * * Copyright © 2012-2016 Jeff Parsons * * This file is part of PCjs, a computer emulation software project at . * * PCjs 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. * * PCjs 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 PCjs. 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 COPYRIGHT in /modules/shared/lib/defines.js). * * Some PCjs 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 PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ var ReportAPI = { ENDPOINT: "/api/v1/report", QUERY: { APP: "app", VER: "ver", URL: "url", USER: "user", TYPE: "type", DATA: "data" }, TYPE: { BUG: "bug" }, RES: { OK: "Thank you" } }; // ./modules/shared/lib/userapi.js /** * @fileoverview User API, as defined by httpapi.js * @author Jeff Parsons * @version 1.0 * Created 2014-May-13 * * Copyright © 2012-2016 Jeff Parsons * * This file is part of PCjs, a computer emulation software project at . * * PCjs 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. * * PCjs 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 PCjs. 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 COPYRIGHT in /modules/shared/lib/defines.js). * * Some PCjs 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 PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ /* * Examples of User API requests: * * web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUser; */ var UserAPI = { ENDPOINT: "/api/v1/user", QUERY: { REQ: "req", // specifies a request USER: "user", // specifies a user ID STATE: "state", // specifies a state ID DATA: "data" // specifies state data }, REQ: { CREATE: "create", // creates a user ID VERIFY: "verify", // requests verification of a user ID STORE: "store", // stores a machine state on the server LOAD: "load" // loads a machine state from the server }, RES: { CODE: "code", DATA: "data" }, CODE: { OK: "ok", FAIL: "error" }, FAIL: { DUPLICATE: "user already exists", VERIFY: "unable to verify user", BADSTATE: "invalid state parameter", NOSTATE: "no machine state", BADLOAD: "unable to load machine state", BADSTORE: "unable to save machine state" } }; // ./modules/shared/lib/strlib.js /** * @fileoverview String-related helper functions * @author Jeff Parsons (@jeffpar) * @version 1.0 * Created 2014-03-09 * * Copyright © 2012-2016 Jeff Parsons * * This file is part of PCjs, a computer emulation software project at . * * PCjs 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. * * PCjs 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 PCjs. 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 COPYRIGHT in /modules/shared/lib/defines.js). * * Some PCjs 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 PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ var str = {}; /** * isValidInt(s, base) * * The built-in parseInt() function has the annoying feature of returning a partial value (ie, * up to the point where it encounters an invalid character); eg, parseInt("foo", 16) returns 0xf. * So use this function to validate the entire string. * * @param {string} s is the string representation of some number * @param {number} [base] is the radix of the number represented above (only 2, 10 and 16 are supported) * @return {boolean} true if valid, false if invalid (or the specified base isn't supported) */ str.isValidInt = function(s, base) { if (!base || base == 10) return s.match(/^[0-9]+$/) !== null; if (base == 16) return s.match(/^[0-9a-f]+$/i) !== null; if (base == 2) return s.match(/^[01]+$/i) !== null; return false; }; /** * parseInt(s, base) * * This is a wrapper around the built-in parseInt() function, which recognizes certain prefixes (eg, * '$' or "0x" for hex) and suffixes (eg, 'h' for hex, or '.' for decimal), and then calls isValidInt() * to ensure we don't convert strings that contain partial values (see isValidInt() for details). * * We don't support multiple prefix/suffix combinations, nor do we support the "0b" prefix (or "b" suffix) * for binary, because 1) it's not commonly used, and 2) it conflicts with valid hex sequences. * * @param {string} s is the string representation of some number * @param {number} [base] is the default radix to use (default is 16); can be overridden by prefixes/suffixes * @return {number|undefined} corresponding value, or undefined if invalid */ str.parseInt = function(s, base) { var value; if (s) { if (!base) base = 16; if (s.charAt(0) == '$') { base = 16; s = s.substr(1); } else if (s.substr(0, 2) == "0x") { base = 16; s = s.substr(2); } else { var chSuffix = s.charAt(s.length-1).toLowerCase(); if (chSuffix == 'h') { base = 16; chSuffix = null; } else if (chSuffix == '.') { base = 10; chSuffix = null; } if (chSuffix == null) s = s.substr(0, s.length-1); } var v; if (str.isValidInt(s, base) && !isNaN(v = parseInt(s, base))) { value = v|0; } } return value; }; /** * toBin(n, cch) * * Converts an integer to binary, with the specified number of digits (up to the default of 32). * * @param {number|null|undefined} n is a 32-bit value * @param {number} [cch] is the desired number of binary digits (32 is both the default and the maximum) * @return {string} the binary representation of n */ str.toBin = function(n, cch) { var s = ""; if (cch === undefined) { cch = 32; } else { if (cch > 32) cch = 32; } /* * An initial "falsey" check for null takes care of both null and undefined; * we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough. * * Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN, * since JavaScript coerces such operands to zero, but I think there's "value" in seeing those * values displayed differently. */ if (n == null || isNaN(n)) { while (cch-- > 0) s = '?' + s; } else { while (cch-- > 0) { s = ((n & 0x1)? '1' : '0') + s; n >>= 1; } } return s; }; /** * toBinBytes(n, cb) * * Converts an integer to binary, with the specified number of bytes (up to the default of 4). * * @param {number|null|undefined} n is a 32-bit value * @param {number} [cb] is the desired number of binary bytes (4 is both the default and the maximum) * @return {string} the binary representation of n */ str.toBinBytes = function(n, cb) { var s = ""; if (!cb || cb > 4) cb = 4; for (var i = 0; i < cb; i++) { if (s) s = ',' + s; s = str.toBin(n & 0xff, 8) + 'b' + s; n >>= 8; } return s; }; /** * toHex(n, cch) * * Converts an integer to hex, with the specified number of digits (up to the default of 8). * * You might be tempted to use the built-in n.toString(16) instead, but it doesn't zero-pad and it * doesn't properly convert negative values; for example, if n is -2147483647, then n.toString(16) * will return "-7fffffff" instead of "80000001". Moreover, if n is undefined, n.toString() will * throw an exception, whereas this function will return '?' characters. * * NOTE: The following work-around (adapted from code found on StackOverflow) would be another solution, * taking care of negative values, zero-padding, and upper-casing, but not null/undefined/NaN values: * * s = (n < 0? n + 0x100000000 : n).toString(16); * s = "00000000".substr(0, 8 - s.length) + s; * s = s.substr(0, cch).toUpperCase(); * * @param {number|null|undefined} n is a 32-bit value * @param {number} [cch] is the desired number of hex digits (8 is both the default and the maximum) * @return {string} the hex representation of n */ str.toHex = function(n, cch) { var s = ""; if (cch === undefined) { cch = 8; } else { if (cch > 8) cch = 8; } /* * An initial "falsey" check for null takes care of both null and undefined; * we can't rely entirely on isNaN(), because isNaN(null) returns false, oddly enough. * * Alternatively, we could mask and shift n regardless of whether it's null/undefined/NaN, * since JavaScript coerces such operands to zero, but I think there's "value" in seeing those * values displayed differently. */ if (n == null || isNaN(n)) { while (cch-- > 0) s = '?' + s; } else { while (cch-- > 0) { var d = n & 0xf; d += (d >= 0 && d <= 9? 0x30 : 0x41 - 10); s = String.fromCharCode(d) + s; n >>= 4; } } return s; }; /** * toHexByte(b) * * Alias for "0x" + str.toHex(b, 2) * * @param {number|null|undefined} b is a byte value * @return {string} the hex representation of b */ str.toHexByte = function(b) { return "0x" + str.toHex(b, 2); }; /** * toHexWord(w) * * Alias for "0x" + str.toHex(w, 4) * * @param {number|null|undefined} w is a word (16-bit) value * @return {string} the hex representation of w */ str.toHexWord = function(w) { return "0x" + str.toHex(w, 4); }; /** * toHexLong(l) * * Alias for "0x" + toHex(l) * * @param {number|null|undefined} l is a dword (32-bit) value * @return {string} the hex representation of w */ str.toHexLong = function(l) { return "0x" + str.toHex(l); }; /** * getBaseName(sFileName, fStripExt) * * This is a poor-man's version of Node's path.basename(), which Node-only components should use instead. * * Note that if fStripExt is true, this strips ANY extension, whereas path.basename() strips the extension only * if it matches the second parameter (eg, path.basename("/foo/bar/baz/asdf/quux.html", ".html") returns "quux"). * * @param {string} sFileName * @param {boolean} [fStripExt] * @return {string} */ str.getBaseName = function(sFileName, fStripExt) { var sBaseName = sFileName; var i = sFileName.lastIndexOf('/'); if (i >= 0) sBaseName = sFileName.substr(i + 1); /* * This next bit is a kludge to clean up names that are part of a URL that includes unsightly query parameters. */ i = sBaseName.indexOf('&'); if (i > 0) sBaseName = sBaseName.substr(0, i); if (fStripExt) { i = sBaseName.lastIndexOf("."); if (i > 0) { sBaseName = sBaseName.substring(0, i); } } return sBaseName; }; /** * getExtension(sFileName) * * This is a poor-man's version of Node's path.extname(), which Node-only components should use instead. * * Note that we EXCLUDE the period from the returned extension, whereas path.extname() includes it. * * @param {string} sFileName * @return {string} the filename's extension (in lower-case and EXCLUDING the "."), or an empty string */ str.getExtension = function(sFileName) { var sExtension = ""; var i = sFileName.lastIndexOf("."); if (i >= 0) { sExtension = sFileName.substr(i + 1).toLowerCase(); } return sExtension; }; /** * endsWith(s, sSuffix) * * @param {string} s * @param {string} sSuffix * @return {boolean} true if s ends with sSuffix, false if not */ str.endsWith = function(s, sSuffix) { return s.indexOf(sSuffix, s.length - sSuffix.length) !== -1; }; str.aHTMLEscapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; /** * escapeHTML(sHTML) * * @param {string} sHTML * @return {string} with HTML entities "escaped", similar to PHP's htmlspecialchars() */ str.escapeHTML = function(sHTML) { return sHTML.replace(/[&<>"']/g, function(m) { return str.aHTMLEscapeMap[m]; }); }; /** * replaceAll(sFind, sReplace, s) * * @param {string} sFind * @param {string} sReplace * @param {string} s * @return {string} */ str.replaceAll = function(sFind, sReplace, s) { var a = {}; a[sFind] = sReplace; return str.replaceArray(a, s); }; /** * replaceArray(a, s) * * @param {Object} a * @param {string} s * @return {string} */ str.replaceArray = function(a, s) { var sMatch = ""; for (var k in a) { /* * As noted in: * * http://www.regexguru.com/2008/04/escape-characters-only-when-necessary/ * * inside character classes, only backslash, caret, hyphen and the closing bracket need to be * escaped. And in fact, if you ensure that the closing bracket is first, the caret is not first, * and the hyphen is last, you can avoid escaping those as well. */ k = k.replace(/([\\[\]*{}().+?])/g, "\\$1"); sMatch += (sMatch? '|' : '') + k; } return s.replace(new RegExp('(' + sMatch + ')', "g"), function(m) { return a[m]; }); }; /** * pad(s, cch, fPadLeft) * * NOTE: the maximum amount of padding currently supported is 40 spaces. * * @param {string} s is a string * @param {number} cch is desired length * @param {boolean} [fPadLeft] (default is padding on the right) * @returns {string} the original string (s) with spaces padding it to the specified length */ str.pad = function(s, cch, fPadLeft) { var sPadding = " "; return fPadLeft? (sPadding + s).slice(-cch) : (s + sPadding).slice(0, cch); }; /** * trim(s) * * @param {string} s * @returns {string} */ str.trim = function(s) { if (String.prototype.trim) { return s.trim(); } return s.replace(/^\s+|\s+$/g, ""); }; // ./modules/shared/lib/usrlib.js /** * @fileoverview Assorted helper functions * @author Jeff Parsons (@jeffpar) * @version 1.0 * Created 2014-03-09 * * Copyright © 2012-2016 Jeff Parsons * * This file is part of PCjs, a computer emulation software project at . * * PCjs 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. * * PCjs 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 PCjs. 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 COPYRIGHT in /modules/shared/lib/defines.js). * * Some PCjs 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 PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ var usr = {}; /** * binarySearch(a, v, fnCompare) * * @param {Array} a is an array * @param {number|string|Array|Object} v * @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare] * @return {number} the index of matching entry if non-negative, otherwise the index of the insertion point */ usr.binarySearch = function(a, v, fnCompare) { var left = 0; var right = a.length; var found = 0; if (fnCompare === undefined) { fnCompare = function(a, b) { return a > b? 1 : a < b? -1 : 0; }; } while (left < right) { var middle = (left + right) >> 1; var compareResult; compareResult = fnCompare(v, a[middle]); if (compareResult > 0) { left = middle + 1; } else { right = middle; found = !compareResult; } } return found? left : ~left; }; /** * binaryInsert(a, v, fnCompare) * * If element v already exists in array a, the array is unchanged (we don't allow duplicates); otherwise, the * element is inserted into the array at the appropriate index. * * @param {Array} a is an array * @param {number|string|Array|Object} v is the value to insert * @param {function((number|string|Array|Object), (number|string|Array|Object))} [fnCompare] */ usr.binaryInsert = function(a, v, fnCompare) { var index = usr.binarySearch(a, v, fnCompare); if (index < 0) { a.splice(-(index + 1), 0, v); } }; /** * getTime() * * @return {number} the current time, in milliseconds */ usr.getTime = Date.now || function() { return +new Date(); }; /** * getTimestamp() * * @return {string} timestamp containing the current date and time ("yyyy-mm-dd hh:mm:ss") */ usr.getTimestamp = function() { var date = new Date(); var padNum = function(n) { return (n < 10? "0" : "") + n; }; return date.getFullYear() + "-" + padNum(date.getMonth() + 1) + "-" + padNum(date.getDate()) + " " + padNum(date.getHours()) + ":" + padNum(date.getMinutes()) + ":" + padNum(date.getSeconds()); }; /** * getMonthDays(nMonth, nYear) * * Note that if we're being called on behalf of the RTC, its year is always truncated to two digits (mod 100), * so we have no idea what century the year 0 might refer to. When using the normal leap-year formula, 0 fails * the mod 100 test but passes the mod 400 test, so as far as the RTC is concerned, every century year is a leap * year. Since we're most likely dealing with the year 2000, that's fine, since 2000 was also a leap year. * * TODO: There IS a separate CMOS byte that's supposed to be set to CMOS_ADDR.CENTURY_DATE; it's always BCD, * so theoretically it will contain values like 0x19 or 0x20 (for the 20th and 21st centuries, respectively), and * we could add that as another parameter to this function, to improve the accuracy, but that would go beyond what * a real RTC actually does. * * @param {number} nMonth (1-12) * @param {number} nYear (normally a 4-digit year, but it may also be mod 100) * @return {number} the maximum (1-based) day allowed for the specified month and year */ usr.getMonthDays = function(nMonth, nYear) { var nDays = usr.aMonthDays[nMonth - 1]; if (nDays == 28) { if ((nYear % 4) === 0 && ((nYear % 100) || (nYear % 400) === 0)) { nDays++; } } return nDays; }; usr.asDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; usr.asMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; usr.aMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; /** * formatDate(sFormat, date) * * @param {string} sFormat (eg, "F j, Y", "Y-m-d H:i:s") * @param {Date} [date] (default is the current time) * @return {string} * * Supported identifiers in sFormat include: * * a: lowercase ante meridiem and post meridiem (am or pm) * d: day of the month, 2 digits with leading zeros (01,...,31) * g: hour in 12-hour format, without leading zeros (1,...,12) * i: minutes, with leading zeros (00,...,59) * j: day of the month, without leading zeros (1,...,31) * l: day of the week ("Sunday",...,"Saturday") * m: month, with leading zeros (01,...,12) * s: seconds, with leading zeros (00,...,59) * F: month ("January",...,"December") * H: hour in 24-hour format, with leading zeros (00,...,23) * Y: year (eg, 2014) * * For more inspiration, see: http://php.net/manual/en/function.date.php */ usr.formatDate = function(sFormat, date) { var sDate = ""; if (!date) date = new Date(); var iHour = date.getHours(); var iDay = date.getDate(); var iMonth = date.getMonth() + 1; for (var i = 0; i < sFormat.length; i++) { var ch; switch((ch = sFormat.charAt(i))) { case 'a': sDate += (iHour < 12? "am" : "pm"); break; case 'd': sDate += ('0' + iDay).slice(-2); break; case 'g': sDate += (!iHour? 12 : (iHour > 12? iHour - 12 : iHour)); break; case 'i': sDate += ('0' + date.getMinutes()).slice(-2); break; case 'j': sDate += iDay; break; case 'l': sDate += usr.asDays[date.getDay()]; break; case 'm': sDate += ('0' + iMonth).slice(-2); break; case 's': sDate += ('0' + date.getSeconds()).slice(-2); break; case 'F': sDate += usr.asMonths[iMonth - 1]; break; case 'H': sDate += ('0' + iHour).slice(-2); break; case 'Y': sDate += date.getFullYear(); break; default: sDate += ch; break; } } return sDate; }; /** * @typedef {{ * mask: number, * shift: number * }} */ var BitField; /** * @typedef {Object.} */ var BitFields; /** * defineBitFields(bfs) * * Prepares a bit field definition for use with getBitField() and setBitField(); eg: * * var bfs = usr.defineBitFields({num:20, count:8, btmod:1, type:3}); * * The above defines a set of bit fields containg four fields: num (bits 0-19), count (bits 20-27), btmod (bit 28), and type (bits 29-31). * * usr.setBitField(bfs.num, n, 1); * * The above set bit field "bfs.num" in numeric variable "n" to the value 1. * * @param {Object} bfs * @return {BitFields} */ usr.defineBitFields = function(bfs) { var bit = 0; for (var f in bfs) { var width = bfs[f]; var mask = ((1 << width) - 1) << bit; bfs[f] = {mask: mask, shift: bit}; bit += width; } // return bfs; }; /** * initBitFields(bfs, ...) * * @param {BitFields} bfs * @param {...number} var_args * @return {number} a value containing all supplied bit fields */ usr.initBitFields = function(bfs, var_args) { var v = 0, i = 1; for (var f in bfs) { if (i >= arguments.length) break; v = usr.setBitField(bfs[f], v, arguments[i++]); } return v; }; /** * getBitField(bf, v) * * @param {BitField} bf * @param {number} v is a value containing bit fields * @return {number} the value of the bit field in v defined by bf */ usr.getBitField = function(bf, v) { return (v & bf.mask) >> bf.shift; }; /** * setBitField(bf, v, n) * * @param {BitField} bf * @param {number} v is a value containing bit fields * @param {number} n is a value to store in v in the bit field defined by bf * @return {number} updated v */ usr.setBitField = function(bf, v, n) { // return (v & ~bf.mask) | ((n << bf.shift) & bf.mask); }; /** * indexOf(a, t, i) * * Use this instead of Array.prototype.indexOf() if you can't be sure the browser supports it. * * @param {Array} a * @param {*} t * @param {number} [i] * @returns {number} */ usr.indexOf = function(a, t, i) { if (Array.prototype.indexOf) { return a.indexOf(t, i); } i = i || 0; if (i < 0) i += a.length; if (i < 0) i = 0; for (var n = a.length; i < n; i++) { if (i in a && a[i] === t) return i; } return -1; }; // ./modules/shared/lib/weblib.js /** * @fileoverview Browser-related helper functions * @author Jeff Parsons (@jeffpar) * @version 1.0 * Created 2014-05-08 * * Copyright © 2012-2016 Jeff Parsons * * This file is part of PCjs, a computer emulation software project at . * * PCjs 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. * * PCjs 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 PCjs. 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 COPYRIGHT in /modules/shared/lib/defines.js). * * Some PCjs 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 PCjs * for purposes of the GNU General Public License, and the author does not claim any copyright * as to their contents. */ /* * According to http://www.w3schools.com/jsref/jsref_obj_global.asp, these are the *global* properties * and functions of JavaScript-in-the-Browser: * * Property Description * --- * Infinity A numeric value that represents positive/negative infinity * NaN "Not-a-Number" value * undefined Indicates that a variable has not been assigned a value * * Function Description * --- * decodeURI() Decodes a URI * decodeURIComponent() Decodes a URI component * encodeURI() Encodes a URI * encodeURIComponent() Encodes a URI component * escape() Deprecated in version 1.5. Use encodeURI() or encodeURIComponent() instead * eval() Evaluates a string and executes it as if it was script code * isFinite() Determines whether a value is a finite, legal number * isNaN() Determines whether a value is an illegal number * Number() Converts an object's value to a number * parseFloat() Parses a string and returns a floating point number * parseInt() Parses a string and returns an integer * String() Converts an object's value to a string * unescape() Deprecated in version 1.5. Use decodeURI() or decodeURIComponent() instead * * And according to http://www.w3schools.com/jsref/obj_window.asp, these are the properties and functions * of the *window* object. * * Property Description * --- * closed Returns a Boolean value indicating whether a window has been closed or not * defaultStatus Sets or returns the default text in the statusbar of a window * document Returns the Document object for the window (See Document object) * frames Returns an array of all the frames (including iframes) in the current window * history Returns the History object for the window (See History object) * innerHeight Returns the inner height of a window's content area * innerWidth Returns the inner width of a window's content area * length Returns the number of frames (including iframes) in a window * location Returns the Location object for the window (See Location object) * name Sets or returns the name of a window * navigator Returns the Navigator object for the window (See Navigator object) * opener Returns a reference to the window that created the window * outerHeight Returns the outer height of a window, including toolbars/scrollbars * outerWidth Returns the outer width of a window, including toolbars/scrollbars * pageXOffset Returns the pixels the current document has been scrolled (horizontally) from the upper left corner of the window * pageYOffset Returns the pixels the current document has been scrolled (vertically) from the upper left corner of the window * parent Returns the parent window of the current window * screen Returns the Screen object for the window (See Screen object) * screenLeft Returns the x coordinate of the window relative to the screen * screenTop Returns the y coordinate of the window relative to the screen * screenX Returns the x coordinate of the window relative to the screen * screenY Returns the y coordinate of the window relative to the screen * self Returns the current window * status Sets or returns the text in the statusbar of a window * top Returns the topmost browser window * * Method Description * --- * alert() Displays an alert box with a message and an OK button * atob() Decodes a base-64 encoded string * blur() Removes focus from the current window * btoa() Encodes a string in base-64 * clearInterval() Clears a timer set with setInterval() * clearTimeout() Clears a timer set with setTimeout() * close() Closes the current window * confirm() Displays a dialog box with a message and an OK and a Cancel button * createPopup() Creates a pop-up window * focus() Sets focus to the current window * moveBy() Moves a window relative to its current position * moveTo() Moves a window to the specified position * open() Opens a new browser window * print() Prints the content of the current window * prompt() Displays a dialog box that prompts the visitor for input * resizeBy() Resizes the window by the specified pixels * resizeTo() Resizes the window to the specified width and height * scroll() This method has been replaced by the scrollTo() method. * scrollBy() Scrolls the content by the specified number of pixels * scrollTo() Scrolls the content to the specified coordinates * setInterval() Calls a function or evaluates an expression at specified intervals (in milliseconds) * setTimeout() Calls a function or evaluates an expression after a specified number of milliseconds * stop() Stops the window from loading */ /* global window: true, setTimeout: false, clearTimeout: false, SITEHOST: false */ var web = {}; /* * We must defer loading the Component module until the function(s) requiring it are * called; otherwise, we create an initialization cycle in which Component requires weblib * and weblib requires Component. * * In an ideal world, weblib would not be dependent on Component, but we really want to use * its I/O functions. The simplest solution is to create wrapper functions. */ /** * log(s, type) * * For diagnostic output only. DEBUG must be true (or "--debug" specified via the command-line) * for Component.log() to display anything. * * @param {string} [s] is the message text * @param {string} [type] is the message type */ web.log = function(s, type) { Component.log(s, type); }; /** * notice(s, fPrintOnly, id) * * If Component.notice() calls web.alertUser(), it will fall back to web.log() if all else fails. * * @param {string} s is the message text * @param {boolean} [fPrintOnly] * @param {string} [id] is the caller's ID, if any */ web.notice = function(s, fPrintOnly, id) { Component.notice(s, fPrintOnly, id); }; /** * getResource(sURL, dataPost, fAsync, done) * * Request the specified resource (sURL), and once the request is complete, notify done(). * * Also, if dataPost is set to a string, that string can be used to control the response format; * by default, the response format is plain text, but you can specify "bytes" to request arbitrary * binary data, which should come back as a string of bytes. * * TODO: The "bytes" option works by calling overrideMimeType(), which was never a best practice. * Instead, we should implement supported response types ("text" and "arraybuffer", at a minimum) * by setting xmlHTTP.responseType to one of those values before calling xmlHTTP.send(). * * @param {string} sURL * @param {string|Object|null} [dataPost] for a POST request (default is a GET request) * @param {boolean} [fAsync] is true for an asynchronous request * @param {function(string,string,number)} [done] * @return {Array|null} Array containing [sResource, nErrorCode], or null if no response yet */ web.getResource = function(sURL, dataPost, fAsync, done) { var nErrorCode = 0, sResource = null, response = null; if (typeof resources == 'object' && (sResource = resources[sURL])) { if (done) done(sURL, sResource, nErrorCode); return [sResource, nErrorCode]; } else if (fAsync && typeof resources == 'function') { resources(sURL, function(sResource, nErrorCode) { if (done) done(sURL, sResource, nErrorCode); }); return response; } if (DEBUG) { /* * The larger resources that we put on archive.pcjs.org should also be available locally... */ sURL = sURL.replace("http://archive.pcjs.org", ""); } var xmlHTTP = (window.XMLHttpRequest? new window.XMLHttpRequest() : new window.ActiveXObject("Microsoft.XMLHTTP")); if (fAsync) { xmlHTTP.onreadystatechange = function() { if (xmlHTTP.readyState === 4) { /* * The following line was recommended for WebKit, as a work-around to prevent the handler firing multiple * times when debugging. Unfortunately, that's not the only XMLHttpRequest problem that occurs when * debugging, so I think the WebKit problem is deeper than that. When we have multiple XMLHttpRequests * pending, any debugging activity means most of them simply get dropped on floor, so what may actually be * happening are mis-notifications rather than redundant notifications. * * xmlHTTP.onreadystatechange = undefined; */ sResource = xmlHTTP.responseText; /* * The normal "success" case is an HTTP status code of 200, but when testing with files loaded * from the local file system (ie, when using the "file:" protocol), we have to be a bit more "flexible". */ if (xmlHTTP.status == 200 || !xmlHTTP.status && sResource.length && web.getHostProtocol() == "file:") { if (MAXDEBUG) web.log("xmlHTTP.onreadystatechange(" + sURL + "): returned " + sResource.length + " bytes"); } else { nErrorCode = xmlHTTP.status || -1; web.log("xmlHTTP.onreadystatechange(" + sURL + "): error code " + nErrorCode); } if (done) done(sURL, sResource, nErrorCode); } }; } if (dataPost && typeof dataPost == "object") { var sDataPost = ""; for (var p in dataPost) { if (!dataPost.hasOwnProperty(p)) continue; if (sDataPost) sDataPost += "&"; sDataPost += p + '=' + encodeURIComponent(dataPost[p]); } sDataPost = sDataPost.replace(/%20/g, '+'); if (MAXDEBUG) web.log("web.getResource(POST " + sURL + "): " + sDataPost.length + " bytes"); xmlHTTP.open("POST", sURL, !!fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it) xmlHTTP.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xmlHTTP.send(sDataPost); } else { if (MAXDEBUG) web.log("web.getResource(GET " + sURL + ")"); xmlHTTP.open("GET", sURL, !!fAsync); // ensure that fAsync is a valid boolean (Internet Explorer xmlHTTP functions insist on it) if (dataPost == "bytes") { xmlHTTP.overrideMimeType("text/plain; charset=x-user-defined"); } xmlHTTP.send(); } if (!fAsync) { sResource = xmlHTTP.responseText; if (xmlHTTP.status == 200) { if (MAXDEBUG) web.log("web.getResource(" + sURL + "): returned " + sResource.length + " bytes"); } else { nErrorCode = xmlHTTP.status || -1; web.log("web.getResource(" + sURL + "): error code " + nErrorCode); } if (done) done(sURL, sResource, nErrorCode); response = [sResource, nErrorCode]; } return response; }; /** * sendReport(sApp, sVer, sURL, sUser, sType, sReport, sHostName) * * Send a report (eg, bug report) to the server. * * @param {string} sApp (eg, "PCjs") * @param {string} sVer (eg, "1.02") * @param {string} sURL (eg, "/devices/pc/machine/5150/mda/64kb/machine.xml") * @param {string} sUser (ie, the user key, if any) * @param {string} sType (eg, "bug"); one of ReportAPI.TYPE.* * @param {string} sReport (eg, unparsed state data) * @param {string} [sHostName] (default is http://SITEHOST) */ web.sendReport = function(sApp, sVer, sURL, sUser, sType, sReport, sHostName) { var dataPost = {}; dataPost[ReportAPI.QUERY.APP] = sApp; dataPost[ReportAPI.QUERY.VER] = sVer; dataPost[ReportAPI.QUERY.URL] = sURL; dataPost[ReportAPI.QUERY.USER] = sUser; dataPost[ReportAPI.QUERY.TYPE] = sType; dataPost[ReportAPI.QUERY.DATA] = sReport; var sReportURL = (sHostName? sHostName : "http://" + SITEHOST) + ReportAPI.ENDPOINT; web.getResource(sReportURL, dataPost, true); }; /** * getHost() * * @return {string} */ web.getHost = function() { return ("http://" + (window? window.location.host : SITEHOST)); }; /** * getHostURL() * * @return {string|null} */ web.getHostURL = function() { return (window? window.location.href : null); }; /** * getHostProtocol() * * @return {string} */ web.getHostProtocol = function() { return (window? window.location.protocol : "file:"); }; /** * getUserAgent() * * @return {string} */ web.getUserAgent = function() { return (window? window.navigator.userAgent : ""); }; /** * alertUser(sMessage) * * @param {string} sMessage */ web.alertUser = function(sMessage) { if (window) window.alert(sMessage); else web.log(sMessage); }; /** * confirmUser(sPrompt) * * @param {string} sPrompt * @returns {boolean} true if the user clicked OK, false if Cancel/Close */ web.confirmUser = function(sPrompt) { var fResponse = false; if (window) { fResponse = window.confirm(sPrompt); } return fResponse; }; /** * promptUser() * * @param {string} sPrompt * @param {string} [sDefault] * @returns {string|null} */ web.promptUser = function(sPrompt, sDefault) { var sResponse = null; if (window) { sResponse = window.prompt(sPrompt, sDefault === undefined? "" : sDefault); } return sResponse; }; /** * fLocalStorage * * true if localStorage support exists, is enabled, and works; "falsey" otherwise * * @type {boolean|null} */ web.fLocalStorage = null; /** * TODO: Is there any way to get the Closure Compiler to stop inlining this string? This * isn't cutting it. * * @const {string} */ web.sLocalStorageTest = "PCjs.localStorage"; /** * hasLocalStorage * * true if localStorage support exists, is enabled, and works; false otherwise * * @return {boolean} */ web.hasLocalStorage = function() { if (web.fLocalStorage == null) { var f = false; if (window) { try { window.localStorage.setItem(web.sLocalStorageTest, web.sLocalStorageTest); f = (window.localStorage.getItem(web.sLocalStorageTest) == web.sLocalStorageTest); window.localStorage.removeItem(web.sLocalStorageTest); } catch(e) { web.logLocalStorageError(e); f = false; } } web.fLocalStorage = f; } return web.fLocalStorage; }; /** * logLocalStorageError(e) * * @param {Error} e is an exception */ web.logLocalStorageError = function(e) { web.log(e.message, "localStorage error"); }; /** * getLocalStorageItem(sKey) * * Returns the requested key value, or null if the key does not exist, or undefined if localStorage is not available * * @param {string} sKey * @return {string|null|undefined} sValue */ web.getLocalStorageItem = function(sKey) { var sValue; if (window) { try { sValue = window.localStorage.getItem(sKey); } catch(e) { web.logLocalStorageError(e); } } return sValue; }; /** * setLocalStorageItem(sKey, sValue) * * @param {string} sKey * @param {string} sValue * @return {boolean} true if localStorage is available, false if not */ web.setLocalStorageItem = function(sKey, sValue) { try { window.localStorage.setItem(sKey, sValue); return true; } catch(e) { web.logLocalStorageError(e); } return false; }; /** * removeLocalStorageItem(sKey) * * @param {string} sKey */ web.removeLocalStorageItem = function(sKey) { try { window.localStorage.removeItem(sKey); } catch(e) { web.logLocalStorageError(e); } }; /** * getLocalStorageKeys() * * @return {Array} */ web.getLocalStorageKeys = function() { var a = []; try { for (var i = 0, c = window.localStorage.length; i < c; i++) { a.push(window.localStorage.key(i)); } } catch(e) { web.logLocalStorageError(e); } return a; }; /** * reloadPage() */ web.reloadPage = function() { if (window) window.location.reload(); }; /** * isUserAgent(s) * * Check the browser's user-agent string for the given substring; "iOS" and "MSIE" are special values you can * use that will match any iOS or MSIE browser, respectively (even IE11, in the case of "MSIE"). * * 2013-11-06: In a questionable move, MSFT changed the user-agent reported by IE11 on Windows 8.1, eliminating * the "MSIE" string (which MSDN calls a "version token"; see http://msdn.microsoft.com/library/ms537503.aspx); * they say "public websites should rely on feature detection, rather than browser detection, in order to design * their sites for browsers that don't support the features used by the website." So, in IE11, we get a user-agent * that tries to fool apps into thinking the browser is more like WebKit or Gecko: * * Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko * * That's a nice idea, but in the meantime, they hosed the XSL transform code in embed.js, which contained * some very critical browser-specific code; turning on IE's "Compatibility Mode" didn't help either, because * that's a sledgehammer solution which restores the old user-agent string but also disables other features like * HTML5 canvas support. As an interim solution, I'm treating any "MSIE" check as a check for either "MSIE" or * "Trident". * * UPDATE: I've since found ways to make the code in embed.js more browser-agnostic, so for now, there's isn't * any code that cares about "MSIE", but I've left the change in place, because I wouldn't be surprised if I'll * need more IE-specific code in the future, perhaps for things like copy/paste functionality, or mouse capture. * * @param {string} s is a substring to search for in the user-agent; as noted above, "iOS" and "MSIE" are special values * @return {boolean} is true if the string was found, false if not */ web.isUserAgent = function(s) { if (window) { var userAgent = web.getUserAgent(); /* * Here's one case where we have to be careful with Component, because when isUserAgent() is called by * the init code below, component.js hasn't been loaded yet. The simple solution for now is to remove the call. * * web.log("agent: " + userAgent); * * And yes, it would be pointless to use the conditional (?) operator below, if not for the Google Closure * Compiler (v20130823) failing to detect the entire expression as a boolean. */ return (s == "iOS" && userAgent.match(/(iPod|iPhone|iPad)/) && userAgent.match(/AppleWebKit/) || s == "MSIE" && userAgent.match(/(MSIE|Trident)/) || (userAgent.indexOf(s) >= 0))? true : false; } return false; }; /** * isMobile() * * Check the browser's user-agent string for the substring "Mobi", as per Mozilla recommendation: * * https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent * * @return {boolean} is true if the browser appears to be a mobile (ie, non-desktop) web browser, false if not */ web.isMobile = function() { return web.isUserAgent("Mobi"); }; /** * getURLParameters(sParms) * * @param {string} [sParms] containing the parameter portion of a URL (ie, after the '?') * @return {Object} containing properties for each parameter found */ web.getURLParameters = function(sParms) { var aParms = {}; if (window) { // an alternative to "if (typeof module === 'undefined')" if require("defines") was used if (!sParms) { /* * Note that window.location.href returns the entire URL, whereas window.location.search * returns only the parameters, if any (starting with the '?', which we skip over with a substr() call). */ sParms = window.location.search.substr(1); } var match; var pl = /\+/g; // RegExp for replacing addition symbol with a space var search = /([^&=]+)=?([^&]*)/g; var decode = function(s) { return decodeURIComponent(s.replace(pl, " ")); }; while ((match = search.exec(sParms))) { aParms[decode(match[1])] = decode(match[2]); } } return aParms; }; /** * downloadFile(sData, sType, fBase64, sFileName) * * @param {string} sData * @param {string} sType * @param {boolean} [fBase64] * @param {string} [sFileName] */ web.downloadFile = function(sData, sType, fBase64, sFileName) { var link = null, sAlert; var sURI = "data:application/" + sType + (fBase64? ";base64" : "") + ","; if (!web.isUserAgent("Firefox")) { sURI += (fBase64? sData : encodeURI(sData)); } else { sURI += (fBase64? sData : encodeURIComponent(sData)); } if (sFileName) { link = document.createElement('a'); if (typeof link.download != 'string') link = null; } if (link) { link.href = sURI; link.download = sFileName; document.body.appendChild(link); // Firefox allegedly requires the link to be in the body link.click(); document.body.removeChild(link); sAlert = 'Check your Downloads folder for ' + sFileName + '.'; } else { window.open(sURI); sAlert = 'Check your browser for a new window/tab containing the requested data' + (sFileName? (' (' + sFileName + ')') : '') + '.'; } return sAlert; }; /** * onCountRepeat(n, fnRepeat, fnComplete, msDelay) * * Call fnRepeat() n times with an msDelay millisecond delay between calls, * then call fnComplete() when n has been exhausted OR fnRepeat() returns false. * * @param {number} n * @param {function()} fnRepeat * @param {function()} fnComplete * @param {number} [msDelay] */ web.onCountRepeat = function(n, fnRepeat, fnComplete, msDelay) { var fnTimeout = function doCountRepeat() { n -= 1; if (n >= 0) { if (!fnRepeat()) n = 0; } if (n > 0) { setTimeout(fnTimeout, msDelay || 0); return; } fnComplete(); }; fnTimeout(); }; /** * onClickRepeat(e, msDelay, msRepeat, fn) * * Repeatedly call fn() with an initial msDelay, and an msRepeat delay thereafter, * as long as HTML control Object e has an active "down" event and fn() returns true. * * @param {Object} e * @param {number} msDelay * @param {number} msRepeat * @param {function(boolean)} fn is passed false on the first call, true on all repeated calls */ web.onClickRepeat = function(e, msDelay, msRepeat, fn) { var ms = 0, timer = null, fIgnoreMouseEvents = false; var fnRepeat = function doClickRepeat() { if (fn(ms === msRepeat)) { timer = setTimeout(fnRepeat, ms); ms = msRepeat; } }; e.onmousedown = function() { // web.log("onMouseDown()"); if (!fIgnoreMouseEvents) { if (!timer) { ms = msDelay; fnRepeat(); } } }; e.ontouchstart = function() { // web.log("onTouchStart()"); if (!timer) { ms = msDelay; fnRepeat(); } }; e.onmouseup = e.onmouseout = function() { // web.log("onMouseUp()/onMouseOut()"); if (timer) { clearTimeout(timer); timer = null; } }; e.ontouchend = e.ontouchcancel = function() { // web.log("onTouchEnd()/onTouchCancel()"); if (timer) { clearTimeout(timer); timer = null; } /* * Devices that generate ontouch* events ALSO generate onmouse* events, * and generally do so immediately after all the touch events are complete, * so unless we want double the action, we need to ignore mouse events. */ fIgnoreMouseEvents = true; }; }; web.aPageEventHandlers = { 'init': [], // list of window 'onload' handlers 'show': [], // list of window 'onpageshow' handlers 'exit': [] // list of window 'onunload' handlers (although we prefer to use 'onbeforeunload' if possible) }; web.fPageReady = false; // set once the browser's first page initialization has occurred web.fPageEventsEnabled = true; /** * onPageEvent(sName, fn) * * @param {string} sFunc * @param {function()} fn * * Use this instead of setting window['onload'], window['onunload'], etc. * Allows multiple JavaScript modules to define a handler for the same event. * * Moreover, it's risky to refer to obscure event handlers with "dot" names, because * the Closure Compiler may erroneously replace them (eg, window.onpageshow is a good example). */ web.onPageEvent = function(sFunc, fn) { if (window) { var fnPrev = window[sFunc]; if (typeof fnPrev !== 'function') { window[sFunc] = fn; } else { /* * TODO: Determine whether there's any value in receiving/sending the Event object that the * browser provides when it generates the original event. */ window[sFunc] = function onWindowEvent() { if (fnPrev) fnPrev(); fn(); }; } } }; /** * onInit(fn) * * @param {function()} fn * * Use this instead of setting window.onload. Allows multiple JavaScript modules to define their own 'onload' event handler. */ web.onInit = function(fn) { web.aPageEventHandlers['init'].push(fn); }; /** * onShow(fn) * * @param {function()} fn * * Use this instead of setting window.onpageshow. Allows multiple JavaScript modules to define their own 'onpageshow' event handler. */ web.onShow = function(fn) { web.aPageEventHandlers['show'].push(fn); }; /** * onExit(fn) * * @param {function()} fn * * Use this instead of setting window.onunload. Allows multiple JavaScript modules to define their own 'onunload' event handler. */ web.onExit = function(fn) { web.aPageEventHandlers['exit'].push(fn); }; /** * doPageEvent(afn) * * @param {Array.} afn */ web.doPageEvent = function(afn) { if (web.fPageEventsEnabled) { try { for (var i = 0; i < afn.length; i++) { afn[i](); } } catch(e) { web.notice("An unexpected exception occurred:\n\n" + e.message + "\n\nPlease send this information to support@pcjs.org. Thanks."); } } }; /** * enablePageEvents(fEnable) * * @param {boolean} fEnable is true to enable page events, false to disable (they're enabled by default) */ web.enablePageEvents = function(fEnable) { if (!web.fPageEventsEnabled && fEnable) { web.fPageEventsEnabled = true; if (web.fPageReady) web.sendPageEvent('init'); return; } web.fPageEventsEnabled = fEnable; }; /** * sendPageEvent(sEvent) * * This allows us to manually trigger page events. * * @param {string} sEvent (one of 'init', 'show' or 'exit') */ web.sendPageEvent = function(sEvent) { if (web.aPageEventHandlers[sEvent]) { web.doPageEvent(web.aPageEventHandlers[sEvent]); } }; web.onPageEvent('onload', function onPageLoad() { web.fPageReady = true; web.doPageEvent(web.aPageEventHandlers['init']); }); web.onPageEvent('onpageshow', function onPageShow() { web.doPageEvent(web.aPageEventHandlers['show']); }); web.onPageEvent(web.isUserAgent("Opera") || web.isUserAgent("iOS")? 'onunload' : 'onbeforeunload', function onPageUnload() { web.doPageEvent(web.aPageEventHandlers['exit']); }); // ./modules/shared/lib/component.js /** * @fileoverview The Component class used by C1Pjs and PCx86. * @author Jeff Parsons * @version 1.0 * Created 2012-May-14 * * Copyright © 2012-2016 Jeff Parsons * * This file is part of PCjs, a computer emulation software project at . * * PCjs 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. * * PCjs 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 PCjs. 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 COPYRIGHT in /modules/shared/lib/defines.js). * * Some PCjs 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 PCjs * 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. */ /* global window: true, DEBUG: true */ /** * 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] selects message(s) that the component wants to enable (default is 0) */ 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.flags = { 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 || 0; /* * TODO: Consider adding another parameter to the Component() constructor that allows components to tell * us if they support single or multiple instances per machine. For example, there can be multiple SerialPort * components per machine, but only one CPU component (well, OK, an FPU is also supported, but that's considered * a different component). * * It's not critical, but it would help catch machine configuration errors; for example, a machine that mistakenly * includes two CPU components may, aside from wasting memory, end up with odd side-effects, like unresponsive * CPU controls. */ Component.add(this); } /** * Component.parmsURL * * Initialized to the set of URL parameters, if any, for the current web page. * * @type {Object} */ 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(subclass, superclass, 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} subclass is the constructor for the new subclass * @param {Object} [superclass] is the constructor of the superclass (default is Component) * @param {Object} [methods] contains all instance methods * @param {Object} [statics] contains all class properties and methods */ Component.subclass = function(subclass, superclass, methods, statics) { if (!superclass) superclass = Component; subclass.prototype = Component.inherit(superclass.prototype); subclass.prototype.constructor = subclass; subclass.prototype.parent = superclass.prototype; 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.components = []; /** * 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.components.push(component); }; /* * Every machine on the page are now recorded as well, by their machine ID. We then record the various resources * used by that machine. */ Component.machines = {}; /** * Component.addMachine(idMachine) * * @param {string} idMachine */ Component.addMachine = function(idMachine) { Component.machines[idMachine] = {}; }; /** * Component.addMachineResource(idMachine, sName, data) * * @param {string} idMachine * @param {string|null} sName (name of the resource) * @param {*} data */ Component.addMachineResource = function(idMachine, sName, data) { /* * I used to assert(Component.machines[idMachine]), but when we're running as a Node app, embed.js is not used, * so addMachine() is never called, so resources do not need to be recorded. */ if (Component.machines[idMachine] && sName) { Component.machines[idMachine][sName] = data; } }; /** * Component.getMachineResources(idMachine) * * @param {string} idMachine * @return {Object|undefined} */ Component.getMachineResources = function(idMachine) { return Component.machines[idMachine]; }; /** * 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 sElapsed = "", sMsg = (type? (type + ": ") : "") + s; if (typeof usr != "undefined") { if (Component.msStart === undefined) { Component.msStart = usr.getTime(); } sElapsed = (usr.getTime() - Component.msStart) + "ms: "; } if (window && window.console) console.log(sElapsed + 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 instances of "assertion failure" in RELEASE builds. * * @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) { 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, 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.components.length; i++) { var component = Component.components[i]; if (!idRelated || !component.id.indexOf(idRelated)) { aComponents.push(component); } } return aComponents; }; /** * Component.getComponentByID(id, idRelated) * * We could store components as properties, 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.components.length; i++) { if (Component.components[i].id === id) { return Component.components[i]; } } if (Component.components.length) { 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|null} [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.components.length; i++) { if (componentPrev) { if (componentPrev == Component.components[i]) componentPrev = null; continue; } if (sType == Component.components[i].type && (!idRelated || !Component.components[i].id.indexOf(idRelated))) { return Component.components[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; var 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); } } } }; /** * 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 !== 1 /* 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, parms['value']); } else if (!parms || parms['type'] != "description") { 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, parent: null, /** * 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, sValue) * * 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) * @param {string} [sValue] optional data value * @return {boolean} true if binding was successful, false if unrecognized binding request */ setBinding: function(sHTMLType, sBinding, control, sValue) { 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