OpoTests/Public/ecma/Views/TestsView.ecma.js

481 lines
19 KiB
JavaScript

"use strict";
import {Utils} from "../Utils/Utils.ecma.js";
import {Check} from "../Utils/Check.ecma.js";
/**
* @class TestsView
* @constructor
* @param {!OpoTests} ot
* @returns {void}
* @access public
* @static
*/
export const TestsView = (function(){
/**
* @constructs TestsView
* @param {!OpoTests} ot
* @returns {void}
* @access private
* @static
*/
const TestsView = function(ot){
/** @type {TestsView} */
const self = this;
/**
* @returns {void}
* @access private
*/
const constructor = () => {};
/**
* @param {!Object.<string, number|boolean|Array.<boolean>>} data
* @param {?(Object.<string, any|null>|Array.<any|null>)} [inputs = null]
* @returns {Array.<any>}
* @access public
*/
this.create = (data, inputs = null) => {
console.log(data.blocks);
/** @type {Array.<any>} */
const info = [],
/** @type {Array.<[string]>} */
graphic = [],
/** @type {Array.<[number, number]>} */
all_questions = data.blocks.reduce((total, i) => (
total.concat(ot.database[i].queries.map((_, j) => [i, j]))
), []),
/** @type {Array.<[number, number]>} */
questions = [],
/** @type {number} */
number_of_questions = Utils.get_random(data.minimum_questions, data.maximum_questions);
do{
questions.push(...all_questions.sort(() => Math.random() - .5));
}while(data.allow_repeated_questions && questions.length < number_of_questions);
questions.splice(number_of_questions, questions.length);
return ["form", {
class : "test-form",
method : "get",
action : "#",
onsubmit : (item, event) => {
event.preventDefault();
return false;
},
data_show_rights : data.show_rights,
data_show_results : data.show_results
}, [
["fieldset", null, [
ot.comp.i18n("test", "legend"),
ot.comp.i18n("test_text", "p"),
["ul", {class : "test-info"}, info],
["section", null, questions.map(([i, j], q) => {
/** @type {Array.<string>} */
const answers = [],
/** @type {Array.<[string, boolean]>} */
raw_answers = [],
/** @type {Object.<[string, number|null], Array.<[string, boolean]>>} */
originals = ["rights", "wrongs"].reduce((all, key) => {
if(ot.database[i].queries[j][key]){
/** @type {boolean} */
const ok = key == "rights";
Check.is_array(ot.database[i].queries[j][key]) ||
(ot.database[i].queries[j][key] = [ot.database[i].queries[j][key]]);
ot.database[i].queries[j][key].forEach(answer => {
TestsView.add_answer_to(all, answer, ok);
});
ot.database[i].queries[j].brothers_are_wrongs &&
ot.database[i].queries.forEach((query, k) => {
k == j || query.rights.forEach(answer => {
TestsView.add_answer_to(all, answer, false);
});
});
};
return all;
}, []),
/** @type {number} */
number_of_answers = Utils.get_random(data.minimum_answers, data.maximum_answers),
/** @type {boolean} */
multiple_answers = data.allow_multiple_answers && (!data.allow_unique_right || Math.random() < .5)
/** @type {number} */
let x = 0;
for(let y = 0, tries = 0; y < number_of_answers && originals.length; y ++){
/** @type {number} */
let k = Utils.get_random(originals.length),
/** @type {[[string, number|null], boolean, boolean]} */
[[answer, probability], ok, dynamic] = [...originals[k]];
if(probability !== null && Math.random() > probability){
y --;
continue;
};
if(dynamic)
answer = TestsView.format(answer);
else
originals.splice(k, 1);
if(
answer &&
!raw_answers.some(([base, _]) => answer == base) &&
!originals.some(([base, right, _]) => (
right &&
!ok &&
answer == base &&
!originals.some(([base, right, _]) => !right && ok && answer == base)
))
){
if(ok){
if(!multiple_answers)
for(let z = originals.length - 1; z >= 0; z --)
originals[z][1] && originals.splice(z, 1);
};
raw_answers.push([answer, ok]);
}else
++ tries < 100 && y --;
};
if(!raw_answers.some(([_, ok]) => ok) && (!data.allow_all_answers_false || !multiple_answers))
TestsView.replace_answer(originals, raw_answers, true);
else if(!raw_answers.some(([_, ok]) => !ok) && !data.allow_all_answers_true)
TestsView.replace_answer(originals, raw_answers, false);
raw_answers.sort(() => Math.random() - .5).forEach(([answer, ok], i) => {
answers.push(answer);
ok && (x |= 1 << i);
});
return ["fieldset", {
data_q : q,
data_block : i,
data_question : j,
data_status : "unanswered",
data_x : x
}, [
["legend", null, "#" + (q + 1)],
["ul", {class : "block-info"}, [
["li", null, [
ot.comp.i18n("block", "strong"),
["span", null, ot.database[i].title]
]],
["li", null, [
ot.comp.i18n("question", "strong"),
["span", null, "#" + (j + 1)]
]],
["li", null, [
ot.comp.i18n("status", "strong"),
ot.comp.i18n("unanswered")
]]
]],
["p", null, TestsView.format(Check.is_array(ot.database[i].queries[j].question) ? Utils.get_random(ot.database[i].queries[j].question) : ot.database[i].queries[j].question)],
["ul", {class : "answers"}, answers.map((answer, i) => ["li", {data_i : i}, [
ot.comp[multiple_answers ? "checkbox" : "radio"]("q" + q + "[]", false, (item, event) => {
/** @type {HTMLFieldSetElement} */
const block = item.parentNode.parentNode.parentNode.parentNode,
/** @type {HTMLLiElement} */
status_field = block.querySelector("strong[data-i18n=status]+span"),
/** @type {HTMLFormElement} */
test_box = block.parentNode.parentNode.parentNode;
/** @type {string} */
let status,
/** @type {number} */
results;
if(item.getAttribute("type") == "radio")
block.querySelectorAll("[type=radio]").forEach(check_answer);
else
check_answer(item);
results = [...block.querySelectorAll("[data-right]")].reduce((status, item) => {
/** @type {string} */
const status_key = item.getAttribute("data-right");
return (
status_key == "false" ? status | 1 :
status_key == "true" ? status | 2 :
status_key == "null" ? status | 4 :
status);
}, 0);
status = (
results & 4 || test_box.getAttribute("data-show-results") == "false" ? "answered" :
results == 2 ? "right" :
results == 1 || item.getAttribute("type") == "radio" ? "wrong" :
"partially");
if(status_field.getAttribute("data-i18n") != status){
status_field.setAttribute("data-i18n", status);
status_field.innerText = ot.i18n.get(status);
block.setAttribute("data-status", status);
};
test_box.querySelector(".graphic [data-q='" + block.getAttribute("data-q") + "']").setAttribute("data-status", status);
}, answer, null, "answer_" + (i + 1)),
["span", {data_is_ok : !!(x & (1 << i))}],
["span", {data_right : null}]
]])]
]];
})],
["ul", {class : "graphic"}, questions.map((_, q) => ["li", {
data_q : q,
data_status : "unanswered"
}])],
ot.comp.buttons()
]]
]];
};
/**
* @param {!Object.<string, number|boolean|Array.<boolean>>} data
* @param {?(Object.<string, any|null>|Array.<any|null>)} [inputs = null]
* @returns {void}
* @access public
*/
this.build = (data, inputs = null) => {
/** @type {HTMLMainElement} */
const main = document.querySelector("main");
main.innerHTML = ``;
Utils.html(main, self.create(data, inputs));
};
/**
* @param {!HTMLInputElement} item
* @returns {void}
* @access private
*/
const check_answer = item => {
/** @type {HTMLLiElement} */
const level = item.parentNode.parentNode,
/** @type {boolean} */
is_right = level.querySelector("[data-is-ok]").getAttribute("data-is-ok") == "true";
level.querySelector("[data-right]").setAttribute("data-right", (item.checked && is_right) || (!item.checked && !is_right));
};
constructor();
};
/**
* @param {!string} string
* @returns {string}
* @access public
* @static
*/
TestsView.reformat_pattern = string => {
/** @type {Array.<string>} */
const fragments = [];
/** @type {number} */
let j = 0;
for(let i = 9; i > 1; i --)
string = string.replace(new RegExp("\\[" + i + "(?:(?!(?:" + i + "\\]))(?:.|[\\r\\n]+))+" + i + "\\]", "g"), match => {
fragments.push(match);
return "[" + j ++ + "]";
});
string = string.replace(/:::/g, "|");
while(string != (string = string.replace(/\${3}([0-9]+)\${3}/g, (_, j) => fragments[j])));
return string;
}
/**
* @param {!string} string
* @returns {string}
* @access public
* @static
*/
TestsView.format_substring = string => {
console.log(string);
return TestsView.format([9, 8, 7, 6, 5, 4, 3, 2, 1].reduce((string, i) => (
i == 1 ? TestsView.reformat_pattern(string.replace(new RegExp("(\\[)" + i + "|" + i + "(\\])", "g"), "$1$1$2$2")) :
string.replace(new RegExp("(\\[)" + i + "|" + i + "(\\])", "g"), "$1" + (i - 1) + "$2")), string));
};
/**
* @param {!string} type
* @param {!string} items
* @param {Object.<string, any|null>} shared
* @returns {string}
* @access public
* @static
*/
TestsView.build_rand = (type, items, shared) => TestsView.format_substring(Utils.get_random(items.split("|")));
/**
* @param {!string} type
* @param {!string} items
* @param {Object.<string, any|null>} shared
* @returns {string}
* @access public
* @static
*/
TestsView.build_range = (type, items, shared) => {
/** @type {Array.<number>} */
const range = Utils.get_random(items.split("|")).split("-").map(Number);
return range.length == 1 ? range[0] : Utils.get_random(...range);
};
/**
* @param {!string} type
* @param {!string} items
* @param {Object.<string, any|null>} shared
* @returns {string}
* @access public
* @static
*/
TestsView.build_serie = (type, items, shared) => {
/** @type {boolean} */
const start_serie = Check.is_null_or_undefined(shared.serie),
/** @type {[number, number]} */
[from, to] = Utils.get_random(items.split("|").map(item => item.split("-").map(Number)).filter(([from, to]) => (
start_serie || (isNaN(to) ? from : to) >= shared.serie
)));
return shared.serie = isNaN(to) ? from : Utils.get_random(start_serie ? from : Math.max(shared.serie, from), to);
};
/**
* @param {!string} type
* @param {!string} items
* @param {Object.<string, any|null>} shared
* @returns {string}
* @access public
* @static
*/
TestsView.build_mix = (type, items, shared) => {
/** @type {string} */
const nexus = items.match(/^[^,]+/)[0].trim(),
/** @type {Array.<string>} */
items_box = items.replace(/^[^,]+,/, "").split("|").sort(() => Math.random() - .5).map(TestsView.format_substring),
/** @type {number} */
last = items_box.length - 1;
type == "Mix" &&
(items_box[0] = items_box[0][0].toUpperCase() + items_box[0].slice(1).toLowerCase());
return items_box.slice(0, last).join(", ") + " " + nexus + " " + items_box[last];
};
/**
* @param {!string} type
* @param {!string} items
* @param {Object.<string, any|null>} shared
* @returns {string}
* @access public
* @static
*/
TestsView.build_select = (type, items, shared) => {
/** @type {[string, string, string, string]} */
const [_, range, nexus, items_string] = items.match(/^([^,]+),([^,]+),(.*)$/),
/** @type {Array.<string>} */
item_box = items_string.split("|").sort(() => Math.random() - .5).sort(() => Math.random() - .5).slice(0, ...range.split("-").map(Number)).map(TestsView.format_substring);
if(item_box.length < 2)
return item_box[0] || "";
/** @type {number} */
const last = item_box.length - 1;
type == "Select" &&
(item_box[0] = item_box[0][0].toUpperCase() + item_box[0].slice(1).toLowerCase());
return item_box.slice(0, last).join(", ") + " " + nexus + " " + item_box[last];
};
/**
* @param {!string} string
* @returns {string}
* @access public
* @static
*/
TestsView.format = string => {
/** @type {Object.<string, any|null>} */
const shared = {};
return string.replace(/\[{2}([^,\]]+),((?:(?!(?:\]\])).)+\]?)\]{2}/g, (original, type, items) => {
if(TestsView["build_" + type.toLowerCase()])
return TestsView["build_" + type.toLowerCase()](type, items, shared);
return original;
});
};
/**
* @param {!Array.<[string, boolean, boolean]>} originals
* @param {!Array.<[string, boolean]>} raw_answers
* @param {boolean} right
* @returns {void}
* @access public
* @static
*/
TestsView.replace_answer = (originals, raw_answers, right) => {
/** @type {Array.<[string, boolean, boolean]>} */
const set = originals.filter(([_, ok]) => right ? ok : !ok);
if(set.length)
while(true){
/** @type {number} */
const k = Utils.get_random(raw_answers.length),
/** @type {[string, boolean, boolean]} */
[[answer, probability], ok, dynamic] = Utils.get_random(set);
if(!probability || Math.random() < probability){
raw_answers.splice(k, 1, [dynamic ? TestsView.format(answer) : answer, ok]);
break;
};
};
};
/**
* @param {!Array.<[[string, number|null], boolean, boolean]>} answers
* @param {!(string|[string, number|null])} answer
* @param {!boolean} right
* @returns {void}
* @access public
* @static
*/
TestsView.add_answer_to = (answers, answer, right) => {
/** @type {[string, number|null]} */
const [text, probability] = Check.is_array(answer) ? answer : [answer, null];
answers.some(([[base, __], ..._]) => base == text) ||
answers.push([[text, probability], right, text.includes("[[")]);
};
return TestsView;
})();