//NAMESPACE TESTRON
/** @namespace testron
* @summary The namespace for all testrons objects
* @description This 'namespace' is the starting point for building tests of 'testrons' type.
* It contains the methods and objects necessary for even build all full UI.
* @example <caption>To create complete Test UI:</caption>
* var tests = testron.parseTestrons(
* objJS,
* {container: 'divUI',
* markerContainer: 'divMarker'
* });
*
* @example <caption>Using the events:</caption>
* testron.onOk(function(answer, marker){
* alert("OK: " +
* answer.parent.num + "[" +
* answer.name + "] ). " +
* marker.score);
* contMarker.style.visibility = "visible";
* });
* testron.onFinished(function(test){
* alert("FINISHED TEST: " + test.id);
* });
*
* @author GuerraTron - 2015. <dinertron@gmail.com> - GPL-v3
* @version 1.0.0b */
var testron = {};
//END NAMESPACE TESTRON
//MODULE TESTRON & UTILS
/** @lends testron
* @module utils */
(function (t) {
"use strict";
//INNER PRIVATES VARS
/** @private
* @memberof testron
* @summary Static Alphanumeric variable for "id" generated */
var alpha = "abcdefghijklmnopqrstuvwxyz";
/** @private
* @memberof testron
* @summary Static pref for Base64 images with 'monocrome GIF' format */
var prefImg64GIF = "data:image/gif;base64,R0lGODlhEAAQAIABAAAAAP///yH5BAEKAAEALAAAAAAQABAAAAI";
/** @private
* @memberof testron
* @summary testrons array */
var tests = [];
//LISTENERS
/** @private
* @memberof testron
* @summary Event Listener for successed Answer. This must be a function with two parameters: Answer object and Marker object */
var successAnswerListener = null;
/** @private
* @memberof testron
* @summary Event Listener for failed Answer. This must be a function with two parameters: Answer object and Marker object */
var failAnswerListener = null;
/** @private
* @memberof testron
* @summary Event Listener for clicks Answer. This must be a function with two parameters: Answer object and Marker object */
var clickAnswerListener = null;
/** @private
* @memberof testron
* @summary Event to Listener from next Question. The listener passed must be a function with two parameters: Question object and Marker object */
var nextQuestionListener = null;
/** @private
* @memberof testron
* @summary Event to Listener from update Marker. The listener passed must be a function with one parameter: Marker object */
var updateMarkerListener = null;
/** @private
* @memberof testron
* @summary Event to Listener from finished Test. The listener passed must be a function with one parameter: Test object */
var finishedListener = null;
//INNER PRIVATES FUNCTIONS
/** @private
* @summary Getter method
* @return {Array:testrons} */
function getTests() {
return tests;
}
function addTest(test) {
tests.push(test);
}
function removeTestIndex(index) {
if ((index < 0) || (index > tests.length)) { return tests; }
if (index === 0) {
if (tests.length === 1) { return (tests = []); }
return (tests = tests.slice(1));
}
var pre = tests.slice(0, index);
return (tests = pre.concat(tests.slice(index + 1)));
}
function removeTest(test) {
for (var i = 0; i < tests.length; i++) {
if (tests[i].id === test.id) { return t.removeTestIndex(i); }
}
return tests;
}
function resetTests() {
tests = [];
}
/** Get Randomized Alphanumeric Id. It is based on "alpha" static variable */
function generateId() {
var temp = alpha[parseInt(Math.random() * alpha.length, 10)].toLowerCase() +
alpha[parseInt(Math.random() * alpha.length, 10)].toUpperCase();
return temp + (Math.random() * 1000) + alpha[parseInt(Math.random() * alpha.length, 10)].toUpperCase();
}
/** @memberof module:utils
* @summary Set the accuracy of number depending on parameter.
* @description This method processes the number as strings, and returns a number
* [es-ES]
* <p>Retorna un número 'n' flotante redondeado al número de decimales indicado en 'precision'.
* Método para formatear números en coma flotante hasta X decimales (puede contener menos)</p>
* @param floatNumber {number} The number to which adjust its precision
* @param precision {number} The precision for show digits
* @returns {number}
* @see #toDecimals(number, number) */
function toPrecision(floatNumber, precision) {
//return parseFloat((Math.round(floatNumber * Math.pow(10, decimals)) / Math.pow(10, decimals)) +"" );
return Math.round(floatNumber * Math.pow(10, precision)) / Math.pow(10, precision);
}
/** @summary Set the accuracy of number depending on parameter.
* @description This method processes the number as strings, and returns a string
* <p>[es-ES]
* Retorna un número 'n' flotante redondeado al número de decimales indicado en 'precision'.
* Método para formatear números en coma flotante hasta X decimales.
* Retorna una cadena, no un número.</p>
* @param floatNumber {number} The number to which adjust its decimals
* @param precision {number} The precision for show decimals
* @returns {string}
* @see #toPrecision(number, number) */
function toDecimals(floatNumber, decimals) {
var n = parseFloat(toPrecision(floatNumber, decimals) + "");
n = (n + "").split(".");
if (n.length > 1) {
n[1] = toDigits(n[1].split("").reverse().join(""), decimals).split("").reverse().join("");
}
return n.join(".");
}
/** @summary Fix to max digits
* @description It returns a number which is fixed to max number of digits, adding leading zeros.
* This don't truncate the number, if it is too long don't anything.
* @param num {number} The number to treat
* @param digits {number} The maximum total digits for the number
* @param sign {boolean} If you want to count the sign as a digit
* @returns {string}
* @see #digits(number, number) */
function toDigits(num, digits, sign) {
if (isNaN(num) || (digits < 2)) { return num; }
var abs = ((num < 0) ? (num + "").substr(1) : num) + "";
var signo = (num < 0) ? "-" : "";
var partes = abs.split(".");
var discount = (sign && (num < 0)) ? 1 : 0;
digits -= ((partes.length > 1) ? (partes[1].length + discount) : 0);
var d = digits;
do {
if (partes[0].length < digits) {
partes[0] = "0" + partes[0];
}
d--;
} while (d > 0);
return (signo + partes.join("."));
}
/** @summary Fix to max digits
* @description It returns a number which is fixed to max number of digits, adding leading zeros.
* This don't truncate the number, if it is too long don't anything.
* @param num {number} The number to treat
* @param digits {number} The maximum total digits for the number
* @returns {string}
* @see #digits2(number, number)*/
function toDigits2(num, digits) {
var n = parseFloat(num + "") + "";
if (isNaN(n)) { return num; }
var sign = (num >= 0) ? "" : "-";
//alert(digits - n.length);
//for (var i = 0; i < (digits - n.length); i++) {
/*while (n.length < digits) {
n = "0" + n;
}*/
n = sign + stringFill3("0", (digits - n.length)) + Math.abs(n);
return n;
}
/** Una MUY MUY BUENA (LA MEJOR) manera de sintetizar una supuesta función '.repeat(...)' */
function stringFill3(x, n) {
if ((typeof(x) === "undefined") || n < 1) { return x; }
var s = '';
for (;;) {
if (n & 1) { s += x; }
n >>= 1;
if (n) { x += x; } else { break; }
}
/*do {
if (n & 1) { s += x; }
n >>= 1;
x += x;
} while (n);*/
return s;
}
/**
* @summary SELF-SUFFICIENT and complete Method. If the containers are contributed, it generates even the UI
* @description Create Test from 'testrons' parameter.
* The parameter can be a Javascript object or a JSON string object with follow structure:
* <pre>
* {
* testrons: [
* {
* conf: {...},
* questions: [
* {
* conf: {..., txt: "Question 1 ??"},
* answers: [
* {name: "A", txt: "Answer 1"}, {...}, ...
* ]
* },
* { ... }, //OTHER QUESTION
* ...
* ]
* },
* { ... } //OTHER TEST
* ]
* }
* </pre>
*
* The second parameter is Optional, it represents any extra configuration variable, by example
* the containers for UI at the way: <code>{container: "idElement1", markerContainer: "idElement2"}</code>.
* @returns {Array} An Testrons objects array
*/
function parseTestrons(testrons, conf) {
//var tests = [];
//tests = [];
conf = conf || {};
if (testrons && testrons.replace) { //Comprobar si es una cadena
testrons = JSON.parse(testrons);
}
var str = ["Guerratron Analizing with testron.parseTestrons() ... "];
if (!testrons) {
str.push("ERROR: 'testrons' object not exists!");
return ("ERROR::\n" + str.join("\n"));
}
if (testrons.testrons) { testrons = testrons.testrons; }
if (!testrons || (testrons.length < 1)) {
str.push("ERROR: 'testrons' array not exists, or is empty!");
return ("ERROR::\n" + str.join("\n"));
}
t.resetTests();
var confLength = 0;
for(var p in conf){ if(conf.hasOwnProperty(p)) { confLength++; } }
for (var i = 0; i < testrons.length; i++) {
var test = testrons[i];
if (!test || !test.conf) {
str.push("WARNING: 'testrons.test[" + i + "].conf' object not exists!");
if (!confLength) { continue; }
}
test.conf = merge(test.conf, conf);
test.conf.container = filterContainer(test.conf.container, test.conf.remove);
test.conf.markerContainer = filterContainer(test.conf.markerContainer, test.conf.markerRemove);
test = new t.Test({testron: test});
//tests.push(new t.Test({testron: test}));
//t.addTest(test);
test.update();
}
//return tests;
return t.getTests();
//return str.join("\n");
}
function filterContainer(container, remove) {
if (container) {
if (!container.getAttribute && container.replace) { // string
container = document.getElementById(container);
}
if (container) {
if (remove) { container.innerHTML = ""; }
} else {
container = null;
}
} else {
container = null;
}
return container;
}
/** @summary Base64 icons to attach the 'src' atributes images, the 'href' attribute in 'a' labels,
* or for use in CSS in the 'background' as 'url (...)'
* @returns {Object} */
function icos() {
return {
/*"PREF": prefImg64GIF, /*Prefijo GIF monocromo. No utilizar externamente */
/** Image representing the version number of these icons. (1) */
VER: "data:image/gif;base64,R0lGODlhCAAIAIABAAAAAP///yH5BAEKAAEALAAAAAAIAAgAAAINjGEZgIqtVjxTOsZqKgA7",
GUERRATRON: "data:image/gif;base64,R0lGODlhEAAQAOMMABUQEL4aF38yEmZXVCRmmU9mHf9ERVqrLM2Ue/uUMpSwwvrKgP///////////////yH5BAEKAA8ALAAAAAAQABAAAARY8J1HZb31jIzx+SDQXUg4XuCnnFT6JfCydAdBHIAgxAhmEwpCIAAbLESV34+YAMyQD+VAMNT1oEGbYhAwdFfQKEABEBkezjD6QaYI2OuLaPXYPHqsvP4SAQA7",
LOGO: "data:image/gif;base64,R0lGODlhEAAQAKECAAcJBOIeFP///////yH5BAEKAAIALAAAAAAQABAAAAI0lC8RyAeqQHMPAtmuVEIz/11HNHUctEwoWrJcmaQiPIPZ3OEUNqbI24toWMGApgI0JJW+AgA7",
//
"ADJUST": prefImg64GIF + "njI9pwKDtAlNoyktd3G/1E1EeJoJhuZwlh54q9rKaOEtvtTWPjioFADs=",
"ADJUST1": prefImg64GIF + "ljI9pwLodoItgxVkr0nvew2GWB4akdI5eaHaJmYkaqkBM++BkAQA7",
"ADJUST2": prefImg64GIF + "rjI9pwIztQlQLLGmTy7a9fWFclH0QZZaoqFLQdIxy2MEehylgvpJn/FspCgA7",
"ADJUST3": prefImg64GIF + "pjI9pwIztQlMrTnrBUlruD3ZJJ1JHZD5lSmLn83oyLF+chY64LUGpWQAAOw==",
"AXES": prefImg64GIF + "mjANwy5ja1nvRJIcjvKge7n1QaI2lZp4MKCnTC8cyy4prmrU3GhYAOw==",
"AXES2": prefImg64GIF + "sjAEWuKrITnMQPrcSi4merl1dhnnPiIVfyZIpupbR2W7Vjef6OL8UHKP5DAUAOw==",
"BROOM": prefImg64GIF + "gjI+pywzQ0IMxzGpfnVRyrnxfKGpL2SVoJLKje2GmUgAAOw==",
"CLOSE": prefImg64GIF + "njI9pwLodGEQSPEnrqTbqzGFKOI2cs50o2SVs6YlqXKof2IlmiyoFADs=",
"COLOR": prefImg64GIF + "gjI+pywcPmoGw1XBTxlE9iXzg1I2b6ImbRpUMNcayXAAAOw==",
"COG": prefImg64GIF + "mjA8Jx63bQkLPzIez3ptlOzkhCHmjdJJmSUFfy8UyhpJjakdR1RQAOw==",
"DECIMALS": prefImg64GIF + "pjI+pqwDsjotpPkoNzuFJxHkaKHYgeX5SyaLquHqhfE0aneUix/T+UQAAOw==",
"DOWN": prefImg64GIF + "kjA8Jx63b4otSUWcvyhjOYD3LJJZmeIpbCq0mdWqlZnw02DIFADs=",
"DOWN_IMAGE": prefImg64GIF + "jhI+pyxGsXHpw0gqcjFRv5IWSeInRcTWoFaLkx5nx675eJhYAOw==",
"DOWNLOAD": prefImg64GIF + "eDI6Zpt0Bo4Rr2mql27y/6AGaN24Z41xduaKh+YoFADs=",
"DRAW": prefImg64GIF + "jjI+pCrDc3DMyThva03y7L0FZFiadtkTgRaEeOZHYVcb2vRQAOw==",
"EMPTY": prefImg64GIF, /*Igual al Prefijo GIF monocromo. */
"ERASER": prefImg64GIF + "mjH+gyKYPWoJPUsrunRryHgUhuFRi55jaN3psGyKbKJ1ubZt4vhUAOw==",
"FRACTION": prefImg64GIF + "njI+JkMDq2lthVtsWpiaqM3mQw4ifVmXoBWJiZLLaZsJWHeYceJ4FADs=",
"GRID" : prefImg64GIF + "ljGGBl72anos0gouzrpA134UTKGnmRnJjKHFPecauOoPr+6V2AQA7",
"HELP": prefImg64GIF + "jjAOpcI0L3TrRTKlscnQ/3jFgKIJQyVWjumYj+cbvFdPyWwAAOw==",
"IMAGE": prefImg64GIF + "ghI+pyxDR0HvRTVhnwtXydimaNxoXdGJoepJs28Xy3BQAOw==",
"INFO": prefImg64GIF + "mjI+pywYPWoDuLTtjVnq7hHHdhITkMZppF5qn96JjdWFUA83SzhcAOw==",
"INFO2": prefImg64GIF + "mjA2px6G/GDyPTlTd1Qv6n3FgYo2kOJamWoUAip1xJM6yvTVeUwAAOw==",
"INFO3": prefImg64GIF + "ijI+pBg2LHoPHSYqVvfDy5WEN8yWhWI1dFZRsSqlq7GR2AQA7",
"INVERT_X": prefImg64GIF + "njI+piwDnWIiGzteetVBjnn3OOEIViZ4iGrXi4nbJhl2ZLOX6zgcFADs=",
"INVERT_Y": prefImg64GIF + "rjI+pCX1uYGjg1Xin1NxmvlHMpn1YhF1q5oitWbknQtXdKN74spj8D0QUAAA7",
"LAYERS": prefImg64GIF + "mjI+ZwO2MngTRmXcD1lZzukDeJo1dSU2owqZqk70wx7YyeN31fhQAOw==",
"LOCK_LOCKED": prefImg64GIF + "ojA8Jx6zaXDIwhomWxVRHfnnZQ4plKZ1V94Sri5Fp3I4Taquznp9GAQA7",
"LOCK_UNLOCKED": prefImg64GIF + "njA8Jx6zaXDIwhomWjXhL/oTdJYZSWVEPuWaV6WKwus4sqtH4uQcFADs=",
"MAGIC": prefImg64GIF + "mRB6Gitn7ImsG1GTPdW9CT0Uf422ZqC1nmpIlaoGwLMaoROP3jhQAOw==",
"MINUS": prefImg64GIF + "TjI+py+0Po3Sg2quuBrP7D4ZOAQA7",
"MOUSE_DROP": prefImg64GIF + "ljI+py+DOwptQvXPT3cB031XaB5UUF36ImEmV6p5RLM72jedMAQA7",
"MOUSE_POINT": prefImg64GIF + "ojIFoy+nKDgwpUlVrAzfzfVlR1jnb93yL+Yhh6kplrJrxqt2z3vFSAQA7",
"MOVE": prefImg64GIF + "ljB8AyKwN1YNHSoomXUbz+nmcRZbmOYmgs3ZY8mHlC2uvfcdHAQA7",
"NEXT": prefImg64GIF + "ljI+pq9ALIoquyUPhNdlWvk1hl5CBeY5h+rGl2rrYaqLcw+RIAQA7",
"PAINT": prefImg64GIF + "kjAOpe7cPGpwRIVpNxNm2/1HgqJFjZzKmpqxs4l6wq6512jYFADs=",
"PALETTE": prefImg64GIF + "mjI+ZwM0anASrmXnkpfH17mzURJYYaHrpSoqt5aIyGKOhmGnQbhQAOw==",
"PENCIL": prefImg64GIF + "pjAGmq3jJFgxoMuiobXrmizWbxj3R542oeKbJSFJSRFpwXNLoLVY8WAAAOw==",
"PLUS": prefImg64GIF + "ajI+py+0AHILy0Boug7zH5HnPV2mSOWLqihUAOw==",
"POLYGON": prefImg64GIF + "qjA8Jx73xmjNRUpXsZVl2jmHa01XcBo3h50Us6JiQLM5vipPeVbalcigAADs=",
"POLYGON2": prefImg64GIF + "mjA8Jx63b4ovuQZpYRrPaLX2XZ1HBBGJXqo2n+4Zb186KLOIGXQAAOw==",
"POLYGON3": prefImg64GIF + "rjA8Jx73xmjNRIiUxfIli1W3fAjJlaXloqmlmeGWVJ9cbfYF6y/WcChN1CgA7",
"PROPORTION_X": prefImg64GIF + "rjI+pywafYJSHogcqzgFi3W3c94WWeHGparJoK5HemJXmYtsaGyp631AUAAA7",
"PROPORTION_Y": prefImg64GIF + "pjI+pAbDbHIqqpSgZswZLzm2bA1rUmKHpebBtOlaweM3hKz+5Ps38UQAAOw==",
"REFRESH": prefImg64GIF + "kjI+pywEPG5xgPXVRbqdyQzGesyUROZpbqk0fqbJlR80x+yYFADs=",
"REFRESH2": prefImg64GIF + "oRB6gi2e5okKyMihpxLNx/njWJnbeiabNtY5my5DwlGmkW4N16ORJAQA7",
"ROUNDED": prefImg64GIF + "ojB8AyKe/moFQPnSxanlG/m2SR45JJI5iaoUfw3YOe77yTL0wZc5SAQA7",
"RULER": prefImg64GIF + "gjAOpeY2rnITSwFgvy7o2yl2eBoydZy2mGILTiboO2RQAOw==",
"RULER_WIDTH": prefImg64GIF + "qDIIIl2usoomwJVvn1TWz5Shgk23mNKaOerBPu8YwSNf2jef6ztfnHygAADs=",
"RULER_HEIGHT": prefImg64GIF + "rDI4Xa5zpQESsyvUoVibfumXRxDlY+ICX95Usin4gCXWtp8ZaR8+nnbItCgA7",
"SAVE": prefImg64GIF + "aDI6Zpt0Bo4Rr2mql27z7T4GRGHrlh6YoAxYAOw==",
"SELECT": prefImg64GIF + "pjI9pwNEOYHRKzVkPXo9tCiLXQiZjhqZWKZZnG3LXo6ngV81Rt9O2UQAAOw==",
"SHAPE": prefImg64GIF + "mjI8Hm20LnIIM0aga1HrbhD3ZB0aY1zFoGqzWGk5SWEmt6+B5nRQAOw==",
"SIZE": prefImg64GIF + "ljI+pywrfzpsRGutAshhfPWmBuHEg+aFnaX6SGCKhiqbRjedJAQA7",
"STAR": prefImg64GIF + "ejI+pywitHjygTlPlypzHnjGgtnlQd1qhmJDXCzcFADs=",
"STAR2": prefImg64GIF + "pjI+pBrDa2kPRzSCxTdV29h1ddIHalWmbmJrc6lLw+UJORctjC66kUQAAOw==",
"TEXT": prefImg64GIF + "rjAN5y+kMnYJLAlrvxE/Jdhnakx3WhoSiZbLTdqZrm4Udenp4Lrdq+puBCgA7",
"TOROTRON": prefImg64GIF + "pjI9pgKwHonxtUijdDVnTiGXJ1FkLx5Weo4Jhq4ipea7piJ55s7MbVQAAOw==",
"TRASH": prefImg64GIF + "rjA2pxwicWlC0nicbrnyhfEHgg33ihIYnuZqsun1lGqNzHd+ufI5aFzEUAAA7",
"TRASH2": prefImg64GIF + "qjA2pxwicWlC0nicbrnzlf3UUBE5k9mCItqrmu70r3J6x6d5xXrqTqCoAADs=",
"TRASH3": prefImg64GIF + "pjA2pxwicWlC0norXzFh6yVFQGF3cNopR6Tyr+5gwym7zm9KzHDbkUQAAOw==",
"UNDO": prefImg64GIF + "lRB6gi6e54nOyTothbFIzjn1IKHrNOZYoNa1sC4EiOdMn6VZnAQA7",
"UNDO2": prefImg64GIF + "uRB6gi6f5EHOwtWjuyi9TySWX1UUS6WzbKFpu6KJtnFag9kEq5pmklwJOXipLAQA7",
"USER": prefImg64GIF + "gjI+pAQ2LnINGNsostm9PDIYQdynkd5xSpJZV24lyUgAAOw==",
"VECTOR": prefImg64GIF + "jhBGpee3/1oJIshOqnWzLqnlZJ5KhB5amOrYl0qCQTNUODRQAOw==",
"VECTOR2": prefImg64GIF + "ohBGpecYNH1sLhUifs7nO7l1gyI1WCZ6joq7iuyJNOsPUiUOH5nRNAQA7",
"WEB": prefImg64GIF + "qjI+pwK3WokNyMlrpyekG8HkLFIKPiUrat0WrWF6iCXE15uL1tKjPnygAADs=",
"WRENCH": prefImg64GIF + "sjB+gi83a0IPRQHaA1Il331ncAlLktYlTRjlmu43t+Wr2md5g+lUq5vvsIgUAOw==",
"XY": prefImg64GIF + "ojI+py30AEAzzVTMvvDR2HFVc6G2bdIIcaJmJVnoqa9U0nX726vROAQA7",
"ZOOM": prefImg64GIF + "ojAOZx6YPmlNzNcvo0Rkjv0WheJGfWZUmN0Jg5b6n+Eh2bXdsjudjAQA7",
"ZOOM_SIZE": prefImg64GIF + "pjI95oB0AHYJUzlQVMzRfvj0hB2LjFk1nqX7iMj5qh3Ywk3laGu8WVgAAOw==",
"ZOOM_IN": prefImg64GIF + "qjAOZx6YPmltHSYrprHFzBIHMAkWa6KEfek6dK4GlddFPXLdxiHP6nigAADs=",
"ZOOM_OUT": prefImg64GIF + "qjAOZx6YPmltHScpsjRLx6T1eFkHlN3YY2KwTqrWmeGl0R7rqjYdorygAADs=",
"ZOOM_RESTORE": prefImg64GIF + "sRA6ZeK2vmlvTyBCrRYYuynDghklgOYnWiWXP1ZKZCdWOpsL6q9NzHyMBFQUAOw=="
};
}
/** EVENTS COMPATIBILIZER
* @param elem {Object:HTMLElement} is the element that you want to set the listener.
* @param enventType {string} The event name (without 'on' prefix)
* @param handler {Function|lambda} the function to run
* @returns {Object} It returns the 'util' object to allow 'chaining' */
function addEvent (elem, eventType, handler) {
if (elem.addEventListener){ //ALL (IE>8)
elem.addEventListener(eventType, handler, false);
} else if (elem.attachEvent){ //IE<=8
elem.attachEvent('on'+eventType, handler);
}else{
alert("NO PUEDEN ASOCIARSE EVENTOS AL ELEMENTO");
}
return this;
}
function editListener(el) {
var elP = el.parentNode;
if (elP.getAttribute("class").indexOf("hidden") > -1) {
elP.setAttribute("class", "visible");
elP.style.display = "table-cell";
el.style.display = "block";
el.focus();
el.select();
var a= document.getElementById("testron-download-link");
if (a) { a.style.display = "none"; }
} else {
elP.setAttribute("class", "hidden");
elP.style.display = "none";
}
}
function saveListener(el) {
if (!encodeURIComponent) { return copyClipboardListener(el); }
var elP = el.parentNode;
elP.setAttribute("class", "visible");
elP.style.display = "table-cell";
el.style.display = "block";
el.focus();
el.select();
var a= document.getElementById("testron-download-link");
if (!a) {
a = document.createElement("a");
a.setAttribute("id", "testron-download-link");
a.innerHTML = "∇ donwload ∇";
var d = new Date();
a.setAttribute("download", "testron-" + (d.getTime()) + ".json");
a.setAttribute("target", "_blank");
a.setAttribute("title", "download json");
a.style.textShadow = "1px 1px 2px #333333";
addEvent(a, "click", function onClickDownload() {
a.style.display = "none";
return false;
});
elP.appendChild(a);
}
a.setAttribute("style", "display:inline");
a.setAttribute("href", 'data:text/plain;charset=utf-8,' +
encodeURIComponent(el.innerHTML));
//el.style.display = "none";
//el.style.visibility = "hidden";
el.setAttribute("style", "display:none");
//a.style.display = "inline";
//el.toDataURL()); //base64.encode(el.innerHTML));
}
function copyClipboardListener(el) {
var elP = el.parentNode;
if (elP.getAttribute("class").indexOf("hidden") > -1) {
elP.setAttribute("class", "visible");
elP.style.display = "table-cell";
el.focus();
el.select();
//CLIPBOARD
var clip = false;
if (window && window.clipboardData && window.clipboardData.setData){// CLIPBOARD ONLY IE
window.clipboardData.setData("Text", el.innerHTML);
clip = true;
} else if(document.execCommand) { //ALL ANOTHERS
clip = document.execCommand('copy');
//window.getSelection().removeAllRanges();
}
if (clip === false) {
alert("Your browser doesn't allows copy from 'textarea'");
} else {
alert("OKAY. Copied text. \n Now you can paste it into any text file to save");
}
} else {
elP.setAttribute("class", "hidden");
elP.style.display = "none";
}
}
function resetListener(el) {
//self.root.parseTestrons(JSON.parse(areaEditSave.value));
return t.parseTestrons(JSON.parse(el.value));
}
/** @summary Method to detect the container parent element through timers */
function setTimerContainer(obj) {
var success = false, interval;
function timmerContainer(obj) {
var testronParent = null;
if (!obj) { window.clearInterval(interval); }
if (obj && obj.element && obj.element.parentNode) {
testronParent = obj.element.parentNode;
if (testronParent) {
testronParent.id = testronParent.id ? testronParent.id : ("testron-parent-" + t.getTests().length);
obj.conf.container = testronParent;
obj.container = testronParent;
}
}
/*
var li = document.createElement("li");
li.innerHTML = (interval + "-" + obj.element.parentNode + "; ");
obj.element.appendChild(li);
*/
if (testronParent) { window.clearInterval(interval); /*alert("out Interval " + interval);*/ }
return testronParent;
}
if (!obj.conf.container && !obj.container && obj.element) { //&& obj.element.parentNode) {
if (window && window.setInterval) {
try {
//interval = window.setInterval(timmerContainer, 500, obj); //WARNING IE
interval = window.setInterval(function () { timmerContainer(obj); }, 500);
success = true;
} catch (e) {
/*try {
interval = window.setInterval(function () { timmerContainer(obj); }, 500);
success = true;
} catch (e2) {
return false;
}*/
}
}
}
return success;
}
//EVENTS INNER FUNCTIONS
function onOk(listener, answer){
successAnswerListener = listener ? listener : successAnswerListener;
if(successAnswerListener && answer){
var marker = (answer.parent) ? (answer.parent.parent ? answer.parent.parent.marker : null) : null;
successAnswerListener.call(null, answer, marker);
}
}
function onFail(listener, answer){
failAnswerListener = listener ? listener : failAnswerListener;
if(failAnswerListener && answer){
var marker = (answer.parent) ? (answer.parent.parent ? answer.parent.parent.marker : null) : null;
failAnswerListener.call(null, answer, marker);
}
}
function onClick(listener, answer){
clickAnswerListener = listener ? listener : clickAnswerListener;
if(clickAnswerListener && answer){
var marker = (answer.parent) ? (answer.parent.parent ? answer.parent.parent.marker : null) : null;
clickAnswerListener.call(null, answer, marker);
}
}
function onNext(listener, question){
nextQuestionListener = listener ? listener : nextQuestionListener;
if(nextQuestionListener && question){
var marker = (question.parent) ? question.parent.marker : null;
nextQuestionListener.call(null, question, marker);
}
}
function onMarker(listener, marker){
updateMarkerListener = listener ? listener : updateMarkerListener;
if(updateMarkerListener && marker){
updateMarkerListener.call(null, marker);
}
}
function onFinished(listener, test){
finishedListener = listener ? listener : finishedListener;
if(finishedListener && test){
finishedListener.call(null, test);
}
}
function toJSON() {
var str = [];
for (var i = 0; i < t.getTests().length; i++) {
var test = t.getTests()[i];
/*if (!test.conf.container && test.element && test.element.parentNode) {
var testronParent = test.element.parentNode;
if (testronParent) {
testronParent.id = testronParent.id ? testronParent.id : ("testron-parent-" + t.getTests().length);
test.conf.container = testronParent;
test.container = testronParent;
}
}*/
str.push(test.toJSON());
}
return ('{ "testrons": [ ' + str.join(", ") + ' ] }');
}
/** @summary Utility for merge two configuration objects */
function merge(confInitial, confFinal) {
confInitial = confInitial || {};
confFinal = confFinal || {};
for (var p in confFinal) {
if (confFinal.hasOwnProperty(p)) {
confInitial[p] = confFinal[p];
//alert(p + " == " + confInitial[p] + " == " + confFinal[p]);
}
}
return confInitial;
}
//PUBLIC API
/** @memberof testron
* @summary obtains this identificator
* @type {string} */
t.id = generateId();
/** @method
* @memberof testron
* @summary Getter method
* @return {Array:tests} */
t.getTests = getTests;
/** @method
* @memberof testron
* @summary Add a Test to array */
t.addTest = addTest;
/** @method
* @memberof testron
* @summary Remove a Test from array, from its index
* @return {Array} the updated tests array */
t.removeTestIndex = removeTestIndex;
/** @method
* @memberof testron
* @summary Remove a Test from array
* @return {Array} the updated tests array */
t.removeTest = removeTest;
/** @method
* @memberof testron
* @summary Remove ALL Test from array */
t.resetTests = resetTests;
/** @memberof testron
* @summary The utils object
* @property {string} generateId obtains a unique identificator
* @property {Function} Util.toPrecision - Obtains a number which is fixed to exact precision
* @property {Function} Util.toDecimals - Obtains a number which is fixed to concrete decimals
* @property {Function} Util.toDigits - Obtains a number which is fixed to exact digits
* @property {Object} Util.icos - Is a object which contains icons on base64 format
* @property {Function} Util.addEvent - Cross-browser Method for adding events
* @property {Function} Util.editListener - Listener for shooting of change events (editArea).
* @property {Function} Util.saveListener - Listener for shooting of save events (button).
* @property {Function} Util.copyClipboardListener - Listener for shooting of copy events (clipboard).
* @property {Function} Util.resetListener - reset the listener for this object. (non-implementable currently)
* @property {Function} Util.setTimerContainer - set a timer for this container element
* @type {Object} */
t.utils = {
generateId: generateId,
toPrecision: toPrecision,
toDecimals: toDecimals,
toDigits: toDigits,
icos: icos(), //returns a literal object
addEvent: addEvent,
editListener: editListener,
saveListener: saveListener,
copyClipboardListener: copyClipboardListener,
resetListener: resetListener,
setTimerContainer: setTimerContainer
};
/** @method
* @memberof testron
* @summary Parse a JSON string which represents the complete testron suite
* @return {Array} the updated tests array */
t.parseTestrons = parseTestrons;
/** @method
* @memberof testron
* @summary Filter the configuration object to sanitize
* @return {Object} the sanitized configuration object */
t.filterContainer = filterContainer;
/** @method
* @memberof testron
* @summary Obtain a JSON string which represents the complete testron suite
* @return {string} JSON string */
t.toJSON = toJSON;
/** @method
* @memberof testron
* @summary Method to merge the properties from two objects (configuration)
* @return {Object} the merged object */
t.merge = merge;
//EVENTS HANDLERS
/** @event
* @memberof testron
* @summary Event to Listener from <b>successed Answer</b>.
* @description The listener passed must be a function with two parameters: Answer object and Marker object */
t.onOk = onOk;
/** @event
* @memberof testron
* @summary Event to Listener from <b>failed Answer</b>.
* @description The listener passed must be a function with two parameters: Answer object and Marker object */
t.onFail = onFail;
/** @event
* @memberof testron
* @summary Event to Listener from <b>clicks Answer</b>.
* @description The listener passed must be a function with two parameters: Answer object and Marker object */
t.onClick = onClick;
/** @event
* @memberof testron
* @summary Event to Listener from <b>next Question</b>.
* @description The listener passed must be a function with two parameters: Question object and Marker object */
t.onNext = onNext;
/** @event
* @memberof testron
* @summary Event to Listener from <b>update Marker</b>.
* @description The listener passed must be a function with one parameter: Marker object */
t.onMarker = onMarker;
/** @event
* @memberof testron
* @summary Event to Listener from <b>finished Test</b>.
* @description The listener passed must be a function with one parameter: Test object */
t.onFinished = onFinished;
})
(testron || (testron = {}));
//END MODULE TESTRON & UTILS
//MODULE TEST
/** @lends testron
* @module Test
* @summary Module for Test objects. Contain the Class and constructor. */
(function (t) {
"use strict";
//INNER PRIVATES VARS
/** Static counter */
var counter = 0;
/** @class A class to build Questions Tests Configurable and dynamically
* @constructor
* @memberof module:testron
* @summary Quiz Tests Class
* @description Constructor for creating "Test" objects. It supports a parameter with a configuration
* object with certain properties or attributes.
* @param conf {Object} Configuration object (see {@link #conf conf})
* @see #conf */
function Test(conf) {
/** @summary Exclusive id (+ or -)
* @type {string} */
this.id;
if (t.utils && t.utils.generateId) {
this.id = t.utils.generateId(); //Exclusive id (+ or -)
}
/** @summary Number of this Test.
* @description This is not equal to this.id (from 0 to +Infinity)
* @type {number} */
this.num = counter++; //from 0 to infinity
/** @summary "testron" Parent object
* @type {namespace:testron} */
this.root = t;
/** @summary Question objects array
* @type {Array:Question} */
this.questions = [];
/** @summary own DOM element
* @type {Object:HTMLElement} */
this.element = null;
/** @summary is valid this Test ?
* @type {boolean} */
this.valid = true;
/** @summary Marker for this Test
* @type {Object:Marker} */
this.marker = null;
/** @summary DOM Element for own Marker
* @type {Object:HTMLElement} */
this.markerElement = null;
/** @summary Configuration Options
* @description a Javascript object with follow structure:
* <pre>
* {
* className: "testron-80",
* container: "divUI",
* markerContainer: "divMarker",
* remove: true,
* markerRemove: true,
* digits: 4,
* precision: 2,
* nQ: 1,
* nA: 4,
* stopOnSuccess: false,
* charset: "utf-8"
* }
* </pre>
* @type {Object} */
this.conf = this.filterConf(conf || {});
if (conf.testron) { //objeto JSON
//var str =
this.parseTestron(conf);
//alert(str);
}
/** @summary clone from {@link conf.className}
* @description if you wish a specific class name for container
* @type {string} */
this.className = this.conf.className;
/** @summary clone from {@link conf.container}
* @description the DOM element which embed the UI
* @type {Object:HTMLElement} */
this.container = this.conf.container;
/** @summary clone from {@link conf.markerContainer}
* @description similarly to 'container' for Marker (It advised in fixed position)
* @type {Object:HTMLElement} */
this.markerContainer = this.conf.markerContainer;
/** @summary clone from {@link conf.remove}
* @description cleans the container before insert the content
* @type {boolean} */
this.remove = this.conf.remove;
/** @summary clone from {@link conf.markerRemove}
* @description similarly to 'remove' for Marker
* @type {boolean} */
this.markerRemove = this.conf.markerRemove;
/** @summary is finished this Test ?
* @type {boolean} */
this.finished = false;
this.root.addTest(this); //var valueJSON = this.root.toJSON().toString();
//this.update_nAnQ();
//this.toHtml();
this.update();
}
/** @method
* @summary Get the self own-counter */
Test.prototype.getCounter = function () { return counter; };
/** @method
* @summary Filter the Configuration Options
* @description The Configuration Options will set to default if not exists
* @param conf {Object} Configuration object
* @return {Object} The sanitized configuration options
* @see #conf */
Test.prototype.filterConf = function (conf) {
var container = this.root.filterContainer(conf.container, conf.remove);
var markerContainer = this.root.filterContainer(conf.markerContainer, conf.markerRemove);
return {
/** Class name for CSS styles */
className: "testron" + (conf.className ? (" " + conf.className) : ""),
/* * Parse force */
//parse: conf.parse || false,
container: container, //conf.container || null,
markerContainer: markerContainer, //conf.markerContainer || null,
remove: conf.remove,
markerRemove: conf.markerRemove,
/** Questions Number */
nQ: conf.nQ || 1,
/** Answers Number */
nA: conf.nA || 4,
digits: conf.digits || 2,
precision: conf.precision || 2,
stopOnSuccess: conf.stopOnSuccess || false,
charset: conf.charset || "utf-8"
};
};
/** @summary Remove the invalid answers */
Test.prototype.sanitizeQuestions = function () {
var questions2 = [];
for (var i = 0; i < this.questions.length; i++) {
if (this.questions[i].valid) { questions2.push(this.questions[i]); }
}
this.questions = questions2;
this.update_nAnQ();
};
/** @summary Updates the answers and questions number and its marker */
Test.prototype.update_nAnQ = function () {
//UPDATE nA AND nQ
var nA = 0;
for (var i = 0; i < this.questions.length; i++) {
nA += this.questions[i].conf.nA;
}
this.conf.nA = nA;
this.conf.nQ = this.questions.length;
};
/** @summary Updates this Test.
* @description Also updates the answers and questions number and its marker */
Test.prototype.update = function () {
//this.update_nAnQ();
this.toHtml();
if (!this.marker) { this.marker = this.createMarker(this.conf); }
if (this.marker && this.marker.update) { this.marker.update(); }
//if (this.marker && this.marker.update) { this.marker.update(); }
/*if (!this.conf.container && this.element && this.element.parentNode) {
var testronParent = this.element.parentNode;
if (testronParent) {
testronParent.id = testronParent.id ? testronParent.id : ("testron-parent-" + this.id);
this.conf.container = testronParent;
this.container = testronParent;
}
}*/
};
/** @summary Checks if this Test is finished.
* @return {boolean} */
Test.prototype.checkFinished = function () {
var finished = true;
for (var i = 0; i < this.questions.length; i++) {
if (!this.questions[i].ok) {
finished = false;
break;
};
}
return (this.finished = finished);
}
/** @method
* @summary Adding a Question object to this Test
* @param question {Object:testron.Test.Question} "Question" object
* @param refresh {boolean} update this Test ?
* @returns {Object:Question} Return the same question input parameter */
Test.prototype.addQuestion = function (question, refresh) {
if (!question || !question.conf) { return question; }
var finded = false, i = 0, q = null; //, nA = 0;
this.sanitizeQuestions();
for (i = 0; i < this.questions.length; i++) {
q = this.questions[i];
if (q.conf.num === question.conf.num) { //Already exists
q = question;
finded = true;
}
}
if (! finded) {
question.position = this.questions.length;
this.questions.push(question);
}
//this.update_nAnQ();
if (refresh) { this.update(); }
return question;
};
/** @method
* @summary Create and adding a Question object to this Test
* @param conf {Object} Configuration object (for Question Objects)
* @returns {Object:Question} Return the new Question created */
Test.prototype.createQuestion = function (conf, refresh) {
return this.addQuestion(new this.Question(this, conf), refresh);
};
/** @method
* @summary Create a Marker object to this Test
* @param conf {Object} Configuration object (for Marker Objects)
* @returns {Object:Marker} Return the new Marker created */
Test.prototype.createMarker = function (conf) {
conf = conf || this.conf;
if (!conf.markerContainer) { return this.marker; }
var markerContainer = conf.markerContainer;
if (!markerContainer.getAttribute && markerContainer.replace) { // string
markerContainer = document.getElementById(markerContainer);
}
if (!markerContainer) { return this.marker; }
this.markerElement = markerContainer.getElementsByClassName("testron-marker")[0];
//this.markerContainer = this.markerElement;
if (!this.markerElement) {
this.marker = new this.Marker(this, conf);
}
this.marker.update();
//if (this.marker && this.marker.update) { this.marker.update(); }
return this.marker;
};
/** @summary Create Test from 'test' parameter.
* @description The parameter can be a Javascript object or a JSON string object with follow structure:
* <pre>
* testron: {
* conf: {...},
* questions: [...] // an Array of questions. (see Test#parseQuestions(...))
* }
* </pre>
* @param test {Object|string} a Javascript object or a JSON string object
* @return {string} a DEBUG string with info
*/
Test.prototype.parseTestron = function (test) {
var str = ["Analizing Test.parseTestron() ... "];
if (test && test.replace) { //Comprobar si es una cadena JSON
test = JSON.parse(test);
str.push("string JSON detected !");
}
if (!test) {
str.push("ERROR: 'test' object not exists!");
return str.join("\n");
}
if (test.testron) { test = test.testron; }
/*if (!test || (test.length < 1)) {
str.push("ERROR: 'testrons' array not exists, or is empty!");
return str.join("\n");
}*/
if (!test.conf) { str.push("WARNING: 'test.conf' object not exists!"); }
this.conf = this.filterConf(test.conf);
//QUESTIONS
var questions = test.questions;
if (!questions) {
str.push("ERROR/WARNING: 'test.questions' array not exists!");
return str.join("\n");
}
for (var i = 0; i < questions.length; i++) {
var q = questions[i];
if (!q) {
str.push("WARNING: 'question[" + i + "]' object not exists!");
continue;
}
this.createQuestion({question: q});
}
str.push("... test FINISH");
return str.join("\n");
};
/** @method
* @summary Create the HTML-UI for this test and return it
* as HTMLElement (HTMLUlElement)
* @return {HTMLElement:HTMLUlElement} */
Test.prototype.toHtml = function () {
/** @type {HTMLElement} */
var ul = document.createElement("ul");
if (this.element && this.element.innerHTML) {
this.element.innerHTML = "";
ul = this.element;
}
/*alert("UNO-"+this.className.substr(0,8)+"-") +" || -"+ (this.className.substr(8).trim()+"-");
alert("DOS::-"+this.conf.className.substr(0,8)+"-") +" || -"+ (this.conf.className.substr(8).trim()+"-");
if ((this.className.substr(0,8) === "test ") && (this.className.substr(8).trim().length > 0)) {
//this.className = this.className.substr(8);
}*/
ul.setAttribute("class", this.className);
if (this.valid) {
var caption = document.createElement("caption");
caption.innerHTML = "Testron";
ul.appendChild(caption);
this.sanitizeQuestions();
for (var i = 0; i < this.questions.length; i++) {
var li = document.createElement("li");
li.setAttribute("class", "li-question");
li.appendChild(this.questions[i].toHtml());
ul.appendChild(li);
}
//if (this.element) { this.element.innerHTML = ""; }
this.element = ul;
if (this.container) {
this.container.appendChild(this.element);
} else {
t.utils.setTimerContainer(this); //TIMER TO DETECT CONTAINER
}
} else {
ul.style.background = "#333333";
}
return ul;
};
/** @summary Parse this element to JSON
* @return {string} a JSON string which represents to this Test object */
Test.prototype.toJSON = function () {
var markerContainer = (this.marker && this.marker.container) ? this.marker.container : "";
if (this.markerContainer) {
if (this.markerContainer.id) { //DOM element
markerContainer = this.markerContainer.id;
} else if (this.markerContainer.replace) { //string
markerContainer = this.markerContainer;
}
}
/*alert("UNO-"+this.className.substr(0,8)+"-") +" || -"+ (this.className.substr(8).trim()+"-");
alert("DOS::-"+this.conf.className.substr(0,8)+"-") +" || -"+ (this.conf.className.substr(8).trim()+"-");
if ((this.className.substr(0,8) === "testron ") && (this.className.substr(8).trim().length > 0)) {
//this.className = this.className.substr(8);
var partes = this.className.split("testron");
this.className = "testron" + partes[partes.length - 1];
this.conf.className = this.className;
}*/
var partes = this.className.split("testron");
this.className = "testron" + partes[partes.length - 1];
this.conf.className = this.className;
var conf = {
className: this.conf.className,
container: (this.container ? this.container.id : ""),
markerContainer: markerContainer,
remove: this.remove || this.conf.remove || true,
markerRemove: this.markerRemove || this.conf.markerRemove || true,
num: this.conf.num,
//parse: true, //this.conf.parse,
nQ: this.conf.nQ,
nA: this.conf.nA,
digits: this.conf.digits,
precision: this.conf.precision,
stopOnSuccess: this.conf.stopOnSuccess,
charset: this.conf.charset
};
var str = [];
for (var i = 0; i < this.questions.length; i++) {
var question = this.questions[i];
str.push(question.toJSON());
}
return ('{"conf": ' + JSON.stringify(conf) + ', "questions": [ ' + str.join(", ") + ' ] }');
};
//EMIT EVENTS FUNCTIONS
/** @emitter
* @summary Emit the Next event for pass to forward Question */
Test.prototype.emitNext = function (question) {
var msg = "Test:: click in " + question.intents + " " +
"intents for Question (" + question.conf.num + ") ";
if (question.position < (this.questions.length - 1)) {
msg += "- to Next Question at :: " + (question.position + 2);
} else {
msg += "- This is the last Question ...";
}
//alert(msg);
this.root.onNext(null, question);
};
/** @emitter
* @summary Emit the Marker event for update the Marker */
Test.prototype.emitMarker = function (question) {
this.update_nAnQ();
//this.marker = new this.Marker(this, this.conf);
if (!this.marker) { this.marker = this.createMarker(this.conf); }
if (this.marker && this.marker.update) { this.marker.update(question); }
//if (this.marker && this.marker.update) { this.marker.update(question); }
this.root.onMarker(null, this.marker);
if (this.checkFinished()) { this.root.onFinished(null, this); }
};
t.Test = Test;
})
(testron || (testron = {}));
//END MODULE TEST
//MODULE MARKER
/** @lends testron.Test
* @module Marker */
(function (t) {
"use strict";
//INNER PRIVATES VARS
/** Static counter */
var counter = 0;
/** STATIC POINTS FOR CALCULATIONS */
var POINTS_TRUE = 1;
var POINTS_FALSE = -0.5;
var POINTS_NULL = -0.1;
var POINTS_BLANK = 0;
var POINTS_TOP = 10; //Max Puntuation (0-10)
//INNER PRIVATES FUNCTIONS
/** @class A class to build Markers for Tests, configurable and dynamically
* @constructor
* @memberof module:testron#Test
* @summary Marker to Tests
* @description Constructor for creating "Marker" objects. It supports a parameter with a
* configuration object with certain properties or attributes.
* @param parent {Object} "Test" object
* @param conf {Object} Configuration object (see {@link #conf conf})
* @see #conf */
function Marker(parent, conf) {
/** @summary "testron" Ancestor object
* @type {namespace:testron} */
this.root = parent.root;
/** @summary clone of {@link testron.utils}"
* @type {Object} */
this.utils = (this.root && this.root.utils) ? this.root.utils : null;
/** @summary "Test" Parent object
* @type {Object:Test} */
this.parent = parent;
/** @summary Exclusive id (+ or -)
* @type {string} */
this.id;
if (this.utils && this.utils.generateId) {
this.id = this.utils.generateId();
} //Exclusive id (+ or -)
this.id = (parent ? (parent.id + "-") : "") + this.id;
/** @summary Number of this Marker.
* @description This is not equal to 'this.id' ( from 0 to +Infinity)
* @type {number} */
this.num = counter++; //from 0 to infinity
/** @summary Configuration Options
* @description a Javascript object with follow structure:
* <pre>
* {
* container: "divMarker",
* digits: 4,
* precision: 2,
* nQ: 1,
* nA: 4,
* charset: "utf-8"
* }
* </pre>
* @type {Object} */
this.conf = this.filterConf(conf || this.parent.conf);
/** @summary clone from {@link conf.container}
* @description this must be equal to {@link parent.markerContainer}
* @type {Object:HTMLElement} */
this.container = this.conf.container; //this.root.filterContainer(this.conf.container);
/** @summary counter for answers solution
* @description These are only properties names, don't are booleans, don't are primitive types.
* Your structure is:
* <code>{"true": 0, "false": 0, "null": 0, "blank": 0}</code>
* @type {Object}
*/
this.results = {"true": 0, "false": 0, "null": 0, "blank": 0};
this.position = 0;
/** @summary own DOM element
* @type {Object:HTMLElement} */
this.element = null;
this.elementScore = null;
this.elementTrue = null;
this.elementFalse = null;
this.elementPointsTrue = null;
this.elementPointsFalse = null;
this.elementBlank = null;
this.elementBtnEdit = null;
this.elementBtnDownload = null;
this.elementBtnReset = null;
this.elementAreaEditSave = null;
/** @summary it counts the intents
* @type {number} */
this.intents = 0;
/** @summary score compute
* @type {number} */
this.score = 0;
/** @summary what is the current Question ?
* @type {number} */
this.currentQuestion = null;
/** @summary is valid this Marker ?
* @type {boolean} */
this.valid = true;
this.toHtml();
}
/**
* @method
* @summary Get the self own-counter */
Marker.prototype.getCounter = function () { return counter; };
/**
* @method
* @summary Filter the Configuration Options
* @description The Configuration Options will are get from
* parent ("Test"), if not exists
* @param conf {Object} Configuration object
* @see #conf */
Marker.prototype.filterConf = function (conf) {
var markerContainer = this.root.filterContainer(conf.markerContainer, conf.markerRemove);
return {
container: markerContainer, //conf.markerContainer,
/** Number of this Marker. This is not equal to this.id */
digits: conf.digits || this.parent.conf.digits || 4,
precision: conf.precision || this.parent.conf.precision || 2,
/** Questions Number */
nQ: conf.nQ || this.parent.conf.nQ || 1,
/** Answers Number */
nA: conf.nA || this.parent.conf.nA || 4,
charset: conf.charset || this.parent.conf.charset || "utf-8"
};
};
/** @method
* @summary Checking the results object
* @see #results */
Marker.prototype.sanitizeResults = function () {
this.results = {"true": this.results["true"],
"false": this.results["false"],
"null": this.results["null"],
"blank": this.results.blank};
};
/**
* @method
* @summary Sum one to "true" object counter
* @returns {number} Return the current updated counter */
Marker.prototype.plusTrue = function () {
if (!this.valid) { return -1; }
this.results["true"]++;
this.intents++;
return this.results["true"];
};
/**
* @method
* @summary Sum one to "false" object counter
* @returns {number} Return the current updated counter */
Marker.prototype.plusFalse = function () {
if (!this.valid) { return -1; }
this.results["false"]++;
this.intents++;
return this.results["false"];
};
/**
* @method
* @summary Sum one to "null" object counter
* @returns {number} Return the current updated counter */
Marker.prototype.plusNull = function () {
if (!this.valid) { return -1; }
this.results["null"]++;
this.intents++;
return this.results["null"];
};
/**
* @method
* @summary Sum one to "blank" object counter
* @returns {number} Return the current updated counter */
Marker.prototype.plusBlank = function () {
if (!this.valid) { return -1; }
this.results.blank++;
this.intents++;
return this.results.blank;
};
/**
* @method
* @summary Reset full the object counter
* @see #results */
Marker.prototype.resetResults = function () {
if (this.valid) {
this.results = {"true": 0, "false": 0, "null": 0, "blank": 0};
}
};
/**
* @method
* @summary Reset the counters
* @see #intents
* @see #results */
Marker.prototype.reset = function () {
if (this.valid) {
this.intents = 0;
this.resetResults();
}
};
/**
* @method
* @summary Updates the counters
* @param {Object:Question} Object Question */
Marker.prototype.update = function (question) {
var dataQuestions = this.parent.questions;
if (this.valid && dataQuestions) {
this.reset();
for (var i = 0; i < dataQuestions.length; i++) {
var q = dataQuestions[i];
var intents = q.ok ? (q.intents - 1) : q.intents;
if (q.ok) { this.plusTrue(); }
if (q.marked) {
for (var j = 0; j < intents; j++) {
this.plusFalse();
}
} else {
this.plusBlank();
}
}
//this.conf.nA = this.parent.conf.nA;
this.conf.nQ = this.parent.conf.nQ;
this.conf.nA = (question && question.conf) ? question.conf.nA : this.parent.conf.nA;
}
this.elementScore.innerHTML = '<strong>TOTAL: </strong> <span class="marker-score">' +
this.calcScore() + "</span>";
if (this.calcScore() >= (POINTS_TOP / 2)) {
this.elementScore.getElementsByClassName("marker-score")[0].style.color = "darkGreen";
} else {
this.elementScore.getElementsByClassName("marker-score")[0].style.color = "red";
}
this.elementPointsTrue.innerHTML = "<strong>Points:</strong> (True:" +
this.filterDigits(this.scoreSuccesses() * POINTS_TOP) + ") ";
this.elementPointsFalse.innerHTML = "(False:" + this.filterDigits(this.scoreFailures() * POINTS_TOP) + ")";
this.elementTrue.innerHTML = this.results["true"];
this.elementFalse.innerHTML = this.results["false"];
this.elementBlank.innerHTML = this.results.blank;
this.elementAreaEditSave.innerHTML = this.root.toJSON();
//alert(this.intents);
};
//SCORE
/** @method
* @summary Calculates the solution for total score
* @returns {number} updated score */
Marker.prototype.calcScore = function () {
this.score = this.filterDigits(POINTS_TOP * (this.scoreSuccesses() - this.scoreFailures()));
return this.score;
};
/** @method
* @summary Calculates the successes coefficient
* @description It's based on total Questions number.
* This coefficient will apply as multiplicand to the True Answers
* @returns {number} successes coefficient */
Marker.prototype.coefficientSuccesses = function () {
return this.filterDigits(1 / this.conf.nQ);
};
/** @method
* @summary Calculates the failures coefficient
* @description It's based on total number of Questions and Answers.
* This coefficient will apply as multiplicand to the False Answers
* @returns {number} failures coefficient */
Marker.prototype.coefficientFailures = function () {
return this.filterDigits(1 / ((this.conf.nA - 1) * this.conf.nQ));
};
/** @method
* @summary Calculates the score for True Answers
* @returns {number} Successes score */
Marker.prototype.scoreSuccesses = function () {
return this.filterDigits(this.results["true"] * this.coefficientSuccesses());
};
/** @method
* @summary Calculates the score for False Answers
* @returns {number} Failures score */
Marker.prototype.scoreFailures = function () {
return this.filterDigits(this.results["false"] * this.coefficientFailures());
};
//END SCORE
/** @summary Filter a number to passed parameters.
* @description It is used to filter a number to configuration properties: 'precision' and 'digits number' */
Marker.prototype.filterDigits = function (floatNumber, precision, digits, sign) {
precision = precision || this.conf.precision;
digits = digits || this.conf.digits;
if (this.utils) {
if (this.utils.toDecimals) { floatNumber = this.utils.toDecimals(floatNumber, precision); }
if (this.utils.toDigits) { floatNumber = this.utils.toDigits(floatNumber, digits, sign); }
}
return floatNumber;
};
/** @method
* @summary Create the HTML-UI
* @description Create the HTML-UI for this Marker and return it
* as HTMLElement (HTMLUlElement)
* @return {Object:HTMLElement}*/
Marker.prototype.toHtml = function () {
var self = this;
/** @type {HTMLElement} */
var table = document.createElement("table");
if (this.element && this.element.innerHTML) {
this.element.innerHTML = "";
table = this.element;
}
table.setAttribute("class", "marker testron-marker");
table.setAttribute("id", this.id);
if (this.valid) {
var caption = document.createElement("caption");
caption.innerHTML = "Marker (" + this.num + ")";
table.appendChild(caption);
this.sanitizeResults();
var thead = document.createElement("thead");
var trh1 = document.createElement("tr");
var th1 = document.createElement("th");
th1.innerHTML = "";
trh1.appendChild(th1);
var th2 = document.createElement("th");
th2.innerHTML = "Value";
trh1.appendChild(th2);
thead.appendChild(trh1);
table.appendChild(thead);
var tfoot = document.createElement("tfoot");
var trf1 = document.createElement("tr");
var tdf1 = document.createElement("td");
tdf1.setAttribute("class", "marker-total");
tdf1.setAttribute("colspan", 2);
tdf1.innerHTML = '<strong>TOTAL: </strong> <span class="marker-score">' +
this.calcScore() + "</span>";
this.elementScore = tdf1; //SCORE
trf1.appendChild(tdf1);
tfoot.appendChild(trf1);
var trf2 = document.createElement("tr");
trf2.style.fontSize = "smaller";
var tdf2 = document.createElement("td");
tdf2.innerHTML = "<strong>Points:</strong> (Success:" +
this.filterDigits(this.scoreSuccesses() * POINTS_TOP) + ") ";
this.elementPointsTrue = tdf2; //POINTS TRUE
trf2.appendChild(tdf2);
var tdf3 = document.createElement("td");
tdf3.innerHTML = "(Fails:" + this.filterDigits(this.scoreFailures() * POINTS_TOP) + ") ";
this.elementPointsFalse = tdf3; //POINTS FALSE
trf2.appendChild(tdf3);
tfoot.appendChild(trf2);
var trf3 = document.createElement("tr");
trf3.style.fontSize = "smaller";
var tdf4 = document.createElement("td");
var i1 = document.createElement("i");
i1.setAttribute("class", "btn btn-16");
i1.innerHTML = '<image src="' + this.root.utils.icos.PENCIL +
'" title="edit/import ... modify or paste other text"/>';
this.elementBtnEdit = i1; //EDIT BUTTON
this.elementBtnEdit.disabled = !document.queryCommandSupported('copy');
this.utils.addEvent(i1, "click", function testronEditListener (){
self.utils.editListener(areaEditSave);
});
tdf4.appendChild(i1);
var i2 = document.createElement("i");
i2.setAttribute("class", "btn btn-16");
i2.innerHTML = '<image src="' + this.root.utils.icos.DOWNLOAD +
'" title="view save/download ... click on download link"/>';
this.elementBtnDownload = i2; //DOWNLOAD BUTTON
this.utils.addEvent(i2, "click", function testronSaveListener (){
self.utils.saveListener(areaEditSave);
});
tdf4.appendChild(i2);
var i3 = document.createElement("i");
i3.setAttribute("class", "btn btn-16");
i3.innerHTML = '<image src="' + this.root.utils.icos.UNDO +
'" title="reset the tests"/>';
this.elementBtnReset = i3; //RESET BUTTON
this.utils.addEvent(i3, "click", function testronResetListener (){
//self.root.parseTestrons(JSON.parse(areaEditSave.value));
var numTests = self.utils.resetListener(areaEditSave);
if(numTests.charAt && (numTests.substr(0,5) === "ERROR")) { //ERROR string
//alert(self.parent.toJSON());
areaEditSave.innerHTML = self.parent.toJSON();
numTests = self.utils.resetListener(areaEditSave);
if(numTests.charAt && (numTests.substr(0,5) === "ERROR")) { //ERROR string
alert("ERROR parsing JSON");
}
}
});
tdf4.appendChild(i3);
trf3.appendChild(tdf4);
tfoot.appendChild(trf3);
var trf4 = document.createElement("tr"); //TEXTAREA IMPORT/SAVE
trf4.style.fontSize = "larger";
var tdf6 = document.createElement("td");
tdf6.setAttribute("colspan", "2");
tdf6.setAttribute("class", "hidden");
tdf6.setAttribute("id", "testron-area-celd");
tdf6.style.textAling = "center";
tdf6.style.display = "none";
var areaEditSave = document.createElement("textarea");
areaEditSave.setAttribute("rows", "5");
areaEditSave.style.width = "98%";
areaEditSave.style.fontSize = "smaller";
var valueJSON = this.root.toJSON().toString();
areaEditSave.innerHTML = ((valueJSON > 30) ? valueJSON : self.parent.toJSON());
this.elementAreaEditSave = areaEditSave; //POINTS TRUE
this.utils.addEvent(this.elementAreaEditSave, "change", function importArea() {
self.root.parseTestrons(JSON.parse(areaEditSave.value));
});
tdf6.appendChild(areaEditSave);
trf4.appendChild(tdf6);
tfoot.appendChild(trf4);
table.appendChild(tfoot);
var tbody = document.createElement("tbody");
var tr1 = document.createElement("tr");
var td11 = document.createElement("td");
td11.innerHTML = "Success";
tr1.appendChild(td11);
var td12 = document.createElement("td");
td12.setAttribute("class", "marker-values");
td12.innerHTML = this.results["true"];
this.elementTrue = td12; //TRUE
tr1.appendChild(td12);
tbody.appendChild(tr1);
var tr2 = document.createElement("tr");
var td21 = document.createElement("td");
td21.innerHTML = "Fails";
tr2.appendChild(td21);
var td22 = document.createElement("td");
td22.setAttribute("class", "marker-values");
td22.innerHTML = this.results["false"];
this.elementFalse = td22; //FALSE
tr2.appendChild(td22);
tbody.appendChild(tr2);
var tr3 = document.createElement("tr");
var td31 = document.createElement("td");
td31.innerHTML = "Blank";
tr3.appendChild(td31);
var td32 = document.createElement("td");
td32.setAttribute("class", "marker-values");
td32.innerHTML = this.results["blank"];
this.elementBlank = td32; //BLANK
tr3.appendChild(td32);
tbody.appendChild(tr3);
table.appendChild(tbody);
//if (this.element) { this.element.parentNode.removeChild(this.element); }
this.element = table;
if (this.container) {
this.container.appendChild(this.element);
} else {
this.root.utils.setTimerContainer(this); //TIMER TO DETECT CONTAINER
}
} else {
table.style.background = "#666666";
}
return table;
};
/*//EVENTS
/** @event
* @summary Emit the Click event for comprobations in this Marker * /
Marker.prototype.emitClick = function (question) {
/*this.intents++;
//alert("Marker:: with Answer:: " + answer.name);
if (this.verify(answer.position)) {
answer.toOK();
this.toOK();
} else {
answer.toFail();
this.toFail();
}* /
this.update(question);
};
/** @event
* @summary Emit the Next event for pass to forward Marker * /
Marker.prototype.emitNext = function () {
//alert("next Marker");
this.parent.emitNext(this); //propagate to parent
};
*/
t.prototype.Marker = Marker;
})
(testron.Test || (testron.Test = function () {}));
//END MODULE MARKER
//MODULE QUESTION
/** @lends testron.Test
* @module Question */
(function (t) {
"use strict";
//INNER PRIVATES VARS
/** Static counter */
var counter = 0;
//INNER PRIVATES FUNCTIONS
/** @class A class to build Questions for Tests configurable and dynamically
* @constructor
* @memberof module:testron#Test
* @summary Questions for test
* @description Constructor for creating "Question" objects. It supports a parameter with a
* configuration object with certain properties or attributes.
* @param parent {Object} "Test" object
* @param conf {Object} Configuration object (see {@link #conf conf})
* @see #conf */
function Question(parent, conf) {
/** @summary "testron" Ancestor object {namespace}
* @type {namespaces:testron} */
this.root = parent.root; //"testron" {namespace}
/** @summary Exclusive id (+ or -)
* @type {string} */
this.id;
if (this.root.utils && this.root.utils.generateId) {
this.id = this.root.utils.generateId();
}
this.id = (parent ? (parent.id + "-") : "") + this.id;
/** @summary Number of this Question.
* @description This is not equal to 'this.id' (from 0 to +Infinity)
* @type {number} */
this.num = counter++;
/** @summary "Test" Parent object
* @type {Object:Test} */
this.parent = parent;
/** @summary Answer objects array
* @type {Array:Answer} */
this.answers = [];
/** @summary Position in 'questions' parent array
* @type {number} */
this.position = 0;
/** @summary Is this question is already succeeded ?
* @type {boolean} */
this.ok = false;
/** @summary own DOM element
* @type {Object:HTMLElement} */
this.element = null;
/** @summary It keeps track marking for this Question
* @type {boolean} */
this.marked = false;
/** @summary Counts the intents for this Question
* @type {number} */
this.intents = 0;
/** @summary is valid this Question ?
* @type {boolean} */
this.valid = true;
/** @summary Configuration Options
* @description a Javascript object with follow structure:
* <pre>
* {
* num: 2,
* nA: 4,
* solution: 0,
* stopOnSuccess : false,
* charset: "utf-8",
* txt: "Question 2 ?"
* }
* </pre>
* @type {Object} */
this.conf = this.filterConf(conf || this.parent.conf);
if (conf.question) { //objeto JSON
var str = this.parseQuestion(conf);
//alert(str);
}
/** @summary clone {@link conf.solution}
* @type {number} */
this.solution = this.conf.solution;
/** @summary clone {@link conf.stopOnSuccess}
* @description False = Only one click, True = clicks until success
* @type {boolean} */
this.stopOnSuccess = this.conf.stopOnSuccess;
this.update_nA();
}
/**
* @method
* @summary Get the self own-counter */
Question.prototype.getCounter = function () { return counter; };
/**
* @method
* @summary Filter the Configuration Options
* @description The Configuration Options will are get from
* parent ("Test"), if not exists
* @param conf {Object} Configuration object
* @return {Object} The sanitized configuration options
* @see #conf */
Question.prototype.filterConf = function (conf) {
return {
/** Number of this Question. This is not equal to this.id */
num: conf.num,
/** Answers Number */
nA: conf.nA || this.parent.conf.nA || 4,
solution: conf.solution || 0,
/** When stop? true = 'Stop on successes', false = 'Stop on click' */
stopOnSuccess : (conf.stopOnSuccess !== undefined) ? conf.stopOnSuccess :
((this.parent.conf.stopOnSuccess !== undefined) ? this.parent.conf.stopOnSuccess : false),
charset: conf.charset || this.parent.conf.charset || "utf-8",
txt: conf.txt || "?"
};
};
/** @method
* @summary Remove the invalid answers */
Question.prototype.sanitizeAnswers = function () {
var answers2 = [];
for (var i = 0; i < this.answers.length; i++) {
if (this.answers[i].valid) { answers2.push(this.answers[i]); }
}
this.answers = answers2;
};
/**
* @method
* @summary Adding an Answer object to this Question
* @param answer {Object:testron.Test.Question.Answer} "Answer" object
* @returns {Object:Answer} Return the same input parameter*/
Question.prototype.addAnswer = function (answer) {
if (!answer || !answer.conf) { return answer; }
var finded = false, i = 0;
this.sanitizeAnswers();
for (i = 0; i < this.answers.length; i++) {
var a = this.answers[i];
if (a.conf.name === answer.conf.name) { //Already exists
a = answer;
finded = true;
}
}
if (! finded) {
answer.position = this.answers.length;
this.answers.push(answer);
}
this.update_nA();
return answer;
};
/** @method
* @summary Create and adding an Answer object to this Test
* @param conf {Object} Configuration object (for Answer Objects)
* @returns {Object:Answer} Return the new Answer created */
Question.prototype.createAnswer = function (conf) {
return this.addAnswer(new this.Answer(this, conf));
};
/** @method
* @summary Remove all answers for this Test Question */
Question.prototype.removeAnswers = function () {
for (var i = 0; i < this.answers.length; i++) {
this.answers[i].remove();
}
this.answers = [];
};
/** @method
* @summary Remove this Test Question (and your children answers) */
Question.prototype.remove = function () {
this.removeAnswers();
this.root = this.parent = this.conf = this.element = null; //NULLIFY
this.valid = false;
//...MORE OPERATIONS FOR FREE MEMORY
};
/** @summary Updates the answers number */
Question.prototype.update_nA = function () {
//UPDATE nA
this.conf.nA = this.answers.length;
};
/** @summary This checks the solution for this Question
* @returns {boolean} True / False */
Question.prototype.verify = function (position) {
//alert(position + " === " + this.solution);
return ((position + 1) === this.solution);
};
/** @method
* @summary This changes the style for this Question DOMElement.
* Also shoots the 'emitNext' event */
Question.prototype.toOK = function () {
if (this.valid && this.element) {
this.element.style.backgroundColor = "aquamarine";
this.ok = true;
this.parent.emitMarker(this);
}
this.emitNext();
};
/** @method
* @summary Change the style to FAIL for this Question DOMElement */
Question.prototype.toFail = function () {
if (this.valid && this.element) {
this.element.style.backgroundColor = "maroon";
this.parent.emitMarker(this);
}
};
/** @summary Create questions from 'questions' parameter.
* @description The parameter can be a Javascript object or a JSON string object with follow structure:
* <pre>
* questions: [
* {
* conf: {..., txt: "Question 1 ??"},
* answers: [ ... ] // see Question.parseAnswers(...)
* },
* { ... },
* ...
* ]
* </pre>
* @param test {Object|string} a Javascript object or a JSON string object
* @return {string} a DEBUG string with info
*/
Question.prototype.parseQuestion = function (question) {
var str = ["Analizing Question.parseQuestion() ... "];
if (question && question.replace) { //Comprobar si es una cadena
question = JSON.parse(question);
str.push("string JSON detected !");
}
if (!question) {
str.push("ERROR: 'question' object not exists!");
return str.join("\n");
}
if (question.question) { question = question.question; }
if (!question.conf) { str.push("WARNING: 'question.conf' object not exists!"); }
this.conf = this.filterConf(question.conf);
//ANSWERS
var answers = question.answers;
if (!answers) {
str.push("ERROR/WARNING: 'anwers' array in this question (" + this.id + ") not exists!");
return str.join("\n");
}
for (var i = 0; i < answers.length; i++) {
var a = answers[i];
if (!a) {
str.push("ERROR/WARNING: 'anwers[" + i + "]' object in this question (" + this.id + ") not exists!");
continue;
}
this.createAnswer({answer: a});
}
str.push("... question FINISH");
return str.join("\n");
};
/** @method
* @summary Create the HTML-UI for this Question and return it
* as HTMLElement (HTMLUlElement)
* @return {Object:HTMLElement} */
Question.prototype.toHtml = function () {
/** @type {HTMLElement} */
var ul = document.createElement("ul");
ul.setAttribute("class", "question");
if (this.valid) {
var caption = document.createElement("caption");
caption.innerHTML = '<span class="caption">(' + this.conf.num + ')</span>   ' + this.conf.txt;
ul.appendChild(caption);
this.sanitizeAnswers();
for (var i = 0; i < this.answers.length; i++) {
ul.appendChild(this.answers[i].toHtml());
}
this.element = ul;
} else {
ul.style.background = "#666666";
}
return ul;
};
/** @summary Parse this element to JSON
* @return {string} a JSON string which represents to this Question object */
Question.prototype.toJSON = function () {
var conf = {
num: this.conf.num,
nA: this.conf.nA,
solution: this.conf.solution,
//stopOnSuccess : this.conf.stopOnSuccess,
charset: this.conf.charset,
txt: this.conf.txt
};
var str = [];
for (var i = 0; i < this.answers.length; i++) {
var answer = this.answers[i];
str.push(answer.toJSON());
}
return ('{"conf": ' + JSON.stringify(conf) + ', "answers": [ ' + str.join(", ") + ' ] }');
};
//EVENTS
/** @emitter
* @summary Emit the Click event for comprobations in this Question */
Question.prototype.emitClick = function (answer) {
if (!this.marked || (this.stopOnSuccess ? !this.ok : false)) {
this.intents++;
//alert("Question:: with Answer:: " + answer.name);
if (this.verify(answer.position)) {
answer.toOK();
this.toOK();
this.root.onOk(null, answer);
} else {
answer.toFail();
this.toFail();
this.root.onFail(null, answer);
}
this.marked = true;
//this.parent.emitMarker(this);
this.root.onClick(null, answer);
}
};
/** @emitter
* @summary Emit the Next event for pass to forward Question */
Question.prototype.emitNext = function () {
this.update_nA();
//alert("next Question");
this.parent.emitNext(this); //propagate to parent
//this.root.onNext(null, this);
};
t.prototype.Question = Question;
})
(testron.Test || (testron.Test = function () {}));
//END MODULE QUESTION
//MODULE ANSWER
/** @lends testron.Test.Question
* @module Answer */
(function (q) {
"use strict";
//INNER PRIVATES VARS
/** Static counter */
var counter = 0;
//INNER PRIVATES FUNCTIONS
/** @class A class to build Answer for Questions Tests, configurable and dynamically
* @constructor
* @memberof module:testron#Question#Test
* @summary Answer for any Question of test
* @description Constructor for creating "Answer" objects. It supports a parameter with a
* configuration object with certain properties or attributes.
* @param parent {Object} "Question" object
* @param conf {Object} Configuration object (see {@link #conf conf})
* @see #conf */
function Answer(parent, conf) {
/** @summary "testron" ancestor object {namespace}
* @type {namespace:testron} */
this.root = parent.root; //"testron" {namespace}
/** @summary Exclusive id (+ or -)
* @type {string} */
this.id;
if (this.root.utils && this.root.utils.generateId) {
this.id = this.root.utils.generateId(); //Exclusive id (+ or -)
}
this.id = (parent ? (parent.id + "-") : "") + this.id;
/** @summary Number of this answer.
* @description This is not equal to 'this.id' (from 0 to +Infinity)
* @type {number} */
this.num = counter++; //from 0 to infinity
/** @summary "Question" Parent object
* @type {Object:Question} */
this.parent = parent;
/** @summary own DOM element
* @type {Object:HTMLElement} */
this.element = null;
/** @summary It keeps track marking for this Answer
* @type {boolean} */
this.checked = false;
/** @summary is valid this Answer ?
* @type {boolean} */
this.valid = true;
/** @summary Configuration Options
* @description a Javascript object with follow structure:
* <pre>
* {
* num: 1,
* name: "A",
* charset: "utf-8",
* txt: "Answer 1"
* }
* </pre>
* @type {Object} */
this.conf = this.filterConf(conf || this.parent.conf);
if (conf.answer) { //objeto JSON
var str = this.parseAnswer(conf);
//alert(str);
}
/** @summary clone of conf.name
* @description Its name ('A', '1', 'a', 'IV', 'pepito', ...)
* @type {string} */
this.name = this.conf.name;
}
/** @method
* @summary Get the self own-counter */
Answer.prototype.getCounter = function () { return counter; };
/** @method
* @summary Filter the Configuration Options
* @description The Configuration Options will are get from parent
* ("Question"), if not exists
* @param conf {Object} Configuration object
* @see #conf*/
Answer.prototype.filterConf = function (conf) {
return {
/** Number of this Answer. This is not equal to this.id */
num: conf.num,
name: conf.name,
charset: conf.charset || this.parent.conf.charset || "utf-8",
txt: conf.txt || ""
};
};
/** @method
* @summary Remove this Answer */
Answer.prototype.remove = function () {
this.root = this.parent = this.conf = this.element = null; //NULLIFY
this.valid = false;
//...MORE OPERATIONS FOR FREE MEMORY
};
/** @method
* @summary Change the style to OK for this Answer DOMElement */
Answer.prototype.toOK = function () {
if (this.valid && this.element) {
this.element.style.backgroundColor = "lightGreen";
//this.root.onOk(null, this);
}
};
/** @method
* @summary Change the style to FAIL for this Answer DOMElement */
Answer.prototype.toFail = function () {
if (this.valid && this.element) {
this.element.style.backgroundColor = "coral";
//this.root.onFail(null, this);
}
};
/** @summary Create answers from 'answers' parameter.
* @description The parameter can be a Javascript object or a JSON string object with follow structure:
* <pre>
* answers: [
* { name: "A", txt: "Answer 1"}, {...}, ...
* ]
* </pre>
* @param test {Object|string} a Javascript object or a JSON string object
* @return {string} a DEBUG string with info
*/
Answer.prototype.parseAnswer = function (answer) {
var str = ["Analizing answer.parseAnswers() ... "];
if (answer && answer.replace) { //Comprobar si es una cadena
answer = JSON.parse(answer);
str.push("string JSON detected !");
}
if (!answer) {
str.push("ERROR: 'answer' object not exists!");
return str.join("\n");
}
if (answer.answer) { answer = answer.answer; }
//if (!answer.conf) { str.push("WARNING: 'answer.conf' object not exists!"); }
this.conf = this.filterConf(answer);
str.push("... answers FINISH");
return str.join("\n");
};
/** @method
* @summary Create the HTML-UI for this Answer and return it
* as HTMLElement (HTMLLiElement) */
Answer.prototype.toHtml = function () {
var self = this;
/** @type {HTMLElement} */
var li = document.createElement("li");
li.setAttribute("class", "answer");
if (this.valid) {
li.innerHTML = '<span class="caption">' + this.conf.name + ')</span> ' + this.conf.txt;
li.addEventListener("click", function () {
self.emitClick();
});
this.element = li;
} else {
li.style.backgroundColor = "#666666";
}
return li;
};
/** @summary Parse this element to JSON
* @return {string} a JSON string which represents to this Answer object */
Answer.prototype.toJSON = function () {
return JSON.stringify({
num: this.conf.num,
name: this.name,
charset: this.conf.charset,
txt: this.conf.txt
});
};
//EVENTS
/** @emitter
* @summary Emit the Click event for comprobations in this Answer */
Answer.prototype.emitClick = function () {
if (this.valid && !this.checked) {
this.checked = true;
this.parent.emitClick(this); //propagate
}
};
q.prototype.Answer = Answer;
})
(testron.Test.prototype.Question ||
(testron.Test.prototype.Question = function () {})
);
//END MODULE ANSWER