"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.>} data * @param {?(Object.|Array.)} [inputs = null] * @returns {Array.} * @access public */ this.create = (data, inputs = null) => { console.log(data.blocks); /** @type {Array.} */ 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.} */ 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.>} 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)); }; constructor(); }; /** * @param {!string} string * @returns {string} * @access public * @static */ TestsView.reformat_pattern = string => { /** @type {Array.} */ 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.} 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.} shared * @returns {string} * @access public * @static */ TestsView.build_range = (type, items, shared) => { /** @type {Array.} */ 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.} 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.} shared * @returns {string} * @access public * @static */ TestsView.build_mix = (type, items, shared) => { /** @type {string} */ const nexus = items.match(/^[^,]+/)[0].trim(), /** @type {Array.} */ 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.} 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.} */ 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.} */ 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; })();