SpreadQuiz/js/tasks.js
2025-10-11 21:18:17 +02:00

927 lines
25 KiB
JavaScript

class Task extends HTMLElement {
static sequence_number = 0;
constructor(type) {
super();
this.task_type = type;
this.sequence_number = Task.sequence_number++;
this.concluded = false;
this.view_only = false;
this.upload_answer_cb = null;
this.player_answer = null;
this.correct_answer = null;
this.shadow = this.attachShadow({mode: "open"});
this.createStyle();
this.createElements();
}
createStyle() {
this.css = document.createElement("style");
this.css.innerHTML = `
span.question {
font-size: 1.3em;
font-weight: bold;
color: #176767;
}
section.task {
display: block;
position: relative;
margin: 0 auto;
width: 40em;
padding: 1em;
background-color: #d3e5e5;
margin-bottom: 0.5em;
border-radius: 0.3em;
}
section.seq-num {
display: block;
position: absolute;
right: 0;
padding: 0.5em;
bottom: 0;
background-color: #176767;
color: whitesmoke;
border-bottom-right-radius: 0.3em;
border-top-left-radius: 0.3em;
width: 2em;
z-index: 10;
}
section.answer-container {
/* (empty) */
}
section.bad-answer {
background-color: #e5d8d3;
}
code {
font-family: 'Monaco', monospace;
}
section#correct-answer {
display: none;
width: 100%;
margin-top: 0.5em;
}
*[visible="true"] {
display: block !important;
}
section#comment {
display: none;
width: 100%;
margin-top: 0.5em;
}
.MathJax {
display: block;
margin: 0.5em auto;
font-size: 120%;
}
@media only screen and (max-width: 800px) {
section.task {
width: calc(100vw - 3em);
}
section.answer-container {
margin-bottom: 1.5em;
}
}
`;
this.shadow.append(this.css);
}
createElements() {
let task_box = document.createElement("section");
task_box.classList.add("task");
let question_span = document.createElement("span");
question_span.classList.add("question");
let answer_container = document.createElement("section");
answer_container.classList.add("answer-container");
let seq_num_section = document.createElement("section");
seq_num_section.classList.add("seq-num");
seq_num_section.innerText = `${this.sequence_number + 1}.`;
let ca_section = document.createElement("section");
ca_section.id = "correct-answer";
let comment_sec = document.createElement("section");
comment_sec.id = "comment";
task_box.append(question_span);
task_box.append(answer_container);
task_box.append(seq_num_section);
task_box.append(ca_section);
task_box.append(comment_sec);
this.shadow.append(task_box);
this.task_box = task_box;
this.question_span = question_span;
this.answer_container = answer_container;
this.seq_num_section = seq_num_section;
this.ca_section = ca_section;
this.comment_sec = comment_sec;
}
connectedCallback() {
}
disconnectedCallback() {
}
get type() {
return this.task_type;
}
set type(type) {
this.task_type = type;
}
get sequenceNumber() {
return this.sequence_number;
}
setQuestion(question) {
this.question_span.innerHTML = preprocess_inserts(question);
}
get isConcluded() {
return this.concluded;
}
set isConcluded(concluded) {
this.concluded = concluded;
this.ca_section.setAttribute("visible", this.concluded ? "true" : "false");
}
get isViewOnly() {
return this.view_only;
}
set isViewOnly(viewOnly) {
this.view_only = viewOnly;
}
get isCorrect() {
return false;
}
set playerAnswer(player_answer) {
this.player_answer = player_answer;
}
get playerAnswer() {
return this.player_answer;
}
set correctAnswer(correct_answer) {
this.correct_answer = correct_answer;
}
get correctAnswer() {
return this.correct_answer;
}
set uploadAnswerCb(cb) {
this.upload_answer_cb = cb;
}
set comment(comment) {
this.comment_sec.innerHTML = preprocess_inserts(comment);
MathJax.typeset([this.comment_sec]);
}
uploadAnswer() {
if (this.upload_answer_cb !== null) {
this.upload_answer_cb(this.sequence_number, this.playerAnswer);
}
}
displayCorrectAnswer() {
}
fromArray(a) {
this.setQuestion(a["question"]);
this.playerAnswer = a["player_answer"];
this.correctAnswer = a["correct_answer"];
let comment = a["comment"];
if ((comment !== undefined) && (comment !== null) && (comment !== "")) {
this.comment = comment;
this.comment_sec.setAttribute("visible", "true");
}
let mark = a["mark"];
if ((mark !== undefined) && (mark !== null) && (mark > -1) && (mark !== a["max_mark"])) {
this.task_box.classList.add("bad-answer");
}
if (this.isConcluded) {
this.displayCorrectAnswer();
}
}
}
class PicturedTask extends Task {
constructor(type) {
super(type);
this.img = null;
this.img_type = "none";
}
createStyle() {
super.createStyle();
this.css.innerHTML += `
.question-image {
display: none;
position: relative;
margin: 1em auto;
border-radius: 0.3em;
max-width: 100%;
text-align: center;
}
`;
}
createElements() {
super.createElements();
}
set imgData(data) {
switch (this.img_type) {
case "url":
{
this.img = document.createElement("img");
this.img.classList.add("question-image");
data = data.trim();
this.img.src = data.trim();
}
break;
case "svg":
{
this.img = document.createElement("section");
this.img.classList.add("question-image");
this.img.innerHTML = data;
}
break;
}
if (this.img != null) {
this.img.style.display = (data !== "") ? "block" : "none";
this.task_box.insertBefore(this.img, this.question_span);
}
}
get imgData() {
return this.img.src;
}
set imgType(t) {
this.img_type = t;
}
get imgType() {
return this.img_type;
}
}
class SingleChoiceTask extends PicturedTask {
constructor() {
super("singlechoice");
this.answers = []
this.correct_answer = -1;
this.player_answer = -1;
}
createStyle() {
super.createStyle();
this.css.innerHTML += `
section.answer {
margin: 0.3em 0.8em;
display: block;
}
section.answer label {
margin-left: 0.5em;
padding: 0.3em 0.5em;
border-radius: 0.3em;
display: inline-block;
max-width: 85%;
vertical-align: middle;
}
section.answer label.correct-answer {
border: 2px solid #176767 !important;
background-color: #176767;
color: whitesmoke;
/*padding: 0.1em;*/
}
section.answer input[type="radio"]:checked+label:not(.correct-answer) {
background-color: #176767;
color: whitesmoke;
}
section.bad-answer section.answer input[type="radio"]:checked+label:not(.correct-answer) {
background-color: #aa8a7d;
}
@media only screen and (max-width: 800px) {
section.answer label {
max-width: calc(100% - 4em);
}
}
`;
}
createElements() {
super.createElements();
}
// --------
setAnswers(answers) {
this.answers = answers;
this.answers.forEach((answer, i) => {
let answer_section = document.createElement("section");
answer_section.classList.add("answer");
let answer_radio = document.createElement("input");
answer_radio.type = "radio";
answer_radio.id = `${this.sequenceNumber}_${i}`;
answer_radio.name = `task_${this.sequenceNumber}`;
answer_radio.disabled = this.isConcluded || this.isViewOnly;
let answer_N_snapshot = i;
answer_radio.addEventListener("input", () => {
this.playerAnswer = answer_N_snapshot;
this.uploadAnswer();
});
let answer_text = document.createElement("label");
answer_text.innerHTML = preprocess_inserts(answer);
answer_text.htmlFor = answer_radio.id;
if (this.isConcluded && (this.correctAnswer === i)) {
answer_text.classList.add("correct-answer")
if (this.playerAnswer !== this.correctAnswer) {
this.task_box.classList.add("bad-answer");
}
}
if (this.playerAnswer === i) {
answer_radio.checked = true;
}
answer_section.append(answer_radio, answer_text);
this.answer_container.append(answer_section);
});
MathJax.typeset([this.task_box]);
}
get isCorrect() {
return this.player_answer === this.correct_answer;
}
fromArray(a) {
super.fromArray(a);
this.setAnswers(a["answers"]);
}
}
class OpenEndedTask extends PicturedTask {
constructor() {
super("openended");
}
createStyle() {
super.createStyle();
this.css.innerHTML += `
input[type="text"] {
font-family: 'Monaco', monospaced;
border-width: 0 0 2.2pt 0;
background-color: transparent;
width: calc(100% - 4em);
margin: 1em 0;
border-bottom-color: #176767;
font-size: 110%;
}
input[type="text"]:hover {
border-bottom-color: #408d8d;
}
`
}
createElements() {
super.createElements();
let answer_tf = document.createElement("input");
answer_tf.type = "text";
answer_tf.placeholder = "(válasz)";
answer_tf.onblur = () => {
this.uploadAnswer();
}
answer_tf.oninput = () => {
this.player_answer = answer_tf.value;
};
this.answer_container.append(answer_tf);
this.answer_tf = answer_tf;
}
displayCorrectAnswer() {
this.ca_section.innerHTML = "<b>Lehetséges megoldások:</b><br style='margin-bottom: 0.3em'>" + this.correctAnswer.join(", <i>VAGY</i><br>");
}
fromArray(a) {
super.fromArray(a);
}
set playerAnswer(player_answer) {
super.playerAnswer = player_answer;
this.answer_tf.value = player_answer;
}
get playerAnswer() {
return this.player_answer;
}
updateAnswerFieldState() {
this.answer_tf.disabled = this.isViewOnly || this.isConcluded;
}
set isConcluded(concluded) {
super.isConcluded = concluded;
this.updateAnswerFieldState();
}
get isConcluded() {
return super.isConcluded;
}
set isViewOnly(is_view_only) {
super.isViewOnly = is_view_only;
}
get isViewOnly() {
return super.isViewOnly;
}
}
class NumberConversionTask extends OpenEndedTask {
constructor() {
super();
this.type = "numberconversion";
}
createStyle() {
super.createStyle();
this.css.innerHTML += `
input[type="text"] {
min-width: 5em;
width: unset;
}
section#src, section#dst {
position: relative;
display: inline-block;
font-family: 'Monaco', monospace;
color: #176767;
}
section#src {
margin-right: 1ch;
}
sub {
position: relative;
top: 0.8em;
}
`;
}
createElements() {
super.createElements();
let src_sec = document.createElement("section");
src_sec.id = "src";
let dst_sec = document.createElement("section");
dst_sec.id = "dst";
this.answer_container.insertBefore(src_sec, this.answer_tf);
this.answer_container.append(dst_sec);
this.src_sec = src_sec;
this.dst_sec = dst_sec;
this.answer_tf.addEventListener("input", () => {
this.updateAnswerFieldLength();
})
}
displayCorrectAnswer() {
this.ca_section.innerHTML = `Megoldás: <code>${this.correctAnswer}</code><sub>(${this.dst_base})</sub>`;
}
fromArray(a) {
const regex = /([0-9]+)([suc]):([0-9]+)->([0-9]+)([suc]):([0-9]+)/g;
let parts = [...a["instruction"].matchAll(regex)][0];
this.src_base = parts[1];
this.dst_base = parts[4];
this.src_len = parts[6];
let src_exp = `${a["source"]}<sub>(${parts[1]})</sub> =`;
let dst_exp = `<sub>(${parts[4]})</sub> <i>(${parts[6]} digiten)</i>`;
super.fromArray(a);
this.src_sec.innerHTML = src_exp;
this.dst_sec.innerHTML = dst_exp;
this.updateAnswerFieldLength();
}
updateAnswerFieldLength() {
this.answer_tf.style.width = this.answer_tf.value.length + "ch";
}
}
class Switch extends HTMLElement {
constructor() {
super();
this.state = "-";
this.highlight = "-";
this.M = 2;
this.disabled = false;
this.shadow = this.attachShadow({mode: "open"});
this.createStyle();
this.createElements();
}
createStyle() {
this.css = document.createElement("style");
this.css.innerHTML = `
section.frame {
display: inline-block;
border: 1pt solid black;
}
section.button {
display: inline-block;
padding: 0.2em 0.5em;
cursor: pointer;
font-family: 'Monaco', monospace;
}
section.button[disabled="false"]:hover {
background-color: #408d8d;
color: white;
}
section.button[highlight="true"] {
background-color: #aa8a7d;
color: white;
}
section.button[selected="true"] {
background-color: #176767;
color: white;
}
section.button:not(:last-child) {
border-right: 1pt dotted black;
}
`;
this.shadow.append(this.css);
}
createElements() {
let frame = document.createElement("section");
frame.classList.add("frame");
let btns = [];
for (let i = 0; i < this.M; i++) {
let btn = document.createElement("section");
btn.classList.add("button");
btn.innerText = i.toString();
btn.id = "btn_" + i.toString();
btn.setAttribute("disabled", "false");
btns.push(btn);
btn.addEventListener("click", (e) => {
if (!this.disabled) {
this.setState(i);
this.dispatchEvent(new Event("change"));
}
})
frame.append(btn);
}
document.createElement("section");
this.frame = frame;
this.btns = btns;
this.shadow.append(frame);
this.setDisabled(false);
}
setState(state) {
this.state = state;
for (let i = 0; i < this.M; i++) {
this.btns[i].setAttribute("selected", (i.toString() === this.state.toString()) ? "true" : "false");
}
}
setHighlight(hl) {
this.highlight = hl;
for (let i = 0; i < this.M; i++) {
this.btns[i].setAttribute("highlight", (i.toString() === this.highlight.toString()) ? "true" : "false");
}
}
getState() {
return this.state;
}
setDisabled(disabled) {
this.disabled = disabled;
this.frame.setAttribute("disabled", disabled ? "true" : "false");
}
}
class TruthTableTask extends PicturedTask {
constructor() {
super("truthtable");
this.input_variables = [];
this.output_variable = "";
this.output_switches = [];
}
createStyle() {
super.createStyle();
this.css.innerHTML += `
table#tt {
border: 1.5pt solid #176767;
margin: 0.5em auto;
border-spacing: 0;
font-family: 'Monaco', monospace;
}
table#tt tr:not(:last-child) td {
border-bottom: 1.2pt solid black;
}
table#tt th {
border-bottom: 1.5pt dotted black
}
table#tt td, table#tt th {
min-width: 3ch;
text-align: center;
}
table#tt td:last-child, table#tt th:last-child {
border-left: 1.5pt dashed black;
}
`;
}
createElements() {
super.createElements();
let tt = document.createElement("table");
tt.id = "tt";
this.answer_container.append(tt);
this.tt = tt;
}
updatePlayerAnswer() {
let pa = "";
for (let i = 0; i < this.output_switches.length; i++) {
pa += this.output_switches[i].getState();
}
super.playerAnswer = pa;
}
buildTTable() {
let N = this.input_variables.length;
let M = (1 << N);
let inside = "<tr>"
for (let i = 0; i < N; i++) {
inside += "<th>" + this.input_variables[i] + "</th>";
}
inside += "<th>" + this.output_variable + "</th>";
inside += "</tr>";
for (let i = 0; i < M; i++) {
inside += "<tr>";
for (let j = 0; j < N; j++) {
inside += "<td>" + ((i >> (N - j - 1)) & 1).toString() + "</td>";
}
inside += "<td><slide-switch id='out_" + i + "'></slide-switch></td>"
inside += "</tr>";
}
this.tt.innerHTML = inside;
for (let i = 0; i < M; i++) {
let sw = this.shadow.getElementById("out_" + i);
sw.addEventListener("change", () => {
this.updatePlayerAnswer();
this.uploadAnswer();
});
sw.setDisabled(this.isConcluded || this.isViewOnly);
this.output_switches[i] = sw;
}
this.playerAnswer = "-".repeat(M);
}
set playerAnswer(playerAnswer) {
if (playerAnswer !== null) {
super.playerAnswer = playerAnswer;
}
for (let i = 0; i < this.output_switches.length; i++) {
let sw = this.output_switches[i];
let pac = this.playerAnswer.charAt(i);
if (!this.isConcluded) {
sw.setState(pac);
} else {
let cac = this.correctAnswer.charAt(i);
if (cac !== pac) {
sw.setHighlight(pac);
}
sw.setState(cac);
}
}
}
get playerAnswer() {
return super.playerAnswer;
}
fromArray(a) {
super.fromArray(a);
this.input_variables = a["input_variables"];
this.output_variable = a["output_variable"];
this.buildTTable();
this.playerAnswer = a["player_answer"];
}
}
class VerilogTask extends PicturedTask {
//static observedAttributes = ["language"]
constructor() {
super("verilog");
}
createStyle() {
super.createStyle();
this.css.innerHTML += `
section.editor-sec {
margin-top: 1em;
min-height: 24em;
font-family: 'JetBrains Mono', monospace;
}
div.ace_content {
font-variant-ligatures: none;
}
section#explain-sec {
font-family: 'JetBrains Mono', monospace;
margin-top: 0.5em;
width: calc(100% - 0.4em);
padding: 0.2em;
background: #e5e5e57f;
}
section#correct-answer-title {
padding: 1em 0 0.5em 0.2em;
font-weight: bold;
}
`;
}
createElements() {
super.createElements();
let editor_sec = document.createElement("section");
editor_sec.classList.add("editor-sec");
this.answer_container.append(editor_sec);
let editor = ace.edit(editor_sec, {
theme: "ace/theme/chrome",
mode: "ace/mode/verilog"
});
editor.renderer.attachToShadowRoot();
editor.addEventListener("blur", () => {
this.uploadAnswer();
});
let explain_sec = document.createElement("section");
explain_sec.id = "explain-sec";
this.ca_section.append(explain_sec);
let solution_title_sec = document.createElement("section");
solution_title_sec.innerText = "Egy lehetséges megoldás:";
solution_title_sec.id = "correct-answer-title";
this.ca_section.append(solution_title_sec);
let solution_sec = document.createElement("section");
solution_sec.id = "solution-sec";
solution_sec.classList.add("editor-sec");
let solution_editor = ace.edit(solution_sec, {
theme: "ace/theme/chrome",
mode: "ace/mode/verilog",
});
solution_editor.setReadOnly(true);
solution_editor.renderer.attachToShadowRoot();
this.ca_section.append(solution_sec);
this.editor_sec = editor_sec;
this.editor = editor;
this.explain_sec = explain_sec;
this.solution_title_sec = solution_title_sec;
this.solution_sec = solution_sec;
this.solution_editor = solution_editor;
}
set playerAnswer(player_answer) {
this.editor.setValue(player_answer);
this.editor.clearSelection();
}
get playerAnswer() {
return this.editor.getValue();
}
set correctAnswer(player_answer) {
this.solution_editor.setValue(player_answer);
this.solution_editor.clearSelection();
}
get correctAnswer() {
return super.correctAnswer;
}
fromArray(a) {
super.fromArray(a);
this.explain_sec.innerText = a["compile_log"];
}
updateEditorFreezeState() {
this.editor.setReadOnly(this.isConcluded || this.isViewOnly);
}
set isConcluded(concluded) {
super.isConcluded = concluded;
this.updateEditorFreezeState();
if (concluded) {
this.solution_editor.setValue(this.correctAnswer);
}
}
get isConcluded() {
return super.isConcluded;
}
set isViewOnly(viewOnly) {
super.isViewOnly = viewOnly;
this.updateEditorFreezeState();
}
get isViewOnly() {
return super.isViewOnly;
}
// attributeChangedCallback(name, oldVal, newVal) {
// switch (name) {
// case "language":
// editor.session.setMode("ace/mode/" + newVal.toLowerCase());
// break;
// }
// }
}
customElements.define('singlechoice-task', SingleChoiceTask);
customElements.define('openended-task', OpenEndedTask);
customElements.define('numberconversion-task', NumberConversionTask);
customElements.define('truthtable-task', TruthTableTask);
customElements.define('slide-switch', Switch);
customElements.define('verilog-task', VerilogTask);