"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.} * @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.>} data * @param {?(Object.|Array.)} [inputs = null] * @returns {Array.} * @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.} */ 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.>} data * @param {?(Object.|Array.)} [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.} */ 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; })();