527 lines
23 KiB
JavaScript
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;
|
|
})(); |