481 lines
19 KiB
JavaScript
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;
|
|
})(); |