OpoTests/Public/ecma/Views/TestsView.ecma.js
2025-10-25 17:34:51 +02:00

527 lines
23 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(){
/**
* @callback test_view_mode_callback
* @param {...(any|null)} [parameters]
* @returns {string}
*/
/**
* @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 {!string} name
* @param {!number} value
* @param {?number} [maximum = null]
* @returns {Array.<any>}
* @access private
*/
const set_info_value = (name, value, maximum = null) => ["li", {
data_i18n : name,
data_i18n_without : true,
title : ot.i18n.get(name)
}, [
ot.comp.i18n(name, "strong"),
["span", {class : "value"}, "" + value],
maximum === null ? null : ["span", {class : "maximum"}, "" + maximum]
]];
/**
* @param {!Array.<[string, boolean, boolean]>} originals
* @param {!Array.<[string, boolean]>} raw_answers
* @param {boolean} right
* @returns {void}
* @access public
* @static
*/
const replace_answer = (originals, raw_answers, right, number_of_answers) => {
/** @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){
/** @type {string} */
const string_answer = dynamic ? ot.format.execute(answer) : answer;
if(raw_answers.length < number_of_answers)
raw_answers.some(([originals, ok]) => ok && originals == string_answer) ||
// originals.some(([[base, __], ok, _]) => ok && base == answer) ||
raw_answers.push([string_answer, ok]);
else
raw_answers.splice(k, 1, [string_answer, ok]);
break;
};
};
};
/**
* @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) => {
/** @type {Array.<[number, number]>} */
const 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,
data_fixed : false,
data_lock_mode : data.lock_mode,
data_marks_maximum : data.marks_over,
data_marks_rest : data.marks_rest,
data_marks_decimals : data.marks_decimals
}, [
["fieldset", null, [
ot.comp.i18n("test", "legend"),
ot.comp.i18n("test_text", "p"),
["ul", {class : "test-info"}, [
set_info_value("number", questions.length),
set_info_value("answered", 0),
set_info_value("unanswered", questions.length),
set_info_value("rights", 0),
set_info_value("wrongs", 0),
set_info_value("partially", 0),
set_info_value("marks", 0, data.marks_over),
]],
["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;
const memory = [];
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 = ot.format.execute(answer);
else
originals.splice(k, 1);
memory.push([k, answer]);
// console.log([q, tries, y, answer, ok, [...originals],
// originals.map(([[base, __], right, _]) => (
// right ? base : null
// )).filter(base => base !== null).concat(
// ok ? [] : ot.database[i].queries[j].not || []
// ),
// ot.format.check(
// answer,
// originals.map(([[base, __], right, _]) => (
// right ? base : null
// )).filter(base => base !== null).concat(
// ok ? [] : ot.database[i].queries[j].not || []
// )
// )[0],
// raw_answers.some(([base, _]) => answer == base),
// answer &&
// !raw_answers.some(([base, _]) => answer == base) &&
// !ot.format.check(
// answer,
// originals.map(([[base, __], right, _]) => (
// right ? base : null
// )).filter(base => base !== null).concat(
// ok ? [] : ot.database[i].queries[j].not || []
// )
// )[0]]
// );
if(
answer &&
!(raw_answers.length && raw_answers.some(([base, _]) => answer == base)) &&
!ot.format.check(
answer,
originals.map(([[base, __], right, _]) => (
right ? base : null
)).filter(base => base !== null).concat(
ok ? [] : ot.database[i].queries[j].not || []
)
)[0]
){
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))
replace_answer(originals, raw_answers, true, number_of_answers);
else if(!raw_answers.some(([_, ok]) => !ok) && !data.allow_all_answers_true)
replace_answer(originals, raw_answers, false, number_of_answers);
raw_answers.length < number_of_answers && console.warn([q, raw_answers, memory]);
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, ot.format.execute(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(
["change_rights_mode", change_rights_mode],
["change_results_mode", change_results_mode],
["fix", fix],
["restart", restart],
["do_other_test", do_other_test]
)
]]
]];
};
/**
* @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));
setTimeout(() => {
/** @type {HTMLFormElement} */
const form = document.querySelector(".test-form"),
/** @type {Object.<string, number>} */
values = {number : Number(form.querySelector(".test-info li[data-i18n=number] .value").innerText)},
/** @type {[number, number, number]} */
[maximum, rest, decimals] = ["maximum", "rest", "decimals"].map(key => Number(form.getAttribute("data-marks-" + key)));
["rights", "wrongs", "unanswered", "partially"].forEach(name => {
form.querySelector(".test-info li[data-i18n=" + name + "] .value").innerText = "" + (values[name] = form.querySelectorAll(".graphic [data-status=" + name.replace(/s$/, "") + "]").length);
});
form.querySelector(".test-info li[data-i18n=answered] .value").innerText = "" + (values.answered = form.querySelectorAll(".graphic [data-status]:not([data-status=unanswered])").length);
form.querySelector(".test-info li[data-i18n=marks] .value").innerText = ((maximum * values.rights / values.number) - values.wrongs * rest - values.partially * rest * .5).toFixed(decimals);
}, 1);
};
/**
* @param {!HTMLElement} item
* @param {!Event} event
* @returns {void}
* @access private
*/
const fix = (item, event) => {
/** @type {HTMLFormElement} */
const form = document.querySelector(".test-form");
if(form.getAttribute("data-fixed") == "false"){
form.setAttribute("data-fixed", "true");
form.querySelectorAll("[type=checkbox],[type=radio]").forEach(input => {
input.setAttribute("disabled", true);
});
};
};
/**
* @param {!HTMLElement} item
* @param {!Event} event
* @returns {void}
* @access private
*/
const restart = (item, event) => {
/** @type {HTMLFormElement} */
const form = document.querySelector(".test-form");
if(form.getAttribute("data-fixed") == "true"){
form.setAttribute("data-fixed", "false");
form.querySelectorAll("[type=checkbox],[type=radio]").forEach(input => {
input.removeAttribute("disabled");
input.checked = false;
});
form.querySelectorAll("[data-right]").forEach(span => {
span.setAttribute("data-right", "null");
});
form.querySelectorAll("strong[data-i18n=status]+span").forEach(span => {
span.setAttribute("data-i18n", "unanswered");
span.innerText = ot.i18n.get("unanswered");
});
form.querySelectorAll("fieldset[data-status],li[data-status]").forEach(fieldset => {
fieldset.setAttribute("data-status", "unanswered");
});
};
};
/**
* @param {!HTMLElement} item
* @param {!Event} event
* @returns {void}
* @access private
*/
const change_results_mode = (item, event) => {
/** @type {HTMLFormElement} */
const form = document.querySelector(".test-form");
form.getAttribute("data-lock-mode") != "true" &&
form.getAttribute("data-fixed") != "true" &&
form.setAttribute("data-show-results", form.getAttribute("data-show-results") == "true" ? "false" : "true");
};
/**
* @param {!HTMLElement} item
* @param {!Event} event
* @returns {void}
* @access private
*/
const change_rights_mode = (item, event) => {
/** @type {HTMLFormElement} */
const form = document.querySelector(".test-form");
form.getAttribute("data-lock-mode") != "true" &&
form.getAttribute("data-fixed") != "true" &&
form.setAttribute("data-show-rights", form.getAttribute("data-show-rights") == "true" ? "false" : "true");
};
/**
* @param {!HTMLElement} item
* @param {!Event} event
* @returns {void}
* @access private
*/
const do_other_test = (item, event) => {
ot.main_form.build();
};
constructor();
};
/**
* @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("[[") || text.includes("{")]);
};
return TestsView;
})();